- Beschattung: Auf/Stop/my/Zu, Hoehe und Lamellen in 25-%-Stufen, Ziehen im Fensterbild, neue Behang-Grafik ueber die ganze Kartenhoehe, Alle im Raum - my-Position: aktuellen Stand per setMemorized1Position/-Orientation in den Motor schreiben (room.php?action=my), gespeicherte Werte werden angezeigt - Heizung: Ring mit Ist-Temperatur und ziehbarem Soll-Punkt, -/+ und Schnellwahl - Licht: Birne als Schalter, Helligkeit, Farbfelder, Effekt/Preset eingeklappt - Logik aus homeMQTT.js nach js/solar/raumfenster.js; bindModalSlider entfaellt Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
361 lines
14 KiB
JavaScript
361 lines
14 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 () {
|
||
// 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/<Etage>/<Raumname> 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; });
|
||
}
|