Automatismen als Zeitleiste des Tages, Liste bleibt umschaltbar
Die Uebersicht auf der Startseite zeigt die Automatiken jetzt als Zeitleiste: eine Bahn je Geraeteart, die Spur ist der Tag. Der Umschalter "Zeitleiste | Liste" oben in der Karte fuehrt zur bisherigen Tabelle zurueck und wird im Browser gemerkt. Jede Automatik erscheint genau einmal, nach festen Regeln (restricted/zeitleiste.php), auch kuenftige: - feste Uhrzeit oder Sonnenstand: Punkt; Zeitfenster mit Messwert: Balken; Kette: hinter ihrem Ausloeser, als gestrichelter Bogen verbunden - ohne jede Zeit: Band "Jederzeit"; pausiert: Band "Pausiert" - an diesem Tag nicht dran (Wochentag, Ferien, Feiertag, Vorabend, wie im Runner gerechnet): gestrichelt mit Grund, nie ausgeblendet - Bahn aus den geschalteten Geraeten ueber bedienform(), Mehrheit gewinnt; Bewaesserung am Geraetetyp - Zaehler "x von y" im Kopf Dazu: Tag vor/zurueck, Etagen einzeln einblendbar (gemerkt), Jetzt-Linie und Nachtschatten aus solarLog.daylight (spaetere Tage vom selben Kalendertag eines Vorjahres), gelaufene Eintraege mit Uhrzeit aus automation_log, Klick oeffnet den Editor. Schmal nur Symbole in den Bahnen und Start bei der aktuellen Uhrzeit. Auf einer Probeseite mit echten Daten geprueft: nichts ragt ueber, keine ueberlappenden Etiketten, alle 22 Automatiken genau einmal, Filter, Tageswechsel und Umschalter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,7 @@ smart/
|
|||||||
│ ├── skoda.php weather.php settings.php logs.php ← die acht Seitenvorlagen
|
│ ├── skoda.php weather.php settings.php logs.php ← die acht Seitenvorlagen
|
||||||
│ ├── rooms.php Etagen und Räume lesen (homeMesh), Kachelvorgabe, Symbole
|
│ ├── rooms.php Etagen und Räume lesen (homeMesh), Kachelvorgabe, Symbole
|
||||||
│ ├── grundriss.php Etagen, Räume, Kachelpositionen, Bilder ändern (Reiter Grundriss)
|
│ ├── grundriss.php Etagen, Räume, Kachelpositionen, Bilder ändern (Reiter Grundriss)
|
||||||
|
│ ├── zeitleiste.php Automatiken eines Tages: Lage, Bahn, ob dran, Läufe
|
||||||
│ ├── meshdb.php meshDb(): Verbindung zu homeMesh
|
│ ├── meshdb.php meshDb(): Verbindung zu homeMesh
|
||||||
│ ├── costs.php Preistabellen + solarDb()
|
│ ├── costs.php Preistabellen + solarDb()
|
||||||
│ ├── automations.php Automatiken, Gerätekatalog
|
│ ├── automations.php Automatiken, Gerätekatalog
|
||||||
@@ -182,7 +183,7 @@ genau die Topics, die auf den Kacheln stehen (`tileTopics()` in `rooms.php`).
|
|||||||
| Seite (`?action=`) | Vorlage | Seitenskripte | holt per fetch | hört auf MQTT |
|
| Seite (`?action=`) | Vorlage | Seitenskripte | holt per fetch | hört auf MQTT |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `solar` (Vorgabe) | `solar.php` | `jahresstatistik.js`, `solarMQTT.js` | `getProdData`, `getConsData`, `getForecastData`, `getSunrise`, `getStats`, `carEG`, `carOG`, `heater`, `watering` | `solarManager/#`, `weatherStation/#`, `wattpilot/#`, `go-eCharger/#`, `Gartenwasser/#` |
|
| `solar` (Vorgabe) | `solar.php` | `jahresstatistik.js`, `solarMQTT.js` | `getProdData`, `getConsData`, `getForecastData`, `getSunrise`, `getStats`, `carEG`, `carOG`, `heater`, `watering` | `solarManager/#`, `weatherStation/#`, `wattpilot/#`, `go-eCharger/#`, `Gartenwasser/#` |
|
||||||
| `home` | `home.php` | `autoActionFuncs.js`, `homeMQTT.js` | `room.php`, `AutoAction.php` | die Topics der Kacheln, `Raumtemp/#` |
|
| `home` | `home.php` | `autoActionFuncs.js`, `zeitleiste.js`, `homeMQTT.js` | `room.php`, `AutoAction.php` | die Topics der Kacheln, `Raumtemp/#` |
|
||||||
| `heat` | `heat.php` | `heatMQTT.js` | `getHeaterData`, `getWaterData`, `getSunrise` | `solarManager/#`, `weatherStation/#`, Wallbox-Topics |
|
| `heat` | `heat.php` | `heatMQTT.js` | `getHeaterData`, `getWaterData`, `getSunrise` | `solarManager/#`, `weatherStation/#`, Wallbox-Topics |
|
||||||
| `history` | `history.php` | `jahresstatistik.js`, `historyMQTT.js` | `energyHistory` (6 ×), `getStats` | — |
|
| `history` | `history.php` | `jahresstatistik.js`, `historyMQTT.js` | `energyHistory` (6 ×), `getStats` | — |
|
||||||
| `skoda` | `skoda.php` | `skodaMQTT.js` | `skoda.php?was=…`, `skodaCmd.php`, `tile.php` | `solarManager/#` |
|
| `skoda` | `skoda.php` | `skodaMQTT.js` | `skoda.php?was=…`, `skodaCmd.php`, `tile.php` | `solarManager/#` |
|
||||||
@@ -324,6 +325,19 @@ Anfragen dazu, und die Auswahlfelder füllten sich asynchron. Dazu die
|
|||||||
Übersicht: Nachfolger einklappen (`toggleAutomationGroup`) und die Rückfrage
|
Übersicht: Nachfolger einklappen (`toggleAutomationGroup`) und die Rückfrage
|
||||||
vor dem Löschen (`frageNachfolger`).
|
vor dem Löschen (`frageNachfolger`).
|
||||||
|
|
||||||
|
**`zeitleiste.js`** — die Automatismen als Zeitleiste eines Tages, neben der
|
||||||
|
bisherigen Liste (Umschalter oben in der Karte, gemerkt im Browser). Eine Bahn
|
||||||
|
je Geräteart, abgeleitet aus den geschalteten Geräten über `bedienform()` — die
|
||||||
|
Mehrheit gewinnt, Bewässerung wird am Gerätetyp erkannt. Die Lage rechnet
|
||||||
|
`restricted/zeitleiste.php` nach festen Regeln: feste Uhrzeit und Sonnenstand
|
||||||
|
als Punkt, Zeitfenster mit Messwert als Balken, Ketten hinter ihrem Auslöser,
|
||||||
|
ohne jede Zeit unter „Jederzeit“, pausierte unter „Pausiert“. Ob eine
|
||||||
|
Automatik an einem Tag dran ist, wird wie im Runner gerechnet (Wochentage,
|
||||||
|
Ferien, Feiertage, Vorabend); nicht dran heißt gestrichelt, nie ausgeblendet.
|
||||||
|
Der Zähler „x von y“ macht sichtbar, dass keine fehlt. Etagen lassen sich
|
||||||
|
einzeln einblenden. Sonnenzeiten kommen aus `solarLog.daylight`, für spätere
|
||||||
|
Tage vom selben Kalendertag eines Vorjahres.
|
||||||
|
|
||||||
**`grundriss.js`** — der Reiter Grundriss: Etagen, Räume, Grundrissbilder und
|
**`grundriss.js`** — der Reiter Grundriss: Etagen, Räume, Grundrissbilder und
|
||||||
der Plan-Editor, in dem Kacheln gezogen und gesetzt werden. Eigene Datei, weil
|
der Plan-Editor, in dem Kacheln gezogen und gesetzt werden. Eigene Datei, weil
|
||||||
`settings.js` schon für vier Reiter reicht; es nutzt `textSchuetzen()` von dort.
|
`settings.js` schon für vier Reiter reicht; es nutzt `textSchuetzen()` von dort.
|
||||||
@@ -346,7 +360,7 @@ klein: Diagramme füllen bzw. Logzeilen nachladen und einfärben.
|
|||||||
| Endpunkt | Aufruf | liefert / tut |
|
| Endpunkt | Aufruf | liefert / tut |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `room.php` | `?room=<rooms.id>`, `POST ?action=command\|temp\|jalousie` | Raum-Modal bauen; ein Kommando oder eine Solltemperatur senden |
|
| `room.php` | `?room=<rooms.id>`, `POST ?action=command\|temp\|jalousie` | Raum-Modal bauen; ein Kommando oder eine Solltemperatur senden |
|
||||||
| `AutoAction.php` | `?action=editor\|list\|followers\|werte`, `POST save\|delete\|toggle` | Automatik-Editor und -Übersicht |
|
| `AutoAction.php` | `?action=editor\|list\|zeitleiste\|followers\|werte`, `POST save\|delete\|toggle` | Automatik-Editor, Übersicht als Liste und als Zeitleiste (`&datum=YYYY-MM-DD`) |
|
||||||
| `settings.php` | `?action=list\|kacheln\|geraete\|grundriss\|skoda-keys`, `POST save\|kacheln-save\|geraete-raeume\|geraet-loeschen\|etage-speichern\|etagen-reihenfolge\|etage-loeschen\|etage-bild\|raum-speichern\|raum-position\|raum-loeschen\|skoda-keys-save` | Einstellungsseite; die Grundriss-Aktionen antworten mit dem ganzen neuen Stand, `skoda-keys` meldet nur, ob ein Schlüssel hinterlegt ist und worauf er endet |
|
| `settings.php` | `?action=list\|kacheln\|geraete\|grundriss\|skoda-keys`, `POST save\|kacheln-save\|geraete-raeume\|geraet-loeschen\|etage-speichern\|etagen-reihenfolge\|etage-loeschen\|etage-bild\|raum-speichern\|raum-position\|raum-loeschen\|skoda-keys-save` | Einstellungsseite; die Grundriss-Aktionen antworten mit dem ganzen neuen Stand, `skoda-keys` meldet nur, ob ein Schlüssel hinterlegt ist und worauf er endet |
|
||||||
| `getStats.php` | `GET` | Jahresstatistik, alle Jahre, mit Metadaten je Kennzahl |
|
| `getStats.php` | `GET` | Jahresstatistik, alle Jahre, mit Metadaten je Kennzahl |
|
||||||
| `energyHistory.php` | `?series=prod\|cons&range=month\|year\|decade` | Verbrauchs-/Erzeugungsverlauf (Rohdaten oder Stundenarchiv) |
|
| `energyHistory.php` | `?series=prod\|cons&range=month\|year\|decade` | Verbrauchs-/Erzeugungsverlauf (Rohdaten oder Stundenarchiv) |
|
||||||
@@ -371,6 +385,7 @@ Kennzahlen, `energyHistory.php` wählt die Quelle nach Zeitraum,
|
|||||||
| Datei | Zuständig für | Wichtige Funktionen |
|
| Datei | Zuständig für | Wichtige Funktionen |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `rooms.php` | Etagen und Räume lesen (`homeMesh`), Kachelvorgabe, Symbole | `grundriss()`, `allRooms()`, `roomsOnFloor()`, `floorsWithPlan()`, `standardEtage()`, `etageBild()`, `roomsWithTile()`, `tileTopics()`, `kachelVorgabe()`, `bootstrapIcons()`, `kachelIcon()` |
|
| `rooms.php` | Etagen und Räume lesen (`homeMesh`), Kachelvorgabe, Symbole | `grundriss()`, `allRooms()`, `roomsOnFloor()`, `floorsWithPlan()`, `standardEtage()`, `etageBild()`, `roomsWithTile()`, `tileTopics()`, `kachelVorgabe()`, `bootstrapIcons()`, `kachelIcon()` |
|
||||||
|
| `zeitleiste.php` | Automatiken eines Tages für die Zeitleiste | `zeitleiste()`, `zlTagPasst()`, `zlSonne()`, `zlBahn()` |
|
||||||
| `grundriss.php` | Etagen, Räume, Kachelpositionen und Grundrissbilder ändern | `grundrissStand()`, `etageSpeichern()`, `etagenReihenfolge()`, `etageLoeschen()`, `etageBildSpeichern()`, `raumSpeichern()`, `raumPosition()`, `raumLoeschen()` |
|
| `grundriss.php` | Etagen, Räume, Kachelpositionen und Grundrissbilder ändern | `grundrissStand()`, `etageSpeichern()`, `etagenReihenfolge()`, `etageLoeschen()`, `etageBildSpeichern()`, `raumSpeichern()`, `raumPosition()`, `raumLoeschen()` |
|
||||||
| `meshdb.php` | Verbindung zu `homeMesh` | `meshDb()` |
|
| `meshdb.php` | Verbindung zu `homeMesh` | `meshDb()` |
|
||||||
| `costs.php` | Preistabellen, Verbindung zu `solarLog` | `solarDb()`, `preiseLaden()`, `preiseSpeichern()`, `grundpreisImZeitraum()` |
|
| `costs.php` | Preistabellen, Verbindung zu `solarLog` | `solarDb()`, `preiseLaden()`, `preiseSpeichern()`, `grundpreisImZeitraum()` |
|
||||||
|
|||||||
@@ -83,6 +83,17 @@ if ($action === "werte") {
|
|||||||
jsonAntwort(["werte" => stateValues($ids)]);
|
jsonAntwort(["werte" => stateValues($ids)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Zeitleiste ----------------------------------------------------------
|
||||||
|
// Alle Automatiken eines Tages mit ihrer Lage, siehe restricted/zeitleiste.php.
|
||||||
|
if ($action === "zeitleiste") {
|
||||||
|
require_once("../restricted/zeitleiste.php");
|
||||||
|
try {
|
||||||
|
jsonAntwort(zeitleiste(strval($_GET["datum"] ?? date("Y-m-d"))));
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
jsonAntwort(["error" => "Zeitleiste nicht abrufbar: " . $e->getMessage()], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Uebersichtstabelle --------------------------------------------------
|
// --- Uebersichtstabelle --------------------------------------------------
|
||||||
if ($action === "list") {
|
if ($action === "list") {
|
||||||
$tage = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
$tage = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||||||
|
|||||||
+346
@@ -2739,3 +2739,349 @@ input[type='range']::-webkit-slider-runnable-track {
|
|||||||
}
|
}
|
||||||
.gr-raum-stand { color: var(--ton-schrift-3, #6f7a81); font-size: .8rem; }
|
.gr-raum-stand { color: var(--ton-schrift-3, #6f7a81); font-size: .8rem; }
|
||||||
.gr-raum .btn.gr-platzieren { padding: 0 .45rem; font-size: .75rem; }
|
.gr-raum .btn.gr-platzieren { padding: 0 .45rem; font-size: .75rem; }
|
||||||
|
|
||||||
|
/* ===========================================================================
|
||||||
|
Automatismen als Zeitleiste (js/solar/zeitleiste.js)
|
||||||
|
|
||||||
|
Eine Bahn je Geraeteart, die Spur ist der Tag von 0 bis 24 Uhr. Die Nacht
|
||||||
|
liegt als leichter Schatten dahinter, jetzt ist eine duenne Linie in der
|
||||||
|
Sonnenfarbe. Jede Bahn traegt ihre Akzentfarbe als --zl-farbe; Punkte,
|
||||||
|
Balken und Etiketten nehmen sie auf.
|
||||||
|
=========================================================================== */
|
||||||
|
.zl {
|
||||||
|
--zl-linie: var(--ton-linie, rgba(138, 148, 158, .2));
|
||||||
|
--zl-farbe: #9aa6ad;
|
||||||
|
--zl-farbe-rgb: 154, 166, 173;
|
||||||
|
}
|
||||||
|
.zl-bahn-licht { --zl-farbe: #f0b429; --zl-farbe-rgb: 240, 180, 41; }
|
||||||
|
.zl-bahn-beschattung { --zl-farbe: #5fb0dc; --zl-farbe-rgb: 95, 176, 220; }
|
||||||
|
.zl-bahn-heizung { --zl-farbe: #e8836a; --zl-farbe-rgb: 232, 131, 106; }
|
||||||
|
.zl-bahn-bewaesserung { --zl-farbe: #4cc18a; --zl-farbe-rgb: 76, 193, 138; }
|
||||||
|
.zl-bahn-schalter { --zl-farbe: #b59cf0; --zl-farbe-rgb: 181, 156, 240; }
|
||||||
|
|
||||||
|
/* Kopf */
|
||||||
|
.zl-kopf {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: .6rem 1rem;
|
||||||
|
margin-bottom: .9rem;
|
||||||
|
}
|
||||||
|
.zl-tag .btn.zl-tag-name { min-width: 9.5rem; font-variant-numeric: tabular-nums; }
|
||||||
|
.zl-etagen { display: flex; flex-wrap: wrap; gap: .3rem; }
|
||||||
|
.zl-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .35rem;
|
||||||
|
padding: .2rem .65rem;
|
||||||
|
border: 1px solid var(--zl-linie);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ton-schrift-2, #9aa6ad);
|
||||||
|
font: 500 .8rem var(--font-anzeige, inherit);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background .15s, border-color .15s, color .15s;
|
||||||
|
}
|
||||||
|
.zl-chip:hover { border-color: var(--ton-linie-satt, rgba(138, 148, 158, .38)); color: var(--ton-schrift, #e8ede9); }
|
||||||
|
.zl-chip.an {
|
||||||
|
background: rgba(0, 120, 143, .22);
|
||||||
|
border-color: #00788F;
|
||||||
|
color: var(--ton-schrift, #e8ede9);
|
||||||
|
}
|
||||||
|
.zl-chip.leer { opacity: .55; }
|
||||||
|
.zl-chip-zahl {
|
||||||
|
font-size: .68rem;
|
||||||
|
padding: 0 .35rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(138, 148, 158, .18);
|
||||||
|
color: var(--ton-schrift-2, #9aa6ad);
|
||||||
|
}
|
||||||
|
.zl-zahl {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: .8rem;
|
||||||
|
color: var(--ton-schrift-2, #9aa6ad);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.zl-zahl-leise { color: var(--ton-schrift-3, #6f7a81); }
|
||||||
|
|
||||||
|
/* Raster */
|
||||||
|
.zl-rahmen {
|
||||||
|
overflow-x: auto;
|
||||||
|
border-radius: var(--ton-radius-klein, .5rem);
|
||||||
|
}
|
||||||
|
.zl-raster { position: relative; }
|
||||||
|
.zl-zeile {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: var(--zl-kopf) minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
.zl-bahn .zl-spur { border-top: 1px solid var(--zl-linie); }
|
||||||
|
.zl-bahn-kopf {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 3;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: .45rem;
|
||||||
|
padding: .6rem .6rem 0 0;
|
||||||
|
background: var(--ton-flaeche, #23282c);
|
||||||
|
font: 500 .82rem var(--font-anzeige, inherit);
|
||||||
|
color: var(--ton-schrift, #e8ede9);
|
||||||
|
}
|
||||||
|
.zl-bahn-kopf .bi { color: var(--zl-farbe); font-size: .95rem; line-height: 1.1; }
|
||||||
|
/* Schmal nur das Symbol; der Name steht im Hinweis beim Antippen der Eintraege. */
|
||||||
|
.zl-rahmen.schmal .zl-bahn-kopf { justify-content: center; padding-right: 0; }
|
||||||
|
.zl-rahmen.schmal .zl-bahn-kopf span { display: none; }
|
||||||
|
.zl-bahn-zahl {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: .68rem;
|
||||||
|
color: var(--ton-schrift-3, #6f7a81);
|
||||||
|
}
|
||||||
|
.zl-spur {
|
||||||
|
position: relative;
|
||||||
|
min-height: 38px;
|
||||||
|
/* Alle drei Stunden eine Linie, jede volle Stunde ein Hauch. */
|
||||||
|
background-image:
|
||||||
|
linear-gradient(to right, rgba(138, 148, 158, .16) 1px, transparent 1px),
|
||||||
|
linear-gradient(to right, rgba(138, 148, 158, .06) 1px, transparent 1px);
|
||||||
|
background-size: 12.5% 100%, 4.1667% 100%;
|
||||||
|
}
|
||||||
|
.zl-nacht {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, .24);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.zl-jetzt {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 0;
|
||||||
|
border-left: 1.5px solid var(--ton-sonne, #f0b429);
|
||||||
|
opacity: .8;
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Achse */
|
||||||
|
.zl-achse .zl-bahn-kopf { padding-top: 0; }
|
||||||
|
.zl-achse .zl-spur { height: 26px; min-height: 0; background: none; }
|
||||||
|
.zl-stunde {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: .7rem;
|
||||||
|
color: var(--ton-schrift-3, #6f7a81);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.zl-stunde.anfang { transform: none; }
|
||||||
|
.zl-stunde.ende { transform: translateX(-100%); }
|
||||||
|
.zl-sonne {
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: .78rem;
|
||||||
|
color: var(--ton-sonne, #f0b429);
|
||||||
|
opacity: .75;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
.zl-jetzt-marke {
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 0 .35rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--ton-sonne, #f0b429);
|
||||||
|
color: #1b1f22;
|
||||||
|
font: 600 .66rem var(--font-anzeige, inherit);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Eintraege */
|
||||||
|
.zl-ding {
|
||||||
|
position: absolute;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.zl-ding:hover { z-index: 4; }
|
||||||
|
.zl-marke {
|
||||||
|
flex: none;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--zl-farbe);
|
||||||
|
background: var(--ton-flaeche, #23282c);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.zl-punkt { transform: translateX(-6px); }
|
||||||
|
.zl-punkt.links { flex-direction: row-reverse; transform: translateX(calc(-100% + 6px)); }
|
||||||
|
.zl-etikett {
|
||||||
|
margin-left: 6px;
|
||||||
|
padding: .1rem .5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: rgba(var(--zl-farbe-rgb), .1);
|
||||||
|
color: var(--ton-schrift, #e8ede9);
|
||||||
|
font-size: .75rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background .15s, border-color .15s;
|
||||||
|
}
|
||||||
|
.zl-punkt.links .zl-etikett { margin-left: 0; margin-right: 6px; }
|
||||||
|
.zl-etikett b {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: .4rem;
|
||||||
|
color: var(--zl-farbe);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.zl-etikett .bi-check2 { color: var(--ton-gut, #4caf7d); }
|
||||||
|
.zl-ding:hover .zl-etikett {
|
||||||
|
background: rgba(var(--zl-farbe-rgb), .2);
|
||||||
|
border-color: rgba(var(--zl-farbe-rgb), .45);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zustaende */
|
||||||
|
.zl-ding.gelaufen .zl-marke { background: var(--zl-farbe); }
|
||||||
|
.zl-ding.vorbei { opacity: .5; }
|
||||||
|
.zl-ding.vorbei .zl-etikett b { color: var(--ton-schrift-2, #9aa6ad); }
|
||||||
|
.zl-ding.aktiv .zl-marke { box-shadow: 0 0 0 3px rgba(var(--zl-farbe-rgb), .25); }
|
||||||
|
.zl-ding.nichtdran .zl-marke { border-style: dashed; border-color: var(--ton-schrift-3, #6f7a81); background: transparent; }
|
||||||
|
.zl-ding.nichtdran .zl-etikett {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px dashed var(--zl-linie);
|
||||||
|
color: var(--ton-schrift-3, #6f7a81);
|
||||||
|
}
|
||||||
|
.zl-ding.nichtdran .zl-etikett b { color: var(--ton-schrift-3, #6f7a81); }
|
||||||
|
|
||||||
|
/* Zeitfenster: der Balken traegt Laeufe und die frueheste Marke. */
|
||||||
|
.zl-fenster { min-width: 10px; }
|
||||||
|
.zl-balken {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(var(--zl-farbe-rgb), .22);
|
||||||
|
border: 1px solid rgba(var(--zl-farbe-rgb), .4);
|
||||||
|
}
|
||||||
|
.zl-fenster .zl-etikett {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 0;
|
||||||
|
background: var(--ton-flaeche, #23282c);
|
||||||
|
border-color: rgba(var(--zl-farbe-rgb), .3);
|
||||||
|
}
|
||||||
|
/* Am rechten Rand endet das Etikett am Balkenende statt dahinter. */
|
||||||
|
.zl-fenster.links { justify-content: flex-end; }
|
||||||
|
.zl-lauf {
|
||||||
|
margin-left: .25rem;
|
||||||
|
color: var(--ton-gut, #4caf7d);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.zl-fenster.nichtdran .zl-balken { background: transparent; border-style: dashed; border-color: var(--zl-linie); }
|
||||||
|
.zl-marke-innen {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ketten */
|
||||||
|
.zl-ketten {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
.zl-ketten path {
|
||||||
|
fill: none;
|
||||||
|
stroke: rgba(154, 166, 173, .55);
|
||||||
|
stroke-width: 1.2;
|
||||||
|
stroke-dasharray: 3 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zl-leer {
|
||||||
|
padding: 1.2rem 0 .6rem 132px;
|
||||||
|
color: var(--ton-schrift-3, #6f7a81);
|
||||||
|
font-size: .85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Baender */
|
||||||
|
.zl-baender {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .45rem;
|
||||||
|
margin-top: .8rem;
|
||||||
|
padding-top: .7rem;
|
||||||
|
border-top: 1px solid var(--zl-linie);
|
||||||
|
}
|
||||||
|
.zl-band {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 132px minmax(0, 1fr);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.zl-band-chips { display: flex; flex-wrap: wrap; gap: .35rem; }
|
||||||
|
@media (max-width: 575.98px) {
|
||||||
|
.zl-band { grid-template-columns: minmax(0, 1fr); gap: .3rem; }
|
||||||
|
.zl-zahl { margin-left: 0; }
|
||||||
|
}
|
||||||
|
.zl-band-titel {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .4rem;
|
||||||
|
padding-top: .2rem;
|
||||||
|
font: 500 .82rem var(--font-anzeige, inherit);
|
||||||
|
color: var(--ton-schrift-2, #9aa6ad);
|
||||||
|
}
|
||||||
|
.zl-band-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .4rem;
|
||||||
|
padding: .2rem .6rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--zl-linie);
|
||||||
|
background: rgba(138, 148, 158, .07);
|
||||||
|
color: var(--ton-schrift, #e8ede9);
|
||||||
|
font-size: .75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.zl-band-chip:hover { border-color: var(--ton-linie-satt, rgba(138, 148, 158, .38)); }
|
||||||
|
.zl-band-pause .zl-band-chip { color: var(--ton-schrift-2, #9aa6ad); }
|
||||||
|
.zl-band-chip.nichtdran { border-style: dashed; }
|
||||||
|
.zl-band-etage {
|
||||||
|
font-size: .64rem;
|
||||||
|
padding: 0 .3rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(138, 148, 158, .16);
|
||||||
|
color: var(--ton-schrift-3, #6f7a81);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legende */
|
||||||
|
.zl-legende {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: .4rem 1.1rem;
|
||||||
|
margin-top: .8rem;
|
||||||
|
font-size: .72rem;
|
||||||
|
color: var(--ton-schrift-3, #6f7a81);
|
||||||
|
}
|
||||||
|
.zl-legende span { display: inline-flex; align-items: center; gap: .4rem; }
|
||||||
|
.zl-legende i { display: inline-block; }
|
||||||
|
.zl-l-punkt { width: 10px; height: 10px; border-radius: 50%; border: 2px solid #5fb0dc; }
|
||||||
|
.zl-l-punkt.gelaufen { background: #5fb0dc; }
|
||||||
|
.zl-l-punkt.nichtdran { border-style: dashed; border-color: var(--ton-schrift-3, #6f7a81); }
|
||||||
|
.zl-l-balken { width: 22px; height: 7px; border-radius: 4px; background: rgba(95, 176, 220, .22); border: 1px solid rgba(95, 176, 220, .4); }
|
||||||
|
.zl-l-kette { width: 22px; border-top: 1.2px dashed rgba(154, 166, 173, .7); }
|
||||||
|
|
||||||
|
/* Ansicht umschalten: eine Klasse mehr als Bootstrap (solar.css laedt vorher). */
|
||||||
|
.zl-ansicht .btn.active {
|
||||||
|
background: rgba(0, 120, 143, .25);
|
||||||
|
border-color: #00788F;
|
||||||
|
color: var(--ton-schrift, #e8ede9);
|
||||||
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ if (isset($_GET["addUser"])) {
|
|||||||
"solar" => ["template" => "solar.php", "mqtt" => true, "charts" => true, "meteogram" => true, "leaflet" => false,
|
"solar" => ["template" => "solar.php", "mqtt" => true, "charts" => true, "meteogram" => true, "leaflet" => false,
|
||||||
"scripts" => ["js/solar/jahresstatistik.js", "js/solar/solarMQTT.js"]],
|
"scripts" => ["js/solar/jahresstatistik.js", "js/solar/solarMQTT.js"]],
|
||||||
"home" => ["template" => "home.php", "mqtt" => true, "charts" => false, "meteogram" => false, "leaflet" => false,
|
"home" => ["template" => "home.php", "mqtt" => true, "charts" => false, "meteogram" => false, "leaflet" => false,
|
||||||
"scripts" => ["js/solar/autoActionFuncs.js", "js/solar/homeMQTT.js"]],
|
"scripts" => ["js/solar/autoActionFuncs.js", "js/solar/zeitleiste.js", "js/solar/homeMQTT.js"]],
|
||||||
"heat" => ["template" => "heat.php", "mqtt" => true, "charts" => true, "meteogram" => false, "leaflet" => false,
|
"heat" => ["template" => "heat.php", "mqtt" => true, "charts" => true, "meteogram" => false, "leaflet" => false,
|
||||||
"scripts" => ["js/solar/heatMQTT.js"]],
|
"scripts" => ["js/solar/heatMQTT.js"]],
|
||||||
"history" => ["template" => "history.php", "mqtt" => false, "charts" => true, "meteogram" => false, "leaflet" => false,
|
"history" => ["template" => "history.php", "mqtt" => false, "charts" => true, "meteogram" => false, "leaflet" => false,
|
||||||
|
|||||||
@@ -1396,6 +1396,9 @@ function automationRequest(action, body) {
|
|||||||
* ausgelassen: die Tabelle waere leer geblieben, ohne Fehlermeldung.
|
* ausgelassen: die Tabelle waere leer geblieben, ohne Fehlermeldung.
|
||||||
*/
|
*/
|
||||||
function refreshAutomations(floor) {
|
function refreshAutomations(floor) {
|
||||||
|
// Die Zeitleiste zeigt dieselben Automatiken - nach Speichern, Pausieren
|
||||||
|
// oder Loeschen muss sie genauso nachziehen wie die Tabelle.
|
||||||
|
if (typeof zeitleisteLaden === "function") zeitleisteLaden();
|
||||||
const etagen = floor ? [floor]
|
const etagen = floor ? [floor]
|
||||||
: Array.from(document.querySelectorAll("#actions-tabContent .tab-pane"))
|
: Array.from(document.querySelectorAll("#actions-tabContent .tab-pane"))
|
||||||
.map(pane => pane.id.replace(/^actions-/, ""));
|
.map(pane => pane.id.replace(/^actions-/, ""));
|
||||||
|
|||||||
@@ -0,0 +1,484 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
Automatismen als Zeitleiste eines Tages.
|
||||||
|
|
||||||
|
Die Daten rechnet der Server (restricted/zeitleiste.php): wo eine
|
||||||
|
Automatik auf dem Tag liegt, in welcher Bahn sie steht, ob sie an dem Tag
|
||||||
|
dran ist und wann sie gelaufen ist. Hier wird nur gezeichnet.
|
||||||
|
|
||||||
|
Jede Automatik erscheint genau einmal - in ihrer Bahn, unter "Jederzeit"
|
||||||
|
oder unter "Pausiert". Der Zähler oben sagt, wie viele es insgesamt sind;
|
||||||
|
fehlte eine, fiele das dort auf.
|
||||||
|
|
||||||
|
Die bisherige Tabelle bleibt als zweite Ansicht ("Liste"). Welche offen
|
||||||
|
ist und welche Etagen eingeblendet sind, merkt sich der Browser.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
const zeitleiste = {
|
||||||
|
datum: null, // "YYYY-MM-DD", null = heute
|
||||||
|
daten: null, // Antwort von ?action=zeitleiste
|
||||||
|
etagen: null, // Set der eingeblendeten Etagen, null = alle
|
||||||
|
uhr: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ZL_ZEILE = 30; // Höhe einer Zeile in einer Bahn, px
|
||||||
|
const ZL_KOPF = 132; // Breite der Bahnbeschriftung, px
|
||||||
|
const ZL_MIN_SPUR = 620; // schmaler wird die Spur nicht, der Rahmen scrollt dann
|
||||||
|
|
||||||
|
/* --- Kleinigkeiten ------------------------------------------------------- */
|
||||||
|
|
||||||
|
function zlSpeicher(schluessel, wert) {
|
||||||
|
try {
|
||||||
|
if (wert === undefined) return localStorage.getItem(schluessel);
|
||||||
|
localStorage.setItem(schluessel, wert);
|
||||||
|
} catch (e) { return null; }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function zlText(s) {
|
||||||
|
const d = document.createElement("div");
|
||||||
|
d.textContent = String(s === undefined || s === null ? "" : s);
|
||||||
|
return d.innerHTML.replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
|
function zlUhr(m) {
|
||||||
|
m = ((Math.round(m) % 1440) + 1440) % 1440;
|
||||||
|
return String(Math.floor(m / 60)).padStart(2, "0") + ":" + String(m % 60).padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
function zlIso(d) {
|
||||||
|
return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
function zlDatumText(iso, heute) {
|
||||||
|
const d = new Date(iso + "T12:00:00");
|
||||||
|
const tage = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"];
|
||||||
|
const kurz = tage[d.getDay()] + " " + String(d.getDate()).padStart(2, "0") + "." + String(d.getMonth() + 1).padStart(2, "0") + ".";
|
||||||
|
const morgen = new Date(); morgen.setDate(morgen.getDate() + 1);
|
||||||
|
if (heute) return "Heute · " + kurz;
|
||||||
|
if (iso === zlIso(morgen)) return "Morgen · " + kurz;
|
||||||
|
return kurz;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Textbreite in px, für das Stapeln - einmal ein Canvas, dann nur messen. */
|
||||||
|
function zlBreite(text) {
|
||||||
|
const c = zlBreite.c || (zlBreite.c = document.createElement("canvas").getContext("2d"));
|
||||||
|
c.font = "500 12px " + getComputedStyle(document.body).fontFamily;
|
||||||
|
return c.measureText(text).width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Einordnen ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
/** Was an Automatiken nach dem Etagenfilter übrig bleibt. */
|
||||||
|
function zlSichtbar() {
|
||||||
|
const alle = zeitleiste.daten.automatiken;
|
||||||
|
return zeitleiste.etagen ? alle.filter(a => zeitleiste.etagen.has(a.floor)) : alle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wo ein Eintrag auf dem Tag beginnt und endet, in Minuten. */
|
||||||
|
function zlLage(a) {
|
||||||
|
if (a.art === "fenster") return [a.von, a.bis];
|
||||||
|
const m = a.laeufe.length ? zlMinuteAus(a.laeufe[a.laeufe.length - 1]) : a.punkt;
|
||||||
|
return [m, m];
|
||||||
|
}
|
||||||
|
|
||||||
|
function zlMinuteAus(hhmm) {
|
||||||
|
const t = String(hhmm).split(":");
|
||||||
|
return Number(t[0]) * 60 + Number(t[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Zustand eines Eintrags - bestimmt Farbe und Form. */
|
||||||
|
function zlZustand(a, d) {
|
||||||
|
if (!a.dran) return "nichtdran";
|
||||||
|
if (a.laeufe.length) return "gelaufen";
|
||||||
|
if (d.jetzt !== null) {
|
||||||
|
const ende = a.art === "fenster" ? a.bis : a.punkt;
|
||||||
|
if (ende !== null && ende < d.jetzt) return "vorbei";
|
||||||
|
const anfang = a.art === "fenster" ? a.von : a.punkt;
|
||||||
|
if (anfang !== null && anfang <= d.jetzt) return "aktiv";
|
||||||
|
}
|
||||||
|
return "kommt";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einträge einer Bahn in Zeilen verteilen, wie im Kalender: jeder kommt in
|
||||||
|
* die erste Zeile, in der er nichts überdeckt. Gerechnet in px, weil das
|
||||||
|
* Etikett mitzählt - zwei Punkte zehn Minuten auseinander liegen zeitlich
|
||||||
|
* getrennt, ihre Beschriftungen aber übereinander.
|
||||||
|
*/
|
||||||
|
function zlStapeln(eintraege, breite) {
|
||||||
|
const px = m => m / 1440 * breite;
|
||||||
|
const liste = eintraege.map(a => {
|
||||||
|
const [von, bis] = zlLage(a);
|
||||||
|
const etikett = zlEtikettText(a);
|
||||||
|
const lang = zlBreite(etikett) + 26;
|
||||||
|
let links = px(von) - 8;
|
||||||
|
let rechts = a.art === "fenster" ? Math.max(px(bis), px(von) + lang) : px(von) + lang;
|
||||||
|
// Am rechten Rand steht das Etikett links vom Punkt bzw. endet am
|
||||||
|
// Balkenende - sonst ragte es aus der Spur und der Rahmen bekaeme einen
|
||||||
|
// Scrollbalken fuer ein paar Buchstaben.
|
||||||
|
const nachLinks = px(von) + lang > breite - 4;
|
||||||
|
if (nachLinks && a.art !== "fenster") { links = px(von) - lang; rechts = px(von) + 8; }
|
||||||
|
if (nachLinks && a.art === "fenster") { links = Math.min(px(von), px(bis) - lang) - 8; rechts = px(bis); }
|
||||||
|
return { a, links, rechts, nachLinks, etikett };
|
||||||
|
}).sort((x, y) => x.links - y.links);
|
||||||
|
|
||||||
|
const zeilen = [];
|
||||||
|
liste.forEach(e => {
|
||||||
|
let z = zeilen.findIndex(ende => ende + 6 <= e.links);
|
||||||
|
if (z < 0) { z = zeilen.length; zeilen.push(0); }
|
||||||
|
zeilen[z] = e.rechts;
|
||||||
|
e.zeile = z;
|
||||||
|
});
|
||||||
|
return { liste, zeilen: Math.max(1, zeilen.length) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Zeit vorne im Etikett. Beim Punkt die Zeit des Punkts - nach einem
|
||||||
|
* Lauf also die echte. Beim Fenster immer das Fenster; der Lauf steht dann
|
||||||
|
* hinten mit Haken, sonst stünde "12:50" am Balkenanfang bei 09:30.
|
||||||
|
*/
|
||||||
|
function zlEtikettZeit(a) {
|
||||||
|
if (a.art === "fenster") return zlUhr(a.von) + "–" + zlUhr(a.bis);
|
||||||
|
return a.laeufe.length ? a.laeufe[a.laeufe.length - 1] : zlUhr(a.punkt);
|
||||||
|
}
|
||||||
|
|
||||||
|
function zlEtikettText(a) {
|
||||||
|
const lauf = a.art === "fenster" && a.laeufe.length ? " ✓ " + a.laeufe[a.laeufe.length - 1] : (a.laeufe.length ? " ✓" : "");
|
||||||
|
return zlEtikettZeit(a) + " " + a.name + lauf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Der Hinweis beim Überfahren: was, wann, wovon abhängig, wie steht es. */
|
||||||
|
function zlTitel(a, zustand) {
|
||||||
|
const zeilen = [a.name, a.satz];
|
||||||
|
if (a.art === "fenster") zeilen.push("Zeitfenster " + zlUhr(a.von) + "–" + zlUhr(a.bis) + (a.punkt !== null ? ", frühestens " + zlUhr(a.punkt) : ""));
|
||||||
|
else if (a.punkt !== null) zeilen.push((a.art === "kette" ? "frühestens " : "um ") + zlUhr(a.punkt) + (a.sonne ? " (Sonnenstand)" : ""));
|
||||||
|
if (a.wenn) zeilen.push("Wenn: " + a.wenn);
|
||||||
|
const stand = {
|
||||||
|
gelaufen: "gelaufen um " + a.laeufe.join(", "),
|
||||||
|
nichtdran: "an diesem Tag nicht dran: " + a.grund,
|
||||||
|
vorbei: "nicht gelaufen" + (a.wartet ? " – Bedingung war nicht erfüllt" : ""),
|
||||||
|
aktiv: a.wartet ? "wartet auf die Bedingung" : "steht an",
|
||||||
|
kommt: "kommt noch",
|
||||||
|
}[zustand];
|
||||||
|
zeilen.push("Stand: " + stand);
|
||||||
|
return zeilen.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Zeichnen ------------------------------------------------------------ */
|
||||||
|
|
||||||
|
function zeitleisteZeichnen() {
|
||||||
|
const ziel = document.getElementById("zeitleiste");
|
||||||
|
const d = zeitleiste.daten;
|
||||||
|
if (!ziel || !d || ziel.hidden) return;
|
||||||
|
|
||||||
|
const sichtbar = zlSichtbar();
|
||||||
|
const aktiv = sichtbar.filter(a => a.enabled);
|
||||||
|
const pausiert = sichtbar.filter(a => !a.enabled);
|
||||||
|
const jederzeit = aktiv.filter(a => a.art === "jederzeit");
|
||||||
|
const gelegt = aktiv.filter(a => a.art !== "jederzeit" && (a.punkt !== null || a.von !== null));
|
||||||
|
|
||||||
|
// --- Kopf: Tag, Etagen, Zähler
|
||||||
|
const alleEtagen = !zeitleiste.etagen;
|
||||||
|
let html = '<div class="zl-kopf">'
|
||||||
|
+ '<div class="btn-group btn-group-sm zl-tag" role="group">'
|
||||||
|
+ '<button type="button" class="btn btn-outline-secondary" data-tag="-1" title="Tag davor"><i class="bi bi-chevron-left"></i></button>'
|
||||||
|
+ '<button type="button" class="btn btn-outline-secondary zl-tag-name" data-tag="0" title="Zurück zu heute">'
|
||||||
|
+ zlText(zlDatumText(d.datum, d.heute)) + "</button>"
|
||||||
|
+ '<button type="button" class="btn btn-outline-secondary" data-tag="1" title="Tag danach"><i class="bi bi-chevron-right"></i></button>'
|
||||||
|
+ "</div>"
|
||||||
|
+ '<div class="zl-etagen" role="group" aria-label="Etagen">'
|
||||||
|
+ '<button type="button" class="zl-chip' + (alleEtagen ? " an" : "") + '" data-etage="">Alle</button>'
|
||||||
|
+ d.etagen.map(e => {
|
||||||
|
const an = alleEtagen || zeitleiste.etagen.has(e.code);
|
||||||
|
const anzahl = d.automatiken.filter(a => a.floor === e.code).length;
|
||||||
|
return '<button type="button" class="zl-chip' + (an && !alleEtagen ? " an" : "") + (anzahl ? "" : " leer") + '"'
|
||||||
|
+ ' data-etage="' + zlText(e.code) + '" title="' + zlText(e.label) + '">'
|
||||||
|
+ zlText(e.code) + '<span class="zl-chip-zahl">' + anzahl + "</span></button>";
|
||||||
|
}).join("")
|
||||||
|
+ "</div>"
|
||||||
|
+ '<div class="zl-zahl" title="Jede Automatik steht genau einmal: in ihrer Bahn, unter Jederzeit oder unter Pausiert.">'
|
||||||
|
+ '<i class="bi bi-eye"></i> ' + sichtbar.length + " von " + d.automatiken.length
|
||||||
|
+ (pausiert.length ? ' <span class="zl-zahl-leise">· ' + pausiert.length + " pausiert</span>" : "")
|
||||||
|
+ "</div></div>";
|
||||||
|
|
||||||
|
// --- Raster. Schmal (Handy) traegt die Bahn nur ihr Symbol - der Name
|
||||||
|
// nähme sonst ein Drittel der Breite, und die Spur scrollt ohnehin.
|
||||||
|
const rahmenBreite = ziel.clientWidth || 800;
|
||||||
|
const schmal = rahmenBreite < 560;
|
||||||
|
const kopf = schmal ? 40 : ZL_KOPF;
|
||||||
|
const breite = Math.max(ZL_MIN_SPUR, rahmenBreite - kopf - 2);
|
||||||
|
const altRahmen = ziel.querySelector(".zl-rahmen");
|
||||||
|
const altScroll = altRahmen ? altRahmen.scrollLeft : null;
|
||||||
|
const prozent = m => (m / 1440 * 100).toFixed(3) + "%";
|
||||||
|
const nacht = d.sonne.auf !== null
|
||||||
|
? '<div class="zl-nacht" style="left:0;width:' + prozent(d.sonne.auf) + '"></div>'
|
||||||
|
+ '<div class="zl-nacht" style="left:' + prozent(d.sonne.unter) + ';right:0"></div>'
|
||||||
|
: "";
|
||||||
|
const jetzt = d.jetzt !== null ? '<div class="zl-jetzt" style="left:' + prozent(d.jetzt) + '"></div>' : "";
|
||||||
|
|
||||||
|
html += '<div class="zl-rahmen' + (schmal ? " schmal" : "") + '"><div class="zl-raster" style="--zl-kopf:' + kopf + "px;min-width:" + (breite + kopf) + 'px">';
|
||||||
|
|
||||||
|
// Stundenachse mit Sonnenmarken
|
||||||
|
html += '<div class="zl-zeile zl-achse"><div class="zl-bahn-kopf"></div><div class="zl-spur">';
|
||||||
|
for (let h = 0; h <= 24; h += 3) {
|
||||||
|
const rand = h === 0 ? " anfang" : h === 24 ? " ende" : "";
|
||||||
|
html += '<span class="zl-stunde' + rand + '" style="left:' + prozent(h * 60) + '">' + h + "</span>";
|
||||||
|
}
|
||||||
|
if (d.sonne.auf !== null) {
|
||||||
|
const geschaetzt = d.sonne.genau ? "" : " (Vorjahr)";
|
||||||
|
html += '<span class="zl-sonne" style="left:' + prozent(d.sonne.auf) + '" title="Sonnenaufgang ' + zlUhr(d.sonne.auf) + geschaetzt + '"><i class="bi bi-sunrise"></i></span>'
|
||||||
|
+ '<span class="zl-sonne" style="left:' + prozent(d.sonne.unter) + '" title="Sonnenuntergang ' + zlUhr(d.sonne.unter) + geschaetzt + '"><i class="bi bi-sunset"></i></span>';
|
||||||
|
}
|
||||||
|
if (d.jetzt !== null) html += '<span class="zl-jetzt-marke" style="left:' + prozent(d.jetzt) + '">' + zlUhr(d.jetzt) + "</span>";
|
||||||
|
html += "</div></div>";
|
||||||
|
|
||||||
|
// Bahnen
|
||||||
|
const bahnen = Object.keys(d.bahnen).filter(b => gelegt.some(a => a.bahn === b));
|
||||||
|
if (!bahnen.length) {
|
||||||
|
html += '<div class="zl-leer">Keine Automatik mit einer Zeit auf diesem Tag'
|
||||||
|
+ (zeitleiste.etagen ? " in den gewählten Etagen" : "") + ".</div>";
|
||||||
|
}
|
||||||
|
bahnen.forEach(b => {
|
||||||
|
const eintraege = gelegt.filter(a => a.bahn === b);
|
||||||
|
const stapel = zlStapeln(eintraege, breite);
|
||||||
|
const hoehe = stapel.zeilen * ZL_ZEILE + 8;
|
||||||
|
html += '<div class="zl-zeile zl-bahn zl-bahn-' + b + '">'
|
||||||
|
+ '<div class="zl-bahn-kopf"><i class="bi ' + d.bahnen[b].symbol + '"></i><span>' + zlText(d.bahnen[b].titel) + "</span>"
|
||||||
|
+ '<span class="zl-bahn-zahl">' + eintraege.length + "</span></div>"
|
||||||
|
+ '<div class="zl-spur" style="height:' + hoehe + 'px">' + nacht + jetzt;
|
||||||
|
stapel.liste.forEach(e => { html += zlEintrag(e, d, prozent); });
|
||||||
|
html += "</div></div>";
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '<svg class="zl-ketten" aria-hidden="true"></svg></div></div>';
|
||||||
|
|
||||||
|
// --- Bänder ohne Lage auf dem Tag
|
||||||
|
if (jederzeit.length || pausiert.length) {
|
||||||
|
html += '<div class="zl-baender">';
|
||||||
|
if (jederzeit.length) {
|
||||||
|
html += '<div class="zl-band"><span class="zl-band-titel"><i class="bi bi-infinity"></i>Jederzeit</span>'
|
||||||
|
+ '<div class="zl-band-chips">'
|
||||||
|
+ jederzeit.map(a => zlChip(a, d.bahnen[a.bahn] ? d.bahnen[a.bahn].symbol : "bi-dot")).join("") + "</div></div>";
|
||||||
|
}
|
||||||
|
if (pausiert.length) {
|
||||||
|
html += '<div class="zl-band zl-band-pause"><span class="zl-band-titel"><i class="bi bi-pause-circle"></i>Pausiert</span>'
|
||||||
|
+ '<div class="zl-band-chips">' + pausiert.map(a => zlChip(a, "bi-pause-fill")).join("") + "</div></div>";
|
||||||
|
}
|
||||||
|
html += "</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '<div class="zl-legende">'
|
||||||
|
+ '<span><i class="zl-l-punkt gelaufen"></i>gelaufen</span>'
|
||||||
|
+ '<span><i class="zl-l-punkt"></i>kommt noch</span>'
|
||||||
|
+ '<span><i class="zl-l-balken"></i>Zeitfenster, wartet auf Bedingung</span>'
|
||||||
|
+ '<span><i class="zl-l-kette"></i>Kette</span>'
|
||||||
|
+ '<span><i class="zl-l-punkt nichtdran"></i>an diesem Tag nicht dran</span>'
|
||||||
|
+ "</div>";
|
||||||
|
|
||||||
|
ziel.innerHTML = html;
|
||||||
|
zlKettenZeichnen(ziel);
|
||||||
|
|
||||||
|
// Muss gescrollt werden, steht die Leiste beim ersten Zeichnen bei "jetzt"
|
||||||
|
// (an einem anderen Tag beim Morgen) statt bei Mitternacht. Beim
|
||||||
|
// Neuzeichnen bleibt sie, wo man sie hingeschoben hat.
|
||||||
|
const rahmen = ziel.querySelector(".zl-rahmen");
|
||||||
|
if (rahmen && rahmen.scrollWidth > rahmen.clientWidth) {
|
||||||
|
if (altScroll !== null && zeitleiste.gescrollt) {
|
||||||
|
rahmen.scrollLeft = altScroll;
|
||||||
|
} else {
|
||||||
|
const ziel_m = d.jetzt !== null ? d.jetzt - 120 : 5 * 60;
|
||||||
|
rahmen.scrollLeft = Math.max(0, ziel_m / 1440 * breite);
|
||||||
|
zeitleiste.gescrollt = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ein Eintrag in einer Bahn. */
|
||||||
|
function zlEintrag(e, d, prozent) {
|
||||||
|
const a = e.a;
|
||||||
|
const zustand = zlZustand(a, d);
|
||||||
|
const oben = 4 + e.zeile * ZL_ZEILE;
|
||||||
|
const titel = zlText(zlTitel(a, zustand));
|
||||||
|
const lauf = zustand !== "gelaufen" ? ""
|
||||||
|
: ' <i class="bi bi-check2"></i>' + (a.art === "fenster" ? '<span class="zl-lauf">' + zlText(a.laeufe[a.laeufe.length - 1]) + "</span>" : "");
|
||||||
|
const etikett = '<span class="zl-etikett"><b>' + zlText(zlEtikettZeit(a)) + "</b>" + zlText(a.name) + lauf + "</span>";
|
||||||
|
|
||||||
|
if (a.art === "fenster") {
|
||||||
|
// Läufe und die früheste Marke als Punkte auf dem Balken.
|
||||||
|
let marken = "";
|
||||||
|
if (a.punkt !== null && a.punkt > a.von && !a.laeufe.length) {
|
||||||
|
marken += '<span class="zl-marke zl-marke-innen" style="left:' + ((a.punkt - a.von) / Math.max(1, a.bis - a.von) * 100).toFixed(2) + '%"></span>';
|
||||||
|
}
|
||||||
|
a.laeufe.forEach(t => {
|
||||||
|
const m = zlMinuteAus(t);
|
||||||
|
marken += '<span class="zl-marke zl-marke-innen gelaufen" style="left:' + ((m - a.von) / Math.max(1, a.bis - a.von) * 100).toFixed(2) + '%"></span>';
|
||||||
|
});
|
||||||
|
return '<div class="zl-ding zl-fenster ' + zustand + (e.nachLinks ? " links" : "") + '" data-id="' + a.id + '" title="' + titel + '"'
|
||||||
|
+ ' style="top:' + oben + "px;left:" + prozent(a.von) + ";width:" + prozent(Math.max(10, a.bis - a.von)) + '">'
|
||||||
|
+ '<span class="zl-balken">' + marken + "</span>" + etikett + "</div>";
|
||||||
|
}
|
||||||
|
const m = a.laeufe.length ? zlMinuteAus(a.laeufe[a.laeufe.length - 1]) : a.punkt;
|
||||||
|
return '<div class="zl-ding zl-punkt ' + zustand + (e.nachLinks ? " links" : "") + '" data-id="' + a.id + '"'
|
||||||
|
+ (a.kette ? ' data-kette="' + a.kette + '"' : "") + ' title="' + titel + '"'
|
||||||
|
+ ' style="top:' + oben + "px;left:" + prozent(m) + '">'
|
||||||
|
+ '<span class="zl-marke' + (a.sonne ? " sonne" : "") + '"></span>' + etikett + "</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
function zlChip(a, symbol) {
|
||||||
|
return '<button type="button" class="zl-band-chip' + (a.dran ? "" : " nichtdran") + '" data-id="' + a.id + '"'
|
||||||
|
+ ' title="' + zlText(a.satz + (a.wenn ? "\nWenn: " + a.wenn : "")) + '">'
|
||||||
|
+ '<i class="bi ' + symbol + '"></i>' + zlText(a.name)
|
||||||
|
+ '<span class="zl-band-etage">' + zlText(a.floor) + "</span></button>";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ketten als gestrichelte Bögen vom Auslöser zum Nachfolger. Erst nach dem
|
||||||
|
* Einsetzen, weil die Lage beider Punkte aus dem Layout kommt.
|
||||||
|
*/
|
||||||
|
function zlKettenZeichnen(ziel) {
|
||||||
|
const raster = ziel.querySelector(".zl-raster");
|
||||||
|
const svg = ziel.querySelector(".zl-ketten");
|
||||||
|
if (!raster || !svg) return;
|
||||||
|
const basis = raster.getBoundingClientRect();
|
||||||
|
svg.setAttribute("width", basis.width);
|
||||||
|
svg.setAttribute("height", basis.height);
|
||||||
|
let pfade = "";
|
||||||
|
raster.querySelectorAll(".zl-punkt[data-kette]").forEach(kind => {
|
||||||
|
const vater = raster.querySelector('.zl-ding[data-id="' + kind.dataset.kette + '"]');
|
||||||
|
if (!vater) return;
|
||||||
|
const mv = (vater.querySelector(".zl-marke") || vater).getBoundingClientRect();
|
||||||
|
const mk = kind.querySelector(".zl-marke").getBoundingClientRect();
|
||||||
|
const x1 = mv.left + mv.width / 2 - basis.left, y1 = mv.top + mv.height / 2 - basis.top;
|
||||||
|
const x2 = mk.left + mk.width / 2 - basis.left, y2 = mk.top + mk.height / 2 - basis.top;
|
||||||
|
const bogen = Math.max(18, Math.abs(y2 - y1) * 0.5);
|
||||||
|
pfade += '<path d="M' + x1 + " " + y1 + " C" + (x1 + bogen) + " " + y1 + " " + (x2 - bogen) + " " + y2 + " " + x2 + " " + y2 + '"/>';
|
||||||
|
});
|
||||||
|
svg.innerHTML = pfade;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Laden und Bedienen -------------------------------------------------- */
|
||||||
|
|
||||||
|
async function zeitleisteLaden() {
|
||||||
|
const ziel = document.getElementById("zeitleiste");
|
||||||
|
if (!ziel) return;
|
||||||
|
const datum = zeitleiste.datum || zlIso(new Date());
|
||||||
|
try {
|
||||||
|
const antwort = await fetch("./ajax/AutoAction.php?action=zeitleiste&datum=" + datum,
|
||||||
|
{ headers: { "Requested-With-Ajax": "ajax" }, cache: "no-store" });
|
||||||
|
const d = await antwort.json();
|
||||||
|
if (!antwort.ok || d.error) throw new Error(d.error || "HTTP " + antwort.status);
|
||||||
|
zeitleiste.daten = d;
|
||||||
|
// Etagen, die es nicht mehr gibt, fallen aus dem Filter.
|
||||||
|
if (zeitleiste.etagen) {
|
||||||
|
const codes = new Set(d.etagen.map(e => e.code));
|
||||||
|
zeitleiste.etagen = new Set([...zeitleiste.etagen].filter(c => codes.has(c)));
|
||||||
|
if (!zeitleiste.etagen.size) zeitleiste.etagen = null;
|
||||||
|
}
|
||||||
|
zeitleisteZeichnen();
|
||||||
|
} catch (e) {
|
||||||
|
ziel.innerHTML = '<div class="alert alert-danger mb-0">Zeitleiste nicht abrufbar: ' + zlText(e.message) + "</div>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function zlAnsicht(welche) {
|
||||||
|
const zl = document.getElementById("zeitleiste");
|
||||||
|
const liste = document.getElementById("automatikListe");
|
||||||
|
if (!zl || !liste) return;
|
||||||
|
zl.hidden = welche !== "zeitleiste";
|
||||||
|
liste.hidden = welche === "zeitleiste";
|
||||||
|
document.querySelectorAll(".zl-ansicht [data-ansicht]").forEach(k =>
|
||||||
|
k.classList.toggle("active", k.dataset.ansicht === welche));
|
||||||
|
zlSpeicher("automatik.ansicht", welche);
|
||||||
|
if (welche === "zeitleiste") {
|
||||||
|
if (zeitleiste.daten) zeitleisteZeichnen(); else zeitleisteLaden();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function zlEtagenMerken() {
|
||||||
|
zlSpeicher("automatik.etagen", zeitleiste.etagen ? [...zeitleiste.etagen].join(",") : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const ziel = document.getElementById("zeitleiste");
|
||||||
|
if (!ziel) return;
|
||||||
|
|
||||||
|
const gemerkt = zlSpeicher("automatik.etagen");
|
||||||
|
zeitleiste.etagen = gemerkt ? new Set(gemerkt.split(",").filter(Boolean)) : null;
|
||||||
|
if (zeitleiste.etagen && !zeitleiste.etagen.size) zeitleiste.etagen = null;
|
||||||
|
|
||||||
|
document.querySelectorAll(".zl-ansicht [data-ansicht]").forEach(k =>
|
||||||
|
k.addEventListener("click", () => zlAnsicht(k.dataset.ansicht)));
|
||||||
|
zlAnsicht(zlSpeicher("automatik.ansicht") === "liste" ? "liste" : "zeitleiste");
|
||||||
|
|
||||||
|
// Neue Automatik: in der ersten eingeblendeten Etage, sonst der Standard.
|
||||||
|
const neu = document.querySelector(".zl-neu");
|
||||||
|
if (neu) neu.addEventListener("click", function () {
|
||||||
|
let etage = typeof homeStandardEtage !== "undefined" ? homeStandardEtage : "";
|
||||||
|
if (zeitleiste.etagen && zeitleiste.daten) {
|
||||||
|
const erste = zeitleiste.daten.etagen.find(e => zeitleiste.etagen.has(e.code));
|
||||||
|
if (erste) etage = erste.code;
|
||||||
|
}
|
||||||
|
const liste = document.getElementById("automatikListe");
|
||||||
|
if (liste && !liste.hidden) {
|
||||||
|
const offen = document.querySelector("#actions-tabContent .tab-pane.active");
|
||||||
|
if (offen) etage = offen.id.replace(/^actions-/, "");
|
||||||
|
}
|
||||||
|
openAutoActionModal("?action=editor&floor=" + encodeURIComponent(etage));
|
||||||
|
});
|
||||||
|
|
||||||
|
ziel.addEventListener("click", function (ereignis) {
|
||||||
|
const t = ereignis.target;
|
||||||
|
const ding = t.closest("[data-id]");
|
||||||
|
if (ding) {
|
||||||
|
openAutoActionModal("?action=editor&id=" + ding.dataset.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tag = t.closest("[data-tag]");
|
||||||
|
if (tag) {
|
||||||
|
const schritt = Number(tag.dataset.tag);
|
||||||
|
if (schritt === 0) {
|
||||||
|
zeitleiste.datum = null;
|
||||||
|
} else {
|
||||||
|
const d = new Date((zeitleiste.datum || zlIso(new Date())) + "T12:00:00");
|
||||||
|
d.setDate(d.getDate() + schritt);
|
||||||
|
zeitleiste.datum = zlIso(d) === zlIso(new Date()) ? null : zlIso(d);
|
||||||
|
}
|
||||||
|
zeitleisteLaden();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const chip = t.closest("[data-etage]");
|
||||||
|
if (chip && zeitleiste.daten) {
|
||||||
|
const code = chip.dataset.etage;
|
||||||
|
if (!code) {
|
||||||
|
zeitleiste.etagen = null;
|
||||||
|
} else if (!zeitleiste.etagen) {
|
||||||
|
// Aus "alle" heraus blendet ein Klick genau diese Etage ein.
|
||||||
|
zeitleiste.etagen = new Set([code]);
|
||||||
|
} else {
|
||||||
|
zeitleiste.etagen.has(code) ? zeitleiste.etagen.delete(code) : zeitleiste.etagen.add(code);
|
||||||
|
if (!zeitleiste.etagen.size || zeitleiste.etagen.size === zeitleiste.daten.etagen.length) {
|
||||||
|
zeitleiste.etagen = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zlEtagenMerken();
|
||||||
|
zeitleisteZeichnen();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Neu zeichnen, wenn sich die Breite ändert - Stapeln und Ketten hängen daran.
|
||||||
|
let warte = null;
|
||||||
|
if (typeof ResizeObserver !== "undefined") {
|
||||||
|
let letzteBreite = 0;
|
||||||
|
new ResizeObserver(() => {
|
||||||
|
if (Math.abs(ziel.clientWidth - letzteBreite) < 4) return;
|
||||||
|
letzteBreite = ziel.clientWidth;
|
||||||
|
clearTimeout(warte);
|
||||||
|
warte = setTimeout(zeitleisteZeichnen, 120);
|
||||||
|
}).observe(ziel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heute rückt die Jetzt-Linie weiter, und neue Läufe kommen dazu.
|
||||||
|
zeitleiste.uhr = setInterval(() => {
|
||||||
|
if (!ziel.hidden && !zeitleiste.datum && !document.hidden) zeitleisteLaden();
|
||||||
|
}, 5 * 60 * 1000);
|
||||||
|
});
|
||||||
+16
-8
@@ -203,16 +203,23 @@ require_once(__DIR__ . "/reiter.php");
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-xxl-12 col-xl-12 col-lg-12 col-md-12 d-flex align-items-stretch">
|
<div class="col-xxl-12 col-xl-12 col-lg-12 col-md-12 d-flex align-items-stretch">
|
||||||
<div class="card p-0 mb-4" style="width:100%">
|
<div class="card p-0 mb-4" style="width:100%">
|
||||||
<div class="card-header pb-0 pt-1">
|
<div class="card-header pb-1 pt-1 d-flex align-items-center gap-2 flex-wrap">
|
||||||
<h3 class="card-title">Automatismen</h3>
|
<h3 class="card-title me-auto">Automatismen</h3>
|
||||||
<div class="card-tools">
|
<!-- Zwei Ansichten derselben Automatiken. Die Zeitleiste zeigt den
|
||||||
<button type="button" class="btn btn-tool" data-lte-toggle="card-remove">
|
Tag (js/solar/zeitleiste.js), die Liste ist die bisherige
|
||||||
<i class="bi bi-x-lg"></i>
|
Tabelle je Etage. Welche offen ist, merkt sich der Browser. -->
|
||||||
</button>
|
<div class="btn-group btn-group-sm zl-ansicht" role="group" aria-label="Ansicht">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-ansicht="zeitleiste">
|
||||||
|
<i class="bi bi-bar-chart-steps me-1"></i>Zeitleiste</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-ansicht="liste">
|
||||||
|
<i class="bi bi-list-ul me-1"></i>Liste</button>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="btn btn-sm btn-success zl-neu" title="Neue Automatik">
|
||||||
|
<i class="bi bi-plus-lg"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body pb-1">
|
<div class="card-body pb-2">
|
||||||
|
<div id="zeitleiste" class="zl" hidden></div>
|
||||||
|
<div id="automatikListe">
|
||||||
<?php
|
<?php
|
||||||
/*
|
/*
|
||||||
* Eine Etage, ein Reiter. Gezeichnet von reiterLeiste() aus
|
* Eine Etage, ein Reiter. Gezeichnet von reiterLeiste() aus
|
||||||
@@ -244,6 +251,7 @@ require_once(__DIR__ . "/reiter.php");
|
|||||||
<?= reiterFlaecheZu() ?>
|
<?= reiterFlaecheZu() ?>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
</div><!-- /automatikListe -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,419 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Die Automatiken eines Tages als Zeitleiste.
|
||||||
|
*
|
||||||
|
* 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 -
|
||||||
|
* und zwar ausnahmslos jede, auch künftige, nach festen Regeln:
|
||||||
|
*
|
||||||
|
* feste Uhrzeit (um / ab 07:00) Punkt
|
||||||
|
* Sonnenauf-/-untergang ± Versatz Punkt, an diesem Tag ausgerechnet
|
||||||
|
* Kette (andere Automatik + 00:10) Punkt hinter dem Auslöser
|
||||||
|
* Zeitfenster und Messwerte Balken über das Fenster
|
||||||
|
* gar keine Zeit, nur Messwerte "jederzeit"
|
||||||
|
*
|
||||||
|
* Die Bahn - die Zeile, in der sie steht - ergibt sich aus den Geräten, die
|
||||||
|
* sie schaltet: dieselbe Einordnung wie im Raum-Modal (bedienform() in
|
||||||
|
* roomControls.php), die Mehrheit gewinnt. Bewässerung ist dort "sonstiges"
|
||||||
|
* und wird hier am Gerätetyp erkannt.
|
||||||
|
*
|
||||||
|
* Ob eine Automatik an diesem Tag überhaupt dran ist, wird genau so
|
||||||
|
* gerechnet wie im Runner (tag_passt und gemeinter_tag in
|
||||||
|
* autoaction_runner.py) - sonst hieße "heute nicht" hier etwas anderes als
|
||||||
|
* dort. Ausgeblendet wird trotzdem nichts: wer nicht dran ist, steht
|
||||||
|
* gestrichelt da, mit Grund.
|
||||||
|
*
|
||||||
|
* Gerechnet wird nur die Lage auf dem Tag, nicht, ob eine Bedingung gerade
|
||||||
|
* erfüllt ist. Das entscheidet der Runner; hier steht, ob sie gelaufen ist
|
||||||
|
* (automation_log) und wann sie frühestens könnte.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once(__DIR__ . "/automations.php");
|
||||||
|
require_once(__DIR__ . "/roomControls.php");
|
||||||
|
require_once(__DIR__ . "/costs.php"); // solarDb() für die Sonnenzeiten
|
||||||
|
|
||||||
|
/** Reihenfolge der Bahnen und wie sie heißen. */
|
||||||
|
function zeitleisteBahnen()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
"licht" => ["titel" => "Licht", "symbol" => "bi-lightbulb"],
|
||||||
|
"beschattung" => ["titel" => "Beschattung", "symbol" => "bi-window-split"],
|
||||||
|
"heizung" => ["titel" => "Heizung", "symbol" => "bi-thermometer-half"],
|
||||||
|
"bewaesserung" => ["titel" => "Bewässerung", "symbol" => "bi-droplet"],
|
||||||
|
"schalter" => ["titel" => "Schalter", "symbol" => "bi-toggle-on"],
|
||||||
|
"sonstiges" => ["titel" => "Sonstiges", "symbol" => "bi-three-dots"],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "07:30" -> 450, "-00:30" wird von aufrufender Seite behandelt. */
|
||||||
|
function zlMinuten($text)
|
||||||
|
{
|
||||||
|
if (!preg_match('/^(\d{1,2}):(\d{2})/', trim(strval($text)), $t)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return intval($t[1]) * 60 + intval($t[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 450 -> "07:30" */
|
||||||
|
function zlUhrzeit($m)
|
||||||
|
{
|
||||||
|
$m = (($m % 1440) + 1440) % 1440;
|
||||||
|
return sprintf("%02d:%02d", intdiv($m, 60), $m % 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ferien und Feiertag an einem Datum aus calendar_days. */
|
||||||
|
function zlKalender($datum)
|
||||||
|
{
|
||||||
|
static $cache = [];
|
||||||
|
if (isset($cache[$datum])) {
|
||||||
|
return $cache[$datum];
|
||||||
|
}
|
||||||
|
$stand = ["feiertag" => null, "ferien" => null];
|
||||||
|
$stmt = meshDb()->prepare("SELECT holiday, vacation FROM calendar_days WHERE date = ?");
|
||||||
|
$stmt->bind_param("s", $datum);
|
||||||
|
$stmt->execute();
|
||||||
|
$z = $stmt->get_result()->fetch_assoc();
|
||||||
|
$stmt->close();
|
||||||
|
if ($z) {
|
||||||
|
$stand = ["feiertag" => $z["holiday"], "ferien" => $z["vacation"]];
|
||||||
|
}
|
||||||
|
return $cache[$datum] = $stand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ist die Automatik an diesem Datum dran? Gibt [ja/nein, Grund] zurück.
|
||||||
|
* Dieselbe Reihenfolge wie tag_passt(): erst öffnen, dann sperren.
|
||||||
|
*/
|
||||||
|
function zlTagPasst($a, $datum)
|
||||||
|
{
|
||||||
|
$tag = new DateTimeImmutable($datum);
|
||||||
|
if (!empty($a["next_day"])) {
|
||||||
|
$tag = $tag->modify("+1 day");
|
||||||
|
}
|
||||||
|
$kal = zlKalender($tag->format("Y-m-d"));
|
||||||
|
$bit = 1 << (intval($tag->format("N")) - 1); // Montag = Bit 0
|
||||||
|
$vorabend = !empty($a["next_day"]) ? "morgen " : "";
|
||||||
|
|
||||||
|
$passt = (bool)($a["weekdays"] & $bit);
|
||||||
|
if ($kal["feiertag"] && $a["on_holiday"] == 2) $passt = true;
|
||||||
|
if ($kal["ferien"] && $a["on_vacation"] == 2) $passt = true;
|
||||||
|
if (!$passt) {
|
||||||
|
$namen = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||||||
|
$tage = [];
|
||||||
|
for ($i = 0; $i < 7; $i++) {
|
||||||
|
if ($a["weekdays"] & (1 << $i)) $tage[] = $namen[$i];
|
||||||
|
}
|
||||||
|
$extra = [];
|
||||||
|
if ($a["on_holiday"] == 2) $extra[] = "Feiertage";
|
||||||
|
if ($a["on_vacation"] == 2) $extra[] = "Ferien";
|
||||||
|
$liste = implode(", ", array_merge($tage, $extra));
|
||||||
|
return [false, ($vorabend ? "nur am Vorabend von " : "nur ") . ($liste ?: "an keinem Tag")];
|
||||||
|
}
|
||||||
|
if ($kal["feiertag"] && $a["on_holiday"] == 0) {
|
||||||
|
return [false, $vorabend . "Feiertag (" . $kal["feiertag"] . ")"];
|
||||||
|
}
|
||||||
|
if ($kal["ferien"] && $a["on_vacation"] == 0) {
|
||||||
|
return [false, $vorabend . "Ferien (" . $kal["ferien"] . ")"];
|
||||||
|
}
|
||||||
|
return [true, ""];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sonnenauf- und -untergang eines Datums in Minuten, wie im Runner.
|
||||||
|
*
|
||||||
|
* solarLog.daylight kennt nur heute, morgen und die Vergangenheit. Für einen
|
||||||
|
* späteren Tag gilt derselbe 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. "genau"
|
||||||
|
* sagt, ob es der Eintrag des Tages selbst war.
|
||||||
|
*/
|
||||||
|
function zlSonne($datum)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$db = solarDb();
|
||||||
|
$stmt = $db->prepare("SELECT sunrise, sunset, date = ? AS genau FROM daylight
|
||||||
|
WHERE date = ? OR DATE_FORMAT(date, '%m-%d') = DATE_FORMAT(?, '%m-%d')
|
||||||
|
ORDER BY genau DESC, date DESC LIMIT 1");
|
||||||
|
$stmt->bind_param("sss", $datum, $datum, $datum);
|
||||||
|
$stmt->execute();
|
||||||
|
$z = $stmt->get_result()->fetch_assoc();
|
||||||
|
$stmt->close();
|
||||||
|
if ($z) {
|
||||||
|
return ["auf" => zlMinuten($z["sunrise"]), "unter" => zlMinuten($z["sunset"]),
|
||||||
|
"genau" => (bool)$z["genau"]];
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// ohne Sonnenzeiten eben ohne - die betroffenen stehen dann unter "jederzeit"
|
||||||
|
}
|
||||||
|
return ["auf" => null, "unter" => null, "genau" => false];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Bahn einer Automatik: welche Art Gerät sie überwiegend schaltet.
|
||||||
|
* $geraete ist command_id => Gerät aus deviceCatalog().
|
||||||
|
*/
|
||||||
|
function zlBahn($aktionen, $geraete)
|
||||||
|
{
|
||||||
|
$zaehler = [];
|
||||||
|
foreach ($aktionen as $akt) {
|
||||||
|
$d = $geraete[$akt["command_id"]] ?? null;
|
||||||
|
if (!$d) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$form = bedienform($d);
|
||||||
|
if (stripos($d["type"], "bewässer") !== false || stripos($d["type"], "bewaesser") !== false
|
||||||
|
|| stripos($d["url"], "Gartenwasser") !== false) {
|
||||||
|
$form = "bewaesserung";
|
||||||
|
}
|
||||||
|
if (!isset(zeitleisteBahnen()[$form])) {
|
||||||
|
$form = "sonstiges";
|
||||||
|
}
|
||||||
|
$zaehler[$form] = ($zaehler[$form] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
if (!$zaehler) {
|
||||||
|
return "sonstiges";
|
||||||
|
}
|
||||||
|
// Bei Gleichstand gewinnt die Bahn, die weiter oben steht.
|
||||||
|
$reihe = array_flip(array_keys(zeitleisteBahnen()));
|
||||||
|
uksort($zaehler, function ($x, $y) use ($zaehler, $reihe) {
|
||||||
|
return [$zaehler[$y], $reihe[$x]] <=> [$zaehler[$x], $reihe[$y]];
|
||||||
|
});
|
||||||
|
return array_key_first($zaehler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Was die Automatik tut, kurz: "Wozi Fensterfront, Küche Tür +5: Auf". */
|
||||||
|
function zlSatz($aktionen, $geraete, $kommandos)
|
||||||
|
{
|
||||||
|
$namen = [];
|
||||||
|
$was = [];
|
||||||
|
foreach ($aktionen as $akt) {
|
||||||
|
$d = $geraete[$akt["command_id"]] ?? null;
|
||||||
|
$namen[] = $d ? $d["label"] : "?";
|
||||||
|
$voll = $kommandos[$akt["command_id"]] ?? "";
|
||||||
|
$was[] = strpos($voll, ": ") !== false ? substr($voll, strpos($voll, ": ") + 2) : $voll;
|
||||||
|
}
|
||||||
|
$namen = array_values(array_unique($namen));
|
||||||
|
$wer = implode(", ", array_slice($namen, 0, 2)) . (count($namen) > 2 ? " +" . (count($namen) - 2) : "");
|
||||||
|
$was = array_values(array_unique($was));
|
||||||
|
return $wer . ": " . (count($was) === 1 ? $was[0] : implode(" / ", array_slice($was, 0, 2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Zeitleiste eines Datums (YYYY-MM-DD).
|
||||||
|
*/
|
||||||
|
function zeitleiste($datum)
|
||||||
|
{
|
||||||
|
$db = meshDb();
|
||||||
|
$d = DateTimeImmutable::createFromFormat("Y-m-d", $datum) ?: new DateTimeImmutable("today");
|
||||||
|
$datum = $d->format("Y-m-d");
|
||||||
|
$sonne = zlSonne($datum);
|
||||||
|
|
||||||
|
// Geräte je Kommando, einmal für alle.
|
||||||
|
$geraete = [];
|
||||||
|
foreach (deviceCatalog() as $g) {
|
||||||
|
foreach ($g["commands"] as $c) {
|
||||||
|
$geraete[$c["id"]] = $g;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$kommandos = commandNames();
|
||||||
|
$namesState = stateNames();
|
||||||
|
|
||||||
|
// Messwerte, die eine Zeit tragen: Uhrzeit/Datum (Logic), Sonnenstand,
|
||||||
|
// Automatik-Auslöser. Alles andere ist ein Messwert, auf den gewartet wird.
|
||||||
|
$zeitStates = [];
|
||||||
|
$res = $db->query("SELECT s.id, s.url, a.url AS actor_url, t.type
|
||||||
|
FROM actor_states s JOIN actors a ON a.id = s.actor_id
|
||||||
|
LEFT JOIN state_types t ON t.id = s.state_type
|
||||||
|
WHERE a.url IN ('Logic', 'Automatik')");
|
||||||
|
while ($z = $res->fetch_assoc()) {
|
||||||
|
$zeitStates[intval($z["id"])] = $z;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Läufe an diesem Tag.
|
||||||
|
$laeufe = [];
|
||||||
|
$stmt = $db->prepare("SELECT automation_id, ts, result FROM automation_log
|
||||||
|
WHERE ts >= ? AND ts < ? + INTERVAL 1 DAY AND result IN ('fired', 'forced')
|
||||||
|
ORDER BY ts");
|
||||||
|
$stmt->bind_param("ss", $datum, $datum);
|
||||||
|
$stmt->execute();
|
||||||
|
$r = $stmt->get_result();
|
||||||
|
while ($z = $r->fetch_assoc()) {
|
||||||
|
$laeufe[intval($z["automation_id"])][] = substr($z["ts"], 11, 5);
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
$ids = array_map("intval", array_column($db->query("SELECT id FROM automations")->fetch_all(MYSQLI_ASSOC), "id"));
|
||||||
|
$liste = [];
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
$a = loadAutomation($id);
|
||||||
|
if (!$a) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$von = zlMinuten($a["window_from"]) ?? 0;
|
||||||
|
$bis = zlMinuten($a["window_to"]) ?? 1439;
|
||||||
|
$ganzerTag = $von === 0 && $bis >= 1439;
|
||||||
|
|
||||||
|
// Je ODER-Gruppe: Zeitpunkt, frühester Beginn, spätestes Ende,
|
||||||
|
// Kette, wartet auf Messwert. Innerhalb der Gruppe UND.
|
||||||
|
$gruppen = [];
|
||||||
|
foreach ($a["conditions"] as $c) {
|
||||||
|
$g = &$gruppen[$c["group"]];
|
||||||
|
$g = $g ?? ["punkt" => null, "start" => null, "ende" => null, "kette" => null,
|
||||||
|
"versatz" => 0, "sensor" => false, "sonne" => false];
|
||||||
|
$st = $zeitStates[$c["state_id"]] ?? null;
|
||||||
|
$op = $c["operator"];
|
||||||
|
if ($st && $st["actor_url"] === "Logic" && $st["type"] === "time") {
|
||||||
|
$m = zlMinuten($c["value"]);
|
||||||
|
if ($op === "=") $g["punkt"] = $m;
|
||||||
|
elseif ($op === ">=") $g["start"] = max($g["start"] ?? 0, $m);
|
||||||
|
elseif ($op === "<") $g["ende"] = min($g["ende"] ?? 1440, $m);
|
||||||
|
} elseif ($st && $st["actor_url"] === "Logic" && $st["type"] === "deltatime") {
|
||||||
|
$basis = $st["url"] === "sunrise" ? $sonne["auf"] : $sonne["unter"];
|
||||||
|
$versatz = zlMinuten($c["value"]) ?? 0;
|
||||||
|
if ($basis === null) {
|
||||||
|
$g["sensor"] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$m = $basis + (substr($op, -1) === "-" ? -$versatz : $versatz);
|
||||||
|
$g["sonne"] = true;
|
||||||
|
if ($op === "+" || $op === "-") $g["punkt"] = $m;
|
||||||
|
elseif (strpos($op, ">=") === 0) $g["start"] = max($g["start"] ?? 0, $m);
|
||||||
|
else $g["ende"] = min($g["ende"] ?? 1440, $m);
|
||||||
|
} elseif ($st && $st["actor_url"] === "Automatik" && preg_match('/^auto:(\d+)$/', $st["url"], $t)) {
|
||||||
|
$g["kette"] = intval($t[1]);
|
||||||
|
$g["versatz"] = zlMinuten($c["value"]) ?? 0;
|
||||||
|
} elseif ($st && $st["actor_url"] === "Logic") {
|
||||||
|
// Datum: ein Tagesfilter, keine Lage auf dem Tag.
|
||||||
|
} else {
|
||||||
|
$g["sensor"] = true;
|
||||||
|
}
|
||||||
|
unset($g);
|
||||||
|
}
|
||||||
|
|
||||||
|
[$dran, $grund] = zlTagPasst($a, $datum);
|
||||||
|
$eintrag = [
|
||||||
|
"id" => $id,
|
||||||
|
"name" => $a["name"],
|
||||||
|
"floor" => $a["floor"],
|
||||||
|
"enabled" => $a["enabled"],
|
||||||
|
"dran" => $dran,
|
||||||
|
"grund" => $grund,
|
||||||
|
"bahn" => zlBahn($a["actions"], $geraete),
|
||||||
|
"satz" => zlSatz($a["actions"], $geraete, $kommandos),
|
||||||
|
"wenn" => describeConditions($a["conditions"], $namesState),
|
||||||
|
"laeufe" => $laeufe[$id] ?? [],
|
||||||
|
"art" => "jederzeit",
|
||||||
|
"punkt" => null,
|
||||||
|
"von" => null,
|
||||||
|
"bis" => null,
|
||||||
|
"kette" => null,
|
||||||
|
"versatz" => 0,
|
||||||
|
"sonne" => false,
|
||||||
|
"wartet" => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
$punkte = array_filter(array_column($gruppen, "punkt"), fn($m) => $m !== null);
|
||||||
|
$ketten = array_filter($gruppen, fn($g) => $g["kette"] !== null);
|
||||||
|
$eintrag["wartet"] = (bool)array_filter($gruppen, fn($g) => $g["sensor"]);
|
||||||
|
$eintrag["sonne"] = (bool)array_filter($gruppen, fn($g) => $g["sonne"]);
|
||||||
|
|
||||||
|
if ($punkte) {
|
||||||
|
$eintrag["art"] = "punkt";
|
||||||
|
$eintrag["punkt"] = min($punkte);
|
||||||
|
} elseif ($ketten) {
|
||||||
|
$k = reset($ketten);
|
||||||
|
$eintrag["art"] = "kette";
|
||||||
|
$eintrag["kette"] = $k["kette"];
|
||||||
|
$eintrag["versatz"] = $k["versatz"];
|
||||||
|
// Die eigenen Grenzen der Kette - aufgelöst wird unten, wenn der
|
||||||
|
// Auslöser bekannt ist.
|
||||||
|
$eintrag["_von"] = max($von, $k["start"] ?? $von);
|
||||||
|
$eintrag["_bis"] = min($bis, $k["ende"] ?? $bis);
|
||||||
|
} else {
|
||||||
|
$anfaenge = [];
|
||||||
|
$enden = [];
|
||||||
|
$zeitlich = false;
|
||||||
|
foreach ($gruppen as $g) {
|
||||||
|
if ($g["start"] !== null || $g["ende"] !== null) $zeitlich = true;
|
||||||
|
$anfaenge[] = max($von, $g["start"] ?? $von);
|
||||||
|
$enden[] = min($bis, $g["ende"] ?? $bis);
|
||||||
|
}
|
||||||
|
if ($zeitlich || !$ganzerTag) {
|
||||||
|
$eintrag["art"] = "fenster";
|
||||||
|
$eintrag["von"] = $anfaenge ? min($anfaenge) : $von;
|
||||||
|
$eintrag["bis"] = $enden ? max($enden) : $bis;
|
||||||
|
// Der früheste zeitlich bestimmte Beginn, falls es einen gibt -
|
||||||
|
// bei "Sonnenuntergang + 30 oder dunkel" die Sonnenmarke.
|
||||||
|
$marken = array_filter(array_column($gruppen, "start"), fn($m) => $m !== null);
|
||||||
|
if ($marken) {
|
||||||
|
$eintrag["punkt"] = min($marken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$liste[$id] = $eintrag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Ketten auflösen. Die Lage hängt am Auslöser:
|
||||||
|
*
|
||||||
|
* Auslöser lief schon Punkt bei dessen Zeit + Versatz
|
||||||
|
* Auslöser hat einen Punkt Punkt dort + Versatz
|
||||||
|
* Auslöser ist ein Fenster Fenster ab dessen Beginn + Versatz
|
||||||
|
*
|
||||||
|
* Jeweils nicht vor dem eigenen Fensterbeginn und nicht nach dem eigenen
|
||||||
|
* Ende. Mehrstufig (Wecker -> Rollos -> Schlafzimmer), deshalb reihum.
|
||||||
|
*/
|
||||||
|
$aufgeloest = [];
|
||||||
|
for ($runde = 0; $runde < 6; $runde++) {
|
||||||
|
foreach ($liste as $id => $e) {
|
||||||
|
if ($e["art"] !== "kette" || isset($aufgeloest[$id])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$vater = $liste[$e["kette"]] ?? null;
|
||||||
|
if (!$vater || ($vater["art"] === "kette" && !isset($aufgeloest[$e["kette"]]))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$aufgeloest[$id] = true;
|
||||||
|
$eigenVon = $e["_von"];
|
||||||
|
$eigenBis = $e["_bis"];
|
||||||
|
if ($vater["laeufe"]) {
|
||||||
|
$liste[$id]["punkt"] = max($eigenVon, zlMinuten(end($vater["laeufe"])) + $e["versatz"]);
|
||||||
|
} elseif ($vater["punkt"] !== null && $vater["art"] !== "fenster") {
|
||||||
|
$liste[$id]["punkt"] = max($eigenVon, $vater["punkt"] + $e["versatz"]);
|
||||||
|
} elseif ($vater["art"] === "fenster") {
|
||||||
|
$liste[$id]["art"] = "fenster";
|
||||||
|
$liste[$id]["von"] = max($eigenVon, $vater["von"] + $e["versatz"]);
|
||||||
|
$liste[$id]["bis"] = max($liste[$id]["von"], $eigenBis);
|
||||||
|
} else {
|
||||||
|
$liste[$id]["art"] = "jederzeit"; // Auslöser ohne Lage: dann eben ohne
|
||||||
|
}
|
||||||
|
// Eine Kette erbt, ob der Tag passt: läuft der Wecker nicht, kommt auch der Nachfolger nicht.
|
||||||
|
if (!$vater["dran"] && $liste[$id]["dran"]) {
|
||||||
|
$liste[$id]["dran"] = false;
|
||||||
|
$liste[$id]["grund"] = "Auslöser „" . $vater["name"] . "“ ist nicht dran";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($liste as $id => $e) {
|
||||||
|
if ($e["art"] === "kette" && $e["punkt"] === null) {
|
||||||
|
$liste[$id]["art"] = "jederzeit"; // Kreis oder fehlender Auslöser
|
||||||
|
}
|
||||||
|
unset($liste[$id]["_von"], $liste[$id]["_bis"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$etagen = [];
|
||||||
|
foreach (grundriss()[0] as $code => $e) {
|
||||||
|
$etagen[] = ["code" => $code, "label" => $e["label"]];
|
||||||
|
}
|
||||||
|
$heute = (new DateTimeImmutable("today"))->format("Y-m-d");
|
||||||
|
return [
|
||||||
|
"datum" => $datum,
|
||||||
|
"heute" => $datum === $heute,
|
||||||
|
"jetzt" => $datum === $heute ? intval(date("G")) * 60 + intval(date("i")) : null,
|
||||||
|
"sonne" => $sonne,
|
||||||
|
"bahnen" => zeitleisteBahnen(),
|
||||||
|
"etagen" => $etagen,
|
||||||
|
"automatiken" => array_values($liste),
|
||||||
|
];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user