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>
11 KiB
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
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
{
"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
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:
- Die Karte machte eine zweite Bildlaufleiste. AdminLTE gibt
main.app-mainoverflow: autobei Inhaltshöhe. Wurde die Karte zum Messen kurz ohne Lage eingehängt, stand sie unterhalb aller Bahnen und ragte ausmain— Chrome zeigte dann dauerhaft eine eigene Leiste. Heute wird sie unsichtbar oben links eingehängt, gemessen und erst dann platziert. - Die Karte verschwand sofort wieder. Die neue Leiste machte die
Zeitleiste schmaler, der
ResizeObserverzeichnete neu — und dabei ging die Karte verloren. Heute merkt sichzeitleisteZeichnen(), welche Karte offen war, und öffnet sie danach wieder. Außerdem klappt die Karte nach oben, wenn sie unten ausmainliefe.
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 |
