122 lines
4.4 KiB
Python
122 lines
4.4 KiB
Python
import asyncio
|
|
from contextlib import asynccontextmanager, suppress
|
|
from collections.abc import AsyncIterator
|
|
from pathlib import Path
|
|
from typing import cast
|
|
|
|
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.behavior.engine import BehaviorEngine
|
|
from app.config import load_settings
|
|
from app.core.exception_handlers import register_exception_handlers
|
|
from app.ha.client import HaClient, HaClientSettings
|
|
from app.ha.reader import HaReader
|
|
from app.ml.registry.model_registry import ModelRegistry
|
|
from backend.routes.ml import init_ml_routes
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
settings = app.state.settings
|
|
client: HaClient | None = None
|
|
reconcile_task: asyncio.Task[None] | None = None
|
|
prediction_task: asyncio.Task[None] | None = None
|
|
app.state.registry = ModelRegistry(settings.model_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 hasattr(app.state, "behavior_engine"):
|
|
del app.state.behavior_engine
|
|
if settings.ha_configured:
|
|
client = HaClient(
|
|
settings=HaClientSettings(
|
|
url=cast(str, settings.ha_url),
|
|
token=cast(str, settings.ha_token),
|
|
)
|
|
)
|
|
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,
|
|
)
|
|
app.state.behavior_engine = BehaviorEngine(
|
|
ha_reader=app.state.ha_reader,
|
|
store=app.state.actuator_store,
|
|
settings=settings,
|
|
)
|
|
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)
|
|
reconcile_task = asyncio.create_task(_periodic_reconciliation(app))
|
|
prediction_task = asyncio.create_task(_periodic_prediction(app))
|
|
try:
|
|
yield
|
|
finally:
|
|
if reconcile_task is not None:
|
|
reconcile_task.cancel()
|
|
with suppress(asyncio.CancelledError):
|
|
await reconcile_task
|
|
if prediction_task is not None:
|
|
prediction_task.cancel()
|
|
with suppress(asyncio.CancelledError):
|
|
await prediction_task
|
|
if client is not None:
|
|
client.close()
|
|
|
|
|
|
app = FastAPI(
|
|
title="SillyHome Next API",
|
|
description="Lokales Smart-Home-Intelligenzsystem für Home Assistant.",
|
|
version="0.5.2",
|
|
lifespan=lifespan,
|
|
)
|
|
app.state.settings = load_settings()
|
|
register_exception_handlers(app)
|
|
app.include_router(entities_router)
|
|
app.include_router(actuators_router)
|
|
init_ml_routes(app, model_store=app.state.settings.model_store)
|
|
|
|
STATIC_DIR = Path(__file__).with_name("static")
|
|
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
|
|
|
|
|
@app.get("/health")
|
|
def health() -> dict[str, str]:
|
|
return {"status": "ok"}
|
|
|
|
|
|
@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")
|
|
engine = getattr(app.state, "behavior_engine", None)
|
|
if isinstance(engine, BehaviorEngine):
|
|
await asyncio.to_thread(engine.train_all)
|
|
|
|
|
|
async def _periodic_prediction(app: FastAPI) -> None:
|
|
while True:
|
|
await asyncio.sleep(app.state.settings.prediction_interval_seconds)
|
|
engine = getattr(app.state, "behavior_engine", None)
|
|
if not isinstance(engine, BehaviorEngine):
|
|
continue
|
|
await asyncio.to_thread(engine.evaluate_all)
|