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 * POST ?action=jalousie {"actor": 5} - Stand einer Jalousie * POST ?action=my {"actor": 5} - aktuellen Stand als * my-Position speichern * * 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"), "myPosition" => $zahl("core:Memorized1PositionState"), "myNeigung" => $zahl("core:Memorized1OrientationState"), ]); } // Den Stand, auf dem die Jalousie gerade steht, als my-Position in den // Motor schreiben. Die Box kennt dafuer setMemorized1Position und bei // Raffstores zusaetzlich setMemorized1Orientation; die Geraetesuche // uebernimmt beide nicht als Kommando, deshalb geht es hier direkt. // Gespeichert wird, was die Box meldet - nicht, was im Browser steht: // der koennte einer Fahrt hinterherhinken. if (($_GET["action"] ?? "") === "my") { $url = tahomaAktorUrl(intval($body["actor"] ?? 0)); if ($url === null) { raumAntwort(["error" => "Kein Tahoma-Gerät mit dieser Nummer."], 404); } $vorher = tahomaZustaende($url); if ($vorher === null) { raumAntwort(["error" => "Tahoma-Box antwortet nicht."], 503); } if (($vorher["core:MovingState"] ?? null) === true) { raumAntwort(["error" => "Die Jalousie fährt noch – bitte warten, bis sie steht."], 409); } if (!isset($vorher["core:Memorized1PositionState"]) || !is_numeric($vorher["core:ClosureState"] ?? null)) { raumAntwort(["error" => "Dieses Gerät kann keine my-Position speichern."], 400); } $position = intval($vorher["core:ClosureState"]); $neigung = null; tahomaKommando($url, "setMemorized1Position", [$position]); $text = "my-Position " . $position . " %"; if (isset($vorher["core:Memorized1OrientationState"]) && is_numeric($vorher["core:SlateOrientationState"] ?? null)) { $neigung = intval($vorher["core:SlateOrientationState"]); tahomaKommando($url, "setMemorized1Orientation", [$neigung]); $text .= ", Lamellen " . $neigung . " %"; } // Die Box braucht einen Moment, bis sie den neuen Wert meldet. sleep(2); $nachher = tahomaZustaende($url); if ($nachher !== null) { tahomaZustaendeSchreiben($url, $nachher); } raumAntwort([ "ok" => $text . " gespeichert", "myPosition" => $position, "myNeigung" => $neigung, ]); } 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) { // Ring mit Ist-Temperatur und ziehbarem Soll-Punkt, daneben - und + und // die Schnellwahl. Alle Zahlen kommen live ueber MQTT (der Browser hoert // ohnehin auf den Thermostat-Zweig) und werden von js/solar/raumfenster.js // eingetragen. Das Thermostat nimmt ganze Grad von 10 bis 30 °C, siehe // action=temp. $fertig["heizung"] = "
" . "
" . "" . "" . "" . "" . "" . "" . "" . "
im RaumSoll –
" . "

Solltemperatur

" . "
" . "" . "
" . "
" . "" . "" . "" . "
" . "" . "
" . "
" . "
"; } // 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 "
Änderungen gelten sofort
"; echo "
";