Raum-Modal: Heizung, Rollladen und Licht auf einer Seite

Der Grundriss zeigt Raeume, ein Klick darauf zeigte aber nur den
Temperaturregler. Die Rollladen im selben Zimmer waren von dort gar nicht
erreichbar, obwohl die Karte genau sagt, wo sie haengen.

Das Modal zeigt jetzt alles, was dem Raum zugeordnet ist: die
Solltemperatur wie bisher, dazu je Geraet seine Kommandos. Kommandos ohne
Parameter werden Knoepfe nebeneinander - "Auf", "Zu", "Stop" will man
antippen und nicht erst einstellen -, Kommandos mit Parametern bekommen
ihre Eingabefelder und einen Ausloeser. Geraete ohne Kommandos zeigen ihre
Messwerte. Die Reihenfolge der Knoepfe kommt nicht mehr aus dem
Geraete-Suchlauf: die haeufig gebrauchten stehen vorn.

Ausgefuehrt wird ueber restricted/commands.php - das Gegenstueck zu
transports.py, nur fuer den Browser. Dieselbe Zuordnung "welche URL gehoert
zu welchem Weg", nur eben in PHP:

    mqtt://   Nutzlast auf das Topic des Parameters
    wled://   JSON-Vorlage mit Platzhaltern an /json/state
    http://   Abfrageargumente an die Geraete-URL
    Tahoma    exec/apply, erkannt an der Box-Kennung in der URL

Zwei Umsetzungen derselben Sache sind nicht schoen. Die Alternative waere,
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; der Kommentar sagt das an
beiden Stellen.

Der Endpunkt kennt einen Probelauf: mit "probe" beschreibt er, was er
schicken wuerde, ohne es zu schicken. Damit sind alle vier Wege geprueft,
ohne einen Rollladen in Bewegung zu setzen - Tahoma mit und ohne Parameter,
WLED mit gefuellter Vorlage, Shelly Gen1 und MQTT.

ajax/roomtemp.php entfaellt, es ging darin ausschliesslich um den Regler,
den das Raum-Modal jetzt enthaelt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-31 16:43:21 +02:00
co-authored by Claude Opus 5
parent bed1884b04
commit 98a1da6cd5
5 changed files with 543 additions and 78 deletions
+191
View File
@@ -0,0 +1,191 @@
<?php
/**
* Raum-Modal: alles, was in einem Raum haengt, auf einer Seite.
*
* Loest das frühere Thermostat-Modal ab. Der Grundriss zeigt Raeume, also
* sollte ein Klick darauf den Raum oeffnen und nicht nur seine Heizung - die
* Rollladen im selben Zimmer waren bisher gar nicht erreichbar.
*
* Welche Geraete zu einem Raum gehoeren, sagt die Spalte actors.room (siehe
* den Zuordnungsdialog in der Karte "Automatismen").
*
* GET ?room=EG_Bad Inhalt des Modals
* POST ?action=command {"command_id": N, "params": {...}} - ein Kommando
* POST ?action=temp {"room": "EG_Bad", "temp": 21} - Solltemperatur
*/
require_once("../helper.php");
require_once("../restricted/automations.php");
require_once("../restricted/commands.php");
require_once("./phpMQTT.php");
if (!checkLogin()) {
http_response_code(403);
exit;
}
function raumAntwort($data, $code = 200)
{
http_response_code($code);
header("Content-Type: application/json; charset=utf-8");
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
// --- Schaltende Aufrufe ---------------------------------------------------
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$body = json_decode(file_get_contents("php://input"), true);
if (!is_array($body)) {
raumAntwort(["error" => "Konnte die Anfrage nicht lesen."], 400);
}
try {
if (($_GET["action"] ?? "") === "command") {
$text = executeCommand(intval($body["command_id"] ?? 0),
is_array($body["params"] ?? null) ? $body["params"] : [],
!empty($body["probe"]));
raumAntwort(["ok" => $text]);
}
if (($_GET["action"] ?? "") === "temp") {
// Bewusst derselbe Weg wie im alten Thermostat-Modal: das Topic
// Raumtemp/<Etage>/<Raum>/changeSetTemp.
$raum = preg_replace('/[^A-Za-z0-9_]/', '', strval($body["room"] ?? ""));
$temp = intval($body["temp"] ?? 0);
if ($temp < 10 || $temp > 30) {
raumAntwort(["error" => "Temperatur außerhalb 1030 °C."], 400);
}
$mqtt = new Bluerhinos\phpMQTT(MQTT_BROKER, MQTT_PORT, "RoomTempSetter_" . getmypid());
if (!$mqtt->connect(true, null, "", "")) {
raumAntwort(["error" => "Keine Verbindung zum MQTT-Broker."], 500);
}
$mqtt->publish("Raumtemp/" . str_replace("_", "/", $raum) . "/changeSetTemp",
strval($temp), 0, false);
$mqtt->close();
raumAntwort(["ok" => "Solltemperatur " . $temp . " °C"]);
}
raumAntwort(["error" => "Unbekannte Aktion."], 400);
} catch (InvalidArgumentException $e) {
raumAntwort(["error" => $e->getMessage()], 400);
} catch (Throwable $e) {
raumAntwort(["error" => $e->getMessage()], 500);
}
}
// --- Inhalt des Modals ----------------------------------------------------
$schluessel = preg_replace('/[^A-Za-z0-9_]/', '', strval($_GET["room"] ?? ""));
$teile = explode("_", $schluessel, 2);
$etage = $teile[0] ?? "";
$name = $teile[1] ?? "";
$raumKey = $etage . "/" . $name;
// Hat der Raum ein Thermostat? Nur dann gibt es etwas zu heizen.
$hatThermostat = false;
foreach (allRooms() as $raum) {
if ($raum["floor"] === $etage && $raum["mqtt"] === $name) {
$hatThermostat = isset($raum["x"]); // Kachel = Thermostat vorhanden
}
}
$katalog = deviceCatalog();
$geraete = array_values(array_filter($katalog, function ($d) use ($raumKey) {
return $d["room"] === $raumKey && count($d["commands"]) > 0;
}));
$messwerte = array_values(array_filter($katalog, function ($d) use ($raumKey) {
return $d["room"] === $raumKey && count($d["states"]) > 0 && count($d["commands"]) === 0;
}));
echo "<div class='p-2' id='roomView' data-room='" . htmlspecialchars($schluessel, ENT_QUOTES) . "'>";
if ($hatThermostat) {
echo "<div class='card mb-2'><div class='card-body py-2'>";
echo "<label class='form-label mb-0'>Solltemperatur "
. "<span id='modal-slider-label' style='font-size:15px;color:#297195;display:inline-block'></span></label>";
echo "<input type='range' id='modal-slider' class='form-range range-color-track' min='10' max='30'>";
echo "<div class='d-flex justify-content-between small text-body-secondary'>"
. "<span>10 °C</span><span>30 °C</span></div>";
echo "</div></div>";
}
if (!$geraete && !$messwerte && !$hatThermostat) {
echo "<p class='text-body-secondary mb-0'>Diesem Raum ist noch nichts zugeordnet. "
. "Das geht über das Haus-Symbol in der Karte „Automatismen“.</p>";
}
foreach ($geraete as $d) {
echo "<div class='card mb-2'><div class='card-body py-2'>";
echo "<div class='fw-bold small mb-2'>" . htmlspecialchars($d["label"])
. " <span class='fw-normal text-body-secondary'>" . htmlspecialchars($d["type"]) . "</span></div>";
// Kommandos ohne Parameter werden Knoepfe nebeneinander - "Auf", "Zu",
// "Stop" will man antippen und nicht erst einstellen. Die Reihenfolge in
// der Datenbank ist die des Geraete-Suchlaufs und damit Zufall; die
// haeufig gebrauchten kommen deshalb nach vorne.
$knoepfe = array_values(array_filter($d["commands"], function ($c) { return !count($c["params"]); }));
$vorne = ["Auf", "Zu", "Stop", "An", "Aus", "turn_on", "turn_off", "toggle"];
usort($knoepfe, function ($a, $b) use ($vorne) {
$ia = array_search($a["name"], $vorne, true);
$ib = array_search($b["name"], $vorne, true);
$ia = $ia === false ? 99 : $ia;
$ib = $ib === false ? 99 : $ib;
return $ia === $ib ? strcoll($a["name"], $b["name"]) : $ia - $ib;
});
if ($knoepfe) {
echo "<div class='d-flex flex-wrap gap-1 mb-2'>";
foreach ($knoepfe as $c) {
echo "<button type='button' class='btn btn-outline-primary btn-sm' data-cmd='" . $c["id"] . "'>"
. htmlspecialchars($c["name"]) . "</button>";
}
echo "</div>";
}
foreach ($d["commands"] as $c) {
if (!count($c["params"])) {
continue;
}
echo "<div class='input-group input-group-sm mb-1 raum-cmd' data-cmdrow='" . $c["id"] . "'>";
echo "<span class='input-group-text'>" . htmlspecialchars($c["name"]) . "</span>";
foreach ($c["params"] as $p) {
if (count($c["params"]) > 1) {
echo "<span class='input-group-text'>" . htmlspecialchars($p["name"]) . "</span>";
}
if ($p["options"]) {
echo "<select class='form-select' data-param='" . $p["id"] . "'>";
foreach ($p["options"] as $o) {
echo "<option value='" . htmlspecialchars($o["value"], ENT_QUOTES) . "'>"
. htmlspecialchars($o["label"]) . "</option>";
}
echo "</select>";
} else {
echo "<input type='" . ($p["input"] === "number" ? "number" : "text")
. "' class='form-control' data-param='" . $p["id"] . "'"
. ($p["min"] !== null ? " min='" . $p["min"] . "'" : "")
. ($p["max"] !== null ? " max='" . $p["max"] . "'" : "")
. " value='" . ($p["min"] !== null ? intval($p["min"]) : 0) . "'>";
}
}
echo "<button type='button' class='btn btn-outline-primary' data-cmd='" . $c["id"] . "'>"
. "<i class='bi bi-play-fill'></i></button>";
echo "</div>";
}
echo "</div></div>";
}
// Geraete ohne Kommandos: nur anzeigen, was sie melden.
foreach ($messwerte as $d) {
echo "<div class='card mb-2'><div class='card-body py-2'>";
echo "<div class='fw-bold small mb-1'>" . htmlspecialchars($d["label"])
. " <span class='fw-normal text-body-secondary'>" . htmlspecialchars($d["type"]) . "</span></div>";
echo "<div class='small text-body-secondary'>";
$teile = [];
foreach (array_slice($d["states"], 0, 8) as $s) {
$teile[] = htmlspecialchars($s["name"]) . ": " . htmlspecialchars(strval($s["value"]))
. ($s["unit"] ? " " . htmlspecialchars($s["unit"]) : "");
}
echo implode(" · ", $teile);
if (count($d["states"]) > 8) {
echo " · … " . (count($d["states"]) - 8) . " weitere";
}
echo "</div></div></div>";
}
echo "<div id='roomFeedback' class='small mt-2'></div>";
echo "</div>";
-48
View File
@@ -1,48 +0,0 @@
<?php
require_once("./phpMQTT.php");
require_once("../helper.php");
if (checkLogin()) {
$close = 0;
$hostname = "192.168.179.169";
$server = 'localhost'; // change if necessary
$port = 1883; // change if necessary
$username = ''; // set your username
$password = ''; // set your password
$client_id = 'RoomtempSetter'; // make sure this is unique for connecting to sever - you could use uniqid()
if (isset($_POST["setTemp"]) && isset($_GET["heater"])) {
$topic = "Raumtemp/".str_replace("_","/",$_GET["heater"])."/changeSetTemp";
$mqtt = new Bluerhinos\phpMQTT($server, $port, $client_id);
//send new value
$temp = intval($_POST["setTemp"]);
if($temp > 9 && $temp < 31){
if ($mqtt->connect(true, NULL, $username, $password)) {
$mqtt->publish($topic, $temp, 0, false);
$mqtt->close();
}
}
$close = 1;
}
} else {
$close = 1;
}
if (!$close) {
echo <<<ENDE
<!--begin::Form-->
<form id="heater_form">
<div class="col-sm-11">
<div class="form-check">
<label class="form-label" for="addedCharge">Temperatur: </label>
<div style="font-size: 15px;color: #297195;display: inline-block;" id="modal-slider-label">0</div>
<input type="range" id="modal-slider" name="setTemp" class="form-range range-color-track" min="10" max="30" />
<div class="d-flex justify-content-between">
<span class="max-amount f-16 font-weight-normal darkGray-color text-right mt-1">10 °C</span>
<span class="max-amount f-16 font-weight-normal darkGray-color text-right mt-1">30 °C</span>
</div>
</div>
</div>
</form>
ENDE;
}
+9
View File
@@ -102,3 +102,12 @@ input[type='range']::-webkit-slider-runnable-track {
width: auto;
}
}
/* Raum-Modal. Ein Kommando mit mehreren Parametern ("Position+Neigung")
traegt Beschriftung, zwei Zahlenfelder und den Ausloeseknopf in einer
input-group - auf einem Handy schrumpfen die Felder dabei auf Nullbreite.
Eine Mindestbreite laesst die Zeile stattdessen umbrechen. */
.input-group.raum-cmd > .form-control,
.input-group.raum-cmd > .form-select {
min-width: 5rem;
}
+92 -30
View File
@@ -151,7 +151,7 @@ function switchFloor(floor){
var mqttData = {};
function openHeaterSettings(heater){
openModal(heater)
openRoomView(heater);
}
document.addEventListener('readystatechange', function () {
@@ -162,36 +162,98 @@ function switchFloor(floor){
});
function openModal(heater) {
var contentURL = "./ajax/roomtemp.php?heater="+heater;
let heaterMQTT = heater.toString().split("_");
document.getElementById("modal-title").innerHTML = "Thermostat "+heater.replace("_"," ").replace("ue","ü").replace("ae","ä").replace("oe","ö");
modalBodyElement = document.getElementById("modal-body");
modalBodyElement.innerHTML = loadingHTML("Wird geladen...");
document.getElementById("modalSaveBtn").addEventListener("click", submitFormAjax);
document.getElementById("modalSaveBtn").contentURL = contentURL;
modalEV.show();
fetch(contentURL, {
method: 'GET',
headers: {
'X-Requested-From-Modal': 'a',
'Requested-With-Ajax': 'ajax'
}
})
.then(response => response.text())
.then(html => {
modalBodyElement.innerHTML = html;
var slider = document.getElementById("modal-slider");
var span = document.getElementById("modal-slider-label");
bindModalSlider(slider, span, value => value + " °C");
/**
* Alles, was in einem Raum haengt, auf einer Seite: Heizung, Rollladen,
* Licht. Der Grundriss zeigt Raeume, also sollte ein Klick darauf den Raum
* oeffnen - vorher kam nur der Temperaturregler, die Rollladen im selben
* Zimmer waren von hier gar nicht erreichbar.
*
* Welche Geraete dazugehoeren, sagt die Raumzuordnung (actors.room).
*/
function openRoomView(raum) {
const teile = raum.toString().split("_");
document.getElementById("modal-title").innerHTML =
teile[0] + " · " + teile.slice(1).join(" ")
.replace("ue", "ü").replace("ae", "ä").replace("oe", "ö");
if(typeof(mqttData["Raumtemp"][heaterMQTT[0]][heaterMQTT[1]]["Set Temp[degC]"])!= "undefined")
slider.setAttribute("value",mqttData["Raumtemp"][heaterMQTT[0]][heaterMQTT[1]]["Set Temp[degC]"]);
else
slider.setAttribute("value",10);
slider.dispatchEvent(new Event('input'));
// Der Speichern-Knopf gehoert allen Modals gemeinsam. Ein Klon ohne
// Zuhoerer stellt sicher, dass nicht auch der Handler eines vorher
// geoeffneten Modals mitfeuert.
const alt = document.getElementById("modalSaveBtn");
const btn = alt.cloneNode(true);
alt.replaceWith(btn);
btn.addEventListener("click", () => sendeSolltemperatur(raum));
const koerper = document.getElementById("modal-body");
koerper.innerHTML = loadingHTML("Wird geladen...");
modalEV.show();
fetch("./ajax/room.php?room=" + encodeURIComponent(raum), {
method: "GET",
headers: { "X-Requested-From-Modal": "a", "Requested-With-Ajax": "ajax" }
})
.then(a => a.text())
.then(html => {
koerper.innerHTML = html;
const slider = document.getElementById("modal-slider");
if (slider) {
bindModalSlider(slider, document.getElementById("modal-slider-label"),
wert => wert + " °C");
const stand = mqttData["Raumtemp"]?.[teile[0]]?.[teile[1]]?.["Set Temp[degC]"];
slider.value = typeof stand !== "undefined" ? stand : 10;
slider.dispatchEvent(new Event("input"));
}
// Ohne Thermostat gibt es nichts zu speichern - dann waere der Knopf
// eine Falle.
btn.hidden = !slider;
koerper.querySelectorAll("button[data-cmd]").forEach(k => {
k.addEventListener("click", () => schalte(k));
});
})
.catch(error => {
modalBodyElement.innerHTML += error.message;
.catch(fehler => { koerper.innerHTML = "Konnte den Raum nicht laden: " + fehler.message; });
}
/** Ein Kommando des angeklickten Knopfes ausfuehren. */
function schalte(knopf) {
const zeile = knopf.closest("[data-cmdrow]");
const params = {};
if (zeile) {
zeile.querySelectorAll("[data-param]").forEach(f => { params[f.dataset.param] = f.value; });
}
knopf.disabled = true;
raumRequest("command", { command_id: Number(knopf.dataset.cmd), params: params })
.then(antwort => {
knopf.disabled = false;
raumMeldung(antwort);
});
}
function sendeSolltemperatur(raum) {
const slider = document.getElementById("modal-slider");
if (!slider) return;
raumRequest("temp", { room: raum, temp: Number(slider.value) })
.then(antwort => {
raumMeldung(antwort);
if (!antwort.error) setTimeout(() => modalEV.hide(), 700);
});
}
function raumRequest(action, body) {
return fetch("./ajax/room.php?action=" + action, {
method: "POST",
headers: { "Content-Type": "application/json", "Requested-With-Ajax": "ajax" },
body: JSON.stringify(body)
})
.then(a => a.json())
.catch(fehler => ({ error: fehler.message }));
}
/** Rueckmeldung im Modal statt als alert - man schaltet oft mehrmals. */
function raumMeldung(antwort) {
const ziel = document.getElementById("roomFeedback");
if (!ziel) return;
ziel.className = "small mt-2 " + (antwort.error ? "text-danger" : "text-body-secondary");
ziel.textContent = antwort.error ? antwort.error : "✓ " + antwort.ok;
}
+251
View File
@@ -0,0 +1,251 @@
<?php
/**
* Ein Kommando aus `actor_commands` ausfuehren.
*
* Das Gegenstueck zu restricted/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;
/** 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"];
}
/**
* 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"];
}