""" JH Photomaton — Interface de gestion Raspberry Pi 4 / Les Sapins Du Web (LSDW) Remplace Node-RED pour : - Contrôle bouton GPIO23 (multi-clic + long press) + relay GPIO12 - LEDs WS2812b GPIO18 (35 LEDs) — effets selon état - Galerie publique et galerie admin - File d'attente d'impression (SQLite) - Dashboard admin avec monitoring système Port : 8090 (configurable dans config/settings.yaml) """ import asyncio import logging from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.staticfiles import StaticFiles from starlette.middleware.sessions import SessionMiddleware from backend.services.config_service import ConfigService from backend.services.led_service import LEDService from backend.services.button_service import ButtonService from backend.services.photobooth_service import PhotoboothService from backend.services.printer_service import PrinterService from backend.services.system_service import SystemService from backend.utils.ws_manager import WSManager from backend.api import ( gallery, admin_api, admin_gallery_api, leds_api, system_api, print_api, actions_api, webhooks, ) logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) logger = logging.getLogger(__name__) BASE_DIR = Path(__file__).parent @asynccontextmanager async def lifespan(app: FastAPI): # ── Chargement config ─────────────────────────────────────────────────── config_svc = ConfigService(BASE_DIR / "config" / "settings.yaml") config = config_svc.load() # ── Services ──────────────────────────────────────────────────────────── ws_manager = WSManager() photobooth_svc = PhotoboothService(config) system_svc = SystemService() printer_svc = PrinterService(config) await printer_svc.init_db(BASE_DIR / "data" / "print_queue.db") # LEDs led_svc = LEDService(config) led_svc.start() # Bouton GPIO — passe le loop asyncio pour les callbacks loop = asyncio.get_event_loop() led_svc.set_loop(loop) led_svc.set_on_change(ws_manager.broadcast) button_svc = ButtonService(config, led_svc, photobooth_svc, ws_manager, loop) button_svc.start() # ── État partagé dans l'app ────────────────────────────────────────────── app.state.config = config app.state.config_service = config_svc app.state.ws_manager = ws_manager app.state.led_service = led_svc app.state.button_service = button_svc app.state.photobooth_service = photobooth_svc app.state.printer_service = printer_svc app.state.system_service = system_svc # ── Tâche de fond : broadcast stats système ────────────────────────────── stats_task = asyncio.create_task(_system_stats_loop(system_svc, ws_manager)) logger.info("JH Photomaton démarré — port %d", config.app.port) led_svc.play("idle") yield # ← l'app tourne ici # ── Arrêt propre ───────────────────────────────────────────────────────── stats_task.cancel() button_svc.stop() led_svc.stop() await photobooth_svc.close() await printer_svc.close() logger.info("JH Photomaton arrêté") async def _system_stats_loop(sys_svc: SystemService, ws: WSManager): """Broadcast des stats système toutes les 5 secondes.""" while True: try: stats = sys_svc.get_stats() await ws.broadcast({"type": "system_stats", "data": stats}) except Exception as e: logger.debug("Stats loop: %s", e) await asyncio.sleep(5) # ── Application FastAPI ────────────────────────────────────────────────────── app = FastAPI( title="JH Photomaton", description="Interface de gestion du photomaton — Les Sapins Du Web", version="1.0.0", lifespan=lifespan, ) app.add_middleware( SessionMiddleware, secret_key="jh-photomaton-session-2026", # remplacé par config au démarrage max_age=86400, # 24h ) # Fichiers statiques app.mount( "/static", StaticFiles(directory=BASE_DIR / "frontend" / "static"), name="static", ) # Routes app.include_router(gallery.router) app.include_router(admin_api.router) app.include_router(admin_gallery_api.router) app.include_router(leds_api.router, prefix="/api") app.include_router(system_api.router, prefix="/api") app.include_router(print_api.router, prefix="/api") app.include_router(actions_api.router, prefix="/api") app.include_router(webhooks.router, prefix="/api") # ── WebSocket ──────────────────────────────────────────────────────────────── @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): ws_manager: WSManager = websocket.app.state.ws_manager await ws_manager.connect(websocket) # Envoie l'état initial à la connexion try: led = websocket.app.state.led_service sys_svc = websocket.app.state.system_service await ws_manager.send(websocket, {"type": "connected", "led_effect": led.current_effect}) await ws_manager.send(websocket, {"type": "system_stats", "data": sys_svc.get_stats()}) except Exception: pass try: while True: data = await websocket.receive_text() # Commandes entrantes du client (optionnel) import json try: msg = json.loads(data) if msg.get("type") == "ping": await ws_manager.send(websocket, {"type": "pong"}) except Exception: pass except WebSocketDisconnect: ws_manager.disconnect(websocket) # ── Point d'entrée ─────────────────────────────────────────────────────────── if __name__ == "__main__": import uvicorn cfg = ConfigService(BASE_DIR / "config" / "settings.yaml").load() uvicorn.run( "main:app", host=cfg.app.host, port=cfg.app.port, reload=cfg.app.debug, log_level="info", )