Kazeia-central/kazeia_central/features/voices/autosync.py

98 lines
3.7 KiB
Python

"""Déclencheur automatique d'enrôlement à la connexion d'une tablette.
Piloté depuis l'interface : démarre **désarmé** (`enabled=False`). Quand l'opérateur
l'arme, toute tablette qui se **connecte** déclenche un `sync_admin` (enrôle +
déploie les voix en attente, verrou-conscient). Statut par tablette consultable.
Sécurité : ne fait rien si le store est verrouillé (les PII restent protégées) ; la
suppression du WAV device est opt-in (`delete_source`, défaut False). Détecte les
**nouvelles** connexions ; pour traiter une tablette déjà branchée, l'UI utilise le
bouton « synchroniser » (endpoint sync manuel).
"""
from __future__ import annotations
import threading
import time
from typing import Any
from .orchestrator import sync_admin
class VoiceAutoSync:
def __init__(self, adb, store, bridge, *, interval: float = 3.0) -> None:
self.adb = adb
self.store = store
self.bridge = bridge
self.interval = interval
self.enabled = False
self.delete_source = False
self._known: set[str] = set()
self._status: dict[str, dict] = {}
self._lock = threading.Lock()
self._thread: threading.Thread | None = None
self._stop = threading.Event()
@staticmethod
def _now() -> int:
return int(time.time() * 1000)
# ---- cycle de vie -----------------------------------------------------
def start(self) -> None:
if self._thread and self._thread.is_alive():
return
self._stop.clear()
self._thread = threading.Thread(target=self._loop, name="voice-autosync", daemon=True)
self._thread.start()
def stop(self) -> None:
self._stop.set()
def set_enabled(self, enabled: bool, *, delete_source: bool | None = None) -> None:
self.enabled = bool(enabled)
if delete_source is not None:
self.delete_source = bool(delete_source)
def status(self) -> dict[str, Any]:
with self._lock:
return {"enabled": self.enabled, "delete_source": self.delete_source,
"running": bool(self._thread and self._thread.is_alive()),
"devices": dict(self._status)}
# ---- boucle -----------------------------------------------------------
def _loop(self) -> None:
while not self._stop.wait(self.interval):
try:
ready = {d.serial for d in self.adb.ready_devices()}
except Exception:
continue
new = ready - self._known
self._known = ready
if not self.enabled:
continue
for serial in sorted(new):
self.sync_now(serial)
def sync_now(self, serial: str) -> dict:
"""Lance une sync pour une tablette (utilisé par le watcher ET par le bouton
manuel). Ne lève pas : renvoie/enregistre un statut."""
if not self.store.is_unlocked:
st = {"state": "store_verrouille", "at": self._now()}
self._set(serial, st)
return st
self._set(serial, {"state": "sync_en_cours", "at": self._now()})
try:
rep = sync_admin(self.adb, self.store, self.bridge, serial,
delete_source=self.delete_source, now=self._now(), actor="auto")
ok = sum(1 for r in rep if r.get("ok"))
st = {"state": "termine", "at": self._now(), "enrolled": ok,
"total": len(rep), "report": rep}
except Exception as e:
st = {"state": "erreur", "at": self._now(), "error": str(e)}
self._set(serial, st)
return st
def _set(self, serial: str, st: dict) -> None:
with self._lock:
self._status[serial] = st