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.

Files changed (77) hide show
  1. aiohomematic/__init__.py +61 -0
  2. aiohomematic/async_support.py +212 -0
  3. aiohomematic/central/__init__.py +2309 -0
  4. aiohomematic/central/decorators.py +155 -0
  5. aiohomematic/central/rpc_server.py +295 -0
  6. aiohomematic/client/__init__.py +1848 -0
  7. aiohomematic/client/_rpc_errors.py +81 -0
  8. aiohomematic/client/json_rpc.py +1326 -0
  9. aiohomematic/client/rpc_proxy.py +311 -0
  10. aiohomematic/const.py +1127 -0
  11. aiohomematic/context.py +18 -0
  12. aiohomematic/converter.py +108 -0
  13. aiohomematic/decorators.py +302 -0
  14. aiohomematic/exceptions.py +164 -0
  15. aiohomematic/hmcli.py +186 -0
  16. aiohomematic/model/__init__.py +140 -0
  17. aiohomematic/model/calculated/__init__.py +84 -0
  18. aiohomematic/model/calculated/climate.py +290 -0
  19. aiohomematic/model/calculated/data_point.py +327 -0
  20. aiohomematic/model/calculated/operating_voltage_level.py +299 -0
  21. aiohomematic/model/calculated/support.py +234 -0
  22. aiohomematic/model/custom/__init__.py +177 -0
  23. aiohomematic/model/custom/climate.py +1532 -0
  24. aiohomematic/model/custom/cover.py +792 -0
  25. aiohomematic/model/custom/data_point.py +334 -0
  26. aiohomematic/model/custom/definition.py +871 -0
  27. aiohomematic/model/custom/light.py +1128 -0
  28. aiohomematic/model/custom/lock.py +394 -0
  29. aiohomematic/model/custom/siren.py +275 -0
  30. aiohomematic/model/custom/support.py +41 -0
  31. aiohomematic/model/custom/switch.py +175 -0
  32. aiohomematic/model/custom/valve.py +114 -0
  33. aiohomematic/model/data_point.py +1123 -0
  34. aiohomematic/model/device.py +1445 -0
  35. aiohomematic/model/event.py +208 -0
  36. aiohomematic/model/generic/__init__.py +217 -0
  37. aiohomematic/model/generic/action.py +34 -0
  38. aiohomematic/model/generic/binary_sensor.py +30 -0
  39. aiohomematic/model/generic/button.py +27 -0
  40. aiohomematic/model/generic/data_point.py +171 -0
  41. aiohomematic/model/generic/dummy.py +147 -0
  42. aiohomematic/model/generic/number.py +76 -0
  43. aiohomematic/model/generic/select.py +39 -0
  44. aiohomematic/model/generic/sensor.py +74 -0
  45. aiohomematic/model/generic/switch.py +54 -0
  46. aiohomematic/model/generic/text.py +29 -0
  47. aiohomematic/model/hub/__init__.py +333 -0
  48. aiohomematic/model/hub/binary_sensor.py +24 -0
  49. aiohomematic/model/hub/button.py +28 -0
  50. aiohomematic/model/hub/data_point.py +340 -0
  51. aiohomematic/model/hub/number.py +39 -0
  52. aiohomematic/model/hub/select.py +49 -0
  53. aiohomematic/model/hub/sensor.py +37 -0
  54. aiohomematic/model/hub/switch.py +44 -0
  55. aiohomematic/model/hub/text.py +30 -0
  56. aiohomematic/model/support.py +586 -0
  57. aiohomematic/model/update.py +143 -0
  58. aiohomematic/property_decorators.py +496 -0
  59. aiohomematic/py.typed +0 -0
  60. aiohomematic/rega_scripts/fetch_all_device_data.fn +92 -0
  61. aiohomematic/rega_scripts/get_program_descriptions.fn +30 -0
  62. aiohomematic/rega_scripts/get_serial.fn +44 -0
  63. aiohomematic/rega_scripts/get_system_variable_descriptions.fn +30 -0
  64. aiohomematic/rega_scripts/set_program_state.fn +12 -0
  65. aiohomematic/rega_scripts/set_system_variable.fn +15 -0
  66. aiohomematic/store/__init__.py +34 -0
  67. aiohomematic/store/dynamic.py +551 -0
  68. aiohomematic/store/persistent.py +988 -0
  69. aiohomematic/store/visibility.py +812 -0
  70. aiohomematic/support.py +664 -0
  71. aiohomematic/validator.py +112 -0
  72. aiohomematic-2025.11.3.dist-info/METADATA +144 -0
  73. aiohomematic-2025.11.3.dist-info/RECORD +77 -0
  74. aiohomematic-2025.11.3.dist-info/WHEEL +5 -0
  75. aiohomematic-2025.11.3.dist-info/entry_points.txt +2 -0
  76. aiohomematic-2025.11.3.dist-info/licenses/LICENSE +21 -0
  77. aiohomematic-2025.11.3.dist-info/top_level.txt +1 -0
@@ -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
+ )