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();