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 () { // Welche Zweige gebraucht werden, sagt die Raumtabelle: seit eine // Kachel jeden Messwert zeigen kann, reicht Raumtemp allein nicht // mehr. homeTopics kommt aus restricted/rooms.php. homeTopics.forEach(function (topic) { client.subscribe(topic); }); }); 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 () { }, /** * Eine Nachricht aus dem Baum holen. mqttData ist nach den Teilen des * Topics verschachtelt, siehe setNestedProp() weiter oben. */ ausTopic: function (mqttData, topic) { return topic.split("/").reduce(function (zweig, teil) { return zweig && zweig[teil]; }, mqttData); }, /** * Die Leistung benannter Wechselrichter, aufsummiert. * * Der SolarManager legt sie unter solarManager/inverters0, /inverters1 * und so fort ab - die Nummer ist eine Position in einer Liste und sagt * nichts darueber, welches Dach gemeint ist. Deshalb wird ueber * invertersN/name gesucht: aendert sich die Reihenfolge, findet die * Kachel ihren Wechselrichter trotzdem, statt stillschweigend das * falsche Dach anzuzeigen. */ wechselrichterWert: function (mqttData, wert) { const zweig = mqttData["solarManager"]; if (!zweig) return null; const gesucht = wert.wechselrichter; const feld = wert.feld || "p_AC"; let summe = 0; const gefunden = []; const vorhanden = []; for (const schluessel in zweig) { if (!schluessel.startsWith("inverters")) continue; const wr = zweig[schluessel]; if (!wr || typeof wr.name === "undefined") continue; const name = String(wr.name); vorhanden.push(name); if (gesucht.indexOf(name) === -1) continue; if (typeof wr[feld] === "undefined") continue; summe += Number(wr[feld]); gefunden.push(name); } if (!gefunden.length) { // Ein Name, den es nicht gibt, ergaebe sonst nur einen Strich auf // der Kachel und keinen Hinweis, woran es liegt. if (vorhanden.length && !homeSVG.gemeldet[gesucht.join("|")]) { homeSVG.gemeldet[gesucht.join("|")] = true; console.warn("Kachel: kein Wechselrichter namens " + gesucht.join(", ") + ". Vorhanden sind: " + vorhanden.join(", ")); } return null; } return summe; }, /** Merkt sich gemeldete Namen, damit die Konsole nicht zulaeuft. */ gemeldet: {}, /** * Einen Kachelwert lesen und hinschreiben. * * Woher der Wert kommt, sagt entweder "topic" oder "wechselrichter". * "pfad" ist gesetzt, wenn das Geraet mehrere Werte in einer einzigen * JSON-Nachricht schickt - die beiden Shelly-Zaehler tun das. * * Wie er dasteht, sagt entweder "format" oder das Paar aus "einheit" und * "stellen": "stellen" schneidet Nachkommastellen ab, ohne Angabe bleibt * der Text so, wie das Geraet ihn meldet. * * "negativ" dreht das Vorzeichen. Ein Zaehler zaehlt in der Richtung, in * der er eingebaut ist - einer der beiden meldet den Bezug deshalb mit * Minus. Das gehoert an die Anzeige und nicht an den Zaehler: die * Automatiken und die Statistik lesen dieselbe Nachricht und rechnen mit * dem Vorzeichen, das dort steht. */ kachelWert: function (mqttData, wert) { const roh = wert.wechselrichter ? homeSVG.wechselrichterWert(mqttData, wert) : homeSVG.ausTopic(mqttData, wert.topic); if (roh === null || typeof roh === "undefined") return null; let text = String(roh); if (wert.pfad) { try { const daten = JSON.parse(text); if (!(wert.pfad in daten)) return null; text = String(daten[wert.pfad]); } catch (fehler) { return null; // noch keine vollstaendige Nachricht } } if (wert.negativ) { const zahl = Number(text); if (text === "" || isNaN(zahl)) return null; // "+ 0" macht aus -0 wieder 0: "-0 W" hat noch niemanden erfreut. text = String(-zahl + 0); } // Leistungen skalieren mit: unter tausend Watt in W, darueber in kW. // powerToString() aus common.js macht genau das und wird auf der // Solarseite fuer dieselben Zahlen benutzt - eine eigene Regel hier // haette bedeutet, dass dieselbe Wallbox an zwei Stellen verschieden // gerundet dasteht. // // "leistung_kw" ist dasselbe fuer Quellen, die schon Kilowatt melden - // die Wallboxen im SolarManager. Umgerechnet wird hier in Watt, damit // es eine Regel fuer beide bleibt. if (wert.format === "leistung" || wert.format === "leistung_kw") { const zahl = Number(text) * (wert.format === "leistung_kw" ? 1000 : 1); if (text === "" || isNaN(zahl)) return null; // Ersetzt wird nur das Trennzeichen: auf der Kachel steht ein // schmales Leerzeichen, im Rest der Anwendung ein gewoehnliches. return powerToString(zahl).replace(" ", " "); } if (typeof wert.stellen === "number" && text !== "" && !isNaN(Number(text))) { text = Number(text).toFixed(wert.stellen); } // Schmales Leerzeichen zwischen Zahl und Einheit, wie es sich gehoert. return wert.einheit ? text + "\u2009" + wert.einheit : text; }, updateValuesMQTT: function (mqttData) { // homeRooms liefert restricted/rooms.php (in home.php eingebettet), // mitsamt den Werten, die auf der Kachel stehen sollen. Jeder Raum // bekommt die gleiche Behandlung - Raeume und ihre Werte kommen dort // dazu, nicht hier. if (typeof raumfensterMqtt === "function") raumfensterMqtt(); homeRooms.forEach(function (room) { const el = (part) => document.getElementById(room.floor + "_" + part); room.werte.forEach(function (wert, i) { const text = homeSVG.kachelWert(mqttData, wert); const feld = el("w" + i + "_" + room.id); if (text !== null && feld) feld.innerHTML = text; }); // Heizsymbol und Pufferbetrieb gibt es nur, wo ein Thermostat // haengt - anderswo sind die Elemente gar nicht gezeichnet. if (!room.heizung) return; const values = mqttZweig(room.thermostat); if (!values) return; 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"); }); } } // Die Etage, mit der die Seite oeffnet - standardEtage() in rooms.php, von // home.php in die Seite geschrieben. Stand frueher fest auf "OG". var currentFloor = (typeof homeStandardEtage !== "undefined") ? homeStandardEtage : ""; 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; } // Die Etagen stehen nicht mehr im Code: was es gibt, sagen die Reiter, die // home.php aus der Raumtabelle gebaut hat. Vorher standen OG, EG und UG hier // sechsmal, und eine vierte Etage haette sechs neue Zeilen gebraucht. 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(); // Welche Etage gerade zu sehen ist, stand den Schaltern bisher nicht an - // alle vier sahen immer gleich aus. classList und nicht die Helfer // addClass/removeClass weiter oben: deren "el.className +=" wirkt auf // SVG-Elementen nicht, dort ist className schreibgeschuetzt. document.querySelectorAll(".floorBtn").forEach(function (schalter) { schalter.classList.toggle("active", schalter.id === floor); }); currentFloor = floor; } var modalEV = new bootstrap.Modal(document.getElementById('modalEV'), { keyboard: false }); var offcanvas = new bootstrap.Offcanvas(document.getElementById('offcanvas'), { keyboard: false }); var mqttData = {}; /** * Ein Zweig des Nachrichtenbaums nach seinem Topic, z.B. "Raumtemp/EG/Bad". * * Frueher stand hier fest mqttData["Raumtemp"][Etage][Raum] - das setzte * voraus, dass jedes Thermostat unter Raumtemp// meldet. * Jetzt steht der Zweig am Raum (rooms.thermostat). */ function mqttZweig(topic) { if (!topic) return undefined; return String(topic).split("/").filter(t => t !== "") .reduce((knoten, teil) => knoten?.[teil], mqttData); } /** Ein Raum aus homeRooms nach seiner festen Nummer. */ function raumNachNummer(nr) { return (typeof homeRooms !== "undefined" ? homeRooms : []) .find(r => r.nr === Number(nr)); } 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_id). * `raum` ist die feste Raumnummer (rooms.id). */ function openRoomView(raum) { const eintrag = raumNachNummer(raum); // Der Name kommt jetzt aus der Datenbank und darf Umlaute tragen. Die // Ersetzung bleibt fuer die alten Namen ("Kueche"), bis sie jemand // unter Einstellungen -> Grundriss umbenennt. const titel = eintrag ? eintrag.mqtt.replace("ue", "ü").replace("ae", "ä").replace("oe", "ö") : "Raum"; const modalTitel = document.getElementById("modal-title"); modalTitel.textContent = titel; if (eintrag) { const etage = document.createElement("span"); etage.className = "rf-etage"; etage.textContent = eintrag.floor; modalTitel.append(" ", etage); } // Der Speichern-Knopf gehoert allen Modals gemeinsam. Ein Klon ohne // Zuhoerer stellt sicher, dass nicht der Handler eines vorher // geoeffneten Modals mitfeuert; im Raum-Modal bleibt er weg. Hier wird // nichts gesammelt und dann abgeschickt - jeder Rollladen, jeder // Schalter und jede Lampe wirkt sofort, und die Solltemperatur tut es // jetzt auch. Ein Knopf, der nur fuer einen der Reiter gilt, waere in // allen anderen eine Falle. const alt = document.getElementById("modalSaveBtn"); const btn = alt.cloneNode(true); alt.replaceWith(btn); btn.hidden = true; 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; // Verdrahten, zeichnen, Jalousien nachfragen: js/solar/raumfenster.js. raumfensterStarten(koerper.querySelector("#roomView") || koerper, raum, eintrag?.thermostat); }) .catch(fehler => { koerper.innerHTML = "Konnte den Raum nicht laden: " + fehler.message; }); }