Geraete den Raeumen zuordnen

Beim Anlegen einer Automatik denkt man in Raeumen ("im Bad soll ..."), nicht
in Geraetearten. Ein Raummodell gibt es schon - restricted/rooms.php, die
einzige Quelle fuer die Home-Ansicht -, nur fehlte die Zuordnung Geraet ->
Raum.

Die neue Spalte actors.room haelt sie als "Etage/Raum" (OG/Bad), dazu
"aussen" fuer Carport, Garage und Regensensor. Etage und Raum zusammen,
weil "Bad" allein auf drei Etagen vorkommt. Discovery fasst die Spalte nicht
an - der Upsert setzt nur type und name -, die Zuordnung ueberlebt also
jeden Suchlauf.

Zugeordnet wird ueber das Haus-Symbol in der Karte "Automatismen". Wo der
Raum aus dem MQTT-Topic hervorgeht ("Raumtemp/OG/Bad/Temp[degC]"), schlaegt
die Ansicht ihn vor und uebernimmt ihn auf Knopfdruck - das sind die
dreizehn Thermostate, ohne jedes Raten. Bei allen anderen wird bewusst nicht
geraten: "Bad Links" sagt nichts darueber, auf welcher der drei Etagen
dieses Bad liegt. Noch nicht zugeordnete Geraete stehen in der Tabelle oben.

Die Geraeteauswahl im Editor gruppiert danach nach Raum, in der Reihenfolge
aus rooms.php. Solange niemand zugeordnet hat, bleibt es bei der Gruppierung
nach Geraeteart - eine einzige Gruppe "Nicht zugeordnet" waere ein
Rueckschritt. Und weil auch nach der ersten Zuordnung noch viel ohne Raum
ist, wird dieser Rest weiter nach Geraeteart unterteilt ("Ohne Raum ·
Aussenjalousie (11)").

Anders als bei den Gerätearten behalten Raeume mit nur einem Geraet ihre
Ueberschrift: "OG · Bad" ist auch mit einem einzigen Thermostat genau die
Zeile, die man sucht - und mehr als eines haengt selten in einem Raum.

Die dreizehn Thermostate sind bereits zugeordnet, weil das ihre eigenen
Topics hergeben; alles andere steht auf "-" und will von Hand vergeben
werden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-31 16:16:48 +02:00
co-authored by Claude Opus 5
parent 2ac504ca29
commit 4b47372015
5 changed files with 306 additions and 19 deletions
+74
View File
@@ -9,9 +9,11 @@
* GET ?action=editor&id=N Editor fuer eine vorhandene Automatik
* GET ?action=editor&floor=OG Editor fuer eine neue Automatik
* GET ?action=list&floor=OG Tabelle fuer die Karte "Automatismen"
* GET ?action=rooms Zuordnung Geraet -> Raum zum Bearbeiten
* POST ?action=save JSON-Rumpf, legt an oder ueberschreibt
* POST ?action=delete {"id": N}
* POST ?action=toggle {"id": N} - pausieren / fortsetzen
* POST ?action=saveRooms {"<actor_id>": "OG/Bad", ...}
*/
require_once("../helper.php");
@@ -49,6 +51,8 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
jsonAntwort(["ok" => true]);
case "toggle":
jsonAntwort(["enabled" => toggleAutomation(intval($body["id"] ?? 0))]);
case "saveRooms":
jsonAntwort(["gespeichert" => saveRooms($body["rooms"] ?? [])]);
default:
jsonAntwort(["error" => "Unbekannte Aktion."], 400);
}
@@ -126,6 +130,73 @@ if ($action === "list") {
exit;
}
// --- Geraete den Raeumen zuordnen ---------------------------------------
if ($action === "rooms") {
$labels = geraeteBeschriftungen();
$raeume = geraeteRaeume();
$vorschlag = raumVorschlaege();
$auswahl = raumListe();
$res = meshDb()->query("SELECT id, type FROM actors");
$geraete = [];
while ($row = $res->fetch_assoc()) {
$id = intval($row["id"]);
$geraete[] = [
"id" => $id,
"label" => $labels[$id] ?? "?",
"type" => $row["type"],
"room" => $raeume[$id] ?? null,
"tipp" => $vorschlag[$id] ?? null,
];
}
// Unzugeordnete nach oben: das ist die Arbeit, die noch zu tun ist.
usort($geraete, function ($a, $b) {
$offen = (int)!empty($b["room"]) - (int)!empty($a["room"]);
return $offen !== 0 ? $offen : strcoll($a["label"], $b["label"]);
});
$offen = count(array_filter($geraete, function ($g) { return empty($g["room"]); }));
$tipps = count(array_filter($geraete, function ($g) {
return empty($g["room"]) && !empty($g["tipp"]);
}));
echo "<div class='p-2'>";
echo "<p class='small text-body-secondary'>Die Zuordnung ordnet die Geräteliste im Editor "
. "nach Räumen statt nach Geräteart. Sie wird von Hand vergeben und bleibt bei "
. "einem neuen Geräte-Suchlauf erhalten.</p>";
if ($tipps) {
echo "<div class='alert alert-secondary py-2 small d-flex justify-content-between align-items-center'>"
. "<span>" . $tipps . " Gerät(e) verraten ihren Raum im MQTT-Topic.</span>"
. "<button type='button' class='btn btn-outline-secondary btn-sm' id='btnRoomTipps'>"
. "Vorschläge übernehmen</button></div>";
}
echo "<div class='table-responsive' style='max-height:60vh;overflow-y:auto'>";
echo "<table class='table table-sm table-hover align-middle'><thead><tr>"
. "<th>Gerät</th><th class='d-none d-sm-table-cell'>Art</th><th>Raum</th>"
. "</tr></thead><tbody>";
foreach ($geraete as $g) {
echo "<tr" . (empty($g["room"]) ? " class='table-active'" : "") . ">";
echo "<td>" . htmlspecialchars($g["label"]) . "</td>";
echo "<td class='d-none d-sm-table-cell small text-body-secondary'>"
. htmlspecialchars($g["type"]) . "</td>";
echo "<td><select class='form-select form-select-sm' data-actor='" . $g["id"] . "'"
. ($g["tipp"] ? " data-tipp='" . htmlspecialchars($g["tipp"], ENT_QUOTES) . "'" : "")
. ">";
echo "<option value=''" . (empty($g["room"]) ? " selected" : "") . "></option>";
foreach ($auswahl as $raum) {
echo "<option value='" . htmlspecialchars($raum["key"], ENT_QUOTES) . "'"
. ($g["room"] === $raum["key"] ? " selected" : "") . ">"
. htmlspecialchars($raum["label"]) . "</option>";
}
echo "</select></td></tr>";
}
echo "</tbody></table></div>";
echo "<p class='small text-body-secondary mb-0'>" . $offen . " von " . count($geraete)
. " Geräten noch ohne Raum.</p>";
echo "</div>";
exit;
}
// --- Editor --------------------------------------------------------------
$id = intval($_GET["id"] ?? 0);
$auto = $id > 0 ? loadAutomation($id) : null;
@@ -141,6 +212,9 @@ $nutzdaten = json_encode([
"automation" => $auto,
"devices" => deviceCatalog(),
"lockouts" => lockoutChoices(),
// Nur fuer die Reihenfolge der Raum-Gruppen: rooms.php gibt sie vor, und
// Etage fuer Etage liest sich besser als alphabetisch.
"rooms" => raumListe(),
], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
?>