Files
Smart-Dashboard/restricted/commands.php
T
adminandClaude Opus 5 67c2a774ac Nach dem Fahrbefehl liest der Server nach, nicht der Browser
Wer das Raum-Modal gleich nach einem Fahrbefehl zumachte, nahm den
einzigen Zuschauer mit: die Verfolgung im Browser endet mit dem Modal,
und niemand hielt fest, wo die Jalousie stehen blieb. In actor_states
stand bis zum naechsten Poll des Runners der alte Wert - genau die fuenf
Minuten, die eigentlich weg sein sollten.

ajax/room.php schickt die Antwort auf ein Tahoma-Kommando jetzt ab,
schliesst die Verbindung und wartet danach selbst auf das Ende der Fahrt
(fastcgi_finish_request, dieselbe Bauart wie in ajax/tahoma.php). Ob der
Browser noch da ist, spielt keine Rolle mehr; ignore_user_abort deckt
auch den Fall ab, dass er mitten im Aufruf verschwindet.

Die Verfolgung im Modal bleibt, sie ist jetzt aber nur noch fuer die
Anzeige zustaendig, solange jemand hinsieht. Das Aufschreiben haengt
nicht mehr daran.

Geprueft an der Wozi-Schiebetuer: Kommando auf Position 70, 0,7 Sekunden
spaeter die Seite neu geladen. 48 Sekunden danach zeigte das frisch
geoeffnete Modal 70 - und eine Neigung von 59 statt 62, die sich beim
Fahren von selbst verstellt hatte und die nur die Box kannte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 12:55:45 +02:00

612 lines
22 KiB
PHP

<?php
/**
* Ein Kommando aus `actor_commands` ausfuehren.
*
* Das Gegenstueck zu SolarManager/autoActions/transports.py, nur fuer den
* Browser: dort schickt der Runner die Kommandos einer Automatik, hier
* schickt sie der Benutzer aus dem Raum-Modal. Die Zuordnung "welche URL
* gehoert zu welchem Weg" ist dieselbe und steht in beiden Dateien am selben
* Ort beschrieben:
*
* mqtt://... Nutzlast auf das Topic des Parameters
* wled://... JSON-Vorlage mit Platzhaltern an /json/state
* http://... Abfrageargumente an die Geraete-URL
* Tahoma exec/apply an die Box, erkannt an der Box-Kennung
*
* Zwei Umsetzungen derselben Sache sind nicht schoen. Sie in eine zu legen
* hiesse aber, entweder den Runner um eine HTTP-Schnittstelle zu erweitern
* (dann haengt die Bedienung daran, dass er laeuft) oder das Web-UI Python
* aufrufen zu lassen. Beides waere teurer als diese Datei.
*/
require_once(__DIR__ . "/mysql.php");
require_once(__DIR__ . "/tahoma_EG.php");
require_once(__DIR__ . "/../ajax/phpMQTT.php");
const MQTT_BROKER = "localhost";
const MQTT_PORT = 1883;
/**
* Bis zu dieser Neigung fahren die Aussenjalousien direkt.
*
* Sie haben eine Kugelschreiber-Mechanik: beim Herunterfahren stehen die
* Lamellen bei etwa 30 %. In Richtung 0 % laesst sich von jeder Stellung aus
* direkt neigen; ueber 30 % hinaus muss die Mechanik erst einmal auf 0 %
* zurueck, sonst rastet sie nicht um. Ein "Neigung 80 %" ohne diesen Umweg
* bleibt wirkungslos.
*
* Das gilt fuer jedes Kommando, das die Neigung setzt - "Neigung" ebenso wie
* "Position+Neigung". Erkannt wird es deshalb am Parameter und nicht am
* Kommandonamen: beide heissen ihren Neigungsparameter "Neigung".
*
* Dasselbe steht in SolarManager/autoActions/transports.py - beide Versender
* brauchen es, und beide muessen es wissen.
*/
const NEIGUNG_DIREKT_MAX = 30;
/** So heissen die beiden Parameter einer Jalousie im Geraetemodell. */
const NEIGUNG_PARAMETER = "Neigung";
const POSITION_PARAMETER = "Position";
/**
* So lange wird hoechstens auf das Ende einer Fahrt gewartet.
*
* Gemessen: eine Neigung von 100 % auf 0 % dauert gut fuenfzehn Sekunden,
* eine volle Fahrt von oben nach unten rund sechzig. Die Grenze ist die
* Notbremse, nicht die uebliche Dauer.
*/
const JALOUSIE_WARTE_SEKUNDEN = 120;
/**
* So lange gilt ein "faehrt nicht" direkt nach dem Absenden als noch nicht
* aussagekraeftig.
*
* Die Box meldet core:MovingState traege: kurz nach einem Kommando steht
* dort noch der alte Wert. Ohne diesen Vorlauf haelt die Warteschleife die
* Fahrt fuer beendet, bevor sie begonnen hat - und das naechste Kommando
* schneidet sie ab.
*/
const JALOUSIE_VORLAUF_SEKUNDEN = 8;
/** Verbindung zur Geraetedatenbank. */
function commandDb()
{
static $db = null;
if ($db === null) {
$db = new mysqli($GLOBALS["mysql_server"], $GLOBALS["mysql_MeshUser"],
$GLOBALS["mysql_MeshPass"], $GLOBALS["mysql_MeshDB"]);
$db->set_charset("utf8mb4");
}
return $db;
}
/** Kommando mit Geraet und Parametern laden. */
function loadCommand($commandId)
{
$db = commandDb();
$stmt = $db->prepare("SELECT c.id, c.command_name, c.command_url,
a.name AS actor_name, a.url AS actor_url
FROM actor_commands c JOIN actors a ON a.id = c.actor_id
WHERE c.id = ?");
$stmt->bind_param("i", $commandId);
$stmt->execute();
$cmd = $stmt->get_result()->fetch_assoc();
$stmt->close();
if (!$cmd) {
return null;
}
$stmt = $db->prepare("SELECT id, parameter_name, url FROM command_parameters
WHERE command_id = ? ORDER BY id");
$stmt->bind_param("i", $commandId);
$stmt->execute();
$res = $stmt->get_result();
$cmd["params"] = [];
while ($row = $res->fetch_assoc()) {
$cmd["params"][] = $row;
}
$stmt->close();
return $cmd;
}
/**
* Kommando ausfuehren. $werte ist [parameter_id => Wert].
*
* Mit $probe = true wird nichts geschickt, nur beschrieben, was geschickt
* wuerde - zum Nachsehen, ohne einen Rollladen in Bewegung zu setzen.
*
* Zurueck kommt ein Text, der den Vorgang beschreibt.
*/
function executeCommand($commandId, $werte, $probe = false)
{
$cmd = loadCommand($commandId);
if (!$cmd) {
throw new InvalidArgumentException("Kommando " . $commandId . " gibt es nicht.");
}
$url = $cmd["actor_url"];
if (strpos($url, "mqtt://") === 0) {
return sendeMqtt($cmd, $werte, $probe);
}
if (strpos($url, "wled://") === 0) {
return sendeWled($cmd, $werte, $probe);
}
if ($GLOBALS["tahoma_PIN"] && strpos($url, "://" . $GLOBALS["tahoma_PIN"] . "/") !== false) {
return sendeTahoma($cmd, $werte, $probe);
}
if (strpos($url, "http") === 0) {
return sendeHttp($cmd, $werte, $probe);
}
throw new RuntimeException("Für " . $cmd["actor_name"] . " gibt es keinen Weg zum Gerät.");
}
/**
* Die Tahoma-Geraete-URL hinter einem Kommando, null wenn es keine ist.
* Damit der Aufrufer nach dem Schalten weiss, ob sich ein Nachlesen lohnt.
*/
function tahomaUrlZuKommando($commandId)
{
$cmd = loadCommand($commandId);
$pin = strval($GLOBALS["tahoma_PIN"] ?? "");
if (!$cmd || $pin === ""
|| strpos(strval($cmd["actor_url"]), "://" . $pin . "/") === false) {
return null;
}
return $cmd["actor_url"];
}
/** Werte in der Reihenfolge der Parameter, fehlende als leer. */
function parameterWerte($cmd, $werte)
{
$liste = [];
foreach ($cmd["params"] as $p) {
$liste[] = [
"name" => $p["parameter_name"],
"url" => $p["url"],
"wert" => strval($werte[$p["id"]] ?? $werte[strval($p["id"])] ?? ""),
];
}
return $liste;
}
/** MQTT: je Parameter eine Nachricht auf dessen Topic. */
function sendeMqtt($cmd, $werte, $probe)
{
$params = parameterWerte($cmd, $werte);
$nachrichten = [];
if (!$params) {
// Kommando ohne Parameter: das Kommando selbst ist die Nutzlast.
$nachrichten[] = [$cmd["command_url"], ""];
} else {
foreach ($params as $p) {
$nachrichten[] = [$p["url"] ?: $cmd["command_url"], $p["wert"]];
}
}
foreach ($nachrichten as $n) {
if (!$n[0]) {
throw new RuntimeException("Kommando ohne Topic.");
}
}
if ($probe) {
return "MQTT: " . implode(", ", array_map(function ($n) {
return $n[0] . " <- " . $n[1];
}, $nachrichten));
}
$mqtt = new Bluerhinos\phpMQTT(MQTT_BROKER, MQTT_PORT, "RoomControl_" . getmypid());
if (!$mqtt->connect(true, null, "", "")) {
throw new RuntimeException("Keine Verbindung zum MQTT-Broker.");
}
foreach ($nachrichten as $n) {
$mqtt->publish($n[0], $n[1], 0, false);
}
$mqtt->close();
return $cmd["actor_name"] . ": " . $cmd["command_name"];
}
/** WLED: die JSON-Vorlage aus command_url fuellen und am Stueck schicken. */
function sendeWled($cmd, $werte, $probe)
{
$vorlage = $cmd["command_url"];
foreach (parameterWerte($cmd, $werte) as $p) {
$vorlage = str_replace("%" . $p["name"] . "%", $p["wert"], $vorlage);
}
$rumpf = json_decode($vorlage, true);
if ($rumpf === null) {
throw new RuntimeException("WLED-Vorlage ergibt kein gültiges JSON: " . $vorlage);
}
$ziel = "http://" . rtrim(substr($cmd["actor_url"], strlen("wled://")), "/") . "/json/state";
if ($probe) {
return "WLED: " . $ziel . " <- " . json_encode($rumpf, JSON_UNESCAPED_UNICODE);
}
$ch = curl_init($ziel);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($rumpf));
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 400 || $code === 0) {
throw new RuntimeException("WLED antwortete mit " . $code);
}
return $cmd["actor_name"] . ": " . $cmd["command_name"];
}
/** HTTP: command_url und Parameter als Abfrageargumente anhaengen. */
function sendeHttp($cmd, $werte, $probe)
{
$argumente = [];
foreach (explode("&", strval($cmd["command_url"])) as $teil) {
if (strpos($teil, "=") !== false) {
list($k, $v) = explode("=", $teil, 2);
$argumente[$k] = $v;
}
}
foreach (parameterWerte($cmd, $werte) as $p) {
if ($p["url"]) {
$argumente[$p["url"]] = $p["wert"];
}
}
if (!$argumente) {
throw new RuntimeException("Kommando ohne URL und ohne Parameter.");
}
$ziel = $cmd["actor_url"] . (strpos($cmd["actor_url"], "?") === false ? "?" : "&")
. http_build_query($argumente);
if ($probe) {
return "HTTP: " . $ziel;
}
$ch = curl_init($ziel);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 400 || $code === 0) {
throw new RuntimeException("Gerät antwortete mit " . $code);
}
return $cmd["actor_name"] . ": " . $cmd["command_name"];
}
/** Ein Kommando an die Tahoma-Box schicken. */
function tahomaKommando($deviceUrl, $name, $parameter)
{
$rumpf = [
"label" => "Raumbedienung",
"actions" => [[
"deviceURL" => $deviceUrl,
"commands" => [["name" => $name, "parameters" => $parameter]],
]],
];
$ch = curl_init("https://gateway-" . $GLOBALS["tahoma_PIN"]
. ":8443/enduser-mobile-web/1/enduserAPI/exec/apply");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json",
"Authorization: Bearer " . $GLOBALS["tahoma_token"]]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($rumpf));
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$antwort = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 400 || $code === 0) {
throw new RuntimeException("Tahoma antwortete mit " . $code . ": " . substr(strval($antwort), 0, 120));
}
}
/**
* Alle Zustaende eines Tahoma-Geraets in einem Zug lesen.
*
* Die Box liefert auf eine Abfrage ohnehin die ganze Liste. Drei einzelne
* Aufrufe fuer drei Felder waren dreimal derselbe Weg dorthin - und
* ausgerechnet in der Warteschleife, die alle zwei Sekunden nachsieht.
*
* Rueckgabe: Feldname => Wert, oder null, wenn die Box nicht antwortet. Ein
* leeres Array waere hier falsch: es hiesse "Geraet kennt keine Zustaende"
* und liesse eine Warteschleife durchlaufen, statt sie warten zu lassen.
*/
function tahomaZustaende($deviceUrl)
{
$ch = curl_init("https://gateway-" . $GLOBALS["tahoma_PIN"]
. ":8443/enduser-mobile-web/1/enduserAPI/setup/devices/"
. rawurlencode($deviceUrl) . "/states");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer " . $GLOBALS["tahoma_token"]]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$antwort = json_decode(curl_exec($ch), true);
curl_close($ch);
if (!is_array($antwort)) {
return null;
}
$werte = [];
foreach ($antwort as $zustand) {
if (isset($zustand["name"])) {
$werte[$zustand["name"]] = $zustand["value"] ?? null;
}
}
return $werte;
}
/** Einen Zustandswert eines Tahoma-Geraets lesen, null wenn unbekannt. */
function tahomaZustand($deviceUrl, $feld)
{
$zustaende = tahomaZustaende($deviceUrl);
return $zustaende === null ? null : ($zustaende[$feld] ?? null);
}
/**
* Einen Zustandswert so in Text fassen, wie der Runner es tut.
*
* In actor_states.current_value schrieb bisher nur der Runner, und der macht
* str() daraus - aus einem Wahrheitswert wird dort "True" bzw. "False", nicht
* "1" und "". Das sieht aus PHP heraus schraeg aus, ist aber wichtig: sonst
* stuenden fuer denselben Zustand zwei Schreibweisen in der Tabelle, je
* nachdem wer zuletzt geschrieben hat. istAn() in roomControls.php kennt
* beide, eine Bedingung im Automatik-Editor vergleicht aber stur.
*/
function tahomaWertText($wert)
{
if (is_bool($wert)) {
return $wert ? "True" : "False";
}
// Nicht jeder Zustand der Box ist eine Zahl oder ein Wort: manche sind
// Listen oder Objekte. Als Messwert taugt das nicht, und strval() darauf
// ist eine PHP-Warnung - die stuende dann mitten in der JSON-Antwort und
// machte sie unlesbar, ohne dass irgendwo ein Fehler auftaucht.
if (!is_scalar($wert)) {
return null;
}
return strval($wert);
}
/**
* Die frisch gelesenen Zustaende eines Tahoma-Geraets in actor_states
* festhalten.
*
* Sonst schreibt dort nur der Runner, und der fragt die Box alle fuenf
* Minuten ab (poll_tahoma in seiner config.ini) - nach einem Fahrbefehl
* zeigte jedes Modal bis dahin den Stand von vor der Fahrt. Die Werte liegen
* hier ohnehin schon vor, weil die Warteschleife sie gelesen hat.
*
* Geschrieben wird nur, was sich geaendert hat. Der Runner haelt einen
* eigenen Merkzettel darueber, was er zuletzt geschrieben hat; er wird
* unsere Zeile beim naechsten Poll hoechstens einmal ueberfluessig
* ueberschreiben, denn er liest dieselbe Box.
*
* Rueckgabe: wie viele Zeilen geaendert wurden.
*/
function tahomaZustaendeSchreiben($deviceUrl, $zustaende)
{
if (!is_array($zustaende) || !$zustaende) {
return 0;
}
$db = commandDb();
$stmt = $db->prepare("SELECT s.id, s.url, s.current_value FROM actor_states s
JOIN actors a ON a.id = s.actor_id
WHERE a.url = ? AND s.url IS NOT NULL AND s.url <> ''");
$stmt->bind_param("s", $deviceUrl);
$stmt->execute();
$zeilen = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt->close();
$geaendert = 0;
$schreib = $db->prepare("UPDATE actor_states SET current_value = ? WHERE id = ?");
foreach ($zeilen as $zeile) {
if (!array_key_exists($zeile["url"], $zustaende)) {
continue;
}
$text = tahomaWertText($zustaende[$zeile["url"]]);
if ($text === null || strval($zeile["current_value"]) === $text) {
continue;
}
$id = intval($zeile["id"]);
$schreib->bind_param("si", $text, $id);
$schreib->execute();
$geaendert++;
}
$schreib->close();
return $geaendert;
}
/**
* Wartet, bis die Jalousie ihre Fahrt beendet hat und die Zielwerte zeigt.
*
* Zwei Auskuenfte zusammen, weil einzeln keine traegt:
*
* core:MovingState taugt fuer die lange Fahrt hoch und runter, wird
* bei kurzen Neigungsfahrten aber nie gesetzt.
* die Zustandswerte sind die eigentliche Wahrheit, brauchen aber
* einen Vorlauf, weil sie kurz nach dem Kommando
* noch den alten Stand zeigen.
*
* Fertig ist die Fahrt, wenn nichts mehr faehrt, die Ziele erreicht sind und
* entweder ein "faehrt" gesehen wurde oder der Vorlauf um ist.
*
* $schliessungZiel darf null sein, wenn das Kommando die Position nicht
* anfasst.
*/
function warteAufJalousie($deviceUrl, $neigungZiel, $schliessungZiel = null)
{
$start = time();
$gestartet = false;
while (time() - $start < JALOUSIE_WARTE_SEKUNDEN) {
sleep(2);
$zustaende = tahomaZustaende($deviceUrl);
if ($zustaende === null) {
continue;
}
$faehrt = $zustaende["core:MovingState"] ?? null;
$neigung = $zustaende["core:SlateOrientationState"] ?? null;
$schliessung = $zustaende["core:ClosureState"] ?? null;
if ($faehrt === true) {
$gestartet = true;
continue;
}
// null heisst "nicht gelesen". intval() daraus waere 0 - und damit
// ausgerechnet beim Ziel 0 ein falsches Erfolgssignal.
$neigungOk = $neigung !== null && intval($neigung) === $neigungZiel;
$schliessungOk = $schliessungZiel === null
|| ($schliessung !== null && intval($schliessung) === $schliessungZiel);
if ($neigungOk && $schliessungOk
&& ($gestartet || time() - $start >= JALOUSIE_VORLAUF_SEKUNDEN)) {
// Die Fahrt ist zu Ende und die Werte sind gerade gelesen - der
// guenstigste Moment, sie festzuhalten.
tahomaZustaendeSchreiben($deviceUrl, $zustaende);
return true;
}
}
return false;
}
/**
* Nach einem Kommando abwarten, bis das Geraet steht, und seinen Stand in
* actor_states festhalten.
*
* Gedacht fuer den Aufruf *nach* fastcgi_finish_request(): der Browser hat
* seine Antwort dann laengst, und ob jemand noch zusieht, spielt keine
* Rolle mehr. Genau daran scheiterte die Verfolgung im Modal - wer es
* direkt nach dem Fahrbefehl zumachte, liess niemanden zurueck, der den
* Endstand haette aufschreiben koennen. Bis zum naechsten Poll des Runners
* stand dann der alte Wert in der Tabelle, fuenf Minuten lang.
*
* Gewartet wird nur auf core:MovingState, ohne Zielwerte - welche das
* waeren, weiss hier niemand, und nicht jedes Kommando loest eine Fahrt
* aus. Der Vorlauf faengt die Traegheit der Box ab: kurz nach dem Kommando
* steht dort noch "faehrt nicht".
*
* Anders als die Warteschleife der Kugelschreiber-Mechanik laeuft das hier
* erst, wenn das ganze Manoever durch ist - beide Schwenks sind geschickt,
* die Fahrt, auf die gewartet wird, ist die letzte.
*/
function tahomaNachlesen($deviceUrl)
{
$start = time();
$gestartet = false;
$zustaende = null;
while (time() - $start < JALOUSIE_WARTE_SEKUNDEN) {
sleep(2);
$gelesen = tahomaZustaende($deviceUrl);
if ($gelesen === null) {
continue;
}
$zustaende = $gelesen;
if (($zustaende["core:MovingState"] ?? null) === true) {
$gestartet = true;
continue;
}
if ($gestartet || time() - $start >= JALOUSIE_VORLAUF_SEKUNDEN) {
break;
}
}
return $zustaende === null ? 0 : tahomaZustaendeSchreiben($deviceUrl, $zustaende);
}
/**
* Die Geraete-URL eines Aktors, null wenn es ihn nicht gibt oder er nicht an
* der Tahoma-Box haengt.
*
* Die Pruefung auf die Box-Kennung ist dieselbe wie im Runner: am Schema
* laesst sich ein Tahoma-Geraet nicht erkennen, dieselbe Box liefert io://
* fuer die Jalousien, rts:// fuer die Dachfenster und internal:// fuer die
* Alarmanlage. Ohne sie wuerde eine Anfrage mit der Nummer einer Shelly die
* Box nach einer URL fragen, die sie nicht kennt.
*/
function tahomaAktorUrl($actorId)
{
$pin = strval($GLOBALS["tahoma_PIN"] ?? "");
if ($pin === "") {
return null;
}
$db = commandDb();
$stmt = $db->prepare("SELECT url FROM actors WHERE id = ?");
$id = intval($actorId);
$stmt->bind_param("i", $id);
$stmt->execute();
$zeile = $stmt->get_result()->fetch_assoc();
$stmt->close();
if (!$zeile || strpos(strval($zeile["url"]), "://" . $pin . "/") === false) {
return null;
}
return $zeile["url"];
}
/**
* Hat das Geraet ein Kommando, das Position und Neigung zusammen setzt?
* Nur solche Geraete sind Jalousien mit der Kugelschreiber-Mechanik.
*/
function jalousieKombiKommando($actorUrl)
{
$db = commandDb();
$stmt = $db->prepare("SELECT c.command_url FROM actor_commands c
JOIN actors a ON a.id = c.actor_id
WHERE a.url = ? AND c.command_url = 'setClosureAndOrientation'");
$stmt->bind_param("s", $actorUrl);
$stmt->execute();
$treffer = $stmt->get_result()->fetch_assoc();
$stmt->close();
return $treffer ? $treffer["command_url"] : null;
}
/**
* Tahoma: exec/apply an die Box - dieselbe Form wie in ajax/tahoma.php, nur
* ohne dessen Sonderbehandlung fuer "faehrt gerade".
*/
function sendeTahoma($cmd, $werte, $probe)
{
$befehl = $cmd["command_url"];
$parameter = [];
$neigungIndex = null;
$positionIndex = null;
foreach (parameterWerte($cmd, $werte) as $i => $p) {
// Die Box erwartet Zahlen als Zahlen, Text als Text.
$parameter[] = is_numeric($p["wert"]) ? $p["wert"] + 0 : $p["wert"];
if ($p["name"] === NEIGUNG_PARAMETER) {
$neigungIndex = $i;
} elseif ($p["name"] === POSITION_PARAMETER) {
$positionIndex = $i;
}
}
// "Zu" allein macht diese Jalousien nicht dicht: sie faehrt herunter, die
// Lamellen bleiben durch die Mechanik aber bei etwa 30 % offen stehen.
// Gemeint ist "ganz unten, Lamellen geschlossen" - also dasselbe wie
// Position 100 mit Neigung 100, und damit ein Fall fuer die Regel unten.
if ($befehl === "down" && jalousieKombiKommando($cmd["actor_url"])) {
$befehl = "setClosureAndOrientation";
$parameter = [100, 100];
$positionIndex = 0;
$neigungIndex = 1;
}
// Kugelschreiber-Mechanik: ueber NEIGUNG_DIREKT_MAX hinaus rastet sie nur
// um, wenn sie vorher einmal auf 0 % war. Geschickt wird deshalb derselbe
// Befehl zweimal - erst mit Neigung 0, dann mit dem gewuenschten Wert.
// Die Position bleibt dabei stehen, die Jalousie faehrt also nur einmal.
$umweg = $neigungIndex !== null
&& is_numeric($parameter[$neigungIndex])
&& $parameter[$neigungIndex] > NEIGUNG_DIREKT_MAX;
$vorstufe = $parameter;
if ($umweg) {
$vorstufe[$neigungIndex] = 0;
}
if ($probe) {
$text = "Tahoma: " . $befehl . " " . json_encode($parameter);
return $umweg
? $text . " (zuerst " . json_encode($vorstufe) . ", dann warten)"
: $text;
}
if ($umweg) {
tahomaKommando($cmd["actor_url"], $befehl, $vorstufe);
warteAufJalousie($cmd["actor_url"], 0,
$positionIndex === null ? null : intval($parameter[$positionIndex]));
}
tahomaKommando($cmd["actor_url"], $befehl, $parameter);
return $cmd["actor_name"] . ": " . $cmd["command_name"]
. ($umweg ? " (über Neigung 0 gefahren)" : "");
}