Files
photoBooth/backend/services/config_service.py
T
admin 69b9cd0901
🚀 Deploy — JH Photomaton / 🔍 Vérification (push) Has been cancelled
🚀 Deploy — JH Photomaton / 🍓 Deploy sur le Pi (push) Has been cancelled
feat: pre-flash configurable avant fin countdown
2026-07-20 23:29:55 +02:00

294 lines
11 KiB
Python

"""Service de chargement et sauvegarde de la configuration YAML."""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
import yaml
logger = logging.getLogger(__name__)
@dataclass
class AppConfig:
name: str = "JH Photomaton"
host: str = "0.0.0.0"
port: int = 8090
debug: bool = False
secret_key: str = ""
admin_password: str = "PhotoBooth2026!"
@dataclass
class PhotoboothConfig:
base_url: str = "http://localhost:8000"
public_url: str = "" # URL publique pour les medias (ex: https://photobooth.lessapinsduweb.com)
data_dir: str = "/home/pi/photobooth-data"
config_file: str = "/home/pi/photobooth-data/config/config.json"
media_dir: str = "/home/pi/photobooth-data/media/processed_full"
userdata_dir: str = "/home/pi/photobooth-data/userdata"
show_delete_button: bool = False
@dataclass
class ButtonConfig:
pin: int = 23
relay_pin: int = 12
debounce_ms: int = 50
double_click_ms: int = 400
long_press_ms: int = 1500
max_clicks: int = 4
long_press_action: str = "print_last"
print_enabled: bool = True
@dataclass
class LEDEffectConfig:
color: list = field(default_factory=lambda: [0, 30, 80])
mode: str = "solid"
speed: float = 0.05
flashes: int = 2
flash_duration: float = 0.1
duration: float = 2.0
@dataclass
class LEDConfig:
pin: int = 18
count: int = 35
brightness: int = 180
auto_brightness: bool = False
freq_hz: int = 800000
dma: int = 10
strip_type: str = "WS2812"
effects: dict = field(default_factory=dict)
countdown_duration: float = 3.0 # Durée du countdown photobooth-app (secondes)
pre_flash_advance: float = 0.5 # Démarrer le flash X sec avant la fin du countdown
def get_effect(self, name: str) -> LEDEffectConfig:
raw = self.effects.get(name, {})
return LEDEffectConfig(
color=raw.get("color", [0, 30, 80]),
mode=raw.get("mode", "solid"),
speed=raw.get("speed", 0.05),
flashes=raw.get("flashes", 2),
flash_duration=raw.get("flash_duration", 0.1),
duration=raw.get("duration", 2.0),
)
@dataclass
class PrintConfig:
mode: str = "validation"
script_path: str = "/home/pi/photobooth-data/script/script_print.sh"
default_copies: int = 1
printers: list = field(default_factory=list)
# Impression depuis la galerie utilisateur
user_print_enabled: bool = False
user_print_quota: int = 0 # 0 = illimité
user_print_max_copies: int = 1 # copies max par demande
@dataclass
class GalleryConfig:
public_enabled: bool = True
photos_per_page: int = 24
qr_base_url: str = "https://photomaton.lessapinsduweb.com"
@dataclass
class EventConfig:
name: str = "Evenement"
slug: str = "evenement"
started_at: float = 0.0
@dataclass
class Config:
app: AppConfig = field(default_factory=AppConfig)
photobooth: PhotoboothConfig = field(default_factory=PhotoboothConfig)
button: ButtonConfig = field(default_factory=ButtonConfig)
leds: LEDConfig = field(default_factory=LEDConfig)
print: PrintConfig = field(default_factory=PrintConfig)
gallery: GalleryConfig = field(default_factory=GalleryConfig)
event: EventConfig = field(default_factory=EventConfig)
button_actions: dict = field(default_factory=dict)
class ConfigService:
def __init__(self, path: Path):
self.path = path
self._config: Config | None = None
def load(self) -> Config:
if not self.path.exists():
logger.warning("Config introuvable: %s", self.path)
self._config = Config()
return self._config
with open(self.path, encoding="utf-8") as f:
raw: dict[str, Any] = yaml.safe_load(f) or {}
cfg = Config()
if "app" in raw:
cfg.app = AppConfig(**{k: v for k, v in raw["app"].items() if k in AppConfig.__dataclass_fields__})
if "photobooth" in raw:
cfg.photobooth = PhotoboothConfig(**{k: v for k, v in raw["photobooth"].items() if k in PhotoboothConfig.__dataclass_fields__})
if "button" in raw:
cfg.button = ButtonConfig(**{k: v for k, v in raw["button"].items() if k in ButtonConfig.__dataclass_fields__})
if "leds" in raw:
led_raw = raw["leds"]
cfg.leds = LEDConfig(
pin=led_raw.get("pin", 18),
count=led_raw.get("count", 35),
brightness=led_raw.get("brightness", 180),
auto_brightness=led_raw.get("auto_brightness", False),
freq_hz=led_raw.get("freq_hz", 800000),
dma=led_raw.get("dma", 10),
strip_type=led_raw.get("strip_type", "WS2812"),
effects=led_raw.get("effects", {}),
countdown_duration=led_raw.get("countdown_duration", 3.0),
pre_flash_advance=led_raw.get("pre_flash_advance", 0.5),
)
if "print" in raw:
cfg.print = PrintConfig(**{k: v for k, v in raw["print"].items() if k in PrintConfig.__dataclass_fields__})
if "gallery" in raw:
cfg.gallery = GalleryConfig(**{k: v for k, v in raw["gallery"].items() if k in GalleryConfig.__dataclass_fields__})
if "event" in raw:
cfg.event = EventConfig(**{k: v for k, v in raw["event"].items() if k in EventConfig.__dataclass_fields__})
cfg.button_actions = raw.get("button_actions", {
1: {"label": "Photo normale", "photobooth_index": 0},
2: {"label": "Photo etoile", "photobooth_index": 1},
3: {"label": "Photo cailloux", "photobooth_index": 2},
4: {"label": "Photo soiree", "photobooth_index": 3},
})
self._config = cfg
logger.info("Configuration chargee depuis %s", self.path)
return cfg
def save_button_actions(self, button_actions: dict):
with open(self.path, encoding="utf-8") as f:
raw = yaml.safe_load(f) or {}
raw["button_actions"] = button_actions
with open(self.path, "w", encoding="utf-8") as f:
yaml.dump(raw, f, allow_unicode=True, default_flow_style=False)
if self._config:
self._config.button_actions = button_actions
def save_show_delete_button(self, visible: bool):
with open(self.path, encoding="utf-8") as f:
raw = yaml.safe_load(f) or {}
raw.setdefault("photobooth", {})["show_delete_button"] = visible
with open(self.path, "w", encoding="utf-8") as f:
yaml.dump(raw, f, allow_unicode=True, default_flow_style=False)
if self._config:
self._config.photobooth.show_delete_button = visible
def save_button_config(self, data: dict):
with open(self.path, encoding="utf-8") as f:
raw = yaml.safe_load(f) or {}
raw.setdefault("button", {}).update(data)
with open(self.path, "w", encoding="utf-8") as f:
yaml.dump(raw, f, allow_unicode=True, default_flow_style=False)
if self._config:
for k, v in data.items():
if hasattr(self._config.button, k):
setattr(self._config.button, k, v)
def save_event_config(self, name: str, slug: str, started_at: float):
with open(self.path, encoding="utf-8") as f:
raw = yaml.safe_load(f) or {}
raw["event"] = {"name": name, "slug": slug, "started_at": started_at}
with open(self.path, "w", encoding="utf-8") as f:
yaml.dump(raw, f, allow_unicode=True, default_flow_style=False)
if self._config:
self._config.event.name = name
self._config.event.slug = slug
self._config.event.started_at = started_at
def save_user_print_config(self, enabled: bool, quota: int, max_copies: int):
with open(self.path, encoding="utf-8") as f:
raw = yaml.safe_load(f) or {}
raw.setdefault("print", {}).update({
"user_print_enabled": enabled,
"user_print_quota": quota,
"user_print_max_copies": max_copies,
})
with open(self.path, "w", encoding="utf-8") as f:
yaml.dump(raw, f, allow_unicode=True, default_flow_style=False)
if self._config:
self._config.print.user_print_enabled = enabled
self._config.print.user_print_quota = quota
self._config.print.user_print_max_copies = max_copies
def save_printer_excluded(self, printer_name: str, excluded: bool):
"""Marque (ou démarque) une imprimante comme exclue du load-balancing."""
with open(self.path, encoding="utf-8") as f:
raw = yaml.safe_load(f) or {}
printers = raw.get("print", {}).get("printers", [])
for p in printers:
if isinstance(p, dict) and p.get("name") == printer_name:
p["excluded"] = excluded
break
raw.setdefault("print", {})["printers"] = printers
with open(self.path, "w", encoding="utf-8") as f:
yaml.dump(raw, f, allow_unicode=True, default_flow_style=False)
if self._config:
for p in self._config.print.printers:
if isinstance(p, dict) and p.get("name") == printer_name:
p["excluded"] = excluded
break
def save_print_mode(self, mode: str):
with open(self.path, encoding="utf-8") as f:
raw = yaml.safe_load(f) or {}
raw.setdefault("print", {})["mode"] = mode
with open(self.path, "w", encoding="utf-8") as f:
yaml.dump(raw, f, allow_unicode=True, default_flow_style=False)
if self._config:
self._config.print.mode = mode
@property
def config(self) -> Config:
if self._config is None:
self.load()
return self._config
def save_led_brightness(self, brightness: int):
with open(self.path, encoding="utf-8") as f:
raw = yaml.safe_load(f) or {}
raw.setdefault("leds", {})["brightness"] = brightness
with open(self.path, "w", encoding="utf-8") as f:
yaml.dump(raw, f, allow_unicode=True, default_flow_style=False)
if self._config:
self._config.leds.brightness = brightness
def save_led_auto_brightness(self, enabled: bool):
with open(self.path, encoding="utf-8") as f:
raw = yaml.safe_load(f) or {}
raw.setdefault("leds", {})["auto_brightness"] = enabled
with open(self.path, "w", encoding="utf-8") as f:
yaml.dump(raw, f, allow_unicode=True, default_flow_style=False)
if self._config:
self._config.leds.auto_brightness = enabled
def save_led_effect(self, effect_name: str, effect_data: dict):
with open(self.path, encoding="utf-8") as f:
raw = yaml.safe_load(f) or {}
raw.setdefault("leds", {}).setdefault("effects", {})[effect_name] = effect_data
with open(self.path, "w", encoding="utf-8") as f:
yaml.dump(raw, f, allow_unicode=True, default_flow_style=False)
if self._config:
self._config.leds.effects[effect_name] = effect_data