Grundriss gepflegt. Diese Datei liest sie und reicht sie in * der Form weiter, die Startseite, Menü, Automatik-Editor und Einstellungen * seit jeher erwarten. * * Ein Raum sieht deshalb aus wie früher, mit zwei Feldern mehr: * * 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 * (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. */ require_once(__DIR__ . "/meshdb.php"); /** * 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; } // 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. * * Alles, was zeichnet oder aktualisiert, geht hierüber - ein Raum ohne * Position hätte keine Stelle im SVG und keine Elemente, die sich füllen * liessen. */ function roomsOnFloor($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 grundriss()[1]; } /** Ausgeschriebener Name einer Etage; unbekannte bleiben ihr Kürzel. */ function floorLabel($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; } /** * Alle Bootstrap-Icons als Name => Codepunkt. * * Die Zahlen standen früher als Handabschrift hier - drei Stück, und jedes * weitere Symbol war eine Codeänderung. Sie stehen aber ohnehin schon im * Haus: css/bootstrap-icons.min.css führt sie unter .bi-::before, das * ist die Quelle, aus der auch der Browser liest. Also wird sie gelesen, * statt sie abzuschreiben; damit steht jedes der gut zweitausend Symbole zur * Verfügung, und ein Wechsel der Icon-Version zieht von selbst mit. * * Einmal je Anfrage - das Auslesen kostet rund eine Millisekunde, und die * Startseite fragt für jede Kachel nach. */ function bootstrapIcons() { static $icons = null; if ($icons !== null) { return $icons; } $icons = []; $css = @file_get_contents(__DIR__ . "/../css/bootstrap-icons.min.css"); if ($css === false) { return $icons; // ohne Datei eben ohne Symbole } // Im CSS steht content:"\f5a2" - der Backslash gehoert zum Inhalt, im // Muster ist er deshalb doppelt. if (preg_match_all('/\.bi-([a-z0-9-]+)::before\{content:"\\\\([0-9a-f]+)"\}/', $css, $treffer, PREG_SET_ORDER)) { foreach ($treffer as $t) { $icons[$t[1]] = hexdec($t[2]); } } return $icons; } /** * Das Zeichen eines Bootstrap-Icons für die Kacheln. * * Die Icons kommen als Schrift, nicht als Pfad: css/bootstrap-icons.min.css * ist ohnehin eingebunden (restricted/header.php), und SVG-Text darf dieselbe * Schriftfamilie benutzen wie HTML. Ein eingebettetes je Symbol wäre * dieselbe Wirkung mit mehr Zeilen. * * Ein unbekannter Name gibt einen leeren Text - die Kachel zeigt dann die * Zahl ohne Symbol, statt ein leeres Kästchen anzuzeigen. */ function kachelIcon($name) { $icons = bootstrapIcons(); return isset($icons[$name]) ? mb_chr($icons[$name], "UTF-8") : ""; } /** * Etagen mit Grundriss - die, die im Menü und im SVG auftauchen. * * 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 jede * Etage von Anfang an dabei. */ function floorsWithPlan() { return array_values(array_filter($GLOBALS["floors"], function ($floor) { return count(roomsOnFloor($floor)) > 0; })); } /** Nur die Räume mit Kachel, für die Aktualisierungsschleife im Browser. */ function roomsWithTile() { return array_values(array_map("mitWerten", array_filter(grundriss()[1], function ($room) { return isset($room["x"]); }))); } /** * Was eine Kachel ohne Zutun zeigt. * * 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 (empty($room["thermostat"])) { return []; } $zweig = rtrim($room["thermostat"], "/") . "/"; return [ ["topic" => $zweig . "Set Temp[degC]", "einheit" => "°C", "stellen" => 0], ["topic" => $zweig . "Temp[degC]", "einheit" => "°C", "gross" => true], ["topic" => $zweig . "rHum[%]", "einheit" => "%rF"], ]; } /** * 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 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"] = !empty($room["thermostat"]); $room["werte"] = $room["werte"] ?? kachelVorgabe($room); return $room; } /** * Was der Browser beim Broker anmelden muss, damit die Kacheln laufen. * * Nicht die einzelnen Topics, sondern ihre Zweige: aus vierzig Anmeldungen * für Raumtemp werden so vier insgesamt. Der Zweig bringt ausserdem mit, * was die Kachel nebenbei braucht - "Heating" und "mode" stehen in keiner * Werteliste, das Heizsymbol lebt aber davon. */ function tileTopics() { $zweige = []; foreach (roomsWithTile() as $room) { foreach ($room["werte"] as $wert) { // Ein Wert nennt entweder sein Topic oder einen Wechselrichter, // den homeMQTT.js unter solarManager/invertersN sucht. Ohne diese // Unterscheidung entstand aus dem fehlenden Topic ein "/#" - eine // Anmeldung auf einen leeren Zweig. $zweig = isset($wert["topic"]) ? explode("/", $wert["topic"])[0] : "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); }