diff --git a/ajax/AutoAction.php b/ajax/AutoAction.php index 9875d12..fcbcf38 100644 --- a/ajax/AutoAction.php +++ b/ajax/AutoAction.php @@ -93,6 +93,12 @@ if ($action === "list") { if (!$a["on_vacation"]) { $wann .= ", nicht in den Ferien"; } + if ($a["lockout_secs"] > 0) { + $wann .= ", frühestens alle " + . ($a["lockout_secs"] >= 60 + ? intdiv($a["lockout_secs"], 60) . " Min." + : $a["lockout_secs"] . " Sek."); + } $zuletzt = $a["last_run"] ? date("d.m. H:i", strtotime($a["last_run"])) : "–"; $klasse = $a["enabled"] ? "" : " class='opacity-50'"; @@ -134,6 +140,7 @@ if (!$auto) { $nutzdaten = json_encode([ "automation" => $auto, "devices" => deviceCatalog(), + "lockouts" => lockoutChoices(), ], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); ?> @@ -212,6 +219,10 @@ $nutzdaten = json_encode([ +
+ Sperrzeit nach dem Auslösen + +
diff --git a/homeMesh_automations.sql b/homeMesh_automations.sql index f2e7ef9..dca92a2 100644 --- a/homeMesh_automations.sql +++ b/homeMesh_automations.sql @@ -37,6 +37,7 @@ CREATE TABLE IF NOT EXISTS `automations` ( `on_vacation` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Auch in den Ferien ausfuehren', `on_holiday` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Auch an Feiertagen ausfuehren', `force_once` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Am Ende des Zeitraums auf jeden Fall ausfuehren', + `lockout_secs` int(11) NOT NULL DEFAULT 60 COMMENT 'Sperrzeit: so lange nach einer Ausloesung wird nicht erneut geschaltet. Gegen Messwerte, die um die Schwelle pendeln - jedes Ueberschreiten waere sonst eine echte Flanke. Der Editor bietet 0, 60 und 900 an', `cond_met` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'War die Bedingung beim letzten Durchlauf erfuellt? Nur die steigende Flanke loest aus, sonst wuerde Temperatur groesser 22 im Sekundentakt feuern', `last_run` datetime DEFAULT NULL COMMENT 'Zuletzt ausgeloest, NULL = noch nie', `changed` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT 'Signal an den Runner, das Regelwerk neu zu laden', diff --git a/js/solar/autoActionFuncs.js b/js/solar/autoActionFuncs.js index 2414c09..81b8523 100644 --- a/js/solar/autoActionFuncs.js +++ b/js/solar/autoActionFuncs.js @@ -19,6 +19,7 @@ let autoModel = null; let autoDevices = []; +let autoLockouts = []; // --- Katalog-Helfer ------------------------------------------------------ @@ -368,6 +369,9 @@ function renderWeekdays() { } function fuelleFormular() { + const sperre = document.getElementById("lockoutSecs"); + optionListe(sperre, autoLockouts.map(o => ({ value: o.value, text: o.label })), + autoModel.lockout_secs); document.getElementById("autoName").value = autoModel.name; document.getElementById("autoFloor").value = autoModel.floor; document.getElementById("tSpanFrom").value = autoModel.window_from; @@ -389,6 +393,7 @@ function leseFormular() { autoModel.on_vacation = document.getElementById("actFerien").checked; autoModel.on_holiday = document.getElementById("actFeier").checked; autoModel.force_once = document.getElementById("runOnce").checked; + autoModel.lockout_secs = Number(document.getElementById("lockoutSecs").value); } // --- Modal --------------------------------------------------------------- @@ -396,6 +401,7 @@ function leseFormular() { function loadAutomatic() { const daten = JSON.parse(document.getElementById("autoActionData").textContent); autoDevices = daten.devices; + autoLockouts = daten.lockouts; const a = daten.automation; autoModel = { @@ -409,6 +415,7 @@ function loadAutomatic() { on_vacation: a.on_vacation, on_holiday: a.on_holiday, force_once: a.force_once, + lockout_secs: a.lockout_secs, groups: [], actions: a.actions.map(x => ({ command_id: x.command_id, params: Object.assign({}, x.params) })) }; @@ -501,6 +508,7 @@ function submitAutoAction() { on_vacation: autoModel.on_vacation, on_holiday: autoModel.on_holiday, force_once: autoModel.force_once, + lockout_secs: autoModel.lockout_secs, conditions: [], actions: autoModel.actions }; diff --git a/restricted/autoActions/README.md b/restricted/autoActions/README.md index 8418f51..24a1eba 100644 --- a/restricted/autoActions/README.md +++ b/restricted/autoActions/README.md @@ -81,6 +81,35 @@ und `vor` verschiebt sich der wahre Bereich entsprechend mit. `force_once` („am Ende des Zeitraums auf jeden Fall ausführen") greift, wenn das Fenster zugeht und in diesem Fenster noch nichts passiert ist. +## Sperrzeit + +Die Flanke allein schützt nicht gegen einen Messwert, der um die Schwelle +**pendelt**: „Temperatur > 22" bei 22,1 / 21,9 / 22,1 °C ist jedes Mal eine +echte steigende Flanke, und über MQTT können die Werte im Sekundentakt +hereinkommen. `automations.lockout_secs` sagt, wie lange nach einer Auslösung +nicht wieder geschaltet wird. Der Editor bietet drei Stufen an: + +| | | gedacht für | +|---|---|---| +| Ohne | 0 s | volle Geschwindigkeit, jede Flanke schaltet | +| Kurz | 60 s | Licht, Farbe, Dimmwert | +| Lang | 900 s | Rollläden, Ventile, alles mit Motor | + +Gespeichert werden Sekunden, angeboten werden nur die drei Stufen — eine +vierte ist damit eine Zeile in `lockoutChoices()` und keine Wanderung durch +die Datenbank. + +Eine Flanke innerhalb der Sperrzeit wird **verworfen, nicht aufgehoben**. Ein +Rollladen, der eine Viertelstunde später doch noch losfährt, weil vor langer +Zeit einmal eine Schwelle gestreift wurde, wäre unangenehmer als einer, der +gar nicht fährt — und der nächste echte Anlass nach Ablauf der Sperre kommt +ohnehin durch. Verworfene Flanken stehen im Log auf `DEBUG`, nicht in +`automation_log`; bei einem zappelnden Sensor wäre die Tabelle sonst voll +davon. + +`force_once` ist von der Sperre nicht betroffen: es greift nur, wenn im +Fenster gar nichts gelaufen ist — dann ist auch keine Sperre aktiv. + ## Transporte Welcher Weg zum Gerät führt, entscheidet die URL des Aktors in `actors`: diff --git a/restricted/autoActions/autoaction_runner.py b/restricted/autoActions/autoaction_runner.py index 2121790..186282d 100644 --- a/restricted/autoActions/autoaction_runner.py +++ b/restricted/autoActions/autoaction_runner.py @@ -575,7 +575,11 @@ class Runner: self.lief_im_fenster[automatik["id"]] = False erfuellt = gruppen_erfuellt(automatik, self.regelwerk, self.werte, jetzt) if erfuellt and not automatik["cond_met"]: - self.ausloesen(automatik, "fired") + if self.gesperrt(automatik, jetzt): + logger.debug("%s: Flanke faellt in die Sperrzeit, uebersprungen", + automatik["name"]) + else: + self.ausloesen(automatik, "fired") self.flanke_merken(automatik, erfuellt) else: # Das Fenster ist gerade zugegangen. Wer "auf jeden Fall" @@ -595,6 +599,30 @@ class Runner: self.war_aktiv[automatik["id"]] = aktiv + @staticmethod + def gesperrt(automatik, jetzt): + """ + Liegt die letzte Ausloesung noch innerhalb der Sperrzeit? + + Gegen Messwerte, die um die Schwelle pendeln: "Temperatur > 22" bei + 22,1 / 21,9 / 22,1 Grad ist jedes Mal eine echte steigende Flanke, und + ueber MQTT koennen die Werte im Sekundentakt hereinkommen. + + Die Flanke wird dabei verworfen und nicht aufgehoben. Ein Rollladen, + der eine Viertelstunde spaeter doch noch losfaehrt, weil vor langer + Zeit einmal eine Schwelle gestreift wurde, waere unangenehmer als + einer, der gar nicht faehrt. Der naechste echte Anlass nach Ablauf + der Sperre kommt ohnehin durch. + + force_once ist davon nicht betroffen: es greift nur, wenn im Fenster + gar nichts gelaufen ist - dann ist auch keine Sperre aktiv. + """ + sperre = int(automatik.get("lockout_secs") or 0) + letzter = automatik.get("last_run") + if not sperre or not letzter: + return False + return (jetzt - letzter).total_seconds() < sperre + @staticmethod def lief_heute(automatik, jetzt): letzter = automatik.get("last_run") diff --git a/restricted/automations.php b/restricted/automations.php index a5f7d8e..d8c9f76 100644 --- a/restricted/automations.php +++ b/restricted/automations.php @@ -79,6 +79,27 @@ function operatorChoices($type) return $liste; } +/** + * Sperrzeit: so lange nach einer Ausloesung wird nicht erneut geschaltet. + * + * Gedacht gegen Messwerte, die um die Schwelle pendeln - "Temperatur > 22" + * bei 22,1 / 21,9 / 22,1 Grad ist jedes Mal eine echte steigende Flanke, und + * ohne Sperre ginge jedes Mal ein Kommando raus. Wie schlimm das ist, haengt + * am Geraet: ein Rollladen soll nicht alle zwanzig Sekunden losfahren, eine + * Lichtfarbe darf das. + * + * Gespeichert werden Sekunden, angeboten werden nur diese Stufen - eine + * vierte ist damit eine Zeile und keine Wanderung durch die Datenbank. + */ +function lockoutChoices() +{ + return [ + ["value" => 0, "label" => "Ohne – jede Flanke schaltet"], + ["value" => 60, "label" => "1 Minute – z.B. Licht und Farbe"], + ["value" => 900, "label" => "15 Minuten – z.B. Rollläden und Ventile"], + ]; +} + /** Wie das Wertfeld im Editor aussieht. */ function inputForType($type, $hasOptions) { @@ -254,6 +275,7 @@ function emptyAutomation($floor) "on_vacation" => true, "on_holiday" => true, "force_once" => false, + "lockout_secs" => 60, "last_run" => null, "conditions" => [], "actions" => [], @@ -284,6 +306,7 @@ function loadAutomation($id) "on_vacation" => (bool)$row["on_vacation"], "on_holiday" => (bool)$row["on_holiday"], "force_once" => (bool)$row["force_once"], + "lockout_secs" => intval($row["lockout_secs"]), "last_run" => $row["last_run"], "conditions" => [], "actions" => [], @@ -367,6 +390,12 @@ function saveAutomation($data) $onVacation = !empty($data["on_vacation"]) ? 1 : 0; $onHoliday = !empty($data["on_holiday"]) ? 1 : 0; $forceOnce = !empty($data["force_once"]) ? 1 : 0; + // Nur die angebotenen Stufen, sonst die Vorgabe. Eine krumme Zahl kaeme + // hier nur aus einer selbstgebauten Anfrage. + $lockout = intval($data["lockout_secs"] ?? 60); + if (!in_array($lockout, array_column(lockoutChoices(), "value"), true)) { + $lockout = 60; + } $db->begin_transaction(); try { @@ -386,10 +415,11 @@ function saveAutomation($data) $stmt = $db->prepare("UPDATE automations SET name = ?, floor = ?, enabled = ?, window_from = ?, window_to = ?, weekdays = ?, - on_vacation = ?, on_holiday = ?, force_once = ? + on_vacation = ?, on_holiday = ?, force_once = ?, + lockout_secs = ? WHERE id = ?"); - $stmt->bind_param("ssissiiiii", $name, $floor, $enabled, $windowFrom, $windowTo, - $weekdays, $onVacation, $onHoliday, $forceOnce, $id); + $stmt->bind_param("ssissiiiiii", $name, $floor, $enabled, $windowFrom, $windowTo, + $weekdays, $onVacation, $onHoliday, $forceOnce, $lockout, $id); $stmt->execute(); $stmt->close(); $db->query("DELETE FROM automation_conditions WHERE automation_id = " . $id); @@ -397,10 +427,10 @@ function saveAutomation($data) } else { $stmt = $db->prepare("INSERT INTO automations (name, floor, enabled, window_from, window_to, weekdays, - on_vacation, on_holiday, force_once) - VALUES (?,?,?,?,?,?,?,?,?)"); - $stmt->bind_param("ssissiiii", $name, $floor, $enabled, $windowFrom, $windowTo, - $weekdays, $onVacation, $onHoliday, $forceOnce); + on_vacation, on_holiday, force_once, lockout_secs) + VALUES (?,?,?,?,?,?,?,?,?,?)"); + $stmt->bind_param("ssissiiiii", $name, $floor, $enabled, $windowFrom, $windowTo, + $weekdays, $onVacation, $onHoliday, $forceOnce, $lockout); $stmt->execute(); $id = $db->insert_id; $stmt->close();