event.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. # -*- coding: utf-8 -*-
  2. """
  3. Event Trigger - Reagiert auf Events des EventManagers.
  4. """
  5. from datetime import datetime
  6. from typing import Any, Callable
  7. from trixy_core.scheduler.form_fields import FieldType, FormField, FormValidationError
  8. from trixy_core.scheduler.trigger.base import Trigger, TriggerContext, TriggerState
  9. class EventTrigger(Trigger):
  10. """
  11. Trigger der auf Events des EventManagers reagiert.
  12. Beispiele:
  13. - EventTrigger("wakeword_received") - Bei Wakeword
  14. - EventTrigger("satellite_connected", filter_fn=lambda d: d.get("room") == "kitchen")
  15. - EventTrigger(["motion_detected", "door_opened"]) - Bei mehreren Events
  16. """
  17. def __init__(
  18. self,
  19. event_names: str | list[str],
  20. filter_fn: Callable[[dict], bool] | None = None,
  21. debounce_seconds: float = 0,
  22. name: str | None = None,
  23. ):
  24. """
  25. Initialisiert den Event-Trigger.
  26. Args:
  27. event_names: Event-Name(n) auf die reagiert wird
  28. filter_fn: Optionale Filter-Funktion für Event-Daten
  29. debounce_seconds: Mindestzeit zwischen Auslösungen
  30. name: Trigger-Name
  31. """
  32. super().__init__(name)
  33. if isinstance(event_names, str):
  34. self._event_names = [event_names]
  35. else:
  36. self._event_names = list(event_names)
  37. self._filter_fn = filter_fn
  38. self._debounce_seconds = debounce_seconds
  39. self._pending_event: dict | None = None
  40. @property
  41. def event_names(self) -> list[str]:
  42. """Event-Namen auf die reagiert wird."""
  43. return list(self._event_names)
  44. @property
  45. def debounce_seconds(self) -> float:
  46. """Debounce-Zeit in Sekunden."""
  47. return self._debounce_seconds
  48. def on_event(self, event_name: str, event_data: dict | None = None) -> bool:
  49. """
  50. Wird aufgerufen wenn ein Event eintrifft.
  51. Args:
  52. event_name: Name des Events
  53. event_data: Event-Daten
  54. Returns:
  55. True wenn Trigger aktiviert wurde
  56. """
  57. if not self.is_active:
  58. return False
  59. # Event-Name prüfen
  60. if event_name not in self._event_names:
  61. return False
  62. # Filter prüfen
  63. if self._filter_fn and event_data:
  64. try:
  65. if not self._filter_fn(event_data):
  66. return False
  67. except Exception:
  68. return False
  69. # Debounce prüfen
  70. if self._debounce_seconds > 0 and self._last_fire_time:
  71. elapsed = (datetime.now() - self._last_fire_time).total_seconds()
  72. if elapsed < self._debounce_seconds:
  73. return False
  74. # Event speichern für should_fire
  75. self._pending_event = {
  76. "event_name": event_name,
  77. "event_data": event_data or {},
  78. "timestamp": datetime.now(),
  79. }
  80. return True
  81. def should_fire(self, context: TriggerContext) -> bool:
  82. """Prüft ob Trigger auslösen soll."""
  83. if not self.is_active:
  84. return False
  85. # Event-basiert: Prüfe ob Event im Kontext
  86. if context.event_name:
  87. # Event-Name prüfen
  88. if context.event_name not in self._event_names:
  89. return False
  90. # Filter prüfen
  91. if self._filter_fn and context.event_data:
  92. try:
  93. if not self._filter_fn(context.event_data):
  94. return False
  95. except Exception:
  96. return False
  97. # Debounce prüfen
  98. if self._debounce_seconds > 0 and context.last_fire_time:
  99. elapsed = (context.current_time - context.last_fire_time).total_seconds()
  100. if elapsed < self._debounce_seconds:
  101. return False
  102. return True
  103. # Alternativ: Pending Event prüfen
  104. if self._pending_event:
  105. self._pending_event = None # Einmalig
  106. return True
  107. return False
  108. def get_next_fire_time(
  109. self,
  110. last_fire_time: datetime | None = None,
  111. ) -> datetime | None:
  112. """
  113. Berechnet nächsten Auslösezeitpunkt.
  114. Event-Trigger haben keinen planbaren Zeitpunkt.
  115. """
  116. # Event-basierte Trigger haben keinen festen Zeitpunkt
  117. return None
  118. def clear_pending(self) -> None:
  119. """Löscht pending Event."""
  120. self._pending_event = None
  121. def to_dict(self) -> dict:
  122. """Konvertiert zu Dictionary."""
  123. return {
  124. **self._base_dict(),
  125. "event_names": self._event_names,
  126. "debounce_seconds": self._debounce_seconds,
  127. # filter_fn kann nicht serialisiert werden
  128. }
  129. @classmethod
  130. def from_dict(cls, data: dict) -> "EventTrigger":
  131. """Erstellt Trigger aus Dictionary."""
  132. return cls(
  133. event_names=data["event_names"],
  134. debounce_seconds=data.get("debounce_seconds", 0),
  135. name=data.get("name"),
  136. # filter_fn muss separat gesetzt werden
  137. )
  138. @classmethod
  139. def display_name(cls) -> str:
  140. """Anzeigename fuer die TUI."""
  141. return "Event"
  142. @classmethod
  143. def form_fields(cls) -> list[FormField]:
  144. """Gibt die Formular-Felder fuer die TUI zurueck."""
  145. return [
  146. FormField(
  147. name="event_names", label="Event-Namen",
  148. field_type=FieldType.TAGS, required=True,
  149. placeholder="wakeword_received, satellite_connected",
  150. help_text="Komma-separierte Event-Namen",
  151. ),
  152. FormField(
  153. name="debounce_seconds", label="Debounce (Sek.)",
  154. field_type=FieldType.NUMBER,
  155. default=0, min_value=0, is_float=True,
  156. ),
  157. ]
  158. @classmethod
  159. def validate_form(cls, data: dict) -> list[FormValidationError]:
  160. """Validiert Formular-Daten. Gibt Liste von Fehlern zurueck."""
  161. errors: list[FormValidationError] = []
  162. names = data.get("event_names", "")
  163. if isinstance(names, str):
  164. names = [n.strip() for n in names.split(",") if n.strip()]
  165. if not names:
  166. errors.append(FormValidationError("event_names", "Mindestens ein Event-Name ist erforderlich"))
  167. return errors
  168. def __repr__(self) -> str:
  169. return f"EventTrigger(events={self._event_names})"