Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
128acae48e | ||
|
|
37962224c1 |
+110
-64
@@ -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)));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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" => [],
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user