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:
@@ -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);
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user