Add actuator sensor weighting controls
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 11:41:46 +02:00
parent 94530d3ecf
commit b9b5def7bb
9 changed files with 331 additions and 14 deletions

View File

@@ -271,6 +271,7 @@ let cachedActuators = null;
let cachedEntities = null;
let cachedDiscovery = null;
let discoveryLoadPromise = null;
let currentSensorWeightGroups = [];
const ACTUATOR_RESULT_LIMIT = 50;
const STATUS_TIMEOUT_MS = 2000;
const DASHBOARD_TIMEOUT_MS = 4500;
@@ -803,6 +804,62 @@ async function showActuator(actuatorId, evaluationMessage = "") {
.filter(candidate => contexts.includes(candidate.entity_id))
.map(candidate => `<li><strong>${escapeHtml(candidate.friendly_name || candidate.entity_id)}</strong>: ${uniqueValues(candidate.evidence).map(escapeHtml).join(", ") || "statistisch relevanter Kandidat"}</li>`)
.join("");
const weightedCandidates = [...record.numeric_candidates, ...record.context_candidates]
.filter(candidate => contexts.includes(candidate.entity_id));
const weightGroups = record.manual_override?.sensor_weight_groups || [];
currentSensorWeightGroups = weightGroups;
const weightControls = weightedCandidates.length ? `
<div class="card-list">
${weightedCandidates.map(candidate => {
const relevance = Math.round((candidate.confidence ?? 0) * 100);
const effective = Math.round((candidate.effective_weight ?? 1) * 100);
const manual = candidate.manual_weight == null ? effective : Math.round(candidate.manual_weight * 100);
return `
<article class="actuator-card">
<div class="card-title">
<div>
<div><strong>${escapeHtml(candidate.friendly_name || candidate.entity_id)}</strong></div>
<div class="entity-id">${escapeHtml(candidate.entity_id)}</div>
</div>
<span class="chip">${relevance} % relevant</span>
</div>
<div class="metric-grid">
<div class="metric"><strong>Automatische Relevanz</strong>${relevance} %</div>
<div class="metric"><strong>Aktive Gewichtung</strong>${effective} %</div>
<div class="metric"><strong>Score</strong>${escapeHtml(candidate.score)}</div>
</div>
<label for="weight-${escapeHtml(candidate.entity_id)}">Gewichtung korrigieren</label>
<input id="weight-${escapeHtml(candidate.entity_id)}" data-weight-entity="${escapeHtml(candidate.entity_id)}" type="number" min="0" max="100" step="5" value="${manual}">
</article>
`;
}).join("")}
</div>
<div class="actions">
<button onclick="saveWeightOverrides('${escapeHtml(record.actuator_entity_id)}')">Gewichtungen speichern</button>
</div>
` : "<p class='muted'>Noch keine verwendeten Sensoren oder Zustände für eine Gewichtung ausgewählt.</p>";
const weightGroupControls = `
<details class="manual-context">
<summary>Gruppen-Gewichtung</summary>
${weightGroups.length ? `<ul>${weightGroups.map(group => `
<li><strong>${escapeHtml(group.name)}</strong>: ${Math.round(group.weight * 100)} %
<span class="muted">${group.entity_ids.map(escapeHtml).join(", ")}</span></li>
`).join("")}</ul>` : "<p class='muted'>Noch keine Gruppe gespeichert.</p>"}
<div class="inline-controls">
<div>
<label for="weight-group-name">Gruppenname</label>
<input id="weight-group-name" placeholder="z. B. Flur Bewegung + Helligkeit">
</div>
<div>
<label for="weight-group-value">Gruppen-Gewicht in %</label>
<input id="weight-group-value" type="number" min="0" max="100" step="5" value="100">
</div>
</div>
<label for="weight-group-entities">Entity-IDs der Gruppe</label>
<textarea id="weight-group-entities" class="manual-entry" placeholder="Eine oder mehrere Entity-IDs">${escapeHtml(contexts.join("\n"))}</textarea>
<button class="secondary" onclick="saveWeightOverrides('${escapeHtml(record.actuator_entity_id)}', true)">Als Gruppe speichern</button>
</details>
`;
const currentContextControls = contexts.length
? `<ul>${contexts.map(entityId => `
<li>
@@ -927,6 +984,10 @@ async function showActuator(actuatorId, evaluationMessage = "") {
${automationControls}
<h3>Welche Zusammenhänge automatisch verwendet werden</h3>
${evidence ? `<ul>${evidence}</ul>` : "<p class='warn'>Noch kein geeigneter Kontext erkannt. SillyHome prüft bei neuen HA-Daten erneut.</p>"}
<h3>Sensor-Gewichtung</h3>
<p class="muted">Automatische Relevanz kommt aus der Zuordnung. Die aktive Gewichtung kannst du korrigieren; Gruppen bündeln mehrere Sensoren/Zustände.</p>
${weightControls}
${weightGroupControls}
<h3>Verwendete Sensoren/Zustände ändern</h3>
${currentContextControls}
${manualAssignment}
@@ -986,6 +1047,45 @@ async function hydrateCurrentContextOptions(actuatorId) {
await hydrateContextOptions(record);
}
async function saveWeightOverrides(actuatorId, includeNewGroup = false) {
const sensorWeights = {};
for (const input of document.querySelectorAll("[data-weight-entity]")) {
const value = Number(input.value);
if (Number.isFinite(value)) {
sensorWeights[input.dataset.weightEntity] = Math.max(0, Math.min(100, value)) / 100;
}
}
const groups = [...currentSensorWeightGroups];
if (includeNewGroup) {
const name = document.getElementById("weight-group-name")?.value.trim();
const value = Number(document.getElementById("weight-group-value")?.value || 100);
const entityIds = parseEntityIds(document.getElementById("weight-group-entities")?.value || "");
if (name && entityIds.length) {
groups.push({
group_id: name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64) || "gruppe",
name,
entity_ids: entityIds,
weight: Math.max(0, Math.min(100, Number.isFinite(value) ? value : 100)) / 100,
});
}
}
try {
await api(`v1/actuators/${encodeURIComponent(actuatorId)}/weights`, {
method: "POST",
body: JSON.stringify({
sensor_weights: sensorWeights,
sensor_weight_groups: groups,
note: "Gewichtung im Dashboard korrigiert",
}),
});
invalidateDashboardCache();
await loadConfiguredActuators();
await showActuator(actuatorId, "Sensor-Gewichtung gespeichert.");
} catch (error) {
alert(error.message);
}
}
async function saveManualAssignment(actuatorId) {
const numericEntityId = document.getElementById("manual-numeric-select").value || null;
const selectedContextIds = Array.from(