aiohomematic 2025.9.2__py3-none-any.whl → 2025.9.4__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.

@@ -1,22 +1,36 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  # Copyright (c) 2021-2025 Daniel Perna, SukramJ
3
- """Decorators for data points used within aiohomematic."""
3
+ """
4
+ Decorators and helpers for declaring public attributes on data point classes.
5
+
6
+ This module provides four decorator factories that behave like the built-in
7
+ @property, but additionally annotate properties with a semantic category so they
8
+ can be automatically collected to build payloads and log contexts:
9
+ - config_property: configuration-related properties.
10
+ - info_property: informational/metadata properties.
11
+ - state_property: dynamic state properties.
12
+ - hm_property: can be used to mark log_context or cached, where the other properties don't match
13
+
14
+ All decorators accept an optional keyword-only argument log_context. If set to
15
+ True, the property will be included in the LogContextMixin.log_context mapping.
16
+
17
+ Notes on caching
18
+ - Marked with cached=True always caches on first access and invalidates on set/delete.
19
+ """
4
20
 
5
21
  from __future__ import annotations
6
22
 
7
23
  from collections.abc import Callable, Mapping
8
24
  from datetime import datetime
9
- from enum import Enum
10
- from typing import Any, ParamSpec, TypeVar, cast, overload
25
+ from enum import Enum, StrEnum
26
+ from typing import Any, Final, ParamSpec, TypeVar, cast, overload
11
27
  from weakref import WeakKeyDictionary
12
28
 
13
29
  from aiohomematic import support as hms
14
30
 
15
31
  __all__ = [
16
32
  "config_property",
17
- "get_attributes_for_config_property",
18
- "get_attributes_for_info_property",
19
- "get_attributes_for_state_property",
33
+ "get_hm_property_by_kind",
20
34
  "info_property",
21
35
  "state_property",
22
36
  ]
@@ -26,8 +40,30 @@ T = TypeVar("T")
26
40
  R = TypeVar("R")
27
41
 
28
42
 
43
+ class Kind(StrEnum):
44
+ """Enum for property feature flags."""
45
+
46
+ CONFIG = "config"
47
+ INFO = "info"
48
+ SIMPLE = "simple"
49
+ STATE = "state"
50
+
51
+
29
52
  class _GenericProperty[GETTER, SETTER](property):
30
- """Generic property implementation."""
53
+ """
54
+ Base descriptor used by all property decorators in this module.
55
+
56
+ Extends the built-in property to optionally cache the computed value on the
57
+ instance and to carry a log_context flag.
58
+
59
+ Args:
60
+ - fget/fset/fdel: Standard property callables.
61
+ - doc: Optional docstring of the property.
62
+ - cached: If True, the computed value is cached per instance and
63
+ invalidated when the descriptor receives a set/delete.
64
+ - log_context: If True, the property is included in get_attributes_for_log_context().
65
+
66
+ """
31
67
 
32
68
  fget: Callable[[Any], GETTER] | None
33
69
  fset: Callable[[Any, SETTER], None] | None
@@ -39,210 +75,338 @@ class _GenericProperty[GETTER, SETTER](property):
39
75
  fset: Callable[[Any, SETTER], None] | None = None,
40
76
  fdel: Callable[[Any], None] | None = None,
41
77
  doc: str | None = None,
78
+ kind: Kind = Kind.SIMPLE,
79
+ cached: bool = False,
42
80
  log_context: bool = False,
43
81
  ) -> None:
44
- """Init the generic property."""
82
+ """
83
+ Initialize the descriptor.
84
+
85
+ Mirrors the standard property signature and adds two options:
86
+ - kind: specify the kind of property (e.g. simple, cached, config, info, state).
87
+ - cached: enable per-instance caching of the computed value.
88
+ - log_context: mark this property as relevant for structured logging.
89
+ """
45
90
  super().__init__(fget, fset, fdel, doc)
46
91
  if doc is None and fget is not None:
47
92
  doc = fget.__doc__
48
93
  self.__doc__ = doc
94
+ self.kind: Final = kind
95
+ self._cached: Final = cached
49
96
  self.log_context = log_context
97
+ if cached:
98
+ if fget is not None:
99
+ func_name = fget.__name__
100
+ elif fset is not None:
101
+ func_name = fset.__name__
102
+ elif fdel is not None:
103
+ func_name = fdel.__name__
104
+ else:
105
+ func_name = "prop"
106
+ self._cache_attr = f"_cached_{func_name}" # Default name of the cache attribute
50
107
 
51
108
  def getter(self, fget: Callable[[Any], GETTER], /) -> _GenericProperty:
52
109
  """Return generic getter."""
53
- return type(self)(fget, self.fset, self.fdel, self.__doc__) # pragma: no cover
110
+ return type(self)(
111
+ fget=fget,
112
+ fset=self.fset,
113
+ fdel=self.fdel,
114
+ doc=self.__doc__,
115
+ kind=self.kind,
116
+ cached=self._cached,
117
+ log_context=self.log_context,
118
+ ) # pragma: no cover
54
119
 
55
120
  def setter(self, fset: Callable[[Any, SETTER], None], /) -> _GenericProperty:
56
121
  """Return generic setter."""
57
- return type(self)(self.fget, fset, self.fdel, self.__doc__)
122
+ return type(self)(
123
+ fget=self.fget,
124
+ fset=fset,
125
+ fdel=self.fdel,
126
+ doc=self.__doc__,
127
+ kind=self.kind,
128
+ cached=self._cached,
129
+ log_context=self.log_context,
130
+ )
58
131
 
59
132
  def deleter(self, fdel: Callable[[Any], None], /) -> _GenericProperty:
60
133
  """Return generic deleter."""
61
- return type(self)(self.fget, self.fset, fdel, self.__doc__)
62
-
63
- def __get__(self, obj: Any, gtype: type | None = None, /) -> GETTER: # type: ignore[override]
64
- """Return the attribute."""
65
- if obj is None:
66
- return self # type: ignore[return-value]
134
+ return type(self)(
135
+ fget=self.fget,
136
+ fset=self.fset,
137
+ fdel=fdel,
138
+ doc=self.__doc__,
139
+ kind=self.kind,
140
+ cached=self._cached,
141
+ log_context=self.log_context,
142
+ )
143
+
144
+ def __get__(self, instance: Any, gtype: type | None = None, /) -> GETTER: # type: ignore[override]
145
+ """
146
+ Return the attribute value.
147
+
148
+ If caching is enabled, compute on first access and return the per-instance
149
+ cached value on subsequent accesses.
150
+ """
151
+ if instance is None:
152
+ # Accessed from class, return the descriptor itself
153
+ return cast(GETTER, self)
67
154
  if self.fget is None:
68
155
  raise AttributeError("unreadable attribute") # pragma: no cover
69
- return self.fget(obj)
70
156
 
71
- def __set__(self, obj: Any, value: Any, /) -> None:
72
- """Set the attribute."""
157
+ if not self._cached:
158
+ return self.fget(instance)
159
+
160
+ # If the cached value is not set yet, compute and store it
161
+ if not hasattr(instance, self._cache_attr):
162
+ value = self.fget(instance)
163
+ setattr(instance, self._cache_attr, value)
164
+
165
+ # Return the cached value
166
+ return cast(GETTER, getattr(instance, self._cache_attr))
167
+
168
+ def __set__(self, instance: Any, value: Any, /) -> None:
169
+ """Set the attribute value and invalidate cache if enabled."""
170
+ # Delete the cached value so it can be recomputed on next access.
171
+ if self._cached and hasattr(instance, self._cache_attr):
172
+ delattr(instance, self._cache_attr)
173
+
73
174
  if self.fset is None:
74
175
  raise AttributeError("can't set attribute") # pragma: no cover
75
- self.fset(obj, value)
176
+ self.fset(instance, value)
177
+
178
+ def __delete__(self, instance: Any, /) -> None:
179
+ """Delete the attribute and invalidate cache if enabled."""
180
+
181
+ # Delete the cached value so it can be recomputed on next access.
182
+ if self._cached and hasattr(instance, self._cache_attr):
183
+ delattr(instance, self._cache_attr)
76
184
 
77
- def __delete__(self, obj: Any, /) -> None:
78
- """Delete the attribute."""
79
185
  if self.fdel is None:
80
186
  raise AttributeError("can't delete attribute") # pragma: no cover
81
- self.fdel(obj)
187
+ self.fdel(instance)
82
188
 
83
189
 
84
- # ----- config_property -----
190
+ # ----- hm_property -----
85
191
 
86
192
 
87
- class _ConfigProperty[GETTER, SETTER](_GenericProperty[GETTER, SETTER]):
88
- """Decorate to mark own config properties."""
193
+ @overload
194
+ def hm_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ...
89
195
 
90
196
 
91
197
  @overload
92
- def config_property[PR](func: Callable[[Any], PR], /) -> _ConfigProperty[PR, Any]: ...
198
+ def hm_property(
199
+ *, kind: Kind = ..., cached: bool = ..., log_context: bool = ...
200
+ ) -> Callable[[Callable[[Any], R]], _GenericProperty[R, Any]]: ...
201
+
202
+
203
+ def hm_property[PR](
204
+ func: Callable[[Any], PR] | None = None,
205
+ *,
206
+ kind: Kind = Kind.SIMPLE,
207
+ cached: bool = False,
208
+ log_context: bool = False,
209
+ ) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
210
+ """
211
+ Decorate a method as a computed attribute.
212
+
213
+ Supports both usages:
214
+ - @hm_property
215
+ - @hm_property(kind=..., cached=True, log_context=True)
216
+
217
+ Args:
218
+ func: The function being decorated when used as @hm_property without
219
+ parentheses. When used as a factory (i.e., @hm_property(...)), this
220
+ is None and the returned callable expects the function to decorate.
221
+ kind: Specify the kind of property (e.g. simple, config, info, state).
222
+ cached: Optionally enable per-instance caching for this property.
223
+ log_context: Include this property in structured log context if True.
224
+
225
+ """
226
+ if func is None:
227
+
228
+ def wrapper(f: Callable[[Any], PR]) -> _GenericProperty[PR, Any]:
229
+ return _GenericProperty(f, kind=kind, cached=cached, log_context=log_context)
230
+
231
+ return wrapper
232
+ return _GenericProperty(func, kind=kind, cached=cached, log_context=log_context)
233
+
234
+
235
+ # ----- config_property -----
93
236
 
94
237
 
95
238
  @overload
96
- def config_property(*, log_context: bool = ...) -> Callable[[Callable[[Any], R]], _ConfigProperty[R, Any]]: ...
239
+ def config_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ...
240
+
241
+
242
+ @overload
243
+ def config_property(
244
+ *, cached: bool = ..., log_context: bool = ...
245
+ ) -> Callable[[Callable[[Any], R]], _GenericProperty[R, Any]]: ...
97
246
 
98
247
 
99
248
  def config_property[PR](
100
249
  func: Callable[[Any], PR] | None = None,
101
250
  *,
251
+ cached: bool = False,
102
252
  log_context: bool = False,
103
- ) -> _ConfigProperty[PR, Any] | Callable[[Callable[[Any], PR]], _ConfigProperty[PR, Any]]:
253
+ ) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
104
254
  """
105
- Return an instance of _ConfigProperty wrapping the given function.
255
+ Decorate a method as a configuration property.
106
256
 
107
- Decorator for config properties supporting both usages:
257
+ Supports both usages:
108
258
  - @config_property
109
- - @config_property(log_context=True)
259
+ - @config_property(cached=True, log_context=True)
260
+
261
+ Args:
262
+ func: The function being decorated when used as @config_property without
263
+ parentheses. When used as a factory (i.e., @config_property(...)), this is
264
+ None and the returned callable expects the function to decorate.
265
+ cached: Enable per-instance caching for this property when True.
266
+ log_context: Include this property in structured log context if True.
267
+
110
268
  """
111
269
  if func is None:
112
270
 
113
- def wrapper(f: Callable[[Any], PR]) -> _ConfigProperty[PR, Any]:
114
- return _ConfigProperty(f, log_context=log_context)
271
+ def wrapper(f: Callable[[Any], PR]) -> _GenericProperty[PR, Any]:
272
+ return _GenericProperty(f, kind=Kind.CONFIG, cached=cached, log_context=log_context)
115
273
 
116
274
  return wrapper
117
- return _ConfigProperty(func, log_context=log_context)
118
-
119
-
120
- # Expose the underlying property class for discovery
121
- setattr(config_property, "__property_class__", _ConfigProperty)
275
+ return _GenericProperty(func, kind=Kind.CONFIG, cached=cached, log_context=log_context)
122
276
 
123
277
 
124
278
  # ----- info_property -----
125
279
 
126
280
 
127
- class _InfoProperty[GETTER, SETTER](_GenericProperty[GETTER, SETTER]):
128
- """Decorate to mark own info properties."""
129
-
130
-
131
281
  @overload
132
- def info_property[PR](func: Callable[[Any], PR], /) -> _InfoProperty[PR, Any]: ...
282
+ def info_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ...
133
283
 
134
284
 
135
285
  @overload
136
- def info_property(*, log_context: bool = ...) -> Callable[[Callable[[Any], R]], _InfoProperty[R, Any]]: ...
286
+ def info_property(
287
+ *, cached: bool = ..., log_context: bool = ...
288
+ ) -> Callable[[Callable[[Any], R]], _GenericProperty[R, Any]]: ...
137
289
 
138
290
 
139
291
  def info_property[PR](
140
292
  func: Callable[[Any], PR] | None = None,
141
293
  *,
294
+ cached: bool = False,
142
295
  log_context: bool = False,
143
- ) -> _InfoProperty[PR, Any] | Callable[[Callable[[Any], PR]], _InfoProperty[PR, Any]]:
296
+ ) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
144
297
  """
145
- Return an instance of _InfoProperty wrapping the given function.
298
+ Decorate a method as an informational/metadata property.
146
299
 
147
- Decorator for info properties supporting both usages:
300
+ Supports both usages:
148
301
  - @info_property
149
- - @info_property(log_context=True)
302
+ - @info_property(cached=True, log_context=True)
303
+
304
+ Args:
305
+ func: The function being decorated when used as @info_property without
306
+ parentheses. When used as a factory (i.e., @info_property(...)), this is
307
+ None and the returned callable expects the function to decorate.
308
+ cached: Enable per-instance caching for this property when True.
309
+ log_context: Include this property in structured log context if True.
310
+
150
311
  """
151
312
  if func is None:
152
313
 
153
- def wrapper(f: Callable[[Any], PR]) -> _InfoProperty[PR, Any]:
154
- return _InfoProperty(f, log_context=log_context)
314
+ def wrapper(f: Callable[[Any], PR]) -> _GenericProperty[PR, Any]:
315
+ return _GenericProperty(f, kind=Kind.INFO, cached=cached, log_context=log_context)
155
316
 
156
317
  return wrapper
157
- return _InfoProperty(func, log_context=log_context)
158
-
159
-
160
- # Expose the underlying property class for discovery
161
- setattr(info_property, "__property_class__", _InfoProperty)
318
+ return _GenericProperty(func, kind=Kind.INFO, cached=cached, log_context=log_context)
162
319
 
163
320
 
164
321
  # ----- state_property -----
165
322
 
166
323
 
167
- class _StateProperty[GETTER, SETTER](_GenericProperty[GETTER, SETTER]):
168
- """Decorate to mark own config properties."""
169
-
170
-
171
324
  @overload
172
- def state_property[PR](func: Callable[[Any], PR], /) -> _StateProperty[PR, Any]: ...
325
+ def state_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ...
173
326
 
174
327
 
175
328
  @overload
176
- def state_property(*, log_context: bool = ...) -> Callable[[Callable[[Any], R]], _StateProperty[R, Any]]: ...
329
+ def state_property(
330
+ *, cached: bool = ..., log_context: bool = ...
331
+ ) -> Callable[[Callable[[Any], R]], _GenericProperty[R, Any]]: ...
177
332
 
178
333
 
179
334
  def state_property[PR](
180
335
  func: Callable[[Any], PR] | None = None,
181
336
  *,
337
+ cached: bool = False,
182
338
  log_context: bool = False,
183
- ) -> _StateProperty[PR, Any] | Callable[[Callable[[Any], PR]], _StateProperty[PR, Any]]:
339
+ ) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
184
340
  """
185
- Return an instance of _StateProperty wrapping the given function.
341
+ Decorate a method as a dynamic state property.
186
342
 
187
- Decorator for state properties supporting both usages:
343
+ Supports both usages:
188
344
  - @state_property
189
- - @state_property(log_context=True)
345
+ - @state_property(cached=True, log_context=True)
346
+
347
+ Args:
348
+ func: The function being decorated when used as @state_property without
349
+ parentheses. When used as a factory (i.e., @state_property(...)), this is
350
+ None and the returned callable expects the function to decorate.
351
+ cached: Enable per-instance caching for this property when True.
352
+ log_context: Include this property in structured log context if True.
353
+
190
354
  """
191
355
  if func is None:
192
356
 
193
- def wrapper(f: Callable[[Any], PR]) -> _StateProperty[PR, Any]:
194
- return _StateProperty(f, log_context=log_context)
357
+ def wrapper(f: Callable[[Any], PR]) -> _GenericProperty[PR, Any]:
358
+ return _GenericProperty(f, kind=Kind.STATE, cached=cached, log_context=log_context)
195
359
 
196
360
  return wrapper
197
- return _StateProperty(func, log_context=log_context)
198
-
361
+ return _GenericProperty(func, kind=Kind.STATE, cached=cached, log_context=log_context)
199
362
 
200
- # Expose the underlying property class for discovery
201
- setattr(state_property, "__property_class__", _StateProperty)
202
363
 
203
364
  # ----------
204
365
 
366
+
205
367
  # Cache for per-class attribute names by decorator to avoid repeated dir() scans
206
368
  # Use WeakKeyDictionary to allow classes to be garbage-collected without leaking cache entries.
207
369
  # Structure: {cls: {decorator_class: (attr_name1, attr_name2, ...)}}
208
- _PUBLIC_ATTR_CACHE: WeakKeyDictionary[type, dict[type, tuple[str, ...]]] = WeakKeyDictionary()
370
+ _PUBLIC_ATTR_CACHE: WeakKeyDictionary[type, dict[Kind, tuple[str, ...]]] = WeakKeyDictionary()
209
371
 
210
372
 
211
- def _get_attributes_by_decorator(
212
- data_object: Any, decorator: Callable, context: bool = False, only_names: bool = False
213
- ) -> Mapping[str, Any]:
373
+ def get_hm_property_by_kind(data_object: Any, kind: Kind, context: bool = False) -> Mapping[str, Any]:
214
374
  """
215
- Return the object attributes by decorator.
375
+ Collect properties from an object that are defined using a specific decorator.
376
+
377
+ Args:
378
+ data_object: The instance to inspect.
379
+ kind: The decorator class to use for filtering.
380
+ context: If True, only include properties where the descriptor has
381
+ log_context=True. When such a property's value is a LogContextMixin, its
382
+ items are flattened into the result using a short prefix of the property
383
+ name (e.g. "p.key").
384
+
385
+ Returns:
386
+ Mapping[str, Any]: A mapping of attribute name to normalized value. Values are converted via
387
+ _get_text_value() to provide stable JSON/log-friendly types.
388
+
389
+ Notes:
390
+ Attribute NAMES are cached per (class, decorator) to avoid repeated dir()
391
+ scans. Values are never cached here since they are instance-dependent.
392
+ Getter exceptions are swallowed and represented as None so payload building
393
+ remains robust and side-effect free.
216
394
 
217
- This caches the attribute names per (class, decorator) to reduce overhead
218
- from repeated dir()/getattr() scans. Values are not cached as they are
219
- instance-dependent and may change over time.
220
-
221
- To minimize side effects, exceptions raised by property getters are caught
222
- and the corresponding value is set to None. This ensures that payload
223
- construction and attribute introspection do not fail due to individual
224
- properties with transient errors or expensive side effects.
225
395
  """
226
396
  cls = data_object.__class__
227
397
 
228
- # Resolve function-based decorators to their underlying property class, if provided
229
- resolved_decorator: Any = decorator
230
- if not isinstance(decorator, type):
231
- resolved_decorator = getattr(decorator, "__property_class__", decorator)
232
-
233
398
  # Get or create the per-class cache dict
234
399
  if (decorator_cache := _PUBLIC_ATTR_CACHE.get(cls)) is None:
235
400
  decorator_cache = {}
236
401
  _PUBLIC_ATTR_CACHE[cls] = decorator_cache
237
402
 
238
- # Get or compute the attribute names for this decorator
239
- if (names := decorator_cache.get(resolved_decorator)) is None:
240
- names = tuple(y for y in dir(cls) if isinstance(getattr(cls, y), resolved_decorator))
241
- decorator_cache[resolved_decorator] = names
403
+ if (names := decorator_cache.get(kind)) is None:
404
+ names = tuple(
405
+ y for y in dir(cls) if (gp := getattr(cls, y)) and isinstance(gp, _GenericProperty) and gp.kind == kind
406
+ )
407
+ decorator_cache[kind] = names
242
408
 
243
409
  result: dict[str, Any] = {}
244
- if only_names:
245
- return dict.fromkeys(names)
246
410
  for name in names:
247
411
  if context and getattr(cls, name).log_context is False:
248
412
  continue
@@ -259,7 +423,21 @@ def _get_attributes_by_decorator(
259
423
 
260
424
 
261
425
  def _get_text_value(value: Any) -> Any:
262
- """Convert value to text."""
426
+ """
427
+ Normalize values for payload/logging purposes.
428
+
429
+ - list/tuple/set are converted to tuples and their items normalized recursively
430
+ - Enum values are converted to their string representation
431
+ - datetime objects are converted to unix timestamps (float)
432
+ - all other types are returned unchanged
433
+
434
+ Args:
435
+ value: The input value to normalize into a log-/JSON-friendly representation.
436
+
437
+ Returns:
438
+ Any: The normalized value, potentially converted as described above.
439
+
440
+ """
263
441
  if isinstance(value, list | tuple | set):
264
442
  return tuple(_get_text_value(v) for v in value)
265
443
  if isinstance(value, Enum):
@@ -269,59 +447,23 @@ def _get_text_value(value: Any) -> Any:
269
447
  return value
270
448
 
271
449
 
272
- def get_attributes_for_config_property(data_object: Any) -> Mapping[str, Any]:
273
- """Return the object attributes by decorator config_property."""
274
- return _get_attributes_by_decorator(data_object=data_object, decorator=config_property)
275
-
276
-
277
- def get_attributes_for_info_property(data_object: Any) -> Mapping[str, Any]:
278
- """Return the object attributes by decorator info_property."""
279
- return _get_attributes_by_decorator(data_object=data_object, decorator=info_property)
280
-
281
-
282
- def get_attributes_for_log_context(data_object: Any) -> Mapping[str, Any]:
283
- """Return the object attributes by decorator info_property."""
284
- return (
285
- dict(_get_attributes_by_decorator(data_object=data_object, decorator=config_property, context=True))
286
- | dict(_get_attributes_by_decorator(data_object=data_object, decorator=info_property, context=True))
287
- | dict(_get_attributes_by_decorator(data_object=data_object, decorator=state_property, context=True))
288
- )
289
-
290
-
291
- def get_attributes_for_state_property(data_object: Any) -> Mapping[str, Any]:
292
- """Return the object attributes by decorator state_property."""
293
- return _get_attributes_by_decorator(data_object=data_object, decorator=state_property)
294
-
295
-
296
- # pylint: disable=invalid-name
297
- class cached_slot_property[T, R]:
298
- """A property-like descriptor that caches the computed value in a slot attribute. Designed to work with classes that use __slots__ and do not define __dict__."""
299
-
300
- def __init__(self, func: Callable[[T], R]) -> None:
301
- """Init the cached property."""
302
- self._func = func # The function to compute the value
303
- self._cache_attr = f"_cached_{func.__name__}" # Default name of the cache attribute
304
- self._name = func.__name__
450
+ def get_hm_property_by_log_context(data_object: Any) -> Mapping[str, Any]:
451
+ """
452
+ Return combined log context attributes across all property categories.
305
453
 
306
- def __get__(self, instance: T | None, owner: type | None = None) -> R:
307
- """Return the cached value if it exists. Otherwise, compute it using the function and cache it."""
308
- if instance is None:
309
- # Accessed from class, return the descriptor itself
310
- return cast(R, self)
454
+ Includes only properties declared with log_context=True and flattens
455
+ values that implement LogContextMixin by prefixing with a short key.
311
456
 
312
- # If the cached value is not set yet, compute and store it
313
- if not hasattr(instance, self._cache_attr):
314
- value = self._func(instance)
315
- setattr(instance, self._cache_attr, value)
457
+ Args:
458
+ data_object: The instance from which to collect attributes marked for
459
+ log context across all property categories.
316
460
 
317
- # Return the cached value
318
- return cast(R, getattr(instance, self._cache_attr))
461
+ Returns:
462
+ Mapping[str, Any]: A mapping of attribute name to normalized value for logging.
319
463
 
320
- def __set__(self, instance: T, value: Any) -> None:
321
- """Raise an error to prevent manual assignment to the property."""
322
- raise AttributeError(f"Can't set read-only cached property '{self._name}'")
464
+ """
465
+ result: dict[str, Any] = {}
466
+ for kind in Kind:
467
+ result.update(get_hm_property_by_kind(data_object=data_object, kind=kind, context=True))
323
468
 
324
- def __delete__(self, instance: T) -> None:
325
- """Delete the cached value so it can be recomputed on next access."""
326
- if hasattr(instance, self._cache_attr):
327
- delattr(instance, self._cache_attr)
469
+ return result
aiohomematic/support.py CHANGED
@@ -49,13 +49,7 @@ from aiohomematic.const import (
49
49
  SysvarType,
50
50
  )
51
51
  from aiohomematic.exceptions import AioHomematicException, BaseHomematicException
52
- from aiohomematic.property_decorators import (
53
- cached_slot_property,
54
- get_attributes_for_config_property,
55
- get_attributes_for_info_property,
56
- get_attributes_for_log_context,
57
- get_attributes_for_state_property,
58
- )
52
+ from aiohomematic.property_decorators import Kind, get_hm_property_by_kind, get_hm_property_by_log_context, hm_property
59
53
 
60
54
  _LOGGER: Final = logging.getLogger(__name__)
61
55
 
@@ -586,11 +580,11 @@ class LogContextMixin:
586
580
 
587
581
  __slots__ = ("_cached_log_context",)
588
582
 
589
- @cached_slot_property
583
+ @hm_property(cached=True)
590
584
  def log_context(self) -> Mapping[str, Any]:
591
585
  """Return the log context for this object."""
592
586
  return {
593
- key: value for key, value in get_attributes_for_log_context(data_object=self).items() if value is not None
587
+ key: value for key, value in get_hm_property_by_log_context(data_object=self).items() if value is not None
594
588
  }
595
589
 
596
590
 
@@ -604,7 +598,7 @@ class PayloadMixin:
604
598
  """Return the config payload."""
605
599
  return {
606
600
  key: value
607
- for key, value in get_attributes_for_config_property(data_object=self).items()
601
+ for key, value in get_hm_property_by_kind(data_object=self, kind=Kind.CONFIG).items()
608
602
  if value is not None
609
603
  }
610
604
 
@@ -612,7 +606,9 @@ class PayloadMixin:
612
606
  def info_payload(self) -> Mapping[str, Any]:
613
607
  """Return the info payload."""
614
608
  return {
615
- key: value for key, value in get_attributes_for_info_property(data_object=self).items() if value is not None
609
+ key: value
610
+ for key, value in get_hm_property_by_kind(data_object=self, kind=Kind.INFO).items()
611
+ if value is not None
616
612
  }
617
613
 
618
614
  @property
@@ -620,7 +616,7 @@ class PayloadMixin:
620
616
  """Return the state payload."""
621
617
  return {
622
618
  key: value
623
- for key, value in get_attributes_for_state_property(data_object=self).items()
619
+ for key, value in get_hm_property_by_kind(data_object=self, kind=Kind.STATE).items()
624
620
  if value is not None
625
621
  }
626
622