Files
Smart-Dashboard/js/solar/bedienfenster.js
T
adminandClaude Opus 5 a50905bb6c Heizstab kann ganz aus
Dritte Betriebsart neben Automatik und Fest: Aus schickt eine feste Leistung
von null (setMode=-1). In der Automatik liefe der Heizstab beim naechsten
Ueberschuss wieder an. Erkannt wird der Zustand daran, dass das Geraet
feste Betriebsart, aber keine Leistung meldet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 10:08:13 +02:00

255 lines
12 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ==========================================================================
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";
// Der Heizstab meldet seine Betriebsart und die Leistung, die er gerade
// zieht - nicht die eingestellte. "Fest" ohne Leistung ist deshalb das,
// was hier "Aus" heisst: eine feste Leistung von null.
const modus = g.gilt("modus", !gemeldetFest ? "auto" : w >= 20 ? "fest" : "aus");
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" : s.modus === "aus" ? "aus" : "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."
: s.modus === "aus"
? "Bleibt aus, auch wenn die Anlage Überschuss hat. Das Warmwasser macht dann allein die Therme."
: "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 : modus === "aus" ? -1 : 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);
};
})();