Die Heizung stand ueber den Tabs und war in jedem Raum sichtbar, auch wenn man wegen der Rollladen gekommen war. Sie ist jetzt eine Gruppe wie die anderen - nur baut sie sich nicht aus einer Geraeteliste, sondern ist ein Regler fuer den Raum. zeichneGruppen() nimmt dafuer fertige Inhalte entgegen. Bei einem Raum mit nur einem Thermostat bleibt die Leiste weg, dort sieht es aus wie vorher. Aus den Pillen sind Reiter geworden - sie sehen jetzt aus wie das, was sie sind, und nicht wie eine Knopfreihe. Position und Neigung standen um 0,35 rem versetzt: die Regel fuer gestapelte Regler (Helligkeit ueber Farbe) griff auch auf die zweite Spalte, die aber danebensteht. Sie gilt jetzt nur noch fuer Regler, die keine Spalte sind - nachgemessen stehen beide Bahnen auf 181/104. Das Fensterbild ist von "so breit wie moeglich" auf 5 rem zurueckgenommen; der uebrige Platz verteilt sich auf die Zwischenraeume, statt das Bild breitzuziehen. Zwischen ihm und den Bedienelementen liegt jetzt Luft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
700 lines
27 KiB
PHP
700 lines
27 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 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"],
|
||
"heizung" => ["Heizung", "bi-thermometer-half"],
|
||
"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.
|
||
*
|
||
* $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 "";
|
||
}
|
||
|
||
$html = "";
|
||
if (count($vorhanden) > 1) {
|
||
$html .= "<ul class='nav nav-tabs flex-nowrap 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'>";
|
||
if (isset($fertig[$form])) {
|
||
$html .= $fertig[$form];
|
||
} else {
|
||
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, $beschriftung = "")
|
||
{
|
||
return "<input type='range' class='form-range range-color-track' min='" . $min . "' max='" . $max . "'"
|
||
. " step='1' value='" . $wert . "' data-einheit='" . htmlspecialchars($einheit, ENT_QUOTES) . "'"
|
||
. ($beschriftung === "" ? ""
|
||
: " aria-label='" . htmlspecialchars($beschriftung, ENT_QUOTES) . "'")
|
||
. ">";
|
||
}
|
||
|
||
/**
|
||
* Ein senkrechter Regler mit Wort an jedem Ende.
|
||
*
|
||
* Beide Werte einer Jalousie bewegen etwas, das oben und unten hat; ein
|
||
* waagerechter Schieber dafuer muss erst uebersetzt werden. Senkrecht steht
|
||
* der Griff dort, wo der Rollladen steht.
|
||
*
|
||
* Gedreht wird mit transform, nicht mit writing-mode. writing-mode ist zwar
|
||
* der neuere Weg, aber am Handy laesst sich ein so gebauter Regler nur
|
||
* schlecht anfassen - das Ziehen wird als Wischen gelesen und blaettert die
|
||
* Seite, statt den Griff mitzunehmen. Ein gedrehter gewoehnlicher Regler
|
||
* bewegt sich in seinen eigenen Koordinaten waagerecht, wird von jedem
|
||
* Browser unterstuetzt und traegt dieselbe Gestaltung wie die liegenden.
|
||
*/
|
||
function senkrechterRegler($cmd, $param, $wert, $titel, $oben, $unten, $rolle)
|
||
{
|
||
$min = reglerMin($param);
|
||
$max = reglerMax($param);
|
||
$wert = reglerWert($wert, $min, $max);
|
||
// Keine Prozentzahl darunter: was der Regler bewirkt, zeigt das
|
||
// Lamellenbild daneben, und die gesparte Zeile macht die Bahn laenger.
|
||
return "<div class='raum-regler raum-spalte' data-cmd='" . intval($cmd["id"]) . "'"
|
||
. " data-param='" . intval($param["id"]) . "' data-rolle='" . $rolle . "'>"
|
||
. "<span class='raum-name'>" . htmlspecialchars($titel) . "</span>"
|
||
. "<span class='raum-ende'>" . htmlspecialchars($oben) . "</span>"
|
||
. "<div class='raum-senkrecht'>" . reglerFeld($min, $max, $wert, "%", $titel) . "</div>"
|
||
. "<span class='raum-ende'>" . htmlspecialchars($unten) . "</span>"
|
||
. "</div>";
|
||
}
|
||
|
||
/**
|
||
* Das Fenster, wie die Jalousie davor gerade steht.
|
||
*
|
||
* Zwei Zahlen in einem Bild: wie weit sie heruntergefahren ist und wie die
|
||
* Lamellen stehen. Weder "60 %" noch "30 %" sagt, ob man noch hinaussieht -
|
||
* das Bild sagt es.
|
||
*
|
||
* Die Hoehe schneidet ab: gezeichnet wird nur der Teil des Fensters, den die
|
||
* Jalousie bedeckt, mit der Endschiene an seiner Unterkante. Die Neigung
|
||
* bestimmt, wie dick eine Lamelle von vorn erscheint - w * sin(Winkel),
|
||
* genau das, was das Licht abhaelt. Beides setzt lamellenZeichnen() in
|
||
* js/solar/homeMQTT.js.
|
||
*
|
||
* Die Kennung des Beschnitts traegt die Geraetenummer: in einem Raum haengen
|
||
* mehrere dieser Bilder, und url(#...) findet sonst immer dasselbe.
|
||
*/
|
||
function lamellenBild($actorId)
|
||
{
|
||
$schnitt = "lamellen" . intval($actorId);
|
||
$html = "<svg class='raum-lamellen' viewBox='0 0 100 60' preserveAspectRatio='none' aria-hidden='true'>"
|
||
. "<defs><clipPath id='" . $schnitt . "'>"
|
||
. "<rect class='raum-abdeckung' x='0' y='0' width='100' height='60'></rect>"
|
||
. "</clipPath></defs>"
|
||
. "<g clip-path='url(#" . $schnitt . ")'>";
|
||
foreach ([5, 15, 25, 35, 45, 55] as $mitte) {
|
||
$html .= "<rect class='raum-lamelle' x='0' width='100' data-mitte='" . $mitte . "'"
|
||
. " y='" . ($mitte - 1) . "' height='2'></rect>";
|
||
}
|
||
return $html . "</g>"
|
||
. "<rect class='raum-endschiene' x='0' y='57' width='100' height='3'></rect>"
|
||
. "</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>";
|
||
}
|
||
// Erst das Bild, dann die Regler: neben den Fahrknoepfen faende der
|
||
// Daumen sonst beim Antippen gleich einen Schieber.
|
||
if ($position || $neigung) {
|
||
$html .= lamellenBild($d["id"]);
|
||
}
|
||
if ($position) {
|
||
$html .= senkrechterRegler($position, $position["params"][0], $standPos,
|
||
"Position", "oben", "unten", "position");
|
||
}
|
||
if ($neigung) {
|
||
$html .= senkrechterRegler($neigung, $neigung["params"][0], $standNeigung,
|
||
"Neigung", "auf", "zu", "neigung");
|
||
}
|
||
$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);
|
||
}
|
||
}
|