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>
237 lines
11 KiB
Markdown
237 lines
11 KiB
Markdown
# 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 |
|