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,187 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Error mapping helpers for RPC transports.
|
|
5
|
+
|
|
6
|
+
This module centralizes small, transport-agnostic utilities to turn the backend
|
|
7
|
+
errors into domain-specific exceptions with useful context. It is used by both
|
|
8
|
+
JSON-RPC and XML-RPC clients.
|
|
9
|
+
|
|
10
|
+
Key types and functions
|
|
11
|
+
- RpcContext: Lightweight context container that formats protocol/method/host
|
|
12
|
+
for readable error messages and logs.
|
|
13
|
+
- map_jsonrpc_error: Maps a JSON-RPC error object to an appropriate exception
|
|
14
|
+
(AuthFailure, InternalBackendException, ClientException).
|
|
15
|
+
- map_transport_error: Maps generic transport-level exceptions like OSError to
|
|
16
|
+
domain exceptions (NoConnectionException/ClientException).
|
|
17
|
+
- map_xmlrpc_fault: Maps XML-RPC faults to domain exceptions with context.
|
|
18
|
+
- sanitize_error_message: Sanitizes error messages to remove sensitive data.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from asyncio import TimeoutError as AsyncTimeoutError
|
|
24
|
+
from collections.abc import Mapping
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
import re
|
|
27
|
+
from typing import Any, Final
|
|
28
|
+
|
|
29
|
+
from aiohomematic.const import FailureReason
|
|
30
|
+
from aiohomematic.exceptions import (
|
|
31
|
+
AuthFailure,
|
|
32
|
+
CircuitBreakerOpenException,
|
|
33
|
+
ClientException,
|
|
34
|
+
InternalBackendException,
|
|
35
|
+
NoConnectionException,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Patterns that may contain sensitive information
|
|
39
|
+
_SENSITIVE_PATTERNS: Final[tuple[tuple[str, str], ...]] = (
|
|
40
|
+
# IP addresses (IPv4)
|
|
41
|
+
(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", "<ip-redacted>"),
|
|
42
|
+
# Hostnames with domains
|
|
43
|
+
(r"\b[a-zA-Z0-9][-a-zA-Z0-9]*(\.[a-zA-Z0-9][-a-zA-Z0-9]*)+\b", "<host-redacted>"),
|
|
44
|
+
# Session IDs (common patterns)
|
|
45
|
+
(r"['\"]?session[_-]?id['\"]?\s*[:=]\s*['\"]?[\w-]+['\"]?", "session_id=<redacted>"),
|
|
46
|
+
# Passwords in URLs or params
|
|
47
|
+
(r"['\"]?password['\"]?\s*[:=]\s*['\"][^'\"]*['\"]", "password=<redacted>"),
|
|
48
|
+
(r"['\"]?passwd['\"]?\s*[:=]\s*['\"][^'\"]*['\"]", "passwd=<redacted>"),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def sanitize_error_message(*, message: str) -> str:
|
|
53
|
+
"""
|
|
54
|
+
Sanitize error message by removing potentially sensitive information.
|
|
55
|
+
|
|
56
|
+
Removes or masks:
|
|
57
|
+
- IP addresses
|
|
58
|
+
- Hostnames
|
|
59
|
+
- Session IDs
|
|
60
|
+
- Passwords
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
message: The error message to sanitize.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Sanitized error message.
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
result = message
|
|
70
|
+
for pattern, replacement in _SENSITIVE_PATTERNS:
|
|
71
|
+
result = re.sub(pattern, replacement, result, flags=re.IGNORECASE)
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(slots=True)
|
|
76
|
+
class RpcContext:
|
|
77
|
+
"""
|
|
78
|
+
Context container for RPC operations.
|
|
79
|
+
|
|
80
|
+
Provides formatted output for error messages with optional sanitization
|
|
81
|
+
to protect sensitive information in logs.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
protocol: str
|
|
85
|
+
method: str
|
|
86
|
+
host: str | None = None
|
|
87
|
+
interface: str | None = None
|
|
88
|
+
params: Mapping[str, Any] | None = None
|
|
89
|
+
|
|
90
|
+
def fmt(self, *, sanitize: bool = False) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Format context for error messages.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
sanitize: If True, omit host information for security.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Formatted context string.
|
|
99
|
+
|
|
100
|
+
"""
|
|
101
|
+
parts: list[str] = [f"protocol={self.protocol}", f"method={self.method}"]
|
|
102
|
+
if self.interface:
|
|
103
|
+
parts.append(f"interface={self.interface}")
|
|
104
|
+
if self.host and not sanitize:
|
|
105
|
+
parts.append(f"host={self.host}")
|
|
106
|
+
return ", ".join(parts)
|
|
107
|
+
|
|
108
|
+
def fmt_sanitized(self) -> str:
|
|
109
|
+
"""Format context with sensitive information redacted."""
|
|
110
|
+
return self.fmt(sanitize=True)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def map_jsonrpc_error(*, error: Mapping[str, Any], ctx: RpcContext) -> Exception:
|
|
114
|
+
"""Map JSON-RPC error to exception."""
|
|
115
|
+
# JSON-RPC 2.0 like error: {code, message, data?}
|
|
116
|
+
code = int(error.get("code", 0))
|
|
117
|
+
message = str(error.get("message", ""))
|
|
118
|
+
# Enrich message with context
|
|
119
|
+
base_msg = f"{message} ({ctx.fmt()})"
|
|
120
|
+
|
|
121
|
+
# Map common codes
|
|
122
|
+
if message.startswith("access denied") or code in (401, -32001):
|
|
123
|
+
return AuthFailure(base_msg)
|
|
124
|
+
if "internal error" in message.lower() or code in (-32603, 500):
|
|
125
|
+
return InternalBackendException(base_msg)
|
|
126
|
+
# Generic client exception for others
|
|
127
|
+
return ClientException(base_msg)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def map_transport_error(*, exc: BaseException, ctx: RpcContext) -> Exception:
|
|
131
|
+
"""Map transport error to exception."""
|
|
132
|
+
msg = f"{exc} ({ctx.fmt()})"
|
|
133
|
+
if isinstance(exc, OSError):
|
|
134
|
+
return NoConnectionException(msg)
|
|
135
|
+
return ClientException(msg)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def map_xmlrpc_fault(*, code: int, fault_string: str, ctx: RpcContext) -> Exception:
|
|
139
|
+
"""Map XML-RPC fault to exception."""
|
|
140
|
+
# Enrich message with context
|
|
141
|
+
fault_msg = f"XMLRPC Fault {code}: {fault_string} ({ctx.fmt()})"
|
|
142
|
+
# Simple mappings
|
|
143
|
+
if "unauthorized" in fault_string.lower():
|
|
144
|
+
return AuthFailure(fault_msg)
|
|
145
|
+
if "internal" in fault_string.lower():
|
|
146
|
+
return InternalBackendException(fault_msg)
|
|
147
|
+
return ClientException(fault_msg)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def exception_to_failure_reason(*, exc: BaseException) -> FailureReason:
|
|
151
|
+
"""
|
|
152
|
+
Map an exception to a FailureReason enum value.
|
|
153
|
+
|
|
154
|
+
This function translates exceptions into categorized failure reasons
|
|
155
|
+
that can be used by state machines and propagated to integrations.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
exc: The exception to categorize.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
The appropriate FailureReason for the exception type.
|
|
162
|
+
|
|
163
|
+
Example:
|
|
164
|
+
```python
|
|
165
|
+
try:
|
|
166
|
+
await client.login()
|
|
167
|
+
except BaseException as exc:
|
|
168
|
+
reason = exception_to_failure_reason(exc=exc)
|
|
169
|
+
state_machine.transition_to(
|
|
170
|
+
target=ClientState.FAILED,
|
|
171
|
+
reason=str(exc),
|
|
172
|
+
failure_reason=reason,
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
if isinstance(exc, AuthFailure):
|
|
178
|
+
return FailureReason.AUTH
|
|
179
|
+
if isinstance(exc, NoConnectionException):
|
|
180
|
+
return FailureReason.NETWORK
|
|
181
|
+
if isinstance(exc, InternalBackendException):
|
|
182
|
+
return FailureReason.INTERNAL
|
|
183
|
+
if isinstance(exc, CircuitBreakerOpenException):
|
|
184
|
+
return FailureReason.CIRCUIT_BREAKER
|
|
185
|
+
if isinstance(exc, TimeoutError | AsyncTimeoutError):
|
|
186
|
+
return FailureReason.TIMEOUT
|
|
187
|
+
return FailureReason.UNKNOWN
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Backend implementations for different Homematic systems.
|
|
5
|
+
|
|
6
|
+
This package provides backend-specific implementations that abstract
|
|
7
|
+
the transport layer (XML-RPC, JSON-RPC) from the client business logic.
|
|
8
|
+
|
|
9
|
+
Public API
|
|
10
|
+
----------
|
|
11
|
+
- BackendOperationsProtocol: Interface for all backend operations
|
|
12
|
+
- BackendCapabilities: Capability flags dataclass
|
|
13
|
+
- CcuBackend: CCU backend (XML-RPC + JSON-RPC)
|
|
14
|
+
- JsonCcuBackend: CCU-Jack backend (JSON-RPC only)
|
|
15
|
+
- HomegearBackend: Homegear backend (XML-RPC with extensions)
|
|
16
|
+
- create_backend: Factory function to create appropriate backend
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from aiohomematic.client.backends.capabilities import (
|
|
23
|
+
CCU_CAPABILITIES,
|
|
24
|
+
HOMEGEAR_CAPABILITIES,
|
|
25
|
+
JSON_CCU_CAPABILITIES,
|
|
26
|
+
BackendCapabilities,
|
|
27
|
+
)
|
|
28
|
+
from aiohomematic.client.backends.ccu import CcuBackend
|
|
29
|
+
from aiohomematic.client.backends.factory import create_backend
|
|
30
|
+
from aiohomematic.client.backends.homegear import HomegearBackend
|
|
31
|
+
from aiohomematic.client.backends.json_ccu import JsonCcuBackend
|
|
32
|
+
from aiohomematic.client.backends.protocol import BackendOperationsProtocol
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# Protocol
|
|
36
|
+
"BackendOperationsProtocol",
|
|
37
|
+
# Capabilities
|
|
38
|
+
"BackendCapabilities",
|
|
39
|
+
"CCU_CAPABILITIES",
|
|
40
|
+
"HOMEGEAR_CAPABILITIES",
|
|
41
|
+
"JSON_CCU_CAPABILITIES",
|
|
42
|
+
# Backends
|
|
43
|
+
"CcuBackend",
|
|
44
|
+
"HomegearBackend",
|
|
45
|
+
"JsonCcuBackend",
|
|
46
|
+
# Factory
|
|
47
|
+
"create_backend",
|
|
48
|
+
]
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
# pylint: disable=unnecessary-ellipsis
|
|
4
|
+
"""
|
|
5
|
+
Base backend class with shared functionality.
|
|
6
|
+
|
|
7
|
+
Provides default implementations that return empty/False for unsupported
|
|
8
|
+
operations, allowing subclasses to only implement what they support.
|
|
9
|
+
|
|
10
|
+
Public API
|
|
11
|
+
----------
|
|
12
|
+
- BaseBackend: Abstract base class for backend implementations
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
import logging
|
|
19
|
+
from typing import TYPE_CHECKING, Any, Final
|
|
20
|
+
|
|
21
|
+
from aiohomematic.client.backends.capabilities import BackendCapabilities
|
|
22
|
+
from aiohomematic.const import (
|
|
23
|
+
BackupData,
|
|
24
|
+
CommandRxMode,
|
|
25
|
+
DescriptionMarker,
|
|
26
|
+
DeviceDescription,
|
|
27
|
+
DeviceDetail,
|
|
28
|
+
InboxDeviceData,
|
|
29
|
+
Interface,
|
|
30
|
+
ParameterData,
|
|
31
|
+
ParamsetKey,
|
|
32
|
+
ProgramData,
|
|
33
|
+
ServiceMessageData,
|
|
34
|
+
ServiceMessageType,
|
|
35
|
+
SystemInformation,
|
|
36
|
+
SystemUpdateData,
|
|
37
|
+
SystemVariableData,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from aiohomematic.client.circuit_breaker import CircuitBreaker
|
|
42
|
+
|
|
43
|
+
__all__ = ["BaseBackend"]
|
|
44
|
+
|
|
45
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BaseBackend(ABC):
|
|
49
|
+
"""
|
|
50
|
+
Abstract base class for backend implementations.
|
|
51
|
+
|
|
52
|
+
Provides default implementations that return empty/False for
|
|
53
|
+
unsupported operations, allowing subclasses to only implement
|
|
54
|
+
what they support.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
__slots__ = (
|
|
58
|
+
"_capabilities",
|
|
59
|
+
"_interface",
|
|
60
|
+
"_interface_id",
|
|
61
|
+
"_system_information",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
*,
|
|
67
|
+
interface: Interface,
|
|
68
|
+
interface_id: str,
|
|
69
|
+
capabilities: BackendCapabilities,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Initialize the base backend."""
|
|
72
|
+
self._interface: Final = interface
|
|
73
|
+
self._interface_id: Final = interface_id
|
|
74
|
+
self._capabilities = capabilities
|
|
75
|
+
self._system_information: SystemInformation
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def all_circuit_breakers_closed(self) -> bool:
|
|
79
|
+
"""Return True if all circuit breakers are in closed state."""
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def capabilities(self) -> BackendCapabilities:
|
|
84
|
+
"""Return the capability flags for this backend."""
|
|
85
|
+
return self._capabilities
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def circuit_breaker(self) -> CircuitBreaker | None:
|
|
89
|
+
"""Return the primary circuit breaker for metrics access."""
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def interface(self) -> Interface:
|
|
94
|
+
"""Return the interface type."""
|
|
95
|
+
return self._interface
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def interface_id(self) -> str:
|
|
99
|
+
"""Return the interface identifier."""
|
|
100
|
+
return self._interface_id
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
@abstractmethod
|
|
104
|
+
def model(self) -> str:
|
|
105
|
+
"""Return the backend model name."""
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def system_information(self) -> SystemInformation:
|
|
110
|
+
"""Return system information."""
|
|
111
|
+
return self._system_information
|
|
112
|
+
|
|
113
|
+
async def accept_device_in_inbox(self, *, device_address: str) -> bool:
|
|
114
|
+
"""Accept inbox device (unsupported by default)."""
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
async def add_link(
|
|
118
|
+
self,
|
|
119
|
+
*,
|
|
120
|
+
sender_address: str,
|
|
121
|
+
receiver_address: str,
|
|
122
|
+
name: str,
|
|
123
|
+
description: str,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Add link (unsupported by default)."""
|
|
126
|
+
|
|
127
|
+
@abstractmethod
|
|
128
|
+
async def check_connection(self, *, handle_ping_pong: bool, caller_id: str | None = None) -> bool:
|
|
129
|
+
"""Check if connection is alive."""
|
|
130
|
+
...
|
|
131
|
+
|
|
132
|
+
async def create_backup_and_download(
|
|
133
|
+
self,
|
|
134
|
+
*,
|
|
135
|
+
max_wait_time: float = 300.0,
|
|
136
|
+
poll_interval: float = 5.0,
|
|
137
|
+
) -> BackupData | None:
|
|
138
|
+
"""Create backup (unsupported by default)."""
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
@abstractmethod
|
|
142
|
+
async def deinit_proxy(self, *, init_url: str) -> None:
|
|
143
|
+
"""De-initialize proxy."""
|
|
144
|
+
...
|
|
145
|
+
|
|
146
|
+
async def delete_system_variable(self, *, name: str) -> bool:
|
|
147
|
+
"""Delete system variable (unsupported by default)."""
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
async def execute_program(self, *, pid: str) -> bool:
|
|
151
|
+
"""Execute program (unsupported by default)."""
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
async def get_all_device_data(self, *, interface: Interface) -> dict[str, Any] | None:
|
|
155
|
+
"""Return all device data (unsupported by default)."""
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
async def get_all_functions(self) -> dict[str, set[str]]:
|
|
159
|
+
"""Return all functions (unsupported by default)."""
|
|
160
|
+
return {}
|
|
161
|
+
|
|
162
|
+
async def get_all_programs(self, *, markers: tuple[DescriptionMarker | str, ...]) -> tuple[ProgramData, ...]:
|
|
163
|
+
"""Return all programs (unsupported by default)."""
|
|
164
|
+
return ()
|
|
165
|
+
|
|
166
|
+
async def get_all_rooms(self) -> dict[str, set[str]]:
|
|
167
|
+
"""Return all rooms (unsupported by default)."""
|
|
168
|
+
return {}
|
|
169
|
+
|
|
170
|
+
async def get_all_system_variables(
|
|
171
|
+
self, *, markers: tuple[DescriptionMarker | str, ...]
|
|
172
|
+
) -> tuple[SystemVariableData, ...] | None:
|
|
173
|
+
"""Return all system variables (unsupported by default)."""
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
@abstractmethod
|
|
177
|
+
async def get_device_description(self, *, address: str) -> DeviceDescription | None:
|
|
178
|
+
"""Return device description for address."""
|
|
179
|
+
...
|
|
180
|
+
|
|
181
|
+
async def get_device_details(self, *, addresses: tuple[str, ...] | None = None) -> list[DeviceDetail] | None:
|
|
182
|
+
"""Return device details (unsupported by default)."""
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
async def get_inbox_devices(self) -> tuple[InboxDeviceData, ...]:
|
|
186
|
+
"""Return inbox devices (unsupported by default)."""
|
|
187
|
+
return ()
|
|
188
|
+
|
|
189
|
+
async def get_install_mode(self) -> int:
|
|
190
|
+
"""Return install mode time (unsupported by default)."""
|
|
191
|
+
return 0
|
|
192
|
+
|
|
193
|
+
async def get_link_peers(self, *, address: str) -> tuple[str, ...]:
|
|
194
|
+
"""Return link peers (unsupported by default)."""
|
|
195
|
+
return ()
|
|
196
|
+
|
|
197
|
+
async def get_links(self, *, address: str, flags: int) -> dict[str, Any]:
|
|
198
|
+
"""Return links (unsupported by default)."""
|
|
199
|
+
return {}
|
|
200
|
+
|
|
201
|
+
async def get_metadata(self, *, address: str, data_id: str) -> dict[str, Any]:
|
|
202
|
+
"""Return metadata (unsupported by default)."""
|
|
203
|
+
return {}
|
|
204
|
+
|
|
205
|
+
@abstractmethod
|
|
206
|
+
async def get_paramset(self, *, address: str, paramset_key: ParamsetKey | str) -> dict[str, Any]:
|
|
207
|
+
"""Return paramset."""
|
|
208
|
+
...
|
|
209
|
+
|
|
210
|
+
@abstractmethod
|
|
211
|
+
async def get_paramset_description(
|
|
212
|
+
self, *, address: str, paramset_key: ParamsetKey
|
|
213
|
+
) -> dict[str, ParameterData] | None:
|
|
214
|
+
"""Return paramset description."""
|
|
215
|
+
...
|
|
216
|
+
|
|
217
|
+
async def get_rega_id_by_address(self, *, address: str) -> int | None:
|
|
218
|
+
"""Return ReGa ID (unsupported by default)."""
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
async def get_service_messages(
|
|
222
|
+
self, *, message_type: ServiceMessageType | None = None
|
|
223
|
+
) -> tuple[ServiceMessageData, ...]:
|
|
224
|
+
"""Return service messages (unsupported by default)."""
|
|
225
|
+
return ()
|
|
226
|
+
|
|
227
|
+
async def get_system_update_info(self) -> SystemUpdateData | None:
|
|
228
|
+
"""Return system update info (unsupported by default)."""
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
async def get_system_variable(self, *, name: str) -> Any:
|
|
232
|
+
"""Return system variable value (unsupported by default)."""
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
@abstractmethod
|
|
236
|
+
async def get_value(self, *, address: str, parameter: str) -> Any:
|
|
237
|
+
"""Return parameter value."""
|
|
238
|
+
...
|
|
239
|
+
|
|
240
|
+
async def has_program_ids(self, *, rega_id: int) -> bool:
|
|
241
|
+
"""Check program IDs (unsupported by default)."""
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
@abstractmethod
|
|
245
|
+
async def init_proxy(self, *, init_url: str, interface_id: str) -> None:
|
|
246
|
+
"""Initialize proxy with callback URL."""
|
|
247
|
+
...
|
|
248
|
+
|
|
249
|
+
@abstractmethod
|
|
250
|
+
async def initialize(self) -> None:
|
|
251
|
+
"""Initialize the backend."""
|
|
252
|
+
...
|
|
253
|
+
|
|
254
|
+
@abstractmethod
|
|
255
|
+
async def list_devices(self) -> tuple[DeviceDescription, ...] | None:
|
|
256
|
+
"""Return all device descriptions."""
|
|
257
|
+
...
|
|
258
|
+
|
|
259
|
+
@abstractmethod
|
|
260
|
+
async def put_paramset(
|
|
261
|
+
self,
|
|
262
|
+
*,
|
|
263
|
+
address: str,
|
|
264
|
+
paramset_key: ParamsetKey | str,
|
|
265
|
+
values: dict[str, Any],
|
|
266
|
+
rx_mode: CommandRxMode | None = None,
|
|
267
|
+
) -> None:
|
|
268
|
+
"""Set paramset values."""
|
|
269
|
+
...
|
|
270
|
+
|
|
271
|
+
async def remove_link(self, *, sender_address: str, receiver_address: str) -> None:
|
|
272
|
+
"""Remove link (unsupported by default)."""
|
|
273
|
+
|
|
274
|
+
async def rename_channel(self, *, rega_id: int, new_name: str) -> bool:
|
|
275
|
+
"""Rename channel (unsupported by default)."""
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
async def rename_device(self, *, rega_id: int, new_name: str) -> bool:
|
|
279
|
+
"""Rename device (unsupported by default)."""
|
|
280
|
+
return False
|
|
281
|
+
|
|
282
|
+
async def report_value_usage(self, *, address: str, value_id: str, ref_counter: int) -> bool:
|
|
283
|
+
"""Report value usage (unsupported by default)."""
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
def reset_circuit_breakers(self) -> None:
|
|
287
|
+
"""Reset all circuit breakers to closed state."""
|
|
288
|
+
|
|
289
|
+
async def set_install_mode(
|
|
290
|
+
self,
|
|
291
|
+
*,
|
|
292
|
+
on: bool = True,
|
|
293
|
+
time: int = 60,
|
|
294
|
+
mode: int = 1,
|
|
295
|
+
device_address: str | None = None,
|
|
296
|
+
) -> bool:
|
|
297
|
+
"""Set install mode (unsupported by default)."""
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
async def set_metadata(self, *, address: str, data_id: str, value: dict[str, Any]) -> dict[str, Any]:
|
|
301
|
+
"""Set metadata (unsupported by default)."""
|
|
302
|
+
return {}
|
|
303
|
+
|
|
304
|
+
async def set_program_state(self, *, pid: str, state: bool) -> bool:
|
|
305
|
+
"""Set program state (unsupported by default)."""
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
async def set_system_variable(self, *, name: str, value: Any) -> bool:
|
|
309
|
+
"""Set system variable (unsupported by default)."""
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
@abstractmethod
|
|
313
|
+
async def set_value(
|
|
314
|
+
self,
|
|
315
|
+
*,
|
|
316
|
+
address: str,
|
|
317
|
+
parameter: str,
|
|
318
|
+
value: Any,
|
|
319
|
+
rx_mode: CommandRxMode | None = None,
|
|
320
|
+
) -> None:
|
|
321
|
+
"""Set parameter value."""
|
|
322
|
+
...
|
|
323
|
+
|
|
324
|
+
@abstractmethod
|
|
325
|
+
async def stop(self) -> None:
|
|
326
|
+
"""Stop the backend."""
|
|
327
|
+
...
|
|
328
|
+
|
|
329
|
+
async def trigger_firmware_update(self) -> bool:
|
|
330
|
+
"""Trigger system firmware update (unsupported by default)."""
|
|
331
|
+
return False
|
|
332
|
+
|
|
333
|
+
async def update_device_firmware(self, *, device_address: str) -> bool:
|
|
334
|
+
"""Update device firmware (unsupported by default)."""
|
|
335
|
+
return False
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Backend capabilities dataclass.
|
|
5
|
+
|
|
6
|
+
Consolidates all capability flags into a single immutable structure,
|
|
7
|
+
replacing the 20+ properties spread across client classes.
|
|
8
|
+
|
|
9
|
+
Public API
|
|
10
|
+
----------
|
|
11
|
+
- BackendCapabilities: Frozen dataclass with capability flags
|
|
12
|
+
- CCU_CAPABILITIES: Default capabilities for CCU backend
|
|
13
|
+
- JSON_CCU_CAPABILITIES: Default capabilities for CCU-Jack backend
|
|
14
|
+
- HOMEGEAR_CAPABILITIES: Default capabilities for Homegear backend
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Final
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"BackendCapabilities",
|
|
24
|
+
"CCU_CAPABILITIES",
|
|
25
|
+
"HOMEGEAR_CAPABILITIES",
|
|
26
|
+
"JSON_CCU_CAPABILITIES",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class BackendCapabilities:
|
|
32
|
+
"""
|
|
33
|
+
Immutable capability flags for a backend.
|
|
34
|
+
|
|
35
|
+
Consolidates the capability properties from ClientCCU, ClientJsonCCU,
|
|
36
|
+
and ClientHomegear into a single dataclass. Backends declare their
|
|
37
|
+
capabilities at initialization; clients expose them via the
|
|
38
|
+
`capabilities` property.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
# Device Operations
|
|
42
|
+
device_firmware_update: bool = False
|
|
43
|
+
firmware_update_trigger: bool = False
|
|
44
|
+
firmware_updates: bool = False
|
|
45
|
+
linking: bool = False
|
|
46
|
+
value_usage_reporting: bool = False
|
|
47
|
+
|
|
48
|
+
# Metadata Operations
|
|
49
|
+
functions: bool = False
|
|
50
|
+
rooms: bool = False
|
|
51
|
+
metadata: bool = False
|
|
52
|
+
rename: bool = False
|
|
53
|
+
rega_id_lookup: bool = False
|
|
54
|
+
service_messages: bool = False
|
|
55
|
+
system_update_info: bool = False
|
|
56
|
+
inbox_devices: bool = False
|
|
57
|
+
install_mode: bool = False
|
|
58
|
+
|
|
59
|
+
# Programs & System Variables
|
|
60
|
+
programs: bool = False
|
|
61
|
+
|
|
62
|
+
# Backup
|
|
63
|
+
backup: bool = False
|
|
64
|
+
|
|
65
|
+
# Connection
|
|
66
|
+
ping_pong: bool = False
|
|
67
|
+
push_updates: bool = True
|
|
68
|
+
rpc_callback: bool = True
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Default capability sets for each backend type.
|
|
72
|
+
# These can be adjusted at runtime based on interface type or system info.
|
|
73
|
+
|
|
74
|
+
CCU_CAPABILITIES: Final = BackendCapabilities(
|
|
75
|
+
device_firmware_update=True,
|
|
76
|
+
firmware_update_trigger=True,
|
|
77
|
+
firmware_updates=True,
|
|
78
|
+
linking=True,
|
|
79
|
+
value_usage_reporting=True,
|
|
80
|
+
functions=True,
|
|
81
|
+
rooms=True,
|
|
82
|
+
metadata=True,
|
|
83
|
+
rename=True,
|
|
84
|
+
rega_id_lookup=True,
|
|
85
|
+
service_messages=True,
|
|
86
|
+
system_update_info=True,
|
|
87
|
+
inbox_devices=True,
|
|
88
|
+
install_mode=True,
|
|
89
|
+
programs=True,
|
|
90
|
+
backup=True,
|
|
91
|
+
ping_pong=True,
|
|
92
|
+
push_updates=True,
|
|
93
|
+
rpc_callback=True,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
JSON_CCU_CAPABILITIES: Final = BackendCapabilities(
|
|
97
|
+
device_firmware_update=False,
|
|
98
|
+
firmware_update_trigger=False,
|
|
99
|
+
firmware_updates=False,
|
|
100
|
+
linking=False,
|
|
101
|
+
value_usage_reporting=False,
|
|
102
|
+
functions=False,
|
|
103
|
+
rooms=False,
|
|
104
|
+
metadata=False,
|
|
105
|
+
rename=False,
|
|
106
|
+
rega_id_lookup=False,
|
|
107
|
+
service_messages=False,
|
|
108
|
+
system_update_info=False,
|
|
109
|
+
inbox_devices=False,
|
|
110
|
+
install_mode=False,
|
|
111
|
+
programs=False,
|
|
112
|
+
backup=False,
|
|
113
|
+
ping_pong=False,
|
|
114
|
+
push_updates=True,
|
|
115
|
+
rpc_callback=False,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
HOMEGEAR_CAPABILITIES: Final = BackendCapabilities(
|
|
119
|
+
device_firmware_update=False,
|
|
120
|
+
firmware_update_trigger=False,
|
|
121
|
+
firmware_updates=False,
|
|
122
|
+
linking=False,
|
|
123
|
+
value_usage_reporting=False,
|
|
124
|
+
functions=False,
|
|
125
|
+
rooms=False,
|
|
126
|
+
metadata=False,
|
|
127
|
+
rename=False,
|
|
128
|
+
rega_id_lookup=False,
|
|
129
|
+
service_messages=False,
|
|
130
|
+
system_update_info=False,
|
|
131
|
+
inbox_devices=False,
|
|
132
|
+
install_mode=False,
|
|
133
|
+
programs=False,
|
|
134
|
+
backup=False,
|
|
135
|
+
ping_pong=False,
|
|
136
|
+
push_updates=True,
|
|
137
|
+
rpc_callback=True,
|
|
138
|
+
)
|