So gehst du vor
Systemstatus
Zeigt, ob Verbindung, Lernsystem und automatische Prüfungen funktionieren. Hier musst du normalerweise nichts einstellen.
1. Gerät zum Lernen auswählen
Wähle eine Lampe, einen Rollladen oder einen anderen unterstützten Aktor. Du wählst keine Sensoren und erstellst keine Regeln.
@@ -84,13 +125,13 @@Noch kein Aktor ausgewählt.
2. Beobachtete Geräte
Öffne „Details“, um Lernfortschritt, aktuelle Vorhersage und den automatisch gefundenen Kontext zu sehen.
3. Lernfortschritt und Freigabe
Die Freigabe erscheint erst, wenn genug eindeutig zugeordnete Handlungen gelernt wurden. Vorher bleibt das Gerät sicher im Beobachtungsmodus.
System bereit
Letzte automatische Prüfung: ${escapeHtml(reconciliation.last_completed_at || "noch nie")}
`; chips.innerHTML = [ `API: ${escapeHtml(health.status)}`, + `WebSocket: ${escapeHtml(websocket.status)}`, `Lernsystem: ${escapeHtml(ml.status)}`, `Aktoren: ${actuators.length}`, `Lernbereite Geräte: ${reconciliation.trained_models}`, @@ -194,6 +243,7 @@ async function configureActuator() { result.textContent = `${record.actuator_entity_id}: ${lifecycleLabel(record)}.`; await loadOverview(); await showActuator(record.actuator_entity_id); + document.getElementById("detail").scrollIntoView({behavior: "smooth", block: "start"}); } catch (error) { result.textContent = error.message; } @@ -204,29 +254,33 @@ async function loadConfiguredActuators() { try { const rows = await api("v1/actuators"); box.innerHTML = rows.length ? ` -| Gerät | Lernstatus | Freigabe | Gelernte Handlungen | Letzte Vorhersage | Aktionen |
|---|---|---|---|---|---|
| ${escapeHtml(record.actuator_entity_id)} | -${escapeHtml(behaviorLabel(record))} | -${escapeHtml(record.behavior.activation_ready ? "bereit" : record.behavior.activation_reason)} | -${record.behavior.sample_count} | -${record.behavior.prediction - ? `${escapeHtml(record.behavior.prediction.target_state)} (${Math.round(record.behavior.prediction.confidence * 100)} %)` - : "-"} | -
-
+
+
+
+
+ ${escapeHtml(lifecycleLabel(record))}
+ ${escapeHtml(record.actuator_entity_id)}
+ ${escapeHtml(behaviorLabel(record))}
+
+
+ Freigabe${escapeHtml(record.behavior.activation_ready ? "bereit" : record.behavior.activation_reason)}
+ Handlungen${record.behavior.sample_count}
+ Vorhersage${escapeHtml(predictionLabel(record))}
+
+
${record.behavior.mode === "active"
? ``
: record.behavior.activation_ready
? ``
: ""}
- |
-
Noch keine Aktoren ausgewählt.
"; +Noch keine Aktoren ausgewählt.
"; } catch (error) { box.textContent = error.message; } @@ -268,13 +322,20 @@ async function showActuator(actuatorId, evaluationMessage = "") { ${escapeHtml(automation.friendly_name)}${escapeHtml(automation.entity_id)}:
${automation.enabled ? "aktiv" : "pausiert"}
-
+
`).join("")}`
: "Keine eindeutig passende HA-Automation gefunden.
"; box.innerHTML = ` -${escapeHtml(record.actuator_entity_id)}
+Alle wichtigen Aktionen für dieses Gerät.
+Zuordnung
Status: ${escapeHtml(lifecycleLabel(record))}
Kontextzuordnung: automatisch erledigt
Zuordnungssicherheit: ${Math.round(record.assignment.confidence * 100)} %
@@ -290,7 +351,7 @@ async function showActuator(actuatorId, evaluationMessage = "") {Letztes Training: ${escapeHtml(record.behavior.last_trained_at || "noch nicht")}
Was noch passiert: ${escapeHtml(record.behavior.reason)}
Freigabestatus: ${escapeHtml(record.behavior.activation_reason)}
- ${activationButton} +Die Prüfung simuliert keinen Sensorwechsel und schaltet keinen Aktor.
${evaluationMessage ? `${escapeHtml(evaluationMessage)}
` : ""} @@ -306,6 +367,7 @@ async function showActuator(actuatorId, evaluationMessage = "") {Welche Zusammenhänge automatisch verwendet werden
${evidence ? `- ${evidence}
Noch kein geeigneter Kontext erkannt. SillyHome prüft bei neuen HA-Daten erneut.
"} `; + document.getElementById("detail").scrollIntoView({behavior: "smooth", block: "start"}); } catch (error) { box.textContent = error.message; } diff --git a/pyproject.toml b/pyproject.toml index f33edb3..2e52ea4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "sillyhome-next" -version = "0.7.1" +version = "0.7.2" description = "Lokales Smart-Home-Intelligenzsystem für Home Assistant" requires-python = ">=3.11" dependencies = [ diff --git a/tests/ha/test_ha_client.py b/tests/ha/test_ha_client.py index 3f18516..9309712 100644 --- a/tests/ha/test_ha_client.py +++ b/tests/ha/test_ha_client.py @@ -108,6 +108,27 @@ def test_list_entity_metadata_calls_template_api() -> None: } +def test_list_entity_metadata_batches_template_calls() -> None: + responses = [] + for index in range(3): + response = _response() + response.text = ( + f'[{{"entity_id":"sensor.test_{index}",' + f'"area_name":"Area {index}","device_name":"Device {index}"}}]' + ) + responses.append(response) + client = HaClient(HaClientSettings(url="http://ha.local", token="test-token")) + client._session.post = Mock(side_effect=responses) # type: ignore[method-assign] + + entity_ids = [f"sensor.test_{index}" for index in range(401)] + metadata = client.list_entity_metadata(entity_ids) + + assert client._session.post.call_count == 3 + assert metadata["sensor.test_0"]["area_name"] == "Area 0" + assert metadata["sensor.test_1"]["device_name"] == "Device 1" + assert metadata["sensor.test_2"]["device_name"] == "Device 2" + + def test_get_logbook_filters_entity_and_period() -> None: response = _response(payload=[{"entity_id": "light.office"}]) client = _client_with_response(response) diff --git a/tests/ha/test_ha_reader.py b/tests/ha/test_ha_reader.py index 2ceef09..af34248 100644 --- a/tests/ha/test_ha_reader.py +++ b/tests/ha/test_ha_reader.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import datetime, timezone from app.ha.client import HaClient, HaClientSettings +from app.ha.exceptions import HaHttpError from app.ha.reader import HaReader @@ -150,3 +151,23 @@ def test_ha_reader_finds_automation_that_targets_entity() -> None: assert len(matches) == 1 assert matches[0].entity_id == "automation.storage_light" assert matches[0].enabled is True + + +def test_ha_reader_ignores_automation_configs_not_exposed_by_ha() -> None: + client = FakeHaClient() + client.list_entities = lambda: [ # type: ignore[method-assign] + { + "entity_id": "automation.storage_light", + "state": "on", + "attributes": { + "id": "123", + "friendly_name": "Storage light", + }, + } + ] + client.get_automation_config = lambda automation_id: (_ for _ in ()).throw( # type: ignore[method-assign] + HaHttpError(404, "Resource not found") + ) + reader = HaReader(client) + + assert reader.find_automations_for_entity("light.storage") == []