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');