Bewaesserungsanzeige loest sich vom alten Regenskript

Die Anzeigen im Realtime-SVG und im Bewaesserungs-Modal lasen ihre
Vorschau aus Gartenwasser/AutoWatering/<zone>/RainStatus, das
auto_watering.py stuendlich veroeffentlicht hat. Solange das so blieb,
konnte das Skript nicht abgeschaltet werden, obwohl die Entscheidung
laengst in den Automatiken des Runners steht - und dessen Schwellen
haetten dann neben denen im Skript gestanden, ohne dass ein Unterschied
auffaellt.

Statt des Plans zeigen die Anzeigen jetzt, ob die Zone heute schon
gelaufen ist. Die Regenautomatik loest zum Sonnenaufgang aus; wer
spaeter auf das Dashboard schaut, will ohnehin wissen, was passiert ist,
nicht was vorgesehen war. Die Angabe kommt aus dem retained
WateringToday der Ventilsteuerung, das die Karte schon fuer "Heute"
auswertet - eine Bewaesserung von Hand zaehlt damit genauso, was hier
richtig ist.

Im SVG steht dafuer ein Haekchen statt der Stoppuhr, das Schild im
Modal sagt "heute bewaessert" statt "heute geplant", und die
Planungszeilen in der Automatik-Karte entfallen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 11:33:15 +02:00
co-authored by Claude Opus 5
parent 7c646c5aee
commit 97b5c78fda
4 changed files with 44 additions and 113 deletions
+3 -7
View File
@@ -81,12 +81,6 @@ $zonen = [
["key" => "frontGarden", "label" => "Vorn", "minuten" => 30], ["key" => "frontGarden", "label" => "Vorn", "minuten" => 30],
]; ];
// Was die Automatik je Zone vorhat, fuellt solarMQTT.js aus dem RainStatus.
$planZeilen = "";
foreach ($zonen as $zone) {
$planZeilen .= " <div>{$zone['label']}: <span data-watering-plan=\"{$zone['key']}\">--</span></div>\n";
}
echo <<<HTML echo <<<HTML
<div class="p-3"> <div class="p-3">
<div id="watering-feedback" class="alert d-none mb-3" role="status"></div> <div id="watering-feedback" class="alert d-none mb-3" role="status"></div>
@@ -103,7 +97,9 @@ echo <<<HTML
</div> </div>
</div> </div>
<div class="small text-muted"> <div class="small text-muted">
$planZeilen <div class="fst-italic" data-watering-plan-updated></div> Startet die Ventilfolge von Hand. Die Regenautomatik läuft davon
unabhängig zum Sonnenaufgang; ob eine Zone dabei schon versorgt
wurde, steht unten bei &bdquo;Heute&ldquo;.
</div> </div>
</div> </div>
</div> </div>
+35 -100
View File
@@ -38,7 +38,6 @@ const solarMQTT = {
client.subscribe("Gartenwasser/hinten/LastWatering"); client.subscribe("Gartenwasser/hinten/LastWatering");
client.subscribe("Gartenwasser/hinten/WateringToday"); client.subscribe("Gartenwasser/hinten/WateringToday");
client.subscribe("Gartenwasser/hinten/LastWateringDay"); client.subscribe("Gartenwasser/hinten/LastWateringDay");
client.subscribe("Gartenwasser/AutoWatering/#");
}); });
client.on("error", function (error) { client.on("error", function (error) {
//alert("MQTT Error: " + error); //alert("MQTT Error: " + error);
@@ -69,7 +68,7 @@ const solarMQTT = {
if (topic === "Gartenwasser/vorn/Timers" || topic === "Gartenwasser/hinten/Timers") { if (topic === "Gartenwasser/vorn/Timers" || topic === "Gartenwasser/hinten/Timers") {
wateringTimersLastSeen[topic] = Date.now(); wateringTimersLastSeen[topic] = Date.now();
} }
if (topic.startsWith("Gartenwasser/") && (topic.endsWith("/status") || topic.endsWith("/Timers") || topic.endsWith("/LastWatering") || topic.endsWith("/RainStatus") if (topic.startsWith("Gartenwasser/") && (topic.endsWith("/status") || topic.endsWith("/Timers") || topic.endsWith("/LastWatering")
|| topic.endsWith("/WateringToday") || topic.endsWith("/LastWateringDay"))) { || topic.endsWith("/WateringToday") || topic.endsWith("/LastWateringDay"))) {
updateWateringStatus(); updateWateringStatus();
} }
@@ -729,20 +728,20 @@ var chartSettings = {
} }
const wateringTimersLastSeen = {}; const wateringTimersLastSeen = {};
// Eine Tabelle fuer alles: Ventil, Automatik-Zone des Skripts, Beschriftung // Eine Tabelle fuer alles: Ventil, Beschriftung und die Anzeige-Ids im
// und die Anzeige-Ids im Realtime-SVG. Vorher standen dieselben Zuordnungen // Realtime-SVG. Vorher standen dieselben Zuordnungen in drei getrennten
// in drei getrennten Tabellen. // Tabellen.
const wateringZones = { const wateringZones = {
troughs: { troughs: {
topic: "Gartenwasser/vorn", valve: 1, auto: "troege", label: "Tröge", topic: "Gartenwasser/vorn", valve: 1, label: "Tröge",
statusId: "wateringTroughStatus", iconId: "wateringTroughIcon" statusId: "wateringTroughStatus", iconId: "wateringTroughIcon"
}, },
frontGarden: { frontGarden: {
topic: "Gartenwasser/vorn", valve: 6, auto: "garten_vorn", label: "Vorn", topic: "Gartenwasser/vorn", valve: 6, label: "Vorn",
statusId: "wateringFrontStatus", iconId: "wateringFrontIcon" statusId: "wateringFrontStatus", iconId: "wateringFrontIcon"
}, },
raisedBeds: { raisedBeds: {
topic: "Gartenwasser/hinten", valve: 6, auto: "hochbeete", label: "Beete", topic: "Gartenwasser/hinten", valve: 6, label: "Beete",
statusId: "wateringBedsStatus", iconId: "wateringBedsIcon" statusId: "wateringBedsStatus", iconId: "wateringBedsIcon"
} }
}; };
@@ -854,12 +853,14 @@ var chartSettings = {
// "heute" in der Karte. // "heute" in der Karte.
function wateringDayTotal(topic, subTopic, feld, valve, mussHeuteSein) { function wateringDayTotal(topic, subTopic, feld, valve, mussHeuteSein) {
const data = wateringJson(topic + "/" + subTopic); const data = wateringJson(topic + "/" + subTopic);
if (!data) return { label: "--", day: "" }; if (!data) return { label: "--", day: "", sekunden: null };
const tag = data.wateringDay || ""; const tag = data.wateringDay || "";
const sekunden = data[feld + valve]; const sekunden = data[feld + valve];
if (sekunden === undefined) return { label: "--", day: tag }; if (sekunden === undefined) return { label: "--", day: tag, sekunden: null };
if (mussHeuteSein && tag !== wateringDayStamp(new Date())) return { label: "--", day: tag }; if (mussHeuteSein && tag !== wateringDayStamp(new Date())) {
return { label: wateringTimeLabel(Number(sekunden)), day: tag }; return { label: "--", day: tag, sekunden: null };
}
return { label: wateringTimeLabel(Number(sekunden)), day: tag, sekunden: Number(sekunden) };
} }
function wateringAutoActive(topic, mode) { function wateringAutoActive(topic, mode) {
@@ -931,101 +932,35 @@ var chartSettings = {
return remaining !== null && wateringSeconds(remaining) < 0; return remaining !== null && wateringSeconds(remaining) < 0;
} }
// Liest den von auto_watering.py stündlich veröffentlichten Regen-Status
// (retained, siehe Gartenwasser/AutoWatering/<zone>/RainStatus) und sagt,
// ob für die Zone heute noch eine automatische Bewässerung aussteht.
function wateringAutoPlanned(autoZoneKey) {
const status = wateringJson("Gartenwasser/AutoWatering/" + autoZoneKey + "/RainStatus");
return !!(status && status.would_water && !status.watered_recently);
}
// Zeitraeume und Regenmengen in Klartext. "2 T." war zu kryptisch.
function wateringTage(tage) {
const zahl = Number(tage);
const text = Number.isInteger(zahl) ? String(zahl) : String(zahl).replace(".", ",");
return zahl === 1 ? text + " Tag" : text + " Tagen";
}
function wateringMm(wert) {
return String(wert).replace(".", ",") + " mm";
}
// Sagt in einem Satz, was die Automatik fuer die Zone vorhat und warum.
// Speist sich aus derselben RainStatus-Nachricht wie die Anzeige im SVG.
// Der Titel nennt zusaetzlich die Regel, nach der entschieden wird - die
// Schwellenwerte unterscheiden sich je Zone deutlich.
function wateringPlanText(autoZoneKey) {
const status = wateringJson("Gartenwasser/AutoWatering/" + autoZoneKey + "/RainStatus");
if (!status) return { text: "keine Planungsdaten", titel: "" };
const regel = [];
if (status.threshold_mm !== undefined && status.threshold_days !== undefined) {
regel.push("Bewässert wird, wenn in " + wateringTage(status.threshold_days)
+ " weniger als " + wateringMm(status.threshold_mm) + " Regen fallen.");
}
if (status.max_dry_days) {
regel.push("Ebenso, wenn seit " + wateringTage(status.max_dry_days) + " nicht bewässert wurde.");
}
const titel = regel.join(" ");
if (status.watered_recently) {
const zeitpunkt = status.last_watering
? " zuletzt " + String(status.last_watering).replace(/[.]\d{4} /, ". ")
: "";
return { text: "erledigt" + zeitpunkt, titel: titel };
}
const regen = status.rain_mm !== undefined && status.threshold_days !== undefined
? wateringMm(status.rain_mm) + " Regen in " + wateringTage(status.threshold_days)
: "";
if (!status.would_water) {
return { text: "kein Bedarf" + (regen ? " " + regen : ""), titel: titel };
}
if (status.forced_dry) {
const trocken = status.max_dry_days
? " seit " + wateringTage(status.max_dry_days) + " nicht bewässert"
: " lange nicht bewässert";
return { text: "heute geplant" + trocken, titel: titel };
}
return { text: "heute geplant" + (regen ? " nur " + regen : ""), titel: titel };
}
// Alle Zonen stammen aus demselben Lauf des Skripts - der erste gefundene
// Zeitstempel steht daher fuer alle. Die Sekunden schneidet slice(0, 16) ab.
function wateringPlanUpdated() {
for (const [, zone] of wateringZoneList) {
const status = wateringJson("Gartenwasser/AutoWatering/" + zone.auto + "/RainStatus");
if (status && status.updated) return "Planung vom " + String(status.updated).slice(0, 16);
}
return "";
}
function updateWateringStatus() { function updateWateringStatus() {
wateringZoneList.forEach(([key, zone]) => { wateringZoneList.forEach(([key, zone]) => {
const state = wateringLabel(zone.topic, zone.valve); const state = wateringLabel(zone.topic, zone.valve);
const paused = wateringIsPaused(zone.topic, zone.valve); const paused = wateringIsPaused(zone.topic, zone.valve);
const planned = state === "aus" && wateringAutoPlanned(zone.auto); const heute = wateringDayTotal(zone.topic, "WateringToday", "wateringDurationTodaySecs", zone.valve, true);
// Die Regenautomatik laeuft zum Sonnenaufgang; wenn jemand auf die
// Anzeige schaut, ist die Entscheidung laengst gefallen. Angezeigt wird
// deshalb nicht mehr, was geplant ist, sondern was passiert ist. Der
// Zaehler unterscheidet nicht, wer ausgeloest hat - eine Bewaesserung
// von Hand zaehlt genauso, und das ist so gewollt.
const bewaessert = state === "aus" && heute.sekunden > 0;
// Die Anzeigen liegen im Realtime-SVG und bleiben bestehen -> svgElement. // Die Anzeigen liegen im Realtime-SVG und bleiben bestehen -> svgElement.
const status = svgElement(zone.statusId); const status = svgElement(zone.statusId);
const icon = svgElement(zone.iconId); const icon = svgElement(zone.iconId);
if (status && icon) updateWateringIndicator(status, icon, zone.label, state, paused, planned); if (status && icon) updateWateringIndicator(status, icon, zone.label, state, paused, bewaessert);
// Die Modal-Elemente werden bei jedem Oeffnen neu erzeugt -> nicht cachen. // Die Modal-Elemente werden bei jedem Oeffnen neu erzeugt -> nicht cachen.
setzeZonenButton(document.querySelector(`[data-watering-toggle][data-watering-zone='${key}']`), state); setzeZonenButton(document.querySelector(`[data-watering-toggle][data-watering-zone='${key}']`), state);
setzeSchild(`[data-watering-state='${key}']`, state, paused, planned); setzeSchild(`[data-watering-state='${key}']`, state, paused, bewaessert);
const history = wateringHistory(zone.topic, zone.valve); const history = wateringHistory(zone.topic, zone.valve);
setzeZelle(`[data-watering-last='${key}']`, history.dateTime); setzeZelle(`[data-watering-last='${key}']`, history.dateTime);
setzeZelle(`[data-watering-duration='${key}']`, history.duration); setzeZelle(`[data-watering-duration='${key}']`, history.duration);
const heute = wateringDayTotal(zone.topic, "WateringToday", "wateringDurationTodaySecs", zone.valve, true);
const vortag = wateringDayTotal(zone.topic, "LastWateringDay", "wateringDurationDaySecs", zone.valve, false); const vortag = wateringDayTotal(zone.topic, "LastWateringDay", "wateringDurationDaySecs", zone.valve, false);
setzeZelle(`[data-watering-today='${key}']`, heute.label, heute.day && "Stand: " + heute.day); setzeZelle(`[data-watering-today='${key}']`, heute.label, heute.day && "Stand: " + heute.day);
setzeZelle(`[data-watering-prevday='${key}']`, vortag.label, vortag.day && "Bewässerungstag: " + vortag.day); setzeZelle(`[data-watering-prevday='${key}']`, vortag.label, vortag.day && "Bewässerungstag: " + vortag.day);
const plan = wateringPlanText(zone.auto);
setzeZelle(`[data-watering-plan='${key}']`, plan.text, plan.titel);
}); });
document.querySelectorAll("[data-watering-auto-toggle]").forEach(updateWateringAutoButton); document.querySelectorAll("[data-watering-auto-toggle]").forEach(updateWateringAutoButton);
setzeZelle("[data-watering-plan-updated]", wateringPlanUpdated());
} }
// Der Knopf zeigt die naechste Aktion, nicht den Zustand: laeuft die Zone, // Der Knopf zeigt die naechste Aktion, nicht den Zustand: laeuft die Zone,
@@ -1039,8 +974,8 @@ var chartSettings = {
} }
// Zustandsschild der Zonenkarte, Farben wie im SVG: laufend gruen, // Zustandsschild der Zonenkarte, Farben wie im SVG: laufend gruen,
// pausiert gelb, von der Automatik geplant blau, sonst grau. // pausiert gelb, heute schon bewaessert blau, sonst grau.
function setzeSchild(selektor, state, paused, planned) { function setzeSchild(selektor, state, paused, bewaessert) {
const schild = document.querySelector(selektor); const schild = document.querySelector(selektor);
if (!schild) return; if (!schild) return;
let text = "unbekannt"; let text = "unbekannt";
@@ -1049,8 +984,8 @@ var chartSettings = {
text = paused ? "pausiert " + state : "läuft " + state; text = paused ? "pausiert " + state : "läuft " + state;
farbe = paused ? "bg-warning text-dark" : "bg-success"; farbe = paused ? "bg-warning text-dark" : "bg-success";
} else if (state === "aus") { } else if (state === "aus") {
text = planned ? "heute geplant" : "aus"; text = bewaessert ? "heute bewässert" : "aus";
farbe = planned ? "bg-primary" : "bg-secondary"; farbe = bewaessert ? "bg-primary" : "bg-secondary";
} }
if (schild.textContent !== text) schild.textContent = text; if (schild.textContent !== text) schild.textContent = text;
const klasse = "badge " + farbe; const klasse = "badge " + farbe;
@@ -1082,23 +1017,23 @@ var chartSettings = {
button.classList.toggle("btn-outline-danger", active); button.classList.toggle("btn-outline-danger", active);
} }
function updateWateringIndicator(status, icon, name, state, paused, planned) { function updateWateringIndicator(status, icon, name, state, paused, bewaessert) {
// "Aus" bleibt bei nicht geplant stehen; bei geplant ersetzt die // "Aus" bleibt stehen, solange die Zone heute noch kein Wasser hatte;
// Stoppuhr (U+23F1, ohne Emoji-Variationsselektor -> einfarbig, folgt // danach ersetzt das Haekchen (U+2713, ohne Emoji-Variationsselektor ->
// der Icon-Farbe) den Text, um Platz auf der textPath zu sparen. // einfarbig, folgt der Icon-Farbe) den Text, um Platz auf der textPath zu
// Laeuft im Sekundentakt, aendert sich aber selten - ohne Pruefung // sparen. Laeuft im Sekundentakt, aendert sich aber selten - ohne Pruefung
// wuerde jede Sekunde derselbe Wert geschrieben und das Icon samt // wuerde jede Sekunde derselbe Wert geschrieben und das Icon samt
// Filter neu gezeichnet. // Filter neu gezeichnet.
const text = state === "aus" ? (planned ? "" : "Aus") : state; const text = state === "aus" ? (bewaessert ? "" : "Aus") : state;
if (status.textContent !== text) status.textContent = text; if (status.textContent !== text) status.textContent = text;
// Geplant-Blau ("#3A6292") entspricht dem Wassertropfen-Icon (id="water"). // Das Blau ("#3A6292") entspricht dem Wassertropfen-Icon (id="water").
const color = state !== "aus" ? (paused ? "#ffc107" : "#20c997") : planned ? "#3A6292" : "#6c757d"; const color = state !== "aus" ? (paused ? "#ffc107" : "#20c997") : bewaessert ? "#3A6292" : "#6c757d";
if (icon.getAttribute("color") !== color) { if (icon.getAttribute("color") !== color) {
icon.setAttribute("color", color); icon.setAttribute("color", color);
icon.setAttribute("fill", color); icon.setAttribute("fill", color);
icon.style.color = color; icon.style.color = color;
} }
const titel = name + ": " + (state === "aus" && planned ? "Aus (Automatik heute noch geplant)" : state); const titel = name + ": " + (state === "aus" && bewaessert ? "Aus (heute bereits bewässert)" : state);
const titelKnoten = icon.querySelector("title"); const titelKnoten = icon.querySelector("title");
if (titelKnoten.textContent !== titel) titelKnoten.textContent = titel; if (titelKnoten.textContent !== titel) titelKnoten.textContent = titel;
} }
+3 -3
View File
@@ -45,9 +45,9 @@ function meshDb()
* ein langsamer Durchlauf den Termin nicht verschluckt. * ein langsamer Durchlauf den Termin nicht verschluckt.
* ab 16:30 ab da bis Mitternacht wahr. Ausgeloest wird trotzdem nur * ab 16:30 ab da bis Mitternacht wahr. Ausgeloest wird trotzdem nur
* einmal, weil nur die steigende Flanke zaehlt; dafuer kostet * einmal, weil nur die steigende Flanke zaehlt; dafuer kostet
* ein verpasster Takt nicht den ganzen Tag. Dieselbe * ein verpasster Takt nicht den ganzen Tag. Fuer alles, was
* Ueberlegung steht hinter den breiten Zeitfenstern in * laufen muss, auch wenn der Termin selbst verpasst wurde,
* auto_watering.py. * ist das die richtige Wahl - etwa die Bewaesserung.
* vor 16:30 bis dahin wahr, danach nicht mehr. * vor 16:30 bis dahin wahr, danach nicht mehr.
* *
* Beim Sonnenauf- und -untergang ist der Wert ein Versatz, der davor oder * Beim Sonnenauf- und -untergang ist der Wert ein Versatz, der davor oder
+3 -3
View File
@@ -59,9 +59,9 @@ const WMS_LAYER = "by_dop20c";
const WMS_SCHAERFE = 3; const WMS_SCHAERFE = 3;
/** /**
* Mittelpunkt des Ausschnitts. Dieselben Koordinaten stehen in * Mittelpunkt des Ausschnitts. Dieselben Koordinaten stehen als Vorgabe in
* restricted/gartenbewaesserung/auto_watering.py, von wo die Regendaten * gatherRainData.py im SolarManager, von wo die Regendaten geholt werden -
* geholt werden - es ist derselbe Ort. * es ist derselbe Ort.
*/ */
$lat = isset($_GET["lat"]) ? floatval($_GET["lat"]) : 47.5781; $lat = isset($_GET["lat"]) ? floatval($_GET["lat"]) : 47.5781;
$lon = isset($_GET["lon"]) ? floatval($_GET["lon"]) : 10.2628; $lon = isset($_GET["lon"]) ? floatval($_GET["lon"]) : 10.2628;