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,390 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Common Decorators used within aiohomematic.
|
|
5
|
+
|
|
6
|
+
Public API of this module is defined by __all__.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from functools import wraps
|
|
13
|
+
import inspect
|
|
14
|
+
import logging
|
|
15
|
+
from time import monotonic
|
|
16
|
+
from typing import Any, Final, cast, overload
|
|
17
|
+
from weakref import WeakKeyDictionary
|
|
18
|
+
|
|
19
|
+
from aiohomematic.const import ServiceScope
|
|
20
|
+
from aiohomematic.context import RequestContext, is_in_service, reset_request_context, set_request_context
|
|
21
|
+
from aiohomematic.exceptions import BaseHomematicException
|
|
22
|
+
from aiohomematic.metrics import MetricKeys, emit_counter, emit_latency
|
|
23
|
+
from aiohomematic.support import LogContextMixin, log_boundary_error
|
|
24
|
+
from aiohomematic.type_aliases import CallableAny, ServiceMethodMap
|
|
25
|
+
|
|
26
|
+
_LOGGER_PERFORMANCE: Final = logging.getLogger(f"{__package__}.performance")
|
|
27
|
+
|
|
28
|
+
# Cache for per-class service call method names to avoid repeated scans.
|
|
29
|
+
# Structure: {cls: (method_name1, method_name2, ...)}
|
|
30
|
+
_SERVICE_CALLS_CACHE: WeakKeyDictionary[type, tuple[str, ...]] = WeakKeyDictionary()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@overload
|
|
34
|
+
def inspector[**P, R]( # kwonly: disable
|
|
35
|
+
func: Callable[P, R],
|
|
36
|
+
/,
|
|
37
|
+
*,
|
|
38
|
+
log_level: int = ...,
|
|
39
|
+
re_raise: bool = ...,
|
|
40
|
+
no_raise_return: Any = ...,
|
|
41
|
+
measure_performance: bool = ...,
|
|
42
|
+
scope: ServiceScope = ...,
|
|
43
|
+
) -> Callable[P, R]: ...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@overload
|
|
47
|
+
def inspector[**P, R]( # kwonly: disable
|
|
48
|
+
func: None = ...,
|
|
49
|
+
/,
|
|
50
|
+
*,
|
|
51
|
+
log_level: int = ...,
|
|
52
|
+
re_raise: bool = ...,
|
|
53
|
+
no_raise_return: Any = ...,
|
|
54
|
+
measure_performance: bool = ...,
|
|
55
|
+
scope: ServiceScope = ...,
|
|
56
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def inspector[**P, R]( # noqa: C901, kwonly: disable
|
|
60
|
+
func: Callable[P, R] | None = None,
|
|
61
|
+
/,
|
|
62
|
+
*,
|
|
63
|
+
log_level: int = logging.ERROR,
|
|
64
|
+
re_raise: bool = True,
|
|
65
|
+
no_raise_return: Any = None,
|
|
66
|
+
measure_performance: bool = False,
|
|
67
|
+
scope: ServiceScope = ServiceScope.EXTERNAL,
|
|
68
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]] | Callable[P, R]:
|
|
69
|
+
"""
|
|
70
|
+
Support with exception handling and performance measurement.
|
|
71
|
+
|
|
72
|
+
A decorator that works for both synchronous and asynchronous functions,
|
|
73
|
+
providing common functionality such as exception handling and performance measurement.
|
|
74
|
+
|
|
75
|
+
Can be used both with and without parameters:
|
|
76
|
+
- @inspector
|
|
77
|
+
- @inspector(log_level=logging.ERROR, re_raise=True, ...)
|
|
78
|
+
- @inspector(scope=ServiceScope.INTERNAL)
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
func: The function to decorate when used without parameters.
|
|
82
|
+
log_level: Logging level for exceptions.
|
|
83
|
+
re_raise: Whether to re-raise exceptions.
|
|
84
|
+
no_raise_return: Value to return when an exception is caught and not re-raised.
|
|
85
|
+
measure_performance: Whether to measure function execution time.
|
|
86
|
+
scope: The scope of this service method (see ServiceScope enum).
|
|
87
|
+
EXTERNAL: Methods for external consumers (HA) - user-invokable commands
|
|
88
|
+
like turn_on, turn_off, set_temperature. Appears in service_method_names.
|
|
89
|
+
INTERNAL: Infrastructure methods for library operation like
|
|
90
|
+
load_data_point_value, fetch_*_data. Does NOT appear in service_method_names.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Either the decorated function (when used without parameters) or
|
|
94
|
+
a decorator that wraps sync or async functions (when used with parameters).
|
|
95
|
+
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def create_wrapped_decorator(func: Callable[P, R]) -> Callable[P, R]: # noqa: C901
|
|
99
|
+
"""
|
|
100
|
+
Decorate function for wrapping sync or async functions.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
func: The function to decorate.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
The decorated function.
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def handle_exception(
|
|
111
|
+
exc: Exception,
|
|
112
|
+
func: CallableAny,
|
|
113
|
+
is_sub_service_call: bool,
|
|
114
|
+
is_homematic: bool,
|
|
115
|
+
context_obj: Any | None,
|
|
116
|
+
) -> R:
|
|
117
|
+
"""Handle exceptions for decorated functions with structured logging."""
|
|
118
|
+
if not is_sub_service_call and log_level > logging.NOTSET:
|
|
119
|
+
logger = logging.getLogger(func.__module__)
|
|
120
|
+
log_context = context_obj.log_context if isinstance(context_obj, LogContextMixin) else None
|
|
121
|
+
# Reuse centralized boundary logging to ensure consistent 'extra' structure
|
|
122
|
+
log_boundary_error(
|
|
123
|
+
logger=logger,
|
|
124
|
+
boundary="service",
|
|
125
|
+
action=func.__name__,
|
|
126
|
+
err=exc,
|
|
127
|
+
level=log_level,
|
|
128
|
+
log_context=log_context,
|
|
129
|
+
)
|
|
130
|
+
if re_raise or not is_homematic:
|
|
131
|
+
raise exc
|
|
132
|
+
return cast(R, no_raise_return)
|
|
133
|
+
|
|
134
|
+
@wraps(func)
|
|
135
|
+
def wrap_sync_function(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
136
|
+
"""Wrap sync functions with minimized per-call overhead."""
|
|
137
|
+
# Start timing if measure_performance is enabled (for metrics and/or logging)
|
|
138
|
+
start = monotonic() if measure_performance else None
|
|
139
|
+
had_error = False
|
|
140
|
+
|
|
141
|
+
# Check if already in a service call context
|
|
142
|
+
in_service = is_in_service()
|
|
143
|
+
token = None
|
|
144
|
+
if not in_service:
|
|
145
|
+
# Create new request context for this service call
|
|
146
|
+
ctx = RequestContext(operation=f"service:{func.__name__}")
|
|
147
|
+
token = set_request_context(ctx=ctx)
|
|
148
|
+
context_obj = args[0] if args else None
|
|
149
|
+
try:
|
|
150
|
+
return_value: R = func(*args, **kwargs)
|
|
151
|
+
except BaseHomematicException as bhexc:
|
|
152
|
+
had_error = True
|
|
153
|
+
if token is not None:
|
|
154
|
+
reset_request_context(token=token)
|
|
155
|
+
return handle_exception(
|
|
156
|
+
exc=bhexc,
|
|
157
|
+
func=func,
|
|
158
|
+
is_sub_service_call=in_service,
|
|
159
|
+
is_homematic=True,
|
|
160
|
+
context_obj=context_obj,
|
|
161
|
+
)
|
|
162
|
+
except Exception as exc:
|
|
163
|
+
had_error = True
|
|
164
|
+
if token is not None:
|
|
165
|
+
reset_request_context(token=token)
|
|
166
|
+
return handle_exception(
|
|
167
|
+
exc=exc,
|
|
168
|
+
func=func,
|
|
169
|
+
is_sub_service_call=in_service,
|
|
170
|
+
is_homematic=False,
|
|
171
|
+
context_obj=context_obj,
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
if token is not None:
|
|
175
|
+
reset_request_context(token=token)
|
|
176
|
+
return return_value
|
|
177
|
+
finally:
|
|
178
|
+
if start is not None:
|
|
179
|
+
duration_ms = (monotonic() - start) * 1000
|
|
180
|
+
# Emit service call metrics if event_bus is available
|
|
181
|
+
_emit_service_metrics(
|
|
182
|
+
context_obj=context_obj,
|
|
183
|
+
method_name=func.__name__,
|
|
184
|
+
duration_ms=duration_ms,
|
|
185
|
+
had_error=had_error,
|
|
186
|
+
)
|
|
187
|
+
# Log performance if debug logging is enabled
|
|
188
|
+
if _LOGGER_PERFORMANCE.isEnabledFor(level=logging.DEBUG):
|
|
189
|
+
_log_performance_message(func, start, *args, **kwargs)
|
|
190
|
+
|
|
191
|
+
@wraps(func)
|
|
192
|
+
async def wrap_async_function(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
193
|
+
"""Wrap async functions with minimized per-call overhead."""
|
|
194
|
+
# Start timing if measure_performance is enabled (for metrics and/or logging)
|
|
195
|
+
start = monotonic() if measure_performance else None
|
|
196
|
+
had_error = False
|
|
197
|
+
|
|
198
|
+
# Check if already in a service call context
|
|
199
|
+
in_service = is_in_service()
|
|
200
|
+
token = None
|
|
201
|
+
if not in_service:
|
|
202
|
+
# Create new request context for this service call
|
|
203
|
+
ctx = RequestContext(operation=f"service:{func.__name__}")
|
|
204
|
+
token = set_request_context(ctx=ctx)
|
|
205
|
+
context_obj = args[0] if args else None
|
|
206
|
+
try:
|
|
207
|
+
return_value = await func(*args, **kwargs) # type: ignore[misc]
|
|
208
|
+
except BaseHomematicException as bhexc:
|
|
209
|
+
had_error = True
|
|
210
|
+
if token is not None:
|
|
211
|
+
reset_request_context(token=token)
|
|
212
|
+
return handle_exception(
|
|
213
|
+
exc=bhexc,
|
|
214
|
+
func=func,
|
|
215
|
+
is_sub_service_call=in_service,
|
|
216
|
+
is_homematic=True,
|
|
217
|
+
context_obj=context_obj,
|
|
218
|
+
)
|
|
219
|
+
except Exception as exc:
|
|
220
|
+
had_error = True
|
|
221
|
+
if token is not None:
|
|
222
|
+
reset_request_context(token=token)
|
|
223
|
+
return handle_exception(
|
|
224
|
+
exc=exc,
|
|
225
|
+
func=func,
|
|
226
|
+
is_sub_service_call=in_service,
|
|
227
|
+
is_homematic=False,
|
|
228
|
+
context_obj=context_obj,
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
if token is not None:
|
|
232
|
+
reset_request_context(token=token)
|
|
233
|
+
return cast(R, return_value)
|
|
234
|
+
finally:
|
|
235
|
+
if start is not None:
|
|
236
|
+
duration_ms = (monotonic() - start) * 1000
|
|
237
|
+
# Emit service call metrics if event_bus is available
|
|
238
|
+
_emit_service_metrics(
|
|
239
|
+
context_obj=context_obj,
|
|
240
|
+
method_name=func.__name__,
|
|
241
|
+
duration_ms=duration_ms,
|
|
242
|
+
had_error=had_error,
|
|
243
|
+
)
|
|
244
|
+
# Log performance if debug logging is enabled
|
|
245
|
+
if _LOGGER_PERFORMANCE.isEnabledFor(level=logging.DEBUG):
|
|
246
|
+
_log_performance_message(func, start, *args, **kwargs)
|
|
247
|
+
|
|
248
|
+
# Check if the function is a coroutine or not and select the appropriate wrapper
|
|
249
|
+
is_external = scope == ServiceScope.EXTERNAL
|
|
250
|
+
if inspect.iscoroutinefunction(func):
|
|
251
|
+
if is_external:
|
|
252
|
+
setattr(wrap_async_function, "lib_service", True)
|
|
253
|
+
return wrap_async_function # type: ignore[return-value]
|
|
254
|
+
if is_external:
|
|
255
|
+
setattr(wrap_sync_function, "lib_service", True)
|
|
256
|
+
return wrap_sync_function
|
|
257
|
+
|
|
258
|
+
# If used without parameters: @inspector
|
|
259
|
+
if func is not None:
|
|
260
|
+
return create_wrapped_decorator(func)
|
|
261
|
+
|
|
262
|
+
# If used with parameters: @inspector(...)
|
|
263
|
+
return create_wrapped_decorator
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _emit_service_metrics(
|
|
267
|
+
*,
|
|
268
|
+
context_obj: Any,
|
|
269
|
+
method_name: str,
|
|
270
|
+
duration_ms: float,
|
|
271
|
+
had_error: bool,
|
|
272
|
+
) -> None:
|
|
273
|
+
"""
|
|
274
|
+
Emit service call metrics via EventBus if available.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
context_obj: The object the method was called on (first arg)
|
|
278
|
+
method_name: Name of the service method
|
|
279
|
+
duration_ms: Execution duration in milliseconds
|
|
280
|
+
had_error: Whether the call raised an exception
|
|
281
|
+
|
|
282
|
+
"""
|
|
283
|
+
# Get event_bus from context object if available
|
|
284
|
+
if (event_bus_provider := getattr(context_obj, "_event_bus_provider", None)) is None:
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
if (event_bus := getattr(event_bus_provider, "event_bus", None)) is None:
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
# Emit latency for all calls
|
|
291
|
+
emit_latency(
|
|
292
|
+
event_bus=event_bus,
|
|
293
|
+
key=MetricKeys.service_call(method=method_name),
|
|
294
|
+
duration_ms=duration_ms,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Emit error counter if there was an error
|
|
298
|
+
if had_error:
|
|
299
|
+
emit_counter(
|
|
300
|
+
event_bus=event_bus,
|
|
301
|
+
key=MetricKeys.service_error(method=method_name),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _log_performance_message[**P](func: Callable[P, Any], start: float, *args: P.args, **kwargs: P.kwargs) -> None:
|
|
306
|
+
delta = monotonic() - start
|
|
307
|
+
caller = str(args[0]) if len(args) > 0 else ""
|
|
308
|
+
|
|
309
|
+
iface: str = ""
|
|
310
|
+
if interface := str(kwargs.get("interface", "")):
|
|
311
|
+
iface = f"interface: {interface}"
|
|
312
|
+
if interface_id := kwargs.get("interface_id", ""):
|
|
313
|
+
iface = f"interface_id: {interface_id}"
|
|
314
|
+
|
|
315
|
+
message = f"Execution of {func.__name__.upper()} took {delta}s from {caller}"
|
|
316
|
+
if iface:
|
|
317
|
+
message += f"/{iface}"
|
|
318
|
+
|
|
319
|
+
_LOGGER_PERFORMANCE.info(message)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def get_service_calls(*, obj: object) -> ServiceMethodMap:
|
|
323
|
+
"""
|
|
324
|
+
Get all methods decorated with the service decorator (lib_service attribute).
|
|
325
|
+
|
|
326
|
+
To reduce overhead, we cache the discovered method names per class using a WeakKeyDictionary.
|
|
327
|
+
"""
|
|
328
|
+
cls = obj.__class__
|
|
329
|
+
|
|
330
|
+
# Try cache first
|
|
331
|
+
if (names := _SERVICE_CALLS_CACHE.get(cls)) is None:
|
|
332
|
+
# Compute method names using class attributes to avoid creating bound methods during checks
|
|
333
|
+
exclusions = {"service_methods", "service_method_names"}
|
|
334
|
+
computed: list[str] = []
|
|
335
|
+
for name in dir(cls):
|
|
336
|
+
if name.startswith("_") or name in exclusions:
|
|
337
|
+
continue
|
|
338
|
+
try:
|
|
339
|
+
# Check the attribute on the class (function/descriptor)
|
|
340
|
+
attr = getattr(cls, name)
|
|
341
|
+
except Exception:
|
|
342
|
+
continue
|
|
343
|
+
# Only consider callables exposed on the instance and marked with lib_service on the function/wrapper
|
|
344
|
+
if callable(getattr(obj, name, None)) and hasattr(attr, "lib_service"):
|
|
345
|
+
computed.append(name)
|
|
346
|
+
names = tuple(computed)
|
|
347
|
+
_SERVICE_CALLS_CACHE[cls] = names
|
|
348
|
+
|
|
349
|
+
# Return a mapping of bound methods for this instance
|
|
350
|
+
return {name: getattr(obj, name) for name in names}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def measure_execution_time[CallableT: CallableAny](func: CallableT) -> CallableT: # kwonly: disable
|
|
354
|
+
"""Decorate function to measure the function execution time."""
|
|
355
|
+
|
|
356
|
+
@wraps(func)
|
|
357
|
+
async def async_measure_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
358
|
+
"""Wrap method."""
|
|
359
|
+
start = monotonic() if _LOGGER_PERFORMANCE.isEnabledFor(level=logging.DEBUG) else None
|
|
360
|
+
try:
|
|
361
|
+
return await func(*args, **kwargs)
|
|
362
|
+
finally:
|
|
363
|
+
if start:
|
|
364
|
+
_log_performance_message(func, start, *args, **kwargs)
|
|
365
|
+
|
|
366
|
+
@wraps(func)
|
|
367
|
+
def measure_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
368
|
+
"""Wrap method."""
|
|
369
|
+
start = monotonic() if _LOGGER_PERFORMANCE.isEnabledFor(level=logging.DEBUG) else None
|
|
370
|
+
try:
|
|
371
|
+
return func(*args, **kwargs)
|
|
372
|
+
finally:
|
|
373
|
+
if start:
|
|
374
|
+
_log_performance_message(func, start, *args, **kwargs)
|
|
375
|
+
|
|
376
|
+
if inspect.iscoroutinefunction(func):
|
|
377
|
+
return cast(CallableT, async_measure_wrapper)
|
|
378
|
+
return cast(CallableT, measure_wrapper)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# Define public API for this module
|
|
382
|
+
__all__ = tuple(
|
|
383
|
+
sorted(
|
|
384
|
+
name
|
|
385
|
+
for name, obj in globals().items()
|
|
386
|
+
if not name.startswith("_")
|
|
387
|
+
and (inspect.isfunction(obj) or inspect.isclass(obj))
|
|
388
|
+
and getattr(obj, "__module__", __name__) == __name__
|
|
389
|
+
)
|
|
390
|
+
)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Module for AioHomematicExceptions.
|
|
5
|
+
|
|
6
|
+
Public API of this module is defined by __all__.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Awaitable, Callable
|
|
12
|
+
from functools import wraps
|
|
13
|
+
import inspect
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Any, Final, cast
|
|
16
|
+
|
|
17
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseHomematicException(Exception):
|
|
21
|
+
"""aiohomematic base exception."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, name: str, *args: Any) -> None:
|
|
24
|
+
"""Initialize the AioHomematicException."""
|
|
25
|
+
if args and isinstance(args[0], BaseException):
|
|
26
|
+
self.name = args[0].__class__.__name__
|
|
27
|
+
args = _reduce_args(args=args[0].args)
|
|
28
|
+
else:
|
|
29
|
+
self.name = name
|
|
30
|
+
super().__init__(_reduce_args(args=args))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ClientException(BaseHomematicException):
|
|
34
|
+
"""aiohomematic Client exception."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, *args: Any) -> None:
|
|
37
|
+
"""Initialize the ClientException."""
|
|
38
|
+
super().__init__("ClientException", *args)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class UnsupportedException(BaseHomematicException):
|
|
42
|
+
"""aiohomematic unsupported exception."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, *args: Any) -> None:
|
|
45
|
+
"""Initialize the UnsupportedException."""
|
|
46
|
+
super().__init__("UnsupportedException", *args)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ValidationException(BaseHomematicException):
|
|
50
|
+
"""aiohomematic validation exception."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, *args: Any) -> None:
|
|
53
|
+
"""Initialize the ValidationException."""
|
|
54
|
+
super().__init__("ValidationException", *args)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class NoConnectionException(BaseHomematicException):
|
|
58
|
+
"""aiohomematic NoConnectionException exception."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, *args: Any) -> None:
|
|
61
|
+
"""Initialize the NoConnection."""
|
|
62
|
+
super().__init__("NoConnectionException", *args)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CircuitBreakerOpenException(BaseHomematicException):
|
|
66
|
+
"""
|
|
67
|
+
Exception raised when the circuit breaker is open.
|
|
68
|
+
|
|
69
|
+
This exception is NOT retryable because the circuit breaker has its own
|
|
70
|
+
recovery mechanism (transitions to HALF_OPEN after recovery_timeout).
|
|
71
|
+
Retrying immediately would just waste resources.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, *args: Any) -> None:
|
|
75
|
+
"""Initialize the CircuitBreakerOpenException."""
|
|
76
|
+
super().__init__("CircuitBreakerOpenException", *args)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class NoClientsException(BaseHomematicException):
|
|
80
|
+
"""aiohomematic NoClientsException exception."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, *args: Any) -> None:
|
|
83
|
+
"""Initialize the NoClientsException."""
|
|
84
|
+
super().__init__("NoClientsException", *args)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AuthFailure(BaseHomematicException):
|
|
88
|
+
"""aiohomematic AuthFailure exception."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, *args: Any) -> None:
|
|
91
|
+
"""Initialize the AuthFailure."""
|
|
92
|
+
super().__init__("AuthFailure", *args)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AioHomematicException(BaseHomematicException):
|
|
96
|
+
"""aiohomematic AioHomematicException exception."""
|
|
97
|
+
|
|
98
|
+
def __init__(self, *args: Any) -> None:
|
|
99
|
+
"""Initialize the AioHomematicException."""
|
|
100
|
+
super().__init__("AioHomematicException", *args)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AioHomematicConfigException(BaseHomematicException):
|
|
104
|
+
"""aiohomematic AioHomematicConfigException exception."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, *args: Any) -> None:
|
|
107
|
+
"""Initialize the AioHomematicConfigException."""
|
|
108
|
+
super().__init__("AioHomematicConfigException", *args)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class InternalBackendException(BaseHomematicException):
|
|
112
|
+
"""aiohomematic InternalBackendException exception."""
|
|
113
|
+
|
|
114
|
+
def __init__(self, *args: Any) -> None:
|
|
115
|
+
"""Initialize the InternalBackendException."""
|
|
116
|
+
super().__init__("InternalBackendException", *args)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class DescriptionNotFoundException(BaseHomematicException):
|
|
120
|
+
"""Exception raised when a device/channel description is not found in the cache."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, *args: Any) -> None:
|
|
123
|
+
"""Initialize the DescriptionNotFoundException."""
|
|
124
|
+
super().__init__("DescriptionNotFoundException", *args)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _reduce_args(*, args: tuple[Any, ...]) -> tuple[Any, ...] | Any:
|
|
128
|
+
"""Return the first arg, if there is only one arg."""
|
|
129
|
+
return args[0] if len(args) == 1 else args
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def log_exception[**P, R](
|
|
133
|
+
*,
|
|
134
|
+
exc_type: type[BaseException],
|
|
135
|
+
logger: logging.Logger = _LOGGER,
|
|
136
|
+
level: int = logging.ERROR,
|
|
137
|
+
extra_msg: str = "",
|
|
138
|
+
re_raise: bool = False,
|
|
139
|
+
exc_return: Any = None,
|
|
140
|
+
) -> Callable[[Callable[P, R | Awaitable[R]]], Callable[P, R | Awaitable[R]]]:
|
|
141
|
+
"""Decorate methods for exception logging."""
|
|
142
|
+
|
|
143
|
+
def decorator_log_exception(
|
|
144
|
+
func: Callable[P, R | Awaitable[R]],
|
|
145
|
+
) -> Callable[P, R | Awaitable[R]]:
|
|
146
|
+
"""Decorate log exception method."""
|
|
147
|
+
function_name = func.__name__
|
|
148
|
+
|
|
149
|
+
@wraps(func)
|
|
150
|
+
async def async_wrapper_log_exception(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
151
|
+
"""Wrap async methods."""
|
|
152
|
+
try:
|
|
153
|
+
return_value = cast(R, await func(*args, **kwargs)) # type: ignore[misc]
|
|
154
|
+
except exc_type as exc:
|
|
155
|
+
message = (
|
|
156
|
+
f"{function_name.upper()} failed: {exc_type.__name__} [{_reduce_args(args=exc.args)}] {extra_msg}"
|
|
157
|
+
)
|
|
158
|
+
logger.log(level, message)
|
|
159
|
+
if re_raise:
|
|
160
|
+
raise
|
|
161
|
+
return cast(R, exc_return)
|
|
162
|
+
return return_value
|
|
163
|
+
|
|
164
|
+
@wraps(func)
|
|
165
|
+
def wrapper_log_exception(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
166
|
+
"""Wrap sync methods."""
|
|
167
|
+
return cast(R, func(*args, **kwargs))
|
|
168
|
+
|
|
169
|
+
if inspect.iscoroutinefunction(func):
|
|
170
|
+
return async_wrapper_log_exception
|
|
171
|
+
return wrapper_log_exception
|
|
172
|
+
|
|
173
|
+
return decorator_log_exception
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# Define public API for this module
|
|
177
|
+
__all__ = tuple(
|
|
178
|
+
sorted(
|
|
179
|
+
name
|
|
180
|
+
for name, obj in globals().items()
|
|
181
|
+
if not name.startswith("_")
|
|
182
|
+
and (name.isupper() or inspect.isclass(obj) or inspect.isfunction(obj))
|
|
183
|
+
and getattr(obj, "__module__", __name__) == __name__
|
|
184
|
+
)
|
|
185
|
+
)
|