[BETA][Skript] Octopus Energy Deutschland – Strompreise via Kraken API in IP-Symcon

Hallo zusammen,

ich habe ein PHP-Script entwickelt, das die aktuellen Strompreise von Octopus Energy Deutschland über die offizielle Kraken GraphQL API abruft und automatisch als Variablen in IP-Symcon speichert.


:high_voltage: Was macht das Script?

  • Authentifizierung via E-Mail & Passwort an der Octopus Kraken API

  • Erkennt automatisch den aktuell aktiven Tarif (z.B. Octopus Go)

  • Liest den aktuellen Strompreis (Brutto & Netto in ct/kWh) aus dem aktiven Zeitfenster

  • Speichert eine Preisvorschau der nächsten 24h als JSON

  • Legt alle Variablen automatisch unterhalb des Scripts an und aktualisiert sie

  • Erstellt automatisch ein zyklisches Event (Standard: alle 15 Minuten)

  • Integrierter Debug-Modus (ein/aus per Variable)

  • Fehlerprotokollierung im IP-Symcon Logbuch


:card_index_dividers: Angelegte Objekte (vollautomatisch)

:page_facing_up: Octopus Energy Script
├── :stopwatch: Auto-Abfrage alle 15 Min ← Zyklisches Event
├── :file_folder: Status
│ ├── Letzte Ausführung
│ ├── Token Status → :white_check_mark: OK (312 ms)
│ ├── API Status → :white_check_mark: OK (458 ms)
│ ├── Letzter Fehler
│ ├── Fehler Timestamp
│ └── Erfolgreiche Abfragen → 42 Abfragen
├── :file_folder: Tarif
│ ├── Aktiver Tarif → „Octopus Go“
│ ├── Vollständiger Name
│ ├── Gültig ab
│ ├── Gültig bis
│ └── Letzte Abfrage
├── :file_folder: Aktueller Preis
│ ├── Brutto ct/kWh → 12.3400 ct/kWh
│ ├── Netto ct/kWh → 10.3700 ct/kWh
│ ├── Zeitfenster → „Off-Peak (22:00 - 06:00)“
│ └── Letzte Abfrage
└── :file_folder: Preisvorschau
├── Forecast JSON → „[{…}]“
├── Anzahl Slots → 96 Abfragen
└── Letzte Abfrage


:white_check_mark: Voraussetzungen

  • IP-Symcon ab Version 6.x

  • Octopus Energy Deutschland Kundenkonto

  • Login-Daten (E-Mail & Passwort)

  • Kundennummer (Format: A-XXXXXXXX, im Kundenportal sichtbar)


:wrench: Einrichtung

  1. Neues PHP-Script in IP-Symcon anlegen

  2. Script-Inhalt einfügen (siehe unten)

  3. Nur den Konfigurationsbereich oben im Script anpassen:


$email           = "deine@email.de";   // Octopus Login E-Mail
$password        = "deinPasswort";     // Octopus Login Passwort
$accountNumber   = "A-XXXXXXXX";       // Kundennummer
  1. Script einmal manuell ausführen – alle Variablen und das zyklische Event werden automatisch angelegt

:clipboard: Getestete Tarife

Tarif Unterstützt
Octopus Go (Zeitfenster) :white_check_mark:
Festpreis-Tarife :white_check_mark:

:folded_hands: Feedback

Ich freue mich über Rückmeldungen, Verbesserungsvorschläge und Erfahrungsberichte – besonders von Nutzern anderer Octopus-Tarife (Relax, Agile etc.).
Oder sollte sich jemand finden, der dies weiterentwickeln will bzw. ein Modul daraus baut, mir fehlt hierzu leider die Freizeit neben der Familie und das Wetter wird schöner und schöner :sweat_smile: .


Getestet mit IP-Symcon 6.x und Octopus Go Tarif (Deutschland)

Viele Grüße
Stefan

<?php
// ╔══════════════════════════════════════════════════════════════════════╗
// ║           OCTOPUS ENERGY – STROMPREIS ABFRAGE                        ║
// ║           für IP-Symcon                                              ║
// ╠══════════════════════════════════════════════════════════════════════╣
// ║  BESCHREIBUNG:                                                       ║
// ║  Dieses Script fragt über die Octopus Energy (Kraken) API            ║
// ║  den aktuellen Strompreis sowie die Preisvorschau für die            ║
// ║  nächsten 24h ab und speichert diese als Variablen in                ║
// ║  IP-Symcon direkt unterhalb dieses Scripts.                          ║
// ║                                                                      ║
// ║  VORAUSSETZUNGEN:                                                    ║
// ║  - Octopus Energy Deutschland Kundenkonto                            ║
// ║  - Kundennummer (Format: A-XXXXXXXX)                                 ║
// ║  - IP-Symcon ab Version 6.x                                          ║
// ║                                                                      ║
// ║  ANGELEGTE OBJEKTE (automatisch):                                    ║
// ║  - Event:       Zyklische Ausführung (Standard: alle 15 Min)         ║
// ║  - Kategorie:   Status    – Token/API Status, Fehlermeldungen        ║
// ║  - Kategorie:   Tarif     – Tarifname, Laufzeit                      ║
// ║  - Kategorie:   Aktueller Preis – Brutto/Netto, Zeitfenster          ║
// ║  - Kategorie:   Preisvorschau   – JSON mit allen Slots 24h           ║
// ║                                                                      ║
// ║  UNTERSTÜTZTE/GETESTETE TARIFE:                                      ║
// ║  - Octopus Go        (Zeitfenster-Tarif)                             ║
// ║  - Festpreis-Tarife  (SimpleProductUnitRateInformation)              ║
// ║                                                                      ║
// ║  API ENDPUNKT/Dokumentation:                                         ║
// ║  https://api.oeg-kraken.energy/v1/graphql/                           ║
// ║                                                                      ║
// ║  AUTOR:    Kelevra26                                                 ║
// ║  VERSION:  1.0                                                       ║
// ║  DATUM:    23.04.2026                                                ║
// ╚══════════════════════════════════════════════════════════════════════╝

// ╔══════════════════════════════════════════════════════════════════════╗
// ║                        KONFIGURATION                                 ║
// ║                  Nur diesen Bereich anpassen!                       ║
// ╠══════════════════════════════════════════════════════════════════════╣

$email           = "deine@email.de";   // Octopus Login E-Mail
$password        = "deinPasswort";     // Octopus Login Passwort
$accountNumber   = "A-XXXXXXXX";       // Kundennummer (A-XXXXXXXX)
$intervalMinutes = 15;                 // Abfrage-Intervall in Minuten
$debug           = false;              // Debug-Meldungen: true = AN / false = AUS

// ╚══════════════════════════════════════════════════════════════════════╝

$url = "https://api.oeg-kraken.energy/v1/graphql/";

$headers_base = [
    'Content-Type: application/json',
    'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    'Accept: application/json',
    'Origin: https://api.oeg-kraken.energy',
    'Referer: https://api.oeg-kraken.energy/v1/graphql/'
];

// ── Hilfsfunktionen ──────────────────────────────────────────────────
function log_msg($message, $isError = false) {
    global $debug;
    if ($isError || $debug) {
        $prefix = $isError ? "❌ " : "[DEBUG] ";
        IPS_LogMessage("Octopus", $prefix . $message);
    }
}

function dbg($message, $data = null) {
    global $debug;
    if (!$debug) return;
    $log = "[DEBUG] " . $message;
    if ($data !== null) {
        $log .= " → " . (is_array($data) || is_object($data) ? json_encode($data) : $data);
    }
    IPS_LogMessage("Octopus", $log);
}

function krakenRequest($url, $query, $variables = [], $token = null) {
    global $headers_base;
    $headers = $headers_base;
    if ($token) $headers[] = 'Authorization: ' . $token;

    dbg("krakenRequest START", ['url' => $url, 'variables' => $variables]);

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['query' => $query, 'variables' => $variables]));
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    $response  = curl_exec($ch);
    $httpCode  = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curlError = curl_error($ch);
    curl_close($ch);

    dbg("krakenRequest HTTP Code", $httpCode);

    if ($curlError) {
        log_msg("cURL Fehler: " . $curlError, true);
        return null;
    }

    dbg("krakenRequest RAW Response", $response);
    return json_decode($response, true);
}

function getOrCreateCategory($name, $parentId) {
    foreach (IPS_GetChildrenIDs($parentId) as $id) {
        if (IPS_ObjectExists($id)
            && IPS_GetObject($id)['ObjectType'] == 0
            && IPS_GetName($id) == $name) {
            return $id;
        }
    }
    $id = IPS_CreateCategory();
    IPS_SetName($id, $name);
    IPS_SetParent($id, $parentId);
    return $id;
}

function getOrCreateProfile($name, $type, $suffix, $digits = 2) {
    if (!IPS_VariableProfileExists($name)) {
        IPS_CreateVariableProfile($name, $type);
        IPS_SetVariableProfileText($name, '', ' ' . $suffix);
        IPS_SetVariableProfileDigits($name, $digits);
    }
    return $name;
}

function setVar($name, $type, $parentId, $value, $profile = '') {
    foreach (IPS_GetChildrenIDs($parentId) as $id) {
        if (IPS_ObjectExists($id)
            && IPS_GetObject($id)['ObjectType'] == 2
            && IPS_GetName($id) == $name) {
            SetValue($id, $value);
            dbg("setVar UPDATE", ['name' => $name, 'value' => $value]);
            return $id;
        }
    }
    $id = IPS_CreateVariable($type);
    IPS_SetName($id, $name);
    IPS_SetParent($id, $parentId);
    if ($profile !== '') {
        IPS_SetVariableCustomProfile($id, $profile);
    }
    SetValue($id, $value);
    dbg("setVar CREATE", ['name' => $name, 'value' => $value, 'type' => $type]);
    return $id;
}

function getOrCreateCyclicEvent($name, $scriptId, $intervalMinutes) {
    foreach (IPS_GetChildrenIDs($scriptId) as $id) {
        if (IPS_ObjectExists($id)
            && IPS_GetObject($id)['ObjectType'] == 4
            && IPS_GetName($id) == $name) {
            IPS_SetEventCyclic($id, 0, 0, 0, 0, 2, $intervalMinutes);
            IPS_SetEventCyclicTimeFrom($id, 0, 0, 0);
            IPS_SetEventCyclicTimeTo($id, 0, 0, 0);
            IPS_SetEventActive($id, true);
            dbg("Event aktualisiert", ['name' => $name, 'interval' => $intervalMinutes . ' Min']);
            return $id;
        }
    }
    $id = IPS_CreateEvent(1);
    IPS_SetName($id, $name);
    IPS_SetParent($id, $scriptId);
    IPS_SetEventCyclic($id, 0, 0, 0, 0, 2, $intervalMinutes);
    IPS_SetEventCyclicTimeFrom($id, 0, 0, 0);
    IPS_SetEventCyclicTimeTo($id, 0, 0, 0);
    IPS_SetEventActive($id, true);
    dbg("Event erstellt", ['name' => $name, 'interval' => $intervalMinutes . ' Min']);
    return $id;
}

// ── Script-ID & Kategorien ───────────────────────────────────────────
$scriptId    = $_IPS['SELF'];
$catStatus   = getOrCreateCategory('Status',           $scriptId);
$catTariff   = getOrCreateCategory('Tarif',            $scriptId);
$catCurrent  = getOrCreateCategory('Aktueller Preis',  $scriptId);
$catForecast = getOrCreateCategory('Preisvorschau',    $scriptId);

dbg("Kategorien angelegt/gefunden", [
    'Status'   => $catStatus,
    'Tarif'    => $catTariff,
    'Aktuell'  => $catCurrent,
    'Forecast' => $catForecast
]);

// ── Profile anlegen ──────────────────────────────────────────────────
$profileCtKwh = getOrCreateProfile('Octopus.CentPerKwh', 2, 'ct/kWh', 4);
$profileCount = getOrCreateProfile('Octopus.Count',      1, 'Abfragen', 0);

// ── Event anlegen ────────────────────────────────────────────────────
getOrCreateCyclicEvent('⏱ Auto-Abfrage alle ' . $intervalMinutes . ' Min', $scriptId, $intervalMinutes);

// ── Status initialisieren ────────────────────────────────────────────
setVar('Letzte Ausführung', 3, $catStatus, date('d.m.Y H:i:s'));
setVar('Token Status',      3, $catStatus, 'Wird abgefragt...');
setVar('API Status',        3, $catStatus, 'Wird abgefragt...');
setVar('Letzter Fehler',    3, $catStatus, '');
setVar('Fehler Timestamp',  3, $catStatus, '');

// Erfolgs-Zähler auslesen
$count = 0;
foreach (IPS_GetChildrenIDs($catStatus) as $id) {
    if (IPS_ObjectExists($id) && IPS_GetName($id) == 'Erfolgreiche Abfragen') {
        $count = GetValue($id);
        break;
    }
}
dbg("Aktueller Zählerstand", $count);

// ── 1. Token holen ───────────────────────────────────────────────────
dbg("Token wird abgefragt...");
$tokenStart  = microtime(true);
$tokenResult = krakenRequest($url,
    'mutation krakenTokenAuthentication($email: String!, $password: String!) {
        obtainKrakenToken(input: {email: $email, password: $password}) { token }
    }',
    ['email' => $email, 'password' => $password]
);
$tokenDauer = round((microtime(true) - $tokenStart) * 1000) . ' ms';

$token = $tokenResult['data']['obtainKrakenToken']['token'] ?? null;

dbg("Token Ergebnis", [
    'dauer'     => $tokenDauer,
    'erhalten'  => $token ? 'JA (gekürzt: ' . substr($token, 0, 20) . '...)' : 'NEIN',
    'raw_error' => $tokenResult['errors'][0]['message'] ?? 'kein Fehler'
]);

if (!$token) {
    $fehler = 'Token-Fehler: ' . ($tokenResult['errors'][0]['message'] ?? json_encode($tokenResult));
    setVar('Token Status',     3, $catStatus, '❌ Fehler');
    setVar('API Status',       3, $catStatus, '⏸ Nicht ausgeführt');
    setVar('Letzter Fehler',   3, $catStatus, $fehler);
    setVar('Fehler Timestamp', 3, $catStatus, date('d.m.Y H:i:s'));
    log_msg($fehler, true);
    return;
}

setVar('Token Status', 3, $catStatus, '✅ OK (' . $tokenDauer . ')');
dbg("Token OK", $tokenDauer);

// ── 2. API Abfrage ───────────────────────────────────────────────────
dbg("API wird abgefragt...");
$apiStart = microtime(true);
$result   = krakenRequest($url,
    'query($accountNumber: String!) {
        account(accountNumber: $accountNumber) {
            properties {
                electricityMalos {
                    agreements {
                        validFrom
                        validTo
                        product { displayName fullName }
                        unitRateInformation {
                            ... on TimeOfUseProductUnitRateInformation {
                                rates {
                                    netUnitRateCentsPerKwh
                                    latestGrossUnitRateCentsPerKwh
                                    timeslotName
                                    timeslotActivationRules {
                                        activeFromTime
                                        activeToTime
                                    }
                                }
                            }
                            ... on SimpleProductUnitRateInformation {
                                netUnitRateCentsPerKwh
                                latestGrossUnitRateCentsPerKwh
                            }
                        }
                        unitRateForecast {
                            validFrom
                            validTo
                            unitRateInformation {
                                ... on TimeOfUseProductUnitRateInformation {
                                    rates {
                                        netUnitRateCentsPerKwh
                                        latestGrossUnitRateCentsPerKwh
                                        timeslotName
                                    }
                                }
                                ... on SimpleProductUnitRateInformation {
                                    netUnitRateCentsPerKwh
                                    latestGrossUnitRateCentsPerKwh
                                }
                            }
                        }
                    }
                }
            }
        }
    }',
    ['accountNumber' => $accountNumber],
    $token
);
$apiDauer = round((microtime(true) - $apiStart) * 1000) . ' ms';

dbg("API Abfrage Ergebnis", [
    'dauer'      => $apiDauer,
    'fehler'     => $result['errors'][0]['message'] ?? 'kein Fehler',
    'agreements' => count($result['data']['account']['properties'][0]['electricityMalos'][0]['agreements'] ?? []) . ' gefunden'
]);

if (!$result || isset($result['errors'])) {
    $fehler = 'API Fehler: ' . ($result['errors'][0]['message'] ?? json_encode($result));
    setVar('API Status',       3, $catStatus, '❌ Fehler (' . $apiDauer . ')');
    setVar('Letzter Fehler',   3, $catStatus, $fehler);
    setVar('Fehler Timestamp', 3, $catStatus, date('d.m.Y H:i:s'));
    log_msg($fehler, true);
    return;
}

setVar('API Status', 3, $catStatus, '✅ OK (' . $apiDauer . ')');
dbg("API OK", $apiDauer);

// ── 3. Aktives Agreement finden ──────────────────────────────────────
$nowTs      = time();
$agreements = $result['data']['account']['properties'][0]['electricityMalos'][0]['agreements'] ?? [];
$activeAgreement = null;

dbg("Agreements gesamt", count($agreements));

foreach ($agreements as $idx => $agreement) {
    $from = strtotime($agreement['validFrom']);
    $to   = strtotime($agreement['validTo']);
    dbg("Agreement #" . $idx, [
        'produkt'  => $agreement['product']['displayName'] ?? '?',
        'validFrom'=> date('d.m.Y H:i', $from),
        'validTo'  => date('d.m.Y H:i', $to),
        'aktiv'    => ($nowTs >= $from && $nowTs < $to) ? 'JA' : 'NEIN'
    ]);
    if ($nowTs >= $from && $nowTs < $to) {
        $activeAgreement = $agreement;
        break;
    }
}

if (!$activeAgreement) {
    $fehler = 'Kein aktives Agreement gefunden';
    setVar('API Status',       3, $catStatus, '⚠️ ' . $fehler);
    setVar('Letzter Fehler',   3, $catStatus, $fehler);
    setVar('Fehler Timestamp', 3, $catStatus, date('d.m.Y H:i:s'));
    log_msg($fehler . ' | RAW: ' . json_encode($agreements), true);
    return;
}

dbg("Aktives Agreement", [
    'produkt'                  => $activeAgreement['product']['displayName'],
    'fullName'                 => $activeAgreement['product']['fullName'],
    'validFrom'                => $activeAgreement['validFrom'],
    'validTo'                  => $activeAgreement['validTo'],
    'unitRateInformation leer' => empty($activeAgreement['unitRateInformation']) ? 'JA' : 'NEIN',
    'unitRateForecast Slots'   => count($activeAgreement['unitRateForecast'] ?? [])
]);

// ── 4. Tarifinformationen speichern ──────────────────────────────────
$displayName = $activeAgreement['product']['displayName'] ?? 'unbekannt';
$fullName    = $activeAgreement['product']['fullName']    ?? 'unbekannt';

setVar('Aktiver Tarif',      3, $catTariff, $displayName);
setVar('Vollständiger Name', 3, $catTariff, $fullName);
setVar('Gültig ab',          3, $catTariff, date('d.m.Y H:i', strtotime($activeAgreement['validFrom'])));
setVar('Gültig bis',         3, $catTariff, date('d.m.Y H:i', strtotime($activeAgreement['validTo'])));
setVar('Letzte Abfrage',     3, $catTariff, date('d.m.Y H:i:s'));

dbg("Tarif gespeichert", $displayName);

// ── 5. Aktuellen Preis ermitteln ─────────────────────────────────────
$currentTime  = date('H:i:s');
$currentPrice = null;
$currentNet   = null;
$currentSlot  = 'unbekannt';
$unitRateInfo = $activeAgreement['unitRateInformation'];

dbg("unitRateInformation Typ", [
    'ist_array'       => is_array($unitRateInfo) ? 'JA' : 'NEIN',
    'hat_rates'       => isset($unitRateInfo['rates']) ? 'JA' : 'NEIN',
    'hat_netUnitRate' => isset($unitRateInfo['netUnitRateCentsPerKwh']) ? 'JA' : 'NEIN',
    'aktuelle_uhrzeit'=> $currentTime
]);

// Festpreis
if (isset($unitRateInfo['netUnitRateCentsPerKwh'])) {
    $currentPrice = floatval($unitRateInfo['latestGrossUnitRateCentsPerKwh']);
    $currentNet   = floatval($unitRateInfo['netUnitRateCentsPerKwh']);
    $currentSlot  = 'Festpreis';
    dbg("Festpreis erkannt", ['brutto' => $currentPrice, 'netto' => $currentNet]);
}

// Zeitfenster-Tarif (Go)
if (isset($unitRateInfo['rates'])) {
    dbg("Zeitfenster-Tarif erkannt, Anzahl rates", count($unitRateInfo['rates']));
    foreach ($unitRateInfo['rates'] as $rate) {
        dbg("Rate prüfen", [
            'name'   => $rate['timeslotName'],
            'brutto' => $rate['latestGrossUnitRateCentsPerKwh'],
            'regeln' => count($rate['timeslotActivationRules'])
        ]);
        foreach ($rate['timeslotActivationRules'] as $rule) {
            $from = $rule['activeFromTime'];
            $to   = $rule['activeToTime'];
            if ($from <= $to) {
                $active = ($currentTime >= $from && $currentTime < $to);
            } else {
                $active = ($currentTime >= $from || $currentTime < $to);
            }
            dbg("Zeitregel prüfen", [
                'von'   => $from,
                'bis'   => $to,
                'jetzt' => $currentTime,
                'aktiv' => $active ? 'JA' : 'NEIN'
            ]);
            if ($active) {
                $currentPrice = floatval($rate['latestGrossUnitRateCentsPerKwh']);
                $currentNet   = floatval($rate['netUnitRateCentsPerKwh']);
                $currentSlot  = $rate['timeslotName'] . ' (' . $from . ' - ' . $to . ')';
                break 2;
            }
        }
    }
}

if ($currentPrice !== null) {
    setVar('Brutto ct/kWh',  2, $catCurrent, $currentPrice, $profileCtKwh);
    setVar('Netto ct/kWh',   2, $catCurrent, $currentNet,   $profileCtKwh);
    setVar('Zeitfenster',    3, $catCurrent, $currentSlot);
    setVar('Letzte Abfrage', 3, $catCurrent, date('d.m.Y H:i:s'));
    dbg("Aktueller Preis gespeichert", [
        'slot'   => $currentSlot,
        'brutto' => $currentPrice,
        'netto'  => $currentNet
    ]);
} else {
    setVar('Status',         3, $catCurrent, '⚠️ Kein Preisslot – unitRateInformation leer');
    log_msg('Kein aktiver Preisslot gefunden | unitRateInformation RAW: ' . json_encode($unitRateInfo), true);
}

// ── 6. Forecast nächste 24h ──────────────────────────────────────────
$forecast   = $activeAgreement['unitRateForecast'] ?? [];
$preisliste = [];

dbg("Forecast Slots gesamt", count($forecast));

foreach ($forecast as $idx => $slot) {
    $slotFrom = strtotime($slot['validFrom']);
    $slotTo   = strtotime($slot['validTo']);
    $slotInfo = $slot['unitRateInformation'];
    $gross = null; $net = null; $name = '';

    if (isset($slotInfo['latestGrossUnitRateCentsPerKwh'])) {
        $gross = floatval($slotInfo['latestGrossUnitRateCentsPerKwh']);
        $net   = floatval($slotInfo['netUnitRateCentsPerKwh']);
    }
    if (isset($slotInfo['rates'][0])) {
        $gross = floatval($slotInfo['rates'][0]['latestGrossUnitRateCentsPerKwh']);
        $net   = floatval($slotInfo['rates'][0]['netUnitRateCentsPerKwh']);
        $name  = $slotInfo['rates'][0]['timeslotName'] ?? '';
    }

    if ($gross !== null) {
        $preisliste[] = [
            'von'   => date('d.m.Y H:i', $slotFrom),
            'bis'   => date('d.m.Y H:i', $slotTo),
            'name'  => $name,
            'gross' => $gross,
            'net'   => $net
        ];
        dbg("Forecast Slot #" . $idx, [
            'von'   => date('d.m.Y H:i', $slotFrom),
            'bis'   => date('d.m.Y H:i', $slotTo),
            'name'  => $name,
            'gross' => $gross
        ]);
    }
}

setVar('Forecast JSON',  3, $catForecast, json_encode($preisliste, JSON_PRETTY_PRINT));
setVar('Anzahl Slots',   1, $catForecast, count($preisliste), $profileCount);
setVar('Letzte Abfrage', 3, $catForecast, date('d.m.Y H:i:s'));

dbg("Forecast gespeichert", count($preisliste) . ' Slots');

// ── 7. Status abschließen ────────────────────────────────────────────
$count++;
setVar('Erfolgreiche Abfragen', 1, $catStatus, $count, $profileCount);
setVar('Letzter Fehler',        3, $catStatus, '');
setVar('Letzte Ausführung',     3, $catStatus, date('d.m.Y H:i:s'));

dbg("✅ Fertig", [
    'abfrage_nr' => $count,
    'token_dauer'=> $tokenDauer,
    'api_dauer'  => $apiDauer,
    'forecast'   => count($preisliste) . ' Slots'
]);
1 „Gefällt mir“