OpenData – Gasspeicher in Deutschland – Teil 4: Updates

Um in den Diagrammen immer die neusten Daten darzustellen, muss gasspeicher_history.json inkrementell mit der GIE-AGSI-API für deutsche Gasspeicherdaten aktualisiert werden. Die Kernidee: die bestehende Historie bleibt unangetastet, es werden ausschließlich fehlende Tage ergänzt und dedupliziert.

1 – Konfiguration

Zunächst werden drei Konstanten definiert:

API_KEY für die Authentifizierung gegenüber der GIE-API

COUNTRY_CODE (DE) für den Länderfilter, und JSON_FILE für den Zielpfad.

__DIR__ . '/gasspeicher_history.json' Der Pfad wid absoult gebildet gebildet und nicht relativ, weil ein Cronjob typischerweise mit einem anderen Arebeitsverzeichnis läuft als ein manueller Aufruf im Projektverzeichnis. Ohne __DIR__ würde das Skript abhängig vom Aufrufkontext eine falsche Datei lesen oder schreiben, im schlimmsten Fall eine neue leere Historie an unerwarteter Stelle anlegen.

2 – Bestandsaufnahme

if (file_exists(JSON_FILE)) {
$decoded = json_decode(file_get_contents(JSON_FILE), true);
if (is_array($decoded)) {
$existingData = $decoded;
foreach ($existingData as $entry) {
if (isset($entry['gasDayStart'])) {
$dateLookup[$entry['gasDayStart']] = true;
}
}
}
}

if (file_exists(JSON_FILE)) {
$decoded = json_decode(file_get_contents(JSON_FILE), true);
if (is_array($decoded)) {
$existingData = $decoded;
foreach ($existingData as $entry) {
if (isset($entry['gasDayStart'])) {
$dateLookup[$entry['gasDayStart']] = true;
}
}
}
}

Die Existenzprüfung ist notwendig, damit der Erstlauf (noch keine Datei vorhanden) nicht zum Fehler führt. $existingData und $dateLookup bleiben dann schlicht leere Arrays. $dateLookup ist ein assoziatives Array, das gasDayStart-Strings auf true mappt. Das ist die Standard-PHP-Technik für O(1)-Existenzprüfungen (isset() auf Array-Keys ist ein Hashtable-Lookup); die Alternative — in_array() über $existingData bei jedem neuen Eintrag — wäre O(n) pro Prüfung und würde bei wachsender Historie linear langsamer.

3 – API-Abruf

$apiUrl = 'https://agsi.gie.eu/api?country=' . strtolower(COUNTRY_CODE) . '&size=30';
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['x-key: ' . API_KEY, 'Accept: application/json']);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);

$apiUrl = 'https://agsi.gie.eu/api?country=' . strtolower(COUNTRY_CODE) . '&size=30';
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['x-key: ' . API_KEY, 'Accept: application/json']);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);

Mit dem URL-Parametersize=30 fragt das Skript die Daten der letzten 30 Tage ab anstatt nur die des aktuellen Tages. Sollte ein ein Lauf ausfallen (Serverausfall, Wartungsfenster, Cron-Fehlkonfiguration), schließt sich die entstandene Lücke beim nächsten erfolgreichen Lauf automatisch, weil die fehlenden Tage im 30-Tage-Fenster erneut mitkommen und über den $dateLookup-Abgleich als neu erkannt werden. CURLOPT_TIMEOUT auf 15s verhindert ein unbegrenztes Blockieren bei Netzwerkproblemen. Dis ist wichtig, weil das Skript typischerweise unbeaufsichtigt per Cron läuft und ein hängender Prozess sonst unbemerkt bliebe.

4 – Dreistufige Fehlerprüfung

Zuerst wird überprüft, ob die API erfolgreich abgefragt wurde. Sollte keine Antwort zurückgegeben worden sein (! $response) oder kein Dokument (httpCode !== 200) wird ein Fehler ausgeben und das Skript abgebrochen.

if ($httpCode !== 200 || !$response) { fwrite(STDERR, "Fehler beim API-Abruf. HTTP-Statuscode: $httpCode" . ($curlError ? " ($curlError)" : "") . "\n"); exit(1); }

if ($httpCode !== 200 || !$response) { fwrite(STDERR, "Fehler beim API-Abruf. HTTP-Statuscode: $httpCode" . ($curlError ? " ($curlError)" : "") . "\n"); exit(1); }

Anschließend wir überprüft, ob das Dokument ein Fehlermeldung enthält (!empty($apiData[‚error‘])). Die GIS-API hat die Eigenheit, keinen Fehlercode zurückzugeben, sondern ein valides Dokument mit Fehlermeldung.

$apiData = json_decode($response, true);
if (!empty($apiData['error'])) {
fwrite(STDERR, "API-Fehler: " . $apiData['error'] . (isset($apiData['message']) ? " - " . $apiData['message'] : "") . "\n");
exit(1);

$apiData = json_decode($response, true);
if (!empty($apiData['error'])) {
fwrite(STDERR, "API-Fehler: " . $apiData['error'] . (isset($apiData['message']) ? " - " . $apiData['message'] : "") . "\n");
exit(1);

Schließlich wird noch geprüft, ob überhaupt Daten geliefert wurden, um zu vermeiden, dass das Skript später abstürzt, weil keine Daten vorhanden sind.

if (!isset($apiData['data']) || !is_array($apiData['data'])) {
fwrite(STDERR, "Ungueltige oder leere API-Antwort erhalten.\n");
exit(1);
}

if (!isset($apiData['data']) || !is_array($apiData['data'])) {
fwrite(STDERR, "Ungueltige oder leere API-Antwort erhalten.\n");
exit(1);
}

Alle drei Fehlerpfade schreiben auf STDERR statt STDOUT und terminieren mit exit(1). Das ist die Grundlage für die Unterscheidbarkeit im Cron-Kontext: STDERR kann separat umgeleitet werden (2>> error.log), sodass echte Fehler auffallen (z. B. via Mail-Benachrichtigung bei nicht-leerem Cron-Output), während ein Lauf ohne neue Daten, kein Fehler, sondern Normalfall, auf STDOUT bleibt und den Exit-Code 0 behält.

5 – Bereinigung und Deduplizierung

foreach ($apiData['data'] as $apiEntry) {
$date = $apiEntry['gasDayStart'] ?? null;
if (!$date) continue;

if (!isset($dateLookup[$date])) {
$cleanedEntry = [
'status' => $apiEntry['status'] ?? 'E',
'gasDayStart' => $date,
'gasInStorage' => parseGieValue($apiEntry['gasInStorage'] ?? null),
// ... weitere Felder
];
$existingData[] = $cleanedEntry;
$dateLookup[$date] = true;
$newDataCount++;
}
}

foreach ($apiData['data'] as $apiEntry) {
$date = $apiEntry['gasDayStart'] ?? null;
if (!$date) continue;

if (!isset($dateLookup[$date])) {
$cleanedEntry = [
'status' => $apiEntry['status'] ?? 'E',
'gasDayStart' => $date,
'gasInStorage' => parseGieValue($apiEntry['gasInStorage'] ?? null),
// ... weitere Felder
];
$existingData[] = $cleanedEntry;
$dateLookup[$date] = true;
$newDataCount++;
}
}

Jetzt geht das Skript in einer Schleife (foreach) jeden einzelnen Datensatz durch, den die GIE-API für die letzten 30 Tage zurückgegeben hat ($apiData['data']).

foreach ($apiData['data'] as $apiEntry) {
$date = $apiEntry['gasDayStart'] ?? null;
if (!$date) continue;

foreach ($apiData['data'] as $apiEntry) {
$date = $apiEntry['gasDayStart'] ?? null;
if (!$date) continue;

Zuerst wird das Datum des Gastages (gasDayStart, z. B. "2026-08-17") ausgelesen. Sollte dieses Feld in der API-Antwort überraschenderweise fehlen (über den Null-Coalescing-Operator ?? abgefangen), wird $date auf null gesetzt.

Die Zeile if (!$date) continue; bricht die Verarbeitung für diesen spezifischen Eintrag sofort ab und springt zum nächsten Tag in der Schleife, falls kein gültiges Datum existiert. Das verhindert Folgefehler im Skript.

if (!isset($dateLookup[$date])) {

if (!isset($dateLookup[$date])) {

Das Skript prüft, ob das Datum des aktuellen API-Eintrags ($date) bereits in Ihrer lokalen JSON-Datei existiert.

$dateLookup ist ein assoziatives Array, das beim Laden der bestehenden JSON-Datei mit allen bereits gespeicherten Daten befüllt wurde (z.B. $dateLookup['2026-08-08'] = true)

$cleanedEntry = [ 'status' => $apiEntry['status'] ?? 'E', 'gasDayStart' => $date, 'gasInStorage' => parseGieValue($apiEntry['gasInStorage'] ?? null), // ... weitere Felder ]; $existingData[] = $cleanedEntry; $dateLookup[$date] = true; $newDataCount++;

$cleanedEntry = [ 'status' => $apiEntry['status'] ?? 'E', 'gasDayStart' => $date, 'gasInStorage' => parseGieValue($apiEntry['gasInStorage'] ?? null), // ... weitere Felder ]; $existingData[] = $cleanedEntry; $dateLookup[$date] = true; $newDataCount++;

Hier wird ein neues, bereinigtes Array ($cleanedEntry) für die lokale Speicherung aufgebaut

Für das Feld status (das die Datenqualität wie C für Confirmed oder E für Estimated angibt) wird ein Fallback definiert. Falls die API keinen Status liefert, wird standardmäßig 'E' (Estimated) angenommen

parseGieValue() ist die wichtigste Bereinigung. Die GIE-API liefert Zahlenwerte oft als Text-Strings oder bei Datenlücken als Bindestrich "-". Die Hilfsfunktion parseGieValue() fängt diese Sonderfälle ab und konvertiert sie in echte Gleitkommazahlen (float) oder saubere null-Wert.

function parseGieValue($val) {
if ($val === null || $val === '-' || trim((string)$val) === '') {
return null;
}
return (float)$val;
}

function parseGieValue($val) {
if ($val === null || $val === '-' || trim((string)$val) === '') {
return null;
}
return (float)$val;
}

Zeile 12 -14:

$existingData[] = $cleanedEntry;
$dateLookup[$date] = true;
$newDataCount++;

$existingData[] = $cleanedEntry;
$dateLookup[$date] = true;
$newDataCount++;

Der frisch bereinigte Datensatz wird an das Ende des Arrays $existingData angehängt (welches alle historischen Daten enthält)

Das neue Datum wird dann in $dateLookup als true markiert. Sollte die API-Antwort denselben Tag doppelt enthalten, wird er beim zweiten Durchlauf der Schleife dank dieser Zeile blockiert.

Der Zähler $newDataCount wird um eins erhöht. Am Ende des Gesamtskripts wird anhand überprüft , obnewDataCount > 0 ist und lokale JSON-Datei tatsächlich neu auf die Festplatte geschrieben werden muss

6 – Sortierung und bedingtes Schreiben

if ($newDataCount > 0) {
usort($existingData, function ($a, $b) {
return strcmp($a['gasDayStart'], $b['gasDayStart']);
});
$newJsonContent = json_encode($existingData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
if (file_put_contents(JSON_FILE, $newJsonContent) !== false) {
echo "Erfolgreich aktualisiert: $newDataCount neue(r) Eintrag/Einträge hinzugefügt.\n";
} else {
fwrite(STDERR, "Fehler: JSON-Datei konnte nicht beschrieben werden. Berechtigungen pruefen.\n");
exit(1);
}
} else {
echo "Datenbestand bereits auf dem neuesten Stand. Keine neuen Einträge gefunden.\n";
}

if ($newDataCount > 0) {
usort($existingData, function ($a, $b) {
return strcmp($a['gasDayStart'], $b['gasDayStart']);
});
$newJsonContent = json_encode($existingData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
if (file_put_contents(JSON_FILE, $newJsonContent) !== false) {
echo "Erfolgreich aktualisiert: $newDataCount neue(r) Eintrag/Einträge hinzugefügt.\n";
} else {
fwrite(STDERR, "Fehler: JSON-Datei konnte nicht beschrieben werden. Berechtigungen pruefen.\n");
exit(1);
}
} else {
echo "Datenbestand bereits auf dem neuesten Stand. Keine neuen Einträge gefunden.\n";
}

Dieser Code-Abschnitt steuert den abschließenden Sortierungs-, Formatierungs- und Speichervorgang der Gasspeicherdaten. Er stellt sicher, dass Festplatten-Schreibzugriffe nur bei echten Datenänderungen stattfinden, die Daten chronologisch konsistent bleiben und Fehler beim Schreiben sauber abgefangen werden.

1. Bedingter Schreibschutz (if ($newDataCount > 0))

if ($newDataCount > 0) {

if ($newDataCount > 0) {

Das Skript prüft zuerst, ob der Zähler für neue Einträge ($newDataCount) größer als Null ist.

Der Zweck: Liegen keine neuen Gastage vor (was an den meisten Tagen der Fall ist, da die API nur einmal täglich aktualisiert wird), bleibt die lokale JSON-Datei physisch unangetastet. Das spart Systemressourcen (I/O-Zyklen auf der Festplatte).

Ist der Datenbestand bereits aktuell, wird lediglich eine informative Meldung auf dem Standard-Ausgabekanal (STDOUT) ausgegeben, und das Skript beendet sich ohne Schreibvorgang.

2. Chronologische Sortierung (usort & strcmp)

usort($existingData, function ($a, $b) {
return strcmp($a['gasDayStart'], $b['gasDayStart']);
});

usort($existingData, function ($a, $b) {
return strcmp($a['gasDayStart'], $b['gasDayStart']);
});

Der Zweck: Da die API-Daten nicht zwingend in der richtigen Reihenfolge geliefert werden oder neue Einträge unsortiert an das Ende des lokalen Arrays angehängt wurden, muss die gesamte Zeitreihe vor dem Speichern geordnet werden.
Unsortierte Daten erzeugen in ECharts Darstellungsfehler.

Die Funktionsweise: usort sortiert das Array $existingData nach benutzerdefinierten Kriterien. Die anonyme Vergleichsfunktion nutzt strcmp, um die Datums-Strings im Format YYYY-MM-DD (Feld gasDayStart) miteinander zu vergleichen. Dies garantiert, dass die JSON-Datei von der ältesten bis zur neuesten Messung aufsteigend sortiert abgelegt wird

3: JSON-Formatierung (json_encode)

$newJsonContent = json_encode($existingData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

$newJsonContent = json_encode($existingData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

Das sortierte PHP-Array wird wieder in eine JSON-Zeichenkette umgewandelt. Dabei kommen zwei wichtige Formatierungs-Flags zum Einsatz:

JSON_PRETTY_PRINT: Formatiert das JSON mit Einrückungen und Zeilenumbrüchen. Das macht die Datei für Entwickler direkt lesbar und erleichtert das Debugging.

JSON_UNESCAPED_SLASHES: Verhindert, dass Schrägstriche (Slashes /) automatisch maskiert werden (aus / wird nicht \/). Dies hält Pfade und URLs in den Metadaten schlank und lesbar.

4. Sicheres Schreiben & Fehlerbehandlung (file_put_contents)

if (file_put_contents(JSON_FILE, $newJsonContent)) {
echo "Erfolgreich aktualisiert: $newDataCount neue(r) Eintrag/Einträge hinzugefügt.\n";
} else {
fwrite(STDERR, "Fehler: JSON-Datei konnte nicht beschrieben werden. Berechtigungen pruefen.\n");
exit(1);
}

if (file_put_contents(JSON_FILE, $newJsonContent)) {
echo "Erfolgreich aktualisiert: $newDataCount neue(r) Eintrag/Einträge hinzugefügt.\n";
} else {
fwrite(STDERR, "Fehler: JSON-Datei konnte nicht beschrieben werden. Berechtigungen pruefen.\n");
exit(1);
}

file_put_contents(…): Versucht, die formatierte JSON-Zeichenkette in die konfigurierte Datei zu schreiben. Die Funktion gibt bei Erfolg die Anzahl der geschriebenen Bytes zurück, im Fehlerfall den booleschen Wert false.

Erfolgsfall: Das Skript meldet die erfolgreiche Aktualisierung und die Anzahl der hinzugefügten Tage auf STDOUT. Das Skript beendet sich implizit mit dem Erfolgs-Exit-Code 0.

Fehlerfall: Sollte der Schreibvorgang fehlschlagen (z. B. weil das Skript keine Schreibrechte im Zielverzeichnis besitzt), wird eine präzise Fehlermeldung an den Standard-Fehlerkanal (STDERR) gesendet. Danach bricht das Skript mittels exit(1) kontrolliert ab und signalisiert dem Betriebssystem (oder dem Cron-Daemon) über den Exit-Code 1 einen Fehler.

7 – Betrieb

Um die Daten immer auf dem neuesten Stand zu halten, muss das Skript über Cron regelmäßig gestartet werden.

0 20 * * * php /home/user/gasspeicher/update_data.php > /dev/null 2>&1

20 Uhr liegt nach der ersten täglichen GIE-Aktualisierung um 19:30 Uhr. Für Fehlerüberwachung empfiehlt sich statt 2>&1 > /dev/null eine gezielte Umleitung von STDERR in eine Logdatei, z. B. 2>> update.log, da nur dieser Kanal echte Fehlerfälle signalisiert. Wer wirklich aktuelle Daten, sollte weitere Läufe um 0 und 6 Uhr einplanen.