From 37962224c12787980e397ee26504d2dd19f96c5f Mon Sep 17 00:00:00 2001 From: Moirtz Wagner Date: Fri, 4 Sep 2026 13:37:25 +0200 Subject: [PATCH] Geraeteauswahl im Automatik-Editor gruppiert nach Etage Bisher stand ueber jedem Raum eine eigene Ueberschrift - bei neunzehn Raeumen aus rooms.php also neunzehn Stueck, jede mit der Etage davor: "OG - Bad", "EG - Bad", "UG - Bad". In einer Liste, die fuenfzehn Zeilen hoch ist, sah man dadurch mehr Ueberschrift als Geraet. Jetzt gruppiert nur die Etage, und der Raum steht klein und rechtsbuendig an jeder Zeile. Sortiert wird innerhalb der Etage nach Raum (Reihenfolge wie in rooms.php), sodass die Geraete eines Raums trotzdem beisammen stehen; aus zwanzig Ueberschriften werden fuenf. Ein Raum, den es in rooms.php nicht mehr gibt, bekommt eine eigene Ueberschrift, statt stillschweigend unter den Unzugeordneten zu verschwinden. Die Suche laeuft zusaetzlich ueber den Raum: "bad" findet damit auch den Handtuchtrockner, der im Bad haengt, ohne es im Namen zu tragen. In der Trefferliste steht keine Ueberschrift darueber, deshalb traegt dort jede Zeile die volle Angabe "OG - Bad". Dafuer nennt raumListe() Etage und Raum jetzt auch einzeln, und der Geraetekatalog reicht beides durch - der Schluessel "Etage/Raum" muss so nicht im JavaScript wieder auseinandergenommen werden. Co-Authored-By: Claude Opus 5 --- js/solar/autoActionFuncs.js | 174 +++++++++++++++++++++++------------- restricted/automations.php | 41 ++++++++- 2 files changed, 150 insertions(+), 65 deletions(-) diff --git a/js/solar/autoActionFuncs.js b/js/solar/autoActionFuncs.js index 1a2403a..50d279d 100644 --- a/js/solar/autoActionFuncs.js +++ b/js/solar/autoActionFuncs.js @@ -82,74 +82,115 @@ function option(e, gewaehlt) { } /** - * Geraeteliste gruppiert - nach Raum, wo zugeordnet, sonst nach Geraeteart. + * Geraeteliste fuer die Auswahl: eine Ueberschrift je Etage, darin nach Raum + * sortiert. * - * Beim Anlegen einer Automatik denkt man in Raeumen ("im Bad soll..."), nicht - * in Geraetearten. Der Raum steht aber nur da, wo er von Hand vergeben wurde - * (siehe Zuordnung ueber das Zahnrad in der Karte "Automatismen"); solange - * das niemand getan hat, bleibt die Gruppierung nach Art - besser als eine - * einzige Gruppe "Nicht zugeordnet". + * Beim Anlegen einer Automatik denkt man in Raeumen ("im Bad soll ..."), eine + * Ueberschrift je Raum waeren aber zwanzig Stueck - in einer Liste, die + * fuenfzehn Zeilen hoch ist, saehe man dann mehr Ueberschrift als Geraet, und + * "OG · Bad", "EG · Bad", "UG · Bad" schrieben die Etage immer wieder + * dazu. Deshalb gruppiert nur die Etage; der Raum steht klein an jeder Zeile, + * und weil danach sortiert wird, stehen die Geraete eines Raums trotzdem + * beisammen. + * + * Der Raum steht nur da, wo er von Hand vergeben wurde (Zuordnung ueber das + * Zahnrad in der Karte "Automatismen"). Solange das niemand getan hat, gaebe + * die Etage nichts her - dann bleibt es bei der Ordnung nach Geraeteart. */ function geraeteGruppen(geraete) { if (!geraete.some(d => d.room)) { - return gruppiere(geraete, d => d.type, null, "Einzelne", true); + return nachGeraeteart(geraete); } - 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"]); + const etagen = etagenReihenfolge(); + // "Ohne Raum" und "Unbekannter Raum" stehen in keiner Raumliste. Ihr + // indexOf() waere -1 und damit ganz vorne; sie gehoeren aber ans Ende. + const platz = etage => etagen.includes(etage) ? etagen.indexOf(etage) : etagen.length; + const nach = new Map(); + geraete.forEach(d => { + if (!nach.has(d.floorLabel)) nach.set(d.floorLabel, []); + nach.get(d.floorLabel).push(d); + }); + return Array.from(nach.keys()) + .sort((a, b) => platz(a) - platz(b) || a.localeCompare(b, "de")) + .map(etage => { + const liste = nach.get(etage); + liste.sort((a, b) => raumIndex(a.room) - raumIndex(b.room) + || a.type.localeCompare(b.type, "de") + || a.label.localeCompare(b.label, "de")); + return { titel: etage + " (" + liste.length + ")", geraete: liste }; + }); } /** - * 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. + * Reihenfolge der Etagen-Ueberschriften, wie rooms.php sie notiert - erst OG, + * dann EG, dann UG, dann Aussen. */ -function gruppiere(geraete, merkmal, reihenfolge, restTitel, einzelneSammeln) { +function etagenReihenfolge() { + const etagen = []; + autoRooms.forEach(r => { if (!etagen.includes(r.floor)) etagen.push(r.floor); }); + return etagen; +} + +/** + * Platz eines Raums in rooms.php - die Sortierung innerhalb einer Etage. + * + * Alles ohne Raum bekommt denselben letzten Platz; dort entscheidet dann die + * Geraeteart, sonst stuenden Jalousie, Steckdose und Thermostat wild + * durcheinander. + */ +function raumIndex(key) { + const i = autoRooms.findIndex(r => r.key === key); + return i < 0 ? autoRooms.length : i; +} + +/** + * Was klein hinter dem Geraetenamen steht. + * + * Unter einer Etagen-Ueberschrift genuegt der Raum. In der Trefferliste einer + * Suche steht keine Ueberschrift darueber, dort muss die Etage mit. Wo kein + * Raum vergeben ist, steht die Geraeteart - die ist immer da und sagt + * wenigstens, was fuer ein Ding das ist. + */ +function zeilenZusatz(d, mitEtage) { + if (!d.roomName) return d.type; + return mitEtage ? d.roomLabel : d.roomName; +} + +/** + * Worueber die Suche laeuft: Name, Raum und Geraeteart in einem Zug. + * + * Der Raum gehoert dazu, seit die Liste nach Etagen gruppiert - "bad" soll + * auch den Handtuchtrockner finden, der im Bad haengt, ohne es im Namen zu + * tragen. Ohne Zuordnung faellt der Raum weg; "Nicht zugeordnet" als + * Suchbegriff waere kein Fund, sondern ein Versehen. + */ +function suchfeld(d) { + return (d.label + " " + (d.roomName ? d.roomLabel : "") + " " + d.type).toLowerCase(); +} + +/** + * Nach Geraeteart gruppieren - der Rueckfall, solange kein Raum vergeben ist. + * + * Gruppen aus einem einzigen Geraet fallen dabei unter "Einzelne" zusammen: + * fuenfzehn Ueberschriften mit je einem Eintrag waeren unuebersichtlicher als + * die flache Liste darunter. + */ +function nachGeraeteart(geraete) { const nach = new Map(); geraete.forEach(d => { - const schluessel = merkmal(d) || restTitel; - 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"); + if (!nach.has(d.type)) nach.set(d.type, []); + nach.get(d.type).push(d); }); const gruppen = []; const rest = []; - titel.forEach(t => { - const liste = nach.get(t); - if (!einzelneSammeln || liste.length > 1 || t === restTitel) { - gruppen.push({ titel: t + " (" + liste.length + ")", geraete: liste }); - } else { - rest.push(liste[0]); - } + Array.from(nach.keys()).sort((a, b) => a.localeCompare(b, "de")).forEach(art => { + const liste = nach.get(art); + if (liste.length > 1) gruppen.push({ titel: art + " (" + liste.length + ")", geraete: liste }); + else rest.push(liste[0]); }); if (rest.length) { rest.sort((a, b) => a.label.localeCompare(b.label, "de")); - gruppen.push({ titel: restTitel + " (" + rest.length + ")", geraete: rest }); + gruppen.push({ titel: "Einzelne (" + rest.length + ")", geraete: rest }); } return gruppen; } @@ -158,9 +199,10 @@ function gruppiere(geraete, merkmal, reihenfolge, restTitel, einzelneSammeln) { * Geraeteauswahl mit Suchfeld. * * Bei ueber fuenfzig Geraeten hilft Tippen mehr als Blaettern - und es hilft - * quer zur Gruppierung: "bad" findet das Thermostat im Bad und die Jalousie - * "Bad Links", "jalousie" findet alle elf auf einmal. Gesucht wird deshalb - * ueber Beschriftung und Geraeteart zugleich. + * quer zur Gruppierung: "bad" findet das Thermostat im Bad, die Jalousie + * "Bad Links" und alles andere, was einem Bad zugeordnet ist, "jalousie" + * findet alle elf auf einmal. Gesucht wird deshalb ueber Beschriftung, Raum + * und Geraeteart zugleich. * * Statt einer Auswahlliste steht ein Knopf in der Zeile; die Liste klappt * darunter ueber die volle Breite auf. Auf dem Handy ist das mehr Platz als @@ -195,7 +237,7 @@ function oeffneWaehler(knopf, geraete, aktuelleId, beiAuswahl) { const suche = document.createElement("input"); suche.type = "search"; suche.className = "form-control form-control-sm mb-2"; - suche.placeholder = "Suchen – Name oder Geräteart"; + suche.placeholder = "Suchen – Name, Raum oder Geräteart"; const liste = document.createElement("div"); liste.className = "list-group list-group-flush"; liste.style.maxHeight = "15rem"; @@ -206,13 +248,18 @@ function oeffneWaehler(knopf, geraete, aktuelleId, beiAuswahl) { // input-group stuende die Liste sonst als weitere Spalte daneben. knopf.closest(".input-group").after(panel); - const eintrag = (d) => { + const eintrag = (d, mitEtage) => { const zeile = document.createElement("button"); zeile.type = "button"; + // Der Zusatz steht rechts am Rand statt gleich hinter dem Namen: so + // stehen die Raeume untereinander in einer Spalte und lassen sich + // ueberfliegen, statt bei jeder Zeile woanders anzufangen. zeile.className = "list-group-item list-group-item-action py-1" + + " d-flex justify-content-between align-items-center gap-2" + (d.id === aktuelleId ? " active" : ""); - zeile.innerHTML = "" + textSicher(d.label) + "" - + "" + textSicher(d.type) + ""; + zeile.innerHTML = "" + textSicher(d.label) + "" + + "" + + textSicher(zeilenZusatz(d, mitEtage)) + ""; zeile.onclick = () => { schliesseWaehler(); beiAuswahl(d); }; return zeile; }; @@ -220,17 +267,16 @@ function oeffneWaehler(knopf, geraete, aktuelleId, beiAuswahl) { const zeichne = () => { const suchtext = suche.value.trim().toLowerCase(); liste.innerHTML = ""; - const passend = geraete.filter(d => !suchtext - || d.label.toLowerCase().includes(suchtext) - || d.type.toLowerCase().includes(suchtext)); + const passend = geraete.filter(d => !suchtext || suchfeld(d).includes(suchtext)); if (!passend.length) { liste.innerHTML = "
Nichts gefunden.
"; return; } // Beim Suchen die Zwischenueberschriften weglassen: die Treffer stehen - // dann quer ueber alle Arten, und die Art steht ohnehin an jeder Zeile. + // dann quer ueber alle Etagen. Dafuer traegt jede Zeile die Etage mit - + // "Bad" allein sagt sonst nicht, welches der drei Baeder gemeint ist. if (suchtext) { - passend.forEach(d => liste.appendChild(eintrag(d))); + passend.forEach(d => liste.appendChild(eintrag(d, true))); return; } geraeteGruppen(passend).forEach(g => { @@ -238,7 +284,7 @@ function oeffneWaehler(knopf, geraete, aktuelleId, beiAuswahl) { kopf.className = "px-2 pt-2 pb-1 small fw-bold text-body-secondary"; kopf.textContent = g.titel; liste.appendChild(kopf); - g.geraete.forEach(d => liste.appendChild(eintrag(d))); + g.geraete.forEach(d => liste.appendChild(eintrag(d, false))); }); }; diff --git a/restricted/automations.php b/restricted/automations.php index e575797..b510224 100644 --- a/restricted/automations.php +++ b/restricted/automations.php @@ -225,6 +225,12 @@ function anzeigeName($name) * 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. + * + * Etage und Raum stehen zusaetzlich einzeln daneben: die Geraeteauswahl im + * Editor gruppiert nach der einen und sortiert nach dem anderen, und der + * Schluessel soll dafuer nicht erst im JavaScript wieder auseinandergenommen + * werden. Die Reihenfolge dieser Liste ist zugleich die Reihenfolge der + * Ueberschriften dort - erst OG, dann EG, dann UG, wie in rooms.php notiert. */ function raumListe() { @@ -233,9 +239,15 @@ function raumListe() $liste[] = [ "key" => $raum["floor"] . "/" . $raum["mqtt"], "label" => $raum["floor"] . " · " . $raum["mqtt"], + "floor" => $raum["floor"], + "room" => $raum["mqtt"], ]; } - $liste[] = ["key" => RAUM_AUSSEN, "label" => "Außen"]; + // Aussen zaehlt als eigene Etage mit genau einem Raum: Carport, Garage und + // Regensensor liegen auf keinem Stockwerk, sollen aber ihre eigene + // Ueberschrift bekommen und nicht unter "Ohne Raum" fallen. + $liste[] = ["key" => RAUM_AUSSEN, "label" => "Außen", + "floor" => "Außen", "room" => ""]; return $liste; } @@ -255,6 +267,28 @@ function raumBeschriftung($key) return $key . " (unbekannt)"; } +/** + * Etage und Raumname zu einem gespeicherten Raumschluessel. + * + * Die Geraeteauswahl im Editor braucht beides getrennt: die Etage wird zur + * Ueberschrift, der Raum zur Sortierung darunter. Geraete ohne Zuordnung + * bekommen ihre eigene Ueberschrift ganz am Ende; ein Raum, den es in + * rooms.php nicht mehr gibt, ebenso - er verschwindet nicht still zwischen + * den Unzugeordneten, sondern faellt auf. + */ +function raumEinordnung($key) +{ + if (!$key) { + return ["floor" => "Ohne Raum", "room" => ""]; + } + foreach (raumListe() as $raum) { + if ($raum["key"] === $key) { + return ["floor" => $raum["floor"], "room" => $raum["room"]]; + } + } + return ["floor" => "Unbekannter Raum", "room" => $key]; +} + /** * Raumvorschlaege aus den MQTT-Topics. * @@ -377,6 +411,7 @@ function deviceCatalog() return strcoll($labels[intval($a["id"])], $labels[intval($b["id"])]); }); foreach ($zeilen as $row) { + $wo = raumEinordnung($row["room"]); $devices[intval($row["id"])] = [ "id" => intval($row["id"]), "name" => $row["name"], @@ -385,6 +420,10 @@ function deviceCatalog() "url" => $row["url"], "room" => $row["room"], "roomLabel" => raumBeschriftung($row["room"]), + // Etage und Raum getrennt: die Auswahlliste im Editor gruppiert + // nach der Etage und sortiert darin nach dem Raum. + "floorLabel" => $wo["floor"], + "roomName" => $wo["room"], "states" => [], "commands" => [], ];