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,864 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2026
3
+ """
4
+ Hub orchestration for AioHomematic.
5
+
6
+ This module provides the Hub class that orchestrates scanning and synchronization
7
+ of hub-level data points representing backend state (programs, system variables,
8
+ install mode, metrics, inbox, and system updates).
9
+
10
+ Public API
11
+ ----------
12
+ - Hub: Main orchestrator for hub-level data point lifecycle.
13
+ - ProgramDpType: Named tuple grouping button and switch for a program.
14
+ - MetricsDpType: Named tuple grouping system health, latency, and event age sensors.
15
+
16
+ Key responsibilities
17
+ --------------------
18
+ - Fetch and synchronize programs from CCU backend
19
+ - Fetch and synchronize system variables with type-appropriate data points
20
+ - Manage install mode data points per interface
21
+ - Create and refresh metrics sensors (system health, connection latency, event age)
22
+ - Track inbox devices pending adoption
23
+ - Monitor system update availability (OpenCCU)
24
+
25
+ Data flow
26
+ ---------
27
+ 1. Hub.fetch_*_data methods retrieve data from the primary client
28
+ 2. Existing data points are updated or new ones created as needed
29
+ 3. Removed items are cleaned up from the data point manager
30
+ 4. HUB_REFRESHED events notify consumers of new data points
31
+
32
+ Concurrency
33
+ -----------
34
+ Fetch operations are protected by semaphores to prevent concurrent updates
35
+ of the same data category.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import asyncio
41
+ from collections.abc import Callable, Collection, Mapping, Set as AbstractSet
42
+ from datetime import datetime
43
+ import logging
44
+ from typing import Final, NamedTuple
45
+
46
+ from aiohomematic.central.events.types import ClientStateChangedEvent
47
+ from aiohomematic.const import (
48
+ HUB_CATEGORIES,
49
+ Backend,
50
+ DataPointCategory,
51
+ HubValueType,
52
+ InstallModeData,
53
+ Interface,
54
+ ProgramData,
55
+ ServiceScope,
56
+ SystemEventType,
57
+ SystemVariableData,
58
+ )
59
+ from aiohomematic.decorators import inspector
60
+ from aiohomematic.interfaces.central import (
61
+ CentralInfoProtocol,
62
+ ChannelLookupProtocol,
63
+ ConfigProviderProtocol,
64
+ EventBusProviderProtocol,
65
+ EventPublisherProtocol,
66
+ HealthTrackerProtocol,
67
+ HubDataFetcherProtocol,
68
+ HubDataPointManagerProtocol,
69
+ MetricsProviderProtocol,
70
+ )
71
+ from aiohomematic.interfaces.client import ClientProviderProtocol, PrimaryClientProviderProtocol
72
+ from aiohomematic.interfaces.model import GenericHubDataPointProtocol, HubProtocol
73
+ from aiohomematic.interfaces.operations import (
74
+ ParameterVisibilityProviderProtocol,
75
+ ParamsetDescriptionProviderProtocol,
76
+ TaskSchedulerProtocol,
77
+ )
78
+ from aiohomematic.model.hub.binary_sensor import SysvarDpBinarySensor
79
+ from aiohomematic.model.hub.button import ProgramDpButton
80
+ from aiohomematic.model.hub.connectivity import HmInterfaceConnectivitySensor
81
+ from aiohomematic.model.hub.data_point import GenericProgramDataPoint, GenericSysvarDataPoint
82
+ from aiohomematic.model.hub.inbox import HmInboxSensor
83
+ from aiohomematic.model.hub.install_mode import InstallModeDpButton, InstallModeDpSensor, InstallModeDpType
84
+ from aiohomematic.model.hub.metrics import HmConnectionLatencySensor, HmLastEventAgeSensor, HmSystemHealthSensor
85
+ from aiohomematic.model.hub.number import SysvarDpNumber
86
+ from aiohomematic.model.hub.select import SysvarDpSelect
87
+ from aiohomematic.model.hub.sensor import SysvarDpSensor
88
+ from aiohomematic.model.hub.switch import ProgramDpSwitch, SysvarDpSwitch
89
+ from aiohomematic.model.hub.text import SysvarDpText
90
+ from aiohomematic.model.hub.update import HmUpdate
91
+ from aiohomematic.property_decorators import DelegatedProperty
92
+
93
+ _LOGGER: Final = logging.getLogger(__name__)
94
+
95
+ _EXCLUDED: Final = [
96
+ "OldVal",
97
+ "pcCCUID",
98
+ ]
99
+
100
+
101
+ class ProgramDpType(NamedTuple):
102
+ """Key for data points."""
103
+
104
+ pid: str
105
+ button: ProgramDpButton
106
+ switch: ProgramDpSwitch
107
+
108
+
109
+ class MetricsDpType(NamedTuple):
110
+ """Container for metrics hub sensors."""
111
+
112
+ system_health: HmSystemHealthSensor
113
+ connection_latency: HmConnectionLatencySensor
114
+ last_event_age: HmLastEventAgeSensor
115
+
116
+
117
+ class ConnectivityDpType(NamedTuple):
118
+ """Container for interface connectivity sensors."""
119
+
120
+ interface_id: str
121
+ interface: Interface
122
+ sensor: HmInterfaceConnectivitySensor
123
+
124
+
125
+ class Hub(HubProtocol):
126
+ """The Homematic hub."""
127
+
128
+ __slots__ = (
129
+ "_central_info",
130
+ "_channel_lookup",
131
+ "_client_provider",
132
+ "_config_provider",
133
+ "_connectivity_dps",
134
+ "_event_bus_provider",
135
+ "_event_publisher",
136
+ "_health_tracker",
137
+ "_hub_data_fetcher",
138
+ "_hub_data_point_manager",
139
+ "_inbox_dp",
140
+ "_install_mode_dps",
141
+ "_metrics_dps",
142
+ "_metrics_provider",
143
+ "_parameter_visibility_provider",
144
+ "_paramset_description_provider",
145
+ "_primary_client_provider",
146
+ "_sema_fetch_inbox",
147
+ "_sema_fetch_programs",
148
+ "_sema_fetch_sysvars",
149
+ "_sema_fetch_update",
150
+ "_task_scheduler",
151
+ "_unsubscribers",
152
+ "_update_dp",
153
+ )
154
+
155
+ def __init__(
156
+ self,
157
+ *,
158
+ config_provider: ConfigProviderProtocol,
159
+ central_info: CentralInfoProtocol,
160
+ client_provider: ClientProviderProtocol,
161
+ hub_data_point_manager: HubDataPointManagerProtocol,
162
+ primary_client_provider: PrimaryClientProviderProtocol,
163
+ event_publisher: EventPublisherProtocol,
164
+ event_bus_provider: EventBusProviderProtocol,
165
+ task_scheduler: TaskSchedulerProtocol,
166
+ paramset_description_provider: ParamsetDescriptionProviderProtocol,
167
+ parameter_visibility_provider: ParameterVisibilityProviderProtocol,
168
+ channel_lookup: ChannelLookupProtocol,
169
+ hub_data_fetcher: HubDataFetcherProtocol,
170
+ metrics_provider: MetricsProviderProtocol,
171
+ health_tracker: HealthTrackerProtocol,
172
+ ) -> None:
173
+ """Initialize Homematic hub."""
174
+ self._sema_fetch_sysvars: Final = asyncio.Semaphore()
175
+ self._sema_fetch_programs: Final = asyncio.Semaphore()
176
+ self._sema_fetch_update: Final = asyncio.Semaphore()
177
+ self._sema_fetch_inbox: Final = asyncio.Semaphore()
178
+ self._config_provider: Final = config_provider
179
+ self._central_info: Final = central_info
180
+ self._client_provider: Final = client_provider
181
+ self._hub_data_point_manager: Final = hub_data_point_manager
182
+ self._primary_client_provider: Final = primary_client_provider
183
+ self._event_publisher: Final = event_publisher
184
+ self._event_bus_provider: Final = event_bus_provider
185
+ self._task_scheduler: Final = task_scheduler
186
+ self._paramset_description_provider: Final = paramset_description_provider
187
+ self._parameter_visibility_provider: Final = parameter_visibility_provider
188
+ self._channel_lookup: Final = channel_lookup
189
+ self._hub_data_fetcher: Final = hub_data_fetcher
190
+ self._metrics_provider: Final = metrics_provider
191
+ self._health_tracker: Final = health_tracker
192
+ self._update_dp: HmUpdate | None = None
193
+ self._inbox_dp: HmInboxSensor | None = None
194
+ self._install_mode_dps: dict[Interface, InstallModeDpType] = {}
195
+ self._metrics_dps: MetricsDpType | None = None
196
+ self._connectivity_dps: dict[str, ConnectivityDpType] = {}
197
+ self._unsubscribers: list[Callable[[], None]] = []
198
+
199
+ connectivity_dps: Final = DelegatedProperty[Mapping[str, ConnectivityDpType]](path="_connectivity_dps")
200
+ inbox_dp: Final = DelegatedProperty[HmInboxSensor | None](path="_inbox_dp")
201
+ install_mode_dps: Final = DelegatedProperty[Mapping[Interface, InstallModeDpType]](path="_install_mode_dps")
202
+ metrics_dps: Final = DelegatedProperty[MetricsDpType | None](path="_metrics_dps")
203
+ update_dp: Final = DelegatedProperty[HmUpdate | None](path="_update_dp")
204
+
205
+ def create_connectivity_dps(self) -> Mapping[str, ConnectivityDpType]:
206
+ """
207
+ Create connectivity binary sensors for all interfaces.
208
+
209
+ Returns a dict of ConnectivityDpType by interface_id.
210
+ """
211
+ if self._connectivity_dps:
212
+ return self._connectivity_dps
213
+
214
+ for client in self._client_provider.clients:
215
+ connectivity_dp = ConnectivityDpType(
216
+ interface_id=client.interface_id,
217
+ interface=client.interface,
218
+ sensor=HmInterfaceConnectivitySensor(
219
+ interface_id=client.interface_id,
220
+ interface=client.interface,
221
+ health_tracker=self._health_tracker,
222
+ config_provider=self._config_provider,
223
+ central_info=self._central_info,
224
+ event_bus_provider=self._event_bus_provider,
225
+ event_publisher=self._event_publisher,
226
+ task_scheduler=self._task_scheduler,
227
+ paramset_description_provider=self._paramset_description_provider,
228
+ parameter_visibility_provider=self._parameter_visibility_provider,
229
+ ),
230
+ )
231
+ self._connectivity_dps[client.interface_id] = connectivity_dp
232
+ _LOGGER.debug(
233
+ "CREATE_CONNECTIVITY_DPS: Created connectivity sensor for %s",
234
+ client.interface_id,
235
+ )
236
+
237
+ return self._connectivity_dps
238
+
239
+ def create_install_mode_dps(self) -> Mapping[Interface, InstallModeDpType]:
240
+ """
241
+ Create install mode data points for all supported interfaces.
242
+
243
+ Returns a dict of InstallModeDpType by Interface.
244
+ """
245
+ if self._install_mode_dps:
246
+ return self._install_mode_dps
247
+
248
+ # Check which interfaces support install mode
249
+ for interface in (Interface.BIDCOS_RF, Interface.HMIP_RF):
250
+ if self._create_install_mode_dp_for_interface(interface=interface):
251
+ _LOGGER.debug(
252
+ "CREATE_INSTALL_MODE_DPS: Created install mode data points for %s",
253
+ interface,
254
+ )
255
+
256
+ return self._install_mode_dps
257
+
258
+ def create_metrics_dps(self) -> MetricsDpType | None:
259
+ """
260
+ Create metrics hub sensors.
261
+
262
+ Returns MetricsDpType containing all three metrics sensors.
263
+ """
264
+ if self._metrics_dps is not None:
265
+ return self._metrics_dps
266
+
267
+ self._metrics_dps = MetricsDpType(
268
+ system_health=HmSystemHealthSensor(
269
+ metrics_observer=self._metrics_provider.metrics,
270
+ config_provider=self._config_provider,
271
+ central_info=self._central_info,
272
+ event_bus_provider=self._event_bus_provider,
273
+ event_publisher=self._event_publisher,
274
+ task_scheduler=self._task_scheduler,
275
+ paramset_description_provider=self._paramset_description_provider,
276
+ parameter_visibility_provider=self._parameter_visibility_provider,
277
+ ),
278
+ connection_latency=HmConnectionLatencySensor(
279
+ metrics_observer=self._metrics_provider.metrics,
280
+ config_provider=self._config_provider,
281
+ central_info=self._central_info,
282
+ event_bus_provider=self._event_bus_provider,
283
+ event_publisher=self._event_publisher,
284
+ task_scheduler=self._task_scheduler,
285
+ paramset_description_provider=self._paramset_description_provider,
286
+ parameter_visibility_provider=self._parameter_visibility_provider,
287
+ ),
288
+ last_event_age=HmLastEventAgeSensor(
289
+ metrics_observer=self._metrics_provider.metrics,
290
+ config_provider=self._config_provider,
291
+ central_info=self._central_info,
292
+ event_bus_provider=self._event_bus_provider,
293
+ event_publisher=self._event_publisher,
294
+ task_scheduler=self._task_scheduler,
295
+ paramset_description_provider=self._paramset_description_provider,
296
+ parameter_visibility_provider=self._parameter_visibility_provider,
297
+ ),
298
+ )
299
+
300
+ _LOGGER.debug(
301
+ "CREATE_METRICS_DPS: Created metrics hub sensors for %s",
302
+ self._central_info.name,
303
+ )
304
+
305
+ return self._metrics_dps
306
+
307
+ def fetch_connectivity_data(self, *, scheduled: bool) -> None:
308
+ """
309
+ Refresh connectivity binary sensors with current values.
310
+
311
+ This is a synchronous method as connectivity is read directly from the
312
+ HealthTracker without backend calls.
313
+ """
314
+ if not self._connectivity_dps:
315
+ return
316
+ _LOGGER.debug(
317
+ "FETCH_CONNECTIVITY_DATA: %s refreshing of connectivity for %s",
318
+ "Scheduled" if scheduled else "Manual",
319
+ self._central_info.name,
320
+ )
321
+ write_at = datetime.now()
322
+ for connectivity_dp in self._connectivity_dps.values():
323
+ connectivity_dp.sensor.refresh(write_at=write_at)
324
+
325
+ @inspector(re_raise=False, scope=ServiceScope.INTERNAL)
326
+ async def fetch_inbox_data(self, *, scheduled: bool) -> None:
327
+ """Fetch inbox data for the hub."""
328
+ if self._central_info.model is not Backend.CCU:
329
+ return
330
+ _LOGGER.debug(
331
+ "FETCH_INBOX_DATA: %s fetching of inbox for %s",
332
+ "Scheduled" if scheduled else "Manual",
333
+ self._central_info.name,
334
+ )
335
+ async with self._sema_fetch_inbox:
336
+ if self._central_info.available:
337
+ await self._update_inbox_data_point()
338
+
339
+ @inspector(re_raise=False, scope=ServiceScope.INTERNAL)
340
+ async def fetch_install_mode_data(self, *, scheduled: bool) -> None:
341
+ """Fetch install mode data from the backend for all interfaces."""
342
+ if not self._install_mode_dps:
343
+ return
344
+ _LOGGER.debug(
345
+ "FETCH_INSTALL_MODE_DATA: %s fetching of install mode for %s",
346
+ "Scheduled" if scheduled else "Manual",
347
+ self._central_info.name,
348
+ )
349
+ if not self._central_info.available:
350
+ return
351
+
352
+ # Fetch install mode for each interface using the appropriate client
353
+ for interface, install_mode_dp in self._install_mode_dps.items():
354
+ try:
355
+ client = self._client_provider.get_client(interface=interface)
356
+ remaining_seconds = await client.get_install_mode()
357
+ install_mode_dp.sensor.sync_from_backend(remaining_seconds=remaining_seconds)
358
+ except Exception: # noqa: BLE001
359
+ _LOGGER.debug(
360
+ "FETCH_INSTALL_MODE_DATA: No client available for interface %s",
361
+ interface,
362
+ )
363
+
364
+ def fetch_metrics_data(self, *, scheduled: bool) -> None:
365
+ """
366
+ Refresh metrics hub sensors with current values.
367
+
368
+ This is a synchronous method as metrics are read directly from the
369
+ MetricsObserver without backend calls.
370
+ """
371
+ if self._metrics_dps is None:
372
+ return
373
+ _LOGGER.debug(
374
+ "FETCH_METRICS_DATA: %s refreshing of metrics for %s",
375
+ "Scheduled" if scheduled else "Manual",
376
+ self._central_info.name,
377
+ )
378
+ write_at = datetime.now()
379
+ self._metrics_dps.system_health.refresh(write_at=write_at)
380
+ self._metrics_dps.connection_latency.refresh(write_at=write_at)
381
+ self._metrics_dps.last_event_age.refresh(write_at=write_at)
382
+
383
+ @inspector(re_raise=False)
384
+ async def fetch_program_data(self, *, scheduled: bool) -> None:
385
+ """Fetch program data for the hub."""
386
+ if self._config_provider.config.enable_program_scan:
387
+ _LOGGER.debug(
388
+ "FETCH_PROGRAM_DATA: %s fetching of programs for %s",
389
+ "Scheduled" if scheduled else "Manual",
390
+ self._central_info.name,
391
+ )
392
+ async with self._sema_fetch_programs:
393
+ # Check primary client availability instead of central availability
394
+ # to allow hub operations when secondary clients (e.g., CUxD) fail
395
+ if (client := self._primary_client_provider.primary_client) and client.available:
396
+ await self._update_program_data_points()
397
+
398
+ @inspector(re_raise=False, scope=ServiceScope.INTERNAL)
399
+ async def fetch_system_update_data(self, *, scheduled: bool) -> None:
400
+ """Fetch system update data for the hub."""
401
+ if self._central_info.model is not Backend.CCU:
402
+ return
403
+ _LOGGER.debug(
404
+ "FETCH_SYSTEM_UPDATE_DATA: %s fetching of system update info for %s",
405
+ "Scheduled" if scheduled else "Manual",
406
+ self._central_info.name,
407
+ )
408
+ async with self._sema_fetch_update:
409
+ if self._central_info.available:
410
+ await self._update_system_update_data_point()
411
+
412
+ @inspector(re_raise=False)
413
+ async def fetch_sysvar_data(self, *, scheduled: bool) -> None:
414
+ """Fetch sysvar data for the hub."""
415
+ if self._config_provider.config.enable_sysvar_scan:
416
+ _LOGGER.debug(
417
+ "FETCH_SYSVAR_DATA: %s fetching of system variables for %s",
418
+ "Scheduled" if scheduled else "Manual",
419
+ self._central_info.name,
420
+ )
421
+ async with self._sema_fetch_sysvars:
422
+ # Check primary client availability instead of central availability
423
+ # to allow hub operations when secondary clients (e.g., CUxD) fail
424
+ if (client := self._primary_client_provider.primary_client) and client.available:
425
+ await self._update_sysvar_data_points()
426
+
427
+ def init_connectivity(self) -> Mapping[str, ConnectivityDpType]:
428
+ """
429
+ Initialize connectivity binary sensors.
430
+
431
+ Creates sensors, fetches initial values, subscribes to client state events,
432
+ and publishes refresh event.
433
+ Returns dict of ConnectivityDpType by interface_id.
434
+ """
435
+ if not (connectivity_dps := self.create_connectivity_dps()):
436
+ return {}
437
+
438
+ # Subscribe to client state changes for reactive updates
439
+ unsub = self._event_bus_provider.event_bus.subscribe(
440
+ event_type=ClientStateChangedEvent,
441
+ event_key=None, # Subscribe to all interfaces
442
+ handler=self._on_client_state_changed,
443
+ )
444
+ self._unsubscribers.append(unsub)
445
+
446
+ # Fetch initial values
447
+ self.fetch_connectivity_data(scheduled=False)
448
+
449
+ # Publish refresh event to notify consumers
450
+ self.publish_connectivity_refreshed()
451
+
452
+ return connectivity_dps
453
+
454
+ async def init_install_mode(self) -> Mapping[Interface, InstallModeDpType]:
455
+ """
456
+ Initialize install mode data points for all supported interfaces.
457
+
458
+ Creates data points, fetches initial state from backend, and publishes refresh event.
459
+ Returns a dict of InstallModeDpType by Interface.
460
+ """
461
+ if not (install_mode_dps := self.create_install_mode_dps()):
462
+ return {}
463
+
464
+ # Fetch initial state from backend
465
+ await self.fetch_install_mode_data(scheduled=False)
466
+
467
+ # Publish refresh event to notify consumers
468
+ self.publish_install_mode_refreshed()
469
+
470
+ return install_mode_dps
471
+
472
+ def init_metrics(self) -> MetricsDpType | None:
473
+ """
474
+ Initialize metrics hub sensors.
475
+
476
+ Creates sensors, fetches initial values, and publishes refresh event.
477
+ Returns MetricsDpType or None if creation failed.
478
+ """
479
+ if not (metrics_dps := self.create_metrics_dps()):
480
+ return None
481
+
482
+ # Fetch initial values
483
+ self.fetch_metrics_data(scheduled=False)
484
+
485
+ # Publish refresh event to notify consumers
486
+ self.publish_metrics_refreshed()
487
+
488
+ return metrics_dps
489
+
490
+ def publish_connectivity_refreshed(self) -> None:
491
+ """Publish HUB_REFRESHED event for connectivity binary sensors."""
492
+ if not self._connectivity_dps:
493
+ return
494
+ data_points: list[GenericHubDataPointProtocol] = [
495
+ connectivity_dp.sensor for connectivity_dp in self._connectivity_dps.values()
496
+ ]
497
+ self._event_publisher.publish_system_event(
498
+ system_event=SystemEventType.HUB_REFRESHED,
499
+ new_data_points=_get_new_hub_data_points(data_points=data_points),
500
+ )
501
+
502
+ def publish_install_mode_refreshed(self) -> None:
503
+ """Publish HUB_REFRESHED event for install mode data points."""
504
+ if not self._install_mode_dps:
505
+ return
506
+ data_points: list[GenericHubDataPointProtocol] = []
507
+ for install_mode_dp in self._install_mode_dps.values():
508
+ data_points.append(install_mode_dp.button)
509
+ data_points.append(install_mode_dp.sensor)
510
+
511
+ self._event_publisher.publish_system_event(
512
+ system_event=SystemEventType.HUB_REFRESHED,
513
+ new_data_points=_get_new_hub_data_points(data_points=data_points),
514
+ )
515
+
516
+ def publish_metrics_refreshed(self) -> None:
517
+ """Publish HUB_REFRESHED event for metrics hub sensors."""
518
+ if self._metrics_dps is None:
519
+ return
520
+ data_points: list[GenericHubDataPointProtocol] = [
521
+ self._metrics_dps.system_health,
522
+ self._metrics_dps.connection_latency,
523
+ self._metrics_dps.last_event_age,
524
+ ]
525
+ self._event_publisher.publish_system_event(
526
+ system_event=SystemEventType.HUB_REFRESHED,
527
+ new_data_points=_get_new_hub_data_points(data_points=data_points),
528
+ )
529
+
530
+ def _create_install_mode_dp_for_interface(self, *, interface: Interface) -> InstallModeDpType | None:
531
+ """Create install mode data points for a specific interface."""
532
+ if interface in self._install_mode_dps:
533
+ return self._install_mode_dps[interface]
534
+
535
+ # Check if a client exists for this specific interface and supports install mode
536
+ client = next(
537
+ (c for c in self._client_provider.clients if c.interface == interface and c.capabilities.install_mode),
538
+ None,
539
+ )
540
+ if not client:
541
+ return None
542
+
543
+ # Create interface-specific parameter names (used for unique_id generation)
544
+ # The unique_id will be: install_mode_<suffix> where INSTALL_MODE_ADDRESS is the base
545
+ interface_suffix = "hmip" if interface == Interface.HMIP_RF else "bidcos"
546
+ sensor_parameter = interface_suffix
547
+ button_parameter = f"{interface_suffix}_button"
548
+
549
+ sensor = InstallModeDpSensor(
550
+ data=InstallModeData(name=sensor_parameter, interface=interface),
551
+ central_info=self._central_info,
552
+ channel_lookup=self._channel_lookup,
553
+ config_provider=self._config_provider,
554
+ event_bus_provider=self._event_bus_provider,
555
+ event_publisher=self._event_publisher,
556
+ parameter_visibility_provider=self._parameter_visibility_provider,
557
+ paramset_description_provider=self._paramset_description_provider,
558
+ primary_client_provider=self._primary_client_provider,
559
+ task_scheduler=self._task_scheduler,
560
+ )
561
+ button = InstallModeDpButton(
562
+ sensor=sensor,
563
+ data=InstallModeData(name=button_parameter, interface=interface),
564
+ central_info=self._central_info,
565
+ channel_lookup=self._channel_lookup,
566
+ config_provider=self._config_provider,
567
+ event_bus_provider=self._event_bus_provider,
568
+ event_publisher=self._event_publisher,
569
+ parameter_visibility_provider=self._parameter_visibility_provider,
570
+ paramset_description_provider=self._paramset_description_provider,
571
+ primary_client_provider=self._primary_client_provider,
572
+ task_scheduler=self._task_scheduler,
573
+ )
574
+
575
+ install_mode_dp = InstallModeDpType(button=button, sensor=sensor)
576
+ self._install_mode_dps[interface] = install_mode_dp
577
+ return install_mode_dp
578
+
579
+ def _create_program_dp(self, *, data: ProgramData) -> ProgramDpType:
580
+ """Create program as data_point."""
581
+ program_dp = ProgramDpType(
582
+ pid=data.pid,
583
+ button=ProgramDpButton(
584
+ config_provider=self._config_provider,
585
+ central_info=self._central_info,
586
+ event_bus_provider=self._event_bus_provider,
587
+ event_publisher=self._event_publisher,
588
+ task_scheduler=self._task_scheduler,
589
+ paramset_description_provider=self._paramset_description_provider,
590
+ parameter_visibility_provider=self._parameter_visibility_provider,
591
+ channel_lookup=self._channel_lookup,
592
+ primary_client_provider=self._primary_client_provider,
593
+ hub_data_fetcher=self._hub_data_fetcher,
594
+ data=data,
595
+ ),
596
+ switch=ProgramDpSwitch(
597
+ config_provider=self._config_provider,
598
+ central_info=self._central_info,
599
+ event_bus_provider=self._event_bus_provider,
600
+ event_publisher=self._event_publisher,
601
+ task_scheduler=self._task_scheduler,
602
+ paramset_description_provider=self._paramset_description_provider,
603
+ parameter_visibility_provider=self._parameter_visibility_provider,
604
+ channel_lookup=self._channel_lookup,
605
+ primary_client_provider=self._primary_client_provider,
606
+ hub_data_fetcher=self._hub_data_fetcher,
607
+ data=data,
608
+ ),
609
+ )
610
+ self._hub_data_point_manager.add_program_data_point(program_dp=program_dp)
611
+ return program_dp
612
+
613
+ def _create_system_variable(self, *, data: SystemVariableData) -> GenericSysvarDataPoint:
614
+ """Create system variable as data_point."""
615
+ sysvar_dp = self._create_sysvar_data_point(data=data)
616
+ self._hub_data_point_manager.add_sysvar_data_point(sysvar_data_point=sysvar_dp)
617
+ return sysvar_dp
618
+
619
+ def _create_sysvar_data_point(self, *, data: SystemVariableData) -> GenericSysvarDataPoint:
620
+ """Create sysvar data_point."""
621
+ data_type = data.data_type
622
+ extended_sysvar = data.extended_sysvar
623
+ # Common protocol interfaces for all sysvar data points
624
+ protocols = {
625
+ "config_provider": self._config_provider,
626
+ "central_info": self._central_info,
627
+ "event_bus_provider": self._event_bus_provider,
628
+ "event_publisher": self._event_publisher,
629
+ "task_scheduler": self._task_scheduler,
630
+ "paramset_description_provider": self._paramset_description_provider,
631
+ "parameter_visibility_provider": self._parameter_visibility_provider,
632
+ "channel_lookup": self._channel_lookup,
633
+ "primary_client_provider": self._primary_client_provider,
634
+ "data": data,
635
+ }
636
+ if data_type:
637
+ if data_type in (HubValueType.ALARM, HubValueType.LOGIC):
638
+ if extended_sysvar:
639
+ return SysvarDpSwitch(**protocols) # type: ignore[arg-type]
640
+ return SysvarDpBinarySensor(**protocols) # type: ignore[arg-type]
641
+ if data_type == HubValueType.LIST and extended_sysvar:
642
+ return SysvarDpSelect(**protocols) # type: ignore[arg-type]
643
+ if data_type in (HubValueType.FLOAT, HubValueType.INTEGER) and extended_sysvar:
644
+ return SysvarDpNumber(**protocols) # type: ignore[arg-type]
645
+ if data_type == HubValueType.STRING and extended_sysvar:
646
+ return SysvarDpText(**protocols) # type: ignore[arg-type]
647
+
648
+ return SysvarDpSensor(**protocols) # type: ignore[arg-type]
649
+
650
+ def _identify_missing_program_ids(self, *, programs: tuple[ProgramData, ...]) -> set[str]:
651
+ """Identify missing programs."""
652
+ return {
653
+ dp.pid for dp in self._hub_data_point_manager.program_data_points if dp.pid not in [x.pid for x in programs]
654
+ }
655
+
656
+ def _identify_missing_variable_ids(self, *, variables: tuple[SystemVariableData, ...]) -> set[str]:
657
+ """Identify missing variables."""
658
+ variable_ids: dict[str, bool] = {x.vid: x.extended_sysvar for x in variables}
659
+ missing_variable_ids: list[str] = []
660
+ for dp in self._hub_data_point_manager.sysvar_data_points:
661
+ if dp.data_type == HubValueType.STRING:
662
+ continue
663
+ if (vid := dp.vid) is not None and (
664
+ vid not in variable_ids or (dp.is_extended is not variable_ids.get(vid))
665
+ ):
666
+ missing_variable_ids.append(vid)
667
+ return set(missing_variable_ids)
668
+
669
+ async def _on_client_state_changed(self, *, event: ClientStateChangedEvent) -> None:
670
+ """Handle client state change event for reactive connectivity updates."""
671
+ if not self._connectivity_dps:
672
+ return
673
+
674
+ # Find the connectivity sensor for this interface
675
+ if (connectivity_dp := self._connectivity_dps.get(event.interface_id)) is None:
676
+ return
677
+
678
+ # Refresh the sensor to reflect the new state
679
+ connectivity_dp.sensor.refresh(write_at=event.timestamp)
680
+ _LOGGER.debug(
681
+ "ON_CLIENT_STATE_CHANGED: Updated connectivity sensor for %s (%s -> %s)",
682
+ event.interface_id,
683
+ event.old_state.name,
684
+ event.new_state.name,
685
+ )
686
+
687
+ def _remove_program_data_point(self, *, ids: set[str]) -> None:
688
+ """Remove sysvar data_point from hub."""
689
+ for pid in ids:
690
+ self._hub_data_point_manager.remove_program_button(pid=pid)
691
+
692
+ def _remove_sysvar_data_point(self, *, del_data_point_ids: set[str]) -> None:
693
+ """Remove sysvar data_point from hub."""
694
+ for vid in del_data_point_ids:
695
+ self._hub_data_point_manager.remove_sysvar_data_point(vid=vid)
696
+
697
+ async def _update_inbox_data_point(self) -> None:
698
+ """Retrieve inbox devices and update the data point."""
699
+ if not (client := self._primary_client_provider.primary_client):
700
+ return
701
+
702
+ devices = await client.get_inbox_devices()
703
+ is_new = False
704
+
705
+ if self._inbox_dp is None:
706
+ self._inbox_dp = HmInboxSensor(
707
+ config_provider=self._config_provider,
708
+ central_info=self._central_info,
709
+ event_bus_provider=self._event_bus_provider,
710
+ event_publisher=self._event_publisher,
711
+ task_scheduler=self._task_scheduler,
712
+ paramset_description_provider=self._paramset_description_provider,
713
+ parameter_visibility_provider=self._parameter_visibility_provider,
714
+ )
715
+ is_new = True
716
+
717
+ self._inbox_dp.update_data(devices=devices, write_at=datetime.now())
718
+
719
+ if is_new:
720
+ self._event_publisher.publish_system_event(
721
+ system_event=SystemEventType.HUB_REFRESHED,
722
+ new_data_points=_get_new_hub_data_points(data_points=[self._inbox_dp]),
723
+ )
724
+
725
+ async def _update_program_data_points(self) -> None:
726
+ """Retrieve all program data and update program values."""
727
+ if not (client := self._primary_client_provider.primary_client):
728
+ return
729
+ if not (programs := await client.get_all_programs(markers=self._config_provider.config.program_markers)):
730
+ _LOGGER.debug("UPDATE_PROGRAM_DATA_POINTS: Unable to retrieve programs for %s", self._central_info.name)
731
+ return
732
+
733
+ _LOGGER.debug(
734
+ "UPDATE_PROGRAM_DATA_POINTS: %i programs received for %s",
735
+ len(programs),
736
+ self._central_info.name,
737
+ )
738
+
739
+ if missing_program_ids := self._identify_missing_program_ids(programs=programs):
740
+ self._remove_program_data_point(ids=missing_program_ids)
741
+
742
+ new_programs: list[GenericProgramDataPoint] = []
743
+
744
+ for program_data in programs:
745
+ if program_dp := self._hub_data_point_manager.get_program_data_point(pid=program_data.pid):
746
+ program_dp.button.update_data(data=program_data)
747
+ program_dp.switch.update_data(data=program_data)
748
+ else:
749
+ program_dp = self._create_program_dp(data=program_data)
750
+ new_programs.append(program_dp.button)
751
+ new_programs.append(program_dp.switch)
752
+
753
+ if new_programs:
754
+ self._event_publisher.publish_system_event(
755
+ system_event=SystemEventType.HUB_REFRESHED,
756
+ new_data_points=_get_new_hub_data_points(data_points=new_programs),
757
+ )
758
+
759
+ async def _update_system_update_data_point(self) -> None:
760
+ """
761
+ Retrieve system update info and update the data point.
762
+
763
+ Only supported on OpenCCU.
764
+ """
765
+ if not (client := self._primary_client_provider.primary_client):
766
+ return
767
+
768
+ # Only supported on OpenCCU
769
+ if not client.system_information.has_backup:
770
+ return
771
+
772
+ if (update_data := await client.get_system_update_info()) is None:
773
+ _LOGGER.debug(
774
+ "UPDATE_SYSTEM_UPDATE_DATA_POINT: Unable to retrieve system update info for %s",
775
+ self._central_info.name,
776
+ )
777
+ return
778
+
779
+ is_new = False
780
+
781
+ if self._update_dp is None:
782
+ self._update_dp = HmUpdate(
783
+ config_provider=self._config_provider,
784
+ central_info=self._central_info,
785
+ event_bus_provider=self._event_bus_provider,
786
+ event_publisher=self._event_publisher,
787
+ task_scheduler=self._task_scheduler,
788
+ paramset_description_provider=self._paramset_description_provider,
789
+ parameter_visibility_provider=self._parameter_visibility_provider,
790
+ primary_client_provider=self._primary_client_provider,
791
+ )
792
+ is_new = True
793
+
794
+ self._update_dp.update_data(data=update_data, write_at=datetime.now())
795
+
796
+ if is_new:
797
+ self._event_publisher.publish_system_event(
798
+ system_event=SystemEventType.HUB_REFRESHED,
799
+ new_data_points=_get_new_hub_data_points(data_points=[self._update_dp]),
800
+ )
801
+
802
+ async def _update_sysvar_data_points(self) -> None:
803
+ """Retrieve all variable data and update hmvariable values."""
804
+ if not (client := self._primary_client_provider.primary_client):
805
+ return
806
+ if (
807
+ variables := await client.get_all_system_variables(markers=self._config_provider.config.sysvar_markers)
808
+ ) is None:
809
+ _LOGGER.debug("UPDATE_SYSVAR_DATA_POINTS: Unable to retrieve sysvars for %s", self._central_info.name)
810
+ return
811
+
812
+ _LOGGER.debug(
813
+ "UPDATE_SYSVAR_DATA_POINTS: %i sysvars received for %s",
814
+ len(variables),
815
+ self._central_info.name,
816
+ )
817
+
818
+ # remove some variables in case of CCU backend
819
+ # - OldValue(s) are for internal calculations
820
+ if self._central_info.model is Backend.CCU:
821
+ variables = _clean_variables(variables=variables)
822
+
823
+ if missing_variable_ids := self._identify_missing_variable_ids(variables=variables):
824
+ self._remove_sysvar_data_point(del_data_point_ids=missing_variable_ids)
825
+
826
+ new_sysvars: list[GenericSysvarDataPoint] = []
827
+
828
+ for sysvar in variables:
829
+ if dp := self._hub_data_point_manager.get_sysvar_data_point(vid=sysvar.vid):
830
+ dp.write_value(value=sysvar.value, write_at=datetime.now())
831
+ else:
832
+ new_sysvars.append(self._create_system_variable(data=sysvar))
833
+
834
+ if new_sysvars:
835
+ self._event_publisher.publish_system_event(
836
+ system_event=SystemEventType.HUB_REFRESHED,
837
+ new_data_points=_get_new_hub_data_points(data_points=new_sysvars),
838
+ )
839
+
840
+
841
+ def _is_excluded(*, variable: str, excludes: list[str]) -> bool:
842
+ """Check if variable is excluded by exclude_list."""
843
+ return any(marker in variable for marker in excludes)
844
+
845
+
846
+ def _clean_variables(*, variables: tuple[SystemVariableData, ...]) -> tuple[SystemVariableData, ...]:
847
+ """Clean variables by removing excluded."""
848
+ return tuple(sv for sv in variables if not _is_excluded(variable=sv.legacy_name, excludes=_EXCLUDED))
849
+
850
+
851
+ def _get_new_hub_data_points(
852
+ *,
853
+ data_points: Collection[GenericHubDataPointProtocol],
854
+ ) -> Mapping[DataPointCategory, AbstractSet[GenericHubDataPointProtocol]]:
855
+ """Return data points as category dict."""
856
+ hub_data_points: dict[DataPointCategory, set[GenericHubDataPointProtocol]] = {}
857
+ for hub_category in HUB_CATEGORIES:
858
+ hub_data_points[hub_category] = set()
859
+
860
+ for dp in data_points:
861
+ if dp.is_registered is False:
862
+ hub_data_points[dp.category].add(dp)
863
+
864
+ return hub_data_points