- 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>
105 lines
3.2 KiB
Python
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"))
|