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,1166 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Device coordinator for managing device lifecycle and operations.
|
|
5
|
+
|
|
6
|
+
This module provides centralized device management including creation,
|
|
7
|
+
registration, removal, and device-related operations.
|
|
8
|
+
|
|
9
|
+
The DeviceCoordinator provides:
|
|
10
|
+
- Device creation and initialization
|
|
11
|
+
- Device registration via DeviceRegistry
|
|
12
|
+
- Device removal and cleanup
|
|
13
|
+
- Device description management
|
|
14
|
+
- Data point and event creation for devices
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
from collections import defaultdict
|
|
21
|
+
from collections.abc import Mapping
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
import logging
|
|
24
|
+
from typing import TYPE_CHECKING, Any, Final
|
|
25
|
+
|
|
26
|
+
from aiohomematic import i18n
|
|
27
|
+
from aiohomematic.central.decorators import callback_backend_system
|
|
28
|
+
from aiohomematic.central.events import DeviceRemovedEvent
|
|
29
|
+
from aiohomematic.const import (
|
|
30
|
+
CATEGORIES,
|
|
31
|
+
DATA_POINT_EVENTS,
|
|
32
|
+
DataPointCategory,
|
|
33
|
+
DeviceDescription,
|
|
34
|
+
DeviceFirmwareState,
|
|
35
|
+
ParamsetKey,
|
|
36
|
+
SourceOfDeviceCreation,
|
|
37
|
+
SystemEventType,
|
|
38
|
+
)
|
|
39
|
+
from aiohomematic.decorators import inspector
|
|
40
|
+
from aiohomematic.exceptions import AioHomematicException
|
|
41
|
+
from aiohomematic.interfaces import (
|
|
42
|
+
CallbackDataPointProtocol,
|
|
43
|
+
CentralInfoProtocol,
|
|
44
|
+
ChannelProtocol,
|
|
45
|
+
ClientProviderProtocol,
|
|
46
|
+
ConfigProviderProtocol,
|
|
47
|
+
CoordinatorProviderProtocol,
|
|
48
|
+
DataCacheProviderProtocol,
|
|
49
|
+
DataPointProviderProtocol,
|
|
50
|
+
DeviceDescriptionProviderProtocol,
|
|
51
|
+
DeviceDetailsProviderProtocol,
|
|
52
|
+
DeviceProtocol,
|
|
53
|
+
EventBusProviderProtocol,
|
|
54
|
+
EventPublisherProtocol,
|
|
55
|
+
EventSubscriptionManagerProtocol,
|
|
56
|
+
FileOperationsProtocol,
|
|
57
|
+
GenericEventProtocolAny,
|
|
58
|
+
ParameterVisibilityProviderProtocol,
|
|
59
|
+
ParamsetDescriptionProviderProtocol,
|
|
60
|
+
TaskSchedulerProtocol,
|
|
61
|
+
)
|
|
62
|
+
from aiohomematic.interfaces.central import FirmwareDataRefresherProtocol
|
|
63
|
+
from aiohomematic.interfaces.client import DeviceDiscoveryAndMetadataProtocol, DeviceDiscoveryWithIdentityProtocol
|
|
64
|
+
from aiohomematic.model import create_data_points_and_events
|
|
65
|
+
from aiohomematic.model.custom import create_custom_data_points
|
|
66
|
+
from aiohomematic.model.device import Device
|
|
67
|
+
from aiohomematic.property_decorators import DelegatedProperty
|
|
68
|
+
from aiohomematic.support import extract_exc_args
|
|
69
|
+
|
|
70
|
+
if TYPE_CHECKING:
|
|
71
|
+
from aiohomematic.central import DeviceRegistry # noqa: F401
|
|
72
|
+
|
|
73
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class DeviceCoordinator(FirmwareDataRefresherProtocol):
|
|
77
|
+
"""Coordinator for device lifecycle and operations."""
|
|
78
|
+
|
|
79
|
+
__slots__ = (
|
|
80
|
+
"_central_info",
|
|
81
|
+
"_client_provider",
|
|
82
|
+
"_config_provider",
|
|
83
|
+
"_coordinator_provider",
|
|
84
|
+
"_data_cache_provider",
|
|
85
|
+
"_data_point_provider",
|
|
86
|
+
"_delayed_device_descriptions",
|
|
87
|
+
"_device_add_semaphore",
|
|
88
|
+
"_device_description_provider",
|
|
89
|
+
"_device_details_provider",
|
|
90
|
+
"_event_bus_provider",
|
|
91
|
+
"_event_publisher",
|
|
92
|
+
"_event_subscription_manager",
|
|
93
|
+
"_file_operations",
|
|
94
|
+
"_parameter_visibility_provider",
|
|
95
|
+
"_paramset_description_provider",
|
|
96
|
+
"_task_scheduler",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
*,
|
|
102
|
+
central_info: CentralInfoProtocol,
|
|
103
|
+
client_provider: ClientProviderProtocol,
|
|
104
|
+
config_provider: ConfigProviderProtocol,
|
|
105
|
+
coordinator_provider: CoordinatorProviderProtocol,
|
|
106
|
+
data_cache_provider: DataCacheProviderProtocol,
|
|
107
|
+
data_point_provider: DataPointProviderProtocol,
|
|
108
|
+
device_description_provider: DeviceDescriptionProviderProtocol,
|
|
109
|
+
device_details_provider: DeviceDetailsProviderProtocol,
|
|
110
|
+
event_bus_provider: EventBusProviderProtocol,
|
|
111
|
+
event_publisher: EventPublisherProtocol,
|
|
112
|
+
event_subscription_manager: EventSubscriptionManagerProtocol,
|
|
113
|
+
file_operations: FileOperationsProtocol,
|
|
114
|
+
parameter_visibility_provider: ParameterVisibilityProviderProtocol,
|
|
115
|
+
paramset_description_provider: ParamsetDescriptionProviderProtocol,
|
|
116
|
+
task_scheduler: TaskSchedulerProtocol,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Initialize the device coordinator.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
----
|
|
123
|
+
central_info: Provider for central system information
|
|
124
|
+
client_provider: Provider for client access
|
|
125
|
+
config_provider: Provider for configuration access
|
|
126
|
+
coordinator_provider: Provider for accessing other coordinators
|
|
127
|
+
data_cache_provider: Provider for data cache access
|
|
128
|
+
data_point_provider: Provider for data point access
|
|
129
|
+
device_description_provider: Provider for device descriptions
|
|
130
|
+
device_details_provider: Provider for device details
|
|
131
|
+
event_bus_provider: Provider for event bus access
|
|
132
|
+
event_publisher: Provider for event publisher access
|
|
133
|
+
event_subscription_manager: Manager for event subscriptions
|
|
134
|
+
file_operations: Provider for file operations
|
|
135
|
+
parameter_visibility_provider: Provider for parameter visibility rules
|
|
136
|
+
paramset_description_provider: Provider for paramset descriptions
|
|
137
|
+
task_scheduler: Scheduler for async tasks
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
self._central_info: Final = central_info
|
|
141
|
+
self._client_provider: Final = client_provider
|
|
142
|
+
self._config_provider: Final = config_provider
|
|
143
|
+
self._coordinator_provider: Final = coordinator_provider
|
|
144
|
+
self._data_cache_provider: Final = data_cache_provider
|
|
145
|
+
self._data_point_provider: Final = data_point_provider
|
|
146
|
+
self._device_description_provider: Final = device_description_provider
|
|
147
|
+
self._device_details_provider: Final = device_details_provider
|
|
148
|
+
self._event_bus_provider: Final = event_bus_provider
|
|
149
|
+
self._event_publisher: Final = event_publisher
|
|
150
|
+
self._event_subscription_manager: Final = event_subscription_manager
|
|
151
|
+
self._file_operations: Final = file_operations
|
|
152
|
+
self._parameter_visibility_provider: Final = parameter_visibility_provider
|
|
153
|
+
self._paramset_description_provider: Final = paramset_description_provider
|
|
154
|
+
self._task_scheduler: Final = task_scheduler
|
|
155
|
+
self._delayed_device_descriptions: Final[dict[str, list[DeviceDescription]]] = defaultdict(list)
|
|
156
|
+
self._device_add_semaphore: Final = asyncio.Semaphore()
|
|
157
|
+
|
|
158
|
+
device_registry: Final = DelegatedProperty["DeviceRegistry"](path="_coordinator_provider.device_registry")
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def devices(self) -> tuple[DeviceProtocol, ...]:
|
|
162
|
+
"""Return all devices."""
|
|
163
|
+
return self.device_registry.devices
|
|
164
|
+
|
|
165
|
+
@callback_backend_system(system_event=SystemEventType.NEW_DEVICES)
|
|
166
|
+
async def add_new_devices(self, *, interface_id: str, device_descriptions: tuple[DeviceDescription, ...]) -> None:
|
|
167
|
+
"""
|
|
168
|
+
Add new devices to central unit (callback from backend).
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
----
|
|
172
|
+
interface_id: Interface identifier
|
|
173
|
+
device_descriptions: Tuple of device descriptions
|
|
174
|
+
|
|
175
|
+
"""
|
|
176
|
+
source = (
|
|
177
|
+
SourceOfDeviceCreation.NEW
|
|
178
|
+
if self._coordinator_provider.cache_coordinator.device_descriptions.has_device_descriptions(
|
|
179
|
+
interface_id=interface_id
|
|
180
|
+
)
|
|
181
|
+
else SourceOfDeviceCreation.INIT
|
|
182
|
+
)
|
|
183
|
+
await self._add_new_devices(interface_id=interface_id, device_descriptions=device_descriptions, source=source)
|
|
184
|
+
|
|
185
|
+
async def add_new_devices_manually(self, *, interface_id: str, address_names: Mapping[str, str | None]) -> None:
|
|
186
|
+
"""
|
|
187
|
+
Add new devices manually triggered to central unit.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
interface_id: Interface identifier.
|
|
191
|
+
address_names: Device addresses and their names.
|
|
192
|
+
|
|
193
|
+
"""
|
|
194
|
+
if not self._coordinator_provider.client_coordinator.has_client(interface_id=interface_id):
|
|
195
|
+
_LOGGER.error( # i18n-log: ignore
|
|
196
|
+
"ADD_NEW_DEVICES_MANUALLY failed: Missing client for interface_id %s",
|
|
197
|
+
interface_id,
|
|
198
|
+
)
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
client = self._coordinator_provider.client_coordinator.get_client(interface_id=interface_id)
|
|
202
|
+
device_descriptions: list[DeviceDescription] = []
|
|
203
|
+
for address, device_name in address_names.items():
|
|
204
|
+
if not (dds := self._delayed_device_descriptions.pop(address, None)):
|
|
205
|
+
_LOGGER.error( # i18n-log: ignore
|
|
206
|
+
"ADD_NEW_DEVICES_MANUALLY failed: No device description found for address %s on interface_id %s",
|
|
207
|
+
address,
|
|
208
|
+
interface_id,
|
|
209
|
+
)
|
|
210
|
+
return
|
|
211
|
+
device_descriptions.extend(dds)
|
|
212
|
+
|
|
213
|
+
await client.accept_device_in_inbox(device_address=address)
|
|
214
|
+
|
|
215
|
+
if device_name:
|
|
216
|
+
await self._rename_new_device(
|
|
217
|
+
client=client,
|
|
218
|
+
device_descriptions=tuple(dds),
|
|
219
|
+
device_name=device_name,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
await self._add_new_devices(
|
|
223
|
+
interface_id=interface_id,
|
|
224
|
+
device_descriptions=tuple(device_descriptions),
|
|
225
|
+
source=SourceOfDeviceCreation.MANUAL,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
async def check_and_create_devices_from_cache(self) -> None:
|
|
229
|
+
"""
|
|
230
|
+
Check for new devices in cache and create them atomically.
|
|
231
|
+
|
|
232
|
+
Race condition prevention:
|
|
233
|
+
This method acquires the device_add_semaphore to ensure it doesn't
|
|
234
|
+
race with _add_new_devices() which is populating the cache from
|
|
235
|
+
newDevices callbacks. Without this synchronization, the startup
|
|
236
|
+
code could try to create devices while descriptions are still
|
|
237
|
+
being added, resulting in devices with missing channels.
|
|
238
|
+
|
|
239
|
+
"""
|
|
240
|
+
async with self._device_add_semaphore:
|
|
241
|
+
if new_device_addresses := self.check_for_new_device_addresses():
|
|
242
|
+
await self.create_devices(
|
|
243
|
+
new_device_addresses=new_device_addresses,
|
|
244
|
+
source=SourceOfDeviceCreation.CACHE,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def check_for_new_device_addresses(self, *, interface_id: str | None = None) -> Mapping[str, set[str]]:
|
|
248
|
+
"""
|
|
249
|
+
Check if there are new devices that need to be created.
|
|
250
|
+
|
|
251
|
+
Algorithm:
|
|
252
|
+
This method identifies device addresses that exist in the cache
|
|
253
|
+
(from device descriptions) but haven't been created as Device objects yet.
|
|
254
|
+
|
|
255
|
+
1. Get all existing device addresses from device registry (O(1) lookup set)
|
|
256
|
+
2. For each interface, get cached addresses from device descriptions
|
|
257
|
+
3. Compute set difference: cached_addresses - existing_addresses
|
|
258
|
+
4. Non-empty differences indicate devices that need creation
|
|
259
|
+
|
|
260
|
+
Why use a helper function?
|
|
261
|
+
The helper function allows the same logic to work for:
|
|
262
|
+
- Single interface check (when interface_id is provided)
|
|
263
|
+
- All interfaces check (when interface_id is None)
|
|
264
|
+
This avoids code duplication while keeping the interface flexible.
|
|
265
|
+
|
|
266
|
+
Performance note:
|
|
267
|
+
Set difference (addresses - existing_addresses) is O(n) where n is the
|
|
268
|
+
smaller set, making this efficient even for large device counts.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
----
|
|
272
|
+
interface_id: Optional interface identifier to check
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
-------
|
|
276
|
+
Mapping of interface IDs to sets of new device addresses
|
|
277
|
+
|
|
278
|
+
"""
|
|
279
|
+
new_device_addresses: dict[str, set[str]] = {}
|
|
280
|
+
|
|
281
|
+
# Cache existing device addresses once - this set is used for all difference operations
|
|
282
|
+
existing_addresses = self.device_registry.get_device_addresses()
|
|
283
|
+
|
|
284
|
+
def _check_for_new_device_addresses_helper(*, iid: str) -> None:
|
|
285
|
+
"""
|
|
286
|
+
Check a single interface for new devices.
|
|
287
|
+
|
|
288
|
+
Encapsulates the per-interface logic to avoid duplication between
|
|
289
|
+
single-interface and all-interfaces code paths.
|
|
290
|
+
"""
|
|
291
|
+
# Skip interfaces without paramset descriptions (not fully initialized)
|
|
292
|
+
if not self._coordinator_provider.cache_coordinator.paramset_descriptions.has_interface_id(
|
|
293
|
+
interface_id=iid
|
|
294
|
+
):
|
|
295
|
+
_LOGGER.debug(
|
|
296
|
+
"CHECK_FOR_NEW_DEVICE_ADDRESSES: Skipping interface %s, missing paramsets",
|
|
297
|
+
iid,
|
|
298
|
+
)
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
# Convert to set once for efficient set difference operation
|
|
302
|
+
addresses = set(
|
|
303
|
+
self._coordinator_provider.cache_coordinator.device_descriptions.get_addresses(interface_id=iid)
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Set difference: addresses in cache but not yet created as Device objects
|
|
307
|
+
if new_set := addresses - existing_addresses:
|
|
308
|
+
new_device_addresses[iid] = new_set
|
|
309
|
+
|
|
310
|
+
# Dispatch: single interface or all interfaces
|
|
311
|
+
if interface_id:
|
|
312
|
+
_check_for_new_device_addresses_helper(iid=interface_id)
|
|
313
|
+
else:
|
|
314
|
+
for iid in self._coordinator_provider.client_coordinator.interface_ids:
|
|
315
|
+
_check_for_new_device_addresses_helper(iid=iid)
|
|
316
|
+
|
|
317
|
+
if _LOGGER.isEnabledFor(level=logging.DEBUG):
|
|
318
|
+
count = sum(len(item) for item in new_device_addresses.values())
|
|
319
|
+
_LOGGER.debug(
|
|
320
|
+
"CHECK_FOR_NEW_DEVICE_ADDRESSES: %s: %i.",
|
|
321
|
+
"Found new device addresses" if new_device_addresses else "Did not find any new device addresses",
|
|
322
|
+
count,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return new_device_addresses
|
|
326
|
+
|
|
327
|
+
@inspector
|
|
328
|
+
async def create_central_links(self) -> None:
|
|
329
|
+
"""Create central links to support press events on all channels with click events."""
|
|
330
|
+
for device in self.devices:
|
|
331
|
+
await device.create_central_links()
|
|
332
|
+
|
|
333
|
+
async def create_devices(
|
|
334
|
+
self, *, new_device_addresses: Mapping[str, set[str]], source: SourceOfDeviceCreation
|
|
335
|
+
) -> None:
|
|
336
|
+
"""
|
|
337
|
+
Trigger creation of the objects that expose the functionality.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
----
|
|
341
|
+
new_device_addresses: Mapping of interface IDs to device addresses
|
|
342
|
+
source: Source of device creation
|
|
343
|
+
|
|
344
|
+
"""
|
|
345
|
+
if not self._coordinator_provider.client_coordinator.has_clients:
|
|
346
|
+
raise AioHomematicException(
|
|
347
|
+
i18n.tr(
|
|
348
|
+
key="exception.central.create_devices.no_clients",
|
|
349
|
+
name=self._central_info.name,
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
_LOGGER.debug("CREATE_DEVICES: Starting to create devices for %s", self._central_info.name)
|
|
353
|
+
|
|
354
|
+
new_devices = set[DeviceProtocol]()
|
|
355
|
+
|
|
356
|
+
for interface_id, device_addresses in new_device_addresses.items():
|
|
357
|
+
for device_address in device_addresses:
|
|
358
|
+
# Do we check for duplicates here? For now, we do.
|
|
359
|
+
if self.device_registry.has_device(address=device_address):
|
|
360
|
+
continue
|
|
361
|
+
device: DeviceProtocol | None = None
|
|
362
|
+
try:
|
|
363
|
+
device = Device(
|
|
364
|
+
interface_id=interface_id,
|
|
365
|
+
device_address=device_address,
|
|
366
|
+
central_info=self._central_info,
|
|
367
|
+
channel_lookup=self,
|
|
368
|
+
client_provider=self._client_provider,
|
|
369
|
+
config_provider=self._config_provider,
|
|
370
|
+
data_cache_provider=self._data_cache_provider,
|
|
371
|
+
data_point_provider=self._data_point_provider,
|
|
372
|
+
device_data_refresher=self,
|
|
373
|
+
device_description_provider=self._device_description_provider,
|
|
374
|
+
device_details_provider=self._device_details_provider,
|
|
375
|
+
event_bus_provider=self._event_bus_provider,
|
|
376
|
+
event_publisher=self._event_publisher,
|
|
377
|
+
event_subscription_manager=self._event_subscription_manager,
|
|
378
|
+
file_operations=self._file_operations,
|
|
379
|
+
parameter_visibility_provider=self._parameter_visibility_provider,
|
|
380
|
+
paramset_description_provider=self._paramset_description_provider,
|
|
381
|
+
task_scheduler=self._task_scheduler,
|
|
382
|
+
)
|
|
383
|
+
except Exception as exc:
|
|
384
|
+
_LOGGER.error( # i18n-log: ignore
|
|
385
|
+
"CREATE_DEVICES failed: %s [%s] Unable to create device: %s, %s",
|
|
386
|
+
type(exc).__name__,
|
|
387
|
+
extract_exc_args(exc=exc),
|
|
388
|
+
interface_id,
|
|
389
|
+
device_address,
|
|
390
|
+
)
|
|
391
|
+
try:
|
|
392
|
+
if device:
|
|
393
|
+
create_data_points_and_events(device=device)
|
|
394
|
+
create_custom_data_points(device=device)
|
|
395
|
+
new_devices.add(device)
|
|
396
|
+
await self.device_registry.add_device(device=device)
|
|
397
|
+
except Exception as exc:
|
|
398
|
+
_LOGGER.error( # i18n-log: ignore
|
|
399
|
+
"CREATE_DEVICES failed: %s [%s] Unable to create data points: %s, %s",
|
|
400
|
+
type(exc).__name__,
|
|
401
|
+
extract_exc_args(exc=exc),
|
|
402
|
+
interface_id,
|
|
403
|
+
device_address,
|
|
404
|
+
)
|
|
405
|
+
_LOGGER.debug("CREATE_DEVICES: Finished creating devices for %s", self._central_info.name)
|
|
406
|
+
|
|
407
|
+
if new_devices:
|
|
408
|
+
for device in new_devices:
|
|
409
|
+
await device.finalize_init()
|
|
410
|
+
new_dps: dict[DataPointCategory, Any] = _get_new_data_points(new_devices=new_devices)
|
|
411
|
+
new_dps[DataPointCategory.EVENT] = _get_new_channel_events(new_devices=new_devices)
|
|
412
|
+
self._coordinator_provider.event_coordinator.publish_system_event(
|
|
413
|
+
system_event=SystemEventType.DEVICES_CREATED,
|
|
414
|
+
new_data_points=new_dps,
|
|
415
|
+
source=source,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
async def delete_device(self, *, interface_id: str, device_address: str) -> None:
|
|
419
|
+
"""
|
|
420
|
+
Delete a device from central.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
----
|
|
424
|
+
interface_id: Interface identifier
|
|
425
|
+
device_address: Device address
|
|
426
|
+
|
|
427
|
+
"""
|
|
428
|
+
_LOGGER.debug(
|
|
429
|
+
"DELETE_DEVICE: interface_id = %s, device_address = %s",
|
|
430
|
+
interface_id,
|
|
431
|
+
device_address,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
if (device := self.device_registry.get_device(address=device_address)) is None:
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
await self.delete_devices(interface_id=interface_id, addresses=(device_address, *tuple(device.channels.keys())))
|
|
438
|
+
|
|
439
|
+
@callback_backend_system(system_event=SystemEventType.DELETE_DEVICES)
|
|
440
|
+
async def delete_devices(self, *, interface_id: str, addresses: tuple[str, ...]) -> None:
|
|
441
|
+
"""
|
|
442
|
+
Delete multiple devices from central.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
----
|
|
446
|
+
interface_id: Interface identifier
|
|
447
|
+
addresses: Tuple of addresses to delete
|
|
448
|
+
|
|
449
|
+
"""
|
|
450
|
+
_LOGGER.debug(
|
|
451
|
+
"DELETE_DEVICES: interface_id = %s, addresses = %s",
|
|
452
|
+
interface_id,
|
|
453
|
+
str(addresses),
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
for address in addresses:
|
|
457
|
+
if device := self.device_registry.get_device(address=address):
|
|
458
|
+
await self.remove_device(device=device)
|
|
459
|
+
|
|
460
|
+
await self._coordinator_provider.cache_coordinator.save_all(
|
|
461
|
+
save_device_descriptions=True,
|
|
462
|
+
save_paramset_descriptions=True,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
def get_channel(self, *, channel_address: str) -> ChannelProtocol | None:
|
|
466
|
+
"""
|
|
467
|
+
Return Homematic channel.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
----
|
|
471
|
+
channel_address: Channel address
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
-------
|
|
475
|
+
Channel instance or None if not found
|
|
476
|
+
|
|
477
|
+
"""
|
|
478
|
+
return self.device_registry.get_channel(channel_address=channel_address)
|
|
479
|
+
|
|
480
|
+
def get_device(self, *, address: str) -> DeviceProtocol | None:
|
|
481
|
+
"""
|
|
482
|
+
Return Homematic device.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
----
|
|
486
|
+
address: Device address
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
-------
|
|
490
|
+
Device instance or None if not found
|
|
491
|
+
|
|
492
|
+
"""
|
|
493
|
+
return self.device_registry.get_device(address=address)
|
|
494
|
+
|
|
495
|
+
def get_virtual_remotes(self) -> tuple[DeviceProtocol, ...]:
|
|
496
|
+
"""Get the virtual remotes for all clients."""
|
|
497
|
+
return self.device_registry.get_virtual_remotes()
|
|
498
|
+
|
|
499
|
+
def identify_channel(self, *, text: str) -> ChannelProtocol | None:
|
|
500
|
+
"""
|
|
501
|
+
Identify channel within a text.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
----
|
|
505
|
+
text: Text to search for channel identification
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
-------
|
|
509
|
+
Channel instance or None if not found
|
|
510
|
+
|
|
511
|
+
"""
|
|
512
|
+
return self.device_registry.identify_channel(text=text)
|
|
513
|
+
|
|
514
|
+
@callback_backend_system(system_event=SystemEventType.LIST_DEVICES)
|
|
515
|
+
def list_devices(self, *, interface_id: str) -> list[DeviceDescription]:
|
|
516
|
+
"""
|
|
517
|
+
Return already existing devices to the backend.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
----
|
|
521
|
+
interface_id: Interface identifier
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
-------
|
|
525
|
+
List of device descriptions
|
|
526
|
+
|
|
527
|
+
"""
|
|
528
|
+
result = self._coordinator_provider.cache_coordinator.device_descriptions.get_raw_device_descriptions(
|
|
529
|
+
interface_id=interface_id
|
|
530
|
+
)
|
|
531
|
+
_LOGGER.debug("LIST_DEVICES: interface_id = %s, channel_count = %i", interface_id, len(result))
|
|
532
|
+
return result
|
|
533
|
+
|
|
534
|
+
@callback_backend_system(system_event=SystemEventType.RE_ADDED_DEVICE)
|
|
535
|
+
async def readd_device(self, *, interface_id: str, device_addresses: tuple[str, ...]) -> None:
|
|
536
|
+
"""
|
|
537
|
+
Handle re-added device after re-pairing in learn mode.
|
|
538
|
+
|
|
539
|
+
This method is called when the CCU sends a readdedDevice callback, which
|
|
540
|
+
occurs when a known device is put into learn-mode while installation mode
|
|
541
|
+
is active (re-pairing). The device parameters may have changed, so we
|
|
542
|
+
refresh the device data.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
----
|
|
546
|
+
interface_id: Interface identifier
|
|
547
|
+
device_addresses: Addresses of the re-added devices
|
|
548
|
+
|
|
549
|
+
"""
|
|
550
|
+
_LOGGER.debug(
|
|
551
|
+
"READD_DEVICE: interface_id = %s, device_addresses = %s",
|
|
552
|
+
interface_id,
|
|
553
|
+
str(device_addresses),
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
if not self._coordinator_provider.client_coordinator.has_client(interface_id=interface_id):
|
|
557
|
+
_LOGGER.error( # i18n-log: ignore
|
|
558
|
+
"READD_DEVICE failed: Missing client for interface_id %s",
|
|
559
|
+
interface_id,
|
|
560
|
+
)
|
|
561
|
+
return
|
|
562
|
+
|
|
563
|
+
client = self._coordinator_provider.client_coordinator.get_client(interface_id=interface_id)
|
|
564
|
+
|
|
565
|
+
for device_address in device_addresses:
|
|
566
|
+
# Get existing device
|
|
567
|
+
if device := self.device_registry.get_device(address=device_address):
|
|
568
|
+
# Remove from caches to force refresh
|
|
569
|
+
self._coordinator_provider.cache_coordinator.device_descriptions.remove_device(device=device)
|
|
570
|
+
self._coordinator_provider.cache_coordinator.paramset_descriptions.remove_device(device=device)
|
|
571
|
+
await self.remove_device(device=device)
|
|
572
|
+
|
|
573
|
+
# Fetch fresh device descriptions and recreate
|
|
574
|
+
await self.refresh_device_descriptions_and_create_missing_devices(
|
|
575
|
+
client=client, refresh_only_existing=False, device_address=device_address
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
# Save updated caches
|
|
579
|
+
await self._coordinator_provider.cache_coordinator.save_all(
|
|
580
|
+
save_device_descriptions=True,
|
|
581
|
+
save_paramset_descriptions=True,
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
async def refresh_device_descriptions_and_create_missing_devices(
|
|
585
|
+
self,
|
|
586
|
+
*,
|
|
587
|
+
client: DeviceDiscoveryWithIdentityProtocol,
|
|
588
|
+
refresh_only_existing: bool,
|
|
589
|
+
device_address: str | None = None,
|
|
590
|
+
) -> None:
|
|
591
|
+
"""
|
|
592
|
+
Refresh device descriptions and create missing devices.
|
|
593
|
+
|
|
594
|
+
Args:
|
|
595
|
+
----
|
|
596
|
+
client: Client to use for refreshing
|
|
597
|
+
refresh_only_existing: Whether to only refresh existing devices
|
|
598
|
+
device_address: Optional device address to refresh
|
|
599
|
+
|
|
600
|
+
"""
|
|
601
|
+
device_descriptions: tuple[DeviceDescription, ...] | None = None
|
|
602
|
+
|
|
603
|
+
if (
|
|
604
|
+
device_address
|
|
605
|
+
and (device_description := await client.get_device_description(address=device_address)) is not None
|
|
606
|
+
):
|
|
607
|
+
device_descriptions = (device_description,)
|
|
608
|
+
else:
|
|
609
|
+
device_descriptions = await client.list_devices()
|
|
610
|
+
|
|
611
|
+
if (
|
|
612
|
+
device_descriptions
|
|
613
|
+
and refresh_only_existing
|
|
614
|
+
and (
|
|
615
|
+
existing_device_descriptions := tuple(
|
|
616
|
+
dev_desc
|
|
617
|
+
for dev_desc in list(device_descriptions)
|
|
618
|
+
if dev_desc["ADDRESS"]
|
|
619
|
+
in self._coordinator_provider.cache_coordinator.device_descriptions.get_device_descriptions(
|
|
620
|
+
interface_id=client.interface_id
|
|
621
|
+
)
|
|
622
|
+
)
|
|
623
|
+
)
|
|
624
|
+
):
|
|
625
|
+
device_descriptions = existing_device_descriptions
|
|
626
|
+
|
|
627
|
+
if device_descriptions:
|
|
628
|
+
await self._add_new_devices(
|
|
629
|
+
interface_id=client.interface_id,
|
|
630
|
+
device_descriptions=device_descriptions,
|
|
631
|
+
source=SourceOfDeviceCreation.REFRESH,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
async def refresh_device_link_peers(self, *, device_address: str) -> None:
|
|
635
|
+
"""
|
|
636
|
+
Refresh link peer information for a device after link partner change.
|
|
637
|
+
|
|
638
|
+
This method is called when the CCU sends an updateDevice callback with
|
|
639
|
+
hint=1 (link partner change). It refreshes the link peer addresses for
|
|
640
|
+
all channels of the device.
|
|
641
|
+
|
|
642
|
+
Args:
|
|
643
|
+
----
|
|
644
|
+
device_address: Device address to refresh link peers for
|
|
645
|
+
|
|
646
|
+
"""
|
|
647
|
+
_LOGGER.debug(
|
|
648
|
+
"REFRESH_DEVICE_LINK_PEERS: device_address = %s",
|
|
649
|
+
device_address,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
if (device := self.device_registry.get_device(address=device_address)) is None:
|
|
653
|
+
_LOGGER.debug(
|
|
654
|
+
"REFRESH_DEVICE_LINK_PEERS: Device %s not found in registry",
|
|
655
|
+
device_address,
|
|
656
|
+
)
|
|
657
|
+
return
|
|
658
|
+
|
|
659
|
+
# Refresh link peers for all channels
|
|
660
|
+
for channel in device.channels.values():
|
|
661
|
+
await channel.init_link_peer()
|
|
662
|
+
|
|
663
|
+
@inspector(re_raise=False)
|
|
664
|
+
async def refresh_firmware_data(self, *, device_address: str | None = None) -> None:
|
|
665
|
+
"""
|
|
666
|
+
Refresh device firmware data.
|
|
667
|
+
|
|
668
|
+
Args:
|
|
669
|
+
----
|
|
670
|
+
device_address: Optional device address to refresh, or None for all devices
|
|
671
|
+
|
|
672
|
+
"""
|
|
673
|
+
if device_address and (device := self.get_device(address=device_address)) is not None and device.is_updatable:
|
|
674
|
+
await self.refresh_device_descriptions_and_create_missing_devices(
|
|
675
|
+
client=device.client, refresh_only_existing=True, device_address=device_address
|
|
676
|
+
)
|
|
677
|
+
device.refresh_firmware_data()
|
|
678
|
+
else:
|
|
679
|
+
for client in self._coordinator_provider.client_coordinator.clients:
|
|
680
|
+
await self.refresh_device_descriptions_and_create_missing_devices(
|
|
681
|
+
client=client, refresh_only_existing=True
|
|
682
|
+
)
|
|
683
|
+
for device in self.devices:
|
|
684
|
+
if device.is_updatable:
|
|
685
|
+
device.refresh_firmware_data()
|
|
686
|
+
|
|
687
|
+
@inspector(re_raise=False)
|
|
688
|
+
async def refresh_firmware_data_by_state(self, *, device_firmware_states: tuple[DeviceFirmwareState, ...]) -> None:
|
|
689
|
+
"""Refresh firmware by state (internal use - use device_coordinator for external access)."""
|
|
690
|
+
for device in [
|
|
691
|
+
device_in_state
|
|
692
|
+
for device_in_state in self.devices
|
|
693
|
+
if device_in_state.firmware_update_state in device_firmware_states
|
|
694
|
+
]:
|
|
695
|
+
await self.refresh_firmware_data(device_address=device.address)
|
|
696
|
+
|
|
697
|
+
@inspector
|
|
698
|
+
async def remove_central_links(self) -> None:
|
|
699
|
+
"""Remove central links."""
|
|
700
|
+
for device in self.devices:
|
|
701
|
+
await device.remove_central_links()
|
|
702
|
+
|
|
703
|
+
async def remove_device(self, *, device: DeviceProtocol) -> None:
|
|
704
|
+
"""
|
|
705
|
+
Remove device from central collections.
|
|
706
|
+
|
|
707
|
+
Emits DeviceRemovedEvent to trigger decoupled cache invalidation.
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
----
|
|
711
|
+
device: Device to remove
|
|
712
|
+
|
|
713
|
+
"""
|
|
714
|
+
if not self.device_registry.has_device(address=device.address):
|
|
715
|
+
_LOGGER.debug(
|
|
716
|
+
"REMOVE_DEVICE: device %s not registered in central",
|
|
717
|
+
device.address,
|
|
718
|
+
)
|
|
719
|
+
return
|
|
720
|
+
|
|
721
|
+
# Capture data before removal for event emission
|
|
722
|
+
device_address = device.address
|
|
723
|
+
interface_id = device.interface_id
|
|
724
|
+
channel_addresses = tuple(device.channels.keys())
|
|
725
|
+
identifier = device.identifier
|
|
726
|
+
|
|
727
|
+
device.remove()
|
|
728
|
+
|
|
729
|
+
# Emit event for decoupled cache invalidation
|
|
730
|
+
await self._event_bus_provider.event_bus.publish(
|
|
731
|
+
event=DeviceRemovedEvent(
|
|
732
|
+
timestamp=datetime.now(),
|
|
733
|
+
unique_id=identifier,
|
|
734
|
+
device_address=device_address,
|
|
735
|
+
interface_id=interface_id,
|
|
736
|
+
channel_addresses=channel_addresses,
|
|
737
|
+
)
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
await self.device_registry.remove_device(device_address=device_address)
|
|
741
|
+
|
|
742
|
+
@callback_backend_system(system_event=SystemEventType.REPLACE_DEVICE)
|
|
743
|
+
async def replace_device(self, *, interface_id: str, old_device_address: str, new_device_address: str) -> None:
|
|
744
|
+
"""
|
|
745
|
+
Replace an old device with a new device after CCU device replacement.
|
|
746
|
+
|
|
747
|
+
This method is called when the CCU sends a replaceDevice callback, which
|
|
748
|
+
occurs when a user replaces a broken device with a new one using the CCU's
|
|
749
|
+
"Replace device" function. The CCU transfers configuration from the old
|
|
750
|
+
device to the new one.
|
|
751
|
+
|
|
752
|
+
Args:
|
|
753
|
+
----
|
|
754
|
+
interface_id: Interface identifier
|
|
755
|
+
old_device_address: Address of the device being replaced
|
|
756
|
+
new_device_address: Address of the replacement device
|
|
757
|
+
|
|
758
|
+
"""
|
|
759
|
+
_LOGGER.debug(
|
|
760
|
+
"REPLACE_DEVICE: interface_id = %s, old_device_address = %s, new_device_address = %s",
|
|
761
|
+
interface_id,
|
|
762
|
+
old_device_address,
|
|
763
|
+
new_device_address,
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
if not self._coordinator_provider.client_coordinator.has_client(interface_id=interface_id):
|
|
767
|
+
_LOGGER.error( # i18n-log: ignore
|
|
768
|
+
"REPLACE_DEVICE failed: Missing client for interface_id %s",
|
|
769
|
+
interface_id,
|
|
770
|
+
)
|
|
771
|
+
return
|
|
772
|
+
|
|
773
|
+
# Remove old device from registry and caches
|
|
774
|
+
if old_device := self.device_registry.get_device(address=old_device_address):
|
|
775
|
+
self._coordinator_provider.cache_coordinator.device_descriptions.remove_device(device=old_device)
|
|
776
|
+
self._coordinator_provider.cache_coordinator.paramset_descriptions.remove_device(device=old_device)
|
|
777
|
+
await self.remove_device(device=old_device)
|
|
778
|
+
|
|
779
|
+
# Fetch and create new device
|
|
780
|
+
client = self._coordinator_provider.client_coordinator.get_client(interface_id=interface_id)
|
|
781
|
+
await self.refresh_device_descriptions_and_create_missing_devices(
|
|
782
|
+
client=client, refresh_only_existing=False, device_address=new_device_address
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
# Save updated caches
|
|
786
|
+
await self._coordinator_provider.cache_coordinator.save_all(
|
|
787
|
+
save_device_descriptions=True,
|
|
788
|
+
save_paramset_descriptions=True,
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
@callback_backend_system(system_event=SystemEventType.UPDATE_DEVICE)
|
|
792
|
+
async def update_device(self, *, interface_id: str, device_address: str) -> None:
|
|
793
|
+
"""
|
|
794
|
+
Update device after firmware update by invalidating cache and reloading.
|
|
795
|
+
|
|
796
|
+
This method is called when the CCU sends an updateDevice callback with
|
|
797
|
+
hint=0 (firmware update). It invalidates the cached device and paramset
|
|
798
|
+
descriptions, fetches fresh data from the backend, and recreates the
|
|
799
|
+
Device object.
|
|
800
|
+
|
|
801
|
+
Args:
|
|
802
|
+
----
|
|
803
|
+
interface_id: Interface identifier
|
|
804
|
+
device_address: Device address to update
|
|
805
|
+
|
|
806
|
+
"""
|
|
807
|
+
_LOGGER.debug(
|
|
808
|
+
"UPDATE_DEVICE: interface_id = %s, device_address = %s",
|
|
809
|
+
interface_id,
|
|
810
|
+
device_address,
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
if not self._coordinator_provider.client_coordinator.has_client(interface_id=interface_id):
|
|
814
|
+
_LOGGER.error( # i18n-log: ignore
|
|
815
|
+
"UPDATE_DEVICE failed: Missing client for interface_id %s",
|
|
816
|
+
interface_id,
|
|
817
|
+
)
|
|
818
|
+
return
|
|
819
|
+
|
|
820
|
+
# Get existing device to collect all channel addresses for cache invalidation
|
|
821
|
+
if device := self.device_registry.get_device(address=device_address):
|
|
822
|
+
# Remove device from caches using the device's channel information
|
|
823
|
+
self._coordinator_provider.cache_coordinator.device_descriptions.remove_device(device=device)
|
|
824
|
+
self._coordinator_provider.cache_coordinator.paramset_descriptions.remove_device(device=device)
|
|
825
|
+
# Remove the Device object from registry
|
|
826
|
+
await self.remove_device(device=device)
|
|
827
|
+
|
|
828
|
+
# Fetch fresh device descriptions from backend
|
|
829
|
+
client = self._coordinator_provider.client_coordinator.get_client(interface_id=interface_id)
|
|
830
|
+
await self.refresh_device_descriptions_and_create_missing_devices(
|
|
831
|
+
client=client, refresh_only_existing=False, device_address=device_address
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
# Save updated caches
|
|
835
|
+
await self._coordinator_provider.cache_coordinator.save_all(
|
|
836
|
+
save_device_descriptions=True,
|
|
837
|
+
save_paramset_descriptions=True,
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
@inspector(measure_performance=True)
|
|
841
|
+
async def _add_new_devices(
|
|
842
|
+
self, *, interface_id: str, device_descriptions: tuple[DeviceDescription, ...], source: SourceOfDeviceCreation
|
|
843
|
+
) -> None:
|
|
844
|
+
"""
|
|
845
|
+
Add new devices to central unit.
|
|
846
|
+
|
|
847
|
+
Device creation pipeline:
|
|
848
|
+
This is a multi-step orchestration process:
|
|
849
|
+
|
|
850
|
+
1. Validation: Skip if no descriptions or client missing
|
|
851
|
+
2. Semaphore: Acquire lock to prevent concurrent device creation
|
|
852
|
+
3. Filtering: Identify truly new devices (not already known)
|
|
853
|
+
4. Delay check: Optionally defer creation for user confirmation
|
|
854
|
+
5. Cache population:
|
|
855
|
+
- Add device descriptions to cache
|
|
856
|
+
- Fetch paramset descriptions from backend
|
|
857
|
+
6. Persistence: Save updated caches to disk
|
|
858
|
+
7. Device creation: Create Device objects from cached descriptions
|
|
859
|
+
|
|
860
|
+
Semaphore pattern:
|
|
861
|
+
The _device_add_semaphore ensures only one device addition operation
|
|
862
|
+
runs at a time. This prevents race conditions when multiple interfaces
|
|
863
|
+
report new devices simultaneously.
|
|
864
|
+
|
|
865
|
+
Delayed device creation:
|
|
866
|
+
When delay_new_device_creation is enabled, newly discovered devices
|
|
867
|
+
are stored in _delayed_device_descriptions instead of being created.
|
|
868
|
+
This allows the user to review and approve new devices before they
|
|
869
|
+
appear in Home Assistant.
|
|
870
|
+
|
|
871
|
+
Args:
|
|
872
|
+
----
|
|
873
|
+
interface_id: Interface identifier
|
|
874
|
+
device_descriptions: Tuple of device descriptions
|
|
875
|
+
source: Source of device creation (STARTUP, NEW, MANUAL)
|
|
876
|
+
|
|
877
|
+
"""
|
|
878
|
+
if not device_descriptions:
|
|
879
|
+
_LOGGER.debug(
|
|
880
|
+
"ADD_NEW_DEVICES: Nothing to add for interface_id %s",
|
|
881
|
+
interface_id,
|
|
882
|
+
)
|
|
883
|
+
return
|
|
884
|
+
|
|
885
|
+
_LOGGER.debug(
|
|
886
|
+
"ADD_NEW_DEVICES: interface_id = %s, device_descriptions = %s",
|
|
887
|
+
interface_id,
|
|
888
|
+
len(device_descriptions),
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
if not self._coordinator_provider.client_coordinator.has_client(interface_id=interface_id):
|
|
892
|
+
_LOGGER.error( # i18n-log: ignore
|
|
893
|
+
"ADD_NEW_DEVICES failed: Missing client for interface_id %s",
|
|
894
|
+
interface_id,
|
|
895
|
+
)
|
|
896
|
+
return
|
|
897
|
+
|
|
898
|
+
async with self._device_add_semaphore:
|
|
899
|
+
new_device_descriptions = self._identify_new_device_descriptions(
|
|
900
|
+
device_descriptions=device_descriptions, interface_id=interface_id
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
# For REFRESH operations, we need to update the cache even for existing devices
|
|
904
|
+
# (e.g., firmware data may have changed)
|
|
905
|
+
descriptions_to_cache = (
|
|
906
|
+
device_descriptions if source == SourceOfDeviceCreation.REFRESH else new_device_descriptions
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
if not descriptions_to_cache:
|
|
910
|
+
# Check if there are devices with missing paramset descriptions
|
|
911
|
+
# This can happen when device_descriptions were cached but paramsets weren't
|
|
912
|
+
# (e.g., previous run was interrupted after saving device_descriptions)
|
|
913
|
+
devices_missing_paramsets = self._identify_devices_missing_paramsets(
|
|
914
|
+
interface_id=interface_id, device_descriptions=device_descriptions
|
|
915
|
+
)
|
|
916
|
+
if not devices_missing_paramsets:
|
|
917
|
+
_LOGGER.debug("ADD_NEW_DEVICES: Nothing to add/update for interface_id %s", interface_id)
|
|
918
|
+
return
|
|
919
|
+
|
|
920
|
+
# Fetch missing paramset descriptions
|
|
921
|
+
_LOGGER.debug(
|
|
922
|
+
"ADD_NEW_DEVICES: Fetching missing paramsets for %s devices on interface_id %s",
|
|
923
|
+
len(devices_missing_paramsets),
|
|
924
|
+
interface_id,
|
|
925
|
+
)
|
|
926
|
+
client = self._coordinator_provider.client_coordinator.get_client(interface_id=interface_id)
|
|
927
|
+
for dev_desc in devices_missing_paramsets:
|
|
928
|
+
await client.fetch_paramset_descriptions(device_description=dev_desc)
|
|
929
|
+
|
|
930
|
+
await self._coordinator_provider.cache_coordinator.save_all(save_paramset_descriptions=True)
|
|
931
|
+
|
|
932
|
+
# Now check if we can create devices
|
|
933
|
+
if new_device_addresses := self.check_for_new_device_addresses(interface_id=interface_id):
|
|
934
|
+
await self._coordinator_provider.cache_coordinator.device_details.load()
|
|
935
|
+
await self._coordinator_provider.cache_coordinator.load_data_cache(interface=client.interface)
|
|
936
|
+
await self.create_devices(new_device_addresses=new_device_addresses, source=source)
|
|
937
|
+
return
|
|
938
|
+
|
|
939
|
+
# Here we block the automatic creation of new devices, if required
|
|
940
|
+
if self._config_provider.config.delay_new_device_creation and source == SourceOfDeviceCreation.NEW:
|
|
941
|
+
self._store_delayed_device_descriptions(device_descriptions=new_device_descriptions)
|
|
942
|
+
self._coordinator_provider.event_coordinator.publish_system_event(
|
|
943
|
+
system_event=SystemEventType.DEVICES_DELAYED,
|
|
944
|
+
new_addresses=tuple(self._delayed_device_descriptions.keys()),
|
|
945
|
+
interface_id=interface_id,
|
|
946
|
+
source=source,
|
|
947
|
+
)
|
|
948
|
+
return
|
|
949
|
+
|
|
950
|
+
client = self._coordinator_provider.client_coordinator.get_client(interface_id=interface_id)
|
|
951
|
+
save_descriptions = False
|
|
952
|
+
for dev_desc in descriptions_to_cache:
|
|
953
|
+
try:
|
|
954
|
+
self._coordinator_provider.cache_coordinator.device_descriptions.add_device(
|
|
955
|
+
interface_id=interface_id, device_description=dev_desc
|
|
956
|
+
)
|
|
957
|
+
# Only fetch paramset descriptions for new devices (not needed for refresh)
|
|
958
|
+
if source != SourceOfDeviceCreation.REFRESH or dev_desc in new_device_descriptions:
|
|
959
|
+
await client.fetch_paramset_descriptions(device_description=dev_desc)
|
|
960
|
+
save_descriptions = True
|
|
961
|
+
except Exception as exc: # pragma: no cover
|
|
962
|
+
save_descriptions = False
|
|
963
|
+
_LOGGER.error( # i18n-log: ignore
|
|
964
|
+
"UPDATE_CACHES_WITH_NEW_DEVICES failed: %s [%s]",
|
|
965
|
+
type(exc).__name__,
|
|
966
|
+
extract_exc_args(exc=exc),
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
await self._coordinator_provider.cache_coordinator.save_all(
|
|
970
|
+
save_device_descriptions=save_descriptions,
|
|
971
|
+
save_paramset_descriptions=save_descriptions,
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
# Device creation MUST be inside semaphore to prevent race condition:
|
|
975
|
+
# Without this, startup code can call check_for_new_device_addresses()
|
|
976
|
+
# while callback is still adding descriptions, causing incomplete devices.
|
|
977
|
+
if new_device_addresses := self.check_for_new_device_addresses(interface_id=interface_id):
|
|
978
|
+
await self._coordinator_provider.cache_coordinator.device_details.load()
|
|
979
|
+
await self._coordinator_provider.cache_coordinator.load_data_cache(interface=client.interface)
|
|
980
|
+
await self.create_devices(new_device_addresses=new_device_addresses, source=source)
|
|
981
|
+
|
|
982
|
+
def _identify_devices_missing_paramsets(
|
|
983
|
+
self, *, interface_id: str, device_descriptions: tuple[DeviceDescription, ...]
|
|
984
|
+
) -> tuple[DeviceDescription, ...]:
|
|
985
|
+
"""
|
|
986
|
+
Identify devices that have device_descriptions but missing paramset_descriptions.
|
|
987
|
+
|
|
988
|
+
This handles the case where device_descriptions were persisted but paramset_descriptions
|
|
989
|
+
weren't (e.g., previous run was interrupted, or paramset fetch failed).
|
|
990
|
+
|
|
991
|
+
Synchronization check:
|
|
992
|
+
For each device_description, we verify that ALL expected paramsets (from the
|
|
993
|
+
PARAMSETS field) are present in the paramset_descriptions cache. This ensures
|
|
994
|
+
both caches are synchronized - not just that "some" paramset exists.
|
|
995
|
+
|
|
996
|
+
Args:
|
|
997
|
+
----
|
|
998
|
+
interface_id: Interface identifier
|
|
999
|
+
device_descriptions: Tuple of device descriptions to check
|
|
1000
|
+
|
|
1001
|
+
Returns:
|
|
1002
|
+
-------
|
|
1003
|
+
Tuple of device descriptions that need paramset fetching
|
|
1004
|
+
|
|
1005
|
+
"""
|
|
1006
|
+
paramset_cache = self._coordinator_provider.cache_coordinator.paramset_descriptions
|
|
1007
|
+
missing: list[DeviceDescription] = []
|
|
1008
|
+
|
|
1009
|
+
for dev_desc in device_descriptions:
|
|
1010
|
+
address = dev_desc["ADDRESS"]
|
|
1011
|
+
|
|
1012
|
+
# Skip if no paramsets expected (shouldn't happen, but be safe)
|
|
1013
|
+
if not (expected_paramsets := dev_desc.get("PARAMSETS", [])):
|
|
1014
|
+
continue
|
|
1015
|
+
|
|
1016
|
+
# Get cached paramsets for this address
|
|
1017
|
+
cached_paramsets = paramset_cache.get_channel_paramset_descriptions(
|
|
1018
|
+
interface_id=interface_id, channel_address=address
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
# Check if ALL expected paramsets are present in cache
|
|
1022
|
+
cached_keys = set(cached_paramsets.keys())
|
|
1023
|
+
expected_keys = {ParamsetKey(p) for p in expected_paramsets}
|
|
1024
|
+
|
|
1025
|
+
if not expected_keys.issubset(cached_keys):
|
|
1026
|
+
missing.append(dev_desc)
|
|
1027
|
+
|
|
1028
|
+
return tuple(missing)
|
|
1029
|
+
|
|
1030
|
+
def _identify_new_device_descriptions(
|
|
1031
|
+
self, *, device_descriptions: tuple[DeviceDescription, ...], interface_id: str | None = None
|
|
1032
|
+
) -> tuple[DeviceDescription, ...]:
|
|
1033
|
+
"""
|
|
1034
|
+
Identify devices whose ADDRESS isn't already known on any interface.
|
|
1035
|
+
|
|
1036
|
+
Address resolution with PARENT fallback:
|
|
1037
|
+
Device descriptions come in two forms:
|
|
1038
|
+
- Device entries: ADDRESS is the device address, PARENT is empty/missing
|
|
1039
|
+
- Channel entries: ADDRESS is channel address (e.g., "ABC:1"), PARENT is device address
|
|
1040
|
+
|
|
1041
|
+
When checking if a device is new, we need to check the device address,
|
|
1042
|
+
not the channel address. The expression:
|
|
1043
|
+
dev_desc["ADDRESS"] if not parent_address else parent_address
|
|
1044
|
+
Handles both cases:
|
|
1045
|
+
- For device entries: Use ADDRESS (PARENT is empty)
|
|
1046
|
+
- For channel entries: Use PARENT (the actual device address)
|
|
1047
|
+
|
|
1048
|
+
This ensures we don't treat the same device as "new" multiple times
|
|
1049
|
+
when we receive descriptions for both the device and its channels.
|
|
1050
|
+
|
|
1051
|
+
Args:
|
|
1052
|
+
----
|
|
1053
|
+
device_descriptions: Tuple of device descriptions
|
|
1054
|
+
interface_id: Optional interface identifier
|
|
1055
|
+
|
|
1056
|
+
Returns:
|
|
1057
|
+
-------
|
|
1058
|
+
Tuple of new device descriptions
|
|
1059
|
+
|
|
1060
|
+
"""
|
|
1061
|
+
known_addresses = self._coordinator_provider.cache_coordinator.device_descriptions.get_addresses(
|
|
1062
|
+
interface_id=interface_id
|
|
1063
|
+
)
|
|
1064
|
+
return tuple(
|
|
1065
|
+
dev_desc
|
|
1066
|
+
for dev_desc in device_descriptions
|
|
1067
|
+
# Use PARENT if present (channel entry), else ADDRESS (device entry)
|
|
1068
|
+
if (parent_address if (parent_address := dev_desc.get("PARENT")) else dev_desc["ADDRESS"])
|
|
1069
|
+
not in known_addresses
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
async def _rename_new_device(
|
|
1073
|
+
self,
|
|
1074
|
+
*,
|
|
1075
|
+
client: DeviceDiscoveryAndMetadataProtocol,
|
|
1076
|
+
device_descriptions: tuple[DeviceDescription, ...],
|
|
1077
|
+
device_name: str,
|
|
1078
|
+
) -> None:
|
|
1079
|
+
"""
|
|
1080
|
+
Rename a new device and its channels before adding to the system.
|
|
1081
|
+
|
|
1082
|
+
Args:
|
|
1083
|
+
client: The client to use for renaming.
|
|
1084
|
+
device_descriptions: Tuple of device descriptions (device + channels).
|
|
1085
|
+
device_name: The new name for the device.
|
|
1086
|
+
|
|
1087
|
+
"""
|
|
1088
|
+
await client.fetch_device_details()
|
|
1089
|
+
for device_desc in device_descriptions:
|
|
1090
|
+
address = device_desc["ADDRESS"]
|
|
1091
|
+
parent = device_desc.get("PARENT")
|
|
1092
|
+
|
|
1093
|
+
if (rega_id := await client.get_rega_id_by_address(address=address)) is None:
|
|
1094
|
+
_LOGGER.warning( # i18n-log: ignore
|
|
1095
|
+
"RENAME_NEW_DEVICE: Could not get rega_id for address %s",
|
|
1096
|
+
address,
|
|
1097
|
+
)
|
|
1098
|
+
continue
|
|
1099
|
+
|
|
1100
|
+
if not parent:
|
|
1101
|
+
# This is the device itself
|
|
1102
|
+
await client.rename_device(rega_id=rega_id, new_name=device_name)
|
|
1103
|
+
elif (channel_no := address.split(":")[-1] if ":" in address else None) is not None:
|
|
1104
|
+
# This is a channel - extract channel number from address
|
|
1105
|
+
channel_name = f"{device_name}:{channel_no}"
|
|
1106
|
+
await client.rename_channel(rega_id=rega_id, new_name=channel_name)
|
|
1107
|
+
|
|
1108
|
+
await asyncio.sleep(0.1)
|
|
1109
|
+
|
|
1110
|
+
def _store_delayed_device_descriptions(self, *, device_descriptions: tuple[DeviceDescription, ...]) -> None:
|
|
1111
|
+
"""Store device descriptions for delayed creation."""
|
|
1112
|
+
for dev_desc in device_descriptions:
|
|
1113
|
+
device_address = dev_desc.get("PARENT") or dev_desc["ADDRESS"]
|
|
1114
|
+
self._delayed_device_descriptions[device_address].append(dev_desc)
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
def _get_new_channel_events(*, new_devices: set[DeviceProtocol]) -> tuple[tuple[GenericEventProtocolAny, ...], ...]:
|
|
1118
|
+
"""
|
|
1119
|
+
Return new channel events.
|
|
1120
|
+
|
|
1121
|
+
Args:
|
|
1122
|
+
----
|
|
1123
|
+
new_devices: Set of new devices
|
|
1124
|
+
|
|
1125
|
+
Returns:
|
|
1126
|
+
-------
|
|
1127
|
+
Tuple of channel event tuples
|
|
1128
|
+
|
|
1129
|
+
"""
|
|
1130
|
+
channel_events: list[tuple[GenericEventProtocolAny, ...]] = []
|
|
1131
|
+
|
|
1132
|
+
for device in new_devices:
|
|
1133
|
+
for event_type in DATA_POINT_EVENTS:
|
|
1134
|
+
if (hm_channel_events := list(device.get_events(event_type=event_type, registered=False).values())) and len(
|
|
1135
|
+
hm_channel_events
|
|
1136
|
+
) > 0:
|
|
1137
|
+
channel_events.extend(hm_channel_events) # noqa: PERF401
|
|
1138
|
+
|
|
1139
|
+
return tuple(channel_events)
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
def _get_new_data_points(
|
|
1143
|
+
*,
|
|
1144
|
+
new_devices: set[DeviceProtocol],
|
|
1145
|
+
) -> dict[DataPointCategory, set[CallbackDataPointProtocol]]:
|
|
1146
|
+
"""
|
|
1147
|
+
Return new data points by category.
|
|
1148
|
+
|
|
1149
|
+
Args:
|
|
1150
|
+
----
|
|
1151
|
+
new_devices: Set of new devices
|
|
1152
|
+
|
|
1153
|
+
Returns:
|
|
1154
|
+
-------
|
|
1155
|
+
Mapping of categories to data points
|
|
1156
|
+
|
|
1157
|
+
"""
|
|
1158
|
+
data_points_by_category: dict[DataPointCategory, set[CallbackDataPointProtocol]] = {
|
|
1159
|
+
category: set() for category in CATEGORIES if category != DataPointCategory.EVENT
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
for device in new_devices:
|
|
1163
|
+
for category, data_points in data_points_by_category.items():
|
|
1164
|
+
data_points.update(device.get_data_points(category=category, exclude_no_create=True, registered=False))
|
|
1165
|
+
|
|
1166
|
+
return data_points_by_category
|