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 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 22:47:50 +02:00
co-authored by Claude Opus 5
parent 37270acb23
commit eca5c8f041
2 changed files with 197 additions and 61 deletions
+138 -43
View File
@@ -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) {