aiohomematic 2026.1.29__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. aiohomematic/__init__.py +110 -0
  2. aiohomematic/_log_context_protocol.py +29 -0
  3. aiohomematic/api.py +410 -0
  4. aiohomematic/async_support.py +250 -0
  5. aiohomematic/backend_detection.py +462 -0
  6. aiohomematic/central/__init__.py +103 -0
  7. aiohomematic/central/async_rpc_server.py +760 -0
  8. aiohomematic/central/central_unit.py +1152 -0
  9. aiohomematic/central/config.py +463 -0
  10. aiohomematic/central/config_builder.py +772 -0
  11. aiohomematic/central/connection_state.py +160 -0
  12. aiohomematic/central/coordinators/__init__.py +38 -0
  13. aiohomematic/central/coordinators/cache.py +414 -0
  14. aiohomematic/central/coordinators/client.py +480 -0
  15. aiohomematic/central/coordinators/connection_recovery.py +1141 -0
  16. aiohomematic/central/coordinators/device.py +1166 -0
  17. aiohomematic/central/coordinators/event.py +514 -0
  18. aiohomematic/central/coordinators/hub.py +532 -0
  19. aiohomematic/central/decorators.py +184 -0
  20. aiohomematic/central/device_registry.py +229 -0
  21. aiohomematic/central/events/__init__.py +104 -0
  22. aiohomematic/central/events/bus.py +1392 -0
  23. aiohomematic/central/events/integration.py +424 -0
  24. aiohomematic/central/events/types.py +194 -0
  25. aiohomematic/central/health.py +762 -0
  26. aiohomematic/central/rpc_server.py +353 -0
  27. aiohomematic/central/scheduler.py +794 -0
  28. aiohomematic/central/state_machine.py +391 -0
  29. aiohomematic/client/__init__.py +203 -0
  30. aiohomematic/client/_rpc_errors.py +187 -0
  31. aiohomematic/client/backends/__init__.py +48 -0
  32. aiohomematic/client/backends/base.py +335 -0
  33. aiohomematic/client/backends/capabilities.py +138 -0
  34. aiohomematic/client/backends/ccu.py +487 -0
  35. aiohomematic/client/backends/factory.py +116 -0
  36. aiohomematic/client/backends/homegear.py +294 -0
  37. aiohomematic/client/backends/json_ccu.py +252 -0
  38. aiohomematic/client/backends/protocol.py +316 -0
  39. aiohomematic/client/ccu.py +1857 -0
  40. aiohomematic/client/circuit_breaker.py +459 -0
  41. aiohomematic/client/config.py +64 -0
  42. aiohomematic/client/handlers/__init__.py +40 -0
  43. aiohomematic/client/handlers/backup.py +157 -0
  44. aiohomematic/client/handlers/base.py +79 -0
  45. aiohomematic/client/handlers/device_ops.py +1085 -0
  46. aiohomematic/client/handlers/firmware.py +144 -0
  47. aiohomematic/client/handlers/link_mgmt.py +199 -0
  48. aiohomematic/client/handlers/metadata.py +436 -0
  49. aiohomematic/client/handlers/programs.py +144 -0
  50. aiohomematic/client/handlers/sysvars.py +100 -0
  51. aiohomematic/client/interface_client.py +1304 -0
  52. aiohomematic/client/json_rpc.py +2068 -0
  53. aiohomematic/client/request_coalescer.py +282 -0
  54. aiohomematic/client/rpc_proxy.py +629 -0
  55. aiohomematic/client/state_machine.py +324 -0
  56. aiohomematic/const.py +2207 -0
  57. aiohomematic/context.py +275 -0
  58. aiohomematic/converter.py +270 -0
  59. aiohomematic/decorators.py +390 -0
  60. aiohomematic/exceptions.py +185 -0
  61. aiohomematic/hmcli.py +997 -0
  62. aiohomematic/i18n.py +193 -0
  63. aiohomematic/interfaces/__init__.py +407 -0
  64. aiohomematic/interfaces/central.py +1067 -0
  65. aiohomematic/interfaces/client.py +1096 -0
  66. aiohomematic/interfaces/coordinators.py +63 -0
  67. aiohomematic/interfaces/model.py +1921 -0
  68. aiohomematic/interfaces/operations.py +217 -0
  69. aiohomematic/logging_context.py +134 -0
  70. aiohomematic/metrics/__init__.py +125 -0
  71. aiohomematic/metrics/_protocols.py +140 -0
  72. aiohomematic/metrics/aggregator.py +534 -0
  73. aiohomematic/metrics/dataclasses.py +489 -0
  74. aiohomematic/metrics/emitter.py +292 -0
  75. aiohomematic/metrics/events.py +183 -0
  76. aiohomematic/metrics/keys.py +300 -0
  77. aiohomematic/metrics/observer.py +563 -0
  78. aiohomematic/metrics/stats.py +172 -0
  79. aiohomematic/model/__init__.py +189 -0
  80. aiohomematic/model/availability.py +65 -0
  81. aiohomematic/model/calculated/__init__.py +89 -0
  82. aiohomematic/model/calculated/climate.py +276 -0
  83. aiohomematic/model/calculated/data_point.py +315 -0
  84. aiohomematic/model/calculated/field.py +147 -0
  85. aiohomematic/model/calculated/operating_voltage_level.py +286 -0
  86. aiohomematic/model/calculated/support.py +232 -0
  87. aiohomematic/model/custom/__init__.py +214 -0
  88. aiohomematic/model/custom/capabilities/__init__.py +67 -0
  89. aiohomematic/model/custom/capabilities/climate.py +41 -0
  90. aiohomematic/model/custom/capabilities/light.py +87 -0
  91. aiohomematic/model/custom/capabilities/lock.py +44 -0
  92. aiohomematic/model/custom/capabilities/siren.py +63 -0
  93. aiohomematic/model/custom/climate.py +1130 -0
  94. aiohomematic/model/custom/cover.py +722 -0
  95. aiohomematic/model/custom/data_point.py +360 -0
  96. aiohomematic/model/custom/definition.py +300 -0
  97. aiohomematic/model/custom/field.py +89 -0
  98. aiohomematic/model/custom/light.py +1174 -0
  99. aiohomematic/model/custom/lock.py +322 -0
  100. aiohomematic/model/custom/mixins.py +445 -0
  101. aiohomematic/model/custom/profile.py +945 -0
  102. aiohomematic/model/custom/registry.py +251 -0
  103. aiohomematic/model/custom/siren.py +462 -0
  104. aiohomematic/model/custom/switch.py +195 -0
  105. aiohomematic/model/custom/text_display.py +289 -0
  106. aiohomematic/model/custom/valve.py +78 -0
  107. aiohomematic/model/data_point.py +1416 -0
  108. aiohomematic/model/device.py +1840 -0
  109. aiohomematic/model/event.py +216 -0
  110. aiohomematic/model/generic/__init__.py +327 -0
  111. aiohomematic/model/generic/action.py +40 -0
  112. aiohomematic/model/generic/action_select.py +62 -0
  113. aiohomematic/model/generic/binary_sensor.py +30 -0
  114. aiohomematic/model/generic/button.py +31 -0
  115. aiohomematic/model/generic/data_point.py +177 -0
  116. aiohomematic/model/generic/dummy.py +150 -0
  117. aiohomematic/model/generic/number.py +76 -0
  118. aiohomematic/model/generic/select.py +56 -0
  119. aiohomematic/model/generic/sensor.py +76 -0
  120. aiohomematic/model/generic/switch.py +54 -0
  121. aiohomematic/model/generic/text.py +33 -0
  122. aiohomematic/model/hub/__init__.py +100 -0
  123. aiohomematic/model/hub/binary_sensor.py +24 -0
  124. aiohomematic/model/hub/button.py +28 -0
  125. aiohomematic/model/hub/connectivity.py +190 -0
  126. aiohomematic/model/hub/data_point.py +342 -0
  127. aiohomematic/model/hub/hub.py +864 -0
  128. aiohomematic/model/hub/inbox.py +135 -0
  129. aiohomematic/model/hub/install_mode.py +393 -0
  130. aiohomematic/model/hub/metrics.py +208 -0
  131. aiohomematic/model/hub/number.py +42 -0
  132. aiohomematic/model/hub/select.py +52 -0
  133. aiohomematic/model/hub/sensor.py +37 -0
  134. aiohomematic/model/hub/switch.py +43 -0
  135. aiohomematic/model/hub/text.py +30 -0
  136. aiohomematic/model/hub/update.py +221 -0
  137. aiohomematic/model/support.py +592 -0
  138. aiohomematic/model/update.py +140 -0
  139. aiohomematic/model/week_profile.py +1827 -0
  140. aiohomematic/property_decorators.py +719 -0
  141. aiohomematic/py.typed +0 -0
  142. aiohomematic/rega_scripts/accept_device_in_inbox.fn +51 -0
  143. aiohomematic/rega_scripts/create_backup_start.fn +28 -0
  144. aiohomematic/rega_scripts/create_backup_status.fn +89 -0
  145. aiohomematic/rega_scripts/fetch_all_device_data.fn +97 -0
  146. aiohomematic/rega_scripts/get_backend_info.fn +25 -0
  147. aiohomematic/rega_scripts/get_inbox_devices.fn +61 -0
  148. aiohomematic/rega_scripts/get_program_descriptions.fn +31 -0
  149. aiohomematic/rega_scripts/get_serial.fn +44 -0
  150. aiohomematic/rega_scripts/get_service_messages.fn +83 -0
  151. aiohomematic/rega_scripts/get_system_update_info.fn +39 -0
  152. aiohomematic/rega_scripts/get_system_variable_descriptions.fn +31 -0
  153. aiohomematic/rega_scripts/set_program_state.fn +17 -0
  154. aiohomematic/rega_scripts/set_system_variable.fn +19 -0
  155. aiohomematic/rega_scripts/trigger_firmware_update.fn +67 -0
  156. aiohomematic/schemas.py +256 -0
  157. aiohomematic/store/__init__.py +55 -0
  158. aiohomematic/store/dynamic/__init__.py +43 -0
  159. aiohomematic/store/dynamic/command.py +250 -0
  160. aiohomematic/store/dynamic/data.py +175 -0
  161. aiohomematic/store/dynamic/details.py +187 -0
  162. aiohomematic/store/dynamic/ping_pong.py +416 -0
  163. aiohomematic/store/persistent/__init__.py +71 -0
  164. aiohomematic/store/persistent/base.py +285 -0
  165. aiohomematic/store/persistent/device.py +233 -0
  166. aiohomematic/store/persistent/incident.py +380 -0
  167. aiohomematic/store/persistent/paramset.py +241 -0
  168. aiohomematic/store/persistent/session.py +556 -0
  169. aiohomematic/store/serialization.py +150 -0
  170. aiohomematic/store/storage.py +689 -0
  171. aiohomematic/store/types.py +526 -0
  172. aiohomematic/store/visibility/__init__.py +40 -0
  173. aiohomematic/store/visibility/parser.py +141 -0
  174. aiohomematic/store/visibility/registry.py +722 -0
  175. aiohomematic/store/visibility/rules.py +307 -0
  176. aiohomematic/strings.json +237 -0
  177. aiohomematic/support.py +706 -0
  178. aiohomematic/tracing.py +236 -0
  179. aiohomematic/translations/de.json +237 -0
  180. aiohomematic/translations/en.json +237 -0
  181. aiohomematic/type_aliases.py +51 -0
  182. aiohomematic/validator.py +128 -0
  183. aiohomematic-2026.1.29.dist-info/METADATA +296 -0
  184. aiohomematic-2026.1.29.dist-info/RECORD +188 -0
  185. aiohomematic-2026.1.29.dist-info/WHEEL +5 -0
  186. aiohomematic-2026.1.29.dist-info/entry_points.txt +2 -0
  187. aiohomematic-2026.1.29.dist-info/licenses/LICENSE +21 -0
  188. aiohomematic-2026.1.29.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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