Heizstab- und Bewaesserungsfenster im neuen Look, Bewaesserungszeilen enger

Beide Fenster lesen den Zustand live aus MQTT und uebernehmen jede Wahl
sofort (bedienfenster.js). submitFormAjax entfaellt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 21:26:16 +02:00
co-authored by Claude Opus 5
parent bf5fdfefa4
commit 6e805678d4
9 changed files with 466 additions and 311 deletions
+2 -3
View File
@@ -183,7 +183,7 @@ genau die Topics, die auf den Kacheln stehen (`tileTopics()` in `rooms.php`).
| Seite (`?action=`) | Vorlage | Seitenskripte | holt per fetch | hört auf MQTT | | Seite (`?action=`) | Vorlage | Seitenskripte | holt per fetch | hört auf MQTT |
|---|---|---|---|---| |---|---|---|---|---|
| `solar` (Vorgabe) | `solar.php` | `jahresstatistik.js`, `anlageKatalog.js`, `anlage.js`, `speicher.js`, `energieflussKatalog.js`, `energiefluss.js`, `bewaesserungAnzeige.js`, `ladefenster.js`, `solarMQTT.js` | `speicher`, `getProdData`, `getConsData`, `getForecastData`, `getSunrise`, `getStats`, `carEG`, `carOG`, `heater`, `watering` | `solarManager/#`, `weatherStation/#`, `wattpilot/#`, `go-eCharger/#`, `Gartenwasser/#` | | `solar` (Vorgabe) | `solar.php` | `jahresstatistik.js`, `anlageKatalog.js`, `anlage.js`, `speicher.js`, `energieflussKatalog.js`, `energiefluss.js`, `bewaesserungAnzeige.js`, `ladefenster.js`, `bedienfenster.js`, `solarMQTT.js` | `speicher`, `getProdData`, `getConsData`, `getForecastData`, `getSunrise`, `getStats`, `carEG`, `carOG`, `heater`, `watering` | `solarManager/#`, `weatherStation/#`, `wattpilot/#`, `go-eCharger/#`, `Gartenwasser/#` |
| `home` | `home.php` | `autoActionFuncs.js`, `zeitleiste.js`, `homeMQTT.js` | `room.php`, `AutoAction.php` | die Topics der Kacheln, `Raumtemp/#` | | `home` | `home.php` | `autoActionFuncs.js`, `zeitleiste.js`, `homeMQTT.js` | `room.php`, `AutoAction.php` | die Topics der Kacheln, `Raumtemp/#` |
| `heat` | `heat.php` | `heatMQTT.js` | `getHeaterData`, `getWaterData`, `getSunrise` | `solarManager/#`, `weatherStation/#`, Wallbox-Topics | | `heat` | `heat.php` | `heatMQTT.js` | `getHeaterData`, `getWaterData`, `getSunrise` | `solarManager/#`, `weatherStation/#`, Wallbox-Topics |
| `history` | `history.php` | `jahresstatistik.js`, `historyMQTT.js` | `energyHistory` (6 ×), `getStats` | — | | `history` | `history.php` | `jahresstatistik.js`, `historyMQTT.js` | `energyHistory` (6 ×), `getStats` | — |
@@ -296,7 +296,6 @@ eine Bibliothek es vorgibt.
|---|---| |---|---|
| `powerToString(w)` | Leistung als `W` oder `kW`, dieselbe Rundung überall | | `powerToString(w)` | Leistung als `W` oder `kW`, dieselbe Rundung überall |
| `getData(chart, url, opt)` | Diagrammdaten holen, eintragen, alle 5 min auffrischen; optional Sonnenauf-/-untergang aus `getSunrise.php` dazu | | `getData(chart, url, opt)` | Diagrammdaten holen, eintragen, alle 5 min auffrischen; optional Sonnenauf-/-untergang aus `getSunrise.php` dazu |
| `submitFormAjax(form)` | Formular abschicken, ohne die Seite zu verlassen |
| `bindModalSlider()` | Schieberegler im Modal an ihre Anzeige koppeln | | `bindModalSlider()` | Schieberegler im Modal an ihre Anzeige koppeln |
| `mountMeteogram()`, `updateWeatherCards()` | Wetterteile der Solar- und Wetterseite | | `mountMeteogram()`, `updateWeatherCards()` | Wetterteile der Solar- und Wetterseite |
| `reiterMerken()` | offener Reiter steht in der Adresse und übersteht ein Neuladen | | `reiterMerken()` | offener Reiter steht in der Adresse und übersteht ein Neuladen |
@@ -433,7 +432,7 @@ klein: Diagramme füllen bzw. Logzeilen nachladen und einfärben.
| `skodaCmd.php` | `POST befehl=…` | Laden/Klima/Lüftung am Fahrzeug | | `skodaCmd.php` | `POST befehl=…` | Laden/Klima/Lüftung am Fahrzeug |
| `tile.php` | `?z=&x=&y=` | Kartenkachel aus dem eigenen Zwischenspeicher, sonst einmalig von OSM | | `tile.php` | `?z=&x=&y=` | Kartenkachel aus dem eigenen Zwischenspeicher, sonst einmalig von OSM |
| `logs.php` | `?action=list\|read` | Zustand und Zeilen der Logdateien | | `logs.php` | `?action=list\|read` | Zustand und Zeilen der Logdateien |
| `carEG.php` `carOG.php` `carSteuerung.php` `carForm.php` `heater.php` `watering.php` `tahoma.php` | | Wallboxen, Heizstab, Bewässerung, Tahoma-Bedienung. Das Ladefenster (`carForm.php` + `js/solar/ladefenster.js`) liest live aus MQTT und schreibt jede Änderung sofort als JSON an `carSteuerung.php` (Aktionen `frc`, `modus`, `strom`, `plan`) | | `carEG.php` `carOG.php` `carSteuerung.php` `carForm.php` `heater.php` `watering.php` `tahoma.php` | | Wallboxen, Heizstab, Bewässerung, Tahoma-Bedienung. Das Ladefenster (`carForm.php` + `js/solar/ladefenster.js`) liest live aus MQTT und schreibt jede Änderung sofort als JSON an `carSteuerung.php` (Aktionen `frc`, `modus`, `strom`, `plan`). Heizstab- und Bewässerungsfenster (`heater.php`, `watering.php` + `js/solar/bedienfenster.js`) arbeiten genauso: Zustand live aus MQTT, jede Wahl gilt sofort |
| `chartData.php` | (kein Endpunkt) | gemeinsame Bausteine der Chart-Endpunkte | | `chartData.php` | (kein Endpunkt) | gemeinsame Bausteine der Chart-Endpunkte |
| `phpMQTT.php` | (Bibliothek) | MQTT aus PHP heraus, für Kommandos | | `phpMQTT.php` | (Bibliothek) | MQTT aus PHP heraus, für Kommandos |
+80 -38
View File
@@ -1,33 +1,36 @@
<?php <?php
/**
* Heizstab im Pufferspeicher.
*
* GET das Bedienfenster (Werte und Logik: js/solar/bedienfenster.js)
* POST setMode=0 Automatik: der SolarManager regelt nach Überschuss
* POST setMode=1..60 feste Leistung in 100 W (60 = 6,0 kW)
*
* Seit September 2026 gilt jede Änderung sofort, ohne "Save changes". Die
* Antwort ist leer, wenn der Befehl beim Heizstab ankam, sonst steht dort
* eine Meldung für das Fenster.
*/
require_once("../helper.php"); require_once("../helper.php");
if (checkLogin()) {
$close = 0;
$hostname = "192.168.179.169"; $hostname = "192.168.179.169";
if (!checkLogin()) {
exit;
}
if (isset($_POST["setMode"]) || isset($_GET["setMode"])) { if (isset($_POST["setMode"]) || isset($_GET["setMode"])) {
/* // Der Wert kommt aus dem Fenster (POST) oder von Hand aus der Adresse
* Der Wert kommt aus dem Formular (POST) oder von Hand aus der // (GET) - immer nur einer von beiden.
* Adresse (GET) - immer nur einer von beiden. Frueher wurden beide
* addiert; der fehlende erzeugte eine PHP-Warnung, und die stand dann
* als Meldung im Browser.
*
* 0 = Automatik (Ueberschuss), 1..60 = feste Leistung in 100 W. Der
* Regler reicht bis 60; die alte Pruefung "< 60" liess genau 6,0 kW
* still nicht durch.
*/
$stufe = intval($_POST["setMode"] ?? $_GET["setMode"] ?? 0); $stufe = intval($_POST["setMode"] ?? $_GET["setMode"] ?? 0);
if ($stufe >= 1 && $stufe <= 60) { if ($stufe >= 1 && $stufe <= 60) {
$pfad = "/set?mode=man&pwr=" . ($stufe * 100); $pfad = "/set?mode=man&pwr=" . ($stufe * 100);
} elseif ($stufe === 0) { } elseif ($stufe === 0) {
$pfad = "/set?mode=eco&pwr=0"; $pfad = "/set?mode=eco&pwr=0";
} else { } else {
$pfad = null; echo "Unbekannte Leistungsstufe.";
exit;
} }
if ($pfad !== null) {
$remote = @fsockopen("tcp://" . $hostname, 80, $errno, $errstr, 5); $remote = @fsockopen("tcp://" . $hostname, 80, $errno, $errstr, 5);
if ($remote) { if ($remote) {
fwrite($remote, "GET " . $pfad . " HTTP/1.1\r\nHost: " . $hostname . "\r\nConnection: close\r\n\r\n"); fwrite($remote, "GET " . $pfad . " HTTP/1.1\r\nHost: " . $hostname . "\r\nConnection: close\r\n\r\n");
@@ -35,32 +38,71 @@ if (checkLogin()) {
fread($remote, 1024); fread($remote, 1024);
fclose($remote); fclose($remote);
} else { } else {
// Ohne Verbindung eine verstaendliche Meldung statt eines PHP-Fehlers.
// Bewusst Status 200: submitFormAjax() in common.js zeigt die
// Antwort nur dann als Meldung an.
echo "Der Heizstab ist nicht erreichbar."; echo "Der Heizstab ist nicht erreichbar.";
} }
exit;
} }
$close = 1; ?>
} <div class="lade heiz">
} else { <div class="lade-links">
$close = 1; <div class="lade-status">
} <div class="heiz-tank" aria-hidden="true">
if (!$close) { <svg viewBox="0 0 80 120">
echo <<<ENDE <defs><clipPath id="heiz-innen"><rect x="14" y="8" width="52" height="100" rx="20"/></clipPath></defs>
<!--begin::Form--> <g clip-path="url(#heiz-innen)">
<form id="heater_form"> <rect x="0" y="0" width="80" height="42" data-heiz="t0"/>
<div class="col-sm-11"> <rect x="0" y="42" width="80" height="34" data-heiz="t1"/>
<div class="form-check"> <rect x="0" y="76" width="80" height="44" data-heiz="t2"/>
<label class="form-label" for="addedCharge">Leistung: </label> </g>
<div style="font-size: 15px;color: #297195;display: inline-block;" id="modal-slider-label">0</div> <rect x="14" y="8" width="52" height="100" rx="20" class="heiz-huelle"/>
<input type="range" id="modal-slider" name="setMode" class="form-range range-color-track" min="0" max="60" /> <path d="M26 103l4-6 4 6 4-6 4 6 4-6 4 6 4-6" class="heiz-spirale" data-heiz="spirale"/>
<div class="d-flex justify-content-between"> <path d="M26 108v7M54 108v7" class="heiz-huelle"/>
<span class="max-amount f-16 font-weight-normal darkGray-color text-right mt-1">Auto</span> <text x="40" y="30" data-heiz="tt0"></text>
<span class="max-amount f-16 font-weight-normal darkGray-color text-right mt-1">6.0 kW</span> <text x="40" y="63" data-heiz="tt1"></text>
<text x="40" y="90" data-heiz="tt2" class="klein"></text>
</svg>
</div>
<div class="lade-text">
<div class="lade-auto">Pufferspeicher <span data-heiz="pille" class="lade-pille"></span></div>
<div class="lade-gross" data-heiz="leistung"></div>
<div class="lade-zeile" data-heiz="temps"></div>
</div> </div>
</div> </div>
<div class="lade-erklaerung" data-heiz="erklaerung"></div>
</div>
<div class="lade-rechts">
<div class="lade-abschnitt">
<h3 id="heiz-modus">Wie heizen?</h3>
<div class="lade-modi zwei" role="radiogroup" aria-labelledby="heiz-modus">
<button type="button" class="lade-modus" role="radio" aria-checked="false" data-heiz-modus="auto" style="--c:#f7c000">
<span class="lade-sym"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg></span>
<b>Automatik</b><span>heizt mit dem Überschuss der Anlage</span>
</button>
<button type="button" class="lade-modus" role="radio" aria-checked="false" data-heiz-modus="fest" style="--c:#e5624f">
<span class="lade-sym"><svg viewBox="0 0 24 24"><path d="M12 3c3 4 5 6.5 5 10a5 5 0 0 1-10 0c0-2 1-3.5 2-4.5.3 1.5 1 2.5 2 3 0-3 .5-5.5 1-8.5z"/></svg></span>
<b>Fest</b><span>eingestellte Leistung, auch aus dem Netz</span>
</button>
</div>
</div>
<div class="lade-planung" data-heiz="fest" hidden>
<div>
<div class="lade-plan-zeile">
<label for="heiz-regler">Leistung</label>
<output data-heiz="reglerAusgabe"></output>
</div>
<input type="range" id="heiz-regler" data-heiz="regler" min="1" max="60" step="1" value="30" class="heiz-regler">
<div class="lade-marken"><span>0,1kW</span><span>3kW</span><span>6kW</span></div>
</div>
<div class="lade-chips" role="group" aria-label="Schnellwahl">
<button type="button" class="lade-chip" aria-pressed="false" data-heiz-stufe="10">1kW</button>
<button type="button" class="lade-chip" aria-pressed="false" data-heiz-stufe="20">2kW</button>
<button type="button" class="lade-chip" aria-pressed="false" data-heiz-stufe="30">3kW</button>
<button type="button" class="lade-chip" aria-pressed="false" data-heiz-stufe="60">6kW</button>
</div>
</div>
<div class="lade-meldung" data-heiz="meldung" aria-live="polite">Änderungen gelten sofort</div>
</div>
</div> </div>
</form>
ENDE;
}
+51 -53
View File
@@ -73,64 +73,62 @@ if (!checkLogin()) {
exit; exit;
} }
// Zonen des Modals. Die Reihenfolge bestimmt die Reihenfolge der Karten, die // Zonen des Fensters. Die Reihenfolge bestimmt die Reihenfolge der Karten;
// Keys muessen zu wateringZones in js/solar/solarMQTT.js passen. // die Keys muessen zu wateringZones in js/solar/bewaesserungAnzeige.js passen,
// "bild" zu ZONENBILDER in js/solar/energiefluss.js. "dauern" sind die
// Schnellwahlen in Minuten, die erste ist die Vorgabe.
$zonen = [ $zonen = [
["key" => "troughs", "label" => "Tröge", "minuten" => 5], ["key" => "troughs", "label" => "Tröge", "bild" => "kuebel", "dauern" => [5, 10, 15, 30]],
["key" => "raisedBeds", "label" => "Beete", "minuten" => 30], ["key" => "raisedBeds", "label" => "Beete", "bild" => "hochbeet", "dauern" => [30, 15, 45, 60]],
["key" => "frontGarden", "label" => "Vorn", "minuten" => 30], ["key" => "frontGarden", "label" => "Vorn", "bild" => "garten", "dauern" => [30, 15, 45, 60]],
]; ];
echo <<<HTML // Die Abläufe der Ventilsteuerung (Topic .../auto). Mehrere Einträge in
<div class="p-3"> // "modi" starten bzw. stoppen gemeinsam.
<div id="watering-feedback" class="alert d-none mb-3" role="status"></div> $ablaeufe = [
["label" => "Tröge + Beete", "modi" => ["Gartenwasser/vorn" => "Trog", "Gartenwasser/hinten" => "Hoch"]],
["label" => "Tröge", "modi" => ["Gartenwasser/vorn" => "Trog"]],
["label" => "Beete", "modi" => ["Gartenwasser/hinten" => "Hoch"]],
["label" => "Vorn", "modi" => ["Gartenwasser/vorn" => "Vorn"]],
];
?>
<div class="lade wasser">
<div class="wasser-zonen">
<?php foreach ($zonen as $zone):
$dauern = $zone["dauern"];
sort($dauern); ?>
<div class="wasser-zone" data-zone="<?= $zone["key"] ?>" data-dauer="<?= $zone["dauern"][0] ?>">
<div class="wasser-kopf">
<span class="wasser-bild" data-bild="<?= $zone["bild"] ?>" aria-hidden="true"></span>
<div class="wasser-titel">
<b><?= htmlspecialchars($zone["label"]) ?></b>
<span data-w="zustand"></span>
</div>
<span class="lade-pille" data-w="pille"></span>
</div>
<div class="wasser-fuss">
<div class="lade-chips" role="group" aria-label="Dauer für <?= htmlspecialchars($zone["label"]) ?>">
<?php foreach ($dauern as $min): ?>
<button type="button" class="lade-chip" aria-pressed="false" data-min="<?= $min ?>"><?= $min ?>&#8201;min</button>
<?php endforeach; ?>
</div>
<button type="button" class="lade-start wasser-knopf" data-w="knopf"><span class="lade-punkt"></span><span data-w="knopfText">Gießen</span></button>
</div>
<div class="wasser-info" data-w="info"></div>
</div>
<?php endforeach; ?>
</div>
<div class="card mb-3"> <div class="lade-abschnitt">
<div class="card-body"> <h3>Abläufe</h3>
<div class="fw-semibold mb-3">Automatik</div> <div class="lade-chips wasser-ablaeufe">
<div class="d-grid gap-2 mb-3"> <?php foreach ($ablaeufe as $ablauf): ?>
<button type="button" class="btn btn-outline-primary" data-watering-auto-toggle data-watering-auto="Trog,Hoch" data-watering-topics="Gartenwasser/vorn,Gartenwasser/hinten" data-watering-label="Tröge und Hochbeete">Tröge und Hochbeete</button> <button type="button" class="lade-chip" aria-pressed="false"
<div class="d-flex flex-wrap gap-2"> data-ablauf="<?= htmlspecialchars(json_encode($ablauf["modi"], JSON_UNESCAPED_SLASHES)) ?>"><?= htmlspecialchars($ablauf["label"]) ?></button>
<button type="button" class="btn btn-outline-primary btn-sm flex-fill" data-watering-auto-toggle data-watering-auto="Trog" data-watering-topic="Gartenwasser/vorn" data-watering-label="Tröge">Tröge</button> <?php endforeach; ?>
<button type="button" class="btn btn-outline-primary btn-sm flex-fill" data-watering-auto-toggle data-watering-auto="Hoch" data-watering-topic="Gartenwasser/hinten" data-watering-label="Hochbeete">Hochbeete</button>
<button type="button" class="btn btn-outline-primary btn-sm flex-fill" data-watering-auto-toggle data-watering-auto="Vorn" data-watering-topic="Gartenwasser/vorn" data-watering-label="Garten vorn">Garten vorn</button>
</div> </div>
<div class="lade-erklaerung">Ein Ablauf gießt die Zonen nacheinander antippen startet, nochmal antippen stoppt. Die Regenautomatik läuft davon unabhängig zum Sonnenaufgang.</div>
</div> </div>
<div class="small text-muted">
Startet die Ventilfolge von Hand. Die Regenautomatik läuft davon
unabhängig zum Sonnenaufgang; ob eine Zone dabei schon versorgt
wurde, steht unten bei &bdquo;Heute&ldquo;.
</div>
</div>
</div>
HTML;
foreach ($zonen as $zone) { <div class="lade-meldung" data-w="meldung" aria-live="polite">Befehle gehen sofort an die Ventilsteuerung</div>
$key = $zone["key"];
$label = $zone["label"];
$minuten = $zone["minuten"];
echo <<<HTML
<div class="card mb-2">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="fw-semibold">$label</span>
<span class="badge bg-secondary" data-watering-state="$key">unbekannt</span>
</div> </div>
<div class="d-flex align-items-center gap-2 mb-2">
<div class="input-group input-group-sm" style="max-width: 7.5rem;">
<input class="form-control" type="number" min="1" max="120" value="$minuten" data-watering-timer="$key" aria-label="Dauer in Minuten für $label">
<span class="input-group-text">Min.</span>
</div>
<button type="button" class="btn btn-success px-4 ms-auto" data-watering-toggle data-watering-zone="$key">Ein</button>
</div>
<div class="small text-muted">
Zuletzt <span data-watering-last="$key">--</span> · <span data-watering-duration="$key">--</span><br>
Heute <span data-watering-today="$key">--</span> · Vortag <span data-watering-prevday="$key">--</span>
</div>
</div>
</div>
HTML;
}
echo "\n</div>\n";
+51
View File
@@ -4047,6 +4047,51 @@ body.anzeige-ohne-maus * { cursor: none !important; }
.lade .lade-meldung.fehler { color: #e5624f; } .lade .lade-meldung.fehler { color: #e5624f; }
@media (max-width: 420px) { .lade .lade-modus > span:last-child { display: none; } } @media (max-width: 420px) { .lade .lade-modus > span:last-child { display: none; } }
.lade .lade-pille.warn { color: #e8a33a; background: rgba(232, 163, 58, .14); }
.lade .lade-pille.blau { color: #6fb3e8; background: rgba(75, 159, 214, .15); }
.lade .lade-modi.zwei { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* --- Heizstab (ajax/heater.php, bedienfenster.js) --- */
/* Akzent: Regler und gewählte Chips im Rot der Karte "Fest" statt Türkis. */
.lade.heiz { --lade-wallbox: #e5624f; }
.lade.heiz .lade-status { grid-template-columns: 5.5rem minmax(0, 1fr); }
.lade .heiz-tank { width: 5.5rem; }
.lade .heiz-tank svg { width: 100%; height: auto; display: block; }
.lade .heiz-tank rect[data-heiz] { fill: #2b3237; transition: fill 1.5s; }
.lade .heiz-huelle { fill: none; stroke: #9aa6ad; stroke-width: 2.5; stroke-linecap: round; }
.lade .heiz-spirale { fill: none; stroke: rgba(255, 255, 255, .35); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; transition: stroke .4s; }
.lade .heiz-spirale.an { stroke: #ffb04a; animation: lade-gluehen 1.1s ease-in-out infinite alternate; }
@keyframes lade-gluehen { to { stroke: #ff5a2a; } }
.lade .heiz-tank text { fill: #fff; font-size: 13px; font-weight: 600; text-anchor: middle; paint-order: stroke; stroke: rgba(0, 0, 0, .35); stroke-width: 3px; }
.lade .heiz-tank text.klein { font-size: 11px; }
.lade .heiz-regler { --ist: 0%; }
/* --- Bewässerung (ajax/watering.php, bedienfenster.js) --- */
.lade.wasser { gap: .8rem; --lade-wallbox: #4b9fd6; }
.lade .wasser-zonen { display: grid; gap: .55rem; }
.lade .wasser-zone {
display: grid; gap: .5rem; padding: .65rem .75rem; border-radius: .8rem; background: var(--ton-flaeche-2, #2b3237);
border: 1.5px solid transparent; transition: border-color .3s;
}
.lade .wasser-zone.laeuft { border-color: #20c997; }
.lade .wasser-kopf { display: flex; align-items: center; gap: .6rem; }
.lade .wasser-bild { width: 2.1rem; height: 2.1rem; flex: 0 0 auto; display: grid; place-items: center; border-radius: .6rem; background: rgba(75, 159, 214, .14); color: #6fb3e8; }
.lade .wasser-zone.laeuft .wasser-bild { background: rgba(32, 201, 151, .16); color: #20c997; }
.lade .wasser-bild svg { width: 1.35rem; height: 1.35rem; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.lade .wasser-titel { display: grid; min-width: 0; margin-right: auto; }
.lade .wasser-titel b { font-weight: 600; }
.lade .wasser-titel span { font-size: .78rem; color: var(--ton-schrift-2, #9aa6ad); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lade .wasser-fuss { display: flex; flex-wrap: wrap; align-items: center; gap: .45rem; }
.lade .wasser-fuss .lade-chips { flex: 1 1 auto; }
.lade .wasser-fuss .lade-chip { padding: .3rem .6rem; font-size: .76rem; }
.lade .lade-start.wasser-knopf { height: 2.4rem; padding: 0 1rem; font-size: .9rem; flex: 0 0 auto; background: linear-gradient(180deg, #4b9fd6, #3b88bf); color: #06121b; box-shadow: 0 .3rem .9rem rgba(75, 159, 214, .22); }
.lade .lade-start.wasser-knopf .lade-punkt { display: none; }
.lade .lade-start.wasser-knopf.laeuft { background: linear-gradient(180deg, #3a4147, #30363b); color: var(--ton-schrift, #e8ede9); box-shadow: none; }
.lade .lade-start.wasser-knopf.laeuft .lade-punkt { display: block; background: #20c997; }
.lade .wasser-info { font-size: .72rem; color: var(--ton-schrift-3, #6f7a81); }
.lade .wasser-info:empty { display: none; }
.lade .wasser-ablaeufe .lade-chip[aria-pressed="true"] { background: #20c997; color: #06150f; }
/* Am 800er-Display zweispaltig ohne Scrollen. */ /* Am 800er-Display zweispaltig ohne Scrollen. */
.anzeige #modalEV .modal-dialog:has(.lade) { max-width: 46rem; } .anzeige #modalEV .modal-dialog:has(.lade) { max-width: 46rem; }
.anzeige .lade { grid-template-columns: 15rem minmax(0, 1fr); gap: 1rem; padding: .7rem .9rem; } .anzeige .lade { grid-template-columns: 15rem minmax(0, 1fr); gap: 1rem; padding: .7rem .9rem; }
@@ -4058,6 +4103,12 @@ body.anzeige-ohne-maus * { cursor: none !important; }
.anzeige #modalEV .modal-body:has(.lade) { max-height: none; overflow: visible; } .anzeige #modalEV .modal-body:has(.lade) { max-height: none; overflow: visible; }
.anzeige #modalEV .modal-content:has(.lade) .modal-footer { display: none; } .anzeige #modalEV .modal-content:has(.lade) .modal-footer { display: none; }
.anzeige #modalEV .modal-content:has(.lade) .modal-header { padding: .45rem .9rem; } .anzeige #modalEV .modal-content:has(.lade) .modal-header { padding: .45rem .9rem; }
.anzeige .lade.heiz .lade-status { grid-template-columns: 1fr; justify-items: center; }
.anzeige .lade.heiz .heiz-tank { width: 6.5rem; }
.anzeige .lade.wasser { grid-template-columns: 1fr; }
.anzeige .lade.wasser .wasser-zonen { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.anzeige .lade.wasser .wasser-fuss { flex-direction: column; align-items: stretch; }
.anzeige .lade.wasser .wasser-knopf { width: 100%; justify-content: center; }
/* --- Karte: Umschalter Übersicht / Anlage -------------------------------- */ /* --- Karte: Umschalter Übersicht / Anlage -------------------------------- */
.enf-umschalter { display: inline-flex; gap: 2px; padding: 3px; border-radius: var(--ton-radius-klein, .5rem); background: var(--ton-grund, #191d20); } .enf-umschalter { display: inline-flex; gap: 2px; padding: 3px; border-radius: var(--ton-radius-klein, .5rem); background: var(--ton-grund, #191d20); }
+1 -1
View File
@@ -101,7 +101,7 @@ if (isset($_GET["addUser"])) {
$pages = [ $pages = [
"solar" => ["template" => "solar.php", "mqtt" => true, "charts" => true, "meteogram" => true, "leaflet" => false, "solar" => ["template" => "solar.php", "mqtt" => true, "charts" => true, "meteogram" => true, "leaflet" => false,
"scripts" => ["js/solar/jahresstatistik.js", "js/solar/anlageKatalog.js", "js/solar/anlage.js", "js/solar/speicher.js", "scripts" => ["js/solar/jahresstatistik.js", "js/solar/anlageKatalog.js", "js/solar/anlage.js", "js/solar/speicher.js",
"js/solar/energieflussKatalog.js", "js/solar/energiefluss.js", "js/solar/bewaesserungAnzeige.js", "js/solar/ladefenster.js", "js/solar/solarMQTT.js"]], "js/solar/energieflussKatalog.js", "js/solar/energiefluss.js", "js/solar/bewaesserungAnzeige.js", "js/solar/ladefenster.js", "js/solar/bedienfenster.js", "js/solar/solarMQTT.js"]],
"home" => ["template" => "home.php", "mqtt" => true, "charts" => false, "meteogram" => false, "leaflet" => false, "home" => ["template" => "home.php", "mqtt" => true, "charts" => false, "meteogram" => false, "leaflet" => false,
"scripts" => ["js/solar/autoActionFuncs.js", "js/solar/zeitleiste.js", "js/solar/homeMQTT.js"]], "scripts" => ["js/solar/autoActionFuncs.js", "js/solar/zeitleiste.js", "js/solar/homeMQTT.js"]],
"heat" => ["template" => "heat.php", "mqtt" => true, "charts" => true, "meteogram" => false, "leaflet" => false, "heat" => ["template" => "heat.php", "mqtt" => true, "charts" => true, "meteogram" => false, "leaflet" => false,
+249
View File
@@ -0,0 +1,249 @@
/* ==========================================================================
Bedienfenster Heizstab und Bewässerung - im selben Stil wie das
Ladefenster (ladefenster.js), mit denselben Bausteinen aus solar.css.
heizfensterStarten(wurzel, url) Gerüst: ajax/heater.php
wasserfensterStarten(wurzel, url) Gerüst: ajax/watering.php
Beide lesen jede Sekunde aus mqttData und schicken jede Änderung sofort.
Was eben geschickt wurde, gilt HALTEN ms lang lokal, bis das Gerät den
neuen Stand meldet. Der Takt endet, wenn das Fenster zugeht.
========================================================================== */
(function () {
"use strict";
const HALTEN = 8000;
const S = ""; // schmales Leerzeichen vor Einheiten
const komma = (z, n = 1) => Number(z).toFixed(n).replace(".", ",");
const text = t => { const v = window.mqttData && mqttData[t]; return v === undefined ? undefined : String(v); };
const zahl = t => { const v = Number(text(t)); return isFinite(v) ? v : null; };
/** Gemeinsames Gerüst: lokaler Stand, Meldungszeile, Takt bis zum Schließen. */
function geruest(wurzel, meldungsFeld, grundText) {
const lokal = {};
const g = {
merke(name, wert) { lokal[name] = { wert, bis: Date.now() + HALTEN }; },
gilt(name, gemeldet) { return lokal[name] && lokal[name].bis > Date.now() ? lokal[name].wert : gemeldet; },
meldung(inhalt, art) {
const m = meldungsFeld();
m.textContent = inhalt;
m.className = "lade-meldung" + (art ? " " + art : "");
clearTimeout(g.uhr);
if (art === "gut") g.uhr = setTimeout(() => { m.textContent = grundText; m.className = "lade-meldung"; }, 2200);
},
takt(zeichnen) {
zeichnen();
const uhr = setInterval(zeichnen, 1000);
const fenster = wurzel.closest(".modal");
if (fenster) fenster.addEventListener("hidden.bs.modal", () => clearInterval(uhr), { once: true });
},
};
return g;
}
function setze(el, inhalt) {
if (el && el.textContent !== inhalt) el.textContent = inhalt;
}
/** Temperatur als Farbe, wie der Speicher in der Übersicht. */
function tempFarbe(t) {
const f = Math.max(0, Math.min(1, (t - 20) / 60));
const kalt = [35, 137, 186], heiss = [186, 59, 35];
return "rgb(" + kalt.map((k, i) => Math.round(k + (heiss[i] - k) * f)).join(",") + ")";
}
/* ======================================================== Heizstab */
window.heizfensterStarten = function (wurzel, url) {
const $ = name => wurzel.querySelector('[data-heiz="' + name + '"]');
const g = geruest(wurzel, () => $("meldung"), "Änderungen gelten sofort");
let ziehen = false;
async function senden(stufe) {
g.meldung("wird geschickt …");
try {
const antwort = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "setMode=" + stufe,
});
const inhalt = (await antwort.text()).trim();
if (!antwort.ok || inhalt) throw new Error(inhalt || "HTTP " + antwort.status);
g.meldung("✓ an den Heizstab geschickt", "gut");
} catch (e) {
g.meldung("Nicht übernommen: " + e.message, "fehler");
}
zeichnen();
}
function stand() {
const w = zahl("solarManager/pHeat") || 0;
const gemeldetFest = text("solarManager/heatMode") !== undefined && text("solarManager/heatMode") !== "eco";
const modus = g.gilt("modus", gemeldetFest ? "fest" : "auto");
const stufe = g.gilt("stufe", Math.max(1, Math.min(60, Math.round(w / 100) || 30)));
return {
w, modus, stufe,
temps: ["t_buffT", "t_buffM", "t_buffB"].map(t => zahl("solarManager/" + t)),
};
}
function zeichnen() {
const s = stand();
const heizt = s.w >= 20;
// Speicher
s.temps.forEach((t, i) => {
const schicht = $("t" + i);
if (t !== null) schicht.style.fill = tempFarbe(t);
setze($("tt" + i), t !== null ? Math.round(t) + "°" : "");
});
$("spirale").classList.toggle("an", heizt);
const namen = ["oben", "Mitte", "unten"];
setze($("temps"), s.temps.map((t, i) => t !== null ? namen[i] + " " + komma(t) : null).filter(Boolean).join(" · ") + S + "°C");
$("leistung").innerHTML = komma(s.w / 1000, 2) + S + "kW<small>" + (heizt ? "heizt" : "aus") + "</small>";
const pille = $("pille");
setze(pille, s.modus === "auto" ? "Automatik" : "fest");
pille.className = "lade-pille" + (heizt ? " an" : "");
setze($("erklaerung"), s.modus === "auto"
? "Heizt nur mit dem Überschuss der Anlage ohne Sonne bleibt der Heizstab aus."
: "Heizt dauerhaft mit " + komma(s.stufe / 10) + S + "kW reicht die Sonne nicht, kommt der Strom aus dem Netz.");
wurzel.querySelectorAll("[data-heiz-modus]").forEach(b => b.setAttribute("aria-checked", b.dataset.heizModus === s.modus ? "true" : "false"));
$("fest").hidden = s.modus !== "fest";
const regler = $("regler");
if (!ziehen) regler.value = s.stufe;
const wert = Number(regler.value);
regler.style.setProperty("--wert", ((wert - 1) / 59 * 100).toFixed(1) + "%");
$("reglerAusgabe").innerHTML = komma(wert / 10) + S + "kW";
wurzel.querySelectorAll("[data-heiz-stufe]").forEach(c => c.setAttribute("aria-pressed", Number(c.dataset.heizStufe) === wert ? "true" : "false"));
}
wurzel.querySelectorAll("[data-heiz-modus]").forEach(b => b.addEventListener("click", () => {
const modus = b.dataset.heizModus;
g.merke("modus", modus);
senden(modus === "auto" ? 0 : Number($("regler").value));
}));
$("regler").addEventListener("input", () => { ziehen = true; g.merke("stufe", Number($("regler").value)); zeichnen(); });
$("regler").addEventListener("change", () => {
ziehen = false;
g.merke("modus", "fest");
senden(Number($("regler").value));
});
wurzel.querySelectorAll("[data-heiz-stufe]").forEach(c => c.addEventListener("click", () => {
const stufe = Number(c.dataset.heizStufe);
g.merke("stufe", stufe);
g.merke("modus", "fest");
senden(stufe);
}));
g.takt(zeichnen);
};
/* ===================================================== Bewässerung */
window.wasserfensterStarten = function (wurzel, url) {
const g = geruest(wurzel, () => wurzel.querySelector('[data-w="meldung"]'), "Befehle gehen sofort an die Ventilsteuerung");
const bilder = window.ENERGIEFLUSS_ZONENBILDER || {};
const dauer = {}; // Zone -> gewählte Minuten
wurzel.querySelectorAll("[data-bild]").forEach(b => {
if (bilder[b.dataset.bild]) b.innerHTML = '<svg viewBox="0 0 24 24">' + bilder[b.dataset.bild] + "</svg>";
});
wurzel.querySelectorAll("[data-zone]").forEach(k => { dauer[k.dataset.zone] = Number(k.dataset.dauer); });
const zeitText = sek => sek < 60 ? Math.round(sek) + S + "s" : Math.ceil(sek / 60) + S + "min";
const labelText = l => String(l).replace(/^(\d+)m$/, "$1" + S + "min").replace(/^(\d+)s$/, "$1" + S + "s");
async function senden(topic, payload) {
g.meldung("wird geschickt …");
try {
const antwort = await fetch(url, {
method: "POST", credentials: "same-origin",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ topic, payload: typeof payload === "string" ? payload : JSON.stringify(payload) }),
});
const d = await antwort.json().catch(() => ({}));
if (!antwort.ok || !d.success) throw new Error(d.message || "HTTP " + antwort.status);
g.meldung("✓ an die Ventilsteuerung geschickt", "gut");
} catch (e) {
g.meldung("Nicht übernommen: " + e.message, "fehler");
}
zeichnen();
}
function zeichnen() {
wurzel.querySelectorAll("[data-zone]").forEach(karte => {
const key = karte.dataset.zone;
const zone = wateringZones[key];
if (!zone) return;
const z = wateringZonenZustand(zone);
const gemeldetLaeuft = z.state !== "aus" && z.state !== "--";
const laeuft = g.gilt("laeuft-" + key, gemeldetLaeuft);
const timer = wateringValue(wateringJson(zone.topic + "/Timers"), zone.valve);
const rest = timer !== null ? Math.abs(wateringSeconds(timer)) : null;
const $ = name => karte.querySelector('[data-w="' + name + '"]');
let zustand;
if (laeuft && gemeldetLaeuft) zustand = (z.paused ? "pausiert" : "läuft") + (rest ? " · noch " + zeitText(rest) : "");
else if (laeuft) zustand = "startet …";
else if (z.bewaessert) zustand = "heute " + labelText(z.heute.label) + " gegossen";
else if (z.state === "--") zustand = "Zustand unbekannt";
else zustand = "heute noch nicht gegossen";
setze($("zustand"), zustand);
const pille = $("pille");
setze(pille, laeuft ? (z.paused ? "pausiert" : "läuft") : z.bewaessert ? "gegossen" : "");
pille.className = "lade-pille" + (laeuft ? (z.paused ? " warn" : " an") : z.bewaessert ? " blau" : "");
karte.classList.toggle("laeuft", laeuft);
const knopf = $("knopf");
knopf.className = "lade-start wasser-knopf" + (laeuft ? " laeuft" : "");
setze($("knopfText"), laeuft ? "Stoppen" : "Gießen · " + dauer[key] + S + "min");
knopf.dataset.aktion = laeuft ? "stopp" : "start";
karte.querySelectorAll("[data-min]").forEach(c => c.setAttribute("aria-pressed", Number(c.dataset.min) === dauer[key] ? "true" : "false"));
const verlauf = wateringHistory(zone.topic, zone.valve);
const vortag = wateringDayTotal(zone.topic, "LastWateringDay", "wateringDurationDaySecs", zone.valve, false);
const teile = [];
if (verlauf.dateTime !== "--") teile.push("zuletzt " + verlauf.dateTime + (verlauf.duration !== "--" ? " · " + labelText(verlauf.duration) : ""));
if (vortag.label !== "--" && vortag.sekunden > 0) teile.push("letzter Gießtag " + labelText(vortag.label));
setze($("info"), teile.join(" · "));
});
wurzel.querySelectorAll("[data-ablauf]").forEach(c => {
const modi = JSON.parse(c.dataset.ablauf);
const aktiv = g.gilt("ablauf-" + c.textContent, Object.entries(modi).every(([t, m]) => wateringAutoActive(t, m)));
c.setAttribute("aria-pressed", aktiv ? "true" : "false");
});
}
wurzel.querySelectorAll("[data-zone]").forEach(karte => {
const key = karte.dataset.zone;
const zone = wateringZones[key];
karte.querySelectorAll("[data-min]").forEach(c => c.addEventListener("click", () => {
dauer[key] = Number(c.dataset.min);
zeichnen();
}));
karte.querySelector('[data-w="knopf"]').addEventListener("click", e => {
const start = e.currentTarget.dataset.aktion === "start";
g.merke("laeuft-" + key, start);
senden(zone.topic + "/set", start
? { valve: zone.valve, set: "open", timer: dauer[key] }
: { valve: zone.valve, set: "close" });
});
});
wurzel.querySelectorAll("[data-ablauf]").forEach(c => c.addEventListener("click", async () => {
const modi = JSON.parse(c.dataset.ablauf);
const aktiv = c.getAttribute("aria-pressed") === "true";
g.merke("ablauf-" + c.textContent, !aktiv);
for (const [topic, modus] of Object.entries(modi)) {
await senden(topic + "/auto", aktiv ? "Stop" : modus);
}
}));
g.takt(zeichnen);
};
})();
-40
View File
@@ -73,46 +73,6 @@ async function getData(chart, url, options = {}) {
} }
} }
/**
* Formular des Modals per POST abschicken ("Save changes", heute nur noch
* der Heizstab). Das Ladefenster schreibt selbst, siehe ladefenster.js.
*/
function submitFormAjax(event) {
let xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) {
setTimeout(function () { modalEV.hide(); }, 700);
if (xmlhttp.responseText) {
alert(xmlhttp.responseText);
}
}
};
let post = "";
const form = document.getElementById('modalEV').querySelector('form');
for (const element of Array.from(form.elements)) {
if (!element.name) continue;
if (element.type == "radio" || element.type == "checkbox") {
if (element.checked) {
post += element.name + "=" + encodeURIComponent(element.value) + "&";
}
} else if (element.type == "button") {
if (!element.classList.contains("accordion-button")) {
post += element.name + "=" + encodeURIComponent(element.innerHTML) + "&";
}
} else {
post += element.name + "=" + encodeURIComponent(element.value) + "&";
}
}
xmlhttp.open("POST", event.currentTarget.contentURL, true);
xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
document.getElementById('modalEV').querySelector('.modal-body').innerHTML = loadingHTML("Änderungen werden übernommen...");
xmlhttp.send(post);
return false;
}
/** /**
* Schieberegler im Modal mit seiner Beschriftung verbinden. * Schieberegler im Modal mit seiner Beschriftung verbinden.
* label(wert) liefert den anzuzeigenden Text. * label(wert) liefert den anzuzeigenden Text.
+3 -1
View File
@@ -381,7 +381,7 @@
wert.remove(); neben.remove(); wert.remove(); neben.remove();
symbol.setAttribute("transform", `translate(0 ${-r * 0.5}) scale(1.4)`); symbol.setAttribute("transform", `translate(0 ${-r * 0.5}) scale(1.4)`);
k.zonen.forEach((z, i) => { k.zonen.forEach((z, i) => {
const zy = -r * 0.1 + i * 25; const zy = -r * 0.06 + i * 20;
// Eigenes Symbol (ZONENBILDER) oder ein Zeichen aus Bootstrap Icons. // Eigenes Symbol (ZONENBILDER) oder ein Zeichen aus Bootstrap Icons.
// updateWateringIndicator() färbt es über color und schreibt den // updateWateringIndicator() färbt es über color und schreibt den
// Tooltip in das <title>. // Tooltip in das <title>.
@@ -622,4 +622,6 @@
} }
window.Energiefluss = Energiefluss; window.Energiefluss = Energiefluss;
// Auch die Bedienfenster (bedienfenster.js) zeigen die Zonen mit diesen Symbolen.
window.ENERGIEFLUSS_ZONENBILDER = ZONENBILDER;
})(); })();
+9 -155
View File
@@ -443,9 +443,9 @@ var chartSettings = {
function openModal(type) { function openModal(type) {
// Das Ladefenster und die Bewaesserung uebernehmen jede Aenderung sofort - // Alle Fenster der Solar-Seite uebernehmen jede Aenderung sofort -
// "Save changes" braucht nur noch der Heizstab. // "Save changes" braucht hier keines mehr.
document.getElementById("modalSaveBtn").style.display = type === "heater" ? "" : "none"; document.getElementById("modalSaveBtn").style.display = "none";
switch(type){ switch(type){
case "CarEG": case "CarEG":
document.getElementById("modal-title").innerHTML = "Autoladung EG"; document.getElementById("modal-title").innerHTML = "Autoladung EG";
@@ -471,8 +471,6 @@ var chartSettings = {
modalBodyElement = document.getElementById('modal-body'); modalBodyElement = document.getElementById('modal-body');
modalBodyElement.innerHTML = loadingHTML("Wird geladen..."); modalBodyElement.innerHTML = loadingHTML("Wird geladen...");
document.getElementById("modalSaveBtn").addEventListener("click", submitFormAjax);
document.getElementById("modalSaveBtn").contentURL = contentURL;
modalEV.show(); modalEV.show();
fetch(contentURL, { fetch(contentURL, {
method: 'GET', method: 'GET',
@@ -485,25 +483,17 @@ var chartSettings = {
.then(html => { .then(html => {
modalBodyElement.innerHTML = html; modalBodyElement.innerHTML = html;
var slider = document.getElementById("modal-slider"); // Aufbau und Logik der Fenster: ladefenster.js und bedienfenster.js.
var span = document.getElementById("modal-slider-label");
switch(type){ switch(type){
case "CarEG": case "CarEG":
case "CarOG": case "CarOG":
// Alles Weitere in js/solar/ladefenster.js.
ladefensterStarten(modalBodyElement.querySelector(".lade"), contentURL); ladefensterStarten(modalBodyElement.querySelector(".lade"), contentURL);
break; break;
case "heater": case "heater":
bindModalSlider(slider, span, value => value == 0 ? "Automatik" : (value / 10) + " kW"); heizfensterStarten(modalBodyElement.querySelector(".heiz"), contentURL);
if(mqttData["solarManager/heatMode"] == "eco"){
document.getElementById("modal-slider").value = 0;
}else{
document.getElementById("modal-slider").value = Math.round(mqttData["solarManager/pHeat"]/100);
}
document.getElementById("modal-slider").dispatchEvent(new Event('input'));
break; break;
case "watering": case "watering":
initializeWateringModal(); wasserfensterStarten(modalBodyElement.querySelector(".wasser"), contentURL);
break; break;
} }
@@ -513,146 +503,10 @@ var chartSettings = {
}); });
} }
function wateringCommand(topic, command, directTopic = false) { // Die Zonensymbole im Bewaesserungskreis; das Fenster aktualisiert sich
const feedback = document.getElementById("watering-feedback"); // selbst (bedienfenster.js).
const targetTopic = directTopic ? topic : topic + "/set";
const payload = typeof command === "string" ? command : JSON.stringify(command);
const endpoint = window.location.pathname.replace(/\/index\.php$/, "").replace(/\/$/, "") + "/ajax/watering.php";
const showFeedback = (message, type) => {
if (!feedback) return;
feedback.textContent = message;
feedback.className = "alert alert-" + type + " mb-3";
feedback.style.whiteSpace = "pre-line";
};
fetch(endpoint, {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ topic: targetTopic, payload: payload })
}).then(async (response) => {
const result = await response.json();
if (!response.ok || !result.success) throw new Error(result.message || "Unbekannter MQTT-Fehler.");
showFeedback("Befehl gesendet.\nTopic: " + result.topic + "\nInhalt: " + result.payload, "success");
}).catch((error) => {
showFeedback("Befehl konnte nicht gesendet werden: " + error.message + "\nTopic: " + targetTopic + "\nInhalt: " + payload, "danger");
});
}
function initializeWateringModal() {
document.querySelectorAll("[data-watering-toggle]").forEach((button) => {
const updateButton = () => {
const zone = wateringZones[button.dataset.wateringZone];
setzeZonenButton(button, wateringLabel(zone.topic, zone.valve));
};
button.addEventListener("click", () => {
const zone = wateringZones[button.dataset.wateringZone];
if (wateringLabel(zone.topic, zone.valve) === "aus") {
const timerInput = document.querySelector(`[data-watering-timer='${button.dataset.wateringZone}']`);
const timer = Math.min(120, Math.max(1, Number(timerInput.value) || 30));
wateringCommand(zone.topic, { valve: zone.valve, set: "open", timer: timer });
} else {
wateringCommand(zone.topic, { valve: zone.valve, set: "close" });
}
});
updateButton();
});
document.querySelectorAll("[data-watering-auto-toggle]").forEach((button) => {
button.addEventListener("click", () => {
const topics = (button.dataset.wateringTopics || button.dataset.wateringTopic).split(",");
const modes = button.dataset.wateringAuto.split(",");
const active = topics.every((topic, index) => wateringAutoActive(topic, modes[index]));
topics.forEach((topic, index) => {
wateringCommand(topic + "/auto", active ? "Stop" : modes[index], true);
});
});
updateWateringAutoButton(button);
});
updateWateringStatus();
}
function updateWateringStatus() { function updateWateringStatus() {
wateringZoneList.forEach(([key, zone]) => { wateringSymboleSetzen(svgElement);
const state = wateringLabel(zone.topic, zone.valve);
const paused = wateringIsPaused(zone.topic, zone.valve);
const heute = wateringDayTotal(zone.topic, "WateringToday", "wateringDurationTodaySecs", zone.valve, true);
// Die Regenautomatik laeuft zum Sonnenaufgang; wenn jemand auf die
// Anzeige schaut, ist die Entscheidung laengst gefallen. Angezeigt wird
// deshalb nicht mehr, was geplant ist, sondern was passiert ist. Der
// Zaehler unterscheidet nicht, wer ausgeloest hat - eine Bewaesserung
// von Hand zaehlt genauso, und das ist so gewollt.
const bewaessert = state === "aus" && heute.sekunden > 0;
// Die Anzeigen liegen im Realtime-SVG und bleiben bestehen -> svgElement.
const status = svgElement(zone.statusId);
const icon = svgElement(zone.iconId);
if (status && icon) updateWateringIndicator(status, icon, zone.label, state, paused, bewaessert);
// Die Modal-Elemente werden bei jedem Oeffnen neu erzeugt -> nicht cachen.
setzeZonenButton(document.querySelector(`[data-watering-toggle][data-watering-zone='${key}']`), state);
setzeSchild(`[data-watering-state='${key}']`, state, paused, bewaessert);
const history = wateringHistory(zone.topic, zone.valve);
setzeZelle(`[data-watering-last='${key}']`, history.dateTime);
setzeZelle(`[data-watering-duration='${key}']`, history.duration);
const vortag = wateringDayTotal(zone.topic, "LastWateringDay", "wateringDurationDaySecs", zone.valve, false);
setzeZelle(`[data-watering-today='${key}']`, heute.label, heute.day && "Stand: " + heute.day);
setzeZelle(`[data-watering-prevday='${key}']`, vortag.label, vortag.day && "Bewässerungstag: " + vortag.day);
});
document.querySelectorAll("[data-watering-auto-toggle]").forEach(updateWateringAutoButton);
}
// Der Knopf zeigt die naechste Aktion, nicht den Zustand: laeuft die Zone,
// steht "Aus" darauf.
function setzeZonenButton(button, state) {
if (!button) return;
const beschriftung = state === "aus" ? "Ein" : "Aus";
if (button.textContent !== beschriftung) button.textContent = beschriftung;
button.classList.toggle("btn-success", state === "aus");
button.classList.toggle("btn-danger", state !== "aus");
}
// Zustandsschild der Zonenkarte, Farben wie im SVG: laufend gruen,
// pausiert gelb, heute schon bewaessert blau, sonst grau.
function setzeSchild(selektor, state, paused, bewaessert) {
const schild = document.querySelector(selektor);
if (!schild) return;
let text = "unbekannt";
let farbe = "bg-secondary";
if (state !== "--" && state !== "aus") {
text = paused ? "pausiert " + state : "läuft " + state;
farbe = paused ? "bg-warning text-dark" : "bg-success";
} else if (state === "aus") {
text = bewaessert ? "heute bewässert" : "aus";
farbe = bewaessert ? "bg-primary" : "bg-secondary";
}
if (schild.textContent !== text) schild.textContent = text;
const klasse = "badge " + farbe;
if (schild.className !== klasse) schild.className = klasse;
}
// Schreibt nur bei echter Aenderung - die Tabelle wird im Sekundentakt
// aktualisiert, ihre Werte aendern sich aber hoechstens minuetlich.
function setzeZelle(selektor, text, titel) {
const element = document.querySelector(selektor);
if (!element) return;
if (element.textContent !== text) element.textContent = text;
if (titel) {
if (element.getAttribute("title") !== titel) element.setAttribute("title", titel);
} else if (element.hasAttribute("title")) {
element.removeAttribute("title");
}
}
function updateWateringAutoButton(button) {
const topics = (button.dataset.wateringTopics || button.dataset.wateringTopic).split(",");
const modes = button.dataset.wateringAuto.split(",");
const active = topics.every((topic, index) => wateringAutoActive(topic, modes[index]));
if (!button.dataset.wateringLabel) {
button.dataset.wateringLabel = button.textContent.replace(/^Automatik\s+/, "");
}
button.textContent = (active ? "Aus: " : "Ein: ") + button.dataset.wateringLabel;
button.classList.toggle("btn-outline-primary", !active);
button.classList.toggle("btn-outline-danger", active);
} }
window.setInterval(updateWateringStatus, 1000); window.setInterval(updateWateringStatus, 1000);