sensor.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. # -*- coding: utf-8 -*-
  2. """
  3. Sensor Conditions - Sensorbasierte Bedingungen.
  4. """
  5. from enum import Enum
  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 ComparisonOperator(Enum):
  10. """Vergleichsoperatoren."""
  11. EQUAL = "eq"
  12. NOT_EQUAL = "ne"
  13. GREATER = "gt"
  14. GREATER_EQUAL = "gte"
  15. LESS = "lt"
  16. LESS_EQUAL = "lte"
  17. BETWEEN = "between"
  18. IN = "in"
  19. NOT_IN = "not_in"
  20. class SensorThresholdCondition(Condition):
  21. """
  22. Prüft ob Sensorwert Schwellenwert erreicht.
  23. Beispiel:
  24. - SensorThresholdCondition("temperature", ComparisonOperator.GREATER, 25)
  25. → Nur wenn Temperatur > 25
  26. - SensorThresholdCondition("volume", ComparisonOperator.BETWEEN, (0.5, 0.8))
  27. → Nur wenn Lautstärke zwischen 0.5 und 0.8
  28. """
  29. def __init__(
  30. self,
  31. sensor_key: str,
  32. operator: ComparisonOperator,
  33. value: Any,
  34. name: str | None = None,
  35. invert: bool = False,
  36. ):
  37. """
  38. Initialisiert die Condition.
  39. Args:
  40. sensor_key: Schlüssel für Sensorwert im Kontext
  41. operator: Vergleichsoperator
  42. value: Vergleichswert (bei BETWEEN: Tuple (min, max))
  43. name: Condition-Name
  44. invert: Ergebnis invertieren
  45. """
  46. super().__init__(name, invert)
  47. self._sensor_key = sensor_key
  48. self._operator = operator
  49. self._value = value
  50. @property
  51. def sensor_key(self) -> str:
  52. """Sensor-Schlüssel."""
  53. return self._sensor_key
  54. @property
  55. def operator(self) -> ComparisonOperator:
  56. """Vergleichsoperator."""
  57. return self._operator
  58. @property
  59. def value(self) -> Any:
  60. """Vergleichswert."""
  61. return self._value
  62. def _evaluate(self, context: dict) -> bool:
  63. """Prüft ob Sensorwert Bedingung erfüllt."""
  64. sensor_value = context.get(self._sensor_key)
  65. if sensor_value is None:
  66. return False
  67. try:
  68. if self._operator == ComparisonOperator.EQUAL:
  69. return sensor_value == self._value
  70. elif self._operator == ComparisonOperator.NOT_EQUAL:
  71. return sensor_value != self._value
  72. elif self._operator == ComparisonOperator.GREATER:
  73. return sensor_value > self._value
  74. elif self._operator == ComparisonOperator.GREATER_EQUAL:
  75. return sensor_value >= self._value
  76. elif self._operator == ComparisonOperator.LESS:
  77. return sensor_value < self._value
  78. elif self._operator == ComparisonOperator.LESS_EQUAL:
  79. return sensor_value <= self._value
  80. elif self._operator == ComparisonOperator.BETWEEN:
  81. min_val, max_val = self._value
  82. return min_val <= sensor_value <= max_val
  83. elif self._operator == ComparisonOperator.IN:
  84. return sensor_value in self._value
  85. elif self._operator == ComparisonOperator.NOT_IN:
  86. return sensor_value not in self._value
  87. except (TypeError, ValueError):
  88. return False
  89. return False
  90. def to_dict(self) -> dict:
  91. """Konvertiert zu Dictionary."""
  92. return {
  93. **self._base_dict(),
  94. "sensor_key": self._sensor_key,
  95. "operator": self._operator.value,
  96. "value": self._value,
  97. }
  98. @classmethod
  99. def from_dict(cls, data: dict) -> "SensorThresholdCondition":
  100. """Erstellt Condition aus Dictionary."""
  101. return cls(
  102. sensor_key=data["sensor_key"],
  103. operator=ComparisonOperator(data["operator"]),
  104. value=data["value"],
  105. name=data.get("name"),
  106. invert=data.get("invert", False),
  107. )
  108. @classmethod
  109. def display_name(cls) -> str:
  110. """Anzeigename fuer die TUI."""
  111. return "Sensor-Schwellwert"
  112. @classmethod
  113. def form_fields(cls) -> list[FormField]:
  114. """Gibt die Formular-Felder fuer die TUI zurueck."""
  115. return [
  116. FormField(
  117. name="sensor_key", label="Sensor-Schluessel", required=True,
  118. placeholder="temperature",
  119. help_text="Schluessel fuer den Sensorwert im Kontext",
  120. ),
  121. FormField(
  122. name="operator", label="Operator",
  123. field_type=FieldType.SELECT, default="gt",
  124. choices=[
  125. ("eq", "Gleich (==)"),
  126. ("ne", "Ungleich (!=)"),
  127. ("gt", "Groesser (>)"),
  128. ("gte", "Groesser/Gleich (>=)"),
  129. ("lt", "Kleiner (<)"),
  130. ("lte", "Kleiner/Gleich (<=)"),
  131. ],
  132. ),
  133. FormField(
  134. name="threshold", label="Schwellwert",
  135. field_type=FieldType.NUMBER, required=True,
  136. is_float=True,
  137. ),
  138. FormField(
  139. name="invert", label="Invertieren",
  140. field_type=FieldType.CHECKBOX, default=False,
  141. ),
  142. ]
  143. @classmethod
  144. def validate_form(cls, data: dict) -> list[FormValidationError]:
  145. """Validiert Formular-Daten. Gibt Liste von Fehlern zurueck."""
  146. errors: list[FormValidationError] = []
  147. if not data.get("sensor_key", "").strip():
  148. errors.append(FormValidationError("sensor_key", "Sensor-Schluessel ist erforderlich"))
  149. if data.get("threshold") is None:
  150. errors.append(FormValidationError("threshold", "Schwellwert ist erforderlich"))
  151. return errors
  152. class SensorChangeCondition(Condition):
  153. """
  154. Prüft ob Sensorwert sich um bestimmten Betrag geändert hat.
  155. Beispiel:
  156. - SensorChangeCondition("temperature", min_change=5.0)
  157. → Nur wenn Temperatur sich um min. 5 Grad geändert hat
  158. """
  159. def __init__(
  160. self,
  161. sensor_key: str,
  162. previous_key: str | None = None,
  163. min_change: float | None = None,
  164. max_change: float | None = None,
  165. percentage: bool = False,
  166. name: str | None = None,
  167. invert: bool = False,
  168. ):
  169. """
  170. Initialisiert die Condition.
  171. Args:
  172. sensor_key: Schlüssel für aktuellen Sensorwert
  173. previous_key: Schlüssel für vorherigen Wert (default: {sensor_key}_previous)
  174. min_change: Mindeständerung
  175. max_change: Maximaländerung
  176. percentage: Änderung als Prozentsatz
  177. name: Condition-Name
  178. invert: Ergebnis invertieren
  179. """
  180. super().__init__(name, invert)
  181. self._sensor_key = sensor_key
  182. self._previous_key = previous_key or f"{sensor_key}_previous"
  183. self._min_change = min_change
  184. self._max_change = max_change
  185. self._percentage = percentage
  186. @property
  187. def sensor_key(self) -> str:
  188. """Sensor-Schlüssel."""
  189. return self._sensor_key
  190. def _evaluate(self, context: dict) -> bool:
  191. """Prüft ob Sensorwert sich ausreichend geändert hat."""
  192. current = context.get(self._sensor_key)
  193. previous = context.get(self._previous_key)
  194. if current is None or previous is None:
  195. return False
  196. try:
  197. current = float(current)
  198. previous = float(previous)
  199. except (TypeError, ValueError):
  200. return False
  201. # Änderung berechnen
  202. if self._percentage and previous != 0:
  203. change = abs((current - previous) / previous * 100)
  204. else:
  205. change = abs(current - previous)
  206. if self._min_change is not None and change < self._min_change:
  207. return False
  208. if self._max_change is not None and change > self._max_change:
  209. return False
  210. return True
  211. def to_dict(self) -> dict:
  212. """Konvertiert zu Dictionary."""
  213. return {
  214. **self._base_dict(),
  215. "sensor_key": self._sensor_key,
  216. "previous_key": self._previous_key,
  217. "min_change": self._min_change,
  218. "max_change": self._max_change,
  219. "percentage": self._percentage,
  220. }
  221. @classmethod
  222. def from_dict(cls, data: dict) -> "SensorChangeCondition":
  223. """Erstellt Condition aus Dictionary."""
  224. return cls(
  225. sensor_key=data["sensor_key"],
  226. previous_key=data.get("previous_key"),
  227. min_change=data.get("min_change"),
  228. max_change=data.get("max_change"),
  229. percentage=data.get("percentage", False),
  230. name=data.get("name"),
  231. invert=data.get("invert", False),
  232. )
  233. @classmethod
  234. def display_name(cls) -> str:
  235. """Anzeigename fuer die TUI."""
  236. return "Sensor-Aenderung"
  237. @classmethod
  238. def form_fields(cls) -> list[FormField]:
  239. """Gibt die Formular-Felder fuer die TUI zurueck."""
  240. return [
  241. FormField(
  242. name="sensor_key", label="Sensor-Schluessel", required=True,
  243. placeholder="temperature",
  244. help_text="Schluessel fuer den Sensorwert im Kontext",
  245. ),
  246. FormField(
  247. name="change_threshold", label="Aenderungs-Schwellwert",
  248. field_type=FieldType.NUMBER, is_float=True,
  249. help_text="Mindestaenderung fuer Ausloesung",
  250. ),
  251. FormField(
  252. name="invert", label="Invertieren",
  253. field_type=FieldType.CHECKBOX, default=False,
  254. ),
  255. ]
  256. @classmethod
  257. def validate_form(cls, data: dict) -> list[FormValidationError]:
  258. """Validiert Formular-Daten. Gibt Liste von Fehlern zurueck."""
  259. errors: list[FormValidationError] = []
  260. if not data.get("sensor_key", "").strip():
  261. errors.append(FormValidationError("sensor_key", "Sensor-Schluessel ist erforderlich"))
  262. return errors