diff --git a/ajax/settings.php b/ajax/settings.php index 83fc5be..1d083bd 100644 --- a/ajax/settings.php +++ b/ajax/settings.php @@ -122,7 +122,7 @@ if ($action === "kacheln") { "katalog" => kachelKatalog(), // Fuer die Eingabe von Hand, wenn etwas nicht im Katalog steht. "topics" => kachelTopicVorschlaege(), - "tabelleDa" => kachelTabelleDa(), + "tabelleDa" => grundrissDa(), "max" => KACHEL_MAX, ]); } catch (Throwable $e) { diff --git a/homeMesh_grundriss.sql b/homeMesh_grundriss.sql new file mode 100644 index 0000000..606e760 --- /dev/null +++ b/homeMesh_grundriss.sql @@ -0,0 +1,65 @@ +-- --------------------------------------------------------------------------- +-- Grundriss der Home-Ansicht: Etagen, Raeume, Kacheln - Datenbank `homeMesh` +-- --------------------------------------------------------------------------- +-- Vorher stand das alles fest in restricted/rooms.php, und die Werte auf den +-- Kacheln ließen sich getrennt davon in solarLog.kachelWerte ueberschreiben. +-- Fuer ein anderes Haus haette man den Quelltext umschreiben muessen. Jetzt +-- ist es Inhalt: eingestellt unter Einstellungen -> Grundriss, gelesen von +-- rooms.php. +-- +-- In homeMesh und nicht in solarLog: actors.room und automations.floor zeigen +-- auf Raeume und Etagen, und Fremdschluessel greifen nur innerhalb einer +-- Datenbank. +-- +-- Die Tabellen duerfen leer sein. Dann zeigt die Startseite einen Hinweis auf +-- die Einstellungen statt eines Hauses - das ist der Anfang jeder neuen +-- Installation. +-- --------------------------------------------------------------------------- + +SET NAMES utf8mb4; + +-- --------------------------------------------------------------------------- +-- Etagen. `code` ist der Schluessel und bleibt es: er steht in URLs +-- (?floor=EG), in SVG-Element-Ids (EG_Info), in automations.floor und im +-- MQTT-Baum. Umbenennen heisst deshalb `label` aendern, nicht `code`. +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS `floors` ( + `code` varchar(4) NOT NULL COMMENT 'Kuerzel: URL, SVG-Ids, automations.floor. Nach dem Anlegen fest', + `label` varchar(40) NOT NULL COMMENT 'Ausgeschriebener Name, z.B. Erdgeschoss', + `position` int(11) NOT NULL DEFAULT 0 COMMENT 'Reihenfolge in Menue, Reitern und Etagenschaltern', + `bild` varchar(255) DEFAULT NULL COMMENT 'Grundriss relativ zum Web-Root, z.B. tiles/grundriss/EG-1a2b.png. NULL = noch keins', + `standard` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Diese Etage oeffnet, wenn keine gewaehlt ist. Hoechstens eine', + PRIMARY KEY (`code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + +-- --------------------------------------------------------------------------- +-- Raeume. `id` ist die feste Nummer, auf die Geraete zeigen sollen; Name und +-- Kuerzel duerfen sich aendern. +-- +-- kuerzel Teil der SVG-Element-Ids: _w0_. Je Etage +-- eindeutig, sonst schriebe ein Wert in zwei Kacheln. +-- x, y linke obere Ecke der 24 x 19 grossen Kachel in der 400 x 300 +-- grossen Zeichenflaeche. NULL = Raum ohne Kachel. +-- thermostat Zweig der Thermostatwerte, z.B. Raumtemp/EG/Bad. Traegt das +-- Heizsymbol und die Vorgabewerte der Kachel (Soll, Ist, +-- Feuchte). NULL = kein Thermostat. +-- werte JSON-Liste der Kachelwerte (Form siehe restricted/kacheln.php). +-- NULL = die Vorgabe aus dem Thermostat; [] = bewusst kein +-- Messwert, die Kachel traegt nur ihren Namen. +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS `rooms` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `floor` varchar(4) NOT NULL, + `name` varchar(40) NOT NULL COMMENT 'Anzeigename, z.B. Wohnzimmer', + `kuerzel` varchar(40) NOT NULL COMMENT 'Teil der SVG-Ids, je Etage eindeutig', + `position` int(11) NOT NULL DEFAULT 0 COMMENT 'Reihenfolge innerhalb der Etage', + `x` smallint(6) DEFAULT NULL COMMENT 'Kachel: linke obere Ecke, NULL = keine Kachel', + `y` smallint(6) DEFAULT NULL, + `thermostat` varchar(120) DEFAULT NULL COMMENT 'MQTT-Zweig des Thermostats, NULL = keins', + `werte` text DEFAULT NULL COMMENT 'JSON der Kachelwerte, NULL = Vorgabe', + PRIMARY KEY (`id`), + UNIQUE KEY `uq_rooms_kuerzel` (`floor`, `kuerzel`), + UNIQUE KEY `uq_rooms_name` (`floor`, `name`), + CONSTRAINT `fk_rooms_floor` FOREIGN KEY (`floor`) REFERENCES `floors` (`code`) + ON UPDATE CASCADE ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/js/solar/homeMQTT.js b/js/solar/homeMQTT.js index 6597fa4..468adaf 100644 --- a/js/solar/homeMQTT.js +++ b/js/solar/homeMQTT.js @@ -203,7 +203,9 @@ const homeSVG = { } -var currentFloor = "OG"; +// 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; diff --git a/js/solar/settings.js b/js/solar/settings.js index 0513cf1..f208ef5 100644 --- a/js/solar/settings.js +++ b/js/solar/settings.js @@ -353,10 +353,9 @@ function zeichneKacheln() { if (!ziel || !kachelModell) return; if (!kachelModell.tabelleDa) { - ziel.innerHTML = '
Die Tabelle ' - + 'kachelWerte fehlt noch. Sie steht in ' - + 'solarLog_kacheln.sql – bis dahin zeigen alle Kacheln ' - + 'ihre Vorgabe, und Speichern ist nicht möglich.
'; + ziel.innerHTML = '
Etagen und Räume ' + + 'lassen sich nicht lesen – die Datenbank homeMesh ist nicht ' + + 'erreichbar, oder homeMesh_grundriss.sql fehlt noch.
'; return; } diff --git a/restricted/automations.php b/restricted/automations.php index 4be348f..1f0da51 100644 --- a/restricted/automations.php +++ b/restricted/automations.php @@ -14,21 +14,9 @@ * ausgewertet wird any(all(gruppe)). */ -require_once(__DIR__ . "/mysql.php"); +require_once(__DIR__ . "/meshdb.php"); require_once(__DIR__ . "/rooms.php"); -/** Verbindung zur Geraete- und Automatik-Datenbank. */ -function meshDb() -{ - static $db = null; - if ($db === null) { - $db = new mysqli($GLOBALS["mysql_server"], $GLOBALS["mysql_MeshUser"], - $GLOBALS["mysql_MeshPass"], $GLOBALS["mysql_MeshDB"]); - $db->set_charset("utf8mb4"); - } - return $db; -} - /** * Operatoren, die zu einem Datentyp passen. Immer ASCII - die huebschen * Zeichen macht erst die Anzeige daraus. Frueher schickte der Editor sein @@ -796,7 +784,7 @@ function emptyAutomation($floor) "name" => "", // Aufgerufen wird der Editor immer aus einem Etagenreiter. Fehlt die // Angabe trotzdem, gilt dieselbe Etage wie auf der Startseite. - "floor" => in_array($floor, $GLOBALS["floors"], true) ? $floor : "OG", + "floor" => in_array($floor, $GLOBALS["floors"], true) ? $floor : standardEtage(), "enabled" => true, "window_from" => "00:00", "window_to" => "23:59", diff --git a/restricted/header.php b/restricted/header.php index 75db266..979b519 100644 --- a/restricted/header.php +++ b/restricted/header.php @@ -9,7 +9,7 @@ require_once(__DIR__ . "/rooms.php"); // $_GET["action"] ist von index.php bereits auf eine gueltige Seite gesetzt. if (!isset($_GET["floor"])) - $_GET["floor"] = "OG"; + $_GET["floor"] = standardEtage(); ?> diff --git a/restricted/home.php b/restricted/home.php index a8c7fd2..03444bd 100644 --- a/restricted/home.php +++ b/restricted/home.php @@ -74,7 +74,7 @@ require_once(__DIR__ . "/reiter.php"); to= dur="0.7s" repeatCount="1" calcMode="spline" keyTimes="0;1" keySplines=".1,0,.27,1" fill="freeze" /> - + - > + > ; const homeTopics = ; - + const homeStandardEtage = ; + document.addEventListener('readystatechange', function () { if (event.target.readyState === 'complete') { switchFloor(''); diff --git a/restricted/kacheln.php b/restricted/kacheln.php index 481258a..2ed6b0d 100644 --- a/restricted/kacheln.php +++ b/restricted/kacheln.php @@ -2,13 +2,13 @@ /** * Modell-Schicht für die Anzeigewerte der Home-Kacheln. * - * Was auf einer Kachel steht, stand bisher fest in restricted/rooms.php. Das - * ist weiterhin die Vorgabe - hier kommt nur die Möglichkeit dazu, sie zu - * überschreiben, ohne die Datei anzufassen. Eine Kachel ohne Zeile in - * kachelWerte zeigt also genau das, was sie immer gezeigt hat. + * Gespeichert wird in homeMesh.rooms.werte. Steht dort NULL, gilt die Vorgabe + * (kachelVorgabe() in rooms.php: mit Thermostat Soll, Ist und Feuchte, sonst + * nichts). Bis September 2026 lagen die Werte getrennt in solarLog.kachelWerte + * und überschrieben eine Tabelle im Quelltext - beides gibt es nicht mehr. * - * Die Form einer Wertdefinition ist dieselbe wie in rooms.php, damit zwischen - * Maske, Datenbank und Anzeige nichts übersetzt werden muss: + * Die Form einer Wertdefinition ist überall dieselbe, damit zwischen Maske, + * Datenbank und Anzeige nichts übersetzt werden muss: * * topic MQTT-Topic, aus dem der Wert kommt * pfad optional: Schlüssel in einer JSON-Nachricht @@ -26,21 +26,15 @@ * das "bi-" davor ("thermometer-half"). Erlaubt ist jedes, * das im Stylesheet steht - siehe bootstrapIcons() in * rooms.php; kachelSymbole() ist nur die Vorschlagsliste. - * - * Die Tabelle darf fehlen. Ist sie nicht angelegt (solarLog_kacheln.sql), so - * verhält sich alles wie vorher, statt dass die Startseite mit einem - * SQL-Fehler stehenbleibt - die Kacheln sind keine Einstellung, ohne die man - * das Haus nicht mehr sieht. */ /* - * Geladen wird nicht hier, sondern in rooms.php: kachelVorgabe() und - * kachelUeberschreibungen() stehen dort, weil die Startseite sie braucht und - * diese Datei nicht. So bleibt der Weg zur Anzeige kurz - und es gibt keine + * Geladen wird nicht hier, sondern in rooms.php: kachelVorgabe() und das + * Lesen der Werte stehen dort, weil die Startseite sie braucht und diese + * Datei nicht. So bleibt der Weg zur Anzeige kurz - und es gibt keine * Ringabhängigkeit zwischen Raum-Modell und Maske. */ -require_once(__DIR__ . "/rooms.php"); // Räume, Vorgaben, Überschreibungen -require_once(__DIR__ . "/costs.php"); // solarDb() +require_once(__DIR__ . "/rooms.php"); // Räume, Vorgaben, meshDb() /** Höchstens drei Werte je Kachel - mehr passt nicht auf 24 x 19 Einheiten. */ const KACHEL_MAX = 3; @@ -153,20 +147,18 @@ function kachelIconNamen() */ function kachelListe() { - $ueber = kachelUeberschreibungen(); $liste = []; foreach (allRooms() as $room) { if (!isset($room["x"])) { continue; // kein Platz im Grundriss, keine Kachel } - $key = $room["floor"] . "|" . $room["id"]; $liste[] = [ "floor" => $room["floor"], "floorLabel" => floorLabel($room["floor"]), "room" => $room["id"], "name" => $room["mqtt"], - "eigen" => isset($ueber[$key]), - "werte" => $ueber[$key] ?? kachelVorgabe($room), + "eigen" => isset($room["werte"]), + "werte" => $room["werte"] ?? kachelVorgabe($room), "vorgabe" => kachelVorgabe($room), ]; } @@ -275,14 +267,7 @@ function kachelTopicVorschlaege() $topics = []; foreach (allRooms() as $room) { - foreach (kachelVorgabe($room) as $wert) { - if (!empty($wert["topic"])) { - $topics[$wert["topic"]] = true; - } - } - } - foreach (kachelUeberschreibungen() as $werte) { - foreach ($werte as $wert) { + foreach (array_merge(kachelVorgabe($room), $room["werte"] ?? []) as $wert) { if (!empty($wert["topic"])) { $topics[$wert["topic"]] = true; } @@ -381,19 +366,15 @@ function kachelWertPruefen($wert, $nummer) /** * Die Werte einer Kachel speichern. * - * $werte === null setzt auf die Vorgabe zurück - die Zeile verschwindet, statt - * die Vorgabe hineinzukopieren. Sonst stünde in der Datenbank eine Kopie, die - * nicht mitzöge, wenn sich rooms.php ändert. + * $werte === null setzt auf die Vorgabe zurück - in der Spalte steht dann + * NULL, statt die Vorgabe hineinzukopieren. Sonst stünde dort eine Kopie, die + * nicht mitzöge, wenn sich das Thermostat des Raums ändert. * * Eine leere Liste ist etwas anderes: die Kachel zeigt dann bewusst keinen * Messwert, sondern nur ihren Namen. */ function kachelWerteSpeichern($floor, $room, $werte) { - if (!kachelTabelleDa()) { - throw new RuntimeException( - "Die Tabelle kachelWerte fehlt. Sie steht in solarLog_kacheln.sql."); - } $treffer = null; foreach (allRooms() as $r) { if ($r["floor"] === $floor && $r["id"] === $room && isset($r["x"])) { @@ -404,10 +385,10 @@ function kachelWerteSpeichern($floor, $room, $werte) throw new InvalidArgumentException("Diese Kachel gibt es nicht."); } - $db = solarDb(); + $db = meshDb(); if ($werte === null) { - $stmt = $db->prepare("DELETE FROM kachelWerte WHERE floor = ? AND room = ?"); - $stmt->bind_param("ss", $floor, $room); + $stmt = $db->prepare("UPDATE rooms SET werte = NULL WHERE id = ?"); + $stmt->bind_param("i", $treffer["nr"]); $stmt->execute(); return null; } @@ -432,9 +413,8 @@ function kachelWerteSpeichern($floor, $room, $werte) } $json = json_encode($sauber, JSON_UNESCAPED_UNICODE); - $stmt = $db->prepare("INSERT INTO kachelWerte (floor, room, werte) VALUES (?, ?, ?) - ON DUPLICATE KEY UPDATE werte = VALUES(werte)"); - $stmt->bind_param("sss", $floor, $room, $json); + $stmt = $db->prepare("UPDATE rooms SET werte = ? WHERE id = ?"); + $stmt->bind_param("si", $json, $treffer["nr"]); if (!$stmt->execute()) { throw new RuntimeException($stmt->error); } diff --git a/restricted/meshdb.php b/restricted/meshdb.php new file mode 100644 index 0000000..dec153f --- /dev/null +++ b/restricted/meshdb.php @@ -0,0 +1,22 @@ +set_charset("utf8mb4"); + } + return $db; +} diff --git a/restricted/rooms.php b/restricted/rooms.php index 014a2bc..9b53817 100644 --- a/restricted/rooms.php +++ b/restricted/rooms.php @@ -1,180 +1,103 @@ Grundriss gepflegt. Diese Datei liest sie und reicht sie in + * der Form weiter, die Startseite, Menü, Automatik-Editor und Einstellungen + * seit jeher erwarten. * - * Aus dieser Tabelle entstehen sowohl die SVG-Kacheln in home.php als auch - * die Aktualisierungsschleife in js/solar/homeMQTT.js (die Tabelle wird dafür - * als JSON in die Seite geschrieben). Ein neuer oder umbenannter Raum ist - * damit eine Zeile hier statt drei Stellen im Code. + * Ein Raum sieht deshalb aus wie früher, mit zwei Feldern mehr: * - * floor Etage; bestimmt zugleich den Zweig Raumtemp// - * mqtt Name des Raums im MQTT-Baum: Raumtemp///... - * id Kürzel in den SVG-Element-IDs: _w0_, __heater - * x, y Position der Kachel im SVG (viewBox 400x300) - fehlt bei Räumen - * ohne Kachel - * werte Was auf der Kachel steht, sofern auf der Einstellungsseite nichts - * anderes gewählt wurde (Tabelle kachelWerte, siehe - * restricted/kacheln.php): bis zu drei Messwerte, jeder mit - * topic (oder wechselrichter, siehe Carport), optional pfad - * (Schlüssel in einer JSON-Nachricht) und entweder format - * ("leistung" skaliert selbst zwischen W und kW, "leistung_kw" - * ebenso fuer Quellen, die schon Kilowatt melden) oder dem Paar - * einheit und stellen (Nachkommastellen). Fehlt der Eintrag, sind - * es die drei Thermostatwerte - Soll, Ist, Feuchte - wie bisher. - * Eine leere Liste heisst: kein Messwert, die Kachel trägt nur - * ihren Namen und öffnet den Raum. + * nr feste Nummer (rooms.id) - bleibt, wenn der Raum umbenannt wird + * floor Etage; dieselben Kürzel wie in floors.code + * mqtt Name des Raums (rooms.name). Der Schlüssel heißt aus + * Gewohnheit weiter "mqtt": er stand früher zugleich im Topic + * Raumtemp///..., und zwanzig Stellen lesen ihn so. + * id Kürzel in den SVG-Element-IDs: _w0_, __heater + * x, y Position der Kachel im SVG (viewBox 400x300) - fehlt bei Räumen + * ohne Kachel + * thermostat Zweig der Thermostatwerte (Raumtemp/EG/Bad) oder null + * werte die auf der Einstellungsseite gesetzten Kachelwerte - fehlt, + * solange die Vorgabe gilt (siehe kachelVorgabe()) * * Ein Raum ohne x/y ist trotzdem ein Raum: Geräte lassen sich ihm zuordnen - * (siehe die Spalte actors.room und den Zuordnungsdialog in der Karte - * "Automatismen"), er erscheint nur nicht als Kachel im Grundriss. Derzeit - * hat jeder Raum eine Kachel; die Unterscheidung bleibt, weil ein neuer Raum - * zuerst als Zeile ohne Position entsteht und erst dann seinen Platz im - * Grundriss bekommt. + * (actors.room), er erscheint nur nicht als Kachel im Grundriss. * - * Wo eine Kachel hingehört, misst tools/kachelpositionen.php aus: Grundriss - * mit Raster, Geisterkachel am Zeiger, fertige Zeile zum Abschreiben. x/y - * ist die linke obere Ecke der 24 x 19 grossen Kachel, nicht ihre Mitte. + * Ohne Datenbank oder ohne die beiden Tabellen gibt es keine Etagen - die + * Startseite zeigt dann einen Hinweis statt eines Fatal Errors. */ -// solarDb() für die Kachelwerte. costs.php definiert nur Funktionen und -// verbindet sich erst, wenn eine davon gerufen wird. -require_once(__DIR__ . "/costs.php"); +require_once(__DIR__ . "/meshdb.php"); -// Reihenfolge im Menü und in der Anzeige; OG ist die Standardetage. -// -// AG ist das Außengelände - eine Etage ohne Grundriss. Was draußen hängt, -// stand vorher als Pseudo-Raum "aussen" in automations.php und war damit ein -// einziger Topf: Carport, Pergola, Garage und die Wechselrichter alle -// nebeneinander. Als Etage bekommt jeder davon einen Raum, und die -// Geräteauswahl im Automatik-Editor gruppiert danach wie überall sonst. -$floors = ["OG", "EG", "UG", "AG"]; +/** + * Etagen und Räume aus homeMesh, einmal je Anfrage. + * + * Gibt [etagen, raeume] zurück: etagen ist code => Zeile aus floors in der + * eingestellten Reihenfolge, raeume die Liste im Format oben. + */ +function grundriss() +{ + static $stand = null; + if ($stand !== null) { + return $stand; + } + $etagen = []; + $raeume = []; + try { + $db = meshDb(); + $res = $db->query("SELECT code, label, position, bild, standard FROM floors + ORDER BY position, code"); + while ($res && $z = $res->fetch_assoc()) { + $z["standard"] = (bool)$z["standard"]; + $etagen[$z["code"]] = $z; + } + $res = $db->query("SELECT r.id, r.floor, r.name, r.kuerzel, r.x, r.y, r.thermostat, r.werte + FROM rooms r JOIN floors f ON f.code = r.floor + ORDER BY f.position, r.position, r.id"); + while ($res && $z = $res->fetch_assoc()) { + $raum = [ + "nr" => intval($z["id"]), + "floor" => $z["floor"], + "mqtt" => $z["name"], + "id" => $z["kuerzel"], + "thermostat" => $z["thermostat"] !== null && $z["thermostat"] !== "" + ? $z["thermostat"] : null, + ]; + // Nur mit Position eine Kachel - isset($room["x"]) ist die Frage, + // die überall gestellt wird. + if ($z["x"] !== null && $z["y"] !== null) { + $raum["x"] = intval($z["x"]); + $raum["y"] = intval($z["y"]); + } + if ($z["werte"] !== null) { + $liste = json_decode($z["werte"], true); + if (is_array($liste)) { + $raum["werte"] = array_slice(array_values($liste), 0, 3); + } + } + $raeume[] = $raum; + } + } catch (Throwable $e) { + // Siehe oben: lieber ein leeres Haus mit Hinweis als ein Abbruch. + $etagen = []; + $raeume = []; + $GLOBALS["grundrissFehler"] = $e->getMessage(); + } + $stand = [$etagen, $raeume]; + return $stand; +} -// Ausgeschriebene Namen für Beschriftungen, wo Platz ist. Das Kürzel steht in -// URLs (?floor=AG), in SVG-Element-Ids (AG_Info) und im MQTT-Baum - dort will -// man weder Umlaute noch Länge. -$floorLabels = [ - "OG" => "Obergeschoss", - "EG" => "Erdgeschoss", - "UG" => "Untergeschoss", - "AG" => "Außengelände", -]; - -$rooms = [ - ["floor" => "OG", "mqtt" => "Wohnzimmer", "id" => "wozi", "x" => 180, "y" => 140], - ["floor" => "OG", "mqtt" => "KiZi", "id" => "kizi", "x" => 130, "y" => 80], - ["floor" => "OG", "mqtt" => "Schlafzimmer", "id" => "schlafen", "x" => 235, "y" => 70], - ["floor" => "OG", "mqtt" => "Bad", "id" => "bad", "x" => 260, "y" => 120], - ["floor" => "OG", "mqtt" => "Buero", "id" => "buero", "x" => 265, "y" => 175], - // Kein Thermostat, und noch kein Gerät zugeordnet - die Kachel trägt - // vorerst nur ihren Namen. - ["floor" => "OG", "mqtt" => "Kueche", "id" => "kueche", "x" => 122, "y" => 138, - "werte" => []], - ["floor" => "OG", "mqtt" => "Technik", "id" => "technik", "x" => 173, "y" => 67, - "werte" => []], - ["floor" => "EG", "mqtt" => "Wohnzimmer", "id" => "wozi", "x" => 185, "y" => 150], - ["floor" => "EG", "mqtt" => "Florian", "id" => "kizi", "x" => 130, "y" => 90], - ["floor" => "EG", "mqtt" => "Schlafen", "id" => "schlafen", "x" => 235, "y" => 80], - ["floor" => "EG", "mqtt" => "Bad", "id" => "bad", "x" => 253, "y" => 130], - ["floor" => "EG", "mqtt" => "Magdalena", "id" => "buero", "x" => 258, "y" => 180], - ["floor" => "EG", "mqtt" => "Kueche", "id" => "kueche", "x" => 126, "y" => 146, - "werte" => []], - // Der Shelly-Zähler des EG meldet seine Werte als JSON in einer einzigen - // Nachricht; "pfad" greift den Schlüssel daraus heraus. - ["floor" => "EG", "mqtt" => "Technik", "id" => "technik", "x" => 172, "y" => 78, - "werte" => [ - ["topic" => "Power_EG/status/em:0", "pfad" => "total_act_power", - "einheit" => "W", "stellen" => 0, "gross" => true], - ]], - ["floor" => "UG", "mqtt" => "Buero", "id" => "buero", "x" => 140, "y" => 140], - ["floor" => "UG", "mqtt" => "Bad", "id" => "bad", "x" => 210, "y" => 80], - ["floor" => "UG", "mqtt" => "Kueche", "id" => "kueche", "x" => 190, "y" => 195], - ["floor" => "UG", "mqtt" => "Technik", "id" => "technik", "x" => 238, "y" => 93, - "werte" => [ - ["topic" => "Power_UG/status/em:0", "pfad" => "total_act_power", - "einheit" => "W", "stellen" => 0, "gross" => true], - ["topic" => "wasser/main/value", "einheit" => "m³", "stellen" => 1], - ]], - // Außengelände: der Grundriss ist ein freigestelltes Rendering des - // Grundstücks (assets/img/AG.png). Die Kachelpositionen sind darauf - // eingemessen; nachklicken geht mit tools/kachelpositionen.php. - // - // Messwerte nur, wo es etwas zu messen gibt: die beiden Wallboxen und - // der Regen. Veranda, Terrasse und Pergola tragen ihren Namen - dort - // haengen Licht und Rollladen, und die zeigt das Raum-Modal. - // - // Die Namen stammen von den Geräten selbst: die Hoymiles heißen seit jeher - // "Carport 1-6" und "Veranda OG2-4", das Sonnensegel sitzt an der Pergola. - // Zwei Carports, einer je Wohnpartei - deshalb dieselben Kuerzel wie die - // Etagen. Die Wattpilot der EG-Partei steht dagegen in der Garage, siehe - // restricted/config.php; am Carport OG haengt der go-e Charger. - // Die PV des Carports haengt an mehreren Hoymiles. Sie werden ueber - // ihren Namen gesucht und aufsummiert, nicht ueber ihre Nummer: die - // Topics heissen solarManager/invertersN, und N ist eine Position in - // einer Liste. Aendert der SolarManager die Reihenfolge, zeigte eine - // fest eingetragene Nummer stillschweigend das falsche Dach an. - // - // Die Namen muessen genau so lauten wie unter invertersN/name. Passt - // einer nicht, schreibt homeMQTT.js die vorhandenen Namen in die - // Browser-Konsole - dann steht dort, was hier hingehoert. - ["floor" => "AG", "mqtt" => "Carport EG", "id" => "carport_eg", "x" => 179, "y" => 207, - "werte" => [ - ["wechselrichter" => ["Carport 1-6", "Carport 7-12", "Carport 13-16", - "Carport 17-19"], - "feld" => "p_AC", "format" => "leistung", "icon" => "sun"], - ]], - // Am Carport OG haengt der go-e Charger. Die Ladeleistung ist das - // Einzige, was sich hier messen laesst - und sie beantwortet die Frage, - // die man sich vor dem Carport stellt: laedt gerade jemand? - ["floor" => "AG", "mqtt" => "Carport OG", "id" => "carport_og", "x" => 72, "y" => 154, - "werte" => [ - ["topic" => "solarManager/evPowerOG", "format" => "leistung_kw", - "icon" => "ev-station"], - ]], - // Veranda und Terrasse gibt es zweimal, je Wohnpartei eine. In der - // Draufsicht laege das obere Paar genau auf dem unteren - deshalb die - // Schraegsicht, siehe restricted/aussenplan.php. - // - // Die beiden Veranden haben bewusst kein x/y: ihre Kacheln lagen auf der - // Westfassade des Hauses so dicht beieinander, dass sie mehr gestiftet - // als erklaert haben. Als Raeume bleiben sie bestehen - Geraete sind - // ihnen zugeordnet und Automatiken laufen darauf, das haengt allein - // daran, dass sie hier stehen. - // - // Kommen die Kacheln zurueck, sind es zwei Zahlen je Zeile (gemessen mit - // tools/kachelpositionen.php, zuletzt 156/132 und 150/92) und fuer die - // Veranda OG diese Wechselrichter: - // - // ["wechselrichter" => ["Veranda OG2-4", "Veranda UG/OG1"], - // "feld" => "p_AC", "format" => "leistung", "icon" => "sun"] - ["floor" => "AG", "mqtt" => "Veranda EG", "id" => "veranda_eg", "werte" => []], - ["floor" => "AG", "mqtt" => "Veranda OG", "id" => "veranda_og", "werte" => []], - ["floor" => "AG", "mqtt" => "Terrasse EG", "id" => "terrasse_eg", "x" => 229, "y" => 158, "werte" => []], - ["floor" => "AG", "mqtt" => "Terrasse OG", "id" => "terrasse_og", "x" => 223, "y" => 124, "werte" => []], - ["floor" => "AG", "mqtt" => "Pergola", "id" => "pergola", "x" => 344, "y" => 111, "werte" => []], - // In der Garage steht die Wattpilot der EG-Partei, siehe fahrzeuge() in - // restricted/config.php. Ohne Kuerzel im Topic ist das die EG-Wallbox. - ["floor" => "AG", "mqtt" => "Garage", "id" => "garage", "x" => 131, "y" => 135, - "werte" => [ - ["topic" => "solarManager/evPower", "format" => "leistung_kw", - "icon" => "ev-station"], - ]], - // Der Garten ist kein Bauteil wie die anderen, aber ein Ort: hier haengen - // der Regensensor und die beiden Ventilsteuerungen. - ["floor" => "AG", "mqtt" => "Garten", "id" => "garten", "x" => 298, "y" => 176, - "werte" => [ - // gatherRainData.py im SolarManager legt auf Wetter/Regen ein JSON - // mit tage1 bis tage7 und dem Stand des Sammlers. "tage1" ist der - // laufende Tag - die Zahl, die man im Garten wissen will. Ein - // laengeres Fenster ist ein geaenderter Pfad und sonst nichts. - ["topic" => "Wetter/Regen", "pfad" => "tage1", "einheit" => "mm", - "stellen" => 1, "icon" => "cloud-rain"], - ]], -]; +// Die drei globalen Namen, die seit jeher gelesen werden: Reihenfolge der +// Etagen, ihre Namen und die Raumliste. +// Über $GLOBALS und nicht als schlichte Zuweisung: wird diese Datei zuerst aus +// einer Funktion heraus eingebunden, landeten die Namen sonst in deren +// lokalem Gültigkeitsbereich. +$GLOBALS["rooms"] = grundriss()[1]; +$GLOBALS["floors"] = array_keys(grundriss()[0]); +$GLOBALS["floorLabels"] = array_column(grundriss()[0], "label", "code"); /** * Räume einer Etage, die eine Kachel im Grundriss haben. @@ -185,21 +108,64 @@ $rooms = [ */ function roomsOnFloor($floor) { - return array_values(array_filter($GLOBALS["rooms"], function ($room) use ($floor) { + return array_values(array_filter(grundriss()[1], function ($room) use ($floor) { return $room["floor"] == $floor && isset($room["x"]); })); } +/** + * Ließen sich Etagen und Räume lesen? Nein heißt: Datenbank weg oder + * homeMesh_grundriss.sql noch nicht eingespielt - nicht "noch nichts + * angelegt", das ist ein leeres, aber gültiges Haus. + */ +function grundrissDa() +{ + grundriss(); + return !isset($GLOBALS["grundrissFehler"]); +} + /** Alle Räume, auch die ohne Kachel - für die Geräte-Zuordnung. */ function allRooms() { - return $GLOBALS["rooms"]; + return grundriss()[1]; } /** Ausgeschriebener Name einer Etage; unbekannte bleiben ihr Kürzel. */ function floorLabel($floor) { - return $GLOBALS["floorLabels"][$floor] ?? $floor; + return grundriss()[0][$floor]["label"] ?? $floor; +} + +/** + * Die Etage, die öffnet, wenn keine gewählt ist. + * + * Stand früher als "OG" an vier Stellen. Ist keine als Standard markiert, + * gilt die erste mit Grundriss, sonst die erste überhaupt. + */ +function standardEtage() +{ + $etagen = grundriss()[0]; + foreach ($etagen as $code => $e) { + if ($e["standard"]) { + return $code; + } + } + $mitPlan = floorsWithPlan(); + if ($mitPlan) { + return $mitPlan[0]; + } + return array_key_first($etagen) ?? ""; +} + +/** + * Der Grundriss einer Etage als Pfad relativ zum Web-Root, oder null. + * + * Mitgelieferte Bilder liegen unter assets/img, hochgeladene unter + * tiles/grundriss - der Webserver darf nur dort schreiben. + */ +function etageBild($floor) +{ + return grundriss()[0][$floor]["bild"] ?? null; } /** @@ -257,13 +223,12 @@ function kachelIcon($name) /** * Etagen mit Grundriss - die, die im Menü und im SVG auftauchen. * - * Das Außengelände ist eine Etage ohne Bild: seine Räume haben keine Kachel, - * und ein Menüpunkt dorthin führte auf eine leere Zeichenfläche. Sobald der - * erste AG-Raum ein x/y bekommt, erscheint die Etage von selbst auch dort - - * es gibt keine zweite Liste, die man nachziehen müsste. + * Eine Etage ohne einen einzigen platzierten Raum führte auf eine leere + * Zeichenfläche. Sobald ihr erster Raum ein x/y bekommt, erscheint sie von + * selbst - es gibt keine zweite Liste, die man nachziehen müsste. * - * Für die Zuordnung und die Automatiken zählt dagegen $floors: dort ist das - * Außengelände von Anfang an dabei. + * Für die Zuordnung und die Automatiken zählt dagegen $floors: dort ist jede + * Etage von Anfang an dabei. */ function floorsWithPlan() { @@ -275,7 +240,7 @@ function floorsWithPlan() /** Nur die Räume mit Kachel, für die Aktualisierungsschleife im Browser. */ function roomsWithTile() { - return array_values(array_map("mitWerten", array_filter($GLOBALS["rooms"], function ($room) { + return array_values(array_map("mitWerten", array_filter(grundriss()[1], function ($room) { return isset($room["x"]); }))); } @@ -283,16 +248,16 @@ function roomsWithTile() /** * Was eine Kachel ohne Zutun zeigt. * - * Ohne eigene Angabe sind es die drei Thermostatwerte, die dort schon immer - * standen: Solltemperatur klein darüber, Isttemperatur gross, Feuchte klein - * darunter. + * Mit Thermostat die drei Werte, die dort schon immer standen: Solltemperatur + * klein darüber, Isttemperatur gross, Feuchte klein darunter. Ohne Thermostat + * nichts - die Kachel trägt dann ihren Namen. */ function kachelVorgabe($room) { - if (isset($room["werte"])) { - return array_slice($room["werte"], 0, 3); + if (empty($room["thermostat"])) { + return []; } - $zweig = "Raumtemp/" . $room["floor"] . "/" . $room["mqtt"] . "/"; + $zweig = rtrim($room["thermostat"], "/") . "/"; return [ ["topic" => $zweig . "Set Temp[degC]", "einheit" => "°C", "stellen" => 0], ["topic" => $zweig . "Temp[degC]", "einheit" => "°C", "gross" => true], @@ -300,78 +265,19 @@ function kachelVorgabe($room) ]; } -/** - * Gibt es die Tabelle mit den selbst gewählten Kachelwerten? - * - * Sie darf fehlen (solarLog_kacheln.sql legt sie an). Dann gilt überall die - * Vorgabe - die Startseite bleibt nicht mit einem SQL-Fehler stehen, nur weil - * eine Einstellung noch nie benutzt wurde. - */ -function kachelTabelleDa() -{ - static $da = null; - if ($da === null) { - /* - * Auch der Ausfall der Datenbank fuehrt hier nur zur Vorgabe. Die - * Kacheln holen ihre Zahlen vom Broker, nicht aus MySQL - die - * Startseite lief bisher ohne Datenbank, und das soll so bleiben. - * Ohne dieses try stuende dort im Ernstfall ein Fatal Error statt des - * Hauses. - */ - try { - $res = solarDb()->query("SHOW TABLES LIKE 'kachelWerte'"); - $da = $res && $res->num_rows > 0; - } catch (Throwable $e) { - $da = false; - } - } - return $da; -} - -/** - * Die von Hand gesetzten Werte, nach "Etage|Raum" abgelegt. - * - * Eine Abfrage je Seitenaufbau, nicht eine je Kachel - die Startseite zeichnet - * zwanzig davon. - */ -function kachelUeberschreibungen() -{ - static $werte = null; - if ($werte !== null) { - return $werte; - } - $werte = []; - if (!kachelTabelleDa()) { - return $werte; - } - try { - $res = solarDb()->query("SELECT floor, room, werte FROM kachelWerte"); - while ($res && $row = $res->fetch_assoc()) { - $liste = json_decode($row["werte"], true); - if (is_array($liste)) { - $werte[$row["floor"] . "|" . $row["room"]] = array_slice($liste, 0, 3); - } - } - } catch (Throwable $e) { - $werte = []; // siehe kachelTabelleDa(): dann eben die Vorgabe - } - return $werte; -} - /** * Die Messwerte einer Kachel, fertig ausgeschrieben. * * Zuerst das, was auf der Einstellungsseite gewählt wurde; sonst die Vorgabe. * - * Die Symbole für "heizt" und "Pufferbetrieb" hängen am Raum, nicht an der - * Anzeige: ein Thermostatraum bleibt einer, auch wenn auf seiner Kachel + * Die Symbole für "heizt" und "Pufferbetrieb" hängen am Thermostat, nicht an + * der Anzeige: ein Thermostatraum bleibt einer, auch wenn auf seiner Kachel * inzwischen etwas anderes steht. Anderswo gibt es nichts zu heizen. */ function mitWerten($room) { - $room["heizung"] = !isset($room["werte"]); - $eigen = kachelUeberschreibungen()[$room["floor"] . "|" . $room["id"]] ?? null; - $room["werte"] = $eigen !== null ? $eigen : kachelVorgabe($room); + $room["heizung"] = !empty($room["thermostat"]); + $room["werte"] = $room["werte"] ?? kachelVorgabe($room); return $room; } @@ -397,6 +303,11 @@ function tileTopics() : "solarManager"; $zweige[$zweig . "/#"] = true; } + // Das Heizsymbol braucht den Thermostatzweig, auch wenn kein Wert der + // Kachel daraus stammt. + if (!empty($room["thermostat"])) { + $zweige[explode("/", $room["thermostat"])[0] . "/#"] = true; + } } return array_keys($zweige); } diff --git a/solarLog_kacheln.sql b/solarLog_kacheln.sql deleted file mode 100644 index 5d0ddc5..0000000 --- a/solarLog_kacheln.sql +++ /dev/null @@ -1,26 +0,0 @@ --- Anzeigewerte der Home-Kacheln. --- --- Eine Zeile je Kachel, und nur fuer die Kacheln, an denen etwas geaendert --- wurde. Steht keine Zeile da, gilt die Vorgabe aus restricted/rooms.php: bei --- Thermostatraeumen die drei Werte Soll, Ist und Feuchte, sonst die dort --- eingetragene Liste. Die Tabelle ist damit reine Ueberschreibung - sie muss --- weder vollstaendig sein noch mitwachsen, wenn ein Raum hinzukommt. --- --- Die Werte stehen als JSON in einer Spalte und nicht in Einzelspalten. Eine --- Wertdefinition ist heterogen: mal ein Topic mit Pfad in einer --- JSON-Nachricht, mal eine Liste von Wechselrichternamen, dazu Einheit, --- Nachkommastellen, Symbol und ob die Zahl gross steht. In Spalten gegossen --- waeren das ein Dutzend Felder, von denen je nach Quelle die Haelfte leer --- bleibt - und jede neue Anzeigeoption waere eine Schemaaenderung. So ist es --- genau die Form, die restricted/rooms.php ohnehin verarbeitet. --- --- Geprueft wird beim Speichern in restricted/kacheln.php, nicht hier: die --- Datenbank kann JSON zwar halten, aber nicht wissen, welche Symbole es gibt. - -CREATE TABLE IF NOT EXISTS `kachelWerte` ( - `floor` varchar(4) NOT NULL COMMENT 'Etage, wie in rooms.php', - `room` varchar(40) NOT NULL COMMENT 'Kuerzel des Raums (rooms.id)', - `werte` text NOT NULL COMMENT 'JSON-Liste der Wertdefinitionen, hoechstens drei', - `aktualisiert` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), - PRIMARY KEY (`floor`, `room`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/tools/kachelpositionen.php b/tools/kachelpositionen.php index c8d2fae..50f2f3b 100644 --- a/tools/kachelpositionen.php +++ b/tools/kachelpositionen.php @@ -121,7 +121,7 @@ const PLAN_HOCH = 300; -