Files
adminandClaude Opus 5 a0a8ad04de 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>
2026-09-21 07:39:43 +02:00

237 lines
11 KiB
Markdown
Raw Permalink 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.
# 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.
![Die Zeitleiste](bilder/zeitleiste.png)
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:3018: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:0018: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 |