From eca5c8f0410795d2781485582d487f85afae1895 Mon Sep 17 00:00:00 2001 From: Moirtz Wagner Date: Sat, 29 Aug 2026 22:47:50 +0200 Subject: [PATCH] Bewaesserungs-Modal: Zonenkarten statt Tabelle Das Modal ist 500 px breit (modal-dialog ohne Groessenklasse). Die Tabelle mit fuenf Spalten samt Zahlenfeld war darin schon eng; jede weitere Angabe haette sie gesprengt. Statt der Tabelle bekommt jede Zone eine eigene Karte mit Zustandsschild, Steuerzeile und den Zeiten darunter, die Automatik eine eigene Karte darueber - dort sitzt auch die kombinierte Automatik, die zwei Zonen bedient. Die Automatik-Karte zeigt zusaetzlich, was auto_watering.py je Zone vorhat, und begruendet es: "heute geplant - nur 0,2 mm Regen in 2 Tagen". Die Zahlen stammen aus dem RainStatus-Topic, das die Seite ohnehin schon abonniert hat, bisher aber nur fuer das Uhr-Symbol im SVG benutzte. Die genaue Regel steht im Titel der Zeile, weil sich die Schwellen je Zone deutlich unterscheiden. Die letzte Bewaesserung bleibt in der Zonenkarte, sie ist eine Eigenschaft der Zone; die Automatik nennt sie nur als Begruendung. Nebenbei sind die drei getrennten Zuordnungstabellen (wateringZones, wateringIndicators, wateringHistoryRows) zu einer verschmolzen, die Ventil, Automatik-Zone, Beschriftung und die SVG-Anzeige-Ids traegt. Damit wird aus updateWateringStatus eine Schleife statt dreier, und die doppelt vorhandene Knopflogik faellt weg. Alle 18 Zuordnungen und die Historienzeilen sind gegen den vorherigen Stand geprueft, ohne Abweichung. Co-Authored-By: Claude Opus 5 --- ajax/watering.php | 77 +++++++++++++----- js/solar/solarMQTT.js | 181 ++++++++++++++++++++++++++++++++---------- 2 files changed, 197 insertions(+), 61 deletions(-) diff --git a/ajax/watering.php b/ajax/watering.php index 4c39e4d..0320c49 100644 --- a/ajax/watering.php +++ b/ajax/watering.php @@ -73,26 +73,67 @@ if (!checkLogin()) { exit; } +// Zonen des Modals. Die Reihenfolge bestimmt die Reihenfolge der Karten, die +// Keys muessen zu wateringZones in js/solar/solarMQTT.js passen. +$zonen = [ + ["key" => "troughs", "label" => "Tröge", "minuten" => 5], + ["key" => "raisedBeds", "label" => "Beete", "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 .= "
{$zone['label']}: --
\n"; +} + echo << +
-
-
Automatik
-
- - - - + +
+
+
Automatik
+
+ +
+ + + +
+
+
+$planZeilen
+
-
Manuell
- - - - - - - -
ZoneDauer (Min.)Letzte BewässerungDauerSteuerung
Tröge----
Beete----
Vorn----
-
HTML; + +foreach ($zonen as $zone) { + $key = $zone["key"]; + $label = $zone["label"]; + $minuten = $zone["minuten"]; + echo << +
+
+ $label + unbekannt +
+
+
+ + Min. +
+ +
+
+ Zuletzt -- · -- +
+
+
+HTML; +} + +echo "\n
\n"; diff --git a/js/solar/solarMQTT.js b/js/solar/solarMQTT.js index f3ffe06..9d5ac47 100644 --- a/js/solar/solarMQTT.js +++ b/js/solar/solarMQTT.js @@ -707,11 +707,25 @@ var chartSettings = { } const wateringTimersLastSeen = {}; + // Eine Tabelle fuer alles: Ventil, Automatik-Zone des Skripts, Beschriftung + // und die Anzeige-Ids im Realtime-SVG. Vorher standen dieselben Zuordnungen + // in drei getrennten Tabellen. const wateringZones = { - troughs: { topic: "Gartenwasser/vorn", valve: 1 }, - frontGarden: { topic: "Gartenwasser/vorn", valve: 6 }, - raisedBeds: { topic: "Gartenwasser/hinten", valve: 6 } + troughs: { + topic: "Gartenwasser/vorn", valve: 1, auto: "troege", label: "Tröge", + statusId: "wateringTroughStatus", iconId: "wateringTroughIcon" + }, + frontGarden: { + topic: "Gartenwasser/vorn", valve: 6, auto: "garten_vorn", label: "Vorn", + statusId: "wateringFrontStatus", iconId: "wateringFrontIcon" + }, + raisedBeds: { + topic: "Gartenwasser/hinten", valve: 6, auto: "hochbeete", label: "Beete", + statusId: "wateringBedsStatus", iconId: "wateringBedsIcon" + } }; + // Feste Liste - der Sekundentakt soll sie nicht jedes Mal neu aufbauen. + const wateringZoneList = Object.entries(wateringZones); function wateringCommand(topic, command, directTopic = false) { const feedback = document.getElementById("watering-feedback"); @@ -742,11 +756,7 @@ var chartSettings = { document.querySelectorAll("[data-watering-toggle]").forEach((button) => { const updateButton = () => { const zone = wateringZones[button.dataset.wateringZone]; - const state = wateringLabel(zone.topic, zone.valve); - const beschriftung = state === "aus" ? "Ein" : "Aus"; - if (button.textContent !== beschriftung) button.textContent = beschriftung; - button.classList.toggle("btn-success", state === "aus"); - button.classList.toggle("btn-danger", state !== "aus"); + setzeZonenButton(button, wateringLabel(zone.topic, zone.valve)); }; button.addEventListener("click", () => { const zone = wateringZones[button.dataset.wateringZone]; @@ -885,46 +895,131 @@ var chartSettings = { return !!(status && status.would_water && !status.watered_recently); } - // Konstante Tabellen - stehen ausserhalb, damit der Sekundentakt sie nicht - // jedes Mal neu aufbaut. - const wateringIndicators = [ - ["wateringFrontStatus", "wateringFrontIcon", "Vorn", "Gartenwasser/vorn", 6, "garten_vorn"], - ["wateringTroughStatus", "wateringTroughIcon", "Tröge", "Gartenwasser/vorn", 1, "troege"], - ["wateringBedsStatus", "wateringBedsIcon", "Beete", "Gartenwasser/hinten", 6, "hochbeete"] - ]; - const wateringHistoryRows = [ - ["troughs", "Gartenwasser/vorn", 1], - ["frontGarden", "Gartenwasser/vorn", 6], - ["raisedBeds", "Gartenwasser/hinten", 6] - ]; + // 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() { - // Die Anzeigen liegen im Realtime-SVG und bleiben bestehen -> svgElement. - wateringIndicators.forEach(([statusId, iconId, name, topic, valve, autoZoneKey]) => { - const status = svgElement(statusId); - const icon = svgElement(iconId); - if (!status || !icon) return; - const state = wateringLabel(topic, valve); - const planned = state === "aus" && wateringAutoPlanned(autoZoneKey); - updateWateringIndicator(status, icon, name, state, wateringIsPaused(topic, valve), planned); - }); - document.querySelectorAll("[data-watering-toggle]").forEach((button) => { - const zone = wateringZones[button.dataset.wateringZone]; + wateringZoneList.forEach(([key, zone]) => { const state = wateringLabel(zone.topic, zone.valve); - const beschriftung = state === "aus" ? "Ein" : "Aus"; - if (button.textContent !== beschriftung) button.textContent = beschriftung; - button.classList.toggle("btn-success", state === "aus"); - button.classList.toggle("btn-danger", state !== "aus"); + const paused = wateringIsPaused(zone.topic, zone.valve); + const planned = state === "aus" && wateringAutoPlanned(zone.auto); + + // Die Anzeigen liegen im Realtime-SVG und bleiben bestehen -> svgElement. + const status = svgElement(zone.statusId); + const icon = svgElement(zone.iconId); + if (status && icon) updateWateringIndicator(status, icon, zone.label, state, paused, planned); + + // Die Modal-Elemente werden bei jedem Oeffnen neu erzeugt -> nicht cachen. + setzeZonenButton(document.querySelector(`[data-watering-toggle][data-watering-zone='${key}']`), state); + setzeSchild(`[data-watering-state='${key}']`, state, paused, planned); + + const history = wateringHistory(zone.topic, zone.valve); + setzeZelle(`[data-watering-last='${key}']`, history.dateTime); + setzeZelle(`[data-watering-duration='${key}']`, history.duration); + const plan = wateringPlanText(zone.auto); + setzeZelle(`[data-watering-plan='${key}']`, plan.text, plan.titel); }); document.querySelectorAll("[data-watering-auto-toggle]").forEach(updateWateringAutoButton); - // Die Modal-Elemente werden bei jedem Oeffnen neu erzeugt -> nicht cachen. - wateringHistoryRows.forEach(([zone, topic, valve]) => { - const history = wateringHistory(topic, valve); - const dateElement = document.querySelector(`[data-watering-last='${zone}']`); - const durationElement = document.querySelector(`[data-watering-duration='${zone}']`); - if (dateElement) dateElement.textContent = history.dateTime; - if (durationElement) durationElement.textContent = history.duration; - }); + setzeZelle("[data-watering-plan-updated]", wateringPlanUpdated()); + } + + // Der Knopf zeigt die naechste Aktion, nicht den Zustand: laeuft die Zone, + // steht "Aus" darauf. + function setzeZonenButton(button, state) { + if (!button) return; + const beschriftung = state === "aus" ? "Ein" : "Aus"; + if (button.textContent !== beschriftung) button.textContent = beschriftung; + button.classList.toggle("btn-success", state === "aus"); + button.classList.toggle("btn-danger", state !== "aus"); + } + + // Zustandsschild der Zonenkarte, Farben wie im SVG: laufend gruen, + // pausiert gelb, von der Automatik geplant blau, sonst grau. + function setzeSchild(selektor, state, paused, planned) { + const schild = document.querySelector(selektor); + if (!schild) return; + let text = "unbekannt"; + let farbe = "bg-secondary"; + if (state !== "--" && state !== "aus") { + text = paused ? "pausiert " + state : "läuft " + state; + farbe = paused ? "bg-warning text-dark" : "bg-success"; + } else if (state === "aus") { + text = planned ? "heute geplant" : "aus"; + farbe = planned ? "bg-primary" : "bg-secondary"; + } + if (schild.textContent !== text) schild.textContent = text; + const klasse = "badge " + farbe; + if (schild.className !== klasse) schild.className = klasse; + } + + // Schreibt nur bei echter Aenderung - die Tabelle wird im Sekundentakt + // aktualisiert, ihre Werte aendern sich aber hoechstens minuetlich. + function setzeZelle(selektor, text, titel) { + const element = document.querySelector(selektor); + if (!element) return; + if (element.textContent !== text) element.textContent = text; + if (titel) { + if (element.getAttribute("title") !== titel) element.setAttribute("title", titel); + } else if (element.hasAttribute("title")) { + element.removeAttribute("title"); + } } function updateWateringAutoButton(button) {