Haltezeit: erst ausloesen, wenn die Bedingung dabei bleibt

Bisher loeste eine Automatik in dem Moment aus, in dem die Bedingung wahr
wurde. Fuer Dauerzustaende taugt das nicht: "der Wasserzaehler laeuft" ist
jedes Haendewaschen, gemeint war "laeuft seit einer halben Stunde ohne
Pause".

hold_secs schiebt die Flanke nach hinten, statt sie zu verbrauchen -
cond_met bleibt bis dahin 0, und alles danach (Tagessperre, Sperrzeit,
Protokoll) bleibt unberuehrt. Ein Aussetzer setzt die Zeit zurueck, das
Zugehen des Zeitfensters ebenfalls.

Gezaehlt wird im Speicher (erfuellt_seit) wie war_aktiv: nach einem Neustart
weiss niemand, ob die Bedingung zwischendurch anlag.

hold_secs = 0 ist die Vorgabe und heisst: wie bisher.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 08:57:14 +02:00
co-authored by Claude Opus 5
parent 35382a6ae1
commit e1e678f6c0
2 changed files with 98 additions and 3 deletions
+64 -3
View File
@@ -35,6 +35,16 @@ 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.
Die Haltezeit (`automations.hold_secs`) schiebt diese Flanke nach hinten: Erst
wenn die Bedingung so viele Sekunden am Stueck erfuellt war, gilt sie als
erfuellt. Gedacht fuer Dauerzustaende, die sich nicht in einem einzelnen
Messwert zeigen - "der Wasserzaehler laeuft" ist jedes Haendewaschen, "laeuft
seit einer halben Stunde ohne Pause" ist ein offener Hahn. Gezaehlt wird hier
im Speicher (`erfuellt_seit`) und nicht in der Datenbank: es ist ein
Laufzustand wie `war_aktiv`, und ein Neustart soll ihn bewusst verwerfen -
nach einem Neustart weiss niemand, ob die Bedingung in der Zwischenzeit
durchgehend anlag.
Zeit-Ausloeser gibt es in drei Formen: "um 16:30" gilt ab dieser Minute noch
catchup_minutes lang, "ab 16:30" von da an bis Mitternacht, "vor 16:30" bis
dahin. Ausgeloest wird in allen drei Faellen nur einmal, eben wegen der
@@ -750,6 +760,7 @@ class Runner:
self.geschrieben_um = {} # state_id -> wann das war
self.war_aktiv = {} # automation_id -> war im Zeitfenster
self.lief_im_fenster = {} # automation_id -> hat im Fenster ausgeloest
self.erfuellt_seit = {} # automation_id -> seit wann die Bedingung anliegt
self._sonne = (None, "00:00", "00:00") # (datum, aufgang, untergang)
self._kalender = {} # datum -> {"feiertag": .., "ferien": ..}
self._letzte_saeuberung = None
@@ -1311,7 +1322,12 @@ class Runner:
self.lief_im_fenster[automatik["id"]] = False
erfuellt = gruppen_erfuellt(automatik, self.regelwerk, self.werte,
jetzt, fenster)
if erfuellt and not automatik["cond_met"]:
# Mit Haltezeit zaehlt nicht, ob die Bedingung erfuellt ist,
# sondern ob sie es lange genug am Stueck ist. Weil das Ergebnis
# an dieselbe Stelle tritt, bleibt alles danach - Flanke,
# Tagessperre, Sperrzeit, Protokoll - unveraendert.
reif = self.haltezeit_reif(automatik, erfuellt, jetzt)
if reif and not automatik["cond_met"]:
if automatik.get("once_per_day") and self.lief_heute(automatik, jetzt):
logger.debug("%s: lief heute schon, bis Mitternacht gesperrt",
automatik["name"])
@@ -1320,7 +1336,7 @@ class Runner:
automatik["name"])
else:
self.ausloesen(automatik, "fired")
self.flanke_merken(automatik, erfuellt)
self.flanke_merken(automatik, reif)
else:
# Das Fenster ist gerade zugegangen. Wer "auf jeden Fall"
# angehakt hat, bekommt jetzt seinen Lauf - aber nur, wenn in
@@ -1333,12 +1349,57 @@ class Runner:
automatik["name"])
self.ausloesen(automatik, "forced")
# Ausserhalb des Fensters die Flanke zuruecksetzen, sonst
# koennte sie im naechsten Fenster nicht mehr steigen.
# koennte sie im naechsten Fenster nicht mehr steigen. Eine
# angefangene Haltezeit verfaellt mit: sie soll innerhalb des
# Fensters voll gelaufen sein, nicht ueber dessen Rand hinweg.
self.flanke_merken(automatik, False)
self.erfuellt_seit.pop(automatik["id"], None)
self.lief_im_fenster[automatik["id"]] = False
self.war_aktiv[automatik["id"]] = aktiv
def haltezeit_reif(self, automatik, erfuellt, jetzt):
"""
Liegt die Bedingung lange genug am Stueck an?
Ohne Haltezeit (hold_secs = 0, die Vorgabe) ist die Antwort schlicht
die Bedingung selbst - dann verhaelt sich alles wie vorher.
Sonst wird der Zeitpunkt gemerkt, an dem die Bedingung wahr wurde, und
erst nach Ablauf der Haltezeit "ja" gemeldet. Faellt sie zwischendurch
auch nur einen Takt aus, wird der Zeitpunkt verworfen und faengt beim
naechsten Mal von vorn an; genau das ist mit "ununterbrochen" gemeint.
Wichtig ist, dass cond_met bis dahin auf 0 bleibt - die Flanke wird
also nicht verbraucht, sondern aufgeschoben. Deshalb wird sie hier
nicht selbst gesetzt, sondern das Ergebnis nach oben gereicht.
Zwei Dinge, die man wissen sollte:
* Ein punktgenauer Zeit-Ausloeser ("um 16:30") ist nur das
Nachholfenster lang wahr. Eine Haltezeit darueber hinaus wuerde nie
reif - was kein Fehler, aber auch keine sinnvolle Kombination ist.
* Eine Bedingung gilt so lange weiter, wie ihr letzter Messwert gilt.
Bei einem Zaehler, der alle fuenf Minuten meldet, ist die Haltezeit
also auf fuenf Minuten genau - sinnvolle Stufen beginnen deutlich
darueber.
"""
halten = int(automatik.get("hold_secs") or 0)
if not halten:
return erfuellt
if not erfuellt:
if self.erfuellt_seit.pop(automatik["id"], None) is not None:
logger.debug("%s: Haltezeit abgebrochen, faengt von vorn an",
automatik["name"])
return False
seit = self.erfuellt_seit.setdefault(automatik["id"], jetzt)
offen = halten - (jetzt - seit).total_seconds()
if offen > 0:
logger.debug("%s: Bedingung erfuellt, noch %d s Haltezeit",
automatik["name"], offen)
return False
return True
@staticmethod
def gesperrt(automatik, jetzt):
"""