Zeit-Ausloeser: "um" ergaenzt, Sonnenstand vervollstaendigt

Bei Uhrzeit und Datum gab es nur "ab" und "vor". Der haeufigste Fall - eine
Aktion genau um 16:30 - liess sich damit gar nicht ausdruecken. "um" ist
jetzt der erste Eintrag und damit die Vorbelegung.

Der Hinweis von frueher bleibt trotzdem richtig und steht in der README:
"um" trifft nur eine einzige Minute, faellt der Runner ausgerechnet in
dieser Minute aus, ist die Automatik fuer den Tag verloren. "ab" holt der
naechste Takt nach. Wer beides will, hakt force_once an.

Beim Sonnenauf- und -untergang fehlte die halbe Tabelle: der Versatz kann
davor oder danach liegen, und verglichen werden kann davor, danach oder
genau. Statt zwei gibt es jetzt sechs Operatoren - "+", "-", "ab +",
"ab -", "vor +", "vor -". Das Vorzeichen steckt im Operator, weil der
Editor eine einzige Auswahlliste zeigt und nicht zwei Bedienelemente fuer
eine Angabe.

Rutscht ein Versatz rechnerisch ueber den Tagesrand (Sonnenaufgang 05:34
minus sechs Stunden), bleibt es beim Tagesrand statt auf die andere Seite
von Mitternacht zu springen - "kurz vor Sonnenaufgang" soll nicht ploetzlich
gestern abend bedeuten.

Der Editor brauchte keine Aenderung: er liest Wert und Beschriftung der
Operatoren aus dem Geraetekatalog, statt sie selbst zu kennen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-31 12:30:31 +02:00
co-authored by Claude Opus 5
parent 5f643b9844
commit ee417f302f
4 changed files with 91 additions and 39 deletions
+1 -1
View File
@@ -65,7 +65,7 @@ CREATE TABLE IF NOT EXISTS `automation_conditions` (
`group_no` int(11) NOT NULL DEFAULT 0 COMMENT 'UND-Block. Gleiche Nummer = UND, verschiedene Nummern = ODER', `group_no` int(11) NOT NULL DEFAULT 0 COMMENT 'UND-Block. Gleiche Nummer = UND, verschiedene Nummern = ODER',
`position` int(11) NOT NULL DEFAULT 0 COMMENT 'Reihenfolge innerhalb des Blocks, nur Darstellung', `position` int(11) NOT NULL DEFAULT 0 COMMENT 'Reihenfolge innerhalb des Blocks, nur Darstellung',
`state_id` int(11) NOT NULL COMMENT 'Referenz auf actor_states: Geraet und Messwert in einem', `state_id` int(11) NOT NULL COMMENT 'Referenz auf actor_states: Geraet und Messwert in einem',
`operator` enum('=','!=','>','<','>=','<=','+','-') NOT NULL DEFAULT '=' COMMENT 'Immer ASCII. Die Knoepfe im Editor zeigen Sonderzeichen und werden beim Speichern normalisiert', `operator` enum('=','!=','>','<','>=','<=','+','-','>=+','>=-','<+','<-') NOT NULL DEFAULT '=' COMMENT 'Immer ASCII, die Beschriftung macht erst die Anzeige daraus. Bei Zeit und Datum: = um, >= ab, < vor. Beim Sonnenstand traegt der Operator zusaetzlich das Vorzeichen des Versatzes: + genau danach, >=- ab davor, <+ bis danach',
`value` varchar(255) NOT NULL DEFAULT '' COMMENT 'Schwelle als Text, der Datentyp steckt im Messwert', `value` varchar(255) NOT NULL DEFAULT '' COMMENT 'Schwelle als Text, der Datentyp steckt im Messwert',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_cond_automation` (`automation_id`,`group_no`,`position`), KEY `idx_cond_automation` (`automation_id`,`group_no`,`position`),
+21 -5
View File
@@ -51,11 +51,27 @@ erfüllt war. Ohne das würde „Temperatur über 22 Grad" bei jedem Takt erneut
feuern. Verlässt die Automatik ihr Zeitfenster, wird die Flanke feuern. Verlässt die Automatik ihr Zeitfenster, wird die Flanke
zurückgesetzt, damit sie im nächsten Fenster wieder steigen kann. zurückgesetzt, damit sie im nächsten Fenster wieder steigen kann.
Aus demselben Grund heißen Zeit-Auslöser **„ab 16:30"** und nicht „gleich Zeit-Auslöser gibt es in drei Formen:
16:30". Ein Gleichheitsvergleich wäre nur in genau einer Minute wahr — fällt
der Runner in dieser Minute aus, ist die Automatik für den Tag verloren. „Ab | | wahr, wenn |
16:30" bleibt bis Mitternacht wahr, ausgelöst wird trotzdem nur einmal. Die |---|---|
breiten Zeitfenster in `auto_watering.py` folgen derselben Überlegung. | `um 16:30` | genau in dieser Minute |
| `ab 16:30` | von da an bis Mitternacht |
| `vor 16:30` | bis dahin |
Ausgelöst wird in allen drei Fällen nur einmal, eben wegen der Flanke. `ab`
ist trotzdem das robustere: fällt der Runner in genau der Minute aus, auf die
`um` zeigt, ist die Automatik für den Tag verloren — bei `ab` holt der nächste
Takt es nach. Wer `um` braucht und den Ausfall nicht riskieren will, hakt
zusätzlich `force_once` an. Die breiten Zeitfenster in `auto_watering.py`
folgen derselben Überlegung.
Beim Sonnenauf- und -untergang ist der Wert ein Versatz, und der kann davor
oder danach liegen — deshalb dieselben drei Fälle mal zwei: `+ 00:30` eine
halbe Stunde nach Sonnenaufgang, `ab - 00:30` ab einer halben Stunde davor,
`vor + 00:30` bis eine halbe Stunde danach. Rutscht ein Versatz rechnerisch
über den Tagesrand, bleibt es beim Tagesrand — „kurz vor Sonnenaufgang" soll
nicht plötzlich gestern Abend bedeuten.
`force_once` („am Ende des Zeitraums auf jeden Fall ausführen") greift, wenn `force_once` („am Ende des Zeitraums auf jeden Fall ausführen") greift, wenn
das Fenster zugeht und in diesem Fenster noch nichts passiert ist. das Fenster zugeht und in diesem Fenster noch nichts passiert ist.
+34 -18
View File
@@ -26,11 +26,18 @@ Nur die steigende Flanke loest aus: `automations.cond_met` haelt fest, ob die
Bedingung beim letzten Durchlauf schon erfuellt war. Ohne das wuerde Bedingung beim letzten Durchlauf schon erfuellt war. Ohne das wuerde
"Temperatur ueber 22 Grad" bei jedem Takt erneut feuern. "Temperatur ueber 22 Grad" bei jedem Takt erneut feuern.
Zeit-Ausloeser heissen "ab 16:30" und nicht "gleich 16:30" - damit bleibt die Zeit-Ausloeser gibt es in drei Formen: "um 16:30" ist genau in dieser Minute
Bedingung bis Mitternacht wahr und ein verpasster Takt kostet nicht den wahr, "ab 16:30" von da an bis Mitternacht, "vor 16:30" bis dahin.
ganzen Tag. Ausgeloest wird trotzdem nur einmal, eben wegen der Flanke. Ausgeloest wird in allen drei Faellen nur einmal, eben wegen der Flanke -
Dieselbe Ueberlegung steht hinter den breiten Zeitfenstern in "ab" ist aber das robustere: faellt der Runner in der einen Minute aus, auf
auto_watering.py. die "um" zeigt, ist die Automatik fuer den Tag verloren; bei "ab" holt der
naechste Takt es nach. Wer "um" braucht und den Ausfall nicht riskieren
will, hakt zusaetzlich force_once an. Dieselbe Ueberlegung steht hinter den
breiten Zeitfenstern in auto_watering.py.
Beim Sonnenauf- und -untergang traegt der Operator zusaetzlich das
Vorzeichen des Versatzes: "+ 00:30" eine halbe Stunde danach, ">=- 00:30"
ab einer halben Stunde davor, "<+ 00:30" bis eine halbe Stunde danach.
Tabellen siehe homeMesh_automations.sql, Konfiguration siehe config.ini.example. Tabellen siehe homeMesh_automations.sql, Konfiguration siehe config.ini.example.
""" """
@@ -265,26 +272,35 @@ def bedingung_erfuellt(bedingung, state, wert, jetzt):
if typ == "time": if typ == "time":
ist_m, soll_m = minuten(wert), minuten(soll) ist_m, soll_m = minuten(wert), minuten(soll)
return ist_m >= soll_m if op == ">=" else ist_m < soll_m if op == "=": return ist_m == soll_m
if op == "<": return ist_m < soll_m
return ist_m >= soll_m
if typ == "deltatime": if typ == "deltatime":
# Der Messwert ist der Sonnenauf- bzw. -untergang, die Schwelle # Der Messwert ist der Sonnenauf- bzw. -untergang, die Schwelle
# ein Versatz davor oder danach. Verglichen wird gegen die Uhr. # ein Versatz. Das Vorzeichen steckt im Operator, der Vergleich
# davor: ">=-" heisst "ab einer halben Stunde davor".
versatz = minuten(soll) versatz = minuten(soll)
ziel = minuten(wert) + (versatz if op == "+" else -versatz) ziel = minuten(wert) + (-versatz if op.endswith("-") else versatz)
return jetzt.hour * 60 + jetzt.minute >= ziel # Ein Versatz kann rechnerisch ueber den Tagesrand rutschen
# (Sonnenaufgang 05:34 minus sechs Stunden). Dann bleibt es beim
# Tagesrand, statt auf die andere Seite von Mitternacht zu
# springen - "kurz vor Sonnenaufgang" soll nicht ploetzlich
# gestern abend bedeuten.
ziel = max(0, min(1439, ziel))
jetzt_m = jetzt.hour * 60 + jetzt.minute
if op.startswith(">="): return jetzt_m >= ziel
if op.startswith("<"): return jetzt_m < ziel
return jetzt_m == ziel
if typ == "date": if typ in ("date", "datetime"):
ist_d, soll_d = als_datum(wert), als_datum(soll) wandeln = als_datum if typ == "date" else als_zeitpunkt
ist_d, soll_d = wandeln(wert), wandeln(soll)
if ist_d is None or soll_d is None: if ist_d is None or soll_d is None:
return False return False
return ist_d >= soll_d if op == ">=" else ist_d < soll_d if op == "=": return ist_d == soll_d
if op == "<": return ist_d < soll_d
if typ == "datetime": return ist_d >= soll_d
ist_d, soll_d = als_zeitpunkt(wert), als_zeitpunkt(soll)
if ist_d is None or soll_d is None:
return False
return ist_d >= soll_d if op == ">=" else ist_d < soll_d
if typ == "bool": if typ == "bool":
ist_b = str(wert).strip().lower() in WAHR ist_b = str(wert).strip().lower() in WAHR
+35 -15
View File
@@ -34,12 +34,21 @@ function meshDb()
* innerHTML ("", "&gt;") an den Server, wo eine Whitelist mit "!=" stand: * innerHTML ("", "&gt;") an den Server, wo eine Whitelist mit "!=" stand:
* jede Bedingung "ungleich" wurde still zu "gleich". * jede Bedingung "ungleich" wurde still zu "gleich".
* *
* Bei Zeit und Datum gibt es bewusst kein "gleich", sondern "ab" und "vor". * Bei Zeit und Datum heissen die Operatoren "um", "ab" und "vor":
* "Uhrzeit = 16:30" waere nur in genau der einen Minute wahr - faellt der *
* Runner in dieser Minute aus, ist die Automatik fuer den Tag verloren. "ab * um 16:30 genau in dieser Minute. Der uebliche Fall - einmal taeglich
* 16:30" bleibt bis Mitternacht wahr; ausgeloest wird trotzdem nur einmal, * zu einem festen Zeitpunkt.
* weil nur die steigende Flanke zaehlt. Dieselbe Ueberlegung steht hinter den * ab 16:30 ab da bis Mitternacht wahr. Ausgeloest wird trotzdem nur
* breiten Zeitfenstern in auto_watering.py. * einmal, weil nur die steigende Flanke zaehlt; dafuer kostet
* ein verpasster Takt nicht den ganzen Tag. Dieselbe
* Ueberlegung steht hinter den breiten Zeitfenstern in
* auto_watering.py.
* vor 16:30 bis dahin wahr, danach nicht mehr.
*
* Beim Sonnenauf- und -untergang ist der Wert ein Versatz, der davor oder
* danach liegen kann - deshalb dieselben drei Faelle noch einmal mal zwei:
* "+ 00:30" eine halbe Stunde nach Sonnenaufgang, "ab - 00:30" ab einer
* halben Stunde davor, "vor + 00:30" bis eine halbe Stunde danach.
*/ */
function operatorsForType($type) function operatorsForType($type)
{ {
@@ -50,9 +59,9 @@ function operatorsForType($type)
case "time": case "time":
case "date": case "date":
case "datetime": case "datetime":
return [">=", "<"]; return ["=", ">=", "<"];
case "deltatime": // Sonnenauf-/-untergang: Wert ist ein Versatz case "deltatime": // Sonnenauf-/-untergang: Wert ist ein Versatz
return ["+", "-"]; return ["+", "-", ">=+", ">=-", "<+", "<-"];
case "bool": case "bool":
return ["="]; // JA/NEIN steht im Wertfeld, nicht im Operator return ["="]; // JA/NEIN steht im Wertfeld, nicht im Operator
default: default:
@@ -96,19 +105,29 @@ function inputForType($type, $hasOptions)
} }
/** /**
* Anzeigeform eines Operators. Bei Zeitangaben liest sich "ab"/"vor" * Anzeigeform eines Operators. Bei Zeitangaben liest sich "um"/"ab"/"vor"
* verstaendlicher als ">="/"<", und bei Sonnenauf-/-untergang steht der * verstaendlicher als "="/">="/"<", und beim Sonnenstand steht das
* Versatz dahinter: "ab + 00:30" heisst eine halbe Stunde danach. * Vorzeichen des Versatzes dahinter: "ab + 00:30" heisst ab einer halben
* Stunde nach Sonnenaufgang.
*/ */
function operatorLabel($op, $type = "") function operatorLabel($op, $type = "")
{ {
if ($type === "time" || $type === "date" || $type === "datetime") { if ($type === "time" || $type === "datetime") {
if ($op === "=") return "um";
if ($op === ">=") return "ab";
if ($op === "<") return "vor";
}
if ($type === "date") {
if ($op === "=") return "am";
if ($op === ">=") return "ab"; if ($op === ">=") return "ab";
if ($op === "<") return "vor"; if ($op === "<") return "vor";
} }
if ($type === "deltatime") { if ($type === "deltatime") {
if ($op === "+") return "ab +"; $beschriftung = ["+" => "+", "-" => "-", ">=+" => "ab +", ">=-" => "ab -",
if ($op === "-") return "ab -"; "<+" => "vor +", "<-" => "vor -"];
if (isset($beschriftung[$op])) {
return $beschriftung[$op];
}
} }
return $op === "!=" ? "" : $op; return $op === "!=" ? "" : $op;
} }
@@ -409,7 +428,8 @@ function saveAutomation($data)
if ($stateId <= 0) { if ($stateId <= 0) {
throw new InvalidArgumentException("Eine Bedingung hat keinen Messwert."); throw new InvalidArgumentException("Eine Bedingung hat keinen Messwert.");
} }
if (!in_array($operator, ["=", "!=", ">", "<", ">=", "<=", "+", "-"], true)) { if (!in_array($operator, ["=", "!=", ">", "<", ">=", "<=",
"+", "-", ">=+", ">=-", "<+", "<-"], true)) {
throw new InvalidArgumentException("Unbekannter Operator: " . $operator); throw new InvalidArgumentException("Unbekannter Operator: " . $operator);
} }
$positions[$group] = ($positions[$group] ?? -1) + 1; $positions[$group] = ($positions[$group] ?? -1) + 1;