state.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. # -*- coding: utf-8 -*-
  2. """
  3. State Conditions - Zustandsbasierte Bedingungen.
  4. """
  5. from typing import Any
  6. from trixy_core.scheduler.condition.base import Condition
  7. from trixy_core.scheduler.form_fields import FieldType, FormField, FormValidationError
  8. class StateCondition(Condition):
  9. """
  10. Prüft ob System/Komponente in bestimmtem Zustand ist.
  11. Beispiel:
  12. - StateCondition("satellite_kitchen", "connected")
  13. - StateCondition("system_mode", ["active", "standby"])
  14. """
  15. def __init__(
  16. self,
  17. state_key: str,
  18. expected_state: str | list[str],
  19. name: str | None = None,
  20. invert: bool = False,
  21. ):
  22. """
  23. Initialisiert die Condition.
  24. Args:
  25. state_key: Schlüssel für Zustand im Kontext
  26. expected_state: Erwarteter Zustand oder Liste
  27. name: Condition-Name
  28. invert: Ergebnis invertieren
  29. """
  30. super().__init__(name, invert)
  31. self._state_key = state_key
  32. if isinstance(expected_state, str):
  33. self._expected_states = [expected_state]
  34. else:
  35. self._expected_states = list(expected_state)
  36. @property
  37. def state_key(self) -> str:
  38. """Zustands-Schlüssel."""
  39. return self._state_key
  40. @property
  41. def expected_states(self) -> list[str]:
  42. """Erwartete Zustände."""
  43. return list(self._expected_states)
  44. def _evaluate(self, context: dict) -> bool:
  45. """Prüft ob Zustand erwartet ist."""
  46. current_state = context.get(self._state_key)
  47. if current_state is None:
  48. return False
  49. # String-Vergleich (case-insensitive)
  50. current_str = str(current_state).lower()
  51. return any(
  52. s.lower() == current_str
  53. for s in self._expected_states
  54. )
  55. def to_dict(self) -> dict:
  56. """Konvertiert zu Dictionary."""
  57. return {
  58. **self._base_dict(),
  59. "state_key": self._state_key,
  60. "expected_states": self._expected_states,
  61. }
  62. @classmethod
  63. def from_dict(cls, data: dict) -> "StateCondition":
  64. """Erstellt Condition aus Dictionary."""
  65. return cls(
  66. state_key=data["state_key"],
  67. expected_state=data["expected_states"],
  68. name=data.get("name"),
  69. invert=data.get("invert", False),
  70. )
  71. @classmethod
  72. def display_name(cls) -> str:
  73. """Anzeigename fuer die TUI."""
  74. return "Status-Pruefung"
  75. @classmethod
  76. def form_fields(cls) -> list[FormField]:
  77. """Gibt die Formular-Felder fuer die TUI zurueck."""
  78. return [
  79. FormField(
  80. name="state_key", label="Status-Schluessel", required=True,
  81. placeholder="satellite_kitchen",
  82. help_text="Schluessel fuer den Zustand im Kontext",
  83. ),
  84. FormField(
  85. name="expected_value", label="Erwarteter Wert",
  86. placeholder="connected",
  87. help_text="Erwarteter Zustandswert",
  88. ),
  89. FormField(
  90. name="comparison", label="Vergleich",
  91. field_type=FieldType.SELECT,
  92. default="eq",
  93. choices=[
  94. ("eq", "Gleich"),
  95. ("ne", "Ungleich"),
  96. ("gt", "Groesser"),
  97. ("lt", "Kleiner"),
  98. ("gte", "Groesser/Gleich"),
  99. ("lte", "Kleiner/Gleich"),
  100. ],
  101. ),
  102. FormField(
  103. name="invert", label="Invertieren",
  104. field_type=FieldType.CHECKBOX, default=False,
  105. ),
  106. ]
  107. @classmethod
  108. def validate_form(cls, data: dict) -> list[FormValidationError]:
  109. """Validiert Formular-Daten. Gibt Liste von Fehlern zurueck."""
  110. errors: list[FormValidationError] = []
  111. if not data.get("state_key", "").strip():
  112. errors.append(FormValidationError("state_key", "Status-Schluessel ist erforderlich"))
  113. return errors
  114. class BooleanCondition(Condition):
  115. """
  116. Prüft ob Boolean-Flag gesetzt ist.
  117. Beispiel:
  118. - BooleanCondition("is_home") - Nur wenn zu Hause
  119. - BooleanCondition("debug_mode", expected=False) - Nur wenn kein Debug
  120. """
  121. def __init__(
  122. self,
  123. flag_key: str,
  124. expected: bool = True,
  125. name: str | None = None,
  126. invert: bool = False,
  127. ):
  128. """
  129. Initialisiert die Condition.
  130. Args:
  131. flag_key: Schlüssel für Flag im Kontext
  132. expected: Erwarteter Wert (default: True)
  133. name: Condition-Name
  134. invert: Ergebnis invertieren
  135. """
  136. super().__init__(name, invert)
  137. self._flag_key = flag_key
  138. self._expected = expected
  139. @property
  140. def flag_key(self) -> str:
  141. """Flag-Schlüssel."""
  142. return self._flag_key
  143. @property
  144. def expected(self) -> bool:
  145. """Erwarteter Wert."""
  146. return self._expected
  147. def _evaluate(self, context: dict) -> bool:
  148. """Prüft ob Flag erwartet ist."""
  149. value = context.get(self._flag_key)
  150. # None behandeln
  151. if value is None:
  152. return not self._expected
  153. # Boolean konvertieren
  154. if isinstance(value, bool):
  155. return value == self._expected
  156. elif isinstance(value, str):
  157. truthy = value.lower() in ("true", "1", "yes", "on")
  158. return truthy == self._expected
  159. elif isinstance(value, (int, float)):
  160. return bool(value) == self._expected
  161. return False
  162. def to_dict(self) -> dict:
  163. """Konvertiert zu Dictionary."""
  164. return {
  165. **self._base_dict(),
  166. "flag_key": self._flag_key,
  167. "expected": self._expected,
  168. }
  169. @classmethod
  170. def from_dict(cls, data: dict) -> "BooleanCondition":
  171. """Erstellt Condition aus Dictionary."""
  172. return cls(
  173. flag_key=data["flag_key"],
  174. expected=data.get("expected", True),
  175. name=data.get("name"),
  176. invert=data.get("invert", False),
  177. )
  178. @classmethod
  179. def display_name(cls) -> str:
  180. """Anzeigename fuer die TUI."""
  181. return "Boolean-Pruefung"
  182. @classmethod
  183. def form_fields(cls) -> list[FormField]:
  184. """Gibt die Formular-Felder fuer die TUI zurueck."""
  185. return [
  186. FormField(
  187. name="state_key", label="Flag-Schluessel", required=True,
  188. placeholder="is_home",
  189. help_text="Schluessel fuer das Boolean-Flag im Kontext",
  190. ),
  191. FormField(
  192. name="invert", label="Invertieren",
  193. field_type=FieldType.CHECKBOX, default=False,
  194. ),
  195. ]
  196. @classmethod
  197. def validate_form(cls, data: dict) -> list[FormValidationError]:
  198. """Validiert Formular-Daten. Gibt Liste von Fehlern zurueck."""
  199. errors: list[FormValidationError] = []
  200. if not data.get("state_key", "").strip():
  201. errors.append(FormValidationError("state_key", "Flag-Schluessel ist erforderlich"))
  202. return errors
  203. class ExistsCondition(Condition):
  204. """
  205. Prüft ob Schlüssel im Kontext existiert.
  206. Beispiel:
  207. - ExistsCondition("user_preferences")
  208. - ExistsCondition("error_message", exists=False)
  209. """
  210. def __init__(
  211. self,
  212. key: str,
  213. exists: bool = True,
  214. allow_none: bool = False,
  215. name: str | None = None,
  216. invert: bool = False,
  217. ):
  218. """
  219. Initialisiert die Condition.
  220. Args:
  221. key: Schlüssel im Kontext
  222. exists: Soll existieren (True) oder nicht (False)
  223. allow_none: None als existent zählen
  224. name: Condition-Name
  225. invert: Ergebnis invertieren
  226. """
  227. super().__init__(name, invert)
  228. self._key = key
  229. self._exists = exists
  230. self._allow_none = allow_none
  231. @property
  232. def key(self) -> str:
  233. """Schlüssel."""
  234. return self._key
  235. def _evaluate(self, context: dict) -> bool:
  236. """Prüft ob Schlüssel existiert."""
  237. key_exists = self._key in context
  238. if key_exists and not self._allow_none:
  239. key_exists = context[self._key] is not None
  240. return key_exists == self._exists
  241. def to_dict(self) -> dict:
  242. """Konvertiert zu Dictionary."""
  243. return {
  244. **self._base_dict(),
  245. "key": self._key,
  246. "exists": self._exists,
  247. "allow_none": self._allow_none,
  248. }
  249. @classmethod
  250. def from_dict(cls, data: dict) -> "ExistsCondition":
  251. """Erstellt Condition aus Dictionary."""
  252. return cls(
  253. key=data["key"],
  254. exists=data.get("exists", True),
  255. allow_none=data.get("allow_none", False),
  256. name=data.get("name"),
  257. invert=data.get("invert", False),
  258. )
  259. @classmethod
  260. def display_name(cls) -> str:
  261. """Anzeigename fuer die TUI."""
  262. return "Existenz-Pruefung"
  263. @classmethod
  264. def form_fields(cls) -> list[FormField]:
  265. """Gibt die Formular-Felder fuer die TUI zurueck."""
  266. return [
  267. FormField(
  268. name="state_key", label="Schluessel", required=True,
  269. placeholder="user_preferences",
  270. help_text="Schluessel der im Kontext existieren soll",
  271. ),
  272. FormField(
  273. name="invert", label="Invertieren",
  274. field_type=FieldType.CHECKBOX, default=False,
  275. ),
  276. ]
  277. @classmethod
  278. def validate_form(cls, data: dict) -> list[FormValidationError]:
  279. """Validiert Formular-Daten. Gibt Liste von Fehlern zurueck."""
  280. errors: list[FormValidationError] = []
  281. if not data.get("state_key", "").strip():
  282. errors.append(FormValidationError("state_key", "Schluessel ist erforderlich"))
  283. return errors