aiohomematic 2025.11.3__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.
Potentially problematic release.
This version of aiohomematic might be problematic. Click here for more details.
- aiohomematic/__init__.py +61 -0
- aiohomematic/async_support.py +212 -0
- aiohomematic/central/__init__.py +2309 -0
- aiohomematic/central/decorators.py +155 -0
- aiohomematic/central/rpc_server.py +295 -0
- aiohomematic/client/__init__.py +1848 -0
- aiohomematic/client/_rpc_errors.py +81 -0
- aiohomematic/client/json_rpc.py +1326 -0
- aiohomematic/client/rpc_proxy.py +311 -0
- aiohomematic/const.py +1127 -0
- aiohomematic/context.py +18 -0
- aiohomematic/converter.py +108 -0
- aiohomematic/decorators.py +302 -0
- aiohomematic/exceptions.py +164 -0
- aiohomematic/hmcli.py +186 -0
- aiohomematic/model/__init__.py +140 -0
- aiohomematic/model/calculated/__init__.py +84 -0
- aiohomematic/model/calculated/climate.py +290 -0
- aiohomematic/model/calculated/data_point.py +327 -0
- aiohomematic/model/calculated/operating_voltage_level.py +299 -0
- aiohomematic/model/calculated/support.py +234 -0
- aiohomematic/model/custom/__init__.py +177 -0
- aiohomematic/model/custom/climate.py +1532 -0
- aiohomematic/model/custom/cover.py +792 -0
- aiohomematic/model/custom/data_point.py +334 -0
- aiohomematic/model/custom/definition.py +871 -0
- aiohomematic/model/custom/light.py +1128 -0
- aiohomematic/model/custom/lock.py +394 -0
- aiohomematic/model/custom/siren.py +275 -0
- aiohomematic/model/custom/support.py +41 -0
- aiohomematic/model/custom/switch.py +175 -0
- aiohomematic/model/custom/valve.py +114 -0
- aiohomematic/model/data_point.py +1123 -0
- aiohomematic/model/device.py +1445 -0
- aiohomematic/model/event.py +208 -0
- aiohomematic/model/generic/__init__.py +217 -0
- aiohomematic/model/generic/action.py +34 -0
- aiohomematic/model/generic/binary_sensor.py +30 -0
- aiohomematic/model/generic/button.py +27 -0
- aiohomematic/model/generic/data_point.py +171 -0
- aiohomematic/model/generic/dummy.py +147 -0
- aiohomematic/model/generic/number.py +76 -0
- aiohomematic/model/generic/select.py +39 -0
- aiohomematic/model/generic/sensor.py +74 -0
- aiohomematic/model/generic/switch.py +54 -0
- aiohomematic/model/generic/text.py +29 -0
- aiohomematic/model/hub/__init__.py +333 -0
- aiohomematic/model/hub/binary_sensor.py +24 -0
- aiohomematic/model/hub/button.py +28 -0
- aiohomematic/model/hub/data_point.py +340 -0
- aiohomematic/model/hub/number.py +39 -0
- aiohomematic/model/hub/select.py +49 -0
- aiohomematic/model/hub/sensor.py +37 -0
- aiohomematic/model/hub/switch.py +44 -0
- aiohomematic/model/hub/text.py +30 -0
- aiohomematic/model/support.py +586 -0
- aiohomematic/model/update.py +143 -0
- aiohomematic/property_decorators.py +496 -0
- aiohomematic/py.typed +0 -0
- aiohomematic/rega_scripts/fetch_all_device_data.fn +92 -0
- aiohomematic/rega_scripts/get_program_descriptions.fn +30 -0
- aiohomematic/rega_scripts/get_serial.fn +44 -0
- aiohomematic/rega_scripts/get_system_variable_descriptions.fn +30 -0
- aiohomematic/rega_scripts/set_program_state.fn +12 -0
- aiohomematic/rega_scripts/set_system_variable.fn +15 -0
- aiohomematic/store/__init__.py +34 -0
- aiohomematic/store/dynamic.py +551 -0
- aiohomematic/store/persistent.py +988 -0
- aiohomematic/store/visibility.py +812 -0
- aiohomematic/support.py +664 -0
- aiohomematic/validator.py +112 -0
- aiohomematic-2025.11.3.dist-info/METADATA +144 -0
- aiohomematic-2025.11.3.dist-info/RECORD +77 -0
- aiohomematic-2025.11.3.dist-info/WHEEL +5 -0
- aiohomematic-2025.11.3.dist-info/entry_points.txt +2 -0
- aiohomematic-2025.11.3.dist-info/licenses/LICENSE +21 -0
- aiohomematic-2025.11.3.dist-info/top_level.txt +1 -0
aiohomematic/context.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
|
+
"""
|
|
4
|
+
Collection of context variables.
|
|
5
|
+
|
|
6
|
+
Public API of this module is defined by __all__.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from contextvars import ContextVar
|
|
12
|
+
|
|
13
|
+
# context var for storing if call is running within a service
|
|
14
|
+
IN_SERVICE_VAR: ContextVar[bool] = ContextVar("in_service_var", default=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Define public API for this module
|
|
18
|
+
__all__ = ["IN_SERVICE_VAR"]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
|
+
"""
|
|
4
|
+
Converters used by aiohomematic.
|
|
5
|
+
|
|
6
|
+
Public API of this module is defined by __all__.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
import inspect
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Any, Final, cast
|
|
16
|
+
|
|
17
|
+
from aiohomematic.const import Parameter
|
|
18
|
+
from aiohomematic.support import extract_exc_args
|
|
19
|
+
|
|
20
|
+
_LOGGER = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@lru_cache(maxsize=1024)
|
|
24
|
+
def _convert_cpv_to_hm_level(*, value: Any) -> Any:
|
|
25
|
+
"""Convert combined parameter value for hm level."""
|
|
26
|
+
if isinstance(value, str) and value.startswith("0x"):
|
|
27
|
+
return ast.literal_eval(value) / 100 / 2
|
|
28
|
+
return value
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@lru_cache(maxsize=1024)
|
|
32
|
+
def _convert_cpv_to_hmip_level(*, value: Any) -> Any:
|
|
33
|
+
"""Convert combined parameter value for hmip level."""
|
|
34
|
+
return int(value) / 100
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@lru_cache(maxsize=1024)
|
|
38
|
+
def convert_hm_level_to_cpv(*, value: Any) -> Any:
|
|
39
|
+
"""Convert hm level to combined parameter value."""
|
|
40
|
+
return format(int(value * 100 * 2), "#04x")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
CONVERTABLE_PARAMETERS: Final = (Parameter.COMBINED_PARAMETER, Parameter.LEVEL_COMBINED)
|
|
44
|
+
|
|
45
|
+
_COMBINED_PARAMETER_TO_HM_CONVERTER: Final = {
|
|
46
|
+
Parameter.LEVEL_COMBINED: _convert_cpv_to_hm_level,
|
|
47
|
+
Parameter.LEVEL: _convert_cpv_to_hmip_level,
|
|
48
|
+
Parameter.LEVEL_2: _convert_cpv_to_hmip_level,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_COMBINED_PARAMETER_NAMES: Final = {"L": Parameter.LEVEL, "L2": Parameter.LEVEL_2}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@lru_cache(maxsize=1024)
|
|
55
|
+
def _convert_combined_parameter_to_paramset(*, value: str) -> dict[str, Any]:
|
|
56
|
+
"""Convert combined parameter to paramset."""
|
|
57
|
+
paramset: dict[str, Any] = {}
|
|
58
|
+
for cp_param_value in value.split(","):
|
|
59
|
+
cp_param, value = cp_param_value.split("=")
|
|
60
|
+
if parameter := _COMBINED_PARAMETER_NAMES.get(cp_param):
|
|
61
|
+
if converter := _COMBINED_PARAMETER_TO_HM_CONVERTER.get(parameter):
|
|
62
|
+
paramset[parameter] = converter(value=value)
|
|
63
|
+
else:
|
|
64
|
+
paramset[parameter] = value
|
|
65
|
+
return paramset
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@lru_cache(maxsize=1024)
|
|
69
|
+
def _convert_level_combined_to_paramset(*, value: str) -> dict[str, Any]:
|
|
70
|
+
"""Convert combined parameter to paramset."""
|
|
71
|
+
if "," in value:
|
|
72
|
+
l1_value, l2_value = value.split(",")
|
|
73
|
+
if converter := _COMBINED_PARAMETER_TO_HM_CONVERTER.get(Parameter.LEVEL_COMBINED):
|
|
74
|
+
return {
|
|
75
|
+
Parameter.LEVEL: converter(value=l1_value),
|
|
76
|
+
Parameter.LEVEL_SLATS: converter(value=l2_value),
|
|
77
|
+
}
|
|
78
|
+
return {}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
_COMBINED_PARAMETER_TO_PARAMSET_CONVERTER: Final = {
|
|
82
|
+
Parameter.COMBINED_PARAMETER: _convert_combined_parameter_to_paramset,
|
|
83
|
+
Parameter.LEVEL_COMBINED: _convert_level_combined_to_paramset,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@lru_cache(maxsize=1024)
|
|
88
|
+
def convert_combined_parameter_to_paramset(*, parameter: str, value: str) -> dict[str, Any]:
|
|
89
|
+
"""Convert combined parameter to paramset."""
|
|
90
|
+
try:
|
|
91
|
+
if converter := _COMBINED_PARAMETER_TO_PARAMSET_CONVERTER.get(parameter): # type: ignore[call-overload]
|
|
92
|
+
return cast(dict[str, Any], converter(value=value))
|
|
93
|
+
_LOGGER.debug("CONVERT_COMBINED_PARAMETER_TO_PARAMSET: No converter found for %s: %s", parameter, value)
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
_LOGGER.debug("CONVERT_COMBINED_PARAMETER_TO_PARAMSET: Convert failed %s", extract_exc_args(exc=exc))
|
|
96
|
+
return {}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Define public API for this module
|
|
100
|
+
__all__ = tuple(
|
|
101
|
+
sorted(
|
|
102
|
+
name
|
|
103
|
+
for name, obj in globals().items()
|
|
104
|
+
if not name.startswith("_")
|
|
105
|
+
and (name.isupper() or inspect.isfunction(obj) or inspect.isclass(obj))
|
|
106
|
+
and getattr(obj, "__module__", __name__) == __name__
|
|
107
|
+
)
|
|
108
|
+
)
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
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.context import IN_SERVICE_VAR
|
|
20
|
+
from aiohomematic.exceptions import BaseHomematicException
|
|
21
|
+
from aiohomematic.support import LogContextMixin, log_boundary_error
|
|
22
|
+
|
|
23
|
+
_LOGGER_PERFORMANCE: Final = logging.getLogger(f"{__package__}.performance")
|
|
24
|
+
|
|
25
|
+
# Cache for per-class service call method names to avoid repeated scans.
|
|
26
|
+
# Structure: {cls: (method_name1, method_name2, ...)}
|
|
27
|
+
_SERVICE_CALLS_CACHE: WeakKeyDictionary[type, tuple[str, ...]] = WeakKeyDictionary()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@overload
|
|
31
|
+
def inspector[**P, R](
|
|
32
|
+
func: Callable[P, R],
|
|
33
|
+
/,
|
|
34
|
+
*,
|
|
35
|
+
log_level: int = ...,
|
|
36
|
+
re_raise: bool = ...,
|
|
37
|
+
no_raise_return: Any = ...,
|
|
38
|
+
measure_performance: bool = ...,
|
|
39
|
+
) -> Callable[P, R]: ...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@overload
|
|
43
|
+
def inspector[**P, R](
|
|
44
|
+
func: None = ...,
|
|
45
|
+
/,
|
|
46
|
+
*,
|
|
47
|
+
log_level: int = ...,
|
|
48
|
+
re_raise: bool = ...,
|
|
49
|
+
no_raise_return: Any = ...,
|
|
50
|
+
measure_performance: bool = ...,
|
|
51
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def inspector[**P, R]( # noqa: C901
|
|
55
|
+
func: Callable[P, R] | None = None,
|
|
56
|
+
/,
|
|
57
|
+
*,
|
|
58
|
+
log_level: int = logging.ERROR,
|
|
59
|
+
re_raise: bool = True,
|
|
60
|
+
no_raise_return: Any = None,
|
|
61
|
+
measure_performance: bool = False,
|
|
62
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]] | Callable[P, R]:
|
|
63
|
+
"""
|
|
64
|
+
Support with exception handling and performance measurement.
|
|
65
|
+
|
|
66
|
+
A decorator that works for both synchronous and asynchronous functions,
|
|
67
|
+
providing common functionality such as exception handling and performance measurement.
|
|
68
|
+
|
|
69
|
+
Can be used both with and without parameters:
|
|
70
|
+
- @inspector
|
|
71
|
+
- @inspector(log_level=logging.ERROR, re_raise=True, ...)
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
func: The function to decorate when used without parameters.
|
|
75
|
+
log_level: Logging level for exceptions.
|
|
76
|
+
re_raise: Whether to re-raise exceptions.
|
|
77
|
+
no_raise_return: Value to return when an exception is caught and not re-raised.
|
|
78
|
+
measure_performance: Whether to measure function execution time.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Either the decorated function (when used without parameters) or
|
|
82
|
+
a decorator that wraps sync or async functions (when used with parameters).
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def create_wrapped_decorator(func: Callable[P, R]) -> Callable[P, R]: # noqa: C901
|
|
87
|
+
"""
|
|
88
|
+
Decorate function for wrapping sync or async functions.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
func: The function to decorate.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The decorated function.
|
|
95
|
+
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def handle_exception(
|
|
99
|
+
exc: Exception, func: Callable, is_sub_service_call: bool, is_homematic: bool, context_obj: Any | None
|
|
100
|
+
) -> R:
|
|
101
|
+
"""Handle exceptions for decorated functions with structured logging."""
|
|
102
|
+
if not is_sub_service_call and log_level > logging.NOTSET:
|
|
103
|
+
logger = logging.getLogger(func.__module__)
|
|
104
|
+
log_context = context_obj.log_context if isinstance(context_obj, LogContextMixin) else None
|
|
105
|
+
# Reuse centralized boundary logging to ensure consistent 'extra' structure
|
|
106
|
+
log_boundary_error(
|
|
107
|
+
logger=logger,
|
|
108
|
+
boundary="service",
|
|
109
|
+
action=func.__name__,
|
|
110
|
+
err=exc,
|
|
111
|
+
level=log_level,
|
|
112
|
+
log_context=log_context,
|
|
113
|
+
)
|
|
114
|
+
if re_raise or not is_homematic:
|
|
115
|
+
raise exc
|
|
116
|
+
return cast(R, no_raise_return)
|
|
117
|
+
|
|
118
|
+
@wraps(func)
|
|
119
|
+
def wrap_sync_function(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
120
|
+
"""Wrap sync functions with minimized per-call overhead."""
|
|
121
|
+
|
|
122
|
+
# Fast-path: avoid logger check and time call unless explicitly enabled
|
|
123
|
+
start_needed = measure_performance and _LOGGER_PERFORMANCE.isEnabledFor(level=logging.DEBUG)
|
|
124
|
+
start = monotonic() if start_needed else None
|
|
125
|
+
|
|
126
|
+
# Avoid repeated ContextVar.get() calls; only set/reset when needed
|
|
127
|
+
was_in_service = IN_SERVICE_VAR.get()
|
|
128
|
+
token = IN_SERVICE_VAR.set(True) if not was_in_service else None
|
|
129
|
+
context_obj = args[0] if args else None
|
|
130
|
+
try:
|
|
131
|
+
return_value: R = func(*args, **kwargs)
|
|
132
|
+
except BaseHomematicException as bhexc:
|
|
133
|
+
if token is not None:
|
|
134
|
+
IN_SERVICE_VAR.reset(token)
|
|
135
|
+
return handle_exception(
|
|
136
|
+
exc=bhexc,
|
|
137
|
+
func=func,
|
|
138
|
+
is_sub_service_call=was_in_service,
|
|
139
|
+
is_homematic=True,
|
|
140
|
+
context_obj=context_obj,
|
|
141
|
+
)
|
|
142
|
+
except Exception as exc:
|
|
143
|
+
if token is not None:
|
|
144
|
+
IN_SERVICE_VAR.reset(token)
|
|
145
|
+
return handle_exception(
|
|
146
|
+
exc=exc,
|
|
147
|
+
func=func,
|
|
148
|
+
is_sub_service_call=was_in_service,
|
|
149
|
+
is_homematic=False,
|
|
150
|
+
context_obj=context_obj,
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
if token is not None:
|
|
154
|
+
IN_SERVICE_VAR.reset(token)
|
|
155
|
+
return return_value
|
|
156
|
+
finally:
|
|
157
|
+
if start is not None:
|
|
158
|
+
_log_performance_message(func, start, *args, **kwargs)
|
|
159
|
+
|
|
160
|
+
@wraps(func)
|
|
161
|
+
async def wrap_async_function(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
162
|
+
"""Wrap async functions with minimized per-call overhead."""
|
|
163
|
+
|
|
164
|
+
start_needed = measure_performance and _LOGGER_PERFORMANCE.isEnabledFor(level=logging.DEBUG)
|
|
165
|
+
start = monotonic() if start_needed else None
|
|
166
|
+
|
|
167
|
+
was_in_service = IN_SERVICE_VAR.get()
|
|
168
|
+
token = IN_SERVICE_VAR.set(True) if not was_in_service else None
|
|
169
|
+
context_obj = args[0] if args else None
|
|
170
|
+
try:
|
|
171
|
+
return_value = await func(*args, **kwargs) # type: ignore[misc]
|
|
172
|
+
except BaseHomematicException as bhexc:
|
|
173
|
+
if token is not None:
|
|
174
|
+
IN_SERVICE_VAR.reset(token)
|
|
175
|
+
return handle_exception(
|
|
176
|
+
exc=bhexc,
|
|
177
|
+
func=func,
|
|
178
|
+
is_sub_service_call=was_in_service,
|
|
179
|
+
is_homematic=True,
|
|
180
|
+
context_obj=context_obj,
|
|
181
|
+
)
|
|
182
|
+
except Exception as exc:
|
|
183
|
+
if token is not None:
|
|
184
|
+
IN_SERVICE_VAR.reset(token)
|
|
185
|
+
return handle_exception(
|
|
186
|
+
exc=exc,
|
|
187
|
+
func=func,
|
|
188
|
+
is_sub_service_call=was_in_service,
|
|
189
|
+
is_homematic=False,
|
|
190
|
+
context_obj=context_obj,
|
|
191
|
+
)
|
|
192
|
+
else:
|
|
193
|
+
if token is not None:
|
|
194
|
+
IN_SERVICE_VAR.reset(token)
|
|
195
|
+
return cast(R, return_value)
|
|
196
|
+
finally:
|
|
197
|
+
if start is not None:
|
|
198
|
+
_log_performance_message(func, start, *args, **kwargs)
|
|
199
|
+
|
|
200
|
+
# Check if the function is a coroutine or not and select the appropriate wrapper
|
|
201
|
+
if inspect.iscoroutinefunction(func):
|
|
202
|
+
setattr(wrap_async_function, "ha_service", True)
|
|
203
|
+
return wrap_async_function # type: ignore[return-value]
|
|
204
|
+
setattr(wrap_sync_function, "ha_service", True)
|
|
205
|
+
return wrap_sync_function
|
|
206
|
+
|
|
207
|
+
# If used without parameters: @inspector
|
|
208
|
+
if func is not None:
|
|
209
|
+
return create_wrapped_decorator(func)
|
|
210
|
+
|
|
211
|
+
# If used with parameters: @inspector(...)
|
|
212
|
+
return create_wrapped_decorator
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _log_performance_message[**P](func: Callable[P, Any], start: float, *args: P.args, **kwargs: P.kwargs) -> None:
|
|
216
|
+
delta = monotonic() - start
|
|
217
|
+
caller = str(args[0]) if len(args) > 0 else ""
|
|
218
|
+
|
|
219
|
+
iface: str = ""
|
|
220
|
+
if interface := str(kwargs.get("interface", "")):
|
|
221
|
+
iface = f"interface: {interface}"
|
|
222
|
+
if interface_id := kwargs.get("interface_id", ""):
|
|
223
|
+
iface = f"interface_id: {interface_id}"
|
|
224
|
+
|
|
225
|
+
message = f"Execution of {func.__name__.upper()} took {delta}s from {caller}"
|
|
226
|
+
if iface:
|
|
227
|
+
message += f"/{iface}"
|
|
228
|
+
|
|
229
|
+
_LOGGER_PERFORMANCE.info(message)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_service_calls(obj: object) -> dict[str, Callable]:
|
|
233
|
+
"""
|
|
234
|
+
Get all methods decorated with the service decorator (ha_service attribute).
|
|
235
|
+
|
|
236
|
+
To reduce overhead, we cache the discovered method names per class using a WeakKeyDictionary.
|
|
237
|
+
"""
|
|
238
|
+
cls = obj.__class__
|
|
239
|
+
|
|
240
|
+
# Try cache first
|
|
241
|
+
if (names := _SERVICE_CALLS_CACHE.get(cls)) is None:
|
|
242
|
+
# Compute method names using class attributes to avoid creating bound methods during checks
|
|
243
|
+
exclusions = {"service_methods", "service_method_names"}
|
|
244
|
+
computed: list[str] = []
|
|
245
|
+
for name in dir(cls):
|
|
246
|
+
if name.startswith("_") or name in exclusions:
|
|
247
|
+
continue
|
|
248
|
+
try:
|
|
249
|
+
# Check the attribute on the class (function/descriptor)
|
|
250
|
+
attr = getattr(cls, name)
|
|
251
|
+
except Exception:
|
|
252
|
+
continue
|
|
253
|
+
# Only consider callables exposed on the instance and marked with ha_service on the function/wrapper
|
|
254
|
+
if callable(getattr(obj, name, None)) and hasattr(attr, "ha_service"):
|
|
255
|
+
computed.append(name)
|
|
256
|
+
names = tuple(computed)
|
|
257
|
+
_SERVICE_CALLS_CACHE[cls] = names
|
|
258
|
+
|
|
259
|
+
# Return a mapping of bound methods for this instance
|
|
260
|
+
return {name: getattr(obj, name) for name in names}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def measure_execution_time[CallableT: Callable[..., Any]](func: CallableT) -> CallableT:
|
|
264
|
+
"""Decorate function to measure the function execution time."""
|
|
265
|
+
|
|
266
|
+
@wraps(func)
|
|
267
|
+
async def async_measure_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
268
|
+
"""Wrap method."""
|
|
269
|
+
|
|
270
|
+
start = monotonic() if _LOGGER_PERFORMANCE.isEnabledFor(level=logging.DEBUG) else None
|
|
271
|
+
try:
|
|
272
|
+
return await func(*args, **kwargs)
|
|
273
|
+
finally:
|
|
274
|
+
if start:
|
|
275
|
+
_log_performance_message(func, start, *args, **kwargs)
|
|
276
|
+
|
|
277
|
+
@wraps(func)
|
|
278
|
+
def measure_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
279
|
+
"""Wrap method."""
|
|
280
|
+
|
|
281
|
+
start = monotonic() if _LOGGER_PERFORMANCE.isEnabledFor(level=logging.DEBUG) else None
|
|
282
|
+
try:
|
|
283
|
+
return func(*args, **kwargs)
|
|
284
|
+
finally:
|
|
285
|
+
if start:
|
|
286
|
+
_log_performance_message(func, start, *args, **kwargs)
|
|
287
|
+
|
|
288
|
+
if inspect.iscoroutinefunction(func):
|
|
289
|
+
return cast(CallableT, async_measure_wrapper)
|
|
290
|
+
return cast(CallableT, measure_wrapper)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# Define public API for this module
|
|
294
|
+
__all__ = tuple(
|
|
295
|
+
sorted(
|
|
296
|
+
name
|
|
297
|
+
for name, obj in globals().items()
|
|
298
|
+
if not name.startswith("_")
|
|
299
|
+
and (inspect.isfunction(obj) or inspect.isclass(obj))
|
|
300
|
+
and getattr(obj, "__module__", __name__) == __name__
|
|
301
|
+
)
|
|
302
|
+
)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
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
|
+
"""Init 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
|
+
"""Init 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
|
+
"""Init 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
|
+
"""Init 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
|
+
"""Init the NoConnection."""
|
|
62
|
+
super().__init__("NoConnectionException", *args)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class NoClientsException(BaseHomematicException):
|
|
66
|
+
"""aiohomematic NoClientsException exception."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, *args: Any) -> None:
|
|
69
|
+
"""Init the NoClientsException."""
|
|
70
|
+
super().__init__("NoClientsException", *args)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class AuthFailure(BaseHomematicException):
|
|
74
|
+
"""aiohomematic AuthFailure exception."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, *args: Any) -> None:
|
|
77
|
+
"""Init the AuthFailure."""
|
|
78
|
+
super().__init__("AuthFailure", *args)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class AioHomematicException(BaseHomematicException):
|
|
82
|
+
"""aiohomematic AioHomematicException exception."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, *args: Any) -> None:
|
|
85
|
+
"""Init the AioHomematicException."""
|
|
86
|
+
super().__init__("AioHomematicException", *args)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AioHomematicConfigException(BaseHomematicException):
|
|
90
|
+
"""aiohomematic AioHomematicConfigException exception."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, *args: Any) -> None:
|
|
93
|
+
"""Init the AioHomematicConfigException."""
|
|
94
|
+
super().__init__("AioHomematicConfigException", *args)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class InternalBackendException(BaseHomematicException):
|
|
98
|
+
"""aiohomematic InternalBackendException exception."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, *args: Any) -> None:
|
|
101
|
+
"""Init the InternalBackendException."""
|
|
102
|
+
super().__init__("InternalBackendException", *args)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _reduce_args(*, args: tuple[Any, ...]) -> tuple[Any, ...] | Any:
|
|
106
|
+
"""Return the first arg, if there is only one arg."""
|
|
107
|
+
return args[0] if len(args) == 1 else args
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def log_exception[**P, R](
|
|
111
|
+
*,
|
|
112
|
+
exc_type: type[BaseException],
|
|
113
|
+
logger: logging.Logger = _LOGGER,
|
|
114
|
+
level: int = logging.ERROR,
|
|
115
|
+
extra_msg: str = "",
|
|
116
|
+
re_raise: bool = False,
|
|
117
|
+
exc_return: Any = None,
|
|
118
|
+
) -> Callable:
|
|
119
|
+
"""Decorate methods for exception logging."""
|
|
120
|
+
|
|
121
|
+
def decorator_log_exception(
|
|
122
|
+
func: Callable[P, R | Awaitable[R]],
|
|
123
|
+
) -> Callable[P, R | Awaitable[R]]:
|
|
124
|
+
"""Decorate log exception method."""
|
|
125
|
+
|
|
126
|
+
function_name = func.__name__
|
|
127
|
+
|
|
128
|
+
@wraps(func)
|
|
129
|
+
async def async_wrapper_log_exception(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
130
|
+
"""Wrap async methods."""
|
|
131
|
+
try:
|
|
132
|
+
return_value = cast(R, await func(*args, **kwargs)) # type: ignore[misc]
|
|
133
|
+
except exc_type as exc:
|
|
134
|
+
message = (
|
|
135
|
+
f"{function_name.upper()} failed: {exc_type.__name__} [{_reduce_args(args=exc.args)}] {extra_msg}"
|
|
136
|
+
)
|
|
137
|
+
logger.log(level, message)
|
|
138
|
+
if re_raise:
|
|
139
|
+
raise
|
|
140
|
+
return cast(R, exc_return)
|
|
141
|
+
return return_value
|
|
142
|
+
|
|
143
|
+
@wraps(func)
|
|
144
|
+
def wrapper_log_exception(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
145
|
+
"""Wrap sync methods."""
|
|
146
|
+
return cast(R, func(*args, **kwargs))
|
|
147
|
+
|
|
148
|
+
if inspect.iscoroutinefunction(func):
|
|
149
|
+
return async_wrapper_log_exception
|
|
150
|
+
return wrapper_log_exception
|
|
151
|
+
|
|
152
|
+
return decorator_log_exception
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# Define public API for this module
|
|
156
|
+
__all__ = tuple(
|
|
157
|
+
sorted(
|
|
158
|
+
name
|
|
159
|
+
for name, obj in globals().items()
|
|
160
|
+
if not name.startswith("_")
|
|
161
|
+
and (name.isupper() or inspect.isclass(obj) or inspect.isfunction(obj))
|
|
162
|
+
and getattr(obj, "__module__", __name__) == __name__
|
|
163
|
+
)
|
|
164
|
+
)
|