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.
Files changed (188) hide show
  1. aiohomematic/__init__.py +110 -0
  2. aiohomematic/_log_context_protocol.py +29 -0
  3. aiohomematic/api.py +410 -0
  4. aiohomematic/async_support.py +250 -0
  5. aiohomematic/backend_detection.py +462 -0
  6. aiohomematic/central/__init__.py +103 -0
  7. aiohomematic/central/async_rpc_server.py +760 -0
  8. aiohomematic/central/central_unit.py +1152 -0
  9. aiohomematic/central/config.py +463 -0
  10. aiohomematic/central/config_builder.py +772 -0
  11. aiohomematic/central/connection_state.py +160 -0
  12. aiohomematic/central/coordinators/__init__.py +38 -0
  13. aiohomematic/central/coordinators/cache.py +414 -0
  14. aiohomematic/central/coordinators/client.py +480 -0
  15. aiohomematic/central/coordinators/connection_recovery.py +1141 -0
  16. aiohomematic/central/coordinators/device.py +1166 -0
  17. aiohomematic/central/coordinators/event.py +514 -0
  18. aiohomematic/central/coordinators/hub.py +532 -0
  19. aiohomematic/central/decorators.py +184 -0
  20. aiohomematic/central/device_registry.py +229 -0
  21. aiohomematic/central/events/__init__.py +104 -0
  22. aiohomematic/central/events/bus.py +1392 -0
  23. aiohomematic/central/events/integration.py +424 -0
  24. aiohomematic/central/events/types.py +194 -0
  25. aiohomematic/central/health.py +762 -0
  26. aiohomematic/central/rpc_server.py +353 -0
  27. aiohomematic/central/scheduler.py +794 -0
  28. aiohomematic/central/state_machine.py +391 -0
  29. aiohomematic/client/__init__.py +203 -0
  30. aiohomematic/client/_rpc_errors.py +187 -0
  31. aiohomematic/client/backends/__init__.py +48 -0
  32. aiohomematic/client/backends/base.py +335 -0
  33. aiohomematic/client/backends/capabilities.py +138 -0
  34. aiohomematic/client/backends/ccu.py +487 -0
  35. aiohomematic/client/backends/factory.py +116 -0
  36. aiohomematic/client/backends/homegear.py +294 -0
  37. aiohomematic/client/backends/json_ccu.py +252 -0
  38. aiohomematic/client/backends/protocol.py +316 -0
  39. aiohomematic/client/ccu.py +1857 -0
  40. aiohomematic/client/circuit_breaker.py +459 -0
  41. aiohomematic/client/config.py +64 -0
  42. aiohomematic/client/handlers/__init__.py +40 -0
  43. aiohomematic/client/handlers/backup.py +157 -0
  44. aiohomematic/client/handlers/base.py +79 -0
  45. aiohomematic/client/handlers/device_ops.py +1085 -0
  46. aiohomematic/client/handlers/firmware.py +144 -0
  47. aiohomematic/client/handlers/link_mgmt.py +199 -0
  48. aiohomematic/client/handlers/metadata.py +436 -0
  49. aiohomematic/client/handlers/programs.py +144 -0
  50. aiohomematic/client/handlers/sysvars.py +100 -0
  51. aiohomematic/client/interface_client.py +1304 -0
  52. aiohomematic/client/json_rpc.py +2068 -0
  53. aiohomematic/client/request_coalescer.py +282 -0
  54. aiohomematic/client/rpc_proxy.py +629 -0
  55. aiohomematic/client/state_machine.py +324 -0
  56. aiohomematic/const.py +2207 -0
  57. aiohomematic/context.py +275 -0
  58. aiohomematic/converter.py +270 -0
  59. aiohomematic/decorators.py +390 -0
  60. aiohomematic/exceptions.py +185 -0
  61. aiohomematic/hmcli.py +997 -0
  62. aiohomematic/i18n.py +193 -0
  63. aiohomematic/interfaces/__init__.py +407 -0
  64. aiohomematic/interfaces/central.py +1067 -0
  65. aiohomematic/interfaces/client.py +1096 -0
  66. aiohomematic/interfaces/coordinators.py +63 -0
  67. aiohomematic/interfaces/model.py +1921 -0
  68. aiohomematic/interfaces/operations.py +217 -0
  69. aiohomematic/logging_context.py +134 -0
  70. aiohomematic/metrics/__init__.py +125 -0
  71. aiohomematic/metrics/_protocols.py +140 -0
  72. aiohomematic/metrics/aggregator.py +534 -0
  73. aiohomematic/metrics/dataclasses.py +489 -0
  74. aiohomematic/metrics/emitter.py +292 -0
  75. aiohomematic/metrics/events.py +183 -0
  76. aiohomematic/metrics/keys.py +300 -0
  77. aiohomematic/metrics/observer.py +563 -0
  78. aiohomematic/metrics/stats.py +172 -0
  79. aiohomematic/model/__init__.py +189 -0
  80. aiohomematic/model/availability.py +65 -0
  81. aiohomematic/model/calculated/__init__.py +89 -0
  82. aiohomematic/model/calculated/climate.py +276 -0
  83. aiohomematic/model/calculated/data_point.py +315 -0
  84. aiohomematic/model/calculated/field.py +147 -0
  85. aiohomematic/model/calculated/operating_voltage_level.py +286 -0
  86. aiohomematic/model/calculated/support.py +232 -0
  87. aiohomematic/model/custom/__init__.py +214 -0
  88. aiohomematic/model/custom/capabilities/__init__.py +67 -0
  89. aiohomematic/model/custom/capabilities/climate.py +41 -0
  90. aiohomematic/model/custom/capabilities/light.py +87 -0
  91. aiohomematic/model/custom/capabilities/lock.py +44 -0
  92. aiohomematic/model/custom/capabilities/siren.py +63 -0
  93. aiohomematic/model/custom/climate.py +1130 -0
  94. aiohomematic/model/custom/cover.py +722 -0
  95. aiohomematic/model/custom/data_point.py +360 -0
  96. aiohomematic/model/custom/definition.py +300 -0
  97. aiohomematic/model/custom/field.py +89 -0
  98. aiohomematic/model/custom/light.py +1174 -0
  99. aiohomematic/model/custom/lock.py +322 -0
  100. aiohomematic/model/custom/mixins.py +445 -0
  101. aiohomematic/model/custom/profile.py +945 -0
  102. aiohomematic/model/custom/registry.py +251 -0
  103. aiohomematic/model/custom/siren.py +462 -0
  104. aiohomematic/model/custom/switch.py +195 -0
  105. aiohomematic/model/custom/text_display.py +289 -0
  106. aiohomematic/model/custom/valve.py +78 -0
  107. aiohomematic/model/data_point.py +1416 -0
  108. aiohomematic/model/device.py +1840 -0
  109. aiohomematic/model/event.py +216 -0
  110. aiohomematic/model/generic/__init__.py +327 -0
  111. aiohomematic/model/generic/action.py +40 -0
  112. aiohomematic/model/generic/action_select.py +62 -0
  113. aiohomematic/model/generic/binary_sensor.py +30 -0
  114. aiohomematic/model/generic/button.py +31 -0
  115. aiohomematic/model/generic/data_point.py +177 -0
  116. aiohomematic/model/generic/dummy.py +150 -0
  117. aiohomematic/model/generic/number.py +76 -0
  118. aiohomematic/model/generic/select.py +56 -0
  119. aiohomematic/model/generic/sensor.py +76 -0
  120. aiohomematic/model/generic/switch.py +54 -0
  121. aiohomematic/model/generic/text.py +33 -0
  122. aiohomematic/model/hub/__init__.py +100 -0
  123. aiohomematic/model/hub/binary_sensor.py +24 -0
  124. aiohomematic/model/hub/button.py +28 -0
  125. aiohomematic/model/hub/connectivity.py +190 -0
  126. aiohomematic/model/hub/data_point.py +342 -0
  127. aiohomematic/model/hub/hub.py +864 -0
  128. aiohomematic/model/hub/inbox.py +135 -0
  129. aiohomematic/model/hub/install_mode.py +393 -0
  130. aiohomematic/model/hub/metrics.py +208 -0
  131. aiohomematic/model/hub/number.py +42 -0
  132. aiohomematic/model/hub/select.py +52 -0
  133. aiohomematic/model/hub/sensor.py +37 -0
  134. aiohomematic/model/hub/switch.py +43 -0
  135. aiohomematic/model/hub/text.py +30 -0
  136. aiohomematic/model/hub/update.py +221 -0
  137. aiohomematic/model/support.py +592 -0
  138. aiohomematic/model/update.py +140 -0
  139. aiohomematic/model/week_profile.py +1827 -0
  140. aiohomematic/property_decorators.py +719 -0
  141. aiohomematic/py.typed +0 -0
  142. aiohomematic/rega_scripts/accept_device_in_inbox.fn +51 -0
  143. aiohomematic/rega_scripts/create_backup_start.fn +28 -0
  144. aiohomematic/rega_scripts/create_backup_status.fn +89 -0
  145. aiohomematic/rega_scripts/fetch_all_device_data.fn +97 -0
  146. aiohomematic/rega_scripts/get_backend_info.fn +25 -0
  147. aiohomematic/rega_scripts/get_inbox_devices.fn +61 -0
  148. aiohomematic/rega_scripts/get_program_descriptions.fn +31 -0
  149. aiohomematic/rega_scripts/get_serial.fn +44 -0
  150. aiohomematic/rega_scripts/get_service_messages.fn +83 -0
  151. aiohomematic/rega_scripts/get_system_update_info.fn +39 -0
  152. aiohomematic/rega_scripts/get_system_variable_descriptions.fn +31 -0
  153. aiohomematic/rega_scripts/set_program_state.fn +17 -0
  154. aiohomematic/rega_scripts/set_system_variable.fn +19 -0
  155. aiohomematic/rega_scripts/trigger_firmware_update.fn +67 -0
  156. aiohomematic/schemas.py +256 -0
  157. aiohomematic/store/__init__.py +55 -0
  158. aiohomematic/store/dynamic/__init__.py +43 -0
  159. aiohomematic/store/dynamic/command.py +250 -0
  160. aiohomematic/store/dynamic/data.py +175 -0
  161. aiohomematic/store/dynamic/details.py +187 -0
  162. aiohomematic/store/dynamic/ping_pong.py +416 -0
  163. aiohomematic/store/persistent/__init__.py +71 -0
  164. aiohomematic/store/persistent/base.py +285 -0
  165. aiohomematic/store/persistent/device.py +233 -0
  166. aiohomematic/store/persistent/incident.py +380 -0
  167. aiohomematic/store/persistent/paramset.py +241 -0
  168. aiohomematic/store/persistent/session.py +556 -0
  169. aiohomematic/store/serialization.py +150 -0
  170. aiohomematic/store/storage.py +689 -0
  171. aiohomematic/store/types.py +526 -0
  172. aiohomematic/store/visibility/__init__.py +40 -0
  173. aiohomematic/store/visibility/parser.py +141 -0
  174. aiohomematic/store/visibility/registry.py +722 -0
  175. aiohomematic/store/visibility/rules.py +307 -0
  176. aiohomematic/strings.json +237 -0
  177. aiohomematic/support.py +706 -0
  178. aiohomematic/tracing.py +236 -0
  179. aiohomematic/translations/de.json +237 -0
  180. aiohomematic/translations/en.json +237 -0
  181. aiohomematic/type_aliases.py +51 -0
  182. aiohomematic/validator.py +128 -0
  183. aiohomematic-2026.1.29.dist-info/METADATA +296 -0
  184. aiohomematic-2026.1.29.dist-info/RECORD +188 -0
  185. aiohomematic-2026.1.29.dist-info/WHEEL +5 -0
  186. aiohomematic-2026.1.29.dist-info/entry_points.txt +2 -0
  187. aiohomematic-2026.1.29.dist-info/licenses/LICENSE +21 -0
  188. 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
+ )