ACT-001: actuator-first sensor lifecycle

This commit is contained in:
2026-06-13 22:45:07 +02:00
parent d6631fe752
commit 6305f52cd2
30 changed files with 2010 additions and 103 deletions

View File

@@ -1,4 +1,5 @@
from contextlib import asynccontextmanager
import asyncio
from contextlib import asynccontextmanager, suppress
from collections.abc import AsyncIterator
from pathlib import Path
from typing import cast
@@ -7,6 +8,9 @@ from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from app.actuators.lifecycle import ActuatorReconciliationService
from app.actuators.store import ActuatorStore
from app.api.v1.actuators import router as actuators_router
from app.api.v1.entities import router as entities_router
from app.api.v1.automations import router as automations_router
from app.automations.store import AutomationStore
@@ -22,10 +26,14 @@ from backend.routes.ml import init_ml_routes
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
settings = app.state.settings
client: HaClient | None = None
reconcile_task: asyncio.Task[None] | None = None
app.state.registry = ModelRegistry(settings.model_store)
app.state.automation_store = AutomationStore(settings.automation_store)
app.state.actuator_store = ActuatorStore(settings.actuator_store)
if hasattr(app.state, "ha_reader"):
del app.state.ha_reader
if hasattr(app.state, "actuator_service"):
del app.state.actuator_service
if settings.ha_configured:
client = HaClient(
settings=HaClientSettings(
@@ -34,9 +42,21 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
)
)
app.state.ha_reader = HaReader(client=client)
app.state.actuator_service = ActuatorReconciliationService(
ha_reader=app.state.ha_reader,
store=app.state.actuator_store,
registry=app.state.registry,
settings=settings,
)
await asyncio.to_thread(app.state.actuator_service.reconcile_all, "startup")
reconcile_task = asyncio.create_task(_periodic_reconciliation(app))
try:
yield
finally:
if reconcile_task is not None:
reconcile_task.cancel()
with suppress(asyncio.CancelledError):
await reconcile_task
if client is not None:
client.close()
@@ -44,13 +64,14 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
app = FastAPI(
title="SillyHome Next API",
description="Lokales Smart-Home-Intelligenzsystem für Home Assistant.",
version="0.3.0",
version="0.4.0",
lifespan=lifespan,
)
app.state.settings = load_settings()
register_exception_handlers(app)
app.include_router(entities_router)
app.include_router(automations_router)
app.include_router(actuators_router)
init_ml_routes(app, model_store=app.state.settings.model_store)
STATIC_DIR = Path(__file__).with_name("static")
@@ -65,3 +86,12 @@ def health() -> dict[str, str]:
@app.get("/")
def root() -> FileResponse:
return FileResponse(STATIC_DIR / "index.html")
async def _periodic_reconciliation(app: FastAPI) -> None:
while True:
await asyncio.sleep(app.state.settings.reconcile_interval_seconds)
service = getattr(app.state, "actuator_service", None)
if not isinstance(service, ActuatorReconciliationService):
continue
await asyncio.to_thread(service.reconcile_all, "scheduled")