Optimize dashboard categories and context loading
Some checks failed
quality / test (3.11) (push) Has been cancelled
quality / test (3.13) (push) Has been cancelled

This commit is contained in:
2026-06-17 00:02:19 +02:00
parent 9ddb065f62
commit f8bee92e64
9 changed files with 203 additions and 27 deletions

View File

@@ -144,6 +144,7 @@
<option value="humidifier">Befeuchter / Entfeuchter</option>
<option value="media_player">TV / Medien</option>
<option value="remote">Fernbedienungen</option>
<option value="scene">Szenen</option>
<option value="number">Numerische Helper</option>
<option value="valve">Ventile</option>
</select>
@@ -185,8 +186,15 @@ let currentActuatorId = null;
let actuatorChoices = [];
let contextOptions = [];
let manualContextState = {options: [], selected: new Set()};
let cachedActuators = null;
let cachedEntities = null;
let cachedDiscovery = null;
const ACTUATOR_RESULT_LIMIT = 50;
function uniqueValues(values) {
return [...new Set(values.filter(Boolean))];
}
async function api(path, options = {}) {
const response = await fetch(path, {headers: {"Content-Type": "application/json"}, ...options});
const body = response.status === 204 ? null : await response.json().catch(() => ({}));
@@ -251,14 +259,33 @@ function matchesSearch(entity, query) {
function categoryForEntity(entity) {
const cls = entity.device_class || "";
const text = normalizedSearch([
entity.entity_id,
entity.friendly_name,
entity.area_name,
entity.device_name,
].filter(Boolean).join(" "));
if (["pv", "solar", "akku", "batterie", "battery", "einspeisung", "wechselrichter"].some(token => text.includes(token))) {
return "PV / Akku / Einspeisung";
}
if (entity.domain === "fan") return "Lüftung / Ventilatoren";
if (entity.domain === "climate") return "Heizung / Klima";
if (entity.domain === "weather") return "Wetter";
if (entity.domain === "person" || entity.domain === "device_tracker") return "Anwesenheit / Personen";
if (entity.domain === "cover") return "Rollläden / Cover";
if (entity.domain === "light") return "Lichtzustände";
if (entity.domain === "switch") return "Schalter / Helper";
if (entity.domain === "switch") return "Schalter / Steckdosen";
if (entity.domain.startsWith("input_")) return "Helper";
if (entity.domain === "scene") return "Szenen";
if (entity.domain === "media_player" || entity.domain === "remote") return "TV / Medien";
if (["motion", "occupancy", "presence"].includes(cls)) return "PIR / Präsenz";
if (["illuminance"].includes(cls)) return "Helligkeit";
if (["door", "garage_door", "opening", "window"].includes(cls)) return "Tür / Fenster";
if (["smoke", "safety", "problem"].includes(cls)) return "Sicherheit / Diagnose";
if (["humidity", "moisture"].includes(cls)) return "Luftfeuchtigkeit";
if (["temperature"].includes(cls)) return "Temperatur";
if (["power", "energy", "current", "voltage"].includes(cls)) return "Strom / Energie";
if (["battery", "signal_strength"].includes(cls)) return "Batterie / Signal";
if (entity.domain === "binary_sensor") return "Binäre Sensoren";
if (entity.domain === "sensor") return "Weitere Messsensoren";
return "Weitere Zustände";
@@ -305,18 +332,36 @@ async function loadOverview() {
status.innerHTML = `<p class="bad">${escapeHtml(error.message)}</p>`;
chips.innerHTML = "";
}
await Promise.all([loadActuatorDiscovery(), loadConfiguredActuators()]);
await loadDashboardData();
renderActuatorDiscovery();
renderConfiguredActuators();
void loadActuatorSuggestions();
}
async function loadDashboardData() {
const [actuators, entities, discovery] = await Promise.all([
api("v1/actuators"),
api("v1/entities"),
api("v1/actuators/discovery"),
]);
cachedActuators = actuators;
cachedEntities = entities;
cachedDiscovery = discovery;
}
async function loadActuatorDiscovery() {
if (!cachedActuators || !cachedDiscovery) {
await loadDashboardData();
}
renderActuatorDiscovery();
}
function renderActuatorDiscovery() {
const options = document.getElementById("actuator-options");
const select = document.getElementById("actuator-select");
try {
const [available, configured] = await Promise.all([
api("v1/actuators/discovery"),
api("v1/actuators"),
]);
const available = cachedDiscovery || [];
const configured = cachedActuators || [];
const configuredIds = new Set(configured.map(record => record.actuator_entity_id));
actuatorChoices = available.filter(entity => !configuredIds.has(entity.entity_id));
options.innerHTML = actuatorChoices.slice(0, 120).map(entity =>
@@ -366,6 +411,7 @@ function actuatorGroupLabel(domain) {
media_player: "TV / Medien",
number: "Numerische Helper",
remote: "Fernbedienungen",
scene: "Szenen",
switch: "Schalter / Steckdosen",
cover: "Rollläden / Cover",
fan: "Lüftung / Ventilatoren",
@@ -463,12 +509,17 @@ async function configureActuator() {
}
async function loadConfiguredActuators() {
if (!cachedActuators || !cachedEntities) {
await loadDashboardData();
}
renderConfiguredActuators();
}
function renderConfiguredActuators() {
const box = document.getElementById("configured-actuators");
try {
const [rows, entities] = await Promise.all([
api("v1/actuators"),
api("v1/entities"),
]);
const rows = cachedActuators || [];
const entities = cachedEntities || [];
const entityMap = new Map(entities.map(entity => [entity.entity_id, entity]));
const groups = new Map();
for (const record of rows) {
@@ -532,7 +583,7 @@ async function showActuator(actuatorId, evaluationMessage = "") {
].filter(Boolean);
const evidence = [...record.numeric_candidates, ...record.context_candidates]
.filter(candidate => contexts.includes(candidate.entity_id))
.map(candidate => `<li><strong>${escapeHtml(candidate.friendly_name || candidate.entity_id)}</strong>: ${candidate.evidence.map(escapeHtml).join(", ") || "statistisch relevanter Kandidat"}</li>`)
.map(candidate => `<li><strong>${escapeHtml(candidate.friendly_name || candidate.entity_id)}</strong>: ${uniqueValues(candidate.evidence).map(escapeHtml).join(", ") || "statistisch relevanter Kandidat"}</li>`)
.join("");
const currentContextControls = contexts.length
? `<ul>${contexts.map(entityId => `