Manche Geraete schicken eine Zahl und meinen einen Zustand - der
go-eCharger etwa 2 fuer "Charging". Welche Zahl welchen Namen hat, steht im
value_template der Home-Assistant-Discovery als Werttabelle:
{{ ['Unknown','Idle','Charging','WaitCar','Complete','Error'][value_json|int] }}
Das ist kein Pfad in die Nutzlast, sondern eine Uebersetzung, und sie wurde
bisher verworfen. Jetzt landet sie in possible_values - in derselben
Schreibweise, die WLED fuer seine Effektliste schon benutzt, naemlich einer
Liste aus {Wert: Bezeichnung}. Damit versteht der Editor sie ohne
Zusatzarbeit und macht eine Auswahlliste daraus. Ein Versatz im Ausdruck
wandert in die Schluessel: aus [value_json|int-3] wird {"3":"Default"}.
Der Runner uebersetzt beim Lesen, eine Bedingung vergleicht also den
Klartext. Steht die Zahl nicht in der Tabelle, bleibt sie stehen - ein
erfundener Name waere schlimmer als ein roher Wert.
Dabei ist ein Folgefehler aufgefallen: Auswahlwerte in dieser Schreibweise
kamen im Editor als "[object Object]" an. Die alte addOptions kannte die
Form, der neue Modell-Aufbau nicht - aufgefallen ist es nie, weil WLED beim
Umbau abgeschaltet war. Der Katalog liefert Auswahlwerte jetzt einheitlich
als {value, label}, und zwar seitenrichtig:
Messwert angezeigt Charging, gespeichert Charging
(der Runner hat schon uebersetzt)
Parameter angezeigt Blink, gespeichert 1
(das Geraet will die Zahl)
Die Uebersicht loest die Bezeichnung ebenfalls auf: "Effekt (Blink)" statt
"Effekt (1)".
Nachgemessen: alle fuenf Werttabellen auf dem Broker richtig erkannt,
einschliesslich des Versatzes, kein Fehltreffer unter den 35 uebrigen
Vorlagen. Live liefert der go-eCharger jetzt Idle, Neutral, Auto, Eco und
None statt 1, 0, 0, 4, 0. Ein weiterer Discovery-Lauf aendert keine Zeile.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
216 lines
10 KiB
Markdown
216 lines
10 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.
|
||
|
||
## 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 (gedrosselt auf einmal je Minute), wovon
|
||
auch der Editor profitiert: er zeigt neben jedem Messwert den aktuellen Stand.
|
||
|
||
## 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` | genau in dieser Minute |
|
||
| `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. `ab`
|
||
ist trotzdem das robustere: fällt der Runner in genau der Minute aus, auf die
|
||
`um` zeigt, ist die Automatik für den Tag verloren — bei `ab` holt der nächste
|
||
Takt es nach. Wer `um` braucht und den Ausfall nicht riskieren will, hakt
|
||
zusätzlich `force_once` an. Die breiten Zeitfenster in `auto_watering.py`
|
||
folgen derselben Überlegung.
|
||
|
||
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.
|
||
|
||
Ü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.
|
||
|
||
## 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.
|
||
|
||
## 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 | – |
|
||
|
||
Alle fünf stehen in `transports.py`. Eine sechste 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
|
||
* In `../deviceDiscovery/config.ini` muss **`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
|
||
python3 fetch_calendar.py # Feiertage und Ferien holen
|
||
python3 autoaction_runner.py --once --dry-run --verbose # Probelauf
|
||
```
|
||
|
||
`--dry-run` schaltet nichts, protokolliert aber jedes Kommando, das geschickt
|
||
würde. `--once` macht einen einzigen Durchlauf.
|
||
|
||
Im Dauerbetrieb wird `autoaction_runner.py` beim Booten gestartet (auf der
|
||
Synology über den Aufgabenplaner, Ereignis „Hochfahren", als root). Er
|
||
verbindet sich selbst neu, wenn MQTT wegbricht, und lädt das Regelwerk nach,
|
||
sobald im Browser etwas gespeichert wurde — ein Neustart nach jeder Änderung
|
||
ist nicht nötig.
|
||
|
||
`fetch_calendar.py` gehört einmal jährlich in den Cron:
|
||
|
||
```
|
||
0 4 1 1 * /usr/bin/python3 /volume1/web/smart/restricted/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.
|