141 lines
4.0 KiB
Python
141 lines
4.0 KiB
Python
"""
|
|
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()
|