Files
Smart-Dashboard/restricted/autoActions/README.md
T
adminandClaude Opus 5 631baa7333 Discovery: alle Geraete finden und stabil ablegen
Ein Lauf mit allen Modulen brachte 54 Geraete und 441 Messwerte - und fuenf
Fehler ans Licht, die vorher niemand sehen konnte, weil clear_tables die
Tabellen bei jedem Lauf geleert hat.

Vier davon sind Schluessel- und Upsert-Fehler derselben Familie:

 * command_parameters war ueber (command_id, url) eindeutig. WLED-Parameter
   haben keine URL - ihr Name ist der Platzhalter in der Kommando-Vorlage -,
   und eine NULL kollidiert in MySQL nie. ON DUPLICATE KEY UPDATE griff
   also nicht, und jeder Lauf legte dieselben Parameter erneut an: aus 18
   wurden nach drei Laeufen 54. Schluessel jetzt (command_id,
   parameter_name).

 * actor_states war ueber (actor_id, url) eindeutig. Umgekehrtes Problem:
   Messwerte, die sich ein Topic teilen, ueberschrieben einander. Der
   go-eCharger schickt sechzehn Werte als JSON-Feld auf einem Topic - von
   denen kam genau einer in der Datenbank an. Schluessel jetzt (actor_id,
   state_name), das bringt 50 verlorene Messwerte zurueck.

 * Kommandos, Messwerte und Parameter aktualisierten ihre URL beim
   Wiederholungslauf nicht - sie stand nicht im UPDATE-Teil. Eine Korrektur
   in einem Modul kam damit nie in einer bestehenden Datenbank an.

 * Der Typ eines Kommando-Parameters wurde als Text in eine int-Spalte
   geschrieben. MariaDB macht daraus stillschweigend 0, und 0 ist "bool" -
   deshalb bot der Editor fuer die WLED-Helligkeit (0..255) ein Ja/Nein an.

Der fuenfte: die Shelly-Gen1-Relais hatten weder eine Kommando-URL noch eine
URL am Zustand. Ohne die weiss niemand, was zu schicken und wo nachzusehen
ist. Gen1 schaltet ueber ?turn=on|off|toggle und meldet sich in "ison".

Damit entfaellt auch die Uebersetzungstabelle im HTTPTransport: die
Zuordnung gehoert ins Geraetemodell, nicht in den Runner.

Zwei Luecken auf der Runner-Seite, die derselbe Lauf gezeigt hat:

 * WLED liess sich gar nicht ansteuern - es gab keinen Transport dafuer.
   Der neue nutzt die JSON-Vorlage aus command_url, fuellt die Platzhalter
   und schickt sie am Stueck an /json/state.

 * Tahoma wurde am Schema "io://" erkannt. Das Schema beschreibt aber die
   Funkart: dieselbe Box liefert rts:// fuer die Dachfenster und
   internal:// fuer die Alarmanlage. Drei von 22 Geraeten fielen durch.
   Erkannt wird jetzt an der Box-Kennung in der URL.

Nachgemessen: zwei Laeufe hintereinander aendern keine einzige Zeile mehr,
die Geraete-IDs bleiben ueber Laeufe stabil (Voraussetzung fuer die
Fremdschluessel der Automatiken), alle 54 Geraete finden genau einen
Transport, und 440 der 441 Messwerte sind tatsaechlich lesbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 14:57:29 +02:00

179 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | 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.
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.