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