Geraete). * * GET ?room=12 Inhalt des Modals, 12 = rooms.id * POST ?action=command {"command_id": N, "params": {...}} - ein Kommando * POST ?action=temp {"room": 12, "temp": 21} - Solltemperatur * * Frueher hiess der Raum "EG_Bad" und wurde am Unterstrich zerlegt. Das hielt * weder ein Leerzeichen im Namen aus (erst nachgeruestet) noch eine * Umbenennung - die feste Nummer haelt beides. */ /** Ein Raum nach seiner festen Nummer, oder null. */ function raumNachNummer($nr) { foreach (allRooms() as $r) { if ($r["nr"] === intval($nr)) { return $r; } } return null; } // Ein "Zu" an einer Jalousie faehrt herunter, wartet und neigt danach - das // dauert in der Summe ueber eine Minute. Die Vorgabe von PHP reicht dafuer // nicht; ajax/tahoma.php macht es an derselben Stelle genauso. set_time_limit(180); require_once("../helper.php"); require_once("../restricted/automations.php"); require_once("../restricted/commands.php"); require_once("../restricted/roomControls.php"); require_once("./phpMQTT.php"); if (!checkLogin()) { http_response_code(403); exit; } // Die Sitzung wird ab hier nur noch gelesen, und PHP haelt ihre Datei bis // zum Ende der Anfrage exklusiv gesperrt. Ein "Zu" an einer Jalousie wartet // weiter unten ueber eine Minute auf das Ende der Fahrt - solange stand jede // weitere Anfrage desselben Browsers still, auch die, die waehrend der Fahrt // nachsehen will, wie weit sie ist. Alle Schreibzugriffe auf $_SESSION // stecken in checkLogin() und sind hier durch. session_write_close(); 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; } /** * Die Antwort abschicken, aber weiterarbeiten. * * Nach einem Fahrbefehl muss noch jemand abwarten, wo die Jalousie stehen * bleibt, und das aufschreiben - sonst steht in actor_states bis zum * naechsten Poll des Runners der alte Wert. Das dem Browser zu ueberlassen * ging schief: wer das Modal gleich nach dem Fahrbefehl zumachte, nahm den * einzigen Zuschauer mit. * * Dieselbe Bauart wie in ajax/tahoma.php: Content-Length setzen, ausliefern, * die Verbindung schliessen, weiterlaufen. Ohne FPM bleibt nur, die Puffer zu * leeren - dann hat der Browser seine Daten zwar, wartet aber trotzdem. */ function raumAntwortUndWeiter($data) { http_response_code(200); header("Content-Type: application/json; charset=utf-8"); $text = json_encode($data, JSON_UNESCAPED_UNICODE); header("Content-Length: " . strlen($text)); echo $text; // Der Aufrufer ist weg, das Skript soll trotzdem zu Ende kommen. ignore_user_abort(true); if (function_exists("fastcgi_finish_request")) { fastcgi_finish_request(); return; } while (ob_get_level() > 0) { ob_end_flush(); } flush(); } // --- 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") { $id = intval($body["command_id"] ?? 0); $text = executeCommand($id, is_array($body["params"] ?? null) ? $body["params"] : [], !empty($body["probe"])); // Bei einem Tahoma-Geraet ist mit dem Absenden noch nichts // festgehalten: die Box meldet die neue Stellung erst, wenn die // Fahrt zu Ende ist. Also Antwort raus und in Ruhe nachsehen. $url = empty($body["probe"]) ? tahomaUrlZuKommando($id) : null; if ($url === null) { raumAntwort(["ok" => $text]); } raumAntwortUndWeiter(["ok" => $text]); tahomaNachlesen($url); exit; } if (($_GET["action"] ?? "") === "temp") { // Derselbe Weg wie im alten Thermostat-Modal: /changeSetTemp. // Der Zweig steht am Raum (rooms.thermostat) und wird nicht mehr // aus Etage und Name zusammengesetzt - ein Thermostat in einem // anderen Haus meldet vielleicht unter einem ganz anderen. $raum = raumNachNummer($body["room"] ?? 0); if (!$raum || empty($raum["thermostat"])) { raumAntwort(["error" => "Dieser Raum hat kein Thermostat."], 400); } $temp = intval($body["temp"] ?? 0); if ($temp < 10 || $temp > 30) { raumAntwort(["error" => "Temperatur außerhalb 10–30 °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(rtrim($raum["thermostat"], "/") . "/changeSetTemp", strval($temp), 0, false); $mqtt->close(); raumAntwort(["ok" => "Solltemperatur " . $temp . " °C"]); } // Eine Jalousie waehrend ihrer Fahrt nachfuehren. actor_states wird // sonst nur vom Runner fortgeschrieben, und der fragt die Box alle // fuenf Minuten ab - so lange stand nach einem Fahrbefehl der alte // Wert im Modal. Hier wird genau ein Geraet gelesen, das kostet die // Box wenig, und der frische Stand landet gleich in der Tabelle. if (($_GET["action"] ?? "") === "jalousie") { $url = tahomaAktorUrl(intval($body["actor"] ?? 0)); if ($url === null) { raumAntwort(["error" => "Kein Tahoma-Gerät mit dieser Nummer."], 404); } $zustaende = tahomaZustaende($url); if ($zustaende === null) { raumAntwort(["error" => "Tahoma-Box antwortet nicht."], 503); } tahomaZustaendeSchreiben($url, $zustaende); $zahl = function ($feld) use ($zustaende) { return isset($zustaende[$feld]) && is_numeric($zustaende[$feld]) ? intval($zustaende[$feld]) : null; }; raumAntwort([ "faehrt" => ($zustaende["core:MovingState"] ?? null) === true, "position" => $zahl("core:ClosureState"), "neigung" => $zahl("core:SlateOrientationState"), ]); } raumAntwort(["error" => "Unbekannte Aktion."], 400); } catch (InvalidArgumentException $e) { raumAntwort(["error" => $e->getMessage()], 400); } catch (Throwable $e) { raumAntwort(["error" => $e->getMessage()], 500); } } // --- Inhalt des Modals ---------------------------------------------------- $raum = raumNachNummer($_GET["room"] ?? 0); if (!$raum) { echo "

Diesen Raum gibt es nicht (mehr).

"; exit; } // Derselbe Schluessel wie actors.room_id im Katalog - als Text, siehe raumListe(). $raumKey = strval($raum["nr"]); // Alles im Raum, sortiert nach Bedienform. Das Thermostat selbst wird nicht // als Karte gezeichnet: es wird zum Schieberegler im Heizungs-Reiter und // braucht daneben keine zweite Bedienung - "Solltemperatur" stand sonst // zweimal im Modal. // // Ob es einen Heizungs-Reiter gibt, entscheidet genau das: haengt in diesem // Raum ein Thermostat? Frueher stand hier "hat der Raum eine Kachel" - das // stimmte, solange nur Raeume mit Thermostat eine bekamen. Seit auch Kueche // und Technik eine haben, bekamen sie einen Heizungsregler fuer eine // Heizung, die es dort nicht gibt. $katalog = deviceCatalog(); $hatThermostat = false; $gruppen = []; foreach ($katalog as $d) { if ($d["room"] !== $raumKey) { continue; } $form = bedienform($d); if ($form === "heizung") { $hatThermostat = true; continue; } $gruppen[$form][] = $d; } // Die Heizung ist eine Gruppe wie die anderen, nur baut sie sich nicht aus // einer Geraeteliste: ein Regler fuer den Raum statt einer Karte je Geraet. $fertig = []; if ($hatThermostat) { $karte = "
" . "
" . "" . "–" . "
" . "" // Der Ist-Zustand kommt live ueber MQTT, nicht aus der Datenbank - // der Browser hoert ohnehin schon auf Raumtemp/#, und ein Wert von // vorhin waere neben dem Regler nur irrefuehrend. . "
" . "– °C" . "– %rF" . "heizt" . "
"; $fertig["heizung"] = $karte; } // Der Thermostat-Zweig reist mit: das Modal fuellt Ist-Werte und Heizsymbol // aus dem laufenden MQTT-Strom und muss wissen, wo es nachsehen soll. echo "
"; if (!$gruppen && !$fertig) { echo "

Diesem Raum ist noch nichts zugeordnet. " . "Das geht über das Haus-Symbol in der Karte „Automatismen“.

"; } echo zeichneGruppen($gruppen, $fertig); echo "
"; echo "
";