Sperrzeit je Automatik in drei Stufen
Die steigende Flanke allein schuetzt nicht gegen einen Messwert, der um die Schwelle pendelt: "Temperatur > 22" bei 22,1 / 21,9 / 22,1 Grad ist jedes Mal eine echte Flanke, und ueber MQTT koennen die Werte im Sekundentakt hereinkommen. Gemessen: fuenf Kommandos in einer halben Minute. automations.lockout_secs sagt jetzt, wie lange nach einer Ausloesung nicht wieder geschaltet wird. Der Editor bietet drei Stufen an - ohne, eine Minute, eine Viertelstunde -, weil die passende Wahl am Geraet haengt und nicht an einer Zahl: ein Rollladen soll nicht alle zwanzig Sekunden losfahren, eine Lichtfarbe darf das. Gespeichert werden Sekunden, damit eine vierte Stufe eine Zeile in lockoutChoices() ist und keine Wanderung durch die Datenbank. Vorbelegt ist eine Minute. Eine Flanke in der Sperrzeit wird verworfen, 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 - und der naechste echte Anlass nach Ablauf der Sperre kommt ohnehin durch. Verworfene Flanken stehen auf DEBUG und nicht in automation_log, sonst waere die Tabelle bei einem zappelnden Sensor voll davon. force_once bleibt unberuehrt: es greift nur, wenn im Fenster gar nichts gelaufen ist - dann ist auch keine Sperre aktiv. Nachgemessen am pendelnden Sensor: mit 60 s Sperre ein Kommando, ohne Sperre fuenf. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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([
|
||||
<label class="form-check-label" for="actFeier">An Feiertagen ausführen</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Sperrzeit nach dem Auslösen</span>
|
||||
<select class="form-select" id="lockoutSecs"></select>
|
||||
</div>
|
||||
<div class="input-group mb-0">
|
||||
<div class="input-group-text">
|
||||
<input class="form-check-input mt-0 me-2" type="checkbox" id="runOnce">
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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`:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user