Compare commits
2
Commits
77651a860a
...
128acae48e
| 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
|
* Beim Anlegen einer Automatik denkt man in Raeumen ("im Bad soll ..."), eine
|
||||||
* in Geraetearten. Der Raum steht aber nur da, wo er von Hand vergeben wurde
|
* Ueberschrift je Raum waeren aber zwanzig Stueck - in einer Liste, die
|
||||||
* (siehe Zuordnung ueber das Zahnrad in der Karte "Automatismen"); solange
|
* fuenfzehn Zeilen hoch ist, saehe man dann mehr Ueberschrift als Geraet, und
|
||||||
* das niemand getan hat, bleibt die Gruppierung nach Art - besser als eine
|
* "OG · Bad", "EG · Bad", "UG · Bad" schrieben die Etage immer wieder
|
||||||
* einzige Gruppe "Nicht zugeordnet".
|
* 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) {
|
function geraeteGruppen(geraete) {
|
||||||
if (!geraete.some(d => d.room)) {
|
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(),
|
const etagen = etagenReihenfolge();
|
||||||
"Nicht zugeordnet", false);
|
// "Ohne Raum" und "Unbekannter Raum" stehen in keiner Raumliste. Ihr
|
||||||
// Was noch keinen Raum hat, bleibt sonst ein einziger Eimer - solange erst
|
// indexOf() waere -1 und damit ganz vorne; sie gehoeren aber ans Ende.
|
||||||
// ein Teil zugeordnet ist, waeren das die meisten Geraete. Innerhalb des
|
const platz = etage => etagen.includes(etage) ? etagen.indexOf(etage) : etagen.length;
|
||||||
// Eimers gilt deshalb weiter die Ordnung nach Geraeteart.
|
const nach = new Map();
|
||||||
const letzte = gruppen[gruppen.length - 1];
|
geraete.forEach(d => {
|
||||||
if (!letzte || !letzte.titel.startsWith("Nicht zugeordnet")) return gruppen;
|
if (!nach.has(d.floorLabel)) nach.set(d.floorLabel, []);
|
||||||
gruppen.pop();
|
nach.get(d.floorLabel).push(d);
|
||||||
return gruppen.concat(
|
});
|
||||||
gruppiere(letzte.geraete, d => d.type, null, "Einzelne", true)
|
return Array.from(nach.keys())
|
||||||
.map(g => ({ titel: "Ohne Raum · " + g.titel, geraete: g.geraete })));
|
.sort((a, b) => platz(a) - platz(b) || a.localeCompare(b, "de"))
|
||||||
}
|
.map(etage => {
|
||||||
|
const liste = nach.get(etage);
|
||||||
/** Reihenfolge der Raum-Ueberschriften, wie in rooms.php notiert. */
|
liste.sort((a, b) => raumIndex(a.room) - raumIndex(b.room)
|
||||||
function raumReihenfolge() {
|
|| a.type.localeCompare(b.type, "de")
|
||||||
return autoRooms.map(r => r.label).concat(["Nicht zugeordnet"]);
|
|| a.label.localeCompare(b.label, "de"));
|
||||||
|
return { titel: etage + " (" + liste.length + ")", geraete: liste };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nach einem Merkmal gruppieren.
|
* Reihenfolge der Etagen-Ueberschriften, wie rooms.php sie notiert - erst OG,
|
||||||
*
|
* dann EG, dann UG, dann Aussen.
|
||||||
* `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.
|
|
||||||
*/
|
*/
|
||||||
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();
|
const nach = new Map();
|
||||||
geraete.forEach(d => {
|
geraete.forEach(d => {
|
||||||
const schluessel = merkmal(d) || restTitel;
|
if (!nach.has(d.type)) nach.set(d.type, []);
|
||||||
if (!nach.has(schluessel)) nach.set(schluessel, []);
|
nach.get(d.type).push(d);
|
||||||
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");
|
|
||||||
});
|
});
|
||||||
const gruppen = [];
|
const gruppen = [];
|
||||||
const rest = [];
|
const rest = [];
|
||||||
titel.forEach(t => {
|
Array.from(nach.keys()).sort((a, b) => a.localeCompare(b, "de")).forEach(art => {
|
||||||
const liste = nach.get(t);
|
const liste = nach.get(art);
|
||||||
if (!einzelneSammeln || liste.length > 1 || t === restTitel) {
|
if (liste.length > 1) gruppen.push({ titel: art + " (" + liste.length + ")", geraete: liste });
|
||||||
gruppen.push({ titel: t + " (" + liste.length + ")", geraete: liste });
|
else rest.push(liste[0]);
|
||||||
} else {
|
|
||||||
rest.push(liste[0]);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
if (rest.length) {
|
if (rest.length) {
|
||||||
rest.sort((a, b) => a.label.localeCompare(b.label, "de"));
|
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;
|
return gruppen;
|
||||||
}
|
}
|
||||||
@@ -158,9 +199,10 @@ function gruppiere(geraete, merkmal, reihenfolge, restTitel, einzelneSammeln) {
|
|||||||
* Geraeteauswahl mit Suchfeld.
|
* Geraeteauswahl mit Suchfeld.
|
||||||
*
|
*
|
||||||
* Bei ueber fuenfzig Geraeten hilft Tippen mehr als Blaettern - und es hilft
|
* Bei ueber fuenfzig Geraeten hilft Tippen mehr als Blaettern - und es hilft
|
||||||
* quer zur Gruppierung: "bad" findet das Thermostat im Bad und die Jalousie
|
* quer zur Gruppierung: "bad" findet das Thermostat im Bad, die Jalousie
|
||||||
* "Bad Links", "jalousie" findet alle elf auf einmal. Gesucht wird deshalb
|
* "Bad Links" und alles andere, was einem Bad zugeordnet ist, "jalousie"
|
||||||
* ueber Beschriftung und Geraeteart zugleich.
|
* 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
|
* 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
|
* 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");
|
const suche = document.createElement("input");
|
||||||
suche.type = "search";
|
suche.type = "search";
|
||||||
suche.className = "form-control form-control-sm mb-2";
|
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");
|
const liste = document.createElement("div");
|
||||||
liste.className = "list-group list-group-flush";
|
liste.className = "list-group list-group-flush";
|
||||||
liste.style.maxHeight = "15rem";
|
liste.style.maxHeight = "15rem";
|
||||||
@@ -206,13 +248,18 @@ function oeffneWaehler(knopf, geraete, aktuelleId, beiAuswahl) {
|
|||||||
// input-group stuende die Liste sonst als weitere Spalte daneben.
|
// input-group stuende die Liste sonst als weitere Spalte daneben.
|
||||||
knopf.closest(".input-group").after(panel);
|
knopf.closest(".input-group").after(panel);
|
||||||
|
|
||||||
const eintrag = (d) => {
|
const eintrag = (d, mitEtage) => {
|
||||||
const zeile = document.createElement("button");
|
const zeile = document.createElement("button");
|
||||||
zeile.type = "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"
|
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" : "");
|
+ (d.id === aktuelleId ? " active" : "");
|
||||||
zeile.innerHTML = "<span>" + textSicher(d.label) + "</span>"
|
zeile.innerHTML = "<span class='text-truncate'>" + textSicher(d.label) + "</span>"
|
||||||
+ "<small class='ms-2 opacity-75'>" + textSicher(d.type) + "</small>";
|
+ "<small class='opacity-75 text-nowrap'>"
|
||||||
|
+ textSicher(zeilenZusatz(d, mitEtage)) + "</small>";
|
||||||
zeile.onclick = () => { schliesseWaehler(); beiAuswahl(d); };
|
zeile.onclick = () => { schliesseWaehler(); beiAuswahl(d); };
|
||||||
return zeile;
|
return zeile;
|
||||||
};
|
};
|
||||||
@@ -220,17 +267,16 @@ function oeffneWaehler(knopf, geraete, aktuelleId, beiAuswahl) {
|
|||||||
const zeichne = () => {
|
const zeichne = () => {
|
||||||
const suchtext = suche.value.trim().toLowerCase();
|
const suchtext = suche.value.trim().toLowerCase();
|
||||||
liste.innerHTML = "";
|
liste.innerHTML = "";
|
||||||
const passend = geraete.filter(d => !suchtext
|
const passend = geraete.filter(d => !suchtext || suchfeld(d).includes(suchtext));
|
||||||
|| d.label.toLowerCase().includes(suchtext)
|
|
||||||
|| d.type.toLowerCase().includes(suchtext));
|
|
||||||
if (!passend.length) {
|
if (!passend.length) {
|
||||||
liste.innerHTML = "<div class='p-2 small text-body-secondary'>Nichts gefunden.</div>";
|
liste.innerHTML = "<div class='p-2 small text-body-secondary'>Nichts gefunden.</div>";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Beim Suchen die Zwischenueberschriften weglassen: die Treffer stehen
|
// 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) {
|
if (suchtext) {
|
||||||
passend.forEach(d => liste.appendChild(eintrag(d)));
|
passend.forEach(d => liste.appendChild(eintrag(d, true)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
geraeteGruppen(passend).forEach(g => {
|
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.className = "px-2 pt-2 pb-1 small fw-bold text-body-secondary";
|
||||||
kopf.textContent = g.titel;
|
kopf.textContent = g.titel;
|
||||||
liste.appendChild(kopf);
|
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
|
* Einzige Quelle bleibt restricted/rooms.php - ein neuer Raum ist dort eine
|
||||||
* Zeile und taucht hier automatisch auf. Der Schluessel ist "Etage/Raum",
|
* Zeile und taucht hier automatisch auf. Der Schluessel ist "Etage/Raum",
|
||||||
* weil "Bad" allein auf drei Etagen vorkommt.
|
* 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()
|
function raumListe()
|
||||||
{
|
{
|
||||||
@@ -233,9 +239,15 @@ function raumListe()
|
|||||||
$liste[] = [
|
$liste[] = [
|
||||||
"key" => $raum["floor"] . "/" . $raum["mqtt"],
|
"key" => $raum["floor"] . "/" . $raum["mqtt"],
|
||||||
"label" => $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;
|
return $liste;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,6 +267,28 @@ function raumBeschriftung($key)
|
|||||||
return $key . " (unbekannt)";
|
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.
|
* Raumvorschlaege aus den MQTT-Topics.
|
||||||
*
|
*
|
||||||
@@ -377,6 +411,7 @@ function deviceCatalog()
|
|||||||
return strcoll($labels[intval($a["id"])], $labels[intval($b["id"])]);
|
return strcoll($labels[intval($a["id"])], $labels[intval($b["id"])]);
|
||||||
});
|
});
|
||||||
foreach ($zeilen as $row) {
|
foreach ($zeilen as $row) {
|
||||||
|
$wo = raumEinordnung($row["room"]);
|
||||||
$devices[intval($row["id"])] = [
|
$devices[intval($row["id"])] = [
|
||||||
"id" => intval($row["id"]),
|
"id" => intval($row["id"]),
|
||||||
"name" => $row["name"],
|
"name" => $row["name"],
|
||||||
@@ -385,6 +420,10 @@ function deviceCatalog()
|
|||||||
"url" => $row["url"],
|
"url" => $row["url"],
|
||||||
"room" => $row["room"],
|
"room" => $row["room"],
|
||||||
"roomLabel" => raumBeschriftung($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" => [],
|
"states" => [],
|
||||||
"commands" => [],
|
"commands" => [],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from modules.tahoma_module import TahomaModule
|
|||||||
from modules.wled_module import WLEDModule
|
from modules.wled_module import WLEDModule
|
||||||
from modules.mqtt_module import MQTTModule
|
from modules.mqtt_module import MQTTModule
|
||||||
from modules.shelly_module import ShellyModule
|
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!!
|
from modules.logic_module import LogicModule #nicht Vergessen neue Module auch in Zeile 546 einzufügen!!
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
@@ -150,6 +151,13 @@ class Config:
|
|||||||
return False
|
return False
|
||||||
return self._get_bool('logic', 'enable', 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
|
# MQTT
|
||||||
@property
|
@property
|
||||||
@@ -561,7 +569,8 @@ def main():
|
|||||||
WLEDModule(config),
|
WLEDModule(config),
|
||||||
MQTTModule(config),
|
MQTTModule(config),
|
||||||
ShellyModule(config),
|
ShellyModule(config),
|
||||||
LogicModule(config)
|
LogicModule(config),
|
||||||
|
GartenwasserModule(config)
|
||||||
]
|
]
|
||||||
|
|
||||||
# Jedes Modul durchlaufen
|
# 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",
|
"symbol" => "bi-diagram-3",
|
||||||
"hinweis" => "Wertet die Automatiken aus und schickt die Kommandos.",
|
"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" => [
|
"wattpilot" => [
|
||||||
"titel" => "Wattpilot",
|
"titel" => "Wattpilot",
|
||||||
"datei" => "wattpilotshell.log",
|
"datei" => "wattpilotshell.log",
|
||||||
|
|||||||
Reference in New Issue
Block a user