Files
Smart-Dashboard/ajax/room.php
T
adminandClaude Opus 5 62a8af3fc6 Raeume ueber ihre feste Nummer: actors.room_id, Raum-Modal, Thermostat-Zweig
Geraete hingen ueber den Text "Etage/Raum" an ihrem Raum, das Raum-Modal
zerlegte "EG_Bad" am Unterstrich, und das Thermostat-Topic wurde aus Etage
und Name zusammengesetzt. Ein umbenannter Raum haette still alle Geraete,
seinen Temperaturregler und sein Heizsymbol verloren.

- actors.room (Text) -> actors.room_id, Fremdschluessel auf rooms, beim
  Loeschen eines Raums SET NULL. Alle 59 Zuordnungen uebernommen.
- automations.floor: ENUM(UG,EG,OG,AG) -> Verweis auf floors.
  Eine neue Etage kann damit auch Automatiken tragen.
- room.php nimmt ?room=<Nummer>; Solltemperatur geht an
  rooms.thermostat + /changeSetTemp.
- homeMQTT.js: Heizsymbol, Regler und Ist-Werte ueber mqttZweig(thermostat)
  statt mqttData.Raumtemp[Etage][Name].
- Raumvorschlaege in den Einstellungen: Messwerte unter dem Thermostat-
  Zweig eines Raums statt fest Raumtemp/<Etage>/<Name>.
- Schluessel in raumListe(), Katalog und Maske ist die Raumnummer als Text.

Geprueft gegen den Stand davor: Startseite gleich bis auf das Klickziel,
jede der 25 Kacheln oeffnet ihren eigenen Raum; Geraeteliste, Vorschlaege,
Editor-Katalog und die eigenen Geraete je Kachel gleich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:58:30 +02:00

244 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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_id
* (Einstellungen -> 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: <Zweig>/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 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(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 "<p class='text-body-secondary mb-0'>Diesen Raum gibt es nicht (mehr).</p>";
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 = "<div class='card mb-2 raum-geraet'><div class='card-body py-2'>"
. "<div class='d-flex justify-content-between align-items-baseline'>"
. "<label class='form-label mb-0 small' for='modal-slider'>Solltemperatur</label>"
. "<span id='modal-slider-label' class='fw-semibold text-nowrap ms-2' style='color:#297195'></span>"
. "</div>"
. "<input type='range' id='modal-slider' class='form-range range-color-track' min='10' max='30'>"
// 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.
. "<div class='d-flex flex-wrap gap-3 small text-body-secondary'>"
. "<span><i class='bi bi-thermometer-half me-1'></i><span data-mqtt='Temp[degC]'></span> °C</span>"
. "<span><i class='bi bi-droplet me-1'></i><span data-mqtt='rHum[%]'></span> %rF</span>"
. "<span data-mqtt-heizung class='d-none'><i class='bi bi-fire text-danger me-1'></i>heizt</span>"
. "</div></div></div>";
$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 "<div class='p-1' id='roomView' data-room='" . $raum["nr"] . "'"
. " data-thermostat='" . htmlspecialchars((string)$raum["thermostat"], ENT_QUOTES) . "'>";
if (!$gruppen && !$fertig) {
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>";
}
echo zeichneGruppen($gruppen, $fertig);
echo "<div id='roomFeedback' class='small mt-2'></div>";
echo "</div>";