import asyncio import logging import sys import pathlib from contextlib import asynccontextmanager # Windows compatibility: use SelectorEventLoop for paho-mqtt (add_reader/add_writer) if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from mqtt_home.config import get_settings from mqtt_home.database import init_db, get_session_factory, Base from mqtt_home.mqtt_client import MqttClient from mqtt_home.emqx_api import EmqxApiClient from mqtt_home.discovery import handle_discovery_message from mqtt_home.device_registry import handle_state_update from mqtt_home.api import api_router from mqtt_home.ws import websocket_endpoint, broadcast_device_update logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): settings = get_settings() await init_db() logger.info("Database initialized") emqx = EmqxApiClient(settings) app.state.emqx_client = emqx logger.info("EMQX API client initialized") mqtt = MqttClient(settings) app.state.mqtt_client = mqtt session_factory = get_session_factory() async def on_discovery(topic: str, payload: str): async with session_factory() as db: await handle_discovery_message(topic, payload, db, mqtt) async def on_state(topic: str, payload: str): async with session_factory() as db: device = await handle_state_update(db, topic, payload) if device: await broadcast_device_update(device.id, { "state": device.state, "is_online": device.is_online, "last_seen": device.last_seen.isoformat() if device.last_seen else None, }) mqtt.on_message("homeassistant/#", on_discovery) mqtt.on_message("home/#", on_state) await mqtt.start() logger.info("MQTT client started") yield await mqtt.stop() await emqx.close() logger.info("Shutdown complete") app = FastAPI(title="MQTT Home", version="0.1.0", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(api_router) app.websocket("/ws/devices")(websocket_endpoint) @app.get("/health") async def health(): mqtt = getattr(app.state, "mqtt_client", None) return { "status": "ok", "mqtt_connected": mqtt.is_connected if mqtt else False, } # Serve frontend static files in production _frontend_dist = pathlib.Path(__file__).parent.parent.parent / "frontend" / "dist" if _frontend_dist.exists(): app.mount("/assets", StaticFiles(directory=str(_frontend_dist / "assets")), name="assets") @app.get("/{full_path:path}") async def serve_spa(full_path: str): file = _frontend_dist / full_path if file.exists() and file.is_file(): return FileResponse(str(file)) return FileResponse(str(_frontend_dist / "index.html"))