aiohomematic 2025.11.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aiohomematic might be problematic. Click here for more details.
- aiohomematic/__init__.py +61 -0
- aiohomematic/async_support.py +212 -0
- aiohomematic/central/__init__.py +2309 -0
- aiohomematic/central/decorators.py +155 -0
- aiohomematic/central/rpc_server.py +295 -0
- aiohomematic/client/__init__.py +1848 -0
- aiohomematic/client/_rpc_errors.py +81 -0
- aiohomematic/client/json_rpc.py +1326 -0
- aiohomematic/client/rpc_proxy.py +311 -0
- aiohomematic/const.py +1127 -0
- aiohomematic/context.py +18 -0
- aiohomematic/converter.py +108 -0
- aiohomematic/decorators.py +302 -0
- aiohomematic/exceptions.py +164 -0
- aiohomematic/hmcli.py +186 -0
- aiohomematic/model/__init__.py +140 -0
- aiohomematic/model/calculated/__init__.py +84 -0
- aiohomematic/model/calculated/climate.py +290 -0
- aiohomematic/model/calculated/data_point.py +327 -0
- aiohomematic/model/calculated/operating_voltage_level.py +299 -0
- aiohomematic/model/calculated/support.py +234 -0
- aiohomematic/model/custom/__init__.py +177 -0
- aiohomematic/model/custom/climate.py +1532 -0
- aiohomematic/model/custom/cover.py +792 -0
- aiohomematic/model/custom/data_point.py +334 -0
- aiohomematic/model/custom/definition.py +871 -0
- aiohomematic/model/custom/light.py +1128 -0
- aiohomematic/model/custom/lock.py +394 -0
- aiohomematic/model/custom/siren.py +275 -0
- aiohomematic/model/custom/support.py +41 -0
- aiohomematic/model/custom/switch.py +175 -0
- aiohomematic/model/custom/valve.py +114 -0
- aiohomematic/model/data_point.py +1123 -0
- aiohomematic/model/device.py +1445 -0
- aiohomematic/model/event.py +208 -0
- aiohomematic/model/generic/__init__.py +217 -0
- aiohomematic/model/generic/action.py +34 -0
- aiohomematic/model/generic/binary_sensor.py +30 -0
- aiohomematic/model/generic/button.py +27 -0
- aiohomematic/model/generic/data_point.py +171 -0
- aiohomematic/model/generic/dummy.py +147 -0
- aiohomematic/model/generic/number.py +76 -0
- aiohomematic/model/generic/select.py +39 -0
- aiohomematic/model/generic/sensor.py +74 -0
- aiohomematic/model/generic/switch.py +54 -0
- aiohomematic/model/generic/text.py +29 -0
- aiohomematic/model/hub/__init__.py +333 -0
- aiohomematic/model/hub/binary_sensor.py +24 -0
- aiohomematic/model/hub/button.py +28 -0
- aiohomematic/model/hub/data_point.py +340 -0
- aiohomematic/model/hub/number.py +39 -0
- aiohomematic/model/hub/select.py +49 -0
- aiohomematic/model/hub/sensor.py +37 -0
- aiohomematic/model/hub/switch.py +44 -0
- aiohomematic/model/hub/text.py +30 -0
- aiohomematic/model/support.py +586 -0
- aiohomematic/model/update.py +143 -0
- aiohomematic/property_decorators.py +496 -0
- aiohomematic/py.typed +0 -0
- aiohomematic/rega_scripts/fetch_all_device_data.fn +92 -0
- aiohomematic/rega_scripts/get_program_descriptions.fn +30 -0
- aiohomematic/rega_scripts/get_serial.fn +44 -0
- aiohomematic/rega_scripts/get_system_variable_descriptions.fn +30 -0
- aiohomematic/rega_scripts/set_program_state.fn +12 -0
- aiohomematic/rega_scripts/set_system_variable.fn +15 -0
- aiohomematic/store/__init__.py +34 -0
- aiohomematic/store/dynamic.py +551 -0
- aiohomematic/store/persistent.py +988 -0
- aiohomematic/store/visibility.py +812 -0
- aiohomematic/support.py +664 -0
- aiohomematic/validator.py +112 -0
- aiohomematic-2025.11.3.dist-info/METADATA +144 -0
- aiohomematic-2025.11.3.dist-info/RECORD +77 -0
- aiohomematic-2025.11.3.dist-info/WHEEL +5 -0
- aiohomematic-2025.11.3.dist-info/entry_points.txt +2 -0
- aiohomematic-2025.11.3.dist-info/licenses/LICENSE +21 -0
- aiohomematic-2025.11.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
|
+
"""Module for data points implemented using the update category."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from functools import partial
|
|
10
|
+
from typing import Final
|
|
11
|
+
|
|
12
|
+
from aiohomematic.const import (
|
|
13
|
+
CALLBACK_TYPE,
|
|
14
|
+
HMIP_FIRMWARE_UPDATE_IN_PROGRESS_STATES,
|
|
15
|
+
HMIP_FIRMWARE_UPDATE_READY_STATES,
|
|
16
|
+
DataPointCategory,
|
|
17
|
+
Interface,
|
|
18
|
+
InternalCustomID,
|
|
19
|
+
)
|
|
20
|
+
from aiohomematic.decorators import inspector
|
|
21
|
+
from aiohomematic.exceptions import AioHomematicException
|
|
22
|
+
from aiohomematic.model import device as hmd
|
|
23
|
+
from aiohomematic.model.data_point import CallbackDataPoint
|
|
24
|
+
from aiohomematic.model.support import DataPointPathData, generate_unique_id
|
|
25
|
+
from aiohomematic.property_decorators import config_property, state_property
|
|
26
|
+
from aiohomematic.support import PayloadMixin
|
|
27
|
+
|
|
28
|
+
__all__ = ["DpUpdate"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DpUpdate(CallbackDataPoint, PayloadMixin):
|
|
32
|
+
"""
|
|
33
|
+
Implementation of a update.
|
|
34
|
+
|
|
35
|
+
This is a default data point that gets automatically generated.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
__slots__ = ("_device",)
|
|
39
|
+
|
|
40
|
+
_category = DataPointCategory.UPDATE
|
|
41
|
+
|
|
42
|
+
def __init__(self, *, device: hmd.Device) -> None:
|
|
43
|
+
"""Init the callback data_point."""
|
|
44
|
+
PayloadMixin.__init__(self)
|
|
45
|
+
self._device: Final = device
|
|
46
|
+
super().__init__(
|
|
47
|
+
central=device.central,
|
|
48
|
+
unique_id=generate_unique_id(central=device.central, address=device.address, parameter="Update"),
|
|
49
|
+
)
|
|
50
|
+
self._set_modified_at(modified_at=datetime.now())
|
|
51
|
+
|
|
52
|
+
@state_property
|
|
53
|
+
def available(self) -> bool:
|
|
54
|
+
"""Return the availability of the device."""
|
|
55
|
+
return self._device.available
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def device(self) -> hmd.Device:
|
|
59
|
+
"""Return the device of the data_point."""
|
|
60
|
+
return self._device
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def full_name(self) -> str:
|
|
64
|
+
"""Return the full name of the data_point."""
|
|
65
|
+
return f"{self._device.name} Update"
|
|
66
|
+
|
|
67
|
+
@config_property
|
|
68
|
+
def name(self) -> str:
|
|
69
|
+
"""Return the name of the data_point."""
|
|
70
|
+
return "Update"
|
|
71
|
+
|
|
72
|
+
@state_property
|
|
73
|
+
def firmware(self) -> str | None:
|
|
74
|
+
"""Version installed and in use."""
|
|
75
|
+
return self._device.firmware
|
|
76
|
+
|
|
77
|
+
@state_property
|
|
78
|
+
def firmware_update_state(self) -> str | None:
|
|
79
|
+
"""Latest version available for install."""
|
|
80
|
+
return self._device.firmware_update_state
|
|
81
|
+
|
|
82
|
+
@state_property
|
|
83
|
+
def in_progress(self) -> bool:
|
|
84
|
+
"""Update installation progress."""
|
|
85
|
+
if self._device.interface == Interface.HMIP_RF:
|
|
86
|
+
return self._device.firmware_update_state in HMIP_FIRMWARE_UPDATE_IN_PROGRESS_STATES
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
@state_property
|
|
90
|
+
def latest_firmware(self) -> str | None:
|
|
91
|
+
"""Latest firmware available for install."""
|
|
92
|
+
if self._device.available_firmware and (
|
|
93
|
+
(
|
|
94
|
+
self._device.interface == Interface.HMIP_RF
|
|
95
|
+
and self._device.firmware_update_state in HMIP_FIRMWARE_UPDATE_READY_STATES
|
|
96
|
+
)
|
|
97
|
+
or self._device.interface in (Interface.BIDCOS_RF, Interface.BIDCOS_WIRED)
|
|
98
|
+
):
|
|
99
|
+
return self._device.available_firmware
|
|
100
|
+
return self._device.firmware
|
|
101
|
+
|
|
102
|
+
def _get_signature(self) -> str:
|
|
103
|
+
"""Return the signature of the data_point."""
|
|
104
|
+
return f"{self._category}/{self._device.model}"
|
|
105
|
+
|
|
106
|
+
def _get_path_data(self) -> DataPointPathData:
|
|
107
|
+
"""Return the path data of the data_point."""
|
|
108
|
+
return DataPointPathData(
|
|
109
|
+
interface=None,
|
|
110
|
+
address=self._device.address,
|
|
111
|
+
channel_no=None,
|
|
112
|
+
kind=DataPointCategory.UPDATE,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def register_data_point_updated_callback(self, *, cb: Callable, custom_id: str) -> CALLBACK_TYPE:
|
|
116
|
+
"""Register update callback."""
|
|
117
|
+
if custom_id != InternalCustomID.DEFAULT:
|
|
118
|
+
if self._custom_id is not None:
|
|
119
|
+
raise AioHomematicException(
|
|
120
|
+
f"REGISTER_UPDATE_CALLBACK failed: hm_data_point: {self.full_name} is already registered by {self._custom_id}"
|
|
121
|
+
)
|
|
122
|
+
self._custom_id = custom_id
|
|
123
|
+
|
|
124
|
+
if self._device.register_firmware_update_callback(cb=cb) is not None:
|
|
125
|
+
return partial(self._unregister_data_point_updated_callback, cb=cb, custom_id=custom_id)
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
def _unregister_data_point_updated_callback(self, *, cb: Callable, custom_id: str) -> None:
|
|
129
|
+
"""Unregister update callback."""
|
|
130
|
+
if custom_id is not None:
|
|
131
|
+
self._custom_id = None
|
|
132
|
+
self._device.unregister_firmware_update_callback(cb=cb)
|
|
133
|
+
|
|
134
|
+
@inspector
|
|
135
|
+
async def update_firmware(self, *, refresh_after_update_intervals: tuple[int, ...]) -> bool:
|
|
136
|
+
"""Turn the update on."""
|
|
137
|
+
return await self._device.update_firmware(refresh_after_update_intervals=refresh_after_update_intervals)
|
|
138
|
+
|
|
139
|
+
@inspector
|
|
140
|
+
async def refresh_firmware_data(self) -> None:
|
|
141
|
+
"""Refresh device firmware data."""
|
|
142
|
+
await self._device.central.refresh_firmware_data(device_address=self._device.address)
|
|
143
|
+
self._set_modified_at(modified_at=datetime.now())
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
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 store on first access and invalidates on set/delete.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from collections.abc import Callable, Mapping
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from enum import Enum, StrEnum
|
|
26
|
+
from typing import Any, Final, ParamSpec, TypeVar, cast, overload
|
|
27
|
+
from weakref import WeakKeyDictionary
|
|
28
|
+
|
|
29
|
+
from aiohomematic import support as hms
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"config_property",
|
|
33
|
+
"get_hm_property_by_kind",
|
|
34
|
+
"info_property",
|
|
35
|
+
"state_property",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
P = ParamSpec("P")
|
|
39
|
+
T = TypeVar("T")
|
|
40
|
+
R = TypeVar("R")
|
|
41
|
+
|
|
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
|
+
|
|
52
|
+
class _GenericProperty[GETTER, SETTER](property):
|
|
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
|
+
"""
|
|
67
|
+
|
|
68
|
+
__kwonly_check__ = False
|
|
69
|
+
|
|
70
|
+
fget: Callable[[Any], GETTER] | None
|
|
71
|
+
fset: Callable[[Any, SETTER], None] | None
|
|
72
|
+
fdel: Callable[[Any], None] | None
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
fget: Callable[[Any], GETTER] | None = None,
|
|
77
|
+
fset: Callable[[Any, SETTER], None] | None = None,
|
|
78
|
+
fdel: Callable[[Any], None] | None = None,
|
|
79
|
+
doc: str | None = None,
|
|
80
|
+
kind: Kind = Kind.SIMPLE,
|
|
81
|
+
cached: bool = False,
|
|
82
|
+
log_context: bool = False,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Initialize the descriptor.
|
|
86
|
+
|
|
87
|
+
Mirrors the standard property signature and adds two options:
|
|
88
|
+
- kind: specify the kind of property (e.g. simple, cached, config, info, state).
|
|
89
|
+
- cached: enable per-instance caching of the computed value.
|
|
90
|
+
- log_context: mark this property as relevant for structured logging.
|
|
91
|
+
"""
|
|
92
|
+
super().__init__(fget, fset, fdel, doc)
|
|
93
|
+
if doc is None and fget is not None:
|
|
94
|
+
doc = fget.__doc__
|
|
95
|
+
self.__doc__ = doc
|
|
96
|
+
self.kind: Final = kind
|
|
97
|
+
self._cached: Final = cached
|
|
98
|
+
self.log_context = log_context
|
|
99
|
+
if cached:
|
|
100
|
+
if fget is not None:
|
|
101
|
+
func_name = fget.__name__
|
|
102
|
+
elif fset is not None:
|
|
103
|
+
func_name = fset.__name__
|
|
104
|
+
elif fdel is not None:
|
|
105
|
+
func_name = fdel.__name__
|
|
106
|
+
else:
|
|
107
|
+
func_name = "prop"
|
|
108
|
+
self._cache_attr = f"_cached_{func_name}"
|
|
109
|
+
|
|
110
|
+
def getter(self, fget: Callable[[Any], GETTER], /) -> _GenericProperty:
|
|
111
|
+
"""Return generic getter."""
|
|
112
|
+
return type(self)(
|
|
113
|
+
fget=fget,
|
|
114
|
+
fset=self.fset,
|
|
115
|
+
fdel=self.fdel,
|
|
116
|
+
doc=self.__doc__,
|
|
117
|
+
kind=self.kind,
|
|
118
|
+
cached=self._cached,
|
|
119
|
+
log_context=self.log_context,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def setter(self, fset: Callable[[Any, SETTER], None], /) -> _GenericProperty:
|
|
123
|
+
"""Return generic setter."""
|
|
124
|
+
return type(self)(
|
|
125
|
+
fget=self.fget,
|
|
126
|
+
fset=fset,
|
|
127
|
+
fdel=self.fdel,
|
|
128
|
+
doc=self.__doc__,
|
|
129
|
+
kind=self.kind,
|
|
130
|
+
cached=self._cached,
|
|
131
|
+
log_context=self.log_context,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def deleter(self, fdel: Callable[[Any], None], /) -> _GenericProperty:
|
|
135
|
+
"""Return generic deleter."""
|
|
136
|
+
return type(self)(
|
|
137
|
+
fget=self.fget,
|
|
138
|
+
fset=self.fset,
|
|
139
|
+
fdel=fdel,
|
|
140
|
+
doc=self.__doc__,
|
|
141
|
+
kind=self.kind,
|
|
142
|
+
cached=self._cached,
|
|
143
|
+
log_context=self.log_context,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def __get__(self, instance: Any, gtype: type | None = None, /) -> GETTER: # type: ignore[override]
|
|
147
|
+
"""
|
|
148
|
+
Return the attribute value.
|
|
149
|
+
|
|
150
|
+
If caching is enabled, compute on first access and return the per-instance
|
|
151
|
+
cached value on subsequent accesses.
|
|
152
|
+
"""
|
|
153
|
+
if instance is None:
|
|
154
|
+
# Accessed from class, return the descriptor itself
|
|
155
|
+
return cast(GETTER, self)
|
|
156
|
+
|
|
157
|
+
if (fget := self.fget) is None:
|
|
158
|
+
raise AttributeError("unreadable attribute")
|
|
159
|
+
|
|
160
|
+
if not self._cached:
|
|
161
|
+
return fget(instance)
|
|
162
|
+
|
|
163
|
+
# Use direct __dict__ access when available for better performance
|
|
164
|
+
# Store cache_attr in local variable to avoid repeated attribute lookup
|
|
165
|
+
cache_attr = self._cache_attr
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
inst_dict = instance.__dict__
|
|
169
|
+
# Use 'in' check first to distinguish between missing and None
|
|
170
|
+
if cache_attr in inst_dict:
|
|
171
|
+
return cast(GETTER, inst_dict[cache_attr])
|
|
172
|
+
|
|
173
|
+
# Not cached yet, compute and store
|
|
174
|
+
value = fget(instance)
|
|
175
|
+
inst_dict[cache_attr] = value
|
|
176
|
+
except AttributeError:
|
|
177
|
+
# Object uses __slots__, fall back to getattr/setattr
|
|
178
|
+
try:
|
|
179
|
+
return cast(GETTER, getattr(instance, cache_attr))
|
|
180
|
+
except AttributeError:
|
|
181
|
+
value = fget(instance)
|
|
182
|
+
setattr(instance, cache_attr, value)
|
|
183
|
+
return value
|
|
184
|
+
|
|
185
|
+
def __set__(self, instance: Any, value: Any, /) -> None:
|
|
186
|
+
"""Set the attribute value and invalidate cache if enabled."""
|
|
187
|
+
# Delete the cached value so it can be recomputed on next access.
|
|
188
|
+
if self._cached:
|
|
189
|
+
try:
|
|
190
|
+
instance.__dict__.pop(self._cache_attr, None)
|
|
191
|
+
except AttributeError:
|
|
192
|
+
# Object uses __slots__, fall back to delattr
|
|
193
|
+
if hasattr(instance, self._cache_attr):
|
|
194
|
+
delattr(instance, self._cache_attr)
|
|
195
|
+
|
|
196
|
+
if self.fset is None:
|
|
197
|
+
raise AttributeError("can't set attribute")
|
|
198
|
+
self.fset(instance, value)
|
|
199
|
+
|
|
200
|
+
def __delete__(self, instance: Any, /) -> None:
|
|
201
|
+
"""Delete the attribute and invalidate cache if enabled."""
|
|
202
|
+
|
|
203
|
+
# Delete the cached value so it can be recomputed on next access.
|
|
204
|
+
if self._cached:
|
|
205
|
+
try:
|
|
206
|
+
instance.__dict__.pop(self._cache_attr, None)
|
|
207
|
+
except AttributeError:
|
|
208
|
+
# Object uses __slots__, fall back to delattr
|
|
209
|
+
if hasattr(instance, self._cache_attr):
|
|
210
|
+
delattr(instance, self._cache_attr)
|
|
211
|
+
|
|
212
|
+
if self.fdel is None:
|
|
213
|
+
raise AttributeError("can't delete attribute")
|
|
214
|
+
self.fdel(instance)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ----- hm_property -----
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@overload
|
|
221
|
+
def hm_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ...
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@overload
|
|
225
|
+
def hm_property(
|
|
226
|
+
*, kind: Kind = ..., cached: bool = ..., log_context: bool = ...
|
|
227
|
+
) -> Callable[[Callable[[Any], R]], _GenericProperty[R, Any]]: ...
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def hm_property[PR](
|
|
231
|
+
func: Callable[[Any], PR] | None = None,
|
|
232
|
+
*,
|
|
233
|
+
kind: Kind = Kind.SIMPLE,
|
|
234
|
+
cached: bool = False,
|
|
235
|
+
log_context: bool = False,
|
|
236
|
+
) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
|
|
237
|
+
"""
|
|
238
|
+
Decorate a method as a computed attribute.
|
|
239
|
+
|
|
240
|
+
Supports both usages:
|
|
241
|
+
- @hm_property
|
|
242
|
+
- @hm_property(kind=..., cached=True, log_context=True)
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
func: The function being decorated when used as @hm_property without
|
|
246
|
+
parentheses. When used as a factory (i.e., @hm_property(...)), this
|
|
247
|
+
is None and the returned callable expects the function to decorate.
|
|
248
|
+
kind: Specify the kind of property (e.g. simple, config, info, state).
|
|
249
|
+
cached: Optionally enable per-instance caching for this property.
|
|
250
|
+
log_context: Include this property in structured log context if True.
|
|
251
|
+
|
|
252
|
+
"""
|
|
253
|
+
if func is None:
|
|
254
|
+
|
|
255
|
+
def wrapper(f: Callable[[Any], PR]) -> _GenericProperty[PR, Any]:
|
|
256
|
+
return _GenericProperty(f, kind=kind, cached=cached, log_context=log_context)
|
|
257
|
+
|
|
258
|
+
return wrapper
|
|
259
|
+
return _GenericProperty(func, kind=kind, cached=cached, log_context=log_context)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ----- config_property -----
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@overload
|
|
266
|
+
def config_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ...
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@overload
|
|
270
|
+
def config_property(
|
|
271
|
+
*, cached: bool = ..., log_context: bool = ...
|
|
272
|
+
) -> Callable[[Callable[[Any], R]], _GenericProperty[R, Any]]: ...
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def config_property[PR](
|
|
276
|
+
func: Callable[[Any], PR] | None = None,
|
|
277
|
+
*,
|
|
278
|
+
cached: bool = False,
|
|
279
|
+
log_context: bool = False,
|
|
280
|
+
) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
|
|
281
|
+
"""
|
|
282
|
+
Decorate a method as a configuration property.
|
|
283
|
+
|
|
284
|
+
Supports both usages:
|
|
285
|
+
- @config_property
|
|
286
|
+
- @config_property(cached=True, log_context=True)
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
func: The function being decorated when used as @config_property without
|
|
290
|
+
parentheses. When used as a factory (i.e., @config_property(...)), this is
|
|
291
|
+
None and the returned callable expects the function to decorate.
|
|
292
|
+
cached: Enable per-instance caching for this property when True.
|
|
293
|
+
log_context: Include this property in structured log context if True.
|
|
294
|
+
|
|
295
|
+
"""
|
|
296
|
+
if func is None:
|
|
297
|
+
|
|
298
|
+
def wrapper(f: Callable[[Any], PR]) -> _GenericProperty[PR, Any]:
|
|
299
|
+
return _GenericProperty(f, kind=Kind.CONFIG, cached=cached, log_context=log_context)
|
|
300
|
+
|
|
301
|
+
return wrapper
|
|
302
|
+
return _GenericProperty(func, kind=Kind.CONFIG, cached=cached, log_context=log_context)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ----- info_property -----
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@overload
|
|
309
|
+
def info_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ...
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@overload
|
|
313
|
+
def info_property(
|
|
314
|
+
*, cached: bool = ..., log_context: bool = ...
|
|
315
|
+
) -> Callable[[Callable[[Any], R]], _GenericProperty[R, Any]]: ...
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def info_property[PR](
|
|
319
|
+
func: Callable[[Any], PR] | None = None,
|
|
320
|
+
*,
|
|
321
|
+
cached: bool = False,
|
|
322
|
+
log_context: bool = False,
|
|
323
|
+
) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
|
|
324
|
+
"""
|
|
325
|
+
Decorate a method as an informational/metadata property.
|
|
326
|
+
|
|
327
|
+
Supports both usages:
|
|
328
|
+
- @info_property
|
|
329
|
+
- @info_property(cached=True, log_context=True)
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
func: The function being decorated when used as @info_property without
|
|
333
|
+
parentheses. When used as a factory (i.e., @info_property(...)), this is
|
|
334
|
+
None and the returned callable expects the function to decorate.
|
|
335
|
+
cached: Enable per-instance caching for this property when True.
|
|
336
|
+
log_context: Include this property in structured log context if True.
|
|
337
|
+
|
|
338
|
+
"""
|
|
339
|
+
if func is None:
|
|
340
|
+
|
|
341
|
+
def wrapper(f: Callable[[Any], PR]) -> _GenericProperty[PR, Any]:
|
|
342
|
+
return _GenericProperty(f, kind=Kind.INFO, cached=cached, log_context=log_context)
|
|
343
|
+
|
|
344
|
+
return wrapper
|
|
345
|
+
return _GenericProperty(func, kind=Kind.INFO, cached=cached, log_context=log_context)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ----- state_property -----
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@overload
|
|
352
|
+
def state_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ...
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@overload
|
|
356
|
+
def state_property(
|
|
357
|
+
*, cached: bool = ..., log_context: bool = ...
|
|
358
|
+
) -> Callable[[Callable[[Any], R]], _GenericProperty[R, Any]]: ...
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def state_property[PR](
|
|
362
|
+
func: Callable[[Any], PR] | None = None,
|
|
363
|
+
*,
|
|
364
|
+
cached: bool = False,
|
|
365
|
+
log_context: bool = False,
|
|
366
|
+
) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
|
|
367
|
+
"""
|
|
368
|
+
Decorate a method as a dynamic state property.
|
|
369
|
+
|
|
370
|
+
Supports both usages:
|
|
371
|
+
- @state_property
|
|
372
|
+
- @state_property(cached=True, log_context=True)
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
func: The function being decorated when used as @state_property without
|
|
376
|
+
parentheses. When used as a factory (i.e., @state_property(...)), this is
|
|
377
|
+
None and the returned callable expects the function to decorate.
|
|
378
|
+
cached: Enable per-instance caching for this property when True.
|
|
379
|
+
log_context: Include this property in structured log context if True.
|
|
380
|
+
|
|
381
|
+
"""
|
|
382
|
+
if func is None:
|
|
383
|
+
|
|
384
|
+
def wrapper(f: Callable[[Any], PR]) -> _GenericProperty[PR, Any]:
|
|
385
|
+
return _GenericProperty(f, kind=Kind.STATE, cached=cached, log_context=log_context)
|
|
386
|
+
|
|
387
|
+
return wrapper
|
|
388
|
+
return _GenericProperty(func, kind=Kind.STATE, cached=cached, log_context=log_context)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# ----------
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# Cache for per-class attribute names by decorator to avoid repeated dir() scans
|
|
395
|
+
# Use WeakKeyDictionary to allow classes to be garbage-collected without leaking cache entries.
|
|
396
|
+
# Structure: {cls: {decorator_class: (attr_name1, attr_name2, ...)}}
|
|
397
|
+
_PUBLIC_ATTR_CACHE: WeakKeyDictionary[type, dict[Kind, tuple[str, ...]]] = WeakKeyDictionary()
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def get_hm_property_by_kind(data_object: Any, kind: Kind, context: bool = False) -> Mapping[str, Any]:
|
|
401
|
+
"""
|
|
402
|
+
Collect properties from an object that are defined using a specific decorator.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
data_object: The instance to inspect.
|
|
406
|
+
kind: The decorator class to use for filtering.
|
|
407
|
+
context: If True, only include properties where the descriptor has
|
|
408
|
+
log_context=True. When such a property's value is a LogContextMixin, its
|
|
409
|
+
items are flattened into the result using a short prefix of the property
|
|
410
|
+
name (e.g. "p.key").
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
Mapping[str, Any]: A mapping of attribute name to normalized value. Values are converted via
|
|
414
|
+
_get_text_value() to provide stable JSON/log-friendly types.
|
|
415
|
+
|
|
416
|
+
Notes:
|
|
417
|
+
Attribute NAMES are cached per (class, decorator) to avoid repeated dir()
|
|
418
|
+
scans. Values are never cached here since they are instance-dependent.
|
|
419
|
+
Getter exceptions are swallowed and represented as None so payload building
|
|
420
|
+
remains robust and side-effect free.
|
|
421
|
+
|
|
422
|
+
"""
|
|
423
|
+
cls = data_object.__class__
|
|
424
|
+
|
|
425
|
+
# Get or create the per-class cache dict
|
|
426
|
+
if (decorator_cache := _PUBLIC_ATTR_CACHE.get(cls)) is None:
|
|
427
|
+
decorator_cache = {}
|
|
428
|
+
_PUBLIC_ATTR_CACHE[cls] = decorator_cache
|
|
429
|
+
|
|
430
|
+
if (names := decorator_cache.get(kind)) is None:
|
|
431
|
+
names = tuple(
|
|
432
|
+
y for y in dir(cls) if (gp := getattr(cls, y)) and isinstance(gp, _GenericProperty) and gp.kind == kind
|
|
433
|
+
)
|
|
434
|
+
decorator_cache[kind] = names
|
|
435
|
+
|
|
436
|
+
result: dict[str, Any] = {}
|
|
437
|
+
for name in names:
|
|
438
|
+
if context and getattr(cls, name).log_context is False:
|
|
439
|
+
continue
|
|
440
|
+
try:
|
|
441
|
+
value = getattr(data_object, name)
|
|
442
|
+
if isinstance(value, hms.LogContextMixin):
|
|
443
|
+
result.update({f"{name[:1]}.{k}": v for k, v in value.log_context.items()})
|
|
444
|
+
else:
|
|
445
|
+
result[name] = _get_text_value(value)
|
|
446
|
+
except Exception:
|
|
447
|
+
# Avoid propagating side effects/errors from getters
|
|
448
|
+
result[name] = None
|
|
449
|
+
return result
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _get_text_value(value: Any) -> Any:
|
|
453
|
+
"""
|
|
454
|
+
Normalize values for payload/logging purposes.
|
|
455
|
+
|
|
456
|
+
- list/tuple/set are converted to tuples and their items normalized recursively
|
|
457
|
+
- Enum values are converted to their string representation
|
|
458
|
+
- datetime objects are converted to unix timestamps (float)
|
|
459
|
+
- all other types are returned unchanged
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
value: The input value to normalize into a log-/JSON-friendly representation.
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Any: The normalized value, potentially converted as described above.
|
|
466
|
+
|
|
467
|
+
"""
|
|
468
|
+
if isinstance(value, list | tuple | set):
|
|
469
|
+
return tuple(_get_text_value(v) for v in value)
|
|
470
|
+
if isinstance(value, Enum):
|
|
471
|
+
return str(value)
|
|
472
|
+
if isinstance(value, datetime):
|
|
473
|
+
return datetime.timestamp(value)
|
|
474
|
+
return value
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def get_hm_property_by_log_context(data_object: Any) -> Mapping[str, Any]:
|
|
478
|
+
"""
|
|
479
|
+
Return combined log context attributes across all property categories.
|
|
480
|
+
|
|
481
|
+
Includes only properties declared with log_context=True and flattens
|
|
482
|
+
values that implement LogContextMixin by prefixing with a short key.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
data_object: The instance from which to collect attributes marked for
|
|
486
|
+
log context across all property categories.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
Mapping[str, Any]: A mapping of attribute name to normalized value for logging.
|
|
490
|
+
|
|
491
|
+
"""
|
|
492
|
+
result: dict[str, Any] = {}
|
|
493
|
+
for kind in Kind:
|
|
494
|
+
result.update(get_hm_property_by_kind(data_object=data_object, kind=kind, context=True))
|
|
495
|
+
|
|
496
|
+
return result
|
aiohomematic/py.typed
ADDED
|
File without changes
|