Add safety dashboard and decision transparency
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 18:26:49 +02:00
parent ca253d1e6c
commit 0101596e93
12 changed files with 792 additions and 23 deletions

View File

@@ -30,6 +30,7 @@ class FakeHaReader(HaReader):
self._entities = entities
self._history = history
self.read_entities_calls = 0
self.service_calls: list[tuple[str, str, dict[str, object]]] = []
def read_entities(self) -> list[HaEntitySummary]:
self.read_entities_calls += 1
@@ -86,6 +87,7 @@ class FakeHaReader(HaReader):
service: str,
service_data: dict[str, object],
) -> list[object]:
self.service_calls.append((domain, service, service_data))
return []
def find_automations_for_entity(
@@ -264,6 +266,40 @@ def test_weight_override_endpoint_updates_sensor_relevance(tmp_path: Path) -> No
assert numeric["sensor.abstellkammer_illuminance"]["effective_weight"] == 0.75
def test_safety_profile_can_block_actuator_manually(tmp_path: Path) -> None:
with TestClient(app) as client:
_install_service(tmp_path)
client.post("/v1/actuators", json={"actuator_entity_id": "light.abstellkammer"})
response = client.post(
"/v1/actuators/light.abstellkammer/safety",
json={
"safety": {
"stage": "shadow",
"manual_block": True,
"min_confidence": 0.9,
"cooldown_seconds": 120,
"rules": [
{
"rule_id": "manual_block",
"label": "Manuelle Sperre respektieren",
"enabled": True,
"blocking": True,
"reason": "Test",
}
],
"note": "Test",
}
},
)
assert response.status_code == 200
payload = response.json()
assert payload["behavior"]["safety"]["manual_block"] is True
assert payload["behavior"]["safety"]["min_confidence"] == 0.9
assert payload["behavior"]["safety"]["cooldown_seconds"] == 120
def test_summary_is_lightweight_and_uses_cached_entity_metadata(tmp_path: Path) -> None:
with TestClient(app) as client:
_install_service(tmp_path)
@@ -298,6 +334,26 @@ def test_dashboard_overview_uses_cache_without_ha_roundtrip(tmp_path: Path) -> N
assert payload["cache"]["entity_count"] == 4
assert payload["actuators"][0]["friendly_name"] == "Abstellkammer Licht"
assert payload["discovery_groups"]
assert payload["jobs"]["jobs"][-1]["kind"] == "discovery"
def test_reconciliation_run_records_visible_job_queue(tmp_path: Path) -> None:
with TestClient(app) as client:
_install_service(tmp_path)
client.post("/v1/actuators", json={"actuator_entity_id": "light.abstellkammer"})
response = client.post("/v1/actuators/reconciliation/run")
jobs = client.get("/v1/actuators/job-queue/state")
assert response.status_code == 200
assert jobs.status_code == 200
payload = jobs.json()
assert [job["kind"] for job in payload["jobs"][-3:]] == [
"reconciliation",
"training",
"evaluation",
]
assert payload["jobs"][-1]["status"] == "completed"
def test_dashboard_start_path_stays_within_five_second_budget(tmp_path: Path) -> None: