Dasselbe <input type="time"> wie von/bis daneben, nur als Dauer gelesen -
"03:30" tippt sich leichter als "210" und steht in derselben Zeile wie die
beiden Uhrzeiten darueber. Dass es keine Uhrzeit meint, sagt der Text
ringsum ("erst nach ... am Stueck"); die Kopfzeile des Rahmens schreibt es
beim Tippen aus.
Die Obergrenze ist damit 23:59 statt 24 Stunden - genau das, was ein
Zeitfeld hergibt.
Dabei aufgefallen: haltezeitKurz() rundete auf Zehntelstunden und machte aus
23:59 die Auskunft "24 Std.". Eine Haltezeit, die laenger aussieht als sie
ist, ist genau die falsche Auskunft - krumme Werte stehen jetzt als
"23 Std. 59 Min." da, halbe behalten ihr Komma.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
453 lines
21 KiB
Markdown
453 lines
21 KiB
Markdown
# Die Automatiken (AutoActions)
|
||
|
||
Eine Automatik ist ein Satz: **„Wenn … , dann …, aber nur wenn der Rahmen
|
||
passt.“** Sie wird im Browser gebaut, liegt in der Datenbank `homeMesh` und
|
||
wird von einem Dauerprozess auf der NAS ausgeführt.
|
||
|
||
```
|
||
Rahmen Auslöser Aktionen
|
||
────── ──────── ────────
|
||
Mo–Fr, Uhrzeit um 07:00 Magdalena Fenster: Auf
|
||
nicht in Ferien, UND Außentemperatur > −10 °C Magdalena Tür: Auf
|
||
06:00–09:00,
|
||
Sperrzeit 15 min
|
||
```
|
||
|
||
Beteiligt sind drei Stellen:
|
||
|
||
| Wo | Datei | Aufgabe |
|
||
|---|---|---|
|
||
| Browser | `js/solar/autoActionFuncs.js` | Editor: Modell bauen, zeichnen, abschicken |
|
||
| Web-Server | `restricted/automations.php`, `ajax/AutoAction.php` | Laden, Prüfen, Speichern, Beschreiben |
|
||
| NAS | `SolarManager/autoActions/autoaction_runner.py` | Auswerten und Ausführen |
|
||
|
||

|
||
|
||
---
|
||
|
||
## 1. Datenmodell
|
||
|
||
Alles steht in `homeMesh` — dort, wo auch die Geräte liegen, damit
|
||
Fremdschlüssel greifen können. Schema: [`homeMesh_automations.sql`](../homeMesh_automations.sql).
|
||
|
||
```mermaid
|
||
erDiagram
|
||
actors ||--o{ actor_states : "hat Messwerte"
|
||
actors ||--o{ actor_commands : "hat Kommandos"
|
||
actor_commands ||--o{ command_parameters : "hat Parameter"
|
||
|
||
automations ||--o{ automation_conditions : "Auslöser"
|
||
automations ||--o{ automation_actions : "Aktionen"
|
||
automations ||--o{ automation_log : "Protokoll"
|
||
automation_actions ||--o{ automation_action_params : "Werte"
|
||
|
||
actor_states ||--o{ automation_conditions : "state_id"
|
||
actor_commands ||--o{ automation_actions : "command_id"
|
||
command_parameters ||--o{ automation_action_params : "parameter_id"
|
||
|
||
automations {
|
||
int id
|
||
varchar name
|
||
varchar floor "Reiter in der Übersicht"
|
||
tinyint enabled "pausiert oder nicht"
|
||
time window_from "Rahmen: Zeitfenster"
|
||
time window_to
|
||
tinyint weekdays "Bitmaske Mo..So"
|
||
tinyint on_vacation "0 nie, 1 egal, 2 zusätzlich"
|
||
tinyint on_holiday "0 nie, 1 egal, 2 zusätzlich"
|
||
tinyint next_day "Rahmen gilt für morgen"
|
||
tinyint force_once "am Fensterende notfalls doch"
|
||
tinyint once_per_day "höchstens einmal am Tag"
|
||
int lockout_secs "Sperrzeit nach einem Lauf"
|
||
int hold_secs "Haltezeit vor dem Auslösen"
|
||
tinyint cond_met "Laufzustand: war die Bedingung zuletzt wahr?"
|
||
datetime last_run "Laufzustand: wann zuletzt ausgelöst"
|
||
timestamp changed "Signal an den Runner"
|
||
}
|
||
automation_conditions {
|
||
int automation_id
|
||
int group_no "gleich = UND, verschieden = ODER"
|
||
int state_id "zeigt auf actor_states"
|
||
enum operator "ASCII, nie das hübsche Zeichen"
|
||
varchar value "Schwelle als Text"
|
||
}
|
||
```
|
||
|
||
Vier Felder sind **nachgerüstet** und stehen deshalb nicht in der
|
||
Schema-Datei, sondern in eigenen Skripten im SolarManager-Repo
|
||
(`autoActions/`): `next_day` (`vorabend.sql`), `once_per_day` und die
|
||
Dreiwertigkeit von `on_vacation`/`on_holiday` (`rahmen_erweitern.sql`),
|
||
`hold_secs` (`haltezeit.sql`), sowie der Datentyp `elapsed` samt gerechnetem
|
||
Gerät „Automatiken“ (`automatik_ausloeser.sql`). Wer die Datenbank neu
|
||
aufsetzt, spielt sie nach `homeMesh_automations.sql` ein.
|
||
|
||
Drei Spalten in `automations` sind **kein Regelwerk, sondern Laufzustand**:
|
||
`cond_met`, `last_run` und `changed`. Sie werden vom Runner geschrieben.
|
||
Wer eine Automatik per SQL kopiert, sollte sie leeren.
|
||
|
||
### Warum `state_id` und nicht „Gerät + Feld“
|
||
|
||
Eine Bedingung zeigt mit einer einzigen Zahl auf `actor_states`. Damit hängen
|
||
Gerät, Messwertname, Datentyp, Einheit und Quelle (Topic, HTTP-Feld, Tahoma)
|
||
an dieser Zeile. Der Editor muss nichts davon wissen: Er listet Geräte und
|
||
deren Messwerte. Genau deshalb funktioniert auch die Verkettung
|
||
(→ [Abschnitt 6](#6-verkettung-eine-automatik-löst-die-nächste-aus)) ohne eine
|
||
einzige Sonderregel im Editor.
|
||
|
||
---
|
||
|
||
## 2. Der Editor
|
||
|
||
Der Editor arbeitet **auf einem Modell und zeichnet daraus die Oberfläche** —
|
||
nicht umgekehrt:
|
||
|
||
```js
|
||
autoModel.groups = [ [ {state_id, operator, value}, … ], … ] // innen UND, außen ODER
|
||
autoModel.actions = [ {command_id, params: {parameterID: wert}}, … ]
|
||
```
|
||
|
||
Der Gerätekatalog (`deviceCatalog()`) kommt **im selben Dokument** mit, es
|
||
gibt also kein Nachladen je Zeile.
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
autonumber
|
||
participant B as Browser<br/>autoActionFuncs.js
|
||
participant A as ajax/AutoAction.php
|
||
participant M as restricted/automations.php
|
||
participant DB as homeMesh
|
||
|
||
B->>A: GET ?action=editor&id=42
|
||
A->>M: loadAutomation(42) + deviceCatalog()
|
||
M->>DB: SELECT automations / conditions / actions
|
||
A-->>B: HTML + Katalog + Datensatz (ein Dokument)
|
||
Note over B: autoModel füllen, rendern
|
||
|
||
loop alle 8 s, solange der Editor offen ist
|
||
B->>A: GET ?action=werte&ids=…
|
||
A->>DB: SELECT current_value FROM actor_states
|
||
A-->>B: aktuelle Messwerte → Punkte neben den Zeilen
|
||
end
|
||
|
||
B->>A: POST ?action=save (JSON)
|
||
A->>M: saveAutomation()
|
||
M->>M: pruefeKreis()
|
||
M->>DB: UPDATE automations; DELETE+INSERT conditions/actions
|
||
A-->>B: {ok, id}
|
||
Note over DB: changed = NOW() → der Runner lädt neu
|
||
```
|
||
|
||
Drei Eigenheiten, die man kennen sollte:
|
||
|
||
* **Speichern heißt löschen und neu einfügen.** Bedingungen und Aktionen
|
||
bekommen dabei neue IDs. Deshalb reicht `changed` allein dem Runner nicht,
|
||
um Änderungen zu erkennen (→ [Abschnitt 4](#4-der-runner)).
|
||
* **Live-Werte neben jeder Bedingung** kommen aus `actor_states`, nicht aus
|
||
MQTT: Über den Broker kommt nur ein Teil der Geräte (Jalousien hängen an
|
||
der Tahoma-Box, gerechnete Werte an gar nichts). Eine Tabelle, ein Zugriff,
|
||
alle Gerätearten.
|
||
* **Kreise werden beim Speichern abgelehnt** (`pruefeKreis()`), nicht im
|
||
Betrieb geheilt. Im Protokoll sähe ein Kreis nur wie „läuft halt oft“ aus.
|
||
|
||
### Der Rahmen in Worten
|
||
|
||
`rahmenText()` im Editor und `describeConditions()` auf dem Server erzeugen
|
||
denselben Satz in Alltagssprache („werktags, 06:00–09:00, höchstens einmal
|
||
am Tag“). Wer die Bedeutung eines Feldes ändert, muss beide anfassen —
|
||
sonst steht in der Übersicht etwas anderes als im Editor.
|
||
|
||
---
|
||
|
||
## 3. Die drei Teile einer Automatik
|
||
|
||
### 3.1 Rahmen: darf sie überhaupt?
|
||
|
||
| Feld | Bedeutung | Fallstrick |
|
||
|---|---|---|
|
||
| `enabled` | pausiert oder nicht | Pausierte laden der Runner gar nicht erst — ihre Nachfolger stehen mit still |
|
||
| `weekdays` | Bitmaske, Bit 0 = Montag | |
|
||
| `on_vacation`, `on_holiday` | **dreiwertig**: 0 nie, 1 egal, 2 zusätzlich | „zusätzlich“ zählt wie ein passender Wochentag; ein Verbot schlägt eine Erweiterung |
|
||
| `next_day` | Der Rahmen gilt für **morgen** (Vorabend-Form) | Uhrzeit und Fenster bleiben bei heute |
|
||
| `window_from`/`window_to` | Zeitfenster; `von > bis` heißt über Mitternacht | |
|
||
| `lockout_secs` | Sperrzeit nach einem Lauf (0 / 60 / 900) | |
|
||
| `hold_secs` | Haltezeit: so lange muss die Bedingung **ununterbrochen** erfüllt sein (0 = sofort, frei als Stunden:Minuten bis 23:59) | Zählt im Speicher des Runners — ein Neustart fängt die Zeit von vorn an |
|
||
| `once_per_day` | höchstens ein Lauf je Kalendertag | |
|
||
| `force_once` | am Ende des Fensters notfalls doch ausführen | greift nur, wenn im Fenster gar nichts lief |
|
||
|
||
Warum dreiwertig? „Wochenenden **und** Feiertage“ ließ sich vorher nicht
|
||
schreiben: Die Wochentagsmaske kennt nur Sa und So, und ein Feiertag am
|
||
Dienstag ist eben ein Dienstag. Mit „Feiertage: zusätzlich“ kommt er über
|
||
den Kalender (`calendar_days`, gefüllt von `fetch_calendar.py`) herein.
|
||
|
||
Warum `next_day`? „Kinderrollos zu, wenn **morgen** Schule ist“ ließ sich mit
|
||
dem heutigen Tag nur annähern (So–Do, heute keine Ferien) — und das ging am
|
||
letzten Ferientag, am Abend vor einem Feiertag und am Abend eines Feiertags
|
||
daneben.
|
||
|
||
### 3.2 Auslöser: `any(all(gruppe))`
|
||
|
||
Bedingungen mit derselben `group_no` sind mit **UND** verknüpft, verschiedene
|
||
Gruppen mit **ODER**. Im Editor ist eine Gruppe ein gerahmter Block und
|
||
zwischen den Blöcken steht ein ODER — die Klammerung ist also gezeichnet und
|
||
nicht bloß vereinbart.
|
||
|
||
> Eine Automatik **ohne** Bedingung löst nie aus. Sonst würde sie nach einem
|
||
> Gerätesuchlauf, der ihren Messwert entfernt hat, plötzlich dauernd feuern.
|
||
|
||
### 3.3 Aktionen
|
||
|
||
Eine Aktion ist ein Kommando eines Geräts plus je Parameter ein Wert
|
||
(`automation_action_params`, eine Zeile je Parameter statt fester Spalten).
|
||
Geschickt wird über denselben Transport-Zoo wie beim Lesen
|
||
(→ [Abschnitt 4](#4-der-runner)); Jalousien bekommen dabei die
|
||
Kugelschreiber-Mechanik (erst Neigung 0, dann das Ziel), siehe
|
||
`TahomaTransport` und `sendeTahoma()` in `restricted/commands.php`.
|
||
|
||
---
|
||
|
||
## 4. Der Runner
|
||
|
||
`autoaction_runner.py` läuft als Dauerprozess.
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
START([Start]) --> LOAD["Regelwerk laden<br/><small>Automatiken, Messwerte, Kommandos</small>"]
|
||
LOAD --> SUB["MQTT abonnieren<br/>Sammlerfäden starten"]
|
||
SUB --> TAKT
|
||
|
||
subgraph TAKT["Takt, alle tick_seconds (10 s)"]
|
||
direction TB
|
||
T1["Werte einsammeln<br/><small>MQTT-Puffer + Queue der Sammler</small>"]
|
||
T2["durchlauf(): jede Automatik prüfen"]
|
||
T3["Ergebnisse des Versands verbuchen"]
|
||
T4["alle reload_seconds: Signatur vergleichen"]
|
||
T1 --> T2 --> T3 --> T4
|
||
end
|
||
|
||
T4 -- "Signatur geändert" --> LOAD
|
||
T2 -- "Aktionen" --> VERSAND[["Versand<br/><small>je Gerät der Reihe nach,<br/>über Geräte hinweg parallel</small>"]]
|
||
|
||
SAMMLER[["Sammler<br/><small>HTTP/WLED 1 min,<br/>Tahoma 5 min</small>"]] -. "Queue" .-> T1
|
||
```
|
||
|
||
**Warum ein Dauerprozess und nicht Cron?**
|
||
|
||
1. Schwellwerte („Temperatur über 22 °C“) sollen greifen, wenn die Nachricht
|
||
hereinkommt — nicht im nächsten Minutenraster.
|
||
2. Nur so gibt es einen Vorzustand für die steigende Flanke.
|
||
3. `actor_states.current_value` würde sonst niemand fortschreiben.
|
||
|
||
**Warum drei Fäden?** Früher wurde mitten in der Hauptschleife gepollt:
|
||
neunzehn Tahoma-Geräte nacheinander, jedes mit bis zu zehn Sekunden
|
||
Zeitlimit. Eine Runde dauerte 45 statt 30 Sekunden, dadurch kam **jede
|
||
dritte Minute nie vor** — und ein Auslöser „um 18:26“ wurde schlicht nie
|
||
wahr. Heute hat jeder Transport seinen eigenen Faden, die Hauptschleife
|
||
macht nur noch Billiges, und die Uhr wird gar nicht mehr abgetastet, sondern
|
||
gegen `datetime.now()` verglichen.
|
||
|
||
### Transporte
|
||
|
||
| URL des Aktors | Transport | Lesen | Schreiben |
|
||
|---|---|---|---|
|
||
| `mqtt://…` | MQTT | abonniert, gepuffert | `publish` |
|
||
| `http://…` | HTTP | Poll, 1 min | Abfrageargumente |
|
||
| `wled://…` | WLED | Poll, 1 min | JSON an `/json/state` |
|
||
| `io://`, `rts://`, `internal://`, `ogp://` | Tahoma | Poll, 5 min | `exec/apply` |
|
||
| `Logic` | gerechnet | Uhrzeit, Datum, Sonnenauf-/-untergang | — |
|
||
| `Automatik` | gerechnet | letzte Auslösung je Automatik | — |
|
||
|
||
Ausnahme beim Lesen: Steht in `actor_states.url` ein **Topic**, liest der
|
||
MQTT-Transport — auch wenn das Gerät über HTTP geschaltet wird. So sind die
|
||
Shellys der zweiten Generation angebunden.
|
||
|
||
### Woran der Runner merkt, dass er neu laden muss
|
||
|
||
Nicht an `changed` allein — eine gelöschte Automatik ändert den größten
|
||
Zeitstempel nicht, und wer nur die Uhrzeit einer Bedingung verstellt, ändert
|
||
in `automations` gar keine Spalte. Stattdessen eine **Signatur**: Zeilenzahl
|
||
plus Summe der `CRC32` je Zeile, über Bedingungen, Aktionen, Aktionsparameter
|
||
*und* die Gerätetabellen (dort auch `url` und `value_path`, weil ein
|
||
Discovery-Lauf bestehende Zeilen umschreibt).
|
||
|
||
---
|
||
|
||
## 5. Die Auswertung im Detail
|
||
|
||
Das Herzstück, `durchlauf()`, für jede Automatik in topologischer Reihenfolge
|
||
(Auslöser vor Nachfolger):
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
A["Tag bestimmen<br/><small>heute, mit next_day morgen</small>"] --> B{"tag_passt?<br/><small>Wochentag, Ferien, Feiertag</small>"}
|
||
B -- nein --> Z1
|
||
B -- ja --> C{"im Zeitfenster?"}
|
||
C -- nein --> Z1["Fenster zu:<br/>force_once prüfen,<br/>Flanke zurücksetzen"]
|
||
C -- ja --> D{"Bedingung erfüllt?<br/><small>any(all(gruppe))</small>"}
|
||
D -- nein --> E["cond_met = 0"]
|
||
D -- ja --> HZ{"hold_secs erfüllt?<br/><small>lange genug am Stück</small>"}
|
||
HZ -- nein --> G
|
||
HZ -- ja --> F{"cond_met schon 1?"}
|
||
F -- ja --> G["nichts tun<br/><small>keine neue Flanke</small>"]
|
||
F -- nein --> H{"once_per_day<br/>und lief heute?"}
|
||
H -- ja --> G
|
||
H -- nein --> I{"innerhalb<br/>lockout_secs?"}
|
||
I -- ja --> J["Flanke verwerfen"]
|
||
I -- nein --> K["ausloesen()"]
|
||
K --> L["Aktionen in den Versand,<br/>last_run + Protokoll,<br/>eigenen Auslöserwert setzen"]
|
||
```
|
||
|
||
### Nur die steigende Flanke
|
||
|
||
`cond_met` hält fest, ob die Bedingung beim letzten Durchlauf schon erfüllt
|
||
war. Ohne das würde „Temperatur über 22 °C“ in jedem Takt erneut feuern.
|
||
|
||
### Haltezeit: erst, wenn es dabei bleibt
|
||
|
||
Manches zeigt sich nicht in einem einzelnen Messwert, sondern erst in seiner
|
||
Dauer. „Der Wasserzähler läuft“ ist jedes Händewaschen; „läuft seit einer
|
||
halben Stunde ohne Pause“ ist ein offener Hahn. `hold_secs` schiebt deshalb
|
||
die Flanke nach hinten: Erst wenn die Bedingung so viele Sekunden **am
|
||
Stück** erfüllt war, gilt sie als erfüllt.
|
||
|
||
```
|
||
Bedingung ──┐ ┌──────────────────────────────────┐
|
||
└───┘ └────
|
||
↑ ↑ ↑
|
||
angefangen nach 30 min: Bedingung weg,
|
||
(Zeit läuft) ausgelöst Zeit verfällt
|
||
```
|
||
|
||
Entscheidend ist, dass `cond_met` bis dahin auf 0 bleibt: Die Flanke wird
|
||
**aufgeschoben, nicht verbraucht**. Alles danach — Tagessperre, Sperrzeit,
|
||
Protokoll — bleibt unverändert.
|
||
|
||
Was man dazu wissen sollte:
|
||
|
||
* **Ein Aussetzer setzt zurück.** Auch ein einzelner Takt genügt; genau das
|
||
ist mit „ununterbrochen“ gemeint.
|
||
* **Gezählt wird im Speicher** (`erfuellt_seit`), nicht in der Datenbank. Es
|
||
ist Laufzustand wie `war_aktiv` — und nach einem Neustart weiß niemand, ob
|
||
die Bedingung zwischendurch angelegen hat. Die Zeit beginnt dann von vorn.
|
||
* **Die Messrate ist die Untergrenze der Genauigkeit.** Ein Zähler, der alle
|
||
fünf Minuten meldet, macht die Haltezeit auf fünf Minuten genau. Mit einem
|
||
punktgenauen Zeit-Auslöser („um 16:30“, nur im Nachholfenster wahr) ist sie
|
||
gar nicht sinnvoll zu kombinieren — sie würde nie reif.
|
||
* **Das Zeitfenster schneidet sie ab.** Geht das Fenster zu, verfällt eine
|
||
angefangene Haltezeit: Sie soll innerhalb des Fensters voll gelaufen sein.
|
||
|
||
### Die drei Zeitformen
|
||
|
||
| Schreibweise | Operator | Bedeutung | Gedacht für |
|
||
|---|---|---|---|
|
||
| `um 16:30` | `=` | ab dieser Minute, plus **Nachholfenster** (5 min) | der Normalfall: einmal täglich zu einem Termin |
|
||
| `ab 16:30` | `>=` | von da bis Mitternacht wahr | alles, was auch nach einem verpassten Takt noch laufen soll (Bewässerung) |
|
||
| `vor 16:30` | `<` | bis dahin wahr | als Zusatzbedingung |
|
||
|
||
Ausgelöst wird in allen drei Fällen **höchstens einmal** — wegen der Flanke.
|
||
Das Nachholfenster ersetzt die früher verlangte Punktgenauigkeit: Ein
|
||
Neustart oder ein hängendes Gerät kostet die Automatik nicht mehr den ganzen
|
||
Tag. Gerechnet wird modulo 24 h, damit ein Ziel um 23:58 auch um 00:01 noch
|
||
zieht.
|
||
|
||
```
|
||
Takt: ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ (alle 10 s)
|
||
│ │
|
||
um 16:30 └── wahr ──────┘ (16:30:00 bis 16:34:59, Nachholfenster)
|
||
↑ Flanke: genau hier wird ausgelöst
|
||
|
||
ab 16:30 └── wahr ───────────────────────────────────► 23:59
|
||
↑ Flanke: ebenfalls nur einmal
|
||
```
|
||
|
||
### Sonnenstand und Ketten
|
||
|
||
Beim Sonnenauf-/-untergang trägt der Operator zusätzlich **das Vorzeichen des
|
||
Versatzes**: `+ 00:30` = eine halbe Stunde danach, `>=- 00:30` = ab einer
|
||
halben Stunde davor, `<+ 00:30` = bis eine halbe Stunde danach. Über den
|
||
Tagesrand wird gerechnet und nicht abgeschnitten — „sechs Stunden vor
|
||
Sonnenaufgang“ landet dann eben am Vorabend.
|
||
|
||
Beim Datentyp `elapsed` (Verkettung) ist es fast dasselbe, mit einem
|
||
wichtigen Unterschied: Verglichen wird der **echte Abstand**, nicht die
|
||
Uhrzeit im Tag. 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.
|
||
`ab + 00:10` gilt zusätzlich nur am selben Kalendertag.
|
||
|
||
### Sperrzeit, „einmal am Tag“, „auf jeden Fall“
|
||
|
||
| Mittel | Wogegen | Verhalten |
|
||
|---|---|---|
|
||
| `hold_secs` | Zustände, die erst durch ihre Dauer auffallen (Wasser läuft, Fenster steht offen) | Die Flanke wird **aufgeschoben**, bis die Bedingung lange genug am Stück steht. Der Gegenspieler zur Sperrzeit: die bremst die Wiederholung, diese den ersten Lauf |
|
||
| `lockout_secs` | Messwerte, die um die Schwelle pendeln (22,1 / 21,9 / 22,1) | Die Flanke wird **verworfen**, nicht aufgehoben — ein Rollladen, der eine Viertelstunde später doch losfährt, wäre unangenehmer als einer, der gar nicht fährt |
|
||
| `once_per_day` | Dinge, die man hinterher von Hand anders stellt | Nach dem ersten Lauf ist bis Mitternacht Ruhe. Die Sperrzeit taugt dafür nicht: sie zählt Sekunden und verfehlte am nächsten Morgen den Termin |
|
||
| `force_once` | verpasste Fenster | Beim Zugehen des Fensters, aber nur, wenn darin nichts lief (zusätzlich gegen `last_run` geprüft, damit ein Neustart nicht doppelt auslöst) |
|
||
|
||
---
|
||
|
||
## 6. Verkettung: eine Automatik löst die nächste aus
|
||
|
||
Das gerechnete Gerät **„Automatiken“** führt jede Automatik als Messwert;
|
||
ihr Wert ist der Zeitpunkt der letzten Auslösung (Datentyp `elapsed`, URL
|
||
`auto:<id>`). Damit ist eine Automatik für den Editor ein Messwert wie jeder
|
||
andere — „Wecker Magdalena + 00:10“.
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
W["Wecker Magdalena<br/><small>um 07:00</small>"] -->|"+ 00:10"| R["Magdalena Rollos<br/><small>und es ist hell</small>"]
|
||
R -->|"+ 00:05"| S["Schlafzimmer<br/>Tür auf"]
|
||
```
|
||
|
||
Warum nicht einfach eine Verzögerung an der Aktion? Weil der Nachfolger eine
|
||
**vollwertige Automatik mit eigenem Rahmen** bleiben soll: „zehn Minuten
|
||
später, aber nur wenn es dann schon hell ist“ ließe sich als bloße
|
||
Verzögerung nicht formulieren — die Zusatzbedingung gilt erst zum späteren
|
||
Zeitpunkt.
|
||
|
||
Was daran hängt:
|
||
|
||
* **Reihenfolge:** `reihenfolge_bestimmen()` sortiert topologisch, damit ein
|
||
Versatz von null noch im selben Takt greift. Nach dem Auslösen trägt der
|
||
Runner den eigenen Auslöserwert sofort nach.
|
||
* **Pausieren wirkt weiter:** Der Runner lädt nur `enabled = 1`. Wer den
|
||
Wecker pausiert, lässt morgens auch die Rollos unten — gewollt.
|
||
* **Anlegen/Löschen der Messwerte** macht `ausloeser_nachfuehren()` vor jedem
|
||
Regelwerk-Laden. Angelegt wird auch für pausierte Automatiken, sonst
|
||
löschte `ON DELETE CASCADE` still die Bedingung des Nachfolgers.
|
||
* **Löschen mit Anhang:** `deleteAutomation($id, "abhaengen"|"mitloeschen")`.
|
||
Der Editor fragt nach, wenn Nachfolger existieren.
|
||
* **Kreise** lehnt der Server beim Speichern ab; der Runner meldet sie nur
|
||
und lässt die Beteiligten weiterlaufen.
|
||
|
||
---
|
||
|
||
## 7. Protokoll und Fehlerbilder
|
||
|
||
`automation_log` bekommt je Lauf eine Zeile (`fired` / `forced`) und je
|
||
fehlgeschlagenem Kommando eine weitere (`error`). Einträge älter als 30 Tage
|
||
räumt der Runner selbst weg.
|
||
|
||
| Beobachtung | Wahrscheinliche Ursache |
|
||
|---|---|
|
||
| Automatik läuft gar nicht, ohne Protokolleintrag | Rahmen: falscher Wochentag, Ferien/Feiertag auf „nie“, Fenster zu eng — oder die Bedingung war nie *neu* erfüllt (`cond_met` stand schon auf 1) |
|
||
| Läuft einmal und dann nicht mehr am selben Tag | `once_per_day` |
|
||
| Läuft kurz nacheinander nicht erneut | `lockout_secs` |
|
||
| Läuft gar nicht, obwohl die Bedingung sichtbar zutrifft | `hold_secs`: sie war noch nicht lange genug am Stück erfüllt, oder ein Aussetzer hat die Zeit zurückgesetzt |
|
||
| Kette bleibt stehen | Auslöser pausiert oder an dem Tag nicht dran |
|
||
| Änderung im Editor wirkt nicht | Regelwerk-Signatur: erst beim nächsten `reload_seconds`-Fenster (30 s) |
|
||
| `error` im Protokoll | Gerät nicht erreichbar, Kommando oder Transport gibt es nicht mehr |
|
||
|
||
---
|
||
|
||
## 8. Wo fange ich an, wenn ich …
|
||
|
||
| Vorhaben | Ort |
|
||
|---|---|
|
||
| … einen neuen Operator oder Datentyp anbieten | `operatorsForType()` in `restricted/automations.php` **und** `bedingung_erfuellt()` im Runner |
|
||
| … ein neues Rahmenfeld einführen | Spalte in `homeMesh_automations.sql`, `saveAutomation()`/`loadAutomation()`, Editor (`renderRahmen`, `leseFormular`), `durchlauf()` im Runner, Satz in `rahmenText()` |
|
||
| … eine neue Geräteart anschließen | eine Klasse in `SolarManager/autoActions/transports.py` (Lesen + Senden), Erkennung an der Aktor-URL |
|
||
| … verstehen, warum etwas lief | `automation_log`, dann die Zeitleiste des Tages (→ [zeitleiste.md](zeitleiste.md)) |
|
||
| … das Nachholfenster ändern | `catchup_minutes` in `config.ini` des Runners |
|
||
| … eine weitere Stufe für die Sperre | `lockoutChoices()` in `restricted/automations.php` — eine Zeile, der Rest zieht nach |
|
||
| … die Grenzen der Haltezeit ändern | `haltezeitPruefen()` in `restricted/automations.php` **und** `haltezeitAusUhr()` im Editor |
|
||
| … auf Dauerzustände reagieren (Wasser läuft, Tür steht offen) | Haltezeit im Rahmen, dazu eine Aktion des Geräts „Benachrichtigungen“ (→ [benachrichtigungen.md](benachrichtigungen.md)) |
|