Geraeteauswahl: nach Art gruppiert, Namen geputzt

Nach dem Discovery-Lauf stehen 51 Geraete in der Ausloeser-Liste und 40 in
der Aktions-Liste, flach und alphabetisch. Dreizehn Thermostate und elf
Jalousien sind darin vierundzwanzig gleich aussehende Zeilen.

Die Liste ist jetzt nach Geraeteart gruppiert (optgroup, das koennen auch
die Handy-Browser nativ). Arten mit nur einem Geraet bekommen keine eigene
Ueberschrift - fuenfzehn Gruppen mit je einem Eintrag waeren
unuebersichtlicher als die flache Liste -, sie sammeln sich am Ende unter
"Einzelne".

Dazu ein Namensputz fuer die Anzeige, ohne die Datenbank anzufassen: das
u_-Praefix aus den Tahoma-Beschriftungen faellt weg, ebenso eine
angehaengte einstellige Kanalnummer, und Unterstriche werden zu Leerzeichen.
Aus "u_Wozi Terrassentuer" wird "Wozi Terrassentuer", aus "Power_EG_EM_0"
wird "Power EG EM". Bewusst nur die einstellige Endung - "go-eCharger_270003"
traegt seine Seriennummer und behaelt sie.

Beschriftungen, die danach doppelt waeren, bekommen die Geraeteart dazu: die
Tahoma-Box nennt sowohl den Rollladen als auch den Sonnenenergie-Sensor
"Terasse", in der Liste untereinander waren sie nicht zu unterscheiden.
Daraus wird "Terasse (Rollladen)" und "Terasse (Licht)".

Die Beschriftung entsteht an einer Stelle und wird von Editor und
Uebersicht geteilt - sonst stuende in der Auswahlliste "Wozi Terrassentuer"
und im Satz darunter weiter "u_Wozi Terrassentuer".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-31 16:02:49 +02:00
co-authored by Claude Opus 5
parent 9f8c9016c2
commit 00f5944a7a
2 changed files with 127 additions and 20 deletions
+53 -9
View File
@@ -63,12 +63,60 @@ function ersterBefehl() {
function optionListe(select, eintraege, gewaehlt) { function optionListe(select, eintraege, gewaehlt) {
select.innerHTML = ""; select.innerHTML = "";
eintraege.forEach(e => { eintraege.forEach(e => select.appendChild(option(e, gewaehlt)));
}
function option(e, gewaehlt) {
const opt = document.createElement("option"); const opt = document.createElement("option");
opt.value = e.value; opt.value = e.value;
opt.text = e.text; opt.text = e.text;
if (String(e.value) === String(gewaehlt)) opt.selected = true; if (String(e.value) === String(gewaehlt)) opt.selected = true;
select.appendChild(opt); return opt;
}
/**
* Geraeteliste nach Geraeteart gruppiert.
*
* Bei ueber fuenfzig Geraeten ist die flache Liste der Engpass: dreizehn
* Thermostate und elf Jalousien stehen sonst als vierundzwanzig gleich
* aussehende Zeilen untereinander. Arten mit nur einem Geraet bekommen
* keine eigene Ueberschrift - fuenfzehn Gruppen mit je einem Eintrag waeren
* unuebersichtlicher als die flache Liste. Sie sammeln sich am Ende.
*/
function geraeteGruppen(geraete) {
const nachArt = new Map();
geraete.forEach(d => {
if (!nachArt.has(d.type)) nachArt.set(d.type, []);
nachArt.get(d.type).push(d);
});
const gruppen = [];
const einzelne = [];
Array.from(nachArt.keys()).sort((a, b) => a.localeCompare(b, "de")).forEach(art => {
const liste = nachArt.get(art);
if (liste.length > 1) gruppen.push({ titel: art + " (" + liste.length + ")", geraete: liste });
else einzelne.push(liste[0]);
});
if (einzelne.length) {
einzelne.sort((a, b) => a.label.localeCompare(b.label, "de"));
gruppen.push({ titel: "Einzelne", geraete: einzelne });
}
return gruppen;
}
/** Auswahlliste mit Zwischenueberschriften je Geraeteart fuellen. */
function geraeteListe(select, geraete, gewaehlt) {
select.innerHTML = "";
const gruppen = geraeteGruppen(geraete);
// Nur eine Gruppe? Dann waere die Ueberschrift blosse Zierde.
if (gruppen.length < 2) {
geraete.forEach(d => select.appendChild(option({ value: d.id, text: d.label }, gewaehlt)));
return;
}
gruppen.forEach(g => {
const kopf = document.createElement("optgroup");
kopf.label = g.titel;
g.geraete.forEach(d => kopf.appendChild(option({ value: d.id, text: d.label }, gewaehlt)));
select.appendChild(kopf);
}); });
} }
@@ -176,9 +224,7 @@ function conditionRow(gi, bi, bed) {
// Geraet // Geraet
const geraet = document.createElement("select"); const geraet = document.createElement("select");
geraet.className = "form-select"; geraet.className = "form-select";
optionListe(geraet, autoDevices.filter(d => d.states.length) geraeteListe(geraet, autoDevices.filter(d => d.states.length), dev ? dev.id : "");
.map(d => ({ value: d.id, text: d.name })),
dev ? dev.id : "");
geraet.onchange = () => { geraet.onchange = () => {
const neu = deviceById(Number(geraet.value)); const neu = deviceById(Number(geraet.value));
if (!neu || !neu.states.length) return; if (!neu || !neu.states.length) return;
@@ -257,7 +303,7 @@ function conditionText() {
const text = gruppe.map(bed => { const text = gruppe.map(bed => {
const treffer = stateById(bed.state_id); const treffer = stateById(bed.state_id);
if (!treffer) return "?"; if (!treffer) return "?";
let s = treffer.device.name + ": " + treffer.state.name + " " let s = treffer.device.label + ": " + treffer.state.name + " "
+ operatorLabel(treffer.state, bed.operator) + " " + bed.value; + operatorLabel(treffer.state, bed.operator) + " " + bed.value;
if (treffer.state.unit) s += " " + treffer.state.unit; if (treffer.state.unit) s += " " + treffer.state.unit;
return s; return s;
@@ -292,9 +338,7 @@ function actionRow(ai, aktion) {
const geraet = document.createElement("select"); const geraet = document.createElement("select");
geraet.className = "form-select"; geraet.className = "form-select";
optionListe(geraet, autoDevices.filter(d => d.commands.length) geraeteListe(geraet, autoDevices.filter(d => d.commands.length), dev ? dev.id : "");
.map(d => ({ value: d.id, text: d.name })),
dev ? dev.id : "");
geraet.onchange = () => { geraet.onchange = () => {
const neu = deviceById(Number(geraet.value)); const neu = deviceById(Number(geraet.value));
if (!neu || !neu.commands.length) return; if (!neu || !neu.commands.length) return;
+70 -7
View File
@@ -190,6 +190,59 @@ function operatorLabel($op, $type = "")
return $op === "!=" ? "" : $op; return $op === "!=" ? "" : $op;
} }
/**
* Technische Zutaten aus einem Geraetenamen nehmen, ohne ihn in der
* Datenbank anzufassen.
*
* Die Discovery vergibt Namen, wie das Geraet sie meldet: "u_Wozi
* Terrassentuer" (das u_ stammt aus der Tahoma-Beschriftung),
* "Power_EG_EM_0" oder "Handtuchtrockner_OG_Relay_0" (die angehaengte Ziffer
* ist der Kanal, und es gibt nur einen). In der Auswahlliste stoert das nur.
*
* Bewusst nur die einstellige Endung: "go-eCharger_270003" traegt seine
* Seriennummer, die bleibt stehen.
*/
function anzeigeName($name)
{
$name = preg_replace('/^u_/', '', $name);
$name = preg_replace('/_\d$/', '', $name);
$name = str_replace('_', ' ', $name);
return trim(preg_replace('/\s+/', ' ', $name));
}
/**
* id => Beschriftung fuer alle Geraete, geputzt und eindeutig.
*
* Eine Stelle fuer Editor und Uebersicht, damit in der Auswahlliste nicht
* "Wozi Terrassentuer" steht und im Satz darunter "u_Wozi Terrassentuer".
*/
function geraeteBeschriftungen()
{
static $labels = null;
if ($labels !== null) {
return $labels;
}
$res = meshDb()->query("SELECT id, name, type FROM actors");
$zeilen = [];
$haeufigkeit = [];
while ($row = $res->fetch_assoc()) {
$row["label"] = anzeigeName($row["name"]);
$haeufigkeit[$row["label"]] = ($haeufigkeit[$row["label"]] ?? 0) + 1;
$zeilen[] = $row;
}
// Die Tahoma-Box vergibt Beschriftungen doppelt: "Terasse" ist einmal der
// Rollladen und einmal der Sonnenenergie-Sensor. Untereinander in der
// Liste waeren sie nicht auseinanderzuhalten, deshalb kommt bei
// Doppelungen die Geraeteart dazu.
$labels = [];
foreach ($zeilen as $row) {
$labels[intval($row["id"])] = $haeufigkeit[$row["label"]] > 1
? $row["label"] . " (" . $row["type"] . ")"
: $row["label"];
}
return $labels;
}
/** /**
* Alle Geraete mit ihren Messwerten und Kommandos in einem Rutsch. * Alle Geraete mit ihren Messwerten und Kommandos in einem Rutsch.
* *
@@ -202,11 +255,20 @@ function deviceCatalog()
$db = meshDb(); $db = meshDb();
$devices = []; $devices = [];
$res = $db->query("SELECT id, name, type, url FROM actors ORDER BY name"); $labels = geraeteBeschriftungen();
$res = $db->query("SELECT id, name, type, url FROM actors");
$zeilen = [];
while ($row = $res->fetch_assoc()) { while ($row = $res->fetch_assoc()) {
$zeilen[] = $row;
}
usort($zeilen, function ($a, $b) use ($labels) {
return strcoll($labels[intval($a["id"])], $labels[intval($b["id"])]);
});
foreach ($zeilen as $row) {
$devices[intval($row["id"])] = [ $devices[intval($row["id"])] = [
"id" => intval($row["id"]), "id" => intval($row["id"]),
"name" => $row["name"], "name" => $row["name"],
"label" => $labels[intval($row["id"])],
"type" => $row["type"], "type" => $row["type"],
"url" => $row["url"], "url" => $row["url"],
"states" => [], "states" => [],
@@ -614,14 +676,14 @@ function listAutomations($floor)
function stateNames() function stateNames()
{ {
$db = meshDb(); $db = meshDb();
$res = $db->query("SELECT s.id, s.state_name, s.unit, a.name AS actor_name, t.type $labels = geraeteBeschriftungen();
$res = $db->query("SELECT s.id, s.state_name, s.unit, s.actor_id, t.type
FROM actor_states s FROM actor_states s
JOIN actors a ON a.id = s.actor_id
LEFT JOIN state_types t ON s.state_type = t.id"); LEFT JOIN state_types t ON s.state_type = t.id");
$names = []; $names = [];
while ($row = $res->fetch_assoc()) { while ($row = $res->fetch_assoc()) {
$names[intval($row["id"])] = [ $names[intval($row["id"])] = [
"text" => $row["actor_name"] . ": " . $row["state_name"], "text" => ($labels[intval($row["actor_id"])] ?? "?") . ": " . $row["state_name"],
"unit" => $row["unit"], "unit" => $row["unit"],
"type" => $row["type"] ?: "string", "type" => $row["type"] ?: "string",
]; ];
@@ -652,11 +714,12 @@ function parameterLabels()
function commandNames() function commandNames()
{ {
$db = meshDb(); $db = meshDb();
$res = $db->query("SELECT c.id, c.command_name, a.name AS actor_name $labels = geraeteBeschriftungen();
FROM actor_commands c JOIN actors a ON a.id = c.actor_id"); $res = $db->query("SELECT id, command_name, actor_id FROM actor_commands");
$names = []; $names = [];
while ($row = $res->fetch_assoc()) { while ($row = $res->fetch_assoc()) {
$names[intval($row["id"])] = $row["actor_name"] . ": " . $row["command_name"]; $names[intval($row["id"])] = ($labels[intval($row["actor_id"])] ?? "?")
. ": " . $row["command_name"];
} }
return $names; return $names;
} }