⚙️ dwdWetter.php
<?php
/*
===============================================================================
dwdWetter – Wetterdaten vom Deutschen Wetterdienst (DWD)
Moderne, strukturierte JSON-Ausgabe pro Station inkl. Icon-Mapping (wwPic)
Version: V 2025-11-24
PHP: min. 8.2
Quelle: https://opendata.dwd.de/
===============================================================================
*/
// -----------------------------------------------------------------------------
// EINSTELLUNGEN
// -----------------------------------------------------------------------------
$config = [
// Liste der Stationen (DWD MOSMIX IDs)
'stations' => [
'K428' => 'Bitburg',
'10609' => 'Trier'
],
// Anzahl Vorhersagetage 1-10 (Default: 3)
'forecast_days' => 10,
// Datenintervall (Cache) in Minuten
'cache_minutes' => 60,
// Ausgabeordner für JSON-Dateien (empfohlen: separater json-Ordner)
'output_path' => __DIR__ . '/json/',
// Debug-Modus
'debug' => true,
// ------------------------- Erweiterte Parameter ---------------------------
// Anzahl Stunden pro Vorhersageschritt (1, 3, 6 ...)
'step_hours' => 1,
// Timeout für DWD HTTP-Abfragen (Sekunden)
'curl_timeout' => 30,
// maximale Debug-Loggrösse in KB (0 = unbegrenzt)
'debug_max_kb' => 1024,
// temporärer Ordner für KMZ/KML-Dateien
'tmp_path' => __DIR__ . '/tmp/',
// Stationen überspringen
'skip_stations' => [],
// Nur speichern, wenn gültige Daten vorhanden sind
'save_only_if_valid' => true,
// Log-Level: debug | info | verbose
'log_level' => 'info',
// Muster für Ausgabedateien
// Platzhalter: {stationID} {stationName} {date}
'filename_pattern' => 'wetter_{stationID}.json',
// Zeitzone
'timezone' => 'Europe/Berlin',
// ------------------------- Icons / Wettercodes ----------------------------
// Basisordner für Icon-Sets
'icons_path' => __DIR__ . '/dwd_img/',
// Name des Icon-Sets (Unterordner; leer = direkt in icons_path)
'icon_set' => '',
// Icon-Mapping aktiv
'icons_enabled' => true,
// Ab wie viel % Sonnenschein (SunD3) Tag als "sonnig" gilt (für 1s, 2s, 80s)
'sun_threshold' => 30,
// Ab wie viel °C Hitzesymbol (0h) verwendet wird
'hot_temp' => 35.0
];
date_default_timezone_set($config['timezone']);
// -----------------------------------------------------------------------------
// LOG-ORDNER ANLEGEN (vor erster dbg()-Nutzung)
// -----------------------------------------------------------------------------
$logsDir = __DIR__ . '/logs/';
if (!is_dir($logsDir)) {
mkdir($logsDir, 0755, true);
}
// -----------------------------------------------------------------------------
// DEBUG-FUNKTION
// -----------------------------------------------------------------------------
function dbg($msg, $config, $level = 'info')
{
$allowed = ['debug' => 1, 'info' => 2, 'verbose' => 3];
if (!isset($allowed[$config['log_level']])) {
return;
}
if ($allowed[$config['log_level']] < $allowed[$level]) {
return;
}
if (!$config['debug']) {
return;
}
// Logdatei im logs-Ordner
$file = __DIR__ . '/logs/dwdWetter_debug.log';
// Grösse prüfen
if ($config['debug_max_kb'] > 0 && file_exists($file)) {
if (filesize($file) > ($config['debug_max_kb'] * 1024)) {
file_put_contents($file, "=== Log gekuerzt " . date('Y-m-d H:i:s') . " ===\n");
}
}
$line = "[" . date('Y-m-d H:i:s') . "] " . $msg . PHP_EOL;
file_put_contents($file, $line, FILE_APPEND);
}
// -----------------------------------------------------------------------------
// Ordner erzeugen
// -----------------------------------------------------------------------------
// JSON-Ausgabeordner
if (!is_dir($config['output_path'])) {
mkdir($config['output_path'], 0755, true);
dbg("Ordner erstellt: " . $config['output_path'], $config, 'info');
}
// TMP-Ordner
if (!is_dir($config['tmp_path'])) {
mkdir($config['tmp_path'], 0755, true);
dbg("TMP-Ordner erstellt: " . $config['tmp_path'], $config, 'info');
}
// Icon-Basis- und ggf. Set-Ordner anlegen
$iconsBaseDir = rtrim($config['icons_path'], "/\\") . DIRECTORY_SEPARATOR;
$iconsDir = $iconsBaseDir;
if ($config['icon_set'] !== '') {
$iconsDir .= rtrim($config['icon_set'], "/\\") . DIRECTORY_SEPARATOR;
}
if (!is_dir($iconsDir)) {
mkdir($iconsDir, 0755, true);
dbg("Icon-Ordner erstellt: " . $iconsDir, $config, 'debug');
}
// -----------------------------------------------------------------------------
// Hilfsfunktionen
// -----------------------------------------------------------------------------
function getHumidity($T, $TD)
{
if (!is_numeric($T) || !is_numeric($TD)) {
return null;
}
$T = $T - 273.15;
$TD = $TD - 273.15;
return round(100 * (exp((17.625 * $TD) / (243.04 + $TD)) / exp((17.625 * $T) / (243.04 + $T))));
}
function getWindDirection($deg)
{
$dirs = ['N','NNO','NO','ONO','O','OSO','SO','SSO','S','SSW','SW','WSW','W','WNW','NW','NNW'];
$step = 360 / count($dirs);
return $dirs[floor(($deg + ($step / 2)) / $step) % count($dirs)];
}
/**
* Liest ein MOSMIX-Parameter-Array aus dem dwd:Forecast-Block (DOM/XPath).
*
* @param DOMXPath $xpath XPath-Objekt mit registrierten kml/dwd-Namespaces
* @param DOMElement $forecastNode kml:ExtendedData unterhalb Placemark
* @param string $id z.B. 'TTT', 'Td', 'DD', ...
* @param string $nsDwd Namespace-URI für dwd (wird nur zur Info durchgereicht)
* @return array Werte-Array (Strings)
*/
function getParamArray($xpath, $forecastNode, $id, $nsDwd)
{
if (!$forecastNode || !$xpath || !$id) {
return [];
}
// dwd:Forecast/@dwd:elementName = $id → dwd:value
$query = './/dwd:Forecast[@dwd:elementName="'.$id.'"]/dwd:value';
$nodes = $xpath->query($query, $forecastNode);
if ($nodes === false || $nodes->length === 0) {
return [];
}
// Wir nehmen den ersten passenden dwd:value-Knoten
$valNode = $nodes->item(0);
if (!$valNode) {
return [];
}
$v = trim($valNode->textContent);
if ($v === '') {
return [];
}
// Mehrfache Leerzeichen/Zeilenumbrüche normalisieren und in Array aufsplitten
$v = preg_replace('!\s+!', ' ', $v);
$arr = explode(' ', trim($v));
return $arr;
}
// wwPic
if (!function_exists('wwPic')) {
function wwPic($Code, $bolSun, $intTagBeginn, $intTagEnde, $WT, $bolMaxT)
{
$intHour = $WT;
$bolDay = ($intHour > $intTagBeginn && $intHour < $intTagEnde);
switch ($Code) {
case 0:
// wenn Tag und heiss: 0h | wenn Tag: 0d | wenn Nacht: 0n
if ($bolDay == true and $bolMaxT == true) {
$icon = '0h';
} elseif ($bolDay == true) {
$icon = '0d';
} else {
$icon = '0n';
}
break;
case 1:
// wenn sonniger Tag: 1s | wenn Tag: 1d | wenn Nacht: 1n
if ($bolDay == true and $bolSun == true) {
$icon = '1s';
} elseif ($bolDay == true) {
$icon = '1d';
} else {
$icon = '1n';
}
break;
case 2:
// wenn sonniger Tag: 2s | wenn Tag: 2d | wenn Nacht: 2n
if ($bolDay == true and $bolSun == true) {
$icon = '2s';
} elseif ($bolDay == true) {
$icon = '2d';
} else {
$icon = '2n';
}
break;
case 3:
// wenn Tag: 3d | wenn Nacht: 3n
$icon = ($bolDay) ? '3d' : '3n';
break;
case 4:
case 5:
case 6:
case 7:
case 8:
case 9:
$icon = '4-9';
break;
case 10:
case 11:
case 12:
case 13:
case 14:
case 15:
case 16:
$icon = '10-16';
break;
case 17:
$icon = '17';
break;
case 18:
$icon = '18';
break;
case 19:
$icon = '19';
break;
case 20:
$icon = '20';
break;
case 21:
$icon = '21';
break;
case 22:
$icon = '22';
break;
case 23:
case 24:
$icon = '23-24';
break;
case 25:
$icon = '25';
break;
case 26:
$icon = '26';
break;
case 27:
$icon = '27';
break;
case 28:
$icon = '28';
break;
case 29:
$icon = '29';
break;
case 30:
case 31:
case 32:
$icon = '30-32';
break;
case 33:
case 34:
case 35:
$icon = '33-35';
break;
case 36:
case 37:
case 38:
case 39:
$icon = '36-39';
break;
case 40:
case 41:
case 42:
case 43:
case 44:
case 45:
case 46:
case 47:
case 48:
case 49:
$icon = '40-49';
break;
case 50:
case 51:
case 52:
case 53:
$icon = '50-53';
break;
case 54:
case 55:
case 56:
case 57:
case 58:
case 59:
$icon = '55-59';
break;
case 60:
case 61:
case 62:
case 63:
case 64:
case 65:
$icon = '60-65';
break;
case 66:
case 67:
$icon = '66-67';
break;
case 68:
case 69:
$icon = '68-69';
break;
case 70:
case 71:
case 72:
case 73:
case 74:
case 75:
case 76:
case 77:
case 78:
case 79:
$icon = '70-79';
break;
case 80:
// wenn sonniger Tag: 80s | wenn Tag: 80d | wenn Nacht: 80n
if ($bolDay == true and $bolSun == true) {
$icon = '80s';
} elseif ($bolDay == true) {
$icon = '80d';
} else {
$icon = '80n';
}
break;
case 81:
$icon = '81';
break;
case 82:
$icon = '82';
break;
case 83:
case 84:
$icon = '83-84';
break;
case 85:
case 86:
case 87:
case 88:
$icon = '85-88';
break;
case 89:
case 90:
$icon = '89-90';
break;
case 91:
case 92:
case 93:
case 94:
case 95:
case 96:
case 97:
case 98:
case 99:
$icon = '91-99';
break;
default:
$icon = 'unknown';
break;
}
return $icon . '.png';
}
}
// Tagesicon aus DWD-ww-Code für daily-Block ableiten
if (!function_exists('getDailyIconFromCode')) {
function getDailyIconFromCode(int $code): string
{
$icon = 'unknown';
switch (true) {
case $code === 0:
$icon = '0d';
break;
case $code === 1:
$icon = '1d';
break;
case $code === 2:
$icon = '2d';
break;
case $code === 3:
$icon = '3d';
break;
case ($code >= 4 && $code <= 9):
$icon = '4-9';
break;
case ($code >= 10 && $code <= 16):
$icon = '10-16';
break;
case $code === 17:
case $code === 18:
case $code === 19:
case $code === 20:
case $code === 21:
case $code === 22:
case $code === 25:
case $code === 26:
case $code === 27:
case $code === 28:
case $code === 29:
case $code === 81:
case $code === 82:
$icon = (string)$code;
break;
case ($code >= 23 && $code <= 24):
$icon = '23-24';
break;
case ($code >= 30 && $code <= 32):
$icon = '30-32';
break;
case ($code >= 33 && $code <= 35):
$icon = '33-35';
break;
case ($code >= 36 && $code <= 39):
$icon = '36-39';
break;
case ($code >= 40 && $code <= 49):
$icon = '40-49';
break;
case ($code >= 50 && $code <= 53):
$icon = '50-53';
break;
case ($code >= 54 && $code <= 59):
// wie im wwPic-Mapping: 55-59.png
$icon = '55-59';
break;
case ($code >= 60 && $code <= 65):
$icon = '60-65';
break;
case ($code >= 66 && $code <= 67):
$icon = '66-67';
break;
case ($code >= 68 && $code <= 69):
$icon = '68-69';
break;
case ($code >= 70 && $code <= 79):
$icon = '70-79';
break;
case $code === 80:
$icon = '80d';
break;
case ($code >= 83 && $code <= 84):
$icon = '83-84';
break;
case ($code >= 85 && $code <= 88):
$icon = '85-88';
break;
case ($code >= 89 && $code <= 90):
$icon = '89-90';
break;
case ($code >= 91 && $code <= 99):
$icon = '91-99';
break;
}
return $icon . '.png';
}
}
// -----------------------------------------------------------------------------
// Verarbeitung pro Station
// -----------------------------------------------------------------------------
foreach ($config['stations'] as $stationID => $stationName) {
if (in_array($stationID, $config['skip_stations'])) {
dbg("Station uebersprungen: $stationID", $config, 'debug');
continue;
}
dbg("Verarbeite Station $stationID ($stationName)", $config, 'info');
// Dateiname nach Pattern
$filename = str_replace(
['{stationID}', '{stationName}', '{date}'],
[$stationID, $stationName, date('Y-m-d')],
$config['filename_pattern']
);
$targetJSON = $config['output_path'] . $filename;
// Cache prüfen
if (file_exists($targetJSON)) {
$age = time() - filemtime($targetJSON);
if ($age < ($config['cache_minutes'] * 60)) {
dbg("Cache gueltig fuer $stationID – kein Reload", $config, 'info');
continue;
}
}
// URL
$url =
"https://opendata.dwd.de/weather/local_forecasts/mos/MOSMIX_L/single_stations/" .
$stationID . "/kml/MOSMIX_L_LATEST_" . $stationID . ".kmz";
dbg("Lade KMZ: $url", $config, 'info');
// Download – TMP-Datei im tmp-Ordner
$tmpFile = rtrim($config['tmp_path'], "/\\") . '/tmp_' . $stationID . '.kmz';
$curl = curl_init($url);
$fp = fopen($tmpFile, 'w');
curl_setopt($curl, CURLOPT_FILE, $fp);
curl_setopt($curl, CURLOPT_TIMEOUT, $config['curl_timeout']);
curl_exec($curl);
$http = curl_getinfo($curl, CURLINFO_HTTP_CODE);
fclose($fp);
curl_close($curl);
if ($http !== 200) {
dbg("Fehler HTTP $http bei $stationID", $config, 'info');
continue;
}
// KMZ öffnen
$zip = new ZipArchive();
if ($zip->open($tmpFile) !== true) {
dbg("ZipArchive Fehler bei $stationID", $config, 'info');
continue;
}
$kmlName = $zip->getNameIndex(0);
$kmlContent = $zip->getFromName($kmlName);
$zip->close();
if (!$kmlContent) {
dbg("Keine KML in KMZ gefunden ($stationID)", $config, 'info');
continue;
}
dbg(
"KML-Laenge fuer $stationID: " . strlen($kmlContent) .
" Bytes, erster Ausschnitt: " . substr($kmlContent, 0, 120),
$config,
'debug'
);
// Encoding ggf. auf UTF-8 konvertieren (DWD-KML kommt i.d.R. als ISO-8859-1)
if (preg_match('/encoding="([^"]+)"/i', substr($kmlContent, 0, 200), $m)) {
$enc = strtoupper($m[1]);
if ($enc !== 'UTF-8') {
$kmlContent = iconv($enc, 'UTF-8//IGNORE', $kmlContent);
$kmlContent = preg_replace('/encoding="[^"]+"/i', 'encoding="UTF-8"', $kmlContent, 1);
}
}
// XML mit DOM laden (ohne SimpleXML)
libxml_use_internal_errors(true);
$dom = new DOMDocument();
$dom->preserveWhiteSpace = false;
$dom->formatOutput = false;
if (!$dom->loadXML($kmlContent)) {
$errors = libxml_get_errors();
$msg = "DOMDocument loadXML Fehler bei $stationID:";
foreach ($errors as $e) {
$msg .= " [".$e->code."] Line ".$e->line.": ".trim($e->message);
}
dbg($msg, $config, 'info');
libxml_clear_errors();
continue;
}
libxml_clear_errors();
// Namespaces aus dem Root-Element holen
$root = $dom->documentElement;
$nsKml = $root->lookupnamespaceURI('kml');
$nsDwd = $root->lookupnamespaceURI('dwd');
if (!$nsKml || !$nsDwd) {
dbg(
"Namespaces kml/dwd fehlen oder unerwartet fuer $stationID",
$config,
'info'
);
continue;
}
// XPath vorbereiten
$xpath = new DOMXPath($dom);
$xpath->registerNamespace('kml', $nsKml);
$xpath->registerNamespace('dwd', $nsDwd);
// kml:Document holen
$docNodeList = $xpath->query('/kml:kml/kml:Document');
if ($docNodeList->length === 0) {
dbg("kml:Document nicht gefunden fuer $stationID", $config, 'info');
continue;
}
$docNode = $docNodeList->item(0);
// -------------------- ForecastTimeSteps (dwd-Namespace) -------------------
$tsNodes = $xpath->query('./kml:ExtendedData/dwd:ProductDefinition/dwd:ForecastTimeSteps/dwd:TimeStep', $docNode);
$timeSteps = [];
if ($tsNodes !== false) {
foreach ($tsNodes as $ts) {
$timeSteps[] = trim($ts->textContent);
}
}
if (count($timeSteps) === 0) {
dbg("Keine ForecastTimeSteps fuer $stationID gefunden", $config, 'info');
continue;
}
// IssueTime
$issueNodeList = $xpath->query('./kml:ExtendedData/dwd:ProductDefinition/dwd:IssueTime', $docNode);
$issueStr = null;
if ($issueNodeList !== false && $issueNodeList->length > 0) {
$issueStr = trim($issueNodeList->item(0)->textContent);
}
$issueTS = $issueStr ? strtotime($issueStr) : time();
// ------------------------- Forecast-Parameter -----------------------------
// Ersten Placemark (Station) holen
$placemarkList = $xpath->query('./kml:Placemark', $docNode);
if ($placemarkList->length === 0) {
dbg("kml:Placemark nicht gefunden fuer $stationID", $config, 'info');
continue;
}
$placemarkNode = $placemarkList->item(0);
// ExtendedData unter Placemark
$extDataList = $xpath->query('./kml:ExtendedData', $placemarkNode);
if ($extDataList->length === 0) {
dbg("kml:ExtendedData (Forecast) nicht gefunden fuer $stationID", $config, 'info');
continue;
}
$extDataNode = $extDataList->item(0);
// Diesen ExtendedData-Knoten geben wir an getParamArray weiter
$forecastNode = $extDataNode;
$ids = ['TN','TX','TTT','Td','DD','FF','FX3','Neff','PPPP','RRdc','RR6c','SunD3','VV','ww'];
$data = [];
foreach ($ids as $id) {
$data[$id] = getParamArray($xpath, $forecastNode, $id, $nsDwd);
}
// Ausgabe-Header
// Station Koordinaten
$coordNodeList = $xpath->query('./kml:Placemark/kml:Point/kml:coordinates', $docNode);
$lon = 0.0;
$lat = 0.0;
if ($coordNodeList !== false && $coordNodeList->length > 0) {
$coordStr = trim($coordNodeList->item(0)->textContent);
$coord = explode(',', $coordStr);
$lon = isset($coord[0]) ? floatval($coord[0]) : 0.0;
$lat = isset($coord[1]) ? floatval($coord[1]) : 0.0;
}
// Sonnenzeiten (Timestamps)
$sun = date_sun_info(time(), $lat, $lon);
$out = [
'stationID' => $stationID,
'stationName' => $stationName,
'coordinates' => ['lat' => $lat, 'lon' => $lon],
'pubDate' => date('Y-m-d H:i', $issueTS),
'sunrise' => date('H:i', $sun['sunrise']),
'sunset' => date('H:i', $sun['sunset']),
'iconSet' => $config['icon_set'],
'forecast' => [],
'daily' => [] // NEU: Tageszusammenfassungen
];
$maxEntries = $config['forecast_days'] * 24;
// Tag-Beginn / Tag-Ende (für wwPic)
$intTagBeginn = (int)date('G', $sun['sunrise']) - 1;
$intTagEnde = (int)date('G', $sun['sunset']) + 1;
foreach ($timeSteps as $k => $t) {
if ($k >= $maxEntries) {
break;
}
$dt = new DateTime($t);
$hour = (int)$dt->format('H');
// step_hours filtern
if ($hour % $config['step_hours'] !== 0) {
continue;
}
$weekday = ['So.','Mo.','Di.','Mi.','Do.','Fr.','Sa.'][$dt->format('w')];
// Rohdaten
$T = isset($data['TTT'][$k]) ? floatval($data['TTT'][$k]) : 0.0; // Kelvin
$TD = isset($data['Td'][$k]) ? floatval($data['Td'][$k]) : 0.0; // Kelvin
$windDirDeg = isset($data['DD'][$k]) ? floatval($data['DD'][$k]) : 0.0;
$wwCode = isset($data['ww'][$k]) ? intval($data['ww'][$k]) : 0;
$sunD3 = isset($data['SunD3'][$k]) ? floatval($data['SunD3'][$k]) : 0.0;
// Temperatur °C
$tempC = round($T - 273.15, 1);
$dewC = round($TD - 273.15, 1);
// Sonnenschein in % (wie im alten Code)
$intS = $sunD3 / 3600;
$intS = round($intS * 100 / 3);
$bolSun = ($intS >= $config['sun_threshold']);
// Hitzetag?
$bolMaxT = ($tempC >= $config['hot_temp']);
// Uhrzeit als Stunde für wwPic
$WT = (int)$dt->format('G');
// Icon-Dateiname
$iconFile = null;
if ($config['icons_enabled']) {
$iconFile = wwPic($wwCode, $bolSun, $intTagBeginn, $intTagEnde, $WT, $bolMaxT);
}
$entry = [
'date' => $dt->format('Y-m-d'),
'time' => $dt->format('H:i'),
'weekday' => $weekday,
'temp2m' => $tempC,
'dewPoint' => $dewC,
'humidity' => getHumidity($T, $TD),
'windDirection' => getWindDirection($windDirDeg),
'windDirectionDeg' => $windDirDeg,
'windSpeedKmh' => round((isset($data['FF'][$k]) ? floatval($data['FF'][$k]) : 0.0) * 3.6),
'windGustKmh' => round((isset($data['FX3'][$k]) ? floatval($data['FX3'][$k]) : 0.0) * 3.6),
'cloud' => intval(isset($data['Neff'][$k]) ? $data['Neff'][$k] : 0),
'pressure' => round((isset($data['PPPP'][$k]) ? floatval($data['PPPP'][$k]) : 0.0) / 100),
'rain6h' => isset($data['RR6c'][$k]) ? floatval($data['RR6c'][$k]) : 0.0,
'rain24h' => isset($data['RRdc'][$k]) ? floatval($data['RRdc'][$k]) : 0.0,
'sunPercent' => $intS,
'visibilityKm' => round((isset($data['VV'][$k]) ? floatval($data['VV'][$k]) : 0.0) / 1000, 2),
'sigWeatherCode' => $wwCode,
'icon' => $iconFile // z.B. "60-65.png" oder "1d.png"
];
$out['forecast'][] = $entry;
}
// -------------------- Tageszusammenfassungen aus forecast -----------------
if (count($out['forecast']) > 0) {
$dailyAgg = [];
foreach ($out['forecast'] as $entry) {
$d = $entry['date'];
if (!isset($dailyAgg[$d])) {
$dailyAgg[$d] = [
'date' => $d,
'weekday' => $entry['weekday'],
'tempMin' => $entry['temp2m'],
'tempMax' => $entry['temp2m'],
'tempSum' => 0.0,
'dewMin' => $entry['dewPoint'],
'dewMax' => $entry['dewPoint'],
'humidityMin' => $entry['humidity'],
'humidityMax' => $entry['humidity'],
'humiditySum' => 0.0,
'cloudSum' => 0.0,
'sunPercentSum' => 0.0,
'windSpeedMax' => $entry['windSpeedKmh'],
'windGustMax' => $entry['windGustKmh'],
'rain6hSum' => 0.0,
'rain24hMax' => $entry['rain24h'],
'count' => 0,
'wwCounts' => []
];
}
$agg =& $dailyAgg[$d];
$agg['count']++;
$agg['tempSum'] += $entry['temp2m'];
$agg['humiditySum'] += $entry['humidity'];
$agg['cloudSum'] += $entry['cloud'];
$agg['sunPercentSum'] += $entry['sunPercent'];
$agg['rain6hSum'] += $entry['rain6h'];
if ($entry['temp2m'] < $agg['tempMin']) {
$agg['tempMin'] = $entry['temp2m'];
}
if ($entry['temp2m'] > $agg['tempMax']) {
$agg['tempMax'] = $entry['temp2m'];
}
if ($entry['dewPoint'] < $agg['dewMin']) {
$agg['dewMin'] = $entry['dewPoint'];
}
if ($entry['dewPoint'] > $agg['dewMax']) {
$agg['dewMax'] = $entry['dewPoint'];
}
if ($entry['humidity'] < $agg['humidityMin']) {
$agg['humidityMin'] = $entry['humidity'];
}
if ($entry['humidity'] > $agg['humidityMax']) {
$agg['humidityMax'] = $entry['humidity'];
}
if ($entry['windSpeedKmh'] > $agg['windSpeedMax']) {
$agg['windSpeedMax'] = $entry['windSpeedKmh'];
}
if ($entry['windGustKmh'] > $agg['windGustMax']) {
$agg['windGustMax'] = $entry['windGustKmh'];
}
if ($entry['rain24h'] > $agg['rain24hMax']) {
$agg['rain24hMax'] = $entry['rain24h'];
}
$ww = $entry['sigWeatherCode'];
if (!isset($agg['wwCounts'][$ww])) {
$agg['wwCounts'][$ww] = 0;
}
$agg['wwCounts'][$ww]++;
}
foreach ($dailyAgg as $d => $agg) {
if ($agg['count'] <= 0) {
continue;
}
// häufigster ww-Code des Tages
$mainWw = null;
$maxCount = -1;
foreach ($agg['wwCounts'] as $ww => $cnt) {
if ($cnt > $maxCount) {
$maxCount = $cnt;
$mainWw = $ww;
}
}
$out['daily'][] = [
'date' => $agg['date'],
'weekday' => $agg['weekday'],
'tempMin' => round($agg['tempMin'], 1),
'tempMax' => round($agg['tempMax'], 1),
'tempMean' => round($agg['tempSum'] / $agg['count'], 1),
'dewPointMin' => round($agg['dewMin'], 1),
'dewPointMax' => round($agg['dewMax'], 1),
'humidityMin' => $agg['humidityMin'],
'humidityMax' => $agg['humidityMax'],
'humidityMean' => round($agg['humiditySum'] / $agg['count']),
'cloudMean' => round($agg['cloudSum'] / $agg['count']),
'sunPercentMean' => round($agg['sunPercentSum'] / $agg['count']),
'windSpeedMax' => $agg['windSpeedMax'],
'windGustMax' => $agg['windGustMax'],
'rain6hSum' => round($agg['rain6hSum'], 1), // Summe 6h-Mengen des Tages
'rain24hMax' => round($agg['rain24hMax'], 1), // max. 24h-Wert im Tag
'mainSigWeatherCode' => $mainWw,
'mainIcon' => ($mainWw !== null)
? getDailyIconFromCode((int)$mainWw)
: 'unknown.png'
];
}
}
// Validitätsprüfung
if ($config['save_only_if_valid'] && count($out['forecast']) < 1) {
dbg("Abbruch: Keine gueltigen Daten fuer $stationID", $config, 'info');
continue;
}
// JSON speichern
$json = json_encode($out, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
file_put_contents($targetJSON, $json);
dbg("JSON gespeichert: " . $targetJSON, $config, 'info');
}
dbg("Fertig.", $config, 'info');
// Optionale Ausgabe im Browser bei Direktaufruf
# if (php_sapi_name() !== 'cli') {
# echo "dwdWetter: Verarbeitung abgeschlossen. Siehe JSON-Ordner und Logdatei.";
# }
?>