/ * 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. * * 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. * * 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. */ // solarDb() für die Kachelwerte. costs.php definiert nur Funktionen und // verbindet sich erst, wenn eine davon gerufen wird. require_once(__DIR__ . "/costs.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"]; // 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"], ]], ]; /** * 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($GLOBALS["rooms"], function ($room) use ($floor) { return $room["floor"] == $floor && isset($room["x"]); })); } /** Alle Räume, auch die ohne Kachel - für die Geräte-Zuordnung. */ function allRooms() { return $GLOBALS["rooms"]; } /** Ausgeschriebener Name einer Etage; unbekannte bleiben ihr Kürzel. */ function floorLabel($floor) { return $GLOBALS["floorLabels"][$floor] ?? $floor; } /** * 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. * * 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. * * Für die Zuordnung und die Automatiken zählt dagegen $floors: dort ist das * Außengelände 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($GLOBALS["rooms"], function ($room) { return isset($room["x"]); }))); } /** * 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. */ function kachelVorgabe($room) { if (isset($room["werte"])) { return array_slice($room["werte"], 0, 3); } $zweig = "Raumtemp/" . $room["floor"] . "/" . $room["mqtt"] . "/"; return [ ["topic" => $zweig . "Set Temp[degC]", "einheit" => "°C", "stellen" => 0], ["topic" => $zweig . "Temp[degC]", "einheit" => "°C", "gross" => true], ["topic" => $zweig . "rHum[%]", "einheit" => "%rF"], ]; } /** * 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 * 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); 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; } } return array_keys($zweige); }