/*
* Raum-Modal: Beschattung, Heizung, Licht und Schalter eines Raums.
*
* Das Markup kommt fertig aus ajax/room.php (Bedienformen in
* restricted/roomControls.php); hier haengen die Zuhoerer an den
* data-Attributen, und hier wird gezeichnet, was sich beim Bedienen aendert:
* Zustand der Reiter, Behang-Bild, Heizring, Lampen.
*
* Wie im Lade-, Heiz- und Bewaesserungsfenster gilt jede Wahl sofort - es
* gibt keinen Speichern-Knopf.
*
* Aufgerufen von openRoomView() in js/solar/homeMQTT.js; die Ist-Werte der
* Heizung kommen ueber raumfensterMqtt() aus dem laufenden MQTT-Strom.
*/
const RF_S = " "; // schmales Leerzeichen vor Einheiten
let raumfenster = null; // {wurzel, raum, thermostat}
function raumfensterStarten(wurzel, raum, thermostat) {
raumfenster = { wurzel: wurzel, raum: raum, thermostat: thermostat };
wurzel.querySelectorAll("[data-rf-reiter]").forEach(k => k.addEventListener("click", () => rfReiter(wurzel, k)));
wurzel.querySelectorAll(".rf-behang").forEach(rfBehangStarten);
wurzel.querySelectorAll(".rf-alle [data-alle]").forEach(k => k.addEventListener("click", () => rfAlle(k)));
wurzel.querySelectorAll(".rf-lampe").forEach(rfLampeStarten);
wurzel.querySelectorAll(".rf-schalter").forEach(k => k.addEventListener("click", () => rfSchalten(k, k)));
// Mehr-Menue und "Sonstiges": Kommandos so, wie sie in der Datenbank stehen.
wurzel.querySelectorAll("button[data-cmd]").forEach(k => k.addEventListener("click", () => rfSchalte(k)));
const heiz = wurzel.querySelector("[data-rf-heiz-teil]");
if (heiz) rfHeizungStarten(heiz);
rfStatus();
raumfensterMqtt();
// Jede Jalousie im Raum einmal an der Box nachfragen. Der Wert im Modal
// kommt aus actor_states und ist damit hoechstens so frisch wie der letzte
// Poll des Runners - und wer das Modal waehrend einer Fahrt aufmacht, saehe
// sonst den Stand von davor.
wurzel.querySelectorAll(".rf-behang").forEach(k => rfJalousieVerfolgen(k, null));
}
/* --- Reiter ---------------------------------------------------------------- */
function rfReiter(wurzel, knopf) {
wurzel.querySelectorAll("[data-rf-reiter]").forEach(k => k.setAttribute("aria-selected", String(k === knopf)));
wurzel.querySelectorAll("[data-rf-flaeche]").forEach(f => { f.hidden = f.dataset.rfFlaeche !== knopf.dataset.rfReiter; });
}
/** Die Zeile unter jedem Reiter: was in dieser Gruppe gerade los ist. */
function rfStatus() {
if (!raumfenster) return;
const w = raumfenster.wurzel;
const setze = (form, text) => { const el = w.querySelector("[data-rf-status='" + form + "']"); if (el) el.textContent = text; };
const behaenge = [...w.querySelectorAll(".rf-behang")];
const bekannt = behaenge.filter(k => k.dataset.pos !== "");
const zu = bekannt.filter(k => Number(k.dataset.pos) >= 95).length;
setze("beschattung", !bekannt.length ? behaenge.length + " Geräte"
: zu === 0 ? "alle offen" : zu === bekannt.length ? "alle zu" : zu + " von " + bekannt.length + " zu");
const an = w.querySelectorAll(".rf-lampe[data-an='1']").length;
setze("licht", an ? an + " an" : "alles aus");
const schalter = w.querySelectorAll(".rf-schalter[aria-pressed='true']").length;
setze("schalter", schalter ? schalter + " an" : "alles aus");
const zahl = (sel, wort) => { const n = w.querySelectorAll(sel).length; return n + " " + wort; };
setze("sonstiges", zahl("[data-rf-flaeche='sonstiges'] .rf-karte", "Geräte"));
setze("messwerte", zahl("[data-rf-flaeche='messwerte'] .rf-karte", "Geräte"));
// "heizung" setzt rfHeizungZeichnen() mit der Ist-Temperatur.
}
/* --- Kommandos --------------------------------------------------------------- */
function rfRequest(action, body) {
return fetch("./ajax/room.php?action=" + action, {
method: "POST",
headers: { "Content-Type": "application/json", "Requested-With-Ajax": "ajax" },
body: JSON.stringify(body)
})
.then(a => a.json())
.catch(fehler => ({ error: fehler.message }));
}
/** Rueckmeldung unten im Fenster; nach ein paar Sekunden wieder der Hinweis. */
function rfMeldung(antwort) {
const ziel = document.getElementById("roomFeedback");
if (!ziel) return;
clearTimeout(rfMeldung.takt);
ziel.classList.toggle("fehler", !!antwort.error);
ziel.classList.toggle("gut", !antwort.error);
ziel.textContent = antwort.error ? antwort.error : "✓ " + antwort.ok;
rfMeldung.takt = setTimeout(() => {
ziel.classList.remove("fehler", "gut");
ziel.textContent = "Änderungen gelten sofort";
}, antwort.error ? 8000 : 4000);
}
/**
* Ein Kommando schicken. Eine Jalousie zeigt dabei "fährt…" und wird bis zum
* Stillstand verfolgt; alles andere ist kurz gesperrt, damit man nicht
* nachtippt.
*/
function rfKommando(cmdId, params, element) {
const karte = element.closest(".raum-geraet") || element;
const behang = karte.classList.contains("rf-behang");
if (behang) {
karte.dataset.faehrt = "1";
rfBehangZeichnen(karte);
} else {
karte.classList.add("raum-warte");
}
const auftrag = rfRequest("command", { command_id: cmdId, params: params });
if (behang) rfJalousieVerfolgen(karte, auftrag);
return auftrag.then(antwort => {
karte.classList.remove("raum-warte");
if (antwort.error && behang) {
delete karte.dataset.faehrt;
rfBehangZeichnen(karte);
}
rfMeldung(antwort);
return antwort;
});
}
/** Ein Knopf mit data-cmd; Parameter stehen, wenn noetig, in seiner Zeile. */
function rfSchalte(knopf) {
const zeile = knopf.closest("[data-cmdrow]");
const params = {};
if (zeile) zeile.querySelectorAll("[data-param]").forEach(f => { params[f.dataset.param] = f.value; });
rfKommando(Number(knopf.dataset.cmd), params, knopf);
}
/* --- Beschattung -------------------------------------------------------------- */
function rfLamellenWort(n) {
n = Number(n);
return n <= 12 ? "offen" : n >= 88 ? "zu" : Math.abs(n - 50) <= 12 ? "halb" : n + RF_S + "%";
}
function rfStandText(pos, neig) {
if (pos === "") return "";
const p = Number(pos);
let text = p <= 2 ? "offen" : p >= 98 ? "zu" : p + RF_S + "% zu";
if (neig !== "" && p > 2) text += " · Lamellen " + rfLamellenWort(neig);
return text;
}
function rfBehangZeichnen(karte) {
const d = karte.dataset;
const pos = d.pos, neig = d.neig;
const teil = karte.querySelector(".rf-behang-teil");
if (teil) teil.style.height = (pos === "" ? 0 : Number(pos)) + "%";
const lamellen = karte.querySelector(".rf-lamellen");
if (lamellen) lamellen.style.setProperty("--kipp", 0.2 + 0.8 * (neig === "" ? 0 : Number(neig)) / 100);
const faehrt = d.faehrt === "1";
karte.classList.toggle("rf-bewegt", faehrt);
const pille = karte.querySelector("[data-rf-stand]");
if (pille) {
pille.textContent = faehrt ? "fährt…" : rfStandText(pos, neig);
pille.classList.toggle("faehrt", faehrt);
}
const aufMy = d.myPos !== "" && pos !== "" && Math.abs(Number(pos) - Number(d.myPos)) <= 2
&& (d.myNeig === "" || neig === "" || Math.abs(Number(neig) - Number(d.myNeig)) <= 2);
const myKnopf = karte.querySelector("[data-rf-fahrt='my']");
if (myKnopf) myKnopf.classList.toggle("aktiv", aufMy && !faehrt);
karte.querySelectorAll("[data-rf-pos]").forEach(k =>
k.setAttribute("aria-checked", String(pos !== "" && !aufMy && Math.abs(Number(k.dataset.rfPos) - Number(pos)) <= 2)));
karte.querySelectorAll("[data-rf-neig]").forEach(k =>
k.setAttribute("aria-checked", String(neig !== "" && Math.abs(Number(k.dataset.rfNeig) - Number(neig)) <= 2)));
const my = karte.querySelector("[data-rf-my]");
if (my) {
const myText = (p, n) => p + RF_S + "%" + (n !== "" && n !== null && typeof n !== "undefined" ? " · Lamellen " + rfLamellenWort(n) : "");
if (d.myFrage === "1") {
my.className = "rf-my-zeile frage";
my.innerHTML = "my von " + myText(d.myPos, d.myNeig) + " auf " + myText(pos, neig) + " ändern?"
+ ""
+ "";
} else {
my.className = "rf-my-zeile";
my.innerHTML = "my-Position " + myText(d.myPos, d.myNeig) + ""
+ (aufMy || faehrt || pos === "" || d.mySpeichert === "1" ? ""
: "")
+ (d.mySpeichert === "1" ? "wird gespeichert…" : "");
}
}
rfStatus();
}
function rfBehangStarten(karte) {
const d = karte.dataset;
rfBehangZeichnen(karte);
karte.querySelectorAll("[data-rf-fahrt]").forEach(k => k.addEventListener("click", () => {
const cmd = d[k.dataset.rfFahrt];
if (!cmd) return;
rfKommando(Number(cmd), {}, karte);
}));
const hoehe = function (wert) {
const params = {};
params[d.hoeheParam] = wert;
// Die Hoehe nimmt die Neigung mit: eine Hoehenfahrt rastet die Lamellen
// um, und der Server faehrt sie nur dann wieder an, wenn der Befehl ein
// Neigungsziel dabei hat (siehe sendeTahoma() in restricted/commands.php).
if (d.hoeheMit) params[d.hoeheMit] = d.neig === "" ? 0 : Number(d.neig);
d.pos = String(wert);
rfKommando(Number(d.hoeheCmd), params, karte);
};
karte.querySelectorAll("[data-rf-pos]").forEach(k => k.addEventListener("click", () => hoehe(Number(k.dataset.rfPos))));
karte.querySelectorAll("[data-rf-neig]").forEach(k => k.addEventListener("click", () => {
const params = {};
params[d.neigParam] = Number(k.dataset.rfNeig);
d.neig = k.dataset.rfNeig;
rfKommando(Number(d.neigCmd), params, karte);
}));
// Im Fensterbild ziehen: die Hoehe fein einstellen. Waehrend des Ziehens
// nur das Bild, geschickt wird beim Loslassen - sonst bekaeme die Jalousie
// bei jedem Pixel ein neues Ziel.
const fenster = karte.querySelector("[data-rf-zieh]");
if (fenster && d.hoeheCmd) {
let zug = null;
const wert = e => {
const r = fenster.querySelector(".rf-glas").getBoundingClientRect();
return Math.max(0, Math.min(100, (e.clientY - r.top) / r.height * 100));
};
fenster.addEventListener("pointerdown", e => {
zug = e.pointerId;
d.zieht = "1";
fenster.setPointerCapture(e.pointerId);
karte.querySelector(".rf-behang-teil").style.height = wert(e) + "%";
e.preventDefault();
});
fenster.addEventListener("pointermove", e => {
if (zug !== e.pointerId) return;
const w = wert(e);
karte.querySelector(".rf-behang-teil").style.height = w + "%";
const pille = karte.querySelector("[data-rf-stand]");
if (pille) pille.textContent = rfStandText(String(Math.round(w)), d.neig);
});
const ende = e => {
if (zug !== e.pointerId) return;
zug = null;
delete d.zieht;
hoehe(Math.round(wert(e) / 5) * 5);
};
fenster.addEventListener("pointerup", ende);
fenster.addEventListener("pointercancel", () => { zug = null; delete d.zieht; rfBehangZeichnen(karte); });
}
// my-Position speichern: erst nachfragen, dann schreibt der Server den
// Stand, den die Box meldet, in den Motor.
karte.addEventListener("click", e => {
const k = e.target.closest("[data-rf-my-tun]");
if (!k) return;
const tun = k.dataset.rfMyTun;
if (tun === "frage") d.myFrage = "1";
if (tun === "nein") delete d.myFrage;
if (tun === "ja") {
delete d.myFrage;
d.mySpeichert = "1";
rfRequest("my", { actor: Number(d.actor) }).then(antwort => {
delete d.mySpeichert;
if (!antwort.error) {
d.myPos = String(antwort.myPosition);
if (antwort.myNeigung !== null && typeof antwort.myNeigung !== "undefined") d.myNeig = String(antwort.myNeigung);
}
rfMeldung(antwort);
rfBehangZeichnen(karte);
});
}
rfBehangZeichnen(karte);
});
}
/** "Alle im Raum": jedem Behang sein eigenes Kommando. */
function rfAlle(knopf) {
const was = knopf.dataset.alle;
knopf.closest("[data-rf-flaeche]").querySelectorAll(".rf-behang").forEach(karte => {
const d = karte.dataset;
if (was === "sicht") {
if (!d.hoeheMit) return;
const params = {};
params[d.hoeheParam] = 100;
params[d.hoeheMit] = 50;
d.pos = "100";
d.neig = "50";
rfKommando(Number(d.hoeheCmd), params, karte);
return;
}
if (d[was]) rfKommando(Number(d[was]), {}, karte);
});
}
// Takt und Grenzen der Verfolgung. Dieselben Zahlen wie die Konstanten
// JALOUSIE_* in restricted/commands.php: die Box meldet core:MovingState
// traege, kurz nach dem Kommando steht dort noch "faehrt nicht" - deshalb
// der Vorlauf, in dem ein Stillstand nichts bedeutet.
const RF_JALOUSIE_TAKT_MS = 2000;
const RF_JALOUSIE_VORLAUF_MS = 8000;
const RF_JALOUSIE_MAX_MS = 120000;
// Wie lange nach dem Ende des Auftrags noch nachgesehen wird. Eine
// Neigungsfahrt setzt core:MovingState nie (siehe warteAufJalousie() in
// restricted/commands.php), dauert aber gemessen bis zu fuenfzehn Sekunden
// von Anschlag zu Anschlag - ohne diesen Nachlauf stuende der Wert von vor
// dem letzten Schwenk in der Anzeige.
const RF_JALOUSIE_NACHLAUF_MS = 14000;
/**
* Nach einem Fahrbefehl abwarten, bis die Jalousie steht, und dann ihren
* wirklichen Stand zeigen.
*
* actor_states schreibt sonst nur der Runner fort, und der fragt die
* Tahoma-Box alle fuenf Minuten ab. Hier wird genau dieses eine Geraet
* gefragt; ajax/room.php haelt den frischen Wert gleich in der Tabelle fest.
*
* Waehrend der Fahrt wird kein Zwischenstand angezeigt. An der Anlage
* gemessen: die Hoehe (core:ClosureState) steht die ganze Fahrt ueber auf dem
* Wert von vor dem Kommando und springt erst am Ende um, und die Neigung
* meldet ausgerechnet die Umwege der Kugelschreiber-Mechanik.
*/
function rfJalousieVerfolgen(karte, auftrag) {
const actor = Number(karte.dataset.actor);
if (!actor) return;
// Ohne Auftrag - beim Oeffnen des Modals - gibt es keine Traegheit
// abzuwarten und nichts nachlaufen zu lassen.
const vorlaufNoetig = !!auftrag;
// Ein zweiter Fahrbefehl schiebt nur das Ende hinaus. Zwei Schleifen
// wuerden dieselbe Box doppelt so oft fragen, ohne mehr zu zeigen.
karte.dataset.jalousieBis = String(Date.now() + RF_JALOUSIE_MAX_MS);
if (karte.dataset.jalousieLaeuft) return;
karte.dataset.jalousieLaeuft = "1";
const beginn = Date.now();
let gefahren = false;
// Fertig ist es erst, wenn zwei Abfragen im Stillstand dasselbe sagen: die
// Box meldet das Ende der Fahrt, bevor die Werte nachgezogen haben.
let letzterStand = null;
// Solange der Auftrag laeuft, ist nichts entschieden: die
// Kugelschreiber-Mechanik schickt zwei Befehle mit einer Wartezeit
// dazwischen, und zwischen ihnen steht die Jalousie still.
let ruheAb = 0;
if (auftrag) {
ruheAb = Infinity;
Promise.resolve(auftrag).then(
function () { ruheAb = Date.now() + RF_JALOUSIE_NACHLAUF_MS; },
function () { ruheAb = Date.now(); }
);
}
function aufhoeren() {
delete karte.dataset.jalousieLaeuft;
delete karte.dataset.jalousieBis;
delete karte.dataset.faehrt;
rfBehangZeichnen(karte);
}
function runde() {
rfRequest("jalousie", { actor: actor }).then(stand => {
// Ein Fehler soll auffallen, sonst bliebe die Anzeige still stehen.
if (stand && stand.error) rfMeldung(stand);
if (!stand || stand.error || !document.body.contains(karte)) {
aufhoeren();
return;
}
const abgelesen = stand.faehrt ? null : String(stand.position) + "/" + String(stand.neigung);
const steht = abgelesen !== null && abgelesen === letzterStand;
letzterStand = abgelesen;
if (stand.faehrt) {
gefahren = true;
} else if (Date.now() >= ruheAb) {
rfJalousieAnzeigen(karte, stand);
}
if (Date.now() > Number(karte.dataset.jalousieBis)) {
aufhoeren();
return;
}
// Beim blossen Aufmachen eines Raums, in dem sich nichts geruehrt hat,
// bleibt es bei einer Abfrage je Jalousie.
if (!vorlaufNoetig && !gefahren && !stand.faehrt) {
aufhoeren();
return;
}
if (!vorlaufNoetig && stand.faehrt) {
karte.dataset.faehrt = "1";
rfBehangZeichnen(karte);
}
const fertig = steht && Date.now() >= ruheAb
&& (gefahren || !vorlaufNoetig || Date.now() - beginn >= RF_JALOUSIE_VORLAUF_MS);
if (fertig) {
aufhoeren();
return;
}
setTimeout(runde, RF_JALOUSIE_TAKT_MS);
});
}
// Beim Oeffnen sofort nachsehen, nach einem Kommando erst im Takt: da
// stuende die Box ohnehin noch auf dem alten Wert.
setTimeout(runde, auftrag ? RF_JALOUSIE_TAKT_MS : 0);
}
/** Die gemeldete Stellung uebernehmen - nur im Stillstand, siehe oben. */
function rfJalousieAnzeigen(karte, stand) {
const d = karte.dataset;
// Wer gerade im Bild zieht, bestimmt.
if (d.zieht) return;
const setze = (feld, wert) => { if (wert !== null && typeof wert !== "undefined") d[feld] = String(wert); };
setze("pos", stand.position);
if (d.neigCmd) setze("neig", stand.neigung);
if (d.myPos !== undefined && karte.querySelector("[data-rf-my]")) {
setze("myPos", stand.myPosition);
if (d.neigCmd) setze("myNeig", stand.myNeigung);
}
rfBehangZeichnen(karte);
}
/* --- Heizung ------------------------------------------------------------------ */
const RF_RING_R = 70;
const RF_RING_UMFANG = 2 * Math.PI * RF_RING_R;
const RF_RING_BOGEN = RF_RING_UMFANG * 270 / 360;
const RF_HEIZ_HALTEN_MS = 8000; // so lange gilt die eigene Wahl vor dem MQTT-Stand
function rfHeizungStarten(teil) {
const min = Number(teil.dataset.min), max = Number(teil.dataset.max);
const zustand = { soll: null, eigen: 0, senden: null };
teil._rf = zustand;
teil.querySelector(".rf-ring-spur").setAttribute("stroke-dasharray", RF_RING_BOGEN + " " + RF_RING_UMFANG);
teil.querySelector(".rf-ring-griff").setAttribute("stroke-dasharray", RF_RING_BOGEN + " " + RF_RING_UMFANG);
const waehlen = function (wert, sofort) {
wert = Math.max(min, Math.min(max, Math.round(wert)));
zustand.soll = wert;
zustand.eigen = Date.now();
rfHeizungZeichnen(teil);
clearTimeout(zustand.senden);
// Mehrmals - oder + tippen ergibt einen Befehl, nicht fuenf.
zustand.senden = setTimeout(() => {
teil.classList.add("raum-warte");
rfRequest("temp", { room: raumfenster.raum, temp: wert }).then(antwort => {
teil.classList.remove("raum-warte");
rfMeldung(antwort);
});
}, sofort ? 0 : 700);
};
teil.querySelectorAll("[data-rf-schritt]").forEach(k => k.addEventListener("click", () => {
if (zustand.soll === null) return;
waehlen(zustand.soll + Number(k.dataset.rfSchritt), false);
}));
teil.querySelectorAll("[data-rf-soll]").forEach(k => k.addEventListener("click", () => waehlen(Number(k.dataset.rfSoll), true)));
// Den Soll-Punkt auf dem Ring ziehen oder auf den Ring tippen. Waehrend des
// Ziehens wird nur gezeichnet, geschickt wird beim Loslassen.
const svg = teil.querySelector(".rf-ring svg");
const ausZeiger = e => {
const r = svg.getBoundingClientRect();
const grad = Math.atan2(e.clientY - (r.top + r.height / 2), e.clientX - (r.left + r.width / 2)) * 180 / Math.PI;
let a = (grad - 135 + 720) % 360;
if (a > 270) a = a > 315 ? 0 : 270;
return min + a / 270 * (max - min);
};
let zug = null;
svg.addEventListener("pointerdown", e => {
if (!e.target.closest("[data-rf-ringzug]")) return;
zug = e.pointerId;
svg.setPointerCapture(e.pointerId);
teil.classList.add("zieht");
zustand.soll = Math.round(ausZeiger(e));
zustand.eigen = Date.now();
rfHeizungZeichnen(teil);
e.preventDefault();
});
svg.addEventListener("pointermove", e => {
if (zug !== e.pointerId) return;
zustand.soll = Math.round(ausZeiger(e));
zustand.eigen = Date.now();
rfHeizungZeichnen(teil);
});
svg.addEventListener("pointerup", e => {
if (zug !== e.pointerId) return;
zug = null;
teil.classList.remove("zieht");
waehlen(zustand.soll, true);
});
svg.addEventListener("pointercancel", () => { zug = null; teil.classList.remove("zieht"); });
const punkt = teil.querySelector(".rf-ring-soll");
punkt.addEventListener("keydown", e => {
const schritt = { ArrowUp: 1, ArrowRight: 1, ArrowDown: -1, ArrowLeft: -1 }[e.key];
if (!schritt || zustand.soll === null) return;
e.preventDefault();
waehlen(zustand.soll + schritt, false);
});
}
/** Ist, Soll und Hinweis in den Heizungsteil schreiben. */
function rfHeizungZeichnen(teil) {
const z = teil._rf;
const werte = (raumfenster && typeof mqttZweig === "function") ? mqttZweig(raumfenster.thermostat) : null;
const zahl = v => (v === null || v === "" || typeof v === "undefined" || isNaN(Number(v))) ? null : Number(v);
const ist = werte ? zahl(werte["Temp[degC]"]) : null;
const gemeldet = werte ? zahl(werte["Set Temp[degC]"]) : null;
// Die eigene Wahl gilt, bis das Thermostat sie bestaetigt hat.
if (gemeldet !== null && (z.soll === null || Date.now() - z.eigen > RF_HEIZ_HALTEN_MS)) z.soll = gemeldet;
const soll = z.soll;
const min = Number(teil.dataset.min), max = Number(teil.dataset.max);
const anteil = v => Math.max(0, Math.min(1, (v - min) / (max - min)));
const komma = (v, stellen) => v.toFixed(stellen).replace(".", ",");
const feld = name => teil.querySelector("[data-rf-heiz='" + name + "']");
teil.querySelector(".rf-ring-ist").setAttribute("stroke-dasharray",
(ist === null ? 0 : RF_RING_BOGEN * anteil(ist)) + " " + RF_RING_UMFANG);
const punkt = teil.querySelector(".rf-ring-soll");
punkt.style.transform = "rotate(" + (135 + 270 * anteil(soll === null ? min : soll)) + "deg)";
punkt.style.visibility = soll === null ? "hidden" : "";
if (soll !== null) punkt.setAttribute("aria-valuenow", soll);
feld("ist").innerHTML = ist === null ? "–" : komma(ist, 1) + "" + RF_S + "°C";
feld("soll").textContent = "Soll " + (soll === null ? "–" : komma(soll, 0) + RF_S + "°C");
const chip = soll === null ? null : teil.querySelector("[data-rf-soll='" + soll + "']");
feld("wert").innerHTML = soll === null ? "–"
: soll + RF_S + "°C" + "" + (chip ? chip.firstChild.textContent.trim() : " ") + "";
teil.querySelectorAll("[data-rf-soll]").forEach(k => k.setAttribute("aria-pressed", String(k === chip)));
const heizt = werte && String(werte["Heating"]) === "true";
const hinweis = feld("hinweis");
let text = "";
if (ist !== null && soll !== null) {
if (heizt) text = ist < soll ? "Heizt – noch " + komma(soll - ist, 1) + RF_S + "°C bis zum Soll." : "Heizt gerade.";
else if (ist > soll + 0.2) text = "Heizt nicht – der Raum ist " + komma(ist - soll, 1) + RF_S + "°C wärmer als das Soll.";
else text = "Heizt nicht – das Soll ist erreicht.";
}
hinweis.hidden = !text;
hinweis.classList.toggle("an", !!heizt);
hinweis.querySelector("span").textContent = text;
const feuchte = werte ? zahl(werte["rHum[%]"]) : null;
feld("feuchte").textContent = feuchte === null ? "–" : Math.round(feuchte) + RF_S + "% Luftfeuchte";
feld("puffer").hidden = !(werte && werte["mode"] == "Overheating");
const status = raumfenster && raumfenster.wurzel.querySelector("[data-rf-status='heizung']");
if (status) status.textContent = ist === null ? "–" : komma(ist, 1) + RF_S + "°C" + (heizt ? " · heizt" : "");
}
/** Aufgerufen bei jeder MQTT-Nachricht (homeSVG.updateValuesMQTT). */
function raumfensterMqtt() {
if (!raumfenster || !document.body.contains(raumfenster.wurzel)) return;
const teil = raumfenster.wurzel.querySelector("[data-rf-heiz-teil]");
if (teil && !teil.classList.contains("zieht")) rfHeizungZeichnen(teil);
}
/* --- Licht und Schalter --------------------------------------------------------- */
/** An/Aus schicken - kennt das Geraet nur "umschalten", tut es auch das. */
function rfSchalten(element, karte) {
const an = karte.classList.contains("rf-schalter") ? karte.getAttribute("aria-pressed") !== "true" : karte.dataset.an !== "1";
const cmd = (an ? karte.dataset.ein : karte.dataset.aus) || karte.dataset.um;
if (!cmd) return;
if (karte.classList.contains("rf-schalter")) {
karte.setAttribute("aria-pressed", String(an));
karte.querySelector(".rf-st").textContent = an ? "an" : "aus";
} else {
karte.dataset.an = an ? "1" : "";
rfLampeZeichnen(karte);
}
rfStatus();
rfKommando(Number(cmd), {}, element);
}
function rfLampeZeichnen(karte) {
const an = karte.dataset.an === "1";
karte.classList.toggle("aus", !an);
karte.querySelector("[data-rf-birne]").setAttribute("aria-pressed", String(an));
const hell = karte.querySelector(".rf-hell");
let text = an ? "an" : "aus";
if (hell) {
const min = Number(hell.min), max = Number(hell.max);
const prozent = Math.round((Number(hell.value) - min) / (max - min) * 100);
hell.style.setProperty("--wert", prozent + "%");
if (an) text += " · " + prozent + RF_S + "%";
}
karte.querySelector("[data-rf-lampe-stand]").textContent = text;
}
function rfLampeStarten(karte) {
rfLampeZeichnen(karte);
karte.querySelector("[data-rf-birne]").addEventListener("click", e => rfSchalten(e.currentTarget, karte));
const hell = karte.querySelector(".rf-hell");
if (hell) {
hell.addEventListener("input", () => rfLampeZeichnen(karte));
hell.addEventListener("change", () => {
const params = {};
params[hell.dataset.param] = hell.value;
rfKommando(Number(hell.dataset.cmd), params, hell);
});
}
// Farbe: das Geraet will drei Zahlen 0-255, zerlegt wird deshalb hier.
const farben = karte.querySelector(".rf-farben");
if (farben) {
const setzen = hex => {
karte.style.setProperty("--farbe", hex);
farben.querySelectorAll("[data-farbe]").forEach(k => k.setAttribute("aria-pressed", String(k.dataset.farbe === hex)));
const params = {};
params[farben.dataset.red] = parseInt(hex.substr(1, 2), 16);
params[farben.dataset.green] = parseInt(hex.substr(3, 2), 16);
params[farben.dataset.blue] = parseInt(hex.substr(5, 2), 16);
rfKommando(Number(farben.dataset.cmd), params, farben);
};
farben.querySelectorAll("[data-farbe]").forEach(k => k.addEventListener("click", () => setzen(k.dataset.farbe)));
const eigen = farben.querySelector("input[type=color]");
eigen.addEventListener("input", () => karte.style.setProperty("--farbe", eigen.value));
eigen.addEventListener("change", () => setzen(eigen.value));
}
// Preset, Effekt und andere Listen: wenige Eintraege als Chips, lange
// Listen als