aiohomematic 2026.1.29__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. aiohomematic/__init__.py +110 -0
  2. aiohomematic/_log_context_protocol.py +29 -0
  3. aiohomematic/api.py +410 -0
  4. aiohomematic/async_support.py +250 -0
  5. aiohomematic/backend_detection.py +462 -0
  6. aiohomematic/central/__init__.py +103 -0
  7. aiohomematic/central/async_rpc_server.py +760 -0
  8. aiohomematic/central/central_unit.py +1152 -0
  9. aiohomematic/central/config.py +463 -0
  10. aiohomematic/central/config_builder.py +772 -0
  11. aiohomematic/central/connection_state.py +160 -0
  12. aiohomematic/central/coordinators/__init__.py +38 -0
  13. aiohomematic/central/coordinators/cache.py +414 -0
  14. aiohomematic/central/coordinators/client.py +480 -0
  15. aiohomematic/central/coordinators/connection_recovery.py +1141 -0
  16. aiohomematic/central/coordinators/device.py +1166 -0
  17. aiohomematic/central/coordinators/event.py +514 -0
  18. aiohomematic/central/coordinators/hub.py +532 -0
  19. aiohomematic/central/decorators.py +184 -0
  20. aiohomematic/central/device_registry.py +229 -0
  21. aiohomematic/central/events/__init__.py +104 -0
  22. aiohomematic/central/events/bus.py +1392 -0
  23. aiohomematic/central/events/integration.py +424 -0
  24. aiohomematic/central/events/types.py +194 -0
  25. aiohomematic/central/health.py +762 -0
  26. aiohomematic/central/rpc_server.py +353 -0
  27. aiohomematic/central/scheduler.py +794 -0
  28. aiohomematic/central/state_machine.py +391 -0
  29. aiohomematic/client/__init__.py +203 -0
  30. aiohomematic/client/_rpc_errors.py +187 -0
  31. aiohomematic/client/backends/__init__.py +48 -0
  32. aiohomematic/client/backends/base.py +335 -0
  33. aiohomematic/client/backends/capabilities.py +138 -0
  34. aiohomematic/client/backends/ccu.py +487 -0
  35. aiohomematic/client/backends/factory.py +116 -0
  36. aiohomematic/client/backends/homegear.py +294 -0
  37. aiohomematic/client/backends/json_ccu.py +252 -0
  38. aiohomematic/client/backends/protocol.py +316 -0
  39. aiohomematic/client/ccu.py +1857 -0
  40. aiohomematic/client/circuit_breaker.py +459 -0
  41. aiohomematic/client/config.py +64 -0
  42. aiohomematic/client/handlers/__init__.py +40 -0
  43. aiohomematic/client/handlers/backup.py +157 -0
  44. aiohomematic/client/handlers/base.py +79 -0
  45. aiohomematic/client/handlers/device_ops.py +1085 -0
  46. aiohomematic/client/handlers/firmware.py +144 -0
  47. aiohomematic/client/handlers/link_mgmt.py +199 -0
  48. aiohomematic/client/handlers/metadata.py +436 -0
  49. aiohomematic/client/handlers/programs.py +144 -0
  50. aiohomematic/client/handlers/sysvars.py +100 -0
  51. aiohomematic/client/interface_client.py +1304 -0
  52. aiohomematic/client/json_rpc.py +2068 -0
  53. aiohomematic/client/request_coalescer.py +282 -0
  54. aiohomematic/client/rpc_proxy.py +629 -0
  55. aiohomematic/client/state_machine.py +324 -0
  56. aiohomematic/const.py +2207 -0
  57. aiohomematic/context.py +275 -0
  58. aiohomematic/converter.py +270 -0
  59. aiohomematic/decorators.py +390 -0
  60. aiohomematic/exceptions.py +185 -0
  61. aiohomematic/hmcli.py +997 -0
  62. aiohomematic/i18n.py +193 -0
  63. aiohomematic/interfaces/__init__.py +407 -0
  64. aiohomematic/interfaces/central.py +1067 -0
  65. aiohomematic/interfaces/client.py +1096 -0
  66. aiohomematic/interfaces/coordinators.py +63 -0
  67. aiohomematic/interfaces/model.py +1921 -0
  68. aiohomematic/interfaces/operations.py +217 -0
  69. aiohomematic/logging_context.py +134 -0
  70. aiohomematic/metrics/__init__.py +125 -0
  71. aiohomematic/metrics/_protocols.py +140 -0
  72. aiohomematic/metrics/aggregator.py +534 -0
  73. aiohomematic/metrics/dataclasses.py +489 -0
  74. aiohomematic/metrics/emitter.py +292 -0
  75. aiohomematic/metrics/events.py +183 -0
  76. aiohomematic/metrics/keys.py +300 -0
  77. aiohomematic/metrics/observer.py +563 -0
  78. aiohomematic/metrics/stats.py +172 -0
  79. aiohomematic/model/__init__.py +189 -0
  80. aiohomematic/model/availability.py +65 -0
  81. aiohomematic/model/calculated/__init__.py +89 -0
  82. aiohomematic/model/calculated/climate.py +276 -0
  83. aiohomematic/model/calculated/data_point.py +315 -0
  84. aiohomematic/model/calculated/field.py +147 -0
  85. aiohomematic/model/calculated/operating_voltage_level.py +286 -0
  86. aiohomematic/model/calculated/support.py +232 -0
  87. aiohomematic/model/custom/__init__.py +214 -0
  88. aiohomematic/model/custom/capabilities/__init__.py +67 -0
  89. aiohomematic/model/custom/capabilities/climate.py +41 -0
  90. aiohomematic/model/custom/capabilities/light.py +87 -0
  91. aiohomematic/model/custom/capabilities/lock.py +44 -0
  92. aiohomematic/model/custom/capabilities/siren.py +63 -0
  93. aiohomematic/model/custom/climate.py +1130 -0
  94. aiohomematic/model/custom/cover.py +722 -0
  95. aiohomematic/model/custom/data_point.py +360 -0
  96. aiohomematic/model/custom/definition.py +300 -0
  97. aiohomematic/model/custom/field.py +89 -0
  98. aiohomematic/model/custom/light.py +1174 -0
  99. aiohomematic/model/custom/lock.py +322 -0
  100. aiohomematic/model/custom/mixins.py +445 -0
  101. aiohomematic/model/custom/profile.py +945 -0
  102. aiohomematic/model/custom/registry.py +251 -0
  103. aiohomematic/model/custom/siren.py +462 -0
  104. aiohomematic/model/custom/switch.py +195 -0
  105. aiohomematic/model/custom/text_display.py +289 -0
  106. aiohomematic/model/custom/valve.py +78 -0
  107. aiohomematic/model/data_point.py +1416 -0
  108. aiohomematic/model/device.py +1840 -0
  109. aiohomematic/model/event.py +216 -0
  110. aiohomematic/model/generic/__init__.py +327 -0
  111. aiohomematic/model/generic/action.py +40 -0
  112. aiohomematic/model/generic/action_select.py +62 -0
  113. aiohomematic/model/generic/binary_sensor.py +30 -0
  114. aiohomematic/model/generic/button.py +31 -0
  115. aiohomematic/model/generic/data_point.py +177 -0
  116. aiohomematic/model/generic/dummy.py +150 -0
  117. aiohomematic/model/generic/number.py +76 -0
  118. aiohomematic/model/generic/select.py +56 -0
  119. aiohomematic/model/generic/sensor.py +76 -0
  120. aiohomematic/model/generic/switch.py +54 -0
  121. aiohomematic/model/generic/text.py +33 -0
  122. aiohomematic/model/hub/__init__.py +100 -0
  123. aiohomematic/model/hub/binary_sensor.py +24 -0
  124. aiohomematic/model/hub/button.py +28 -0
  125. aiohomematic/model/hub/connectivity.py +190 -0
  126. aiohomematic/model/hub/data_point.py +342 -0
  127. aiohomematic/model/hub/hub.py +864 -0
  128. aiohomematic/model/hub/inbox.py +135 -0
  129. aiohomematic/model/hub/install_mode.py +393 -0
  130. aiohomematic/model/hub/metrics.py +208 -0
  131. aiohomematic/model/hub/number.py +42 -0
  132. aiohomematic/model/hub/select.py +52 -0
  133. aiohomematic/model/hub/sensor.py +37 -0
  134. aiohomematic/model/hub/switch.py +43 -0
  135. aiohomematic/model/hub/text.py +30 -0
  136. aiohomematic/model/hub/update.py +221 -0
  137. aiohomematic/model/support.py +592 -0
  138. aiohomematic/model/update.py +140 -0
  139. aiohomematic/model/week_profile.py +1827 -0
  140. aiohomematic/property_decorators.py +719 -0
  141. aiohomematic/py.typed +0 -0
  142. aiohomematic/rega_scripts/accept_device_in_inbox.fn +51 -0
  143. aiohomematic/rega_scripts/create_backup_start.fn +28 -0
  144. aiohomematic/rega_scripts/create_backup_status.fn +89 -0
  145. aiohomematic/rega_scripts/fetch_all_device_data.fn +97 -0
  146. aiohomematic/rega_scripts/get_backend_info.fn +25 -0
  147. aiohomematic/rega_scripts/get_inbox_devices.fn +61 -0
  148. aiohomematic/rega_scripts/get_program_descriptions.fn +31 -0
  149. aiohomematic/rega_scripts/get_serial.fn +44 -0
  150. aiohomematic/rega_scripts/get_service_messages.fn +83 -0
  151. aiohomematic/rega_scripts/get_system_update_info.fn +39 -0
  152. aiohomematic/rega_scripts/get_system_variable_descriptions.fn +31 -0
  153. aiohomematic/rega_scripts/set_program_state.fn +17 -0
  154. aiohomematic/rega_scripts/set_system_variable.fn +19 -0
  155. aiohomematic/rega_scripts/trigger_firmware_update.fn +67 -0
  156. aiohomematic/schemas.py +256 -0
  157. aiohomematic/store/__init__.py +55 -0
  158. aiohomematic/store/dynamic/__init__.py +43 -0
  159. aiohomematic/store/dynamic/command.py +250 -0
  160. aiohomematic/store/dynamic/data.py +175 -0
  161. aiohomematic/store/dynamic/details.py +187 -0
  162. aiohomematic/store/dynamic/ping_pong.py +416 -0
  163. aiohomematic/store/persistent/__init__.py +71 -0
  164. aiohomematic/store/persistent/base.py +285 -0
  165. aiohomematic/store/persistent/device.py +233 -0
  166. aiohomematic/store/persistent/incident.py +380 -0
  167. aiohomematic/store/persistent/paramset.py +241 -0
  168. aiohomematic/store/persistent/session.py +556 -0
  169. aiohomematic/store/serialization.py +150 -0
  170. aiohomematic/store/storage.py +689 -0
  171. aiohomematic/store/types.py +526 -0
  172. aiohomematic/store/visibility/__init__.py +40 -0
  173. aiohomematic/store/visibility/parser.py +141 -0
  174. aiohomematic/store/visibility/registry.py +722 -0
  175. aiohomematic/store/visibility/rules.py +307 -0
  176. aiohomematic/strings.json +237 -0
  177. aiohomematic/support.py +706 -0
  178. aiohomematic/tracing.py +236 -0
  179. aiohomematic/translations/de.json +237 -0
  180. aiohomematic/translations/en.json +237 -0
  181. aiohomematic/type_aliases.py +51 -0
  182. aiohomematic/validator.py +128 -0
  183. aiohomematic-2026.1.29.dist-info/METADATA +296 -0
  184. aiohomematic-2026.1.29.dist-info/RECORD +188 -0
  185. aiohomematic-2026.1.29.dist-info/WHEEL +5 -0
  186. aiohomematic-2026.1.29.dist-info/entry_points.txt +2 -0
  187. aiohomematic-2026.1.29.dist-info/licenses/LICENSE +21 -0
  188. aiohomematic-2026.1.29.dist-info/top_level.txt +1 -0
@@ -0,0 +1,445 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2026
3
+ """
4
+ Shared mixins for custom data point implementations.
5
+
6
+ This module provides reusable mixin classes that extract common patterns
7
+ from custom data point implementations to reduce code duplication.
8
+
9
+ Mixins
10
+ ------
11
+ - StateChangeTimerMixin: Timer-based state change detection logic
12
+ - OnOffActionMixin: Common on/off action logic with timer support
13
+ - GroupStateMixin: Common group state property pattern
14
+ - PositionMixin: Position conversion logic for covers/blinds
15
+ - BrightnessMixin: Brightness conversion logic for lights/dimmers
16
+ - TimerUnitMixin: Timer unit conversion for lights with on_time/ramp_time
17
+
18
+ Usage
19
+ -----
20
+ Mixins are designed to be used with CustomDataPoint subclasses through
21
+ multiple inheritance::
22
+
23
+ class CustomDpSwitch(StateChangeTimerMixin, GroupStateMixin, CustomDataPoint):
24
+ _category = DataPointCategory.SWITCH
25
+ ...
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from abc import abstractmethod
31
+ from enum import StrEnum
32
+ from typing import TYPE_CHECKING, Any, Protocol, TypedDict, Unpack, runtime_checkable
33
+
34
+ if TYPE_CHECKING:
35
+ from aiohomematic.model.data_point import CallParameterCollector
36
+ from aiohomematic.model.generic import DpAction, DpSwitch
37
+
38
+
39
+ class StateChangeArg(StrEnum):
40
+ """Common state change arguments for on/off data points."""
41
+
42
+ OFF = "off"
43
+ ON = "on"
44
+
45
+
46
+ class StateChangeArgs(TypedDict, total=False):
47
+ """Type-safe arguments for is_state_change() method."""
48
+
49
+ # On/Off state (switch, valve)
50
+ on: bool
51
+ off: bool
52
+
53
+ # Light-specific
54
+ brightness: int
55
+ hs_color: tuple[float, float]
56
+ color_temp_kelvin: int
57
+ effect: str
58
+ on_time: float
59
+ ramp_time: float
60
+
61
+ # Climate-specific
62
+ target_temperature: float
63
+ mode: Any # ClimateMode - using Any to avoid circular import
64
+ profile: Any # ClimateProfile - using Any to avoid circular import
65
+
66
+ # Cover-specific
67
+ close: bool
68
+ open: bool
69
+ position: int | float | None
70
+ tilt_close: bool
71
+ tilt_open: bool
72
+ tilt_position: int | float | None
73
+ vent: bool
74
+
75
+
76
+ @runtime_checkable
77
+ class TimerCapable(Protocol):
78
+ """Protocol for data points with timer capabilities."""
79
+
80
+ @property
81
+ def timer_on_time(self) -> float | None:
82
+ """Return the on_time."""
83
+
84
+ @property
85
+ def timer_on_time_running(self) -> bool:
86
+ """Return if on_time is running."""
87
+
88
+ def get_and_start_timer(self) -> float | None:
89
+ """Get and start the timer."""
90
+
91
+ def is_state_change(self, **kwargs: Unpack[StateChangeArgs]) -> bool:
92
+ """Check if the state changes."""
93
+
94
+ def reset_timer_on_time(self) -> None:
95
+ """Reset the on_time."""
96
+
97
+ def set_timer_on_time(self, *, on_time: float) -> None:
98
+ """Set the on_time."""
99
+
100
+
101
+ @runtime_checkable
102
+ class ValueCapable(Protocol):
103
+ """Protocol for data points with value property."""
104
+
105
+ @property
106
+ def value(self) -> bool | None:
107
+ """Return the current value."""
108
+
109
+
110
+ class StateChangeTimerMixin:
111
+ """
112
+ Mixin providing timer-based state change detection.
113
+
114
+ This mixin implements the common state change detection pattern used
115
+ by switch, valve, light, and similar data points that support on_time timers.
116
+
117
+ Provides:
118
+ - is_timer_state_change(): Timer-only state change detection
119
+ - is_state_change_for_on_off(): Full on/off state change detection (requires value property)
120
+
121
+ Requires the class to implement:
122
+ - timer_on_time property (from BaseDataPoint)
123
+ - timer_on_time_running property (from BaseDataPoint)
124
+ - value property (only for is_state_change_for_on_off)
125
+ """
126
+
127
+ __slots__ = ()
128
+
129
+ # Declare expected attributes from BaseDataPoint
130
+ timer_on_time: float | None
131
+ timer_on_time_running: bool
132
+ # value is expected for is_state_change_for_on_off but not declared here
133
+ # to avoid interfering with subclasses that don't need it
134
+
135
+ def is_state_change_for_on_off(self, **kwargs: Unpack[StateChangeArgs]) -> bool:
136
+ """
137
+ Check if the state changes due to on/off kwargs with timer consideration.
138
+
139
+ Requires the subclass to have a `value` property returning bool | None.
140
+
141
+ Returns True if:
142
+ - Timer is currently running
143
+ - Timer on_time is set
144
+ - Turning on when not already on
145
+ - Turning off when not already off
146
+ """
147
+ if self.is_timer_state_change():
148
+ return True
149
+ value: bool | None = getattr(self, "value", None)
150
+ if kwargs.get(StateChangeArg.ON) is not None and value is not True:
151
+ return True
152
+ return kwargs.get(StateChangeArg.OFF) is not None and value is not False
153
+
154
+ def is_timer_state_change(self) -> bool:
155
+ """
156
+ Check if the state should change due to timer conditions only.
157
+
158
+ Returns True if:
159
+ - Timer is currently running
160
+ - Timer on_time is set
161
+
162
+ This is useful for data points like lights that have more complex
163
+ on/off logic but still need timer-based state change detection.
164
+ """
165
+ if self.timer_on_time_running is True:
166
+ return True
167
+ return self.timer_on_time is not None
168
+
169
+
170
+ class OnOffActionMixin:
171
+ """
172
+ Mixin providing common on/off action implementations.
173
+
174
+ This mixin provides reusable turn_on and turn_off logic that handles
175
+ timer management, state change detection, and value sending.
176
+
177
+ Subclasses must provide:
178
+ - _dp_state: DpSwitch data point
179
+ - _dp_on_time_value: DpAction data point for on_time
180
+ - Timer capability methods (from BaseDataPoint)
181
+ """
182
+
183
+ __slots__ = ()
184
+
185
+ # These are expected to be set by the implementing class
186
+ _dp_state: DpSwitch
187
+ _dp_on_time_value: DpAction
188
+
189
+ @abstractmethod
190
+ def get_and_start_timer(self) -> float | None:
191
+ """Get and start the timer."""
192
+
193
+ @abstractmethod
194
+ def is_state_change(self, **kwargs: Unpack[StateChangeArgs]) -> bool:
195
+ """Check if the state changes due to kwargs."""
196
+
197
+ @abstractmethod
198
+ def reset_timer_on_time(self) -> None:
199
+ """Reset the on_time."""
200
+
201
+ @abstractmethod
202
+ def set_timer_on_time(self, *, on_time: float) -> None:
203
+ """Set the on_time."""
204
+
205
+ async def _perform_turn_off(self, *, collector: CallParameterCollector | None = None) -> None:
206
+ """
207
+ Perform turn off action with timer reset.
208
+
209
+ This is the common implementation for turn_off/close operations.
210
+ """
211
+ self.reset_timer_on_time()
212
+ if not self.is_state_change(off=True):
213
+ return
214
+ await self._dp_state.turn_off(collector=collector)
215
+
216
+ async def _perform_turn_on(
217
+ self, *, on_time: float | None = None, collector: CallParameterCollector | None = None
218
+ ) -> None:
219
+ """
220
+ Perform turn on action with optional timer.
221
+
222
+ This is the common implementation for turn_on/open operations.
223
+ """
224
+ if on_time is not None:
225
+ self.set_timer_on_time(on_time=on_time)
226
+ if not self.is_state_change(on=True):
227
+ return
228
+
229
+ if (timer := self.get_and_start_timer()) is not None:
230
+ await self._dp_on_time_value.send_value(value=timer, collector=collector, do_validate=False)
231
+ await self._dp_state.turn_on(collector=collector)
232
+
233
+
234
+ class GroupStateMixin:
235
+ """
236
+ Mixin for data points that have a group state.
237
+
238
+ Provides common group_value property pattern used by switches,
239
+ valves, and other data points with group state tracking.
240
+ """
241
+
242
+ __slots__ = ()
243
+
244
+ # Expected to be set by implementing class
245
+ _dp_group_state: Any # DpBinarySensor
246
+
247
+ @property
248
+ def group_value(self) -> bool | None:
249
+ """Return the current group value."""
250
+ value: bool | None = self._dp_group_state.value
251
+ return value
252
+
253
+
254
+ class PositionMixin:
255
+ """
256
+ Mixin for data points with position values (0-100%).
257
+
258
+ Provides common position conversion logic for covers, blinds,
259
+ and similar data points that work with percentage positions.
260
+ """
261
+
262
+ __slots__ = ()
263
+
264
+ @staticmethod
265
+ def level_to_position(level: float | None, *, inverted: bool = False) -> int | None:
266
+ """
267
+ Convert level (0.0-1.0) to position percentage (0-100).
268
+
269
+ Args:
270
+ level: Level value between 0.0 and 1.0.
271
+ inverted: If True, invert the position (100 - position).
272
+
273
+ Returns:
274
+ Position as integer percentage, or None if level is None.
275
+
276
+ """
277
+ if level is None:
278
+ return None
279
+ position = int(level * 100)
280
+ return 100 - position if inverted else position
281
+
282
+ @staticmethod
283
+ def position_to_level(position: int, *, inverted: bool = False) -> float:
284
+ """
285
+ Convert position percentage (0-100) to level (0.0-1.0).
286
+
287
+ Args:
288
+ position: Position as integer percentage.
289
+ inverted: If True, invert before conversion.
290
+
291
+ Returns:
292
+ Level value between 0.0 and 1.0.
293
+
294
+ """
295
+ if inverted:
296
+ position = 100 - position
297
+ return position / 100.0
298
+
299
+
300
+ class BrightnessMixin:
301
+ """
302
+ Mixin for data points with brightness values (0-255 or 0-100%).
303
+
304
+ Provides common brightness conversion logic for lights and dimmers.
305
+ """
306
+
307
+ __slots__ = ()
308
+
309
+ # Constants for brightness conversion
310
+ _MAX_BRIGHTNESS: int = 255
311
+ _BRIGHTNESS_PCT_MULTIPLIER: int = 100
312
+
313
+ @staticmethod
314
+ def brightness_to_level(brightness: int, *, max_brightness: int = 255) -> float:
315
+ """
316
+ Convert brightness (0-max_brightness) to level (0.0-1.0).
317
+
318
+ Args:
319
+ brightness: Brightness value.
320
+ max_brightness: Maximum brightness value (default 255).
321
+
322
+ Returns:
323
+ Level value between 0.0 and 1.0.
324
+
325
+ """
326
+ return brightness / max_brightness
327
+
328
+ @staticmethod
329
+ def level_to_brightness(level: float | None, *, max_brightness: int = 255) -> int:
330
+ """
331
+ Convert level (0.0-1.0) to brightness (0-max_brightness).
332
+
333
+ Args:
334
+ level: Level value between 0.0 and 1.0.
335
+ max_brightness: Maximum brightness value (default 255).
336
+
337
+ Returns:
338
+ Brightness as integer, or 0 if level is None.
339
+
340
+ """
341
+ if level is None:
342
+ return 0
343
+ return int(level * max_brightness)
344
+
345
+ @staticmethod
346
+ def level_to_brightness_pct(level: float | None) -> int:
347
+ """
348
+ Convert level (0.0-1.0) to brightness percentage (0-100).
349
+
350
+ Args:
351
+ level: Level value between 0.0 and 1.0.
352
+
353
+ Returns:
354
+ Brightness percentage as integer, or 0 if level is None.
355
+
356
+ """
357
+ if level is None:
358
+ return 0
359
+ return int(level * 100)
360
+
361
+
362
+ class _TimeUnit:
363
+ """Time unit constants for timer conversion."""
364
+
365
+ SECONDS: str = "S"
366
+ MINUTES: str = "M"
367
+ HOURS: str = "H"
368
+
369
+
370
+ # Marker value indicating timer is not used
371
+ _TIMER_NOT_USED: float = 111600.0
372
+
373
+ # Threshold for time unit conversion (max value before switching units)
374
+ _TIME_UNIT_THRESHOLD: int = 16343
375
+
376
+
377
+ class TimerUnitMixin:
378
+ """
379
+ Mixin for lights with time unit conversion for on_time and ramp_time.
380
+
381
+ Provides common timer value setting methods that handle automatic
382
+ unit conversion (seconds -> minutes -> hours) for large time values.
383
+
384
+ Requires the class to have:
385
+ - _dp_on_time_value: DpAction for on_time value
386
+ - _dp_on_time_unit: DpAction for on_time unit
387
+ - _dp_ramp_time_value: DpAction for ramp_time value
388
+ - _dp_ramp_time_unit: DpAction for ramp_time unit
389
+ """
390
+
391
+ __slots__ = ()
392
+
393
+ # Expected to be set by implementing class
394
+ _dp_on_time_value: Any
395
+ _dp_on_time_unit: Any
396
+ _dp_ramp_time_value: Any
397
+ _dp_ramp_time_unit: Any
398
+
399
+ @staticmethod
400
+ def _recalc_unit_timer(*, time: float) -> tuple[float, str]:
401
+ """
402
+ Recalculate unit and value of timer.
403
+
404
+ Converts large time values to appropriate units:
405
+ - > 16343 seconds -> minutes
406
+ - > 16343 minutes -> hours
407
+
408
+ For the NOT_USED marker (111600), returns HOURS as unit to ensure
409
+ the device interprets the value correctly (111600 hours ≈ 554 days).
410
+
411
+ Args:
412
+ time: Time value in seconds.
413
+
414
+ Returns:
415
+ Tuple of (converted_time, unit) where unit is "S"/"M"/"H".
416
+
417
+ """
418
+ time_unit = _TimeUnit.SECONDS
419
+ if time == _TIMER_NOT_USED:
420
+ return time, _TimeUnit.HOURS
421
+ if time > _TIME_UNIT_THRESHOLD:
422
+ time /= 60
423
+ time_unit = _TimeUnit.MINUTES
424
+ if time > _TIME_UNIT_THRESHOLD:
425
+ time /= 60
426
+ time_unit = _TimeUnit.HOURS
427
+ return time, time_unit
428
+
429
+ async def _set_on_time_value(self, *, on_time: float, collector: Any | None = None) -> None:
430
+ """Set the on time value with automatic unit conversion."""
431
+ on_time, on_time_unit = self._recalc_unit_timer(time=on_time)
432
+ if on_time_unit:
433
+ await self._dp_on_time_unit.send_value(value=on_time_unit, collector=collector)
434
+ await self._dp_on_time_value.send_value(value=float(on_time), collector=collector)
435
+
436
+ async def _set_ramp_time_off_value(self, *, ramp_time: float, collector: Any | None = None) -> None:
437
+ """Set the ramp time off value with automatic unit conversion."""
438
+ await self._set_ramp_time_on_value(ramp_time=ramp_time, collector=collector)
439
+
440
+ async def _set_ramp_time_on_value(self, *, ramp_time: float, collector: Any | None = None) -> None:
441
+ """Set the ramp time on value with automatic unit conversion."""
442
+ ramp_time, ramp_time_unit = self._recalc_unit_timer(time=ramp_time)
443
+ if ramp_time_unit:
444
+ await self._dp_ramp_time_unit.send_value(value=ramp_time_unit, collector=collector)
445
+ await self._dp_ramp_time_value.send_value(value=float(ramp_time), collector=collector)