- autoActions/README: Rahmen gilt heute oder am Vorabend fuer morgen, vorabend.sql beim Einrichten, startSolarServer.sh startet drei Prozesse, Beispiel der Verkettung wie die echten Automatiken, Neustart ueber SSH - README: skoda_ladepunkte und skoda.conf, Werkzeuge-Tabelle, Datenbank alarm ist geloescht - config.ini.example: [alarm] und zeit.py entfernt, die gibt es nicht mehr - Runner-Kopfkommentar verweist nicht mehr auf auto_watering.py Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
498 lines
24 KiB
Markdown
498 lines
24 KiB
Markdown
# AutoActions
|
||
|
||
Automatiken, die im Web-UI unter „Automatismen" angelegt werden und hier
|
||
ausgeführt werden: *wenn Bedingung, dann Kommando*.
|
||
|
||
```
|
||
Browser Datenbank homeMesh Runner
|
||
────────────────────── ───────────────────────── ────────────────────
|
||
Karte „Automatismen" ──▶ automations ──▶ autoaction_runner.py
|
||
ajax/AutoAction.php automation_conditions MQTT / HTTP / Tahoma
|
||
restricted/automations.php automation_actions ──▶ Geräte
|
||
js/solar/autoActionFuncs.js automation_action_params
|
||
automation_log
|
||
calendar_days ◀── fetch_calendar.py
|
||
```
|
||
|
||
## Wie eine Automatik aufgebaut ist
|
||
|
||
Eine Automatik hat **Auslöser**, **Rahmenbedingungen** und **Aktionen**.
|
||
|
||
Ein Auslöser vergleicht einen Messwert (`actor_states`) mit einer Schwelle.
|
||
Mehrere Auslöser werden über `group_no` verknüpft: gleiche Nummer heißt UND,
|
||
verschiedene Nummern heißen ODER — ausgewertet wird `any(all(gruppe))`. Im
|
||
Editor ist eine Gruppe ein gerahmter Block mit eigenem „+ Bedingung", zwischen
|
||
den Blöcken steht ein ODER. Die Klammerung ist damit gezeichnet und nicht bloß
|
||
vereinbart, und darunter steht derselbe Ausdruck noch einmal als Satz.
|
||
|
||
Eine Aktion ist ein Kommando (`actor_commands`) mit einem Wert je Parameter
|
||
(`command_parameters`) — nicht vier feste Spalten „Wert 1" bis „Wert 4",
|
||
sondern so viele Zeilen, wie das Gerät Parameter hat.
|
||
|
||
Die Rahmenbedingungen (Wochentage, Zeitfenster, Ferien, Feiertage) sagen, wann
|
||
die Automatik überhaupt hinsehen darf — siehe [Der Rahmen](#der-rahmen).
|
||
|
||
## Warum ein Dauerläufer und kein Cronjob
|
||
|
||
Zwei Gründe:
|
||
|
||
* Schwellwert-Auslöser sollen greifen, wenn die MQTT-Nachricht hereinkommt,
|
||
nicht erst im nächsten Minutenraster.
|
||
* `actor_states.current_value` wird sonst von niemandem fortgeschrieben — es
|
||
wird beim Geräte-Discovery einmal gesetzt und danach nie wieder. Ein
|
||
zustandsloser Cronjob hätte gar nichts, womit er vergleichen könnte. Der
|
||
Runner pflegt den Wert nebenbei mit — nur wenn er sich geändert hat und
|
||
höchstens einmal je Minute —, wovon auch der Editor profitiert: er zeigt
|
||
neben jedem Messwert den aktuellen Stand.
|
||
|
||
## Was worauf wartet
|
||
|
||
Die Hauptschleife wartet auf nichts. Alles, was ein Netz braucht, läuft
|
||
daneben:
|
||
|
||
| | wo | Takt |
|
||
|---|---|---|
|
||
| MQTT-Nachrichten | kommen von selbst, Zwischenspeicher im Transport | sofort |
|
||
| Uhrzeit, Datum | im Runner gerechnet | jeder Takt |
|
||
| Sonnenauf-/-untergang | `solarLog.daylight` | einmal je Tag |
|
||
| HTTP- und WLED-Geräte | `Sammler`, eigener Faden | `poll_http`, `poll_wled` |
|
||
| Tahoma | `Sammler`, eigener Faden | `poll_tahoma`, Vorgabe 5 Minuten |
|
||
| Kommandos senden | `Versand`, ein Faden je Gerät | wenn etwas ansteht |
|
||
|
||
Das war nicht immer so. Vorher wurden die Geräte mitten in der Schleife
|
||
abgefragt — neunzehn Tahoma-Geräte nacheinander, jedes mit bis zu zehn
|
||
Sekunden Zeitlimit. Eine Runde dauerte dadurch rund fünfundvierzig statt
|
||
dreißig Sekunden, gepollt wurde erst jede zweite Runde, also alle neunzig
|
||
Sekunden. Und weil die Uhr an derselben Abfrage hing, kam jede dritte Minute
|
||
nie vor: `um 18:26` wurde nie wahr.
|
||
|
||
Die Fäden fassen die Datenbank nicht an. Sie legen ihre Ergebnisse in eine
|
||
Queue, geschrieben wird im Hauptfaden — eine pymysql-Verbindung gehört einem
|
||
Faden.
|
||
|
||
## Nur die steigende Flanke
|
||
|
||
`automations.cond_met` hält fest, ob die Bedingung beim letzten Durchlauf schon
|
||
erfüllt war. Ohne das würde „Temperatur über 22 Grad" bei jedem Takt erneut
|
||
feuern. Verlässt die Automatik ihr Zeitfenster, wird die Flanke
|
||
zurückgesetzt, damit sie im nächsten Fenster wieder steigen kann.
|
||
|
||
Zeit-Auslöser gibt es in drei Formen:
|
||
|
||
| | wahr, wenn |
|
||
|---|---|
|
||
| `um 16:30` | ab dieser Minute, noch `catchup_minutes` lang |
|
||
| `ab 16:30` | von da an bis Mitternacht |
|
||
| `vor 16:30` | bis dahin |
|
||
|
||
Ausgelöst wird in allen drei Fällen nur einmal, eben wegen der Flanke.
|
||
|
||
Das Nachholfenster bei `um` ist der Ersatz für die früher verlangte
|
||
Punktgenauigkeit. Solange `um 16:30` nur in genau dieser Minute wahr war,
|
||
kostete jeder Aussetzer die Automatik für den ganzen Tag — und Aussetzer gab
|
||
es reichlich, weil die Uhr am Geräte-Poll hing und jede dritte Minute
|
||
übersprang. Jetzt gilt die Bedingung fünf Minuten lang (einstellbar), die
|
||
Flanke sorgt weiterhin für genau einen Lauf, und ein Neustart mitten im
|
||
Fenster holt den Lauf nach.
|
||
|
||
Beim Sonnenauf- und -untergang ist der Wert ein Versatz, und der kann davor
|
||
oder danach liegen — deshalb dieselben drei Fälle mal zwei: `+ 00:30` eine
|
||
halbe Stunde nach Sonnenaufgang, `ab - 00:30` ab einer halben Stunde davor,
|
||
`vor + 00:30` bis eine halbe Stunde danach.
|
||
|
||
Dieselben drei Formen trägt die Verkettung einer Automatik mit der nächsten,
|
||
dort aber nur mit Plus — siehe „Eine Automatik löst die nächste aus".
|
||
|
||
Über den Tagesrand wird gerechnet, nicht abgeschnitten: „sechs Stunden vor
|
||
Sonnenaufgang" landet am Vorabend, und das ist so gewollt — abgeschnitten
|
||
wären solche Angaben gar nicht mehr formulierbar. Verglichen wird die Uhrzeit
|
||
innerhalb des Tages; ein Ziel jenseits von Mitternacht gilt als diese Uhrzeit
|
||
am selben Tag. Bei `+` und `-` ist das genau der gemeinte Zeitpunkt, bei `ab`
|
||
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 Tag überhaupt zählt — heute,
|
||
oder bei einer Vorabend-Regel morgen (siehe [Am Vorabend](#am-vorabend)).
|
||
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.
|
||
|
||
### 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.
|
||
|
||
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
|
||
**pendelt**: „Temperatur > 22" bei 22,1 / 21,9 / 22,1 °C ist jedes Mal eine
|
||
echte steigende Flanke, und über MQTT können die Werte im Sekundentakt
|
||
hereinkommen. `automations.lockout_secs` sagt, wie lange nach einer Auslösung
|
||
nicht wieder geschaltet wird. Der Editor bietet drei Stufen an:
|
||
|
||
| | | gedacht für |
|
||
|---|---|---|
|
||
| Ohne | 0 s | volle Geschwindigkeit, jede Flanke schaltet |
|
||
| Kurz | 60 s | Licht, Farbe, Dimmwert |
|
||
| Lang | 900 s | Rollläden, Ventile, alles mit Motor |
|
||
|
||
Gespeichert werden Sekunden, angeboten werden nur die drei Stufen — eine
|
||
vierte ist damit eine Zeile in `lockoutChoices()` und keine Wanderung durch
|
||
die Datenbank.
|
||
|
||
Eine Flanke innerhalb der Sperrzeit wird **verworfen, nicht aufgehoben**. Ein
|
||
Rollladen, der eine Viertelstunde später doch noch losfährt, weil vor langer
|
||
Zeit einmal eine Schwelle gestreift wurde, wäre unangenehmer als einer, der
|
||
gar nicht fährt — und der nächste echte Anlass nach Ablauf der Sperre kommt
|
||
ohnehin durch. Verworfene Flanken stehen im Log auf `DEBUG`, nicht in
|
||
`automation_log`; bei einem zappelnden Sensor wäre die Tabelle sonst voll
|
||
davon.
|
||
|
||
`force_once` ist von der Sperre nicht betroffen: es greift nur, wenn im
|
||
Fenster gar nichts gelaufen ist — dann ist auch keine Sperre aktiv.
|
||
|
||
## Eine Automatik löst die nächste aus
|
||
|
||
Der Anlass war: *zehn Minuten nach dem Wecker den Rollladen hoch — aber nur,
|
||
wenn es dann schon hell ist.*
|
||
|
||
Der naheliegende Weg wäre eine Verzögerung an der Aktion gewesen. Er trägt
|
||
nicht: die Zusatzbedingung gilt erst zum **späteren** Zeitpunkt, und eine
|
||
verzögerte Aktion, die selbst noch Bedingungen prüfen muss, ist keine Aktion
|
||
mehr — sie bräuchte ein zweites Bedingungssystem neben dem ersten. Der zweite
|
||
Schritt ist also eine eigene Automatik mit eigenen Rahmenbedingungen, und was
|
||
ihr fehlte, war nur ein Weg, sich auf die erste zu beziehen.
|
||
|
||
Den gibt jetzt das gerechnete Gerät **„Automatiken"**, Gegenstück zum
|
||
vorhandenen „Zeitpunkt". Jede Automatik ist dort ein Messwert, ihr Wert ist
|
||
der Zeitpunkt der letzten Auslösung:
|
||
|
||
```
|
||
Wecker Magdalena um 05:50 → Licht auf Wakeup
|
||
Wecker Magdalena Rollos Wecker Magdalena ab + 00:10
|
||
UND Sonne Ost > 200 Lux → Rollläden auf
|
||
Schlafzimmer morgens Wecker Magdalena Rollos ab + 00:00 → Rollladen auf
|
||
```
|
||
|
||
Der Editor braucht dafür keine Zeile Änderung. Er listet Geräte und deren
|
||
Messwerte — „Automatiken" ist dann ein Gerät wie jedes andere.
|
||
|
||
### Der Datentyp `elapsed`
|
||
|
||
Er verhält sich zum Auslösezeitpunkt wie `deltatime` zum Sonnenaufgang: der
|
||
Messwert ist der Bezugspunkt, die Schwelle der Versatz, das Vorzeichen steckt
|
||
im Operator. Drei Formen, alle „danach" — ein Minus gibt es nicht, vor dem
|
||
Auslöser kann nichts liegen, das erst der Auslöser anstößt:
|
||
|
||
| | wahr, wenn |
|
||
|---|---|
|
||
| `+ 00:10` | zehn Minuten nach der Auslösung, noch `catchup_minutes` lang |
|
||
| `ab + 00:10` | von da an bis Mitternacht |
|
||
| `vor + 00:10` | in den ersten zehn Minuten danach |
|
||
|
||
Gerechnet wird mit dem **echten Abstand**, nicht mit der Uhrzeit innerhalb des
|
||
Tages. Das ist der eine Unterschied zu `deltatime`, und er ist nötig: sonst
|
||
machte ein Lauf von vorgestern um 05:50 die Bedingung heute um 06:00 wahr, an
|
||
einem Tag, an dem der Auslöser gar nicht lief.
|
||
|
||
Die offene Form `ab +` endet trotzdem am Tagesrand — sonst wäre sie morgen
|
||
früh immer noch wahr, obwohl seither nichts geschehen ist. Ein Auslöser um
|
||
23:55 trägt seinen Nachfolger deshalb nicht über Mitternacht; dieselbe
|
||
Einschränkung hat `ab 23:55` auch. Die Punktform `+` braucht den
|
||
Tagesvergleich nicht und trägt darüber hinweg.
|
||
|
||
### Reihenfolge, Pause, Kreise
|
||
|
||
**Ausgewertet wird topologisch**, Auslöser vor Nachfolger. Nur damit wirkt ein
|
||
Versatz von null noch im selben Takt; bei jedem anderen Versatz wäre die
|
||
Reihenfolge gleichgültig, zehn Minuten sind länger als ein Takt. Dazu trägt
|
||
`ausloesen()` den eigenen Auslösewert sofort nach, statt bis zum nächsten
|
||
`werte_einsammeln()` zu warten.
|
||
|
||
**Eine pausierte Automatik hält ihre Nachfolger mit an.** `Regelwerk.laden()`
|
||
holt nur `enabled = 1`, der Transport liefert für alle übrigen einen leeren
|
||
Wert, und der gilt jeder Bedingung als unerfüllt. Wer den Wecker pausiert,
|
||
will morgens auch den Rollladen unten lassen.
|
||
|
||
**Kreise lehnt der Editor beim Speichern ab** (`pruefeKreis()` in
|
||
`restricted/automations.php`). Im Betrieb wären sie kaum zu bemerken: die
|
||
Sperrzeit begrenzt sie auf eine Auslösung je `lockout_secs`, und im Protokoll
|
||
sieht das aus wie eine Automatik, die halt oft läuft. Der Runner meldet einen
|
||
Kreis trotzdem — falls doch einer an der Datenbank vorbei entsteht — und
|
||
wertet die Beteiligten dann in ihrer ursprünglichen Reihenfolge aus. Sie
|
||
stillzulegen wäre schlimmer: eine wortlos abgeschaltete Automatik fällt
|
||
niemandem auf.
|
||
|
||
### Löschen ist die gefährliche Stelle
|
||
|
||
`fk_cond_state` steht auf `ON DELETE CASCADE`. Verschwindet der Messwert,
|
||
verschwindet die Bedingung — still. Bei der **letzten** Bedingung fängt
|
||
`gruppen_erfuellt()` das ab („eine Automatik ohne Bedingungen löst nie aus").
|
||
Bei **einer von zweien** fängt es niemand: aus *zehn Minuten nach dem Wecker
|
||
UND es ist hell* würde ein bloßes *es ist hell*, und der Rollladen führe ab
|
||
morgen jeden Tag bei Sonnenaufgang hoch.
|
||
|
||
Deshalb fragt der Editor vor dem Löschen, und der Runner räumt nur auf, was
|
||
niemand mehr benutzt:
|
||
|
||
| | |
|
||
|---|---|
|
||
| **Abhängen** | die Bedingung wird entfernt **und der Nachfolger pausiert**. Eine pausierte Automatik mit sichtbarer Lücke ist besser als eine stille Regeländerung. |
|
||
| **Alle löschen** | der Nachfolger geht mit, und dessen Nachfolger auch. |
|
||
|
||
Automatisch mitzulöschen wäre die falsche Vorgabe — „Wecker weg, Rollladen
|
||
still weg" bemerkt man erst im Winter.
|
||
|
||
### Die Messwerte pflegt der Runner
|
||
|
||
`ausloeser_nachfuehren()` legt bei jedem Laden des Regelwerks für **jede**
|
||
Automatik einen Messwert an, auch für pausierte, und zieht den Namen nach.
|
||
Geführt wird nach der Kennung (`auto:15`) und nicht nach dem Namen: wer
|
||
umbenennt, soll die abhängigen Bedingungen behalten.
|
||
|
||
Für pausierte muss der Messwert stehen bleiben, sonst löschte ein Pausieren
|
||
über dieselbe Kaskade die Bedingung des Nachfolgers, und beim Fortsetzen wäre
|
||
sie weg.
|
||
|
||
## Transporte
|
||
|
||
Welcher Weg zum Gerät führt, entscheidet die URL des Aktors in `actors`:
|
||
|
||
| URL | Messwert (`actor_states.url`) | Kommando (`actor_commands.command_url`) |
|
||
|---|---|---|
|
||
| `mqtt://…` | vollständiges Topic, abonniert; bei mehreren Messwerten je Topic zusätzlich `value_path` | Nutzlast auf das Parameter-Topic |
|
||
| `http://…` | Feldname in der JSON-Antwort, gepollt | Abfrageargumente an die Geräte-URL (`turn=on`) |
|
||
| `wled://…` | Pfad in `/json/state` (`seg[0].col[0]`), gepollt | JSON-Vorlage mit Platzhaltern, als Ganzes gesendet |
|
||
| Tahoma | Statusname (`core:ClosureState`), gepollt | `exec/apply` an die Box |
|
||
| `Logic` | gerechnet: Uhrzeit, Datum, Sonne | – |
|
||
| `Automatik` | gerechnet: je Automatik ihre letzte Auslösung | – |
|
||
|
||
Alle sechs stehen in `transports.py`. Eine siebte Geräteart kommt als weitere
|
||
Klasse dazu; sie braucht `passt()`, `zustaende_lesen()` und `senden()`.
|
||
|
||
Tahoma ist der einzige, der nicht am URL-Schema erkannt wird, sondern an der
|
||
**Box-Kennung** in der URL. Das Schema beschreibt dort die Funkart, und
|
||
dieselbe Box liefert `io://` für die Jalousien, `rts://` für die Dachfenster
|
||
und `internal://` für die Alarmanlage. Ohne `pin` in der `config.ini` ist
|
||
niemand zuständig — dann meldet der Runner beim Auslösen „kein Transport",
|
||
statt still nichts zu tun.
|
||
|
||
Mehrere Messwerte teilen sich oft **ein Topic**: der go-eCharger schickt
|
||
sechzehn Zahlen als JSON-Feld auf `…/nrg`, und erst das `value_template` der
|
||
Home-Assistant-Discovery sagt, dass „Strom L1" das fünfte Element ist. Diese
|
||
Angabe steht in `actor_states.value_path` — in derselben Schreibweise, die
|
||
auch WLED benutzt: `[4]`, `ssid`, `seg[0].col[0]`. Ohne Pfad gilt die ganze
|
||
Nutzlast.
|
||
|
||
Gelesen wird nur der einfache Fall aus dem Template: ein Zugriff auf
|
||
`value_json` und was danach an Punkten und Klammern folgt.
|
||
|
||
### Werttabellen
|
||
|
||
Manche Geräte schicken eine Zahl und meinen einen Zustand:
|
||
|
||
```
|
||
{{ ['Unknown','Idle','Charging','WaitCar','Complete','Error'][value_json|int] }}
|
||
{{ ['Default','Eco','NextTrip'][value_json|int-3] }}
|
||
```
|
||
|
||
Das ist kein Pfad, sondern eine Übersetzung von Zahl nach Text. Sie landet in
|
||
`possible_values` — in der Schreibweise, die WLED für seine Effektliste schon
|
||
benutzt: eine Liste aus `{Wert: Bezeichnung}`. Ein Versatz im Ausdruck wandert
|
||
dabei in die Schlüssel, aus `[value_json|int-3]` wird also `{"3":"Default"}`.
|
||
|
||
Der Runner übersetzt beim Lesen: aus der gesendeten `2` wird `Charging`. Eine
|
||
Bedingung vergleicht damit genau den Klartext, den der Editor zur Auswahl
|
||
stellt. Steht die Zahl nicht in der Tabelle, bleibt sie stehen — ein
|
||
erfundener Name wäre schlimmer als ein roher Wert.
|
||
|
||
Auf beiden Seiten des Editors steckt dieselbe Tabelle, aber der gespeicherte
|
||
Wert ist ein anderer:
|
||
|
||
| | angezeigt | gespeichert |
|
||
|---|---|---|
|
||
| **Messwert** (Bedingung) | `Charging` | `Charging` — der Runner hat schon übersetzt |
|
||
| **Parameter** (Aktion) | `Blink` | `1` — das Gerät will die Zahl |
|
||
|
||
Bei WLED trägt die Kommando-Vorlage alles: `{"seg":[{"col":[[%red%,%green%,%blue%]]}]}`
|
||
wird mit den Parameterwerten gefüllt und am Stück geschickt. Deshalb haben die
|
||
Parameter dort keine eigene URL — ihr Name *ist* der Platzhalter.
|
||
|
||
## Voraussetzungen
|
||
|
||
* Python 3 mit `pymysql`, `requests`, `paho-mqtt`
|
||
* `homeMesh_automations.sql` einmal eingespielt
|
||
* Im Web-Verzeichnis muss in `restricted/deviceDiscovery/config.ini`
|
||
**`clear_tables = false`** stehen.
|
||
Discovery schreibt mit `ON DUPLICATE KEY UPDATE` auf den URLs, das Leeren ist
|
||
unnötig — ein `TRUNCATE` würde dagegen die Geräte-IDs neu vergeben, und die
|
||
Automatiken zeigen per Fremdschlüssel genau auf diese IDs.
|
||
|
||
## Einrichten
|
||
|
||
```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
|
||
mysql -h 127.0.0.1 -P 3310 -u homeMesh -p homeMesh < vorabend.sql
|
||
python3 fetch_calendar.py # Feiertage und Ferien holen
|
||
python3 autoaction_runner.py --once --dry-run --verbose # Probelauf
|
||
```
|
||
|
||
`automatik_ausloeser.sql` legt den Datentyp `elapsed` und das gerechnete
|
||
Gerät „Automatiken" an — beides braucht die Verkettung, siehe oben. Die
|
||
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.
|
||
`vorabend.sql` legt `next_day` an. Alle drei 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.
|
||
|
||
## Wo das läuft
|
||
|
||
Der Runner ist ein Hintergrundprozess und wohnt deshalb beim SolarManager,
|
||
nicht im Web-Verzeichnis:
|
||
|
||
```
|
||
/volume1/homes/wagner/SolarManager/
|
||
├── solarManager.py
|
||
├── gatherRainData.py
|
||
├── startSolarServer.sh startet alle drei, siehe unten
|
||
└── autoActions/
|
||
├── autoaction_runner.py
|
||
├── transports.py
|
||
├── fetch_calendar.py
|
||
├── automatik_ausloeser.sql einmalig, siehe Einrichten
|
||
├── rahmen_erweitern.sql einmalig, siehe Einrichten
|
||
├── vorabend.sql einmalig, siehe Einrichten
|
||
└── config.ini Zugangsdaten, nicht im Git
|
||
```
|
||
|
||
Das Web-UI kennt diesen Pfad nicht — Browser und Runner reden ausschließlich
|
||
über die Datenbank `homeMesh` miteinander. Der Runner lädt das Regelwerk nach,
|
||
sobald im Browser etwas gespeichert wurde; ein Neustart nach jeder Änderung ist
|
||
nicht nötig.
|
||
|
||
`startSolarServer.sh` startet `solarManager.py`, den Runner und
|
||
`gatherRainData.py` gemeinsam und beendet vorher, was schon läuft. Aufgerufen wird es beim Booten (auf der
|
||
Synology über den Aufgabenplaner, Ereignis „Hochfahren", als root); dasselbe
|
||
Skript von Hand aufzurufen ist der normale Weg, den Runner neu zu starten —
|
||
als `wagner` genügt, ohne sudo. Über SSH abgekoppelt, damit die Prozesse das
|
||
Abmelden überleben:
|
||
`setsid nohup bash startSolarServer.sh > /tmp/restart_solar.out 2>&1 < /dev/null &`.
|
||
Zwei Instanzen dürfen nie gleichzeitig laufen — sie würden jedes Kommando
|
||
doppelt schicken und sich gegenseitig vom MQTT-Broker werfen, weil beide
|
||
dieselbe Client-Kennung benutzen. Genau davor schützt das Beenden am Anfang.
|
||
|
||
Ausgabe landet in `autoActions.log` neben `solarOutput.log`. `SIGTERM` fängt
|
||
der Runner ab und fährt geordnet herunter.
|
||
|
||
Nachzulesen ist beides im Dashboard unter **Protokolle** — die Seite liest die
|
||
Dateien direkt, filterbar nach Text und Level. Klein gehalten werden sie von
|
||
`logs_rotieren.sh` samt `logrotate.conf` eine Ebene höher: täglich, vierzehn
|
||
Stände, alles über 20 MB sofort. Weil die Prozesse monatelang durchlaufen,
|
||
arbeitet die Rotation mit `copytruncate` — deshalb leiten die Startskripte mit
|
||
`>>` um und nicht mehr mit `&>`. Wer das zurückdreht, bekommt nach der
|
||
nächsten Rotation eine Logdatei, die vorn aus Nullbytes besteht.
|
||
|
||
Die lauteste dieser Dateien war `wattpilotshell.log`, rund ein MB am Tag. Die
|
||
Wallbox schickt in ihren Statusnachrichten 34 Felder, die die
|
||
API-Beschreibung der Bibliothek nicht kennt (`clea`, `cle`, `opad`, die ganze
|
||
`ocpp*`-Familie) — wattpilot 0.2 ist seit Mai 2022 die letzte
|
||
Veröffentlichung, ein Update behebt das also nicht. Die Bibliothek schlug
|
||
jedes Feld ungeprüft nach und lief in einen `KeyError`, der die restliche
|
||
Schleife mitriss: alles, was in derselben Nachricht dahinter stand, wurde nie
|
||
veröffentlicht — im Dauerbetrieb alle 15 Sekunden `fhz`, `lps` und `tpcm`,
|
||
nach einem Verbindungsaufbau deutlich mehr. Dieselbe Ursache aus der
|
||
anderen Richtung traf `lot`: als `integer` beschrieben, als Objekt geschickt,
|
||
von paho abgelehnt — gut vier von zehn Zeilen im alten Log.
|
||
`wattpilot_bruecke.py` ersetzt beide betroffenen Funktionen zur Laufzeit,
|
||
überspringt unbekannte Felder und verschickt Werte notfalls als JSON, jedes
|
||
Feld einmal mit einer Warnung im Log. `startWattpilotMQTT.sh` startet seither
|
||
die Brücke statt der `wattpilotshell` direkt.
|
||
|
||
Seit dem 04.09.2026 trägt dieselbe Brücke auch die Gegenrichtung. Sie nimmt
|
||
Kommandos auf `wattpilot/properties/<schlüssel>/set` entgegen und meldet auf
|
||
`<schlüssel>/result`, ob die Wallbox sie genommen hat — dasselbe Muster, das
|
||
der go-e von Haus aus spricht. Das Dashboard steuert damit beide Wallboxen
|
||
über einen Weg (`restricted/wallboxen.php`) und startet für die Wattpilot
|
||
keinen eigenen Python-Prozess mehr. Nötig waren dafür drei Reparaturen an der
|
||
Bibliothek: Zahlen kamen als Zeichenkette bei der Wallbox an
|
||
(`value must be uint8_t`), ein unbekannter Name und jede Eigenschaft ohne
|
||
`rw`-Angabe — `ftt` und `fte` zum Beispiel — töteten den paho-Callback, und
|
||
eine Rückmeldung gab es überhaupt nicht.
|
||
|
||
`fetch_calendar.py` gehört einmal jährlich in den Cron:
|
||
|
||
```
|
||
0 4 1 1 * /usr/bin/python3 /volume1/homes/wagner/SolarManager/autoActions/fetch_calendar.py
|
||
```
|
||
|
||
Ein zusätzlicher Lauf im Herbst schadet nicht — die Ferientermine des
|
||
übernächsten Schuljahres stehen erst später fest.
|
||
|
||
## Nachsehen, was passiert ist
|
||
|
||
`automation_log` hält je Auslösung fest, ob sie durchlief (`fired`), wegen
|
||
`force_once` nachgeholt wurde (`forced`) oder scheiterte (`error`, mit Grund in
|
||
`detail`). Einträge älter als 30 Tage räumt der Runner selbst weg.
|