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,487 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
CCU backend implementation.
|
|
5
|
+
|
|
6
|
+
Uses XML-RPC for device operations and JSON-RPC for metadata/programs/sysvars.
|
|
7
|
+
|
|
8
|
+
Public API
|
|
9
|
+
----------
|
|
10
|
+
- CcuBackend: Backend for CCU3/CCU2 systems
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from dataclasses import replace
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
import logging
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Final, cast
|
|
21
|
+
|
|
22
|
+
from aiohomematic.client.backends.base import BaseBackend
|
|
23
|
+
from aiohomematic.client.backends.capabilities import CCU_CAPABILITIES
|
|
24
|
+
from aiohomematic.client.circuit_breaker import CircuitBreaker
|
|
25
|
+
from aiohomematic.const import (
|
|
26
|
+
INTERFACES_SUPPORTING_FIRMWARE_UPDATES,
|
|
27
|
+
INTERFACES_SUPPORTING_RPC_CALLBACK,
|
|
28
|
+
LINKABLE_INTERFACES,
|
|
29
|
+
Backend,
|
|
30
|
+
BackupData,
|
|
31
|
+
BackupStatus,
|
|
32
|
+
CircuitState,
|
|
33
|
+
CommandRxMode,
|
|
34
|
+
DescriptionMarker,
|
|
35
|
+
DeviceDescription,
|
|
36
|
+
DeviceDetail,
|
|
37
|
+
InboxDeviceData,
|
|
38
|
+
Interface,
|
|
39
|
+
ParameterData,
|
|
40
|
+
ParamsetKey,
|
|
41
|
+
ProgramData,
|
|
42
|
+
ServiceMessageData,
|
|
43
|
+
ServiceMessageType,
|
|
44
|
+
SystemUpdateData,
|
|
45
|
+
SystemVariableData,
|
|
46
|
+
)
|
|
47
|
+
from aiohomematic.exceptions import BaseHomematicException
|
|
48
|
+
from aiohomematic.schemas import normalize_device_description
|
|
49
|
+
from aiohomematic.support import extract_exc_args
|
|
50
|
+
|
|
51
|
+
if TYPE_CHECKING:
|
|
52
|
+
from aiohomematic.client.json_rpc import AioJsonRpcAioHttpClient
|
|
53
|
+
from aiohomematic.client.rpc_proxy import BaseRpcProxy
|
|
54
|
+
|
|
55
|
+
__all__ = ["CcuBackend"]
|
|
56
|
+
|
|
57
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class CcuBackend(BaseBackend):
|
|
61
|
+
"""
|
|
62
|
+
Backend for CCU3/CCU2 systems.
|
|
63
|
+
|
|
64
|
+
Communication:
|
|
65
|
+
- XML-RPC: Device operations (setValue, getValue, putParamset, listDevices, etc.)
|
|
66
|
+
- JSON-RPC: Metadata, programs, system variables, rooms, functions
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
__slots__ = ("_device_details_provider", "_json_rpc", "_proxy", "_proxy_read")
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
*,
|
|
74
|
+
interface: Interface,
|
|
75
|
+
interface_id: str,
|
|
76
|
+
proxy: BaseRpcProxy,
|
|
77
|
+
proxy_read: BaseRpcProxy,
|
|
78
|
+
json_rpc: AioJsonRpcAioHttpClient,
|
|
79
|
+
device_details_provider: Mapping[str, int],
|
|
80
|
+
has_push_updates: bool,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Initialize the CCU backend."""
|
|
83
|
+
# Build capabilities based on interface and config
|
|
84
|
+
capabilities = replace(
|
|
85
|
+
CCU_CAPABILITIES,
|
|
86
|
+
firmware_updates=interface in INTERFACES_SUPPORTING_FIRMWARE_UPDATES,
|
|
87
|
+
linking=interface in LINKABLE_INTERFACES,
|
|
88
|
+
ping_pong=interface in INTERFACES_SUPPORTING_RPC_CALLBACK,
|
|
89
|
+
push_updates=has_push_updates,
|
|
90
|
+
rpc_callback=interface in INTERFACES_SUPPORTING_RPC_CALLBACK,
|
|
91
|
+
)
|
|
92
|
+
super().__init__(
|
|
93
|
+
interface=interface,
|
|
94
|
+
interface_id=interface_id,
|
|
95
|
+
capabilities=capabilities,
|
|
96
|
+
)
|
|
97
|
+
self._proxy: Final = proxy
|
|
98
|
+
self._proxy_read: Final = proxy_read
|
|
99
|
+
self._json_rpc: Final = json_rpc
|
|
100
|
+
self._device_details_provider: Final = device_details_provider
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def all_circuit_breakers_closed(self) -> bool:
|
|
104
|
+
"""Return True if all circuit breakers are in closed state."""
|
|
105
|
+
if self._proxy.circuit_breaker.state != CircuitState.CLOSED:
|
|
106
|
+
return False
|
|
107
|
+
# Check proxy_read only if it's a different object
|
|
108
|
+
if self._proxy_read is not self._proxy and self._proxy_read.circuit_breaker.state != CircuitState.CLOSED:
|
|
109
|
+
return False
|
|
110
|
+
return self._json_rpc.circuit_breaker.state == CircuitState.CLOSED
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def circuit_breaker(self) -> CircuitBreaker:
|
|
114
|
+
"""Return the primary circuit breaker for metrics access."""
|
|
115
|
+
return self._proxy.circuit_breaker
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def model(self) -> str:
|
|
119
|
+
"""Return the backend model name."""
|
|
120
|
+
return Backend.CCU
|
|
121
|
+
|
|
122
|
+
async def accept_device_in_inbox(self, *, device_address: str) -> bool:
|
|
123
|
+
"""Accept device from inbox."""
|
|
124
|
+
return await self._json_rpc.accept_device_in_inbox(device_address=device_address)
|
|
125
|
+
|
|
126
|
+
async def add_link(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
sender_address: str,
|
|
130
|
+
receiver_address: str,
|
|
131
|
+
name: str,
|
|
132
|
+
description: str,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Add a link."""
|
|
135
|
+
await self._proxy.addLink(sender_address, receiver_address, name, description)
|
|
136
|
+
|
|
137
|
+
async def check_connection(self, *, handle_ping_pong: bool, caller_id: str | None = None) -> bool:
|
|
138
|
+
"""Check if connection is alive via ping."""
|
|
139
|
+
try:
|
|
140
|
+
# Use caller_id with token for ping-pong tracking, or interface_id for simple ping
|
|
141
|
+
await self._proxy.ping(caller_id or self._interface_id)
|
|
142
|
+
except BaseHomematicException:
|
|
143
|
+
return False
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
async def create_backup_and_download(
|
|
147
|
+
self,
|
|
148
|
+
*,
|
|
149
|
+
max_wait_time: float = 300.0,
|
|
150
|
+
poll_interval: float = 5.0,
|
|
151
|
+
) -> BackupData | None:
|
|
152
|
+
"""
|
|
153
|
+
Create and download backup with polling.
|
|
154
|
+
|
|
155
|
+
Start the backup process in the background and poll for completion.
|
|
156
|
+
This avoids blocking the ReGa scripting engine during backup creation.
|
|
157
|
+
"""
|
|
158
|
+
# Start backup in background
|
|
159
|
+
if not await self._json_rpc.create_backup_start():
|
|
160
|
+
_LOGGER.warning("CREATE_BACKUP: Failed to start backup") # i18n-log: ignore
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
# Poll for completion
|
|
164
|
+
elapsed = 0.0
|
|
165
|
+
while elapsed < max_wait_time:
|
|
166
|
+
await asyncio.sleep(poll_interval)
|
|
167
|
+
elapsed += poll_interval
|
|
168
|
+
status_data = await self._json_rpc.create_backup_status()
|
|
169
|
+
|
|
170
|
+
if status_data.status == BackupStatus.COMPLETED:
|
|
171
|
+
_LOGGER.info( # i18n-log: ignore
|
|
172
|
+
"CREATE_BACKUP: Completed - %s (%s bytes)",
|
|
173
|
+
status_data.filename,
|
|
174
|
+
status_data.size,
|
|
175
|
+
)
|
|
176
|
+
if (content := await self._json_rpc.download_backup()) is None:
|
|
177
|
+
return None
|
|
178
|
+
return BackupData(
|
|
179
|
+
filename=self._generate_backup_filename(),
|
|
180
|
+
content=content,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if status_data.status == BackupStatus.FAILED:
|
|
184
|
+
_LOGGER.warning("CREATE_BACKUP: Backup failed") # i18n-log: ignore
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
if status_data.status == BackupStatus.IDLE:
|
|
188
|
+
_LOGGER.warning("CREATE_BACKUP: Unexpected idle status") # i18n-log: ignore
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
_LOGGER.debug("CREATE_BACKUP: Running (elapsed: %.1fs)", elapsed)
|
|
192
|
+
|
|
193
|
+
_LOGGER.warning("CREATE_BACKUP: Timeout after %.1fs", max_wait_time) # i18n-log: ignore
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
async def deinit_proxy(self, *, init_url: str) -> None:
|
|
197
|
+
"""De-initialize the proxy."""
|
|
198
|
+
await self._proxy.init(init_url)
|
|
199
|
+
|
|
200
|
+
async def delete_system_variable(self, *, name: str) -> bool:
|
|
201
|
+
"""Delete a system variable."""
|
|
202
|
+
return await self._json_rpc.delete_system_variable(name=name)
|
|
203
|
+
|
|
204
|
+
async def execute_program(self, *, pid: str) -> bool:
|
|
205
|
+
"""Execute a program."""
|
|
206
|
+
return await self._json_rpc.execute_program(pid=pid)
|
|
207
|
+
|
|
208
|
+
async def get_all_device_data(self, *, interface: Interface) -> dict[str, Any] | None:
|
|
209
|
+
"""Return all device data via JSON-RPC."""
|
|
210
|
+
return dict(await self._json_rpc.get_all_device_data(interface=interface))
|
|
211
|
+
|
|
212
|
+
async def get_all_functions(self) -> dict[str, set[str]]:
|
|
213
|
+
"""Return all functions with their assigned channel addresses."""
|
|
214
|
+
functions: dict[str, set[str]] = {}
|
|
215
|
+
rega_ids_function = await self._json_rpc.get_all_channel_rega_ids_function()
|
|
216
|
+
for address, rega_id in self._device_details_provider.items():
|
|
217
|
+
if (sections := rega_ids_function.get(rega_id)) is not None:
|
|
218
|
+
if address not in functions:
|
|
219
|
+
functions[address] = set()
|
|
220
|
+
functions[address].update(sections)
|
|
221
|
+
return functions
|
|
222
|
+
|
|
223
|
+
async def get_all_programs(self, *, markers: tuple[DescriptionMarker | str, ...]) -> tuple[ProgramData, ...]:
|
|
224
|
+
"""Return all programs."""
|
|
225
|
+
return await self._json_rpc.get_all_programs(markers=markers)
|
|
226
|
+
|
|
227
|
+
async def get_all_rooms(self) -> dict[str, set[str]]:
|
|
228
|
+
"""Return all rooms with their assigned channel addresses."""
|
|
229
|
+
rooms: dict[str, set[str]] = {}
|
|
230
|
+
rega_ids_room = await self._json_rpc.get_all_channel_rega_ids_room()
|
|
231
|
+
for address, rega_id in self._device_details_provider.items():
|
|
232
|
+
if (names := rega_ids_room.get(rega_id)) is not None:
|
|
233
|
+
if address not in rooms:
|
|
234
|
+
rooms[address] = set()
|
|
235
|
+
rooms[address].update(names)
|
|
236
|
+
return rooms
|
|
237
|
+
|
|
238
|
+
async def get_all_system_variables(
|
|
239
|
+
self, *, markers: tuple[DescriptionMarker | str, ...]
|
|
240
|
+
) -> tuple[SystemVariableData, ...] | None:
|
|
241
|
+
"""Return all system variables."""
|
|
242
|
+
return await self._json_rpc.get_all_system_variables(markers=markers)
|
|
243
|
+
|
|
244
|
+
async def get_device_description(self, *, address: str) -> DeviceDescription | None:
|
|
245
|
+
"""Return device description for an address."""
|
|
246
|
+
try:
|
|
247
|
+
return cast(
|
|
248
|
+
DeviceDescription | None,
|
|
249
|
+
await self._proxy_read.getDeviceDescription(address),
|
|
250
|
+
)
|
|
251
|
+
except BaseHomematicException as bhexc:
|
|
252
|
+
_LOGGER.warning( # i18n-log: ignore
|
|
253
|
+
"GET_DEVICE_DESCRIPTION failed: %s [%s]",
|
|
254
|
+
bhexc.name,
|
|
255
|
+
extract_exc_args(exc=bhexc),
|
|
256
|
+
)
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
async def get_device_details(self, *, addresses: tuple[str, ...] | None = None) -> list[DeviceDetail] | None:
|
|
260
|
+
"""
|
|
261
|
+
Return device names, interfaces, and rega IDs via JSON-RPC.
|
|
262
|
+
|
|
263
|
+
Note: The addresses parameter is ignored for CCU backend as JSON-RPC
|
|
264
|
+
returns all device details in a single call.
|
|
265
|
+
"""
|
|
266
|
+
return list(await self._json_rpc.get_device_details())
|
|
267
|
+
|
|
268
|
+
async def get_inbox_devices(self) -> tuple[InboxDeviceData, ...]:
|
|
269
|
+
"""Return inbox devices."""
|
|
270
|
+
return await self._json_rpc.get_inbox_devices()
|
|
271
|
+
|
|
272
|
+
async def get_install_mode(self) -> int:
|
|
273
|
+
"""Return remaining install mode time."""
|
|
274
|
+
return cast(int, await self._proxy.getInstallMode())
|
|
275
|
+
|
|
276
|
+
async def get_link_peers(self, *, address: str) -> tuple[str, ...]:
|
|
277
|
+
"""Return link peers."""
|
|
278
|
+
return tuple(await self._proxy_read.getLinkPeers(address))
|
|
279
|
+
|
|
280
|
+
async def get_links(self, *, address: str, flags: int) -> dict[str, Any]:
|
|
281
|
+
"""Return links."""
|
|
282
|
+
return cast(
|
|
283
|
+
dict[str, Any],
|
|
284
|
+
await self._proxy_read.getLinks(address, flags),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
async def get_metadata(self, *, address: str, data_id: str) -> dict[str, Any]:
|
|
288
|
+
"""Return metadata for an address."""
|
|
289
|
+
return cast(
|
|
290
|
+
dict[str, Any],
|
|
291
|
+
await self._proxy_read.getMetadata(address, data_id),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
async def get_paramset(self, *, address: str, paramset_key: ParamsetKey | str) -> dict[str, Any]:
|
|
295
|
+
"""Return a paramset."""
|
|
296
|
+
return cast(
|
|
297
|
+
dict[str, Any],
|
|
298
|
+
await self._proxy_read.getParamset(address, paramset_key),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
async def get_paramset_description(
|
|
302
|
+
self, *, address: str, paramset_key: ParamsetKey
|
|
303
|
+
) -> dict[str, ParameterData] | None:
|
|
304
|
+
"""Return paramset description."""
|
|
305
|
+
try:
|
|
306
|
+
return cast(
|
|
307
|
+
dict[str, ParameterData],
|
|
308
|
+
await self._proxy_read.getParamsetDescription(address, paramset_key),
|
|
309
|
+
)
|
|
310
|
+
except BaseHomematicException as bhexc:
|
|
311
|
+
_LOGGER.debug(
|
|
312
|
+
"GET_PARAMSET_DESCRIPTION failed: %s [%s] for %s/%s",
|
|
313
|
+
bhexc.name,
|
|
314
|
+
extract_exc_args(exc=bhexc),
|
|
315
|
+
address,
|
|
316
|
+
paramset_key,
|
|
317
|
+
)
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
async def get_rega_id_by_address(self, *, address: str) -> int | None:
|
|
321
|
+
"""Return ReGa ID for an address."""
|
|
322
|
+
return await self._json_rpc.get_rega_id_by_address(address=address)
|
|
323
|
+
|
|
324
|
+
async def get_service_messages(
|
|
325
|
+
self, *, message_type: ServiceMessageType | None = None
|
|
326
|
+
) -> tuple[ServiceMessageData, ...]:
|
|
327
|
+
"""Return service messages."""
|
|
328
|
+
return await self._json_rpc.get_service_messages(message_type=message_type)
|
|
329
|
+
|
|
330
|
+
async def get_system_update_info(self) -> SystemUpdateData | None:
|
|
331
|
+
"""Return system update info."""
|
|
332
|
+
return await self._json_rpc.get_system_update_info()
|
|
333
|
+
|
|
334
|
+
async def get_system_variable(self, *, name: str) -> Any:
|
|
335
|
+
"""Return system variable value."""
|
|
336
|
+
return await self._json_rpc.get_system_variable(name=name)
|
|
337
|
+
|
|
338
|
+
async def get_value(self, *, address: str, parameter: str) -> Any:
|
|
339
|
+
"""Return a parameter value."""
|
|
340
|
+
return await self._proxy_read.getValue(address, parameter)
|
|
341
|
+
|
|
342
|
+
async def has_program_ids(self, *, rega_id: int) -> bool:
|
|
343
|
+
"""Check if channel has program IDs."""
|
|
344
|
+
return await self._json_rpc.has_program_ids(rega_id=rega_id)
|
|
345
|
+
|
|
346
|
+
async def init_proxy(self, *, init_url: str, interface_id: str) -> None:
|
|
347
|
+
"""Initialize the proxy with callback URL."""
|
|
348
|
+
await self._proxy.init(init_url, interface_id)
|
|
349
|
+
|
|
350
|
+
async def initialize(self) -> None:
|
|
351
|
+
"""Initialize the backend by fetching system information."""
|
|
352
|
+
self._system_information = await self._json_rpc.get_system_information()
|
|
353
|
+
# Update backup capability based on system info
|
|
354
|
+
if not self._system_information.has_backup:
|
|
355
|
+
self._capabilities = replace(self._capabilities, backup=False)
|
|
356
|
+
|
|
357
|
+
async def list_devices(self) -> tuple[DeviceDescription, ...] | None:
|
|
358
|
+
"""Return all device descriptions (normalized)."""
|
|
359
|
+
try:
|
|
360
|
+
raw_descriptions = await self._proxy_read.listDevices()
|
|
361
|
+
return tuple(normalize_device_description(device_description=desc) for desc in raw_descriptions)
|
|
362
|
+
except BaseHomematicException as bhexc:
|
|
363
|
+
_LOGGER.debug(
|
|
364
|
+
"LIST_DEVICES failed: %s [%s]",
|
|
365
|
+
bhexc.name,
|
|
366
|
+
extract_exc_args(exc=bhexc),
|
|
367
|
+
)
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
async def put_paramset(
|
|
371
|
+
self,
|
|
372
|
+
*,
|
|
373
|
+
address: str,
|
|
374
|
+
paramset_key: ParamsetKey | str,
|
|
375
|
+
values: dict[str, Any],
|
|
376
|
+
rx_mode: CommandRxMode | None = None,
|
|
377
|
+
) -> None:
|
|
378
|
+
"""Set paramset values."""
|
|
379
|
+
if rx_mode:
|
|
380
|
+
await self._proxy.putParamset(address, paramset_key, values, rx_mode)
|
|
381
|
+
else:
|
|
382
|
+
await self._proxy.putParamset(address, paramset_key, values)
|
|
383
|
+
|
|
384
|
+
async def remove_link(self, *, sender_address: str, receiver_address: str) -> None:
|
|
385
|
+
"""Remove a link."""
|
|
386
|
+
await self._proxy.removeLink(sender_address, receiver_address)
|
|
387
|
+
|
|
388
|
+
async def rename_channel(self, *, rega_id: int, new_name: str) -> bool:
|
|
389
|
+
"""Rename a channel."""
|
|
390
|
+
return await self._json_rpc.rename_channel(rega_id=rega_id, new_name=new_name)
|
|
391
|
+
|
|
392
|
+
async def rename_device(self, *, rega_id: int, new_name: str) -> bool:
|
|
393
|
+
"""Rename a device."""
|
|
394
|
+
return await self._json_rpc.rename_device(rega_id=rega_id, new_name=new_name)
|
|
395
|
+
|
|
396
|
+
async def report_value_usage(self, *, address: str, value_id: str, ref_counter: int) -> bool:
|
|
397
|
+
"""Report value usage to the backend."""
|
|
398
|
+
return bool(await self._proxy.reportValueUsage(address, value_id, ref_counter))
|
|
399
|
+
|
|
400
|
+
def reset_circuit_breakers(self) -> None:
|
|
401
|
+
"""Reset all circuit breakers to closed state."""
|
|
402
|
+
self._proxy.circuit_breaker.reset()
|
|
403
|
+
# Reset proxy_read only if it's a different object
|
|
404
|
+
if self._proxy_read is not self._proxy:
|
|
405
|
+
self._proxy_read.circuit_breaker.reset()
|
|
406
|
+
self._json_rpc.circuit_breaker.reset()
|
|
407
|
+
|
|
408
|
+
async def set_install_mode(
|
|
409
|
+
self,
|
|
410
|
+
*,
|
|
411
|
+
on: bool = True,
|
|
412
|
+
time: int = 60,
|
|
413
|
+
mode: int = 1,
|
|
414
|
+
device_address: str | None = None,
|
|
415
|
+
) -> bool:
|
|
416
|
+
"""Set install mode."""
|
|
417
|
+
if device_address:
|
|
418
|
+
await self._proxy.setInstallMode(on, time, mode, device_address)
|
|
419
|
+
else:
|
|
420
|
+
await self._proxy.setInstallMode(on, time, mode)
|
|
421
|
+
return True
|
|
422
|
+
|
|
423
|
+
async def set_metadata(self, *, address: str, data_id: str, value: dict[str, Any]) -> dict[str, Any]:
|
|
424
|
+
"""Set metadata for an address."""
|
|
425
|
+
await self._proxy.setMetadata(address, data_id, value)
|
|
426
|
+
return value
|
|
427
|
+
|
|
428
|
+
async def set_program_state(self, *, pid: str, state: bool) -> bool:
|
|
429
|
+
"""Set program state."""
|
|
430
|
+
return await self._json_rpc.set_program_state(pid=pid, state=state)
|
|
431
|
+
|
|
432
|
+
async def set_system_variable(self, *, name: str, value: Any) -> bool:
|
|
433
|
+
"""Set system variable value."""
|
|
434
|
+
return await self._json_rpc.set_system_variable(legacy_name=name, value=value)
|
|
435
|
+
|
|
436
|
+
async def set_value(
|
|
437
|
+
self,
|
|
438
|
+
*,
|
|
439
|
+
address: str,
|
|
440
|
+
parameter: str,
|
|
441
|
+
value: Any,
|
|
442
|
+
rx_mode: CommandRxMode | None = None,
|
|
443
|
+
) -> None:
|
|
444
|
+
"""Set a parameter value."""
|
|
445
|
+
if rx_mode:
|
|
446
|
+
await self._proxy.setValue(address, parameter, value, rx_mode)
|
|
447
|
+
else:
|
|
448
|
+
await self._proxy.setValue(address, parameter, value)
|
|
449
|
+
|
|
450
|
+
async def stop(self) -> None:
|
|
451
|
+
"""Stop the backend and release resources."""
|
|
452
|
+
await self._proxy.stop()
|
|
453
|
+
await self._proxy_read.stop()
|
|
454
|
+
|
|
455
|
+
async def trigger_firmware_update(self) -> bool:
|
|
456
|
+
"""Trigger system firmware update."""
|
|
457
|
+
return await self._json_rpc.trigger_firmware_update()
|
|
458
|
+
|
|
459
|
+
async def update_device_firmware(self, *, device_address: str) -> bool:
|
|
460
|
+
"""
|
|
461
|
+
Update device firmware via XML-RPC.
|
|
462
|
+
|
|
463
|
+
Tries installFirmware first (HmIP/HmIPW), falls back to updateFirmware (BidCos).
|
|
464
|
+
"""
|
|
465
|
+
try:
|
|
466
|
+
# Try installFirmware first (for HmIP/HmIPW devices)
|
|
467
|
+
result = await self._proxy.installFirmware(device_address)
|
|
468
|
+
return bool(result) if isinstance(result, bool) else bool(result[0])
|
|
469
|
+
except BaseHomematicException:
|
|
470
|
+
# Fall back to updateFirmware (for BidCos devices)
|
|
471
|
+
try:
|
|
472
|
+
result = await self._proxy.updateFirmware(device_address)
|
|
473
|
+
return bool(result) if isinstance(result, bool) else bool(result[0])
|
|
474
|
+
except BaseHomematicException as bhexc:
|
|
475
|
+
_LOGGER.debug(
|
|
476
|
+
"UPDATE_DEVICE_FIRMWARE failed: %s [%s]",
|
|
477
|
+
bhexc.name,
|
|
478
|
+
extract_exc_args(exc=bhexc),
|
|
479
|
+
)
|
|
480
|
+
return False
|
|
481
|
+
|
|
482
|
+
def _generate_backup_filename(self) -> str:
|
|
483
|
+
"""Generate backup filename with hostname, version, and timestamp."""
|
|
484
|
+
hostname = self._system_information.hostname or "CCU"
|
|
485
|
+
version = self._system_information.version or "unknown"
|
|
486
|
+
timestamp = datetime.now().strftime("%Y-%m-%d-%H%M")
|
|
487
|
+
return f"{hostname}-{version}-{timestamp}.sbk"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Backend factory for creating appropriate backend instances.
|
|
5
|
+
|
|
6
|
+
Public API
|
|
7
|
+
----------
|
|
8
|
+
- create_backend: Factory function to create backend based on interface/version
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
import logging
|
|
15
|
+
from typing import TYPE_CHECKING, Final
|
|
16
|
+
|
|
17
|
+
from aiohomematic.client.backends.ccu import CcuBackend
|
|
18
|
+
from aiohomematic.client.backends.homegear import HomegearBackend
|
|
19
|
+
from aiohomematic.client.backends.json_ccu import JsonCcuBackend
|
|
20
|
+
from aiohomematic.client.backends.protocol import BackendOperationsProtocol
|
|
21
|
+
from aiohomematic.const import INTERFACES_REQUIRING_JSON_RPC_CLIENT, Interface
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from aiohomematic.client.json_rpc import AioJsonRpcAioHttpClient
|
|
25
|
+
from aiohomematic.client.rpc_proxy import BaseRpcProxy
|
|
26
|
+
from aiohomematic.interfaces import ParamsetDescriptionProviderProtocol
|
|
27
|
+
|
|
28
|
+
__all__ = ["create_backend"]
|
|
29
|
+
|
|
30
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def create_backend(
|
|
34
|
+
*,
|
|
35
|
+
interface: Interface,
|
|
36
|
+
interface_id: str,
|
|
37
|
+
version: str,
|
|
38
|
+
proxy: BaseRpcProxy | None,
|
|
39
|
+
proxy_read: BaseRpcProxy | None,
|
|
40
|
+
json_rpc: AioJsonRpcAioHttpClient,
|
|
41
|
+
paramset_provider: ParamsetDescriptionProviderProtocol,
|
|
42
|
+
device_details_provider: Mapping[str, int],
|
|
43
|
+
has_push_updates: bool,
|
|
44
|
+
) -> BackendOperationsProtocol:
|
|
45
|
+
"""
|
|
46
|
+
Create the appropriate backend based on interface and version.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
interface: The interface type (HMIP_RF, BIDCOS_RF, etc.)
|
|
50
|
+
interface_id: Unique interface identifier
|
|
51
|
+
version: Backend version string (from getVersion)
|
|
52
|
+
proxy: XML-RPC proxy for write operations (None for JSON-only backends)
|
|
53
|
+
proxy_read: XML-RPC proxy for read operations (None for JSON-only backends)
|
|
54
|
+
json_rpc: JSON-RPC client
|
|
55
|
+
paramset_provider: Provider for paramset descriptions
|
|
56
|
+
device_details_provider: Mapping of address to rega_id for room/function lookup
|
|
57
|
+
has_push_updates: Whether interface supports push updates (from config)
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Appropriate backend implementation.
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
backend: BackendOperationsProtocol
|
|
64
|
+
|
|
65
|
+
# CCU-Jack: JSON-RPC only
|
|
66
|
+
if interface in INTERFACES_REQUIRING_JSON_RPC_CLIENT:
|
|
67
|
+
_LOGGER.debug(
|
|
68
|
+
"CREATE_BACKEND: Creating JsonCcuBackend for interface %s",
|
|
69
|
+
interface_id,
|
|
70
|
+
)
|
|
71
|
+
backend = JsonCcuBackend(
|
|
72
|
+
interface=interface,
|
|
73
|
+
interface_id=interface_id,
|
|
74
|
+
json_rpc=json_rpc,
|
|
75
|
+
paramset_provider=paramset_provider,
|
|
76
|
+
has_push_updates=has_push_updates,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Homegear/pydevccu: XML-RPC with Homegear extensions
|
|
80
|
+
elif interface == Interface.BIDCOS_RF and ("Homegear" in version or "pydevccu" in version):
|
|
81
|
+
if proxy is None or proxy_read is None:
|
|
82
|
+
raise ValueError("Homegear backend requires XML-RPC proxies") # i18n-exc: ignore
|
|
83
|
+
_LOGGER.debug(
|
|
84
|
+
"CREATE_BACKEND: Creating HomegearBackend for interface %s (version: %s)",
|
|
85
|
+
interface_id,
|
|
86
|
+
version,
|
|
87
|
+
)
|
|
88
|
+
backend = HomegearBackend(
|
|
89
|
+
interface=interface,
|
|
90
|
+
interface_id=interface_id,
|
|
91
|
+
proxy=proxy,
|
|
92
|
+
proxy_read=proxy_read,
|
|
93
|
+
version=version,
|
|
94
|
+
has_push_updates=has_push_updates,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# CCU: XML-RPC + JSON-RPC
|
|
98
|
+
else:
|
|
99
|
+
if proxy is None or proxy_read is None:
|
|
100
|
+
raise ValueError("CCU backend requires XML-RPC proxies") # i18n-exc: ignore
|
|
101
|
+
_LOGGER.debug(
|
|
102
|
+
"CREATE_BACKEND: Creating CcuBackend for interface %s",
|
|
103
|
+
interface_id,
|
|
104
|
+
)
|
|
105
|
+
backend = CcuBackend(
|
|
106
|
+
interface=interface,
|
|
107
|
+
interface_id=interface_id,
|
|
108
|
+
proxy=proxy,
|
|
109
|
+
proxy_read=proxy_read,
|
|
110
|
+
json_rpc=json_rpc,
|
|
111
|
+
device_details_provider=device_details_provider,
|
|
112
|
+
has_push_updates=has_push_updates,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
await backend.initialize()
|
|
116
|
+
return backend
|