Automatik-Editor als drei Baender, mit laufenden Werten
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 Rahmenbedingungen; die echten standen
im ersten. Jetzt drei sichtbare Baender: Wenn, Wann, Dann, gebaut aus
.card mit farbigem Rand und .card-header.
Die Bedingungszeile magert von fuenf gleich schweren Kaesten auf zwei
ab. Das Geraet steht klein und matt ueber dem Messwert - es beantwortet
"woher", nicht "was" -, der Messwert ist Text mit gepunkteter Linie,
und nur Vergleich und Wert sehen noch wie Eingabefelder aus. Dort
steckt die Aussage der Bedingung, dort tippt man auch. Die Einheit
sitzt im Wertfeld statt in einem sechsten Kasten, der Papierkorb
erscheint erst beim Ueberfahren.
Neben jeder Bedingung laeuft ihr aktueller Wert mit. ?action=werte
liefert alle acht Sekunden {state_id: {value, unit}} aus actor_states -
bewusst von dort und nicht ueber MQTT, weil dort nur ein Teil der
Geraete auftaucht: die Jalousien haengen an der Tahoma-Box, die
gerechneten Werte an gar nichts. Das ersetzt den Satz, der frueher am
Ende stand und die Regel Wort fuer Wort nacherzaehlte: statt zu
wiederholen, was darueber steht, beantwortet die Zeile die Frage, die
man wirklich hat - warum laeuft die Automatik gerade nicht?
Bei time, date, datetime, deltatime und elapsed faellt der Browser
bewusst kein Urteil. Ob "Uhrzeit um 07:30" zutrifft, haengt am
Nachholfenster, am Tagesrand und bei elapsed am echten Abstand seit der
letzten Ausloesung - das steht im Runner. Es hier nachzubauen hiesse,
eine zweite Wahrheit zu pflegen, die auseinanderlaeuft. Diese Zeilen
bekommen einen gestrichelten Punkt statt einer Aussage.
Der Takt malt nur nach, statt neu zu bauen: renderConditions() ersetzt
den ganzen Block, damit waeren alle acht Sekunden eine offene
Geraeteliste weg und der Cursor aus einem Wertfeld, in das man gerade
tippt.
Der Rahmen steht zusammengefaltet da, solange nichts von der Vorgabe
abweicht - und das ist der Normalfall; vorher nahm er die meiste
Flaeche fuer den seltensten Inhalt. Aufgeklappt tauschen die drei
Vorlagen (taeglich, werktags, Wochenende) mit der Kurzfassung den
Platz. Sie stehen ausserhalb des Elements mit data-bs-toggle, und das
ist kein Zufall: Bootstrap haelt seinen Umschalter am Dokument in der
Capture-Phase, ein stopPropagation() im Knopf kaeme grundsaetzlich zu
spaet.
Getragen wird das von vier Variablen in solar.css, damit der Rest der
Seite mitzieht statt hinterherzuhinken: --ton-radius auf .875rem,
--ton-radius-klein neu auf .5rem (beide an Bootstraps
--bs-border-radius und -sm), --font-anzeige auf Poppins fuer alle
Ueberschriften. Poppins lag seit jeher unter assets/fonts und trug
bisher die vier Etagenknoepfe im Aussenplan - eine ganze Schrift fuer
vier Buchstaben. Fliesstext und Zahlenkolonnen bleiben beim Theme, weil
Poppins keine Tabellenziffern mitbringt.
Drei neue Klassen stehen absichtlich in solar.css und nicht im Editor.
.feld-als-text ist ein Auswahlfeld, das wie Text aussieht, bis man es
anfaehrt - dieselbe Not haben das Raum-Modal und die
Kachel-Einstellungen. .punkt ist an oder aus. .wert-jetzt ist die eine
echte Ausnahme und als einzelne Klasse auch als solche erkennbar. Der
Satz im Wann-Band braucht gar keine: .callout gibt es in AdminLTE, und
seine Toene kommen aus Bootstraps *-bg-subtle-Variablen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
+158
-66
@@ -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,66 +324,124 @@ $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">
|
||||
<!--
|
||||
Drei Baender statt eines Akkordeons: Wenn - Wann - Dann.
|
||||
|
||||
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> Alternative (oder)
|
||||
<i class="bi bi-plus-lg"></i> oder stattdessen …
|
||||
</button>
|
||||
<div class="alert alert-secondary mt-3 mb-0 py-2 small" id="condSummary"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-2">
|
||||
<!--
|
||||
Der Rahmen steht zusammengefaltet da, solange er nur Vorgaben enthaelt -
|
||||
und das ist der Normalfall. Frueher nahm er die meiste Flaeche fuer den
|
||||
seltensten Inhalt.
|
||||
-->
|
||||
<!--
|
||||
Zugeklappt liest man hier, was gilt; aufgeklappt greift man zu. Die drei
|
||||
Vorlagen tauschen deshalb mit der Kurzfassung den Platz, statt eine
|
||||
eigene Zeile im Rumpf zu bekommen - das war eine Zeile mehr und eine
|
||||
Ueberschrift ("An welchen Tagen?"), die neben dem Wort "Wann" ohnehin
|
||||
nichts Neues sagte.
|
||||
|
||||
ACHTUNG beim Umbauen: die Vorlagen duerfen NICHT innerhalb des Elements
|
||||
stehen, das data-bs-toggle traegt. Bootstrap haengt seinen Umschalter
|
||||
nicht an dieses Element, sondern ans Dokument - und dort in der
|
||||
Capture-Phase, die vor jedem Klick auf einen Knopf darin laeuft. Ein
|
||||
stopPropagation() im Knopf kommt deshalb grundsaetzlich zu spaet, und
|
||||
jede Vorlage waere zugleich ein Zuklappen. Deshalb traegt nur der linke
|
||||
Teil den Umschalter, die Vorlagen stehen daneben.
|
||||
-->
|
||||
<div class="card-header d-flex flex-wrap align-items-center gap-2 py-2">
|
||||
<div class="d-flex align-items-center gap-2 flex-grow-1" id="rahmenKopf"
|
||||
role="button" data-bs-toggle="collapse" data-bs-target="#rahmenBody"
|
||||
aria-expanded="false">
|
||||
<i class="bi bi-calendar-week text-warning"></i>
|
||||
<span class="card-title mb-0 fs-5 text-warning">Wann</span>
|
||||
<span id="rahmenKurz" class="text-body-secondary"></span>
|
||||
<span class="flex-grow-1"></span>
|
||||
<span class="small text-body-secondary text-nowrap" id="rahmenAendern">ändern <i class="bi bi-chevron-down"></i></span>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm" role="group" id="tagVorlagen" hidden></div>
|
||||
<i class="bi bi-chevron-up text-body-secondary" id="rahmenZu" role="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#rahmenBody" hidden></i>
|
||||
</div>
|
||||
<div id="rahmenBody" class="collapse">
|
||||
<div class="card-body">
|
||||
<div class="btn-group btn-group-sm flex-wrap mb-2" role="group" id="weekdayGroup"></div>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-sm-6 d-flex align-items-center gap-2">
|
||||
<span class="text-body-secondary small" style="min-width:5rem">Ferien</span>
|
||||
<div class="btn-group btn-group-sm" role="group" id="ferienGroup"></div>
|
||||
</div>
|
||||
<div class="col-sm-6 d-flex align-items-center gap-2">
|
||||
<span class="text-body-secondary small" style="min-width:5rem">Feiertage</span>
|
||||
<div class="btn-group btn-group-sm" role="group" id="feiertagGroup"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
.callout ist ein AdminLTE-Bauteil: farbiger Balken links, getoenter
|
||||
Grund, und die Toene kommen aus Bootstraps *-bg-subtle-Variablen -
|
||||
es zieht also mit dem Theme mit, ohne dass hier eine Regel steht.
|
||||
Als .alert stand der Satz vorher in einem eingesunkenen Kasten mit
|
||||
Rahmen ringsum und las sich wie eine Warnung; er ist aber das
|
||||
Ergebnis der Schalter darueber. Die Polsterung stellen Hilfsklassen
|
||||
kleiner - .callout ist auf ganze Absaetze ausgelegt, hier steht ein
|
||||
Satz.
|
||||
-->
|
||||
<div class="callout callout-primary py-2 px-3 mt-0 mb-3" id="rahmenSummary"></div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-sm-6">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text">von</span>
|
||||
<input type="time" class="form-control" id="tSpanFrom" value="00:00">
|
||||
<span class="input-group-text">bis</span>
|
||||
<input type="time" class="form-control" id="tSpanTo" value="23:59">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text">Sperre</span>
|
||||
<select class="form-select" id="lockoutSecs"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div 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">
|
||||
<input type="time" class="form-control" id="tSpanFrom" value="00:00">
|
||||
<label for="tSpanFrom">Von</label>
|
||||
<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-floating">
|
||||
<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">
|
||||
<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>
|
||||
@@ -352,23 +449,18 @@ $nutzdaten = json_encode([
|
||||
</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 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> Aktion
|
||||
<i class="bi bi-plus-lg"></i> und noch etwas tun
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+266
-16
@@ -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")
|
||||
@@ -2335,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;
|
||||
|
||||
+708
-111
File diff suppressed because it is too large
Load Diff
+130
-13
@@ -93,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.
|
||||
*
|
||||
@@ -692,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" => [],
|
||||
@@ -723,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" => [],
|
||||
@@ -810,9 +923,10 @@ function saveAutomation($data)
|
||||
$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);
|
||||
@@ -844,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);
|
||||
@@ -855,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();
|
||||
|
||||
Reference in New Issue
Block a user