Compare commits
6
Commits
8f040dec19
...
b01e938905
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b01e938905 | ||
|
|
38e6c3f96b | ||
|
|
b607d4a064 | ||
|
|
c6a879b424 | ||
|
|
01bcbfffc1 | ||
|
|
551b727256 |
@@ -314,7 +314,9 @@ Antwort selbst.
|
|||||||
Bedingungen in UND-/ODER-Gruppen, Aktionen mit Parametern, Klartext-Vorschau.
|
Bedingungen in UND-/ODER-Gruppen, Aktionen mit Parametern, Klartext-Vorschau.
|
||||||
Er bekommt Gerätekatalog und Datensatz in einem Dokument von
|
Er bekommt Gerätekatalog und Datensatz in einem Dokument von
|
||||||
`AutoAction.php?action=editor` — früher kamen je Bedingung zwei weitere
|
`AutoAction.php?action=editor` — früher kamen je Bedingung zwei weitere
|
||||||
Anfragen dazu, und die Auswahlfelder füllten sich asynchron.
|
Anfragen dazu, und die Auswahlfelder füllten sich asynchron. Dazu die
|
||||||
|
Übersicht: Nachfolger einklappen (`toggleAutomationGroup`) und die Rückfrage
|
||||||
|
vor dem Löschen (`frageNachfolger`).
|
||||||
|
|
||||||
**`settings.js`** — drei Reiter in einer Datei, deutlich getrennt: Preistabellen,
|
**`settings.js`** — drei Reiter in einer Datei, deutlich getrennt: Preistabellen,
|
||||||
Home-Kacheln, Geräte. Jeder Abschnitt hat sein Modell (`kachelModell`,
|
Home-Kacheln, Geräte. Jeder Abschnitt hat sein Modell (`kachelModell`,
|
||||||
@@ -334,7 +336,7 @@ klein: Diagramme füllen bzw. Logzeilen nachladen und einfärben.
|
|||||||
| Endpunkt | Aufruf | liefert / tut |
|
| Endpunkt | Aufruf | liefert / tut |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `room.php` | `?room=EG_Bad`, `POST ?action=command\|temp` | Raum-Modal bauen; ein Kommando oder eine Solltemperatur senden |
|
| `room.php` | `?room=EG_Bad`, `POST ?action=command\|temp` | Raum-Modal bauen; ein Kommando oder eine Solltemperatur senden |
|
||||||
| `AutoAction.php` | `?action=editor\|list`, `POST save\|delete\|toggle` | Automatik-Editor und -Übersicht |
|
| `AutoAction.php` | `?action=editor\|list\|followers\|werte`, `POST save\|delete\|toggle` | Automatik-Editor und -Übersicht |
|
||||||
| `settings.php` | `?action=list\|kacheln\|geraete`, `POST save\|kacheln-save\|geraete-raeume\|geraet-loeschen` | Einstellungsseite |
|
| `settings.php` | `?action=list\|kacheln\|geraete`, `POST save\|kacheln-save\|geraete-raeume\|geraet-loeschen` | Einstellungsseite |
|
||||||
| `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) |
|
||||||
@@ -360,7 +362,7 @@ Kennzahlen, `energyHistory.php` wählt die Quelle nach Zeitraum,
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `rooms.php` | Räume, Etagen, Kachelwerte, Symbole | `allRooms()`, `roomsWithTile()`, `tileTopics()`, `kachelVorgabe()`, `kachelUeberschreibungen()`, `bootstrapIcons()`, `kachelIcon()` |
|
| `rooms.php` | Räume, Etagen, Kachelwerte, Symbole | `allRooms()`, `roomsWithTile()`, `tileTopics()`, `kachelVorgabe()`, `kachelUeberschreibungen()`, `bootstrapIcons()`, `kachelIcon()` |
|
||||||
| `costs.php` | Preistabellen, Verbindung zu `solarLog` | `solarDb()`, `preiseLaden()`, `preiseSpeichern()`, `grundpreisImZeitraum()` |
|
| `costs.php` | Preistabellen, Verbindung zu `solarLog` | `solarDb()`, `preiseLaden()`, `preiseSpeichern()`, `grundpreisImZeitraum()` |
|
||||||
| `automations.php` | Automatiken, Gerätekatalog, Verbindung zu `homeMesh` | `meshDb()`, `deviceCatalog()`, `loadAutomation()`, `saveAutomation()`, `geraeteListe()`, `geraetLoeschen()`, `saveRooms()` |
|
| `automations.php` | Automatiken, Gerätekatalog, Verbindung zu `homeMesh` | `meshDb()`, `deviceCatalog()`, `loadAutomation()`, `saveAutomation()`, `deleteAutomation()`, `automationFollowers()`, `pruefeKreis()`, `kalendertagChoices()`, `tagVorlagen()`, `stateValues()`, `geraeteListe()`, `geraetLoeschen()`, `saveRooms()` |
|
||||||
| `kacheln.php` | Anzeigewerte der Home-Kacheln (Maskenseite) | `kachelListe()`, `kachelKatalog()`, `kachelWertPruefen()`, `kachelWerteSpeichern()`, `kachelSymbole()` |
|
| `kacheln.php` | Anzeigewerte der Home-Kacheln (Maskenseite) | `kachelListe()`, `kachelKatalog()`, `kachelWertPruefen()`, `kachelWerteSpeichern()`, `kachelSymbole()` |
|
||||||
| `roomControls.php` | Welches Gerät welches Bedienelement bekommt | `bedienform()`, `zeichneBeschattung()`, `zeichneLicht()`, `zeichneMesswerte()` |
|
| `roomControls.php` | Welches Gerät welches Bedienelement bekommt | `bedienform()`, `zeichneBeschattung()`, `zeichneLicht()`, `zeichneMesswerte()` |
|
||||||
| `commands.php` | Ein Kommando abschicken (MQTT/WLED/HTTP/Tahoma) | `executeCommand()`, `sendeMqtt()`, `sendeWled()`, `sendeHttp()`, `sendeTahoma()` |
|
| `commands.php` | Ein Kommando abschicken (MQTT/WLED/HTTP/Tahoma) | `executeCommand()`, `sendeMqtt()`, `sendeWled()`, `sendeHttp()`, `sendeTahoma()` |
|
||||||
@@ -524,20 +526,115 @@ das Regelwerk nach `homeMesh`; ausgeführt wird es von
|
|||||||
`autoActions/autoaction_runner.py` im anderen Repo. **Die Weboberfläche
|
`autoActions/autoaction_runner.py` im anderen Repo. **Die Weboberfläche
|
||||||
schaltet nichts von sich aus** — sie beschreibt nur, was gelten soll.
|
schaltet nichts von sich aus** — sie beschreibt nur, was gelten soll.
|
||||||
|
|
||||||
|
**Drei Bänder statt eines Akkordeons: Wenn · Wann · Dann.** Das Akkordeon
|
||||||
|
schloss beim Öffnen die anderen Fächer — Auslöser und Aktion waren also nie
|
||||||
|
gleichzeitig zu sehen, ausgerechnet bei einer Regel, die genau daraus
|
||||||
|
besteht. Und das mittlere Fach hieß „Bedingungen", enthielt aber die
|
||||||
|
*Rahmen*bedingungen. Gebaut aus `.card` mit farbigem Rand und `.card-header`,
|
||||||
|
kein eigenes Bauteil.
|
||||||
|
|
||||||
|
**Der Rahmen steht zusammengefaltet da**, solange nichts von der Vorgabe
|
||||||
|
abweicht — und das ist der Normalfall. `rahmenKurzText()` schreibt ihn in
|
||||||
|
eine Zeile („täglich · rund um die Uhr"), `rahmenIstVorgabe()` entscheidet,
|
||||||
|
ob sie matt oder hell ist. Vorher nahm er die meiste Fläche für den
|
||||||
|
seltensten Inhalt.
|
||||||
|
|
||||||
|
**Neben jeder Bedingung läuft ihr aktueller Wert mit.** `?action=werte`
|
||||||
|
liefert alle acht Sekunden `{state_id: {value, unit}}` aus `actor_states` —
|
||||||
|
bewusst aus der Tabelle und nicht über MQTT, weil dort nur ein Teil der
|
||||||
|
Geräte auftaucht (die Jalousien hängen an der Tahoma-Box, die gerechneten
|
||||||
|
Werte an gar nichts); der Runner schreibt dagegen jeden Messwert zurück.
|
||||||
|
Das ersetzt den Satz, der früher am Ende stand und die Regel Wort für Wort
|
||||||
|
nacherzählte: statt zu wiederholen, was darüber steht, beantwortet die Zeile
|
||||||
|
die Frage, die man wirklich hat — warum läuft die Automatik gerade nicht?
|
||||||
|
|
||||||
|
Ein voller Punkt heißt erfüllt, ein leerer noch nicht, ein **gestrichelter
|
||||||
|
heißt „entscheidet der Runner"**. Bei `time`, `date`, `datetime`,
|
||||||
|
`deltatime` und `elapsed` fällt der Browser bewusst kein Urteil: ob
|
||||||
|
„Uhrzeit um 07:30" gerade zutrifft, hängt am Nachholfenster, am Tagesrand
|
||||||
|
und beim Datentyp `elapsed` am echten Abstand seit der letzten Auslösung —
|
||||||
|
das alles steht in `bedingung_erfuellt()` im Runner. Es hier nachzubauen
|
||||||
|
hieße, eine zweite Wahrheit zu pflegen, die irgendwann auseinanderläuft.
|
||||||
|
Lieber keine Aussage als eine, die manchmal falsch ist (`ZEITARTEN`).
|
||||||
|
|
||||||
Bedingungen mit derselben `group_no` sind mit UND verknüpft, verschiedene
|
Bedingungen mit derselben `group_no` sind mit UND verknüpft, verschiedene
|
||||||
Gruppen mit ODER: ausgewertet wird `any(all(gruppe))`. Eine Bedingung zeigt
|
Gruppen mit ODER: ausgewertet wird `any(all(gruppe))`. Eine Bedingung zeigt
|
||||||
auf einen `actor_states`-Eintrag, eine Aktion auf ein `actor_commands` samt
|
auf einen `actor_states`-Eintrag, eine Aktion auf ein `actor_commands` samt
|
||||||
Parametern. Deshalb dürfen Zustands-Ids nicht wandern — siehe die
|
Parametern. Deshalb dürfen Zustands-Ids nicht wandern — siehe die
|
||||||
Geräte-Erkennung im nächsten Abschnitt.
|
Geräte-Erkennung im nächsten Abschnitt.
|
||||||
|
|
||||||
|
**Eine Automatik kann eine andere auslösen.** Dafür steht im Editor auffallend
|
||||||
|
wenig Code: das gerechnete Gerät „Automatiken" führt jede Automatik als
|
||||||
|
Messwert, ihr Wert ist der Zeitpunkt der letzten Auslösung — für den
|
||||||
|
Gerätewähler ist es damit ein Gerät wie jedes andere. Die Messwerte legt der
|
||||||
|
Runner an und pflegt sie, `restricted/automations.php` liest sie nur
|
||||||
|
(`ausloeserStates()`, `automationTriggers()`).
|
||||||
|
|
||||||
|
Drei Stellen wissen trotzdem davon:
|
||||||
|
|
||||||
|
* `pruefeKreis()` lehnt beim Speichern ab, was sich mittelbar selbst auslösen
|
||||||
|
würde. Im Betrieb wäre ein Kreis kaum zu bemerken — die Sperrzeit begrenzt
|
||||||
|
ihn auf eine Auslösung je `lockout_secs`, und im Protokoll sieht das aus wie
|
||||||
|
eine Automatik, die halt oft läuft.
|
||||||
|
* `deleteAutomation()` fragt vorher. `fk_cond_state` steht auf
|
||||||
|
`ON DELETE CASCADE`, mit dem Messwert verschwände also still die Bedingung
|
||||||
|
des Nachfolgers: aus *zehn Minuten nach dem Wecker UND es ist hell* würde
|
||||||
|
ein bloßes *es ist hell*. Zur Wahl stehen Abhängen (Bedingung raus,
|
||||||
|
Nachfolger pausiert) und Mitlöschen.
|
||||||
|
* Die Übersicht rückt Nachfolger unter ihren Auslöser ein und zeigt sie
|
||||||
|
eingeklappt (`toggleAutomationGroup()`). Das ist **nur die Darstellung** —
|
||||||
|
jeder Nachfolger bleibt eine vollwertige Automatik mit eigener Pause,
|
||||||
|
eigenen Rahmenbedingungen und eigener Zeile im Protokoll. Sobald
|
||||||
|
„gruppiert" auch „gehört dazu" hieße, wäre zu klären, wem die Pause gehört.
|
||||||
|
|
||||||
|
**Die Tagesauswahl in vier Stufen, von grob nach fein.** Ganz oben drei
|
||||||
|
Vorlagen (`tagVorlagen()`): *täglich*, *werktags*, *Wochenende*. Sie setzen den
|
||||||
|
**ganzen** Rahmen, nicht nur die Wochentagsmaske — „werktags" heißt auch
|
||||||
|
„nicht an Feiertagen", „Wochenende" auch „dazu alle Feiertage". Wer das aus
|
||||||
|
Maske und zwei Schaltern von Hand zusammensetzt, vergisst den zweiten Teil.
|
||||||
|
Darunter sieben Wochentagsschalter, darunter Ferien und Feiertage mit je drei
|
||||||
|
Stufen — **nie · egal · immer** (`kalendertagChoices()`) —, und ganz unten ein
|
||||||
|
Satz, der zurückliest, was dabei herausgekommen ist: *„Läuft samstags und
|
||||||
|
sonntags, dazu an allen Feiertagen."*
|
||||||
|
|
||||||
|
Der Satz ist der eigentliche Gewinn: man muss das Bedienelement nicht
|
||||||
|
entziffern. `rahmenText()` in `autoActionFuncs.js` baut ihn, zieht drei und
|
||||||
|
mehr aufeinanderfolgende Tage zu „montags bis freitags" zusammen und gibt bei
|
||||||
|
gar keinem wählbaren Tag `""` zurück — dann steht dort die Warnung *„Kein Tag
|
||||||
|
ausgewählt"*, genau wie beim fehlenden Auslöser.
|
||||||
|
|
||||||
|
„Immer" zählt wie ein angehakter Wochentag. Das ist die einzige Art, *an
|
||||||
|
Wochenenden und Feiertagen* zu schreiben — für die Maske ist ein Feiertag am
|
||||||
|
Dienstag eben ein Dienstag —, und mit **gar keinem** Wochentag angehakt ergibt
|
||||||
|
es *nur an Feiertagen*.
|
||||||
|
|
||||||
|
Vorher standen dort zwei Auswahlfelder, deren Einträge ganze Sätze waren
|
||||||
|
(„zusätzlich, auch am falschen Wochentag"). Ein Auswahlfeld zeigt immer nur
|
||||||
|
den gewählten Eintrag — man sieht nie, was es sonst noch gibt, und die
|
||||||
|
Erklärung steht an der Stelle, wo ein Etikett hingehört.
|
||||||
|
|
||||||
|
`kalendertagWert()` prüft beim Speichern mit `is_numeric`, weil `intval("")`
|
||||||
|
sonst 0 wäre — und 0 heißt „nie": eine leere Angabe hätte eine Automatik
|
||||||
|
stillschweigend an Feiertagen abgeschaltet.
|
||||||
|
|
||||||
|
**„Nur einmal am Tag"** (`once_per_day`) sperrt eine Automatik nach dem
|
||||||
|
Auslösen bis Mitternacht. Für alles, was man hinterher von Hand wieder anders
|
||||||
|
stellt: ein Rollladen, den man um acht zugezogen hat, soll nicht um neun von
|
||||||
|
selbst wieder auffahren, nur weil eine Wolke weiterzieht.
|
||||||
|
|
||||||
|
Wie der Runner das auswertet — Datentyp `elapsed`, topologische Reihenfolge,
|
||||||
|
der Rahmen, was beim Pausieren geschieht — steht in
|
||||||
|
[`autoActions/README.md`](../../homes/wagner/SolarManager/autoActions/README.md)
|
||||||
|
im SolarManager-Repo.
|
||||||
|
|
||||||
### 4. Die Geräte-Erkennung
|
### 4. Die Geräte-Erkennung
|
||||||
|
|
||||||
`restricted/deviceDiscovery/device_discovery.py` wird **von Hand** gestartet.
|
`restricted/deviceDiscovery/device_discovery.py` wird **von Hand** gestartet.
|
||||||
Die Module suchen je eine Geräteart (Tahoma, WLED, Shelly, MQTT/Home-Assistant-
|
Die Module suchen je eine Geräteart (Tahoma, WLED, Shelly, MQTT/Home-Assistant-
|
||||||
Discovery, Gartenwasser, SolarManager-Werte, Logic); die Datenbanklogik liegt
|
Discovery, Gartenwasser, SolarManager-Werte, Wetterstation, Logic); die
|
||||||
allein im Hauptskript.
|
Datenbanklogik liegt allein im Hauptskript.
|
||||||
|
|
||||||
Zwei Eigenschaften, die man kennen muss:
|
Drei Eigenschaften, die man kennen muss:
|
||||||
|
|
||||||
* **Ein Suchlauf löscht nichts.** `clear_tables` steht in `config.ini` auf
|
* **Ein Suchlauf löscht nichts.** `clear_tables` steht in `config.ini` auf
|
||||||
`false`, weil die Automatiken auf die Ids von `actor_states` zeigen.
|
`false`, weil die Automatiken auf die Ids von `actor_states` zeigen.
|
||||||
@@ -561,11 +658,49 @@ Zwei Eigenschaften, die man kennen muss:
|
|||||||
|
|
||||||
Geschaltet werden auch die neuen Geräte über HTTP. Der Runner teilt seine
|
Geschaltet werden auch die neuen Geräte über HTTP. Der Runner teilt seine
|
||||||
Messwerte deshalb je Wert zu, nicht je Gerät.
|
Messwerte deshalb je Wert zu, nicht je Gerät.
|
||||||
|
* **Vier Module suchen gar nichts.** Gartenwasser, SolarManager, Wetterstation
|
||||||
|
und Logic schreiben feste Geräte hin, weil sich diese Quellen nicht von
|
||||||
|
selbst melden. Wer einen Messwert vermisst, trägt im jeweiligen Modul eine
|
||||||
|
Zeile ein — bei der Wetterstation in `MESSWERTE`, beim SolarManager in
|
||||||
|
`GERAETE`. Bewusst knapp gehalten: `solarManager/#` allein trägt gut 130
|
||||||
|
Topics, und eine vollständige Liste machte die Auswahl im Automatik-Editor
|
||||||
|
unbrauchbar.
|
||||||
|
|
||||||
|
Die **Wetterstation** hängt am Websocket von `192.168.179.42`;
|
||||||
|
`wsMQTTbridge.py` im SolarManager legt ihre Felder unter `weatherStation/#`
|
||||||
|
ab. Von dort kommen Außentemperatur, Wind und Böe — die Außentemperatur ist
|
||||||
|
für den Hitzeschutz die bessere Bedingung als die Raumtemperatur (ist es
|
||||||
|
drinnen schon warm, ist es zum Verschatten zu spät), und ohne den Wind gibt
|
||||||
|
es für Markise und Sonnensegel keinen Sturmschutz. Die trägen Werte
|
||||||
|
(Temperatur, Feuchte, Druck, Taupunkt) gehen nur bei jeder einundzwanzigsten
|
||||||
|
Nachricht raus; sie sind retained, für eine Bedingung macht das keinen
|
||||||
|
Unterschied.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Wiederkehrende Bauteile und Konventionen
|
## Wiederkehrende Bauteile und Konventionen
|
||||||
|
|
||||||
|
**Zwei Radien, eine Anzeigeschrift.** `solar.css` tauscht die Palette über
|
||||||
|
Variablen aus, statt Regel für Regel zu überschreiben — dasselbe gilt für
|
||||||
|
Form und Schrift. `--ton-radius` (`.875rem`) gilt für Flächen, `--ton-radius-klein`
|
||||||
|
(`.5rem`) für alles, was in einer Reihe steht; beide hängen an
|
||||||
|
`--bs-border-radius` und `--bs-border-radius-sm`, also zieht jede Karte,
|
||||||
|
jedes Modal und jeder kleine Knopf von allein mit. `--font-anzeige` ist
|
||||||
|
Poppins — die Schrift lag seit jeher unter `assets/fonts` und trug nur die
|
||||||
|
vier Etagenknöpfe im Außenplan; jetzt trägt sie alle Überschriften,
|
||||||
|
`.card-title` und `.modal-title`. Fließtext und Zahlenkolonnen bleiben
|
||||||
|
bewusst bei der Schrift des Themes: Poppins bringt keine Tabellenziffern
|
||||||
|
mit, Messwerte würden sonst springen.
|
||||||
|
|
||||||
|
**Drei Klassen aus dem Automatik-Editor stehen absichtlich in `solar.css`
|
||||||
|
und nicht dort.** `.feld-als-text` ist ein Auswahlfeld, das wie Text
|
||||||
|
aussieht, bis man es anfährt — dieselbe Not haben das Raum-Modal und die
|
||||||
|
Kachel-Einstellungen, wo ebenfalls jede Kleinigkeit in einem Kasten steht.
|
||||||
|
`.punkt` ist an oder aus. `.wert-jetzt` ist die eine echte Ausnahme —
|
||||||
|
Bedingungen gibt es sonst nirgends — und als einzelne Klasse auch als solche
|
||||||
|
erkennbar.
|
||||||
|
|
||||||
|
|
||||||
**Reiter.** Es gibt genau eine Bauart, `restricted/reiter.php`. Umgeschaltet
|
**Reiter.** Es gibt genau eine Bauart, `restricted/reiter.php`. Umgeschaltet
|
||||||
wird von Bootstrap (`data-bs-toggle="tab"`), gemerkt wird der offene Reiter von
|
wird von Bootstrap (`data-bs-toggle="tab"`), gemerkt wird der offene Reiter von
|
||||||
`reiterMerken()` in `common.js`. Weniger als zwei Reiter ergeben keine Leiste.
|
`reiterMerken()` in `common.js`. Weniger als zwei Reiter ergeben keine Leiste.
|
||||||
|
|||||||
+276
-71
@@ -10,7 +10,9 @@
|
|||||||
* GET ?action=editor&floor=OG Editor fuer eine neue Automatik
|
* GET ?action=editor&floor=OG Editor fuer eine neue Automatik
|
||||||
* GET ?action=list&floor=OG Tabelle fuer die Karte "Automatismen"
|
* GET ?action=list&floor=OG Tabelle fuer die Karte "Automatismen"
|
||||||
* POST ?action=save JSON-Rumpf, legt an oder ueberschreibt
|
* POST ?action=save JSON-Rumpf, legt an oder ueberschreibt
|
||||||
* POST ?action=delete {"id": N}
|
* GET ?action=followers&id=N JSON: wer haengt an dieser Automatik?
|
||||||
|
* GET ?action=werte&ids=1,2,3 JSON: was diese Messwerte gerade sagen
|
||||||
|
* POST ?action=delete {"id": N, "mode": "abhaengen"|"mitloeschen"}
|
||||||
* POST ?action=toggle {"id": N} - pausieren / fortsetzen
|
* POST ?action=toggle {"id": N} - pausieren / fortsetzen
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -45,7 +47,12 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
$id = saveAutomation($body);
|
$id = saveAutomation($body);
|
||||||
jsonAntwort(["id" => $id]);
|
jsonAntwort(["id" => $id]);
|
||||||
case "delete":
|
case "delete":
|
||||||
deleteAutomation(intval($body["id"] ?? 0));
|
// Der Modus kommt aus der Rueckfrage im Browser. Unbekanntes
|
||||||
|
// faellt auf das schonendere "abhaengen" zurueck - lieber eine
|
||||||
|
// pausierte Automatik zu viel als eine geloeschte zu viel.
|
||||||
|
$modus = ($body["mode"] ?? "") === "mitloeschen"
|
||||||
|
? "mitloeschen" : "abhaengen";
|
||||||
|
deleteAutomation(intval($body["id"] ?? 0), $modus);
|
||||||
jsonAntwort(["ok" => true]);
|
jsonAntwort(["ok" => true]);
|
||||||
case "toggle":
|
case "toggle":
|
||||||
jsonAntwort(["enabled" => toggleAutomation(intval($body["id"] ?? 0))]);
|
jsonAntwort(["enabled" => toggleAutomation(intval($body["id"] ?? 0))]);
|
||||||
@@ -59,6 +66,23 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Wer haengt an dieser Automatik? -------------------------------------
|
||||||
|
// Wird vor dem Loeschen gefragt, damit die Rueckfrage im Browser die Namen
|
||||||
|
// nennen kann und nicht nur eine Zahl.
|
||||||
|
if ($action === "followers") {
|
||||||
|
jsonAntwort(["followers" => automationFollowers(intval($_GET["id"] ?? 0))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Was sagen diese Messwerte gerade? -----------------------------------
|
||||||
|
// Der offene Editor fragt das im Takt nach, damit neben jeder Bedingung der
|
||||||
|
// laufende Wert steht. Absichtlich ein eigener, winziger Endpunkt: den
|
||||||
|
// ganzen Geraetekatalog neu zu holen waeren je Runde ueber hundert
|
||||||
|
// Kilobyte fuer ein paar Zahlen.
|
||||||
|
if ($action === "werte") {
|
||||||
|
$ids = array_slice(explode(",", $_GET["ids"] ?? ""), 0, 200);
|
||||||
|
jsonAntwort(["werte" => stateValues($ids)]);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Uebersichtstabelle --------------------------------------------------
|
// --- Uebersichtstabelle --------------------------------------------------
|
||||||
if ($action === "list") {
|
if ($action === "list") {
|
||||||
$tage = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
$tage = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||||||
@@ -74,7 +98,77 @@ if ($action === "list") {
|
|||||||
. "<th class='d-none d-sm-table-cell'>Aktion</th>"
|
. "<th class='d-none d-sm-table-cell'>Aktion</th>"
|
||||||
. "<th class='d-none d-md-table-cell'>Wann</th>"
|
. "<th class='d-none d-md-table-cell'>Wann</th>"
|
||||||
. "<th class='d-none d-md-table-cell'>Zuletzt</th><th></th></tr></thead><tbody>";
|
. "<th class='d-none d-md-table-cell'>Zuletzt</th><th></th></tr></thead><tbody>";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Eine Automatik, die von einer anderen ausgeloest wird, steht unter
|
||||||
|
* ihr statt alphabetisch irgendwo dazwischen - sonst liest man
|
||||||
|
* "Rollladen Magdalena morgens" und sieht nicht, woran es haengt.
|
||||||
|
*
|
||||||
|
* Eingeklappt, weil die Uebersicht sonst mit jeder Verkettung laenger
|
||||||
|
* wird, ohne mehr zu sagen: der Ausloeser ist das, was man sucht. Der
|
||||||
|
* Zaehler am Pfeil verraet, dass da noch etwas ist.
|
||||||
|
*
|
||||||
|
* Gruppiert wird nur die Darstellung. Jeder Nachfolger bleibt eine
|
||||||
|
* vollwertige Automatik mit eigener Pause, eigenen Rahmenbedingungen
|
||||||
|
* und eigener Zeile im Protokoll - sonst waere zu klaeren, wem die
|
||||||
|
* Pause gehoert und was das Protokoll zeigt.
|
||||||
|
*/
|
||||||
|
$nachFolgern = [];
|
||||||
foreach ($list as $a) {
|
foreach ($list as $a) {
|
||||||
|
foreach ($a["triggers"] as $davor) {
|
||||||
|
$nachFolgern[intval($davor)][] = intval($a["id"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$vorhanden = [];
|
||||||
|
foreach ($list as $a) {
|
||||||
|
$vorhanden[intval($a["id"])] = $a;
|
||||||
|
}
|
||||||
|
// Wessen Ausloeser nicht in dieser Etage steht, faengt selbst oben an.
|
||||||
|
$wurzeln = [];
|
||||||
|
foreach ($list as $a) {
|
||||||
|
$hatVater = false;
|
||||||
|
foreach ($a["triggers"] as $davor) {
|
||||||
|
if (isset($vorhanden[intval($davor)])) {
|
||||||
|
$hatVater = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$hatVater) {
|
||||||
|
$wurzeln[] = intval($a["id"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$reihe = [];
|
||||||
|
$einordnen = function ($id, $tiefe, $vater) use (&$einordnen, &$reihe, $nachFolgern, $vorhanden) {
|
||||||
|
if (!isset($vorhanden[$id]) || isset($reihe[$id])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$reihe[$id] = ["auto" => $vorhanden[$id], "tiefe" => $tiefe,
|
||||||
|
"vater" => $vater, "kinder" => 0];
|
||||||
|
foreach ($nachFolgern[$id] ?? [] as $kind) {
|
||||||
|
$einordnen($kind, $tiefe + 1, $id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
foreach ($wurzeln as $id) {
|
||||||
|
$einordnen($id, 0, 0);
|
||||||
|
}
|
||||||
|
// Was durch einen Kreis nicht erreicht wurde, kommt trotzdem mit.
|
||||||
|
foreach ($list as $a) {
|
||||||
|
$einordnen(intval($a["id"]), 0, 0);
|
||||||
|
}
|
||||||
|
// Gezaehlt wird erst jetzt, und nur was wirklich eingeordnet wurde:
|
||||||
|
// wer zwei Ausloeser hat, steht nur unter dem ersten. Aus der rohen
|
||||||
|
// Nachfolgerliste gezaehlt verspraeche der zweite Pfeil eine Zeile,
|
||||||
|
// die er nicht aufklappen kann.
|
||||||
|
foreach ($reihe as $eintrag) {
|
||||||
|
if ($eintrag["vater"]) {
|
||||||
|
$reihe[$eintrag["vater"]]["kinder"]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($reihe as $eintrag) {
|
||||||
|
$a = $eintrag["auto"];
|
||||||
|
$tiefe = $eintrag["tiefe"];
|
||||||
|
$kinder = $eintrag["kinder"];
|
||||||
|
$vater = $eintrag["vater"];
|
||||||
// Nur die aktiven Tage nennen. Sieben Haekchen untereinander
|
// Nur die aktiven Tage nennen. Sieben Haekchen untereinander
|
||||||
// haben die Zeile frueher unnoetig hoch gemacht.
|
// haben die Zeile frueher unnoetig hoch gemacht.
|
||||||
$aktiveTage = [];
|
$aktiveTage = [];
|
||||||
@@ -83,16 +177,41 @@ if ($action === "list") {
|
|||||||
$aktiveTage[] = $tage[$i];
|
$aktiveTage[] = $tage[$i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$wann = count($aktiveTage) === 7 ? "täglich" : implode(" ", $aktiveTage);
|
// Kalendertage auf "immer" gehoeren zur Tagesangabe, nicht in
|
||||||
|
// die Nachbemerkung: sie fuegen Tage hinzu. Ohne angehakten
|
||||||
|
// Wochentag tragen sie die Automatik sogar allein - "nur
|
||||||
|
// Feiertage" ist damit eine gueltige Einstellung.
|
||||||
|
$dazu = [];
|
||||||
|
if ($a["on_vacation"] == 2) {
|
||||||
|
$dazu[] = "Ferientage";
|
||||||
|
}
|
||||||
|
if ($a["on_holiday"] == 2) {
|
||||||
|
$dazu[] = "Feiertage";
|
||||||
|
}
|
||||||
|
if (count($aktiveTage) === 7) {
|
||||||
|
$wann = "täglich";
|
||||||
|
} elseif ($aktiveTage) {
|
||||||
|
$wann = implode(" ", $aktiveTage);
|
||||||
|
if ($dazu) {
|
||||||
|
$wann .= " + " . implode(" + ", $dazu);
|
||||||
|
}
|
||||||
|
} elseif ($dazu) {
|
||||||
|
$wann = "nur " . implode(" + ", $dazu);
|
||||||
|
} else {
|
||||||
|
$wann = "nie – kein Tag ausgewählt";
|
||||||
|
}
|
||||||
if ($a["window_from"] !== "00:00" || $a["window_to"] !== "23:59") {
|
if ($a["window_from"] !== "00:00" || $a["window_to"] !== "23:59") {
|
||||||
$wann .= ", " . $a["window_from"] . "–" . $a["window_to"];
|
$wann .= ", " . $a["window_from"] . "–" . $a["window_to"];
|
||||||
}
|
}
|
||||||
if (!$a["on_holiday"]) {
|
if ($a["on_holiday"] == 0) {
|
||||||
$wann .= ", nicht an Feiertagen";
|
$wann .= ", nicht an Feiertagen";
|
||||||
}
|
}
|
||||||
if (!$a["on_vacation"]) {
|
if ($a["on_vacation"] == 0) {
|
||||||
$wann .= ", nicht in den Ferien";
|
$wann .= ", nicht in den Ferien";
|
||||||
}
|
}
|
||||||
|
if ($a["once_per_day"]) {
|
||||||
|
$wann .= ", nur einmal am Tag";
|
||||||
|
}
|
||||||
if ($a["lockout_secs"] > 0) {
|
if ($a["lockout_secs"] > 0) {
|
||||||
$wann .= ", frühestens alle "
|
$wann .= ", frühestens alle "
|
||||||
. ($a["lockout_secs"] >= 60
|
. ($a["lockout_secs"] >= 60
|
||||||
@@ -101,13 +220,43 @@ if ($action === "list") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$zuletzt = $a["last_run"] ? date("d.m. H:i", strtotime($a["last_run"])) : "–";
|
$zuletzt = $a["last_run"] ? date("d.m. H:i", strtotime($a["last_run"])) : "–";
|
||||||
$klasse = $a["enabled"] ? "" : " class='opacity-50'";
|
|
||||||
$pause = $a["enabled"] ? "bi-pause-fill" : "bi-play-fill";
|
$pause = $a["enabled"] ? "bi-pause-fill" : "bi-play-fill";
|
||||||
$titel = $a["enabled"] ? "Pausieren" : "Fortsetzen";
|
$titel = $a["enabled"] ? "Pausieren" : "Fortsetzen";
|
||||||
$id = intval($a["id"]);
|
$id = intval($a["id"]);
|
||||||
|
|
||||||
echo "<tr" . $klasse . ">";
|
$klassen = [];
|
||||||
echo "<td>" . htmlspecialchars($a["name"]) . "</td>";
|
if (!$a["enabled"]) {
|
||||||
|
$klassen[] = "opacity-50";
|
||||||
|
}
|
||||||
|
if ($tiefe > 0) {
|
||||||
|
// Eingeklappt starten: die Zeile gehoert dem Ausloeser, und
|
||||||
|
// der steht direkt darueber.
|
||||||
|
$klassen[] = "auto-folge";
|
||||||
|
$klassen[] = "auto-von-" . $vater;
|
||||||
|
$klassen[] = "d-none";
|
||||||
|
}
|
||||||
|
$klasse = $klassen ? " class='" . implode(" ", $klassen) . "'" : "";
|
||||||
|
|
||||||
|
// Der Pfeil klappt die Nachfolger auf. Er steht nur da, wo es
|
||||||
|
// welche gibt - ein Pfeil ohne Inhalt waere eine Einladung ins
|
||||||
|
// Leere.
|
||||||
|
$griff = "";
|
||||||
|
if ($kinder > 0) {
|
||||||
|
$griff = "<button type='button' class='btn btn-link btn-sm p-0 me-1 text-decoration-none'"
|
||||||
|
. " title='Abhängige Automatiken zeigen'"
|
||||||
|
. " data-gruppe='" . $id . "'"
|
||||||
|
. " onclick='toggleAutomationGroup(" . $id . ", this)'>"
|
||||||
|
. "<i class='bi bi-chevron-right'></i> " . $kinder . "</button>";
|
||||||
|
}
|
||||||
|
$einzug = $tiefe > 0
|
||||||
|
? "<span class='text-body-secondary me-1'>"
|
||||||
|
. str_repeat(" ", $tiefe)
|
||||||
|
. "<i class='bi bi-arrow-return-right'></i></span> "
|
||||||
|
: "";
|
||||||
|
|
||||||
|
echo "<tr data-auto='" . $id . "'" . $klasse . ">";
|
||||||
|
echo "<td class='text-nowrap'>" . $einzug . $griff
|
||||||
|
. htmlspecialchars($a["name"]) . "</td>";
|
||||||
echo "<td>" . htmlspecialchars($a["conditionText"]) . "</td>";
|
echo "<td>" . htmlspecialchars($a["conditionText"]) . "</td>";
|
||||||
echo "<td class='d-none d-sm-table-cell'>" . htmlspecialchars($a["actionText"]) . "</td>";
|
echo "<td class='d-none d-sm-table-cell'>" . htmlspecialchars($a["actionText"]) . "</td>";
|
||||||
echo "<td class='text-nowrap d-none d-md-table-cell'>" . htmlspecialchars($wann) . "</td>";
|
echo "<td class='text-nowrap d-none d-md-table-cell'>" . htmlspecialchars($wann) . "</td>";
|
||||||
@@ -141,6 +290,9 @@ $nutzdaten = json_encode([
|
|||||||
"automation" => $auto,
|
"automation" => $auto,
|
||||||
"devices" => deviceCatalog(),
|
"devices" => deviceCatalog(),
|
||||||
"lockouts" => lockoutChoices(),
|
"lockouts" => lockoutChoices(),
|
||||||
|
"ferien" => kalendertagChoices("Ferien", "in den Ferien"),
|
||||||
|
"feiertage" => kalendertagChoices("Feiertage", "an Feiertagen"),
|
||||||
|
"tagvorlagen" => tagVorlagen(),
|
||||||
// Nur fuer die Reihenfolge der Raum-Gruppen: rooms.php gibt sie vor, und
|
// Nur fuer die Reihenfolge der Raum-Gruppen: rooms.php gibt sie vor, und
|
||||||
// Etage fuer Etage liest sich besser als alphabetisch.
|
// Etage fuer Etage liest sich besser als alphabetisch.
|
||||||
"rooms" => raumListe(),
|
"rooms" => raumListe(),
|
||||||
@@ -172,66 +324,124 @@ $nutzdaten = json_encode([
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="accordion" id="actionAccordion">
|
<!--
|
||||||
<div class="accordion-item">
|
Drei Baender statt eines Akkordeons: Wenn - Wann - Dann.
|
||||||
<h2 class="accordion-header">
|
|
||||||
<button class="accordion-button" type="button" data-bs-toggle="collapse"
|
Das Akkordeon schloss beim Oeffnen die anderen Faecher, Ausloeser und
|
||||||
data-bs-target="#collapseOne" aria-expanded="true">
|
Aktion waren also nie gleichzeitig zu sehen - ausgerechnet bei einer
|
||||||
<i class="bi bi-lightning-fill"></i> Auslöser
|
Regel, die genau daraus besteht. Und das mittlere Fach hiess
|
||||||
</button>
|
"Bedingungen", enthielt aber die *Rahmen*bedingungen; die echten
|
||||||
</h2>
|
Bedingungen standen im ersten.
|
||||||
<div id="collapseOne" class="accordion-collapse collapse show" data-bs-parent="#actionAccordion">
|
|
||||||
<div class="accordion-body">
|
Gebaut aus .card mit farbigem Rand und .card-header - kein eigenes
|
||||||
|
Bauteil. Die Ueberschriften tragen .card-title und bekommen darueber die
|
||||||
|
Anzeigeschrift aus solar.css.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="card border-primary mb-2">
|
||||||
|
<div class="card-header d-flex flex-wrap align-items-center gap-2 py-2">
|
||||||
|
<i class="bi bi-lightning-charge-fill text-primary"></i>
|
||||||
|
<span class="card-title mb-0 fs-5 text-primary">Wenn</span>
|
||||||
|
<span class="small text-body-secondary">… das alles zusammenkommt</span>
|
||||||
|
<span class="flex-grow-1"></span>
|
||||||
|
<!-- Wie viele Bedingungen gerade zutreffen. Fuellt renderConditions(). -->
|
||||||
|
<span id="condStand" class="d-flex align-items-center gap-2"></span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2">
|
||||||
<!-- Ein Block ist eine UND-Gruppe, zwischen den Bloecken steht ODER.
|
<!-- Ein Block ist eine UND-Gruppe, zwischen den Bloecken steht ODER.
|
||||||
Die Klammerung ist damit gezeichnet und nicht bloss vereinbart. -->
|
Die Klammerung ist damit gezeichnet und nicht bloss vereinbart. -->
|
||||||
<div id="condBlocks"></div>
|
<div id="condBlocks"></div>
|
||||||
<button class="btn btn-outline-secondary btn-sm mt-2" type="button" id="btnAddGroup">
|
<button class="btn btn-outline-secondary btn-sm mt-2" type="button" id="btnAddGroup">
|
||||||
<i class="bi bi-plus-lg"></i> Alternative (oder)
|
<i class="bi bi-plus-lg"></i> oder stattdessen …
|
||||||
</button>
|
</button>
|
||||||
<div class="alert alert-secondary mt-3 mb-0 py-2 small" id="condSummary"></div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-2">
|
||||||
|
<!--
|
||||||
|
Der Rahmen steht zusammengefaltet da, solange er nur Vorgaben enthaelt -
|
||||||
|
und das ist der Normalfall. Frueher nahm er die meiste Flaeche fuer den
|
||||||
|
seltensten Inhalt.
|
||||||
|
-->
|
||||||
|
<!--
|
||||||
|
Zugeklappt liest man hier, was gilt; aufgeklappt greift man zu. Die drei
|
||||||
|
Vorlagen tauschen deshalb mit der Kurzfassung den Platz, statt eine
|
||||||
|
eigene Zeile im Rumpf zu bekommen - das war eine Zeile mehr und eine
|
||||||
|
Ueberschrift ("An welchen Tagen?"), die neben dem Wort "Wann" ohnehin
|
||||||
|
nichts Neues sagte.
|
||||||
|
|
||||||
|
ACHTUNG beim Umbauen: die Vorlagen duerfen NICHT innerhalb des Elements
|
||||||
|
stehen, das data-bs-toggle traegt. Bootstrap haengt seinen Umschalter
|
||||||
|
nicht an dieses Element, sondern ans Dokument - und dort in der
|
||||||
|
Capture-Phase, die vor jedem Klick auf einen Knopf darin laeuft. Ein
|
||||||
|
stopPropagation() im Knopf kommt deshalb grundsaetzlich zu spaet, und
|
||||||
|
jede Vorlage waere zugleich ein Zuklappen. Deshalb traegt nur der linke
|
||||||
|
Teil den Umschalter, die Vorlagen stehen daneben.
|
||||||
|
-->
|
||||||
|
<div class="card-header d-flex flex-wrap align-items-center gap-2 py-2">
|
||||||
|
<div class="d-flex align-items-center gap-2 flex-grow-1" id="rahmenKopf"
|
||||||
|
role="button" data-bs-toggle="collapse" data-bs-target="#rahmenBody"
|
||||||
|
aria-expanded="false">
|
||||||
|
<i class="bi bi-calendar-week text-warning"></i>
|
||||||
|
<span class="card-title mb-0 fs-5 text-warning">Wann</span>
|
||||||
|
<span id="rahmenKurz" class="text-body-secondary"></span>
|
||||||
|
<span class="flex-grow-1"></span>
|
||||||
|
<span class="small text-body-secondary text-nowrap" id="rahmenAendern">ändern <i class="bi bi-chevron-down"></i></span>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group btn-group-sm" role="group" id="tagVorlagen" hidden></div>
|
||||||
|
<i class="bi bi-chevron-up text-body-secondary" id="rahmenZu" role="button"
|
||||||
|
data-bs-toggle="collapse" data-bs-target="#rahmenBody" hidden></i>
|
||||||
|
</div>
|
||||||
|
<div id="rahmenBody" class="collapse">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="btn-group btn-group-sm flex-wrap mb-2" role="group" id="weekdayGroup"></div>
|
||||||
|
<div class="row g-2 mb-2">
|
||||||
|
<div class="col-sm-6 d-flex align-items-center gap-2">
|
||||||
|
<span class="text-body-secondary small" style="min-width:5rem">Ferien</span>
|
||||||
|
<div class="btn-group btn-group-sm" role="group" id="ferienGroup"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 d-flex align-items-center gap-2">
|
||||||
|
<span class="text-body-secondary small" style="min-width:5rem">Feiertage</span>
|
||||||
|
<div class="btn-group btn-group-sm" role="group" id="feiertagGroup"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
.callout ist ein AdminLTE-Bauteil: farbiger Balken links, getoenter
|
||||||
|
Grund, und die Toene kommen aus Bootstraps *-bg-subtle-Variablen -
|
||||||
|
es zieht also mit dem Theme mit, ohne dass hier eine Regel steht.
|
||||||
|
Als .alert stand der Satz vorher in einem eingesunkenen Kasten mit
|
||||||
|
Rahmen ringsum und las sich wie eine Warnung; er ist aber das
|
||||||
|
Ergebnis der Schalter darueber. Die Polsterung stellen Hilfsklassen
|
||||||
|
kleiner - .callout ist auf ganze Absaetze ausgelegt, hier steht ein
|
||||||
|
Satz.
|
||||||
|
-->
|
||||||
|
<div class="callout callout-primary py-2 px-3 mt-0 mb-3" id="rahmenSummary"></div>
|
||||||
|
|
||||||
|
<div class="row g-2 mb-3">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text">von</span>
|
||||||
|
<input type="time" class="form-control" id="tSpanFrom" value="00:00">
|
||||||
|
<span class="input-group-text">bis</span>
|
||||||
|
<input type="time" class="form-control" id="tSpanTo" value="23:59">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text">Sperre</span>
|
||||||
|
<select class="form-select" id="lockoutSecs"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="accordion-item">
|
<div class="form-check">
|
||||||
<h2 class="accordion-header">
|
<input class="form-check-input" type="checkbox" id="oncePerDay">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
<label class="form-check-label" for="oncePerDay">
|
||||||
data-bs-target="#collapseTwo" aria-expanded="false">
|
Nur einmal am Tag – danach bis Mitternacht gesperrt
|
||||||
<i class="bi bi-calendar-check-fill"></i> Bedingungen
|
</label>
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div id="collapseTwo" class="accordion-collapse collapse" data-bs-parent="#actionAccordion">
|
|
||||||
<div class="accordion-body">
|
|
||||||
<div class="input-group mb-3">
|
|
||||||
<span class="input-group-text">Aktiver Zeitraum</span>
|
|
||||||
<div class="form-floating">
|
|
||||||
<input type="time" class="form-control" id="tSpanFrom" value="00:00">
|
|
||||||
<label for="tSpanFrom">Von</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-floating">
|
<div class="form-check">
|
||||||
<input type="time" class="form-control" id="tSpanTo" value="23:59">
|
<input class="form-check-input" type="checkbox" id="runOnce">
|
||||||
<label for="tSpanTo">Bis</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
Aktive Wochentage:
|
|
||||||
<div class="input-group mb-3" id="weekdayGroup"></div>
|
|
||||||
<div class="input-group mb-3">
|
|
||||||
<div class="input-group-text">
|
|
||||||
<input class="form-check-input mt-0 me-2" type="checkbox" id="actFerien">
|
|
||||||
<label class="form-check-label" for="actFerien">In den Ferien ausführen</label>
|
|
||||||
</div>
|
|
||||||
<div class="input-group-text">
|
|
||||||
<input class="form-check-input mt-0 me-2" type="checkbox" id="actFeier">
|
|
||||||
<label class="form-check-label" for="actFeier">An Feiertagen ausführen</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="input-group mb-3">
|
|
||||||
<span class="input-group-text">Sperrzeit nach dem Auslösen</span>
|
|
||||||
<select class="form-select" id="lockoutSecs"></select>
|
|
||||||
</div>
|
|
||||||
<div class="input-group mb-0">
|
|
||||||
<div class="input-group-text">
|
|
||||||
<input class="form-check-input mt-0 me-2" type="checkbox" id="runOnce">
|
|
||||||
<label class="form-check-label" for="runOnce">
|
<label class="form-check-label" for="runOnce">
|
||||||
Am Ende des Zeitraums auf jeden Fall ausführen, falls der Auslöser nie zutraf
|
Am Ende des Zeitraums auf jeden Fall ausführen, falls der Auslöser nie zutraf
|
||||||
</label>
|
</label>
|
||||||
@@ -239,23 +449,18 @@ $nutzdaten = json_encode([
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="accordion-item">
|
<div class="card border-success mb-0">
|
||||||
<h2 class="accordion-header">
|
<div class="card-header d-flex flex-wrap align-items-center gap-2 py-2">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
<i class="bi bi-play-fill text-success"></i>
|
||||||
data-bs-target="#collapseThree" aria-expanded="false">
|
<span class="card-title mb-0 fs-5 text-success">Dann</span>
|
||||||
<i class="bi bi-play-fill"></i> Aktionen
|
<span class="small text-body-secondary">… passiert das hier</span>
|
||||||
</button>
|
</div>
|
||||||
</h2>
|
<div class="card-body p-2">
|
||||||
<div id="collapseThree" class="accordion-collapse collapse" data-bs-parent="#actionAccordion">
|
|
||||||
<div class="accordion-body">
|
|
||||||
<div id="actionList"></div>
|
<div id="actionList"></div>
|
||||||
<button class="btn btn-outline-success btn-sm mt-2" type="button" id="btnAddActor">
|
<button class="btn btn-outline-success btn-sm mt-2" type="button" id="btnAddActor">
|
||||||
<i class="bi bi-plus-lg"></i> Aktion
|
<i class="bi bi-plus-lg"></i> und noch etwas tun
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+277
-19
@@ -29,7 +29,22 @@
|
|||||||
--ton-sonne-rgb: 240, 180, 41;
|
--ton-sonne-rgb: 240, 180, 41;
|
||||||
--ton-gut: #4caf7d;
|
--ton-gut: #4caf7d;
|
||||||
--ton-schlecht: #e06060;
|
--ton-schlecht: #e06060;
|
||||||
--ton-radius: .7rem;
|
|
||||||
|
/* Zwei Radien statt einem. Der grosse gilt fuer Flaechen (Karten, Modale,
|
||||||
|
Eingabefelder), der kleine fuer alles, was in einer Reihe steht -
|
||||||
|
Schalter, kleine Knoepfe, Abzeichen. Bootstrap kennt beide, benutzte
|
||||||
|
bisher aber nur den grossen; der kleine stand auf seiner Vorgabe von
|
||||||
|
.25rem und wirkte daneben eckig. */
|
||||||
|
--ton-radius: .875rem;
|
||||||
|
--ton-radius-klein: .5rem;
|
||||||
|
|
||||||
|
/* Die Anzeigeschrift. Poppins liegt seit jeher unter assets/fonts und trug
|
||||||
|
bisher nur die Etagenknoepfe im Aussenplan - eine ganze Schrift fuer vier
|
||||||
|
Buchstaben. Als Variable traegt sie jetzt alle Ueberschriften: waermer
|
||||||
|
und eine Spur weniger technisch als die Source Sans 3 des Fliesstextes,
|
||||||
|
ohne dass eine zweite Datei geladen wird. Zahlenkolonnen und Fliesstext
|
||||||
|
bleiben unberuehrt - Poppins hat keine Tabellenziffern. */
|
||||||
|
--font-anzeige: Poppins, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
|
|
||||||
/* ... und dieselben Werte dort, wo Bootstrap sie sucht. */
|
/* ... und dieselben Werte dort, wo Bootstrap sie sucht. */
|
||||||
--bs-body-bg: var(--ton-grund);
|
--bs-body-bg: var(--ton-grund);
|
||||||
@@ -43,11 +58,25 @@
|
|||||||
--bs-tertiary-bg: var(--ton-flaeche-2);
|
--bs-tertiary-bg: var(--ton-flaeche-2);
|
||||||
--bs-border-color: var(--ton-linie-satt);
|
--bs-border-color: var(--ton-linie-satt);
|
||||||
--bs-border-radius: var(--ton-radius);
|
--bs-border-radius: var(--ton-radius);
|
||||||
|
--bs-border-radius-sm: var(--ton-radius-klein);
|
||||||
--bs-link-color: var(--ton-sonne);
|
--bs-link-color: var(--ton-sonne);
|
||||||
--bs-link-hover-color: #ffc94d;
|
--bs-link-hover-color: #ffc94d;
|
||||||
--bs-heading-color: var(--ton-schrift);
|
--bs-heading-color: var(--ton-schrift);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ueberschriften in der Anzeigeschrift. Bewusst nur Ueberschriften: der
|
||||||
|
Fliesstext und alle Zahlenkolonnen bleiben bei der Schrift des Themes,
|
||||||
|
weil Poppins keine Tabellenziffern mitbringt und Messwerte sonst
|
||||||
|
springen wuerden. */
|
||||||
|
h1, h2, h3, h4, h5, h6,
|
||||||
|
.h1, .h2, .h3, .h4, .h5, .h6,
|
||||||
|
.card-title,
|
||||||
|
.modal-title {
|
||||||
|
font-family: var(--font-anzeige);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -.01em;
|
||||||
|
}
|
||||||
|
|
||||||
/* Der Rumpf traegt zwei Grundfarben: die Seite und die Leisten drumherum.
|
/* Der Rumpf traegt zwei Grundfarben: die Seite und die Leisten drumherum.
|
||||||
AdminLTE setzt sie ueber eigene Klassen, die hier auf denselben Ton
|
AdminLTE setzt sie ueber eigene Klassen, die hier auf denselben Ton
|
||||||
gebracht werden - sonst steht eine Karte auf drei verschiedenen Grautoenen. */
|
gebracht werden - sonst steht eine Karte auf drei verschiedenen Grautoenen. */
|
||||||
@@ -655,21 +684,242 @@ input[type='range']::-webkit-slider-runnable-track {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Editor der Automatiken. Eine Bedingungszeile traegt Geraet, Messwert,
|
/* --------------------------------------------------------------------------
|
||||||
Vergleich, Wert, Einheit und den Papierkorb nebeneinander - auf einem
|
Drei Klassen, die der Automatik-Editor gebraucht hat - hier, nicht dort
|
||||||
Handy bleiben davon "Therm OG Bad" und "te" uebrig, weil die Felder in
|
|
||||||
der input-group einfach mitschrumpfen. Unter 576 px bekommen deshalb
|
Sie stehen absichtlich in der gemeinsamen Datei und nicht als Sonderfall
|
||||||
Geraet und Messwert je eine eigene Zeile; Vergleich, Wert und Einheit
|
im Editor. Zwei davon sind allgemein genug, um anderswo zu taugen; die
|
||||||
sind kurz genug, um zusammen in die naechste zu passen. */
|
dritte ist eine echte Ausnahme und als einzelne Klasse auch als solche
|
||||||
@media (max-width: 575.98px) {
|
erkennbar.
|
||||||
/* Die input-group-Klasse muss mit in den Selektor: solar.css wird vor
|
-------------------------------------------------------------------------- */
|
||||||
AdminLTE geladen, und Bootstraps ".input-group > .form-select" haette
|
|
||||||
sonst dasselbe Gewicht und wuerde als spaeteres Blatt gewinnen. */
|
/* Ein Auswahlfeld, das wie Text aussieht, bis man es anfaehrt.
|
||||||
.input-group.auto-zeile > .auto-geraet,
|
|
||||||
.input-group.auto-zeile > .auto-messwert {
|
Eine Bedingung hat sechs Bestandteile, aber nur zwei davon traegt man
|
||||||
flex: 1 1 100%;
|
wirklich ein: Vergleich und Wert. Standen alle sechs als gleich schwere
|
||||||
width: auto;
|
Kaesten nebeneinander, sagte nichts mehr, worauf es ankommt. Die
|
||||||
|
gepunktete Linie sagt "anklickbar", ohne den Platz und das Gewicht eines
|
||||||
|
Eingabefelds zu nehmen.
|
||||||
|
|
||||||
|
Bewusst allgemein: dieselbe Not haben das Raum-Modal und die
|
||||||
|
Kachel-Einstellungen, wo ebenfalls jede Kleinigkeit in einem Kasten
|
||||||
|
steht. Taugt fuer <select> wie fuer <button>. */
|
||||||
|
.feld-als-text {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0 0 1px;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px dashed var(--ton-linie-satt);
|
||||||
|
border-radius: 0;
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feld-als-text:hover,
|
||||||
|
.feld-als-text:focus-visible {
|
||||||
|
border-bottom-color: var(--bs-link-color);
|
||||||
|
color: var(--bs-emphasis-color);
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ein Messwert, wie er in diesem Moment dasteht.
|
||||||
|
|
||||||
|
Steht neben einer Bedingung und beantwortet die Frage, die man wirklich
|
||||||
|
hat: "warum laeuft die nicht?". Das ist die Ausnahme unter den dreien -
|
||||||
|
Bedingungen gibt es sonst nirgends auf der Seite. */
|
||||||
|
.wert-jetzt {
|
||||||
|
font-size: .8125rem;
|
||||||
|
color: var(--ton-schrift-3);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wert-jetzt b { font-weight: 400; color: var(--ton-schrift-2); }
|
||||||
|
.wert-jetzt.erfuellt b { color: var(--ton-gut); }
|
||||||
|
|
||||||
|
/* Erfuellt oder noch nicht. Bewusst kein Rot: eine Bedingung, die gerade
|
||||||
|
nicht zutrifft, ist der Normalfall und kein Fehler. */
|
||||||
|
.punkt {
|
||||||
|
display: inline-block;
|
||||||
|
width: .7rem;
|
||||||
|
height: .7rem;
|
||||||
|
border: 1.5px solid var(--ton-schrift-3);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.punkt.an {
|
||||||
|
border-color: var(--ton-gut);
|
||||||
|
background: var(--ton-gut);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Atmet leise, wo etwas mitlaeuft - ein Wort "live" daneben braucht es dann
|
||||||
|
nicht. */
|
||||||
|
.punkt.atmet { animation: punkt-atmen 2.4s ease-in-out infinite; }
|
||||||
|
|
||||||
|
@keyframes punkt-atmen {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: .35; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.punkt.atmet { animation: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --------------------------------------------------------------------------
|
||||||
|
Editor der Automatiken: die Bedingungszeile
|
||||||
|
|
||||||
|
Frueher eine input-group aus sechs gleich schweren Kaesten. Jetzt ein
|
||||||
|
Raster, in dem Geraet und Messwert uebereinander stehen (das Geraet
|
||||||
|
beantwortet "woher", nicht "was") und nur Vergleich und Wert noch wie
|
||||||
|
Eingabefelder aussehen.
|
||||||
|
|
||||||
|
Auf schmalen Schirmen bricht die Zeile in zwei: oben, worum es geht,
|
||||||
|
unten der Vergleich. Frueher schrumpften die Felder stattdessen mit, bis
|
||||||
|
von "Thermostat OG Bad" ein "Therm" uebrig war.
|
||||||
|
-------------------------------------------------------------------------- */
|
||||||
|
.auto-zeile {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.75rem minmax(0, 1fr) 5.25rem 8rem 8.5rem .75rem 1.75rem;
|
||||||
|
grid-template-areas: "sym was op wert jetzt punkt weg";
|
||||||
|
gap: .625rem;
|
||||||
|
align-items: center;
|
||||||
|
padding: .45rem .55rem;
|
||||||
|
border-radius: var(--ton-radius-klein);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-zeile:nth-of-type(even) { background: rgba(255, 255, 255, .022); }
|
||||||
|
|
||||||
|
.auto-zeile > .auto-symbol { grid-area: sym; color: var(--ton-schrift-2); }
|
||||||
|
.auto-zeile > .auto-was { grid-area: was; min-width: 0; }
|
||||||
|
.auto-zeile > .auto-op { grid-area: op; }
|
||||||
|
.auto-zeile > .auto-wert { grid-area: wert; }
|
||||||
|
.auto-zeile > .wert-jetzt { grid-area: jetzt; }
|
||||||
|
.auto-zeile > .punkt { grid-area: punkt; }
|
||||||
|
.auto-zeile > .auto-weg { grid-area: weg; }
|
||||||
|
.auto-zeile > .auto-werte { grid-area: werte; }
|
||||||
|
|
||||||
|
/* Die Aktionszeile: dasselbe Raster, andere Spalten. Statt Vergleich, Wert
|
||||||
|
und laufendem Messwert steht dort ein Block mit den Parametern des
|
||||||
|
Kommandos - mal keiner ("Auf"), mal zwei ("Position+Neigung"). */
|
||||||
|
.auto-zeile.tat {
|
||||||
|
grid-template-columns: 1.75rem minmax(0, 1fr) auto 1.75rem;
|
||||||
|
grid-template-areas: "sym was werte weg";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Eine Zeile ohne Geraet: dann gibt es nur eines zu tun, und das darf die
|
||||||
|
ganze Breite haben. Vergleich, Wert und der laufende Messwert gibt es in
|
||||||
|
diesem Zustand noch gar nicht - ihre Spalten stuenden sonst nur leer
|
||||||
|
herum, waehrend die eine Schaltflaeche in eine schmale Spalte gequetscht
|
||||||
|
waere. */
|
||||||
|
.auto-zeile.leer { grid-template-areas: "sym was was was was punkt weg"; }
|
||||||
|
|
||||||
|
/* Geraet klein und matt ueber dem Messwert - und trotzdem anklickbar. */
|
||||||
|
.auto-was { display: flex; flex-direction: column; gap: .05rem; }
|
||||||
|
|
||||||
|
/* Nur in der Bedingungszeile: die Aktionszeile benutzt .auto-geraet
|
||||||
|
weiterhin als vollwertige Auswahlflaeche und darf davon nichts
|
||||||
|
abbekommen. */
|
||||||
|
/* Nicht in der leeren Zeile: dort traegt derselbe Knopf zusaetzlich
|
||||||
|
.feld-als-text und ist die einzige Handlung, die es gibt - klein und matt
|
||||||
|
waere er dort genau die Falle, die er vorher war. Die Klasse .auto-geraet
|
||||||
|
behaelt er trotzdem, weil daran haengt, WAS er ist: der Klick-Horcher
|
||||||
|
erkennt ihn daran. Verhalten und Aussehen sind absichtlich zwei
|
||||||
|
verschiedene Klassen. */
|
||||||
|
.auto-was > .auto-geraet:not(.feld-als-text) {
|
||||||
|
align-self: start;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
color: var(--ton-schrift-3);
|
||||||
|
font-size: .72rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-align: left;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-was > .auto-geraet:not(.feld-als-text):hover,
|
||||||
|
.auto-was > .auto-geraet:not(.feld-als-text):focus-visible {
|
||||||
|
color: var(--bs-link-color);
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-messwert { align-self: start; }
|
||||||
|
|
||||||
|
/* Der Papierkorb erscheint erst beim Ueberfahren. Acht rote Knoepfe im
|
||||||
|
Ruhezustand zogen alle Aufmerksamkeit auf die seltenste Handlung.
|
||||||
|
Wo es kein Ueberfahren gibt, steht er dauerhaft da. */
|
||||||
|
.auto-zeile > .auto-weg {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
color: var(--ton-schrift-3);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity .15s ease, color .15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-zeile:hover > .auto-weg,
|
||||||
|
.auto-zeile:focus-within > .auto-weg { opacity: 1; }
|
||||||
|
.auto-zeile > .auto-weg:hover { color: var(--ton-schlecht); }
|
||||||
|
|
||||||
|
@media (hover: none) {
|
||||||
|
.auto-zeile > .auto-weg { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ein Segment je Bedingung, im Kopf des Bandes und ueber jeder Gruppe.
|
||||||
|
Eine Zahl allein ("2 von 4") liest man, die Reihe sieht man. */
|
||||||
|
.auto-segment {
|
||||||
|
width: .8rem;
|
||||||
|
height: .3rem;
|
||||||
|
border-radius: .15rem;
|
||||||
|
background: var(--ton-linie-satt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-segment.an { background: var(--ton-gut); }
|
||||||
|
|
||||||
|
/* Was der Browser nicht entscheiden kann, bleibt hohl - siehe ZEITARTEN in
|
||||||
|
autoActionFuncs.js. */
|
||||||
|
.auto-segment.offen {
|
||||||
|
background: none;
|
||||||
|
box-shadow: inset 0 0 0 1px var(--ton-linie-satt);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.auto-zeile {
|
||||||
|
grid-template-columns: 1.75rem 5.25rem minmax(0, 1fr) .75rem 1.75rem;
|
||||||
|
/* Drei Zeilen statt zwei: der laufende Wert quetschte sich sonst mit
|
||||||
|
dem Wertfeld in dieselbe Reihe und blieb als "gerad…" stehen. Auf
|
||||||
|
einem Handy ist Hoehe billig und Breite teuer. */
|
||||||
|
grid-template-areas:
|
||||||
|
"sym was was punkt weg"
|
||||||
|
"sym op wert wert wert"
|
||||||
|
"sym jetzt jetzt jetzt jetzt";
|
||||||
|
row-gap: .35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auto-zeile.leer { grid-template-areas: "sym was was punkt weg"; }
|
||||||
|
|
||||||
|
.auto-zeile.tat {
|
||||||
|
grid-template-columns: 1.75rem minmax(0, 1fr) 1.75rem;
|
||||||
|
grid-template-areas:
|
||||||
|
"sym was weg"
|
||||||
|
"sym werte werte";
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-zeile.tat.leer { grid-template-areas: "sym was weg"; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Raum-Modal. Ein Kommando mit mehreren Parametern ("Position+Neigung")
|
/* Raum-Modal. Ein Kommando mit mehreren Parametern ("Position+Neigung")
|
||||||
@@ -1280,11 +1530,19 @@ input[type='range']::-webkit-slider-runnable-track {
|
|||||||
.skoda-auto.banane .bananenschale { display: none; }
|
.skoda-auto.banane .bananenschale { display: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.skoda-auto .rad rect { fill: rgba(18, 20, 24, 0.9); }
|
/* Die Reifen liegen als unterste Lage unter der ganzen Karosserie (siehe die
|
||||||
|
Schichtung in restricted/skoda.php) und sind schwarz wie am Auto. Unter zwei
|
||||||
|
getoenten Flaechen bleibt davon nicht viel uebrig, vorn unter der Haube fast
|
||||||
|
nichts - das ist so gewollt. Von oben saehe man die Reifen ohnehin nicht;
|
||||||
|
sie stehen im Bild, damit die Achsen einen Ort haben, nicht damit man sie
|
||||||
|
liest. */
|
||||||
|
.skoda-auto .rad rect { fill: rgba(14, 16, 19, 0.95); }
|
||||||
.skoda-auto .spiegel rect { fill: rgba(var(--skoda-blech), 0.55); }
|
.skoda-auto .spiegel rect { fill: rgba(var(--skoda-blech), 0.55); }
|
||||||
/* Anhaengerkupplung: dunkler als das Blech, sie ist ja auch nicht lackiert. */
|
/* Die Kupplung ist schwarz und bleibt es: sie steht hinten heraus und liegt
|
||||||
|
dort auf dem Hintergrund, nicht unter dem Blech - sie braucht die
|
||||||
|
Aufhellung der Reifen also nicht. */
|
||||||
.skoda-auto .kupplung rect,
|
.skoda-auto .kupplung rect,
|
||||||
.skoda-auto .kupplung circle { fill: rgba(28, 31, 35, 0.95); }
|
.skoda-auto .kupplung circle { fill: rgba(10, 12, 14, 0.95); }
|
||||||
/* Dachspoiler und Reling sind an diesem Fahrzeug schwarz, nicht lackiert -
|
/* Dachspoiler und Reling sind an diesem Fahrzeug schwarz, nicht lackiert -
|
||||||
und nur dadurch heben sie sich vom Dach ab, auf dem sie liegen. Vorher
|
und nur dadurch heben sie sich vom Dach ab, auf dem sie liegen. Vorher
|
||||||
hatten sie den Ton des Blechs und verschwanden darin. */
|
hatten sie den Ton des Blechs und verschwanden darin. */
|
||||||
@@ -2327,7 +2585,7 @@ input[type='range']::-webkit-slider-runnable-track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.floorBtn text {
|
.floorBtn text {
|
||||||
font: 600 34px Poppins, system-ui, sans-serif;
|
font: 600 34px var(--font-anzeige);
|
||||||
fill: #9aa0a6;
|
fill: #9aa0a6;
|
||||||
opacity: .8;
|
opacity: .8;
|
||||||
transition: fill .25s ease, opacity .25s ease;
|
transition: fill .25s ease, opacity .25s ease;
|
||||||
|
|||||||
+828
-113
File diff suppressed because it is too large
Load Diff
+332
-14
@@ -54,6 +54,11 @@ function meshDb()
|
|||||||
* danach liegen kann - deshalb dieselben drei Faelle noch einmal mal zwei:
|
* danach liegen kann - deshalb dieselben drei Faelle noch einmal mal zwei:
|
||||||
* "+ 00:30" eine halbe Stunde nach Sonnenaufgang, "ab - 00:30" ab einer
|
* "+ 00:30" eine halbe Stunde nach Sonnenaufgang, "ab - 00:30" ab einer
|
||||||
* halben Stunde davor, "vor + 00:30" bis eine halbe Stunde danach.
|
* halben Stunde davor, "vor + 00:30" bis eine halbe Stunde danach.
|
||||||
|
*
|
||||||
|
* Dieselbe Schreibweise traegt die Verkettung. Das gerechnete Geraet
|
||||||
|
* "Automatiken" fuehrt jede Automatik als Messwert, ihr Wert ist der
|
||||||
|
* Zeitpunkt der letzten Ausloesung (Datentyp `elapsed`) - "Wecker Magdalena
|
||||||
|
* + 00:10" heisst zehn Minuten danach. Ein Minus gibt es dort nicht.
|
||||||
*/
|
*/
|
||||||
function operatorsForType($type)
|
function operatorsForType($type)
|
||||||
{
|
{
|
||||||
@@ -67,6 +72,10 @@ function operatorsForType($type)
|
|||||||
return ["=", ">=", "<"];
|
return ["=", ">=", "<"];
|
||||||
case "deltatime": // Sonnenauf-/-untergang: Wert ist ein Versatz
|
case "deltatime": // Sonnenauf-/-untergang: Wert ist ein Versatz
|
||||||
return ["+", "-", ">=+", ">=-", "<+", "<-"];
|
return ["+", "-", ">=+", ">=-", "<+", "<-"];
|
||||||
|
case "elapsed": // andere Automatik: Wert ist ein Versatz danach
|
||||||
|
// Nur die drei Formen "danach". Ein Minus gibt es nicht: vor dem
|
||||||
|
// Auslöser kann nichts liegen, das erst der Auslöser anstößt.
|
||||||
|
return ["+", ">=+", "<+"];
|
||||||
case "bool":
|
case "bool":
|
||||||
return ["="]; // JA/NEIN steht im Wertfeld, nicht im Operator
|
return ["="]; // JA/NEIN steht im Wertfeld, nicht im Operator
|
||||||
default:
|
default:
|
||||||
@@ -84,6 +93,115 @@ function operatorChoices($type)
|
|||||||
return $liste;
|
return $liste;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die aktuellen Werte zu einer Liste von Messwerten.
|
||||||
|
*
|
||||||
|
* Der Editor zeigt neben jeder Bedingung, was der Messwert gerade sagt, und
|
||||||
|
* holt sich das alle paar Sekunden nach. Bewusst aus actor_states und nicht
|
||||||
|
* aus MQTT: ueber den Broker kommt nur ein Teil der Geraete: die Jalousien
|
||||||
|
* haengen an der Tahoma-Box, die gerechneten Werte an gar nichts. Der Runner
|
||||||
|
* schreibt dagegen jeden Messwert zurueck, egal ueber welchen Weg er ihn
|
||||||
|
* geholt hat - eine Tabelle, ein Zugriff, alle Geraetearten.
|
||||||
|
*
|
||||||
|
* Die Ids kommen aus dem Browser und werden deshalb zu Zahlen gemacht, bevor
|
||||||
|
* sie in die Abfrage gehen.
|
||||||
|
*/
|
||||||
|
function stateValues(array $ids)
|
||||||
|
{
|
||||||
|
$ids = array_values(array_unique(array_filter(array_map("intval", $ids))));
|
||||||
|
if (!$ids) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$db = meshDb();
|
||||||
|
$liste = implode(",", $ids);
|
||||||
|
$res = $db->query("SELECT id, current_value, unit FROM actor_states
|
||||||
|
WHERE id IN (" . $liste . ")");
|
||||||
|
$werte = [];
|
||||||
|
while ($row = $res->fetch_assoc()) {
|
||||||
|
$werte[intval($row["id"])] = [
|
||||||
|
"value" => $row["current_value"],
|
||||||
|
"unit" => $row["unit"],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $werte;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ferien und Feiertage sind dreiwertig, nicht ja/nein.
|
||||||
|
*
|
||||||
|
* Zwei Zustaende gab es immer: "egal" (der Tag aendert nichts) und "nie"
|
||||||
|
* (an solchen Tagen laeuft die Automatik nicht - so ist "werktags" gebaut).
|
||||||
|
* "Zusaetzlich" ist der dritte und zaehlt wie ein passender Wochentag.
|
||||||
|
*
|
||||||
|
* Ohne ihn liess sich "an Wochenenden und Feiertagen" gar nicht schreiben:
|
||||||
|
* die Wochentagsmaske kennt nur Samstag und Sonntag, und ein Feiertag am
|
||||||
|
* Dienstag ist eben ein Dienstag. Jetzt genuegt Sa+So angehakt und
|
||||||
|
* "Feiertage: zusaetzlich".
|
||||||
|
*
|
||||||
|
* Ein Verbot schlaegt eine Erweiterung: wer in den Ferien nie laeuft und an
|
||||||
|
* Feiertagen zusaetzlich, laeuft an einem Feiertag in den Ferien nicht.
|
||||||
|
* Andersherum liesse sich "nie" nicht mehr verlassen. Der Runner haelt
|
||||||
|
* dieselbe Reihenfolge ein (tag_passt in autoaction_runner.py).
|
||||||
|
*/
|
||||||
|
function kalendertagChoices($was, $wann)
|
||||||
|
{
|
||||||
|
// Drei kurze Worte auf einer Achse, von eng nach weit: nie - egal -
|
||||||
|
// immer. Sie stehen als Schalterreihe neben "Ferien" bzw. "Feiertage",
|
||||||
|
// die Erklaerung haengt als title daran und der ganze Rahmen steht
|
||||||
|
// darunter noch einmal als Satz.
|
||||||
|
//
|
||||||
|
// Vorher stand die Erklaerung *im* Etikett ("zusaetzlich, auch am
|
||||||
|
// falschen Wochentag"). Ein Auswahlfeld, dessen Eintraege Saetze sind,
|
||||||
|
// liest sich wie eine Bedienungsanleitung und zeigt trotzdem nur den
|
||||||
|
// gewaehlten Satz - man sieht nie, was es sonst noch gibt.
|
||||||
|
return [
|
||||||
|
["value" => 0, "label" => "nie",
|
||||||
|
"title" => "Läuft " . $wann . " nicht."],
|
||||||
|
["value" => 1, "label" => "egal",
|
||||||
|
"title" => $was . " ändern nichts."],
|
||||||
|
["value" => 2, "label" => "immer",
|
||||||
|
"title" => "Läuft " . $wann . ", auch wenn kein Wochentag angehakt ist."],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vorlagen fuer die Tagesauswahl - die drei Faelle, die man wirklich meint.
|
||||||
|
*
|
||||||
|
* "Werktags" heisst nicht nur Mo-Fr, sondern auch "nicht an Feiertagen", und
|
||||||
|
* "Wochenende" heisst fast immer "Wochenende und Feiertage". Wer das aus
|
||||||
|
* Wochentagsmaske und zwei Dreifachschaltern von Hand zusammensetzt, vergisst
|
||||||
|
* den zweiten Teil - deshalb steht er hier mit drin.
|
||||||
|
*/
|
||||||
|
function tagVorlagen()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
["label" => "täglich", "weekdays" => 127, "on_vacation" => 1, "on_holiday" => 1,
|
||||||
|
"title" => "Jeden Tag, Feiertage eingeschlossen."],
|
||||||
|
["label" => "werktags", "weekdays" => 31, "on_vacation" => 1, "on_holiday" => 0,
|
||||||
|
"title" => "Montag bis Freitag, aber nicht an Feiertagen."],
|
||||||
|
["label" => "Wochenende", "weekdays" => 96, "on_vacation" => 1, "on_holiday" => 2,
|
||||||
|
"title" => "Samstag und Sonntag, dazu alle Feiertage."],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein Kalenderwert aus dem Editor - alles Krumme wird zu "egal".
|
||||||
|
*
|
||||||
|
* is_numeric davor, und zwar mit Absicht: intval("") ist 0, und 0 heisst hier
|
||||||
|
* "nie". Eine leere oder verunglueckte Angabe wuerde eine Automatik sonst
|
||||||
|
* stillschweigend an Feiertagen abschalten - eine Regelaenderung, die niemand
|
||||||
|
* angeordnet hat und die man erst am naechsten Feiertag bemerkt. "Egal" ist
|
||||||
|
* die harmlose Vorgabe: der Tag aendert dann nichts.
|
||||||
|
*/
|
||||||
|
function kalendertagWert($wert)
|
||||||
|
{
|
||||||
|
if (!is_numeric($wert)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
$zahl = intval($wert);
|
||||||
|
return in_array($zahl, [0, 1, 2], true) ? $zahl : 1;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sperrzeit: so lange nach einer Ausloesung wird nicht erneut geschaltet.
|
* Sperrzeit: so lange nach einer Ausloesung wird nicht erneut geschaltet.
|
||||||
*
|
*
|
||||||
@@ -157,6 +275,7 @@ function inputForType($type, $hasOptions)
|
|||||||
return "number";
|
return "number";
|
||||||
case "time":
|
case "time":
|
||||||
case "deltatime":
|
case "deltatime":
|
||||||
|
case "elapsed":
|
||||||
return "time";
|
return "time";
|
||||||
case "date":
|
case "date":
|
||||||
return "date";
|
return "date";
|
||||||
@@ -192,6 +311,14 @@ function operatorLabel($op, $type = "")
|
|||||||
return $beschriftung[$op];
|
return $beschriftung[$op];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ($type === "elapsed") {
|
||||||
|
// Dieselben Wörter wie beim Sonnenstand, damit ein Versatz überall
|
||||||
|
// gleich gelesen wird: "Wecker + 00:10" sind zehn Minuten danach.
|
||||||
|
$beschriftung = ["+" => "+", ">=+" => "ab +", "<+" => "vor +"];
|
||||||
|
if (isset($beschriftung[$op])) {
|
||||||
|
return $beschriftung[$op];
|
||||||
|
}
|
||||||
|
}
|
||||||
return $op === "!=" ? "≠" : $op;
|
return $op === "!=" ? "≠" : $op;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,9 +801,10 @@ function emptyAutomation($floor)
|
|||||||
"window_from" => "00:00",
|
"window_from" => "00:00",
|
||||||
"window_to" => "23:59",
|
"window_to" => "23:59",
|
||||||
"weekdays" => 127,
|
"weekdays" => 127,
|
||||||
"on_vacation" => true,
|
"on_vacation" => 1,
|
||||||
"on_holiday" => true,
|
"on_holiday" => 1,
|
||||||
"force_once" => false,
|
"force_once" => false,
|
||||||
|
"once_per_day" => false,
|
||||||
"lockout_secs" => 60,
|
"lockout_secs" => 60,
|
||||||
"last_run" => null,
|
"last_run" => null,
|
||||||
"conditions" => [],
|
"conditions" => [],
|
||||||
@@ -705,9 +833,12 @@ function loadAutomation($id)
|
|||||||
"window_from" => substr($row["window_from"], 0, 5),
|
"window_from" => substr($row["window_from"], 0, 5),
|
||||||
"window_to" => substr($row["window_to"], 0, 5),
|
"window_to" => substr($row["window_to"], 0, 5),
|
||||||
"weekdays" => intval($row["weekdays"]),
|
"weekdays" => intval($row["weekdays"]),
|
||||||
"on_vacation" => (bool)$row["on_vacation"],
|
"on_vacation" => intval($row["on_vacation"]),
|
||||||
"on_holiday" => (bool)$row["on_holiday"],
|
"on_holiday" => intval($row["on_holiday"]),
|
||||||
"force_once" => (bool)$row["force_once"],
|
"force_once" => (bool)$row["force_once"],
|
||||||
|
// ?? 0, damit der Editor auch vor der Migration noch aufgeht:
|
||||||
|
// rahmen_erweitern.sql legt die Spalte erst an.
|
||||||
|
"once_per_day" => (bool)($row["once_per_day"] ?? 0),
|
||||||
"lockout_secs" => intval($row["lockout_secs"]),
|
"lockout_secs" => intval($row["lockout_secs"]),
|
||||||
"last_run" => $row["last_run"],
|
"last_run" => $row["last_run"],
|
||||||
"conditions" => [],
|
"conditions" => [],
|
||||||
@@ -785,15 +916,17 @@ function saveAutomation($data)
|
|||||||
if (!$actions) {
|
if (!$actions) {
|
||||||
throw new InvalidArgumentException("Ohne Aktion haette die Automatik nichts zu tun.");
|
throw new InvalidArgumentException("Ohne Aktion haette die Automatik nichts zu tun.");
|
||||||
}
|
}
|
||||||
|
pruefeKreis($data["id"] ?? 0, $conditions);
|
||||||
|
|
||||||
$id = intval($data["id"] ?? 0);
|
$id = intval($data["id"] ?? 0);
|
||||||
$enabled = !empty($data["enabled"]) ? 1 : 0;
|
$enabled = !empty($data["enabled"]) ? 1 : 0;
|
||||||
$windowFrom = normalizeTime($data["window_from"] ?? "00:00", "00:00:00");
|
$windowFrom = normalizeTime($data["window_from"] ?? "00:00", "00:00:00");
|
||||||
$windowTo = normalizeTime($data["window_to"] ?? "23:59", "23:59:00");
|
$windowTo = normalizeTime($data["window_to"] ?? "23:59", "23:59:00");
|
||||||
$weekdays = intval($data["weekdays"] ?? 127) & 127;
|
$weekdays = intval($data["weekdays"] ?? 127) & 127;
|
||||||
$onVacation = !empty($data["on_vacation"]) ? 1 : 0;
|
$onVacation = kalendertagWert($data["on_vacation"] ?? 1);
|
||||||
$onHoliday = !empty($data["on_holiday"]) ? 1 : 0;
|
$onHoliday = kalendertagWert($data["on_holiday"] ?? 1);
|
||||||
$forceOnce = !empty($data["force_once"]) ? 1 : 0;
|
$forceOnce = !empty($data["force_once"]) ? 1 : 0;
|
||||||
|
$oncePerDay = !empty($data["once_per_day"]) ? 1 : 0;
|
||||||
// Nur die angebotenen Stufen, sonst die Vorgabe. Eine krumme Zahl kaeme
|
// Nur die angebotenen Stufen, sonst die Vorgabe. Eine krumme Zahl kaeme
|
||||||
// hier nur aus einer selbstgebauten Anfrage.
|
// hier nur aus einer selbstgebauten Anfrage.
|
||||||
$lockout = intval($data["lockout_secs"] ?? 60);
|
$lockout = intval($data["lockout_secs"] ?? 60);
|
||||||
@@ -825,10 +958,11 @@ function saveAutomation($data)
|
|||||||
$stmt = $db->prepare("UPDATE automations SET name = ?, floor = ?, enabled = ?,
|
$stmt = $db->prepare("UPDATE automations SET name = ?, floor = ?, enabled = ?,
|
||||||
window_from = ?, window_to = ?, weekdays = ?,
|
window_from = ?, window_to = ?, weekdays = ?,
|
||||||
on_vacation = ?, on_holiday = ?, force_once = ?,
|
on_vacation = ?, on_holiday = ?, force_once = ?,
|
||||||
lockout_secs = ?, changed = NOW()
|
once_per_day = ?, lockout_secs = ?, changed = NOW()
|
||||||
WHERE id = ?");
|
WHERE id = ?");
|
||||||
$stmt->bind_param("ssissiiiiii", $name, $floor, $enabled, $windowFrom, $windowTo,
|
$stmt->bind_param("ssissiiiiiii", $name, $floor, $enabled, $windowFrom, $windowTo,
|
||||||
$weekdays, $onVacation, $onHoliday, $forceOnce, $lockout, $id);
|
$weekdays, $onVacation, $onHoliday, $forceOnce, $oncePerDay,
|
||||||
|
$lockout, $id);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
$db->query("DELETE FROM automation_conditions WHERE automation_id = " . $id);
|
$db->query("DELETE FROM automation_conditions WHERE automation_id = " . $id);
|
||||||
@@ -836,10 +970,12 @@ function saveAutomation($data)
|
|||||||
} else {
|
} else {
|
||||||
$stmt = $db->prepare("INSERT INTO automations
|
$stmt = $db->prepare("INSERT INTO automations
|
||||||
(name, floor, enabled, window_from, window_to, weekdays,
|
(name, floor, enabled, window_from, window_to, weekdays,
|
||||||
on_vacation, on_holiday, force_once, lockout_secs)
|
on_vacation, on_holiday, force_once, once_per_day,
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?)");
|
lockout_secs)
|
||||||
$stmt->bind_param("ssissiiiii", $name, $floor, $enabled, $windowFrom, $windowTo,
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)");
|
||||||
$weekdays, $onVacation, $onHoliday, $forceOnce, $lockout);
|
$stmt->bind_param("ssissiiiiii", $name, $floor, $enabled, $windowFrom, $windowTo,
|
||||||
|
$weekdays, $onVacation, $onHoliday, $forceOnce, $oncePerDay,
|
||||||
|
$lockout);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$id = $db->insert_id;
|
$id = $db->insert_id;
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
@@ -925,9 +1061,186 @@ function normalizeTime($value, $fallback)
|
|||||||
return $fallback;
|
return $fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteAutomation($id)
|
/**
|
||||||
|
* Welcher Messwert des gerechneten Geraets "Automatiken" auf welche Automatik
|
||||||
|
* zeigt: state_id => automation_id.
|
||||||
|
*
|
||||||
|
* Die Zeilen legt der Runner an und fuehrt sie nach (ausloeser_nachfuehren()),
|
||||||
|
* hier werden sie nur gelesen. Gefuehrt wird nach der Kennung in der URL
|
||||||
|
* ("auto:15") und nicht nach dem Namen - wer umbenennt, soll die abhaengigen
|
||||||
|
* Bedingungen behalten.
|
||||||
|
*/
|
||||||
|
function ausloeserStates()
|
||||||
{
|
{
|
||||||
|
static $karte = null;
|
||||||
|
if ($karte !== null) {
|
||||||
|
return $karte;
|
||||||
|
}
|
||||||
|
$karte = [];
|
||||||
$db = meshDb();
|
$db = meshDb();
|
||||||
|
$res = $db->query("SELECT s.id, s.url FROM actor_states s
|
||||||
|
JOIN actors a ON a.id = s.actor_id
|
||||||
|
WHERE a.url = 'Automatik'");
|
||||||
|
while ($row = $res->fetch_assoc()) {
|
||||||
|
if (preg_match('/^auto:(\d+)$/', strval($row["url"]), $t)) {
|
||||||
|
$karte[intval($row["id"])] = intval($t[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $karte;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** automation_id => Liste der Automatiken, die sie ausloesen. */
|
||||||
|
function automationTriggers()
|
||||||
|
{
|
||||||
|
$states = ausloeserStates();
|
||||||
|
if (!$states) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$db = meshDb();
|
||||||
|
$res = $db->query("SELECT automation_id, state_id FROM automation_conditions");
|
||||||
|
$karte = [];
|
||||||
|
while ($row = $res->fetch_assoc()) {
|
||||||
|
$wer = intval($row["automation_id"]);
|
||||||
|
$davor = $states[intval($row["state_id"])] ?? null;
|
||||||
|
if ($davor !== null && $davor !== $wer) {
|
||||||
|
$karte[$wer][$davor] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array_map("array_keys", $karte);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Die Automatiken, die diese hier als Ausloeser benutzen. */
|
||||||
|
function automationFollowers($id)
|
||||||
|
{
|
||||||
|
$id = intval($id);
|
||||||
|
$liste = [];
|
||||||
|
foreach (automationTriggers() as $wer => $davor) {
|
||||||
|
if (in_array($id, $davor, true)) {
|
||||||
|
$liste[] = $wer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$liste) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$db = meshDb();
|
||||||
|
$platz = implode(",", array_fill(0, count($liste), "?"));
|
||||||
|
$stmt = $db->prepare("SELECT id, name, floor, enabled FROM automations
|
||||||
|
WHERE id IN ($platz) ORDER BY name");
|
||||||
|
$stmt->bind_param(str_repeat("i", count($liste)), ...$liste);
|
||||||
|
$stmt->execute();
|
||||||
|
$res = $stmt->get_result();
|
||||||
|
$nachfolger = [];
|
||||||
|
while ($row = $res->fetch_assoc()) {
|
||||||
|
$nachfolger[] = [
|
||||||
|
"id" => intval($row["id"]),
|
||||||
|
"name" => $row["name"],
|
||||||
|
"floor" => $row["floor"],
|
||||||
|
"enabled" => (bool)$row["enabled"],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
return $nachfolger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wuerde diese Automatik sich mittelbar selbst ausloesen?
|
||||||
|
*
|
||||||
|
* Ein Kreis (A loest B loest A) ist kein Fehler, den man im Betrieb bemerkt:
|
||||||
|
* die Sperrzeit begrenzt ihn auf eine Ausloesung je lockout_secs, und im
|
||||||
|
* Protokoll sieht das aus wie eine Automatik, die halt oft laeuft. Deshalb
|
||||||
|
* wird er hier abgelehnt und nicht dort geheilt. Der Runner meldet einen
|
||||||
|
* Kreis trotzdem - falls doch einer an der Datenbank vorbei entsteht.
|
||||||
|
*/
|
||||||
|
function pruefeKreis($id, $conditions)
|
||||||
|
{
|
||||||
|
$id = intval($id);
|
||||||
|
if ($id <= 0) {
|
||||||
|
return; // eine neue Automatik kennt noch niemand
|
||||||
|
}
|
||||||
|
$states = ausloeserStates();
|
||||||
|
$davor = [];
|
||||||
|
foreach ($conditions as $c) {
|
||||||
|
$wer = $states[intval($c["state_id"] ?? 0)] ?? null;
|
||||||
|
if ($wer !== null) {
|
||||||
|
$davor[] = $wer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$davor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (in_array($id, $davor, true)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"Eine Automatik kann sich nicht selbst auslösen.");
|
||||||
|
}
|
||||||
|
$kanten = automationTriggers();
|
||||||
|
$kanten[$id] = $davor;
|
||||||
|
$offen = $davor;
|
||||||
|
$gesehen = [];
|
||||||
|
while ($offen) {
|
||||||
|
$aktuell = array_pop($offen);
|
||||||
|
if ($aktuell === $id) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"Das ergäbe einen Kreis: die Automatik würde sich mittelbar "
|
||||||
|
. "selbst auslösen.");
|
||||||
|
}
|
||||||
|
if (isset($gesehen[$aktuell])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$gesehen[$aktuell] = true;
|
||||||
|
foreach ($kanten[$aktuell] ?? [] as $weiter) {
|
||||||
|
$offen[] = $weiter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loeschen - und was mit den Automatiken geschieht, die davon abhaengen.
|
||||||
|
*
|
||||||
|
* Nichts zu tun waere die schlechteste Wahl: `fk_cond_state` steht auf
|
||||||
|
* ON DELETE CASCADE, mit dem Messwert verschwaende also still die Bedingung
|
||||||
|
* des Nachfolgers. Bei der LETZTEN Bedingung faengt der Runner das ab (eine
|
||||||
|
* Automatik ohne Bedingung loest nie aus), bei einer von zweien niemand: aus
|
||||||
|
* "zehn Minuten nach dem Wecker UND es ist hell" wuerde ein blosses "es ist
|
||||||
|
* hell", und der Rollladen fuehre ab morgen jeden Tag bei Sonnenaufgang hoch.
|
||||||
|
*
|
||||||
|
* abhaengen die Bedingung wird entfernt und der Nachfolger pausiert.
|
||||||
|
* Eine pausierte Automatik mit sichtbarer Luecke ist besser
|
||||||
|
* als eine stille Regelaenderung.
|
||||||
|
* mitloeschen der Nachfolger geht mit, und dessen Nachfolger auch.
|
||||||
|
*/
|
||||||
|
function deleteAutomation($id, $modus = "abhaengen", &$gesehen = [])
|
||||||
|
{
|
||||||
|
$id = intval($id);
|
||||||
|
if (isset($gesehen[$id])) {
|
||||||
|
return; // Schutz gegen einen Kreis, den es nicht geben sollte
|
||||||
|
}
|
||||||
|
$gesehen[$id] = true;
|
||||||
|
|
||||||
|
$db = meshDb();
|
||||||
|
$nachfolger = automationFollowers($id);
|
||||||
|
$eigeneStates = array_keys(ausloeserStates(), $id, true);
|
||||||
|
|
||||||
|
if ($nachfolger && $modus === "mitloeschen") {
|
||||||
|
foreach ($nachfolger as $n) {
|
||||||
|
deleteAutomation($n["id"], "mitloeschen", $gesehen);
|
||||||
|
}
|
||||||
|
} elseif ($nachfolger && $eigeneStates) {
|
||||||
|
$platz = implode(",", array_fill(0, count($eigeneStates), "?"));
|
||||||
|
foreach ($nachfolger as $n) {
|
||||||
|
$args = array_merge([$n["id"]], $eigeneStates);
|
||||||
|
$stmt = $db->prepare("DELETE FROM automation_conditions
|
||||||
|
WHERE automation_id = ? AND state_id IN ($platz)");
|
||||||
|
$stmt->bind_param(str_repeat("i", count($args)), ...$args);
|
||||||
|
$stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
$stmt = $db->prepare("UPDATE automations SET enabled = 0, changed = NOW()
|
||||||
|
WHERE id = ?");
|
||||||
|
$stmt->bind_param("i", $n["id"]);
|
||||||
|
$stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$stmt = $db->prepare("DELETE FROM automations WHERE id = ?");
|
$stmt = $db->prepare("DELETE FROM automations WHERE id = ?");
|
||||||
$stmt->bind_param("i", $id);
|
$stmt->bind_param("i", $id);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
@@ -970,6 +1283,7 @@ function listAutomations($floor)
|
|||||||
$namesState = stateNames();
|
$namesState = stateNames();
|
||||||
$namesCommand = commandNames();
|
$namesCommand = commandNames();
|
||||||
$labelsParam = parameterLabels();
|
$labelsParam = parameterLabels();
|
||||||
|
$kanten = automationTriggers();
|
||||||
|
|
||||||
$list = [];
|
$list = [];
|
||||||
foreach ($ids as $id) {
|
foreach ($ids as $id) {
|
||||||
@@ -979,6 +1293,10 @@ function listAutomations($floor)
|
|||||||
}
|
}
|
||||||
$auto["conditionText"] = describeConditions($auto["conditions"], $namesState);
|
$auto["conditionText"] = describeConditions($auto["conditions"], $namesState);
|
||||||
$auto["actionText"] = describeActions($auto["actions"], $namesCommand, $labelsParam);
|
$auto["actionText"] = describeActions($auto["actions"], $namesCommand, $labelsParam);
|
||||||
|
// Woran diese Automatik haengt - die Uebersicht rueckt Nachfolger
|
||||||
|
// unter ihren Ausloeser ein, statt sie alphabetisch dazwischen zu
|
||||||
|
// streuen.
|
||||||
|
$auto["triggers"] = $kanten[$id] ?? [];
|
||||||
$list[] = $auto;
|
$list[] = $auto;
|
||||||
}
|
}
|
||||||
return $list;
|
return $list;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from modules.mqtt_module import MQTTModule
|
|||||||
from modules.shelly_module import ShellyModule
|
from modules.shelly_module import ShellyModule
|
||||||
from modules.gartenwasser_module import GartenwasserModule
|
from modules.gartenwasser_module import GartenwasserModule
|
||||||
from modules.solarmanager_module import SolarManagerModule
|
from modules.solarmanager_module import SolarManagerModule
|
||||||
|
from modules.wetterstation_module import WetterstationModule
|
||||||
from modules.logic_module import LogicModule #nicht Vergessen neue Module auch unten in die Liste "modules" einzufügen!!
|
from modules.logic_module import LogicModule #nicht Vergessen neue Module auch unten in die Liste "modules" einzufügen!!
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
@@ -166,6 +167,13 @@ class Config:
|
|||||||
return False
|
return False
|
||||||
return self._get_bool('solarmanager', 'enable', False)
|
return self._get_bool('solarmanager', 'enable', False)
|
||||||
|
|
||||||
|
# WETTERSTATION
|
||||||
|
@property
|
||||||
|
def wetterstation_enable(self) -> bool:
|
||||||
|
if not self.config.has_section('wetterstation'):
|
||||||
|
return False
|
||||||
|
return self._get_bool('wetterstation', 'enable', False)
|
||||||
|
|
||||||
|
|
||||||
# MQTT
|
# MQTT
|
||||||
@property
|
@property
|
||||||
@@ -431,7 +439,14 @@ class DatabaseManager:
|
|||||||
-- Wert zufaellig einmal aenderte.
|
-- Wert zufaellig einmal aenderte.
|
||||||
current_value = COALESCE(VALUES(current_value), current_value),
|
current_value = COALESCE(VALUES(current_value), current_value),
|
||||||
possible_values = VALUES(possible_values),
|
possible_values = VALUES(possible_values),
|
||||||
unit = VALUES(unit),
|
-- Dieselbe Ueberlegung wie bei current_value: ein Modul,
|
||||||
|
-- das keine Einheit mitliefert, meint "weiss ich nicht"
|
||||||
|
-- und nicht "loesch die vorhandene". Ohne COALESCE hat
|
||||||
|
-- ein Suchlauf am 10.09.2026 den Sonnensensoren ihr
|
||||||
|
-- "Lux" genommen, weil das Tahoma-Modul keine Einheiten
|
||||||
|
-- kennt - im Automatik-Editor stand danach "877" ohne
|
||||||
|
-- Angabe, wovon.
|
||||||
|
unit = COALESCE(VALUES(unit), unit),
|
||||||
url = VALUES(url),
|
url = VALUES(url),
|
||||||
value_path = VALUES(value_path)
|
value_path = VALUES(value_path)
|
||||||
"""
|
"""
|
||||||
@@ -504,7 +519,14 @@ class DatabaseManager:
|
|||||||
-- Siehe insert_actor: NULL heisst "kein Startwert",
|
-- Siehe insert_actor: NULL heisst "kein Startwert",
|
||||||
-- nicht "Messwert loeschen".
|
-- nicht "Messwert loeschen".
|
||||||
current_value = COALESCE(VALUES(current_value), current_value),
|
current_value = COALESCE(VALUES(current_value), current_value),
|
||||||
unit = VALUES(unit),
|
-- Dieselbe Ueberlegung wie bei current_value: ein Modul,
|
||||||
|
-- das keine Einheit mitliefert, meint "weiss ich nicht"
|
||||||
|
-- und nicht "loesch die vorhandene". Ohne COALESCE hat
|
||||||
|
-- ein Suchlauf am 10.09.2026 den Sonnensensoren ihr
|
||||||
|
-- "Lux" genommen, weil das Tahoma-Modul keine Einheiten
|
||||||
|
-- kennt - im Automatik-Editor stand danach "877" ohne
|
||||||
|
-- Angabe, wovon.
|
||||||
|
unit = COALESCE(VALUES(unit), unit),
|
||||||
url = VALUES(url),
|
url = VALUES(url),
|
||||||
value_path = VALUES(value_path),
|
value_path = VALUES(value_path),
|
||||||
possible_values = VALUES(possible_values)
|
possible_values = VALUES(possible_values)
|
||||||
@@ -589,7 +611,8 @@ def main():
|
|||||||
ShellyModule(config),
|
ShellyModule(config),
|
||||||
LogicModule(config),
|
LogicModule(config),
|
||||||
GartenwasserModule(config),
|
GartenwasserModule(config),
|
||||||
SolarManagerModule(config)
|
SolarManagerModule(config),
|
||||||
|
WetterstationModule(config)
|
||||||
]
|
]
|
||||||
|
|
||||||
# Jedes Modul durchlaufen
|
# Jedes Modul durchlaufen
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Wetterstation Module
|
||||||
|
Die eigene Wetterstation am Haus - fest beschrieben, nicht gesucht
|
||||||
|
KEINE Datenbank-Operationen!
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import List, Dict, Tuple
|
||||||
|
from modules.base_module import BaseModule
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Wurzel aller Topics. wsMQTTbridge.py im SolarManager hängt am Websocket der
|
||||||
|
# Station (ws://192.168.179.42/ws) und legt jedes Feld der Nachricht einzeln
|
||||||
|
# unter diese Wurzel - retained, deshalb hat ein frisch verbundener Leser
|
||||||
|
# sofort alle Werte.
|
||||||
|
#
|
||||||
|
# Zwei Eigenheiten der Brücke, die man kennen muss:
|
||||||
|
#
|
||||||
|
# * Die trägen Werte gehen nicht bei jeder Nachricht raus. Die Brücke wirft
|
||||||
|
# tempAmb, hum, qff, dewpt, iaq, avgWindspeed und avgWindDeg aus jeder
|
||||||
|
# Nachricht heraus außer jeder einundzwanzigsten. Wind und Böe kommen
|
||||||
|
# dagegen immer. Für eine Automatik macht das keinen Unterschied - der
|
||||||
|
# Runner hält den zuletzt gesehenen Wert -, für die Fehlersuche schon:
|
||||||
|
# eine Außentemperatur, die minutenlang steht, ist kein Ausfall.
|
||||||
|
#
|
||||||
|
# * Der Zeitstempel kommt von der Station selbst, nicht vom Broker.
|
||||||
|
WURZEL = "weatherStation"
|
||||||
|
|
||||||
|
# Was von der Station als Messwert im Editor auftaucht.
|
||||||
|
#
|
||||||
|
# Nicht dabei sind zwei Topics. "IP" ist keine Messung. Und "QNH [hPa]" ist
|
||||||
|
# der auf Meereshöhe gerechnete Druck nach Standardatmosphäre - er steht bei
|
||||||
|
# uns gut hundert Hektopascal neben qff (1133 gegen 1023) und ist offenbar
|
||||||
|
# nicht auf die Stationshöhe eingestellt. qff rechnet mit der tatsächlichen
|
||||||
|
# Temperatur und ist der Wert, den auch die Wetterkachel auf der Startseite
|
||||||
|
# zeigt. Zwei Drücke nebeneinander, von denen einer falsch ist, wären im
|
||||||
|
# Editor eine Falle.
|
||||||
|
#
|
||||||
|
# Die Windgeschwindigkeiten stehen in km/h - so beschriftet sie
|
||||||
|
# restricted/weather_cards.html, und so passen die Zahlen zur Historie in
|
||||||
|
# solarLog.weatherStation.
|
||||||
|
#
|
||||||
|
# Windrichtung und Böenrichtung sind Grad im Uhrzeigersinn ab Nord, wie beim
|
||||||
|
# Kompass: 0 = aus Norden, 90 = aus Osten, 180 = aus Süden, 270 = aus Westen.
|
||||||
|
# Eine Bedingung darauf ist unhandlich, weil Nord bei 0 und bei 360 liegt -
|
||||||
|
# für "Wind aus Westen" schreibt man zwei Bedingungen (> 225 und < 315).
|
||||||
|
MESSWERTE = [
|
||||||
|
# (Name im Editor, Topic, Typ, Einheit)
|
||||||
|
("Außentemperatur", "tempAmb", "float", "°C"),
|
||||||
|
("Luftfeuchte", "hum", "float", "%"),
|
||||||
|
("Taupunkt", "dewpt", "float", "°C"),
|
||||||
|
("Luftdruck", "qff", "float", "hPa"),
|
||||||
|
("Wind", "windspeed", "float", "km/h"),
|
||||||
|
("Wind im Mittel", "avgWindspeed", "float", "km/h"),
|
||||||
|
("Böe", "maxgust", "float", "km/h"),
|
||||||
|
("Windrichtung", "windDeg", "float", "°"),
|
||||||
|
("Böenrichtung", "gustDeg", "float", "°"),
|
||||||
|
("Luftgüte", "iaq", "float", None),
|
||||||
|
# Kein Wert zum Vergleichen, sondern zum Nachsehen: im Editor steht er
|
||||||
|
# hinter dem Namen und verrät, ob die Brücke noch läuft. Genauso hält es
|
||||||
|
# der Regen mit seinem "Stand".
|
||||||
|
("Stand", "Timestamp", "string", None),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class WetterstationModule(BaseModule):
|
||||||
|
"""
|
||||||
|
Wetterstation - Implementiert BaseModule Interface
|
||||||
|
|
||||||
|
Wie das Gartenwasser-, das Logic- und das SolarManager-Modul sucht dieses
|
||||||
|
nichts, sondern schreibt ein festes Gerät hin. Die Station meldet sich
|
||||||
|
nicht per Home-Assistant-Discovery; das MQTT-Modul findet sie also nicht.
|
||||||
|
|
||||||
|
Warum sie überhaupt hineingehört: für den Hitzeschutz ist die
|
||||||
|
Außentemperatur die bessere Bedingung als die Raumtemperatur. Wenn es
|
||||||
|
drinnen schon warm ist, ist es zum Verschatten zu spät - die Wärme steht
|
||||||
|
dann bereits im Raum und muss wieder heraus. Draußen sieht man sie
|
||||||
|
kommen. Und Wind gab es hier bisher gar nicht: ohne ihn lässt sich für
|
||||||
|
Markise und Sonnensegel kein Sturmschutz bauen.
|
||||||
|
|
||||||
|
Alles hier ist zum Lesen. Zu schalten gibt es an einer Wetterstation
|
||||||
|
nichts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def is_enabled(self) -> bool:
|
||||||
|
"""Prüft ob das Wetterstation-Modul aktiviert ist"""
|
||||||
|
return self.config.wetterstation_enable
|
||||||
|
|
||||||
|
def discover(self) -> Tuple[List[Dict], List[Dict]]:
|
||||||
|
"""
|
||||||
|
Erzeugt das eine Gerät aus MESSWERTE - als Sensor, ohne Kommandos.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple (actors, sensors)
|
||||||
|
"""
|
||||||
|
logger.info("\n" + "=" * 60)
|
||||||
|
logger.info("WETTERSTATION WIRD ERZEUGT")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
|
||||||
|
states = [{
|
||||||
|
"name": name,
|
||||||
|
"url": WURZEL + "/" + topic,
|
||||||
|
"type": typ,
|
||||||
|
"unit": einheit,
|
||||||
|
} for name, topic, typ, einheit in MESSWERTE]
|
||||||
|
|
||||||
|
geraet = {
|
||||||
|
"type": "Wetter",
|
||||||
|
"name": "Wetterstation",
|
||||||
|
# Die URL eines Geräts ist das Topic seines ersten Messwerts, wie
|
||||||
|
# beim SolarManager. Sie muss nur eindeutig sein und mit mqtt://
|
||||||
|
# beginnen, damit der Runner den MQTT-Transport wählt; abonniert
|
||||||
|
# wird über die Topics der Messwerte.
|
||||||
|
"url": "mqtt://" + states[0]["url"],
|
||||||
|
"states": states,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("1 Gerät mit %d Messwerten", len(states))
|
||||||
|
return [], [geraet]
|
||||||
+211
-162
@@ -208,8 +208,82 @@
|
|||||||
<!-- Alles ab hier ist das Fahrzeug selbst, zusammengefasst
|
<!-- Alles ab hier ist das Fahrzeug selbst, zusammengefasst
|
||||||
in einer Gruppe: nur so kann es als Ganzes schleudern,
|
in einer Gruppe: nur so kann es als Ganzes schleudern,
|
||||||
ohne die Straße mitzudrehen. -->
|
ohne die Straße mitzudrehen. -->
|
||||||
|
<!-- Alles ab hier ist das Fahrzeug selbst, zusammengefasst
|
||||||
|
in einer Gruppe: nur so kann es als Ganzes schleudern,
|
||||||
|
ohne die Straße mitzudrehen.
|
||||||
|
|
||||||
|
Die Malreihenfolge ist die Markup-Reihenfolge, und die
|
||||||
|
ist hier durchgehend als Schichtung gedacht - von unten
|
||||||
|
nach oben:
|
||||||
|
|
||||||
|
1. Fahrwerk Räder, Anhängerkupplung
|
||||||
|
2. Blech Karosserie, Tech-Deck, Haube, Dach,
|
||||||
|
Türen, Reling, Spiegel
|
||||||
|
3. Glas+Licht Scheinwerfer, Rückleuchten, Wind-
|
||||||
|
schutzscheibe, Seitenfenster, Heck-
|
||||||
|
klappe samt Scheibe, darüber der
|
||||||
|
Heckspoiler
|
||||||
|
4. Akku die Antriebsbatterie über allem
|
||||||
|
|
||||||
|
Das ist keine Ansicht von oben, sondern ein Schnitt-
|
||||||
|
bild: unten das, was am Boden läuft, oben das, worum
|
||||||
|
es auf dieser Seite geht. Weil jede Lage nur getönt
|
||||||
|
ist, bleibt darunter alles sichtbar.
|
||||||
|
|
||||||
|
Die beiden Knöpfe (Klima, Ladeanschluss) stehen ganz
|
||||||
|
am Schluss, noch über dem Akku. Sie sind kein Teil des
|
||||||
|
Fahrzeugs, sondern Bedienung, und müssen deckend und
|
||||||
|
anklickbar bleiben. -->
|
||||||
<g id="wagen">
|
<g id="wagen">
|
||||||
|
|
||||||
|
<!-- ===== 1. Fahrwerk =================================== -->
|
||||||
|
<!-- Räder. Vorderachse bei 860 mm, Hinterachse bei 3625 mm
|
||||||
|
ab Bugspitze - der Radstand von 2765 mm maßstäblich.
|
||||||
|
|
||||||
|
Auch der Reifen selbst ist es jetzt: 235/50 R20, das
|
||||||
|
sind 744 mm Durchmesser und 235 mm Breite, also 78 auf
|
||||||
|
25 Einheiten. Die Ecken sind nur leicht gebrochen - von
|
||||||
|
oben ist eine Lauffläche ein Rechteck, keine Kapsel. Gezeichnet waren 58 auf 21 - ein Viertel
|
||||||
|
zu klein. Das Rad ist im Bild der einzige Gegenstand
|
||||||
|
mit bekanntem Mass; ist es zu klein, wirkt alles andere
|
||||||
|
zu gross, allen voran die Schnauze.
|
||||||
|
|
||||||
|
Und sie stehen dort, wo die Spur sie hinstellt: 1585 mm
|
||||||
|
vorn, 1575 mm hinten, macht Radmitten bei x 29 und 195.
|
||||||
|
Der Unterschied zwischen den Achsen ist ein Zentimeter
|
||||||
|
und liegt unter dem, was hier ein Bildpunkt auflöst -
|
||||||
|
beide Achsen stehen deshalb gleich.
|
||||||
|
|
||||||
|
Damit liegen die Reifen INNERHALB des Umrisses, wie am
|
||||||
|
Auto auch: die Karosserie ist 1879 mm breit, Spur plus
|
||||||
|
Reifenbreite sind 1820 mm. Vorher standen sie acht
|
||||||
|
Einheiten über die Flanke hinaus, eine Spur von 1,82 m
|
||||||
|
statt 1,59 m.
|
||||||
|
|
||||||
|
Zu sehen sind sie trotzdem, obwohl sie ganz unten
|
||||||
|
liegen: die Karosserie ist durchscheinend gezeichnet.
|
||||||
|
Von oben saehe man sie in Wirklichkeit gar nicht; hier
|
||||||
|
gilt dieselbe Freiheit wie bei der Akkuplatte, die auch
|
||||||
|
durch den Boden zu sehen ist. -->
|
||||||
|
<g class="rad">
|
||||||
|
<rect x="16.5" y="62" width="25" height="78" rx="5"/>
|
||||||
|
<rect x="182.5" y="62" width="25" height="78" rx="5"/>
|
||||||
|
<rect x="16.5" y="352" width="25" height="78" rx="5"/>
|
||||||
|
<rect x="182.5" y="352" width="25" height="78" rx="5"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Anhängerkupplung: Hals und Kugel, mehr sieht man von
|
||||||
|
oben nicht. Sie steht hinten heraus, so wie am Auto -
|
||||||
|
die Kugel sitzt rund neun Zentimeter hinter der
|
||||||
|
Heckkante. Dass das nicht wie ein Loch im Umriss
|
||||||
|
aussieht, liegt an der Farbe: sie ist schwarz und
|
||||||
|
damit erkennbar ein Anbauteil, kein fehlendes Blech. -->
|
||||||
|
<g class="kupplung">
|
||||||
|
<rect x="109" y="480" width="6" height="19" rx="3"/>
|
||||||
|
<circle cx="112" cy="501" r="4.5"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- ===== 2. Blech ====================================== -->
|
||||||
<!-- Karosserie.
|
<!-- Karosserie.
|
||||||
|
|
||||||
Beide Enden sind stumpf: der Enyaq ist ein SUV, keine
|
Beide Enden sind stumpf: der Enyaq ist ein SUV, keine
|
||||||
@@ -232,40 +306,6 @@
|
|||||||
C23.3 24, 39.1 9, 70.6 6
|
C23.3 24, 39.1 9, 70.6 6
|
||||||
L153.4 6 Z"/>
|
L153.4 6 Z"/>
|
||||||
|
|
||||||
<!-- Räder. Vorderachse bei 860 mm, Hinterachse bei 3625 mm
|
|
||||||
ab Bugspitze - der Radstand von 2765 mm maßstäblich.
|
|
||||||
|
|
||||||
Auch der Reifen selbst ist es jetzt: 235/50 R20, das
|
|
||||||
sind 744 mm Durchmesser und 235 mm Breite, also 78 auf
|
|
||||||
25 Einheiten. Die Ecken sind nur leicht gebrochen - von
|
|
||||||
oben ist eine Lauffläche ein Rechteck, keine Kapsel. Gezeichnet waren 58 auf 21 - ein Viertel
|
|
||||||
zu klein. Das Rad ist im Bild der einzige Gegenstand
|
|
||||||
mit bekanntem Mass; ist es zu klein, wirkt alles andere
|
|
||||||
zu gross, allen voran die Schnauze.
|
|
||||||
|
|
||||||
Und sie stehen dort, wo die Spur sie hinstellt: 1585 mm
|
|
||||||
vorn, 1575 mm hinten, macht Radmitten bei x 29 und 195.
|
|
||||||
Der Unterschied zwischen den Achsen ist ein Zentimeter
|
|
||||||
und liegt unter dem, was hier ein Bildpunkt auflöst -
|
|
||||||
beide Achsen stehen deshalb gleich.
|
|
||||||
|
|
||||||
Damit liegen die Reifen INNERHALB des Umrisses, wie am
|
|
||||||
Auto auch: die Karosserie ist 1879 mm breit, Spur plus
|
|
||||||
Reifenbreite sind 1820 mm. Vorher standen sie acht
|
|
||||||
Einheiten über die Flanke hinaus, eine Spur von 1,82 m
|
|
||||||
statt 1,59 m. Zu sehen sind sie trotzdem: die
|
|
||||||
Karosserie ist durchscheinend gezeichnet - und sie
|
|
||||||
stehen im Markup hinter ihr, sonst schluckt das Blech
|
|
||||||
sie doch. Von oben saehe man sie in Wirklichkeit gar
|
|
||||||
nicht; hier gilt dieselbe Freiheit wie bei der
|
|
||||||
Akkuplatte, die auch durch den Boden zu sehen ist. -->
|
|
||||||
<g class="rad">
|
|
||||||
<rect x="16.5" y="62" width="25" height="78" rx="5"/>
|
|
||||||
<rect x="182.5" y="62" width="25" height="78" rx="5"/>
|
|
||||||
<rect x="16.5" y="352" width="25" height="78" rx="5"/>
|
|
||||||
<rect x="182.5" y="352" width="25" height="78" rx="5"/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- Tech-Deck Face: die glänzend schwarze Blende über die
|
<!-- Tech-Deck Face: die glänzend schwarze Blende über die
|
||||||
volle Bugbreite, die den Enyaq seit der Überarbeitung
|
volle Bugbreite, die den Enyaq seit der Überarbeitung
|
||||||
kennzeichnet. Von oben sieht man ihre Oberkante, also
|
kennzeichnet. Von oben sieht man ihre Oberkante, also
|
||||||
@@ -282,6 +322,85 @@
|
|||||||
C 23 26, 40 13, 70 10 L 154 10 C 184 13, 201 26, 205 52
|
C 23 26, 40 13, 70 10 L 154 10 C 184 13, 201 26, 205 52
|
||||||
C 199 33, 180 21, 154 20 L 70 20 C 44 21, 25 33, 19 52 Z"/>
|
C 199 33, 180 21, 154 20 L 70 20 C 44 21, 25 33, 19 52 Z"/>
|
||||||
|
|
||||||
|
<!-- Motorhaube mit angedeuteter Mittelsicke -->
|
||||||
|
<g id="teil-bonnet" class="teil" tabindex="0" data-teil="bonnet">
|
||||||
|
<path d="M31 146 L193 146 L190 66 C176 48, 48 48, 34 66 Z"/>
|
||||||
|
<title>Motorhaube</title>
|
||||||
|
</g>
|
||||||
|
<path class="sicke" d="M112 62 L112 142"/>
|
||||||
|
|
||||||
|
<!-- Windschutzscheibe, Dach, Heckscheibe.
|
||||||
|
|
||||||
|
Die Stationen stammen aus der Seitenansicht des
|
||||||
|
Fahrzeugs, umgerechnet mit 105 Einheiten je Meter:
|
||||||
|
Scheibenfuss bei 1,37 m (y 150), Dachanfang bei 2,15 m
|
||||||
|
(232), Dachende bei 4,0 m (428). Der Dachanfang liegt
|
||||||
|
damit etwa auf halber Laenge der Vordertuer - genau da,
|
||||||
|
wo die A-Saeule im Foto ans Dach stoesst. Gezeichnet
|
||||||
|
war er vorher am Tuerende.
|
||||||
|
|
||||||
|
Der Scheibenfuss liegt VOR der vorderen Tuerfuge
|
||||||
|
(1,46 m), nicht dahinter: am Auto beginnt die Scheibe
|
||||||
|
schon ueber dem Kotfluegel, die A-Saeule setzt erst
|
||||||
|
weiter hinten an. Damit endet die Haube bei 1,33 m
|
||||||
|
statt bei 1,58 m - die Schnauze wird um einen
|
||||||
|
Vierteilmeter kuerzer, ohne dass sich am Ueberhang
|
||||||
|
etwas aendert.
|
||||||
|
|
||||||
|
Das Dach durfte erst so weit nach hinten, seit
|
||||||
|
Heckscheibe und Heckklappe eine gemeinsame Flaeche
|
||||||
|
sind: als getrennte Schaltflaeche waere die Klappe
|
||||||
|
hier nur noch drei Zentimeter hoch und nicht mehr zu
|
||||||
|
treffen. -->
|
||||||
|
<rect class="dach" x="46" y="232" width="132" height="196" rx="10"/>
|
||||||
|
|
||||||
|
<!-- Kein Panoramadach: dieses Fahrzeug hat ein geschlossenes
|
||||||
|
Blechdach. Die Zustandszeile kennt es deshalb auch nicht
|
||||||
|
(TEILE in js/solar/skodaMQTT.js). -->
|
||||||
|
|
||||||
|
<!-- Dachreling. Sie liegt AUF dem Dach und steht deshalb
|
||||||
|
direkt hinter ihm im Markup.
|
||||||
|
|
||||||
|
Die Holme sitzen ein Stück innerhalb der Dachkanten,
|
||||||
|
wie am Auto. Hinten enden sie vor dem Spoiler, nicht
|
||||||
|
an ihm: am Auto laufen die Holme über dem Radlauf aus,
|
||||||
|
der Spoiler sitzt ein Stück dahinter auf der Dachkante.
|
||||||
|
Der steht weiter unten, bei den Scheiben. -->
|
||||||
|
<g class="reling">
|
||||||
|
<line x1="53" y1="240" x2="53" y2="404"/>
|
||||||
|
<line x1="171" y1="240" x2="171" y2="404"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Türen, jeweils mit Seitenfenster darin.
|
||||||
|
|
||||||
|
Die vordere beginnt dicht hinter dem Vorderrad: ihre
|
||||||
|
Fuge liegt bei 1,52 m, das Rad endet bei 1,24 m - ein
|
||||||
|
knapper Vierteilmeter Blech dazwischen, so wie am Auto.
|
||||||
|
Vorher begann sie erst bei 1,75 m und liess dort eine
|
||||||
|
halbe Handbreit Luft, die es nicht gibt.
|
||||||
|
|
||||||
|
Die Fuge zwischen den Türen liegt bei 2,59 m, gemessen
|
||||||
|
an der B-Säule. Die hintere Tür reicht am Auto noch
|
||||||
|
über den Radlauf hinaus bis etwa 3,5 m; gezeichnet
|
||||||
|
endet sie am Radlauf, denn weiter hinten läge ihr Blech
|
||||||
|
im Bild auf dem Rad. -->
|
||||||
|
<g id="teil-doors" class="teil" data-teil="doors">
|
||||||
|
<g tabindex="0"><rect x="12" y="159" width="23" height="119" rx="6"/><title>Tür vorn links</title></g>
|
||||||
|
<g tabindex="0"><rect x="12" y="280" width="23" height="72" rx="6"/><title>Tür hinten links</title></g>
|
||||||
|
<g tabindex="0"><rect x="189" y="159" width="23" height="119" rx="6"/><title>Tür vorn rechts</title></g>
|
||||||
|
<g tabindex="0"><rect x="189" y="280" width="23" height="72" rx="6"/><title>Tür hinten rechts</title></g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Außenspiegel. Sie sitzen am Fuß der A-Säule, also am
|
||||||
|
vorderen Ende der Tür (1,62 m) und nicht in deren
|
||||||
|
Mitte - von oben ist der Spiegel das erste, was nach
|
||||||
|
der Windschutzscheibe kommt. -->
|
||||||
|
<g class="spiegel">
|
||||||
|
<rect x="-0.7" y="176" width="16" height="10" rx="5"/>
|
||||||
|
<rect x="208.7" y="176" width="16" height="10" rx="5"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- ===== 3. Glas und Licht ============================= -->
|
||||||
<!-- Scheinwerfer, vier Elemente je Seite wie beim Original,
|
<!-- Scheinwerfer, vier Elemente je Seite wie beim Original,
|
||||||
dazu die Rückleuchten als durchgehendes Band. -->
|
dazu die Rückleuchten als durchgehendes Band. -->
|
||||||
<g id="teil-lights" class="teil teil-licht" data-teil="lights">
|
<g id="teil-lights" class="teil teil-licht" data-teil="lights">
|
||||||
@@ -302,45 +421,55 @@
|
|||||||
<title>Beleuchtung</title>
|
<title>Beleuchtung</title>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Motorhaube mit angedeuteter Mittelsicke -->
|
<!-- Windschutzscheibe. Ihre Stationen stehen oben beim
|
||||||
<g id="teil-bonnet" class="teil" tabindex="0" data-teil="bonnet">
|
Dach; hier liegt sie, weil Glas ueber dem Blech kommt.
|
||||||
<path d="M31 172 L193 172 L190 66 C176 48, 48 48, 34 66 Z"/>
|
Sie beginnt bei 1,37 m, also vor der vorderen Tuerfuge
|
||||||
<title>Motorhaube</title>
|
bei 1,46 m, und endet mit dem Dachanfang auf halber
|
||||||
|
Laenge der Vordertuer. -->
|
||||||
|
<path class="scheibe" d="M34 150 L190 150 L178 232 L46 232 Z"/>
|
||||||
|
|
||||||
|
<!-- Seitenfenster. Sie sitzen in den Tueren, liegen im
|
||||||
|
Bild aber ueber ihnen: Glas ist die obere Lage.
|
||||||
|
|
||||||
|
Das vordere reicht bis dicht an die A-Saeule heran
|
||||||
|
(1,66 m) - am Auto beginnt es gleich hinter dem
|
||||||
|
Spiegeldreieck. Gezeichnet war es 16 Einheiten kuerzer
|
||||||
|
und liess vorn ein Stueck Blech stehen, das es nicht
|
||||||
|
gibt. -->
|
||||||
|
<g id="teil-windows" class="teil teil-fenster" data-teil="windows">
|
||||||
|
<rect x="17" y="180" width="13" height="92" rx="4"/>
|
||||||
|
<rect x="17" y="286" width="13" height="60" rx="4"/>
|
||||||
|
<rect x="194" y="180" width="13" height="92" rx="4"/>
|
||||||
|
<rect x="194" y="286" width="13" height="60" rx="4"/>
|
||||||
|
<title>Seitenfenster</title>
|
||||||
</g>
|
</g>
|
||||||
<path class="sicke" d="M112 62 L112 168"/>
|
|
||||||
|
|
||||||
<!-- Windschutzscheibe, Dach, Heckscheibe.
|
<!-- Heckklappe samt Heckscheibe: eine Flaeche, ein Zustand.
|
||||||
|
Die Scheibe sitzt in der Klappe und geht mit ihr auf,
|
||||||
|
also ist sie kein eigenes Teil - sie behaelt nur ihr
|
||||||
|
Aussehen (siehe .teil > .scheibe im Stylesheet).
|
||||||
|
|
||||||
Die Stationen stammen aus der Seitenansicht des
|
Die Scheibe steht am Enyaq steil und wird nach unten
|
||||||
Fahrzeugs, umgerechnet mit 105 Einheiten je Meter:
|
schmaler; lief sie nach hinten breiter aus, las sich
|
||||||
Scheibenfuss bei 1,6 m (y 172), Dachanfang bei 2,15 m
|
das von oben wie ein Fliessheck. -->
|
||||||
(232), Dachende bei 4,0 m (428). Der Dachanfang liegt
|
<g id="teil-trunk" class="teil" tabindex="0" data-teil="trunk">
|
||||||
damit etwa auf halber Laenge der Vordertuer - genau da,
|
<path class="scheibe" d="M46 428 L178 428 L172 460 L52 460 Z"/>
|
||||||
wo die A-Saeule im Foto ans Dach stoesst. Gezeichnet
|
<path d="M52 462 L172 462 C180 470, 184 478, 183 481
|
||||||
war er vorher am Tuerende. Vorher begann das Dach
|
C165 490, 59 490, 41 481 C40 478, 44 470, 52 462 Z"/>
|
||||||
schon bei 1,85 m und endete bei 3,15 m - das Glashaus
|
<title>Heckklappe</title>
|
||||||
sass ein knappes Dreiviertel Meter zu weit vorn und war
|
</g>
|
||||||
zu kurz, und mit ihm Reling und Spoiler.
|
|
||||||
|
|
||||||
Das Dach durfte erst so weit nach hinten, seit
|
<!-- Heckspoiler. Er liegt über der Heckscheibe, nicht
|
||||||
Heckscheibe und Heckklappe eine gemeinsame Flaeche
|
darunter: am Auto sitzt er auf der Dachkante und
|
||||||
sind: als getrennte Schaltflaeche waere die Klappe
|
damit vor dem Glas. Ein Blatt über dieser Kante, in
|
||||||
hier nur noch drei Zentimeter hoch und nicht mehr zu
|
der Mitte breit, zu den Enden schmaler - dort aber
|
||||||
treffen. -->
|
stumpf abgeschnitten, nicht spitz zulaufend. Zwei
|
||||||
<path class="scheibe" d="M34 176 L190 176 L178 232 L46 232 Z"/>
|
Spitzen gaben ihm die Form eines Blattes; am Auto
|
||||||
<rect class="dach" x="46" y="232" width="132" height="196" rx="10"/>
|
hört er schlicht auf. -->
|
||||||
|
<path class="spoiler" d="M42 427 C 74 420, 150 420, 182 427
|
||||||
|
L182 433 C 150 440, 74 440, 42 433 Z"/>
|
||||||
|
|
||||||
<!-- Kein Panoramadach: dieses Fahrzeug hat ein geschlossenes
|
<!-- ===== 4. Antriebsbatterie =========================== -->
|
||||||
Blechdach. Die Zustandszeile kennt es deshalb auch nicht
|
|
||||||
(TEILE in js/solar/skodaMQTT.js). -->
|
|
||||||
|
|
||||||
<!-- Dachreling. Sie liegt AUF dem Dach: nach innen gerückt,
|
|
||||||
nicht auf dessen Seitenkanten, und sie bleibt zwischen
|
|
||||||
den Scheiben - vorn hinter der Windschutzscheibe, hinten
|
|
||||||
am Spoiler. Läge sie bündig auf den Kanten, ergäbe sie
|
|
||||||
mit Platte und Spoiler ein geschlossenes Rechteck, und
|
|
||||||
das sieht aus wie ein Fensterrahmen. Die runden Enden
|
|
||||||
kommen von der Strichstärke, nicht aus der Geometrie. -->
|
|
||||||
<!-- ========================================================
|
<!-- ========================================================
|
||||||
Antriebsbatterie - der Ladestand selbst.
|
Antriebsbatterie - der Ladestand selbst.
|
||||||
|
|
||||||
@@ -350,19 +479,12 @@
|
|||||||
also im Bild von unten nach oben - dieselbe Richtung,
|
also im Bild von unten nach oben - dieselbe Richtung,
|
||||||
in der jeder einen Füllstand liest.
|
in der jeder einen Füllstand liest.
|
||||||
|
|
||||||
Die Reihenfolge ist Absicht und gilt ohne Ausnahme:
|
Sie ist die oberste Lage des Fahrzeugs - siehe die
|
||||||
die Platte liegt ÜBER allem, was nur Blech und Glas ist
|
Schichtung oben am Anfang von #wagen. Das geht nur
|
||||||
- Karosserie, Haube, beide Scheiben, Dach, Reling,
|
auf, weil die Platte über dem Radstand liegt und in
|
||||||
Spoiler -, und UNTER allem, was Zustand zeigt oder
|
x nur von 46 bis 178 reicht: Lichter, Fenster, Türen
|
||||||
bedient wird: Türen, Fenster, Klappen, Licht und die
|
und Heckklappe liegen alle davor, dahinter oder
|
||||||
beiden Knöpfe. Eine offene Klappe muss sichtbar
|
daneben, keines davon verschwindet unter ihr.
|
||||||
bleiben, auch wenn die Batterie darunter liegt - der
|
|
||||||
Zustand hat Vorrang vor der Anzeige.
|
|
||||||
|
|
||||||
Deshalb steht dieser Block hier und nicht weiter oben:
|
|
||||||
stand er vor der Heckscheibe, lag die Platte über der
|
|
||||||
Windschutzscheibe, aber unter der Heckscheibe, und die
|
|
||||||
Relingstäbe schwammen auf ihr.
|
|
||||||
|
|
||||||
Alles Veränderliche - Füllhöhe, Zielmarke, Zahlen und
|
Alles Veränderliche - Füllhöhe, Zielmarke, Zahlen und
|
||||||
Farbe - setzt zeichneAkku().
|
Farbe - setzt zeichneAkku().
|
||||||
@@ -431,81 +553,7 @@
|
|||||||
<title id="akkuTitel">Antriebsbatterie</title>
|
<title id="akkuTitel">Antriebsbatterie</title>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Dachreling und Heckspoiler stehen erst hier, nach der
|
<!-- ===== Bedienung ===================================== -->
|
||||||
Akkuplatte: sie liegen auf dem Dach, die Platte liegt
|
|
||||||
im Fahrzeugboden. Weiter oben im Markup lagen sie unter
|
|
||||||
der halbdurchsichtigen Platte und verblassten darin -
|
|
||||||
von den Relingholmen war fast nichts mehr zu sehen.
|
|
||||||
|
|
||||||
Die Holme sitzen ein Stück innerhalb der Dachkanten,
|
|
||||||
wie am Auto, und laufen zwischen den beiden Scheiben.
|
|
||||||
Der Spoiler ist ein Blatt über der hinteren Dachkante:
|
|
||||||
in der Mitte breit, zu den Enden auslaufend - so sitzt
|
|
||||||
er auf der Kante, statt sie nachzuzeichnen. -->
|
|
||||||
<g class="reling">
|
|
||||||
<line x1="53" y1="240" x2="53" y2="424"/>
|
|
||||||
<line x1="171" y1="240" x2="171" y2="424"/>
|
|
||||||
</g>
|
|
||||||
<path class="spoiler" d="M42 430 C 74 420, 150 420, 182 430
|
|
||||||
C 150 440, 74 440, 42 430 Z"/>
|
|
||||||
|
|
||||||
<!-- Heckklappe samt Heckscheibe: eine Flaeche, ein Zustand.
|
|
||||||
Die Scheibe sitzt in der Klappe und geht mit ihr auf,
|
|
||||||
also ist sie kein eigenes Teil - sie behaelt nur ihr
|
|
||||||
Aussehen (siehe .teil > .scheibe im Stylesheet).
|
|
||||||
|
|
||||||
Die Scheibe steht am Enyaq steil und wird nach unten
|
|
||||||
schmaler; lief sie nach hinten breiter aus, las sich
|
|
||||||
das von oben wie ein Fliessheck. -->
|
|
||||||
<g id="teil-trunk" class="teil" tabindex="0" data-teil="trunk">
|
|
||||||
<path class="scheibe" d="M46 428 L178 428 L172 460 L52 460 Z"/>
|
|
||||||
<path d="M52 462 L172 462 C180 470, 184 478, 183 481
|
|
||||||
C165 490, 59 490, 41 481 C40 478, 44 470, 52 462 Z"/>
|
|
||||||
<title>Heckklappe</title>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- Anhängerkupplung: Hals und Kugel, mehr sieht man von
|
|
||||||
oben nicht. Sie bleibt innerhalb der Heckkante - stand
|
|
||||||
sie darüber hinaus, las sich der Umriss an dieser
|
|
||||||
Stelle wie unterbrochen. -->
|
|
||||||
<g class="kupplung">
|
|
||||||
<rect x="109" y="474" width="6" height="13" rx="3"/>
|
|
||||||
<circle cx="112" cy="489" r="4"/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- Türen, jeweils mit Seitenfenster darin.
|
|
||||||
|
|
||||||
Die vordere beginnt dicht hinter dem Vorderrad: ihre
|
|
||||||
Fuge liegt bei 1,52 m, das Rad endet bei 1,24 m - ein
|
|
||||||
knapper Vierteilmeter Blech dazwischen, so wie am Auto.
|
|
||||||
Vorher begann sie erst bei 1,75 m und liess dort eine
|
|
||||||
halbe Handbreit Luft, die es nicht gibt.
|
|
||||||
|
|
||||||
Die Fuge zwischen den Türen liegt bei 2,59 m, gemessen
|
|
||||||
an der B-Säule. Die hintere Tür reicht am Auto noch
|
|
||||||
über den Radlauf hinaus bis etwa 3,5 m; gezeichnet
|
|
||||||
endet sie am Radlauf, denn weiter hinten läge ihr Blech
|
|
||||||
im Bild auf dem Rad. -->
|
|
||||||
<g id="teil-doors" class="teil" data-teil="doors">
|
|
||||||
<g tabindex="0"><rect x="12" y="159" width="23" height="119" rx="6"/><title>Tür vorn links</title></g>
|
|
||||||
<g tabindex="0"><rect x="12" y="280" width="23" height="72" rx="6"/><title>Tür hinten links</title></g>
|
|
||||||
<g tabindex="0"><rect x="189" y="159" width="23" height="119" rx="6"/><title>Tür vorn rechts</title></g>
|
|
||||||
<g tabindex="0"><rect x="189" y="280" width="23" height="72" rx="6"/><title>Tür hinten rechts</title></g>
|
|
||||||
</g>
|
|
||||||
<g id="teil-windows" class="teil teil-fenster" data-teil="windows">
|
|
||||||
<rect x="17" y="196" width="13" height="76" rx="4"/>
|
|
||||||
<rect x="17" y="286" width="13" height="60" rx="4"/>
|
|
||||||
<rect x="194" y="196" width="13" height="76" rx="4"/>
|
|
||||||
<rect x="194" y="286" width="13" height="60" rx="4"/>
|
|
||||||
<title>Seitenfenster</title>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- Außenspiegel -->
|
|
||||||
<g class="spiegel">
|
|
||||||
<rect x="-0.7" y="196" width="16" height="10" rx="5"/>
|
|
||||||
<rect x="208.7" y="196" width="16" height="10" rx="5"/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- ========================================================
|
<!-- ========================================================
|
||||||
Steuerung: die beiden anklickbaren Stellen.
|
Steuerung: die beiden anklickbaren Stellen.
|
||||||
|
|
||||||
@@ -540,18 +588,19 @@
|
|||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Innenraum, auf der Windschutzscheibe: Klima und Lüftung.
|
<!-- Innenraum, auf der Windschutzscheibe: Klima und Lüftung.
|
||||||
Er sitzt im oberen Drittel der Scheibe, nicht in deren
|
Er steht auf deren Mitte (150 bis 232, also 191).
|
||||||
Mitte: darunter steht der Ladestand, und zwischen Knopf
|
Darunter beginnt bei 246 der Ladestand; die 31
|
||||||
und Zahl braucht es Luft, sonst lesen sich beide als
|
Einheiten dazwischen sind der Grund, warum er nicht
|
||||||
|
tiefer sitzt - sonst lesen sich Knopf und Zahl als
|
||||||
ein Block. -->
|
ein Block. -->
|
||||||
<g id="steuer-klima" class="steuer" tabindex="0"
|
<g id="steuer-klima" class="steuer" tabindex="0"
|
||||||
role="button" data-steuer="klima" aria-label="Klima und Lüftung steuern">
|
role="button" data-steuer="klima" aria-label="Klima und Lüftung steuern">
|
||||||
<circle class="ring" cx="112" cy="206" r="19"/>
|
<circle class="ring" cx="112" cy="196" r="19"/>
|
||||||
<circle class="flaeche" cx="112" cy="206" r="13"/>
|
<circle class="flaeche" cx="112" cy="196" r="13"/>
|
||||||
<!-- Lüfterrad. Es dreht sich, solange die Klimatisierung
|
<!-- Lüfterrad. Es dreht sich, solange die Klimatisierung
|
||||||
läuft; die Drehung sitzt in der inneren Gruppe, damit
|
läuft; die Drehung sitzt in der inneren Gruppe, damit
|
||||||
sie die Verschiebung nach aussen nicht überschreibt. -->
|
sie die Verschiebung nach aussen nicht überschreibt. -->
|
||||||
<g class="luefter" transform="translate(112 206)">
|
<g class="luefter" transform="translate(112 196)">
|
||||||
<g class="blaetter">
|
<g class="blaetter">
|
||||||
<use href="#klimaFluegel"/>
|
<use href="#klimaFluegel"/>
|
||||||
<use href="#klimaFluegel" transform="rotate(120)"/>
|
<use href="#klimaFluegel" transform="rotate(120)"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user