195 lines
6.6 KiB
Python
195 lines
6.6 KiB
Python
"""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")
|