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],
];
// 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
<div class="p-3">
<div id="watering-feedback" class="alert d-none mb-3" role="status"></div>
@@ -103,7 +97,9 @@ echo <<<HTML
</div>
</div>
<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>
+35 -100
View File
@@ -38,7 +38,6 @@ const solarMQTT = {
client.subscribe("Gartenwasser/hinten/LastWatering");
client.subscribe("Gartenwasser/hinten/WateringToday");
client.subscribe("Gartenwasser/hinten/LastWateringDay");
client.subscribe("Gartenwasser/AutoWatering/#");
});
client.on("error", function (error) {
//alert("MQTT Error: " + error);
@@ -69,7 +68,7 @@ const solarMQTT = {
if (topic === "Gartenwasser/vorn/Timers" || topic === "Gartenwasser/hinten/Timers") {
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"))) {
updateWateringStatus();
}
@@ -729,20 +728,20 @@ 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.
// Eine Tabelle fuer alles: Ventil, Beschriftung und die Anzeige-Ids im
// Realtime-SVG. Vorher standen dieselben Zuordnungen in drei getrennten
// Tabellen.
const wateringZones = {
troughs: {
topic: "Gartenwasser/vorn", valve: 1, auto: "troege", label: "Tröge",
topic: "Gartenwasser/vorn", valve: 1, label: "Tröge",
statusId: "wateringTroughStatus", iconId: "wateringTroughIcon"
},
frontGarden: {
topic: "Gartenwasser/vorn", valve: 6, auto: "garten_vorn", label: "Vorn",
topic: "Gartenwasser/vorn", valve: 6, label: "Vorn",
statusId: "wateringFrontStatus", iconId: "wateringFrontIcon"
},
raisedBeds: {
topic: "Gartenwasser/hinten", valve: 6, auto: "hochbeete", label: "Beete",
topic: "Gartenwasser/hinten", valve: 6, label: "Beete",
statusId: "wateringBedsStatus", iconId: "wateringBedsIcon"
}
};
@@ -854,12 +853,14 @@ var chartSettings = {
// "heute" in der Karte.
function wateringDayTotal(topic, subTopic, feld, valve, mussHeuteSein) {
const data = wateringJson(topic + "/" + subTopic);
if (!data) return { label: "--", day: "" };
if (!data) return { label: "--", day: "", sekunden: null };
const tag = data.wateringDay || "";
const sekunden = data[feld + valve];
if (sekunden === undefined) return { label: "--", day: tag };
if (mussHeuteSein && tag !== wateringDayStamp(new Date())) return { label: "--", day: tag };
return { label: wateringTimeLabel(Number(sekunden)), day: tag };
if (sekunden === undefined) return { label: "--", day: tag, sekunden: null };
if (mussHeuteSein && tag !== wateringDayStamp(new Date())) {
return { label: "--", day: tag, sekunden: null };
}
return { label: wateringTimeLabel(Number(sekunden)), day: tag, sekunden: Number(sekunden) };
}
function wateringAutoActive(topic, mode) {
@@ -931,101 +932,35 @@ var chartSettings = {
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() {
wateringZoneList.forEach(([key, zone]) => {
const state = wateringLabel(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.
const status = svgElement(zone.statusId);
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.
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);
setzeZelle(`[data-watering-last='${key}']`, history.dateTime);
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);
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);
const plan = wateringPlanText(zone.auto);
setzeZelle(`[data-watering-plan='${key}']`, plan.text, plan.titel);
});
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,
@@ -1039,8 +974,8 @@ var chartSettings = {
}
// Zustandsschild der Zonenkarte, Farben wie im SVG: laufend gruen,
// pausiert gelb, von der Automatik geplant blau, sonst grau.
function setzeSchild(selektor, state, paused, planned) {
// pausiert gelb, heute schon bewaessert blau, sonst grau.
function setzeSchild(selektor, state, paused, bewaessert) {
const schild = document.querySelector(selektor);
if (!schild) return;
let text = "unbekannt";
@@ -1049,8 +984,8 @@ var chartSettings = {
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";
text = bewaessert ? "heute bewässert" : "aus";
farbe = bewaessert ? "bg-primary" : "bg-secondary";
}
if (schild.textContent !== text) schild.textContent = text;
const klasse = "badge " + farbe;
@@ -1082,23 +1017,23 @@ var chartSettings = {
button.classList.toggle("btn-outline-danger", active);
}
function updateWateringIndicator(status, icon, name, state, paused, planned) {
// "Aus" bleibt bei nicht geplant stehen; bei geplant ersetzt die
// Stoppuhr (U+23F1, ohne Emoji-Variationsselektor -> einfarbig, folgt
// der Icon-Farbe) den Text, um Platz auf der textPath zu sparen.
// Laeuft im Sekundentakt, aendert sich aber selten - ohne Pruefung
function updateWateringIndicator(status, icon, name, state, paused, bewaessert) {
// "Aus" bleibt stehen, solange die Zone heute noch kein Wasser hatte;
// danach ersetzt das Haekchen (U+2713, ohne Emoji-Variationsselektor ->
// einfarbig, folgt der Icon-Farbe) den Text, um Platz auf der textPath zu
// sparen. Laeuft im Sekundentakt, aendert sich aber selten - ohne Pruefung
// wuerde jede Sekunde derselbe Wert geschrieben und das Icon samt
// Filter neu gezeichnet.
const text = state === "aus" ? (planned ? "" : "Aus") : state;
const text = state === "aus" ? (bewaessert ? "" : "Aus") : state;
if (status.textContent !== text) status.textContent = text;
// Geplant-Blau ("#3A6292") entspricht dem Wassertropfen-Icon (id="water").
const color = state !== "aus" ? (paused ? "#ffc107" : "#20c997") : planned ? "#3A6292" : "#6c757d";
// Das Blau ("#3A6292") entspricht dem Wassertropfen-Icon (id="water").
const color = state !== "aus" ? (paused ? "#ffc107" : "#20c997") : bewaessert ? "#3A6292" : "#6c757d";
if (icon.getAttribute("color") !== color) {
icon.setAttribute("color", color);
icon.setAttribute("fill", 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");
if (titelKnoten.textContent !== titel) titelKnoten.textContent = titel;
}
+3 -3
View File
@@ -45,9 +45,9 @@ function meshDb()
* ein langsamer Durchlauf den Termin nicht verschluckt.
* ab 16:30 ab da bis Mitternacht wahr. Ausgeloest wird trotzdem nur
* einmal, weil nur die steigende Flanke zaehlt; dafuer kostet
* ein verpasster Takt nicht den ganzen Tag. Dieselbe
* Ueberlegung steht hinter den breiten Zeitfenstern in
* auto_watering.py.
* ein verpasster Takt nicht den ganzen Tag. Fuer alles, was
* laufen muss, auch wenn der Termin selbst verpasst wurde,
* ist das die richtige Wahl - etwa die Bewaesserung.
* vor 16:30 bis dahin wahr, danach nicht mehr.
*
* 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;
/**
* Mittelpunkt des Ausschnitts. Dieselben Koordinaten stehen in
* restricted/gartenbewaesserung/auto_watering.py, von wo die Regendaten
* geholt werden - es ist derselbe Ort.
* Mittelpunkt des Ausschnitts. Dieselben Koordinaten stehen als Vorgabe in
* gatherRainData.py im SolarManager, von wo die Regendaten geholt werden -
* es ist derselbe Ort.
*/
$lat = isset($_GET["lat"]) ? floatval($_GET["lat"]) : 47.5781;
$lon = isset($_GET["lon"]) ? floatval($_GET["lon"]) : 10.2628;