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,175 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Central data cache for device/channel parameter values.
|
|
5
|
+
|
|
6
|
+
This module provides CentralDataCache which stores recently fetched device/channel
|
|
7
|
+
parameter values from interfaces for quick lookup and periodic refresh.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Any, Final
|
|
16
|
+
|
|
17
|
+
from aiohomematic.const import INIT_DATETIME, MAX_CACHE_AGE, NO_CACHE_ENTRY, CallSource, Interface, ParamsetKey
|
|
18
|
+
from aiohomematic.interfaces import (
|
|
19
|
+
CacheWithStatisticsProtocol,
|
|
20
|
+
CentralInfoProtocol,
|
|
21
|
+
ClientProviderProtocol,
|
|
22
|
+
DataCacheWriterProtocol,
|
|
23
|
+
DataPointProviderProtocol,
|
|
24
|
+
DeviceProviderProtocol,
|
|
25
|
+
)
|
|
26
|
+
from aiohomematic.store.types import CacheName, CacheStatistics
|
|
27
|
+
from aiohomematic.support import changed_within_seconds
|
|
28
|
+
|
|
29
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CentralDataCache(DataCacheWriterProtocol, CacheWithStatisticsProtocol):
|
|
33
|
+
"""Central cache for device/channel initial data."""
|
|
34
|
+
|
|
35
|
+
__slots__ = (
|
|
36
|
+
"_central_info",
|
|
37
|
+
"_client_provider",
|
|
38
|
+
"_data_point_provider",
|
|
39
|
+
"_device_provider",
|
|
40
|
+
"_is_initializing",
|
|
41
|
+
"_refreshed_at",
|
|
42
|
+
"_stats",
|
|
43
|
+
"_value_cache",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
*,
|
|
49
|
+
device_provider: DeviceProviderProtocol,
|
|
50
|
+
client_provider: ClientProviderProtocol,
|
|
51
|
+
data_point_provider: DataPointProviderProtocol,
|
|
52
|
+
central_info: CentralInfoProtocol,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Initialize the central data cache."""
|
|
55
|
+
self._device_provider: Final = device_provider
|
|
56
|
+
self._client_provider: Final = client_provider
|
|
57
|
+
self._data_point_provider: Final = data_point_provider
|
|
58
|
+
self._central_info: Final = central_info
|
|
59
|
+
self._stats: Final = CacheStatistics()
|
|
60
|
+
# { key, value}
|
|
61
|
+
self._value_cache: Final[dict[Interface, Mapping[str, Any]]] = {}
|
|
62
|
+
self._refreshed_at: Final[dict[Interface, datetime]] = {}
|
|
63
|
+
# During initialization, cache expiration is disabled to prevent
|
|
64
|
+
# getValue calls when device creation takes longer than MAX_CACHE_AGE
|
|
65
|
+
self._is_initializing: bool = True
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def name(self) -> CacheName:
|
|
69
|
+
"""Return the cache name."""
|
|
70
|
+
return CacheName.DATA
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def size(self) -> int:
|
|
74
|
+
"""Return total number of entries in cache."""
|
|
75
|
+
return sum(len(cache) for cache in self._value_cache.values())
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def statistics(self) -> CacheStatistics:
|
|
79
|
+
"""Return the cache statistics."""
|
|
80
|
+
return self._stats
|
|
81
|
+
|
|
82
|
+
def add_data(self, *, interface: Interface, all_device_data: Mapping[str, Any]) -> None:
|
|
83
|
+
"""Add data to cache."""
|
|
84
|
+
self._value_cache[interface] = all_device_data
|
|
85
|
+
self._refreshed_at[interface] = datetime.now()
|
|
86
|
+
|
|
87
|
+
def clear(self, *, interface: Interface | None = None) -> None:
|
|
88
|
+
"""Clear the cache."""
|
|
89
|
+
if interface:
|
|
90
|
+
self._value_cache[interface] = {}
|
|
91
|
+
self._refreshed_at[interface] = INIT_DATETIME
|
|
92
|
+
else:
|
|
93
|
+
for _interface in self._device_provider.interfaces:
|
|
94
|
+
self.clear(interface=_interface)
|
|
95
|
+
|
|
96
|
+
def get_data(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
interface: Interface,
|
|
100
|
+
channel_address: str,
|
|
101
|
+
parameter: str,
|
|
102
|
+
) -> Any:
|
|
103
|
+
"""Get data from cache."""
|
|
104
|
+
if not self._is_empty(interface=interface) and (iface_cache := self._value_cache.get(interface)) is not None:
|
|
105
|
+
result = iface_cache.get(f"{interface}.{channel_address}.{parameter}", NO_CACHE_ENTRY)
|
|
106
|
+
if result != NO_CACHE_ENTRY:
|
|
107
|
+
self._stats.record_hit()
|
|
108
|
+
else:
|
|
109
|
+
self._stats.record_miss()
|
|
110
|
+
return result
|
|
111
|
+
self._stats.record_miss()
|
|
112
|
+
return NO_CACHE_ENTRY
|
|
113
|
+
|
|
114
|
+
async def load(self, *, direct_call: bool = False, interface: Interface | None = None) -> None:
|
|
115
|
+
"""Fetch data from the backend."""
|
|
116
|
+
_LOGGER.debug("load: Loading device data for %s", self._central_info.name)
|
|
117
|
+
for client in self._client_provider.clients:
|
|
118
|
+
if interface and interface != client.interface:
|
|
119
|
+
continue
|
|
120
|
+
if direct_call is False and changed_within_seconds(
|
|
121
|
+
last_change=self._get_refreshed_at(interface=client.interface),
|
|
122
|
+
max_age=int(MAX_CACHE_AGE / 3),
|
|
123
|
+
):
|
|
124
|
+
return
|
|
125
|
+
await client.fetch_all_device_data()
|
|
126
|
+
|
|
127
|
+
async def refresh_data_point_data(
|
|
128
|
+
self,
|
|
129
|
+
*,
|
|
130
|
+
paramset_key: ParamsetKey | None = None,
|
|
131
|
+
interface: Interface | None = None,
|
|
132
|
+
direct_call: bool = False,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Refresh data_point data."""
|
|
135
|
+
for dp in self._data_point_provider.get_readable_generic_data_points(
|
|
136
|
+
paramset_key=paramset_key, interface=interface
|
|
137
|
+
):
|
|
138
|
+
await dp.load_data_point_value(call_source=CallSource.HM_INIT, direct_call=direct_call)
|
|
139
|
+
|
|
140
|
+
def set_initialization_complete(self) -> None:
|
|
141
|
+
"""
|
|
142
|
+
Mark initialization as complete, enabling cache expiration.
|
|
143
|
+
|
|
144
|
+
Call this after device creation is finished to enable normal cache
|
|
145
|
+
expiration behavior. During initialization, cache entries are kept
|
|
146
|
+
regardless of age to avoid triggering getValue calls when device
|
|
147
|
+
creation takes longer than MAX_CACHE_AGE.
|
|
148
|
+
"""
|
|
149
|
+
self._is_initializing = False
|
|
150
|
+
_LOGGER.debug(
|
|
151
|
+
"SET_INITIALIZATION_COMPLETE: Cache expiration enabled for %s",
|
|
152
|
+
self._central_info.name,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def _get_refreshed_at(self, *, interface: Interface) -> datetime:
|
|
156
|
+
"""Return when cache has been refreshed."""
|
|
157
|
+
return self._refreshed_at.get(interface, INIT_DATETIME)
|
|
158
|
+
|
|
159
|
+
def _is_empty(self, *, interface: Interface) -> bool:
|
|
160
|
+
"""Return if cache is empty for the given interface."""
|
|
161
|
+
# If there is no data stored for the requested interface, treat as empty.
|
|
162
|
+
if not self._value_cache.get(interface):
|
|
163
|
+
return True
|
|
164
|
+
# Skip cache expiration during initialization to prevent getValue calls
|
|
165
|
+
# when device creation takes longer than MAX_CACHE_AGE (10 seconds).
|
|
166
|
+
if self._is_initializing:
|
|
167
|
+
return False
|
|
168
|
+
# Auto-expire stale cache by interface.
|
|
169
|
+
if not changed_within_seconds(last_change=self._get_refreshed_at(interface=interface)):
|
|
170
|
+
# Track eviction before clearing
|
|
171
|
+
if (evicted_count := len(self._value_cache.get(interface, {}))) > 0:
|
|
172
|
+
self._stats.record_eviction(count=evicted_count)
|
|
173
|
+
self.clear(interface=interface)
|
|
174
|
+
return True
|
|
175
|
+
return False
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Device details cache for runtime device metadata.
|
|
5
|
+
|
|
6
|
+
This module provides DeviceDetailsCache which enriches devices with human-readable
|
|
7
|
+
names, interface mapping, rooms, functions, and address IDs fetched via the backend.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Final, cast
|
|
17
|
+
|
|
18
|
+
from aiohomematic.const import INIT_DATETIME, MAX_CACHE_AGE, Interface
|
|
19
|
+
from aiohomematic.interfaces import (
|
|
20
|
+
CentralInfoProtocol,
|
|
21
|
+
DeviceDetailsProviderProtocol,
|
|
22
|
+
DeviceDetailsWriterProtocol,
|
|
23
|
+
PrimaryClientProviderProtocol,
|
|
24
|
+
)
|
|
25
|
+
from aiohomematic.interfaces.model import DeviceRemovalInfoProtocol
|
|
26
|
+
from aiohomematic.property_decorators import DelegatedProperty
|
|
27
|
+
from aiohomematic.support import changed_within_seconds, get_device_address
|
|
28
|
+
|
|
29
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DeviceDetailsCache(DeviceDetailsProviderProtocol, DeviceDetailsWriterProtocol):
|
|
33
|
+
"""Cache for device/channel details."""
|
|
34
|
+
|
|
35
|
+
__slots__ = (
|
|
36
|
+
"_central_info",
|
|
37
|
+
"_channel_rooms",
|
|
38
|
+
"_device_channel_rega_ids",
|
|
39
|
+
"_device_rooms",
|
|
40
|
+
"_functions",
|
|
41
|
+
"_interface_cache",
|
|
42
|
+
"_names_cache",
|
|
43
|
+
"_primary_client_provider",
|
|
44
|
+
"_refreshed_at",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
central_info: CentralInfoProtocol,
|
|
51
|
+
primary_client_provider: PrimaryClientProviderProtocol,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Initialize the device details cache."""
|
|
54
|
+
self._central_info: Final = central_info
|
|
55
|
+
self._primary_client_provider: Final = primary_client_provider
|
|
56
|
+
self._channel_rooms: Final[dict[str, set[str]]] = defaultdict(set)
|
|
57
|
+
self._device_channel_rega_ids: Final[dict[str, int]] = {}
|
|
58
|
+
self._device_rooms: Final[dict[str, set[str]]] = defaultdict(set)
|
|
59
|
+
self._functions: Final[dict[str, set[str]]] = {}
|
|
60
|
+
self._interface_cache: Final[dict[str, Interface]] = {}
|
|
61
|
+
self._names_cache: Final[dict[str, str]] = {}
|
|
62
|
+
self._refreshed_at = INIT_DATETIME
|
|
63
|
+
|
|
64
|
+
device_channel_rega_ids: Final = DelegatedProperty[Mapping[str, int]](path="_device_channel_rega_ids")
|
|
65
|
+
|
|
66
|
+
def add_address_rega_id(self, *, address: str, rega_id: int) -> None:
|
|
67
|
+
"""Add channel id for a channel."""
|
|
68
|
+
self._device_channel_rega_ids[address] = rega_id
|
|
69
|
+
|
|
70
|
+
def add_interface(self, *, address: str, interface: Interface) -> None:
|
|
71
|
+
"""Add interface to cache."""
|
|
72
|
+
self._interface_cache[address] = interface
|
|
73
|
+
|
|
74
|
+
def add_name(self, *, address: str, name: str) -> None:
|
|
75
|
+
"""Add name to cache."""
|
|
76
|
+
self._names_cache[address] = name
|
|
77
|
+
|
|
78
|
+
def clear(self) -> None:
|
|
79
|
+
"""Clear the cache."""
|
|
80
|
+
self._names_cache.clear()
|
|
81
|
+
self._channel_rooms.clear()
|
|
82
|
+
self._device_rooms.clear()
|
|
83
|
+
self._functions.clear()
|
|
84
|
+
self._refreshed_at = INIT_DATETIME
|
|
85
|
+
|
|
86
|
+
def get_address_id(self, *, address: str) -> int:
|
|
87
|
+
"""Get id for address."""
|
|
88
|
+
return self._device_channel_rega_ids.get(address) or 0
|
|
89
|
+
|
|
90
|
+
def get_channel_rooms(self, *, channel_address: str) -> set[str]:
|
|
91
|
+
"""Return rooms by channel_address."""
|
|
92
|
+
return self._channel_rooms[channel_address]
|
|
93
|
+
|
|
94
|
+
def get_device_rooms(self, *, device_address: str) -> set[str]:
|
|
95
|
+
"""Return all rooms by device_address."""
|
|
96
|
+
return set(self._device_rooms.get(device_address, ()))
|
|
97
|
+
|
|
98
|
+
def get_function_text(self, *, address: str) -> str | None:
|
|
99
|
+
"""Return function by address."""
|
|
100
|
+
if functions := self._functions.get(address):
|
|
101
|
+
return ",".join(functions)
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
def get_interface(self, *, address: str) -> Interface:
|
|
105
|
+
"""Get interface from cache."""
|
|
106
|
+
return self._interface_cache.get(address) or Interface.BIDCOS_RF
|
|
107
|
+
|
|
108
|
+
def get_name(self, *, address: str) -> str | None:
|
|
109
|
+
"""Get name from cache."""
|
|
110
|
+
return self._names_cache.get(address)
|
|
111
|
+
|
|
112
|
+
async def load(self, *, direct_call: bool = False) -> None:
|
|
113
|
+
"""Fetch names from the backend."""
|
|
114
|
+
if direct_call is False and changed_within_seconds(
|
|
115
|
+
last_change=self._refreshed_at, max_age=int(MAX_CACHE_AGE / 3)
|
|
116
|
+
):
|
|
117
|
+
return
|
|
118
|
+
self.clear()
|
|
119
|
+
_LOGGER.debug("LOAD: Loading names for %s", self._central_info.name)
|
|
120
|
+
if client := self._primary_client_provider.primary_client:
|
|
121
|
+
await client.fetch_device_details()
|
|
122
|
+
_LOGGER.debug("LOAD: Loading rooms for %s", self._central_info.name)
|
|
123
|
+
self._channel_rooms.clear()
|
|
124
|
+
self._channel_rooms.update(await self._get_all_rooms())
|
|
125
|
+
self._device_rooms.clear()
|
|
126
|
+
self._device_rooms.update(self._prepare_device_rooms())
|
|
127
|
+
_LOGGER.debug("LOAD: Loading functions for %s", self._central_info.name)
|
|
128
|
+
self._functions.clear()
|
|
129
|
+
self._functions.update(await self._get_all_functions())
|
|
130
|
+
self._refreshed_at = datetime.now()
|
|
131
|
+
|
|
132
|
+
def remove_device(self, *, device: DeviceRemovalInfoProtocol) -> None:
|
|
133
|
+
"""Remove device data from all caches."""
|
|
134
|
+
# Clean device-level entries
|
|
135
|
+
self._names_cache.pop(device.address, None)
|
|
136
|
+
self._interface_cache.pop(device.address, None)
|
|
137
|
+
self._device_channel_rega_ids.pop(device.address, None)
|
|
138
|
+
self._device_rooms.pop(device.address, None)
|
|
139
|
+
self._functions.pop(device.address, None)
|
|
140
|
+
|
|
141
|
+
# Clean channel-level entries
|
|
142
|
+
for channel_address in device.channels:
|
|
143
|
+
self._names_cache.pop(channel_address, None)
|
|
144
|
+
self._interface_cache.pop(channel_address, None)
|
|
145
|
+
self._device_channel_rega_ids.pop(channel_address, None)
|
|
146
|
+
self._channel_rooms.pop(channel_address, None)
|
|
147
|
+
self._functions.pop(channel_address, None)
|
|
148
|
+
|
|
149
|
+
async def _get_all_functions(self) -> Mapping[str, set[str]]:
|
|
150
|
+
"""Get all functions, if available."""
|
|
151
|
+
if client := self._primary_client_provider.primary_client:
|
|
152
|
+
return cast(
|
|
153
|
+
Mapping[str, set[str]],
|
|
154
|
+
await client.get_all_functions(),
|
|
155
|
+
)
|
|
156
|
+
return {}
|
|
157
|
+
|
|
158
|
+
async def _get_all_rooms(self) -> Mapping[str, set[str]]:
|
|
159
|
+
"""Get all rooms, if available."""
|
|
160
|
+
if client := self._primary_client_provider.primary_client:
|
|
161
|
+
return cast(
|
|
162
|
+
Mapping[str, set[str]],
|
|
163
|
+
await client.get_all_rooms(),
|
|
164
|
+
)
|
|
165
|
+
return {}
|
|
166
|
+
|
|
167
|
+
def _prepare_device_rooms(self) -> dict[str, set[str]]:
|
|
168
|
+
"""
|
|
169
|
+
Return rooms by device_address.
|
|
170
|
+
|
|
171
|
+
Aggregation algorithm:
|
|
172
|
+
The CCU stores room assignments at the channel level (e.g., "ABC123:1" is in "Living Room").
|
|
173
|
+
Devices themselves don't have direct room assignments - they inherit from their channels.
|
|
174
|
+
This method aggregates channel rooms to the device level by:
|
|
175
|
+
1. Iterating all channel_address -> rooms mappings
|
|
176
|
+
2. Extracting the device_address from each channel_address
|
|
177
|
+
3. Merging all channel rooms into a set per device
|
|
178
|
+
|
|
179
|
+
Result: A device is considered "in" all rooms that any of its channels are in.
|
|
180
|
+
"""
|
|
181
|
+
_device_rooms: Final[dict[str, set[str]]] = defaultdict(set)
|
|
182
|
+
for channel_address, rooms in self._channel_rooms.items():
|
|
183
|
+
if rooms:
|
|
184
|
+
# Extract device address (e.g., "ABC123:1" -> "ABC123")
|
|
185
|
+
# and merge this channel's rooms into the device's room set
|
|
186
|
+
_device_rooms[get_device_address(address=channel_address)].update(rooms)
|
|
187
|
+
return _device_rooms
|