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,324 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2026
3
+ """
4
+ Client state machine for managing connection lifecycle.
5
+
6
+ This module provides a state machine for tracking client connection states
7
+ with validated transitions and event emission.
8
+
9
+ The state machine ensures:
10
+ - Only valid state transitions occur
11
+ - State changes are logged for debugging
12
+ - Invalid transitions raise exceptions for early error detection
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from datetime import datetime
18
+ import logging
19
+ from typing import TYPE_CHECKING, Final
20
+
21
+ from aiohomematic.central.events.types import ClientStateChangedEvent
22
+ from aiohomematic.const import ClientState, FailureReason
23
+ from aiohomematic.property_decorators import DelegatedProperty
24
+
25
+ if TYPE_CHECKING:
26
+ from aiohomematic.central.events import EventBus
27
+
28
+ _LOGGER: Final = logging.getLogger(__name__)
29
+
30
+ # Valid state transitions define which state changes are allowed.
31
+ # This forms a directed graph where each state maps to its valid successors.
32
+ #
33
+ # State Diagram:
34
+ #
35
+ # CREATED ──► INITIALIZING ──► INITIALIZED ──► CONNECTING ──► CONNECTED
36
+ # │ ▲ │
37
+ # ▼ │ ▼
38
+ # FAILED ◄─────────────────────────┬┴─────── DISCONNECTED
39
+ # │ │ ▲
40
+ # ├─────► INITIALIZING │ │
41
+ # ├─────► CONNECTING ◄──────────┴──────── RECONNECTING
42
+ # ├─────► DISCONNECTED (for graceful shutdown) ▲
43
+ # └─────► RECONNECTING ────────────────────────┘
44
+ #
45
+ # STOPPED ◄── STOPPING ◄─────────────────────────(from CONNECTED/DISCONNECTED/RECONNECTING)
46
+ #
47
+ _VALID_TRANSITIONS: Final[dict[ClientState, frozenset[ClientState]]] = {
48
+ # Initial state after client creation - can only begin initialization
49
+ ClientState.CREATED: frozenset({ClientState.INITIALIZING}),
50
+ # During initialization (loading metadata, etc.) - succeeds or fails
51
+ ClientState.INITIALIZING: frozenset({ClientState.INITIALIZED, ClientState.FAILED}),
52
+ # Initialization complete - ready to establish connection
53
+ ClientState.INITIALIZED: frozenset({ClientState.CONNECTING}),
54
+ # Attempting to connect to backend - succeeds or fails
55
+ ClientState.CONNECTING: frozenset({ClientState.CONNECTED, ClientState.FAILED}),
56
+ # Fully connected and operational
57
+ ClientState.CONNECTED: frozenset(
58
+ {
59
+ ClientState.DISCONNECTED, # Connection lost unexpectedly
60
+ ClientState.RECONNECTING, # Attempting automatic reconnection
61
+ ClientState.STOPPING, # Graceful shutdown requested
62
+ }
63
+ ),
64
+ # Connection was lost or intentionally closed
65
+ ClientState.DISCONNECTED: frozenset(
66
+ {
67
+ ClientState.CONNECTING, # Manual reconnection attempt
68
+ ClientState.DISCONNECTED, # Idempotent - allows repeated deinitialize calls
69
+ ClientState.RECONNECTING, # Automatic reconnection attempt
70
+ ClientState.STOPPING, # Graceful shutdown requested
71
+ }
72
+ ),
73
+ # Automatic reconnection in progress
74
+ ClientState.RECONNECTING: frozenset(
75
+ {
76
+ ClientState.CONNECTED, # Reconnection succeeded
77
+ ClientState.DISCONNECTED, # Reconnection abandoned
78
+ ClientState.FAILED, # Reconnection failed permanently
79
+ ClientState.CONNECTING, # Retry connection establishment
80
+ }
81
+ ),
82
+ # Graceful shutdown in progress - one-way to STOPPED
83
+ ClientState.STOPPING: frozenset({ClientState.STOPPED}),
84
+ # Terminal state - client is fully stopped, no transitions allowed
85
+ ClientState.STOPPED: frozenset(),
86
+ # Error state - allows retry via re-initialization, reconnection, or graceful shutdown
87
+ ClientState.FAILED: frozenset(
88
+ {
89
+ ClientState.INITIALIZING, # Retry initialization
90
+ ClientState.CONNECTING, # Retry connection
91
+ ClientState.RECONNECTING, # Automatic reconnection attempt
92
+ ClientState.DISCONNECTED, # Graceful shutdown via deinitialize_proxy
93
+ }
94
+ ),
95
+ }
96
+
97
+
98
+ class InvalidStateTransitionError(Exception):
99
+ """Raised when an invalid state transition is attempted."""
100
+
101
+ def __init__(self, *, current: ClientState, target: ClientState, interface_id: str) -> None:
102
+ """Initialize the error."""
103
+ self.current = current
104
+ self.target = target
105
+ self.interface_id = interface_id
106
+ super().__init__(
107
+ f"Invalid state transition from {current.value} to {target.value} for interface {interface_id}"
108
+ )
109
+
110
+
111
+ class ClientStateMachine:
112
+ """
113
+ State machine for client connection lifecycle.
114
+
115
+ This class manages the connection state of a client with validated
116
+ transitions and event emission via EventBus.
117
+
118
+ Thread Safety
119
+ -------------
120
+ This class is NOT thread-safe. All calls should happen from the same
121
+ event loop/thread.
122
+
123
+ Example:
124
+ -------
125
+ from aiohomematic.central.events import ClientStateChangedEvent, EventBus
126
+
127
+ def on_state_changed(*, event: ClientStateChangedEvent) -> None:
128
+ print(f"State changed: {event.old_state} -> {event.new_state}")
129
+
130
+ event_bus = EventBus()
131
+ sm = ClientStateMachine(interface_id="BidCos-RF", event_bus=event_bus)
132
+
133
+ # Subscribe to state changes via EventBus
134
+ event_bus.subscribe(
135
+ event_type=ClientStateChangedEvent,
136
+ event_key="BidCos-RF",
137
+ handler=on_state_changed,
138
+ )
139
+
140
+ sm.transition_to(target=ClientState.INITIALIZING)
141
+ sm.transition_to(target=ClientState.INITIALIZED)
142
+ sm.transition_to(target=ClientState.CONNECTING)
143
+ sm.transition_to(target=ClientState.CONNECTED)
144
+
145
+ """
146
+
147
+ __slots__ = (
148
+ "_event_bus",
149
+ "_failure_message",
150
+ "_failure_reason",
151
+ "_interface_id",
152
+ "_state",
153
+ )
154
+
155
+ def __init__(
156
+ self,
157
+ *,
158
+ interface_id: str,
159
+ event_bus: EventBus | None = None,
160
+ ) -> None:
161
+ """
162
+ Initialize the state machine.
163
+
164
+ Args:
165
+ ----
166
+ interface_id: Interface identifier for logging
167
+ event_bus: Optional EventBus for state change events
168
+
169
+ """
170
+ self._interface_id: Final = interface_id
171
+ self._event_bus = event_bus
172
+ self._state: ClientState = ClientState.CREATED
173
+ self._failure_reason: FailureReason = FailureReason.NONE
174
+ self._failure_message: str = ""
175
+
176
+ failure_message: Final = DelegatedProperty[str](path="_failure_message")
177
+ failure_reason: Final = DelegatedProperty[FailureReason](path="_failure_reason")
178
+ state: Final = DelegatedProperty[ClientState](path="_state")
179
+
180
+ @property
181
+ def can_reconnect(self) -> bool:
182
+ """Return True if reconnection is allowed from current state."""
183
+ return ClientState.RECONNECTING in _VALID_TRANSITIONS.get(self._state, frozenset())
184
+
185
+ @property
186
+ def is_available(self) -> bool:
187
+ """Return True if client is available (connected or reconnecting)."""
188
+ return self._state in (ClientState.CONNECTED, ClientState.RECONNECTING)
189
+
190
+ @property
191
+ def is_connected(self) -> bool:
192
+ """Return True if client is in connected state."""
193
+ return self._state == ClientState.CONNECTED
194
+
195
+ @property
196
+ def is_failed(self) -> bool:
197
+ """Return True if client is in failed state."""
198
+ return self._state == ClientState.FAILED
199
+
200
+ @property
201
+ def is_stopped(self) -> bool:
202
+ """Return True if client is stopped."""
203
+ return self._state == ClientState.STOPPED
204
+
205
+ def can_transition_to(self, *, target: ClientState) -> bool:
206
+ """
207
+ Check if transition to target state is valid.
208
+
209
+ Args:
210
+ ----
211
+ target: Target state to check
212
+
213
+ Returns:
214
+ -------
215
+ True if transition is valid, False otherwise
216
+
217
+ """
218
+ return target in _VALID_TRANSITIONS.get(self._state, frozenset())
219
+
220
+ def reset(self) -> None:
221
+ """
222
+ Reset state machine to CREATED state.
223
+
224
+ This should only be used during testing or exceptional recovery.
225
+ """
226
+ old_state = self._state
227
+ self._state = ClientState.CREATED
228
+ self._failure_reason = FailureReason.NONE
229
+ self._failure_message = ""
230
+ _LOGGER.warning( # i18n-log: ignore
231
+ "STATE_MACHINE: %s: Reset from %s to CREATED",
232
+ self._interface_id,
233
+ old_state.value,
234
+ )
235
+
236
+ def transition_to(
237
+ self,
238
+ *,
239
+ target: ClientState,
240
+ reason: str = "",
241
+ force: bool = False,
242
+ failure_reason: FailureReason = FailureReason.NONE,
243
+ ) -> None:
244
+ """
245
+ Transition to a new state.
246
+
247
+ Args:
248
+ ----
249
+ target: Target state to transition to
250
+ reason: Human-readable reason for the transition
251
+ force: If True, skip validation (use with caution)
252
+ failure_reason: Categorized failure reason (only used when target is FAILED)
253
+
254
+ Raises:
255
+ ------
256
+ InvalidStateTransitionError: If transition is not valid and force=False
257
+
258
+ """
259
+ if not force and not self.can_transition_to(target=target):
260
+ raise InvalidStateTransitionError(
261
+ current=self._state,
262
+ target=target,
263
+ interface_id=self._interface_id,
264
+ )
265
+
266
+ old_state = self._state
267
+ self._state = target
268
+
269
+ # Track failure reason when entering FAILED state
270
+ if target == ClientState.FAILED:
271
+ self._failure_reason = failure_reason
272
+ self._failure_message = reason
273
+ elif target in (ClientState.CONNECTED, ClientState.INITIALIZED):
274
+ # Clear failure info on successful states
275
+ self._failure_reason = FailureReason.NONE
276
+ self._failure_message = ""
277
+
278
+ # Log at INFO level for important transitions, DEBUG for others
279
+ if target in (ClientState.CONNECTED, ClientState.DISCONNECTED, ClientState.FAILED):
280
+ failure_info = f" [reason={failure_reason.value}]" if target == ClientState.FAILED else ""
281
+ _LOGGER.info( # i18n-log: ignore
282
+ "CLIENT_STATE: %s: %s -> %s%s%s",
283
+ self._interface_id,
284
+ old_state.value,
285
+ target.value,
286
+ f" ({reason})" if reason else "",
287
+ failure_info,
288
+ )
289
+ else:
290
+ _LOGGER.debug(
291
+ "CLIENT_STATE: %s: %s -> %s%s",
292
+ self._interface_id,
293
+ old_state.value,
294
+ target.value,
295
+ f" ({reason})" if reason else "",
296
+ )
297
+
298
+ # Emit state change event
299
+ self._emit_state_change_event(
300
+ old_state=old_state,
301
+ new_state=target,
302
+ trigger=reason or None,
303
+ )
304
+
305
+ def _emit_state_change_event(
306
+ self,
307
+ *,
308
+ old_state: ClientState,
309
+ new_state: ClientState,
310
+ trigger: str | None,
311
+ ) -> None:
312
+ """Emit a client state change event."""
313
+ if self._event_bus is None:
314
+ return
315
+
316
+ self._event_bus.publish_sync(
317
+ event=ClientStateChangedEvent(
318
+ timestamp=datetime.now(),
319
+ interface_id=self._interface_id,
320
+ old_state=old_state,
321
+ new_state=new_state,
322
+ trigger=trigger,
323
+ )
324
+ )