feat: Betriebsmodus global + persistent + Live-Sync
Vorher: - Modus war nur in-memory in der Bridge, Restart = zurueck auf NORMAL - App-Wechsel wurde zwar empfangen, aber nicht an andere Geraete gebroadcastet (nur Bestaetigung an den Sender) - Neue App-Verbindung wusste nicht welcher Modus gerade aktiv ist Jetzt: - Persistiert in /shared/config/mode.json beim Wechsel - Beim Bridge-Start: _load_persisted_mode() holt letzten aktiven Modus - _broadcast_current_mode() sendet an ALLE Clients (Broadcast) — jedes verbundene Geraet bekommt live den Wechsel mit - Bei RVS-Reconnect: sofortiger Broadcast damit neu verbundene Apps/ Diagnostic ihre UI syncen koennen - Loop-Schutz: payload.sender=="bridge" wird im mode-Handler ignoriert (sonst echo → Broadcast-Storm bei verbundenem RVS) Beispiel-Flow: Geraet A aktiviert 'Hangar' → Bridge empfaengt mode-msg → persist in mode.json → broadcast an alle Clients (mit sender="bridge") → Geraet B/C/Diagnostic empfangen → UI updated sofort → Bridge-Restart spaeter: HANGAR wird wieder geladen Anmerkung zu echten OS-Push bei geschlossener App: Das braucht FCM/Firebase + BackgroundService — deutlich mehr Arbeit, ist separat als Feature fuer spaeter zu sehen. Live-Sync bei geoeffneter App (WebSocket verbunden) funktioniert jetzt zuverlaessig. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
47fe4ad655
commit
763e0d79ab
|
|
@ -461,7 +461,8 @@ class ARIABridge:
|
|||
else:
|
||||
self.rvs_url = ""
|
||||
self.rvs_url_fallback = ""
|
||||
self.current_mode = Mode.NORMAL
|
||||
# Mode aus Shared Config laden (persistiert ueber Container-Restarts)
|
||||
self.current_mode = self._load_persisted_mode()
|
||||
self.running = False
|
||||
|
||||
# Komponenten (TTS: immer XTTS remote, Piper wurde entfernt)
|
||||
|
|
@ -821,20 +822,17 @@ class ARIABridge:
|
|||
is_critical = metadata.get("critical", False)
|
||||
requested_voice = metadata.get("voice")
|
||||
|
||||
# Modus-Wechsel pruefen
|
||||
# Modus-Wechsel pruefen (Sprachbefehl im Text)
|
||||
new_mode = detect_mode_switch(text)
|
||||
if new_mode is not None:
|
||||
self.current_mode = new_mode
|
||||
self._persist_mode()
|
||||
logger.info(
|
||||
"[core] Modus → %s %s",
|
||||
self.current_mode.config.emoji,
|
||||
self.current_mode.config.name,
|
||||
)
|
||||
await self._send_to_rvs({
|
||||
"type": "mode",
|
||||
"payload": {"mode": self.current_mode.name},
|
||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||
})
|
||||
await self._broadcast_current_mode()
|
||||
|
||||
# Eindeutige Message-ID fuer Audio-Cache-Zuordnung
|
||||
message_id = str(uuid.uuid4())
|
||||
|
|
@ -892,6 +890,47 @@ class ARIABridge:
|
|||
except Exception as e:
|
||||
logger.error("[core] XTTS-Request fehlgeschlagen: %s — kein Audio", e)
|
||||
|
||||
# ── Mode Persistence (global, nicht pro Geraet) ──────
|
||||
_MODE_FILE = "/shared/config/mode.json"
|
||||
|
||||
def _load_persisted_mode(self) -> Mode:
|
||||
"""Laedt den zuletzt aktiven Modus aus Shared Config oder NORMAL."""
|
||||
try:
|
||||
if os.path.exists(self._MODE_FILE):
|
||||
data = json.loads(Path(self._MODE_FILE).read_text())
|
||||
mode_name = data.get("mode", "NORMAL")
|
||||
for m in Mode:
|
||||
if m.name == mode_name:
|
||||
logger.info("[mode] Persistierter Modus geladen: %s", m.config.name)
|
||||
return m
|
||||
except Exception as e:
|
||||
logger.warning("[mode] Laden fehlgeschlagen: %s", e)
|
||||
return Mode.NORMAL
|
||||
|
||||
def _persist_mode(self) -> None:
|
||||
"""Speichert den aktuellen Modus in Shared Config."""
|
||||
try:
|
||||
os.makedirs("/shared/config", exist_ok=True)
|
||||
Path(self._MODE_FILE).write_text(json.dumps({"mode": self.current_mode.name}))
|
||||
except Exception as e:
|
||||
logger.warning("[mode] Speichern fehlgeschlagen: %s", e)
|
||||
|
||||
async def _broadcast_current_mode(self) -> None:
|
||||
"""Broadcastet den aktuellen Modus an alle RVS-Clients (App + Diagnostic)."""
|
||||
try:
|
||||
await self._send_to_rvs({
|
||||
"type": "mode",
|
||||
"payload": {
|
||||
"mode": self.current_mode.name.lower(),
|
||||
"name": self.current_mode.config.name,
|
||||
"emoji": self.current_mode.config.emoji,
|
||||
"sender": "bridge", # Filter in mode-Handler gegen Loops
|
||||
},
|
||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||
})
|
||||
except Exception as e:
|
||||
logger.debug("[mode] Broadcast fehlgeschlagen: %s", e)
|
||||
|
||||
def _fetch_active_session(self) -> None:
|
||||
"""Holt die aktive Session vom Diagnostic-Endpoint."""
|
||||
try:
|
||||
|
|
@ -957,6 +996,10 @@ class ARIABridge:
|
|||
retry_delay = 2
|
||||
logger.info("[rvs] Verbunden — warte auf App-Nachrichten")
|
||||
|
||||
# Aktuellen Modus broadcasten damit gerade verbundene Apps/Diagnostic
|
||||
# ihren UI-State sofort syncen koennen
|
||||
await self._broadcast_current_mode()
|
||||
|
||||
# Heartbeat senden (RVS erwartet Ping alle 30s)
|
||||
heartbeat_task = asyncio.create_task(self._rvs_heartbeat())
|
||||
|
||||
|
|
@ -1169,21 +1212,22 @@ class ARIABridge:
|
|||
elif msg_type == "mode":
|
||||
# Moduswechsel von der App — ID ('normal', 'dnd', ...) ODER Aktivierungsphrase
|
||||
mode_name = payload.get("mode", "")
|
||||
# Sender kann der Broadcast der Bridge selbst sein — den ignorieren damit
|
||||
# andere Apps nicht in eine Loop geraten
|
||||
if payload.get("sender") == "bridge":
|
||||
return
|
||||
new_mode = mode_from_id(mode_name) or detect_mode_switch(mode_name)
|
||||
if new_mode is not None:
|
||||
if new_mode is not None and new_mode != self.current_mode:
|
||||
self.current_mode = new_mode
|
||||
self._persist_mode()
|
||||
logger.info(
|
||||
"[rvs] Modus → %s %s (von App)",
|
||||
self.current_mode.config.emoji,
|
||||
self.current_mode.config.name,
|
||||
)
|
||||
# Bestaetigung an alle Clients (App/Diagnostic) damit UI sync bleibt
|
||||
await self._send_to_rvs({
|
||||
"type": "mode",
|
||||
"payload": {"mode": mode_name, "name": self.current_mode.config.name},
|
||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||
})
|
||||
else:
|
||||
# Broadcast an ALLE Clients (App + Diagnostic) damit UI ueberall sync ist
|
||||
await self._broadcast_current_mode()
|
||||
elif new_mode is None:
|
||||
logger.warning("[rvs] Unbekannter Modus: '%s'", mode_name)
|
||||
|
||||
elif msg_type == "location":
|
||||
|
|
|
|||
Loading…
Reference in New Issue