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

11 KiB
Raw Permalink Blame History

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

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: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