From 100f5af5786cc9c1d9b3c0cc75668f2ff03b68de Mon Sep 17 00:00:00 2001 From: Otto Date: Sun, 14 Jun 2026 15:28:52 +0200 Subject: [PATCH] UI-002: align context and activation status --- CHANGELOG.md | 8 ++++++++ addon/config.yaml | 2 +- app/actuators/lifecycle.py | 17 +++++++++++++++-- app/static/index.html | 13 +++++++++++-- tests/actuators/test_lifecycle.py | 4 ++++ tests/test_dashboard.py | 3 +++ 6 files changed, 42 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71c9458..786fba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.5.4 - 2026-06-14 +- Tür-, Bewegungs- und andere belastbare Kontextsensoren werden auch ohne + numerischen Sensor als vollständige automatische Kontextzuordnung angezeigt +- Status und Zuordnungssicherheit bilden das aktive Verhaltenslernen ab statt + eines optionalen numerischen Modells +- Ausführungsfreigabe erscheint erst, wenn genügend eindeutig manuelle + Bedienungen vorliegen; bis dahin nennt die Oberfläche die noch fehlende Anzahl + ## 0.5.3 - 2026-06-14 - Verhindert fachlich falsche Sensorzuordnungen nur aufgrund generischer Namen wie `Licht` oder `Lichtschalter` diff --git a/addon/config.yaml b/addon/config.yaml index 0339bd9..5d1f8a0 100644 --- a/addon/config.yaml +++ b/addon/config.yaml @@ -1,5 +1,5 @@ name: SillyHome Next -version: "0.5.3" +version: "0.5.4" slug: sillyhome_next description: Lernt automatisch aus deinem Verhalten und steuert freigegebene Aktoren url: http://192.168.6.31:3000/pino/sillyhome-next diff --git a/app/actuators/lifecycle.py b/app/actuators/lifecycle.py index 05ae1de..f6c3a8d 100644 --- a/app/actuators/lifecycle.py +++ b/app/actuators/lifecycle.py @@ -239,12 +239,25 @@ class ActuatorReconciliationService: (candidate for candidate in numeric_candidates if candidate.auto_accepted), None, ) - top_contexts = [ - candidate.entity_id + accepted_contexts = [ + candidate for candidate in context_candidates if candidate.auto_accepted ][: _MAX_CONTEXT_SELECTIONS] + top_contexts = [candidate.entity_id for candidate in accepted_contexts] if top_numeric is None: + if accepted_contexts: + return AssignmentSelection( + selected_numeric_entity_id=None, + selected_context_entity_ids=top_contexts, + source=AssignmentSource.AUTOMATIC, + confidence=max(candidate.confidence for candidate in accepted_contexts), + review_required=False, + reason=( + "Passender Schaltkontext automatisch erkannt. Für diese " + "Verhaltensvorhersage ist kein numerischer Sensor erforderlich." + ), + ) return AssignmentSelection( selected_numeric_entity_id=None, selected_context_entity_ids=top_contexts, diff --git a/app/static/index.html b/app/static/index.html index 65bb8a6..790039c 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -112,6 +112,7 @@ async function api(path, options = {}) { } function lifecycleLabel(record) { + if (record.behavior.status === "trained") return "Kontext erkannt"; const labels = { trained: "lernt", pending_history: "sammelt Historie", @@ -124,6 +125,7 @@ function lifecycleLabel(record) { } function statusClass(record) { + if (record.behavior.status === "trained") return "ok"; if (record.lifecycle.status === "trained") return "ok"; if (["pending_history", "pending_assignment", "archived"].includes(record.lifecycle.status)) return "warn"; return "bad"; @@ -237,11 +239,18 @@ async function showActuator(actuatorId) { .map(candidate => `
  • ${escapeHtml(candidate.friendly_name || candidate.entity_id)}: ${candidate.evidence.map(escapeHtml).join(", ") || "statistisch relevanter Kandidat"}
  • `) .join(""); const prediction = record.behavior.prediction; + const requiredUserActions = 3; + const missingUserActions = Math.max( + 0, + requiredUserActions - record.behavior.high_confidence_sample_count, + ); const activationButton = record.behavior.mode === "active" ? `` - : record.behavior.status === "trained" + : record.behavior.status === "trained" && missingUserActions === 0 ? `` - : "

    Freigabe wird möglich, sobald genügend Handlungen gelernt wurden.

    "; + : record.behavior.status === "trained" + ? `

    Freigabe noch gesperrt: ${missingUserActions} eindeutig manuelle Bedienung${missingUserActions === 1 ? "" : "en"} fehlen. Bediene das Licht dafür direkt über Home Assistant.

    ` + : "

    Freigabe wird möglich, sobald genügend Handlungen gelernt wurden.

    "; box.innerHTML = `
    diff --git a/tests/actuators/test_lifecycle.py b/tests/actuators/test_lifecycle.py index 23341c0..5fae549 100644 --- a/tests/actuators/test_lifecycle.py +++ b/tests/actuators/test_lifecycle.py @@ -5,6 +5,7 @@ from pathlib import Path from app.actuators.lifecycle import ActuatorReconciliationService from app.actuators.models import ( + AssignmentSource, LifecycleStatus, ManualOverride, model_id_for_actuator, @@ -233,6 +234,9 @@ def test_reconciliation_does_not_cross_assign_other_room_light_energy( assert record.assignment.selected_context_entity_ids == [ "binary_sensor.abstellraum_ture" ] + assert record.assignment.source is AssignmentSource.AUTOMATIC + assert record.assignment.confidence == 1.0 + assert record.assignment.review_required is False assert record.lifecycle.status is LifecycleStatus.ARCHIVED diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 4620eed..2c93b94 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -14,5 +14,8 @@ def test_dashboard_is_served_at_root() -> None: assert "Wie gewohnt bedienen" in response.text assert "Ohne deine spätere Freigabe wird nichts geschaltet" in response.text assert "Du wählst keine Sensoren und erstellst keine Regeln" in response.text + assert "Freigabe noch gesperrt" in response.text + assert "Bediene das Licht dafür direkt über Home Assistant" in response.text + assert 'record.behavior.status === "trained" && missingUserActions === 0' in response.text assert "Automation-Entwurf" not in response.text assert "Manuelle Overrides" not in response.text -- 2.47.3