";
if ($hatThermostat) {
echo "
";
- echo "
Solltemperatur "
- . "– ";
+ echo "
";
+ echo "Solltemperatur ";
+ echo "– ";
+ echo "
";
echo "
";
- echo "
"
- . "10 °C 30 °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 ""
- . htmlspecialchars($c["name"]) . " ";
- }
- 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 "";
- foreach ($p["options"] as $o) {
- echo ""
- . htmlspecialchars($o["label"]) . " ";
- }
- 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 = "
"
+ . " "
+ . ""
+ . ($an ? "An" : "Aus") . "
";
+ 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 .= ""
+ . ""
+ . htmlspecialchars($k[2]) . " ";
+ }
+ $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"]) . " "
+ . "";
+ foreach ($p["options"] as $o) {
+ $html .= ""
+ . htmlspecialchars($o["label"]) . " ";
+ }
+ $html .= "
";
+ }
+
+ $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 .= ""
+ . htmlspecialchars($c["name"]) . " ";
+ }
+ $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 .= "";
+ foreach ($p["options"] as $o) {
+ $html .= ""
+ . htmlspecialchars($o["label"]) . " ";
+ }
+ $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) . " ";
+}