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