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.
- aiohomematic/__init__.py +110 -0
- aiohomematic/_log_context_protocol.py +29 -0
- aiohomematic/api.py +410 -0
- aiohomematic/async_support.py +250 -0
- aiohomematic/backend_detection.py +462 -0
- aiohomematic/central/__init__.py +103 -0
- aiohomematic/central/async_rpc_server.py +760 -0
- aiohomematic/central/central_unit.py +1152 -0
- aiohomematic/central/config.py +463 -0
- aiohomematic/central/config_builder.py +772 -0
- aiohomematic/central/connection_state.py +160 -0
- aiohomematic/central/coordinators/__init__.py +38 -0
- aiohomematic/central/coordinators/cache.py +414 -0
- aiohomematic/central/coordinators/client.py +480 -0
- aiohomematic/central/coordinators/connection_recovery.py +1141 -0
- aiohomematic/central/coordinators/device.py +1166 -0
- aiohomematic/central/coordinators/event.py +514 -0
- aiohomematic/central/coordinators/hub.py +532 -0
- aiohomematic/central/decorators.py +184 -0
- aiohomematic/central/device_registry.py +229 -0
- aiohomematic/central/events/__init__.py +104 -0
- aiohomematic/central/events/bus.py +1392 -0
- aiohomematic/central/events/integration.py +424 -0
- aiohomematic/central/events/types.py +194 -0
- aiohomematic/central/health.py +762 -0
- aiohomematic/central/rpc_server.py +353 -0
- aiohomematic/central/scheduler.py +794 -0
- aiohomematic/central/state_machine.py +391 -0
- aiohomematic/client/__init__.py +203 -0
- aiohomematic/client/_rpc_errors.py +187 -0
- aiohomematic/client/backends/__init__.py +48 -0
- aiohomematic/client/backends/base.py +335 -0
- aiohomematic/client/backends/capabilities.py +138 -0
- aiohomematic/client/backends/ccu.py +487 -0
- aiohomematic/client/backends/factory.py +116 -0
- aiohomematic/client/backends/homegear.py +294 -0
- aiohomematic/client/backends/json_ccu.py +252 -0
- aiohomematic/client/backends/protocol.py +316 -0
- aiohomematic/client/ccu.py +1857 -0
- aiohomematic/client/circuit_breaker.py +459 -0
- aiohomematic/client/config.py +64 -0
- aiohomematic/client/handlers/__init__.py +40 -0
- aiohomematic/client/handlers/backup.py +157 -0
- aiohomematic/client/handlers/base.py +79 -0
- aiohomematic/client/handlers/device_ops.py +1085 -0
- aiohomematic/client/handlers/firmware.py +144 -0
- aiohomematic/client/handlers/link_mgmt.py +199 -0
- aiohomematic/client/handlers/metadata.py +436 -0
- aiohomematic/client/handlers/programs.py +144 -0
- aiohomematic/client/handlers/sysvars.py +100 -0
- aiohomematic/client/interface_client.py +1304 -0
- aiohomematic/client/json_rpc.py +2068 -0
- aiohomematic/client/request_coalescer.py +282 -0
- aiohomematic/client/rpc_proxy.py +629 -0
- aiohomematic/client/state_machine.py +324 -0
- aiohomematic/const.py +2207 -0
- aiohomematic/context.py +275 -0
- aiohomematic/converter.py +270 -0
- aiohomematic/decorators.py +390 -0
- aiohomematic/exceptions.py +185 -0
- aiohomematic/hmcli.py +997 -0
- aiohomematic/i18n.py +193 -0
- aiohomematic/interfaces/__init__.py +407 -0
- aiohomematic/interfaces/central.py +1067 -0
- aiohomematic/interfaces/client.py +1096 -0
- aiohomematic/interfaces/coordinators.py +63 -0
- aiohomematic/interfaces/model.py +1921 -0
- aiohomematic/interfaces/operations.py +217 -0
- aiohomematic/logging_context.py +134 -0
- aiohomematic/metrics/__init__.py +125 -0
- aiohomematic/metrics/_protocols.py +140 -0
- aiohomematic/metrics/aggregator.py +534 -0
- aiohomematic/metrics/dataclasses.py +489 -0
- aiohomematic/metrics/emitter.py +292 -0
- aiohomematic/metrics/events.py +183 -0
- aiohomematic/metrics/keys.py +300 -0
- aiohomematic/metrics/observer.py +563 -0
- aiohomematic/metrics/stats.py +172 -0
- aiohomematic/model/__init__.py +189 -0
- aiohomematic/model/availability.py +65 -0
- aiohomematic/model/calculated/__init__.py +89 -0
- aiohomematic/model/calculated/climate.py +276 -0
- aiohomematic/model/calculated/data_point.py +315 -0
- aiohomematic/model/calculated/field.py +147 -0
- aiohomematic/model/calculated/operating_voltage_level.py +286 -0
- aiohomematic/model/calculated/support.py +232 -0
- aiohomematic/model/custom/__init__.py +214 -0
- aiohomematic/model/custom/capabilities/__init__.py +67 -0
- aiohomematic/model/custom/capabilities/climate.py +41 -0
- aiohomematic/model/custom/capabilities/light.py +87 -0
- aiohomematic/model/custom/capabilities/lock.py +44 -0
- aiohomematic/model/custom/capabilities/siren.py +63 -0
- aiohomematic/model/custom/climate.py +1130 -0
- aiohomematic/model/custom/cover.py +722 -0
- aiohomematic/model/custom/data_point.py +360 -0
- aiohomematic/model/custom/definition.py +300 -0
- aiohomematic/model/custom/field.py +89 -0
- aiohomematic/model/custom/light.py +1174 -0
- aiohomematic/model/custom/lock.py +322 -0
- aiohomematic/model/custom/mixins.py +445 -0
- aiohomematic/model/custom/profile.py +945 -0
- aiohomematic/model/custom/registry.py +251 -0
- aiohomematic/model/custom/siren.py +462 -0
- aiohomematic/model/custom/switch.py +195 -0
- aiohomematic/model/custom/text_display.py +289 -0
- aiohomematic/model/custom/valve.py +78 -0
- aiohomematic/model/data_point.py +1416 -0
- aiohomematic/model/device.py +1840 -0
- aiohomematic/model/event.py +216 -0
- aiohomematic/model/generic/__init__.py +327 -0
- aiohomematic/model/generic/action.py +40 -0
- aiohomematic/model/generic/action_select.py +62 -0
- aiohomematic/model/generic/binary_sensor.py +30 -0
- aiohomematic/model/generic/button.py +31 -0
- aiohomematic/model/generic/data_point.py +177 -0
- aiohomematic/model/generic/dummy.py +150 -0
- aiohomematic/model/generic/number.py +76 -0
- aiohomematic/model/generic/select.py +56 -0
- aiohomematic/model/generic/sensor.py +76 -0
- aiohomematic/model/generic/switch.py +54 -0
- aiohomematic/model/generic/text.py +33 -0
- aiohomematic/model/hub/__init__.py +100 -0
- aiohomematic/model/hub/binary_sensor.py +24 -0
- aiohomematic/model/hub/button.py +28 -0
- aiohomematic/model/hub/connectivity.py +190 -0
- aiohomematic/model/hub/data_point.py +342 -0
- aiohomematic/model/hub/hub.py +864 -0
- aiohomematic/model/hub/inbox.py +135 -0
- aiohomematic/model/hub/install_mode.py +393 -0
- aiohomematic/model/hub/metrics.py +208 -0
- aiohomematic/model/hub/number.py +42 -0
- aiohomematic/model/hub/select.py +52 -0
- aiohomematic/model/hub/sensor.py +37 -0
- aiohomematic/model/hub/switch.py +43 -0
- aiohomematic/model/hub/text.py +30 -0
- aiohomematic/model/hub/update.py +221 -0
- aiohomematic/model/support.py +592 -0
- aiohomematic/model/update.py +140 -0
- aiohomematic/model/week_profile.py +1827 -0
- aiohomematic/property_decorators.py +719 -0
- aiohomematic/py.typed +0 -0
- aiohomematic/rega_scripts/accept_device_in_inbox.fn +51 -0
- aiohomematic/rega_scripts/create_backup_start.fn +28 -0
- aiohomematic/rega_scripts/create_backup_status.fn +89 -0
- aiohomematic/rega_scripts/fetch_all_device_data.fn +97 -0
- aiohomematic/rega_scripts/get_backend_info.fn +25 -0
- aiohomematic/rega_scripts/get_inbox_devices.fn +61 -0
- aiohomematic/rega_scripts/get_program_descriptions.fn +31 -0
- aiohomematic/rega_scripts/get_serial.fn +44 -0
- aiohomematic/rega_scripts/get_service_messages.fn +83 -0
- aiohomematic/rega_scripts/get_system_update_info.fn +39 -0
- aiohomematic/rega_scripts/get_system_variable_descriptions.fn +31 -0
- aiohomematic/rega_scripts/set_program_state.fn +17 -0
- aiohomematic/rega_scripts/set_system_variable.fn +19 -0
- aiohomematic/rega_scripts/trigger_firmware_update.fn +67 -0
- aiohomematic/schemas.py +256 -0
- aiohomematic/store/__init__.py +55 -0
- aiohomematic/store/dynamic/__init__.py +43 -0
- aiohomematic/store/dynamic/command.py +250 -0
- aiohomematic/store/dynamic/data.py +175 -0
- aiohomematic/store/dynamic/details.py +187 -0
- aiohomematic/store/dynamic/ping_pong.py +416 -0
- aiohomematic/store/persistent/__init__.py +71 -0
- aiohomematic/store/persistent/base.py +285 -0
- aiohomematic/store/persistent/device.py +233 -0
- aiohomematic/store/persistent/incident.py +380 -0
- aiohomematic/store/persistent/paramset.py +241 -0
- aiohomematic/store/persistent/session.py +556 -0
- aiohomematic/store/serialization.py +150 -0
- aiohomematic/store/storage.py +689 -0
- aiohomematic/store/types.py +526 -0
- aiohomematic/store/visibility/__init__.py +40 -0
- aiohomematic/store/visibility/parser.py +141 -0
- aiohomematic/store/visibility/registry.py +722 -0
- aiohomematic/store/visibility/rules.py +307 -0
- aiohomematic/strings.json +237 -0
- aiohomematic/support.py +706 -0
- aiohomematic/tracing.py +236 -0
- aiohomematic/translations/de.json +237 -0
- aiohomematic/translations/en.json +237 -0
- aiohomematic/type_aliases.py +51 -0
- aiohomematic/validator.py +128 -0
- aiohomematic-2026.1.29.dist-info/METADATA +296 -0
- aiohomematic-2026.1.29.dist-info/RECORD +188 -0
- aiohomematic-2026.1.29.dist-info/WHEEL +5 -0
- aiohomematic-2026.1.29.dist-info/entry_points.txt +2 -0
- aiohomematic-2026.1.29.dist-info/licenses/LICENSE +21 -0
- 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)}"
|