[ "thermometer-half" => "Temperatur", "thermometer-sun" => "Temperatur draußen", "thermometer-snow" => "Kälte", "droplet-half" => "Feuchte", "moisture" => "Luftfeuchte", "fan" => "Lüftung", "fire" => "Heizung", "wind" => "Wind", ], "Strom" => [ "lightning-charge-fill" => "Leistung", "plug-fill" => "Verbrauch", "sun" => "Erzeugung", "battery-half" => "Speicher", "battery-charging" => "Ladung", "ev-station" => "Wallbox", "car-front" => "Fahrzeug", "speedometer2" => "Zähler", "graph-up-arrow" => "Ertrag", "arrow-down-up" => "Bezug und Einspeisung", ], "Wasser und Wetter" => [ "cloud-rain" => "Regen", "cloud-sun" => "Wetter", "snow" => "Schnee", "water" => "Wasserstand", "bucket" => "Menge", "tree" => "Garten", ], "Haus" => [ "lightbulb" => "Licht", "door-open" => "Tür", "window" => "Fenster", "house-door" => "Haus", "clock-history" => "Zeit", "wifi" => "Verbindung", ], ]; } /** Die Vorschläge flach, wie die Maske sie braucht: Name => Beschriftung. */ function kachelSymboleFlach() { $flach = []; foreach (kachelSymbole() as $gruppe) { foreach ($gruppe as $name => $titel) { $flach[$name] = $titel; } } return $flach; } /** * Alle Symbolnamen für die Vorschlagsliste der Maske. * * Die Vorschläge zuerst und mit ihrer Beschriftung, danach der ganze Rest * alphabetisch: so steht das Übliche oben, und wer etwas anderes will, tippt * los und bekommt es angeboten. */ function kachelIconNamen() { $vorschlag = kachelSymboleFlach(); $liste = []; foreach ($vorschlag as $name => $titel) { if (isset(bootstrapIcons()[$name])) { $liste[] = ["name" => $name, "titel" => $titel]; } } foreach (array_keys(bootstrapIcons()) as $name) { if (!isset($vorschlag[$name])) { $liste[] = ["name" => $name, "titel" => ""]; } } return $liste; } /** * Alle Kacheln mit ihrem aktuellen Stand, für die Einstellungsseite. * * Je Kachel steht dabei, ob die Werte die Vorgabe sind oder von Hand gesetzt - * die Maske zeigt "Standard wiederherstellen" nur dort, wo es etwas * herzustellen gibt. */ function kachelListe() { $liste = []; foreach (allRooms() as $room) { if (!isset($room["x"])) { continue; // kein Platz im Grundriss, keine Kachel } $liste[] = [ "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), "vorgabe" => kachelVorgabe($room), ]; } return $liste; } /** * Die Messwerte, die sich auswählen lassen - nach Gerät gruppiert. * * Quelle ist derselbe Katalog, aus dem der Automatik-Editor seine Bedingungen * baut (deviceCatalog() in restricted/automations.php). Damit steht in beiden * Masken dasselbe zur Auswahl, und ein neu erkanntes Gerät taucht ohne Zutun * auch hier auf. * * Nicht alles daraus taugt für eine Kachel: die Tahoma-Geräte melden ihre * Zustände über die Box, nicht über den Broker (ihre "url" ist dann ein * Zustandsname wie core:OpenClosedState). Eine Kachel liest aber ausschließlich * MQTT - solche Werte blieben ewig auf "–" stehen und sind deshalb hier nicht * dabei. Übrig bleibt, was ein Topic hat. * * Der Katalog darf fehlen: ohne Mesh-Datenbank gibt es eben keine Auswahl, * und die Maske fällt auf die Eingabe von Hand zurück. */ function kachelKatalog() { $datei = __DIR__ . "/automations.php"; if (!file_exists($datei)) { return []; } require_once($datei); try { $katalog = deviceCatalog(); } catch (Throwable $e) { return []; } $geraete = []; foreach ($katalog as $g) { $werte = []; foreach ($g["states"] as $s) { $topic = (string)($s["url"] ?? ""); // Ein Topic hat Schrägstriche und kein Schema davor; "core:..." // und "io://..." kommen von der Tahoma-Box. if ($topic === "" || strpos($topic, "/") === false || preg_match('/^[a-z]+:/', $topic)) { continue; } $werte[] = [ "name" => $s["name"], "einheit" => (string)($s["unit"] ?? ""), "topic" => $topic, "pfad" => (string)($s["pfad"] ?? ""), "wert" => $s["value"], ]; } if (!$werte) { continue; } /* * 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. Der Raum ist die feste Raumnummer * (actors.room_id), dieselbe wie "nr" in kachelListe(); die Etage das * Kuerzel, das deviceCatalog() schon als floorLabel fuehrt. */ $hatRaum = ($g["room"] ?? null) !== null && $g["room"] !== ""; $geraete[] = [ "label" => $g["label"], "floor" => $g["floorLabel"], "floorKey" => $hatRaum ? $g["floorLabel"] : "", "raum" => $g["roomName"], "raumKey" => $hatRaum ? strval($g["room"]) : "", "werte" => $werte, ]; } /* * Nach Etage sortieren, darin nach Raum und Geraet. deviceCatalog() * liefert alphabetisch nach Geraetenamen - in einer nach Etagen * gruppierten Liste stuende dieselbe Etage dann mehrfach da, jedes Mal * mit ein, zwei Geraeten darin. */ $reihenfolge = array_flip($GLOBALS["floors"]); usort($geraete, function ($a, $b) use ($reihenfolge) { $fa = $reihenfolge[$a["floorKey"]] ?? 99; $fb = $reihenfolge[$b["floorKey"]] ?? 99; if ($fa !== $fb) { return $fa <=> $fb; } $raum = strcoll($a["raum"], $b["raum"]); return $raum !== 0 ? $raum : strcoll($a["label"], $b["label"]); }); return $geraete; } /** * Topics für die Vorschlagsliste der Eingabe von Hand. * * Alles aus dem Katalog plus das, was die Kacheln heute zeigen - denn nicht * jedes Topic am Broker ist irgendwo registriert. Die beiden Shelly-Zähler in * den Technikräumen etwa stehen in keinem Katalog, sind aber auf zwei Kacheln * in Gebrauch. */ function kachelTopicVorschlaege() { $topics = []; foreach (allRooms() as $room) { foreach (array_merge(kachelVorgabe($room), $room["werte"] ?? []) as $wert) { if (!empty($wert["topic"])) { $topics[$wert["topic"]] = true; } } } foreach (kachelKatalog() as $g) { foreach ($g["werte"] as $w) { $topics[$w["topic"]] = true; } } $liste = array_keys($topics); sort($liste, SORT_NATURAL | SORT_FLAG_CASE); return $liste; } /** * Eine Wertdefinition prüfen und auf das Nötige eindampfen. * * Geworfen wird bei allem, was die Anzeige nicht darstellen könnte. Was nur * überflüssig ist - eine Einheit neben format "leistung" etwa - fällt still * weg, damit in der Datenbank keine Angaben stehen, die niemand liest. */ function kachelWertPruefen($wert, $nummer) { if (!is_array($wert)) { throw new InvalidArgumentException("Wert " . $nummer . " ist unlesbar."); } $sauber = []; $wechselrichter = array_values(array_filter(array_map( "trim", (array)($wert["wechselrichter"] ?? [])))); $topic = trim((string)($wert["topic"] ?? "")); if ($wechselrichter) { $sauber["wechselrichter"] = $wechselrichter; $feld = trim((string)($wert["feld"] ?? "p_AC")); $sauber["feld"] = $feld !== "" ? $feld : "p_AC"; } elseif ($topic !== "") { if (mb_strlen($topic) > 120) { throw new InvalidArgumentException("Wert " . $nummer . ": das Topic ist zu lang."); } $sauber["topic"] = $topic; $pfad = trim((string)($wert["pfad"] ?? "")); if ($pfad !== "") { $sauber["pfad"] = $pfad; } } else { throw new InvalidArgumentException( "Wert " . $nummer . " braucht ein Topic oder einen Wechselrichter."); } // Entweder das Format rechnet die Einheit selbst aus, oder sie steht da. if (in_array($wert["format"] ?? "", KACHEL_LEISTUNG, true)) { $sauber["format"] = $wert["format"]; } else { $einheit = trim((string)($wert["einheit"] ?? "")); if (mb_strlen($einheit) > 8) { throw new InvalidArgumentException("Wert " . $nummer . ": die Einheit ist zu lang."); } if ($einheit !== "") { $sauber["einheit"] = $einheit; } $stellen = $wert["stellen"] ?? null; if ($stellen !== null && $stellen !== "") { $stellen = intval($stellen); if ($stellen < 0 || $stellen > 3) { throw new InvalidArgumentException( "Wert " . $nummer . ": Nachkommastellen gehen von 0 bis 3."); } $sauber["stellen"] = $stellen; } } // Jedes Bootstrap-Icon darf es sein; geprüft wird gegen dieselbe Liste, // aus der die Anzeige später das Zeichen holt. Ein Tippfehler faellt so // beim Speichern auf und nicht erst als leere Stelle auf der Kachel. $icon = trim((string)($wert["icon"] ?? "")); if ($icon !== "") { if (!isset(bootstrapIcons()[$icon])) { throw new InvalidArgumentException( "Wert " . $nummer . ": das Symbol \"" . $icon . "\" gibt es nicht. " . "Die Namen stehen auf icons.getbootstrap.com, ohne das \"bi-\" davor."); } $sauber["icon"] = $icon; } if (!empty($wert["negativ"])) { $sauber["negativ"] = true; } if (!empty($wert["gross"])) { $sauber["gross"] = true; } return $sauber; } /** * Die Werte einer Kachel speichern. * * $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) { $treffer = null; foreach (allRooms() as $r) { if ($r["floor"] === $floor && $r["id"] === $room && isset($r["x"])) { $treffer = $r; } } if (!$treffer) { throw new InvalidArgumentException("Diese Kachel gibt es nicht."); } $db = meshDb(); if ($werte === null) { $stmt = $db->prepare("UPDATE rooms SET werte = NULL WHERE id = ?"); $stmt->bind_param("i", $treffer["nr"]); $stmt->execute(); return null; } if (!is_array($werte)) { throw new InvalidArgumentException("Die Werteliste ist unlesbar."); } if (count($werte) > KACHEL_MAX) { throw new InvalidArgumentException( "Mehr als " . KACHEL_MAX . " Werte passen nicht auf eine Kachel."); } $sauber = []; foreach (array_values($werte) as $i => $wert) { $sauber[] = kachelWertPruefen($wert, $i + 1); } // Zwei grosse Zahlen nebeneinander gibt es nicht: die Kachel hat genau // eine Zeile, die aus der Entfernung lesbar ist. $grosse = count(array_filter($sauber, function ($w) { return !empty($w["gross"]); })); if ($grosse > 1) { throw new InvalidArgumentException("Nur ein Wert darf gross stehen."); } $json = json_encode($sauber, JSON_UNESCAPED_UNICODE); $stmt = $db->prepare("UPDATE rooms SET werte = ? WHERE id = ?"); $stmt->bind_param("si", $json, $treffer["nr"]); if (!$stmt->execute()) { throw new RuntimeException($stmt->error); } return $sauber; }