Die Einstellungen bestanden aus einer einzigen langen Spalte mit den Preistabellen. Sie bekommt Reiter, und darin zwei neue Flaechen: was die Kacheln der Startseite anzeigen, und welche Geraete es ueberhaupt gibt. Reiter gab es vorher dreimal in drei Ausfuehrungen - die Etagen ueber den Automatismen mit eigenem switchTab(), die Geraetegruppen im Raum-Modal mit eigener CSS-Klasse, und die Einstellungsseite noch einmal anders. Jetzt baut restricted/reiter.php alle drei; umgeschaltet wird von Bootstrap, den offenen Reiter merkt reiterMerken() in der Adresse. Kachelwerte: was auf einer Kachel steht, stand fest in rooms.php. Die neue Tabelle kachelWerte (solarLog_kacheln.sql) ueberschreibt das je Raum, die Maske waehlt die Messwerte aus demselben Katalog wie der Automatik-Editor - nach Etage sortiert, die Geraete des eigenen Raums oben. Fehlt die Tabelle, zeigt jede Kachel weiter ihre Vorgabe. Geraete: die Zuordnung Geraet -> Raum war ein Modal, erreichbar ueber ein Zahnrad neben den Automatismen. Sie ist eine Einstellung und steht jetzt dort, zusammen mit dem, was fehlte: seit wann ein Geraet nichts mehr meldet, in welchen Automatiken es steckt, und ein Loeschknopf. Letzterer ist noetig, weil der Suchlauf nichts loescht - er legt an und frischt auf, damit die Automatiken ihre Zustands-Ids behalten; ein abgebautes Geraet bliebe sonst fuer immer stehen. Geloescht wird nur, was in keiner Automatik vorkommt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
744 lines
28 KiB
PHP
744 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 raumBedienungAktivieren() in
|
||
* js/solar/homeMQTT.js - dort haengen die Zuhoerer an den data-Attributen,
|
||
* die hier gesetzt werden.
|
||
*/
|
||
|
||
// Die Geraetegruppen werden als Reiter gezeichnet - eine Bauart fuer die
|
||
// ganze Seite, siehe restricted/reiter.php.
|
||
require_once(__DIR__ . "/reiter.php");
|
||
|
||
/**
|
||
* 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 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 "";
|
||
}
|
||
|
||
// Aus den Gruppen werden Reiter. Wie sie aussehen und wie umgeschaltet
|
||
// wird, steht in restricted/reiter.php - hier nur, was drinsteht.
|
||
$reiter = [];
|
||
foreach ($vorhanden as $form => $titel) {
|
||
$inhalt = "";
|
||
if (isset($fertig[$form])) {
|
||
$inhalt = $fertig[$form];
|
||
} else {
|
||
foreach ($gruppen[$form] as $d) {
|
||
$inhalt .= zeichneGeraet($d, $form);
|
||
}
|
||
}
|
||
$reiter[] = [
|
||
"id" => $form,
|
||
"titel" => $titel[0],
|
||
"symbol" => $titel[1],
|
||
"inhalt" => $inhalt,
|
||
];
|
||
}
|
||
// "schmal", weil das Modal eng ist: kleinere Reiter, die waagerecht
|
||
// scrollen statt umzubrechen.
|
||
return reiterBlock($reiter, ["name" => "raumtab", "schmal" => true,
|
||
"klasse" => "mb-2"]);
|
||
}
|
||
|
||
// ===========================================================================
|
||
// 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,
|
||
$mitParam = null, $mitRolle = "")
|
||
{
|
||
$min = reglerMin($param);
|
||
$max = reglerMax($param);
|
||
$wert = reglerWert($wert, $min, $max);
|
||
// $mitParam: ein zweiter Parameter desselben Kommandos, dessen Wert der
|
||
// Regler mit der Rolle $mitRolle beisteuert. Damit schickt der
|
||
// Hoehenregler die Neigung mit, ohne dass das JavaScript wissen muss,
|
||
// welche Nummern das sind.
|
||
$mit = $mitParam === null ? ""
|
||
: " data-mit-param='" . intval($mitParam["id"]) . "' data-mit-rolle='" . $mitRolle . "'";
|
||
// 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 . "'" . $mit . ">"
|
||
. "<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_PARAMETER);
|
||
$neigung = kommandoMitParameter($d, NEIGUNG_PARAMETER);
|
||
$kombi = kombiKommando($d);
|
||
|
||
// 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);
|
||
|
||
// $kombi gehoert dazu: es steckt jetzt hinter dem Hoehenregler und hat im
|
||
// Mehr-Menue nichts mehr verloren.
|
||
$benutzt = array_filter([$auf, $stop, $zu, $position, $neigung, $kombi]);
|
||
$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) {
|
||
// Kann das Geraet beides zusammen, geht die Hoehe ueber dieses
|
||
// Kommando und nimmt die Neigung mit - sonst raste die Mechanik bei
|
||
// der Hoehenfahrt um und niemand faehrt sie wieder an.
|
||
$hoehenCmd = $kombi ?: $position;
|
||
$hoehenParam = $kombi ? parameterNamens($kombi, POSITION_PARAMETER)
|
||
: $position["params"][0];
|
||
$mitParam = $kombi ? parameterNamens($kombi, NEIGUNG_PARAMETER) : null;
|
||
$html .= senkrechterRegler($hoehenCmd, $hoehenParam, $standPos,
|
||
"Position", "oben", "unten", "position",
|
||
$neigung ? $mitParam : null, "neigung");
|
||
}
|
||
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);
|
||
}
|
||
}
|