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
|
||||
"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):
|
||||
"""
|
||||
|
||||
@@ -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