Files
HomeOS/src/mqtt_home/main.py
walkpan 38766ca792 feat: production build with FastAPI static file serving for SPA
- Add StaticFiles mount for /assets
- Add catch-all route serving index.html for SPA client routing
- Frontend build: 101KB gzipped total

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-29 22:05:56 +08:00

105 lines
3.2 KiB
Python

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"))