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>
679 lines
28 KiB
PHP
679 lines
28 KiB
PHP
<?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 . " %"; };
|
||
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);
|
||
}
|
||
}
|