Compare commits
6
Commits
| 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.
|
||||
Er bekommt Gerätekatalog und Datensatz in einem Dokument von
|
||||
`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,
|
||||
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 |
|
||||
|---|---|---|
|
||||
| `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 |
|
||||
| `getStats.php` | `GET` | Jahresstatistik, alle Jahre, mit Metadaten je Kennzahl |
|
||||
| `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()` |
|
||||
| `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()` |
|
||||
| `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()` |
|
||||
@@ -524,20 +526,115 @@ das Regelwerk nach `homeMesh`; ausgeführt wird es von
|
||||
`autoActions/autoaction_runner.py` im anderen Repo. **Die Weboberfläche
|
||||
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
|
||||
Gruppen mit ODER: ausgewertet wird `any(all(gruppe))`. Eine Bedingung zeigt
|
||||
auf einen `actor_states`-Eintrag, eine Aktion auf ein `actor_commands` samt
|
||||
Parametern. Deshalb dürfen Zustands-Ids nicht wandern — siehe die
|
||||
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
|
||||
|
||||
`restricted/deviceDiscovery/device_discovery.py` wird **von Hand** gestartet.
|
||||
Die Module suchen je eine Geräteart (Tahoma, WLED, Shelly, MQTT/Home-Assistant-
|
||||
Discovery, Gartenwasser, SolarManager-Werte, Logic); die Datenbanklogik liegt
|
||||
allein im Hauptskript.
|
||||
Discovery, Gartenwasser, SolarManager-Werte, Wetterstation, Logic); die
|
||||
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
|
||||
`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
|
||||
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
|
||||
|
||||
**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
|
||||
wird von Bootstrap (`data-bs-toggle="tab"`), gemerkt wird der offene Reiter von
|
||||
`reiterMerken()` in `common.js`. Weniger als zwei Reiter ergeben keine Leiste.
|
||||
|
||||
+286
-81
@@ -10,7 +10,9 @@
|
||||
* GET ?action=editor&floor=OG Editor fuer eine neue Automatik
|
||||
* GET ?action=list&floor=OG Tabelle fuer die Karte "Automatismen"
|
||||
* 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
|
||||
*/
|
||||
|
||||
@@ -45,7 +47,12 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
$id = saveAutomation($body);
|
||||
jsonAntwort(["id" => $id]);
|
||||
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]);
|
||||
case "toggle":
|
||||
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 --------------------------------------------------
|
||||
if ($action === "list") {
|
||||
$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-md-table-cell'>Wann</th>"
|
||||
. "<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 ($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
|
||||
// haben die Zeile frueher unnoetig hoch gemacht.
|
||||
$aktiveTage = [];
|
||||
@@ -83,16 +177,41 @@ if ($action === "list") {
|
||||
$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") {
|
||||
$wann .= ", " . $a["window_from"] . "–" . $a["window_to"];
|
||||
}
|
||||
if (!$a["on_holiday"]) {
|
||||
if ($a["on_holiday"] == 0) {
|
||||
$wann .= ", nicht an Feiertagen";
|
||||
}
|
||||
if (!$a["on_vacation"]) {
|
||||
if ($a["on_vacation"] == 0) {
|
||||
$wann .= ", nicht in den Ferien";
|
||||
}
|
||||
if ($a["once_per_day"]) {
|
||||
$wann .= ", nur einmal am Tag";
|
||||
}
|
||||
if ($a["lockout_secs"] > 0) {
|
||||
$wann .= ", frühestens alle "
|
||||
. ($a["lockout_secs"] >= 60
|
||||
@@ -101,13 +220,43 @@ if ($action === "list") {
|
||||
}
|
||||
|
||||
$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";
|
||||
$titel = $a["enabled"] ? "Pausieren" : "Fortsetzen";
|
||||
$id = intval($a["id"]);
|
||||
|
||||
echo "<tr" . $klasse . ">";
|
||||
echo "<td>" . htmlspecialchars($a["name"]) . "</td>";
|
||||
$klassen = [];
|
||||
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 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>";
|
||||
@@ -141,6 +290,9 @@ $nutzdaten = json_encode([
|
||||
"automation" => $auto,
|
||||
"devices" => deviceCatalog(),
|
||||
"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
|
||||
// Etage fuer Etage liest sich besser als alphabetisch.
|
||||
"rooms" => raumListe(),
|
||||
@@ -172,90 +324,143 @@ $nutzdaten = json_encode([
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion" id="actionAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#collapseOne" aria-expanded="true">
|
||||
<i class="bi bi-lightning-fill"></i> Auslöser
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseOne" class="accordion-collapse collapse show" data-bs-parent="#actionAccordion">
|
||||
<div class="accordion-body">
|
||||
<!-- Ein Block ist eine UND-Gruppe, zwischen den Bloecken steht ODER.
|
||||
Die Klammerung ist damit gezeichnet und nicht bloss vereinbart. -->
|
||||
<div id="condBlocks"></div>
|
||||
<button class="btn btn-outline-secondary btn-sm mt-2" type="button" id="btnAddGroup">
|
||||
<i class="bi bi-plus-lg"></i> Alternative (oder)
|
||||
</button>
|
||||
<div class="alert alert-secondary mt-3 mb-0 py-2 small" id="condSummary"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
Drei Baender statt eines Akkordeons: Wenn - Wann - Dann.
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#collapseTwo" aria-expanded="false">
|
||||
<i class="bi bi-calendar-check-fill"></i> Bedingungen
|
||||
</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">
|
||||
Das Akkordeon schloss beim Oeffnen die anderen Faecher, Ausloeser und
|
||||
Aktion waren also nie gleichzeitig zu sehen - ausgerechnet bei einer
|
||||
Regel, die genau daraus besteht. Und das mittlere Fach hiess
|
||||
"Bedingungen", enthielt aber die *Rahmen*bedingungen; die echten
|
||||
Bedingungen standen im ersten.
|
||||
|
||||
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.
|
||||
Die Klammerung ist damit gezeichnet und nicht bloss vereinbart. -->
|
||||
<div id="condBlocks"></div>
|
||||
<button class="btn btn-outline-secondary btn-sm mt-2" type="button" id="btnAddGroup">
|
||||
<i class="bi bi-plus-lg"></i> oder stattdessen …
|
||||
</button>
|
||||
</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">
|
||||
<label for="tSpanFrom">Von</label>
|
||||
</div>
|
||||
<div class="form-floating">
|
||||
<span class="input-group-text">bis</span>
|
||||
<input type="time" class="form-control" id="tSpanTo" value="23:59">
|
||||
<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">
|
||||
Am Ende des Zeitraums auf jeden Fall ausführen, falls der Auslöser nie zutraf
|
||||
</label>
|
||||
<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 class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#collapseThree" aria-expanded="false">
|
||||
<i class="bi bi-play-fill"></i> Aktionen
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseThree" class="accordion-collapse collapse" data-bs-parent="#actionAccordion">
|
||||
<div class="accordion-body">
|
||||
<div id="actionList"></div>
|
||||
<button class="btn btn-outline-success btn-sm mt-2" type="button" id="btnAddActor">
|
||||
<i class="bi bi-plus-lg"></i> Aktion
|
||||
</button>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="oncePerDay">
|
||||
<label class="form-check-label" for="oncePerDay">
|
||||
Nur einmal am Tag – danach bis Mitternacht gesperrt
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="runOnce">
|
||||
<label class="form-check-label" for="runOnce">
|
||||
Am Ende des Zeitraums auf jeden Fall ausführen, falls der Auslöser nie zutraf
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-success mb-0">
|
||||
<div class="card-header d-flex flex-wrap align-items-center gap-2 py-2">
|
||||
<i class="bi bi-play-fill text-success"></i>
|
||||
<span class="card-title mb-0 fs-5 text-success">Dann</span>
|
||||
<span class="small text-body-secondary">… passiert das hier</span>
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
<div id="actionList"></div>
|
||||
<button class="btn btn-outline-success btn-sm mt-2" type="button" id="btnAddActor">
|
||||
<i class="bi bi-plus-lg"></i> und noch etwas tun
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+277
-19
@@ -29,7 +29,22 @@
|
||||
--ton-sonne-rgb: 240, 180, 41;
|
||||
--ton-gut: #4caf7d;
|
||||
--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. */
|
||||
--bs-body-bg: var(--ton-grund);
|
||||
@@ -43,11 +58,25 @@
|
||||
--bs-tertiary-bg: var(--ton-flaeche-2);
|
||||
--bs-border-color: var(--ton-linie-satt);
|
||||
--bs-border-radius: var(--ton-radius);
|
||||
--bs-border-radius-sm: var(--ton-radius-klein);
|
||||
--bs-link-color: var(--ton-sonne);
|
||||
--bs-link-hover-color: #ffc94d;
|
||||
--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.
|
||||
AdminLTE setzt sie ueber eigene Klassen, die hier auf denselben Ton
|
||||
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
|
||||
Handy bleiben davon "Therm OG Bad" und "te" uebrig, weil die Felder in
|
||||
der input-group einfach mitschrumpfen. Unter 576 px bekommen deshalb
|
||||
Geraet und Messwert je eine eigene Zeile; Vergleich, Wert und Einheit
|
||||
sind kurz genug, um zusammen in die naechste zu passen. */
|
||||
@media (max-width: 575.98px) {
|
||||
/* 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. */
|
||||
.input-group.auto-zeile > .auto-geraet,
|
||||
.input-group.auto-zeile > .auto-messwert {
|
||||
flex: 1 1 100%;
|
||||
width: auto;
|
||||
/* --------------------------------------------------------------------------
|
||||
Drei Klassen, die der Automatik-Editor gebraucht hat - hier, nicht dort
|
||||
|
||||
Sie stehen absichtlich in der gemeinsamen Datei und nicht als Sonderfall
|
||||
im Editor. Zwei davon sind allgemein genug, um anderswo zu taugen; die
|
||||
dritte ist eine echte Ausnahme und als einzelne Klasse auch als solche
|
||||
erkennbar.
|
||||
-------------------------------------------------------------------------- */
|
||||
|
||||
/* Ein Auswahlfeld, das wie Text aussieht, bis man es anfaehrt.
|
||||
|
||||
Eine Bedingung hat sechs Bestandteile, aber nur zwei davon traegt man
|
||||
wirklich ein: Vergleich und Wert. Standen alle sechs als gleich schwere
|
||||
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")
|
||||
@@ -1280,11 +1530,19 @@ input[type='range']::-webkit-slider-runnable-track {
|
||||
.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); }
|
||||
/* 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 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 -
|
||||
und nur dadurch heben sie sich vom Dach ab, auf dem sie liegen. Vorher
|
||||
hatten sie den Ton des Blechs und verschwanden darin. */
|
||||
@@ -2327,7 +2585,7 @@ input[type='range']::-webkit-slider-runnable-track {
|
||||
}
|
||||
|
||||
.floorBtn text {
|
||||
font: 600 34px Poppins, system-ui, sans-serif;
|
||||
font: 600 34px var(--font-anzeige);
|
||||
fill: #9aa0a6;
|
||||
opacity: .8;
|
||||
transition: fill .25s ease, opacity .25s ease;
|
||||
|
||||
+839
-124
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:
|
||||
* "+ 00:30" eine halbe Stunde nach Sonnenaufgang, "ab - 00:30" ab einer
|
||||
* 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)
|
||||
{
|
||||
@@ -67,6 +72,10 @@ function operatorsForType($type)
|
||||
return ["=", ">=", "<"];
|
||||
case "deltatime": // Sonnenauf-/-untergang: Wert ist ein Versatz
|
||||
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":
|
||||
return ["="]; // JA/NEIN steht im Wertfeld, nicht im Operator
|
||||
default:
|
||||
@@ -84,6 +93,115 @@ function operatorChoices($type)
|
||||
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.
|
||||
*
|
||||
@@ -157,6 +275,7 @@ function inputForType($type, $hasOptions)
|
||||
return "number";
|
||||
case "time":
|
||||
case "deltatime":
|
||||
case "elapsed":
|
||||
return "time";
|
||||
case "date":
|
||||
return "date";
|
||||
@@ -192,6 +311,14 @@ function operatorLabel($op, $type = "")
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -674,9 +801,10 @@ function emptyAutomation($floor)
|
||||
"window_from" => "00:00",
|
||||
"window_to" => "23:59",
|
||||
"weekdays" => 127,
|
||||
"on_vacation" => true,
|
||||
"on_holiday" => true,
|
||||
"on_vacation" => 1,
|
||||
"on_holiday" => 1,
|
||||
"force_once" => false,
|
||||
"once_per_day" => false,
|
||||
"lockout_secs" => 60,
|
||||
"last_run" => null,
|
||||
"conditions" => [],
|
||||
@@ -705,9 +833,12 @@ function loadAutomation($id)
|
||||
"window_from" => substr($row["window_from"], 0, 5),
|
||||
"window_to" => substr($row["window_to"], 0, 5),
|
||||
"weekdays" => intval($row["weekdays"]),
|
||||
"on_vacation" => (bool)$row["on_vacation"],
|
||||
"on_holiday" => (bool)$row["on_holiday"],
|
||||
"on_vacation" => intval($row["on_vacation"]),
|
||||
"on_holiday" => intval($row["on_holiday"]),
|
||||
"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"]),
|
||||
"last_run" => $row["last_run"],
|
||||
"conditions" => [],
|
||||
@@ -785,15 +916,17 @@ function saveAutomation($data)
|
||||
if (!$actions) {
|
||||
throw new InvalidArgumentException("Ohne Aktion haette die Automatik nichts zu tun.");
|
||||
}
|
||||
pruefeKreis($data["id"] ?? 0, $conditions);
|
||||
|
||||
$id = intval($data["id"] ?? 0);
|
||||
$enabled = !empty($data["enabled"]) ? 1 : 0;
|
||||
$windowFrom = normalizeTime($data["window_from"] ?? "00:00", "00:00:00");
|
||||
$windowTo = normalizeTime($data["window_to"] ?? "23:59", "23:59:00");
|
||||
$weekdays = intval($data["weekdays"] ?? 127) & 127;
|
||||
$onVacation = !empty($data["on_vacation"]) ? 1 : 0;
|
||||
$onHoliday = !empty($data["on_holiday"]) ? 1 : 0;
|
||||
$onVacation = kalendertagWert($data["on_vacation"] ?? 1);
|
||||
$onHoliday = kalendertagWert($data["on_holiday"] ?? 1);
|
||||
$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
|
||||
// hier nur aus einer selbstgebauten Anfrage.
|
||||
$lockout = intval($data["lockout_secs"] ?? 60);
|
||||
@@ -825,10 +958,11 @@ function saveAutomation($data)
|
||||
$stmt = $db->prepare("UPDATE automations SET name = ?, floor = ?, enabled = ?,
|
||||
window_from = ?, window_to = ?, weekdays = ?,
|
||||
on_vacation = ?, on_holiday = ?, force_once = ?,
|
||||
lockout_secs = ?, changed = NOW()
|
||||
once_per_day = ?, lockout_secs = ?, changed = NOW()
|
||||
WHERE id = ?");
|
||||
$stmt->bind_param("ssissiiiiii", $name, $floor, $enabled, $windowFrom, $windowTo,
|
||||
$weekdays, $onVacation, $onHoliday, $forceOnce, $lockout, $id);
|
||||
$stmt->bind_param("ssissiiiiiii", $name, $floor, $enabled, $windowFrom, $windowTo,
|
||||
$weekdays, $onVacation, $onHoliday, $forceOnce, $oncePerDay,
|
||||
$lockout, $id);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
$db->query("DELETE FROM automation_conditions WHERE automation_id = " . $id);
|
||||
@@ -836,10 +970,12 @@ function saveAutomation($data)
|
||||
} else {
|
||||
$stmt = $db->prepare("INSERT INTO automations
|
||||
(name, floor, enabled, window_from, window_to, weekdays,
|
||||
on_vacation, on_holiday, force_once, lockout_secs)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)");
|
||||
$stmt->bind_param("ssissiiiii", $name, $floor, $enabled, $windowFrom, $windowTo,
|
||||
$weekdays, $onVacation, $onHoliday, $forceOnce, $lockout);
|
||||
on_vacation, on_holiday, force_once, once_per_day,
|
||||
lockout_secs)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)");
|
||||
$stmt->bind_param("ssissiiiiii", $name, $floor, $enabled, $windowFrom, $windowTo,
|
||||
$weekdays, $onVacation, $onHoliday, $forceOnce, $oncePerDay,
|
||||
$lockout);
|
||||
$stmt->execute();
|
||||
$id = $db->insert_id;
|
||||
$stmt->close();
|
||||
@@ -925,9 +1061,186 @@ function normalizeTime($value, $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();
|
||||
$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->bind_param("i", $id);
|
||||
$stmt->execute();
|
||||
@@ -970,6 +1283,7 @@ function listAutomations($floor)
|
||||
$namesState = stateNames();
|
||||
$namesCommand = commandNames();
|
||||
$labelsParam = parameterLabels();
|
||||
$kanten = automationTriggers();
|
||||
|
||||
$list = [];
|
||||
foreach ($ids as $id) {
|
||||
@@ -979,6 +1293,10 @@ function listAutomations($floor)
|
||||
}
|
||||
$auto["conditionText"] = describeConditions($auto["conditions"], $namesState);
|
||||
$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;
|
||||
}
|
||||
return $list;
|
||||
|
||||
@@ -21,6 +21,7 @@ from modules.mqtt_module import MQTTModule
|
||||
from modules.shelly_module import ShellyModule
|
||||
from modules.gartenwasser_module import GartenwasserModule
|
||||
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!!
|
||||
|
||||
# Logging
|
||||
@@ -166,6 +167,13 @@ class Config:
|
||||
return 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
|
||||
@property
|
||||
@@ -431,7 +439,14 @@ class DatabaseManager:
|
||||
-- Wert zufaellig einmal aenderte.
|
||||
current_value = COALESCE(VALUES(current_value), current_value),
|
||||
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),
|
||||
value_path = VALUES(value_path)
|
||||
"""
|
||||
@@ -504,7 +519,14 @@ class DatabaseManager:
|
||||
-- Siehe insert_actor: NULL heisst "kein Startwert",
|
||||
-- nicht "Messwert loeschen".
|
||||
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),
|
||||
value_path = VALUES(value_path),
|
||||
possible_values = VALUES(possible_values)
|
||||
@@ -589,7 +611,8 @@ def main():
|
||||
ShellyModule(config),
|
||||
LogicModule(config),
|
||||
GartenwasserModule(config),
|
||||
SolarManagerModule(config)
|
||||
SolarManagerModule(config),
|
||||
WetterstationModule(config)
|
||||
]
|
||||
|
||||
# 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
|
||||
in einer Gruppe: nur so kann es als Ganzes schleudern,
|
||||
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">
|
||||
|
||||
<!-- ===== 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.
|
||||
|
||||
Beide Enden sind stumpf: der Enyaq ist ein SUV, keine
|
||||
@@ -232,40 +306,6 @@
|
||||
C23.3 24, 39.1 9, 70.6 6
|
||||
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
|
||||
volle Bugbreite, die den Enyaq seit der Überarbeitung
|
||||
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 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,
|
||||
dazu die Rückleuchten als durchgehendes Band. -->
|
||||
<g id="teil-lights" class="teil teil-licht" data-teil="lights">
|
||||
@@ -302,45 +421,55 @@
|
||||
<title>Beleuchtung</title>
|
||||
</g>
|
||||
|
||||
<!-- Motorhaube mit angedeuteter Mittelsicke -->
|
||||
<g id="teil-bonnet" class="teil" tabindex="0" data-teil="bonnet">
|
||||
<path d="M31 172 L193 172 L190 66 C176 48, 48 48, 34 66 Z"/>
|
||||
<title>Motorhaube</title>
|
||||
<!-- Windschutzscheibe. Ihre Stationen stehen oben beim
|
||||
Dach; hier liegt sie, weil Glas ueber dem Blech kommt.
|
||||
Sie beginnt bei 1,37 m, also vor der vorderen Tuerfuge
|
||||
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>
|
||||
<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
|
||||
Fahrzeugs, umgerechnet mit 105 Einheiten je Meter:
|
||||
Scheibenfuss bei 1,6 m (y 172), 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. Vorher begann das Dach
|
||||
schon bei 1,85 m und endete bei 3,15 m - das Glashaus
|
||||
sass ein knappes Dreiviertel Meter zu weit vorn und war
|
||||
zu kurz, und mit ihm Reling und Spoiler.
|
||||
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>
|
||||
|
||||
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. -->
|
||||
<path class="scheibe" d="M34 176 L190 176 L178 232 L46 232 Z"/>
|
||||
<rect class="dach" x="46" y="232" width="132" height="196" rx="10"/>
|
||||
<!-- Heckspoiler. Er liegt über der Heckscheibe, nicht
|
||||
darunter: am Auto sitzt er auf der Dachkante und
|
||||
damit vor dem Glas. Ein Blatt über dieser Kante, in
|
||||
der Mitte breit, zu den Enden schmaler - dort aber
|
||||
stumpf abgeschnitten, nicht spitz zulaufend. Zwei
|
||||
Spitzen gaben ihm die Form eines Blattes; am Auto
|
||||
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
|
||||
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. -->
|
||||
<!-- ===== 4. Antriebsbatterie =========================== -->
|
||||
<!-- ========================================================
|
||||
Antriebsbatterie - der Ladestand selbst.
|
||||
|
||||
@@ -350,19 +479,12 @@
|
||||
also im Bild von unten nach oben - dieselbe Richtung,
|
||||
in der jeder einen Füllstand liest.
|
||||
|
||||
Die Reihenfolge ist Absicht und gilt ohne Ausnahme:
|
||||
die Platte liegt ÜBER allem, was nur Blech und Glas ist
|
||||
- Karosserie, Haube, beide Scheiben, Dach, Reling,
|
||||
Spoiler -, und UNTER allem, was Zustand zeigt oder
|
||||
bedient wird: Türen, Fenster, Klappen, Licht und die
|
||||
beiden Knöpfe. Eine offene Klappe muss sichtbar
|
||||
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.
|
||||
Sie ist die oberste Lage des Fahrzeugs - siehe die
|
||||
Schichtung oben am Anfang von #wagen. Das geht nur
|
||||
auf, weil die Platte über dem Radstand liegt und in
|
||||
x nur von 46 bis 178 reicht: Lichter, Fenster, Türen
|
||||
und Heckklappe liegen alle davor, dahinter oder
|
||||
daneben, keines davon verschwindet unter ihr.
|
||||
|
||||
Alles Veränderliche - Füllhöhe, Zielmarke, Zahlen und
|
||||
Farbe - setzt zeichneAkku().
|
||||
@@ -431,81 +553,7 @@
|
||||
<title id="akkuTitel">Antriebsbatterie</title>
|
||||
</g>
|
||||
|
||||
<!-- Dachreling und Heckspoiler stehen erst hier, nach der
|
||||
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>
|
||||
|
||||
<!-- ===== Bedienung ===================================== -->
|
||||
<!-- ========================================================
|
||||
Steuerung: die beiden anklickbaren Stellen.
|
||||
|
||||
@@ -540,18 +588,19 @@
|
||||
</g>
|
||||
|
||||
<!-- Innenraum, auf der Windschutzscheibe: Klima und Lüftung.
|
||||
Er sitzt im oberen Drittel der Scheibe, nicht in deren
|
||||
Mitte: darunter steht der Ladestand, und zwischen Knopf
|
||||
und Zahl braucht es Luft, sonst lesen sich beide als
|
||||
Er steht auf deren Mitte (150 bis 232, also 191).
|
||||
Darunter beginnt bei 246 der Ladestand; die 31
|
||||
Einheiten dazwischen sind der Grund, warum er nicht
|
||||
tiefer sitzt - sonst lesen sich Knopf und Zahl als
|
||||
ein Block. -->
|
||||
<g id="steuer-klima" class="steuer" tabindex="0"
|
||||
role="button" data-steuer="klima" aria-label="Klima und Lüftung steuern">
|
||||
<circle class="ring" cx="112" cy="206" r="19"/>
|
||||
<circle class="flaeche" cx="112" cy="206" r="13"/>
|
||||
<circle class="ring" cx="112" cy="196" r="19"/>
|
||||
<circle class="flaeche" cx="112" cy="196" r="13"/>
|
||||
<!-- Lüfterrad. Es dreht sich, solange die Klimatisierung
|
||||
läuft; die Drehung sitzt in der inneren Gruppe, damit
|
||||
sie die Verschiebung nach aussen nicht überschreibt. -->
|
||||
<g class="luefter" transform="translate(112 206)">
|
||||
<g class="luefter" transform="translate(112 196)">
|
||||
<g class="blaetter">
|
||||
<use href="#klimaFluegel"/>
|
||||
<use href="#klimaFluegel" transform="rotate(120)"/>
|
||||
|
||||
Reference in New Issue
Block a user