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,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