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"]; } /** * 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) { $parameter = []; foreach (parameterWerte($cmd, $werte) as $p) { // Die Box erwartet Zahlen als Zahlen, Text als Text. $parameter[] = is_numeric($p["wert"]) ? $p["wert"] + 0 : $p["wert"]; } $rumpf = [ "label" => "Raumbedienung", "actions" => [[ "deviceURL" => $cmd["actor_url"], "commands" => [["name" => $cmd["command_url"], "parameters" => $parameter]], ]], ]; $ziel = "https://gateway-" . $GLOBALS["tahoma_PIN"] . ":8443/enduser-mobile-web/1/enduserAPI/exec/apply"; if ($probe) { return "Tahoma: " . json_encode($rumpf, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } $ch = curl_init($ziel); 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)); } return $cmd["actor_name"] . ": " . $cmd["command_name"]; }