Heizstab- und Bewaesserungsfenster im neuen Look, Bewaesserungszeilen enger

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 21:26:16 +02:00
co-authored by Claude Opus 5
parent bf5fdfefa4
commit 6e805678d4
9 changed files with 466 additions and 311 deletions
+249
View File
@@ -0,0 +1,249 @@
/* ==========================================================================
Bedienfenster Heizstab und Bewässerung - im selben Stil wie das
Ladefenster (ladefenster.js), mit denselben Bausteinen aus solar.css.
heizfensterStarten(wurzel, url) Gerüst: ajax/heater.php
wasserfensterStarten(wurzel, url) Gerüst: ajax/watering.php
Beide lesen jede Sekunde aus mqttData und schicken jede Änderung sofort.
Was eben geschickt wurde, gilt HALTEN ms lang lokal, bis das Gerät den
neuen Stand meldet. Der Takt endet, wenn das Fenster zugeht.
========================================================================== */
(function () {
"use strict";
const HALTEN = 8000;
const S = ""; // schmales Leerzeichen vor Einheiten
const komma = (z, n = 1) => Number(z).toFixed(n).replace(".", ",");
const text = t => { const v = window.mqttData && mqttData[t]; return v === undefined ? undefined : String(v); };
const zahl = t => { const v = Number(text(t)); return isFinite(v) ? v : null; };
/** Gemeinsames Gerüst: lokaler Stand, Meldungszeile, Takt bis zum Schließen. */
function geruest(wurzel, meldungsFeld, grundText) {
const lokal = {};
const g = {
merke(name, wert) { lokal[name] = { wert, bis: Date.now() + HALTEN }; },
gilt(name, gemeldet) { return lokal[name] && lokal[name].bis > Date.now() ? lokal[name].wert : gemeldet; },
meldung(inhalt, art) {
const m = meldungsFeld();
m.textContent = inhalt;
m.className = "lade-meldung" + (art ? " " + art : "");
clearTimeout(g.uhr);
if (art === "gut") g.uhr = setTimeout(() => { m.textContent = grundText; m.className = "lade-meldung"; }, 2200);
},
takt(zeichnen) {
zeichnen();
const uhr = setInterval(zeichnen, 1000);
const fenster = wurzel.closest(".modal");
if (fenster) fenster.addEventListener("hidden.bs.modal", () => clearInterval(uhr), { once: true });
},
};
return g;
}
function setze(el, inhalt) {
if (el && el.textContent !== inhalt) el.textContent = inhalt;
}
/** Temperatur als Farbe, wie der Speicher in der Übersicht. */
function tempFarbe(t) {
const f = Math.max(0, Math.min(1, (t - 20) / 60));
const kalt = [35, 137, 186], heiss = [186, 59, 35];
return "rgb(" + kalt.map((k, i) => Math.round(k + (heiss[i] - k) * f)).join(",") + ")";
}
/* ======================================================== Heizstab */
window.heizfensterStarten = function (wurzel, url) {
const $ = name => wurzel.querySelector('[data-heiz="' + name + '"]');
const g = geruest(wurzel, () => $("meldung"), "Änderungen gelten sofort");
let ziehen = false;
async function senden(stufe) {
g.meldung("wird geschickt …");
try {
const antwort = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "setMode=" + stufe,
});
const inhalt = (await antwort.text()).trim();
if (!antwort.ok || inhalt) throw new Error(inhalt || "HTTP " + antwort.status);
g.meldung("✓ an den Heizstab geschickt", "gut");
} catch (e) {
g.meldung("Nicht übernommen: " + e.message, "fehler");
}
zeichnen();
}
function stand() {
const w = zahl("solarManager/pHeat") || 0;
const gemeldetFest = text("solarManager/heatMode") !== undefined && text("solarManager/heatMode") !== "eco";
const modus = g.gilt("modus", gemeldetFest ? "fest" : "auto");
const stufe = g.gilt("stufe", Math.max(1, Math.min(60, Math.round(w / 100) || 30)));
return {
w, modus, stufe,
temps: ["t_buffT", "t_buffM", "t_buffB"].map(t => zahl("solarManager/" + t)),
};
}
function zeichnen() {
const s = stand();
const heizt = s.w >= 20;
// Speicher
s.temps.forEach((t, i) => {
const schicht = $("t" + i);
if (t !== null) schicht.style.fill = tempFarbe(t);
setze($("tt" + i), t !== null ? Math.round(t) + "°" : "");
});
$("spirale").classList.toggle("an", heizt);
const namen = ["oben", "Mitte", "unten"];
setze($("temps"), s.temps.map((t, i) => t !== null ? namen[i] + " " + komma(t) : null).filter(Boolean).join(" · ") + S + "°C");
$("leistung").innerHTML = komma(s.w / 1000, 2) + S + "kW<small>" + (heizt ? "heizt" : "aus") + "</small>";
const pille = $("pille");
setze(pille, s.modus === "auto" ? "Automatik" : "fest");
pille.className = "lade-pille" + (heizt ? " an" : "");
setze($("erklaerung"), s.modus === "auto"
? "Heizt nur mit dem Überschuss der Anlage ohne Sonne bleibt der Heizstab aus."
: "Heizt dauerhaft mit " + komma(s.stufe / 10) + S + "kW reicht die Sonne nicht, kommt der Strom aus dem Netz.");
wurzel.querySelectorAll("[data-heiz-modus]").forEach(b => b.setAttribute("aria-checked", b.dataset.heizModus === s.modus ? "true" : "false"));
$("fest").hidden = s.modus !== "fest";
const regler = $("regler");
if (!ziehen) regler.value = s.stufe;
const wert = Number(regler.value);
regler.style.setProperty("--wert", ((wert - 1) / 59 * 100).toFixed(1) + "%");
$("reglerAusgabe").innerHTML = komma(wert / 10) + S + "kW";
wurzel.querySelectorAll("[data-heiz-stufe]").forEach(c => c.setAttribute("aria-pressed", Number(c.dataset.heizStufe) === wert ? "true" : "false"));
}
wurzel.querySelectorAll("[data-heiz-modus]").forEach(b => b.addEventListener("click", () => {
const modus = b.dataset.heizModus;
g.merke("modus", modus);
senden(modus === "auto" ? 0 : Number($("regler").value));
}));
$("regler").addEventListener("input", () => { ziehen = true; g.merke("stufe", Number($("regler").value)); zeichnen(); });
$("regler").addEventListener("change", () => {
ziehen = false;
g.merke("modus", "fest");
senden(Number($("regler").value));
});
wurzel.querySelectorAll("[data-heiz-stufe]").forEach(c => c.addEventListener("click", () => {
const stufe = Number(c.dataset.heizStufe);
g.merke("stufe", stufe);
g.merke("modus", "fest");
senden(stufe);
}));
g.takt(zeichnen);
};
/* ===================================================== Bewässerung */
window.wasserfensterStarten = function (wurzel, url) {
const g = geruest(wurzel, () => wurzel.querySelector('[data-w="meldung"]'), "Befehle gehen sofort an die Ventilsteuerung");
const bilder = window.ENERGIEFLUSS_ZONENBILDER || {};
const dauer = {}; // Zone -> gewählte Minuten
wurzel.querySelectorAll("[data-bild]").forEach(b => {
if (bilder[b.dataset.bild]) b.innerHTML = '<svg viewBox="0 0 24 24">' + bilder[b.dataset.bild] + "</svg>";
});
wurzel.querySelectorAll("[data-zone]").forEach(k => { dauer[k.dataset.zone] = Number(k.dataset.dauer); });
const zeitText = sek => sek < 60 ? Math.round(sek) + S + "s" : Math.ceil(sek / 60) + S + "min";
const labelText = l => String(l).replace(/^(\d+)m$/, "$1" + S + "min").replace(/^(\d+)s$/, "$1" + S + "s");
async function senden(topic, payload) {
g.meldung("wird geschickt …");
try {
const antwort = await fetch(url, {
method: "POST", credentials: "same-origin",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ topic, payload: typeof payload === "string" ? payload : JSON.stringify(payload) }),
});
const d = await antwort.json().catch(() => ({}));
if (!antwort.ok || !d.success) throw new Error(d.message || "HTTP " + antwort.status);
g.meldung("✓ an die Ventilsteuerung geschickt", "gut");
} catch (e) {
g.meldung("Nicht übernommen: " + e.message, "fehler");
}
zeichnen();
}
function zeichnen() {
wurzel.querySelectorAll("[data-zone]").forEach(karte => {
const key = karte.dataset.zone;
const zone = wateringZones[key];
if (!zone) return;
const z = wateringZonenZustand(zone);
const gemeldetLaeuft = z.state !== "aus" && z.state !== "--";
const laeuft = g.gilt("laeuft-" + key, gemeldetLaeuft);
const timer = wateringValue(wateringJson(zone.topic + "/Timers"), zone.valve);
const rest = timer !== null ? Math.abs(wateringSeconds(timer)) : null;
const $ = name => karte.querySelector('[data-w="' + name + '"]');
let zustand;
if (laeuft && gemeldetLaeuft) zustand = (z.paused ? "pausiert" : "läuft") + (rest ? " · noch " + zeitText(rest) : "");
else if (laeuft) zustand = "startet …";
else if (z.bewaessert) zustand = "heute " + labelText(z.heute.label) + " gegossen";
else if (z.state === "--") zustand = "Zustand unbekannt";
else zustand = "heute noch nicht gegossen";
setze($("zustand"), zustand);
const pille = $("pille");
setze(pille, laeuft ? (z.paused ? "pausiert" : "läuft") : z.bewaessert ? "gegossen" : "");
pille.className = "lade-pille" + (laeuft ? (z.paused ? " warn" : " an") : z.bewaessert ? " blau" : "");
karte.classList.toggle("laeuft", laeuft);
const knopf = $("knopf");
knopf.className = "lade-start wasser-knopf" + (laeuft ? " laeuft" : "");
setze($("knopfText"), laeuft ? "Stoppen" : "Gießen · " + dauer[key] + S + "min");
knopf.dataset.aktion = laeuft ? "stopp" : "start";
karte.querySelectorAll("[data-min]").forEach(c => c.setAttribute("aria-pressed", Number(c.dataset.min) === dauer[key] ? "true" : "false"));
const verlauf = wateringHistory(zone.topic, zone.valve);
const vortag = wateringDayTotal(zone.topic, "LastWateringDay", "wateringDurationDaySecs", zone.valve, false);
const teile = [];
if (verlauf.dateTime !== "--") teile.push("zuletzt " + verlauf.dateTime + (verlauf.duration !== "--" ? " · " + labelText(verlauf.duration) : ""));
if (vortag.label !== "--" && vortag.sekunden > 0) teile.push("letzter Gießtag " + labelText(vortag.label));
setze($("info"), teile.join(" · "));
});
wurzel.querySelectorAll("[data-ablauf]").forEach(c => {
const modi = JSON.parse(c.dataset.ablauf);
const aktiv = g.gilt("ablauf-" + c.textContent, Object.entries(modi).every(([t, m]) => wateringAutoActive(t, m)));
c.setAttribute("aria-pressed", aktiv ? "true" : "false");
});
}
wurzel.querySelectorAll("[data-zone]").forEach(karte => {
const key = karte.dataset.zone;
const zone = wateringZones[key];
karte.querySelectorAll("[data-min]").forEach(c => c.addEventListener("click", () => {
dauer[key] = Number(c.dataset.min);
zeichnen();
}));
karte.querySelector('[data-w="knopf"]').addEventListener("click", e => {
const start = e.currentTarget.dataset.aktion === "start";
g.merke("laeuft-" + key, start);
senden(zone.topic + "/set", start
? { valve: zone.valve, set: "open", timer: dauer[key] }
: { valve: zone.valve, set: "close" });
});
});
wurzel.querySelectorAll("[data-ablauf]").forEach(c => c.addEventListener("click", async () => {
const modi = JSON.parse(c.dataset.ablauf);
const aktiv = c.getAttribute("aria-pressed") === "true";
g.merke("ablauf-" + c.textContent, !aktiv);
for (const [topic, modus] of Object.entries(modi)) {
await senden(topic + "/auto", aktiv ? "Stop" : modus);
}
}));
g.takt(zeichnen);
};
})();