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:
@@ -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 |
|
||||
|---|---|---|---|---|
|
||||
| `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/#` |
|
||||
| `heat` | `heat.php` | `heatMQTT.js` | `getHeaterData`, `getWaterData`, `getSunrise` | `solarManager/#`, `weatherStation/#`, Wallbox-Topics |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `mountMeteogram()`, `updateWeatherCards()` | Wetterteile der Solar- und Wetterseite |
|
||||
| `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 |
|
||||
| `tile.php` | `?z=&x=&y=` | Kartenkachel aus dem eigenen Zwischenspeicher, sonst einmalig von OSM |
|
||||
| `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 |
|
||||
| `phpMQTT.php` | (Bibliothek) | MQTT aus PHP heraus, für Kommandos |
|
||||
|
||||
|
||||
+80
-38
@@ -1,33 +1,36 @@
|
||||
<?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");
|
||||
|
||||
|
||||
if (checkLogin()) {
|
||||
$close = 0;
|
||||
$hostname = "192.168.179.169";
|
||||
|
||||
if (!checkLogin()) {
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_POST["setMode"]) || isset($_GET["setMode"])) {
|
||||
/*
|
||||
* Der Wert kommt aus dem Formular (POST) oder von Hand aus der
|
||||
* 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.
|
||||
*/
|
||||
// Der Wert kommt aus dem Fenster (POST) oder von Hand aus der Adresse
|
||||
// (GET) - immer nur einer von beiden.
|
||||
$stufe = intval($_POST["setMode"] ?? $_GET["setMode"] ?? 0);
|
||||
if ($stufe >= 1 && $stufe <= 60) {
|
||||
$pfad = "/set?mode=man&pwr=" . ($stufe * 100);
|
||||
} elseif ($stufe === 0) {
|
||||
$pfad = "/set?mode=eco&pwr=0";
|
||||
} else {
|
||||
$pfad = null;
|
||||
echo "Unbekannte Leistungsstufe.";
|
||||
exit;
|
||||
}
|
||||
if ($pfad !== null) {
|
||||
$remote = @fsockopen("tcp://" . $hostname, 80, $errno, $errstr, 5);
|
||||
if ($remote) {
|
||||
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);
|
||||
fclose($remote);
|
||||
} 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.";
|
||||
}
|
||||
exit;
|
||||
}
|
||||
$close = 1;
|
||||
}
|
||||
} else {
|
||||
$close = 1;
|
||||
}
|
||||
if (!$close) {
|
||||
echo <<<ENDE
|
||||
<!--begin::Form-->
|
||||
<form id="heater_form">
|
||||
<div class="col-sm-11">
|
||||
<div class="form-check">
|
||||
<label class="form-label" for="addedCharge">Leistung: </label>
|
||||
<div style="font-size: 15px;color: #297195;display: inline-block;" id="modal-slider-label">0</div>
|
||||
<input type="range" id="modal-slider" name="setMode" class="form-range range-color-track" min="0" max="60" />
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="max-amount f-16 font-weight-normal darkGray-color text-right mt-1">Auto</span>
|
||||
<span class="max-amount f-16 font-weight-normal darkGray-color text-right mt-1">6.0 kW</span>
|
||||
?>
|
||||
<div class="lade heiz">
|
||||
<div class="lade-links">
|
||||
<div class="lade-status">
|
||||
<div class="heiz-tank" aria-hidden="true">
|
||||
<svg viewBox="0 0 80 120">
|
||||
<defs><clipPath id="heiz-innen"><rect x="14" y="8" width="52" height="100" rx="20"/></clipPath></defs>
|
||||
<g clip-path="url(#heiz-innen)">
|
||||
<rect x="0" y="0" width="80" height="42" data-heiz="t0"/>
|
||||
<rect x="0" y="42" width="80" height="34" data-heiz="t1"/>
|
||||
<rect x="0" y="76" width="80" height="44" data-heiz="t2"/>
|
||||
</g>
|
||||
<rect x="14" y="8" width="52" height="100" rx="20" class="heiz-huelle"/>
|
||||
<path d="M26 103l4-6 4 6 4-6 4 6 4-6 4 6 4-6" class="heiz-spirale" data-heiz="spirale"/>
|
||||
<path d="M26 108v7M54 108v7" class="heiz-huelle"/>
|
||||
<text x="40" y="30" data-heiz="tt0">–</text>
|
||||
<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 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,1 kW</span><span>3 kW</span><span>6 kW</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">1 kW</button>
|
||||
<button type="button" class="lade-chip" aria-pressed="false" data-heiz-stufe="20">2 kW</button>
|
||||
<button type="button" class="lade-chip" aria-pressed="false" data-heiz-stufe="30">3 kW</button>
|
||||
<button type="button" class="lade-chip" aria-pressed="false" data-heiz-stufe="60">6 kW</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lade-meldung" data-heiz="meldung" aria-live="polite">Änderungen gelten sofort</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
ENDE;
|
||||
}
|
||||
|
||||
+51
-53
@@ -73,64 +73,62 @@ if (!checkLogin()) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Zonen des Modals. Die Reihenfolge bestimmt die Reihenfolge der Karten, die
|
||||
// Keys muessen zu wateringZones in js/solar/solarMQTT.js passen.
|
||||
// Zonen des Fensters. Die Reihenfolge bestimmt die Reihenfolge der Karten;
|
||||
// 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 = [
|
||||
["key" => "troughs", "label" => "Tröge", "minuten" => 5],
|
||||
["key" => "raisedBeds", "label" => "Beete", "minuten" => 30],
|
||||
["key" => "frontGarden", "label" => "Vorn", "minuten" => 30],
|
||||
["key" => "troughs", "label" => "Tröge", "bild" => "kuebel", "dauern" => [5, 10, 15, 30]],
|
||||
["key" => "raisedBeds", "label" => "Beete", "bild" => "hochbeet", "dauern" => [30, 15, 45, 60]],
|
||||
["key" => "frontGarden", "label" => "Vorn", "bild" => "garten", "dauern" => [30, 15, 45, 60]],
|
||||
];
|
||||
|
||||
echo <<<HTML
|
||||
<div class="p-3">
|
||||
<div id="watering-feedback" class="alert d-none mb-3" role="status"></div>
|
||||
// Die Abläufe der Ventilsteuerung (Topic .../auto). Mehrere Einträge in
|
||||
// "modi" starten bzw. stoppen gemeinsam.
|
||||
$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 ?> 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="card-body">
|
||||
<div class="fw-semibold mb-3">Automatik</div>
|
||||
<div class="d-grid gap-2 mb-3">
|
||||
<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>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<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>
|
||||
<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 class="lade-abschnitt">
|
||||
<h3>Abläufe</h3>
|
||||
<div class="lade-chips wasser-ablaeufe">
|
||||
<?php foreach ($ablaeufe as $ablauf): ?>
|
||||
<button type="button" class="lade-chip" aria-pressed="false"
|
||||
data-ablauf="<?= htmlspecialchars(json_encode($ablauf["modi"], JSON_UNESCAPED_SLASHES)) ?>"><?= htmlspecialchars($ablauf["label"]) ?></button>
|
||||
<?php endforeach; ?>
|
||||
</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 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 „Heute“.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
HTML;
|
||||
|
||||
foreach ($zonen as $zone) {
|
||||
$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 class="lade-meldung" data-w="meldung" aria-live="polite">Befehle gehen sofort an die Ventilsteuerung</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";
|
||||
|
||||
@@ -4047,6 +4047,51 @@ body.anzeige-ohne-maus * { cursor: none !important; }
|
||||
.lade .lade-meldung.fehler { color: #e5624f; }
|
||||
@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. */
|
||||
.anzeige #modalEV .modal-dialog:has(.lade) { max-width: 46rem; }
|
||||
.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-content:has(.lade) .modal-footer { display: none; }
|
||||
.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 -------------------------------- */
|
||||
.enf-umschalter { display: inline-flex; gap: 2px; padding: 3px; border-radius: var(--ton-radius-klein, .5rem); background: var(--ton-grund, #191d20); }
|
||||
|
||||
@@ -101,7 +101,7 @@ if (isset($_GET["addUser"])) {
|
||||
$pages = [
|
||||
"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",
|
||||
"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,
|
||||
"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,
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
})();
|
||||
@@ -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.
|
||||
* label(wert) liefert den anzuzeigenden Text.
|
||||
|
||||
@@ -381,7 +381,7 @@
|
||||
wert.remove(); neben.remove();
|
||||
symbol.setAttribute("transform", `translate(0 ${-r * 0.5}) scale(1.4)`);
|
||||
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.
|
||||
// updateWateringIndicator() färbt es über color und schreibt den
|
||||
// Tooltip in das <title>.
|
||||
@@ -622,4 +622,6 @@
|
||||
}
|
||||
|
||||
window.Energiefluss = Energiefluss;
|
||||
// Auch die Bedienfenster (bedienfenster.js) zeigen die Zonen mit diesen Symbolen.
|
||||
window.ENERGIEFLUSS_ZONENBILDER = ZONENBILDER;
|
||||
})();
|
||||
|
||||
+9
-155
@@ -443,9 +443,9 @@ var chartSettings = {
|
||||
|
||||
|
||||
function openModal(type) {
|
||||
// Das Ladefenster und die Bewaesserung uebernehmen jede Aenderung sofort -
|
||||
// "Save changes" braucht nur noch der Heizstab.
|
||||
document.getElementById("modalSaveBtn").style.display = type === "heater" ? "" : "none";
|
||||
// Alle Fenster der Solar-Seite uebernehmen jede Aenderung sofort -
|
||||
// "Save changes" braucht hier keines mehr.
|
||||
document.getElementById("modalSaveBtn").style.display = "none";
|
||||
switch(type){
|
||||
case "CarEG":
|
||||
document.getElementById("modal-title").innerHTML = "Autoladung EG";
|
||||
@@ -471,8 +471,6 @@ var chartSettings = {
|
||||
|
||||
modalBodyElement = document.getElementById('modal-body');
|
||||
modalBodyElement.innerHTML = loadingHTML("Wird geladen...");
|
||||
document.getElementById("modalSaveBtn").addEventListener("click", submitFormAjax);
|
||||
document.getElementById("modalSaveBtn").contentURL = contentURL;
|
||||
modalEV.show();
|
||||
fetch(contentURL, {
|
||||
method: 'GET',
|
||||
@@ -485,25 +483,17 @@ var chartSettings = {
|
||||
.then(html => {
|
||||
modalBodyElement.innerHTML = html;
|
||||
|
||||
var slider = document.getElementById("modal-slider");
|
||||
var span = document.getElementById("modal-slider-label");
|
||||
// Aufbau und Logik der Fenster: ladefenster.js und bedienfenster.js.
|
||||
switch(type){
|
||||
case "CarEG":
|
||||
case "CarOG":
|
||||
// Alles Weitere in js/solar/ladefenster.js.
|
||||
ladefensterStarten(modalBodyElement.querySelector(".lade"), contentURL);
|
||||
break;
|
||||
case "heater":
|
||||
bindModalSlider(slider, span, value => value == 0 ? "Automatik" : (value / 10) + " kW");
|
||||
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'));
|
||||
heizfensterStarten(modalBodyElement.querySelector(".heiz"), contentURL);
|
||||
break;
|
||||
case "watering":
|
||||
initializeWateringModal();
|
||||
wasserfensterStarten(modalBodyElement.querySelector(".wasser"), contentURL);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -513,146 +503,10 @@ var chartSettings = {
|
||||
});
|
||||
}
|
||||
|
||||
function wateringCommand(topic, command, directTopic = false) {
|
||||
const feedback = document.getElementById("watering-feedback");
|
||||
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();
|
||||
}
|
||||
|
||||
// Die Zonensymbole im Bewaesserungskreis; das Fenster aktualisiert sich
|
||||
// selbst (bedienfenster.js).
|
||||
function updateWateringStatus() {
|
||||
wateringZoneList.forEach(([key, zone]) => {
|
||||
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);
|
||||
wateringSymboleSetzen(svgElement);
|
||||
}
|
||||
|
||||
window.setInterval(updateWateringStatus, 1000);
|
||||
|
||||
Reference in New Issue
Block a user