98 lines
3.7 KiB
Python
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_device` (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
|