Files
adminandClaude Opus 5 f83f8fa2fd Heizfenster: eingestellte Leistung bleibt stehen
Das Geraet meldet nur die Leistung, die es gerade zieht. Nach acht Sekunden
fiel die Anzeige deshalb auf "gemessen/100, sonst 30" zurueck - aus fest
eingestellten 0,1 kW wurden 3 kW, sobald der Heizstab nicht zog. Die eigene
Wahl gilt jetzt, solange das Fenster offen ist; die Automatik meldet das
Geraet weiterhin selbst.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 11:40:49 +02:00

286 lines
13 KiB
JavaScript
Raw Permalink 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;
// Der Heizstab meldet seine Betriebsart und die Leistung, die er gerade
// zieht - nicht die, die eingestellt ist. Wer 0,1 kW fest einstellt und
// dessen Puffer schon warm ist, sieht also 0 W; daraus liesse sich die
// Einstellung nicht zurueckrechnen (vorher sprang die Anzeige dann auf
// 3 kW). Die eigene Wahl bleibt deshalb stehen, solange das Fenster
// offen ist.
const gewaehlt = { modus: null, stufe: null };
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 gemeldet = text("solarManager/heatMode");
const gemeldetFest = gemeldet !== undefined && gemeldet !== "eco";
// Kurz nach dem Absenden meldet das Geraet noch den alten Stand -
// solange gilt die eigene Wahl.
const frisch = g.gilt("frisch", false) === true;
let modus;
if (frisch && gewaehlt.modus) {
modus = gewaehlt.modus;
} else if (!gemeldetFest) {
// Automatik sagt das Geraet eindeutig - auch, wenn sie woanders
// eingeschaltet wurde.
modus = "auto";
if (gewaehlt.modus && gewaehlt.modus !== "auto") gewaehlt.modus = null;
} else if (gewaehlt.modus === "fest" || gewaehlt.modus === "aus") {
// "fest" und "aus" sind fuer das Geraet dasselbe ("man"); welches von
// beiden gemeint war, weiss nur, wer es gewaehlt hat.
modus = gewaehlt.modus;
} else {
modus = w >= 20 ? "fest" : "aus";
gewaehlt.modus = null;
}
const stufe = gewaehlt.stufe !== null
? gewaehlt.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"));
}
/** Eigene Wahl festhalten; "frisch" ueberbrueckt, bis das Geraet meldet. */
function waehlen(modus, stufe) {
gewaehlt.modus = modus;
if (stufe !== undefined) gewaehlt.stufe = stufe;
g.merke("frisch", true);
}
wurzel.querySelectorAll("[data-heiz-modus]").forEach(b => b.addEventListener("click", () => {
const modus = b.dataset.heizModus;
waehlen(modus);
senden(modus === "auto" ? 0 : modus === "aus" ? -1 : Number($("regler").value));
}));
$("regler").addEventListener("input", () => { ziehen = true; gewaehlt.stufe = Number($("regler").value); zeichnen(); });
$("regler").addEventListener("change", () => {
ziehen = false;
waehlen("fest", Number($("regler").value));
senden(Number($("regler").value));
});
wurzel.querySelectorAll("[data-heiz-stufe]").forEach(c => c.addEventListener("click", () => {
const stufe = Number(c.dataset.heizStufe);
waehlen("fest", stufe);
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);
};
})();