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,282 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2026
3
+ """
4
+ Request coalescing for efficient RPC call deduplication.
5
+
6
+ Overview
7
+ --------
8
+ RequestCoalescer merges identical concurrent requests into a single backend call.
9
+ When multiple callers request the same data simultaneously (e.g., during device
10
+ discovery), only one actual RPC call is made and all callers receive the result.
11
+
12
+ This is particularly beneficial for:
13
+ - Device discovery (multiple getParamsetDescription calls for same device type)
14
+ - Bulk operations that may request overlapping data
15
+ - Any scenario where parallel identical requests would waste bandwidth
16
+
17
+ How It Works
18
+ ------------
19
+ 1. First request for a key starts execution and registers a Future
20
+ 2. Subsequent requests for the same key await the existing Future
21
+ 3. When execution completes, all waiters receive the result (or exception)
22
+ 4. The pending entry is cleaned up for future requests
23
+
24
+ Request A (key="X") ──┬──> Execute ──> Result
25
+
26
+ Request B (key="X") ──┤ │
27
+ │ │
28
+ Request C (key="X") ──┴───────────────┴──> All receive Result
29
+
30
+ Example Usage
31
+ -------------
32
+ from aiohomematic.client import RequestCoalescer
33
+
34
+ coalescer = RequestCoalescer()
35
+
36
+ async def get_paramset(address: str, key: str) -> dict:
37
+ return await coalescer.execute(
38
+ key=f"getParamset:{address}:{key}",
39
+ executor=lambda: client.getParamset(address, key),
40
+ )
41
+
42
+ Thread Safety
43
+ -------------
44
+ RequestCoalescer is designed for single-threaded asyncio use.
45
+ All operations assume they run in the same event loop.
46
+
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ import asyncio
52
+ from collections.abc import Awaitable, Callable
53
+ from dataclasses import dataclass, field
54
+ from datetime import datetime
55
+ import logging
56
+ from typing import TYPE_CHECKING, Any, Final, TypeVar, cast
57
+
58
+ from aiohomematic.central.events import RequestCoalescedEvent
59
+ from aiohomematic.metrics import MetricKeys, emit_counter
60
+
61
+ if TYPE_CHECKING:
62
+ from aiohomematic.central.events import EventBus
63
+
64
+ _LOGGER: Final = logging.getLogger(__name__)
65
+
66
+ T = TypeVar("T")
67
+
68
+
69
+ @dataclass(slots=True)
70
+ class _PendingRequest:
71
+ """Internal tracking for a pending request."""
72
+
73
+ future: asyncio.Future[Any]
74
+ created_at: datetime = field(default_factory=datetime.now)
75
+ waiter_count: int = 1
76
+
77
+
78
+ class RequestCoalescer:
79
+ """
80
+ Coalesce identical concurrent requests into a single execution.
81
+
82
+ When multiple callers request the same operation simultaneously,
83
+ only one actual call is made and all callers receive the result.
84
+ This significantly reduces backend load during bulk operations.
85
+ """
86
+
87
+ def __init__(
88
+ self,
89
+ *,
90
+ name: str = "coalescer",
91
+ event_bus: EventBus | None = None,
92
+ interface_id: str | None = None,
93
+ ) -> None:
94
+ """
95
+ Initialize the request coalescer.
96
+
97
+ Args:
98
+ ----
99
+ name: Name for logging identification
100
+ event_bus: Optional event bus for emitting coalesce events
101
+ interface_id: Optional interface ID for event context
102
+
103
+ """
104
+ self._name: Final = name
105
+ self._event_bus = event_bus
106
+ self._interface_id = interface_id or name
107
+ self._pending: dict[str, _PendingRequest] = {}
108
+ self._total_requests: int = 0
109
+ self._executed_requests: int = 0
110
+
111
+ @property
112
+ def executed_requests(self) -> int:
113
+ """Return the number of executed requests (not coalesced)."""
114
+ return self._executed_requests
115
+
116
+ @property
117
+ def pending_count(self) -> int:
118
+ """Return the number of pending requests."""
119
+ return len(self._pending)
120
+
121
+ @property
122
+ def total_requests(self) -> int:
123
+ """Return the total number of requests received."""
124
+ return self._total_requests
125
+
126
+ def clear(self) -> None:
127
+ """
128
+ Clear all pending requests.
129
+
130
+ Warning: This will cancel any pending futures. Use with caution,
131
+ typically only during shutdown.
132
+ """
133
+ for _key, pending in list(self._pending.items()):
134
+ if not pending.future.done():
135
+ pending.future.cancel()
136
+ self._pending.clear()
137
+ _LOGGER.debug("COALESCER[%s]: Cleared all pending requests", self._name)
138
+
139
+ async def execute(
140
+ self,
141
+ *,
142
+ key: str,
143
+ executor: Callable[[], Awaitable[T]],
144
+ ) -> T:
145
+ """
146
+ Execute a request or wait for an identical pending request.
147
+
148
+ If a request with the same key is already in progress, this call
149
+ will wait for that request to complete and return its result.
150
+ Otherwise, it executes the request and shares the result with
151
+ any other callers that arrive while execution is in progress.
152
+
153
+ Args:
154
+ ----
155
+ key: Unique key identifying the request (e.g., "method:arg1:arg2")
156
+ executor: Async callable that performs the actual request
157
+
158
+ Returns:
159
+ -------
160
+ The result of the request execution
161
+
162
+ Raises:
163
+ ------
164
+ Any exception raised by the executor is propagated to all waiters
165
+
166
+ """
167
+ self._total_requests += 1
168
+
169
+ # Check if there's already a pending request for this key
170
+ if key in self._pending:
171
+ pending = self._pending[key]
172
+ pending.waiter_count += 1
173
+ # Coalescing is a significant event worth tracking (shows efficiency)
174
+ self._emit_coalesced_counter()
175
+ _LOGGER.debug(
176
+ "COALESCER[%s]: Coalescing request for key=%s (waiters=%d)",
177
+ self._name,
178
+ key,
179
+ pending.waiter_count,
180
+ )
181
+ # Emit coalesce event
182
+ self._emit_coalesce_event(key=key, coalesced_count=pending.waiter_count)
183
+ return cast(T, await pending.future)
184
+
185
+ # Create a new pending request
186
+ loop = asyncio.get_running_loop()
187
+ future: asyncio.Future[T] = loop.create_future()
188
+ self._pending[key] = _PendingRequest(future=future)
189
+ self._executed_requests += 1
190
+
191
+ _LOGGER.debug(
192
+ "COALESCER[%s]: Executing request for key=%s",
193
+ self._name,
194
+ key,
195
+ )
196
+
197
+ try:
198
+ result = await executor()
199
+ future.set_result(result)
200
+ except Exception as exc:
201
+ self._emit_failure_counter()
202
+ future.set_exception(exc)
203
+ raise
204
+ else:
205
+ return result
206
+ finally:
207
+ # Clean up the pending entry
208
+ del self._pending[key]
209
+
210
+ def _emit_coalesce_event(self, *, key: str, coalesced_count: int) -> None:
211
+ """
212
+ Emit a request coalesced event.
213
+
214
+ Args:
215
+ ----
216
+ key: The request key that was coalesced
217
+ coalesced_count: Total number of waiters for this key
218
+
219
+ """
220
+ if self._event_bus is None:
221
+ return
222
+
223
+ self._event_bus.publish_sync(
224
+ event=RequestCoalescedEvent(
225
+ timestamp=datetime.now(),
226
+ request_key=key,
227
+ coalesced_count=coalesced_count,
228
+ interface_id=self._interface_id,
229
+ )
230
+ )
231
+
232
+ def _emit_coalesced_counter(self) -> None:
233
+ """Emit a counter for coalesced requests (significant event)."""
234
+ if self._event_bus is None:
235
+ return
236
+
237
+ emit_counter(
238
+ event_bus=self._event_bus,
239
+ key=MetricKeys.coalescer_coalesced(interface_id=self._interface_id),
240
+ )
241
+
242
+ def _emit_failure_counter(self) -> None:
243
+ """Emit a counter for failed requests (significant event)."""
244
+ if self._event_bus is None:
245
+ return
246
+
247
+ emit_counter(
248
+ event_bus=self._event_bus,
249
+ key=MetricKeys.coalescer_failure(interface_id=self._interface_id),
250
+ )
251
+
252
+
253
+ def make_coalesce_key(*, method: str, args: tuple[Any, ...]) -> str:
254
+ """
255
+ Create a coalescing key from method name and arguments.
256
+
257
+ This helper creates a consistent key format for use with RequestCoalescer.
258
+
259
+ Args:
260
+ ----
261
+ method: The RPC method name
262
+ args: The method arguments
263
+
264
+ Returns:
265
+ -------
266
+ A string key suitable for coalescing
267
+
268
+ Example:
269
+ -------
270
+ key = make_coalesce_key(method="getParamset", args=("VCU001:1", "VALUES"))
271
+ # Returns: "getParamset:VCU001:1:VALUES"
272
+
273
+ """
274
+ # Convert args to strings, handling special types
275
+ arg_strs = []
276
+ for arg in args:
277
+ if isinstance(arg, dict):
278
+ # Sort dict items for consistent hashing
279
+ arg_strs.append(str(sorted(arg.items())))
280
+ else:
281
+ arg_strs.append(str(arg))
282
+ return f"{method}:{':'.join(arg_strs)}"