Doku: Automatiken und Zeitleiste ausfuehrlich beschrieben
Neues Verzeichnis doku/ mit zwei Tiefenbohrungen: das Regelwerk der Automatiken (Datenmodell, Editor, Runner, Auswertung, Verkettung, Sperren) und die Zeitleiste (Serverrechnung, Zeichnen, Stapeln, Ketten, Karte). Mit Diagrammen und je einem Bild; die Haupt-README verweist darauf. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,12 @@ Dieses Verzeichnis ist zugleich der Web-Root der NAS
|
||||
Der Arbeitsordner *ist* die laufende Anwendung — jede gespeicherte Datei ist
|
||||
sofort live.
|
||||
|
||||
Für zwei Teile reicht diese Landkarte nicht, weil ihr Verhalten aus dem
|
||||
Zusammenspiel von Editor, Datenbank und Runner entsteht. Sie haben eigene,
|
||||
ausführliche Dokumente unter [`doku/`](doku/README.md):
|
||||
**[Automatiken](doku/automatiken.md)** und
|
||||
**[Zeitleiste](doku/zeitleiste.md)**.
|
||||
|
||||
---
|
||||
|
||||
## Inhalt
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# Vertiefende Dokumentation
|
||||
|
||||
Die [README im Wurzelverzeichnis](../README.md) beschreibt das Ganze: welche
|
||||
Seite es gibt, welche Datei was tut, wo eine Zahl herkommt. Sie ist die
|
||||
Landkarte.
|
||||
|
||||
Hier liegen die **Tiefenbohrungen** — für die Teile, bei denen die Landkarte
|
||||
nicht reicht, weil das Verhalten aus dem Zusammenspiel mehrerer Prozesse
|
||||
entsteht und die Regeln nicht aus dem Quelltext einer einzelnen Datei
|
||||
abzulesen sind.
|
||||
|
||||
| Dokument | Worum es geht |
|
||||
|---|---|
|
||||
| [automatiken.md](automatiken.md) | Die Automatiken (AutoActions): Datenmodell, Editor, Runner, wie eine Bedingung wirklich ausgewertet wird, Verkettung, Sperren, Fehlerbilder |
|
||||
| [zeitleiste.md](zeitleiste.md) | Die Zeitleiste der Automatiken: wie der Server den Tag ausrechnet und wie der Browser ihn zeichnet |
|
||||
|
||||
---
|
||||
|
||||
## Die drei Prozesse, die zusammenspielen
|
||||
|
||||
Nichts hier ist eine einzelne Anwendung. Drei Dinge laufen gleichzeitig, und
|
||||
sie reden nur über MQTT und über die Datenbanken miteinander — nie direkt.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Browser["Browser"]
|
||||
UI["Weboberfläche<br/><small>PHP + JS, /volume1/web/smart</small>"]
|
||||
end
|
||||
|
||||
subgraph NAS["NAS, Hintergrundprozesse (SolarManager)"]
|
||||
RUN["autoaction_runner.py<br/><small>führt Automatiken aus</small>"]
|
||||
SM["solarManager.py<br/><small>sammelt Anlagenwerte</small>"]
|
||||
SK["gatherSkodaData.py<br/><small>Fahrzeug</small>"]
|
||||
end
|
||||
|
||||
subgraph Speicher["Speicher"]
|
||||
MQTT[["MQTT-Broker<br/><small>alles Aktuelle</small>"]]
|
||||
HM[("homeMesh<br/><small>Geräte, Automatiken, Grundriss</small>")]
|
||||
SL[("solarLog<br/><small>Messreihen, Sonnenzeiten</small>")]
|
||||
end
|
||||
|
||||
subgraph Geraete["Geräte"]
|
||||
TAH["Tahoma-Box<br/><small>Jalousien</small>"]
|
||||
SHE["Shelly, WLED, ESP32"]
|
||||
end
|
||||
|
||||
UI -- "liest/schreibt Regeln" --> HM
|
||||
UI -- "abonniert" --> MQTT
|
||||
UI -- "schaltet von Hand" --> TAH & SHE
|
||||
RUN -- "Regelwerk" --> HM
|
||||
RUN -- "Messwerte zurück" --> HM
|
||||
RUN -- "Sonnenzeiten" --> SL
|
||||
RUN -- "hört + schaltet" --> MQTT
|
||||
RUN -- "liest + schaltet" --> TAH & SHE
|
||||
SM --> MQTT & SL
|
||||
SK --> SL
|
||||
```
|
||||
|
||||
**Wichtig für das Verständnis aller folgenden Kapitel:**
|
||||
|
||||
* Die **Weboberfläche schaltet nichts von selbst.** Sie beschreibt, was
|
||||
gelten soll (`homeMesh`), und zeigt, was ist. Ausgeführt wird im Runner.
|
||||
* **`actor_states.current_value` pflegt der Runner.** Ohne ihn stünde dort
|
||||
der Wert vom Tag des Gerätesuchlaufs. Deshalb zeigt auch der Editor
|
||||
aktuelle Zahlen, obwohl er nur eine Tabelle liest.
|
||||
* Der Runner ist ein **Dauerprozess, kein Cronjob** — er braucht den
|
||||
Vorzustand (`cond_met`), um steigende Flanken zu erkennen.
|
||||
|
||||
---
|
||||
|
||||
## Lesereihenfolge
|
||||
|
||||
1. [README](../README.md), Abschnitt „Auf einen Blick“ — Seiten und Dateien.
|
||||
2. [automatiken.md](automatiken.md) — das Regelwerk und seine Ausführung.
|
||||
3. [zeitleiste.md](zeitleiste.md) — die Darstellung desselben Regelwerks
|
||||
über einen Tag.
|
||||
|
||||
Wer nur etwas ändern will, findet die Einstiegspunkte am Ende beider
|
||||
Dokumente unter „Wo fange ich an, wenn ich …“.
|
||||
@@ -0,0 +1,409 @@
|
||||
# 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"
|
||||
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"
|
||||
}
|
||||
```
|
||||
|
||||
Drei 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`),
|
||||
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) | |
|
||||
| `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 --> 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.
|
||||
|
||||
### 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 |
|
||||
|---|---|---|
|
||||
| `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` |
|
||||
| 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 |
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 191 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 440 KiB |
@@ -0,0 +1,236 @@
|
||||
# Die Zeitleiste der Automatiken
|
||||
|
||||
Die Übersicht als Tabelle beantwortete nicht, was man eigentlich wissen will:
|
||||
**Läuft der Tag stimmig, und warum ist etwas noch nicht gefahren?** Die
|
||||
Zeitleiste legt deshalb jede Automatik auf die Stunde, zu der sie greift.
|
||||
|
||||

|
||||
|
||||
Zwei Dateien teilen sich die Arbeit, und die Trennung ist scharf:
|
||||
|
||||
| Datei | Aufgabe |
|
||||
|---|---|
|
||||
| `restricted/zeitleiste.php` | **rechnen**: wo liegt eine Automatik auf dem Tag, in welcher Bahn, ist sie heute dran, ist sie gelaufen |
|
||||
| `js/solar/zeitleiste.js` | **zeichnen**: Bahnen, Stapeln, Etiketten, Kettenbögen, Karte, Filter |
|
||||
|
||||
Geliefert wird über `ajax/AutoAction.php?action=zeitleiste&datum=YYYY-MM-DD`
|
||||
als JSON.
|
||||
|
||||
---
|
||||
|
||||
## 1. Was der Server ausrechnet
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
A["Alle Automatiken laden<br/><small>loadAutomation() je Id</small>"] --> B["Bedingungen je ODER-Gruppe einsortieren"]
|
||||
B --> C{"Was trägt die Gruppe?"}
|
||||
C -->|"Uhrzeit =, Sonne ±"| P["punkt"]
|
||||
C -->|"Uhrzeit >= / <"| F["start / ende"]
|
||||
C -->|"Automatik + Versatz"| K["kette + versatz"]
|
||||
C -->|"alles andere"| S["sensor = true<br/><small>wartet auf einen Messwert</small>"]
|
||||
P & F & K & S --> D["Art bestimmen:<br/>punkt › kette › fenster › jederzeit"]
|
||||
D --> E["Ketten auflösen<br/><small>bis zu 6 Runden</small>"]
|
||||
E --> G["Bahn, Satz, Tagesprüfung, Läufe"]
|
||||
G --> H[["JSON"]]
|
||||
```
|
||||
|
||||
### 1.1 Die Regeln, wie eine Lage entsteht
|
||||
|
||||
| Bedingung | Ergebnis auf dem Tag |
|
||||
|---|---|
|
||||
| feste Uhrzeit `um 07:00` | **Punkt** |
|
||||
| Sonnenauf-/-untergang ± Versatz | **Punkt**, an diesem Tag ausgerechnet |
|
||||
| andere Automatik `+ 00:10` | **Kette**: Punkt hinter dem Auslöser |
|
||||
| `ab …` / `vor …` oder ein eingeengtes Zeitfenster | **Balken** über das Fenster |
|
||||
| nur Messwerte, ganzer Tag | **„jederzeit“** (eigenes Band unter der Leiste) |
|
||||
|
||||
Mehrere Gruppen (ODER) werden zusammengefasst: Gibt es Punkte, gewinnt der
|
||||
früheste; sonst eine Kette; sonst das umschließende Fenster aus allen
|
||||
Gruppengrenzen. Trägt eine Gruppe einen zeitlich bestimmten Beginn (etwa
|
||||
„Sonnenuntergang + 30 **oder** dunkel“), merkt sich der Eintrag ihn als
|
||||
`punkt` — er erscheint im Hinweis als „frühestens 19:01“.
|
||||
|
||||
### 1.2 Ketten auflösen
|
||||
|
||||
Die Lage eines Nachfolgers hängt vom Auslöser ab, und die Kette kann mehrere
|
||||
Stufen haben (Wecker → Rollos → Schlafzimmer). Deshalb läuft die Auflösung
|
||||
reihum, höchstens sechs Runden:
|
||||
|
||||
| Zustand des Auslösers | Nachfolger |
|
||||
|---|---|
|
||||
| ist an diesem Tag schon **gelaufen** | Punkt bei der echten Laufzeit + Versatz |
|
||||
| hat einen **Punkt** | Punkt dort + Versatz |
|
||||
| ist ein **Fenster** | Fenster ab dessen Beginn + Versatz |
|
||||
| hat selbst keine Lage / Kreis | „jederzeit“ |
|
||||
|
||||
Immer begrenzt auf das eigene Zeitfenster. Und: Der Nachfolger **erbt**, ob
|
||||
der Tag passt — läuft der Wecker heute nicht, steht auch das Rollo
|
||||
gestrichelt da, mit dem Grund „Auslöser ‚Wecker Magdalena‘ ist nicht dran“.
|
||||
|
||||
### 1.3 Ist sie heute überhaupt dran?
|
||||
|
||||
`zlTagPasst()` rechnet **genau wie der Runner** (`tag_passt()` und
|
||||
`gemeinter_tag()`): erst öffnen (Wochentag, „zusätzlich“ an Ferien/Feiertagen),
|
||||
dann sperren (Ferien/Feiertage auf „nie“), und bei `next_day` für morgen.
|
||||
Sonst hieße „heute nicht“ hier etwas anderes als dort.
|
||||
|
||||
> Ausgeblendet wird trotzdem nichts. Wer nicht dran ist, steht gestrichelt
|
||||
> da — mit Grund im Hinweis. Eine Zeitleiste, die Einträge verschweigt,
|
||||
> beantwortet die Frage „warum ist das nicht gefahren?“ gerade nicht.
|
||||
|
||||
### 1.4 Sonnenzeiten für beliebige Tage
|
||||
|
||||
`solarLog.daylight` kennt heute, morgen und die Vergangenheit. Für einen
|
||||
späteren Tag nimmt `zlSonne()` denselben Kalendertag eines Vorjahres — die
|
||||
Sonne verschiebt sich von Jahr zu Jahr um weniger als eine Minute, und so
|
||||
braucht es keinen Standort, der in einem anderen Haus wieder falsch wäre.
|
||||
Im Hinweis steht dann „(Vorjahr)“.
|
||||
|
||||
### 1.5 Die Bahn
|
||||
|
||||
Die Zeile, in der eine Automatik steht, ergibt sich aus den **Geräten, die
|
||||
sie schaltet** — dieselbe Einordnung wie im Raum-Modal (`bedienform()` in
|
||||
`roomControls.php`), Mehrheit gewinnt, bei Gleichstand die Bahn weiter oben.
|
||||
Bewässerung ist dort „sonstiges“ und wird am Gerätetyp erkannt.
|
||||
|
||||
Bahnen: Licht · Beschattung · Heizung · Bewässerung · Schalter · Sonstiges.
|
||||
|
||||
### 1.6 Das JSON
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"datum": "2026-09-21", "heute": true, "jetzt": 964, // Minuten seit Mitternacht
|
||||
"sonne": { "auf": 431, "unter": 1176, "genau": true },
|
||||
"bahnen": { "licht": {"titel": "Licht", "symbol": "bi-lightbulb"}, … },
|
||||
"etagen": [ {"code": "OG", "label": "Obergeschoss"}, … ],
|
||||
"automatiken": [{
|
||||
"id": 18, "name": "Hitzeschutz Süd", "floor": "EG", "enabled": true,
|
||||
"dran": true, "grund": "",
|
||||
"bahn": "beschattung",
|
||||
"satz": "Wozi Schiebetür, Wozi Fensterfront +1: Position+Neigung",
|
||||
"wenn": "Sonne Süd: Helligkeit > 30000 Lux und …",
|
||||
"laeufe": ["12:50"], // aus automation_log
|
||||
"art": "fenster", // punkt | fenster | kette | jederzeit
|
||||
"punkt": 570, "von": 570, "bis": 1080,
|
||||
"kette": null, "versatz": 0,
|
||||
"sonne": false, // Lage kommt vom Sonnenstand
|
||||
"wartet": true // hängt an einem Messwert
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Was der Browser daraus macht
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
L["zeitleisteLaden()<br/><small>fetch ?action=zeitleiste</small>"] --> Z["zeitleisteZeichnen()"]
|
||||
Z --> F["Etagenfilter anwenden"]
|
||||
F --> T["Dreiteilung:<br/>gelegt · jederzeit · pausiert"]
|
||||
T --> ST["je Bahn: zlStapeln()<br/><small>Zeilen wie im Kalender</small>"]
|
||||
ST --> HTML["HTML bauen:<br/>Achse, Bahnen, Bänder, Legende"]
|
||||
HTML --> KET["zlKettenZeichnen()<br/><small>SVG-Bögen, nach dem Layout</small>"]
|
||||
KET --> SCR["Scrollstand setzen"]
|
||||
SCR --> KAR["offene Karte wieder öffnen"]
|
||||
```
|
||||
|
||||
Jede Automatik erscheint **genau einmal**: in ihrer Bahn, unter „Jederzeit“
|
||||
oder unter „Pausiert“. Der Zähler oben („22 von 22 · 3 pausiert“) ist die
|
||||
Probe aufs Exempel — fehlte eine, fiele es dort auf.
|
||||
|
||||
### 2.1 Stapeln: warum in Pixeln und nicht in Minuten
|
||||
|
||||
Zwei Punkte zehn Minuten auseinander liegen zeitlich getrennt — ihre
|
||||
**Beschriftungen** aber übereinander. `zlStapeln()` rechnet deshalb in
|
||||
Pixeln: Es misst den Etikett-Text (einmal ein Canvas, dann nur messen) und
|
||||
legt jeden Eintrag in die erste Zeile, in der er nichts überdeckt.
|
||||
|
||||
```
|
||||
Zeile 0 ●07:00 Wecker Magdalena ▭ 09:30–18:00 Hitzeschutz Süd ✓12:50
|
||||
Zeile 1 ●07:08 Wecker Rollos ●15:59 Hitzeschutz Süd zurück
|
||||
└── Nachfolger direkt unter seinem Auslöser
|
||||
```
|
||||
|
||||
Drei Sonderfälle stecken darin:
|
||||
|
||||
* **Punkt am rechten Rand** trägt sein Etikett links von sich — die Marke
|
||||
bleibt, wo die Zeit ist.
|
||||
* **Fenster-Etikett** rückt nie vor seinen Balken; sonst sähe „15:00–18:31“
|
||||
aus, als finge es um halb elf an. Reicht der Platz nicht, wird der Name
|
||||
gekürzt (voll steht er im Hinweis).
|
||||
* **Ketten bleiben beisammen:** Eingesetzt wird familienweise — erst der
|
||||
Auslöser, gleich danach seine Nachfolger in die erste freie Zeile
|
||||
*darunter*. Je Zeile werden belegte Strecken geführt, nicht nur ihr Ende,
|
||||
weil ein Nachfolger links von etwas liegen kann, das schon steht.
|
||||
|
||||
### 2.2 Zustand, Farbe, Form
|
||||
|
||||
`zlZustand()` kennt fünf Zustände — sie bestimmen Farbe und Zeichen:
|
||||
|
||||
| Zustand | Wann | Darstellung |
|
||||
|---|---|---|
|
||||
| `nichtdran` | Tag passt nicht | gestrichelt, Kalendersymbol, Grund im Hinweis |
|
||||
| `gelaufen` | es gibt Läufe an dem Tag | Haken und echte Laufzeit |
|
||||
| `vorbei` | Zeit liegt hinter „jetzt“, nichts gelaufen | matt |
|
||||
| `aktiv` | „jetzt“ liegt im Fenster / hinter dem Punkt | hervorgehoben |
|
||||
| `kommt` | liegt noch vor uns | normal |
|
||||
|
||||
Beim Punkt zeigt das Etikett nach einem Lauf die **echte** Zeit, beim Fenster
|
||||
immer das Fenster und den Lauf hinten mit Haken — sonst stünde „12:50“ am
|
||||
Balkenanfang bei 09:30.
|
||||
|
||||
### 2.3 Kettenbögen
|
||||
|
||||
Erst **nach** dem Einsetzen gezeichnet, weil die Lage beider Enden aus dem
|
||||
Layout kommt. Die Linie verlässt den Auslöser lotrecht nach unten und biegt
|
||||
mit einer kleinen Rundung von links in den Nachfolger ein; liegt der
|
||||
Nachfolger links vom Auslöser, kommt sie von oben an. Jeder Pfad trägt
|
||||
`data-von`/`data-nach`, damit das Überfahren eines Eintrags seine ganze Kette
|
||||
hell schaltet (`zlKetteHervorheben()`).
|
||||
|
||||
### 2.4 Die Karte
|
||||
|
||||
Ein Klick auf einen Eintrag öffnet eine kleine Karte: was die Automatik tut,
|
||||
wann, wie sie heute steht — und die drei Dinge, die man damit tun will
|
||||
(Bearbeiten, Pausieren, Löschen). Pausieren und Löschen gehen über dieselben
|
||||
Funktionen wie in der Liste, samt Rückfrage nach abhängigen Automatiken.
|
||||
|
||||
Zwei Fallen stecken in dieser Karte, beide behoben:
|
||||
|
||||
1. **Die Karte machte eine zweite Bildlaufleiste.** AdminLTE gibt
|
||||
`main.app-main` `overflow: auto` bei Inhaltshöhe. Wurde die Karte zum
|
||||
Messen kurz ohne Lage eingehängt, stand sie unterhalb aller Bahnen und
|
||||
ragte aus `main` — Chrome zeigte dann dauerhaft eine eigene Leiste.
|
||||
Heute wird sie unsichtbar oben links eingehängt, gemessen und erst dann
|
||||
platziert.
|
||||
2. **Die Karte verschwand sofort wieder.** Die neue Leiste machte die
|
||||
Zeitleiste schmaler, der `ResizeObserver` zeichnete neu — und dabei ging
|
||||
die Karte verloren. Heute merkt sich `zeitleisteZeichnen()`, welche Karte
|
||||
offen war, und öffnet sie danach wieder. Außerdem klappt die Karte nach
|
||||
oben, wenn sie unten aus `main` liefe.
|
||||
|
||||
### 2.5 Was ein Neuzeichnen auslöst
|
||||
|
||||
| Anlass | Folge |
|
||||
|---|---|
|
||||
| Tag wechseln, Ansicht öffnen | `zeitleisteLaden()` — neue Daten vom Server |
|
||||
| Etagenfilter | nur `zeitleisteZeichnen()`, Auswahl in `localStorage` |
|
||||
| Breite ändert sich (> 4 px) | `zeitleisteZeichnen()` nach 120 ms |
|
||||
| Minutentakt (nur heute, Tab sichtbar) | `zeitleisteLaden()` — „jetzt“-Linie und neue Läufe |
|
||||
| nach Speichern/Pausieren/Löschen | `refreshAutomations()` lädt die Zeitleiste mit |
|
||||
|
||||
Was sich der Browser merkt: offene Ansicht (Zeitleiste oder Liste),
|
||||
Etagenfilter, Scrollstand innerhalb des Tages.
|
||||
|
||||
---
|
||||
|
||||
## 3. Wo fange ich an, wenn ich …
|
||||
|
||||
| Vorhaben | Ort |
|
||||
|---|---|
|
||||
| … eine Bahn hinzufügen oder umbenennen | `zeitleisteBahnen()` in `restricted/zeitleiste.php`; Farbe in `css/solar.css` (`.zl-bahn-<name>`) |
|
||||
| … das Symbol einer Bahn ändern | ebenda — dasselbe Symbol muss auch im Raum-Modal und im Editor stehen (siehe `GERAETE_SYMBOL` in `autoActionFuncs.js`) |
|
||||
| … eine neue Art von Lage einführen | `zeitleiste()` (Gruppen-Auswertung + Art bestimmen), dann `zlLage()`/`zlEintrag()` im Browser |
|
||||
| … an der Höhe/Dichte schrauben | `ZL_ZEILE`, `ZL_KOPF`, `ZL_MIN_SPUR` in `js/solar/zeitleiste.js` |
|
||||
| … verstehen, warum ein Eintrag „nicht dran“ ist | `zlTagPasst()` — und zum Gegenprüfen `tag_passt()` im Runner |
|
||||
Reference in New Issue
Block a user