diff --git a/js/solar/autoActionFuncs.js b/js/solar/autoActionFuncs.js index 6b9c212..5870ea4 100644 --- a/js/solar/autoActionFuncs.js +++ b/js/solar/autoActionFuncs.js @@ -63,12 +63,60 @@ function ersterBefehl() { function optionListe(select, eintraege, gewaehlt) { select.innerHTML = ""; - eintraege.forEach(e => { - const opt = document.createElement("option"); - opt.value = e.value; - opt.text = e.text; - if (String(e.value) === String(gewaehlt)) opt.selected = true; - select.appendChild(opt); + eintraege.forEach(e => select.appendChild(option(e, gewaehlt))); +} + +function option(e, gewaehlt) { + const opt = document.createElement("option"); + opt.value = e.value; + opt.text = e.text; + if (String(e.value) === String(gewaehlt)) opt.selected = true; + 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 const geraet = document.createElement("select"); geraet.className = "form-select"; - optionListe(geraet, autoDevices.filter(d => d.states.length) - .map(d => ({ value: d.id, text: d.name })), - dev ? dev.id : ""); + geraeteListe(geraet, autoDevices.filter(d => d.states.length), dev ? dev.id : ""); geraet.onchange = () => { const neu = deviceById(Number(geraet.value)); if (!neu || !neu.states.length) return; @@ -257,7 +303,7 @@ function conditionText() { const text = gruppe.map(bed => { const treffer = stateById(bed.state_id); 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; if (treffer.state.unit) s += " " + treffer.state.unit; return s; @@ -292,9 +338,7 @@ function actionRow(ai, aktion) { const geraet = document.createElement("select"); geraet.className = "form-select"; - optionListe(geraet, autoDevices.filter(d => d.commands.length) - .map(d => ({ value: d.id, text: d.name })), - dev ? dev.id : ""); + geraeteListe(geraet, autoDevices.filter(d => d.commands.length), dev ? dev.id : ""); geraet.onchange = () => { const neu = deviceById(Number(geraet.value)); if (!neu || !neu.commands.length) return; diff --git a/restricted/automations.php b/restricted/automations.php index 4c1a1ac..905b8f7 100644 --- a/restricted/automations.php +++ b/restricted/automations.php @@ -190,6 +190,59 @@ function operatorLabel($op, $type = "") 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. * @@ -202,11 +255,20 @@ function deviceCatalog() $db = meshDb(); $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()) { + $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"])] = [ "id" => intval($row["id"]), "name" => $row["name"], + "label" => $labels[intval($row["id"])], "type" => $row["type"], "url" => $row["url"], "states" => [], @@ -614,14 +676,14 @@ function listAutomations($floor) function stateNames() { $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 - JOIN actors a ON a.id = s.actor_id LEFT JOIN state_types t ON s.state_type = t.id"); $names = []; while ($row = $res->fetch_assoc()) { $names[intval($row["id"])] = [ - "text" => $row["actor_name"] . ": " . $row["state_name"], + "text" => ($labels[intval($row["actor_id"])] ?? "?") . ": " . $row["state_name"], "unit" => $row["unit"], "type" => $row["type"] ?: "string", ]; @@ -652,11 +714,12 @@ function parameterLabels() function commandNames() { $db = meshDb(); - $res = $db->query("SELECT c.id, c.command_name, a.name AS actor_name - FROM actor_commands c JOIN actors a ON a.id = c.actor_id"); + $labels = geraeteBeschriftungen(); + $res = $db->query("SELECT id, command_name, actor_id FROM actor_commands"); $names = []; 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; }