Files
photoBooth/main.py
T
admin 94def589bd
🚀 Deploy — JH Photomaton / 🔍 Vérification (push) Has been cancelled
🚀 Deploy — JH Photomaton / 🍓 Deploy sur le Pi (push) Has been cancelled
fix: mode descriptions, bordure impression (remove raw+extent), logs httpx silencieux
2026-07-17 18:00:00 +02:00

200 lines
7.0 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",
)
# httpx log tous les appels HTTP en INFO — trop verbeux pour nos probes internes
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
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()
# "offline" = imprimante absente du réseau → normal si non branchée
# On alerte uniquement si l'imprimante est joignable MAIS en panne
# (state=disabled/stopped avec des raisons d'erreur concrètes)
has_error = any(
s.get("state") in ("disabled", "error") and 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")