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