diff --git a/README.md b/README.md index c9aab0c..c15fce5 100644 --- a/README.md +++ b/README.md @@ -336,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\|followers`, `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) | @@ -362,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()`, `deleteAutomation()`, `automationFollowers()`, `pruefeKreis()`, `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()` | @@ -526,6 +526,37 @@ 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 @@ -556,8 +587,43 @@ Drei Stellen wissen trotzdem davon: 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, -was beim Pausieren geschieht — steht in +der Rahmen, was beim Pausieren geschieht — steht in [`autoActions/README.md`](../../homes/wagner/SolarManager/autoActions/README.md) im SolarManager-Repo. @@ -565,10 +631,10 @@ im SolarManager-Repo. `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. @@ -592,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. diff --git a/ajax/AutoAction.php b/ajax/AutoAction.php index 4d2c332..ab48a20 100644 --- a/ajax/AutoAction.php +++ b/ajax/AutoAction.php @@ -11,6 +11,7 @@ * GET ?action=list&floor=OG Tabelle fuer die Karte "Automatismen" * POST ?action=save JSON-Rumpf, legt an oder ueberschreibt * 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 */ @@ -72,6 +73,16 @@ 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"]; @@ -166,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 @@ -254,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(), @@ -285,90 +324,143 @@ $nutzdaten = json_encode([ -