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 |
|
| 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 |
|
||||||
|
|
||||||
|
|||||||
+100
-58
@@ -1,66 +1,108 @@
|
|||||||
<?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");
|
||||||
|
|
||||||
|
$hostname = "192.168.179.169";
|
||||||
|
|
||||||
if (checkLogin()) {
|
if (!checkLogin()) {
|
||||||
$close = 0;
|
exit;
|
||||||
$hostname = "192.168.179.169";
|
}
|
||||||
|
|
||||||
|
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
|
||||||
/*
|
// (GET) - immer nur einer von beiden.
|
||||||
* Der Wert kommt aus dem Formular (POST) oder von Hand aus der
|
$stufe = intval($_POST["setMode"] ?? $_GET["setMode"] ?? 0);
|
||||||
* Adresse (GET) - immer nur einer von beiden. Frueher wurden beide
|
if ($stufe >= 1 && $stufe <= 60) {
|
||||||
* addiert; der fehlende erzeugte eine PHP-Warnung, und die stand dann
|
$pfad = "/set?mode=man&pwr=" . ($stufe * 100);
|
||||||
* als Meldung im Browser.
|
} elseif ($stufe === 0) {
|
||||||
*
|
$pfad = "/set?mode=eco&pwr=0";
|
||||||
* 0 = Automatik (Ueberschuss), 1..60 = feste Leistung in 100 W. Der
|
} else {
|
||||||
* Regler reicht bis 60; die alte Pruefung "< 60" liess genau 6,0 kW
|
echo "Unbekannte Leistungsstufe.";
|
||||||
* still nicht durch.
|
exit;
|
||||||
*/
|
|
||||||
$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;
|
|
||||||
}
|
|
||||||
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");
|
|
||||||
stream_set_timeout($remote, 5);
|
|
||||||
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.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$close = 1;
|
|
||||||
}
|
}
|
||||||
} else {
|
$remote = @fsockopen("tcp://" . $hostname, 80, $errno, $errstr, 5);
|
||||||
$close = 1;
|
if ($remote) {
|
||||||
}
|
fwrite($remote, "GET " . $pfad . " HTTP/1.1\r\nHost: " . $hostname . "\r\nConnection: close\r\n\r\n");
|
||||||
if (!$close) {
|
stream_set_timeout($remote, 5);
|
||||||
echo <<<ENDE
|
fread($remote, 1024);
|
||||||
<!--begin::Form-->
|
fclose($remote);
|
||||||
<form id="heater_form">
|
} else {
|
||||||
<div class="col-sm-11">
|
echo "Der Heizstab ist nicht erreichbar.";
|
||||||
<div class="form-check">
|
}
|
||||||
<label class="form-label" for="addedCharge">Leistung: </label>
|
exit;
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
ENDE;
|
|
||||||
}
|
}
|
||||||
|
?>
|
||||||
|
<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>
|
||||||
|
|||||||
+49
-51
@@ -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"]],
|
||||||
<div class="card mb-3">
|
["label" => "Tröge", "modi" => ["Gartenwasser/vorn" => "Trog"]],
|
||||||
<div class="card-body">
|
["label" => "Beete", "modi" => ["Gartenwasser/hinten" => "Hoch"]],
|
||||||
<div class="fw-semibold mb-3">Automatik</div>
|
["label" => "Vorn", "modi" => ["Gartenwasser/vorn" => "Vorn"]],
|
||||||
<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">
|
<div class="lade wasser">
|
||||||
<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>
|
<div class="wasser-zonen">
|
||||||
<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>
|
<?php foreach ($zonen as $zone):
|
||||||
<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>
|
$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>
|
</div>
|
||||||
|
<span class="lade-pille" data-w="pille"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="small text-muted">
|
<div class="wasser-fuss">
|
||||||
Startet die Ventilfolge von Hand. Die Regenautomatik läuft davon
|
<div class="lade-chips" role="group" aria-label="Dauer für <?= htmlspecialchars($zone["label"]) ?>">
|
||||||
unabhängig zum Sonnenaufgang; ob eine Zone dabei schon versorgt
|
<?php foreach ($dauern as $min): ?>
|
||||||
wurde, steht unten bei „Heute“.
|
<button type="button" class="lade-chip" aria-pressed="false" data-min="<?= $min ?>"><?= $min ?> min</button>
|
||||||
</div>
|
<?php endforeach; ?>
|
||||||
</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>
|
|
||||||
<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>
|
</div>
|
||||||
<button type="button" class="btn btn-success px-4 ms-auto" data-watering-toggle data-watering-zone="$key">Ein</button>
|
<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="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 class="wasser-info" data-w="info"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
HTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\n</div>\n";
|
<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="lade-meldung" data-w="meldung" aria-live="polite">Befehle gehen sofort an die Ventilsteuerung</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -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); }
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.
|
* Schieberegler im Modal mit seiner Beschriftung verbinden.
|
||||||
* label(wert) liefert den anzuzeigenden Text.
|
* label(wert) liefert den anzuzeigenden Text.
|
||||||
|
|||||||
@@ -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;
|
||||||
})();
|
})();
|
||||||
|
|||||||
+11
-157
@@ -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"){
|
break;
|
||||||
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;
|
|
||||||
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);
|
||||||
|
|||||||
Reference in New Issue
Block a user