"""JH Photomaton -- Interface de gestion (Les Sapins Du Web).""" 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.event_service import EventService 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, event_api, ) 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): config_svc = ConfigService(BASE_DIR / "config" / "settings.yaml") config = config_svc.load() ws_manager = WSManager() photobooth_svc = PhotoboothService(config) system_svc = SystemService() printer_svc = PrinterService(config) event_svc = EventService() await printer_svc.init_db(BASE_DIR / "data" / "print_queue.db") await event_svc.init_db(BASE_DIR / "data" / "events.db") from datetime import datetime as _dt ev = config.event if not ev.started_at: ev.started_at = _dt.now().timestamp() await event_svc.ensure_event(ev.slug, ev.name, ev.started_at) led_svc = LEDService(config) led_svc.start() 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() # Injection du service LED dans le printer_service (pour LED après impression background) printer_svc.set_led_service(led_svc) 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 app.state.event_service = event_svc stats_task = asyncio.create_task(_system_stats_loop(system_svc, ws_manager)) printer_task = asyncio.create_task(_printer_monitor_loop(printer_svc, led_svc, ws_manager)) logger.info("JH Photomaton demarre -- port %d", config.app.port) led_svc.play("idle") yield stats_task.cancel() printer_task.cancel() button_svc.stop() led_svc.stop() await photobooth_svc.close() await printer_svc.close() await event_svc.close() logger.info("JH Photomaton arrete") async def _printer_monitor_loop(printer_svc: PrinterService, led_svc: LEDService, ws: WSManager): """Surveille les imprimantes toutes les 30s et joue l'effet error si une est en panne.""" # Effets qui ne doivent pas être écrasés par le moniteur ACTIVE_EFFECTS = {"countdown", "capture", "captured", "printing", "finished"} await asyncio.sleep(15) # délai initial pour laisser les services démarrer while True: try: statuses = await printer_svc.get_printers_status() has_error = any( s.get("state") in ("disabled", "error", "offline") or bool(s.get("reasons")) for s in statuses ) current = led_svc.current_effect if current not in ACTIVE_EFFECTS: if has_error and current != "error": led_svc.play("error") await ws.broadcast({"type": "led_effect", "effect": "error"}) logger.info("Moniteur imprimante: erreur détectée → LED error") elif not has_error and current == "error": led_svc.play("idle") await ws.broadcast({"type": "led_effect", "effect": "idle"}) logger.info("Moniteur imprimante: erreur résolue → LED idle") except Exception as e: logger.debug("Printer monitor: %s", e) await asyncio.sleep(30) async def _system_stats_loop(sys_svc: SystemService, ws: WSManager): 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) 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", max_age=86400, ) app.mount("/static", StaticFiles(directory=BASE_DIR / "frontend" / "static"), name="static") 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") app.include_router(event_api.router, prefix="/api") @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): ws_manager: WSManager = websocket.app.state.ws_manager await ws_manager.connect(websocket) 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() 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) 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")