PV Prognosedaten für forecast.solar

Da leider der Dienst von Solarprognose.de eingestellt wrude, habe ich nach einer Alternative gesucht. Gelandet bin ich bei forecast.solar. Ich habe mir die Professional geholt und hab jetzt mal 14 Tage Testzeitraum.

Mein ursprüngliches Skript habe ich etwas umgebaut. Eventuell kann es ja jemand brauchen.

Es macht noch ein paar Sachen mehr aber die Variablen muss man ja nicht befüllen lassen.

<?php
$diagramm_html_id=20052;
$diagramm_balkon_html_id=29944;
$prognoseheuteid=51826;
$prognosebalkonheuteid=43989;

$endtag=6; //die nächsten Tage

$haus = [
    "lat"=>48.3843,
    "lon"=>15.6111,
    "neigung"=>25,
    "azimut"=>180,
    "kwp"=>2
];

$balkon = [
    "lat"=>48.3843,
    "lon"=>15.6111,
    "neigung"=>35,
    "azimut"=>180,
    "kwp"=>0.7
];
$forecast_api_key="APIKEY";
function datenholen($anlage,$api_key)
{
    
    $url="https://api.forecast.solar/".$api_key."/estimate/watthours/day/".$anlage["lat"]."/".$anlage["lon"]."/".$anlage["neigung"]."/.".$anlage["azimut"]."/".$anlage["kwp"];
    $datenjson=file_get_contents($url);
    return $datenjson;   
}

function erstelleSVG($daten, $endtag, $farbe="green",$breite=700,$hoehe=320) {
    $padding_links = 20;
    $padding_rechts = 0;
    $padding_oben = 10;
    $padding_unten =20;

    $balken_anzahl = $endtag + 1;
    $zeichen_breite = $breite - $padding_links - $padding_rechts;

    $balken_breite = ($zeichen_breite / $balken_anzahl) * 0.7;
    $abstand = ($zeichen_breite / $balken_anzahl);

    // ===== MAX bestimmen =====
    $max = 0;
    for ($i=0; $i <= $endtag; $i++) {
        $wert = $daten["result"][date("Y-m-d", time()+(3600*24)*$i)];
        if ($wert > $max) $max = $wert;
    }
    if ($max == 0) $max = 1;
    // ===== perfekte Y-Skalierung =====
    $range = $max;

    // Grundschritt grob bestimmen
    $raw_step = $range / 4; // weniger Steps → weniger Luft

    $pow = pow(10, floor(log10($raw_step)));
    $multipliers = [1, 2, 2.5, 5, 10];

    foreach ($multipliers as $m) {
        if ($pow * $m >= $raw_step) {
            $step_value = $pow * $m;
            break;
        }
    }

    // Maximalwert knapp über echten Max setzen
    $max_skala = ceil($max / $step_value) * $step_value;

    // Anzahl Steps dynamisch!
    $steps = intval($max_skala / $step_value);

        $nutzhoehe = $hoehe - $padding_oben - $padding_unten;

        $svg = "<svg width='100%' height='100%' viewBox='0 0 $breite $hoehe' xmlns='http://www.w3.org/2000/svg'>";
        $svg .= "<rect width='100%' height='100%' fill='transparent'/>";

        // ===== Y-ACHSE + GRID =====
        for ($i = 0; $i <= $steps; $i++) {

            $wert = $i * $step_value;
            $y = $padding_oben + ($max_skala - $wert) / $max_skala * $nutzhoehe;

        // Gridlinie
        $svg .= "<line x1='$padding_links' y1='$y' x2='".($breite - $padding_rechts)."' y2='$y' stroke='#ccc' stroke-width='1'/>";

        // Beschriftung
        $svg .= "<text x='".($padding_links - 5)."' y='".($y + 5)."' font-size='12' text-anchor='end' fill='white'>"
              . $wert . "</text>";
        }

        // Y-Achse
        $svg .= "<line x1='$padding_links' y1='$padding_oben' x2='$padding_links' y2='".($padding_oben + $nutzhoehe)."' stroke='white'/>";

        // ===== BALKEN =====
        for ($i=0; $i <= $endtag; $i++) {

        $wert = $daten["result"][date("Y-m-d", time()+(3600*24)*$i)];

        $x = $padding_links + $i * $abstand;
        $balken_h = ($wert / $max_skala) * $nutzhoehe;
        $y = $padding_oben + ($nutzhoehe - $balken_h);

        // Balken
        $svg .= "<rect x='$x' y='$y' width='$balken_breite' height='$balken_h' fill='$farbe'>
                    <title>".date("d.", time()+(3600*24)*$i)." : ".number_format(round($wert,1), 2, ',', '.')."</title>
                 </rect>";

        // Wert über Balken
        $svg .= "<text x='".($x + $balken_breite/2)."' y='".($y - 5)."' font-size='14' text-anchor='middle' fill='white'>"
              . number_format(round($wert,1), 0, ',', '.') . "</text>";

        // Datum unten
        $datum = date("d.", time()+(3600*24)*$i);
        $svg .= "<text x='".($x + $balken_breite/2)."' y='".($hoehe + 0)."' font-size='14' text-anchor='middle' fill='white'>"
              . $datum . "</text>";
    }

    $svg .= "</svg>";

    return $svg;
}
//Hauptanlage

//Tagesdaten aufbereiten
$daten_json=datenholen($haus,$forecast_api_key);
$daten=json_decode($daten_json,true); 

//Balkendiagram
$svg = erstelleSVG($daten, $endtag, "green",700,500);
setvalue($diagramm_html_id, $svg);
//Prognose eintragen
SetValue($prognoseheuteid,$daten["result"][date("Y-m-d",time())]);

//Balkon
//Tagesdaten aufbereiten
$daten_json=datenholen($balkon,$forecast_api_key);
$daten=json_decode($daten_json,true); 
//Balkendiagram
$svg = erstelleSVG($daten, $endtag, "green",800,310);
setvalue($diagramm_balkon_html_id, $svg);
//Prognose eintragen
SetValue($prognosebalkonheuteid,$daten["result"][date("Y-m-d",time())]);

Es wird eine SVG Grafik für die Tage erstellt.

Sollte jemand ein Modul daraus basteln wollen, nur zu :wink:

Vielleicht gefällt Dir das hier?

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 '';
}