diff --git a/restricted/deviceDiscovery/device_discovery.py b/restricted/deviceDiscovery/device_discovery.py index 93e079a..3817569 100644 --- a/restricted/deviceDiscovery/device_discovery.py +++ b/restricted/deviceDiscovery/device_discovery.py @@ -19,6 +19,7 @@ from modules.tahoma_module import TahomaModule from modules.wled_module import WLEDModule from modules.mqtt_module import MQTTModule from modules.shelly_module import ShellyModule +from modules.gartenwasser_module import GartenwasserModule from modules.logic_module import LogicModule #nicht Vergessen neue Module auch in Zeile 546 einzufügen!! # Logging @@ -149,6 +150,13 @@ class Config: if not self.config.has_section('logic'): return False return self._get_bool('logic', 'enable', False) + + # GARTENWASSER + @property + def gartenwasser_enable(self) -> bool: + if not self.config.has_section('gartenwasser'): + return False + return self._get_bool('gartenwasser', 'enable', False) # MQTT @@ -561,7 +569,8 @@ def main(): WLEDModule(config), MQTTModule(config), ShellyModule(config), - LogicModule(config) + LogicModule(config), + GartenwasserModule(config) ] # Jedes Modul durchlaufen diff --git a/restricted/deviceDiscovery/modules/gartenwasser_module.py b/restricted/deviceDiscovery/modules/gartenwasser_module.py new file mode 100644 index 0000000..5f0fbde --- /dev/null +++ b/restricted/deviceDiscovery/modules/gartenwasser_module.py @@ -0,0 +1,206 @@ +#!/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. + * WateringToday wird um Mitternacht zurückgesetzt. + """ + + 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"] + + states = [{ + # Klartext der gerade laufenden Automatik, leer wenn keine läuft. + # Ohne Werteliste: was die Steuerung bei Stillstand genau schickt, + # steht in keiner Dokumentation, und eine falsche Übersetzung wäre + # schlimmer als der rohe Wert. + "name": "Laufende Automatik", + "url": topic + "/Timers", + "value_path": "auto", + "type": "string", + }] + + for valve, zone in steuerung["zonen"]: + 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. + "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, + } diff --git a/restricted/logdateien.php b/restricted/logdateien.php index f1e641f..345238f 100644 --- a/restricted/logdateien.php +++ b/restricted/logdateien.php @@ -48,6 +48,14 @@ function logDateien() "symbol" => "bi-diagram-3", "hinweis" => "Wertet die Automatiken aus und schickt die Kommandos.", ], + "regen" => [ + "titel" => "Regen", + "datei" => "rainOutput.log", + "symbol" => "bi-cloud-rain", + "hinweis" => "Holt die Niederschlagsdaten von Open-Meteo und legt " + . "sie nach Wetter/Regen, damit die Bewässerungs-" + . "Automatiken sie als Messwert bekommen.", + ], "wattpilot" => [ "titel" => "Wattpilot", "datei" => "wattpilotshell.log",