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,480 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2026
3
+ """
4
+ Client coordinator for managing client lifecycle and operations.
5
+
6
+ This module provides centralized client management including creation,
7
+ initialization, connection management, and lifecycle operations.
8
+
9
+ The ClientCoordinator provides:
10
+ - Client creation and registration
11
+ - Client initialization and deinitialization
12
+ - Primary client selection
13
+ - Client lifecycle management (start/stop)
14
+ - Client availability checking
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ from typing import Final
21
+
22
+ from aiohomematic import client as hmcl, i18n
23
+ from aiohomematic.central.events.types import HealthRecordedEvent
24
+ from aiohomematic.client._rpc_errors import exception_to_failure_reason
25
+ from aiohomematic.const import PRIMARY_CLIENT_CANDIDATE_INTERFACES, FailureReason, Interface, ProxyInitState
26
+ from aiohomematic.exceptions import AioHomematicException, BaseHomematicException
27
+ from aiohomematic.interfaces import (
28
+ CentralInfoProtocol,
29
+ ClientFactoryProtocol,
30
+ ClientProtocol,
31
+ ClientProviderProtocol,
32
+ ConfigProviderProtocol,
33
+ CoordinatorProviderProtocol,
34
+ EventBusProviderProtocol,
35
+ HealthTrackerProtocol,
36
+ SystemInfoProviderProtocol,
37
+ )
38
+ from aiohomematic.property_decorators import DelegatedProperty
39
+ from aiohomematic.support import extract_exc_args
40
+
41
+ _LOGGER: Final = logging.getLogger(__name__)
42
+
43
+
44
+ class ClientCoordinator(ClientProviderProtocol):
45
+ """Coordinator for client lifecycle and operations."""
46
+
47
+ __slots__ = (
48
+ "_central_info",
49
+ "_client_factory",
50
+ "_clients",
51
+ "_clients_started",
52
+ "_config_provider",
53
+ "_coordinator_provider",
54
+ "_event_bus_provider",
55
+ "_health_tracker",
56
+ "_last_failure_interface_id",
57
+ "_last_failure_reason",
58
+ "_primary_client",
59
+ "_system_info_provider",
60
+ "_unsubscribe_health_record",
61
+ )
62
+
63
+ def __init__(
64
+ self,
65
+ *,
66
+ client_factory: ClientFactoryProtocol,
67
+ central_info: CentralInfoProtocol,
68
+ config_provider: ConfigProviderProtocol,
69
+ coordinator_provider: CoordinatorProviderProtocol,
70
+ event_bus_provider: EventBusProviderProtocol,
71
+ health_tracker: HealthTrackerProtocol,
72
+ system_info_provider: SystemInfoProviderProtocol,
73
+ ) -> None:
74
+ """
75
+ Initialize the client coordinator.
76
+
77
+ Args:
78
+ ----
79
+ client_factory: Factory for creating client instances
80
+ central_info: Provider for central system information
81
+ config_provider: Provider for configuration access
82
+ coordinator_provider: Provider for accessing other coordinators
83
+ event_bus_provider: Provider for EventBus access
84
+ health_tracker: Health tracker for client health monitoring
85
+ system_info_provider: Provider for system information
86
+
87
+ """
88
+ self._client_factory: Final = client_factory
89
+ self._central_info: Final = central_info
90
+ self._config_provider: Final = config_provider
91
+ self._coordinator_provider: Final = coordinator_provider
92
+ self._event_bus_provider: Final = event_bus_provider
93
+ self._health_tracker: Final = health_tracker
94
+ self._system_info_provider: Final = system_info_provider
95
+
96
+ # {interface_id, client}
97
+ self._clients: Final[dict[str, ClientProtocol]] = {}
98
+ self._clients_started: bool = False
99
+ self._primary_client: ClientProtocol | None = None
100
+
101
+ # Track last failure for propagation to central state machine
102
+ self._last_failure_reason: FailureReason = FailureReason.NONE
103
+ self._last_failure_interface_id: str | None = None
104
+
105
+ # Subscribe to health record events from circuit breakers
106
+ self._unsubscribe_health_record = self._event_bus_provider.event_bus.subscribe(
107
+ event_type=HealthRecordedEvent,
108
+ event_key=None,
109
+ handler=self._on_health_record_event,
110
+ )
111
+
112
+ clients_started: Final = DelegatedProperty[bool](path="_clients_started")
113
+ last_failure_interface_id: Final = DelegatedProperty[str | None](path="_last_failure_interface_id")
114
+ last_failure_reason: Final = DelegatedProperty[FailureReason](path="_last_failure_reason")
115
+
116
+ @property
117
+ def all_clients_active(self) -> bool:
118
+ """Check if all configured clients exist and are active."""
119
+ count_client = len(self._clients)
120
+ return count_client > 0 and count_client == len(self._config_provider.config.enabled_interface_configs)
121
+
122
+ @property
123
+ def available(self) -> bool:
124
+ """Return if all clients are available."""
125
+ return all(client.available for client in self._clients.values())
126
+
127
+ @property
128
+ def clients(self) -> tuple[ClientProtocol, ...]:
129
+ """Return all clients."""
130
+ return tuple(self._clients.values())
131
+
132
+ @property
133
+ def has_clients(self) -> bool:
134
+ """Check if any clients exist."""
135
+ return len(self._clients) > 0
136
+
137
+ @property
138
+ def interface_ids(self) -> frozenset[str]:
139
+ """Return all associated interface IDs."""
140
+ return frozenset(self._clients)
141
+
142
+ @property
143
+ def interfaces(self) -> frozenset[Interface]:
144
+ """Return all associated interfaces."""
145
+ return frozenset(client.interface for client in self._clients.values())
146
+
147
+ @property
148
+ def is_alive(self) -> bool:
149
+ """Return if all clients have alive callbacks."""
150
+ return all(client.is_callback_alive() for client in self._clients.values())
151
+
152
+ @property
153
+ def poll_clients(self) -> tuple[ClientProtocol, ...]:
154
+ """Return clients that need to poll data."""
155
+ return tuple(client for client in self._clients.values() if not client.capabilities.push_updates)
156
+
157
+ @property
158
+ def primary_client(self) -> ClientProtocol | None:
159
+ """Return the primary client of the backend."""
160
+ if self._primary_client is not None:
161
+ return self._primary_client
162
+ if client := self._get_primary_client():
163
+ self._primary_client = client
164
+ return self._primary_client
165
+
166
+ def get_client(self, *, interface_id: str | None = None, interface: Interface | None = None) -> ClientProtocol:
167
+ """
168
+ Return a client by interface_id or interface.
169
+
170
+ Args:
171
+ ----
172
+ interface_id: Interface identifier (e.g., "ccu-main-BidCos-RF")
173
+ interface: Interface type (e.g., Interface.BIDCOS_RF)
174
+
175
+ Returns:
176
+ -------
177
+ Client instance
178
+
179
+ Raises:
180
+ ------
181
+ AioHomematicException: If neither parameter is provided or client not found
182
+
183
+ """
184
+ if interface_id is None and interface is None:
185
+ raise AioHomematicException(
186
+ i18n.tr(
187
+ key="exception.central.get_client.no_parameter",
188
+ name=self._central_info.name,
189
+ )
190
+ )
191
+
192
+ # If interface_id is provided, use it directly
193
+ if interface_id is not None:
194
+ if not self.has_client(interface_id=interface_id):
195
+ raise AioHomematicException(
196
+ i18n.tr(
197
+ key="exception.central.get_client.interface_missing",
198
+ interface_id=interface_id,
199
+ name=self._central_info.name,
200
+ )
201
+ )
202
+ return self._clients[interface_id]
203
+
204
+ # If interface is provided, find client by interface type
205
+ for client in self._clients.values():
206
+ if client.interface == interface:
207
+ return client
208
+
209
+ raise AioHomematicException(
210
+ i18n.tr(
211
+ key="exception.central.get_client.interface_type_missing",
212
+ interface=interface,
213
+ name=self._central_info.name,
214
+ )
215
+ )
216
+
217
+ def has_client(self, *, interface_id: str) -> bool:
218
+ """
219
+ Check if client exists.
220
+
221
+ Args:
222
+ ----
223
+ interface_id: Interface identifier
224
+
225
+ Returns:
226
+ -------
227
+ True if client exists, False otherwise
228
+
229
+ """
230
+ return interface_id in self._clients
231
+
232
+ async def restart_clients(self) -> None:
233
+ """Restart all clients."""
234
+ _LOGGER.debug("RESTART_CLIENTS: Restarting clients for %s", self._central_info.name)
235
+ await self.stop_clients()
236
+ if await self.start_clients():
237
+ _LOGGER.info(
238
+ i18n.tr(
239
+ key="log.central.restart_clients.restarted",
240
+ name=self._central_info.name,
241
+ )
242
+ )
243
+
244
+ async def start_clients(self) -> bool:
245
+ """
246
+ Start all clients.
247
+
248
+ Returns
249
+ -------
250
+ True if all clients started successfully, False otherwise
251
+
252
+ """
253
+ # Clear previous failure info before attempting to start
254
+ self._last_failure_reason = FailureReason.NONE
255
+ self._last_failure_interface_id = None
256
+
257
+ if not await self._create_clients():
258
+ return False
259
+
260
+ # Set primary interface on health tracker after all clients are created
261
+ if primary_client := self.primary_client:
262
+ self._health_tracker.set_primary_interface(interface=primary_client.interface)
263
+ _LOGGER.debug(
264
+ "START_CLIENTS: Set primary interface to %s on health tracker",
265
+ primary_client.interface,
266
+ )
267
+
268
+ # Load caches after clients are created
269
+ await self._coordinator_provider.cache_coordinator.load_all()
270
+
271
+ # Initialize clients (sets them to CONNECTED state)
272
+ await self._init_clients()
273
+
274
+ # Create devices from cache BEFORE hub init - required for sysvar-to-channel association
275
+ await self._coordinator_provider.device_coordinator.check_and_create_devices_from_cache()
276
+
277
+ # Enable cache expiration now that device creation is complete.
278
+ # During device creation, cache expiration was disabled to prevent getValue
279
+ # calls when device creation takes longer than MAX_CACHE_AGE (10 seconds).
280
+ self._coordinator_provider.cache_coordinator.set_data_cache_initialization_complete()
281
+
282
+ # Initialize hub (requires connected clients and devices to fetch programs/sysvars)
283
+ await self._coordinator_provider.hub_coordinator.init_hub()
284
+
285
+ self._clients_started = True
286
+ return True
287
+
288
+ async def stop_clients(self) -> None:
289
+ """Stop all clients."""
290
+ _LOGGER.debug("STOP_CLIENTS: Stopping clients for %s", self._central_info.name)
291
+
292
+ # Unsubscribe from health record events
293
+ self._unsubscribe_health_record()
294
+ _LOGGER.debug("STOP_CLIENTS: Unsubscribed from health record events")
295
+
296
+ await self._de_init_clients()
297
+
298
+ # Unregister clients from health tracker before stopping
299
+ for client in self._clients.values():
300
+ self._health_tracker.unregister_client(interface_id=client.interface_id)
301
+ _LOGGER.debug("STOP_CLIENTS: Unregistered client %s from health tracker", client.interface_id)
302
+
303
+ for client in self._clients.values():
304
+ _LOGGER.debug("STOP_CLIENTS: Stopping %s", client.interface_id)
305
+ await client.stop()
306
+
307
+ _LOGGER.debug("STOP_CLIENTS: Clearing existing clients.")
308
+ self._clients.clear()
309
+ self._clients_started = False
310
+
311
+ async def _create_client(self, *, interface_config: hmcl.InterfaceConfig) -> bool:
312
+ """
313
+ Create a single client.
314
+
315
+ Args:
316
+ ----
317
+ interface_config: Interface configuration
318
+
319
+ Returns:
320
+ -------
321
+ True if client was created successfully, False otherwise
322
+
323
+ """
324
+ try:
325
+ if client := await self._client_factory.create_client_instance(
326
+ interface_config=interface_config,
327
+ ):
328
+ _LOGGER.debug(
329
+ "CREATE_CLIENT: Adding client %s to %s",
330
+ client.interface_id,
331
+ self._central_info.name,
332
+ )
333
+ self._clients[client.interface_id] = client
334
+
335
+ # Register client with health tracker
336
+ self._health_tracker.register_client(
337
+ interface_id=client.interface_id,
338
+ interface=client.interface,
339
+ )
340
+ _LOGGER.debug(
341
+ "CREATE_CLIENT: Registered client %s with health tracker",
342
+ client.interface_id,
343
+ )
344
+ return True
345
+ except BaseHomematicException as bhexc: # pragma: no cover
346
+ # Track failure reason for propagation to central state machine
347
+ self._last_failure_reason = exception_to_failure_reason(exc=bhexc)
348
+ self._last_failure_interface_id = interface_config.interface_id
349
+ _LOGGER.error(
350
+ i18n.tr(
351
+ key="log.central.create_client.no_connection",
352
+ interface_id=interface_config.interface_id,
353
+ reason=extract_exc_args(exc=bhexc),
354
+ )
355
+ )
356
+ return False
357
+
358
+ async def _create_clients(self) -> bool:
359
+ """
360
+ Create all configured clients.
361
+
362
+ Returns
363
+ -------
364
+ True if all clients were created successfully, False otherwise
365
+
366
+ """
367
+ if len(self._clients) > 0:
368
+ _LOGGER.error(
369
+ i18n.tr(
370
+ key="log.central.create_clients.already_created",
371
+ name=self._central_info.name,
372
+ )
373
+ )
374
+ return False
375
+
376
+ if len(self._config_provider.config.enabled_interface_configs) == 0:
377
+ _LOGGER.error(
378
+ i18n.tr(
379
+ key="log.central.create_clients.no_interfaces",
380
+ name=self._central_info.name,
381
+ )
382
+ )
383
+ return False
384
+
385
+ # Create primary clients first
386
+ for interface_config in self._config_provider.config.enabled_interface_configs:
387
+ if interface_config.interface in PRIMARY_CLIENT_CANDIDATE_INTERFACES:
388
+ await self._create_client(interface_config=interface_config)
389
+
390
+ # Create secondary clients
391
+ for interface_config in self._config_provider.config.enabled_interface_configs:
392
+ if interface_config.interface not in PRIMARY_CLIENT_CANDIDATE_INTERFACES:
393
+ if (
394
+ self.primary_client is not None
395
+ and interface_config.interface not in self.primary_client.system_information.available_interfaces
396
+ ):
397
+ _LOGGER.error(
398
+ i18n.tr(
399
+ key="log.central.create_clients.interface_not_available",
400
+ interface=interface_config.interface,
401
+ name=self._central_info.name,
402
+ )
403
+ )
404
+ interface_config.disable()
405
+ continue
406
+ await self._create_client(interface_config=interface_config)
407
+
408
+ if not self.all_clients_active:
409
+ _LOGGER.warning(
410
+ i18n.tr(
411
+ key="log.central.create_clients.created_count_failed",
412
+ created=len(self._clients),
413
+ total=len(self._config_provider.config.enabled_interface_configs),
414
+ )
415
+ )
416
+ return False
417
+
418
+ if self.primary_client is None:
419
+ _LOGGER.warning(
420
+ i18n.tr(
421
+ key="log.central.create_clients.no_primary_identified",
422
+ name=self._central_info.name,
423
+ )
424
+ )
425
+ return True
426
+
427
+ _LOGGER.debug("CREATE_CLIENTS successful for %s", self._central_info.name)
428
+ return True
429
+
430
+ async def _de_init_clients(self) -> None:
431
+ """De-initialize all clients."""
432
+ for name, client in self._clients.items():
433
+ if await client.deinitialize_proxy():
434
+ _LOGGER.debug("DE_INIT_CLIENTS: Proxy de-initialized: %s", name)
435
+
436
+ def _get_primary_client(self) -> ClientProtocol | None:
437
+ """
438
+ Get the primary client.
439
+
440
+ Returns
441
+ -------
442
+ Primary client or None if not found
443
+
444
+ """
445
+ client: ClientProtocol | None = None
446
+ for client in self._clients.values():
447
+ if client.interface in PRIMARY_CLIENT_CANDIDATE_INTERFACES and client.available:
448
+ return client
449
+ return client
450
+
451
+ async def _init_clients(self) -> None:
452
+ """Initialize all clients."""
453
+ for client in self._clients.copy().values():
454
+ if client.interface not in self._system_info_provider.system_information.available_interfaces:
455
+ _LOGGER.debug(
456
+ "INIT_CLIENTS failed: Interface: %s is not available for the backend %s",
457
+ client.interface,
458
+ self._central_info.name,
459
+ )
460
+ del self._clients[client.interface_id]
461
+ continue
462
+
463
+ if await client.initialize_proxy() == ProxyInitState.INIT_SUCCESS:
464
+ _LOGGER.debug(
465
+ "INIT_CLIENTS: client %s initialized for %s", client.interface_id, self._central_info.name
466
+ )
467
+
468
+ def _on_health_record_event(self, *, event: HealthRecordedEvent) -> None:
469
+ """
470
+ Handle health record events from circuit breakers.
471
+
472
+ Args:
473
+ ----
474
+ event: Health record event with interface_id and success status
475
+
476
+ """
477
+ if event.success:
478
+ self._health_tracker.record_successful_request(interface_id=event.interface_id)
479
+ else:
480
+ self._health_tracker.record_failed_request(interface_id=event.interface_id)