""" Demeter Server — FastAPI Application Entry point for the Demeter IoT management server. Manages the lifecycle of the aiocoap observer client and FastAPI HTTP server on a shared asyncio event loop. Run with: cd server uvicorn app.main:app --reload --port 8000 """ from __future__ import annotations import asyncio import logging from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.responses import Response from .coap_bridge import router as coap_bridge_router from .coap_observer import CoapObserverClient from .config import Settings, load_devices_config from .dashboard import router as dashboard_router from .device_store import DeviceStore from .devices_api import router as devices_api_router from .logging_config import setup_logging from .metrics import MetricsCollector logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): """ Manage startup and shutdown of the CoAP observer client. Startup: 1. Configure logging (console + optional Loki) 2. Load device registry from YAML 3. Initialize the in-memory store 4. Create the aiocoap client context 5. Subscribe to all enabled device resources Shutdown: 1. Cancel all CoAP subscriptions 2. Close the aiocoap context """ settings: Settings = app.state.settings metrics: MetricsCollector = app.state.metrics # ── Logging ── setup_logging(loki_url=settings.loki_url, debug=settings.debug) logger.info("=" * 50) logger.info(" Demeter IoT Server v%s", settings.app_version) logger.info("=" * 50) # ── Device Config ── devices_config = load_devices_config(settings.devices_config_path) enabled = [d for d in devices_config.devices if d.enabled] logger.info("Loaded %d devices (%d enabled)", len(devices_config.devices), len(enabled)) # ── Store ── store = DeviceStore(devices_config) app.state.store = store # ── Metrics ── metrics.set_server_info(settings.app_version) # ── CoAP Observer ── observer = CoapObserverClient( store=store, metrics=metrics, request_timeout=settings.coap_request_timeout, reconnect_base=settings.coap_reconnect_base, reconnect_max=settings.coap_reconnect_max, ) app.state.observer = observer await observer.startup() # Subscribe to all enabled device resources for device in enabled: for resource in device.resources: await observer.subscribe( device_id=device.id, ip=device.ip, port=device.port, resource_uri=resource.uri, ) total_subs = sum(len(d.resources) for d in enabled) logger.info("Subscribed to %d resources across %d devices", total_subs, len(enabled)) metrics.set_active_subscriptions(total_subs) logger.info("Server ready — dashboard at http://%s:%d/dashboard", settings.host, settings.port) yield # ── Shutdown ── logger.info("Shutting down...") await observer.shutdown() logger.info("Shutdown complete") def create_app() -> FastAPI: """Create and configure the FastAPI application.""" settings = Settings() metrics = MetricsCollector() app = FastAPI( title=settings.app_title, version=settings.app_version, lifespan=lifespan, ) # Store settings and metrics on app state (available before lifespan) app.state.settings = settings app.state.metrics = metrics # ── Routers ── app.include_router(devices_api_router) app.include_router(coap_bridge_router) app.include_router(dashboard_router) # ── Prometheus /metrics ── @app.get("/metrics", tags=["monitoring"], include_in_schema=False) async def prometheus_metrics(): return Response( content=metrics.generate(), media_type="text/plain; version=0.0.4; charset=utf-8", ) return app # Module-level app instance for uvicorn app = create_app()