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