Am echten Geraet ausprobiert - drei Sachen stimmten nicht.
Die PHP-Warnung. Nicht jeder Tahoma-Zustand ist eine Zahl oder ein Wort,
manche sind Listen. strval() darauf ist eine Warnung, und die stand mitten
in der JSON-Antwort. response.json() scheiterte, raumRequest() lieferte
{error}, die Verfolgung brach still ab - genau das Bild, das der Bericht
"kommt noch nicht" beschreibt. Nicht-Skalare werden jetzt uebergangen, und
ein Fehler wird gemeldet statt verschluckt.
Das Mitziehen waehrend der Fahrt. Gemessen: die Hoehe steht die ganzen
zwanzig Sekunden auf dem alten Wert und springt erst am Ende um. Wer das
anzeigte, holte den Regler vom eingestellten Ziel auf den alten Stand
zurueck, um ihn danach wieder vorzuschieben. Angezeigt wird deshalb erst,
was am Ende dasteht.
Das vorzeitige Aufhoeren. Zwischen den beiden Befehlen der
Kugelschreiber-Mechanik steht die Jalousie still und meldet die
Zwischenstellung. Die Verfolgung hielt das fuer das Ende und schrieb
"Neigung 0" fest, wo 62 gemeint war. Sie laeuft jetzt mindestens, bis der
Auftrag zurueck ist, und danach noch vierzehn Sekunden - so lange dauert
eine Neigungsfahrt von Anschlag zu Anschlag.
Gegengeprueft an der Wozi-Schiebetuer: Position 49 -> 20 -> 100 -> 49,
Neigung 62 -> 80 -> 62. Die Neigung wanderte beim Herunterfahren von
selbst auf 100 und stand sofort richtig im Modal. Waehrend des zehn
Sekunden langen Kugelschreiber-Auftrags liefen drei Verfolgungsanfragen
durch - die Sitzungssperre ist also wirklich weg.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
554 lines
20 KiB
PHP
554 lines
20 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.");
|
|
}
|
|
|
|
/** 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;
|
|
}
|
|
|
|
/**
|
|
* 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)" : "");
|
|
}
|