Files
Smart-Dashboard/restricted/commands.php
T
adminandClaude Opus 5 fe8602a79f Ende der Fahrt abwarten, Umweg an die Hoehe binden
Zwei Sachen, beide an der Anlage aufgefallen.

Abgebrochen wurde auf "core:MovingState ist false". Das ist nicht
dasselbe wie "steht": die Box meldet das Ende der Fahrt, bevor Hoehe und
Neigung darauf nachgezogen haben, und der festgehaltene Wert war dann der
von kurz davor. Statt einer festen Wartezeit zaehlt jetzt Stabilitaet -
fertig ist es, wenn zwei Abfragen hintereinander dasselbe sagen. Damit
faellt auch die Bestaetigungsrunde weg, sie war genau dieser Gedanke in
schlechter.

Der Umweg der Kugelschreiber-Mechanik hing am Neigungswert (ueber 30 %).
Er gehoert an die Hoehe: eine Hoehenfahrt rastet die Lamellen um, danach
muss die Neigung ueber 0 % wieder angefahren werden - bei jedem Winkel,
nicht erst bei grossen. Wird dagegen nur die Neigung verstellt, steht die
Mechanik schon richtig und es geht direkt. NEIGUNG_DIREKT_MAX faellt
damit weg.

Der Hoehenregler im Raum-Modal schickt deshalb jetzt das kombinierte
Kommando mit der Stellung des Neigungsreglers daneben - ohne
Neigungsziel gaebe es nichts, was der Umweg anfahren koennte.

Geprueft an der Wozi-Schiebetuer: Hoehe von 48 auf 60 bei Neigung 76, und
die 76 standen danach wieder da. Vorher htte die Mechanik sie auf einen
beliebigen Wert umgerastet. Dazu elf Faelle im Browser und sieben gegen
transports.py, darunter "Position+Neigung 10" (Umweg) gegen "nur Neigung
80" (direkt).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 13:35:38 +02:00

622 lines
23 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;
/**
* So heissen die beiden Parameter einer Jalousie im Geraetemodell.
*
* An ihnen wird die Kugelschreiber-Mechanik erkannt, nicht am Kommandonamen:
* setzt ein Befehl beide, war eine Hoehenfahrt dabei und die Lamellen sind
* umgerastet - dann muss die Neigung ueber 0 % angefahren werden (siehe
* sendeTahoma()). Setzt er nur die Neigung, steht die Mechanik schon richtig.
*/
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;
$letzterStand = null;
while (time() - $start < JALOUSIE_WARTE_SEKUNDEN) {
sleep(2);
$gelesen = tahomaZustaende($deviceUrl);
if ($gelesen === null) {
continue;
}
$zustaende = $gelesen;
$faehrt = ($zustaende["core:MovingState"] ?? null) === true;
// "faehrt nicht mehr" ist nicht "steht": die Box meldet das Ende der
// Fahrt, bevor Hoehe und Neigung darauf nachgezogen haben. Erst zwei
// gleiche Ablesungen hintereinander sind der Endstand.
$stand = $faehrt ? null
: strval($zustaende["core:ClosureState"] ?? "")
. "/" . strval($zustaende["core:SlateOrientationState"] ?? "");
$steht = $stand !== null && $stand === $letzterStand;
$letzterStand = $stand;
if ($faehrt) {
$gestartet = true;
continue;
}
if ($steht && ($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: eine Hoehenfahrt setzt die Lamellen um, und aus
// der neuen Raststellung heraus laesst sich die Neigung nicht direkt
// anfahren - die Mechanik muss dafuer erst einmal auf 0 % zurueck.
// Deshalb wird derselbe Befehl zweimal geschickt, erst mit Neigung 0,
// dann mit dem gewuenschten Wert; die Position bleibt dabei stehen, die
// Jalousie faehrt also nur einmal.
//
// Der Umweg gehoert an die Hoehe, nicht an den Neigungswert: setzt ein
// Befehl beides, wird er immer gefahren, auch bei kleinen Winkeln. Wird
// nur die Neigung verstellt, steht die Mechanik schon richtig und es geht
// direkt. Ein Befehl, der nur die Hoehe setzt, hat kein Neigungsziel -
// fuer den gibt es hier nichts zu tun; das Raum-Modal schickt deshalb
// beides zusammen (siehe zeichneBeschattung() in roomControls.php).
//
// Dasselbe steht in transports.py: beide Versender brauchen es.
$umweg = $neigungIndex !== null && $positionIndex !== null
&& is_numeric($parameter[$neigungIndex]);
$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)" : "");
}