Files
Smart-Dashboard/restricted/autoActions
adminandClaude Opus 5 d557a1651f Neigungs-Umweg gilt fuer jedes Kommando, nicht nur setOrientation
Die Kugelschreiber-Mechanik haengt nicht am Kommando, sondern an der
Neigung selbst - "Position+Neigung" braucht den Umweg ueber 0 % genauso wie
"Neigung". Erkannt wird er deshalb am Parameter und nicht am Kommandonamen:
beide Kommandos nennen ihren Neigungsparameter "Neigung", das
Geraetemodell liefert die Zuordnung also mit.

Geprueft im Probelauf in beiden Versendern: Position+Neigung mit 20 % geht
direkt raus, mit 90 % mit vorgeschaltetem Umweg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 20:14:05 +02:00
..
2026-08-31 15:44:39 +02:00
2026-08-31 15:44:39 +02:00

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

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.