From 62a8af3fc6734e7ca2be6e440c8fb698c46834a8 Mon Sep 17 00:00:00 2001 From: "m0@nas" Date: Tue, 15 Sep 2026 10:58:30 +0200 Subject: [PATCH] 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=; 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//. - 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 --- ajax/room.php | 62 +++++++++++++++++---------- homeMesh_DB-layout.sql | 2 +- homeMesh_automations.sql | 2 +- homeMesh_grundriss.sql | 31 ++++++++++++++ js/solar/homeMQTT.js | 45 +++++++++++++------ js/solar/settings.js | 6 ++- restricted/automations.php | 88 +++++++++++++++++++++----------------- restricted/home.php | 2 +- restricted/kacheln.php | 16 ++++--- restricted/rooms.php | 2 +- 10 files changed, 170 insertions(+), 86 deletions(-) diff --git a/ajax/room.php b/ajax/room.php index 20b9b75..a860184 100644 --- a/ajax/room.php +++ b/ajax/room.php @@ -6,14 +6,29 @@ * sollte ein Klick darauf den Raum oeffnen und nicht nur seine Heizung - die * Rollladen im selben Zimmer waren bisher gar nicht erreichbar. * - * Welche Geraete zu einem Raum gehoeren, sagt die Spalte actors.room (siehe - * den Zuordnungsdialog in der Karte "Automatismen"). + * Welche Geraete zu einem Raum gehoeren, sagt die Spalte actors.room_id + * (Einstellungen -> Geraete). * - * GET ?room=EG_Bad Inhalt des Modals + * GET ?room=12 Inhalt des Modals, 12 = rooms.id * POST ?action=command {"command_id": N, "params": {...}} - ein Kommando - * POST ?action=temp {"room": "EG_Bad", "temp": 21} - Solltemperatur + * POST ?action=temp {"room": 12, "temp": 21} - Solltemperatur + * + * Frueher hiess der Raum "EG_Bad" und wurde am Unterstrich zerlegt. Das hielt + * weder ein Leerzeichen im Namen aus (erst nachgeruestet) noch eine + * Umbenennung - die feste Nummer haelt beides. */ +/** Ein Raum nach seiner festen Nummer, oder null. */ +function raumNachNummer($nr) +{ + foreach (allRooms() as $r) { + if ($r["nr"] === intval($nr)) { + return $r; + } + } + return null; +} + // Ein "Zu" an einer Jalousie faehrt herunter, wartet und neigt danach - das // dauert in der Summe ueber eine Minute. Die Vorgabe von PHP reicht dafuer // nicht; ajax/tahoma.php macht es an derselben Stelle genauso. @@ -102,12 +117,14 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") { exit; } if (($_GET["action"] ?? "") === "temp") { - // Bewusst derselbe Weg wie im alten Thermostat-Modal: das Topic - // Raumtemp///changeSetTemp. - // Das Leerzeichen muss durch: Raumnamen wie "Carport EG" haben - // eines, und ohne es findet der Vergleich weiter unten den Raum - // nicht mehr. - $raum = preg_replace('/[^A-Za-z0-9_ ]/', '', strval($body["room"] ?? "")); + // Derselbe Weg wie im alten Thermostat-Modal: /changeSetTemp. + // Der Zweig steht am Raum (rooms.thermostat) und wird nicht mehr + // aus Etage und Name zusammengesetzt - ein Thermostat in einem + // anderen Haus meldet vielleicht unter einem ganz anderen. + $raum = raumNachNummer($body["room"] ?? 0); + if (!$raum || empty($raum["thermostat"])) { + raumAntwort(["error" => "Dieser Raum hat kein Thermostat."], 400); + } $temp = intval($body["temp"] ?? 0); if ($temp < 10 || $temp > 30) { raumAntwort(["error" => "Temperatur außerhalb 10–30 °C."], 400); @@ -116,7 +133,7 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") { if (!$mqtt->connect(true, null, "", "")) { raumAntwort(["error" => "Keine Verbindung zum MQTT-Broker."], 500); } - $mqtt->publish("Raumtemp/" . str_replace("_", "/", $raum) . "/changeSetTemp", + $mqtt->publish(rtrim($raum["thermostat"], "/") . "/changeSetTemp", strval($temp), 0, false); $mqtt->close(); raumAntwort(["ok" => "Solltemperatur " . $temp . " °C"]); @@ -156,17 +173,13 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") { // --- Inhalt des Modals ---------------------------------------------------- -// Das Leerzeichen gehoert zu den erlaubten Zeichen. Ohne es wurde aus -// "AG_Carport EG" ein "AG_CarportEG", und der Vergleich gegen die -// gespeicherte Zuordnung "AG/Carport EG" ging ins Leere - das Modal meldete -// dann "Diesem Raum ist noch nichts zugeordnet". Aufgefallen ist es erst mit -// dem Aussengelaende: dort haben sechs von neun Raeumen ein Leerzeichen im -// Namen, auf den anderen Etagen keiner von achtzehn. -$schluessel = preg_replace('/[^A-Za-z0-9_ ]/', '', strval($_GET["room"] ?? "")); -$teile = explode("_", $schluessel, 2); -$etage = $teile[0] ?? ""; -$name = $teile[1] ?? ""; -$raumKey = $etage . "/" . $name; +$raum = raumNachNummer($_GET["room"] ?? 0); +if (!$raum) { + echo "

Diesen Raum gibt es nicht (mehr).

"; + exit; +} +// Derselbe Schluessel wie actors.room_id im Katalog - als Text, siehe raumListe(). +$raumKey = strval($raum["nr"]); // Alles im Raum, sortiert nach Bedienform. Das Thermostat selbst wird nicht // als Karte gezeichnet: es wird zum Schieberegler im Heizungs-Reiter und @@ -214,7 +227,10 @@ if ($hatThermostat) { $fertig["heizung"] = $karte; } -echo "
"; +// Der Thermostat-Zweig reist mit: das Modal fuellt Ist-Werte und Heizsymbol +// aus dem laufenden MQTT-Strom und muss wissen, wo es nachsehen soll. +echo "
"; if (!$gruppen && !$fertig) { echo "

Diesem Raum ist noch nichts zugeordnet. " diff --git a/homeMesh_DB-layout.sql b/homeMesh_DB-layout.sql index 12d860a..8f13fad 100644 --- a/homeMesh_DB-layout.sql +++ b/homeMesh_DB-layout.sql @@ -32,7 +32,7 @@ CREATE TABLE `actors` ( `type` varchar(50) NOT NULL COMMENT 'Gerätetyp z.B. RollerShutter', `name` varchar(70) NOT NULL COMMENT 'Name des Geräts', `url` varchar(100) NOT NULL COMMENT 'Tahoma Device URL', - `room` varchar(60) DEFAULT NULL COMMENT 'Raum aus restricted/rooms.php als "Etage/Raum" (z.B. OG/Bad) oder "aussen". Wird von Hand vergeben und deshalb vom Discovery nicht angefasst - der Upsert setzt nur type und name.' + `room_id` int(11) DEFAULT NULL COMMENT 'Raum (rooms.id, siehe homeMesh_grundriss.sql). Wird von Hand vergeben und deshalb vom Discovery nicht angefasst - der Upsert setzt nur type und name.' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; -- diff --git a/homeMesh_automations.sql b/homeMesh_automations.sql index 8e781e4..f813b79 100644 --- a/homeMesh_automations.sql +++ b/homeMesh_automations.sql @@ -29,7 +29,7 @@ SET NAMES utf8mb4; CREATE TABLE IF NOT EXISTS `automations` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT 'Anzeigename, z.B. Kinder zu', - `floor` enum('UG','EG','OG','AG') NOT NULL DEFAULT 'OG' COMMENT 'Reiter in der Uebersicht. Ohne Etage waere die Automatik in keinem davon zu sehen, deshalb gibt es keinen leeren Wert. AG ist das Aussengelaende - eine Etage ohne Grundriss, siehe restricted/rooms.php', + `floor` varchar(4) NOT NULL COMMENT 'Reiter in der Uebersicht (floors.code, Fremdschluessel legt homeMesh_grundriss.sql an). Ohne Etage waere die Automatik in keinem Reiter zu sehen, deshalb kein leerer Wert', `enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Aktiv oder pausiert', `window_from` time NOT NULL DEFAULT '00:00:00' COMMENT 'Aktiver Zeitraum, Beginn', `window_to` time NOT NULL DEFAULT '23:59:00' COMMENT 'Aktiver Zeitraum, Ende. Kleiner als window_from = ueber Mitternacht', diff --git a/homeMesh_grundriss.sql b/homeMesh_grundriss.sql index 606e760..af1e7c0 100644 --- a/homeMesh_grundriss.sql +++ b/homeMesh_grundriss.sql @@ -14,6 +14,10 @@ -- Die Tabellen duerfen leer sein. Dann zeigt die Startseite einen Hinweis auf -- die Einstellungen statt eines Hauses - das ist der Anfang jeder neuen -- Installation. +-- +-- Einspielen NACH homeMesh_DB-layout.sql und homeMesh_automations.sql: am +-- Ende haengt diese Datei die Fremdschluessel von actors.room_id und +-- automations.floor an die neuen Tabellen. Ein zweiter Lauf schadet nicht. -- --------------------------------------------------------------------------- SET NAMES utf8mb4; @@ -63,3 +67,30 @@ CREATE TABLE IF NOT EXISTS `rooms` ( CONSTRAINT `fk_rooms_floor` FOREIGN KEY (`floor`) REFERENCES `floors` (`code`) ON UPDATE CASCADE ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- --------------------------------------------------------------------------- +-- Verweise auf Etagen und Raeume. +-- +-- actors.room_id: ein geloeschter Raum laesst seine Geraete unzugeordnet +-- zurueck (SET NULL) - sie tauchen dann unter "ohne Raum" wieder auf. +-- automations.floor: eine Etage mit Automatiken laesst sich nicht loeschen +-- (RESTRICT); die Einstellungsseite nennt vorher die Namen. +-- +-- Idempotent ueber information_schema, weil MariaDB kein +-- ADD CONSTRAINT IF NOT EXISTS fuer Fremdschluessel kennt. +-- --------------------------------------------------------------------------- +SET @da := (SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS + WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'fk_actors_room'); +SET @sql := IF(@da = 0, + 'ALTER TABLE actors ADD KEY idx_actors_room (room_id), + ADD CONSTRAINT fk_actors_room FOREIGN KEY (room_id) REFERENCES rooms (id) ON DELETE SET NULL', + 'DO 0'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @da := (SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS + WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'fk_automations_floor'); +SET @sql := IF(@da = 0, + 'ALTER TABLE automations ADD CONSTRAINT fk_automations_floor FOREIGN KEY (floor) + REFERENCES floors (code) ON UPDATE CASCADE ON DELETE RESTRICT', + 'DO 0'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/js/solar/homeMQTT.js b/js/solar/homeMQTT.js index 468adaf..ca1ea28 100644 --- a/js/solar/homeMQTT.js +++ b/js/solar/homeMQTT.js @@ -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// 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]; diff --git a/js/solar/settings.js b/js/solar/settings.js index f208ef5..a88a1e0 100644 --- a/js/solar/settings.js +++ b/js/solar/settings.js @@ -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) => '"; diff --git a/restricted/automations.php b/restricted/automations.php index 1f0da51..1bda626 100644 --- a/restricted/automations.php +++ b/restricted/automations.php @@ -333,9 +333,12 @@ function anzeigeName($name) /** * Waehlbare Raeume: die Raeume der Home-Ansicht. * - * Einzige Quelle bleibt restricted/rooms.php - ein neuer Raum ist dort eine - * Zeile und taucht hier automatisch auf. Der Schluessel ist "Etage/Raum", - * weil "Bad" allein auf drei Etagen vorkommt. + * Einzige Quelle bleibt restricted/rooms.php (dahinter homeMesh.rooms) - ein + * neuer Raum taucht hier automatisch auf. Der Schluessel ist die feste + * Raumnummer als Text, so wie actors.room_id sie traegt. Frueher war es + * "Etage/Raum" - ein umbenannter Raum verlor damit still alle Geraete. Als + * Text, weil er im Browser mit dem Wert einer Auswahlliste verglichen wird, + * und der ist immer einer. * * Frueher stand hier ein Pseudo-Raum "aussen" fuer alles, was zu keinem Raum * der Wohnung gehoert - Carport, Pergola, Garage, die Wechselrichter. Das war @@ -353,7 +356,7 @@ function raumListe() $liste = []; foreach (allRooms() as $raum) { $liste[] = [ - "key" => $raum["floor"] . "/" . $raum["mqtt"], + "key" => strval($raum["nr"]), "label" => $raum["floor"] . " · " . $raum["mqtt"], "floor" => $raum["floor"], "room" => $raum["mqtt"], @@ -365,17 +368,17 @@ function raumListe() /** Anzeigename eines gespeicherten Raumschluessels. */ function raumBeschriftung($key) { - if (!$key) { + if ($key === null || $key === "") { return "Nicht zugeordnet"; } foreach (raumListe() as $raum) { - if ($raum["key"] === $key) { + if ($raum["key"] === strval($key)) { return $raum["label"]; } } - // Raum aus rooms.php entfernt - der Eintrag am Geraet bleibt stehen, - // damit eine Umbenennung dort keine Zuordnung stillschweigend loescht. - return $key . " (unbekannt)"; + // Kann nur noch an der Datenbank vorbei entstehen: fk_actors_room setzt + // die Zuordnung beim Loeschen eines Raums auf NULL. + return "Raum " . $key . " (unbekannt)"; } /** @@ -389,43 +392,46 @@ function raumBeschriftung($key) */ function raumEinordnung($key) { - if (!$key) { + if ($key === null || $key === "") { return ["floor" => "Ohne Raum", "room" => ""]; } foreach (raumListe() as $raum) { - if ($raum["key"] === $key) { + if ($raum["key"] === strval($key)) { return ["floor" => $raum["floor"], "room" => $raum["room"]]; } } - return ["floor" => "Unbekannter Raum", "room" => $key]; + return ["floor" => "Unbekannter Raum", "room" => strval($key)]; } /** * Raumvorschlaege aus den MQTT-Topics. * - * Die Thermostate tragen ihren Raum im Topic: "Raumtemp/OG/Bad/Temp[degC]". - * Das ist keine Rateaufgabe, sondern dieselbe Schreibweise wie in rooms.php, - * und deckt dreizehn Geraete ohne Zutun ab. Bei allen anderen wird bewusst - * nicht geraten: "Bad Links" sagt nichts darueber, auf welcher der drei - * Etagen dieses Bad liegt. + * Ein Thermostat meldet unter dem Zweig, der am Raum eingetragen ist + * (rooms.thermostat, etwa "Raumtemp/OG/Bad"). Ein Messwert darunter gehoert + * also zu diesem Raum - das ist keine Rateaufgabe und deckt die Thermostate + * ohne Zutun ab. Bei allen anderen wird bewusst nicht geraten: "Bad Links" + * sagt nichts darueber, auf welcher Etage dieses Bad liegt. */ function raumVorschlaege() { - $bekannt = []; - foreach (raumListe() as $raum) { - $bekannt[strtolower($raum["key"])] = $raum["key"]; + $zweige = []; + foreach (allRooms() as $raum) { + if (!empty($raum["thermostat"])) { + $zweige[strtolower(rtrim($raum["thermostat"], "/")) . "/"] = strval($raum["nr"]); + } } - $res = meshDb()->query("SELECT actor_id, url FROM actor_states - WHERE url LIKE 'Raumtemp/%/%/%'"); + if (!$zweige) { + return []; + } + $res = meshDb()->query("SELECT actor_id, url FROM actor_states WHERE url LIKE '%/%'"); $vorschlag = []; while ($row = $res->fetch_assoc()) { - $teile = explode("/", $row["url"]); - if (count($teile) < 3) { - continue; - } - $key = strtolower($teile[1] . "/" . $teile[2]); - if (isset($bekannt[$key])) { - $vorschlag[intval($row["actor_id"])] = $bekannt[$key]; + $url = strtolower($row["url"]); + foreach ($zweige as $zweig => $key) { + if (strpos($url, $zweig) === 0) { + $vorschlag[intval($row["actor_id"])] = $key; + break; + } } } return $vorschlag; @@ -436,7 +442,7 @@ function saveRooms($zuordnung) { $db = meshDb(); $erlaubt = array_column(raumListe(), "key"); - $stmt = $db->prepare("UPDATE actors SET room = ? WHERE id = ?"); + $stmt = $db->prepare("UPDATE actors SET room_id = ? WHERE id = ?"); $anzahl = 0; foreach ($zuordnung as $actorId => $key) { $actorId = intval($actorId); @@ -447,8 +453,8 @@ function saveRooms($zuordnung) if ($key !== "" && !in_array($key, $erlaubt, true)) { throw new InvalidArgumentException("Unbekannter Raum: " . $key); } - $wert = $key === "" ? null : $key; - $stmt->bind_param("si", $wert, $actorId); + $wert = $key === "" ? null : intval($key); + $stmt->bind_param("ii", $wert, $actorId); $stmt->execute(); $anzahl++; } @@ -475,7 +481,8 @@ function geraeteListe() $tipps = raumVorschlaege(); $geraete = []; - $res = $db->query("SELECT id, name, type, url, room FROM actors"); + // room_id als Text: derselbe Schluessel wie in raumListe(), siehe dort. + $res = $db->query("SELECT id, name, type, url, CAST(room_id AS CHAR) AS room FROM actors"); while ($row = $res->fetch_assoc()) { $id = intval($row["id"]); $wo = raumEinordnung($row["room"]); @@ -488,6 +495,7 @@ function geraeteListe() "room" => $row["room"], "roomLabel" => raumBeschriftung($row["room"]), "floor" => $wo["floor"], + "raumName" => $wo["room"], "tipp" => $tipps[$id] ?? null, "messwerte" => 0, "kommandos" => 0, @@ -542,17 +550,19 @@ function geraeteListe() $reihenfolge = array_flip($GLOBALS["floors"]); $liste = array_values($geraete); usort($liste, function ($a, $b) use ($reihenfolge) { - $wa = explode("/", (string)$a["room"], 2); - $wb = explode("/", (string)$b["room"], 2); // Geraete ohne Raum nach oben: sie sind das, was Arbeit macht. - $ra = ($a["room"] === null || $a["room"] === "") ? -1 : ($reihenfolge[$wa[0]] ?? 98); - $rb = ($b["room"] === null || $b["room"] === "") ? -1 : ($reihenfolge[$wb[0]] ?? 98); + $ra = ($a["room"] === null || $a["room"] === "") ? -1 : ($reihenfolge[$a["floor"]] ?? 98); + $rb = ($b["room"] === null || $b["room"] === "") ? -1 : ($reihenfolge[$b["floor"]] ?? 98); if ($ra !== $rb) { return $ra <=> $rb; } - $raum = strcoll($wa[1] ?? "", $wb[1] ?? ""); + $raum = strcoll($a["raumName"], $b["raumName"]); return $raum !== 0 ? $raum : strcoll($a["label"], $b["label"]); }); + // raumName war nur fuer die Sortierung da; die Maske liest roomLabel. + foreach ($liste as $i => $g) { + unset($liste[$i]["raumName"]); + } return $liste; } @@ -657,7 +667,7 @@ function deviceCatalog() $devices = []; $labels = geraeteBeschriftungen(); - $res = $db->query("SELECT id, name, type, url, room FROM actors"); + $res = $db->query("SELECT id, name, type, url, CAST(room_id AS CHAR) AS room FROM actors"); $zeilen = []; while ($row = $res->fetch_assoc()) { $zeilen[] = $row; diff --git a/restricted/home.php b/restricted/home.php index 03444bd..44b8d45 100644 --- a/restricted/home.php +++ b/restricted/home.php @@ -125,7 +125,7 @@ require_once(__DIR__ . "/reiter.php"); $zeilen = [1 => [12], 2 => [9, 16], 3 => [5, 11.5, 16.5]]; $y = $zeilen[count($room["werte"])] ?? []; ?> - ,)" onclick="openHeaterSettings('_');" cursor="pointer"> + ,)" onclick="openRoomView();" cursor="pointer"> _heater" href="#heater" display="none" /> diff --git a/restricted/kacheln.php b/restricted/kacheln.php index 2ed6b0d..558c0dd 100644 --- a/restricted/kacheln.php +++ b/restricted/kacheln.php @@ -156,6 +156,9 @@ function kachelListe() "floor" => $room["floor"], "floorLabel" => floorLabel($room["floor"]), "room" => $room["id"], + // Die feste Raumnummer, als Text wie in raumListe(): die Maske + // stellt damit die Geraete des eigenen Raums nach oben. + "nr" => strval($room["nr"]), "name" => $room["mqtt"], "eigen" => isset($room["werte"]), "werte" => $room["werte"] ?? kachelVorgabe($room), @@ -219,18 +222,19 @@ function kachelKatalog() continue; } /* - * Etage und Raum auch als Kuerzel, nicht nur ausgeschrieben: die + * Etage und Raum auch als Schluessel, nicht nur ausgeschrieben: die * Maske stellt die Geraete des eigenen Raums nach oben und muss dafuer - * genau vergleichen koennen. actors.room steht als "EG/Technik" da - - * dieselben Kuerzel, die auch rooms.php benutzt. + * genau vergleichen koennen. Der Raum ist die feste Raumnummer + * (actors.room_id), dieselbe wie "nr" in kachelListe(); die Etage das + * Kuerzel, das deviceCatalog() schon als floorLabel fuehrt. */ - $wo = explode("/", (string)($g["room"] ?? ""), 2); + $hatRaum = ($g["room"] ?? null) !== null && $g["room"] !== ""; $geraete[] = [ "label" => $g["label"], "floor" => $g["floorLabel"], - "floorKey" => $wo[0] ?? "", + "floorKey" => $hatRaum ? $g["floorLabel"] : "", "raum" => $g["roomName"], - "raumKey" => $wo[1] ?? "", + "raumKey" => $hatRaum ? strval($g["room"]) : "", "werte" => $werte, ]; } diff --git a/restricted/rooms.php b/restricted/rooms.php index 9b53817..3201952 100644 --- a/restricted/rooms.php +++ b/restricted/rooms.php @@ -24,7 +24,7 @@ * solange die Vorgabe gilt (siehe kachelVorgabe()) * * Ein Raum ohne x/y ist trotzdem ein Raum: Geräte lassen sich ihm zuordnen - * (actors.room), er erscheint nur nicht als Kachel im Grundriss. + * (actors.room_id), er erscheint nur nicht als Kachel im Grundriss. * * Ohne Datenbank oder ohne die beiden Tabellen gibt es keine Etagen - die * Startseite zeigt dann einen Hinweis statt eines Fatal Errors.