activity.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. # -*- coding: utf-8 -*-
  2. """
  3. Activity Conditions - Aktivitätsbasierte Bedingungen.
  4. """
  5. from datetime import datetime, timedelta
  6. from typing import Any
  7. from trixy_core.scheduler.condition.base import Condition
  8. from trixy_core.scheduler.form_fields import FieldType, FormField, FormValidationError
  9. class RecentActivityCondition(Condition):
  10. """
  11. Prüft ob kürzlich Aktivität stattfand.
  12. Beispiel:
  13. - RecentActivityCondition("last_voice_input", minutes=10)
  14. → Nur wenn in letzten 10 Min. Voice-Input war
  15. - RecentActivityCondition("last_motion", seconds=30)
  16. → Nur wenn in letzten 30 Sek. Bewegung war
  17. """
  18. def __init__(
  19. self,
  20. activity_key: str,
  21. seconds: int = 0,
  22. minutes: int = 0,
  23. hours: int = 0,
  24. name: str | None = None,
  25. invert: bool = False,
  26. ):
  27. """
  28. Initialisiert die Condition.
  29. Args:
  30. activity_key: Schlüssel für Aktivitäts-Timestamp im Kontext
  31. seconds: Zeitfenster in Sekunden
  32. minutes: Zeitfenster in Minuten
  33. hours: Zeitfenster in Stunden
  34. name: Condition-Name
  35. invert: Ergebnis invertieren
  36. """
  37. super().__init__(name, invert)
  38. self._activity_key = activity_key
  39. self._window = timedelta(seconds=seconds, minutes=minutes, hours=hours)
  40. if self._window.total_seconds() <= 0:
  41. raise ValueError("Zeitfenster muss größer als 0 sein")
  42. @property
  43. def activity_key(self) -> str:
  44. """Aktivitäts-Schlüssel."""
  45. return self._activity_key
  46. @property
  47. def window(self) -> timedelta:
  48. """Zeitfenster."""
  49. return self._window
  50. def _evaluate(self, context: dict) -> bool:
  51. """Prüft ob Aktivität kürzlich war."""
  52. last_activity = context.get(self._activity_key)
  53. if not last_activity:
  54. return False
  55. # Timestamp parsen
  56. if isinstance(last_activity, str):
  57. try:
  58. last_activity = datetime.fromisoformat(last_activity)
  59. except ValueError:
  60. return False
  61. elif not isinstance(last_activity, datetime):
  62. return False
  63. now = context.get("current_time", datetime.now())
  64. if isinstance(now, str):
  65. now = datetime.fromisoformat(now)
  66. elapsed = now - last_activity
  67. return elapsed <= self._window
  68. def to_dict(self) -> dict:
  69. """Konvertiert zu Dictionary."""
  70. return {
  71. **self._base_dict(),
  72. "activity_key": self._activity_key,
  73. "window_seconds": self._window.total_seconds(),
  74. }
  75. @classmethod
  76. def from_dict(cls, data: dict) -> "RecentActivityCondition":
  77. """Erstellt Condition aus Dictionary."""
  78. return cls(
  79. activity_key=data["activity_key"],
  80. seconds=int(data.get("window_seconds", 0)),
  81. name=data.get("name"),
  82. invert=data.get("invert", False),
  83. )
  84. @classmethod
  85. def display_name(cls) -> str:
  86. """Anzeigename fuer die TUI."""
  87. return "Letzte Aktivitaet"
  88. @classmethod
  89. def form_fields(cls) -> list[FormField]:
  90. """Gibt die Formular-Felder fuer die TUI zurueck."""
  91. return [
  92. FormField(
  93. name="activity_key", label="Aktivitaets-Schluessel", required=True,
  94. placeholder="last_voice_input",
  95. help_text="Schluessel fuer den Aktivitaets-Timestamp im Kontext",
  96. ),
  97. FormField(
  98. name="within_seconds", label="Zeitfenster (Sek.)",
  99. field_type=FieldType.NUMBER, required=True,
  100. min_value=1, is_float=True,
  101. help_text="Maximales Alter der Aktivitaet in Sekunden",
  102. ),
  103. FormField(
  104. name="invert", label="Invertieren",
  105. field_type=FieldType.CHECKBOX, default=False,
  106. ),
  107. ]
  108. @classmethod
  109. def validate_form(cls, data: dict) -> list[FormValidationError]:
  110. """Validiert Formular-Daten. Gibt Liste von Fehlern zurueck."""
  111. errors: list[FormValidationError] = []
  112. if not data.get("activity_key", "").strip():
  113. errors.append(FormValidationError("activity_key", "Aktivitaets-Schluessel ist erforderlich"))
  114. within = data.get("within_seconds", 0)
  115. if not within or within <= 0:
  116. errors.append(FormValidationError("within_seconds", "Zeitfenster muss groesser als 0 sein"))
  117. return errors
  118. class ActivityCountCondition(Condition):
  119. """
  120. Prüft ob Aktivitäts-Zähler einen Schwellenwert erreicht.
  121. Beispiel:
  122. - ActivityCountCondition("error_count", min_count=5)
  123. → Nur wenn mindestens 5 Fehler
  124. - ActivityCountCondition("login_attempts", max_count=3)
  125. → Nur wenn max. 3 Login-Versuche
  126. """
  127. def __init__(
  128. self,
  129. count_key: str,
  130. min_count: int | None = None,
  131. max_count: int | None = None,
  132. name: str | None = None,
  133. invert: bool = False,
  134. ):
  135. """
  136. Initialisiert die Condition.
  137. Args:
  138. count_key: Schlüssel für Zähler im Kontext
  139. min_count: Mindestanzahl (optional)
  140. max_count: Maximalanzahl (optional)
  141. name: Condition-Name
  142. invert: Ergebnis invertieren
  143. """
  144. super().__init__(name, invert)
  145. self._count_key = count_key
  146. self._min_count = min_count
  147. self._max_count = max_count
  148. if min_count is None and max_count is None:
  149. raise ValueError("min_count oder max_count muss gesetzt sein")
  150. @property
  151. def count_key(self) -> str:
  152. """Zähler-Schlüssel."""
  153. return self._count_key
  154. @property
  155. def min_count(self) -> int | None:
  156. """Mindestanzahl."""
  157. return self._min_count
  158. @property
  159. def max_count(self) -> int | None:
  160. """Maximalanzahl."""
  161. return self._max_count
  162. def _evaluate(self, context: dict) -> bool:
  163. """Prüft ob Zähler Bedingung erfüllt."""
  164. count = context.get(self._count_key, 0)
  165. try:
  166. count = int(count)
  167. except (TypeError, ValueError):
  168. return False
  169. if self._min_count is not None and count < self._min_count:
  170. return False
  171. if self._max_count is not None and count > self._max_count:
  172. return False
  173. return True
  174. def to_dict(self) -> dict:
  175. """Konvertiert zu Dictionary."""
  176. return {
  177. **self._base_dict(),
  178. "count_key": self._count_key,
  179. "min_count": self._min_count,
  180. "max_count": self._max_count,
  181. }
  182. @classmethod
  183. def from_dict(cls, data: dict) -> "ActivityCountCondition":
  184. """Erstellt Condition aus Dictionary."""
  185. return cls(
  186. count_key=data["count_key"],
  187. min_count=data.get("min_count"),
  188. max_count=data.get("max_count"),
  189. name=data.get("name"),
  190. invert=data.get("invert", False),
  191. )
  192. @classmethod
  193. def display_name(cls) -> str:
  194. """Anzeigename fuer die TUI."""
  195. return "Aktivitaets-Anzahl"
  196. @classmethod
  197. def form_fields(cls) -> list[FormField]:
  198. """Gibt die Formular-Felder fuer die TUI zurueck."""
  199. return [
  200. FormField(
  201. name="activity_key", label="Zaehler-Schluessel", required=True,
  202. placeholder="error_count",
  203. help_text="Schluessel fuer den Aktivitaets-Zaehler im Kontext",
  204. ),
  205. FormField(
  206. name="min_count", label="Mindestanzahl",
  207. field_type=FieldType.NUMBER, min_value=0,
  208. help_text="Mindestanzahl fuer Ausloesung",
  209. ),
  210. FormField(
  211. name="time_window_seconds", label="Zeitfenster (Sek.)",
  212. field_type=FieldType.NUMBER, min_value=0, is_float=True,
  213. help_text="Optionales Zeitfenster fuer Zaehlung",
  214. ),
  215. FormField(
  216. name="invert", label="Invertieren",
  217. field_type=FieldType.CHECKBOX, default=False,
  218. ),
  219. ]
  220. @classmethod
  221. def validate_form(cls, data: dict) -> list[FormValidationError]:
  222. """Validiert Formular-Daten. Gibt Liste von Fehlern zurueck."""
  223. errors: list[FormValidationError] = []
  224. if not data.get("activity_key", "").strip():
  225. errors.append(FormValidationError("activity_key", "Zaehler-Schluessel ist erforderlich"))
  226. return errors