diff --git a/CHANGELOG.md b/CHANGELOG.md index 920f84b..80c5942 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.7.17 - 2026-06-16 +- WebSocket-Eventpfad ist schneller: irrelevante HA-State-Changes werden vor + dem teuren State-Cache-Listenbau verworfen. +- WebSocket nutzt Keepalive und reconnectet nach Abbrüchen nach 1s statt 5s. + ## 0.7.16 - 2026-06-16 - Beobachtete Aktoren werden in der Übersicht nach Raum oder Typ gruppiert und mit Friendly Name angezeigt. diff --git a/addon/config.yaml b/addon/config.yaml index 86b7e5b..dda4e85 100644 --- a/addon/config.yaml +++ b/addon/config.yaml @@ -1,5 +1,5 @@ name: SillyHome Next -version: "0.7.16" +version: "0.7.17" 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/main.py b/app/main.py index 5b4694c..34b287b 100644 --- a/app/main.py +++ b/app/main.py @@ -105,7 +105,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: app = FastAPI( title="SillyHome Next API", description="Lokales Smart-Home-Intelligenzsystem für Home Assistant.", - version="0.7.16", + version="0.7.17", lifespan=lifespan, ) app.state.settings = load_settings() @@ -211,7 +211,11 @@ async def _ha_event_listener(app: FastAPI, client: HaClient) -> None: if ws_status is not None: ws_status.status = "connecting" try: - async with websockets.connect(ws_url, ping_interval=None) as websocket: + async with websockets.connect( + ws_url, + ping_interval=20, + ping_timeout=10, + ) as websocket: auth_required_msg = await websocket.recv() auth_required_data = json.loads(auth_required_msg) if auth_required_data.get("type") != "auth_required": @@ -263,6 +267,8 @@ async def _ha_event_listener(app: FastAPI, client: HaClient) -> None: continue new_state = event_data.get("new_state") _update_ha_state_cache(state_cache, entity_id, new_state) + if not _is_relevant_state_change(store, str(entity_id)): + continue # Prüfe, ob Entity ein Aktor oder relevanter Kontext ist # Sofortige Vorhersage für betroffene Aktoren auslösen await asyncio.to_thread( @@ -280,17 +286,17 @@ async def _ha_event_listener(app: FastAPI, client: HaClient) -> None: websockets.exceptions.InvalidStatus, OSError, ) as exc: - logger.warning("WebSocket-Verbindung unterbrochen: %s. Wiederholung in 5s...", exc) + logger.warning("WebSocket-Verbindung unterbrochen: %s. Wiederholung in 1s...", exc) if ws_status is not None: ws_status.status = "reconnecting" ws_status.error = str(exc) - await asyncio.sleep(5) + await asyncio.sleep(1) except Exception as exc: logger.exception("Unerwarteter Fehler im Event-Listener: %s", exc) if ws_status is not None: ws_status.status = "error" ws_status.error = str(exc) - await asyncio.sleep(5) + await asyncio.sleep(1) # Fallback: periodische Vorhersage falls Event-Stream ausfällt @@ -341,6 +347,17 @@ def _update_ha_state_cache( ) +def _is_relevant_state_change(store: ActuatorStore, entity_id: str) -> bool: + for record in store.list(): + if record.actuator_entity_id == entity_id: + return True + if record.assignment.selected_numeric_entity_id == entity_id: + return True + if entity_id in record.assignment.selected_context_entity_ids: + return True + return False + + def _ha_entity_from_event( entity_id: str, new_state: dict[str, object], diff --git a/pyproject.toml b/pyproject.toml index a781e4a..a5546f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "sillyhome-next" -version = "0.7.16" +version = "0.7.17" description = "Lokales Smart-Home-Intelligenzsystem für Home Assistant" requires-python = ">=3.11" dependencies = [ diff --git a/tests/test_main.py b/tests/test_main.py index c1c4ff0..994aa15 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -94,7 +94,8 @@ def test_ha_event_listener_processes_state_change(tmp_path: Path) -> None: connect.assert_called_once_with( "ws://homeassistant:8123/api/websocket", - ping_interval=None, + ping_interval=20, + ping_timeout=10, ) assert fake_ws.sent == [ {"type": "auth", "access_token": "test-token"}, @@ -110,6 +111,7 @@ def test_ha_event_listener_processes_state_change(tmp_path: Path) -> None: mock_app.state.behavior_engine = mock_engine mock_app.state.ha_reader = _FakeHaReader() mock_store = ActuatorStore(tmp_path / "store") + mock_store.configure("light.test") mock_app.state.actuator_store = mock_store mock_client = MagicMock()