diff --git a/homeMesh_automations.sql b/homeMesh_automations.sql index 2c0f783..f2e7ef9 100644 --- a/homeMesh_automations.sql +++ b/homeMesh_automations.sql @@ -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', `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', - `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', PRIMARY KEY (`id`), KEY `idx_cond_automation` (`automation_id`,`group_no`,`position`), diff --git a/restricted/autoActions/README.md b/restricted/autoActions/README.md index ffa11b3..1768c49 100644 --- a/restricted/autoActions/README.md +++ b/restricted/autoActions/README.md @@ -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 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 -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 -16:30" bleibt bis Mitternacht wahr, ausgelöst wird trotzdem nur einmal. Die -breiten Zeitfenster in `auto_watering.py` folgen derselben Überlegung. +Zeit-Auslöser gibt es in drei Formen: + +| | wahr, wenn | +|---|---| +| `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 das Fenster zugeht und in diesem Fenster noch nichts passiert ist. diff --git a/restricted/autoActions/autoaction_runner.py b/restricted/autoActions/autoaction_runner.py index 4543463..6bdaf6c 100644 --- a/restricted/autoActions/autoaction_runner.py +++ b/restricted/autoActions/autoaction_runner.py @@ -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 "Temperatur ueber 22 Grad" bei jedem Takt erneut feuern. -Zeit-Ausloeser heissen "ab 16:30" und nicht "gleich 16:30" - damit bleibt die -Bedingung bis Mitternacht wahr und ein verpasster Takt kostet nicht den -ganzen Tag. Ausgeloest wird trotzdem nur einmal, eben wegen der Flanke. -Dieselbe Ueberlegung steht hinter den breiten Zeitfenstern in -auto_watering.py. +Zeit-Ausloeser gibt es in drei Formen: "um 16:30" ist genau in dieser Minute +wahr, "ab 16:30" von da an bis Mitternacht, "vor 16:30" bis dahin. +Ausgeloest wird in allen drei Faellen nur einmal, eben wegen der Flanke - +"ab" ist aber das robustere: faellt der Runner in der einen Minute aus, auf +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. """ @@ -265,26 +272,35 @@ def bedingung_erfuellt(bedingung, state, wert, jetzt): if typ == "time": 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": # 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) - ziel = minuten(wert) + (versatz if op == "+" else -versatz) - return jetzt.hour * 60 + jetzt.minute >= ziel + ziel = minuten(wert) + (-versatz if op.endswith("-") else versatz) + # 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": - ist_d, soll_d = als_datum(wert), als_datum(soll) + if typ in ("date", "datetime"): + 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: return False - return ist_d >= soll_d if op == ">=" else ist_d < soll_d - - if typ == "datetime": - 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 op == "=": return ist_d == soll_d + if op == "<": return ist_d < soll_d + return ist_d >= soll_d if typ == "bool": ist_b = str(wert).strip().lower() in WAHR diff --git a/restricted/automations.php b/restricted/automations.php index 3e99906..a5f7d8e 100644 --- a/restricted/automations.php +++ b/restricted/automations.php @@ -34,12 +34,21 @@ function meshDb() * innerHTML ("≠", ">") an den Server, wo eine Whitelist mit "!=" stand: * jede Bedingung "ungleich" wurde still zu "gleich". * - * Bei Zeit und Datum gibt es bewusst kein "gleich", sondern "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 - * 16:30" bleibt bis Mitternacht wahr; ausgeloest wird trotzdem nur einmal, - * weil nur die steigende Flanke zaehlt. Dieselbe Ueberlegung steht hinter den - * breiten Zeitfenstern in auto_watering.py. + * Bei Zeit und Datum heissen die Operatoren "um", "ab" und "vor": + * + * um 16:30 genau in dieser Minute. Der uebliche Fall - einmal taeglich + * zu einem festen Zeitpunkt. + * ab 16:30 ab da bis Mitternacht wahr. Ausgeloest wird trotzdem nur + * 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) { @@ -50,9 +59,9 @@ function operatorsForType($type) case "time": case "date": case "datetime": - return [">=", "<"]; + return ["=", ">=", "<"]; case "deltatime": // Sonnenauf-/-untergang: Wert ist ein Versatz - return ["+", "-"]; + return ["+", "-", ">=+", ">=-", "<+", "<-"]; case "bool": return ["="]; // JA/NEIN steht im Wertfeld, nicht im Operator default: @@ -96,19 +105,29 @@ function inputForType($type, $hasOptions) } /** - * Anzeigeform eines Operators. Bei Zeitangaben liest sich "ab"/"vor" - * verstaendlicher als ">="/"<", und bei Sonnenauf-/-untergang steht der - * Versatz dahinter: "ab + 00:30" heisst eine halbe Stunde danach. + * Anzeigeform eines Operators. Bei Zeitangaben liest sich "um"/"ab"/"vor" + * verstaendlicher als "="/">="/"<", und beim Sonnenstand steht das + * Vorzeichen des Versatzes dahinter: "ab + 00:30" heisst ab einer halben + * Stunde nach Sonnenaufgang. */ 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 "vor"; } if ($type === "deltatime") { - if ($op === "+") return "ab +"; - if ($op === "-") return "ab -"; + $beschriftung = ["+" => "+", "-" => "-", ">=+" => "ab +", ">=-" => "ab -", + "<+" => "vor +", "<-" => "vor -"]; + if (isset($beschriftung[$op])) { + return $beschriftung[$op]; + } } return $op === "!=" ? "≠" : $op; } @@ -409,7 +428,8 @@ function saveAutomation($data) if ($stateId <= 0) { throw new InvalidArgumentException("Eine Bedingung hat keinen Messwert."); } - if (!in_array($operator, ["=", "!=", ">", "<", ">=", "<=", "+", "-"], true)) { + if (!in_array($operator, ["=", "!=", ">", "<", ">=", "<=", + "+", "-", ">=+", ">=-", "<+", "<-"], true)) { throw new InvalidArgumentException("Unbekannter Operator: " . $operator); } $positions[$group] = ($positions[$group] ?? -1) + 1;