diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fddb86..a3fd4e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.7.15 - 2026-06-16 +- Add-on-Start ist robust gegen Home-Assistant-Core-502 beim Systemboot: + API und WebSocket-Listener starten trotzdem, Reconciliation/Training werden + im Hintergrund mit Retry nachgeholt. +- Periodische Reconciliation und Fallback-Auswertung beenden den Dienst nicht + mehr bei temporären HA-Fehlern. +- Add-on-Watchdog prüft `/health`, damit Supervisor den Dienst nach Absturz + wieder starten kann. + ## 0.7.14 - 2026-06-16 - Onboarding-Vorschläge laden im Dashboard nachgelagert, damit Status, Aktor-Auswahl und bestehende Geräte nicht auf Automation-Discovery warten. diff --git a/addon/config.yaml b/addon/config.yaml index 6c78708..b16adba 100644 --- a/addon/config.yaml +++ b/addon/config.yaml @@ -1,5 +1,5 @@ name: SillyHome Next -version: "0.7.14" +version: "0.7.15" slug: sillyhome_next description: Lernt automatisch aus deinem Verhalten und steuert freigegebene Aktoren url: http://192.168.6.31:3000/pino/sillyhome-next @@ -7,6 +7,7 @@ arch: - amd64 startup: application boot: auto +watchdog: http://[HOST]:[PORT:8000]/health init: false ingress: true ingress_port: 8000 diff --git a/app/main.py b/app/main.py index a2d5d31..0487fed 100644 --- a/app/main.py +++ b/app/main.py @@ -43,6 +43,7 @@ class _WsStatus: async def lifespan(app: FastAPI) -> AsyncIterator[None]: settings = app.state.settings client: HaClient | None = None + startup_task: asyncio.Task[None] | None = None reconcile_task: asyncio.Task[None] | None = None event_listener_task: asyncio.Task[None] | None = None fallback_task: asyncio.Task[None] | None = None @@ -74,15 +75,17 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: settings=settings, ) app.state.ws_status = _WsStatus() - await asyncio.to_thread(app.state.actuator_service.reconcile_all, "startup") - await asyncio.to_thread(app.state.behavior_engine.train_all) - await asyncio.to_thread(app.state.behavior_engine.evaluate_all) + startup_task = asyncio.create_task(_startup_reconciliation(app)) reconcile_task = asyncio.create_task(_periodic_reconciliation(app)) event_listener_task = asyncio.create_task(_ha_event_listener(app, client)) fallback_task = asyncio.create_task(_fallback_prediction(app)) try: yield finally: + if startup_task is not None: + startup_task.cancel() + with suppress(asyncio.CancelledError): + await startup_task if reconcile_task is not None: reconcile_task.cancel() with suppress(asyncio.CancelledError): @@ -102,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.14", + version="0.7.15", lifespan=lifespan, ) app.state.settings = load_settings() @@ -147,10 +150,39 @@ async def _periodic_reconciliation(app: FastAPI) -> None: service = getattr(app.state, "actuator_service", None) if not isinstance(service, ActuatorReconciliationService): continue - await asyncio.to_thread(service.reconcile_all, "scheduled") + try: + await asyncio.to_thread(service.reconcile_all, "scheduled") + engine = getattr(app.state, "behavior_engine", None) + if isinstance(engine, BehaviorEngine): + await asyncio.to_thread(engine.train_all) + except Exception: + logger.exception("Geplante Reconciliation fehlgeschlagen; nächster Lauf versucht es erneut.") + + +async def _startup_reconciliation(app: FastAPI) -> None: + delay_seconds = 5 + while True: + service = getattr(app.state, "actuator_service", None) engine = getattr(app.state, "behavior_engine", None) - if isinstance(engine, BehaviorEngine): + if not isinstance(service, ActuatorReconciliationService) or not isinstance( + engine, + BehaviorEngine, + ): + return + try: + await asyncio.to_thread(service.reconcile_all, "startup") await asyncio.to_thread(engine.train_all) + await asyncio.to_thread(engine.evaluate_all) + logger.info("Startup-Reconciliation erfolgreich abgeschlossen.") + return + except Exception as exc: + logger.warning( + "Startup-Reconciliation verschoben: %s. Neuer Versuch in %ss.", + exc, + delay_seconds, + ) + await asyncio.sleep(delay_seconds) + delay_seconds = min(delay_seconds * 2, 60) async def _ha_event_listener(app: FastAPI, client: HaClient) -> None: @@ -243,7 +275,11 @@ async def _ha_event_listener(app: FastAPI, client: HaClient) -> None: logger.warning("Ungültige JSON-Nachricht von HA-WebSocket") except Exception as exc: logger.exception("Fehler bei Event-Verarbeitung: %s", exc) - except (websockets.exceptions.ConnectionClosed, OSError) as exc: + except ( + websockets.exceptions.ConnectionClosed, + websockets.exceptions.InvalidStatus, + OSError, + ) as exc: logger.warning("WebSocket-Verbindung unterbrochen: %s. Wiederholung in 5s...", exc) if ws_status is not None: ws_status.status = "reconnecting" @@ -280,7 +316,10 @@ async def _fallback_prediction(app: FastAPI) -> None: "Fallback-Vorhersage aktiv (WebSocket-Status: %s)", ws_status.status if ws_status else "unavailable", ) - await asyncio.to_thread(engine.evaluate_all) + try: + await asyncio.to_thread(engine.evaluate_all) + except Exception: + logger.exception("Fallback-Vorhersage fehlgeschlagen.") def _load_ha_state_cache(reader: HaReader) -> dict[str, HaEntitySummary]: diff --git a/pyproject.toml b/pyproject.toml index 67ccf6d..a189fc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "sillyhome-next" -version = "0.7.14" +version = "0.7.15" description = "Lokales Smart-Home-Intelligenzsystem für Home Assistant" requires-python = ">=3.11" dependencies = [