aiohomematic 2025.8.10__py3-none-any.whl → 2025.9.2__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 +1 -6
- aiohomematic/central/__init__.py +35 -24
- aiohomematic/central/xml_rpc_server.py +1 -1
- aiohomematic/client/__init__.py +35 -29
- aiohomematic/client/json_rpc.py +44 -12
- aiohomematic/client/xml_rpc.py +53 -20
- aiohomematic/const.py +2 -2
- aiohomematic/decorators.py +66 -27
- aiohomematic/model/__init__.py +1 -1
- aiohomematic/model/calculated/__init__.py +1 -1
- aiohomematic/model/calculated/climate.py +1 -1
- aiohomematic/model/calculated/data_point.py +2 -2
- aiohomematic/model/calculated/operating_voltage_level.py +7 -21
- aiohomematic/model/calculated/support.py +20 -0
- aiohomematic/model/custom/__init__.py +1 -1
- aiohomematic/model/custom/climate.py +18 -18
- aiohomematic/model/custom/cover.py +1 -1
- aiohomematic/model/custom/data_point.py +1 -1
- aiohomematic/model/custom/light.py +1 -1
- aiohomematic/model/custom/lock.py +1 -1
- aiohomematic/model/custom/siren.py +1 -1
- aiohomematic/model/custom/switch.py +1 -1
- aiohomematic/model/custom/valve.py +1 -1
- aiohomematic/model/data_point.py +18 -18
- aiohomematic/model/device.py +21 -20
- aiohomematic/model/event.py +3 -8
- aiohomematic/model/generic/__init__.py +1 -1
- aiohomematic/model/generic/binary_sensor.py +1 -1
- aiohomematic/model/generic/button.py +1 -1
- aiohomematic/model/generic/data_point.py +3 -5
- aiohomematic/model/generic/number.py +1 -1
- aiohomematic/model/generic/select.py +1 -1
- aiohomematic/model/generic/sensor.py +1 -1
- aiohomematic/model/generic/switch.py +4 -4
- aiohomematic/model/generic/text.py +1 -1
- aiohomematic/model/hub/binary_sensor.py +1 -1
- aiohomematic/model/hub/button.py +2 -2
- aiohomematic/model/hub/data_point.py +4 -7
- aiohomematic/model/hub/number.py +1 -1
- aiohomematic/model/hub/select.py +2 -2
- aiohomematic/model/hub/sensor.py +1 -1
- aiohomematic/model/hub/switch.py +3 -3
- aiohomematic/model/hub/text.py +1 -1
- aiohomematic/model/support.py +1 -40
- aiohomematic/model/update.py +5 -4
- aiohomematic/property_decorators.py +327 -0
- aiohomematic/support.py +70 -85
- {aiohomematic-2025.8.10.dist-info → aiohomematic-2025.9.2.dist-info}/METADATA +8 -5
- aiohomematic-2025.9.2.dist-info/RECORD +78 -0
- aiohomematic_support/client_local.py +5 -5
- aiohomematic/model/decorators.py +0 -194
- aiohomematic-2025.8.10.dist-info/RECORD +0 -78
- {aiohomematic-2025.8.10.dist-info → aiohomematic-2025.9.2.dist-info}/WHEEL +0 -0
- {aiohomematic-2025.8.10.dist-info → aiohomematic-2025.9.2.dist-info}/licenses/LICENSE +0 -0
- {aiohomematic-2025.8.10.dist-info → aiohomematic-2025.9.2.dist-info}/top_level.txt +0 -0
aiohomematic/client/xml_rpc.py
CHANGED
|
@@ -42,7 +42,8 @@ from aiohomematic.exceptions import (
|
|
|
42
42
|
NoConnectionException,
|
|
43
43
|
UnsupportedException,
|
|
44
44
|
)
|
|
45
|
-
from aiohomematic.
|
|
45
|
+
from aiohomematic.property_decorators import info_property
|
|
46
|
+
from aiohomematic.support import LogContextMixin, extract_exc_args, get_tls_context, log_boundary_error
|
|
46
47
|
|
|
47
48
|
_LOGGER: Final = logging.getLogger(__name__)
|
|
48
49
|
|
|
@@ -83,7 +84,7 @@ _OS_ERROR_CODES: Final[dict[int, str]] = {
|
|
|
83
84
|
|
|
84
85
|
|
|
85
86
|
# noinspection PyProtectedMember,PyUnresolvedReferences
|
|
86
|
-
class XmlRpcProxy(xmlrpc.client.ServerProxy):
|
|
87
|
+
class XmlRpcProxy(xmlrpc.client.ServerProxy, LogContextMixin):
|
|
87
88
|
"""ServerProxy implementation with ThreadPoolExecutor when request is executing."""
|
|
88
89
|
|
|
89
90
|
def __init__(
|
|
@@ -95,7 +96,7 @@ class XmlRpcProxy(xmlrpc.client.ServerProxy):
|
|
|
95
96
|
**kwargs: Any,
|
|
96
97
|
) -> None:
|
|
97
98
|
"""Initialize new proxy for server and get local ip."""
|
|
98
|
-
self.
|
|
99
|
+
self._interface_id: Final = interface_id
|
|
99
100
|
self._connection_state: Final = connection_state
|
|
100
101
|
self._looper: Final = Looper()
|
|
101
102
|
self._proxy_executor: Final = (
|
|
@@ -120,11 +121,21 @@ class XmlRpcProxy(xmlrpc.client.ServerProxy):
|
|
|
120
121
|
supported_methods.append(_XmlRpcMethod.PING)
|
|
121
122
|
self._supported_methods = tuple(supported_methods)
|
|
122
123
|
|
|
124
|
+
@info_property(log_context=True)
|
|
125
|
+
def interface_id(self) -> str:
|
|
126
|
+
"""Return the interface_id."""
|
|
127
|
+
return self._interface_id
|
|
128
|
+
|
|
123
129
|
@property
|
|
124
130
|
def supported_methods(self) -> tuple[str, ...]:
|
|
125
131
|
"""Return the supported methods."""
|
|
126
132
|
return self._supported_methods
|
|
127
133
|
|
|
134
|
+
@info_property(log_context=True)
|
|
135
|
+
def tls(self) -> bool:
|
|
136
|
+
"""Return tls."""
|
|
137
|
+
return self._tls
|
|
138
|
+
|
|
128
139
|
async def __async_request(self, *args, **kwargs): # type: ignore[no-untyped-def]
|
|
129
140
|
"""Call method on server side."""
|
|
130
141
|
parent = xmlrpc.client.ServerProxy
|
|
@@ -134,7 +145,7 @@ class XmlRpcProxy(xmlrpc.client.ServerProxy):
|
|
|
134
145
|
raise UnsupportedException(f"__ASYNC_REQUEST: method '{method} not supported by backend.")
|
|
135
146
|
|
|
136
147
|
if method in _VALID_XMLRPC_COMMANDS_ON_NO_CONNECTION or not self._connection_state.has_issue(
|
|
137
|
-
issuer=self, iid=self.
|
|
148
|
+
issuer=self, iid=self._interface_id
|
|
138
149
|
):
|
|
139
150
|
args = _cleanup_args(*args)
|
|
140
151
|
_LOGGER.debug("__ASYNC_REQUEST: %s", args)
|
|
@@ -148,35 +159,57 @@ class XmlRpcProxy(xmlrpc.client.ServerProxy):
|
|
|
148
159
|
executor=self._proxy_executor,
|
|
149
160
|
)
|
|
150
161
|
)
|
|
151
|
-
self._connection_state.remove_issue(issuer=self, iid=self.
|
|
162
|
+
self._connection_state.remove_issue(issuer=self, iid=self._interface_id)
|
|
152
163
|
return result
|
|
153
|
-
raise NoConnectionException(f"No connection to {self.
|
|
164
|
+
raise NoConnectionException(f"No connection to {self._interface_id}")
|
|
154
165
|
except BaseHomematicException:
|
|
155
166
|
raise
|
|
156
167
|
except SSLError as sslerr:
|
|
157
|
-
message = f"SSLError on {self.
|
|
168
|
+
message = f"SSLError on {self._interface_id}: {extract_exc_args(exc=sslerr)}"
|
|
169
|
+
level = logging.ERROR
|
|
158
170
|
if sslerr.args[0] in _SSL_ERROR_CODES:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
171
|
+
message = (
|
|
172
|
+
f"{message} - {sslerr.args[0]}: {sslerr.args[1]}. "
|
|
173
|
+
f"Please check your configuration for {self._interface_id}."
|
|
174
|
+
)
|
|
175
|
+
if not self._connection_state.add_issue(issuer=self, iid=self._interface_id):
|
|
176
|
+
level = logging.DEBUG
|
|
177
|
+
|
|
178
|
+
log_boundary_error(
|
|
179
|
+
logger=_LOGGER,
|
|
180
|
+
boundary="xml-rpc",
|
|
181
|
+
action=str(args[0]),
|
|
182
|
+
err=sslerr,
|
|
183
|
+
level=level,
|
|
184
|
+
message=message,
|
|
185
|
+
log_context=self.log_context,
|
|
186
|
+
)
|
|
162
187
|
raise NoConnectionException(message) from sslerr
|
|
163
188
|
except OSError as oserr:
|
|
164
|
-
message = f"OSError on {self.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
189
|
+
message = f"OSError on {self._interface_id}: {extract_exc_args(exc=oserr)}"
|
|
190
|
+
level = (
|
|
191
|
+
logging.ERROR
|
|
192
|
+
if oserr.args[0] in _OS_ERROR_CODES
|
|
193
|
+
and not self._connection_state.add_issue(issuer=self, iid=self._interface_id)
|
|
194
|
+
else logging.DEBUG
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
log_boundary_error(
|
|
198
|
+
logger=_LOGGER,
|
|
199
|
+
boundary="xml-rpc",
|
|
200
|
+
action=str(args[0]),
|
|
201
|
+
err=oserr,
|
|
202
|
+
level=level,
|
|
203
|
+
log_context=self.log_context,
|
|
204
|
+
)
|
|
172
205
|
raise NoConnectionException(message) from oserr
|
|
173
206
|
except xmlrpc.client.Fault as flt:
|
|
174
|
-
ctx = RpcContext(protocol="xml-rpc", method=str(args[0]), interface=self.
|
|
207
|
+
ctx = RpcContext(protocol="xml-rpc", method=str(args[0]), interface=self._interface_id)
|
|
175
208
|
raise map_xmlrpc_fault(code=flt.faultCode, fault_string=flt.faultString, ctx=ctx) from flt
|
|
176
209
|
except TypeError as terr:
|
|
177
210
|
raise ClientException(terr) from terr
|
|
178
211
|
except xmlrpc.client.ProtocolError as perr:
|
|
179
|
-
if not self._connection_state.has_issue(issuer=self, iid=self.
|
|
212
|
+
if not self._connection_state.has_issue(issuer=self, iid=self._interface_id):
|
|
180
213
|
if perr.errmsg == "Unauthorized":
|
|
181
214
|
raise AuthFailure(perr) from perr
|
|
182
215
|
raise NoConnectionException(perr.errmsg) from perr
|
aiohomematic/const.py
CHANGED
|
@@ -19,7 +19,7 @@ import sys
|
|
|
19
19
|
from types import MappingProxyType
|
|
20
20
|
from typing import Any, Final, NamedTuple, Required, TypeAlias, TypedDict
|
|
21
21
|
|
|
22
|
-
VERSION: Final = "2025.
|
|
22
|
+
VERSION: Final = "2025.9.2"
|
|
23
23
|
|
|
24
24
|
# Detect test speedup mode via environment
|
|
25
25
|
_TEST_SPEEDUP: Final = (
|
|
@@ -856,7 +856,7 @@ __all__ = tuple(
|
|
|
856
856
|
)
|
|
857
857
|
and (
|
|
858
858
|
getattr(obj, "__module__", __name__) == __name__
|
|
859
|
-
if not isinstance(obj,
|
|
859
|
+
if not isinstance(obj, int | float | str | bytes | tuple | frozenset | dict)
|
|
860
860
|
else True
|
|
861
861
|
)
|
|
862
862
|
)
|
aiohomematic/decorators.py
CHANGED
|
@@ -13,43 +13,73 @@ from functools import wraps
|
|
|
13
13
|
import inspect
|
|
14
14
|
import logging
|
|
15
15
|
from time import monotonic
|
|
16
|
-
from typing import Any, Final,
|
|
16
|
+
from typing import Any, Final, cast, overload
|
|
17
17
|
from weakref import WeakKeyDictionary
|
|
18
18
|
|
|
19
19
|
from aiohomematic.context import IN_SERVICE_VAR
|
|
20
20
|
from aiohomematic.exceptions import BaseHomematicException
|
|
21
|
-
from aiohomematic.support import
|
|
21
|
+
from aiohomematic.support import LogContextMixin, log_boundary_error
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
R = TypeVar("R")
|
|
25
|
-
|
|
26
|
-
_LOGGER: Final = logging.getLogger(__name__)
|
|
23
|
+
_LOGGER_PERFORMANCE: Final = logging.getLogger(f"{__package__}.performance")
|
|
27
24
|
|
|
28
25
|
# Cache for per-class service call method names to avoid repeated scans.
|
|
29
26
|
# Structure: {cls: (method_name1, method_name2, ...)}
|
|
30
27
|
_SERVICE_CALLS_CACHE: WeakKeyDictionary[type, tuple[str, ...]] = WeakKeyDictionary()
|
|
31
28
|
|
|
32
29
|
|
|
33
|
-
|
|
30
|
+
@overload
|
|
31
|
+
def inspector[**P, R](
|
|
32
|
+
func: Callable[P, R],
|
|
33
|
+
/,
|
|
34
|
+
*,
|
|
35
|
+
log_level: int = ...,
|
|
36
|
+
re_raise: bool = ...,
|
|
37
|
+
no_raise_return: Any = ...,
|
|
38
|
+
measure_performance: bool = ...,
|
|
39
|
+
) -> Callable[P, R]: ...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@overload
|
|
43
|
+
def inspector[**P, R](
|
|
44
|
+
func: None = ...,
|
|
45
|
+
/,
|
|
46
|
+
*,
|
|
47
|
+
log_level: int = ...,
|
|
48
|
+
re_raise: bool = ...,
|
|
49
|
+
no_raise_return: Any = ...,
|
|
50
|
+
measure_performance: bool = ...,
|
|
51
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def inspector[**P, R]( # noqa: C901
|
|
55
|
+
func: Callable[P, R] | None = None,
|
|
56
|
+
/,
|
|
57
|
+
*,
|
|
34
58
|
log_level: int = logging.ERROR,
|
|
35
59
|
re_raise: bool = True,
|
|
36
60
|
no_raise_return: Any = None,
|
|
37
61
|
measure_performance: bool = False,
|
|
38
|
-
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
62
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]] | Callable[P, R]:
|
|
39
63
|
"""
|
|
40
64
|
Support with exception handling and performance measurement.
|
|
41
65
|
|
|
42
66
|
A decorator that works for both synchronous and asynchronous functions,
|
|
43
67
|
providing common functionality such as exception handling and performance measurement.
|
|
44
68
|
|
|
69
|
+
Can be used both with and without parameters:
|
|
70
|
+
- @inspector
|
|
71
|
+
- @inspector(log_level=logging.ERROR, re_raise=True, ...)
|
|
72
|
+
|
|
45
73
|
Args:
|
|
74
|
+
func: The function to decorate when used without parameters.
|
|
46
75
|
log_level: Logging level for exceptions.
|
|
47
76
|
re_raise: Whether to re-raise exceptions.
|
|
48
77
|
no_raise_return: Value to return when an exception is caught and not re-raised.
|
|
49
78
|
measure_performance: Whether to measure function execution time.
|
|
50
79
|
|
|
51
80
|
Returns:
|
|
52
|
-
|
|
81
|
+
Either the decorated function (when used without parameters) or
|
|
82
|
+
a decorator that wraps sync or async functions (when used with parameters).
|
|
53
83
|
|
|
54
84
|
"""
|
|
55
85
|
|
|
@@ -71,16 +101,16 @@ def inspector( # noqa: C901
|
|
|
71
101
|
"""Handle exceptions for decorated functions with structured logging."""
|
|
72
102
|
if not is_sub_service_call and log_level > logging.NOTSET:
|
|
73
103
|
logger = logging.getLogger(func.__module__)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
log_context = context_obj.log_context if isinstance(context_obj, LogContextMixin) else None
|
|
105
|
+
# Reuse centralized boundary logging to ensure consistent 'extra' structure
|
|
106
|
+
log_boundary_error(
|
|
107
|
+
logger=logger,
|
|
108
|
+
boundary="service",
|
|
109
|
+
action=func.__name__,
|
|
110
|
+
err=exc,
|
|
111
|
+
level=log_level,
|
|
112
|
+
log_context=log_context,
|
|
113
|
+
)
|
|
84
114
|
if re_raise or not is_homematic:
|
|
85
115
|
raise exc
|
|
86
116
|
return cast(R, no_raise_return)
|
|
@@ -89,7 +119,9 @@ def inspector( # noqa: C901
|
|
|
89
119
|
def wrap_sync_function(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
90
120
|
"""Wrap sync functions."""
|
|
91
121
|
|
|
92
|
-
start =
|
|
122
|
+
start = (
|
|
123
|
+
monotonic() if measure_performance and _LOGGER_PERFORMANCE.isEnabledFor(level=logging.DEBUG) else None
|
|
124
|
+
)
|
|
93
125
|
token = IN_SERVICE_VAR.set(True) if not IN_SERVICE_VAR.get() else None
|
|
94
126
|
try:
|
|
95
127
|
return_value: R = func(*args, **kwargs)
|
|
@@ -125,7 +157,9 @@ def inspector( # noqa: C901
|
|
|
125
157
|
async def wrap_async_function(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
126
158
|
"""Wrap async functions."""
|
|
127
159
|
|
|
128
|
-
start =
|
|
160
|
+
start = (
|
|
161
|
+
monotonic() if measure_performance and _LOGGER_PERFORMANCE.isEnabledFor(level=logging.DEBUG) else None
|
|
162
|
+
)
|
|
129
163
|
token = IN_SERVICE_VAR.set(True) if not IN_SERVICE_VAR.get() else None
|
|
130
164
|
try:
|
|
131
165
|
return_value = await func(*args, **kwargs) # type: ignore[misc] # Await the async call
|
|
@@ -164,10 +198,15 @@ def inspector( # noqa: C901
|
|
|
164
198
|
setattr(wrap_sync_function, "ha_service", True)
|
|
165
199
|
return wrap_sync_function
|
|
166
200
|
|
|
201
|
+
# If used without parameters: @inspector
|
|
202
|
+
if func is not None:
|
|
203
|
+
return create_wrapped_decorator(func)
|
|
204
|
+
|
|
205
|
+
# If used with parameters: @inspector(...)
|
|
167
206
|
return create_wrapped_decorator
|
|
168
207
|
|
|
169
208
|
|
|
170
|
-
def _log_performance_message(func: Callable, start: float, *args: P.args, **kwargs: P.kwargs) -> None:
|
|
209
|
+
def _log_performance_message[**P](func: Callable[P, Any], start: float, *args: P.args, **kwargs: P.kwargs) -> None:
|
|
171
210
|
delta = monotonic() - start
|
|
172
211
|
caller = str(args[0]) if len(args) > 0 else ""
|
|
173
212
|
|
|
@@ -181,7 +220,7 @@ def _log_performance_message(func: Callable, start: float, *args: P.args, **kwar
|
|
|
181
220
|
if iface:
|
|
182
221
|
message += f"/{iface}"
|
|
183
222
|
|
|
184
|
-
|
|
223
|
+
_LOGGER_PERFORMANCE.info(message)
|
|
185
224
|
|
|
186
225
|
|
|
187
226
|
def get_service_calls(obj: object) -> dict[str, Callable]:
|
|
@@ -222,7 +261,7 @@ def measure_execution_time[CallableT: Callable[..., Any]](func: CallableT) -> Ca
|
|
|
222
261
|
async def async_measure_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
223
262
|
"""Wrap method."""
|
|
224
263
|
|
|
225
|
-
start = monotonic() if
|
|
264
|
+
start = monotonic() if _LOGGER_PERFORMANCE.isEnabledFor(level=logging.DEBUG) else None
|
|
226
265
|
try:
|
|
227
266
|
return await func(*args, **kwargs)
|
|
228
267
|
finally:
|
|
@@ -233,7 +272,7 @@ def measure_execution_time[CallableT: Callable[..., Any]](func: CallableT) -> Ca
|
|
|
233
272
|
def measure_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
234
273
|
"""Wrap method."""
|
|
235
274
|
|
|
236
|
-
start = monotonic() if
|
|
275
|
+
start = monotonic() if _LOGGER_PERFORMANCE.isEnabledFor(level=logging.DEBUG) else None
|
|
237
276
|
try:
|
|
238
277
|
return func(*args, **kwargs)
|
|
239
278
|
finally:
|
|
@@ -241,8 +280,8 @@ def measure_execution_time[CallableT: Callable[..., Any]](func: CallableT) -> Ca
|
|
|
241
280
|
_log_performance_message(func, start, *args, **kwargs)
|
|
242
281
|
|
|
243
282
|
if inspect.iscoroutinefunction(func):
|
|
244
|
-
return async_measure_wrapper
|
|
245
|
-
return measure_wrapper
|
|
283
|
+
return cast(CallableT, async_measure_wrapper)
|
|
284
|
+
return cast(CallableT, measure_wrapper)
|
|
246
285
|
|
|
247
286
|
|
|
248
287
|
# Define public API for this module
|
aiohomematic/model/__init__.py
CHANGED
|
@@ -45,7 +45,7 @@ _ALLOWED_INTERNAL_PARAMETERS: Final[tuple[Parameter, ...]] = (Parameter.DIRECTIO
|
|
|
45
45
|
_LOGGER: Final = logging.getLogger(__name__)
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
@inspector
|
|
48
|
+
@inspector
|
|
49
49
|
def create_data_points_and_events(device: hmd.Device) -> None:
|
|
50
50
|
"""Create the data points associated to this device."""
|
|
51
51
|
for channel in device.channels.values():
|
|
@@ -59,7 +59,7 @@ _CALCULATED_DATA_POINTS: Final = (ApparentTemperature, DewPoint, FrostPoint, Ope
|
|
|
59
59
|
_LOGGER: Final = logging.getLogger(__name__)
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
@inspector
|
|
62
|
+
@inspector
|
|
63
63
|
def create_calculated_data_points(channel: hmd.Channel) -> None:
|
|
64
64
|
"""Decides which data point category should be used, and creates the required data points."""
|
|
65
65
|
for dp in _CALCULATED_DATA_POINTS:
|
|
@@ -16,8 +16,8 @@ from aiohomematic.model.calculated.support import (
|
|
|
16
16
|
calculate_frost_point,
|
|
17
17
|
calculate_vapor_concentration,
|
|
18
18
|
)
|
|
19
|
-
from aiohomematic.model.decorators import state_property
|
|
20
19
|
from aiohomematic.model.generic import DpSensor
|
|
20
|
+
from aiohomematic.property_decorators import state_property
|
|
21
21
|
from aiohomematic.support import element_matches_key
|
|
22
22
|
|
|
23
23
|
_LOGGER: Final = logging.getLogger(__name__)
|
|
@@ -23,7 +23,6 @@ from aiohomematic.const import (
|
|
|
23
23
|
from aiohomematic.model import device as hmd
|
|
24
24
|
from aiohomematic.model.custom import definition as hmed
|
|
25
25
|
from aiohomematic.model.data_point import BaseDataPoint, NoneTypeDataPoint
|
|
26
|
-
from aiohomematic.model.decorators import cached_slot_property, config_property, state_property
|
|
27
26
|
from aiohomematic.model.generic import data_point as hmge
|
|
28
27
|
from aiohomematic.model.support import (
|
|
29
28
|
DataPointNameData,
|
|
@@ -33,6 +32,7 @@ from aiohomematic.model.support import (
|
|
|
33
32
|
generate_unique_id,
|
|
34
33
|
get_data_point_name_data,
|
|
35
34
|
)
|
|
35
|
+
from aiohomematic.property_decorators import cached_slot_property, config_property, state_property
|
|
36
36
|
|
|
37
37
|
_LOGGER: Final = logging.getLogger(__name__)
|
|
38
38
|
|
|
@@ -306,7 +306,7 @@ class CalculatedDataPoint[ParameterT: GenericParameterType](BaseDataPoint):
|
|
|
306
306
|
@property
|
|
307
307
|
def _should_fire_data_point_updated_callback(self) -> bool:
|
|
308
308
|
"""Check if a data point has been updated or refreshed."""
|
|
309
|
-
if self.fired_recently:
|
|
309
|
+
if self.fired_recently: # pylint: disable=using-constant-test
|
|
310
310
|
return False
|
|
311
311
|
|
|
312
312
|
if (relevant_values_data_point := self._relevant_values_data_points) is not None and len(
|
|
@@ -13,8 +13,9 @@ from typing import Any, Final
|
|
|
13
13
|
from aiohomematic.const import CalulatedParameter, DataPointCategory, Parameter, ParameterType, ParamsetKey
|
|
14
14
|
from aiohomematic.model import device as hmd
|
|
15
15
|
from aiohomematic.model.calculated.data_point import CalculatedDataPoint
|
|
16
|
-
from aiohomematic.model.
|
|
16
|
+
from aiohomematic.model.calculated.support import calculate_operating_voltage_level
|
|
17
17
|
from aiohomematic.model.generic import DpFloat, DpSensor
|
|
18
|
+
from aiohomematic.property_decorators import state_property
|
|
18
19
|
from aiohomematic.support import element_matches_key, extract_exc_args
|
|
19
20
|
|
|
20
21
|
_BATTERY_QTY: Final = "Battery Qty"
|
|
@@ -139,32 +140,17 @@ class OperatingVoltageLevel[SensorT: float | None](CalculatedDataPoint[SensorT])
|
|
|
139
140
|
def value(self) -> float | None:
|
|
140
141
|
"""Return the value."""
|
|
141
142
|
try:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
min(
|
|
148
|
-
100,
|
|
149
|
-
float(
|
|
150
|
-
round(
|
|
151
|
-
(
|
|
152
|
-
(float(self._dp_operating_voltage.value) - low_bat_limit)
|
|
153
|
-
/ (self._voltage_max - low_bat_limit)
|
|
154
|
-
* 100
|
|
155
|
-
),
|
|
156
|
-
1,
|
|
157
|
-
)
|
|
158
|
-
),
|
|
159
|
-
),
|
|
160
|
-
)
|
|
143
|
+
return calculate_operating_voltage_level(
|
|
144
|
+
operating_voltage=self._dp_operating_voltage.value,
|
|
145
|
+
low_bat_limit=self._low_bat_limit_default,
|
|
146
|
+
voltage_max=self._voltage_max,
|
|
147
|
+
)
|
|
161
148
|
except Exception as exc:
|
|
162
149
|
_LOGGER.debug(
|
|
163
150
|
"OperatingVoltageLevel: Failed to calculate sensor for %s: %s",
|
|
164
151
|
self._channel.name,
|
|
165
152
|
extract_exc_args(exc=exc),
|
|
166
153
|
)
|
|
167
|
-
return None
|
|
168
154
|
return None
|
|
169
155
|
|
|
170
156
|
@property
|
|
@@ -174,3 +174,23 @@ def calculate_frost_point(temperature: float, humidity: int) -> float | None:
|
|
|
174
174
|
extract_exc_args(exc=verr),
|
|
175
175
|
)
|
|
176
176
|
return None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def calculate_operating_voltage_level(
|
|
180
|
+
operating_voltage: float | None, low_bat_limit: float | None, voltage_max: float | None
|
|
181
|
+
) -> float | None:
|
|
182
|
+
"""Return the operating voltage level."""
|
|
183
|
+
if operating_voltage is None or low_bat_limit is None or voltage_max is None:
|
|
184
|
+
return None
|
|
185
|
+
return max(
|
|
186
|
+
0,
|
|
187
|
+
min(
|
|
188
|
+
100,
|
|
189
|
+
float(
|
|
190
|
+
round(
|
|
191
|
+
((float(operating_voltage) - low_bat_limit) / (voltage_max - low_bat_limit) * 100),
|
|
192
|
+
1,
|
|
193
|
+
)
|
|
194
|
+
),
|
|
195
|
+
),
|
|
196
|
+
)
|
|
@@ -151,7 +151,7 @@ __all__ = [
|
|
|
151
151
|
_LOGGER: Final = logging.getLogger(__name__)
|
|
152
152
|
|
|
153
153
|
|
|
154
|
-
@inspector
|
|
154
|
+
@inspector
|
|
155
155
|
def create_custom_data_points(device: hmd.Device) -> None:
|
|
156
156
|
"""Decides which data point category should be used, and creates the required data points."""
|
|
157
157
|
|
|
@@ -26,8 +26,8 @@ from aiohomematic.model.custom.const import DeviceProfile, Field
|
|
|
26
26
|
from aiohomematic.model.custom.data_point import CustomDataPoint
|
|
27
27
|
from aiohomematic.model.custom.support import CustomConfig
|
|
28
28
|
from aiohomematic.model.data_point import CallParameterCollector, bind_collector
|
|
29
|
-
from aiohomematic.model.decorators import config_property, state_property
|
|
30
29
|
from aiohomematic.model.generic import DpAction, DpBinarySensor, DpFloat, DpInteger, DpSelect, DpSensor, DpSwitch
|
|
30
|
+
from aiohomematic.property_decorators import config_property, state_property
|
|
31
31
|
|
|
32
32
|
_LOGGER: Final = logging.getLogger(__name__)
|
|
33
33
|
|
|
@@ -351,15 +351,15 @@ class BaseCustomDpClimate(CustomDataPoint):
|
|
|
351
351
|
async def set_profile(self, profile: ClimateProfile, collector: CallParameterCollector | None = None) -> None:
|
|
352
352
|
"""Set new profile."""
|
|
353
353
|
|
|
354
|
-
@inspector
|
|
354
|
+
@inspector
|
|
355
355
|
async def enable_away_mode_by_calendar(self, start: datetime, end: datetime, away_temperature: float) -> None:
|
|
356
356
|
"""Enable the away mode by calendar on thermostat."""
|
|
357
357
|
|
|
358
|
-
@inspector
|
|
358
|
+
@inspector
|
|
359
359
|
async def enable_away_mode_by_duration(self, hours: int, away_temperature: float) -> None:
|
|
360
360
|
"""Enable the away mode by duration on thermostat."""
|
|
361
361
|
|
|
362
|
-
@inspector
|
|
362
|
+
@inspector
|
|
363
363
|
async def disable_away_mode(self) -> None:
|
|
364
364
|
"""Disable the away mode on thermostat."""
|
|
365
365
|
|
|
@@ -375,7 +375,7 @@ class BaseCustomDpClimate(CustomDataPoint):
|
|
|
375
375
|
return True
|
|
376
376
|
return super().is_state_change(**kwargs)
|
|
377
377
|
|
|
378
|
-
@inspector
|
|
378
|
+
@inspector
|
|
379
379
|
async def copy_schedule(self, target_climate_data_point: BaseCustomDpClimate) -> None:
|
|
380
380
|
"""Copy schedule to target device."""
|
|
381
381
|
|
|
@@ -388,7 +388,7 @@ class BaseCustomDpClimate(CustomDataPoint):
|
|
|
388
388
|
values=raw_schedule,
|
|
389
389
|
)
|
|
390
390
|
|
|
391
|
-
@inspector
|
|
391
|
+
@inspector
|
|
392
392
|
async def copy_schedule_profile(
|
|
393
393
|
self,
|
|
394
394
|
source_profile: ScheduleProfile,
|
|
@@ -418,7 +418,7 @@ class BaseCustomDpClimate(CustomDataPoint):
|
|
|
418
418
|
do_validate=False,
|
|
419
419
|
)
|
|
420
420
|
|
|
421
|
-
@inspector
|
|
421
|
+
@inspector
|
|
422
422
|
async def get_schedule_profile(self, profile: ScheduleProfile) -> PROFILE_DICT:
|
|
423
423
|
"""Return a schedule by climate profile."""
|
|
424
424
|
if not self._supports_schedule:
|
|
@@ -426,7 +426,7 @@ class BaseCustomDpClimate(CustomDataPoint):
|
|
|
426
426
|
schedule_data = await self._get_schedule_profile(profile=profile)
|
|
427
427
|
return schedule_data.get(profile, {})
|
|
428
428
|
|
|
429
|
-
@inspector
|
|
429
|
+
@inspector
|
|
430
430
|
async def get_schedule_profile_weekday(self, profile: ScheduleProfile, weekday: ScheduleWeekday) -> WEEKDAY_DICT:
|
|
431
431
|
"""Return a schedule by climate profile."""
|
|
432
432
|
if not self._supports_schedule:
|
|
@@ -478,7 +478,7 @@ class BaseCustomDpClimate(CustomDataPoint):
|
|
|
478
478
|
|
|
479
479
|
return schedule_data
|
|
480
480
|
|
|
481
|
-
@inspector
|
|
481
|
+
@inspector
|
|
482
482
|
async def set_schedule_profile(
|
|
483
483
|
self, profile: ScheduleProfile, profile_data: PROFILE_DICT, do_validate: bool = True
|
|
484
484
|
) -> None:
|
|
@@ -518,7 +518,7 @@ class BaseCustomDpClimate(CustomDataPoint):
|
|
|
518
518
|
values=_get_raw_schedule_paramset(schedule_data=schedule_data),
|
|
519
519
|
)
|
|
520
520
|
|
|
521
|
-
@inspector
|
|
521
|
+
@inspector
|
|
522
522
|
async def set_simple_schedule_profile(
|
|
523
523
|
self,
|
|
524
524
|
profile: ScheduleProfile,
|
|
@@ -531,7 +531,7 @@ class BaseCustomDpClimate(CustomDataPoint):
|
|
|
531
531
|
)
|
|
532
532
|
await self.set_schedule_profile(profile=profile, profile_data=profile_data)
|
|
533
533
|
|
|
534
|
-
@inspector
|
|
534
|
+
@inspector
|
|
535
535
|
async def set_schedule_profile_weekday(
|
|
536
536
|
self,
|
|
537
537
|
profile: ScheduleProfile,
|
|
@@ -559,7 +559,7 @@ class BaseCustomDpClimate(CustomDataPoint):
|
|
|
559
559
|
values=_get_raw_schedule_paramset(schedule_data=schedule_data),
|
|
560
560
|
)
|
|
561
561
|
|
|
562
|
-
@inspector
|
|
562
|
+
@inspector
|
|
563
563
|
async def set_simple_schedule_profile_weekday(
|
|
564
564
|
self,
|
|
565
565
|
profile: ScheduleProfile,
|
|
@@ -844,7 +844,7 @@ class CustomDpRfThermostat(BaseCustomDpClimate):
|
|
|
844
844
|
value=_HM_WEEK_PROFILE_POINTERS_TO_NAMES[profile_idx], collector=collector
|
|
845
845
|
)
|
|
846
846
|
|
|
847
|
-
@inspector
|
|
847
|
+
@inspector
|
|
848
848
|
async def enable_away_mode_by_calendar(self, start: datetime, end: datetime, away_temperature: float) -> None:
|
|
849
849
|
"""Enable the away mode by calendar on thermostat."""
|
|
850
850
|
await self._client.set_value(
|
|
@@ -854,14 +854,14 @@ class CustomDpRfThermostat(BaseCustomDpClimate):
|
|
|
854
854
|
value=_party_mode_code(start=start, end=end, away_temperature=away_temperature),
|
|
855
855
|
)
|
|
856
856
|
|
|
857
|
-
@inspector
|
|
857
|
+
@inspector
|
|
858
858
|
async def enable_away_mode_by_duration(self, hours: int, away_temperature: float) -> None:
|
|
859
859
|
"""Enable the away mode by duration on thermostat."""
|
|
860
860
|
start = datetime.now() - timedelta(minutes=10)
|
|
861
861
|
end = datetime.now() + timedelta(hours=hours)
|
|
862
862
|
await self.enable_away_mode_by_calendar(start=start, end=end, away_temperature=away_temperature)
|
|
863
863
|
|
|
864
|
-
@inspector
|
|
864
|
+
@inspector
|
|
865
865
|
async def disable_away_mode(self) -> None:
|
|
866
866
|
"""Disable the away mode on thermostat."""
|
|
867
867
|
start = datetime.now() - timedelta(hours=11)
|
|
@@ -1095,7 +1095,7 @@ class CustomDpIpThermostat(BaseCustomDpClimate):
|
|
|
1095
1095
|
if profile_idx := self._profiles.get(profile):
|
|
1096
1096
|
await self._dp_active_profile.send_value(value=profile_idx, collector=collector)
|
|
1097
1097
|
|
|
1098
|
-
@inspector
|
|
1098
|
+
@inspector
|
|
1099
1099
|
async def enable_away_mode_by_calendar(self, start: datetime, end: datetime, away_temperature: float) -> None:
|
|
1100
1100
|
"""Enable the away mode by calendar on thermostat."""
|
|
1101
1101
|
await self._client.put_paramset(
|
|
@@ -1109,14 +1109,14 @@ class CustomDpIpThermostat(BaseCustomDpClimate):
|
|
|
1109
1109
|
},
|
|
1110
1110
|
)
|
|
1111
1111
|
|
|
1112
|
-
@inspector
|
|
1112
|
+
@inspector
|
|
1113
1113
|
async def enable_away_mode_by_duration(self, hours: int, away_temperature: float) -> None:
|
|
1114
1114
|
"""Enable the away mode by duration on thermostat."""
|
|
1115
1115
|
start = datetime.now() - timedelta(minutes=10)
|
|
1116
1116
|
end = datetime.now() + timedelta(hours=hours)
|
|
1117
1117
|
await self.enable_away_mode_by_calendar(start=start, end=end, away_temperature=away_temperature)
|
|
1118
1118
|
|
|
1119
|
-
@inspector
|
|
1119
|
+
@inspector
|
|
1120
1120
|
async def disable_away_mode(self) -> None:
|
|
1121
1121
|
"""Disable the away mode on thermostat."""
|
|
1122
1122
|
await self._client.put_paramset(
|
|
@@ -18,8 +18,8 @@ from aiohomematic.model.custom.const import DeviceProfile, Field
|
|
|
18
18
|
from aiohomematic.model.custom.data_point import CustomDataPoint
|
|
19
19
|
from aiohomematic.model.custom.support import CustomConfig, ExtendedConfig
|
|
20
20
|
from aiohomematic.model.data_point import CallParameterCollector, bind_collector
|
|
21
|
-
from aiohomematic.model.decorators import state_property
|
|
22
21
|
from aiohomematic.model.generic import DpAction, DpFloat, DpSelect, DpSensor
|
|
22
|
+
from aiohomematic.property_decorators import state_property
|
|
23
23
|
|
|
24
24
|
_LOGGER: Final = logging.getLogger(__name__)
|
|
25
25
|
|
|
@@ -15,7 +15,6 @@ from aiohomematic.model.custom import definition as hmed
|
|
|
15
15
|
from aiohomematic.model.custom.const import CDPD, DeviceProfile, Field
|
|
16
16
|
from aiohomematic.model.custom.support import CustomConfig
|
|
17
17
|
from aiohomematic.model.data_point import BaseDataPoint, NoneTypeDataPoint
|
|
18
|
-
from aiohomematic.model.decorators import state_property
|
|
19
18
|
from aiohomematic.model.generic import data_point as hmge
|
|
20
19
|
from aiohomematic.model.support import (
|
|
21
20
|
DataPointNameData,
|
|
@@ -24,6 +23,7 @@ from aiohomematic.model.support import (
|
|
|
24
23
|
check_channel_is_the_only_primary_channel,
|
|
25
24
|
get_custom_data_point_name,
|
|
26
25
|
)
|
|
26
|
+
from aiohomematic.property_decorators import state_property
|
|
27
27
|
from aiohomematic.support import get_channel_address
|
|
28
28
|
|
|
29
29
|
_LOGGER: Final = logging.getLogger(__name__)
|
|
@@ -16,8 +16,8 @@ from aiohomematic.model.custom.const import DeviceProfile, Field
|
|
|
16
16
|
from aiohomematic.model.custom.data_point import CustomDataPoint
|
|
17
17
|
from aiohomematic.model.custom.support import CustomConfig, ExtendedConfig
|
|
18
18
|
from aiohomematic.model.data_point import CallParameterCollector, bind_collector
|
|
19
|
-
from aiohomematic.model.decorators import state_property
|
|
20
19
|
from aiohomematic.model.generic import DpAction, DpFloat, DpInteger, DpSelect, DpSensor, GenericDataPoint
|
|
20
|
+
from aiohomematic.property_decorators import state_property
|
|
21
21
|
|
|
22
22
|
_DIMMER_OFF: Final = 0.0
|
|
23
23
|
_EFFECT_OFF: Final = "Off"
|