aiohomematic 2025.9.3__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 +44 -44
- aiohomematic/client/_rpc_errors.py +1 -1
- aiohomematic/client/json_rpc.py +27 -27
- aiohomematic/client/xml_rpc.py +1 -1
- aiohomematic/const.py +1 -1
- aiohomematic/model/__init__.py +1 -1
- aiohomematic/model/calculated/data_point.py +2 -2
- aiohomematic/model/data_point.py +10 -10
- aiohomematic/model/device.py +6 -6
- 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 +2 -44
- aiohomematic/support.py +2 -7
- {aiohomematic-2025.9.3.dist-info → aiohomematic-2025.9.4.dist-info}/METADATA +3 -3
- {aiohomematic-2025.9.3.dist-info → aiohomematic-2025.9.4.dist-info}/RECORD +23 -23
- aiohomematic_support/client_local.py +15 -15
- {aiohomematic-2025.9.3.dist-info → aiohomematic-2025.9.4.dist-info}/WHEEL +0 -0
- {aiohomematic-2025.9.3.dist-info → aiohomematic-2025.9.4.dist-info}/licenses/LICENSE +0 -0
- {aiohomematic-2025.9.3.dist-info → aiohomematic-2025.9.4.dist-info}/top_level.txt +0 -0
aiohomematic/client/json_rpc.py
CHANGED
|
@@ -263,7 +263,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
263
263
|
return delta.seconds < JSON_SESSION_AGE
|
|
264
264
|
|
|
265
265
|
async def _do_login(self) -> str | None:
|
|
266
|
-
"""Login to
|
|
266
|
+
"""Login to the backend and return session."""
|
|
267
267
|
if not self._has_credentials:
|
|
268
268
|
_LOGGER.warning("DO_LOGIN failed: No credentials set")
|
|
269
269
|
return None
|
|
@@ -398,7 +398,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
398
398
|
if not self._has_credentials:
|
|
399
399
|
raise ClientException("No credentials set")
|
|
400
400
|
if self._supported_methods and method not in self._supported_methods:
|
|
401
|
-
raise UnsupportedException(f"POST: method '{method} not supported by backend.")
|
|
401
|
+
raise UnsupportedException(f"POST: method '{method} not supported by the backend.")
|
|
402
402
|
|
|
403
403
|
params = _get_params(session_id=session_id, extra_params=extra_params, use_default_params=use_default_params)
|
|
404
404
|
|
|
@@ -492,7 +492,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
492
492
|
message = f"ClientConnectorCertificateError[{cccerr}]"
|
|
493
493
|
if self._tls is False and cccerr.ssl is True:
|
|
494
494
|
message = (
|
|
495
|
-
f"{message}. Possible reason: 'Automatic forwarding to HTTPS' is enabled in backend, "
|
|
495
|
+
f"{message}. Possible reason: 'Automatic forwarding to HTTPS' is enabled in the backend, "
|
|
496
496
|
f"but this integration is not configured to use TLS"
|
|
497
497
|
)
|
|
498
498
|
log_boundary_error(
|
|
@@ -540,7 +540,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
540
540
|
return orjson.loads((await response.read()).decode(encoding=UTF_8))
|
|
541
541
|
|
|
542
542
|
async def logout(self) -> None:
|
|
543
|
-
"""Logout of
|
|
543
|
+
"""Logout of the backend."""
|
|
544
544
|
try:
|
|
545
545
|
await self._looper.block_till_done()
|
|
546
546
|
await self._do_logout(self._session_id)
|
|
@@ -553,7 +553,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
553
553
|
await self._client_session.close()
|
|
554
554
|
|
|
555
555
|
async def _do_logout(self, session_id: str | None) -> None:
|
|
556
|
-
"""Logout of
|
|
556
|
+
"""Logout of the backend."""
|
|
557
557
|
if not session_id:
|
|
558
558
|
_LOGGER.debug("DO_LOGOUT: Not logged in. Not logging out.")
|
|
559
559
|
return
|
|
@@ -580,7 +580,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
580
580
|
self._session_id = None
|
|
581
581
|
|
|
582
582
|
async def execute_program(self, pid: str) -> bool:
|
|
583
|
-
"""Execute a program on
|
|
583
|
+
"""Execute a program on the backend."""
|
|
584
584
|
params = {
|
|
585
585
|
_JsonKey.ID: pid,
|
|
586
586
|
}
|
|
@@ -597,7 +597,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
597
597
|
return True
|
|
598
598
|
|
|
599
599
|
async def set_program_state(self, pid: str, state: bool) -> bool:
|
|
600
|
-
"""Set the program state on
|
|
600
|
+
"""Set the program state on the backend."""
|
|
601
601
|
params = {
|
|
602
602
|
_JsonKey.ID: pid,
|
|
603
603
|
_JsonKey.STATE: "1" if state else "0",
|
|
@@ -614,7 +614,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
614
614
|
return True
|
|
615
615
|
|
|
616
616
|
async def set_system_variable(self, legacy_name: str, value: Any) -> bool:
|
|
617
|
-
"""Set a system variable on
|
|
617
|
+
"""Set a system variable on the backend."""
|
|
618
618
|
params = {_JsonKey.NAME: legacy_name, _JsonKey.VALUE: value}
|
|
619
619
|
if isinstance(value, bool):
|
|
620
620
|
params[_JsonKey.VALUE] = int(value)
|
|
@@ -640,7 +640,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
640
640
|
return True
|
|
641
641
|
|
|
642
642
|
async def delete_system_variable(self, name: str) -> bool:
|
|
643
|
-
"""Delete a system variable from
|
|
643
|
+
"""Delete a system variable from the backend."""
|
|
644
644
|
params = {_JsonKey.NAME: name}
|
|
645
645
|
response = await self._post(
|
|
646
646
|
method=_JsonRpcMethod.SYSVAR_DELETE_SYSVAR_BY_NAME,
|
|
@@ -655,7 +655,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
655
655
|
return True
|
|
656
656
|
|
|
657
657
|
async def get_system_variable(self, name: str) -> Any:
|
|
658
|
-
"""Get single system variable from
|
|
658
|
+
"""Get single system variable from the backend."""
|
|
659
659
|
params = {_JsonKey.NAME: name}
|
|
660
660
|
response = await self._post(
|
|
661
661
|
method=_JsonRpcMethod.SYSVAR_GET_VALUE_BY_NAME,
|
|
@@ -668,7 +668,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
668
668
|
async def get_all_system_variables(
|
|
669
669
|
self, markers: tuple[DescriptionMarker | str, ...]
|
|
670
670
|
) -> tuple[SystemVariableData, ...]:
|
|
671
|
-
"""Get all system variables from
|
|
671
|
+
"""Get all system variables from the backend."""
|
|
672
672
|
variables: list[SystemVariableData] = []
|
|
673
673
|
|
|
674
674
|
response = await self._post(
|
|
@@ -760,7 +760,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
760
760
|
return tuple(variables)
|
|
761
761
|
|
|
762
762
|
async def _get_program_descriptions(self) -> Mapping[str, str]:
|
|
763
|
-
"""Get all program descriptions from
|
|
763
|
+
"""Get all program descriptions from the backend via script."""
|
|
764
764
|
descriptions: dict[str, str] = {}
|
|
765
765
|
try:
|
|
766
766
|
response = await self._post_script(script_name=RegaScript.GET_PROGRAM_DESCRIPTIONS)
|
|
@@ -779,7 +779,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
779
779
|
return descriptions
|
|
780
780
|
|
|
781
781
|
async def _get_system_variable_descriptions(self) -> Mapping[str, str]:
|
|
782
|
-
"""Get all system variable descriptions from
|
|
782
|
+
"""Get all system variable descriptions from the backend via script."""
|
|
783
783
|
descriptions: dict[str, str] = {}
|
|
784
784
|
try:
|
|
785
785
|
response = await self._post_script(script_name=RegaScript.GET_SYSTEM_VARIABLE_DESCRIPTIONS)
|
|
@@ -798,7 +798,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
798
798
|
return descriptions
|
|
799
799
|
|
|
800
800
|
async def get_all_channel_ids_room(self) -> Mapping[str, set[str]]:
|
|
801
|
-
"""Get all channel_ids per room from
|
|
801
|
+
"""Get all channel_ids per room from the backend."""
|
|
802
802
|
channel_ids_room: dict[str, set[str]] = {}
|
|
803
803
|
|
|
804
804
|
response = await self._post(
|
|
@@ -821,7 +821,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
821
821
|
return channel_ids_room
|
|
822
822
|
|
|
823
823
|
async def get_all_channel_ids_function(self) -> Mapping[str, set[str]]:
|
|
824
|
-
"""Get all channel_ids per function from
|
|
824
|
+
"""Get all channel_ids per function from the backend."""
|
|
825
825
|
channel_ids_function: dict[str, set[str]] = {}
|
|
826
826
|
|
|
827
827
|
response = await self._post(
|
|
@@ -844,7 +844,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
844
844
|
return channel_ids_function
|
|
845
845
|
|
|
846
846
|
async def get_device_description(self, interface: Interface, address: str) -> DeviceDescription | None:
|
|
847
|
-
"""Get device descriptions from
|
|
847
|
+
"""Get device descriptions from the backend."""
|
|
848
848
|
device_description: DeviceDescription | None = None
|
|
849
849
|
params = {
|
|
850
850
|
_JsonKey.INTERFACE: interface,
|
|
@@ -906,7 +906,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
906
906
|
async def get_paramset(
|
|
907
907
|
self, interface: Interface, address: str, paramset_key: ParamsetKey | str
|
|
908
908
|
) -> dict[str, Any] | None:
|
|
909
|
-
"""Get paramset from
|
|
909
|
+
"""Get paramset from the backend."""
|
|
910
910
|
paramset: dict[str, Any] = {}
|
|
911
911
|
params = {
|
|
912
912
|
_JsonKey.INTERFACE: interface,
|
|
@@ -932,7 +932,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
932
932
|
paramset_key: ParamsetKey | str,
|
|
933
933
|
values: list[dict[str, Any]],
|
|
934
934
|
) -> None:
|
|
935
|
-
"""Set paramset to
|
|
935
|
+
"""Set paramset to the backend."""
|
|
936
936
|
params = {
|
|
937
937
|
_JsonKey.INTERFACE: interface,
|
|
938
938
|
_JsonKey.ADDRESS: address,
|
|
@@ -953,7 +953,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
953
953
|
)
|
|
954
954
|
|
|
955
955
|
async def get_value(self, interface: Interface, address: str, paramset_key: ParamsetKey, parameter: str) -> Any:
|
|
956
|
-
"""Get value from
|
|
956
|
+
"""Get value from the backend."""
|
|
957
957
|
value: Any = None
|
|
958
958
|
params = {
|
|
959
959
|
_JsonKey.INTERFACE: interface,
|
|
@@ -974,7 +974,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
974
974
|
return value
|
|
975
975
|
|
|
976
976
|
async def set_value(self, interface: Interface, address: str, parameter: str, value_type: str, value: Any) -> None:
|
|
977
|
-
"""Set value to
|
|
977
|
+
"""Set value to the backend."""
|
|
978
978
|
params = {
|
|
979
979
|
_JsonKey.INTERFACE: interface,
|
|
980
980
|
_JsonKey.ADDRESS: address,
|
|
@@ -998,7 +998,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
998
998
|
async def get_paramset_description(
|
|
999
999
|
self, interface: Interface, address: str, paramset_key: ParamsetKey
|
|
1000
1000
|
) -> Mapping[str, ParameterData] | None:
|
|
1001
|
-
"""Get paramset description from
|
|
1001
|
+
"""Get paramset description from the backend."""
|
|
1002
1002
|
paramset_description: dict[str, ParameterData] = {}
|
|
1003
1003
|
params = {
|
|
1004
1004
|
_JsonKey.INTERFACE: interface,
|
|
@@ -1119,7 +1119,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
1119
1119
|
return tuple(all_programs)
|
|
1120
1120
|
|
|
1121
1121
|
async def is_present(self, interface: Interface) -> bool:
|
|
1122
|
-
"""Get value from
|
|
1122
|
+
"""Get value from the backend."""
|
|
1123
1123
|
value: bool = False
|
|
1124
1124
|
params = {_JsonKey.INTERFACE: interface}
|
|
1125
1125
|
|
|
@@ -1168,19 +1168,19 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
1168
1168
|
return supported_methods
|
|
1169
1169
|
|
|
1170
1170
|
async def _check_supported_methods(self) -> bool:
|
|
1171
|
-
"""Check, if all required api methods are supported by backend."""
|
|
1171
|
+
"""Check, if all required api methods are supported by the backend."""
|
|
1172
1172
|
if self._supported_methods is None:
|
|
1173
1173
|
self._supported_methods = await self._get_supported_methods()
|
|
1174
1174
|
if unsupport_methods := tuple(method for method in _JsonRpcMethod if method not in self._supported_methods):
|
|
1175
1175
|
_LOGGER.warning(
|
|
1176
|
-
"CHECK_SUPPORTED_METHODS: methods not supported by backend: %s",
|
|
1176
|
+
"CHECK_SUPPORTED_METHODS: methods not supported by the backend: %s",
|
|
1177
1177
|
", ".join(unsupport_methods),
|
|
1178
1178
|
)
|
|
1179
1179
|
return False
|
|
1180
1180
|
return True
|
|
1181
1181
|
|
|
1182
1182
|
async def get_system_information(self) -> SystemInformation:
|
|
1183
|
-
"""Get system information of the backend."""
|
|
1183
|
+
"""Get system information of the the backend."""
|
|
1184
1184
|
|
|
1185
1185
|
if (auth_enabled := await self._get_auth_enabled()) is not None and (
|
|
1186
1186
|
system_information := SystemInformation(
|
|
@@ -1207,7 +1207,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
1207
1207
|
return True
|
|
1208
1208
|
|
|
1209
1209
|
async def list_devices(self, interface: Interface) -> tuple[DeviceDescription, ...]:
|
|
1210
|
-
"""List devices from
|
|
1210
|
+
"""List devices from the backend."""
|
|
1211
1211
|
devices: tuple[DeviceDescription, ...] = ()
|
|
1212
1212
|
_LOGGER.debug("LIST_DEVICES: Getting all available interfaces")
|
|
1213
1213
|
params = {
|
|
@@ -1225,7 +1225,7 @@ class JsonRpcAioHttpClient(LogContextMixin):
|
|
|
1225
1225
|
return devices
|
|
1226
1226
|
|
|
1227
1227
|
async def _list_interfaces(self) -> tuple[str, ...]:
|
|
1228
|
-
"""List all available interfaces from
|
|
1228
|
+
"""List all available interfaces from the backend."""
|
|
1229
1229
|
_LOGGER.debug("LIST_INTERFACES: Getting all available interfaces")
|
|
1230
1230
|
|
|
1231
1231
|
response = await self._post(
|
aiohomematic/client/xml_rpc.py
CHANGED
|
@@ -142,7 +142,7 @@ class XmlRpcProxy(xmlrpc.client.ServerProxy, LogContextMixin):
|
|
|
142
142
|
try:
|
|
143
143
|
method = args[0]
|
|
144
144
|
if self._supported_methods and method not in self._supported_methods:
|
|
145
|
-
raise UnsupportedException(f"__ASYNC_REQUEST: method '{method} not supported by backend.")
|
|
145
|
+
raise UnsupportedException(f"__ASYNC_REQUEST: method '{method} not supported by the backend.")
|
|
146
146
|
|
|
147
147
|
if method in _VALID_XMLRPC_COMMANDS_ON_NO_CONNECTION or not self._connection_state.has_issue(
|
|
148
148
|
issuer=self, iid=self._interface_id
|
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.9.
|
|
22
|
+
VERSION: Final = "2025.9.4"
|
|
23
23
|
|
|
24
24
|
# Detect test speedup mode via environment
|
|
25
25
|
_TEST_SPEEDUP: Final = (
|
aiohomematic/model/__init__.py
CHANGED
|
@@ -8,7 +8,7 @@ parameter descriptions into concrete data points and events:
|
|
|
8
8
|
- generic: Default data point types (switch, number, sensor, select, etc.).
|
|
9
9
|
- custom: Higher-level composites and device-specific behaviors.
|
|
10
10
|
- calculated: Derived metrics (e.g., dew point, apparent temperature).
|
|
11
|
-
- hub: Program and system variable data points from the backend hub
|
|
11
|
+
- hub: Program and system variable data points from the backend hub.
|
|
12
12
|
|
|
13
13
|
The create_data_points_and_events entrypoint inspects a device’s paramset
|
|
14
14
|
information, applies visibility rules, creates events where appropriate, and
|
|
@@ -32,7 +32,7 @@ from aiohomematic.model.support import (
|
|
|
32
32
|
generate_unique_id,
|
|
33
33
|
get_data_point_name_data,
|
|
34
34
|
)
|
|
35
|
-
from aiohomematic.property_decorators import
|
|
35
|
+
from aiohomematic.property_decorators import config_property, hm_property, state_property
|
|
36
36
|
|
|
37
37
|
_LOGGER: Final = logging.getLogger(__name__)
|
|
38
38
|
|
|
@@ -147,7 +147,7 @@ class CalculatedDataPoint[ParameterT: GenericParameterType](BaseDataPoint):
|
|
|
147
147
|
"""Return default value."""
|
|
148
148
|
return self._default
|
|
149
149
|
|
|
150
|
-
@
|
|
150
|
+
@hm_property(cached=True)
|
|
151
151
|
def dpk(self) -> DataPointKey:
|
|
152
152
|
"""Return data_point key value."""
|
|
153
153
|
return DataPointKey(
|
aiohomematic/model/data_point.py
CHANGED
|
@@ -73,7 +73,7 @@ from aiohomematic.model.support import (
|
|
|
73
73
|
convert_value,
|
|
74
74
|
generate_unique_id,
|
|
75
75
|
)
|
|
76
|
-
from aiohomematic.property_decorators import
|
|
76
|
+
from aiohomematic.property_decorators import config_property, hm_property, state_property
|
|
77
77
|
from aiohomematic.support import LogContextMixin, PayloadMixin, extract_exc_args, log_boundary_error
|
|
78
78
|
|
|
79
79
|
__all__ = [
|
|
@@ -269,7 +269,7 @@ class CallbackDataPoint(ABC, LogContextMixin):
|
|
|
269
269
|
"""Return the data_point usage."""
|
|
270
270
|
return DataPointUsage.DATA_POINT
|
|
271
271
|
|
|
272
|
-
@
|
|
272
|
+
@hm_property(cached=True)
|
|
273
273
|
def enabled_default(self) -> bool:
|
|
274
274
|
"""Return, if data_point should be enabled based on usage attribute."""
|
|
275
275
|
return self.usage in (
|
|
@@ -295,12 +295,12 @@ class CallbackDataPoint(ABC, LogContextMixin):
|
|
|
295
295
|
return self._path_data.state_path
|
|
296
296
|
|
|
297
297
|
# @property
|
|
298
|
-
@
|
|
298
|
+
@hm_property(cached=True)
|
|
299
299
|
def service_methods(self) -> Mapping[str, Callable]:
|
|
300
300
|
"""Return all service methods."""
|
|
301
301
|
return get_service_calls(obj=self)
|
|
302
302
|
|
|
303
|
-
@
|
|
303
|
+
@hm_property(cached=True)
|
|
304
304
|
def service_method_names(self) -> tuple[str, ...]:
|
|
305
305
|
"""Return all service methods."""
|
|
306
306
|
return tuple(self.service_methods.keys())
|
|
@@ -659,7 +659,7 @@ class BaseParameterDataPoint[
|
|
|
659
659
|
"""Return if the parameter is un ignored."""
|
|
660
660
|
return self._is_un_ignored
|
|
661
661
|
|
|
662
|
-
@
|
|
662
|
+
@hm_property(cached=True)
|
|
663
663
|
def dpk(self) -> DataPointKey:
|
|
664
664
|
"""Return data_point key value."""
|
|
665
665
|
return DataPointKey(
|
|
@@ -699,7 +699,7 @@ class BaseParameterDataPoint[
|
|
|
699
699
|
"""Return raw unit value."""
|
|
700
700
|
return self._raw_unit
|
|
701
701
|
|
|
702
|
-
@
|
|
702
|
+
@hm_property(cached=True)
|
|
703
703
|
def requires_polling(self) -> bool:
|
|
704
704
|
"""Return whether the data_point requires polling."""
|
|
705
705
|
return not self._channel.device.client.supports_push_updates or (
|
|
@@ -757,7 +757,7 @@ class BaseParameterDataPoint[
|
|
|
757
757
|
|
|
758
758
|
@property
|
|
759
759
|
def service(self) -> bool:
|
|
760
|
-
"""Return the if data_point is visible in
|
|
760
|
+
"""Return the if data_point is visible in the backend."""
|
|
761
761
|
return self._service
|
|
762
762
|
|
|
763
763
|
@property
|
|
@@ -782,10 +782,10 @@ class BaseParameterDataPoint[
|
|
|
782
782
|
|
|
783
783
|
@property
|
|
784
784
|
def visible(self) -> bool:
|
|
785
|
-
"""Return the if data_point is visible in
|
|
785
|
+
"""Return the if data_point is visible in the backend."""
|
|
786
786
|
return self._visible
|
|
787
787
|
|
|
788
|
-
@
|
|
788
|
+
@hm_property(cached=True)
|
|
789
789
|
def _enabled_by_channel_operation_mode(self) -> bool | None:
|
|
790
790
|
"""Return, if the data_point/event must be enabled."""
|
|
791
791
|
if self._channel.type_name not in _CONFIGURABLE_CHANNEL:
|
|
@@ -1011,7 +1011,7 @@ class CallParameterCollector:
|
|
|
1011
1011
|
)
|
|
1012
1012
|
|
|
1013
1013
|
async def send_data(self, wait_for_callback: int | None) -> set[DP_KEY_VALUE]:
|
|
1014
|
-
"""Send data to backend."""
|
|
1014
|
+
"""Send data to the backend."""
|
|
1015
1015
|
dpk_values: set[DP_KEY_VALUE] = set()
|
|
1016
1016
|
for paramset_key, paramsets in self._paramsets.items():
|
|
1017
1017
|
for _, paramset_no in sorted(paramsets.items()):
|
aiohomematic/model/device.py
CHANGED
|
@@ -80,7 +80,7 @@ from aiohomematic.model.support import (
|
|
|
80
80
|
get_device_name,
|
|
81
81
|
)
|
|
82
82
|
from aiohomematic.model.update import DpUpdate
|
|
83
|
-
from aiohomematic.property_decorators import
|
|
83
|
+
from aiohomematic.property_decorators import hm_property, info_property, state_property
|
|
84
84
|
from aiohomematic.support import (
|
|
85
85
|
CacheEntry,
|
|
86
86
|
LogContextMixin,
|
|
@@ -371,7 +371,7 @@ class Device(LogContextMixin, PayloadMixin):
|
|
|
371
371
|
|
|
372
372
|
@info_property
|
|
373
373
|
def room(self) -> str | None:
|
|
374
|
-
"""Return the room of the device, if only one assigned in
|
|
374
|
+
"""Return the room of the device, if only one assigned in the backend."""
|
|
375
375
|
if self._rooms and len(self._rooms) == 1:
|
|
376
376
|
return list(self._rooms)[0]
|
|
377
377
|
if (maintenance_channel := self.get_channel(channel_address=f"{self._address}:0")) is not None:
|
|
@@ -441,7 +441,7 @@ class Device(LogContextMixin, PayloadMixin):
|
|
|
441
441
|
for channel in self._channels.values():
|
|
442
442
|
await channel.remove_central_link()
|
|
443
443
|
|
|
444
|
-
@
|
|
444
|
+
@hm_property(cached=True)
|
|
445
445
|
def relevant_for_central_link_management(self) -> bool:
|
|
446
446
|
"""Return if channel is relevant for central link management."""
|
|
447
447
|
return (
|
|
@@ -860,7 +860,7 @@ class Channel(LogContextMixin, PayloadMixin):
|
|
|
860
860
|
|
|
861
861
|
@info_property
|
|
862
862
|
def room(self) -> str | None:
|
|
863
|
-
"""Return the room of the device, if only one assigned in
|
|
863
|
+
"""Return the room of the device, if only one assigned in the backend."""
|
|
864
864
|
if self._rooms and len(self._rooms) == 1:
|
|
865
865
|
return list(self._rooms)[0]
|
|
866
866
|
if self.is_group_master:
|
|
@@ -1190,7 +1190,7 @@ class _ValueCache:
|
|
|
1190
1190
|
)
|
|
1191
1191
|
|
|
1192
1192
|
async def _get_values_for_cache(self, dpk: DataPointKey) -> dict[str, Any]:
|
|
1193
|
-
"""Return a value from
|
|
1193
|
+
"""Return a value from the backend to store in cache."""
|
|
1194
1194
|
if not self._device.available:
|
|
1195
1195
|
_LOGGER.debug(
|
|
1196
1196
|
"GET_VALUES_FOR_CACHE failed: device %s (%s) is not available", self._device.name, self._device.address
|
|
@@ -1212,7 +1212,7 @@ class _ValueCache:
|
|
|
1212
1212
|
def _add_entry_to_device_cache(self, dpk: DataPointKey, value: Any) -> None:
|
|
1213
1213
|
"""Add value to cache."""
|
|
1214
1214
|
# write value to cache even if an exception has occurred
|
|
1215
|
-
# to avoid repetitive calls to
|
|
1215
|
+
# to avoid repetitive calls to the backend within max_age
|
|
1216
1216
|
self._device_cache[dpk] = CacheEntry(value=value, refresh_at=datetime.now())
|
|
1217
1217
|
|
|
1218
1218
|
def _get_value_from_cache(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT
|
|
2
2
|
# Copyright (c) 2021-2025 Daniel Perna, SukramJ
|
|
3
|
-
"""Generic python representation of a
|
|
3
|
+
"""Generic python representation of a backend parameter."""
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
@@ -21,7 +21,7 @@ from aiohomematic.decorators import inspector
|
|
|
21
21
|
from aiohomematic.exceptions import ValidationException
|
|
22
22
|
from aiohomematic.model import data_point as hme, device as hmd
|
|
23
23
|
from aiohomematic.model.support import DataPointNameData, GenericParameterType, get_data_point_name_data
|
|
24
|
-
from aiohomematic.property_decorators import
|
|
24
|
+
from aiohomematic.property_decorators import hm_property
|
|
25
25
|
|
|
26
26
|
_LOGGER: Final = logging.getLogger(__name__)
|
|
27
27
|
|
|
@@ -51,7 +51,7 @@ class GenericDataPoint[ParameterT: GenericParameterType, InputParameterT: Generi
|
|
|
51
51
|
parameter_data=parameter_data,
|
|
52
52
|
)
|
|
53
53
|
|
|
54
|
-
@
|
|
54
|
+
@hm_property(cached=True)
|
|
55
55
|
def usage(self) -> DataPointUsage:
|
|
56
56
|
"""Return the data_point usage."""
|
|
57
57
|
if self._is_forced_sensor or self._is_un_ignored:
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
Hub (backend) data points for AioHomematic.
|
|
5
5
|
|
|
6
6
|
Overview
|
|
7
|
-
- This module reflects the state and capabilities of the backend
|
|
7
|
+
- This module reflects the state and capabilities of the backend
|
|
8
8
|
at the hub level. It exposes backend programs and system variables as data
|
|
9
9
|
points that can be observed and acted upon by higher layers (e.g.,
|
|
10
10
|
integrations).
|
|
@@ -133,7 +133,7 @@ class ProgramDpType(NamedTuple):
|
|
|
133
133
|
|
|
134
134
|
|
|
135
135
|
class Hub:
|
|
136
|
-
"""The HomeMatic hub.
|
|
136
|
+
"""The HomeMatic hub."""
|
|
137
137
|
|
|
138
138
|
__slots__ = (
|
|
139
139
|
"_sema_fetch_sysvars",
|
|
@@ -223,7 +223,7 @@ class Hub:
|
|
|
223
223
|
self._central.name,
|
|
224
224
|
)
|
|
225
225
|
|
|
226
|
-
# remove some variables in case of CCU
|
|
226
|
+
# remove some variables in case of CCU backend
|
|
227
227
|
# - OldValue(s) are for internal calculations
|
|
228
228
|
if self._central.model is Backend.CCU:
|
|
229
229
|
variables = _clean_variables(variables)
|
|
@@ -216,7 +216,7 @@ class GenericSysvarDataPoint(GenericHubDataPoint):
|
|
|
216
216
|
self._reset_temporary_timestamps()
|
|
217
217
|
|
|
218
218
|
def write_value(self, value: Any, write_at: datetime) -> None:
|
|
219
|
-
"""Set variable value on
|
|
219
|
+
"""Set variable value on the backend."""
|
|
220
220
|
self._reset_temporary_value()
|
|
221
221
|
|
|
222
222
|
old_value = self._current_value
|
|
@@ -262,7 +262,7 @@ class GenericSysvarDataPoint(GenericHubDataPoint):
|
|
|
262
262
|
|
|
263
263
|
@inspector
|
|
264
264
|
async def send_variable(self, value: Any) -> None:
|
|
265
|
-
"""Set variable value on
|
|
265
|
+
"""Set variable value on the backend."""
|
|
266
266
|
if client := self.central.primary_client:
|
|
267
267
|
await client.set_system_variable(legacy_name=self._legacy_name, value=parse_sys_var(self._data_type, value))
|
|
268
268
|
self._write_temporary_value(value=value, write_at=datetime.now())
|
|
@@ -316,7 +316,7 @@ class GenericProgramDataPoint(GenericHubDataPoint):
|
|
|
316
316
|
return self._pid
|
|
317
317
|
|
|
318
318
|
def update_data(self, data: ProgramData) -> None:
|
|
319
|
-
"""Set variable value on
|
|
319
|
+
"""Set variable value on the backend."""
|
|
320
320
|
do_update: bool = False
|
|
321
321
|
if self._is_active != data.is_active:
|
|
322
322
|
self._is_active = data.is_active
|
|
@@ -6,19 +6,16 @@ Decorators and helpers for declaring public attributes on data point classes.
|
|
|
6
6
|
This module provides four decorator factories that behave like the built-in
|
|
7
7
|
@property, but additionally annotate properties with a semantic category so they
|
|
8
8
|
can be automatically collected to build payloads and log contexts:
|
|
9
|
-
- cached_property: computed once per instance and cached until the value is
|
|
10
|
-
invalidated by a setter/deleter on the same descriptor.
|
|
11
9
|
- config_property: configuration-related properties.
|
|
12
10
|
- info_property: informational/metadata properties.
|
|
13
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
|
|
14
13
|
|
|
15
14
|
All decorators accept an optional keyword-only argument log_context. If set to
|
|
16
15
|
True, the property will be included in the LogContextMixin.log_context mapping.
|
|
17
16
|
|
|
18
17
|
Notes on caching
|
|
19
|
-
-
|
|
20
|
-
- The other decorators can be created with cached=True to enable the same
|
|
21
|
-
behavior when desired.
|
|
18
|
+
- Marked with cached=True always caches on first access and invalidates on set/delete.
|
|
22
19
|
"""
|
|
23
20
|
|
|
24
21
|
from __future__ import annotations
|
|
@@ -235,45 +232,6 @@ def hm_property[PR](
|
|
|
235
232
|
return _GenericProperty(func, kind=kind, cached=cached, log_context=log_context)
|
|
236
233
|
|
|
237
234
|
|
|
238
|
-
# ----- cached_property -----
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
@overload
|
|
242
|
-
def cached_property[PR](func: Callable[[Any], PR], /) -> _GenericProperty[PR, Any]: ...
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
@overload
|
|
246
|
-
def cached_property(*, log_context: bool = ...) -> Callable[[Callable[[Any], R]], _GenericProperty[R, Any]]: ...
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def cached_property[PR](
|
|
250
|
-
func: Callable[[Any], PR] | None = None,
|
|
251
|
-
*,
|
|
252
|
-
log_context: bool = False,
|
|
253
|
-
) -> _GenericProperty[PR, Any] | Callable[[Callable[[Any], PR]], _GenericProperty[PR, Any]]:
|
|
254
|
-
"""
|
|
255
|
-
Decorate a method as a computed attribute with per-instance caching.
|
|
256
|
-
|
|
257
|
-
Supports both usages:
|
|
258
|
-
- @cached_property
|
|
259
|
-
- @cached_property(log_context=True)
|
|
260
|
-
|
|
261
|
-
Args:
|
|
262
|
-
func: The function being decorated when used as @cached_property without
|
|
263
|
-
parentheses. When used as a factory (i.e., @cached_property(...)), this
|
|
264
|
-
is None and the returned callable expects the function to decorate.
|
|
265
|
-
log_context: Include this property in structured log context if True.
|
|
266
|
-
|
|
267
|
-
"""
|
|
268
|
-
if func is None:
|
|
269
|
-
|
|
270
|
-
def wrapper(f: Callable[[Any], PR]) -> _GenericProperty[PR, Any]:
|
|
271
|
-
return _GenericProperty(f, kind=Kind.SIMPLE, cached=True, log_context=log_context)
|
|
272
|
-
|
|
273
|
-
return wrapper
|
|
274
|
-
return _GenericProperty(func, kind=Kind.SIMPLE, cached=True, log_context=log_context)
|
|
275
|
-
|
|
276
|
-
|
|
277
235
|
# ----- config_property -----
|
|
278
236
|
|
|
279
237
|
|
aiohomematic/support.py
CHANGED
|
@@ -49,12 +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
|
-
Kind,
|
|
54
|
-
cached_property,
|
|
55
|
-
get_hm_property_by_kind,
|
|
56
|
-
get_hm_property_by_log_context,
|
|
57
|
-
)
|
|
52
|
+
from aiohomematic.property_decorators import Kind, get_hm_property_by_kind, get_hm_property_by_log_context, hm_property
|
|
58
53
|
|
|
59
54
|
_LOGGER: Final = logging.getLogger(__name__)
|
|
60
55
|
|
|
@@ -585,7 +580,7 @@ class LogContextMixin:
|
|
|
585
580
|
|
|
586
581
|
__slots__ = ("_cached_log_context",)
|
|
587
582
|
|
|
588
|
-
@
|
|
583
|
+
@hm_property(cached=True)
|
|
589
584
|
def log_context(self) -> Mapping[str, Any]:
|
|
590
585
|
"""Return the log context for this object."""
|
|
591
586
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiohomematic
|
|
3
|
-
Version: 2025.9.
|
|
3
|
+
Version: 2025.9.4
|
|
4
4
|
Summary: Homematic interface for Home Assistant running on Python 3.
|
|
5
5
|
Home-page: https://github.com/sukramj/aiohomematic
|
|
6
6
|
Author-email: SukramJ <sukramj@icloud.com>, Daniel Perna <danielperna84@gmail.com>
|
|
@@ -53,7 +53,7 @@ Use the Home Assistant custom integration "Homematic(IP) Local", which is powere
|
|
|
53
53
|
|
|
54
54
|
1. Prerequisites
|
|
55
55
|
- Use latest version of Home Assistant.
|
|
56
|
-
- A CCU3, RaspberryMatic, or Homegear instance reachable from Home Assistant.
|
|
56
|
+
- A CCU3, OpenCCU/RaspberryMatic, or Homegear instance reachable from Home Assistant.
|
|
57
57
|
- For HomematicIP devices, ensure CCU firmware meets the minimum versions listed below.
|
|
58
58
|
2. Install the integration
|
|
59
59
|
- Add the custom repository and install: https://github.com/sukramj/homematicip_local
|
|
@@ -77,7 +77,7 @@ Due to a bug in earlier CCU2/CCU3 firmware, aiohomematic requires at least the f
|
|
|
77
77
|
- CCU2: 2.53.27
|
|
78
78
|
- CCU3: 3.53.26
|
|
79
79
|
|
|
80
|
-
See details here: https://github.com/
|
|
80
|
+
See details here: https://github.com/OpenCCU/OpenCCU/issues/843. Other CCU‑like platforms using the buggy HmIPServer version are not supported.
|
|
81
81
|
|
|
82
82
|
## Public API and imports
|
|
83
83
|
|