Files
Smart-Dashboard/restricted/roomControls.php
T
adminandClaude Opus 5 d36989ae0d Eigene Auswahllisten ueberall: dunkel und ueber der Seite statt im Fluss
Chrome zeichnet die Liste eines <select> hell auf grau und laesst sich davon
nicht abbringen. js/solar/auswahl.js ergaenzt jedes <select> um einen eigenen
Knopf; die Liste haengt an <body> und liegt ueber der Seite, damit ein Modal
nicht hoeher wird und der Inhalt nicht springt. Ab neun Eintraegen mit
Suchfeld. Der eigene Picker im Raumfenster faellt dafuer weg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 07:55:17 +02:00

679 lines
28 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 js/solar/raumfenster.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, das Hoehe und Neigung zusammen setzt - null, wenn es keins gibt.
*
* Der Hoehenregler benutzt es statt des reinen "Position"-Kommandos: eine
* Hoehenfahrt rastet die Lamellen um, danach muss die Neigung ueber 0 %
* wieder angefahren werden. Den Umweg faehrt sendeTahoma(), aber nur, wenn
* der Befehl ueberhaupt ein Neigungsziel mitbringt.
*/
function kombiKommando($d)
{
foreach ($d["commands"] as $c) {
$namen = array_map(function ($p) { return $p["name"]; }, $c["params"]);
sort($namen);
if ($namen === [NEIGUNG_PARAMETER, POSITION_PARAMETER]) {
return $c;
}
}
return null;
}
/** Der Parameter eines Kommandos mit diesem Namen, null wenn es ihn nicht gibt. */
function parameterNamens($cmd, $name)
{
foreach ($cmd["params"] as $p) {
if ($p["name"] === $name) {
return $p;
}
}
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 Reiter erscheinen, mit Symbol
* und Akzentfarbe - dieselben Farben wie in Lade-, Heiz- und
* Bewaesserungsfenster.
*
* 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-beschattung", "#60afbf"],
"heizung" => ["Heizung", "bi-thermometer-half", "#e5624f"],
"licht" => ["Licht", "bi-lightbulb", "#f7c000"],
"schalter" => ["Schalten", "bi-toggle-on", "#6cbe58"],
"sonstiges" => ["Geräte", "bi-sliders", "#9aa6ad"],
"messwerte" => ["Messwerte", "bi-speedometer2", "#4b9fd6"],
];
}
/**
* Die Geraetegruppen als Reiter-Kacheln.
*
* Jede Kachel traegt unter ihrem Namen den Zustand der Gruppe ("2 von 3 zu",
* "25,7 °C", "1 an") - den setzt js/solar/raumfenster.js, weil er sich
* waehrend des Bedienens aendert. Bei nur einer Gruppe bleibt die Leiste
* trotzdem stehen: sie ist hier auch die Zusammenfassung.
*
* $fertig sind Gruppen, deren Inhalt der Aufrufer schon gebaut hat und die
* nicht aus einer Geraeteliste entstehen - die Heizung ist so eine: ein
* Regler fuer den Raum, nicht eine Karte je Geraet.
*/
function zeichneGruppen($gruppen, $fertig = [])
{
$vorhanden = [];
foreach (raumGruppenTitel() as $form => $titel) {
if (!empty($gruppen[$form]) || isset($fertig[$form])) {
$vorhanden[$form] = $titel;
}
}
if (!$vorhanden) {
return "";
}
$leiste = "<div class='rf-reiter' role='tablist'>";
$flaechen = "";
$erste = true;
foreach ($vorhanden as $form => $titel) {
$leiste .= "<button type='button' role='tab' data-rf-reiter='" . $form . "'"
. " aria-selected='" . ($erste ? "true" : "false") . "' style='--c:" . $titel[2] . "'>"
. "<span class='rf-sym'><i class='bi " . $titel[1] . "'></i></span>"
. "<b>" . htmlspecialchars($titel[0]) . "</b>"
. "<span class='rf-st' data-rf-status='" . $form . "'></span></button>";
$inhalt = "";
if (isset($fertig[$form])) {
$inhalt = $fertig[$form];
} else {
if ($form === "beschattung" && count($gruppen[$form]) > 1) {
$inhalt .= alleImRaum($gruppen[$form]);
}
if ($form === "schalter") {
$inhalt .= "<div class='rf-schalter-liste'>";
}
foreach ($gruppen[$form] as $d) {
$inhalt .= zeichneGeraet($d, $form);
}
if ($form === "schalter") {
$inhalt .= "</div>";
}
}
$flaechen .= "<div class='rf-flaeche' role='tabpanel' data-rf-flaeche='" . $form . "'"
. " style='--a:" . $titel[2] . "'" . ($erste ? "" : " hidden") . ">" . $inhalt . "</div>";
$erste = false;
}
return $leiste . "</div>" . $flaechen;
}
// ===========================================================================
// Bausteine
// ===========================================================================
/** Rahmen fuer Geraete ohne eigene Bedienform (Messwerte, Sonstiges). */
function karteAuf($d, $status = "")
{
$html = "<div class='rf-karte raum-geraet' data-actor='" . intval($d["id"]) . "'>"
. "<div class='rf-kopf-zeile'><b>" . htmlspecialchars($d["label"]) . "</b>";
if ($status !== "") {
$html .= "<span class='rf-pille'>" . $status . "</span>";
}
return $html . "</div>";
}
function karteZu()
{
return "</div>";
}
/** 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 seine Bedienung.
*/
function mehrMenue($cmds)
{
$cmds = array_values(array_filter($cmds, function ($c) { return !count($c["params"]); }));
if (!$cmds) {
return "";
}
$html = "<div class='dropdown rf-mehr'>"
. "<button class='rf-mehr-knopf' 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>";
}
/** Kommandos, die nicht unter $benutzt stehen. */
function restKommandos($d, $benutzt)
{
$ids = array_map(function ($c) { return $c["id"]; }, array_filter($benutzt));
return array_values(array_filter($d["commands"], function ($c) use ($ids) {
return !in_array($c["id"], $ids, true);
}));
}
/** Stufenwahl als Segment: $stufen = [[wert, beschriftung], ...]. */
function stufenReihe($titel, $rolle, $stufen)
{
$html = "<div class='rf-reihe'><span class='rf-reihe-titel'>" . htmlspecialchars($titel) . "</span>"
. "<div class='rf-segment' role='radiogroup' aria-label='" . htmlspecialchars($titel, ENT_QUOTES) . "'>";
foreach ($stufen as $s) {
$html .= "<button type='button' role='radio' aria-checked='false' data-" . $rolle . "='" . $s[0] . "'>"
. $s[1] . "</button>";
}
return $html . "</div></div>";
}
// ===========================================================================
// Die Bedienformen
// ===========================================================================
/** Zahl aus einem Zustand, oder "" wenn es keinen gibt. */
function zustandsZahl($d, $namen)
{
$wert = zustandsWert($d, $namen);
return is_numeric($wert) ? strval(intval($wert)) : "";
}
/**
* "Alle im Raum": ein Knopf, viele Geraete. Das JavaScript geht die Karten
* durch und schickt jedem das passende eigene Kommando - "Sichtschutz" nur
* denen, die Hoehe und Neigung zusammen setzen koennen.
*/
function alleImRaum($geraete)
{
$mitMy = false;
$mitNeigung = false;
foreach ($geraete as $d) {
$mitMy = $mitMy || kommandoMitUrl($d, ["my"]);
$mitNeigung = $mitNeigung || kombiKommando($d);
}
return "<div class='rf-alle'><h3>Alle im Raum</h3><div class='rf-chips'>"
. "<button type='button' class='rf-chip' data-alle='auf'><i class='bi bi-chevron-up'></i>Alle auf</button>"
. "<button type='button' class='rf-chip' data-alle='zu'><i class='bi bi-chevron-down'></i>Alle zu</button>"
. ($mitMy ? "<button type='button' class='rf-chip' data-alle='my'><i class='bi bi-star'></i>Alle auf my</button>" : "")
. ($mitNeigung ? "<button type='button' class='rf-chip' data-alle='sicht'><i class='bi bi-distribute-vertical'></i>Sichtschutz</button>" : "")
. "</div></div>";
}
/**
* Rollladen, Jalousie, Markise, Garagentor.
*
* Links das Fenster, wie der Behang davor gerade steht - ziehen stellt die
* Hoehe fein ein. Rechts gross Auf / Stop / my / Zu, darunter Hoehe und
* Lamellen als Stufen und die my-Position, die sich dort auch speichern
* laesst. Alles wird von js/solar/raumfenster.js ueber die data-Attribute
* verdrahtet; welche es gibt, haengt an den Faehigkeiten des Geraets.
*/
function zeichneBeschattung($d)
{
$auf = kommandoMitUrl($d, ["up"]);
$stop = kommandoMitUrl($d, ["stop"]);
$zu = kommandoMitUrl($d, ["down"]);
$my = kommandoMitUrl($d, ["my"]);
$position = kommandoMitParameter($d, POSITION_PARAMETER);
$neigung = kommandoMitParameter($d, NEIGUNG_PARAMETER);
$kombi = kombiKommando($d);
// Die Hoehe geht, wenn es das gibt, ueber das Kombi-Kommando und nimmt die
// Neigung mit - sonst raste die Mechanik bei der Hoehenfahrt um und
// niemand faehrt sie wieder an (siehe sendeTahoma() in commands.php).
$hoehe = $kombi ?: $position;
$attr = [
"auf" => $auf ? $auf["id"] : "",
"stop" => $stop ? $stop["id"] : "",
"zu" => $zu ? $zu["id"] : "",
"my" => $my ? $my["id"] : "",
"hoehe-cmd" => $hoehe ? $hoehe["id"] : "",
"hoehe-param" => $hoehe ? ($kombi ? parameterNamens($kombi, POSITION_PARAMETER)["id"] : $position["params"][0]["id"]) : "",
"hoehe-mit" => $kombi && $neigung ? parameterNamens($kombi, NEIGUNG_PARAMETER)["id"] : "",
"neig-cmd" => $neigung ? $neigung["id"] : "",
"neig-param" => $neigung ? $neigung["params"][0]["id"] : "",
// Was das Geraet zuletzt gemeldet hat. Bei Somfy heisst die Lamelle
// "Lamellenausrichtung", die Hoehe schlicht "Position".
"pos" => zustandsZahl($d, ["Position"]),
"neig" => $neigung ? zustandsZahl($d, ["Lamellenausrichtung"]) : "",
"my-pos" => zustandsZahl($d, ["Gespeicherte Position"]),
"my-neig" => $neigung ? zustandsZahl($d, ["Gespeicherte Neigung"]) : "",
];
$html = "<div class='rf-behang raum-geraet' data-actor='" . intval($d["id"]) . "'";
foreach ($attr as $name => $wert) {
$html .= " data-" . $name . "='" . htmlspecialchars(strval($wert), ENT_QUOTES) . "'";
}
$html .= ">";
// Das Bild: dunkles Glas, davor Lamellen (mit Neigung) oder ein Panzer.
// Mehr Lamellen als je sichtbar - die Hoehe schneidet ab.
$html .= "<div class='rf-fenster'" . ($hoehe ? " data-rf-zieh title='Ziehen zum Einstellen'" : "") . " aria-hidden='true'>"
. "<div class='rf-glas'><div class='rf-behang-teil'>"
. ($neigung ? "<div class='rf-lamellen'>" . str_repeat("<i></i>", 40) . "</div>" : "<div class='rf-panzer'></div>")
. "<div class='rf-schiene'></div></div></div></div>";
$html .= "<div class='rf-behang-mitte'>"
. "<div class='rf-kopf-zeile'><b>" . htmlspecialchars($d["label"]) . "</b>"
. "<span class='rf-pille' data-rf-stand></span>"
. mehrMenue(restKommandos($d, [$auf, $stop, $zu, $my, $position, $neigung, $kombi])) . "</div>";
if ($auf || $stop || $zu || $my) {
$html .= "<div class='rf-fahrt'>"
. ($auf ? "<button type='button' data-rf-fahrt='auf'><i class='bi bi-chevron-up'></i>Auf</button>" : "<span></span>")
. ($stop ? "<button type='button' class='rf-stop' data-rf-fahrt='stop' aria-label='Stop'><i class='bi bi-stop-fill'></i></button>" : "")
. ($my ? "<button type='button' class='rf-my' data-rf-fahrt='my'><i class='bi bi-star-fill'></i>my</button>" : "")
. ($zu ? "<button type='button' data-rf-fahrt='zu'><i class='bi bi-chevron-down'></i>Zu</button>" : "<span></span>")
. "</div>";
}
$prozent = function ($v) { return $v . "&#8201;%"; };
if ($hoehe) {
$html .= stufenReihe("Höhe", "rf-pos", [[0, "offen"], [25, $prozent(25)], [50, "halb"], [75, $prozent(75)], [100, "zu"]]);
}
if ($neigung) {
$html .= stufenReihe("Lamellen", "rf-neig", [[0, "offen"], [25, $prozent(25)], [50, "halb"], [75, $prozent(75)], [100, "zu"]]);
}
// Die my-Position ist im Motor gespeichert und nur bei IO-Geraeten lesbar.
// Speichern geht ueber ajax/room.php?action=my, nicht ueber ein Kommando
// aus der Geraetedatenbank.
if ($attr["my-pos"] !== "" && $my) {
$html .= "<div class='rf-my-zeile' data-rf-my></div>";
}
return $html . "</div></div>";
}
/**
* Lampe: die Birne ist der Schalter und leuchtet in ihrer Farbe. Darunter
* Helligkeit, Farbfelder und - eingeklappt - Effekt und Preset.
*/
function zeichneLicht($d)
{
$schalter = schaltKommandos($d);
$an = istAn(zustandsWert($d, ["An", "ison", "on", "state"]));
$helligkeit = kommandoMitParameter($d, "brightness");
$farbe = farbKommando($d);
// Der zuletzt gemeldete Farbwert steht als [r, g, b, w] in der Datenbank.
// Steht dort schwarz, leuchtet die Lampe ueber ihren Weisskanal - ein
// schwarzes Feld waere dann eine falsche Auskunft.
$roh = json_decode(strval(zustandsWert($d, ["Farbe", "color"])), true);
$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]))
: "#fff1d6";
$benutzt = array_values($schalter);
$benutzt[] = $helligkeit;
$benutzt[] = $farbe;
$html = "<div class='rf-lampe raum-geraet' data-actor='" . intval($d["id"]) . "'"
. " 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"]) : "") . "'"
. " data-an='" . ($an ? "1" : "") . "'" . (strpos(strval($d["url"]), "wled://") === 0 ? " data-wled='1'" : "")
. " style='--farbe:" . $hex . "'>";
$html .= "<div class='rf-lampe-kopf'>"
. "<button type='button' class='rf-birne' data-rf-birne aria-pressed='" . ($an ? "true" : "false") . "'"
. " aria-label='" . htmlspecialchars($d["label"], ENT_QUOTES) . " schalten'><i class='bi bi-lightbulb'></i></button>"
. "<div class='rf-lampe-name'><b>" . htmlspecialchars($d["label"]) . "</b><span data-rf-lampe-stand></span></div>";
// Effekt und Preset sammeln, bevor das Mehr-Menue die Reste bekommt.
$listen = [];
foreach ($d["commands"] as $c) {
if (in_array($c, $benutzt, true) || count($c["params"]) !== 1 || !count($c["params"][0]["options"])) {
continue;
}
$listen[] = $c;
$benutzt[] = $c;
}
$html .= mehrMenue(restKommandos($d, $benutzt)) . "</div><div class='rf-lampe-regler'>";
if ($helligkeit) {
$p = $helligkeit["params"][0];
$min = $p["min"] === null ? 0 : intval($p["min"]);
$max = $p["max"] === null ? 100 : intval($p["max"]);
$wert = zustandsWert($d, ["Helligkeit", "brightness", "bri"]);
$wert = max($min, min($max, $wert === null || $wert === "" ? $max : intval($wert)));
$html .= "<input type='range' class='rf-hell' min='" . $min . "' max='" . $max . "' value='" . $wert . "'"
. " data-cmd='" . intval($helligkeit["id"]) . "' data-param='" . intval($p["id"]) . "'"
. " aria-label='Helligkeit'>";
}
if ($farbe) {
$html .= "<div class='rf-farben' data-cmd='" . intval($farbe["id"]) . "'";
foreach ($farbe["params"] as $p) {
$html .= " data-" . strtolower($p["name"]) . "='" . intval($p["id"]) . "'";
}
$html .= ">";
foreach (["#ffd9a0" => "Warmweiß", "#fff1d6" => "Weiß", "#dfeeff" => "Kaltweiß", "#ff6b5b" => "Rot",
"#ffb347" => "Orange", "#7ddc6f" => "Grün", "#5bb8ff" => "Blau", "#b48cff" => "Lila"] as $f => $name) {
$html .= "<button type='button' class='rf-farbe' style='--f:" . $f . "' data-farbe='" . $f . "'"
. " aria-pressed='" . ($f === $hex ? "true" : "false") . "' aria-label='" . $name . "' title='" . $name . "'></button>";
}
$html .= "<label class='rf-farbe rf-farbe-eigen' title='Eigene Farbe'>"
. "<input type='color' value='" . $hex . "' aria-label='Eigene Farbe'></label></div>";
}
// Presets, Effekte und andere Listen. Wenige Eintraege stehen als Chips
// da; lange Listen (WLED hat ueber 180 Effekte) bekommen einen Knopf mit
// dem aktuellen Eintrag, der eine durchsuchbare Liste aufklappt. Die
// Welcher Eintrag gerade gilt, liest
// js/solar/raumfenster.js bei WLED live aus (room.php?action=wled);
// data-feld ist der Parametername, "effect" oder "preset".
foreach ($listen as $c) {
$p = $c["params"][0];
$kopf = "<div class='rf-liste' data-cmd='" . intval($c["id"]) . "' data-param='" . intval($p["id"]) . "'"
. " data-feld='" . htmlspecialchars(strtolower($p["name"]), ENT_QUOTES) . "'>"
. "<span class='rf-reihe-titel'>" . htmlspecialchars($c["name"]) . "</span>";
$knoepfe = "";
foreach ($p["options"] as $o) {
$knoepfe .= "<button type='button' class='rf-chip' data-wert='" . htmlspecialchars($o["value"], ENT_QUOTES) . "'"
. " aria-pressed='false'>" . htmlspecialchars($o["label"]) . "</button>";
}
if (count($p["options"]) <= 16) {
$html .= $kopf . "<div class='rf-chips'>" . $knoepfe . "</div></div>";
} else {
// Ueber 180 WLED-Effekte waeren als Chips eine Wand. Aus dem
// <select> macht js/solar/auswahl.js die eigene Liste mit Suchfeld,
// und die liegt ueber dem Fenster, statt es hoeher zu machen.
$html .= $kopf . "<select class='form-select form-select-sm'"
. " aria-label='" . htmlspecialchars($c["name"], ENT_QUOTES) . "'>";
foreach ($p["options"] as $o) {
$html .= "<option value='" . htmlspecialchars($o["value"], ENT_QUOTES) . "'>"
. htmlspecialchars($o["label"]) . "</option>";
}
$html .= "</select></div>";
}
}
return $html . "</div></div>";
}
/** Alles, was nur an und aus kennt: eine Kachel zum Antippen. */
function zeichneSchalter($d)
{
$schalter = schaltKommandos($d);
$an = istAn(zustandsWert($d, ["An", "ison", "on", "state", "Status"]));
return "<button type='button' class='rf-schalter raum-geraet' data-actor='" . intval($d["id"]) . "'"
. " 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"]) : "") . "'"
. " aria-pressed='" . ($an ? "true" : "false") . "'>"
. "<span class='rf-sym'><i class='bi bi-power'></i></span>"
. "<b>" . htmlspecialchars($d["label"]) . "</b><span class='rf-st'>" . ($an ? "an" : "aus") . "</span></button>";
}
/**
* 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='rf-effekte'><summary>" . count($weitere) . " weitere Werte</summary>"
. "<div class='raum-werte'>";
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);
}
}