Compare commits

...
2 Commits
Author SHA1 Message Date
adminandClaude Opus 5 128acae48e Bewaesserung und Regen werden Geraete im Automatik-Editor
Erster Teil der Ablosung von restricted/gartenbewaesserung/auto_watering.py.
Das Skript holt heute stuendlich den Regen, vergleicht ihn mit Schwellen
aus einer ZONES-Tabelle im Quelltext und schaltet selbst. Kuenftig
entscheidet der AutoAction-Runner, und die Schwellen stehen im Editor.

Dafuer legt ein neues Discovery-Modul drei Geraete an. Es sucht nichts,
sondern schreibt sie fest hin - wie das Logic-Modul auch:

  * Die beiden ESP32-Ventilsteuerungen sprechen zwar MQTT, aber ohne
    Home-Assistant-Discovery; das MQTT-Modul findet sie deshalb nicht.
  * "Regen" ist gar kein Geraet, sondern das, was gatherRainData.py im
    SolarManager von Open-Meteo holt und nach Wetter/Regen legt.

Je Steuerung entsteht ein Kommando "Automatik" mit dem Modus als
Parameter - ein Kommando ohne Parameter schickt im Runner eine leere
Nutzlast, der Klartext ("Hoch", "Trog", "Vorn") muss also der Parameter
sein. Dazu je Zone zwei Messwerte:

  Bewaessert heute       ersetzt die Zwoelf-Stunden-Sperre des Skripts;
                         "heute noch nicht" heisst hier "= 0". Die
                         Steuerung setzt den Wert um Mitternacht zurueck,
                         der Runner kann den mitgelieferten "wateringDay"
                         naemlich nicht pruefen.
  Tage seit Bewaesserung ersetzt max_dry_days. Ein Zeitstempel nuetzte
                         nichts: der Runner vergleicht Datumsangaben nur
                         gegen einen festen Wert, "laenger als fuenf Tage
                         her" ist damit nicht formulierbar. Die Firmware
                         liefert deshalb gleich die Anzahl Tage.

Die Regensummen kommen als tage1 bis tage7 herein; welche davon als
Messwert auftauchen, entscheidet allein dieses Modul. Der Sammler
veroeffentlicht immer alle sieben, damit sich die beiden Repositorien
nicht ueber eine Fensterliste einigen muessen.

Die Logseite kennt rainOutput.log jetzt ebenfalls.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 13:37:43 +02:00
adminandClaude Opus 5 37962224c1 Geraeteauswahl im Automatik-Editor gruppiert nach Etage
Bisher stand ueber jedem Raum eine eigene Ueberschrift - bei neunzehn
Raeumen aus rooms.php also neunzehn Stueck, jede mit der Etage davor:
"OG - Bad", "EG - Bad", "UG - Bad". In einer Liste, die fuenfzehn Zeilen
hoch ist, sah man dadurch mehr Ueberschrift als Geraet.

Jetzt gruppiert nur die Etage, und der Raum steht klein und rechtsbuendig
an jeder Zeile. Sortiert wird innerhalb der Etage nach Raum (Reihenfolge
wie in rooms.php), sodass die Geraete eines Raums trotzdem beisammen
stehen; aus zwanzig Ueberschriften werden fuenf. Ein Raum, den es in
rooms.php nicht mehr gibt, bekommt eine eigene Ueberschrift, statt
stillschweigend unter den Unzugeordneten zu verschwinden.

Die Suche laeuft zusaetzlich ueber den Raum: "bad" findet damit auch den
Handtuchtrockner, der im Bad haengt, ohne es im Namen zu tragen. In der
Trefferliste steht keine Ueberschrift darueber, deshalb traegt dort jede
Zeile die volle Angabe "OG - Bad".

Dafuer nennt raumListe() Etage und Raum jetzt auch einzeln, und der
Geraetekatalog reicht beides durch - der Schluessel "Etage/Raum" muss so
nicht im JavaScript wieder auseinandergenommen werden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 13:37:25 +02:00
5 changed files with 374 additions and 66 deletions
+110 -64
View File
@@ -82,74 +82,115 @@ function option(e, gewaehlt) {
}
/**
* Geraeteliste gruppiert - nach Raum, wo zugeordnet, sonst nach Geraeteart.
* Geraeteliste fuer die Auswahl: eine Ueberschrift je Etage, darin nach Raum
* sortiert.
*
* Beim Anlegen einer Automatik denkt man in Raeumen ("im Bad soll..."), nicht
* in Geraetearten. Der Raum steht aber nur da, wo er von Hand vergeben wurde
* (siehe Zuordnung ueber das Zahnrad in der Karte "Automatismen"); solange
* das niemand getan hat, bleibt die Gruppierung nach Art - besser als eine
* einzige Gruppe "Nicht zugeordnet".
* Beim Anlegen einer Automatik denkt man in Raeumen ("im Bad soll ..."), eine
* Ueberschrift je Raum waeren aber zwanzig Stueck - in einer Liste, die
* fuenfzehn Zeilen hoch ist, saehe man dann mehr Ueberschrift als Geraet, und
* "OG · Bad", "EG · Bad", "UG · Bad" schrieben die Etage immer wieder
* dazu. Deshalb gruppiert nur die Etage; der Raum steht klein an jeder Zeile,
* und weil danach sortiert wird, stehen die Geraete eines Raums trotzdem
* beisammen.
*
* Der Raum steht nur da, wo er von Hand vergeben wurde (Zuordnung ueber das
* Zahnrad in der Karte "Automatismen"). Solange das niemand getan hat, gaebe
* die Etage nichts her - dann bleibt es bei der Ordnung nach Geraeteart.
*/
function geraeteGruppen(geraete) {
if (!geraete.some(d => d.room)) {
return gruppiere(geraete, d => d.type, null, "Einzelne", true);
return nachGeraeteart(geraete);
}
const gruppen = gruppiere(geraete, d => d.roomLabel, raumReihenfolge(),
"Nicht zugeordnet", false);
// Was noch keinen Raum hat, bleibt sonst ein einziger Eimer - solange erst
// ein Teil zugeordnet ist, waeren das die meisten Geraete. Innerhalb des
// Eimers gilt deshalb weiter die Ordnung nach Geraeteart.
const letzte = gruppen[gruppen.length - 1];
if (!letzte || !letzte.titel.startsWith("Nicht zugeordnet")) return gruppen;
gruppen.pop();
return gruppen.concat(
gruppiere(letzte.geraete, d => d.type, null, "Einzelne", true)
.map(g => ({ titel: "Ohne Raum · " + g.titel, geraete: g.geraete })));
}
/** Reihenfolge der Raum-Ueberschriften, wie in rooms.php notiert. */
function raumReihenfolge() {
return autoRooms.map(r => r.label).concat(["Nicht zugeordnet"]);
const etagen = etagenReihenfolge();
// "Ohne Raum" und "Unbekannter Raum" stehen in keiner Raumliste. Ihr
// indexOf() waere -1 und damit ganz vorne; sie gehoeren aber ans Ende.
const platz = etage => etagen.includes(etage) ? etagen.indexOf(etage) : etagen.length;
const nach = new Map();
geraete.forEach(d => {
if (!nach.has(d.floorLabel)) nach.set(d.floorLabel, []);
nach.get(d.floorLabel).push(d);
});
return Array.from(nach.keys())
.sort((a, b) => platz(a) - platz(b) || a.localeCompare(b, "de"))
.map(etage => {
const liste = nach.get(etage);
liste.sort((a, b) => raumIndex(a.room) - raumIndex(b.room)
|| a.type.localeCompare(b.type, "de")
|| a.label.localeCompare(b.label, "de"));
return { titel: etage + " (" + liste.length + ")", geraete: liste };
});
}
/**
* Nach einem Merkmal gruppieren.
*
* `einzelneSammeln` entscheidet, was mit Gruppen aus einem einzigen Geraet
* passiert. Bei Gerätearten sollen sie zusammenfallen - fuenfzehn
* Ueberschriften mit je einem Eintrag waeren unuebersichtlicher als die
* flache Liste. Bei Raeumen nicht: "OG · Bad" ist auch mit einem einzigen
* Geraet die Ueberschrift, die man sucht, und in der Wohnung haengt oft nur
* ein Thermostat je Raum.
* Reihenfolge der Etagen-Ueberschriften, wie rooms.php sie notiert - erst OG,
* dann EG, dann UG, dann Aussen.
*/
function gruppiere(geraete, merkmal, reihenfolge, restTitel, einzelneSammeln) {
function etagenReihenfolge() {
const etagen = [];
autoRooms.forEach(r => { if (!etagen.includes(r.floor)) etagen.push(r.floor); });
return etagen;
}
/**
* Platz eines Raums in rooms.php - die Sortierung innerhalb einer Etage.
*
* Alles ohne Raum bekommt denselben letzten Platz; dort entscheidet dann die
* Geraeteart, sonst stuenden Jalousie, Steckdose und Thermostat wild
* durcheinander.
*/
function raumIndex(key) {
const i = autoRooms.findIndex(r => r.key === key);
return i < 0 ? autoRooms.length : i;
}
/**
* Was klein hinter dem Geraetenamen steht.
*
* Unter einer Etagen-Ueberschrift genuegt der Raum. In der Trefferliste einer
* Suche steht keine Ueberschrift darueber, dort muss die Etage mit. Wo kein
* Raum vergeben ist, steht die Geraeteart - die ist immer da und sagt
* wenigstens, was fuer ein Ding das ist.
*/
function zeilenZusatz(d, mitEtage) {
if (!d.roomName) return d.type;
return mitEtage ? d.roomLabel : d.roomName;
}
/**
* Worueber die Suche laeuft: Name, Raum und Geraeteart in einem Zug.
*
* Der Raum gehoert dazu, seit die Liste nach Etagen gruppiert - "bad" soll
* auch den Handtuchtrockner finden, der im Bad haengt, ohne es im Namen zu
* tragen. Ohne Zuordnung faellt der Raum weg; "Nicht zugeordnet" als
* Suchbegriff waere kein Fund, sondern ein Versehen.
*/
function suchfeld(d) {
return (d.label + " " + (d.roomName ? d.roomLabel : "") + " " + d.type).toLowerCase();
}
/**
* Nach Geraeteart gruppieren - der Rueckfall, solange kein Raum vergeben ist.
*
* Gruppen aus einem einzigen Geraet fallen dabei unter "Einzelne" zusammen:
* fuenfzehn Ueberschriften mit je einem Eintrag waeren unuebersichtlicher als
* die flache Liste darunter.
*/
function nachGeraeteart(geraete) {
const nach = new Map();
geraete.forEach(d => {
const schluessel = merkmal(d) || restTitel;
if (!nach.has(schluessel)) nach.set(schluessel, []);
nach.get(schluessel).push(d);
});
const titel = Array.from(nach.keys());
titel.sort((a, b) => {
if (reihenfolge) {
const ia = reihenfolge.indexOf(a), ib = reihenfolge.indexOf(b);
if (ia !== ib) return (ia < 0 ? 999 : ia) - (ib < 0 ? 999 : ib);
}
return a.localeCompare(b, "de");
if (!nach.has(d.type)) nach.set(d.type, []);
nach.get(d.type).push(d);
});
const gruppen = [];
const rest = [];
titel.forEach(t => {
const liste = nach.get(t);
if (!einzelneSammeln || liste.length > 1 || t === restTitel) {
gruppen.push({ titel: t + " (" + liste.length + ")", geraete: liste });
} else {
rest.push(liste[0]);
}
Array.from(nach.keys()).sort((a, b) => a.localeCompare(b, "de")).forEach(art => {
const liste = nach.get(art);
if (liste.length > 1) gruppen.push({ titel: art + " (" + liste.length + ")", geraete: liste });
else rest.push(liste[0]);
});
if (rest.length) {
rest.sort((a, b) => a.label.localeCompare(b.label, "de"));
gruppen.push({ titel: restTitel + " (" + rest.length + ")", geraete: rest });
gruppen.push({ titel: "Einzelne (" + rest.length + ")", geraete: rest });
}
return gruppen;
}
@@ -158,9 +199,10 @@ function gruppiere(geraete, merkmal, reihenfolge, restTitel, einzelneSammeln) {
* Geraeteauswahl mit Suchfeld.
*
* Bei ueber fuenfzig Geraeten hilft Tippen mehr als Blaettern - und es hilft
* quer zur Gruppierung: "bad" findet das Thermostat im Bad und die Jalousie
* "Bad Links", "jalousie" findet alle elf auf einmal. Gesucht wird deshalb
* ueber Beschriftung und Geraeteart zugleich.
* quer zur Gruppierung: "bad" findet das Thermostat im Bad, die Jalousie
* "Bad Links" und alles andere, was einem Bad zugeordnet ist, "jalousie"
* findet alle elf auf einmal. Gesucht wird deshalb ueber Beschriftung, Raum
* und Geraeteart zugleich.
*
* Statt einer Auswahlliste steht ein Knopf in der Zeile; die Liste klappt
* darunter ueber die volle Breite auf. Auf dem Handy ist das mehr Platz als
@@ -195,7 +237,7 @@ function oeffneWaehler(knopf, geraete, aktuelleId, beiAuswahl) {
const suche = document.createElement("input");
suche.type = "search";
suche.className = "form-control form-control-sm mb-2";
suche.placeholder = "Suchen – Name oder Geräteart";
suche.placeholder = "Suchen – Name, Raum oder Geräteart";
const liste = document.createElement("div");
liste.className = "list-group list-group-flush";
liste.style.maxHeight = "15rem";
@@ -206,13 +248,18 @@ function oeffneWaehler(knopf, geraete, aktuelleId, beiAuswahl) {
// input-group stuende die Liste sonst als weitere Spalte daneben.
knopf.closest(".input-group").after(panel);
const eintrag = (d) => {
const eintrag = (d, mitEtage) => {
const zeile = document.createElement("button");
zeile.type = "button";
// Der Zusatz steht rechts am Rand statt gleich hinter dem Namen: so
// stehen die Raeume untereinander in einer Spalte und lassen sich
// ueberfliegen, statt bei jeder Zeile woanders anzufangen.
zeile.className = "list-group-item list-group-item-action py-1"
+ " d-flex justify-content-between align-items-center gap-2"
+ (d.id === aktuelleId ? " active" : "");
zeile.innerHTML = "<span>" + textSicher(d.label) + "</span>"
+ "<small class='ms-2 opacity-75'>" + textSicher(d.type) + "</small>";
zeile.innerHTML = "<span class='text-truncate'>" + textSicher(d.label) + "</span>"
+ "<small class='opacity-75 text-nowrap'>"
+ textSicher(zeilenZusatz(d, mitEtage)) + "</small>";
zeile.onclick = () => { schliesseWaehler(); beiAuswahl(d); };
return zeile;
};
@@ -220,17 +267,16 @@ function oeffneWaehler(knopf, geraete, aktuelleId, beiAuswahl) {
const zeichne = () => {
const suchtext = suche.value.trim().toLowerCase();
liste.innerHTML = "";
const passend = geraete.filter(d => !suchtext
|| d.label.toLowerCase().includes(suchtext)
|| d.type.toLowerCase().includes(suchtext));
const passend = geraete.filter(d => !suchtext || suchfeld(d).includes(suchtext));
if (!passend.length) {
liste.innerHTML = "<div class='p-2 small text-body-secondary'>Nichts gefunden.</div>";
return;
}
// Beim Suchen die Zwischenueberschriften weglassen: die Treffer stehen
// dann quer ueber alle Arten, und die Art steht ohnehin an jeder Zeile.
// dann quer ueber alle Etagen. Dafuer traegt jede Zeile die Etage mit -
// "Bad" allein sagt sonst nicht, welches der drei Baeder gemeint ist.
if (suchtext) {
passend.forEach(d => liste.appendChild(eintrag(d)));
passend.forEach(d => liste.appendChild(eintrag(d, true)));
return;
}
geraeteGruppen(passend).forEach(g => {
@@ -238,7 +284,7 @@ function oeffneWaehler(knopf, geraete, aktuelleId, beiAuswahl) {
kopf.className = "px-2 pt-2 pb-1 small fw-bold text-body-secondary";
kopf.textContent = g.titel;
liste.appendChild(kopf);
g.geraete.forEach(d => liste.appendChild(eintrag(d)));
g.geraete.forEach(d => liste.appendChild(eintrag(d, false)));
});
};
+40 -1
View File
@@ -225,6 +225,12 @@ function anzeigeName($name)
* Einzige Quelle bleibt restricted/rooms.php - ein neuer Raum ist dort eine
* Zeile und taucht hier automatisch auf. Der Schluessel ist "Etage/Raum",
* weil "Bad" allein auf drei Etagen vorkommt.
*
* Etage und Raum stehen zusaetzlich einzeln daneben: die Geraeteauswahl im
* Editor gruppiert nach der einen und sortiert nach dem anderen, und der
* Schluessel soll dafuer nicht erst im JavaScript wieder auseinandergenommen
* werden. Die Reihenfolge dieser Liste ist zugleich die Reihenfolge der
* Ueberschriften dort - erst OG, dann EG, dann UG, wie in rooms.php notiert.
*/
function raumListe()
{
@@ -233,9 +239,15 @@ function raumListe()
$liste[] = [
"key" => $raum["floor"] . "/" . $raum["mqtt"],
"label" => $raum["floor"] . " · " . $raum["mqtt"],
"floor" => $raum["floor"],
"room" => $raum["mqtt"],
];
}
$liste[] = ["key" => RAUM_AUSSEN, "label" => "Außen"];
// Aussen zaehlt als eigene Etage mit genau einem Raum: Carport, Garage und
// Regensensor liegen auf keinem Stockwerk, sollen aber ihre eigene
// Ueberschrift bekommen und nicht unter "Ohne Raum" fallen.
$liste[] = ["key" => RAUM_AUSSEN, "label" => "Außen",
"floor" => "Außen", "room" => ""];
return $liste;
}
@@ -255,6 +267,28 @@ function raumBeschriftung($key)
return $key . " (unbekannt)";
}
/**
* Etage und Raumname zu einem gespeicherten Raumschluessel.
*
* Die Geraeteauswahl im Editor braucht beides getrennt: die Etage wird zur
* Ueberschrift, der Raum zur Sortierung darunter. Geraete ohne Zuordnung
* bekommen ihre eigene Ueberschrift ganz am Ende; ein Raum, den es in
* rooms.php nicht mehr gibt, ebenso - er verschwindet nicht still zwischen
* den Unzugeordneten, sondern faellt auf.
*/
function raumEinordnung($key)
{
if (!$key) {
return ["floor" => "Ohne Raum", "room" => ""];
}
foreach (raumListe() as $raum) {
if ($raum["key"] === $key) {
return ["floor" => $raum["floor"], "room" => $raum["room"]];
}
}
return ["floor" => "Unbekannter Raum", "room" => $key];
}
/**
* Raumvorschlaege aus den MQTT-Topics.
*
@@ -377,6 +411,7 @@ function deviceCatalog()
return strcoll($labels[intval($a["id"])], $labels[intval($b["id"])]);
});
foreach ($zeilen as $row) {
$wo = raumEinordnung($row["room"]);
$devices[intval($row["id"])] = [
"id" => intval($row["id"]),
"name" => $row["name"],
@@ -385,6 +420,10 @@ function deviceCatalog()
"url" => $row["url"],
"room" => $row["room"],
"roomLabel" => raumBeschriftung($row["room"]),
// Etage und Raum getrennt: die Auswahlliste im Editor gruppiert
// nach der Etage und sortiert darin nach dem Raum.
"floorLabel" => $wo["floor"],
"roomName" => $wo["room"],
"states" => [],
"commands" => [],
];
+10 -1
View File
@@ -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
@@ -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<n>" - 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,
}
+8
View File
@@ -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",