message_handler.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. # -*- coding: utf-8 -*-
  2. """
  3. MessageHandler für die Verarbeitung von Netzwerk-Nachrichten.
  4. Zentralisiert die Logik für das Dispatching und die Verarbeitung
  5. von eingehenden Protokoll-Nachrichten.
  6. """
  7. from __future__ import annotations
  8. import asyncio
  9. from typing import TYPE_CHECKING, Callable, Awaitable, Any
  10. from dataclasses import dataclass, field
  11. from trixy_core.network.protocol import ProtocolMessage
  12. from trixy_core.utils.debug import pdebug, perror
  13. if TYPE_CHECKING:
  14. from trixy_core.satellite.satellite import Satellite
  15. # Events die nicht geloggt werden sollen (zu häufig/spammend)
  16. _EVENT_LOG_BLACKLIST: set[str] = {
  17. "music_output_received",
  18. "audio_output_received",
  19. }
  20. # Typ-Alias für Handler-Funktionen
  21. MessageHandlerFunc = Callable[["Satellite", ProtocolMessage], Awaitable[None]]
  22. MessageFilter = Callable[[ProtocolMessage], bool]
  23. @dataclass
  24. class HandlerRegistration:
  25. """Registrierung eines Nachricht-Handlers."""
  26. handler: MessageHandlerFunc
  27. class_names: set[str] = field(default_factory=set)
  28. filter_func: MessageFilter | None = None
  29. priority: int = 0
  30. class MessageHandler:
  31. """
  32. Zentraler Handler für eingehende Protokoll-Nachrichten.
  33. Ermöglicht die Registrierung von Handlern für bestimmte
  34. Nachrichtentypen oder mit benutzerdefinierten Filtern.
  35. Usage:
  36. handler = MessageHandler()
  37. # Handler für bestimmte Nachrichtentypen
  38. handler.register(on_wakeword, ["WakewordDetected", "WakewordSelected"])
  39. # Handler für alle Nachrichten
  40. handler.register(log_all)
  41. # Handler mit Filter
  42. handler.register(on_error, filter_func=lambda m: m.flags & ProtocolFlags.ERROR)
  43. # Nachricht verarbeiten
  44. await handler.dispatch(satellite, message)
  45. """
  46. def __init__(self) -> None:
  47. """Initialisiert den MessageHandler."""
  48. self._handlers: list[HandlerRegistration] = []
  49. self._class_index: dict[str, list[HandlerRegistration]] = {}
  50. def register(
  51. self,
  52. handler: MessageHandlerFunc,
  53. class_names: list[str] | None = None,
  54. filter_func: MessageFilter | None = None,
  55. priority: int = 0,
  56. ) -> None:
  57. """
  58. Registriert einen Handler für Nachrichten.
  59. Args:
  60. handler: Async-Funktion die (satellite, message) verarbeitet
  61. class_names: Liste von Nachrichtenklassen-Namen (None = alle)
  62. filter_func: Optionale Filterfunktion
  63. priority: Priorität (höher = früher)
  64. """
  65. registration = HandlerRegistration(
  66. handler=handler,
  67. class_names=set(class_names) if class_names else set(),
  68. filter_func=filter_func,
  69. priority=priority,
  70. )
  71. # Nach Priorität sortiert einfügen
  72. inserted = False
  73. for i, existing in enumerate(self._handlers):
  74. if registration.priority > existing.priority:
  75. self._handlers.insert(i, registration)
  76. inserted = True
  77. break
  78. if not inserted:
  79. self._handlers.append(registration)
  80. # Index aktualisieren
  81. if class_names:
  82. for name in class_names:
  83. if name not in self._class_index:
  84. self._class_index[name] = []
  85. self._class_index[name].append(registration)
  86. pdebug(f"Message-Handler registriert: {handler.__name__} für {class_names or 'alle'}")
  87. def unregister(self, handler: MessageHandlerFunc) -> bool:
  88. """
  89. Entfernt einen Handler.
  90. Args:
  91. handler: Der zu entfernende Handler
  92. Returns:
  93. True wenn entfernt
  94. """
  95. for reg in list(self._handlers):
  96. if reg.handler == handler:
  97. self._handlers.remove(reg)
  98. # Aus Index entfernen
  99. for name, registrations in list(self._class_index.items()):
  100. if reg in registrations:
  101. registrations.remove(reg)
  102. if not registrations:
  103. del self._class_index[name]
  104. pdebug(f"Message-Handler entfernt: {handler.__name__}")
  105. return True
  106. return False
  107. async def dispatch(
  108. self,
  109. satellite: "Satellite",
  110. message: ProtocolMessage,
  111. stop_on_handled: bool = False,
  112. ) -> int:
  113. """
  114. Verteilt eine Nachricht an passende Handler.
  115. Args:
  116. satellite: Der sendende Satellite
  117. message: Die empfangene Nachricht
  118. stop_on_handled: Bei True nach erstem Handler stoppen
  119. Returns:
  120. Anzahl aufgerufener Handler
  121. """
  122. class_name = message.class_name
  123. called = 0
  124. # Schneller Lookup für spezifische Handler
  125. specific_handlers = self._class_index.get(class_name, [])
  126. for reg in specific_handlers:
  127. if await self._call_handler(reg, satellite, message):
  128. called += 1
  129. if stop_on_handled:
  130. return called
  131. # Allgemeine Handler (ohne class_names)
  132. for reg in self._handlers:
  133. if reg.class_names:
  134. continue # Bereits via Index verarbeitet
  135. if await self._call_handler(reg, satellite, message):
  136. called += 1
  137. if stop_on_handled:
  138. return called
  139. return called
  140. async def _call_handler(
  141. self,
  142. registration: HandlerRegistration,
  143. satellite: "Satellite",
  144. message: ProtocolMessage,
  145. ) -> bool:
  146. """
  147. Ruft einen einzelnen Handler auf.
  148. Returns:
  149. True wenn erfolgreich aufgerufen
  150. """
  151. # Filter prüfen
  152. if registration.filter_func:
  153. try:
  154. if not registration.filter_func(message):
  155. return False
  156. except Exception as e:
  157. perror(f"Filter-Fehler: {e}")
  158. return False
  159. # Handler aufrufen
  160. try:
  161. await registration.handler(satellite, message)
  162. return True
  163. except Exception as e:
  164. perror(f"Handler-Fehler in {registration.handler.__name__}: {e}")
  165. return False
  166. def clear(self) -> None:
  167. """Entfernt alle Handler."""
  168. self._handlers.clear()
  169. self._class_index.clear()
  170. class EventBridge:
  171. """
  172. Brücke zwischen Netzwerk-Nachrichten und dem Event-System.
  173. Konvertiert eingehende Protokoll-Nachrichten automatisch
  174. in Events und löst sie aus.
  175. Usage:
  176. bridge = EventBridge(event_manager)
  177. bridge.map("WakewordDetected", "wakeword_detected")
  178. bridge.map("RecordingComplete", "recording_complete")
  179. # In NetworkService:
  180. await bridge.on_message(satellite, message)
  181. """
  182. def __init__(self, event_manager: Any) -> None:
  183. """
  184. Initialisiert die EventBridge.
  185. Args:
  186. event_manager: Der EventManager der Anwendung
  187. """
  188. self._events = event_manager
  189. self._mappings: dict[str, str] = {}
  190. self._transformers: dict[str, Callable[[Any], dict]] = {}
  191. def map(
  192. self,
  193. class_name: str,
  194. event_name: str,
  195. transformer: Callable[[Any], dict] | None = None,
  196. ) -> None:
  197. """
  198. Mappt eine Nachrichtenklasse auf einen Event-Namen.
  199. Args:
  200. class_name: Name der Nachrichtenklasse
  201. event_name: Ziel-Event-Name
  202. transformer: Optionale Funktion zur Daten-Transformation
  203. """
  204. self._mappings[class_name] = event_name
  205. if transformer:
  206. self._transformers[class_name] = transformer
  207. pdebug(f"Event-Mapping: {class_name} -> {event_name}")
  208. def unmap(self, class_name: str) -> None:
  209. """Entfernt ein Mapping."""
  210. self._mappings.pop(class_name, None)
  211. self._transformers.pop(class_name, None)
  212. async def on_message(
  213. self,
  214. satellite: "Satellite",
  215. message: ProtocolMessage,
  216. ) -> bool:
  217. """
  218. Verarbeitet eine Nachricht und löst ggf. ein Event aus.
  219. Args:
  220. satellite: Der sendende Satellite
  221. message: Die empfangene Nachricht
  222. Returns:
  223. True wenn ein Event ausgelöst wurde
  224. """
  225. class_name = message.class_name
  226. event_name = self._mappings.get(class_name)
  227. if not event_name:
  228. return False
  229. # Daten vorbereiten
  230. data = message.data
  231. if isinstance(data, dict):
  232. event_data = data.copy()
  233. elif hasattr(data, "__dict__"):
  234. event_data = data.__dict__.copy()
  235. else:
  236. event_data = {"data": data}
  237. # Satellite-Info hinzufügen
  238. event_data["satellite_id"] = satellite.id
  239. event_data["satellite_alias"] = satellite.alias
  240. event_data["satellite_room"] = satellite.room_id
  241. # Transformer anwenden
  242. transformer = self._transformers.get(class_name)
  243. if transformer:
  244. try:
  245. event_data = transformer(event_data)
  246. except Exception as e:
  247. perror(f"Transformer-Fehler für {class_name}: {e}")
  248. # Event auslösen (emit() für dict-Daten)
  249. try:
  250. await self._events.emit(event_name, event_data)
  251. # Nicht nochmal loggen - EventManager.trigger() loggt bereits
  252. return True
  253. except Exception as e:
  254. perror(f"Event-Fehler: {e}")
  255. return False
  256. # Standard-Mappings für häufige Nachrichten
  257. DEFAULT_EVENT_MAPPINGS = {
  258. "WakewordDetected": "wakeword_detected",
  259. "WakewordSelected": "wakeword_selected",
  260. "WakewordAbort": "wakeword_abort",
  261. "RecordingComplete": "recording_complete",
  262. "TranscriptionResult": "transcription_result",
  263. "IntentResult": "intent_result",
  264. "AssistantResponse": "assistant_response",
  265. "ConversationEnd": "conversation_end",
  266. "TextInput": "text_input_received",
  267. # Media Commands -> Events
  268. "MusicPlayPause": "music_play_pause",
  269. "MusicNext": "music_next",
  270. "MusicPrevious": "music_previous",
  271. "MusicStop": "music_stop",
  272. "MediaStopAll": "media_stop_all",
  273. "MusicVolumeChange": "music_volume_change",
  274. "MusicStatus": "music_status_request",
  275. # Server-gesteuerte Aufnahme
  276. "SatelliteRecordStopped": "satellite_record_stopped",
  277. }
  278. def setup_default_event_bridge(event_manager: Any) -> EventBridge:
  279. """
  280. Erstellt eine EventBridge mit Standard-Mappings.
  281. Args:
  282. event_manager: Der EventManager der Anwendung
  283. Returns:
  284. Konfigurierte EventBridge
  285. """
  286. bridge = EventBridge(event_manager)
  287. for class_name, event_name in DEFAULT_EVENT_MAPPINGS.items():
  288. bridge.map(class_name, event_name)
  289. return bridge