Bisher entschied jede Arbeitsstation fuer sich, welche Zeilenenden ein Commit bekommt: Git fuer Windows setzt core.autocrlf=input in seiner System-Konfiguration, die NAS setzt nichts. Weil der Bestand ausserdem in sich gemischt war - 34 PHP-Dateien mit LF, 30 mit CRLF -, gab es gar kein richtiges Zeilenende, an das ein Werkzeug sich haette halten koennen. Jede Ergaenzung mit LF in einer CRLF-Datei ergab eine gemischte Datei; helper.php und js/solar/homeMQTT.js waren bereits so entstanden. .gitattributes macht die Zeilenenden zur Eigenschaft des Repositorys. LF, weil dieses Verzeichnis der Web-Ordner der NAS ist und von Linux ausgeliefert wird - und weil Git fuer Windows mit "input" ohnehin schon dorthin zeigt. restricted/WebAuthn bleibt ausgenommen und damit byteweise so, wie die Bibliothek geliefert wurde; darunter liegen 292 Zertifikate. Von den 47 geaenderten Dateien wurde nachgerechnet keine einzige im Inhalt angefasst - der Vergleich HEAD-ohne-CR gegen Index ist ueberall gleich. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
239 lines
9.9 KiB
Python
239 lines
9.9 KiB
Python
#!/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<n>" - 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,
|
|
}
|