From 32e571099b28d6a74113019bef64af3a66f5230e Mon Sep 17 00:00:00 2001 From: "m0@nas" Date: Thu, 10 Sep 2026 20:07:36 +0200 Subject: [PATCH] Ferien und Feiertage dreiwertig, dazu eine Tagessperre "An Wochenenden und Feiertagen" liess sich bisher gar nicht schreiben. Die Wochentagsmaske kennt nur Samstag und Sonntag, und ein Feiertag am Dienstag ist fuer sie eben ein Dienstag; on_holiday war ja/nein und konnte nur wegnehmen, nie hinzufuegen. on_vacation und on_holiday tragen deshalb jetzt drei Werte: 0 nie, 1 egal (Vorgabe), 2 zusaetzlich. Der dritte zaehlt wie ein angehakter Wochentag - Sa+So angehakt und "Feiertage: zusaetzlich" ergibt den gewuenschten Fall, und mit gar keinem angehakten Tag sogar "nur an Feiertagen". In tag_passt() wird erst geoeffnet, dann gesperrt: ein Verbot schlaegt eine Erweiterung. Wer in den Ferien nie laeuft und an Feiertagen zusaetzlich, laeuft an einem Feiertag in den Ferien nicht - andersherum liesse sich "nie" nicht mehr verlassen. Dazu once_per_day: nach dem Ausloesen bis Mitternacht Ruhe. Fuer alles, was man hinterher von Hand wieder anders stellt - ein Rollladen, den man um acht zugezogen hat, soll nicht um neun von selbst wieder auffahren, weil eine Wolke weiterzieht und die Helligkeitsschwelle ein zweites Mal steigt. Die Sperrzeit taugte dafuer nicht: sie zaehlt Sekunden und muesste auf 86400 stehen, womit eine Automatik, die heute um 07:00:05 lief, morgen um 07:00:00 noch gesperrt waere und einen ganzen Tag ausfiele. Gefragt wird deshalb nach dem Datum von last_run - dieselbe Funktion, die auch force_once benutzt, nur mit umgekehrtem Vorzeichen. Die Spalten bleiben TINYINT und tragen die 2 ohne Weiteres; die vorhandenen Zeilen stehen auf 0 oder 1 und behalten damit genau ihre bisherige Bedeutung. Umzurechnen gibt es nichts. rahmen_erweitern.sql setzt nur Kommentare und legt once_per_day an. Co-Authored-By: Claude Opus 5 --- autoActions/README.md | 57 ++++++++++++++++++++++++++- autoActions/autoaction_runner.py | 55 ++++++++++++++++++++++++-- autoActions/rahmen_erweitern.sql | 66 ++++++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 autoActions/rahmen_erweitern.sql diff --git a/autoActions/README.md b/autoActions/README.md index 1ccf7de..6725837 100644 --- a/autoActions/README.md +++ b/autoActions/README.md @@ -30,7 +30,7 @@ Eine Aktion ist ein Kommando (`actor_commands`) mit einem Wert je Parameter sondern so viele Zeilen, wie das Gerät Parameter hat. Die Rahmenbedingungen (Wochentage, Zeitfenster, Ferien, Feiertage) sagen, wann -die Automatik überhaupt hinsehen darf. +die Automatik überhaupt hinsehen darf — siehe [Der Rahmen](#der-rahmen). ## Warum ein Dauerläufer und kein Cronjob @@ -114,6 +114,55 @@ und `vor` verschiebt sich der wahre Bereich entsprechend mit. `force_once` („am Ende des Zeitraums auf jeden Fall ausführen") greift, wenn das Fenster zugeht und in diesem Fenster noch nichts passiert ist. +## Der Rahmen + +Vor jeder Auswertung fragt `tag_passt()`, ob der heutige Tag überhaupt zählt. +Drei Dinge entscheiden das: die Wochentagsmaske (`weekdays`, ein Bit je Tag, +Montag ist Bit 0), und Ferien und Feiertage aus `calendar_days`. + +Ferien und Feiertage sind **dreiwertig**, nicht ja/nein: + +| Editor | `on_vacation` / `on_holiday` | heißt | +|---|---|---| +| **nie** | `0` | an solchen Tagen läuft die Automatik nicht | +| **egal** | `1` (Vorgabe) | der Tag ändert nichts | +| **immer** | `2` | zählt wie ein angehakter Wochentag | + +Die ersten beiden gab es immer; „werktags" ist `weekdays = 31` und +`on_holiday = 0`. Der dritte Wert kam dazu, weil sich **„an Wochenenden und +Feiertagen" sonst gar nicht schreiben ließ**: die Wochentagsmaske kennt nur +Samstag und Sonntag, und ein Feiertag am Dienstag ist für sie eben ein +Dienstag. Mit `2` genügt Sa+So angehakt und „Feiertage: immer", der Dienstag +kommt dann über den Kalender herein. Und weil `2` allein trägt, ergibt +`weekdays = 0` mit „Feiertage: immer" ein **nur an Feiertagen** — das ging +vorher gar nicht. + +Erst wird geöffnet, dann gesperrt: **ein Verbot schlägt eine Erweiterung.** +Wer in den Ferien nie läuft und an Feiertagen zusätzlich, läuft an einem +Feiertag in den Ferien nicht. Andersherum ließe sich „nie" nicht mehr +verlassen. + +Die Spalten sind `TINYINT` geblieben und tragen die `2` ohne Weiteres; die +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. + +## Nur einmal am Tag + +`once_per_day` sperrt eine Automatik nach dem Auslösen bis Mitternacht. + +Gedacht für alles, was man hinterher von Hand wieder anders stellt. Der +Rollladen fährt morgens auf; um acht zieht man ihn nochmal zu, weil die Sonne +blendet — und um neun zieht eine Wolke weiter, die Helligkeitsschwelle steigt +ein zweites Mal, und die Automatik funkt dazwischen. Genau das verhindert der +Haken. + +Die Sperrzeit taugt dafür nicht. Sie zählt Sekunden und müsste auf 86 400 +stehen; eine Automatik, die heute um 07:00:05 lief, wäre morgen um 07:00:00 +noch gesperrt und fiele einen ganzen Tag aus. Gefragt wird deshalb nach dem +**Datum** von `last_run`, nicht nach dem Abstand — dieselbe Funktion +(`lief_heute`), die auch `force_once` benutzt, nur mit umgekehrtem Vorzeichen. + ## Sperrzeit Die Flanke allein schützt nicht gegen einen Messwert, der um die Schwelle @@ -324,6 +373,7 @@ Parameter dort keine eigene URL — ihr Name *ist* der Platzhalter. ```bash cp config.ini.example config.ini # ausfüllen: Datenbank, MQTT, Tahoma mysql -h 127.0.0.1 -P 3310 -u homeMesh -p homeMesh < automatik_ausloeser.sql +mysql -h 127.0.0.1 -P 3310 -u homeMesh -p homeMesh < rahmen_erweitern.sql python3 fetch_calendar.py # Feiertage und Ferien holen python3 autoaction_runner.py --once --dry-run --verbose # Probelauf ``` @@ -334,6 +384,10 @@ Messwerte darunter legt der Runner selbst an. Ohne das Skript läuft alles Übrige weiter, es fehlt nur die Möglichkeit, eine Automatik als Auslöser zu wählen. +`rahmen_erweitern.sql` gehört zum Rahmen: es beschriftet `on_vacation` und +`on_holiday` mit ihren drei Bedeutungen und legt `once_per_day` an. Beide +Skripte sind idempotent — ein zweiter Lauf schadet nicht. + `--dry-run` schaltet nichts, protokolliert aber jedes Kommando, das geschickt würde. `--once` macht einen einzigen Durchlauf. @@ -351,6 +405,7 @@ nicht im Web-Verzeichnis: ├── transports.py ├── fetch_calendar.py ├── automatik_ausloeser.sql einmalig, siehe Einrichten + ├── rahmen_erweitern.sql einmalig, siehe Einrichten └── config.ini Zugangsdaten, nicht im Git ``` diff --git a/autoActions/autoaction_runner.py b/autoActions/autoaction_runner.py index f5d402b..3489574 100644 --- a/autoActions/autoaction_runner.py +++ b/autoActions/autoaction_runner.py @@ -511,12 +511,41 @@ def im_zeitfenster(jetzt, von, bis): return a <= m <= b if a <= b else (m >= a or m <= b) +#: Ferien und Feiertage sind dreiwertig, nicht ja/nein. +NIE, EGAL, ZUSAETZLICH = 0, 1, 2 + + def tag_passt(automatik, jetzt, kalender): - if not automatik["weekdays"] & (1 << jetzt.weekday()): + """ + Faellt der heutige Tag in den Rahmen der Automatik? + + Die Wochentage sind eine Maske, Ferien und Feiertage haben je drei + Zustaende. Zwei davon gab es immer: EGAL (der Tag aendert nichts, die + Vorgabe) und NIE (an solchen Tagen laeuft die Automatik nicht - so ist + "werktags" gebaut). ZUSAETZLICH ist der dritte und zaehlt wie ein + passender Wochentag. + + Der Grund fuer den dritten: "Wochenenden und Feiertage" liess sich vorher + gar nicht schreiben. Die Wochentagsmaske kennt nur Samstag und Sonntag, + und ein Feiertag am Dienstag ist eben ein Dienstag. Mit ZUSAETZLICH + genuegt Sa+So angehakt und "Feiertage: zusaetzlich" - der Dienstag kommt + dann ueber den Kalender herein. + + Reihenfolge: erst wird geoeffnet, dann gesperrt. Wer in den Ferien nie + laufen soll und an Feiertagen zusaetzlich, laeuft an einem Feiertag in + den Ferien nicht - ein Verbot schlaegt eine Erweiterung. Anders herum + liesse sich "nie" nicht mehr verlassen. + """ + passt = bool(automatik["weekdays"] & (1 << jetzt.weekday())) + if kalender["feiertag"] and automatik["on_holiday"] == ZUSAETZLICH: + passt = True + if kalender["ferien"] and automatik["on_vacation"] == ZUSAETZLICH: + passt = True + if not passt: return False - if kalender["feiertag"] and not automatik["on_holiday"]: + if kalender["feiertag"] and automatik["on_holiday"] == NIE: return False - if kalender["ferien"] and not automatik["on_vacation"]: + if kalender["ferien"] and automatik["on_vacation"] == NIE: return False return True @@ -1150,7 +1179,10 @@ class Runner: erfuellt = gruppen_erfuellt(automatik, self.regelwerk, self.werte, jetzt, fenster) if erfuellt and not automatik["cond_met"]: - if self.gesperrt(automatik, jetzt): + if automatik.get("once_per_day") and self.lief_heute(automatik, jetzt): + logger.debug("%s: lief heute schon, bis Mitternacht gesperrt", + automatik["name"]) + elif self.gesperrt(automatik, jetzt): logger.debug("%s: Flanke faellt in die Sperrzeit, uebersprungen", automatik["name"]) else: @@ -1200,6 +1232,21 @@ class Runner: @staticmethod def lief_heute(automatik, jetzt): + """ + Hat die Automatik heute schon ausgeloest? + + Zwei Stellen fragen danach. force_once will wissen, ob es am Ende des + Fensters noch etwas nachzuholen gibt. once_per_day will das Gegenteil: + einmal am Tag genuegt, danach ist bis Mitternacht Ruhe. + + Letzteres ist fuer alles gedacht, was man hinterher von Hand wieder + anders stellt. Ein Rollladen, den man um acht nochmal zugezogen hat, + soll nicht um neun von selbst wieder auffahren, nur weil eine Wolke + weiterzieht und die Helligkeitsschwelle ein zweites Mal steigt. Die + Sperrzeit taugt dafuer nicht: sie zaehlt Sekunden und muesste auf + einen Tag stehen, womit sie am naechsten Morgen den Termin knapp + verfehlen wuerde. + """ letzter = automatik.get("last_run") return bool(letzter) and letzter.date() == jetzt.date() diff --git a/autoActions/rahmen_erweitern.sql b/autoActions/rahmen_erweitern.sql new file mode 100644 index 0000000..5d0b9ab --- /dev/null +++ b/autoActions/rahmen_erweitern.sql @@ -0,0 +1,66 @@ +-- =========================================================================== +-- Rahmenbedingungen erweitern: Ferien und Feiertage dreiwertig, +-- dazu die Tagessperre. +-- +-- Einspielen: +-- mysql -h 127.0.0.1 -P 3310 -u homeMesh -p homeMesh < rahmen_erweitern.sql +-- +-- Danach den Runner neu starten (startSolarServer.sh) - Daten laedt er +-- selbst nach, neuen Code nicht. +-- +-- --------------------------------------------------------------------------- +-- 1. on_vacation / on_holiday sind jetzt dreiwertig +-- +-- 0 = nie an solchen Tagen laeuft die Automatik nicht +-- 1 = egal der Tag aendert nichts (Vorgabe, wie bisher) +-- 2 = zusaetzlich zaehlt wie ein passender Wochentag +-- +-- Die Spalten bleiben, wie sie sind - TINYINT traegt die 2 ohne Weiteres, +-- und die vorhandenen Zeilen stehen bereits auf 0 oder 1 und behalten damit +-- genau ihre bisherige Bedeutung. Es gibt nichts umzurechnen. +-- +-- Gebraucht wird die 2 fuer den Fall, den man vorher gar nicht schreiben +-- konnte: "an Wochenenden und Feiertagen". Die Wochentagsmaske kennt nur +-- Samstag und Sonntag, ein Feiertag am Dienstag ist eben ein Dienstag. Mit +-- der 2 genuegt Sa+So angehakt und "Feiertage: zusaetzlich". +-- +-- 2. once_per_day - nach dem Auslösen bis Mitternacht Ruhe +-- +-- Fuer alles, was man hinterher von Hand wieder anders stellt. Ein Rollladen, +-- den man um acht nochmal zugezogen hat, soll nicht um neun von selbst wieder +-- auffahren, nur weil eine Wolke weiterzieht und die Helligkeitsschwelle ein +-- zweites Mal steigt. +-- +-- Die Sperrzeit (lockout_secs) taugt dafuer nicht: sie zaehlt Sekunden und +-- muesste auf 86400 stehen, womit sie am naechsten Morgen den Termin knapp +-- verfehlen wuerde - eine Automatik, die heute um 07:00:05 lief, waere +-- morgen um 07:00:00 noch gesperrt und fiele einen ganzen Tag aus. +-- =========================================================================== + +ALTER TABLE automations + MODIFY COLUMN on_vacation TINYINT(1) NOT NULL DEFAULT 1 + COMMENT '0=nie, 1=egal, 2=zusaetzlich (zaehlt wie passender Wochentag)', + MODIFY COLUMN on_holiday TINYINT(1) NOT NULL DEFAULT 1 + COMMENT '0=nie, 1=egal, 2=zusaetzlich (zaehlt wie passender Wochentag)'; + +-- 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 = 'once_per_day'); + +SET @sql := IF(@vorhanden = 0, + 'ALTER TABLE automations ADD COLUMN once_per_day TINYINT(1) NOT NULL DEFAULT 0 + COMMENT ''nach dem Auslösen bis Mitternacht gesperrt'' + AFTER force_once', + '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 IN ('on_vacation', 'on_holiday', 'once_per_day');