Das Modal zeigte jedes Kommando so, wie es in der Datenbank steht: eine
Jalousie kam mit acht Rohbefehlen daher, darunter "Winken", die Farbe einer
Lampe als drei Zahlenfelder 0-255, ein Stromzaehler als abgeschnittene
Textzeile. Die Solltemperatur stand doppelt da - einmal als Schieberegler,
einmal als Zahlenfeld der Thermostat-Karte.
Jetzt entscheidet restricted/roomControls.php, welches Geraet welches
Bedienelement bekommt, und zwar an seinen Faehigkeiten statt am Typnamen:
hat es ein "setClosure"? ein An/Aus-Paar? drei Parameter rot/gruen/blau?
Damit bekommt auch ein neu gefundenes Geraet sein passendes Element, ohne
dass jemand einen Typnamen nachtraegt.
Beschattung Auf/Stop/Zu, darunter Position und Neigung als Schieber
auf dem zuletzt gemeldeten Stand; "Winken" und
"my-Position" im Mehr-Menue
Licht Kippschalter, Helligkeit, Farbwaehler des Browsers,
Effekt und Preset als Auswahl
Schalten ein Kippschalter statt dreier Knoepfe
Messwerte Wertegitter, die sechs wichtigsten sichtbar - beim
Wasserzaehler standen vorher MAC-Adresse und freier
Speicher vorn und der Verbrauch hinter "12 weitere"
Heizung nur noch der Schieberegler, daneben Ist-Temperatur und
Feuchte live aus dem MQTT-Strom
Ab zwei Geraeten einer Art ordnet eine Ueberschrift; darunter waere sie
nur Aufwand. Schieberegler schicken beim Loslassen, nicht beim Ziehen -
sonst bekaeme eine Jalousie bei jedem Pixel ein neues Ziel. Waehrend ein
Kommando unterwegs ist, sperrt sich die Karte sichtbar: ein "Zu" dauert
ueber eine Minute, und ohne Sperre tippt man in der Zeit nach.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
354 lines
13 KiB
JavaScript
354 lines
13 KiB
JavaScript
|
|
const homeMQTT = {
|
|
getMQTT: function () {
|
|
const id = Math.random().toString(36).substring(7);
|
|
const topic = "#";
|
|
const connection = "wss://mqtt.nas.el-wa.org:443"
|
|
mqttsolarTreeDone = false;
|
|
// const connection = "ws://username:password@37.97.203.138:8083" // Works
|
|
// const connection = "wss://public:public@public.cloud.shiftr.io" // Works
|
|
const client = mqtt.connect(connection, {
|
|
rejectUnauthorized: false,
|
|
});
|
|
|
|
client.on("message", messageReceived);
|
|
client.on("connect", function () {
|
|
client.subscribe("Raumtemp/#");
|
|
});
|
|
client.on("error", function (error) {
|
|
//alert("MQTT Error: " + error);
|
|
});
|
|
client.on('end', function () {
|
|
setTimeout(getMQTT, 5000);
|
|
alert("MQTT Disconnected, try to reconnect in 5 secs.");
|
|
})
|
|
|
|
function getNestedProp(obj, path) {
|
|
return path.split('/').reduce((acc, key) => acc && acc[key], obj);
|
|
}
|
|
function setNestedProp(obj, path, value) {
|
|
var schema = obj; // a moving reference to internal objects within obj
|
|
var pList = path.split('/');
|
|
var len = pList.length;
|
|
for (var i = 0; i < len - 1; i++) {
|
|
var elem = pList[i];
|
|
if (!schema[elem]) schema[elem] = {}
|
|
schema = schema[elem];
|
|
}
|
|
|
|
schema[pList[len - 1]] = value;
|
|
}
|
|
|
|
function messageReceived(topic, message) {
|
|
setNestedProp(mqttData, topic, message);
|
|
setTimeout(function () { homeSVG.updateValuesMQTT(mqttData) }, 200); //give the object tree some time to build up and receive all values
|
|
}
|
|
}
|
|
}
|
|
|
|
const homeSVG = {
|
|
updateCnt: 99,
|
|
fillElementArray: function () {
|
|
|
|
},
|
|
updateValuesMQTT: function (mqttData) {
|
|
// homeRooms liefert restricted/rooms.php (in home.php eingebettet).
|
|
// Jeder Raum bekommt die gleiche Behandlung - Raeume kommen dort dazu,
|
|
// nicht hier.
|
|
homeRooms.forEach(function (room) {
|
|
const values = mqttData["Raumtemp"]?.[room.floor]?.[room.mqtt];
|
|
if (!values) return; // Topic noch nicht empfangen
|
|
const el = (part) => document.getElementById(room.floor + "_" + part);
|
|
|
|
if (typeof (values["Heating"]) != "undefined")
|
|
el(room.id + "_heater").setAttribute("display", values["Heating"] == "false" ? "none" : "");
|
|
if (typeof (values["mode"]) != "undefined")
|
|
el(room.id + "_buffer").setAttribute("display", values["mode"] == "Overheating" ? "" : "none");
|
|
if (typeof (values["Set Temp[degC]"]) != "undefined")
|
|
el("pres_" + room.id).innerHTML = Math.floor(values["Set Temp[degC]"]) + "\u2009\u00b0C";
|
|
if (typeof (values["Temp[degC]"]) != "undefined")
|
|
el("tmp_" + room.id).innerHTML = values["Temp[degC]"] + "\u2009\u00b0C";
|
|
if (typeof (values["rHum[%]"]) != "undefined")
|
|
el("hum_" + room.id).innerHTML = values["rHum[%]"] + "\u2009%rF";
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
var currentFloor = "OG";
|
|
|
|
function addClass(el, classNameToAdd){
|
|
el.className += ' ' + classNameToAdd;
|
|
}
|
|
|
|
function removeClass(el, classNameToRemove){
|
|
var elClass = ' ' + el.className + ' ';
|
|
while(elClass.indexOf(' ' + classNameToRemove + ' ') !== -1){
|
|
elClass = elClass.replace(' ' + classNameToRemove + ' ', '');
|
|
}
|
|
el.className = elClass;
|
|
}
|
|
|
|
function switchTab(newtab){
|
|
newContent = document.getElementById(newtab);
|
|
newTabBtn = document.getElementById(newtab+"-tab");
|
|
removeClass(document.getElementById("actions-OG-tab"),"active");
|
|
removeClass(document.getElementById("actions-EG-tab"),"active");
|
|
removeClass(document.getElementById("actions-UG-tab"),"active");
|
|
|
|
removeClass(document.getElementById("actions-OG"),"active show");
|
|
removeClass(document.getElementById("actions-EG"),"active show");
|
|
removeClass(document.getElementById("actions-UG"),"active show");
|
|
addClass(newTabBtn,"active");
|
|
addClass(newContent,"active show");
|
|
|
|
|
|
}
|
|
|
|
|
|
function switchFloor(floor){
|
|
if(currentFloor == floor)
|
|
return;
|
|
const targetIn = document.getElementById(floor+'_Info');
|
|
const targetOut = document.getElementById(currentFloor+'_Info');
|
|
var blendIn = new KeyframeEffect(
|
|
targetIn, [{opacity: '0'},{opacity: '100'}],
|
|
{
|
|
duration: 500,
|
|
easing: "ease-in-out",
|
|
fill: "forwards",
|
|
iterations: 1,
|
|
}
|
|
);
|
|
var blendOut = new KeyframeEffect(
|
|
targetOut, [{opacity: '100'},{opacity: '0'}],
|
|
{
|
|
duration: 500,
|
|
easing: "ease-in-out",
|
|
fill: "forwards",
|
|
iterations: 1,
|
|
}
|
|
);
|
|
var inAnim = new Animation(blendIn,document.timeline);
|
|
var outAnim = new Animation(blendOut,document.timeline);
|
|
targetIn.setAttribute("display","");
|
|
outAnim.onfinish= (event) => {
|
|
targetOut.setAttribute("display","none");
|
|
};
|
|
inAnim.play();
|
|
outAnim.play();
|
|
currentFloor = floor;
|
|
}
|
|
|
|
|
|
var modalEV = new bootstrap.Modal(document.getElementById('modalEV'), {
|
|
keyboard: false
|
|
});
|
|
var offcanvas = new bootstrap.Offcanvas(document.getElementById('offcanvas'), {
|
|
keyboard: false
|
|
});
|
|
|
|
var mqttData = {};
|
|
|
|
function openHeaterSettings(heater){
|
|
openRoomView(heater);
|
|
}
|
|
|
|
document.addEventListener('readystatechange', function () {
|
|
if (event.target.readyState === "complete") {
|
|
homeMQTT.getMQTT();
|
|
mountMeteogram();
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* Alles, was in einem Raum haengt, auf einer Seite: Heizung, Rollladen,
|
|
* Licht. Der Grundriss zeigt Raeume, also sollte ein Klick darauf den Raum
|
|
* oeffnen - vorher kam nur der Temperaturregler, die Rollladen im selben
|
|
* Zimmer waren von hier gar nicht erreichbar.
|
|
*
|
|
* Welche Geraete dazugehoeren, sagt die Raumzuordnung (actors.room).
|
|
*/
|
|
function openRoomView(raum) {
|
|
const teile = raum.toString().split("_");
|
|
document.getElementById("modal-title").innerHTML =
|
|
teile[0] + " · " + teile.slice(1).join(" ")
|
|
.replace("ue", "ü").replace("ae", "ä").replace("oe", "ö");
|
|
|
|
// Der Speichern-Knopf gehoert allen Modals gemeinsam. Ein Klon ohne
|
|
// Zuhoerer stellt sicher, dass nicht auch der Handler eines vorher
|
|
// geoeffneten Modals mitfeuert.
|
|
const alt = document.getElementById("modalSaveBtn");
|
|
const btn = alt.cloneNode(true);
|
|
alt.replaceWith(btn);
|
|
btn.addEventListener("click", () => sendeSolltemperatur(raum));
|
|
|
|
const koerper = document.getElementById("modal-body");
|
|
koerper.innerHTML = loadingHTML("Wird geladen...");
|
|
modalEV.show();
|
|
|
|
fetch("./ajax/room.php?room=" + encodeURIComponent(raum), {
|
|
method: "GET",
|
|
headers: { "X-Requested-From-Modal": "a", "Requested-With-Ajax": "ajax" }
|
|
})
|
|
.then(a => a.text())
|
|
.then(html => {
|
|
koerper.innerHTML = html;
|
|
|
|
const slider = document.getElementById("modal-slider");
|
|
if (slider) {
|
|
bindModalSlider(slider, document.getElementById("modal-slider-label"),
|
|
wert => wert + " °C");
|
|
const stand = mqttData["Raumtemp"]?.[teile[0]]?.[teile[1]]?.["Set Temp[degC]"];
|
|
slider.value = typeof stand !== "undefined" ? stand : 10;
|
|
slider.dispatchEvent(new Event("input"));
|
|
}
|
|
// Ohne Thermostat gibt es nichts zu speichern - dann waere der Knopf
|
|
// eine Falle.
|
|
btn.hidden = !slider;
|
|
|
|
raumBedienungAktivieren(koerper);
|
|
raumMesswerteAktualisieren(teile[0], teile[1]);
|
|
})
|
|
.catch(fehler => { koerper.innerHTML = "Konnte den Raum nicht laden: " + fehler.message; });
|
|
}
|
|
|
|
/**
|
|
* Die Bedienelemente des Raum-Modals mit ihren Kommandos verbinden.
|
|
*
|
|
* Welches Geraet welches Element bekommt, entscheidet der Server
|
|
* (restricted/roomControls.php); hier haengen nur die Zuhoerer an den
|
|
* data-Attributen, die er dabei setzt.
|
|
*/
|
|
function raumBedienungAktivieren(koerper) {
|
|
koerper.querySelectorAll("button[data-cmd]").forEach(k => {
|
|
k.addEventListener("click", () => schalte(k));
|
|
});
|
|
|
|
// Schieberegler: waehrend des Ziehens nur die Beschriftung, geschickt
|
|
// wird erst beim Loslassen. Sonst bekaeme eine Jalousie bei jedem Pixel
|
|
// ein neues Ziel und kaeme aus dem Anfahren nicht mehr heraus.
|
|
koerper.querySelectorAll(".raum-regler").forEach(regler => {
|
|
const schieber = regler.querySelector("input[type=range]");
|
|
const anzeige = regler.querySelector(".raum-regler-wert");
|
|
const einheit = schieber.dataset.einheit || "";
|
|
schieber.addEventListener("input", () => { anzeige.textContent = schieber.value + einheit; });
|
|
schieber.addEventListener("change", () => {
|
|
const params = {};
|
|
params[regler.dataset.param] = schieber.value;
|
|
raumKommando(Number(regler.dataset.cmd), params, regler);
|
|
});
|
|
});
|
|
|
|
// Kippschalter: An und Aus sind zwei getrennte Kommandos. Kennt das
|
|
// Geraet nur "umschalten", tut es auch das - dann stimmt die Stellung
|
|
// hinterher, weil wir sie selbst gesetzt haben.
|
|
koerper.querySelectorAll(".raum-schalter").forEach(schalter => {
|
|
const kasten = schalter.querySelector("input[type=checkbox]");
|
|
const beschriftung = schalter.querySelector("label");
|
|
kasten.addEventListener("change", () => {
|
|
const ziel = kasten.checked ? schalter.dataset.ein : schalter.dataset.aus;
|
|
const cmd = ziel || schalter.dataset.um;
|
|
beschriftung.textContent = kasten.checked ? "An" : "Aus";
|
|
if (!cmd) return;
|
|
raumKommando(Number(cmd), {}, schalter);
|
|
});
|
|
});
|
|
|
|
// Farbe: der Browser bringt seinen Farbwaehler mit, das Geraet will drei
|
|
// Zahlen 0-255. Zerlegt wird deshalb erst hier.
|
|
koerper.querySelectorAll(".raum-farbe").forEach(feld => {
|
|
const eingabe = feld.querySelector("input[type=color]");
|
|
eingabe.addEventListener("change", () => {
|
|
const hex = eingabe.value;
|
|
const params = {};
|
|
params[feld.dataset.red] = parseInt(hex.substr(1, 2), 16);
|
|
params[feld.dataset.green] = parseInt(hex.substr(3, 2), 16);
|
|
params[feld.dataset.blue] = parseInt(hex.substr(5, 2), 16);
|
|
raumKommando(Number(feld.dataset.cmd), params, feld);
|
|
});
|
|
});
|
|
|
|
// Auswahllisten, die ohne zusaetzlichen Knopf schalten - Effekt, Preset.
|
|
koerper.querySelectorAll("select.raum-sofort").forEach(auswahl => {
|
|
auswahl.addEventListener("change", () => {
|
|
const params = {};
|
|
params[auswahl.dataset.param] = auswahl.value;
|
|
raumKommando(Number(auswahl.dataset.cmd), params, auswahl);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Die Ist-Werte im Heizungsteil aus dem laufenden MQTT-Strom fuellen.
|
|
*
|
|
* Der Browser hoert ohnehin schon auf Raumtemp/#, und ein Wert aus der
|
|
* Datenbank waere neben dem Regler nur ein Stand von vorhin.
|
|
*/
|
|
function raumMesswerteAktualisieren(etage, raum) {
|
|
const werte = mqttData["Raumtemp"]?.[etage]?.[raum];
|
|
if (!werte) return;
|
|
document.querySelectorAll("#roomView [data-mqtt]").forEach(feld => {
|
|
const wert = werte[feld.dataset.mqtt];
|
|
if (typeof wert !== "undefined") feld.textContent = wert;
|
|
});
|
|
const heizt = document.querySelector("#roomView [data-mqtt-heizung]");
|
|
if (heizt) heizt.classList.toggle("d-none", String(werte["Heating"]) !== "true");
|
|
}
|
|
|
|
/** Ein Kommando des angeklickten Knopfes ausfuehren. */
|
|
function schalte(knopf) {
|
|
const zeile = knopf.closest("[data-cmdrow]");
|
|
const params = {};
|
|
if (zeile) {
|
|
zeile.querySelectorAll("[data-param]").forEach(f => { params[f.dataset.param] = f.value; });
|
|
}
|
|
raumKommando(Number(knopf.dataset.cmd), params, knopf);
|
|
}
|
|
|
|
/**
|
|
* Ein Kommando schicken und die Karte so lange sperren.
|
|
*
|
|
* Ein "Zu" an einer Jalousie dauert ueber eine Minute (die Mechanik will
|
|
* zweimal geschwenkt werden, siehe restricted/commands.php). Ohne sichtbare
|
|
* Sperre tippt man in der Zeit dreimal nach.
|
|
*/
|
|
function raumKommando(cmdId, params, element) {
|
|
const karte = element.closest(".raum-geraet") || element;
|
|
karte.classList.add("raum-warte");
|
|
raumRequest("command", { command_id: cmdId, params: params })
|
|
.then(antwort => {
|
|
karte.classList.remove("raum-warte");
|
|
raumMeldung(antwort);
|
|
});
|
|
}
|
|
|
|
function sendeSolltemperatur(raum) {
|
|
const slider = document.getElementById("modal-slider");
|
|
if (!slider) return;
|
|
raumRequest("temp", { room: raum, temp: Number(slider.value) })
|
|
.then(antwort => {
|
|
raumMeldung(antwort);
|
|
if (!antwort.error) setTimeout(() => modalEV.hide(), 700);
|
|
});
|
|
}
|
|
|
|
function raumRequest(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 im Modal statt als alert - man schaltet oft mehrmals. */
|
|
function raumMeldung(antwort) {
|
|
const ziel = document.getElementById("roomFeedback");
|
|
if (!ziel) return;
|
|
ziel.className = "small mt-2 " + (antwort.error ? "text-danger" : "text-body-secondary");
|
|
ziel.textContent = antwort.error ? antwort.error : "✓ " + antwort.ok;
|
|
}
|