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:
@@ -9,9 +9,11 @@
|
|||||||
* GET ?action=editor&id=N Editor fuer eine vorhandene Automatik
|
* GET ?action=editor&id=N Editor fuer eine vorhandene Automatik
|
||||||
* GET ?action=editor&floor=OG Editor fuer eine neue Automatik
|
* GET ?action=editor&floor=OG Editor fuer eine neue Automatik
|
||||||
* GET ?action=list&floor=OG Tabelle fuer die Karte "Automatismen"
|
* 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=save JSON-Rumpf, legt an oder ueberschreibt
|
||||||
* POST ?action=delete {"id": N}
|
* POST ?action=delete {"id": N}
|
||||||
* POST ?action=toggle {"id": N} - pausieren / fortsetzen
|
* POST ?action=toggle {"id": N} - pausieren / fortsetzen
|
||||||
|
* POST ?action=saveRooms {"<actor_id>": "OG/Bad", ...}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once("../helper.php");
|
require_once("../helper.php");
|
||||||
@@ -49,6 +51,8 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
jsonAntwort(["ok" => true]);
|
jsonAntwort(["ok" => true]);
|
||||||
case "toggle":
|
case "toggle":
|
||||||
jsonAntwort(["enabled" => toggleAutomation(intval($body["id"] ?? 0))]);
|
jsonAntwort(["enabled" => toggleAutomation(intval($body["id"] ?? 0))]);
|
||||||
|
case "saveRooms":
|
||||||
|
jsonAntwort(["gespeichert" => saveRooms($body["rooms"] ?? [])]);
|
||||||
default:
|
default:
|
||||||
jsonAntwort(["error" => "Unbekannte Aktion."], 400);
|
jsonAntwort(["error" => "Unbekannte Aktion."], 400);
|
||||||
}
|
}
|
||||||
@@ -126,6 +130,73 @@ if ($action === "list") {
|
|||||||
exit;
|
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 --------------------------------------------------------------
|
// --- Editor --------------------------------------------------------------
|
||||||
$id = intval($_GET["id"] ?? 0);
|
$id = intval($_GET["id"] ?? 0);
|
||||||
$auto = $id > 0 ? loadAutomation($id) : null;
|
$auto = $id > 0 ? loadAutomation($id) : null;
|
||||||
@@ -141,6 +212,9 @@ $nutzdaten = json_encode([
|
|||||||
"automation" => $auto,
|
"automation" => $auto,
|
||||||
"devices" => deviceCatalog(),
|
"devices" => deviceCatalog(),
|
||||||
"lockouts" => lockoutChoices(),
|
"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);
|
], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ CREATE TABLE `actors` (
|
|||||||
`id` int(11) NOT NULL,
|
`id` int(11) NOT NULL,
|
||||||
`type` varchar(50) NOT NULL COMMENT 'Gerätetyp z.B. RollerShutter',
|
`type` varchar(50) NOT NULL COMMENT 'Gerätetyp z.B. RollerShutter',
|
||||||
`name` varchar(70) NOT NULL COMMENT 'Name des Geräts',
|
`name` varchar(70) NOT NULL COMMENT 'Name des Geräts',
|
||||||
`url` varchar(100) NOT NULL COMMENT 'Tahoma Device URL'
|
`url` varchar(100) NOT NULL COMMENT 'Tahoma Device URL',
|
||||||
|
`room` varchar(60) DEFAULT NULL COMMENT 'Raum aus restricted/rooms.php als "Etage/Raum" (z.B. OG/Bad) oder "aussen". Wird von Hand vergeben und deshalb vom Discovery nicht angefasst - der Upsert setzt nur type und name.'
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
||||||
--
|
--
|
||||||
|
|||||||
+113
-17
@@ -20,6 +20,7 @@
|
|||||||
let autoModel = null;
|
let autoModel = null;
|
||||||
let autoDevices = [];
|
let autoDevices = [];
|
||||||
let autoLockouts = [];
|
let autoLockouts = [];
|
||||||
|
let autoRooms = [];
|
||||||
|
|
||||||
// --- Katalog-Helfer ------------------------------------------------------
|
// --- Katalog-Helfer ------------------------------------------------------
|
||||||
|
|
||||||
@@ -81,30 +82,74 @@ function option(e, gewaehlt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Geraeteliste nach Geraeteart gruppiert.
|
* Geraeteliste gruppiert - nach Raum, wo zugeordnet, sonst nach Geraeteart.
|
||||||
*
|
*
|
||||||
* Bei ueber fuenfzig Geraeten ist die flache Liste der Engpass: dreizehn
|
* Beim Anlegen einer Automatik denkt man in Raeumen ("im Bad soll..."), nicht
|
||||||
* Thermostate und elf Jalousien stehen sonst als vierundzwanzig gleich
|
* in Geraetearten. Der Raum steht aber nur da, wo er von Hand vergeben wurde
|
||||||
* aussehende Zeilen untereinander. Arten mit nur einem Geraet bekommen
|
* (siehe Zuordnung ueber das Zahnrad in der Karte "Automatismen"); solange
|
||||||
* keine eigene Ueberschrift - fuenfzehn Gruppen mit je einem Eintrag waeren
|
* das niemand getan hat, bleibt die Gruppierung nach Art - besser als eine
|
||||||
* unuebersichtlicher als die flache Liste. Sie sammeln sich am Ende.
|
* einzige Gruppe "Nicht zugeordnet".
|
||||||
*/
|
*/
|
||||||
function geraeteGruppen(geraete) {
|
function geraeteGruppen(geraete) {
|
||||||
const nachArt = new Map();
|
if (!geraete.some(d => d.room)) {
|
||||||
|
return gruppiere(geraete, d => d.type, null, "Einzelne", true);
|
||||||
|
}
|
||||||
|
const gruppen = gruppiere(geraete, d => d.roomLabel, raumReihenfolge(),
|
||||||
|
"Nicht zugeordnet", false);
|
||||||
|
// Was noch keinen Raum hat, bleibt sonst ein einziger Eimer - solange erst
|
||||||
|
// ein Teil zugeordnet ist, waeren das die meisten Geraete. Innerhalb des
|
||||||
|
// Eimers gilt deshalb weiter die Ordnung nach Geraeteart.
|
||||||
|
const letzte = gruppen[gruppen.length - 1];
|
||||||
|
if (!letzte || !letzte.titel.startsWith("Nicht zugeordnet")) return gruppen;
|
||||||
|
gruppen.pop();
|
||||||
|
return gruppen.concat(
|
||||||
|
gruppiere(letzte.geraete, d => d.type, null, "Einzelne", true)
|
||||||
|
.map(g => ({ titel: "Ohne Raum · " + g.titel, geraete: g.geraete })));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reihenfolge der Raum-Ueberschriften, wie in rooms.php notiert. */
|
||||||
|
function raumReihenfolge() {
|
||||||
|
return autoRooms.map(r => r.label).concat(["Nicht zugeordnet"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nach einem Merkmal gruppieren.
|
||||||
|
*
|
||||||
|
* `einzelneSammeln` entscheidet, was mit Gruppen aus einem einzigen Geraet
|
||||||
|
* passiert. Bei Gerätearten sollen sie zusammenfallen - fuenfzehn
|
||||||
|
* Ueberschriften mit je einem Eintrag waeren unuebersichtlicher als die
|
||||||
|
* flache Liste. Bei Raeumen nicht: "OG · Bad" ist auch mit einem einzigen
|
||||||
|
* Geraet die Ueberschrift, die man sucht, und in der Wohnung haengt oft nur
|
||||||
|
* ein Thermostat je Raum.
|
||||||
|
*/
|
||||||
|
function gruppiere(geraete, merkmal, reihenfolge, restTitel, einzelneSammeln) {
|
||||||
|
const nach = new Map();
|
||||||
geraete.forEach(d => {
|
geraete.forEach(d => {
|
||||||
if (!nachArt.has(d.type)) nachArt.set(d.type, []);
|
const schluessel = merkmal(d) || restTitel;
|
||||||
nachArt.get(d.type).push(d);
|
if (!nach.has(schluessel)) nach.set(schluessel, []);
|
||||||
|
nach.get(schluessel).push(d);
|
||||||
|
});
|
||||||
|
const titel = Array.from(nach.keys());
|
||||||
|
titel.sort((a, b) => {
|
||||||
|
if (reihenfolge) {
|
||||||
|
const ia = reihenfolge.indexOf(a), ib = reihenfolge.indexOf(b);
|
||||||
|
if (ia !== ib) return (ia < 0 ? 999 : ia) - (ib < 0 ? 999 : ib);
|
||||||
|
}
|
||||||
|
return a.localeCompare(b, "de");
|
||||||
});
|
});
|
||||||
const gruppen = [];
|
const gruppen = [];
|
||||||
const einzelne = [];
|
const rest = [];
|
||||||
Array.from(nachArt.keys()).sort((a, b) => a.localeCompare(b, "de")).forEach(art => {
|
titel.forEach(t => {
|
||||||
const liste = nachArt.get(art);
|
const liste = nach.get(t);
|
||||||
if (liste.length > 1) gruppen.push({ titel: art + " (" + liste.length + ")", geraete: liste });
|
if (!einzelneSammeln || liste.length > 1 || t === restTitel) {
|
||||||
else einzelne.push(liste[0]);
|
gruppen.push({ titel: t + " (" + liste.length + ")", geraete: liste });
|
||||||
|
} else {
|
||||||
|
rest.push(liste[0]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (einzelne.length) {
|
if (rest.length) {
|
||||||
einzelne.sort((a, b) => a.label.localeCompare(b.label, "de"));
|
rest.sort((a, b) => a.label.localeCompare(b.label, "de"));
|
||||||
gruppen.push({ titel: "Einzelne", geraete: einzelne });
|
gruppen.push({ titel: restTitel + " (" + rest.length + ")", geraete: rest });
|
||||||
}
|
}
|
||||||
return gruppen;
|
return gruppen;
|
||||||
}
|
}
|
||||||
@@ -545,6 +590,7 @@ function loadAutomatic() {
|
|||||||
const daten = JSON.parse(document.getElementById("autoActionData").textContent);
|
const daten = JSON.parse(document.getElementById("autoActionData").textContent);
|
||||||
autoDevices = daten.devices;
|
autoDevices = daten.devices;
|
||||||
autoLockouts = daten.lockouts;
|
autoLockouts = daten.lockouts;
|
||||||
|
autoRooms = daten.rooms || [];
|
||||||
|
|
||||||
const a = daten.automation;
|
const a = daten.automation;
|
||||||
autoModel = {
|
autoModel = {
|
||||||
@@ -687,6 +733,56 @@ function refreshAutomations(floor) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zuordnung Geraet -> Raum. Eigenes Modal, weil es kein Teil einer einzelnen
|
||||||
|
* Automatik ist, sondern einmal fuer alle gilt.
|
||||||
|
*/
|
||||||
|
function openRoomModal() {
|
||||||
|
document.getElementById("modal-title").innerHTML = "Geräte den Räumen zuordnen";
|
||||||
|
const alt = document.getElementById("modalSaveBtn");
|
||||||
|
const btn = alt.cloneNode(true);
|
||||||
|
alt.replaceWith(btn);
|
||||||
|
btn.addEventListener("click", speichereRaeume);
|
||||||
|
|
||||||
|
const dialog = document.querySelector("#modalEV .modal-dialog");
|
||||||
|
dialog.classList.add("modal-lg");
|
||||||
|
document.getElementById("modalEV").addEventListener("hidden.bs.modal",
|
||||||
|
() => dialog.classList.remove("modal-lg"), { once: true });
|
||||||
|
|
||||||
|
const koerper = document.getElementById("modal-body");
|
||||||
|
koerper.innerHTML = loadingHTML("Wird geladen...");
|
||||||
|
modalEV.show();
|
||||||
|
|
||||||
|
fetch("./ajax/AutoAction.php?action=rooms", {
|
||||||
|
method: "GET", headers: { "Requested-With-Ajax": "ajax" }
|
||||||
|
})
|
||||||
|
.then(a => a.text())
|
||||||
|
.then(html => {
|
||||||
|
koerper.innerHTML = html;
|
||||||
|
const tipps = document.getElementById("btnRoomTipps");
|
||||||
|
if (tipps) {
|
||||||
|
tipps.addEventListener("click", () => {
|
||||||
|
koerper.querySelectorAll("select[data-tipp]").forEach(s => {
|
||||||
|
if (!s.value) s.value = s.dataset.tipp;
|
||||||
|
});
|
||||||
|
tipps.disabled = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(fehler => { koerper.innerHTML = "Konnte die Zuordnung nicht laden: " + fehler.message; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function speichereRaeume() {
|
||||||
|
const rooms = {};
|
||||||
|
document.querySelectorAll("#modal-body select[data-actor]").forEach(s => {
|
||||||
|
rooms[s.dataset.actor] = s.value;
|
||||||
|
});
|
||||||
|
automationRequest("saveRooms", { rooms: rooms }).then(antwort => {
|
||||||
|
if (antwort.error) alert(antwort.error);
|
||||||
|
else modalEV.hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function toggleAutomation(id) {
|
function toggleAutomation(id) {
|
||||||
automationRequest("toggle", { id: id }).then(antwort => {
|
automationRequest("toggle", { id: id }).then(antwort => {
|
||||||
if (antwort.error) alert(antwort.error);
|
if (antwort.error) alert(antwort.error);
|
||||||
|
|||||||
+111
-1
@@ -15,6 +15,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
require_once(__DIR__ . "/mysql.php");
|
require_once(__DIR__ . "/mysql.php");
|
||||||
|
require_once(__DIR__ . "/rooms.php");
|
||||||
|
|
||||||
|
// Geraete, die zu keinem Raum der Wohnung gehoeren - Carport, Garage,
|
||||||
|
// Regensensor. Kein Raum in rooms.php, aber auch nicht "unzugeordnet".
|
||||||
|
const RAUM_AUSSEN = "aussen";
|
||||||
|
|
||||||
/** Verbindung zur Geraete- und Automatik-Datenbank. */
|
/** Verbindung zur Geraete- und Automatik-Datenbank. */
|
||||||
function meshDb()
|
function meshDb()
|
||||||
@@ -210,6 +215,109 @@ function anzeigeName($name)
|
|||||||
return trim(preg_replace('/\s+/', ' ', $name));
|
return trim(preg_replace('/\s+/', ' ', $name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waehlbare Raeume: die Raeume der Home-Ansicht plus "Aussen".
|
||||||
|
*
|
||||||
|
* Einzige Quelle bleibt restricted/rooms.php - ein neuer Raum ist dort eine
|
||||||
|
* Zeile und taucht hier automatisch auf. Der Schluessel ist "Etage/Raum",
|
||||||
|
* weil "Bad" allein auf drei Etagen vorkommt.
|
||||||
|
*/
|
||||||
|
function raumListe()
|
||||||
|
{
|
||||||
|
$liste = [];
|
||||||
|
foreach ($GLOBALS["rooms"] as $raum) {
|
||||||
|
$liste[] = [
|
||||||
|
"key" => $raum["floor"] . "/" . $raum["mqtt"],
|
||||||
|
"label" => $raum["floor"] . " · " . $raum["mqtt"],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$liste[] = ["key" => RAUM_AUSSEN, "label" => "Außen"];
|
||||||
|
return $liste;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Anzeigename eines gespeicherten Raumschluessels. */
|
||||||
|
function raumBeschriftung($key)
|
||||||
|
{
|
||||||
|
if (!$key) {
|
||||||
|
return "Nicht zugeordnet";
|
||||||
|
}
|
||||||
|
foreach (raumListe() as $raum) {
|
||||||
|
if ($raum["key"] === $key) {
|
||||||
|
return $raum["label"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Raum aus rooms.php entfernt - der Eintrag am Geraet bleibt stehen,
|
||||||
|
// damit eine Umbenennung dort keine Zuordnung stillschweigend loescht.
|
||||||
|
return $key . " (unbekannt)";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raumvorschlaege aus den MQTT-Topics.
|
||||||
|
*
|
||||||
|
* Die Thermostate tragen ihren Raum im Topic: "Raumtemp/OG/Bad/Temp[degC]".
|
||||||
|
* Das ist keine Rateaufgabe, sondern dieselbe Schreibweise wie in rooms.php,
|
||||||
|
* und deckt dreizehn Geraete ohne Zutun ab. Bei allen anderen wird bewusst
|
||||||
|
* nicht geraten: "Bad Links" sagt nichts darueber, auf welcher der drei
|
||||||
|
* Etagen dieses Bad liegt.
|
||||||
|
*/
|
||||||
|
function raumVorschlaege()
|
||||||
|
{
|
||||||
|
$bekannt = [];
|
||||||
|
foreach (raumListe() as $raum) {
|
||||||
|
$bekannt[strtolower($raum["key"])] = $raum["key"];
|
||||||
|
}
|
||||||
|
$res = meshDb()->query("SELECT actor_id, url FROM actor_states
|
||||||
|
WHERE url LIKE 'Raumtemp/%/%/%'");
|
||||||
|
$vorschlag = [];
|
||||||
|
while ($row = $res->fetch_assoc()) {
|
||||||
|
$teile = explode("/", $row["url"]);
|
||||||
|
if (count($teile) < 3) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$key = strtolower($teile[1] . "/" . $teile[2]);
|
||||||
|
if (isset($bekannt[$key])) {
|
||||||
|
$vorschlag[intval($row["actor_id"])] = $bekannt[$key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $vorschlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Raum je Geraet, wie gespeichert. */
|
||||||
|
function geraeteRaeume()
|
||||||
|
{
|
||||||
|
$res = meshDb()->query("SELECT id, room FROM actors");
|
||||||
|
$raeume = [];
|
||||||
|
while ($row = $res->fetch_assoc()) {
|
||||||
|
$raeume[intval($row["id"])] = $row["room"];
|
||||||
|
}
|
||||||
|
return $raeume;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Zuordnungen speichern. $zuordnung ist [actor_id => Raumschluessel|""]. */
|
||||||
|
function saveRooms($zuordnung)
|
||||||
|
{
|
||||||
|
$db = meshDb();
|
||||||
|
$erlaubt = array_column(raumListe(), "key");
|
||||||
|
$stmt = $db->prepare("UPDATE actors SET room = ? WHERE id = ?");
|
||||||
|
$anzahl = 0;
|
||||||
|
foreach ($zuordnung as $actorId => $key) {
|
||||||
|
$actorId = intval($actorId);
|
||||||
|
$key = strval($key);
|
||||||
|
if ($actorId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($key !== "" && !in_array($key, $erlaubt, true)) {
|
||||||
|
throw new InvalidArgumentException("Unbekannter Raum: " . $key);
|
||||||
|
}
|
||||||
|
$wert = $key === "" ? null : $key;
|
||||||
|
$stmt->bind_param("si", $wert, $actorId);
|
||||||
|
$stmt->execute();
|
||||||
|
$anzahl++;
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
return $anzahl;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* id => Beschriftung fuer alle Geraete, geputzt und eindeutig.
|
* id => Beschriftung fuer alle Geraete, geputzt und eindeutig.
|
||||||
*
|
*
|
||||||
@@ -256,7 +364,7 @@ function deviceCatalog()
|
|||||||
$devices = [];
|
$devices = [];
|
||||||
|
|
||||||
$labels = geraeteBeschriftungen();
|
$labels = geraeteBeschriftungen();
|
||||||
$res = $db->query("SELECT id, name, type, url FROM actors");
|
$res = $db->query("SELECT id, name, type, url, room FROM actors");
|
||||||
$zeilen = [];
|
$zeilen = [];
|
||||||
while ($row = $res->fetch_assoc()) {
|
while ($row = $res->fetch_assoc()) {
|
||||||
$zeilen[] = $row;
|
$zeilen[] = $row;
|
||||||
@@ -271,6 +379,8 @@ function deviceCatalog()
|
|||||||
"label" => $labels[intval($row["id"])],
|
"label" => $labels[intval($row["id"])],
|
||||||
"type" => $row["type"],
|
"type" => $row["type"],
|
||||||
"url" => $row["url"],
|
"url" => $row["url"],
|
||||||
|
"room" => $row["room"],
|
||||||
|
"roomLabel" => raumBeschriftung($row["room"]),
|
||||||
"states" => [],
|
"states" => [],
|
||||||
"commands" => [],
|
"commands" => [],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -133,6 +133,12 @@
|
|||||||
<div class="card-header pb-0 pt-1">
|
<div class="card-header pb-0 pt-1">
|
||||||
<h3 class="card-title">Automatismen</h3>
|
<h3 class="card-title">Automatismen</h3>
|
||||||
<div class="card-tools">
|
<div class="card-tools">
|
||||||
|
<!-- Zuordnung Geraet -> Raum: sie ordnet die Geraeteliste im
|
||||||
|
Editor nach Raeumen statt nach Geraeteart. -->
|
||||||
|
<button type="button" class="btn btn-tool" title="Geräte den Räumen zuordnen"
|
||||||
|
onclick="openRoomModal()">
|
||||||
|
<i class="bi bi-house-gear"></i>
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-tool" data-lte-toggle="card-remove">
|
<button type="button" class="btn btn-tool" data-lte-toggle="card-remove">
|
||||||
<i class="bi bi-x-lg"></i>
|
<i class="bi bi-x-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user