Compare commits

..

1 Commits

Author SHA1 Message Date
58d3126a35 Merge pull request 'v0.6.1: sichtbare Rückmeldung bei Situationsprüfung' (#38) from fix/evaluation-feedback-v0.6.1 into main
Some checks failed
quality / test (3.11) (push) Has been cancelled
quality / test (3.13) (push) Has been cancelled
2026-06-14 15:38:26 +02:00
8 changed files with 34 additions and 60 deletions

View File

@@ -1,12 +1,5 @@
# Changelog
## 0.6.2 - 2026-06-14
- Eindeutig im Home-Assistant-Logbuch erkannte Automationen und Scripts zählen für
Lernen und Freigabe gleichwertig wie manuelle Bedienungen
- Automationsmuster erhalten dieselbe Modellgewichtung wie manuelle Handlungen
- Oberfläche zeigt die gemeinsame Zahl als `eindeutig geregelt`; eine
ausdrückliche Aktivierung pro Aktor bleibt weiterhin erforderlich
## 0.6.1 - 2026-06-14
- Manuelle Prüfung als `Aktuelle Situation auswerten` eindeutig von Simulation
oder Aktorschaltung abgegrenzt

View File

@@ -109,9 +109,8 @@ Lernentscheidungen erfolgen automatisch.
System das lokale Modell automatisch.
5. Vorhersagen laufen zunächst ausschließlich im Shadow-Modus.
6. Erst nach ausdrücklicher Freigabe pro Aktor werden hochkonfidente,
erlaubte Zustände geschaltet. Eindeutig im HA-Logbuch erkannte Automationen
und Scripts zählen dabei gleichwertig wie manuelle Bedienungen. Eigene
Schaltungen von SillyHome werden nicht zurückgelernt.
erlaubte Zustände geschaltet. Eigene Schaltungen und erkannte
HA-Automationen werden nicht als Nutzerhandlungen zurückgelernt.
Vor einem Update sollte in Home Assistant unter **Einstellungen → System → Backups**
eine Teil-Sicherung des Add-ons erstellt werden. Zur Wiederherstellung das gewünschte

View File

@@ -1,5 +1,5 @@
name: SillyHome Next
version: "0.6.2"
version: "0.6.1"
slug: sillyhome_next
description: Lernt automatisch aus deinem Verhalten und steuert freigegebene Aktoren
url: http://192.168.6.31:3000/pino/sillyhome-next

View File

@@ -124,9 +124,7 @@ class BehaviorEngine:
logbook=logbook,
own_executions=record.behavior.execution_events,
)
trusted_actions = sum(
1 for pattern in patterns if pattern.source in {"user", "automation"}
)
high_confidence = sum(1 for pattern in patterns if pattern.source == "user")
status = (
BehaviorStatus.TRAINED
if len(patterns) >= self._settings.min_behavior_actions
@@ -144,7 +142,7 @@ class BehaviorEngine:
update={
"status": status,
"sample_count": len(patterns),
"high_confidence_sample_count": trusted_actions,
"high_confidence_sample_count": high_confidence,
"patterns": patterns[-_MAX_PATTERNS:],
"last_trained_at": now,
"reason": reason,
@@ -298,8 +296,8 @@ class BehaviorEngine:
< self._settings.min_behavior_actions
):
raise ValueError(
"Für die Freigabe fehlen noch eindeutig zugeordnete manuelle "
"oder automatisierte Handlungen."
"Für die Freigabe fehlen noch eindeutig dir zugeordnete Handlungen. "
"Bediene den Aktor einige Male über Home Assistant."
)
mode = BehaviorMode.ACTIVE
approved_at = now
@@ -340,6 +338,8 @@ class BehaviorEngine:
context_ids,
point.timestamp,
)
if source == "automation" and trigger is None:
continue
contexts = {
entity_id: state
for entity_id in context_ids
@@ -520,7 +520,7 @@ def _action_source(
if nearest.context_user_id:
return "user", 1.0
if nearest.context_domain in _AUTOMATION_CONTEXT_DOMAINS:
return "automation", 1.0
return "automation", 0.1
return "physical_or_unknown", 0.7

View File

@@ -239,20 +239,20 @@ async function showActuator(actuatorId, evaluationMessage = "") {
.map(candidate => `<li><strong>${escapeHtml(candidate.friendly_name || candidate.entity_id)}</strong>: ${candidate.evidence.map(escapeHtml).join(", ") || "statistisch relevanter Kandidat"}</li>`)
.join("");
const prediction = record.behavior.prediction;
const requiredTrustedActions = 3;
const requiredUserActions = 3;
const learnedAutomationActions = record.behavior.patterns.filter(
pattern => pattern.source === "automation",
).length;
const missingTrustedActions = Math.max(
const missingUserActions = Math.max(
0,
requiredTrustedActions - record.behavior.high_confidence_sample_count,
requiredUserActions - record.behavior.high_confidence_sample_count,
);
const activationButton = record.behavior.mode === "active"
? `<button class="danger" onclick="setActivation('${escapeHtml(record.actuator_entity_id)}', false)">Autonomes Schalten stoppen</button>`
: record.behavior.status === "trained" && missingTrustedActions === 0
: record.behavior.status === "trained" && missingUserActions === 0
? `<button onclick="setActivation('${escapeHtml(record.actuator_entity_id)}', true)">Lernen und Schalten freigeben</button>`
: record.behavior.status === "trained"
? `<p class='muted'>Freigabe noch gesperrt: ${missingTrustedActions} eindeutig zugeordnete manuelle oder automatisierte Handlung${missingTrustedActions === 1 ? "" : "en"} fehlen.</p>`
? `<p class='muted'>Freigabe noch gesperrt: ${missingUserActions} eindeutig manuelle Bedienung${missingUserActions === 1 ? "" : "en"} fehlen. Bediene das Licht dafür direkt über Home Assistant.</p>`
: "<p class='muted'>Freigabe wird möglich, sobald genügend Handlungen gelernt wurden.</p>";
box.innerHTML = `
<div class="grid-two">
@@ -268,7 +268,7 @@ async function showActuator(actuatorId, evaluationMessage = "") {
<h3>Lernfortschritt</h3>
<p><strong>Betriebsart:</strong> ${escapeHtml(behaviorLabel(record))}</p>
<p><strong>Gelernte Handlungen:</strong> ${record.behavior.sample_count}</p>
<p><strong>Davon eindeutig geregelt:</strong> ${record.behavior.high_confidence_sample_count}</p>
<p><strong>Davon eindeutig Benutzer:</strong> ${record.behavior.high_confidence_sample_count}</p>
<p><strong>Davon erkannte HA-Automationen:</strong> ${learnedAutomationActions}</p>
<p><strong>Letztes Training:</strong> ${escapeHtml(record.behavior.last_trained_at || "noch nicht")}</p>
<p><strong>Was noch passiert:</strong> ${escapeHtml(record.behavior.reason)}</p>

View File

@@ -12,10 +12,10 @@ Für jeden Aktor lädt SillyHome Next:
- automatisch zugeordnete Mess- und Kontext-Entities
- deren Zustand zum Zeitpunkt der Handlung
Eindeutig einem Home-Assistant-Benutzer zugeordnete Handlungen und im Logbuch
erkannte Automations- oder Script-Aktionen erhalten das höchste Gewicht.
Physische oder nicht eindeutig zuordenbare Bedienungen dürfen das Shadow-Modell
ergänzen, reichen allein aber nicht zur Aktivierung.
Eindeutig einem Home-Assistant-Benutzer zugeordnete Handlungen erhalten das
höchste Gewicht. Erkannte Automations- und Script-Aktionen werden verworfen.
Physische oder nicht eindeutig zuordenbare Bedienungen dürfen das
Shadow-Modell ergänzen, reichen allein aber nicht zur Aktivierung.
## Modell
@@ -36,8 +36,8 @@ Kontext. Mehrere passende historische Handlungen erhöhen die Confidence.
2. `shadow`: Modell ist trainiert; Vorhersagen werden angezeigt, aber nicht ausgeführt.
3. `active`: Nutzer hat den Aktor ausdrücklich freigegeben.
Die Aktivierung verlangt genügend eindeutig zugeordnete manuelle oder
automatisierte Handlungen. Ausgeführt werden nur erlaubte Zustände reversibler Domains:
Die Aktivierung verlangt genügend eindeutig einem Benutzer zugeordnete
Handlungen. Ausgeführt werden nur erlaubte Zustände reversibler Domains:
`light`, `switch`, `fan`, `humidifier` und `cover`.
## Schutzmechanismen
@@ -48,4 +48,4 @@ automatisierte Handlungen. Ausgeführt werden nur erlaubte Zustände reversibler
- keine Ausführung bei bereits erreichtem Zielzustand
- keine Ausführung unbekannter Zustände oder riskanter Domains
- eigene Schaltungen werden beim nächsten Training herausgefiltert
- Automation-/Script-Aktionen zählen nur bei eindeutiger Herkunft im HA-Logbuch
- bekannte Automation-/Script-Aktionen werden nicht als Nutzerverhalten gelernt

View File

@@ -161,8 +161,8 @@ def test_engine_trains_predicts_in_shadow_and_executes_only_after_approval(
shadow = engine.evaluate("light.office")
assert trained.behavior.status is BehaviorStatus.TRAINED
assert trained.behavior.sample_count == 6
assert trained.behavior.high_confidence_sample_count == 6
assert trained.behavior.sample_count == 3
assert trained.behavior.high_confidence_sample_count == 3
assert shadow.behavior.mode is BehaviorMode.SHADOW
assert shadow.behavior.prediction is not None
assert shadow.behavior.prediction.target_state == "on"
@@ -179,27 +179,17 @@ def test_engine_trains_predicts_in_shadow_and_executes_only_after_approval(
]
def test_engine_counts_known_automation_actions_like_manual_actions(
tmp_path: Path,
) -> None:
def test_engine_excludes_known_automation_actions(tmp_path: Path) -> None:
now = datetime.now(timezone.utc).replace(second=0, microsecond=0)
engine, _ = _engine(tmp_path, now)
trained = engine.train("light.office")
assert {pattern.target_state for pattern in trained.behavior.patterns} == {
"on",
"off",
}
assert {pattern.source for pattern in trained.behavior.patterns} == {
"user",
"automation",
}
assert trained.behavior.high_confidence_sample_count == 6
assert {pattern.weight for pattern in trained.behavior.patterns} == {1.0}
assert {pattern.target_state for pattern in trained.behavior.patterns} == {"on"}
assert {pattern.source for pattern in trained.behavior.patterns} == {"user"}
def test_engine_learns_causal_automation_with_activation_credit(
def test_engine_learns_causal_automation_for_shadow_without_user_credit(
tmp_path: Path,
) -> None:
now = datetime.now(timezone.utc).replace(second=0, microsecond=0)
@@ -280,8 +270,7 @@ def test_engine_learns_causal_automation_with_activation_credit(
]
assert len(automation_patterns) == 3
assert trained.behavior.high_confidence_sample_count == 3
assert {pattern.weight for pattern in automation_patterns} == {1.0}
assert trained.behavior.high_confidence_sample_count == 0
assert {
(
pattern.trigger_entity_id,
@@ -291,10 +280,6 @@ def test_engine_learns_causal_automation_with_activation_credit(
for pattern in automation_patterns
} == {("binary_sensor.storage_door", "off", "on")}
active = engine.set_active("light.storage", active=True)
assert active.behavior.mode is BehaviorMode.ACTIVE
def test_active_mode_rejects_unsafe_domains(tmp_path: Path) -> None:
settings = _settings(tmp_path)
@@ -316,7 +301,7 @@ def test_active_mode_rejects_unsafe_domains(tmp_path: Path) -> None:
engine.set_active("lock.front_door", active=True)
def test_active_mode_requires_trusted_manual_or_automation_actions(tmp_path: Path) -> None:
def test_active_mode_requires_user_attributed_actions(tmp_path: Path) -> None:
settings = _settings(tmp_path)
store = ActuatorStore(settings.actuator_store)
record = store.configure("light.office")
@@ -336,7 +321,7 @@ def test_active_mode_requires_trusted_manual_or_automation_actions(tmp_path: Pat
reader = FakeBehaviorReader(entities=[], history=[], logbook=[])
engine = BehaviorEngine(ha_reader=reader, store=store, settings=settings)
with pytest.raises(ValueError, match="manuelle oder automatisierte"):
with pytest.raises(ValueError, match="eindeutig dir zugeordnete"):
engine.set_active("light.office", active=True)

View File

@@ -15,15 +15,12 @@ def test_dashboard_is_served_at_root() -> None:
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 "manuelle oder automatisierte Handlung" in response.text
assert "Bediene das Licht dafür direkt über Home Assistant" in response.text
assert "Davon erkannte HA-Automationen" in response.text
assert "Aktuelle Situation auswerten" in response.text
assert "Die Prüfung simuliert keinen Sensorwechsel" in response.text
assert "Kein frischer passender Sensorwechsel erkannt" in response.text
assert "Vorhersage jetzt prüfen" not in response.text
assert (
'record.behavior.status === "trained" && missingTrustedActions === 0'
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