first commit
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
"""API de gestion des actions photobooth-app + mapping bouton."""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Request, Body
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/actions/photobooth")
|
||||
async def get_pb_actions(request: Request):
|
||||
"""Retourne les actions image de photobooth-app + le mapping bouton actuel."""
|
||||
pb = request.app.state.photobooth_service
|
||||
cfg = request.app.state.config
|
||||
|
||||
pb_config = await pb.read_pb_config()
|
||||
actions = pb_config.get("actions", {}).get("image", [])
|
||||
|
||||
# Enrichit avec l'index
|
||||
for i, action in enumerate(actions):
|
||||
action["_index"] = i
|
||||
|
||||
return {
|
||||
"actions": actions,
|
||||
"button_mapping": cfg.button_actions,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/actions/assets")
|
||||
async def get_assets(request: Request):
|
||||
"""Liste les cadres et fonds disponibles dans userdata."""
|
||||
pb = request.app.state.photobooth_service
|
||||
frames = await pb.list_userdata_frames()
|
||||
backgrounds = await pb.list_userdata_backgrounds()
|
||||
return {"frames": frames, "backgrounds": backgrounds}
|
||||
|
||||
|
||||
@router.put("/actions/mapping")
|
||||
async def update_button_mapping(
|
||||
request: Request,
|
||||
mapping: dict = Body(...),
|
||||
):
|
||||
"""
|
||||
Met à jour le mapping clics → actions.
|
||||
Body: { "1": {"label": "...", "photobooth_index": 0}, ... }
|
||||
"""
|
||||
config_svc = request.app.state.config_service
|
||||
|
||||
# Validation basique
|
||||
for k, v in mapping.items():
|
||||
if not isinstance(v, dict) or "photobooth_index" not in v:
|
||||
return JSONResponse({"error": f"Format invalide pour clic {k}"}, status_code=400)
|
||||
|
||||
config_svc.save_button_actions(mapping)
|
||||
request.app.state.config.button_actions = mapping
|
||||
|
||||
# Met aussi à jour le button_service
|
||||
btn = request.app.state.button_service
|
||||
btn._btn_actions = mapping
|
||||
|
||||
logger.info("Mapping bouton mis à jour: %s", mapping)
|
||||
return {"ok": True, "mapping": mapping}
|
||||
|
||||
|
||||
@router.put("/actions/photobooth/{index}")
|
||||
async def update_pb_action(
|
||||
request: Request,
|
||||
index: int,
|
||||
updates: dict = Body(...),
|
||||
):
|
||||
"""
|
||||
Met à jour une action image de photobooth-app (cadre, fond, countdown, etc.)
|
||||
updates peut contenir: countdown_capture, img_frame_file, img_background_file,
|
||||
remove_background, image_filter, name
|
||||
"""
|
||||
pb = request.app.state.photobooth_service
|
||||
pb_config = await pb.read_pb_config()
|
||||
actions = pb_config.get("actions", {}).get("image", [])
|
||||
|
||||
if index < 0 or index >= len(actions):
|
||||
return JSONResponse({"error": f"Index {index} invalide (max: {len(actions)-1})"}, status_code=400)
|
||||
|
||||
action = actions[index]
|
||||
|
||||
# Mise à jour des champs autorisés
|
||||
allowed_root = {"name"}
|
||||
allowed_processing = {"remove_background", "img_frame_file", "img_background_file",
|
||||
"image_filter", "fill_background_enable", "fill_background_color",
|
||||
"img_background_enable", "texts_enable"}
|
||||
allowed_jobcontrol = {"countdown_capture"}
|
||||
|
||||
for key, value in updates.items():
|
||||
if key in allowed_root:
|
||||
action[key] = value
|
||||
elif key in allowed_processing:
|
||||
action.setdefault("processing", {})[key] = value
|
||||
elif key in allowed_jobcontrol:
|
||||
action.setdefault("jobcontrol", {})[key] = value
|
||||
|
||||
pb_config["actions"]["image"][index] = action
|
||||
await pb.write_pb_config(pb_config)
|
||||
|
||||
logger.info("Action %d mise à jour: %s", index, list(updates.keys()))
|
||||
return {"ok": True, "index": index, "action": action}
|
||||
|
||||
|
||||
@router.post("/actions/trigger/{index}")
|
||||
async def trigger_action(request: Request, index: int):
|
||||
"""Déclenche une action directement (test)."""
|
||||
pb = request.app.state.photobooth_service
|
||||
led = request.app.state.led_service
|
||||
btn = request.app.state.button_service
|
||||
btn.relay_off()
|
||||
try:
|
||||
result = await pb.trigger_image_action(index)
|
||||
return {"ok": True, "index": index, "result": result}
|
||||
except Exception as e:
|
||||
led.play("error")
|
||||
btn.relay_on()
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Routes du dashboard admin — authentification requise."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Request, Form
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
_templates = Jinja2Templates(
|
||||
directory=Path(__file__).parent.parent.parent / "frontend" / "templates"
|
||||
)
|
||||
|
||||
|
||||
def _is_auth(request: Request) -> bool:
|
||||
return request.session.get("authenticated") is True
|
||||
|
||||
|
||||
def _require_auth(request: Request):
|
||||
if not _is_auth(request):
|
||||
return RedirectResponse(url="/admin/login", status_code=302)
|
||||
return None
|
||||
|
||||
|
||||
# ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/admin/login", response_class=HTMLResponse)
|
||||
async def login_page(request: Request):
|
||||
if _is_auth(request):
|
||||
return RedirectResponse(url="/admin", status_code=302)
|
||||
return _templates.TemplateResponse("admin/login.html", {"request": request, "error": None})
|
||||
|
||||
|
||||
@router.post("/admin/login")
|
||||
async def login(request: Request, password: str = Form(...)):
|
||||
cfg = request.app.state.config
|
||||
if password == cfg.app.admin_password:
|
||||
request.session["authenticated"] = True
|
||||
return RedirectResponse(url="/admin", status_code=302)
|
||||
return _templates.TemplateResponse(
|
||||
"admin/login.html",
|
||||
{"request": request, "error": "Mot de passe incorrect"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/admin/logout")
|
||||
async def logout(request: Request):
|
||||
request.session.clear()
|
||||
return RedirectResponse(url="/admin/login", status_code=302)
|
||||
|
||||
|
||||
# ── Pages admin ───────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/admin", response_class=HTMLResponse)
|
||||
async def admin_dashboard(request: Request):
|
||||
redirect = _require_auth(request)
|
||||
if redirect:
|
||||
return redirect
|
||||
|
||||
sys_svc = request.app.state.system_service
|
||||
cfg = request.app.state.config
|
||||
led = request.app.state.led_service
|
||||
btn = request.app.state.button_service
|
||||
|
||||
stats = sys_svc.get_stats()
|
||||
services = sys_svc.get_services_status()
|
||||
|
||||
return _templates.TemplateResponse("admin/dashboard.html", {
|
||||
"request": request,
|
||||
"config": cfg,
|
||||
"stats": stats,
|
||||
"services": services,
|
||||
"led_effect": led.current_effect,
|
||||
"relay_state": btn.relay_state if btn else True,
|
||||
"print_mode": cfg.print.mode,
|
||||
})
|
||||
|
||||
|
||||
@router.get("/admin/gallery", response_class=HTMLResponse)
|
||||
async def admin_gallery(request: Request):
|
||||
redirect = _require_auth(request)
|
||||
if redirect:
|
||||
return redirect
|
||||
cfg = request.app.state.config
|
||||
return _templates.TemplateResponse("admin/gallery.html", {
|
||||
"request": request,
|
||||
"config": cfg,
|
||||
})
|
||||
|
||||
|
||||
@router.get("/admin/actions", response_class=HTMLResponse)
|
||||
async def admin_actions(request: Request):
|
||||
redirect = _require_auth(request)
|
||||
if redirect:
|
||||
return redirect
|
||||
|
||||
pb = request.app.state.photobooth_service
|
||||
cfg = request.app.state.config
|
||||
pb_config = await pb.read_pb_config()
|
||||
actions = pb_config.get("actions", {}).get("image", [])
|
||||
|
||||
return _templates.TemplateResponse("admin/actions.html", {
|
||||
"request": request,
|
||||
"config": cfg,
|
||||
"pb_actions": actions,
|
||||
"button_mapping": cfg.button_actions,
|
||||
})
|
||||
|
||||
|
||||
@router.get("/admin/print", response_class=HTMLResponse)
|
||||
async def admin_print(request: Request):
|
||||
redirect = _require_auth(request)
|
||||
if redirect:
|
||||
return redirect
|
||||
|
||||
printer_svc = request.app.state.printer_service
|
||||
cfg = request.app.state.config
|
||||
|
||||
queue = await printer_svc.get_queue()
|
||||
printers = await printer_svc.get_printers_status()
|
||||
|
||||
return _templates.TemplateResponse("admin/print.html", {
|
||||
"request": request,
|
||||
"config": cfg,
|
||||
"queue": queue,
|
||||
"printers": printers,
|
||||
})
|
||||
@@ -0,0 +1,122 @@
|
||||
"""API galerie admin — impression et suppression de photos."""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Request, Query
|
||||
from fastapi.responses import JSONResponse, RedirectResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/admin/api/gallery/photos")
|
||||
async def admin_get_photos(
|
||||
request: Request,
|
||||
page: int = Query(default=1, ge=1),
|
||||
limit: int = Query(default=24, ge=1, le=100),
|
||||
):
|
||||
"""Liste des photos pour la galerie admin."""
|
||||
if not request.session.get("authenticated"):
|
||||
return JSONResponse({"error": "Non authentifié"}, status_code=401)
|
||||
|
||||
pb = request.app.state.photobooth_service
|
||||
all_photos = await pb.get_media_collection(limit=500)
|
||||
photos = [p for p in all_photos if _is_image(p)]
|
||||
|
||||
total = len(photos)
|
||||
start = (page - 1) * limit
|
||||
page_photos = photos[start:start + limit]
|
||||
|
||||
for p in page_photos:
|
||||
pid = _get_id(p)
|
||||
p["full_url"] = pb.media_url(pid)
|
||||
p["thumb_url"] = pb.thumbnail_url(pid)
|
||||
|
||||
return {"photos": page_photos, "total": total, "page": page,
|
||||
"pages": max(1, (total + limit - 1) // limit)}
|
||||
|
||||
|
||||
@router.post("/admin/api/gallery/print/{photo_id}")
|
||||
async def admin_print_photo(
|
||||
request: Request,
|
||||
photo_id: str,
|
||||
copies: int = Query(default=1, ge=1, le=3),
|
||||
):
|
||||
"""Impression directe depuis la galerie admin."""
|
||||
if not request.session.get("authenticated"):
|
||||
return JSONResponse({"error": "Non authentifié"}, status_code=401)
|
||||
|
||||
pb = request.app.state.photobooth_service
|
||||
printer_svc = request.app.state.printer_service
|
||||
led = request.app.state.led_service
|
||||
ws = request.app.state.ws_manager
|
||||
|
||||
# Reconstruit le chemin du fichier depuis l'ID
|
||||
from pathlib import Path
|
||||
import re
|
||||
cfg = request.app.state.config
|
||||
|
||||
# photobooth-app identifiant → chemin fichier
|
||||
media_dir = cfg.photobooth.media_dir
|
||||
# Cherche le fichier correspondant
|
||||
filename = _find_file(media_dir, photo_id)
|
||||
if not filename:
|
||||
return JSONResponse({"error": f"Fichier introuvable pour {photo_id}"}, status_code=404)
|
||||
|
||||
thumb_url = pb.thumbnail_url(photo_id)
|
||||
entry = await printer_svc.add_request(str(filename), thumb_url, copies)
|
||||
|
||||
if cfg.print.mode == "direct":
|
||||
led.play("printing")
|
||||
result = await printer_svc.execute_print(entry["id"], copies)
|
||||
await ws.broadcast({"type": "print_result", "result": result})
|
||||
if result["success"]:
|
||||
led.play("finished")
|
||||
else:
|
||||
led.play("error")
|
||||
return result
|
||||
|
||||
await ws.broadcast({"type": "print_request", "entry": entry})
|
||||
return {"ok": True, "entry": entry, "mode": cfg.print.mode}
|
||||
|
||||
|
||||
@router.delete("/admin/api/gallery/{photo_id}")
|
||||
async def admin_delete_photo(request: Request, photo_id: str):
|
||||
"""Supprime une photo via l'API photobooth-app."""
|
||||
if not request.session.get("authenticated"):
|
||||
return JSONResponse({"error": "Non authentifié"}, status_code=401)
|
||||
|
||||
pb = request.app.state.photobooth_service
|
||||
ws = request.app.state.ws_manager
|
||||
|
||||
ok = await pb.delete_media(photo_id)
|
||||
if ok:
|
||||
await ws.broadcast({"type": "photo_deleted", "photo_id": photo_id})
|
||||
return {"ok": ok, "photo_id": photo_id}
|
||||
|
||||
|
||||
def _is_image(item: dict) -> bool:
|
||||
t = item.get("type", item.get("mediaitem_type", "image"))
|
||||
return str(t).lower() in ("image", "still", "photo")
|
||||
|
||||
|
||||
def _get_id(item: dict) -> str:
|
||||
return str(item.get("id", item.get("filename", item.get("uid", ""))))
|
||||
|
||||
|
||||
def _find_file(media_dir: str, photo_id: str):
|
||||
"""Cherche un fichier image correspondant à l'identifiant dans le répertoire media."""
|
||||
from pathlib import Path
|
||||
base = Path(media_dir)
|
||||
if not base.exists():
|
||||
return None
|
||||
|
||||
# L'ID peut être le stem du filename
|
||||
for ext in (".jpg", ".jpeg", ".png"):
|
||||
f = base / f"{photo_id}{ext}"
|
||||
if f.exists():
|
||||
return f
|
||||
# Cherche dans les sous-dossiers
|
||||
matches = list(base.rglob(f"{photo_id}{ext}"))
|
||||
if matches:
|
||||
return matches[0]
|
||||
return None
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Galerie publique — accessible sans authentification."""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Request, Query
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
_templates = Jinja2Templates(directory=Path(__file__).parent.parent.parent / "frontend" / "templates")
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request):
|
||||
"""Page d'accueil → redirige vers la galerie publique."""
|
||||
return RedirectResponse(url="/gallery")
|
||||
|
||||
|
||||
@router.get("/gallery", response_class=HTMLResponse)
|
||||
async def gallery_page(request: Request):
|
||||
cfg = request.app.state.config
|
||||
if not cfg.gallery.public_enabled:
|
||||
return HTMLResponse("<h1>Galerie désactivée</h1>", status_code=403)
|
||||
return _templates.TemplateResponse("public/gallery.html", {"request": request, "config": cfg})
|
||||
|
||||
|
||||
@router.get("/api/gallery/photos")
|
||||
async def api_gallery_photos(
|
||||
request: Request,
|
||||
page: int = Query(default=1, ge=1),
|
||||
limit: int = Query(default=24, ge=1, le=100),
|
||||
):
|
||||
"""Liste des photos depuis photobooth-app (paginée)."""
|
||||
pb = request.app.state.photobooth_service
|
||||
cfg = request.app.state.config
|
||||
|
||||
all_photos = await pb.get_media_collection(limit=500)
|
||||
|
||||
# Filtre sur les images uniquement
|
||||
photos = [p for p in all_photos if _is_image(p)]
|
||||
|
||||
total = len(photos)
|
||||
start = (page - 1) * limit
|
||||
end = start + limit
|
||||
page_photos = photos[start:end]
|
||||
|
||||
# Enrichit avec les URLs
|
||||
for p in page_photos:
|
||||
pid = _get_id(p)
|
||||
p["full_url"] = pb.media_url(pid)
|
||||
p["thumb_url"] = pb.thumbnail_url(pid)
|
||||
p["download_url"] = f"/api/gallery/download/{pid}"
|
||||
|
||||
return {
|
||||
"photos": page_photos,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"pages": max(1, (total + limit - 1) // limit),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/api/gallery/download/{photo_id}")
|
||||
async def download_photo(request: Request, photo_id: str):
|
||||
"""Redirige vers le fichier full-res sur photobooth-app."""
|
||||
pb = request.app.state.photobooth_service
|
||||
return RedirectResponse(url=pb.media_url(photo_id))
|
||||
|
||||
|
||||
def _is_image(item: dict) -> bool:
|
||||
t = item.get("type", item.get("mediaitem_type", "image"))
|
||||
return str(t).lower() in ("image", "still", "photo")
|
||||
|
||||
|
||||
def _get_id(item: dict) -> str:
|
||||
return str(item.get("id", item.get("filename", item.get("uid", ""))))
|
||||
@@ -0,0 +1,51 @@
|
||||
"""API de contrôle des LEDs WS2812b."""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Request, Query
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/leds/status")
|
||||
async def led_status(request: Request):
|
||||
led = request.app.state.led_service
|
||||
return {"effect": led.current_effect}
|
||||
|
||||
|
||||
@router.post("/leds/effect")
|
||||
async def set_effect(
|
||||
request: Request,
|
||||
effect: str = Query(..., description="idle | countdown | capture | captured | finished | printing | error | disabled | off"),
|
||||
):
|
||||
"""Force un effet LED (admin uniquement, pas de vérification auth ici — à protéger via Zoraxy)."""
|
||||
valid = ("idle", "countdown", "capture", "captured", "finished", "printing", "error", "disabled", "off")
|
||||
if effect not in valid:
|
||||
return JSONResponse({"error": f"Effet invalide. Valeurs: {valid}"}, status_code=400)
|
||||
|
||||
led = request.app.state.led_service
|
||||
ws = request.app.state.ws_manager
|
||||
led.play(effect)
|
||||
await ws.broadcast({"type": "led_effect", "effect": effect})
|
||||
return {"ok": True, "effect": effect}
|
||||
|
||||
|
||||
@router.post("/leds/color")
|
||||
async def set_color(
|
||||
request: Request,
|
||||
r: int = Query(default=0, ge=0, le=255),
|
||||
g: int = Query(default=0, ge=0, le=255),
|
||||
b: int = Query(default=0, ge=0, le=255),
|
||||
):
|
||||
"""Couleur fixe immédiate sur tout l'anneau."""
|
||||
led = request.app.state.led_service
|
||||
led.set_color(r, g, b)
|
||||
return {"ok": True, "color": [r, g, b]}
|
||||
|
||||
|
||||
@router.post("/leds/off")
|
||||
async def leds_off(request: Request):
|
||||
led = request.app.state.led_service
|
||||
led.play("off")
|
||||
return {"ok": True}
|
||||
@@ -0,0 +1,114 @@
|
||||
"""API gestion de la file d'attente d'impression."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Request, Query
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/print/request")
|
||||
async def print_request(
|
||||
request: Request,
|
||||
filename: str = Query(..., description="Chemin absolu du fichier à imprimer"),
|
||||
copies: int = Query(default=1, ge=1, le=3),
|
||||
):
|
||||
"""
|
||||
Reçoit la demande d'impression depuis photobooth-app
|
||||
(appelé par le share_command 'Demande d'impression').
|
||||
"""
|
||||
printer_svc = request.app.state.printer_service
|
||||
pb = request.app.state.photobooth_service
|
||||
led = request.app.state.led_service
|
||||
ws = request.app.state.ws_manager
|
||||
|
||||
# Construit l'URL de miniature depuis le filename
|
||||
stem = Path(filename).stem
|
||||
thumb_url = pb.thumbnail_url(stem)
|
||||
|
||||
entry = await printer_svc.add_request(filename, thumb_url, copies)
|
||||
|
||||
await ws.broadcast({"type": "print_request", "entry": entry})
|
||||
|
||||
# Feedback LED si impression directe
|
||||
if request.app.state.config.print.mode == "direct":
|
||||
led.play("printing")
|
||||
|
||||
logger.info("Demande impression reçue: %s", filename)
|
||||
return {"ok": True, "id": entry["id"], "mode": request.app.state.config.print.mode}
|
||||
|
||||
|
||||
@router.get("/print/queue")
|
||||
async def get_queue(
|
||||
request: Request,
|
||||
status: str | None = Query(default=None),
|
||||
):
|
||||
"""Retourne la file d'attente d'impression."""
|
||||
printer_svc = request.app.state.printer_service
|
||||
queue = await printer_svc.get_queue(status)
|
||||
printers = await printer_svc.get_printers_status()
|
||||
return {"queue": queue, "printers": printers}
|
||||
|
||||
|
||||
@router.post("/print/execute/{entry_id}")
|
||||
async def execute_print(
|
||||
request: Request,
|
||||
entry_id: str,
|
||||
copies: int = Query(default=1, ge=1, le=3),
|
||||
):
|
||||
"""Lance l'impression d'une entrée en attente (validation admin)."""
|
||||
printer_svc = request.app.state.printer_service
|
||||
led = request.app.state.led_service
|
||||
ws = request.app.state.ws_manager
|
||||
|
||||
led.play("printing")
|
||||
result = await printer_svc.execute_print(entry_id, copies)
|
||||
|
||||
await ws.broadcast({"type": "print_result", "entry_id": entry_id, "result": result})
|
||||
|
||||
if result["success"]:
|
||||
led.play("finished")
|
||||
else:
|
||||
led.play("error")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/print/cancel/{entry_id}")
|
||||
async def cancel_print(request: Request, entry_id: str):
|
||||
"""Annule une demande en attente."""
|
||||
printer_svc = request.app.state.printer_service
|
||||
ws = request.app.state.ws_manager
|
||||
ok = await printer_svc.cancel(entry_id)
|
||||
if ok:
|
||||
await ws.broadcast({"type": "print_cancelled", "entry_id": entry_id})
|
||||
return {"ok": ok}
|
||||
|
||||
|
||||
@router.post("/print/cups/cancel/{printer_name}")
|
||||
async def cancel_cups_jobs(request: Request, printer_name: str):
|
||||
"""Annule tous les jobs CUPS d'une imprimante."""
|
||||
printer_svc = request.app.state.printer_service
|
||||
ok = await printer_svc.cancel_cups_jobs(printer_name)
|
||||
return {"ok": ok, "printer": printer_name}
|
||||
|
||||
|
||||
@router.get("/print/printers")
|
||||
async def get_printers(request: Request):
|
||||
"""Statut des imprimantes CUPS."""
|
||||
printer_svc = request.app.state.printer_service
|
||||
return await printer_svc.get_printers_status()
|
||||
|
||||
|
||||
@router.post("/print/mode")
|
||||
async def set_print_mode(request: Request, mode: str = Query(...)):
|
||||
"""Change le mode d'impression (direct | validation | gallery)."""
|
||||
if mode not in ("direct", "validation", "gallery"):
|
||||
return JSONResponse({"error": "Mode invalide"}, status_code=400)
|
||||
config_svc = request.app.state.config_service
|
||||
config_svc.save_print_mode(mode)
|
||||
request.app.state.config.print.mode = mode
|
||||
return {"ok": True, "mode": mode}
|
||||
@@ -0,0 +1,52 @@
|
||||
"""API de surveillance des ressources système."""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/system/stats")
|
||||
async def system_stats(request: Request):
|
||||
"""Ressources système en temps réel."""
|
||||
sys_svc = request.app.state.system_service
|
||||
return sys_svc.get_stats()
|
||||
|
||||
|
||||
@router.get("/system/services")
|
||||
async def system_services(request: Request):
|
||||
"""Statut des services systemd."""
|
||||
sys_svc = request.app.state.system_service
|
||||
return sys_svc.get_services_status()
|
||||
|
||||
|
||||
@router.get("/system/photobooth")
|
||||
async def photobooth_status(request: Request):
|
||||
"""Vérifie si photobooth-app répond."""
|
||||
pb = request.app.state.photobooth_service
|
||||
alive = await pb.is_alive()
|
||||
return {"alive": alive, "url": request.app.state.config.photobooth.base_url}
|
||||
|
||||
|
||||
@router.post("/system/button/simulate")
|
||||
async def simulate_button(request: Request, clicks: int = 1):
|
||||
"""Simule un appui bouton (dev/test uniquement)."""
|
||||
btn = request.app.state.button_service
|
||||
if clicks == 0:
|
||||
btn.simulate_long_press()
|
||||
else:
|
||||
btn.simulate_click(clicks)
|
||||
return {"ok": True, "simulated_clicks": clicks}
|
||||
|
||||
|
||||
@router.post("/system/relay")
|
||||
async def control_relay(request: Request, state: str = "on"):
|
||||
"""Force le relay ON/OFF."""
|
||||
btn = request.app.state.button_service
|
||||
if state == "on":
|
||||
btn.relay_on()
|
||||
else:
|
||||
btn.relay_off()
|
||||
return {"ok": True, "relay": state}
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Webhooks reçus de photobooth-app via plugin_commander.
|
||||
|
||||
photobooth-app envoie un GET sur /api/webhook/photobooth?event_key=XXX&mediaitem_type=image
|
||||
pour chaque événement de capture.
|
||||
|
||||
Événements :
|
||||
counting → countdown en cours (LEDs remplissage vert)
|
||||
capture → flash photo (LEDs flash blanc)
|
||||
captured → photo prise (LEDs violet)
|
||||
finished → traitement terminé (LEDs bleu → retour idle + relay ON)
|
||||
start/stop → démarrage/arrêt photobooth-app
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Request, Query
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
# Durée par défaut du countdown (en secondes) — peut être overridé par action
|
||||
DEFAULT_COUNTDOWN = 5.0
|
||||
|
||||
|
||||
@router.get("/webhook/photobooth")
|
||||
async def photobooth_webhook(
|
||||
request: Request,
|
||||
event_key: str = Query(default=""),
|
||||
mediaitem_type: str = Query(default=""),
|
||||
):
|
||||
led = request.app.state.led_service
|
||||
btn = request.app.state.button_service
|
||||
ws = request.app.state.ws_manager
|
||||
|
||||
logger.info("Webhook photobooth: event=%s type=%s", event_key, mediaitem_type)
|
||||
|
||||
await ws.broadcast({"type": "photobooth_event", "event": event_key, "media_type": mediaitem_type})
|
||||
|
||||
match event_key:
|
||||
case "counting":
|
||||
# Countdown en cours — remplissage LED vert
|
||||
# On cherche la durée du countdown dans la config de l'action courante
|
||||
countdown_duration = _get_countdown_duration(request)
|
||||
led.play("countdown", countdown_duration=countdown_duration)
|
||||
|
||||
case "capture":
|
||||
# Flash photo
|
||||
led.play("capture")
|
||||
|
||||
case "captured":
|
||||
# Photo prise, traitement en cours
|
||||
led.play("captured")
|
||||
|
||||
case "finished":
|
||||
# Traitement terminé
|
||||
led.play("finished")
|
||||
# Réactive le relay après l'animation "finished" (durée ~2s gérée par le service LED)
|
||||
import asyncio
|
||||
asyncio.create_task(_delayed_relay_on(btn, 2.5))
|
||||
|
||||
case "start":
|
||||
led.play("idle")
|
||||
|
||||
case "stop":
|
||||
led.play("off")
|
||||
|
||||
case _:
|
||||
logger.debug("Événement inconnu: %s", event_key)
|
||||
|
||||
return {"ok": True, "event": event_key}
|
||||
|
||||
|
||||
async def _delayed_relay_on(btn, delay: float):
|
||||
import asyncio
|
||||
await asyncio.sleep(delay)
|
||||
if btn:
|
||||
btn.relay_on()
|
||||
|
||||
|
||||
def _get_countdown_duration(request: Request) -> float:
|
||||
"""
|
||||
Essaie de récupérer la durée du countdown depuis la config photobooth-app
|
||||
en lisant le dernier index d'action déclenché par le bouton.
|
||||
Retourne la valeur par défaut si indisponible.
|
||||
"""
|
||||
try:
|
||||
btn = request.app.state.button_service
|
||||
# On pourrait stocker le dernier index dans le button_service
|
||||
# Pour l'instant on retourne 5s par défaut
|
||||
return DEFAULT_COUNTDOWN
|
||||
except Exception:
|
||||
return DEFAULT_COUNTDOWN
|
||||
Reference in New Issue
Block a user