composite.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. # -*- coding: utf-8 -*-
  2. """
  3. Composite Triggers - Kombiniert mehrere Trigger.
  4. """
  5. from datetime import datetime
  6. from typing import Any
  7. from trixy_core.scheduler.trigger.base import Trigger, TriggerContext, TriggerState
  8. class AndTrigger(Trigger):
  9. """
  10. Trigger der nur auslöst wenn ALLE Sub-Trigger auslösen.
  11. Beispiel:
  12. - AndTrigger([CronTrigger("0 9 * * *"), EventTrigger("user_present")])
  13. → Nur um 9 Uhr UND wenn User anwesend
  14. """
  15. def __init__(
  16. self,
  17. triggers: list[Trigger],
  18. name: str | None = None,
  19. ):
  20. """
  21. Initialisiert den AND-Trigger.
  22. Args:
  23. triggers: Liste von Sub-Triggern
  24. name: Trigger-Name
  25. """
  26. super().__init__(name)
  27. if not triggers:
  28. raise ValueError("AndTrigger benötigt mindestens einen Trigger")
  29. self._triggers = list(triggers)
  30. @property
  31. def triggers(self) -> list[Trigger]:
  32. """Sub-Trigger."""
  33. return list(self._triggers)
  34. def add_trigger(self, trigger: Trigger) -> None:
  35. """Fügt Trigger hinzu."""
  36. self._triggers.append(trigger)
  37. def remove_trigger(self, trigger: Trigger) -> bool:
  38. """Entfernt Trigger."""
  39. try:
  40. self._triggers.remove(trigger)
  41. return True
  42. except ValueError:
  43. return False
  44. def should_fire(self, context: TriggerContext) -> bool:
  45. """Prüft ob alle Trigger auslösen."""
  46. if not self.is_active:
  47. return False
  48. if not self._triggers:
  49. return False
  50. return all(t.should_fire(context) for t in self._triggers)
  51. def get_next_fire_time(
  52. self,
  53. last_fire_time: datetime | None = None,
  54. ) -> datetime | None:
  55. """
  56. Berechnet nächsten Auslösezeitpunkt.
  57. Für AND: Spätester Zeitpunkt aller Sub-Trigger.
  58. """
  59. if not self.is_active or not self._triggers:
  60. return None
  61. next_times = []
  62. for trigger in self._triggers:
  63. next_time = trigger.get_next_fire_time(last_fire_time)
  64. if next_time is None:
  65. # Wenn ein Trigger keinen Zeitpunkt hat (z.B. EventTrigger),
  66. # kann der AND-Trigger auch keinen planbaren Zeitpunkt haben
  67. return None
  68. next_times.append(next_time)
  69. # Spätester Zeitpunkt
  70. return max(next_times) if next_times else None
  71. def pause(self) -> None:
  72. """Pausiert alle Sub-Trigger."""
  73. super().pause()
  74. for trigger in self._triggers:
  75. trigger.pause()
  76. def resume(self) -> None:
  77. """Setzt alle Sub-Trigger fort."""
  78. super().resume()
  79. for trigger in self._triggers:
  80. trigger.resume()
  81. def reset(self) -> None:
  82. """Setzt alle Sub-Trigger zurück."""
  83. super().reset()
  84. for trigger in self._triggers:
  85. trigger.reset()
  86. def to_dict(self) -> dict:
  87. """Konvertiert zu Dictionary."""
  88. return {
  89. **self._base_dict(),
  90. "triggers": [t.to_dict() for t in self._triggers],
  91. }
  92. @classmethod
  93. def display_name(cls) -> str:
  94. """Anzeigename fuer die TUI."""
  95. return "UND-Trigger"
  96. def __repr__(self) -> str:
  97. return f"AndTrigger(count={len(self._triggers)})"
  98. class OrTrigger(Trigger):
  99. """
  100. Trigger der auslöst wenn MINDESTENS EIN Sub-Trigger auslöst.
  101. Beispiel:
  102. - OrTrigger([CronTrigger("0 9 * * *"), EventTrigger("urgent_task")])
  103. → Um 9 Uhr ODER bei dringendem Task
  104. """
  105. def __init__(
  106. self,
  107. triggers: list[Trigger],
  108. name: str | None = None,
  109. ):
  110. """
  111. Initialisiert den OR-Trigger.
  112. Args:
  113. triggers: Liste von Sub-Triggern
  114. name: Trigger-Name
  115. """
  116. super().__init__(name)
  117. if not triggers:
  118. raise ValueError("OrTrigger benötigt mindestens einen Trigger")
  119. self._triggers = list(triggers)
  120. self._last_fired_trigger: Trigger | None = None
  121. @property
  122. def triggers(self) -> list[Trigger]:
  123. """Sub-Trigger."""
  124. return list(self._triggers)
  125. @property
  126. def last_fired_trigger(self) -> Trigger | None:
  127. """Zuletzt ausgelöster Sub-Trigger."""
  128. return self._last_fired_trigger
  129. def add_trigger(self, trigger: Trigger) -> None:
  130. """Fügt Trigger hinzu."""
  131. self._triggers.append(trigger)
  132. def remove_trigger(self, trigger: Trigger) -> bool:
  133. """Entfernt Trigger."""
  134. try:
  135. self._triggers.remove(trigger)
  136. return True
  137. except ValueError:
  138. return False
  139. def should_fire(self, context: TriggerContext) -> bool:
  140. """Prüft ob mindestens ein Trigger auslöst."""
  141. if not self.is_active:
  142. return False
  143. if not self._triggers:
  144. return False
  145. for trigger in self._triggers:
  146. if trigger.should_fire(context):
  147. self._last_fired_trigger = trigger
  148. return True
  149. return False
  150. def get_next_fire_time(
  151. self,
  152. last_fire_time: datetime | None = None,
  153. ) -> datetime | None:
  154. """
  155. Berechnet nächsten Auslösezeitpunkt.
  156. Für OR: Frühester Zeitpunkt aller Sub-Trigger.
  157. """
  158. if not self.is_active or not self._triggers:
  159. return None
  160. next_times = []
  161. for trigger in self._triggers:
  162. next_time = trigger.get_next_fire_time(last_fire_time)
  163. if next_time:
  164. next_times.append(next_time)
  165. # Frühester Zeitpunkt
  166. return min(next_times) if next_times else None
  167. def pause(self) -> None:
  168. """Pausiert alle Sub-Trigger."""
  169. super().pause()
  170. for trigger in self._triggers:
  171. trigger.pause()
  172. def resume(self) -> None:
  173. """Setzt alle Sub-Trigger fort."""
  174. super().resume()
  175. for trigger in self._triggers:
  176. trigger.resume()
  177. def reset(self) -> None:
  178. """Setzt alle Sub-Trigger zurück."""
  179. super().reset()
  180. self._last_fired_trigger = None
  181. for trigger in self._triggers:
  182. trigger.reset()
  183. def to_dict(self) -> dict:
  184. """Konvertiert zu Dictionary."""
  185. return {
  186. **self._base_dict(),
  187. "triggers": [t.to_dict() for t in self._triggers],
  188. }
  189. @classmethod
  190. def display_name(cls) -> str:
  191. """Anzeigename fuer die TUI."""
  192. return "ODER-Trigger"
  193. def __repr__(self) -> str:
  194. return f"OrTrigger(count={len(self._triggers)})"