Das Widget von meteoblue war ein fremdes Dokument im iframe: es passte farblich nicht, liess sich nicht aendern, wurde auf schmalen Schirmen als Ganzes heruntergerechnet und konnte die eigenen Messwerte nicht zeigen. Genau das kann das neue Modul - links vom Jetzt-Strich steht, was die Wetterstation gemessen hat, rechts die Vorhersage. Aufgebaut wie die uebrigen Anzeige-Module: meteogrammKatalog.js sagt, WAS gezeigt wird, meteogramm.js nur, WIE. Der Renderer kennt kein Feld und keine Spur beim Namen. Eine weitere Spur ist damit ein Objekt im Katalog und keine Codeaenderung. Ein einziges SVG ueber alle drei Felder, damit Zeitachse, Nachtstreifen, Jetzt-Linie und Zeiger nur einmal existieren und ueberall gleich sitzen. Zwei Entscheidungen, die man sonst sucht: Das Wolkenband zeichnet je Schicht ein Rechteck mit waagerechtem Farbverlauf. Ein Rechteck je Stunde war der erste Versuch und sah aus wie Fernsehrauschen - zweihundert harte Kanten nebeneinander. Open-Meteo kennt ohnehin nur drei Schichten statt eines Hoehenprofils. Die Strahlung teilt sich das Feld mit der Temperatur, statt ein viertes zu bekommen: gleicher Tagesrhythmus, gleiche Nacht-Schattierung, und ein viertes Feld machte das Ganze am Handy hoeher als das externe Widget. Die alte Einbindung bleibt vorerst und ist umschaltbar. mountMeteogram() laeuft jetzt erst beim Umschalten - wer meteoblue nie ansieht, laedt es auch nicht mehr. Die Karte heisst "Wetter" statt "Forecast": darunter steht bereits eine zweite Karte dieses Namens. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
187 lines
7.5 KiB
PHP
187 lines
7.5 KiB
PHP
<?php
|
|
/*
|
|
* Daten für das Meteogramm der Solar-Seite (js/solar/meteogramm.js).
|
|
*
|
|
* ?tage=5&rueck=24 tage 2…7 nach vorn, rueck 0…72 Stunden zurück
|
|
*
|
|
* Die Vorhersage kommt aus solarLog.weatherHours und weatherDays (geschrieben
|
|
* von SolarManager/gatherForecastData.py), die gemessenen Stunden links vom
|
|
* Jetzt-Strich aus weatherStation.
|
|
*
|
|
* Verschmolzen wird nur teilweise, und das ist der Kern dieser Datei: Die
|
|
* eigene Station misst Temperatur, Wind, Böe, Richtung, Feuchte und Druck.
|
|
* Sie hat weder Regenmesser noch Pyranometer noch Wolkenkamera. Bewölkung,
|
|
* Strahlung, Niederschlag und Wettercode bleiben deshalb auch in der
|
|
* Vergangenheit aus weatherHours - dort stehen für vergangene Stunden keine
|
|
* Vorhersagen mehr, sondern nachgerechnete Werte (ist_vorhersage = 0).
|
|
* Genau dafür holt der Sammler zwei Tage Vergangenheit mit.
|
|
*
|
|
* Zeiten gehen als Unix-Sekunden hinaus, Messreihen als Tupel und nicht als
|
|
* Objekte: bei 200 Stunden mit 16 Feldern spart das ein Vielfaches. Welche
|
|
* Stelle was bedeutet, steht unten bei SPALTEN und noch einmal im Katalog
|
|
* (js/solar/meteogrammKatalog.js) - beide Listen müssen zueinander passen.
|
|
*
|
|
* Die Live-Werte kommen nicht von hier, sondern über MQTT (weatherStation/#).
|
|
*/
|
|
|
|
require_once("../helper.php");
|
|
require_once("../restricted/costs.php"); // solarDb()
|
|
|
|
header("Content-Type: application/json; charset=utf-8");
|
|
|
|
if (!checkLogin()) {
|
|
http_response_code(403);
|
|
echo json_encode(["error" => "Nicht angemeldet."]);
|
|
exit;
|
|
}
|
|
|
|
/*
|
|
* Die Spaltenlage einer Stundenzeile. Neue Größen hängen HINTEN an - dann
|
|
* bleiben die Stellen bestehender Spuren gültig und der Katalog muss nur
|
|
* einen Schlüssel dazubekommen.
|
|
*
|
|
* 0 t Unix-Sekunden, volle Stunde
|
|
* 1 gemessen 1 = die eigene Station hat diese Stunde gemessen
|
|
* 2 temp °C 3 regen mm 4 schnee cm
|
|
* 5 wind km/h 6 boe km/h 7 richtung Grad
|
|
* 8 strahlung W/m² 9 wtief % 10 wmittel %
|
|
* 11 whoch % 12 code WMO 13 feuchte %
|
|
* 14 druck hPa 15 regenWkt %
|
|
*/
|
|
|
|
try {
|
|
$db = solarDb();
|
|
|
|
$tage = max(2, min(7, intval($_GET["tage"] ?? 5)));
|
|
$rueck = max(0, min(72, intval($_GET["rueck"] ?? 24)));
|
|
|
|
// Auf volle Stunden rasten: die Vorhersage kennt nur Stunden, und eine
|
|
// Achse, die bei 13:47 anfängt, hätte keine runden Tagesgrenzen.
|
|
$jetzt = time();
|
|
$von = (intdiv($jetzt, 3600) - $rueck) * 3600;
|
|
$bis = (intdiv($jetzt, 3600) + $tage * 24) * 3600;
|
|
|
|
// --- Vorhersage und nachgerechnete Stunden ---------------------------
|
|
$reihe = [];
|
|
$stmt = $db->prepare(
|
|
"SELECT UNIX_TIMESTAMP(datetime) AS t, temp, regen, schnee, wind, boe,
|
|
richtung, strahlung, wolken_tief, wolken_mittel, wolken_hoch,
|
|
wettercode, feuchte, druck, regen_wkt
|
|
FROM weatherHours
|
|
WHERE datetime BETWEEN FROM_UNIXTIME(?) AND FROM_UNIXTIME(?)
|
|
ORDER BY datetime");
|
|
$stmt->bind_param("ii", $von, $bis);
|
|
$stmt->execute();
|
|
$res = $stmt->get_result();
|
|
while ($z = $res->fetch_assoc()) {
|
|
$t = (int)$z["t"];
|
|
$reihe[$t] = [
|
|
$t, 0,
|
|
zahl($z["temp"], 1), zahl($z["regen"], 1), zahl($z["schnee"], 1),
|
|
zahl($z["wind"], 1), zahl($z["boe"], 1), ganz($z["richtung"]),
|
|
ganz($z["strahlung"]),
|
|
ganz($z["wolken_tief"]), ganz($z["wolken_mittel"]), ganz($z["wolken_hoch"]),
|
|
ganz($z["wettercode"]), ganz($z["feuchte"]), ganz($z["druck"]),
|
|
ganz($z["regen_wkt"]),
|
|
];
|
|
}
|
|
$stmt->close();
|
|
|
|
// --- Gemessene Stunden der eigenen Station ---------------------------
|
|
//
|
|
// Derselbe Ausdruck dient als Spalte und als GROUP BY - ein Schalter
|
|
// statt zweier Abfragen, wie in ajax/speicher.php.
|
|
//
|
|
// MAX(gustSpeed), aber AVG(windSpeed): Open-Meteos Böe ist die Spitze der
|
|
// Stunde. Mit einem Mittelwert auf der Messseite spränge die Böenlinie
|
|
// genau am Jetzt-Strich.
|
|
//
|
|
// Die Windrichtung ist ein Winkel und verträgt kein arithmetisches
|
|
// Mittel: aus 350° und 10° würden 180°, also die Gegenrichtung. Deshalb
|
|
// die Vektorsumme über Sinus und Kosinus.
|
|
if ($rueck > 0) {
|
|
$stmt = $db->prepare(
|
|
"SELECT FLOOR(UNIX_TIMESTAMP(datetime) / 3600) * 3600 AS t,
|
|
AVG(tempAmb) AS temp, AVG(windSpeed) AS wind,
|
|
MAX(gustSpeed) AS boe, AVG(hum) AS feuchte, AVG(qff) AS druck,
|
|
DEGREES(ATAN2(AVG(SIN(RADIANS(windDir))),
|
|
AVG(COS(RADIANS(windDir))))) AS richtung
|
|
FROM weatherStation
|
|
WHERE datetime >= FROM_UNIXTIME(?) AND datetime < FROM_UNIXTIME(?)
|
|
GROUP BY t ORDER BY t");
|
|
$stmt->bind_param("ii", $von, $jetzt);
|
|
$stmt->execute();
|
|
$res = $stmt->get_result();
|
|
while ($z = $res->fetch_assoc()) {
|
|
$t = (int)$z["t"];
|
|
if (!isset($reihe[$t])) {
|
|
continue; // gemessen, aber ohne Vorhersagezeile - nichts zu zeigen
|
|
}
|
|
$reihe[$t][1] = 1;
|
|
$reihe[$t][2] = zahl($z["temp"], 1);
|
|
$reihe[$t][5] = zahl($z["wind"], 1);
|
|
$reihe[$t][6] = zahl($z["boe"], 1);
|
|
$reihe[$t][7] = ((int)round($z["richtung"]) + 360) % 360;
|
|
$reihe[$t][13] = ganz($z["feuchte"]);
|
|
$reihe[$t][14] = ganz($z["druck"]);
|
|
}
|
|
$stmt->close();
|
|
}
|
|
|
|
ksort($reihe);
|
|
|
|
// --- Tage: Sonnenzeiten und Tagesspanne ------------------------------
|
|
//
|
|
// Sonnenauf- und -untergang kommen aus weatherDays und nicht aus
|
|
// daylight: daylight reicht nur bis morgen, das Meteogramm bis zu sieben
|
|
// Tage. Tage ohne Sonnenzeit (nachgetragenes Archiv) fallen hier heraus -
|
|
// sie liegen ohnehin weit vor dem angezeigten Fenster.
|
|
$tageListe = [];
|
|
$stmt = $db->prepare(
|
|
"SELECT UNIX_TIMESTAMP(date) AS t, temp_min, temp_max, wettercode,
|
|
UNIX_TIMESTAMP(CONCAT(date, ' ', sonnenauf)) AS auf,
|
|
UNIX_TIMESTAMP(CONCAT(date, ' ', sonnenunter)) AS unter
|
|
FROM weatherDays
|
|
WHERE date BETWEEN DATE(FROM_UNIXTIME(?)) AND DATE(FROM_UNIXTIME(?))
|
|
AND sonnenauf IS NOT NULL
|
|
ORDER BY date");
|
|
$stmt->bind_param("ii", $von, $bis);
|
|
$stmt->execute();
|
|
$res = $stmt->get_result();
|
|
while ($z = $res->fetch_assoc()) {
|
|
$tageListe[] = [
|
|
(int)$z["t"], zahl($z["temp_min"], 1), zahl($z["temp_max"], 1),
|
|
ganz($z["wettercode"]), (int)$z["auf"], (int)$z["unter"],
|
|
];
|
|
}
|
|
$stmt->close();
|
|
|
|
// Wie frisch die Vorhersage ist - das Modul schreibt daraus "Stand 09:40"
|
|
// und färbt es warnend, wenn der Sammler stehengeblieben ist.
|
|
$stand = 0;
|
|
if ($res = $db->query("SELECT UNIX_TIMESTAMP(MAX(abgerufen)) AS s FROM weatherHours")) {
|
|
$stand = (int)($res->fetch_assoc()["s"] ?? 0);
|
|
}
|
|
|
|
echo json_encode([
|
|
"von" => $von, "bis" => $bis, "jetzt" => $jetzt, "stand" => $stand,
|
|
"stunden" => array_values($reihe),
|
|
"tage" => $tageListe,
|
|
]);
|
|
} catch (Throwable $e) {
|
|
http_response_code(500);
|
|
echo json_encode(["error" => "Meteogramm nicht abrufbar: " . $e->getMessage()]);
|
|
}
|
|
|
|
/** Zahl mit fester Nachkommastelle, NULL bleibt NULL. */
|
|
function zahl($wert, $stellen)
|
|
{
|
|
return $wert === null ? null : round((float)$wert, $stellen);
|
|
}
|
|
|
|
/** Ganze Zahl, NULL bleibt NULL - eine fehlende Bewölkung ist nicht 0 %. */
|
|
function ganz($wert)
|
|
{
|
|
return $wert === null ? null : (int)round((float)$wert);
|
|
}
|