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 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 09:33:59 +02:00
co-authored by Claude Opus 5
parent 8c62455b2c
commit 8340e8356e
3 changed files with 97 additions and 13 deletions
+13
View File
@@ -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 MoFr,
Ferien nie, Feiertage nie — genau wie der Wecker. Mit dem heutigen Tag ließ
sich das nur annähern (SoDo, 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.
+43 -13
View File
@@ -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)
+41
View File
@@ -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';