Alternativ hier noch eine sehr einfache kostenlose Lösung ohne Anmeldung und ohne API-Key.
Das Skript nutzt die Public-API von Forecast.Solar und schreibt die erwartete PV-Produktion für heute und morgen in die Variablen. Beim ersten manuellen Start legt es die benötigten Variablen und einen automatischen Timer selbst an.
Einzutragen sind nur Breitengrad, Längengrad, Dachneigung, Ausrichtung/Azimut und die Anlagenleistung in kWp.
Azimut bei Forecast.Solar:
Sued = 0, Ost = -90, West = 90
Erzeugt bzw. aktualisiert werden:
- Vorhersage Heute in kWh
- Vorhersage Morgen in kWh
- Status der letzten Abfrage
Das voreingestellte Abrufintervall beträgt 60 Minuten.
<?php
declare(strict_types=1);
/*
* PV-Vorhersage fuer IP-Symcon mit Forecast.Solar
* Version: 1.2.0
* Datum: 18.07.2026
*
* Dieses Skript ruft eine kostenlose PV-Ertragsprognose von Forecast.Solar ab
* und schreibt die Tageswerte fuer heute und morgen in IP-Symcon-Variablen.
*
* Datenquelle:
* https://forecast.solar/
* API-Beispiel:
* https://api.forecast.solar/estimate/LATITUDE/LONGITUDE/DECLINATION/AZIMUTH/KWP
*
* Installation:
* 1. Neues PHP-Skript in IP-Symcon anlegen.
* 2. Diesen Code einfuegen.
* 3. Im Abschnitt "Konfiguration" eine oder mehrere Dachflaechen eintragen.
* 4. Skript speichern.
* 5. Skript einmal manuell ausfuehren.
*
* Beim manuellen Ausfuehren werden automatisch angelegt:
* - Variable "Vorhersage Heute" in kWh
* - Variable "Vorhersage Morgen" in kWh
* - Variable "Status" mit dem letzten Abrufstatus
* - ein Skript-Timer fuer regelmaessige Aktualisierung
* - optional je Dachflaeche eigene Heute/Morgen-Variablen
*
* Optional koennen vorhandene Zielvariablen weiterverwendet werden. Dazu im
* Abschnitt "Optionale vorhandene Variablen" die passenden Objekt-IDs eintragen.
* Bleibt eine ID auf 0, legt das Skript die Variable automatisch unterhalb des
* Skripts an bzw. nutzt dort bereits vorhandene Variablen mit gleichem Namen.
*
* Hinweise:
* - Forecast.Solar Public erlaubt aktuell 12 Abrufe pro 60 Minuten pro IP.
* - Pro Dachflaeche wird ein API-Abruf benoetigt.
* - Das Skript prueft vor dem Abruf, ob das eingestellte Intervall zum
* Public-Limit passt, und bricht bei zu vielen Abrufen mit Statusmeldung ab.
* - Forecast.Solar verwendet beim Azimut: Sued = 0, Ost = -90, West = 90.
*/
/* ------------------------------ Konfiguration ------------------------------ */
// Eine oder mehrere Dachflaechen. Bei nur einer Flaeche bleibt genau ein Block
// im Array stehen. Fuer weitere Flaechen einfach weitere Blöcke ergaenzen.
$ROOF_PLANES = [
[
'name' => 'PV-Dachflaeche 1',
'latitude' => 51.901135, // Breitengrad, z. B. 51.901135
'longitude' => 8.623329, // Laengengrad, z. B. 8.623329
'declination' => 20, // Dachneigung in Grad
'azimuth' => 0, // Sued = 0, Ost = -90, West = 90
'kwp' => 10.0, // installierte Leistung dieser Flaeche in kWp
],
// Beispiel fuer eine zweite Flaeche:
// [
// 'name' => 'PV-Dachflaeche 2',
// 'latitude' => 51.901135,
// 'longitude' => 8.623329,
// 'declination' => 20,
// 'azimuth' => 90,
// 'kwp' => 5.0,
// ],
];
$UPDATE_INTERVAL_MINUTES = 60; // Timerintervall. 60 Minuten schont das API-Limit.
$MAX_CALLS_PER_60_MINUTES = 12; // Forecast.Solar Public Limit pro IP.
$SHOW_ROOF_PLANE_DETAILS = false; // true = zusaetzlich Werte je Dachflaeche anzeigen.
$HTTP_TIMEOUT_SECONDS = 20;
$DEBUG = false;
/* ---------------------- Optionale vorhandene Variablen --------------------- */
// 0 = automatisch anlegen bzw. unterhalb dieses Skripts suchen.
// Alternativ vorhandene Float-Variablen-IDs eintragen, z. B. aus einer
// bestehenden Visualisierung oder einer bereits vorhandenen Regelung.
$TARGET_TODAY_ID = 0; // Variable fuer Vorhersage Heute in kWh
$TARGET_TOMORROW_ID = 0; // Variable fuer Vorhersage Morgen in kWh
$TARGET_STATUS_ID = 0; // optionale String-Variable fuer den Abrufstatus
/* ------------------------------ Programmstart ------------------------------ */
$sender = $_IPS['SENDER'] ?? 'Execute';
$scriptId = $_IPS['SELF'] ?? 0;
$isManualRun = in_array($sender, ['Execute', 'RunScript'], true);
$isTimerRun = ($sender === 'TimerEvent');
if ($isManualRun) {
SetupObjects($scriptId, $UPDATE_INTERVAL_MINUTES, $TARGET_TODAY_ID, $TARGET_TOMORROW_ID, $TARGET_STATUS_ID);
}
if ($isManualRun || $isTimerRun) {
UpdatePvForecast(
$scriptId,
$ROOF_PLANES,
$UPDATE_INTERVAL_MINUTES,
$MAX_CALLS_PER_60_MINUTES,
$SHOW_ROOF_PLANE_DETAILS,
$HTTP_TIMEOUT_SECONDS,
$DEBUG,
$TARGET_TODAY_ID,
$TARGET_TOMORROW_ID,
$TARGET_STATUS_ID
);
}
/* -------------------------------- Funktionen ------------------------------- */
function SetupObjects(int $scriptId, int $updateIntervalMinutes, int $targetTodayId, int $targetTomorrowId, int $targetStatusId): void
{
ResolveTargetVariable($scriptId, $targetTodayId, 'Vorhersage Heute', 'PV_FORECAST_TODAY', 2, 1, '~Electricity');
ResolveTargetVariable($scriptId, $targetTomorrowId, 'Vorhersage Morgen', 'PV_FORECAST_TOMORROW', 2, 2, '~Electricity');
ResolveTargetVariable($scriptId, $targetStatusId, 'Status', 'PV_FORECAST_STATUS', 3, 3, '');
IPS_SetScriptTimer($scriptId, max(1, $updateIntervalMinutes) * 60);
SetForecastStatus($scriptId, $targetStatusId, 'Setup abgeschlossen. Timerintervall: ' . $updateIntervalMinutes . ' Minuten.');
}
function UpdatePvForecast(
int $scriptId,
array $roofPlanes,
int $updateIntervalMinutes,
int $maxCallsPer60Minutes,
bool $showRoofPlaneDetails,
int $timeoutSeconds,
bool $debug,
int $targetTodayId,
int $targetTomorrowId,
int $targetStatusId
): void {
$todayId = ResolveTargetVariable($scriptId, $targetTodayId, 'Vorhersage Heute', 'PV_FORECAST_TODAY', 2, 1, '~Electricity');
$tomorrowId = ResolveTargetVariable($scriptId, $targetTomorrowId, 'Vorhersage Morgen', 'PV_FORECAST_TOMORROW', 2, 2, '~Electricity');
$rateLimitStatus = CheckRateLimit($roofPlanes, $updateIntervalMinutes, $maxCallsPer60Minutes);
if (!$rateLimitStatus['ok']) {
SetForecastStatus($scriptId, $targetStatusId, $rateLimitStatus['status']);
IPS_LogMessage('PV-Vorhersage', $rateLimitStatus['status']);
return;
}
$forecast = FetchCombinedForecastSolar($roofPlanes, $timeoutSeconds, $debug);
if (!$forecast['ok']) {
SetForecastStatus($scriptId, $targetStatusId, $forecast['status']);
IPS_LogMessage('PV-Vorhersage', $forecast['status']);
return;
}
$today = date('Y-m-d');
$tomorrow = date('Y-m-d', strtotime('+1 day'));
$days = $forecast['watt_hours_day'];
if (!isset($days[$today])) {
SetForecastStatus($scriptId, $targetStatusId, 'Fehler: Forecast.Solar liefert keinen Tageswert fuer heute (' . $today . ').');
return;
}
if (!isset($days[$tomorrow])) {
SetForecastStatus($scriptId, $targetStatusId, 'Fehler: Forecast.Solar liefert keinen Tageswert fuer morgen (' . $tomorrow . ').');
return;
}
$todayKwh = round(((float)$days[$today]) / 1000, 2);
$tomorrowKwh = round(((float)$days[$tomorrow]) / 1000, 2);
SetValueFloat($todayId, $todayKwh);
SetValueFloat($tomorrowId, $tomorrowKwh);
if ($showRoofPlaneDetails) {
UpdateRoofPlaneVariables($scriptId, $forecast['roof_planes'], $today, $tomorrow);
}
SetForecastStatus(
$scriptId,
$targetStatusId,
sprintf(
'OK: %s - Heute %.2f kWh, Morgen %.2f kWh. HTTP %d.',
date('d.m.Y H:i:s'),
$todayKwh,
$tomorrowKwh,
$forecast['http_code']
)
);
}
function CheckRateLimit(array $roofPlanes, int $updateIntervalMinutes, int $maxCallsPer60Minutes): array
{
$roofCount = count($roofPlanes);
if ($roofCount < 1) {
return [
'ok' => false,
'status' => 'Konfigurationsfehler: Es ist keine Dachflaeche in $ROOF_PLANES eingetragen.',
];
}
$interval = max(1, $updateIntervalMinutes);
$callsPer60Minutes = $roofCount * (60 / $interval);
if ($callsPer60Minutes > $maxCallsPer60Minutes) {
return [
'ok' => false,
'status' => sprintf(
'Konfigurationsfehler: %d Dachflaechen bei %d Minuten Intervall ergeben %.1f API-Abrufe pro 60 Minuten. Erlaubt sind %d. Intervall erhoehen oder Flaechen reduzieren.',
$roofCount,
$interval,
$callsPer60Minutes,
$maxCallsPer60Minutes
),
];
}
return [
'ok' => true,
'status' => sprintf(
'Rate-Limit OK: %d Dachflaeche(n), %.1f API-Abrufe pro 60 Minuten.',
$roofCount,
$callsPer60Minutes
),
];
}
function FetchCombinedForecastSolar(array $roofPlanes, int $timeoutSeconds, bool $debug): array
{
$combinedDays = [];
$roofPlaneResults = [];
$httpCodes = [];
$roofIndex = 0;
foreach ($roofPlanes as $roofPlane) {
$roofIndex++;
$name = (string)($roofPlane['name'] ?? ('Dachflaeche ' . $roofIndex));
foreach (['latitude', 'longitude', 'declination', 'azimuth', 'kwp'] as $requiredKey) {
if (!array_key_exists($requiredKey, $roofPlane)) {
return [
'ok' => false,
'http_code' => 0,
'status' => 'Konfigurationsfehler: "' . $name . '" enthaelt keinen Wert fuer "' . $requiredKey . '".',
'watt_hours_day' => [],
];
}
}
$forecast = FetchForecastSolar(
(float)$roofPlane['latitude'],
(float)$roofPlane['longitude'],
(float)$roofPlane['declination'],
(float)$roofPlane['azimuth'],
(float)$roofPlane['kwp'],
$timeoutSeconds,
$debug
);
$httpCodes[] = $forecast['http_code'];
if (!$forecast['ok']) {
return [
'ok' => false,
'http_code' => $forecast['http_code'],
'status' => 'Fehler bei "' . $name . '": ' . $forecast['status'],
'watt_hours_day' => [],
];
}
foreach ($forecast['watt_hours_day'] as $day => $wattHours) {
if (!isset($combinedDays[$day])) {
$combinedDays[$day] = 0;
}
$combinedDays[$day] += (float)$wattHours;
}
$roofPlaneResults[] = [
'index' => $roofIndex,
'name' => $name,
'watt_hours_day' => $forecast['watt_hours_day'],
];
}
return [
'ok' => true,
'http_code' => empty($httpCodes) ? 0 : max($httpCodes),
'status' => 'OK',
'watt_hours_day' => $combinedDays,
'roof_planes' => $roofPlaneResults,
];
}
function UpdateRoofPlaneVariables(int $scriptId, array $roofPlaneResults, string $today, string $tomorrow): void
{
$position = 10;
foreach ($roofPlaneResults as $roofPlane) {
$index = (int)$roofPlane['index'];
$name = (string)$roofPlane['name'];
$days = $roofPlane['watt_hours_day'];
$todayValue = isset($days[$today]) ? round(((float)$days[$today]) / 1000, 2) : 0.0;
$tomorrowValue = isset($days[$tomorrow]) ? round(((float)$days[$tomorrow]) / 1000, 2) : 0.0;
$todayId = GetOrCreateVariable(
$scriptId,
$name . ' Heute',
'PV_FORECAST_PLANE_' . $index . '_TODAY',
2,
$position,
'~Electricity'
);
$tomorrowId = GetOrCreateVariable(
$scriptId,
$name . ' Morgen',
'PV_FORECAST_PLANE_' . $index . '_TOMORROW',
2,
$position + 1,
'~Electricity'
);
SetValueFloat($todayId, $todayValue);
SetValueFloat($tomorrowId, $tomorrowValue);
$position += 2;
}
}
function FetchForecastSolar(
float $latitude,
float $longitude,
float $declination,
float $azimuth,
float $kwp,
int $timeoutSeconds,
bool $debug
): array {
$url = sprintf(
'https://api.forecast.solar/estimate/%s/%s/%s/%s/%s',
rawurlencode((string)$latitude),
rawurlencode((string)$longitude),
rawurlencode((string)$declination),
rawurlencode((string)$azimuth),
rawurlencode((string)$kwp)
);
if ($debug) {
IPS_LogMessage('PV-Vorhersage Debug', 'URL: ' . $url);
}
$context = stream_context_create([
'http' => [
'timeout' => max(1, $timeoutSeconds),
'ignore_errors' => true,
'header' => "User-Agent: IP-Symcon PV-Vorhersage\r\n",
],
]);
$response = @file_get_contents($url, false, $context);
$httpCode = ParseHttpCode($http_response_header ?? []);
if ($response === false) {
return [
'ok' => false,
'http_code' => $httpCode,
'status' => 'Fehler: Forecast.Solar ist nicht erreichbar oder antwortet nicht. HTTP ' . $httpCode . '.',
'watt_hours_day' => [],
];
}
if ($debug) {
IPS_LogMessage('PV-Vorhersage Debug', 'Antwort: ' . substr($response, 0, 500));
}
$data = json_decode($response, true);
if (!is_array($data)) {
return [
'ok' => false,
'http_code' => $httpCode,
'status' => 'Fehler: Forecast.Solar Antwort ist kein gueltiges JSON. HTTP ' . $httpCode . '.',
'watt_hours_day' => [],
];
}
if ($httpCode < 200 || $httpCode >= 300) {
$message = ExtractForecastSolarMessage($data);
return [
'ok' => false,
'http_code' => $httpCode,
'status' => 'Fehler: Forecast.Solar meldet HTTP ' . $httpCode . '. ' . $message,
'watt_hours_day' => [],
];
}
if (!isset($data['result']['watt_hours_day']) || !is_array($data['result']['watt_hours_day'])) {
$message = ExtractForecastSolarMessage($data);
return [
'ok' => false,
'http_code' => $httpCode,
'status' => 'Fehler: Forecast.Solar Antwort enthaelt keine Tageswerte. ' . $message,
'watt_hours_day' => [],
];
}
return [
'ok' => true,
'http_code' => $httpCode,
'status' => 'OK',
'watt_hours_day' => $data['result']['watt_hours_day'],
];
}
function ResolveTargetVariable(int $scriptId, int $targetId, string $name, string $ident, int $type, int $position, string $profile): int
{
if ($targetId > 0) {
if (!IPS_ObjectExists($targetId)) {
throw new Exception('Konfigurationsfehler: Zielvariable "' . $name . '" mit ID ' . $targetId . ' existiert nicht.');
}
$object = IPS_GetObject($targetId);
if (($object['ObjectType'] ?? null) !== 2) {
throw new Exception('Konfigurationsfehler: Ziel-ID ' . $targetId . ' fuer "' . $name . '" ist keine Variable.');
}
$variable = IPS_GetVariable($targetId);
if (($variable['VariableType'] ?? null) !== $type) {
throw new Exception('Konfigurationsfehler: Zielvariable "' . $name . '" hat den falschen Typ.');
}
if ($profile !== '') {
IPS_SetVariableCustomProfile($targetId, $profile);
}
return $targetId;
}
return GetOrCreateVariable($scriptId, $name, $ident, $type, $position, $profile);
}
function GetOrCreateVariable(int $parentId, string $name, string $ident, int $type, int $position, string $profile): int
{
$id = @IPS_GetObjectIDByIdent($ident, $parentId);
if ($id === false) {
$id = FindChildByName($parentId, $name);
}
if ($id === false) {
$id = IPS_CreateVariable($type);
IPS_SetParent($id, $parentId);
IPS_SetName($id, $name);
}
IPS_SetIdent($id, $ident);
IPS_SetPosition($id, $position);
if ($profile !== '') {
IPS_SetVariableCustomProfile($id, $profile);
}
return $id;
}
function FindChildByName(int $parentId, string $name)
{
foreach (IPS_GetChildrenIDs($parentId) as $childId) {
$object = IPS_GetObject($childId);
if (($object['ObjectName'] ?? '') === $name) {
return $childId;
}
}
return false;
}
function SetForecastStatus(int $scriptId, int $targetStatusId, string $status): void
{
$statusId = ResolveTargetVariable($scriptId, $targetStatusId, 'Status', 'PV_FORECAST_STATUS', 3, 3, '');
SetValueString($statusId, $status);
}
function ParseHttpCode(array $headers): int
{
if (!isset($headers[0])) {
return 0;
}
if (preg_match('/HTTP\/\S+\s+(\d{3})/', $headers[0], $matches) === 1) {
return (int)$matches[1];
}
return 0;
}
function ExtractForecastSolarMessage(array $data): string
{
if (isset($data['message']['text'])) {
return (string)$data['message']['text'];
}
if (isset($data['message']) && is_string($data['message'])) {
return $data['message'];
}
if (isset($data['error']) && is_string($data['error'])) {
return $data['error'];
}
return '';
}