Files
Smart-Dashboard/restricted/roomControls.php
T
adminandClaude Opus 5 f6e0414866 Raum-Modal: Gruppen als Tabs, Jalousien senkrecht bedienen
Die Gruppenueberschriften sind Tabs geworden, Beschattung zuerst - das ist
es, was man in einem Raum am haeufigsten anfasst. Vorher scrollte man an
drei Rollladen vorbei, um zum Licht zu kommen. Bei nur einer Gruppe bleibt
die Leiste weg; auf schmalen Schirmen scrollt sie seitwaerts, statt die
Namen umzubrechen.

Bei den Jalousien stehen Auf, Stop und Zu jetzt untereinander - so zeigen
die Knoepfe in die Richtung, in die sie fahren. Die Hoehe daneben ist ein
senkrechter Regler mit "oben" und "unten" an den Enden: der Griff steht
dort, wo der Rollladen steht, und niemand muss "100 %" uebersetzen.

Die Neigung hat ein Bild dazubekommen. "30 %" oder "70 %" sagt nicht, ob
man noch hinaussieht; vier Streifen sagen es. Gezeichnet wird die Hoehe,
die eine gekippte Lamelle von vorn zeigt (w * sin(Winkel)) - bei "auf"
sieht man nur ihre Kante und viel Luft dazwischen, bei "zu" deckt sie den
Abstand zur naechsten vollstaendig ab. Genau das, was das Licht abhaelt.

Die Regler faerben ihre Bahn jetzt mit, wie es der Temperaturregler seit
jeher tut - dieselbe Eigenschaft --background-size, nur beim senkrechten
von oben nach unten.

css/solar.css hatte gemischte Zeilenenden und ist dabei durchgehend auf LF
gebracht worden; das erklaert den grossen Diff bei kleiner Aenderung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:38:09 +02:00

687 lines
25 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Bedienformen fuer das Raum-Modal.
*
* Die Geraetedatenbank kennt nur Kommandos mit Parametern - eine Jalousie hat
* acht davon, darunter "Winken". Stellt man die eins zu eins dar, steht im
* Raum-Modal eine Bedienungsanleitung statt einer Bedienung. Hier entscheidet
* sich deshalb, welches Geraet welches Bedienelement bekommt: ein Rollladen
* Auf/Stop/Zu mit zwei Schiebereglern, eine Lampe einen Kippschalter mit
* Helligkeit und Farbe, ein Zaehler ein Wertegitter.
*
* Erkannt wird an den Faehigkeiten, nicht am Typnamen: hat das Geraet ein
* "setClosure"? ein An/Aus-Paar? drei Parameter rot/gruen/blau? Damit bekommt
* auch ein neu gefundenes Geraet sein passendes Bedienelement, ohne dass hier
* jemand einen Typnamen nachtraegt.
*
* Das Gegenstueck im Browser ist raumBedienungAktivieren() in
* js/solar/homeMQTT.js - dort haengen die Zuhoerer an den data-Attributen,
* die hier gesetzt werden.
*/
/**
* Was das Kommando schaltet: "ein", "aus", "um" - oder null, wenn es kein
* Schaltkommando ist.
*
* Der Geraetesuchlauf vergibt die Namen so, wie das Geraet sie meldet: mal
* "An", mal "turn_on", mal "Anschalten". Deshalb zaehlt zuerst die URL.
*/
function schaltRichtung($cmd)
{
if (count($cmd["params"])) {
return null; // Ein Schalter hat nichts einzustellen.
}
$url = strtolower(trim($cmd["url"]));
$name = strtolower(trim($cmd["name"]));
if (strpos($url, "toggle") !== false || $name === "toggle" || $name === "umschalten") {
return "um";
}
if (in_array($url, ["on", "turn=on", '{"on":true}'], true)
|| in_array($name, ["an", "ein", "anschalten", "einschalten", "turn_on"], true)) {
return "ein";
}
if (in_array($url, ["off", "turn=off", '{"on":false}'], true)
|| in_array($name, ["aus", "ausschalten", "turn_off"], true)) {
return "aus";
}
return null;
}
/** Erstes Kommando, dessen URL in $urls steht. */
function kommandoMitUrl($d, $urls)
{
foreach ($d["commands"] as $c) {
if (in_array($c["url"], $urls, true)) {
return $c;
}
}
return null;
}
/** Erstes Kommando, das genau einen Parameter dieses Namens hat. */
function kommandoMitParameter($d, $name)
{
foreach ($d["commands"] as $c) {
if (count($c["params"]) === 1 && $c["params"][0]["name"] === $name) {
return $c;
}
}
return null;
}
/** Kommando mit Parametern rot/gruen/blau - eine Farbe zum Aussuchen. */
function farbKommando($d)
{
foreach ($d["commands"] as $c) {
$namen = array_map(function ($p) { return strtolower($p["name"]); }, $c["params"]);
sort($namen);
if ($namen === ["blue", "green", "red"]) {
return $c;
}
}
return null;
}
/** An/Aus/Um eines Geraets, sofern vorhanden. */
function schaltKommandos($d)
{
$gefunden = [];
foreach ($d["commands"] as $c) {
$richtung = schaltRichtung($c);
if ($richtung !== null && !isset($gefunden[$richtung])) {
$gefunden[$richtung] = $c;
}
}
return $gefunden;
}
/** Aktueller Wert eines Zustands - der erste Name, den es gibt. */
function zustandsWert($d, $namen)
{
foreach ($namen as $name) {
foreach ($d["states"] as $s) {
if (strcasecmp($s["name"], $name) === 0) {
return $s["value"];
}
}
}
return null;
}
/**
* Welche Bedienform passt zu diesem Geraet?
*
* Die Reihenfolge ist die Rangfolge: eine Jalousie hat auch ein "Auf" und
* "Zu", ist aber keine Lampe, und ein Thermostat mit Solltemperatur ist kein
* Schalter.
*/
function bedienform($d)
{
if (!count($d["commands"])) {
return "messwerte";
}
if (stripos($d["type"], "thermostat") !== false) {
return "heizung";
}
if (kommandoMitUrl($d, ["setClosure", "setClosureAndOrientation"])
|| kommandoMitUrl($d, ["up", "down"])) {
return "beschattung";
}
$schalter = schaltKommandos($d);
if ($schalter && (kommandoMitParameter($d, "brightness") || farbKommando($d))) {
return "licht";
}
if ($schalter) {
return "schalter";
}
return "sonstiges";
}
/**
* Die Gruppen in der Reihenfolge, in der ihre Tabs erscheinen.
*
* Beschattung zuerst: das ist es, was man in einem Raum am haeufigsten
* anfasst. Messwerte zuletzt - die schaut man an, man bedient sie nicht.
*/
function raumGruppenTitel()
{
return [
"beschattung" => ["Beschattung", "bi-blinds"],
"licht" => ["Licht", "bi-lightbulb"],
"schalter" => ["Schalten", "bi-toggle-on"],
"sonstiges" => ["Geräte", "bi-sliders"],
"messwerte" => ["Messwerte", "bi-speedometer2"],
];
}
/**
* Die Geraetegruppen als Tabs.
*
* Vorher standen sie untereinander, und man scrollte an drei Rollladen
* vorbei, um zum Licht zu kommen. Bei nur einer Gruppe bleibt die Leiste
* weg - ein einzelner Tab beschriftet nur sich selbst.
*/
function zeichneGruppen($gruppen)
{
$vorhanden = [];
foreach (raumGruppenTitel() as $form => $titel) {
if (!empty($gruppen[$form])) {
$vorhanden[$form] = $titel;
}
}
if (!$vorhanden) {
return "";
}
$html = "";
if (count($vorhanden) > 1) {
$html .= "<ul class='nav nav-pills flex-nowrap gap-1 mb-2 raum-tabs' role='tablist'>";
$erster = true;
foreach ($vorhanden as $form => $titel) {
$html .= "<li class='nav-item' role='presentation'>"
. "<button class='nav-link" . ($erster ? " active" : "") . "' type='button'"
. " data-bs-toggle='tab' data-bs-target='#raumtab-" . $form . "' role='tab'>"
. "<i class='bi " . $titel[1] . "'></i>"
. "<span class='ms-1'>" . htmlspecialchars($titel[0]) . "</span>"
. "</button></li>";
$erster = false;
}
$html .= "</ul>";
}
$html .= "<div class='tab-content'>";
$erster = true;
foreach ($vorhanden as $form => $titel) {
$html .= "<div class='tab-pane fade" . ($erster ? " show active" : "") . "'"
. " id='raumtab-" . $form . "' role='tabpanel'>";
foreach ($gruppen[$form] as $d) {
$html .= zeichneGeraet($d, $form);
}
$html .= "</div>";
$erster = false;
}
return $html . "</div>";
}
// ===========================================================================
// Bausteine
// ===========================================================================
function karteAuf($d, $status = "")
{
$html = "<div class='card mb-2 raum-geraet' data-actor='" . intval($d["id"]) . "'>"
. "<div class='card-body py-2'>"
. "<div class='d-flex justify-content-between align-items-center gap-2 mb-2'>"
. "<span class='fw-semibold small text-truncate'>" . htmlspecialchars($d["label"]) . "</span>";
if ($status !== "") {
$html .= "<span class='small text-body-secondary text-nowrap raum-status'>" . $status . "</span>";
}
return $html . "</div>";
}
function karteZu()
{
return "</div></div>";
}
/**
* 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 = reglerMin($param);
$max = reglerMax($param);
$wert = reglerWert($wert, $min, $max);
return "<div class='raum-regler' data-cmd='" . intval($cmd["id"]) . "'"
. " data-param='" . intval($param["id"]) . "'>"
. "<div class='d-flex justify-content-between small'>"
. "<span>" . htmlspecialchars($titel) . "</span>"
. "<span class='raum-regler-wert fw-semibold'>" . $wert . $einheit . "</span></div>"
. reglerFeld($min, $max, $wert, $einheit)
. "</div>";
}
function reglerMin($param)
{
return $param["min"] === null ? 0 : intval($param["min"]);
}
function reglerMax($param)
{
return $param["max"] === null ? 100 : intval($param["max"]);
}
function reglerWert($wert, $min, $max)
{
$wert = $wert === null || $wert === "" ? $min : intval($wert);
return max($min, min($max, $wert));
}
function reglerFeld($min, $max, $wert, $einheit)
{
return "<input type='range' class='form-range range-color-track' min='" . $min . "' max='" . $max . "'"
. " step='1' value='" . $wert . "' data-einheit='" . htmlspecialchars($einheit, ENT_QUOTES) . "'>";
}
/**
* Die beiden Endstellungen in Worten.
*
* "Position 100 %" muss man uebersetzen, "unten" nicht. Dazwischen bleibt es
* bei der Zahl - fuer halb heruntergefahren gibt es kein Wort. Ausgewertet
* wird das Attribut in raumBedienungAktivieren(), js/solar/homeMQTT.js.
*/
function worteAttribut($beiNull, $beiHundert)
{
return " data-worte='" . htmlspecialchars(
json_encode([0 => $beiNull, 100 => $beiHundert], JSON_UNESCAPED_UNICODE), ENT_QUOTES) . "'";
}
/**
* Die Hoehe als senkrechter Regler - oben ist oben.
*
* Einen waagerechten Schieber fuer eine Bewegung, die hoch und runter geht,
* muss man erst uebersetzen; senkrecht steht der Griff dort, wo der Rollladen
* steht.
*/
function positionsRegler($cmd, $param, $wert)
{
$min = reglerMin($param);
$max = reglerMax($param);
$wert = reglerWert($wert, $min, $max);
return "<div class='raum-regler raum-position' data-cmd='" . intval($cmd["id"]) . "'"
. " data-param='" . intval($param["id"]) . "'" . worteAttribut("oben", "unten") . ">"
. "<span class='raum-ende'>oben</span>"
. reglerFeld($min, $max, $wert, "%")
. "<span class='raum-ende'>unten</span>"
. "</div>";
}
/**
* Die Lamellenstellung mit einem Bild daneben.
*
* Bei 30 % oder 70 % sagt die Zahl nicht, ob man noch hinaussieht. Die vier
* Streifen zeigen es: schmal mit Luft dazwischen heisst offen, geschlossen
* liegen sie aneinander. Gezeichnet wird die Hoehe, die eine gekippte
* Lamelle von vorn zeigt - genau das, was das Licht abhaelt.
*/
function neigungsRegler($cmd, $param, $wert)
{
$min = reglerMin($param);
$max = reglerMax($param);
$wert = reglerWert($wert, $min, $max);
return "<div class='raum-regler raum-neigung raum-neigung-block'"
. " data-cmd='" . intval($cmd["id"]) . "'"
. " data-param='" . intval($param["id"]) . "'" . worteAttribut("auf", "zu") . ">"
. "<div class='d-flex justify-content-between small'>"
. "<span>Neigung</span>"
. "<span class='raum-regler-wert fw-semibold'>" . $wert . "%</span></div>"
. lamellenBild()
. reglerFeld($min, $max, $wert, "%")
. "<div class='d-flex justify-content-between raum-ende'><span>auf</span><span>zu</span></div>"
. "</div>";
}
/** Die vier Lamellen. Ihre Hoehe setzt der Browser, siehe homeMQTT.js. */
function lamellenBild()
{
$html = "<svg class='raum-lamellen' viewBox='0 0 100 40' preserveAspectRatio='none' aria-hidden='true'>";
foreach ([5, 15, 25, 35] as $mitte) {
$html .= "<rect class='raum-lamelle' x='0' width='100' data-mitte='" . $mitte . "'"
. " y='" . ($mitte - 1) . "' height='2'></rect>";
}
return $html . "</svg>";
}
/** Kippschalter mit dem Stand, den das Geraet zuletzt gemeldet hat. */
function kippSchalter($d, $schalter, $an)
{
$id = "sw" . intval($d["id"]);
$html = "<div class='form-check form-switch raum-schalter'"
. " data-ein='" . (isset($schalter["ein"]) ? intval($schalter["ein"]["id"]) : "") . "'"
. " data-aus='" . (isset($schalter["aus"]) ? intval($schalter["aus"]["id"]) : "") . "'"
. " data-um='" . (isset($schalter["um"]) ? intval($schalter["um"]["id"]) : "") . "'>"
. "<input class='form-check-input' type='checkbox' role='switch' id='" . $id . "'"
. ($an ? " checked" : "") . ">"
. "<label class='form-check-label small' for='" . $id . "'>"
. ($an ? "An" : "Aus") . "</label></div>";
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 = "<div class='dropdown'>"
. "<button class='btn btn-sm btn-link text-body-secondary p-0' type='button'"
. " data-bs-toggle='dropdown' aria-label='Weitere Befehle'>"
. "<i class='bi bi-three-dots'></i></button><ul class='dropdown-menu dropdown-menu-end'>";
foreach ($cmds as $c) {
$html .= "<li><button class='dropdown-item small' type='button' data-cmd='" . intval($c["id"]) . "'>"
. htmlspecialchars($c["name"]) . "</button></li>";
}
return $html . "</ul></div>";
}
// ===========================================================================
// 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);
$html .= "<div class='raum-jalousie'>";
// Auf, Stop, Zu untereinander: so zeigen die Knoepfe in die Richtung, in
// die sie fahren.
if ($auf || $stop || $zu) {
$html .= "<div class='btn-group-vertical btn-group-sm raum-fahrt' role='group'>";
foreach ([[$auf, "bi-chevron-up", "Auf"], [$stop, "bi-stop-fill", "Stop"],
[$zu, "bi-chevron-down", "Zu"]] as $k) {
if (!$k[0]) {
continue;
}
$html .= "<button type='button' class='btn btn-outline-primary' data-cmd='" . intval($k[0]["id"]) . "'"
. " title='" . htmlspecialchars($k[2], ENT_QUOTES) . "'"
. " aria-label='" . htmlspecialchars($k[2], ENT_QUOTES) . "'>"
. "<i class='bi " . $k[1] . "'></i></button>";
}
$html .= "</div>";
}
if ($position) {
$html .= positionsRegler($position, $position["params"][0], $standPos);
}
if ($neigung) {
$html .= neigungsRegler($neigung, $neigung["params"][0], $standNeigung);
}
$html .= mehrMenue($rest);
return $html . "</div>" . 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 .= "<div class='d-flex justify-content-between align-items-center mb-2'>";
$html .= $schalter ? kippSchalter($d, $schalter, $an) : "<span></span>";
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 .= "<span class='raum-farbe d-inline-flex align-items-center gap-1'"
. " data-cmd='" . intval($farbe["id"]) . "'";
foreach ($farbe["params"] as $p) {
$html .= " data-" . strtolower($p["name"]) . "='" . intval($p["id"]) . "'";
}
$html .= "><label class='small text-body-secondary' for='col" . intval($d["id"]) . "'>Farbe</label>"
. "<input type='color' class='form-control form-control-color form-control-sm'"
. " id='col" . intval($d["id"]) . "' value='" . $hex . "'></span>";
}
$html .= "</div>";
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 .= "<div class='input-group input-group-sm mb-1 raum-cmd' data-cmdrow='" . intval($c["id"]) . "'>"
. "<span class='input-group-text'>" . htmlspecialchars($c["name"]) . "</span>"
. "<select class='form-select raum-sofort' data-param='" . intval($p["id"]) . "'"
. " data-cmd='" . intval($c["id"]) . "'>";
foreach ($p["options"] as $o) {
$html .= "<option value='" . htmlspecialchars($o["value"], ENT_QUOTES) . "'>"
. htmlspecialchars($o["label"]) . "</option>";
}
$html .= "</select></div>";
}
$rest = array_values(array_filter($d["commands"], function ($c) use ($benutzteIds) {
return !in_array($c["id"], $benutzteIds, true);
}));
if ($rest) {
$html .= "<div class='d-flex justify-content-end'>" . mehrMenue($rest) . "</div>";
}
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 .= "<div class='d-flex justify-content-between align-items-center'>"
. kippSchalter($d, $schalter, $an) . mehrMenue($rest) . "</div>";
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 .= "<div class='raum-werte'>";
foreach ($sichtbar as $s) {
$html .= wertKachel($s);
}
$html .= "</div>";
if ($weitere) {
$html .= "<details class='mt-2'><summary class='small text-body-secondary'>"
. count($weitere) . " weitere Werte</summary>"
. "<div class='raum-werte mt-2'>";
foreach ($weitere as $s) {
$html .= wertKachel($s);
}
$html .= "</div></details>";
}
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 "<div class='raum-wert'>"
. "<div class='raum-wert-zahl'>" . htmlspecialchars($wert)
. ($s["unit"] ? " <span class='raum-wert-einheit'>" . htmlspecialchars($s["unit"]) . "</span>" : "")
. "</div>"
. "<div class='raum-wert-name'>" . htmlspecialchars($s["name"]) . "</div></div>";
}
/** 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 .= "<div class='d-flex flex-wrap gap-1 mb-2'>";
foreach ($knoepfe as $c) {
$html .= "<button type='button' class='btn btn-outline-primary btn-sm' data-cmd='" . intval($c["id"]) . "'>"
. htmlspecialchars($c["name"]) . "</button>";
}
$html .= "</div>";
}
foreach ($d["commands"] as $c) {
if (!count($c["params"])) {
continue;
}
$html .= "<div class='input-group input-group-sm mb-1 raum-cmd' data-cmdrow='" . intval($c["id"]) . "'>"
. "<span class='input-group-text'>" . htmlspecialchars($c["name"]) . "</span>";
foreach ($c["params"] as $p) {
if (count($c["params"]) > 1) {
$html .= "<span class='input-group-text'>" . htmlspecialchars($p["name"]) . "</span>";
}
if ($p["options"]) {
$html .= "<select class='form-select' data-param='" . intval($p["id"]) . "'>";
foreach ($p["options"] as $o) {
$html .= "<option value='" . htmlspecialchars($o["value"], ENT_QUOTES) . "'>"
. htmlspecialchars($o["label"]) . "</option>";
}
$html .= "</select>";
} else {
$html .= "<input type='" . ($p["input"] === "number" ? "number" : "text")
. "' class='form-control' data-param='" . intval($p["id"]) . "'"
. ($p["min"] !== null ? " min='" . intval($p["min"]) . "'" : "")
. ($p["max"] !== null ? " max='" . intval($p["max"]) . "'" : "")
. " value='" . ($p["min"] !== null ? intval($p["min"]) : 0) . "'>";
}
}
$html .= "<button type='button' class='btn btn-outline-primary' data-cmd='" . intval($c["id"]) . "'>"
. "<i class='bi bi-play-fill'></i></button></div>";
}
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);
}
}