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,294 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Homegear backend implementation.
|
|
5
|
+
|
|
6
|
+
Uses XML-RPC exclusively with Homegear-specific extensions.
|
|
7
|
+
|
|
8
|
+
Public API
|
|
9
|
+
----------
|
|
10
|
+
- HomegearBackend: Backend for Homegear and pydevccu systems
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import replace
|
|
16
|
+
import logging
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Final, cast
|
|
18
|
+
|
|
19
|
+
from aiohomematic.client.backends.base import BaseBackend
|
|
20
|
+
from aiohomematic.client.backends.capabilities import HOMEGEAR_CAPABILITIES
|
|
21
|
+
from aiohomematic.client.circuit_breaker import CircuitBreaker
|
|
22
|
+
from aiohomematic.const import (
|
|
23
|
+
DUMMY_SERIAL,
|
|
24
|
+
Backend,
|
|
25
|
+
CircuitState,
|
|
26
|
+
CommandRxMode,
|
|
27
|
+
DescriptionMarker,
|
|
28
|
+
DeviceDescription,
|
|
29
|
+
DeviceDetail,
|
|
30
|
+
Interface,
|
|
31
|
+
ParameterData,
|
|
32
|
+
ParamsetKey,
|
|
33
|
+
SystemInformation,
|
|
34
|
+
SystemVariableData,
|
|
35
|
+
)
|
|
36
|
+
from aiohomematic.exceptions import BaseHomematicException
|
|
37
|
+
from aiohomematic.schemas import normalize_device_description
|
|
38
|
+
from aiohomematic.support import extract_exc_args
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from aiohomematic.client.rpc_proxy import BaseRpcProxy
|
|
42
|
+
|
|
43
|
+
__all__ = ["HomegearBackend"]
|
|
44
|
+
|
|
45
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
46
|
+
_NAME: Final = "NAME"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class HomegearBackend(BaseBackend):
|
|
50
|
+
"""
|
|
51
|
+
Backend for Homegear and pydevccu systems.
|
|
52
|
+
|
|
53
|
+
Communication:
|
|
54
|
+
- XML-RPC exclusively with Homegear-specific methods
|
|
55
|
+
- System variables via getSystemVariable/setSystemVariable (not JSON-RPC)
|
|
56
|
+
- Device names via getMetadata (not JSON-RPC)
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
__slots__ = ("_proxy", "_proxy_read", "_version")
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
*,
|
|
64
|
+
interface: Interface,
|
|
65
|
+
interface_id: str,
|
|
66
|
+
proxy: BaseRpcProxy,
|
|
67
|
+
proxy_read: BaseRpcProxy,
|
|
68
|
+
version: str,
|
|
69
|
+
has_push_updates: bool,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Initialize the Homegear backend."""
|
|
72
|
+
# Build capabilities based on config
|
|
73
|
+
capabilities = replace(
|
|
74
|
+
HOMEGEAR_CAPABILITIES,
|
|
75
|
+
push_updates=has_push_updates,
|
|
76
|
+
)
|
|
77
|
+
super().__init__(
|
|
78
|
+
interface=interface,
|
|
79
|
+
interface_id=interface_id,
|
|
80
|
+
capabilities=capabilities,
|
|
81
|
+
)
|
|
82
|
+
self._proxy: Final = proxy
|
|
83
|
+
self._proxy_read: Final = proxy_read
|
|
84
|
+
self._version: Final = version
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def all_circuit_breakers_closed(self) -> bool:
|
|
88
|
+
"""Return True if all circuit breakers are in closed state."""
|
|
89
|
+
if self._proxy.circuit_breaker.state != CircuitState.CLOSED:
|
|
90
|
+
return False
|
|
91
|
+
# Check proxy_read only if it's a different object
|
|
92
|
+
if self._proxy_read is not self._proxy:
|
|
93
|
+
return self._proxy_read.circuit_breaker.state == CircuitState.CLOSED
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def circuit_breaker(self) -> CircuitBreaker:
|
|
98
|
+
"""Return the primary circuit breaker for metrics access."""
|
|
99
|
+
return self._proxy.circuit_breaker
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def model(self) -> str:
|
|
103
|
+
"""Return the backend model name."""
|
|
104
|
+
if Backend.PYDEVCCU.lower() in self._version.lower():
|
|
105
|
+
return Backend.PYDEVCCU
|
|
106
|
+
return Backend.HOMEGEAR
|
|
107
|
+
|
|
108
|
+
async def check_connection(self, *, handle_ping_pong: bool, caller_id: str | None = None) -> bool:
|
|
109
|
+
"""Check connection via clientServerInitialized."""
|
|
110
|
+
try:
|
|
111
|
+
# Homegear uses clientServerInitialized instead of ping
|
|
112
|
+
await self._proxy.clientServerInitialized(self._interface_id)
|
|
113
|
+
except BaseHomematicException:
|
|
114
|
+
return False
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
async def deinit_proxy(self, *, init_url: str) -> None:
|
|
118
|
+
"""De-initialize the proxy."""
|
|
119
|
+
await self._proxy.init(init_url)
|
|
120
|
+
|
|
121
|
+
async def delete_system_variable(self, *, name: str) -> bool:
|
|
122
|
+
"""Delete system variable via Homegear's deleteSystemVariable."""
|
|
123
|
+
await self._proxy.deleteSystemVariable(name)
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
async def get_all_system_variables(
|
|
127
|
+
self, *, markers: tuple[DescriptionMarker | str, ...]
|
|
128
|
+
) -> tuple[SystemVariableData, ...] | None:
|
|
129
|
+
"""Return all system variables via Homegear's getAllSystemVariables."""
|
|
130
|
+
variables: list[SystemVariableData] = []
|
|
131
|
+
if hg_variables := await self._proxy.getAllSystemVariables():
|
|
132
|
+
for name, value in hg_variables.items():
|
|
133
|
+
variables.append(SystemVariableData(vid=name, legacy_name=name, value=value))
|
|
134
|
+
return tuple(variables)
|
|
135
|
+
|
|
136
|
+
async def get_device_description(self, *, address: str) -> DeviceDescription | None:
|
|
137
|
+
"""Return device description."""
|
|
138
|
+
try:
|
|
139
|
+
return cast(
|
|
140
|
+
DeviceDescription | None,
|
|
141
|
+
await self._proxy_read.getDeviceDescription(address),
|
|
142
|
+
)
|
|
143
|
+
except BaseHomematicException as bhexc:
|
|
144
|
+
_LOGGER.warning( # i18n-log: ignore
|
|
145
|
+
"GET_DEVICE_DESCRIPTION failed: %s [%s]",
|
|
146
|
+
bhexc.name,
|
|
147
|
+
extract_exc_args(exc=bhexc),
|
|
148
|
+
)
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
async def get_device_details(self, *, addresses: tuple[str, ...] | None = None) -> list[DeviceDetail] | None:
|
|
152
|
+
"""
|
|
153
|
+
Return device names from metadata (Homegear-specific).
|
|
154
|
+
|
|
155
|
+
Homegear stores device names in metadata under the "NAME" key.
|
|
156
|
+
This fetches names for all provided addresses.
|
|
157
|
+
"""
|
|
158
|
+
if not addresses:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
_LOGGER.debug("GET_DEVICE_DETAILS: Fetching names via Metadata for %d addresses", len(addresses))
|
|
162
|
+
details: list[DeviceDetail] = []
|
|
163
|
+
for address in addresses:
|
|
164
|
+
try:
|
|
165
|
+
name = await self._proxy_read.getMetadata(address, _NAME)
|
|
166
|
+
# Homegear doesn't have rega IDs or channels in the same way as CCU
|
|
167
|
+
# Create a minimal DeviceDetail with just the name
|
|
168
|
+
details.append(
|
|
169
|
+
DeviceDetail(
|
|
170
|
+
address=address,
|
|
171
|
+
name=name if isinstance(name, str) else str(name) if name else "",
|
|
172
|
+
id=0, # Homegear doesn't use rega IDs
|
|
173
|
+
interface=self._interface_id,
|
|
174
|
+
channels=[], # Homegear doesn't provide channel details this way
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
except BaseHomematicException as bhexc:
|
|
178
|
+
_LOGGER.warning( # i18n-log: ignore
|
|
179
|
+
"GET_DEVICE_DETAILS: %s [%s] Failed to fetch name for %s",
|
|
180
|
+
bhexc.name,
|
|
181
|
+
extract_exc_args(exc=bhexc),
|
|
182
|
+
address,
|
|
183
|
+
)
|
|
184
|
+
return details if details else None
|
|
185
|
+
|
|
186
|
+
async def get_metadata(self, *, address: str, data_id: str) -> dict[str, Any]:
|
|
187
|
+
"""Return metadata (Homegear stores device names here)."""
|
|
188
|
+
return cast(
|
|
189
|
+
dict[str, Any],
|
|
190
|
+
await self._proxy_read.getMetadata(address, data_id),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
async def get_paramset(self, *, address: str, paramset_key: ParamsetKey | str) -> dict[str, Any]:
|
|
194
|
+
"""Return a paramset."""
|
|
195
|
+
return cast(
|
|
196
|
+
dict[str, Any],
|
|
197
|
+
await self._proxy_read.getParamset(address, paramset_key),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
async def get_paramset_description(
|
|
201
|
+
self, *, address: str, paramset_key: ParamsetKey
|
|
202
|
+
) -> dict[str, ParameterData] | None:
|
|
203
|
+
"""Return paramset description."""
|
|
204
|
+
try:
|
|
205
|
+
return cast(
|
|
206
|
+
dict[str, ParameterData],
|
|
207
|
+
await self._proxy_read.getParamsetDescription(address, paramset_key),
|
|
208
|
+
)
|
|
209
|
+
except BaseHomematicException as bhexc:
|
|
210
|
+
_LOGGER.debug(
|
|
211
|
+
"GET_PARAMSET_DESCRIPTION failed: %s [%s] for %s/%s",
|
|
212
|
+
bhexc.name,
|
|
213
|
+
extract_exc_args(exc=bhexc),
|
|
214
|
+
address,
|
|
215
|
+
paramset_key,
|
|
216
|
+
)
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
async def get_system_variable(self, *, name: str) -> Any:
|
|
220
|
+
"""Return system variable via Homegear's getSystemVariable."""
|
|
221
|
+
return await self._proxy.getSystemVariable(name)
|
|
222
|
+
|
|
223
|
+
async def get_value(self, *, address: str, parameter: str) -> Any:
|
|
224
|
+
"""Return a parameter value."""
|
|
225
|
+
return await self._proxy_read.getValue(address, parameter)
|
|
226
|
+
|
|
227
|
+
async def init_proxy(self, *, init_url: str, interface_id: str) -> None:
|
|
228
|
+
"""Initialize the proxy."""
|
|
229
|
+
await self._proxy.init(init_url, interface_id)
|
|
230
|
+
|
|
231
|
+
async def initialize(self) -> None:
|
|
232
|
+
"""Initialize the backend."""
|
|
233
|
+
self._system_information = SystemInformation(
|
|
234
|
+
available_interfaces=(Interface.BIDCOS_RF,),
|
|
235
|
+
serial=f"{self._interface}_{DUMMY_SERIAL}",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
async def list_devices(self) -> tuple[DeviceDescription, ...] | None:
|
|
239
|
+
"""Return all device descriptions (normalized)."""
|
|
240
|
+
try:
|
|
241
|
+
raw_descriptions = await self._proxy_read.listDevices()
|
|
242
|
+
return tuple(normalize_device_description(device_description=desc) for desc in raw_descriptions)
|
|
243
|
+
except BaseHomematicException as bhexc:
|
|
244
|
+
_LOGGER.debug(
|
|
245
|
+
"LIST_DEVICES failed: %s [%s]",
|
|
246
|
+
bhexc.name,
|
|
247
|
+
extract_exc_args(exc=bhexc),
|
|
248
|
+
)
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
async def put_paramset(
|
|
252
|
+
self,
|
|
253
|
+
*,
|
|
254
|
+
address: str,
|
|
255
|
+
paramset_key: ParamsetKey | str,
|
|
256
|
+
values: dict[str, Any],
|
|
257
|
+
rx_mode: CommandRxMode | None = None,
|
|
258
|
+
) -> None:
|
|
259
|
+
"""Set paramset values."""
|
|
260
|
+
if rx_mode:
|
|
261
|
+
await self._proxy.putParamset(address, paramset_key, values, rx_mode)
|
|
262
|
+
else:
|
|
263
|
+
await self._proxy.putParamset(address, paramset_key, values)
|
|
264
|
+
|
|
265
|
+
def reset_circuit_breakers(self) -> None:
|
|
266
|
+
"""Reset all circuit breakers to closed state."""
|
|
267
|
+
self._proxy.circuit_breaker.reset()
|
|
268
|
+
# Reset proxy_read only if it's a different object
|
|
269
|
+
if self._proxy_read is not self._proxy:
|
|
270
|
+
self._proxy_read.circuit_breaker.reset()
|
|
271
|
+
|
|
272
|
+
async def set_system_variable(self, *, name: str, value: Any) -> bool:
|
|
273
|
+
"""Set system variable via Homegear's setSystemVariable."""
|
|
274
|
+
await self._proxy.setSystemVariable(name, value)
|
|
275
|
+
return True
|
|
276
|
+
|
|
277
|
+
async def set_value(
|
|
278
|
+
self,
|
|
279
|
+
*,
|
|
280
|
+
address: str,
|
|
281
|
+
parameter: str,
|
|
282
|
+
value: Any,
|
|
283
|
+
rx_mode: CommandRxMode | None = None,
|
|
284
|
+
) -> None:
|
|
285
|
+
"""Set a parameter value."""
|
|
286
|
+
if rx_mode:
|
|
287
|
+
await self._proxy.setValue(address, parameter, value, rx_mode)
|
|
288
|
+
else:
|
|
289
|
+
await self._proxy.setValue(address, parameter, value)
|
|
290
|
+
|
|
291
|
+
async def stop(self) -> None:
|
|
292
|
+
"""Stop the backend."""
|
|
293
|
+
await self._proxy.stop()
|
|
294
|
+
await self._proxy_read.stop()
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
JSON-RPC CCU backend implementation (CCU-Jack).
|
|
5
|
+
|
|
6
|
+
Uses JSON-RPC exclusively for all operations.
|
|
7
|
+
|
|
8
|
+
Public API
|
|
9
|
+
----------
|
|
10
|
+
- JsonCcuBackend: Backend for CCU-Jack using JSON-RPC exclusively
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import replace
|
|
16
|
+
import logging
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Final, cast
|
|
18
|
+
|
|
19
|
+
from aiohomematic import i18n
|
|
20
|
+
from aiohomematic.client.backends.base import BaseBackend
|
|
21
|
+
from aiohomematic.client.backends.capabilities import JSON_CCU_CAPABILITIES
|
|
22
|
+
from aiohomematic.client.circuit_breaker import CircuitBreaker
|
|
23
|
+
from aiohomematic.const import (
|
|
24
|
+
DUMMY_SERIAL,
|
|
25
|
+
Backend,
|
|
26
|
+
CircuitState,
|
|
27
|
+
CommandRxMode,
|
|
28
|
+
DeviceDescription,
|
|
29
|
+
Interface,
|
|
30
|
+
ParameterData,
|
|
31
|
+
ParameterType,
|
|
32
|
+
ParamsetKey,
|
|
33
|
+
SystemInformation,
|
|
34
|
+
)
|
|
35
|
+
from aiohomematic.exceptions import BaseHomematicException, ClientException
|
|
36
|
+
from aiohomematic.schemas import normalize_device_description
|
|
37
|
+
from aiohomematic.support import extract_exc_args
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from aiohomematic.client.json_rpc import AioJsonRpcAioHttpClient
|
|
41
|
+
from aiohomematic.interfaces import ParamsetDescriptionProviderProtocol
|
|
42
|
+
|
|
43
|
+
__all__ = ["JsonCcuBackend"]
|
|
44
|
+
|
|
45
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
_CCU_JSON_VALUE_TYPE: Final = {
|
|
48
|
+
"ACTION": "bool",
|
|
49
|
+
"BOOL": "bool",
|
|
50
|
+
"ENUM": "list",
|
|
51
|
+
"FLOAT": "double",
|
|
52
|
+
"INTEGER": "int",
|
|
53
|
+
"STRING": "string",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class JsonCcuBackend(BaseBackend):
|
|
58
|
+
"""
|
|
59
|
+
Backend for CCU-Jack using JSON-RPC exclusively.
|
|
60
|
+
|
|
61
|
+
CCU-Jack provides a JSON-RPC interface that exposes Homematic device
|
|
62
|
+
operations without requiring the full CCU infrastructure.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
__slots__ = ("_json_rpc", "_paramset_provider")
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
*,
|
|
70
|
+
interface: Interface,
|
|
71
|
+
interface_id: str,
|
|
72
|
+
json_rpc: AioJsonRpcAioHttpClient,
|
|
73
|
+
paramset_provider: ParamsetDescriptionProviderProtocol,
|
|
74
|
+
has_push_updates: bool,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Initialize the JSON CCU backend."""
|
|
77
|
+
# Build capabilities based on config
|
|
78
|
+
capabilities = replace(
|
|
79
|
+
JSON_CCU_CAPABILITIES,
|
|
80
|
+
push_updates=has_push_updates,
|
|
81
|
+
)
|
|
82
|
+
super().__init__(
|
|
83
|
+
interface=interface,
|
|
84
|
+
interface_id=interface_id,
|
|
85
|
+
capabilities=capabilities,
|
|
86
|
+
)
|
|
87
|
+
self._json_rpc: Final = json_rpc
|
|
88
|
+
self._paramset_provider: Final = paramset_provider
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def all_circuit_breakers_closed(self) -> bool:
|
|
92
|
+
"""Return True if all circuit breakers are in closed state."""
|
|
93
|
+
return self._json_rpc.circuit_breaker.state == CircuitState.CLOSED
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def circuit_breaker(self) -> CircuitBreaker:
|
|
97
|
+
"""Return the primary circuit breaker for metrics access."""
|
|
98
|
+
return self._json_rpc.circuit_breaker
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def model(self) -> str:
|
|
102
|
+
"""Return the backend model name."""
|
|
103
|
+
return Backend.CCU
|
|
104
|
+
|
|
105
|
+
async def check_connection(self, *, handle_ping_pong: bool, caller_id: str | None = None) -> bool:
|
|
106
|
+
"""Check connection via JSON-RPC isPresent."""
|
|
107
|
+
# JSON-RPC backend doesn't support ping-pong, uses isPresent instead
|
|
108
|
+
return await self._json_rpc.is_present(interface=self._interface)
|
|
109
|
+
|
|
110
|
+
async def deinit_proxy(self, *, init_url: str) -> None:
|
|
111
|
+
"""No proxy de-initialization needed."""
|
|
112
|
+
|
|
113
|
+
async def get_device_description(self, *, address: str) -> DeviceDescription | None:
|
|
114
|
+
"""Return device description via JSON-RPC."""
|
|
115
|
+
try:
|
|
116
|
+
return await self._json_rpc.get_device_description(interface=self._interface, address=address)
|
|
117
|
+
except BaseHomematicException as bhexc:
|
|
118
|
+
_LOGGER.warning( # i18n-log: ignore
|
|
119
|
+
"GET_DEVICE_DESCRIPTION failed: %s [%s]",
|
|
120
|
+
bhexc.name,
|
|
121
|
+
extract_exc_args(exc=bhexc),
|
|
122
|
+
)
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
async def get_paramset(self, *, address: str, paramset_key: ParamsetKey | str) -> dict[str, Any]:
|
|
126
|
+
"""Return a paramset via JSON-RPC."""
|
|
127
|
+
return (
|
|
128
|
+
await self._json_rpc.get_paramset(
|
|
129
|
+
interface=self._interface,
|
|
130
|
+
address=address,
|
|
131
|
+
paramset_key=paramset_key,
|
|
132
|
+
)
|
|
133
|
+
or {}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
async def get_paramset_description(
|
|
137
|
+
self, *, address: str, paramset_key: ParamsetKey
|
|
138
|
+
) -> dict[str, ParameterData] | None:
|
|
139
|
+
"""Return paramset description via JSON-RPC."""
|
|
140
|
+
try:
|
|
141
|
+
return cast(
|
|
142
|
+
dict[str, ParameterData],
|
|
143
|
+
await self._json_rpc.get_paramset_description(
|
|
144
|
+
interface=self._interface,
|
|
145
|
+
address=address,
|
|
146
|
+
paramset_key=paramset_key,
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
except BaseHomematicException as bhexc:
|
|
150
|
+
_LOGGER.debug(
|
|
151
|
+
"GET_PARAMSET_DESCRIPTION failed: %s [%s] for %s/%s",
|
|
152
|
+
bhexc.name,
|
|
153
|
+
extract_exc_args(exc=bhexc),
|
|
154
|
+
address,
|
|
155
|
+
paramset_key,
|
|
156
|
+
)
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
async def get_value(self, *, address: str, parameter: str) -> Any:
|
|
160
|
+
"""Return a parameter value via JSON-RPC."""
|
|
161
|
+
return await self._json_rpc.get_value(
|
|
162
|
+
interface=self._interface,
|
|
163
|
+
address=address,
|
|
164
|
+
paramset_key=ParamsetKey.VALUES,
|
|
165
|
+
parameter=parameter,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
async def init_proxy(self, *, init_url: str, interface_id: str) -> None:
|
|
169
|
+
"""No proxy initialization needed for JSON-RPC only backend."""
|
|
170
|
+
|
|
171
|
+
async def initialize(self) -> None:
|
|
172
|
+
"""Initialize the backend."""
|
|
173
|
+
self._system_information = SystemInformation(
|
|
174
|
+
available_interfaces=(self._interface,),
|
|
175
|
+
serial=f"{self._interface}_{DUMMY_SERIAL}",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
async def list_devices(self) -> tuple[DeviceDescription, ...] | None:
|
|
179
|
+
"""Return all device descriptions via JSON-RPC (normalized)."""
|
|
180
|
+
try:
|
|
181
|
+
raw_descriptions = await self._json_rpc.list_devices(interface=self._interface)
|
|
182
|
+
return tuple(normalize_device_description(device_description=desc) for desc in raw_descriptions)
|
|
183
|
+
except BaseHomematicException as bhexc:
|
|
184
|
+
_LOGGER.debug(
|
|
185
|
+
"LIST_DEVICES failed: %s [%s]",
|
|
186
|
+
bhexc.name,
|
|
187
|
+
extract_exc_args(exc=bhexc),
|
|
188
|
+
)
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
async def put_paramset(
|
|
192
|
+
self,
|
|
193
|
+
*,
|
|
194
|
+
address: str,
|
|
195
|
+
paramset_key: ParamsetKey | str,
|
|
196
|
+
values: dict[str, Any],
|
|
197
|
+
rx_mode: CommandRxMode | None = None,
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Set paramset values via JSON-RPC (one value at a time)."""
|
|
200
|
+
for parameter, value in values.items():
|
|
201
|
+
await self.set_value(
|
|
202
|
+
address=address,
|
|
203
|
+
parameter=parameter,
|
|
204
|
+
value=value,
|
|
205
|
+
rx_mode=rx_mode,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def reset_circuit_breakers(self) -> None:
|
|
209
|
+
"""Reset all circuit breakers to closed state."""
|
|
210
|
+
self._json_rpc.circuit_breaker.reset()
|
|
211
|
+
|
|
212
|
+
async def set_value(
|
|
213
|
+
self,
|
|
214
|
+
*,
|
|
215
|
+
address: str,
|
|
216
|
+
parameter: str,
|
|
217
|
+
value: Any,
|
|
218
|
+
rx_mode: CommandRxMode | None = None,
|
|
219
|
+
) -> None:
|
|
220
|
+
"""Set a parameter value via JSON-RPC."""
|
|
221
|
+
if (value_type := self._get_parameter_type(address=address, parameter=parameter)) is None:
|
|
222
|
+
raise ClientException(
|
|
223
|
+
i18n.tr(
|
|
224
|
+
key="exception.client.json_ccu.set_value.unknown_type",
|
|
225
|
+
channel_address=address,
|
|
226
|
+
paramset_key=ParamsetKey.VALUES,
|
|
227
|
+
parameter=parameter,
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
json_type = _CCU_JSON_VALUE_TYPE.get(value_type, "string")
|
|
232
|
+
await self._json_rpc.set_value(
|
|
233
|
+
interface=self._interface,
|
|
234
|
+
address=address,
|
|
235
|
+
parameter=parameter,
|
|
236
|
+
value_type=json_type,
|
|
237
|
+
value=value,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
async def stop(self) -> None:
|
|
241
|
+
"""Stop the backend (no resources to release)."""
|
|
242
|
+
|
|
243
|
+
def _get_parameter_type(self, *, address: str, parameter: str) -> ParameterType | None:
|
|
244
|
+
"""Return the parameter's TYPE from its description."""
|
|
245
|
+
if parameter_data := self._paramset_provider.get_parameter_data(
|
|
246
|
+
interface_id=self._interface_id,
|
|
247
|
+
channel_address=address,
|
|
248
|
+
paramset_key=ParamsetKey.VALUES,
|
|
249
|
+
parameter=parameter,
|
|
250
|
+
):
|
|
251
|
+
return parameter_data["TYPE"]
|
|
252
|
+
return None
|