From 17f972d08150ef96bff0e80fd42def2c48f50636 Mon Sep 17 00:00:00 2001 From: Moirtz Wagner Date: Mon, 31 Aug 2026 21:18:46 +0200 Subject: [PATCH] Raum-Modal: Bedienelemente statt Kommandoliste Das Modal zeigte jedes Kommando so, wie es in der Datenbank steht: eine Jalousie kam mit acht Rohbefehlen daher, darunter "Winken", die Farbe einer Lampe als drei Zahlenfelder 0-255, ein Stromzaehler als abgeschnittene Textzeile. Die Solltemperatur stand doppelt da - einmal als Schieberegler, einmal als Zahlenfeld der Thermostat-Karte. Jetzt entscheidet restricted/roomControls.php, welches Geraet welches Bedienelement bekommt, und zwar an seinen Faehigkeiten statt am Typnamen: hat es ein "setClosure"? ein An/Aus-Paar? drei Parameter rot/gruen/blau? Damit bekommt auch ein neu gefundenes Geraet sein passendes Element, ohne dass jemand einen Typnamen nachtraegt. Beschattung Auf/Stop/Zu, darunter Position und Neigung als Schieber auf dem zuletzt gemeldeten Stand; "Winken" und "my-Position" im Mehr-Menue Licht Kippschalter, Helligkeit, Farbwaehler des Browsers, Effekt und Preset als Auswahl Schalten ein Kippschalter statt dreier Knoepfe Messwerte Wertegitter, die sechs wichtigsten sichtbar - beim Wasserzaehler standen vorher MAC-Adresse und freier Speicher vorn und der Verbrauch hinter "12 weitere" Heizung nur noch der Schieberegler, daneben Ist-Temperatur und Feuchte live aus dem MQTT-Strom Ab zwei Geraeten einer Art ordnet eine Ueberschrift; darunter waere sie nur Aufwand. Schieberegler schicken beim Loslassen, nicht beim Ziehen - sonst bekaeme eine Jalousie bei jedem Pixel ein neues Ziel. Waehrend ein Kommando unterwegs ist, sperrt sich die Karte sichtbar: ein "Zu" dauert ueber eine Minute, und ohne Sperre tippt man in der Zeit nach. Co-Authored-By: Claude Opus 5 --- ajax/room.php | 123 +++----- css/solar.css | 88 ++++++ js/solar/homeMQTT.js | 106 ++++++- restricted/automations.php | 8 +- restricted/roomControls.php | 546 ++++++++++++++++++++++++++++++++++++ 5 files changed, 780 insertions(+), 91 deletions(-) create mode 100644 restricted/roomControls.php diff --git a/ajax/room.php b/ajax/room.php index 647cd55..518d77c 100644 --- a/ajax/room.php +++ b/ajax/room.php @@ -22,6 +22,7 @@ set_time_limit(180); require_once("../helper.php"); require_once("../restricted/automations.php"); require_once("../restricted/commands.php"); +require_once("../restricted/roomControls.php"); require_once("./phpMQTT.php"); if (!checkLogin()) { @@ -75,6 +76,7 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") { } } + // --- Inhalt des Modals ---------------------------------------------------- $schluessel = preg_replace('/[^A-Za-z0-9_]/', '', strval($_GET["room"] ?? "")); $teile = explode("_", $schluessel, 2); @@ -90,106 +92,59 @@ foreach (allRooms() as $raum) { } } +// Alles im Raum, sortiert nach Bedienform. Das Thermostat selbst faellt +// heraus: es wird oben zum Schieberegler und braucht daneben keine zweite +// Bedienung - "Solltemperatur" stand vorher zweimal im Modal. $katalog = deviceCatalog(); -$geraete = array_values(array_filter($katalog, function ($d) use ($raumKey) { - return $d["room"] === $raumKey && count($d["commands"]) > 0; -})); -$messwerte = array_values(array_filter($katalog, function ($d) use ($raumKey) { - return $d["room"] === $raumKey && count($d["states"]) > 0 && count($d["commands"]) === 0; -})); +$gruppen = []; +foreach ($katalog as $d) { + if ($d["room"] !== $raumKey) { + continue; + } + $form = bedienform($d); + if ($form === "heizung") { + continue; + } + $gruppen[$form][] = $d; +} -echo "
"; +echo "
"; if ($hatThermostat) { echo "
"; - echo ""; + echo "
"; + echo ""; + echo ""; + echo "
"; echo ""; - echo "
" - . "10 °C30 °C
"; + // Der Ist-Zustand kommt live ueber MQTT, nicht aus der Datenbank - der + // Browser hoert ohnehin schon auf Raumtemp/#, und ein Wert von vorhin + // waere neben dem Regler nur irrefuehrend. + echo "
"; + echo " °C"; + echo " %rF"; + echo "heizt"; + echo "
"; echo "
"; } -if (!$geraete && !$messwerte && !$hatThermostat) { +if (!$gruppen && !$hatThermostat) { echo "

Diesem Raum ist noch nichts zugeordnet. " . "Das geht über das Haus-Symbol in der Karte „Automatismen“.

"; } -foreach ($geraete as $d) { - echo "
"; - echo "
" . htmlspecialchars($d["label"]) - . " " . htmlspecialchars($d["type"]) . "
"; - - // Kommandos ohne Parameter werden Knoepfe nebeneinander - "Auf", "Zu", - // "Stop" will man antippen und nicht erst einstellen. Die Reihenfolge in - // der Datenbank ist die des Geraete-Suchlaufs und damit Zufall; die - // haeufig gebrauchten kommen deshalb nach vorne. - $knoepfe = array_values(array_filter($d["commands"], function ($c) { return !count($c["params"]); })); - $vorne = ["Auf", "Zu", "Stop", "An", "Aus", "turn_on", "turn_off", "toggle"]; - usort($knoepfe, function ($a, $b) use ($vorne) { - $ia = array_search($a["name"], $vorne, true); - $ib = array_search($b["name"], $vorne, true); - $ia = $ia === false ? 99 : $ia; - $ib = $ib === false ? 99 : $ib; - return $ia === $ib ? strcoll($a["name"], $b["name"]) : $ia - $ib; - }); - if ($knoepfe) { - echo "
"; - foreach ($knoepfe as $c) { - echo ""; - } - echo "
"; +foreach (raumGruppenTitel() as $form => $titel) { + if (empty($gruppen[$form])) { + continue; } - - foreach ($d["commands"] as $c) { - if (!count($c["params"])) { - continue; - } - echo "
"; - echo "" . htmlspecialchars($c["name"]) . ""; - foreach ($c["params"] as $p) { - if (count($c["params"]) > 1) { - echo "" . htmlspecialchars($p["name"]) . ""; - } - if ($p["options"]) { - echo ""; - } else { - echo ""; - } - } - echo ""; - echo "
"; + // Eine einzelne Lampe braucht keine Ueberschrift "Licht" - der Name der + // Lampe steht direkt darunter. Ueberschriften ordnen erst ab zwei. + if (count($gruppen[$form]) > 1) { + echo gruppenTitel($titel[0], $titel[1]); } - echo "
"; -} - -// Geraete ohne Kommandos: nur anzeigen, was sie melden. -foreach ($messwerte as $d) { - echo "
"; - echo "
" . htmlspecialchars($d["label"]) - . " " . htmlspecialchars($d["type"]) . "
"; - echo "
"; - $teile = []; - foreach (array_slice($d["states"], 0, 8) as $s) { - $teile[] = htmlspecialchars($s["name"]) . ": " . htmlspecialchars(strval($s["value"])) - . ($s["unit"] ? " " . htmlspecialchars($s["unit"]) : ""); + foreach ($gruppen[$form] as $d) { + echo zeichneGeraet($d, $form); } - echo implode(" · ", $teile); - if (count($d["states"]) > 8) { - echo " · … " . (count($d["states"]) - 8) . " weitere"; - } - echo "
"; } echo "
"; diff --git a/css/solar.css b/css/solar.css index b13d00d..1bbac26 100644 --- a/css/solar.css +++ b/css/solar.css @@ -111,3 +111,91 @@ input[type='range']::-webkit-slider-runnable-track { .input-group.raum-cmd > .form-select { min-width: 5rem; } + + +/* -------------------------------------------------------------------------- + Raum-Modal: Bedienelemente + Erzeugt von restricted/roomControls.php, verdrahtet in js/solar/homeMQTT.js. + -------------------------------------------------------------------------- */ + +/* Ueberschrift einer Geraetegruppe (Beschattung, Licht, ...). Klein und + ruhig - sie ordnet, sie soll nicht mit den Geraeten um Aufmerksamkeit + ringen. */ +.raum-gruppe { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + margin: 0.75rem 0 0.35rem; +} +.raum-gruppe:first-child { + margin-top: 0; +} + +/* Waehrend ein Kommando unterwegs ist. Ein "Zu" an einer Jalousie dauert + ueber eine Minute; ohne sichtbare Sperre tippt man in der Zeit nach. */ +.raum-geraet.raum-warte { + opacity: 0.55; + pointer-events: none; +} +.raum-geraet.raum-warte::after { + content: ""; + position: absolute; + top: 0.5rem; + right: 0.5rem; + width: 0.9rem; + height: 0.9rem; + border: 2px solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: raum-dreht 0.8s linear infinite; +} +.raum-geraet { + position: relative; +} +@keyframes raum-dreht { + to { transform: rotate(360deg); } +} + +/* Schieberegler stehen dicht untereinander (Position und Neigung). Ohne den + Abstand kleben Beschriftung und Bahn aneinander. */ +.raum-regler + .raum-regler { + margin-top: 0.35rem; +} +.raum-regler .form-range { + margin-bottom: 0; +} + +/* Messwerte als Gitter statt als Textwurst: ein Stromzaehler hat sechzehn + Werte, die in einer Zeile niemand findet. auto-fill statt fester Spalten, + damit auf dem Handy zwei und auf dem Schirm vier nebeneinander passen. */ +.raum-werte { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr)); + gap: 0.4rem; +} +.raum-wert { + background: rgba(128, 128, 128, 0.08); + border-radius: 0.35rem; + padding: 0.3rem 0.45rem; + overflow: hidden; +} +.raum-wert-zahl { + font-size: 0.95rem; + font-weight: 600; + line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.raum-wert-einheit { + font-size: 0.7rem; + font-weight: 400; + opacity: 0.7; +} +.raum-wert-name { + font-size: 0.68rem; + opacity: 0.7; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/js/solar/homeMQTT.js b/js/solar/homeMQTT.js index 967bcd8..915dd79 100644 --- a/js/solar/homeMQTT.js +++ b/js/solar/homeMQTT.js @@ -208,13 +208,95 @@ function switchFloor(floor){ // eine Falle. btn.hidden = !slider; - koerper.querySelectorAll("button[data-cmd]").forEach(k => { - k.addEventListener("click", () => schalte(k)); - }); + raumBedienungAktivieren(koerper); + raumMesswerteAktualisieren(teile[0], teile[1]); }) .catch(fehler => { koerper.innerHTML = "Konnte den Raum nicht laden: " + fehler.message; }); } + /** + * Die Bedienelemente des Raum-Modals mit ihren Kommandos verbinden. + * + * Welches Geraet welches Element bekommt, entscheidet der Server + * (restricted/roomControls.php); hier haengen nur die Zuhoerer an den + * data-Attributen, die er dabei setzt. + */ + function raumBedienungAktivieren(koerper) { + koerper.querySelectorAll("button[data-cmd]").forEach(k => { + k.addEventListener("click", () => schalte(k)); + }); + + // Schieberegler: waehrend des Ziehens nur die Beschriftung, geschickt + // wird erst beim Loslassen. Sonst bekaeme eine Jalousie bei jedem Pixel + // ein neues Ziel und kaeme aus dem Anfahren nicht mehr heraus. + koerper.querySelectorAll(".raum-regler").forEach(regler => { + const schieber = regler.querySelector("input[type=range]"); + const anzeige = regler.querySelector(".raum-regler-wert"); + const einheit = schieber.dataset.einheit || ""; + schieber.addEventListener("input", () => { anzeige.textContent = schieber.value + einheit; }); + schieber.addEventListener("change", () => { + const params = {}; + params[regler.dataset.param] = schieber.value; + raumKommando(Number(regler.dataset.cmd), params, regler); + }); + }); + + // Kippschalter: An und Aus sind zwei getrennte Kommandos. Kennt das + // Geraet nur "umschalten", tut es auch das - dann stimmt die Stellung + // hinterher, weil wir sie selbst gesetzt haben. + koerper.querySelectorAll(".raum-schalter").forEach(schalter => { + const kasten = schalter.querySelector("input[type=checkbox]"); + const beschriftung = schalter.querySelector("label"); + kasten.addEventListener("change", () => { + const ziel = kasten.checked ? schalter.dataset.ein : schalter.dataset.aus; + const cmd = ziel || schalter.dataset.um; + beschriftung.textContent = kasten.checked ? "An" : "Aus"; + if (!cmd) return; + raumKommando(Number(cmd), {}, schalter); + }); + }); + + // Farbe: der Browser bringt seinen Farbwaehler mit, das Geraet will drei + // Zahlen 0-255. Zerlegt wird deshalb erst hier. + koerper.querySelectorAll(".raum-farbe").forEach(feld => { + const eingabe = feld.querySelector("input[type=color]"); + eingabe.addEventListener("change", () => { + const hex = eingabe.value; + const params = {}; + params[feld.dataset.red] = parseInt(hex.substr(1, 2), 16); + params[feld.dataset.green] = parseInt(hex.substr(3, 2), 16); + params[feld.dataset.blue] = parseInt(hex.substr(5, 2), 16); + raumKommando(Number(feld.dataset.cmd), params, feld); + }); + }); + + // Auswahllisten, die ohne zusaetzlichen Knopf schalten - Effekt, Preset. + koerper.querySelectorAll("select.raum-sofort").forEach(auswahl => { + auswahl.addEventListener("change", () => { + const params = {}; + params[auswahl.dataset.param] = auswahl.value; + raumKommando(Number(auswahl.dataset.cmd), params, auswahl); + }); + }); + } + + /** + * Die Ist-Werte im Heizungsteil aus dem laufenden MQTT-Strom fuellen. + * + * 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]; + if (!werte) return; + document.querySelectorAll("#roomView [data-mqtt]").forEach(feld => { + const wert = werte[feld.dataset.mqtt]; + if (typeof wert !== "undefined") feld.textContent = wert; + }); + const heizt = document.querySelector("#roomView [data-mqtt-heizung]"); + if (heizt) heizt.classList.toggle("d-none", String(werte["Heating"]) !== "true"); + } + /** Ein Kommando des angeklickten Knopfes ausfuehren. */ function schalte(knopf) { const zeile = knopf.closest("[data-cmdrow]"); @@ -222,10 +304,22 @@ function switchFloor(floor){ if (zeile) { zeile.querySelectorAll("[data-param]").forEach(f => { params[f.dataset.param] = f.value; }); } - knopf.disabled = true; - raumRequest("command", { command_id: Number(knopf.dataset.cmd), params: params }) + raumKommando(Number(knopf.dataset.cmd), params, knopf); + } + + /** + * Ein Kommando schicken und die Karte so lange sperren. + * + * Ein "Zu" an einer Jalousie dauert ueber eine Minute (die Mechanik will + * zweimal geschwenkt werden, siehe restricted/commands.php). Ohne sichtbare + * Sperre tippt man in der Zeit dreimal nach. + */ + function raumKommando(cmdId, params, element) { + const karte = element.closest(".raum-geraet") || element; + karte.classList.add("raum-warte"); + raumRequest("command", { command_id: cmdId, params: params }) .then(antwort => { - knopf.disabled = false; + karte.classList.remove("raum-warte"); raumMeldung(antwort); }); } diff --git a/restricted/automations.php b/restricted/automations.php index 2802f46..5df855f 100644 --- a/restricted/automations.php +++ b/restricted/automations.php @@ -411,7 +411,7 @@ function deviceCatalog() ]; } - $res = $db->query("SELECT id, actor_id, command_name FROM actor_commands + $res = $db->query("SELECT id, actor_id, command_name, command_url FROM actor_commands ORDER BY actor_id, id"); $commands = []; while ($row = $res->fetch_assoc()) { @@ -423,6 +423,12 @@ function deviceCatalog() $devices[$id]["commands"][] = [ "id" => intval($row["id"]), "name" => $row["command_name"], + // Das Raum-Modal erkennt an der URL, was ein Kommando tut: + // "setClosure" ist eine Position, "up"/"down" faehrt, ein + // WLED-"{"on":true}" schaltet ein. Am Namen ginge das nicht - + // der kommt aus dem Geraetesuchlauf und heisst mal "An", mal + // "turn_on". + "url" => $row["command_url"], "params" => [], ]; } diff --git a/restricted/roomControls.php b/restricted/roomControls.php new file mode 100644 index 0000000..10f0907 --- /dev/null +++ b/restricted/roomControls.php @@ -0,0 +1,546 @@ + ["Beschattung", "bi-blinds"], + "licht" => ["Licht", "bi-lightbulb"], + "schalter" => ["Schalten", "bi-toggle-on"], + "sonstiges" => ["Weitere Geräte", "bi-sliders"], + "messwerte" => ["Messwerte", "bi-speedometer2"], + ]; +} + +// =========================================================================== +// Bausteine +// =========================================================================== + +function karteAuf($d, $status = "") +{ + $html = "
" + . "
" + . "
" + . "" . htmlspecialchars($d["label"]) . ""; + if ($status !== "") { + $html .= "" . $status . ""; + } + return $html . "
"; +} + +function karteZu() +{ + return "
"; +} + +/** + * Ein Schieberegler, der beim Loslassen schaltet. + * + * Waehrend des Ziehens zu schicken hiesse, bei jedem Pixel ein Kommando an + * die Box zu geben - eine Jalousie kaeme aus dem Anfahren nicht mehr heraus. + */ +function reglerZeile($titel, $cmd, $param, $wert, $einheit = "%") +{ + $min = $param["min"] === null ? 0 : intval($param["min"]); + $max = $param["max"] === null ? 100 : intval($param["max"]); + $wert = $wert === null || $wert === "" ? $min : intval($wert); + $wert = max($min, min($max, $wert)); + return "
" + . "
" + . "" . htmlspecialchars($titel) . "" + . "" . $wert . $einheit . "
" + . "" + . "
"; +} + +/** Kippschalter mit dem Stand, den das Geraet zuletzt gemeldet hat. */ +function kippSchalter($d, $schalter, $an) +{ + $id = "sw" . intval($d["id"]); + $html = "
" + . "" + . "
"; + return $html; +} + +/** Ein "wahr"-artiger Wert - die Geraete melden mal True, mal 1, mal "on". */ +function istAn($wert) +{ + if ($wert === null) { + return false; + } + return in_array(strtolower(trim(strval($wert))), ["1", "true", "on", "an", "ein", "yes"], true); +} + +/** + * Nebensaechliche Kommandos in ein Mehr-Menue, damit sie nicht stoeren. + * + * Nur Kommandos ohne Parameter: ein Menuepunkt bietet nichts zum Einstellen, + * ein "Position+Neigung" von hier aus wuerde ohne Werte losgeschickt. Was + * Parameter braucht, hat oben seinen Schieberegler. + */ +function mehrMenue($cmds) +{ + $cmds = array_values(array_filter($cmds, function ($c) { return !count($c["params"]); })); + if (!$cmds) { + return ""; + } + $html = ""; +} + +// =========================================================================== +// Die Bedienformen +// =========================================================================== + +/** + * Der zuletzt gemeldete Stand in Worten. + * + * "0 % zu" muss man erst uebersetzen, "offen" nicht. Die Prozentzahl steht + * ohnehin am Schieberegler darunter. + */ +function beschattungsStand($position, $neigung) +{ + if ($position === null || $position === "") { + return ""; + } + $p = intval($position); + $text = $p === 0 ? "offen" : ($p === 100 ? "geschlossen" : $p . " % zu"); + if ($neigung !== null && $neigung !== "" && $p > 0) { + $text .= ", Neigung " . intval($neigung) . " %"; + } + return htmlspecialchars($text); +} + +/** + * Rollladen, Jalousie, Markise, Garagentor. + * + * Auf/Stop/Zu gross zum Antippen, darunter die Feinheiten. Die Schieber + * starten auf dem zuletzt gemeldeten Stand - so sieht man, wo das Geraet + * gerade steht, bevor man es bewegt. + */ +function zeichneBeschattung($d) +{ + $auf = kommandoMitUrl($d, ["up"]); + $stop = kommandoMitUrl($d, ["stop"]); + $zu = kommandoMitUrl($d, ["down"]); + $position = kommandoMitParameter($d, "Position"); + $neigung = kommandoMitParameter($d, "Neigung"); + + // Was das Geraet zuletzt gemeldet hat. Bei Somfy heisst die Lamelle + // "Lamellenausrichtung", die Hoehe schlicht "Position". + $standPos = zustandsWert($d, ["Position"]); + $standNeigung = zustandsWert($d, ["Lamellenausrichtung"]); + $status = beschattungsStand($standPos, $standNeigung); + + $benutzt = array_filter([$auf, $stop, $zu, $position, $neigung]); + $benutzteIds = array_map(function ($c) { return $c["id"]; }, $benutzt); + $rest = array_values(array_filter($d["commands"], function ($c) use ($benutzteIds) { + return !in_array($c["id"], $benutzteIds, true); + })); + + $html = karteAuf($d, $status); + if ($auf || $stop || $zu) { + $html .= "
"; + $html .= "
"; + foreach ([[$auf, "bi-chevron-up", "Auf"], [$stop, "bi-stop-fill", "Stop"], + [$zu, "bi-chevron-down", "Zu"]] as $k) { + if (!$k[0]) { + continue; + } + $html .= ""; + } + $html .= "
" . mehrMenue($rest) . "
"; + } + if ($position) { + $html .= reglerZeile("Position", $position, $position["params"][0], $standPos); + } + if ($neigung) { + $html .= reglerZeile("Neigung", $neigung, $neigung["params"][0], $standNeigung); + } + return $html . karteZu(); +} + +/** + * Lampe: Kippschalter, Helligkeit, Farbe, Effekt. + * + * Die Farbe kommt in der Datenbank als drei Parameter 0-255 an. Drei + * Zahlenfelder sind keine Farbwahl - deshalb ein Farbfeld, das der Browser + * aufmacht, und die Zerlegung passiert erst beim Absenden. + */ +function zeichneLicht($d) +{ + $schalter = schaltKommandos($d); + $an = istAn(zustandsWert($d, ["An", "ison", "on", "state"])); + $helligkeit = kommandoMitParameter($d, "brightness"); + $farbe = farbKommando($d); + + $benutzteIds = []; + foreach ($schalter as $c) { + $benutzteIds[] = $c["id"]; + } + foreach ([$helligkeit, $farbe] as $c) { + if ($c) { + $benutzteIds[] = $c["id"]; + } + } + + $html = karteAuf($d, ""); + $html .= "
"; + $html .= $schalter ? kippSchalter($d, $schalter, $an) : ""; + if ($farbe) { + // Der zuletzt gemeldete Farbwert steht als [r, g, b, w] in der + // Datenbank; alles andere faellt auf Weiss zurueck. + $roh = json_decode(strval(zustandsWert($d, ["Farbe", "color"])), true); + // Steht dort schwarz, leuchtet die Lampe ueber ihren Weisskanal - ein + // schwarzes Feld waere dann eine falsche Auskunft. + $hex = is_array($roh) && count($roh) >= 3 + && (intval($roh[0]) || intval($roh[1]) || intval($roh[2])) + ? sprintf("#%02x%02x%02x", intval($roh[0]), intval($roh[1]), intval($roh[2])) + : "#ffffff"; + $html .= "Farbe" + . ""; + } + $html .= "
"; + + if ($helligkeit) { + $html .= reglerZeile("Helligkeit", $helligkeit, $helligkeit["params"][0], + zustandsWert($d, ["Helligkeit", "brightness", "bri"]), ""); + } + + // Effekt und Preset sind Listen - die gehoeren in eine Auswahl, nicht in + // ein Zahlenfeld. Sie schalten sofort, ohne zusaetzlichen Knopf. + foreach ($d["commands"] as $c) { + if (in_array($c["id"], $benutzteIds, true) || count($c["params"]) !== 1 + || !count($c["params"][0]["options"])) { + continue; + } + $benutzteIds[] = $c["id"]; + $p = $c["params"][0]; + $html .= "
" + . "" . htmlspecialchars($c["name"]) . "" + . "
"; + } + + $rest = array_values(array_filter($d["commands"], function ($c) use ($benutzteIds) { + return !in_array($c["id"], $benutzteIds, true); + })); + if ($rest) { + $html .= "
" . mehrMenue($rest) . "
"; + } + return $html . karteZu(); +} + +/** Alles, was nur an und aus kennt. */ +function zeichneSchalter($d) +{ + $schalter = schaltKommandos($d); + $an = istAn(zustandsWert($d, ["An", "ison", "on", "state", "Status"])); + $benutzteIds = array_map(function ($c) { return $c["id"]; }, array_values($schalter)); + $rest = array_values(array_filter($d["commands"], function ($c) use ($benutzteIds) { + return !in_array($c["id"], $benutzteIds, true); + })); + + $html = karteAuf($d, ""); + $html .= "
" + . kippSchalter($d, $schalter, $an) . mehrMenue($rest) . "
"; + return $html . karteZu(); +} + +/** + * Geraete, die nur melden: ein Wertegitter statt einer Textwurst. + * + * Ein Stromzaehler hat sechzehn Werte. Alle sechzehn hinzuschreiben ist + * genauso unbrauchbar wie acht davon abzuschneiden - die ersten sechs + * stehen da, der Rest ist einen Klick entfernt. + */ +function zeichneMesswerte($d) +{ + $html = karteAuf($d, htmlspecialchars($d["type"])); + $states = nachWichtigkeit($d["states"]); + $sichtbar = array_slice($states, 0, 6); + $weitere = array_slice($states, 6); + $html .= "
"; + foreach ($sichtbar as $s) { + $html .= wertKachel($s); + } + $html .= "
"; + if ($weitere) { + $html .= "
" + . count($weitere) . " weitere Werte" + . "
"; + foreach ($weitere as $s) { + $html .= wertKachel($s); + } + $html .= "
"; + } + return $html . karteZu(); +} + +/** + * Messwerte so ordnen, dass die sechs sichtbaren die richtigen sind. + * + * Die Reihenfolge in der Datenbank ist die des Geraetesuchlaufs: beim + * Wasserzaehler standen MAC-Adresse, Firmware und freier Speicher vorn, der + * Verbrauch hinter "12 weitere Werte". Vorne gehoert hin, was gemessen wird - + * ein Wert mit Einheit -, hinten, was ueber das Geraet selbst Auskunft gibt. + */ +function nachWichtigkeit($states) +{ + $diagnose = ["mac", "hostname", "fwversion", "uptime", "freemem", "rssi", + "error", "status", "ip", "ssid", "version"]; + $rang = function ($s) use ($diagnose) { + $leer = $s["value"] === null || $s["value"] === ""; + $technisch = in_array(strtolower($s["name"]), $diagnose, true); + return ($technisch ? 4 : 0) + ($leer ? 2 : 0) + ($s["unit"] ? 0 : 1); + }; + usort($states, function ($a, $b) use ($rang) { return $rang($a) - $rang($b); }); + return $states; +} + +function wertKachel($s) +{ + $wert = strval($s["value"]); + if ($wert === "") { + $wert = "–"; + } + // Messwerte kommen mit voller Rechengenauigkeit aus dem Geraet; auf einer + // Kachel liest niemand 231.10000000000002. + if (is_numeric($wert) && strpos($wert, ".") !== false) { + $wert = rtrim(rtrim(number_format(floatval($wert), 2, ",", ""), "0"), ","); + } + return "
" + . "
" . htmlspecialchars($wert) + . ($s["unit"] ? " " . htmlspecialchars($s["unit"]) . "" : "") + . "
" + . "
" . htmlspecialchars($s["name"]) . "
"; +} + +/** Was in keine Form passt: Kommandos so, wie sie in der Datenbank stehen. */ +function zeichneSonstiges($d) +{ + $html = karteAuf($d, htmlspecialchars($d["type"])); + $knoepfe = array_values(array_filter($d["commands"], function ($c) { return !count($c["params"]); })); + if ($knoepfe) { + $html .= "
"; + foreach ($knoepfe as $c) { + $html .= ""; + } + $html .= "
"; + } + foreach ($d["commands"] as $c) { + if (!count($c["params"])) { + continue; + } + $html .= "
" + . "" . htmlspecialchars($c["name"]) . ""; + foreach ($c["params"] as $p) { + if (count($c["params"]) > 1) { + $html .= "" . htmlspecialchars($p["name"]) . ""; + } + if ($p["options"]) { + $html .= ""; + } else { + $html .= ""; + } + } + $html .= "
"; + } + return $html . karteZu(); +} + +/** Ein Geraet in seiner Bedienform zeichnen. */ +function zeichneGeraet($d, $form) +{ + switch ($form) { + case "beschattung": return zeichneBeschattung($d); + case "licht": return zeichneLicht($d); + case "schalter": return zeichneSchalter($d); + case "messwerte": return zeichneMesswerte($d); + default: return zeichneSonstiges($d); + } +} + +/** Ueberschrift einer Gruppe. */ +function gruppenTitel($titel, $symbol) +{ + return "
" + . "" . htmlspecialchars($titel) . "
"; +}