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
aiohomematic/context.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Context variables for request tracking and implicit context propagation.
|
|
5
|
+
|
|
6
|
+
This module provides context variables that flow through async call chains
|
|
7
|
+
without explicit parameter passing, enabling request correlation, tracing,
|
|
8
|
+
and cross-cutting concerns.
|
|
9
|
+
|
|
10
|
+
Key features:
|
|
11
|
+
- RequestContext for tracking operations with correlation IDs
|
|
12
|
+
- Automatic propagation through async call chains
|
|
13
|
+
- Context manager for scoped context setting
|
|
14
|
+
- Service call detection via is_in_service()
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
async with request_context(operation="set_value", device_address="ABC123"):
|
|
18
|
+
await device.send_value(...) # Context propagates automatically
|
|
19
|
+
|
|
20
|
+
# Access context anywhere in the call chain
|
|
21
|
+
ctx = get_request_context()
|
|
22
|
+
request_id = get_request_id()
|
|
23
|
+
|
|
24
|
+
Public API of this module is defined by __all__.
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from contextvars import ContextVar, Token
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from datetime import datetime
|
|
33
|
+
from typing import Any
|
|
34
|
+
import uuid
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class RequestContext:
|
|
39
|
+
"""
|
|
40
|
+
Context for a single request/operation.
|
|
41
|
+
|
|
42
|
+
Automatically propagates through async call chains via context variables.
|
|
43
|
+
Immutable to prevent accidental modification; use with_* methods to create
|
|
44
|
+
modified copies.
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
request_id: Unique identifier for this request (8 chars from UUID).
|
|
48
|
+
operation: Name of the operation being performed.
|
|
49
|
+
device_address: Address of the device being operated on, if applicable.
|
|
50
|
+
interface_id: ID of the interface being used, if applicable.
|
|
51
|
+
started_at: Timestamp when the request started.
|
|
52
|
+
extra: Additional context-specific attributes.
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
request_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
|
|
57
|
+
operation: str = ""
|
|
58
|
+
device_address: str | None = None
|
|
59
|
+
interface_id: str | None = None
|
|
60
|
+
started_at: datetime = field(default_factory=datetime.now)
|
|
61
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def elapsed_ms(self) -> float:
|
|
65
|
+
"""Return milliseconds since request started."""
|
|
66
|
+
return (datetime.now() - self.started_at).total_seconds() * 1000
|
|
67
|
+
|
|
68
|
+
def with_device(self, *, device_address: str) -> RequestContext:
|
|
69
|
+
"""
|
|
70
|
+
Create new context with updated device address.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
device_address: The device address to set.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
New RequestContext with updated device, preserving other fields.
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
return RequestContext(
|
|
80
|
+
request_id=self.request_id,
|
|
81
|
+
operation=self.operation,
|
|
82
|
+
device_address=device_address,
|
|
83
|
+
interface_id=self.interface_id,
|
|
84
|
+
started_at=self.started_at,
|
|
85
|
+
extra=self.extra,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def with_extra(self, **kwargs: Any) -> RequestContext:
|
|
89
|
+
"""
|
|
90
|
+
Create new context with additional extra attributes.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
**kwargs: Additional attributes to merge into extra.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
New RequestContext with merged extra attributes.
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
return RequestContext(
|
|
100
|
+
request_id=self.request_id,
|
|
101
|
+
operation=self.operation,
|
|
102
|
+
device_address=self.device_address,
|
|
103
|
+
interface_id=self.interface_id,
|
|
104
|
+
started_at=self.started_at,
|
|
105
|
+
extra={**self.extra, **kwargs},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def with_operation(self, *, operation: str) -> RequestContext:
|
|
109
|
+
"""
|
|
110
|
+
Create new context with updated operation.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
operation: The new operation name.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
New RequestContext with updated operation, preserving other fields.
|
|
117
|
+
|
|
118
|
+
"""
|
|
119
|
+
return RequestContext(
|
|
120
|
+
request_id=self.request_id,
|
|
121
|
+
operation=operation,
|
|
122
|
+
device_address=self.device_address,
|
|
123
|
+
interface_id=self.interface_id,
|
|
124
|
+
started_at=self.started_at,
|
|
125
|
+
extra=self.extra,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Context variable for request tracking
|
|
130
|
+
_request_context: ContextVar[RequestContext | None] = ContextVar(
|
|
131
|
+
"request_context",
|
|
132
|
+
default=None,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_request_context() -> RequestContext | None:
|
|
137
|
+
"""
|
|
138
|
+
Get the current request context.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
The current RequestContext, or None if no context is set.
|
|
142
|
+
|
|
143
|
+
"""
|
|
144
|
+
return _request_context.get()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_request_id() -> str:
|
|
148
|
+
"""
|
|
149
|
+
Get the current request ID or a default value.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
The current request ID, or "anonymous" if no context is set.
|
|
153
|
+
|
|
154
|
+
"""
|
|
155
|
+
ctx = _request_context.get()
|
|
156
|
+
return ctx.request_id if ctx else "anonymous"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class request_context: # noqa: N801
|
|
160
|
+
"""
|
|
161
|
+
Context manager for request tracking.
|
|
162
|
+
|
|
163
|
+
Sets a RequestContext for the duration of the block, automatically
|
|
164
|
+
propagating through async call chains.
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
async with request_context(operation="set_value", device_address="ABC123"):
|
|
168
|
+
await device.send_value(...) # Context propagates automatically
|
|
169
|
+
|
|
170
|
+
# Synchronous usage also supported
|
|
171
|
+
with request_context(operation="validate"):
|
|
172
|
+
validate_config()
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
__slots__ = ("_ctx", "_token")
|
|
177
|
+
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
*,
|
|
181
|
+
operation: str = "",
|
|
182
|
+
device_address: str | None = None,
|
|
183
|
+
interface_id: str | None = None,
|
|
184
|
+
**extra: Any,
|
|
185
|
+
) -> None:
|
|
186
|
+
"""
|
|
187
|
+
Initialize request context.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
operation: Name of the operation being performed.
|
|
191
|
+
device_address: Address of the device being operated on.
|
|
192
|
+
interface_id: ID of the interface being used.
|
|
193
|
+
**extra: Additional context-specific attributes.
|
|
194
|
+
|
|
195
|
+
"""
|
|
196
|
+
self._ctx = RequestContext(
|
|
197
|
+
operation=operation,
|
|
198
|
+
device_address=device_address,
|
|
199
|
+
interface_id=interface_id,
|
|
200
|
+
extra=extra,
|
|
201
|
+
)
|
|
202
|
+
self._token: Token[RequestContext | None] | None = None
|
|
203
|
+
|
|
204
|
+
async def __aenter__(self) -> RequestContext:
|
|
205
|
+
"""Async enter - delegates to sync enter."""
|
|
206
|
+
return self.__enter__()
|
|
207
|
+
|
|
208
|
+
async def __aexit__(self, *args: object) -> None:
|
|
209
|
+
"""Async exit - delegates to sync exit."""
|
|
210
|
+
self.__exit__()
|
|
211
|
+
|
|
212
|
+
def __enter__(self) -> RequestContext:
|
|
213
|
+
"""Enter context and set the request context."""
|
|
214
|
+
self._token = _request_context.set(self._ctx)
|
|
215
|
+
return self._ctx
|
|
216
|
+
|
|
217
|
+
def __exit__(self, *args: object) -> None:
|
|
218
|
+
"""Exit context and reset the request context."""
|
|
219
|
+
if self._token is not None:
|
|
220
|
+
_request_context.reset(self._token)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def set_request_context(*, ctx: RequestContext) -> Token[RequestContext | None]:
|
|
224
|
+
"""
|
|
225
|
+
Manually set the request context.
|
|
226
|
+
|
|
227
|
+
Returns a token that must be used with reset_request_context().
|
|
228
|
+
Prefer using the request_context context manager instead.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
ctx: The RequestContext to set.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Token for resetting the context.
|
|
235
|
+
|
|
236
|
+
"""
|
|
237
|
+
return _request_context.set(ctx)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def reset_request_context(*, token: Token[RequestContext | None]) -> None:
|
|
241
|
+
"""
|
|
242
|
+
Reset the request context using a token from set_request_context().
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
token: Token returned from set_request_context().
|
|
246
|
+
|
|
247
|
+
"""
|
|
248
|
+
_request_context.reset(token)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def is_in_service() -> bool:
|
|
252
|
+
"""
|
|
253
|
+
Check if currently executing within a service call.
|
|
254
|
+
|
|
255
|
+
A service call is identified by having a RequestContext with an operation
|
|
256
|
+
that starts with "service:".
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
True if currently inside a service call, False otherwise.
|
|
260
|
+
|
|
261
|
+
"""
|
|
262
|
+
ctx = _request_context.get()
|
|
263
|
+
return ctx is not None and ctx.operation.startswith("service:")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# Define public API for this module
|
|
267
|
+
__all__ = [
|
|
268
|
+
"RequestContext",
|
|
269
|
+
"get_request_context",
|
|
270
|
+
"get_request_id",
|
|
271
|
+
"is_in_service",
|
|
272
|
+
"request_context",
|
|
273
|
+
"reset_request_context",
|
|
274
|
+
"set_request_context",
|
|
275
|
+
]
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Converters used by aiohomematic.
|
|
5
|
+
|
|
6
|
+
This module provides two categories of converters:
|
|
7
|
+
|
|
8
|
+
1. General type converters using singledispatch:
|
|
9
|
+
- to_homematic_value: Convert Python types to Homematic-compatible values
|
|
10
|
+
- from_homematic_value: Convert Homematic values to Python types
|
|
11
|
+
|
|
12
|
+
2. Combined parameter converters:
|
|
13
|
+
- convert_combined_parameter_to_paramset: Parse combined parameter strings
|
|
14
|
+
- convert_hm_level_to_cpv: Convert level to combined parameter value
|
|
15
|
+
|
|
16
|
+
The singledispatch converters are extensible - register new type handlers with:
|
|
17
|
+
@to_homematic_value.register(YourType)
|
|
18
|
+
def _(value: YourType) -> Any:
|
|
19
|
+
return converted_value
|
|
20
|
+
|
|
21
|
+
Public API of this module is defined by __all__.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import ast
|
|
27
|
+
from datetime import datetime, timedelta
|
|
28
|
+
from enum import Enum
|
|
29
|
+
from functools import lru_cache, singledispatch
|
|
30
|
+
import inspect
|
|
31
|
+
import logging
|
|
32
|
+
from typing import Any, Final, cast
|
|
33
|
+
|
|
34
|
+
from aiohomematic.const import Parameter
|
|
35
|
+
from aiohomematic.support import extract_exc_args
|
|
36
|
+
|
|
37
|
+
_LOGGER = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# =============================================================================
|
|
41
|
+
# SINGLEDISPATCH CONVERTERS: Python → Homematic
|
|
42
|
+
# =============================================================================
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@singledispatch
|
|
46
|
+
def to_homematic_value(value: Any) -> Any: # kwonly: disable
|
|
47
|
+
"""
|
|
48
|
+
Convert Python values to Homematic-compatible values.
|
|
49
|
+
|
|
50
|
+
Uses singledispatch for type-based conversion. The function automatically
|
|
51
|
+
selects the appropriate converter based on the input type.
|
|
52
|
+
|
|
53
|
+
Default behavior (unregistered types):
|
|
54
|
+
Returns value unchanged.
|
|
55
|
+
|
|
56
|
+
Registered conversions:
|
|
57
|
+
- bool → int (True=1, False=0)
|
|
58
|
+
- float → float (rounded to 6 decimal places)
|
|
59
|
+
- datetime → str (ISO format)
|
|
60
|
+
- timedelta → float (total seconds)
|
|
61
|
+
- Enum → value attribute
|
|
62
|
+
- list → list (items converted recursively)
|
|
63
|
+
- dict → dict (values converted recursively)
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
value: Python value to convert.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Homematic-compatible value.
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
>>> to_homematic_value(True)
|
|
73
|
+
1
|
|
74
|
+
>>> to_homematic_value(3.14159265359)
|
|
75
|
+
3.141593
|
|
76
|
+
>>> to_homematic_value(MyEnum.VALUE)
|
|
77
|
+
'VALUE'
|
|
78
|
+
|
|
79
|
+
Extensibility:
|
|
80
|
+
Register handlers for custom types:
|
|
81
|
+
|
|
82
|
+
@to_homematic_value.register(Color)
|
|
83
|
+
def _(value: Color) -> int:
|
|
84
|
+
return (value.r << 16) | (value.g << 8) | value.b
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
return value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@to_homematic_value.register(bool)
|
|
91
|
+
def _to_hm_bool(value: bool) -> int: # kwonly: disable
|
|
92
|
+
"""Convert boolean to Homematic integer (1/0)."""
|
|
93
|
+
return 1 if value else 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@to_homematic_value.register(float)
|
|
97
|
+
def _to_hm_float(value: float) -> float: # kwonly: disable
|
|
98
|
+
"""Convert float to Homematic float (6 decimal places max)."""
|
|
99
|
+
return round(value, 6)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@to_homematic_value.register(datetime)
|
|
103
|
+
def _to_hm_datetime(value: datetime) -> str: # kwonly: disable
|
|
104
|
+
"""Convert datetime to ISO format string."""
|
|
105
|
+
return value.isoformat()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@to_homematic_value.register(timedelta)
|
|
109
|
+
def _to_hm_timedelta(value: timedelta) -> float: # kwonly: disable
|
|
110
|
+
"""Convert timedelta to total seconds."""
|
|
111
|
+
return value.total_seconds()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@to_homematic_value.register(Enum)
|
|
115
|
+
def _to_hm_enum(value: Enum) -> Any: # kwonly: disable
|
|
116
|
+
"""Convert Enum to its value."""
|
|
117
|
+
return value.value
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@to_homematic_value.register(list)
|
|
121
|
+
def _to_hm_list(value: list[Any]) -> list[Any]: # kwonly: disable
|
|
122
|
+
"""Convert list elements recursively."""
|
|
123
|
+
return [to_homematic_value(item) for item in value]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@to_homematic_value.register(dict)
|
|
127
|
+
def _to_hm_dict(value: dict[str, Any]) -> dict[str, Any]: # kwonly: disable
|
|
128
|
+
"""Convert dict values recursively."""
|
|
129
|
+
return {k: to_homematic_value(v) for k, v in value.items()}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# =============================================================================
|
|
133
|
+
# SINGLEDISPATCH CONVERTERS: Homematic → Python
|
|
134
|
+
# =============================================================================
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@singledispatch
|
|
138
|
+
def from_homematic_value(value: Any, *, target_type: type | None = None) -> Any: # kwonly: disable
|
|
139
|
+
"""
|
|
140
|
+
Convert Homematic values to Python types.
|
|
141
|
+
|
|
142
|
+
Uses singledispatch for type-based conversion. Optionally converts
|
|
143
|
+
to a specific target type when provided.
|
|
144
|
+
|
|
145
|
+
Default behavior (unregistered types):
|
|
146
|
+
Returns value unchanged.
|
|
147
|
+
|
|
148
|
+
Registered conversions:
|
|
149
|
+
- int with target_type=bool → bool
|
|
150
|
+
- str with target_type=datetime → datetime (ISO parse)
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
value: Homematic value to convert.
|
|
154
|
+
target_type: Optional target Python type for conversion hint.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Python value.
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
>>> from_homematic_value(1, target_type=bool)
|
|
161
|
+
True
|
|
162
|
+
>>> from_homematic_value("2025-01-15T10:30:00", target_type=datetime)
|
|
163
|
+
datetime(2025, 1, 15, 10, 30)
|
|
164
|
+
|
|
165
|
+
"""
|
|
166
|
+
return value
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@from_homematic_value.register(int)
|
|
170
|
+
def _from_hm_int(value: int, *, target_type: type | None = None) -> int | bool: # kwonly: disable
|
|
171
|
+
"""Convert Homematic integer, optionally to bool."""
|
|
172
|
+
if target_type is bool:
|
|
173
|
+
return bool(value)
|
|
174
|
+
return value
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@from_homematic_value.register(str)
|
|
178
|
+
def _from_hm_str(value: str, *, target_type: type | None = None) -> str | datetime: # kwonly: disable
|
|
179
|
+
"""Convert Homematic string, optionally to datetime."""
|
|
180
|
+
if target_type is datetime:
|
|
181
|
+
return datetime.fromisoformat(value)
|
|
182
|
+
return value
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@lru_cache(maxsize=1024)
|
|
186
|
+
def _convert_cpv_to_hm_level(*, value: Any) -> Any:
|
|
187
|
+
"""Convert combined parameter value for hm level."""
|
|
188
|
+
if isinstance(value, str) and value.startswith("0x"):
|
|
189
|
+
return ast.literal_eval(value) / 100 / 2
|
|
190
|
+
return value
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@lru_cache(maxsize=1024)
|
|
194
|
+
def _convert_cpv_to_hmip_level(*, value: Any) -> Any:
|
|
195
|
+
"""Convert combined parameter value for hmip level."""
|
|
196
|
+
return int(value) / 100
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@lru_cache(maxsize=1024)
|
|
200
|
+
def convert_hm_level_to_cpv(*, value: Any) -> Any:
|
|
201
|
+
"""Convert hm level to combined parameter value."""
|
|
202
|
+
return format(int(value * 100 * 2), "#04x")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
CONVERTABLE_PARAMETERS: Final = (Parameter.COMBINED_PARAMETER, Parameter.LEVEL_COMBINED)
|
|
206
|
+
|
|
207
|
+
_COMBINED_PARAMETER_TO_HM_CONVERTER: Final = {
|
|
208
|
+
Parameter.LEVEL_COMBINED: _convert_cpv_to_hm_level,
|
|
209
|
+
Parameter.LEVEL: _convert_cpv_to_hmip_level,
|
|
210
|
+
Parameter.LEVEL_2: _convert_cpv_to_hmip_level,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_COMBINED_PARAMETER_NAMES: Final = {"L": Parameter.LEVEL, "L2": Parameter.LEVEL_2}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@lru_cache(maxsize=1024)
|
|
217
|
+
def _convert_combined_parameter_to_paramset(*, value: str) -> dict[str, Any]:
|
|
218
|
+
"""Convert combined parameter to paramset."""
|
|
219
|
+
paramset: dict[str, Any] = {}
|
|
220
|
+
for cp_param_value in value.split(","):
|
|
221
|
+
cp_param, value = cp_param_value.split("=")
|
|
222
|
+
if parameter := _COMBINED_PARAMETER_NAMES.get(cp_param):
|
|
223
|
+
if converter := _COMBINED_PARAMETER_TO_HM_CONVERTER.get(parameter):
|
|
224
|
+
paramset[parameter] = converter(value=value)
|
|
225
|
+
else:
|
|
226
|
+
paramset[parameter] = value
|
|
227
|
+
return paramset
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@lru_cache(maxsize=1024)
|
|
231
|
+
def _convert_level_combined_to_paramset(*, value: str) -> dict[str, Any]:
|
|
232
|
+
"""Convert combined parameter to paramset."""
|
|
233
|
+
if "," in value:
|
|
234
|
+
l1_value, l2_value = value.split(",")
|
|
235
|
+
if converter := _COMBINED_PARAMETER_TO_HM_CONVERTER.get(Parameter.LEVEL_COMBINED):
|
|
236
|
+
return {
|
|
237
|
+
Parameter.LEVEL: converter(value=l1_value),
|
|
238
|
+
Parameter.LEVEL_SLATS: converter(value=l2_value),
|
|
239
|
+
}
|
|
240
|
+
return {}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
_COMBINED_PARAMETER_TO_PARAMSET_CONVERTER: Final = {
|
|
244
|
+
Parameter.COMBINED_PARAMETER: _convert_combined_parameter_to_paramset,
|
|
245
|
+
Parameter.LEVEL_COMBINED: _convert_level_combined_to_paramset,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@lru_cache(maxsize=1024)
|
|
250
|
+
def convert_combined_parameter_to_paramset(*, parameter: str, value: str) -> dict[str, Any]:
|
|
251
|
+
"""Convert combined parameter to paramset."""
|
|
252
|
+
try:
|
|
253
|
+
if converter := _COMBINED_PARAMETER_TO_PARAMSET_CONVERTER.get(parameter): # type: ignore[call-overload]
|
|
254
|
+
return cast(dict[str, Any], converter(value=value))
|
|
255
|
+
_LOGGER.debug("CONVERT_COMBINED_PARAMETER_TO_PARAMSET: No converter found for %s: %s", parameter, value)
|
|
256
|
+
except Exception as exc:
|
|
257
|
+
_LOGGER.debug("CONVERT_COMBINED_PARAMETER_TO_PARAMSET: Convert failed %s", extract_exc_args(exc=exc))
|
|
258
|
+
return {}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# Define public API for this module
|
|
262
|
+
__all__ = tuple(
|
|
263
|
+
sorted(
|
|
264
|
+
name
|
|
265
|
+
for name, obj in globals().items()
|
|
266
|
+
if not name.startswith("_")
|
|
267
|
+
and (name.isupper() or inspect.isfunction(obj) or inspect.isclass(obj))
|
|
268
|
+
and getattr(obj, "__module__", __name__) == __name__
|
|
269
|
+
)
|
|
270
|
+
)
|