From 8340e8356e3788597ccc563aad1b0e428dce9abf Mon Sep 17 00:00:00 2001 From: "m0@nas" Date: Tue, 15 Sep 2026 09:33:59 +0200 Subject: [PATCH] Runner: Kalender am Vorabend fuer den folgenden Tag pruefen Mit next_day = 1 gelten Wochentage, Ferien und Feiertage fuer morgen (gemeinter_tag). kalender() merkt sich dafuer heute und morgen und haelt einen fehlgeschlagenen Abruf nicht mehr den ganzen Tag fest. Uhrzeit, Zeitfenster und once_per_day bleiben beim heutigen Tag. vorabend.sql legt die Spalte an. Co-Authored-By: Claude Opus 5 --- autoActions/README.md | 13 ++++++++ autoActions/autoaction_runner.py | 56 ++++++++++++++++++++++++-------- autoActions/vorabend.sql | 41 +++++++++++++++++++++++ 3 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 autoActions/vorabend.sql diff --git a/autoActions/README.md b/autoActions/README.md index 6725837..33c2936 100644 --- a/autoActions/README.md +++ b/autoActions/README.md @@ -147,6 +147,19 @@ vorhandenen Zeilen stehen auf `0` oder `1` und behalten damit genau ihre bisherige Bedeutung. Umzurechnen gibt es nichts, die Migration (`rahmen_erweitern.sql`) setzt nur Kommentare und legt `once_per_day` an. +### Am Vorabend + +Mit `next_day = 1` gelten Wochentage, Ferien und Feiertage für **morgen** +(`gemeinter_tag()`). Gedacht für alles, was abends für den nächsten Tag +geschieht: „Kinderrollos zu, wenn morgen Schule ist“ heißt dann Mo–Fr, +Ferien nie, Feiertage nie — genau wie der Wecker. Mit dem heutigen Tag ließ +sich das nur annähern (So–Do, heute keine Ferien) und ging am letzten +Ferientag, am Abend vor einem Feiertag und am Abend eines Feiertags daneben. + +Nur der Rahmen verschiebt sich. Uhrzeit, Zeitfenster und „nur einmal am Tag“ +bleiben beim heutigen Tag, denn die Automatik läuft ja heute Abend. Die +Spalte legt `vorabend.sql` an. + ## Nur einmal am Tag `once_per_day` sperrt eine Automatik nach dem Auslösen bis Mitternacht. diff --git a/autoActions/autoaction_runner.py b/autoActions/autoaction_runner.py index 3489574..95a8008 100644 --- a/autoActions/autoaction_runner.py +++ b/autoActions/autoaction_runner.py @@ -515,9 +515,30 @@ def im_zeitfenster(jetzt, von, bis): NIE, EGAL, ZUSAETZLICH = 0, 1, 2 -def tag_passt(automatik, jetzt, kalender): +def gemeinter_tag(automatik, jetzt): """ - Faellt der heutige Tag in den Rahmen der Automatik? + Fuer welchen Tag gelten Wochentage, Ferien und Feiertage? + + Normalerweise fuer heute. Mit `next_day` fuer morgen - die Vorabend-Form: + "Kinderrollos zu, wenn morgen Schule ist" heisst Mo-Fr, Ferien nie, + Feiertage nie, und der Runner schaut dafuer auf den folgenden Tag. Mit + dem heutigen Tag liess sich das nur annaehern (So-Do, heute keine + Ferien), und das ging am letzten Ferientag, am Abend vor einem Feiertag + und am Abend eines Feiertags daneben. + + Nur der Rahmen verschiebt sich. Uhrzeit, Zeitfenster und "einmal am Tag" + bleiben beim heutigen Tag - die Automatik laeuft ja heute Abend. Ein + Zeitfenster ueber Mitternacht meint nach Mitternacht deshalb schon den + uebernaechsten Tag; fuer eine Vorabend-Regel ist das kein sinnvoller Fall. + """ + heute = jetzt.date() + return heute + timedelta(days=1) if automatik.get("next_day") else heute + + +def tag_passt(automatik, tag, kalender): + """ + Faellt der Tag in den Rahmen der Automatik? `tag` ist heute oder morgen, + siehe gemeinter_tag(); `kalender` gehoert zu genau diesem Tag. Die Wochentage sind eine Maske, Ferien und Feiertage haben je drei Zustaende. Zwei davon gab es immer: EGAL (der Tag aendert nichts, die @@ -536,7 +557,7 @@ def tag_passt(automatik, jetzt, kalender): den Ferien nicht - ein Verbot schlaegt eine Erweiterung. Anders herum liesse sich "nie" nicht mehr verlassen. """ - passt = bool(automatik["weekdays"] & (1 << jetzt.weekday())) + passt = bool(automatik["weekdays"] & (1 << tag.weekday())) if kalender["feiertag"] and automatik["on_holiday"] == ZUSAETZLICH: passt = True if kalender["ferien"] and automatik["on_vacation"] == ZUSAETZLICH: @@ -718,7 +739,7 @@ class Runner: self.war_aktiv = {} # automation_id -> war im Zeitfenster self.lief_im_fenster = {} # automation_id -> hat im Fenster ausgeloest self._sonne = (None, "00:00", "00:00") # (datum, aufgang, untergang) - self._kalender = (None, {"feiertag": False, "ferien": False}) + self._kalender = {} # datum -> {"feiertag": .., "ferien": ..} self._letzte_saeuberung = None self.reihenfolge = [] # automation_id, Ausloeser vor Nachfolger self.ausloeser_states = {} # automation_id -> state_id des Ausloesers @@ -991,21 +1012,30 @@ class Runner: return "%02d:%02d" % (minute // 60 % 24, minute % 60) return str(wert)[:5] - def kalender(self): - """Ferien und Feiertage von heute, einmal je Tag geholt.""" - heute = date.today() - if self._kalender[0] == heute: - return self._kalender[1] + def kalender(self, tag): + """ + Ferien und Feiertage eines Tages, je Tag einmal geholt. + + Gefragt wird nach heute und - fuer Vorabend-Regeln - nach morgen. + Gemerkt werden deshalb beide; was vor heute liegt, fliegt raus. + """ + if tag in self._kalender: + return self._kalender[tag] stand = {"feiertag": False, "ferien": False} try: with self.db.cursor() as c: - c.execute("SELECT holiday, vacation FROM calendar_days WHERE date = %s", (heute,)) + c.execute("SELECT holiday, vacation FROM calendar_days WHERE date = %s", (tag,)) zeile = c.fetchone() if zeile: stand = {"feiertag": bool(zeile["holiday"]), "ferien": bool(zeile["vacation"])} except Exception as fehler: + # Nicht merken: beim naechsten Durchlauf noch einmal fragen, + # statt den ganzen Tag mit "kein Feiertag" weiterzurechnen. logger.warning("Kalender nicht lesbar: %r", fehler) - self._kalender = (heute, stand) + return stand + heute = date.today() + self._kalender = {t: s for t, s in self._kalender.items() if t >= heute} + self._kalender[tag] = stand return stand # --- Werte ----------------------------------------------------------- @@ -1163,13 +1193,13 @@ class Runner: def durchlauf(self, fenster=NACHHOLFENSTER): jetzt = datetime.now() - kalender = self.kalender() for automation_id in self.reihenfolge: automatik = self.regelwerk.automatiken.get(automation_id) if automatik is None: continue - aktiv = (tag_passt(automatik, jetzt, kalender) + tag = gemeinter_tag(automatik, jetzt) + aktiv = (tag_passt(automatik, tag, self.kalender(tag)) and im_zeitfenster(jetzt, automatik["window_from"], automatik["window_to"])) vorher_aktiv = self.war_aktiv.get(automatik["id"], aktiv) diff --git a/autoActions/vorabend.sql b/autoActions/vorabend.sql new file mode 100644 index 0000000..420e940 --- /dev/null +++ b/autoActions/vorabend.sql @@ -0,0 +1,41 @@ +-- =========================================================================== +-- Vorabend: Wochentage, Ferien und Feiertage fuer den folgenden Tag. +-- +-- Einspielen: +-- mysql -h 127.0.0.1 -P 3310 -u homeMesh -p homeMesh < vorabend.sql +-- +-- Danach den Runner neu starten (startSolarServer.sh) - Daten laedt er +-- selbst nach, neuen Code nicht. +-- +-- Gebraucht fuer alles, was abends fuer den naechsten Tag geschieht: +-- "Kinderrollos zu, wenn morgen Schule ist". Mit dem heutigen Tag liess sich +-- das nur annaehern (So-Do, heute keine Ferien) und ging am letzten +-- Ferientag, am Abend vor einem Feiertag und am Abend eines Feiertags +-- daneben. Mit next_day = 1 heisst dieselbe Regel einfach Mo-Fr, Ferien nie, +-- Feiertage nie - wie der Wecker. +-- +-- Nur der Rahmen verschiebt sich; Uhrzeit, Zeitfenster und "einmal am Tag" +-- bleiben beim heutigen Tag. Siehe gemeinter_tag() im Runner. +-- =========================================================================== + +-- Idempotent: beim zweiten Lauf steht die Spalte schon da. +SET @vorhanden := (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'automations' + AND COLUMN_NAME = 'next_day'); + +SET @sql := IF(@vorhanden = 0, + 'ALTER TABLE automations ADD COLUMN next_day TINYINT(1) NOT NULL DEFAULT 0 + COMMENT ''Wochentage, Ferien und Feiertage gelten fuer morgen (Vorabend)'' + AFTER on_holiday', + 'DO 0'); + +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SELECT COLUMN_NAME, COLUMN_DEFAULT, COLUMN_COMMENT + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'automations' + AND COLUMN_NAME = 'next_day';