From 71f2f844c862074c3669bb463647af782330ca88 Mon Sep 17 00:00:00 2001 From: Moirtz Wagner Date: Fri, 4 Sep 2026 16:30:22 +0200 Subject: [PATCH] gartenwasser_module.py mit CRLF wie seine Nachbarn Git fuer Windows setzt core.autocrlf=input in seiner System-Konfiguration, die Synology-Seite hat das nicht. Dieselbe Arbeitskopie liegt auf beiden Systemen - unter /volume1/web/smart und als Z:\ -, und die Datei war deshalb im Arbeitsverzeichnis CRLF, im Repository aber LF: Windows sah sie sauber, die NAS dauerhaft geaendert. Ein "git pull" dort waere daran haengengeblieben. Jetzt liegt sie so im Repository, wie sie im Arbeitsverzeichnis steht - und wie die uebrigen Discovery-Module, die ebenfalls mit CRLF eingecheckt sind. Co-Authored-By: Claude Opus 5 --- .../modules/gartenwasser_module.py | 476 +++++++++--------- 1 file changed, 238 insertions(+), 238 deletions(-) diff --git a/restricted/deviceDiscovery/modules/gartenwasser_module.py b/restricted/deviceDiscovery/modules/gartenwasser_module.py index b297727..b27ff7d 100644 --- a/restricted/deviceDiscovery/modules/gartenwasser_module.py +++ b/restricted/deviceDiscovery/modules/gartenwasser_module.py @@ -1,238 +1,238 @@ -#!/usr/bin/env python3 -""" -Gartenwasser Module -Die beiden Ventilsteuerungen und der Regen - fest beschrieben, nicht gesucht -KEINE Datenbank-Operationen! -""" - -import logging -from typing import List, Dict, Tuple -from modules.base_module import BaseModule - -logger = logging.getLogger(__name__) - -# Topic, auf das gatherRainData.py im SolarManager seine Regensummen legt. -# Dasselbe Topic steht dort noch einmal im Quelltext. Bewusst kein -# Konfigurationseintrag: er müsste in beiden Repositorien gleich lauten, und -# eine Einstellung, die man an zwei Orten gleich halten muss, ist keine. -REGEN_TOPIC = "Wetter/Regen" - -# Welche Regenfenster als Messwert im Editor auftauchen. gatherRainData.py -# veröffentlicht immer tage1 bis tage7; welche davon jemand braucht, entscheidet -# allein diese Liste. Die drei Zonen der alten auto_watering.py brauchten 1, 2 -# und 5 Tage - der Rest steht da, damit eine neue Zone keinen Eingriff im -# SolarManager kostet. -REGEN_FENSTER = [1, 2, 3, 5, 7] - -# Die beiden ESP32-Ventilsteuerungen. Sie melden sich nicht per -# Home-Assistant-Discovery, das MQTT-Modul findet sie also nicht. -# -# topic Wurzel aller Topics dieser Steuerung -# modi Nutzlast des /auto-Topics -> Beschriftung im Editor. Dieselben -# Klartexte stehen in der Whitelist von ajax/watering.php. -# zonen Ventilnummer -> Name der Zone. Eine Steuerung bedient mehrere -# Zonen; "vorn" zum Beispiel Ventil 1 für die Tröge und Ventil 6 -# für den Garten. Aus jeder Zone werden zwei Messwerte. -STEUERUNGEN = [ - { - "name": "Gartenwasser vorn", - "topic": "Gartenwasser/vorn", - "modi": [{"Trog": "Tröge"}, {"Vorn": "Garten vorn"}, {"Stop": "Stopp"}], - "zonen": [(1, "Tröge"), (6, "Garten vorn")], - }, - { - "name": "Gartenwasser hinten", - "topic": "Gartenwasser/hinten", - "modi": [{"Hoch": "Hochbeete"}, {"Stop": "Stopp"}], - "zonen": [(6, "Hochbeete")], - }, -] - - -class GartenwasserModule(BaseModule): - """ - Gartenwasser Modul - Implementiert BaseModule Interface - - Anders als die übrigen Module sucht dieses nichts, es schreibt drei feste - Geräte hin - wie das Logic-Modul auch: - - * Die Ventilsteuerungen sprechen MQTT, aber ohne Home-Assistant- - Discovery. Ohne dieses Modul kennt die Datenbank sie nicht, und im - Automatik-Editor lässt sich die Bewässerung nicht auswählen. - * "Regen" ist gar kein Gerät, sondern das, was gatherRainData.py im - SolarManager von Open-Meteo holt. - - Von Hand in die Datenbank geschrieben würden die Zeilen zwar jeden - Discovery-Lauf überleben (es wird nur mit ON DUPLICATE KEY UPDATE - geschrieben, nie gelöscht), aber niemand wüsste mehr, woher sie kommen. - - Zusammen ersetzt das restricted/gartenbewaesserung/auto_watering.py: die - Zeitfenster werden Bedingungen auf Sonnenauf- und -untergang, die - Regenschwellen Bedingungen auf "Regen ... Tage", und geschaltet wird über - das Kommando "Automatik". - - Zwei Zusagen der Ventilsteuerung stecken in den Messwerten unten, und ohne - sie stimmt die Automatik nicht mehr: - - * LastWatering trägt je Ventil ein "daysSinceWatering" - die Anzahl - Tage, nicht den Zeitstempel. Das Feld fehlt, solange für ein Ventil - keine Bewässerung bekannt ist. - * WateringToday wird um Mitternacht zurückgesetzt. - * Running sagt retained, ob gerade etwas läuft, und fällt über eine - Last-Will-Nachricht auch dann auf false, wenn die Steuerung wegbricht. - """ - - def is_enabled(self) -> bool: - """Prüft ob Gartenwasser aktiviert ist""" - return self.config.gartenwasser_enable - - def discover(self) -> Tuple[List[Dict], List[Dict]]: - """ - Erzeugt die beiden Ventilsteuerungen als Actors und den Regen als Sensor - - Returns: - Tuple (actors, sensors) - """ - logger.info("\n" + "=" * 60) - logger.info("GARTENWASSER-GERÄTE WERDEN ERZEUGT") - logger.info("=" * 60) - - actors = [self._steuerung(s) for s in STEUERUNGEN] - sensors = [self._regen()] - return actors, sensors - - @staticmethod - def _steuerung(steuerung: Dict) -> Dict: - """ - Eine Ventilsteuerung: ein Kommando, zwei Messwerte je Zone. - - Das Kommando trägt seinen Klartext im Parameter und nicht im Namen. - Ein Kommando ohne Parameter schickt im Runner eine leere Nutzlast - (siehe MQTTTransport.senden), der Modus muss also der Parameter sein - - im Editor liest sich das dann als "Automatik Modus = Hochbeete". - """ - topic = steuerung["topic"] - - # Läuft an dieser Steuerung gerade etwas? Der Wächter für jede Regel, - # die hier etwas anstoßen will - die Zonen einer Steuerung hängen an - # derselben Leitung, zwei gleichzeitig gibt es nicht. - # - # Gelesen wird das aus Running und nicht aus Timers: Timers ist nicht - # retained und wird nur im Sekundentakt gesendet, solange etwas läuft. - # Der Runner sähe im Ruhezustand gar nichts und behielte nach einem - # Lauf den zuletzt gesehenen Wert - also genau das Gegenteil von - # verlässlich. Running ist retained und fällt über eine - # Last-Will-Nachricht auch dann auf false, wenn die Steuerung mitten - # im Gießen wegbricht. - states = [{ - "name": "Bewässerung läuft", - "url": topic + "/Running", - "value_path": "running", - "type": "bool", - }, { - # Klartext der laufenden Automatik ("Hoch", "Trog", "Vorn"), leer - # wenn von Hand oder gar nicht bewässert wird. - "name": "Laufender Modus", - "url": topic + "/Running", - "value_path": "mode", - "type": "string", - }] - - for valve, zone in steuerung["zonen"]: - if len(steuerung["zonen"]) > 1: - # Bei einer Steuerung mit nur einer Zone wäre das derselbe Wert - # wie "Bewässerung läuft" - ein zweiter Messwert, der nie etwas - # anderes sagt, macht die Auswahl nur länger. - states.append({ - "name": "Bewässerung läuft " + zone, - "url": topic + "/Running", - "value_path": "running%d" % valve, - "type": "bool", - }) - states.append({ - # Ersetzt die Zwölf-Stunden-Sperre von auto_watering.py: - # "heute noch nicht bewässert" heißt hier schlicht "= 0". - # - # Dass das trägt, liegt an der Steuerung: sie setzt den Wert um - # Mitternacht zurück. Die Nachricht ist retained, und der Runner - # sieht nur die Zahl hinter dem value_path - den Tag, den die - # Nutzlast als "wateringDay" mitführt, kann er nicht prüfen. - # Ohne den Reset stünde nach einer bewässerungsfreien Nacht die - # Summe von gestern als "heute" da, und die Zone bliebe trocken. - "name": "Bewässert heute " + zone, - "url": topic + "/WateringToday", - "value_path": "wateringDurationTodaySecs%d" % valve, - "type": "integer", - "unit": "s", - }) - states.append({ - # Der Trockenheits-Notlauf (max_dry_days in auto_watering.py). - # Ein Zeitstempel nützte hier nichts: der Runner vergleicht - # Datumsangaben nur gegen einen festen Wert, "länger als fünf - # Tage her" ist damit nicht formulierbar. Die Steuerung liefert - # deshalb gleich die Anzahl Tage - dafür wurde ihre Firmware - # erweitert, das Feld gab es vorher nicht. - # - # Für ein Ventil, das noch nie lief, lässt sie das Feld weg - # statt eine 0 zu schicken. Eine 0 hieße "heute bewässert" und - # würde den Notlauf für immer unterdrücken; ohne Feld bleibt - # der Messwert leer, und die Bedingung greift schlicht nicht. - "name": "Tage seit Bewässerung " + zone, - "url": topic + "/LastWatering", - "value_path": "daysSinceWatering%d" % valve, - "type": "integer", - "unit": "Tage", - }) - - return { - "type": "Bewässerung", - "name": steuerung["name"], - "url": "mqtt://" + topic, - "commands": [{ - "command": "Automatik", - "url": topic + "/auto", - "parameters": [{ - "name": "Modus", - "type": "string", - "url": topic + "/auto", - "values": steuerung["modi"], - }], - }], - "states": states, - } - - @staticmethod - def _regen() -> Dict: - """ - Der Regen als Gerät: je Zeitraum ein Messwert, dazu der Stand. - - "Regen heute" ist der bereits gefallene Regen des laufenden Tages, die - übrigen zählen ihn plus die abgeschlossenen Tage davor - so hat auch - auto_watering.py gerechnet. - - Der Stand ist kein Messwert zum Vergleichen, sondern zum Nachsehen: im - Editor steht er hinter dem Namen ("= 04.09.2026 09:00:00") und verrät, - ob der Sammler noch läuft. Bleibt er länger aus, leert gatherRainData.py - die Zahlen von sich aus, und die Bewässerung setzt aus. - """ - states = [{ - "name": "Regen heute" if tage == 1 else "Regen %d Tage" % tage, - "url": REGEN_TOPIC, - "value_path": "tage%d" % tage, - "type": "float", - "unit": "mm", - } for tage in REGEN_FENSTER] - - states.append({ - "name": "Stand", - "url": REGEN_TOPIC, - "value_path": "stand", - "type": "string", - }) - - return { - "type": "Wetter", - "name": "Regen", - "url": "mqtt://" + REGEN_TOPIC, - "states": states, - } +#!/usr/bin/env python3 +""" +Gartenwasser Module +Die beiden Ventilsteuerungen und der Regen - fest beschrieben, nicht gesucht +KEINE Datenbank-Operationen! +""" + +import logging +from typing import List, Dict, Tuple +from modules.base_module import BaseModule + +logger = logging.getLogger(__name__) + +# Topic, auf das gatherRainData.py im SolarManager seine Regensummen legt. +# Dasselbe Topic steht dort noch einmal im Quelltext. Bewusst kein +# Konfigurationseintrag: er müsste in beiden Repositorien gleich lauten, und +# eine Einstellung, die man an zwei Orten gleich halten muss, ist keine. +REGEN_TOPIC = "Wetter/Regen" + +# Welche Regenfenster als Messwert im Editor auftauchen. gatherRainData.py +# veröffentlicht immer tage1 bis tage7; welche davon jemand braucht, entscheidet +# allein diese Liste. Die drei Zonen der alten auto_watering.py brauchten 1, 2 +# und 5 Tage - der Rest steht da, damit eine neue Zone keinen Eingriff im +# SolarManager kostet. +REGEN_FENSTER = [1, 2, 3, 5, 7] + +# Die beiden ESP32-Ventilsteuerungen. Sie melden sich nicht per +# Home-Assistant-Discovery, das MQTT-Modul findet sie also nicht. +# +# topic Wurzel aller Topics dieser Steuerung +# modi Nutzlast des /auto-Topics -> Beschriftung im Editor. Dieselben +# Klartexte stehen in der Whitelist von ajax/watering.php. +# zonen Ventilnummer -> Name der Zone. Eine Steuerung bedient mehrere +# Zonen; "vorn" zum Beispiel Ventil 1 für die Tröge und Ventil 6 +# für den Garten. Aus jeder Zone werden zwei Messwerte. +STEUERUNGEN = [ + { + "name": "Gartenwasser vorn", + "topic": "Gartenwasser/vorn", + "modi": [{"Trog": "Tröge"}, {"Vorn": "Garten vorn"}, {"Stop": "Stopp"}], + "zonen": [(1, "Tröge"), (6, "Garten vorn")], + }, + { + "name": "Gartenwasser hinten", + "topic": "Gartenwasser/hinten", + "modi": [{"Hoch": "Hochbeete"}, {"Stop": "Stopp"}], + "zonen": [(6, "Hochbeete")], + }, +] + + +class GartenwasserModule(BaseModule): + """ + Gartenwasser Modul - Implementiert BaseModule Interface + + Anders als die übrigen Module sucht dieses nichts, es schreibt drei feste + Geräte hin - wie das Logic-Modul auch: + + * Die Ventilsteuerungen sprechen MQTT, aber ohne Home-Assistant- + Discovery. Ohne dieses Modul kennt die Datenbank sie nicht, und im + Automatik-Editor lässt sich die Bewässerung nicht auswählen. + * "Regen" ist gar kein Gerät, sondern das, was gatherRainData.py im + SolarManager von Open-Meteo holt. + + Von Hand in die Datenbank geschrieben würden die Zeilen zwar jeden + Discovery-Lauf überleben (es wird nur mit ON DUPLICATE KEY UPDATE + geschrieben, nie gelöscht), aber niemand wüsste mehr, woher sie kommen. + + Zusammen ersetzt das restricted/gartenbewaesserung/auto_watering.py: die + Zeitfenster werden Bedingungen auf Sonnenauf- und -untergang, die + Regenschwellen Bedingungen auf "Regen ... Tage", und geschaltet wird über + das Kommando "Automatik". + + Zwei Zusagen der Ventilsteuerung stecken in den Messwerten unten, und ohne + sie stimmt die Automatik nicht mehr: + + * LastWatering trägt je Ventil ein "daysSinceWatering" - die Anzahl + Tage, nicht den Zeitstempel. Das Feld fehlt, solange für ein Ventil + keine Bewässerung bekannt ist. + * WateringToday wird um Mitternacht zurückgesetzt. + * Running sagt retained, ob gerade etwas läuft, und fällt über eine + Last-Will-Nachricht auch dann auf false, wenn die Steuerung wegbricht. + """ + + def is_enabled(self) -> bool: + """Prüft ob Gartenwasser aktiviert ist""" + return self.config.gartenwasser_enable + + def discover(self) -> Tuple[List[Dict], List[Dict]]: + """ + Erzeugt die beiden Ventilsteuerungen als Actors und den Regen als Sensor + + Returns: + Tuple (actors, sensors) + """ + logger.info("\n" + "=" * 60) + logger.info("GARTENWASSER-GERÄTE WERDEN ERZEUGT") + logger.info("=" * 60) + + actors = [self._steuerung(s) for s in STEUERUNGEN] + sensors = [self._regen()] + return actors, sensors + + @staticmethod + def _steuerung(steuerung: Dict) -> Dict: + """ + Eine Ventilsteuerung: ein Kommando, zwei Messwerte je Zone. + + Das Kommando trägt seinen Klartext im Parameter und nicht im Namen. + Ein Kommando ohne Parameter schickt im Runner eine leere Nutzlast + (siehe MQTTTransport.senden), der Modus muss also der Parameter sein - + im Editor liest sich das dann als "Automatik Modus = Hochbeete". + """ + topic = steuerung["topic"] + + # Läuft an dieser Steuerung gerade etwas? Der Wächter für jede Regel, + # die hier etwas anstoßen will - die Zonen einer Steuerung hängen an + # derselben Leitung, zwei gleichzeitig gibt es nicht. + # + # Gelesen wird das aus Running und nicht aus Timers: Timers ist nicht + # retained und wird nur im Sekundentakt gesendet, solange etwas läuft. + # Der Runner sähe im Ruhezustand gar nichts und behielte nach einem + # Lauf den zuletzt gesehenen Wert - also genau das Gegenteil von + # verlässlich. Running ist retained und fällt über eine + # Last-Will-Nachricht auch dann auf false, wenn die Steuerung mitten + # im Gießen wegbricht. + states = [{ + "name": "Bewässerung läuft", + "url": topic + "/Running", + "value_path": "running", + "type": "bool", + }, { + # Klartext der laufenden Automatik ("Hoch", "Trog", "Vorn"), leer + # wenn von Hand oder gar nicht bewässert wird. + "name": "Laufender Modus", + "url": topic + "/Running", + "value_path": "mode", + "type": "string", + }] + + for valve, zone in steuerung["zonen"]: + if len(steuerung["zonen"]) > 1: + # Bei einer Steuerung mit nur einer Zone wäre das derselbe Wert + # wie "Bewässerung läuft" - ein zweiter Messwert, der nie etwas + # anderes sagt, macht die Auswahl nur länger. + states.append({ + "name": "Bewässerung läuft " + zone, + "url": topic + "/Running", + "value_path": "running%d" % valve, + "type": "bool", + }) + states.append({ + # Ersetzt die Zwölf-Stunden-Sperre von auto_watering.py: + # "heute noch nicht bewässert" heißt hier schlicht "= 0". + # + # Dass das trägt, liegt an der Steuerung: sie setzt den Wert um + # Mitternacht zurück. Die Nachricht ist retained, und der Runner + # sieht nur die Zahl hinter dem value_path - den Tag, den die + # Nutzlast als "wateringDay" mitführt, kann er nicht prüfen. + # Ohne den Reset stünde nach einer bewässerungsfreien Nacht die + # Summe von gestern als "heute" da, und die Zone bliebe trocken. + "name": "Bewässert heute " + zone, + "url": topic + "/WateringToday", + "value_path": "wateringDurationTodaySecs%d" % valve, + "type": "integer", + "unit": "s", + }) + states.append({ + # Der Trockenheits-Notlauf (max_dry_days in auto_watering.py). + # Ein Zeitstempel nützte hier nichts: der Runner vergleicht + # Datumsangaben nur gegen einen festen Wert, "länger als fünf + # Tage her" ist damit nicht formulierbar. Die Steuerung liefert + # deshalb gleich die Anzahl Tage - dafür wurde ihre Firmware + # erweitert, das Feld gab es vorher nicht. + # + # Für ein Ventil, das noch nie lief, lässt sie das Feld weg + # statt eine 0 zu schicken. Eine 0 hieße "heute bewässert" und + # würde den Notlauf für immer unterdrücken; ohne Feld bleibt + # der Messwert leer, und die Bedingung greift schlicht nicht. + "name": "Tage seit Bewässerung " + zone, + "url": topic + "/LastWatering", + "value_path": "daysSinceWatering%d" % valve, + "type": "integer", + "unit": "Tage", + }) + + return { + "type": "Bewässerung", + "name": steuerung["name"], + "url": "mqtt://" + topic, + "commands": [{ + "command": "Automatik", + "url": topic + "/auto", + "parameters": [{ + "name": "Modus", + "type": "string", + "url": topic + "/auto", + "values": steuerung["modi"], + }], + }], + "states": states, + } + + @staticmethod + def _regen() -> Dict: + """ + Der Regen als Gerät: je Zeitraum ein Messwert, dazu der Stand. + + "Regen heute" ist der bereits gefallene Regen des laufenden Tages, die + übrigen zählen ihn plus die abgeschlossenen Tage davor - so hat auch + auto_watering.py gerechnet. + + Der Stand ist kein Messwert zum Vergleichen, sondern zum Nachsehen: im + Editor steht er hinter dem Namen ("= 04.09.2026 09:00:00") und verrät, + ob der Sammler noch läuft. Bleibt er länger aus, leert gatherRainData.py + die Zahlen von sich aus, und die Bewässerung setzt aus. + """ + states = [{ + "name": "Regen heute" if tage == 1 else "Regen %d Tage" % tage, + "url": REGEN_TOPIC, + "value_path": "tage%d" % tage, + "type": "float", + "unit": "mm", + } for tage in REGEN_FENSTER] + + states.append({ + "name": "Stand", + "url": REGEN_TOPIC, + "value_path": "stand", + "type": "string", + }) + + return { + "type": "Wetter", + "name": "Regen", + "url": "mqtt://" + REGEN_TOPIC, + "states": states, + }