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) {