Raeume ueber ihre feste Nummer: actors.room_id, Raum-Modal, Thermostat-Zweig

Geraete hingen ueber den Text "Etage/Raum" an ihrem Raum, das Raum-Modal
zerlegte "EG_Bad" am Unterstrich, und das Thermostat-Topic wurde aus Etage
und Name zusammengesetzt. Ein umbenannter Raum haette still alle Geraete,
seinen Temperaturregler und sein Heizsymbol verloren.

- actors.room (Text) -> actors.room_id, Fremdschluessel auf rooms, beim
  Loeschen eines Raums SET NULL. Alle 59 Zuordnungen uebernommen.
- automations.floor: ENUM(UG,EG,OG,AG) -> Verweis auf floors.
  Eine neue Etage kann damit auch Automatiken tragen.
- room.php nimmt ?room=<Nummer>; Solltemperatur geht an
  rooms.thermostat + /changeSetTemp.
- homeMQTT.js: Heizsymbol, Regler und Ist-Werte ueber mqttZweig(thermostat)
  statt mqttData.Raumtemp[Etage][Name].
- Raumvorschlaege in den Einstellungen: Messwerte unter dem Thermostat-
  Zweig eines Raums statt fest Raumtemp/<Etage>/<Name>.
- Schluessel in raumListe(), Katalog und Maske ist die Raumnummer als Text.

Geprueft gegen den Stand davor: Startseite gleich bis auf das Klickziel,
jede der 25 Kacheln oeffnet ihren eigenen Raum; Geraeteliste, Vorschlaege,
Editor-Katalog und die eigenen Geraete je Kachel gleich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 10:58:30 +02:00
co-authored by Claude Opus 5
parent 3636a134d6
commit 62a8af3fc6
10 changed files with 170 additions and 86 deletions
+33 -12
View File
@@ -192,7 +192,7 @@ const homeSVG = {
// Heizsymbol und Pufferbetrieb gibt es nur, wo ein Thermostat
// haengt - anderswo sind die Elemente gar nicht gezeichnet.
if (!room.heizung) return;
const values = mqttData["Raumtemp"]?.[room.floor]?.[room.mqtt];
const values = mqttZweig(room.thermostat);
if (!values) return;
if (typeof (values["Heating"]) != "undefined")
el(room.id + "_heater").setAttribute("display", values["Heating"] == "false" ? "none" : "");
@@ -276,8 +276,23 @@ function switchFloor(floor){
var mqttData = {};
function openHeaterSettings(heater){
openRoomView(heater);
/**
* 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 () {
@@ -294,13 +309,19 @@ function switchFloor(floor){
* 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).
* Welche Geraete dazugehoeren, sagt die Raumzuordnung (actors.room_id).
* `raum` ist die feste Raumnummer (rooms.id).
*/
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", "ö");
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.floor + " · " + eintrag.mqtt.replace("ue", "ü").replace("ae", "ä").replace("oe", "ö")
: "Raum";
const modalTitel = document.getElementById("modal-title");
modalTitel.textContent = titel;
// Der Speichern-Knopf gehoert allen Modals gemeinsam. Ein Klon ohne
// Zuhoerer stellt sicher, dass nicht der Handler eines vorher
@@ -330,7 +351,7 @@ function switchFloor(floor){
if (slider) {
bindModalSlider(slider, document.getElementById("modal-slider-label"),
wert => wert + " °C");
const stand = mqttData["Raumtemp"]?.[teile[0]]?.[teile[1]]?.["Set Temp[degC]"];
const stand = mqttZweig(eintrag?.thermostat)?.["Set Temp[degC]"];
slider.value = typeof stand !== "undefined" ? stand : 10;
// Nur "input": das zeichnet die Anzeige, schickt aber nichts.
slider.dispatchEvent(new Event("input"));
@@ -345,7 +366,7 @@ function switchFloor(floor){
// Fahrt aufmacht, saehe sonst den Stand von davor und nie wieder
// etwas anderes: gezeichnet wird nur beim Oeffnen.
koerper.querySelectorAll(".raum-geraet").forEach(k => jalousieVerfolgen(k, null));
raumMesswerteAktualisieren(teile[0], teile[1]);
raumMesswerteAktualisieren(eintrag?.thermostat);
})
.catch(fehler => { koerper.innerHTML = "Konnte den Raum nicht laden: " + fehler.message; });
}
@@ -504,8 +525,8 @@ function switchFloor(floor){
* 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];
function raumMesswerteAktualisieren(thermostat) {
const werte = mqttZweig(thermostat);
if (!werte) return;
document.querySelectorAll("#roomView [data-mqtt]").forEach(feld => {
const wert = werte[feld.dataset.mqtt];
+4 -2
View File
@@ -498,7 +498,7 @@ function kachelWertZeile(wert, i, kachel) {
// Ohne Treffer beim ersten Geraet des eigenen Raums beginnen, sonst beim
// ersten ueberhaupt - ein neuer Wert soll nicht im Nachbargeschoss starten.
const ersterEigener = katalog.findIndex(
(g) => kachel && g.floorKey === kachel.floor && g.raumKey === kachel.name);
(g) => kachel && g.raumKey === kachel.nr);
const gi = treffer ? treffer.gi : Math.max(0, ersterEigener);
const wi = treffer ? treffer.wi : -1;
@@ -512,7 +512,9 @@ function kachelWertZeile(wert, i, kachel) {
* in der gefilterten Liste - sonst zeigte die Auswahl nach dem Umsortieren
* auf ein anderes Geraet.
*/
const eigen = (g) => kachel && g.floorKey === kachel.floor && g.raumKey === kachel.name;
// Ueber die feste Raumnummer, nicht ueber den Namen - "Bad" gibt es auf
// drei Etagen, und ein umbenannter Raum bliebe sonst ohne eigene Geraete.
const eigen = (g) => kachel && g.raumKey === kachel.nr;
const option = (g, index) => '<option value="' + index + '"'
+ (index === gi ? " selected" : "") + ">"
+ textSchuetzen(g.label + (g.raum && !eigen(g) ? " " + g.raum : "")) + "</option>";