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.
- aiohomematic/caches/dynamic.py +2 -2
- aiohomematic/central/__init__.py +13 -13
- aiohomematic/central/xml_rpc_server.py +9 -14
- aiohomematic/client/__init__.py +46 -46
- aiohomematic/client/_rpc_errors.py +1 -1
- aiohomematic/client/json_rpc.py +30 -30
- aiohomematic/client/xml_rpc.py +4 -4
- aiohomematic/const.py +1 -1
- aiohomematic/model/__init__.py +1 -1
- aiohomematic/model/calculated/data_point.py +2 -2
- aiohomematic/model/data_point.py +12 -12
- aiohomematic/model/device.py +9 -9
- aiohomematic/model/generic/data_point.py +3 -3
- aiohomematic/model/hub/__init__.py +3 -3
- aiohomematic/model/hub/data_point.py +3 -3
- aiohomematic/property_decorators.py +290 -148
- aiohomematic/support.py +8 -12
- {aiohomematic-2025.9.2.dist-info → aiohomematic-2025.9.4.dist-info}/METADATA +3 -3
- {aiohomematic-2025.9.2.dist-info → aiohomematic-2025.9.4.dist-info}/RECORD +23 -23
- aiohomematic_support/client_local.py +15 -15
- {aiohomematic-2025.9.2.dist-info → aiohomematic-2025.9.4.dist-info}/WHEEL +0 -0
- {aiohomematic-2025.9.2.dist-info → aiohomematic-2025.9.4.dist-info}/licenses/LICENSE +0 -0
- {aiohomematic-2025.9.2.dist-info → aiohomematic-2025.9.4.dist-info}/top_level.txt +0 -0
|
@@ -1,22 +1,36 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT
|
|
2
2
|
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
3
|
-
"""
|
|
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
|
-
"
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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)(
|
|
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)(
|
|
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)(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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(
|
|
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(
|
|
187
|
+
self.fdel(instance)
|
|
82
188
|
|
|
83
189
|
|
|
84
|
-
# -----
|
|
190
|
+
# ----- hm_property -----
|
|
85
191
|
|
|
86
192
|
|
|
87
|
-
|
|
88
|
-
|
|
193
|
+
@overload
|
|
194
|
+
def hm_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ...
|
|
89
195
|
|
|
90
196
|
|
|
91
197
|
@overload
|
|
92
|
-
def
|
|
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(
|
|
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
|
-
) ->
|
|
253
|
+
) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
|
|
104
254
|
"""
|
|
105
|
-
|
|
255
|
+
Decorate a method as a configuration property.
|
|
106
256
|
|
|
107
|
-
|
|
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]) ->
|
|
114
|
-
return
|
|
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
|
|
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], /) ->
|
|
282
|
+
def info_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ...
|
|
133
283
|
|
|
134
284
|
|
|
135
285
|
@overload
|
|
136
|
-
def info_property(
|
|
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
|
-
) ->
|
|
296
|
+
) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
|
|
144
297
|
"""
|
|
145
|
-
|
|
298
|
+
Decorate a method as an informational/metadata property.
|
|
146
299
|
|
|
147
|
-
|
|
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]) ->
|
|
154
|
-
return
|
|
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
|
|
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], /) ->
|
|
325
|
+
def state_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ...
|
|
173
326
|
|
|
174
327
|
|
|
175
328
|
@overload
|
|
176
|
-
def state_property(
|
|
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
|
-
) ->
|
|
339
|
+
) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
|
|
184
340
|
"""
|
|
185
|
-
|
|
341
|
+
Decorate a method as a dynamic state property.
|
|
186
342
|
|
|
187
|
-
|
|
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]) ->
|
|
194
|
-
return
|
|
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
|
|
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[
|
|
370
|
+
_PUBLIC_ATTR_CACHE: WeakKeyDictionary[type, dict[Kind, tuple[str, ...]]] = WeakKeyDictionary()
|
|
209
371
|
|
|
210
372
|
|
|
211
|
-
def
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
"""
|
|
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
|
|
273
|
-
"""
|
|
274
|
-
|
|
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
|
-
|
|
307
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
318
|
-
|
|
461
|
+
Returns:
|
|
462
|
+
Mapping[str, Any]: A mapping of attribute name to normalized value for logging.
|
|
319
463
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|