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,160 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2026
3
+ """
4
+ Connection state tracking for central unit.
5
+
6
+ This module provides connection status management for the central unit,
7
+ tracking issues per transport (JSON-RPC and XML-RPC proxies).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from datetime import datetime
13
+ import logging
14
+ from typing import TYPE_CHECKING, Final
15
+
16
+ from aiohomematic.central.events import SystemStatusChangedEvent
17
+ from aiohomematic.client import AioJsonRpcAioHttpClient, BaseRpcProxy
18
+ from aiohomematic.support import extract_exc_args
19
+
20
+ if TYPE_CHECKING:
21
+ from aiohomematic.interfaces.central import EventBusProviderProtocol
22
+
23
+ _LOGGER: Final = logging.getLogger(__name__)
24
+
25
+ ConnectionProblemIssuer = AioJsonRpcAioHttpClient | BaseRpcProxy
26
+
27
+
28
+ class CentralConnectionState:
29
+ """
30
+ Track connection status for the central unit.
31
+
32
+ Manages connection issues per transport (JSON-RPC and XML-RPC proxies),
33
+ publishing SystemStatusChangedEvent via EventBus for state changes.
34
+ """
35
+
36
+ def __init__(self, *, event_bus_provider: EventBusProviderProtocol | None = None) -> None:
37
+ """Initialize the CentralConnectionStatus."""
38
+ self._json_issues: Final[list[str]] = []
39
+ self._rpc_proxy_issues: Final[list[str]] = []
40
+ self._event_bus_provider: Final = event_bus_provider
41
+
42
+ @property
43
+ def has_any_issue(self) -> bool:
44
+ """Return True if any connection issue exists."""
45
+ return len(self._json_issues) > 0 or len(self._rpc_proxy_issues) > 0
46
+
47
+ @property
48
+ def issue_count(self) -> int:
49
+ """Return total number of connection issues."""
50
+ return len(self._json_issues) + len(self._rpc_proxy_issues)
51
+
52
+ @property
53
+ def json_issue_count(self) -> int:
54
+ """Return number of JSON-RPC connection issues."""
55
+ return len(self._json_issues)
56
+
57
+ @property
58
+ def rpc_proxy_issue_count(self) -> int:
59
+ """Return number of XML-RPC proxy connection issues."""
60
+ return len(self._rpc_proxy_issues)
61
+
62
+ def add_issue(self, *, issuer: ConnectionProblemIssuer, iid: str) -> bool:
63
+ """Add issue to collection and publish event."""
64
+ added = False
65
+ if isinstance(issuer, AioJsonRpcAioHttpClient) and iid not in self._json_issues:
66
+ self._json_issues.append(iid)
67
+ _LOGGER.debug("add_issue: add issue [%s] for JsonRpcAioHttpClient", iid)
68
+ added = True
69
+ elif isinstance(issuer, BaseRpcProxy) and iid not in self._rpc_proxy_issues:
70
+ self._rpc_proxy_issues.append(iid)
71
+ _LOGGER.debug("add_issue: add issue [%s] for RpcProxy", iid)
72
+ added = True
73
+
74
+ if added:
75
+ self._publish_state_change(interface_id=iid, connected=False)
76
+ return added
77
+
78
+ def clear_all_issues(self) -> int:
79
+ """
80
+ Clear all tracked connection issues.
81
+
82
+ Returns the number of issues cleared.
83
+ """
84
+ if (count := self.issue_count) > 0:
85
+ all_iids = list(self._json_issues) + list(self._rpc_proxy_issues)
86
+ self._json_issues.clear()
87
+ self._rpc_proxy_issues.clear()
88
+ for iid in all_iids:
89
+ self._publish_state_change(interface_id=iid, connected=True)
90
+ return count
91
+ return 0
92
+
93
+ def handle_exception_log(
94
+ self,
95
+ *,
96
+ issuer: ConnectionProblemIssuer,
97
+ iid: str,
98
+ exception: Exception,
99
+ logger: logging.Logger = _LOGGER,
100
+ level: int = logging.ERROR,
101
+ extra_msg: str = "",
102
+ multiple_logs: bool = True,
103
+ ) -> None:
104
+ """Handle Exception and derivates logging."""
105
+ exception_name = exception.name if hasattr(exception, "name") else exception.__class__.__name__
106
+ if self.has_issue(issuer=issuer, iid=iid) and multiple_logs is False:
107
+ logger.debug(
108
+ "%s failed: %s [%s] %s",
109
+ iid,
110
+ exception_name,
111
+ extract_exc_args(exc=exception),
112
+ extra_msg,
113
+ )
114
+ else:
115
+ self.add_issue(issuer=issuer, iid=iid)
116
+ logger.log(
117
+ level,
118
+ "%s failed: %s [%s] %s",
119
+ iid,
120
+ exception_name,
121
+ extract_exc_args(exc=exception),
122
+ extra_msg,
123
+ )
124
+
125
+ def has_issue(self, *, issuer: ConnectionProblemIssuer, iid: str) -> bool:
126
+ """Check if issue exists for the given issuer and interface id."""
127
+ if isinstance(issuer, AioJsonRpcAioHttpClient):
128
+ return iid in self._json_issues
129
+ # issuer is BaseRpcProxy (exhaustive union coverage)
130
+ return iid in self._rpc_proxy_issues
131
+
132
+ def has_rpc_proxy_issue(self, *, interface_id: str) -> bool:
133
+ """Return True if XML-RPC proxy has a known connection issue for interface_id."""
134
+ return interface_id in self._rpc_proxy_issues
135
+
136
+ def remove_issue(self, *, issuer: ConnectionProblemIssuer, iid: str) -> bool:
137
+ """Remove issue from collection and publish event."""
138
+ removed = False
139
+ if isinstance(issuer, AioJsonRpcAioHttpClient) and iid in self._json_issues:
140
+ self._json_issues.remove(iid)
141
+ _LOGGER.debug("remove_issue: removing issue [%s] for JsonRpcAioHttpClient", iid)
142
+ removed = True
143
+ elif isinstance(issuer, BaseRpcProxy) and iid in self._rpc_proxy_issues:
144
+ self._rpc_proxy_issues.remove(iid)
145
+ _LOGGER.debug("remove_issue: removing issue [%s] for RpcProxy", iid)
146
+ removed = True
147
+
148
+ if removed:
149
+ self._publish_state_change(interface_id=iid, connected=True)
150
+ return removed
151
+
152
+ def _publish_state_change(self, *, interface_id: str, connected: bool) -> None:
153
+ """Publish SystemStatusChangedEvent via EventBus."""
154
+ if self._event_bus_provider is None:
155
+ return
156
+ event = SystemStatusChangedEvent(
157
+ timestamp=datetime.now(),
158
+ connection_state=(interface_id, connected),
159
+ )
160
+ self._event_bus_provider.event_bus.publish_sync(event=event)
@@ -0,0 +1,38 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2026
3
+ """
4
+ Coordinator sub-package for central orchestration components.
5
+
6
+ This package contains the coordinator classes that manage specific aspects
7
+ of the central unit's functionality:
8
+
9
+ - CacheCoordinator: Cache management (device descriptions, paramsets, data)
10
+ - ClientCoordinator: Client lifecycle and connection management
11
+ - ConnectionRecoveryCoordinator: Unified connection recovery and retry management
12
+ - DeviceCoordinator: Device discovery and creation
13
+ - EventCoordinator: Event handling and system event processing
14
+ - HubCoordinator: Hub-level entities (programs, sysvars, install mode)
15
+
16
+ Public API of this module is defined by __all__.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from aiohomematic.central.coordinators.cache import CacheCoordinator
22
+ from aiohomematic.central.coordinators.client import ClientCoordinator
23
+ from aiohomematic.central.coordinators.connection_recovery import ConnectionRecoveryCoordinator
24
+ from aiohomematic.central.coordinators.device import DeviceCoordinator
25
+ from aiohomematic.central.coordinators.event import EventCoordinator, SystemEventArgs
26
+ from aiohomematic.central.coordinators.hub import HubCoordinator
27
+
28
+ __all__ = [
29
+ # Coordinators
30
+ "CacheCoordinator",
31
+ "ClientCoordinator",
32
+ "ConnectionRecoveryCoordinator",
33
+ "DeviceCoordinator",
34
+ "EventCoordinator",
35
+ "HubCoordinator",
36
+ # Types
37
+ "SystemEventArgs",
38
+ ]
@@ -0,0 +1,414 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2026
3
+ """
4
+ Cache coordinator for managing all cache operations.
5
+
6
+ This module provides centralized cache management for device descriptions,
7
+ paramset descriptions, device details, data cache, and session recording.
8
+
9
+ The CacheCoordinator provides:
10
+ - Unified cache loading and saving
11
+ - Cache clearing operations
12
+ - Device-specific cache management
13
+ - Session recording coordination
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Callable, Mapping
19
+ from dataclasses import dataclass
20
+ from datetime import datetime
21
+ import logging
22
+ from typing import Final
23
+
24
+ from aiohomematic.central.events import CacheInvalidatedEvent, DeviceRemovedEvent
25
+ from aiohomematic.const import (
26
+ FILE_DEVICES,
27
+ FILE_INCIDENTS,
28
+ FILE_PARAMSETS,
29
+ SUB_DIRECTORY_CACHE,
30
+ CacheInvalidationReason,
31
+ CacheType,
32
+ DataOperationResult,
33
+ Interface,
34
+ )
35
+ from aiohomematic.interfaces import (
36
+ CentralInfoProtocol,
37
+ ClientProviderProtocol,
38
+ ConfigProviderProtocol,
39
+ DataPointProviderProtocol,
40
+ DeviceProviderProtocol,
41
+ EventBusProviderProtocol,
42
+ PrimaryClientProviderProtocol,
43
+ SessionRecorderProviderProtocol,
44
+ TaskSchedulerProtocol,
45
+ )
46
+ from aiohomematic.interfaces.model import DeviceRemovalInfoProtocol
47
+ from aiohomematic.metrics._protocols import CacheProviderForMetricsProtocol
48
+ from aiohomematic.property_decorators import DelegatedProperty
49
+ from aiohomematic.store import CacheStatistics, StorageFactoryProtocol
50
+ from aiohomematic.store.dynamic import CentralDataCache, DeviceDetailsCache
51
+ from aiohomematic.store.persistent import (
52
+ DeviceDescriptionRegistry,
53
+ IncidentStore,
54
+ ParamsetDescriptionRegistry,
55
+ SessionRecorder,
56
+ )
57
+ from aiohomematic.store.visibility import ParameterVisibilityRegistry
58
+
59
+ _LOGGER: Final = logging.getLogger(__name__)
60
+
61
+
62
+ @dataclass(frozen=True, slots=True)
63
+ class _DeviceRemovalAdapter:
64
+ """
65
+ Adapter to satisfy DeviceRemovalInfoProtocol from event data.
66
+
67
+ This lightweight adapter allows cache removal methods to work with
68
+ data extracted from DeviceRemovedEvent without requiring a full Device object.
69
+ """
70
+
71
+ address: str
72
+ """Device address."""
73
+
74
+ interface_id: str
75
+ """Interface ID."""
76
+
77
+ channel_addresses: tuple[str, ...]
78
+ """Channel addresses."""
79
+
80
+ @property
81
+ def channels(self) -> Mapping[str, None]:
82
+ """Return channel addresses as a mapping (keys only used)."""
83
+ return dict.fromkeys(self.channel_addresses)
84
+
85
+
86
+ class CacheCoordinator(SessionRecorderProviderProtocol, CacheProviderForMetricsProtocol):
87
+ """Coordinator for all cache operations in the central unit."""
88
+
89
+ __slots__ = (
90
+ "_central_info",
91
+ "_data_cache",
92
+ "_device_descriptions_registry",
93
+ "_device_details_cache",
94
+ "_event_bus_provider",
95
+ "_incident_store",
96
+ "_parameter_visibility_registry",
97
+ "_paramset_descriptions_registry",
98
+ "_session_recorder",
99
+ "_unsubscribers",
100
+ )
101
+
102
+ def __init__(
103
+ self,
104
+ *,
105
+ central_info: CentralInfoProtocol,
106
+ client_provider: ClientProviderProtocol,
107
+ config_provider: ConfigProviderProtocol,
108
+ data_point_provider: DataPointProviderProtocol,
109
+ device_provider: DeviceProviderProtocol,
110
+ event_bus_provider: EventBusProviderProtocol,
111
+ primary_client_provider: PrimaryClientProviderProtocol,
112
+ session_recorder_active: bool,
113
+ storage_factory: StorageFactoryProtocol,
114
+ task_scheduler: TaskSchedulerProtocol,
115
+ ) -> None:
116
+ """
117
+ Initialize the cache coordinator.
118
+
119
+ Args:
120
+ central_info: Provider for central system information.
121
+ device_provider: Provider for device access.
122
+ client_provider: Provider for client access.
123
+ data_point_provider: Provider for data point access.
124
+ event_bus_provider: Provider for event bus access.
125
+ primary_client_provider: Provider for primary client access.
126
+ config_provider: Provider for configuration access.
127
+ storage_factory: Factory for creating storage instances.
128
+ task_scheduler: Provider for task scheduling.
129
+ session_recorder_active: Whether session recording should be active.
130
+
131
+ """
132
+ self._central_info: Final = central_info
133
+ self._event_bus_provider: Final = event_bus_provider
134
+
135
+ # Create storage instances for persistent caches
136
+ device_storage = storage_factory.create_storage(
137
+ key=FILE_DEVICES,
138
+ sub_directory=SUB_DIRECTORY_CACHE,
139
+ )
140
+ paramset_storage = storage_factory.create_storage(
141
+ key=FILE_PARAMSETS,
142
+ sub_directory=SUB_DIRECTORY_CACHE,
143
+ )
144
+ incident_storage = storage_factory.create_storage(
145
+ key=FILE_INCIDENTS,
146
+ sub_directory=SUB_DIRECTORY_CACHE,
147
+ )
148
+
149
+ # Initialize all caches with protocol interfaces
150
+ self._data_cache: Final = CentralDataCache(
151
+ device_provider=device_provider,
152
+ client_provider=client_provider,
153
+ data_point_provider=data_point_provider,
154
+ central_info=central_info,
155
+ )
156
+ self._device_details_cache: Final = DeviceDetailsCache(
157
+ central_info=central_info,
158
+ primary_client_provider=primary_client_provider,
159
+ )
160
+ self._device_descriptions_registry: Final = DeviceDescriptionRegistry(
161
+ storage=device_storage,
162
+ config_provider=config_provider,
163
+ )
164
+ self._paramset_descriptions_registry: Final = ParamsetDescriptionRegistry(
165
+ storage=paramset_storage,
166
+ config_provider=config_provider,
167
+ )
168
+ self._parameter_visibility_registry: Final = ParameterVisibilityRegistry(
169
+ config_provider=config_provider,
170
+ )
171
+ self._incident_store: Final = IncidentStore(
172
+ storage=incident_storage,
173
+ config_provider=config_provider,
174
+ )
175
+ self._session_recorder: Final = SessionRecorder(
176
+ central_info=central_info,
177
+ config_provider=config_provider,
178
+ device_provider=device_provider,
179
+ task_scheduler=task_scheduler,
180
+ storage_factory=storage_factory,
181
+ ttl_seconds=600,
182
+ active=session_recorder_active,
183
+ )
184
+
185
+ # Subscribe to device removal events for decoupled cache invalidation
186
+ self._unsubscribers: list[Callable[[], None]] = []
187
+ self._unsubscribers.append(
188
+ event_bus_provider.event_bus.subscribe(
189
+ event_type=DeviceRemovedEvent,
190
+ event_key=None,
191
+ handler=self._on_device_removed,
192
+ )
193
+ )
194
+
195
+ data_cache: Final = DelegatedProperty[CentralDataCache](path="_data_cache")
196
+ device_descriptions: Final = DelegatedProperty[DeviceDescriptionRegistry](path="_device_descriptions_registry")
197
+ device_details: Final = DelegatedProperty[DeviceDetailsCache](path="_device_details_cache")
198
+ incident_store: Final = DelegatedProperty[IncidentStore](path="_incident_store")
199
+ parameter_visibility: Final = DelegatedProperty[ParameterVisibilityRegistry](path="_parameter_visibility_registry")
200
+ paramset_descriptions: Final = DelegatedProperty[ParamsetDescriptionRegistry](
201
+ path="_paramset_descriptions_registry"
202
+ )
203
+ recorder: Final = DelegatedProperty[SessionRecorder](path="_session_recorder")
204
+
205
+ @property
206
+ def data_cache_size(self) -> int:
207
+ """Return data cache size."""
208
+ return self._data_cache.size
209
+
210
+ @property
211
+ def data_cache_statistics(self) -> CacheStatistics:
212
+ """Return data cache statistics."""
213
+ return self._data_cache.statistics
214
+
215
+ @property
216
+ def device_descriptions_size(self) -> int:
217
+ """Return device descriptions cache size."""
218
+ return self._device_descriptions_registry.size
219
+
220
+ @property
221
+ def paramset_descriptions_size(self) -> int:
222
+ """Return paramset descriptions cache size."""
223
+ return self._paramset_descriptions_registry.size
224
+
225
+ @property
226
+ def visibility_cache_size(self) -> int:
227
+ """Return visibility cache size."""
228
+ return self._parameter_visibility_registry.size
229
+
230
+ async def clear_all(
231
+ self,
232
+ *,
233
+ reason: CacheInvalidationReason = CacheInvalidationReason.MANUAL,
234
+ ) -> None:
235
+ """
236
+ Clear all caches and remove stored files.
237
+
238
+ Args:
239
+ ----
240
+ reason: Reason for cache invalidation
241
+
242
+ """
243
+ _LOGGER.debug("CLEAR_ALL: Clearing all caches for %s", self._central_info.name)
244
+
245
+ await self._device_descriptions_registry.clear()
246
+ await self._paramset_descriptions_registry.clear()
247
+ await self._session_recorder.clear()
248
+ data_cache_size = self._data_cache.size
249
+ self._device_details_cache.clear()
250
+ self._data_cache.clear()
251
+
252
+ # Emit single consolidated cache invalidation event
253
+ await self._event_bus_provider.event_bus.publish(
254
+ event=CacheInvalidatedEvent(
255
+ timestamp=datetime.now(),
256
+ cache_type=CacheType.DATA, # Representative of full clear
257
+ reason=reason,
258
+ scope=None, # Full cache clear
259
+ entries_affected=data_cache_size,
260
+ )
261
+ )
262
+
263
+ def clear_on_stop(self) -> None:
264
+ """Clear in-memory caches on shutdown to free memory."""
265
+ _LOGGER.debug("CLEAR_ON_STOP: Clearing in-memory caches for %s", self._central_info.name)
266
+ data_cache_size = self._data_cache.size
267
+ self._device_details_cache.clear()
268
+ self._data_cache.clear()
269
+ self._parameter_visibility_registry.clear_memoization_caches()
270
+
271
+ # Emit cache invalidation event (sync publish)
272
+ self._event_bus_provider.event_bus.publish_sync(
273
+ event=CacheInvalidatedEvent(
274
+ timestamp=datetime.now(),
275
+ cache_type=CacheType.DATA,
276
+ reason=CacheInvalidationReason.SHUTDOWN,
277
+ scope=None,
278
+ entries_affected=data_cache_size,
279
+ )
280
+ )
281
+
282
+ async def load_all(self) -> bool:
283
+ """
284
+ Load all persistent caches from disk.
285
+
286
+ Returns
287
+ -------
288
+ True if loading succeeded, False if any cache failed to load
289
+
290
+ """
291
+ _LOGGER.debug("LOAD_ALL: Loading caches for %s", self._central_info.name)
292
+
293
+ if DataOperationResult.LOAD_FAIL in (
294
+ await self._device_descriptions_registry.load(),
295
+ await self._paramset_descriptions_registry.load(),
296
+ ):
297
+ _LOGGER.warning( # i18n-log: ignore
298
+ "LOAD_ALL failed: Unable to load caches for %s. Clearing files",
299
+ self._central_info.name,
300
+ )
301
+ await self.clear_all()
302
+ return False
303
+
304
+ await self._device_details_cache.load()
305
+ await self._data_cache.load()
306
+ return True
307
+
308
+ async def load_data_cache(self, *, interface: Interface | None = None) -> None:
309
+ """
310
+ Load data cache for a specific interface or all interfaces.
311
+
312
+ Args:
313
+ ----
314
+ interface: Interface to load cache for, or None for all
315
+
316
+ """
317
+ await self._data_cache.load(interface=interface)
318
+
319
+ def remove_device_from_caches(self, *, device: DeviceRemovalInfoProtocol) -> None:
320
+ """
321
+ Remove a device from all relevant caches.
322
+
323
+ Note: This method is deprecated for direct calls. Prefer publishing
324
+ DeviceRemovedEvent which triggers automatic cache invalidation.
325
+
326
+ Args:
327
+ ----
328
+ device: Device to remove from caches
329
+
330
+ """
331
+ _LOGGER.debug(
332
+ "REMOVE_DEVICE_FROM_CACHES: Removing device %s from caches",
333
+ device.address,
334
+ )
335
+ self._device_descriptions_registry.remove_device(device=device)
336
+ self._paramset_descriptions_registry.remove_device(device=device)
337
+ self._device_details_cache.remove_device(device=device)
338
+
339
+ async def save_all(
340
+ self,
341
+ *,
342
+ save_device_descriptions: bool = False,
343
+ save_paramset_descriptions: bool = False,
344
+ ) -> None:
345
+ """
346
+ Save persistent caches to disk.
347
+
348
+ Args:
349
+ ----
350
+ save_device_descriptions: Whether to save device descriptions
351
+ save_paramset_descriptions: Whether to save paramset descriptions
352
+
353
+ """
354
+ _LOGGER.debug(
355
+ "SAVE_ALL: Saving caches for %s (device_desc=%s, paramset_desc=%s)",
356
+ self._central_info.name,
357
+ save_device_descriptions,
358
+ save_paramset_descriptions,
359
+ )
360
+
361
+ if save_device_descriptions:
362
+ await self._device_descriptions_registry.save()
363
+ if save_paramset_descriptions:
364
+ await self._paramset_descriptions_registry.save()
365
+
366
+ def set_data_cache_initialization_complete(self) -> None:
367
+ """
368
+ Mark data cache initialization as complete.
369
+
370
+ Call this after device creation is finished to enable normal cache
371
+ expiration behavior. During initialization, cache entries are kept
372
+ regardless of age to avoid triggering getValue calls when device
373
+ creation takes longer than MAX_CACHE_AGE.
374
+ """
375
+ self._data_cache.set_initialization_complete()
376
+
377
+ def stop(self) -> None:
378
+ """Stop the coordinator and unsubscribe from events."""
379
+ for unsub in self._unsubscribers:
380
+ unsub()
381
+ self._unsubscribers.clear()
382
+
383
+ def _on_device_removed(self, *, event: DeviceRemovedEvent) -> None:
384
+ """
385
+ Handle DeviceRemovedEvent for decoupled cache invalidation.
386
+
387
+ This handler is triggered when a device is removed, allowing caches
388
+ to react independently without direct coupling to the device coordinator.
389
+
390
+ Only processes device-level removal events (where device_address is set).
391
+ Data point removal events (only unique_id set) are ignored.
392
+
393
+ Args:
394
+ ----
395
+ event: The device removed event
396
+
397
+ """
398
+ # Only process device-level removal events
399
+ if event.device_address is None or event.interface_id is None:
400
+ return
401
+
402
+ _LOGGER.debug(
403
+ "CACHE_COORDINATOR: Received DeviceRemovedEvent for %s, invalidating caches",
404
+ event.device_address,
405
+ )
406
+ # Create adapter for cache removal methods
407
+ removal_info = _DeviceRemovalAdapter(
408
+ address=event.device_address,
409
+ interface_id=event.interface_id,
410
+ channel_addresses=event.channel_addresses,
411
+ )
412
+ self._device_descriptions_registry.remove_device(device=removal_info) # type: ignore[arg-type]
413
+ self._paramset_descriptions_registry.remove_device(device=removal_info) # type: ignore[arg-type]
414
+ self._device_details_cache.remove_device(device=removal_info) # type: ignore[arg-type]