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:
@@ -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
|
Bedingung beim letzten Durchlauf schon erfuellt war. Ohne das wuerde
|
||||||
"Temperatur ueber 22 Grad" bei jedem Takt erneut feuern.
|
"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
|
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
|
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
|
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.geschrieben_um = {} # state_id -> wann das war
|
||||||
self.war_aktiv = {} # automation_id -> war im Zeitfenster
|
self.war_aktiv = {} # automation_id -> war im Zeitfenster
|
||||||
self.lief_im_fenster = {} # automation_id -> hat im Fenster ausgeloest
|
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._sonne = (None, "00:00", "00:00") # (datum, aufgang, untergang)
|
||||||
self._kalender = {} # datum -> {"feiertag": .., "ferien": ..}
|
self._kalender = {} # datum -> {"feiertag": .., "ferien": ..}
|
||||||
self._letzte_saeuberung = None
|
self._letzte_saeuberung = None
|
||||||
@@ -1311,7 +1322,12 @@ class Runner:
|
|||||||
self.lief_im_fenster[automatik["id"]] = False
|
self.lief_im_fenster[automatik["id"]] = False
|
||||||
erfuellt = gruppen_erfuellt(automatik, self.regelwerk, self.werte,
|
erfuellt = gruppen_erfuellt(automatik, self.regelwerk, self.werte,
|
||||||
jetzt, fenster)
|
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):
|
if automatik.get("once_per_day") and self.lief_heute(automatik, jetzt):
|
||||||
logger.debug("%s: lief heute schon, bis Mitternacht gesperrt",
|
logger.debug("%s: lief heute schon, bis Mitternacht gesperrt",
|
||||||
automatik["name"])
|
automatik["name"])
|
||||||
@@ -1320,7 +1336,7 @@ class Runner:
|
|||||||
automatik["name"])
|
automatik["name"])
|
||||||
else:
|
else:
|
||||||
self.ausloesen(automatik, "fired")
|
self.ausloesen(automatik, "fired")
|
||||||
self.flanke_merken(automatik, erfuellt)
|
self.flanke_merken(automatik, reif)
|
||||||
else:
|
else:
|
||||||
# Das Fenster ist gerade zugegangen. Wer "auf jeden Fall"
|
# Das Fenster ist gerade zugegangen. Wer "auf jeden Fall"
|
||||||
# angehakt hat, bekommt jetzt seinen Lauf - aber nur, wenn in
|
# angehakt hat, bekommt jetzt seinen Lauf - aber nur, wenn in
|
||||||
@@ -1333,12 +1349,57 @@ class Runner:
|
|||||||
automatik["name"])
|
automatik["name"])
|
||||||
self.ausloesen(automatik, "forced")
|
self.ausloesen(automatik, "forced")
|
||||||
# Ausserhalb des Fensters die Flanke zuruecksetzen, sonst
|
# 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.flanke_merken(automatik, False)
|
||||||
|
self.erfuellt_seit.pop(automatik["id"], None)
|
||||||
self.lief_im_fenster[automatik["id"]] = False
|
self.lief_im_fenster[automatik["id"]] = False
|
||||||
|
|
||||||
self.war_aktiv[automatik["id"]] = aktiv
|
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
|
@staticmethod
|
||||||
def gesperrt(automatik, jetzt):
|
def gesperrt(automatik, jetzt):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
-- ===========================================================================
|
||||||
|
-- Haltezeit: die Bedingung muss eine Weile durchgehend erfuellt sein
|
||||||
|
--
|
||||||
|
-- Einspielen:
|
||||||
|
-- mysql -h 127.0.0.1 -P 3310 -u homeMesh -p homeMesh < haltezeit.sql
|
||||||
|
--
|
||||||
|
-- Danach den Runner neu starten (startSolarServer.sh) - Daten laedt er
|
||||||
|
-- selbst nach, neuen Code nicht.
|
||||||
|
--
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Wozu
|
||||||
|
--
|
||||||
|
-- Bisher loeste eine Automatik in dem Moment aus, in dem die Bedingung wahr
|
||||||
|
-- wurde. Fuer "Temperatur ueber 22 Grad" ist das richtig. Fuer "der
|
||||||
|
-- Wasserzaehler laeuft und hoert nicht auf" ist es unbrauchbar: Jeder
|
||||||
|
-- Spuelgang macht die Bedingung wahr, und gemeint war "seit einer halben
|
||||||
|
-- Stunde ohne Pause".
|
||||||
|
--
|
||||||
|
-- Die Sperrzeit (lockout_secs) taugt dafuer nicht - sie begrenzt, wie oft
|
||||||
|
-- ausgeloest wird, nicht wie lange etwas anliegen muss. Und die Verkettung
|
||||||
|
-- ueber eine zweite Automatik traefe es nur ungefaehr.
|
||||||
|
--
|
||||||
|
-- hold_secs = 0 ist die Vorgabe und heisst: sofort, also genau wie bisher.
|
||||||
|
-- Jede vorhandene Automatik behaelt damit ihr Verhalten.
|
||||||
|
--
|
||||||
|
-- Gezaehlt wird im Runner, nicht in der Datenbank: Er weiss ohnehin bei
|
||||||
|
-- jedem Takt, ob die Bedingung gerade erfuellt ist. Faellt sie zwischendurch
|
||||||
|
-- aus, faengt die Zeit von vorn an - genau das ist "ohne Pause".
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ALTER TABLE `automations`
|
||||||
|
ADD COLUMN IF NOT EXISTS `hold_secs` int(11) NOT NULL DEFAULT 0
|
||||||
|
COMMENT 'Haltezeit: so lange muss die Bedingung ununterbrochen erfuellt sein, bevor ausgeloest wird. 0 = sofort'
|
||||||
|
AFTER `lockout_secs`;
|
||||||
Reference in New Issue
Block a user