diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ca6dea0..68d29754 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.6.1 + rev: v0.6.2 hooks: - id: ruff args: diff --git a/changelog.md b/changelog.md index 97f8f95c..ec3c7632 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,7 @@ +# Version 2024.8.13 (2024-08-25) + +- Check/convert values of manual executed put_paramset/set_value + # Version 2024.8.12 (2024-08-24) - Add additional validation on config parameters diff --git a/hahomematic/central/__init__.py b/hahomematic/central/__init__.py index 58d829db..7fa8fd23 100644 --- a/hahomematic/central/__init__.py +++ b/hahomematic/central/__init__.py @@ -1049,38 +1049,38 @@ def get_parameters( channels[channel_address].get(paramset_key.value, {}).items() ): p_operations = paramset[Description.OPERATIONS] - for operation in operations: - if all(p_operations & operation for operation in operations): - if un_ignore_candidates_only and ( + + if all(p_operations & operation for operation in operations): + if un_ignore_candidates_only and ( + ( ( - ( - generic_entity := self.get_generic_entity( - channel_address=channel_address, - parameter=parameter, - paramset_key=paramset_key, - ) + generic_entity := self.get_generic_entity( + channel_address=channel_address, + parameter=parameter, + paramset_key=paramset_key, ) - and generic_entity.enabled_default - and not generic_entity.is_un_ignored ) - or parameter in IGNORE_FOR_UN_IGNORE_PARAMETERS - ): - continue - - if not full_format: - parameters.add(parameter) - continue - - channel = ( - UN_IGNORE_WILDCARD - if use_channel_wildcard - else get_channel_no(channel_address) + and generic_entity.enabled_default + and not generic_entity.is_un_ignored ) + or parameter in IGNORE_FOR_UN_IGNORE_PARAMETERS + ): + continue + + if not full_format: + parameters.add(parameter) + continue + + channel = ( + UN_IGNORE_WILDCARD + if use_channel_wildcard + else get_channel_no(channel_address) + ) - full_parameter = f"{parameter}:{paramset_key}@{device_type}:" - if channel is not None: - full_parameter += str(channel) - parameters.add(full_parameter) + full_parameter = f"{parameter}:{paramset_key}@{device_type}:" + if channel is not None: + full_parameter += str(channel) + parameters.add(full_parameter) return list(parameters) diff --git a/hahomematic/client/__init__.py b/hahomematic/client/__init__.py index 66071155..fda3473b 100644 --- a/hahomematic/client/__init__.py +++ b/hahomematic/client/__init__.py @@ -27,6 +27,8 @@ ForcedDeviceAvailability, InterfaceEventType, InterfaceName, + Operations, + ParameterType, ParamsetKey, ProductGroup, ProgramData, @@ -34,9 +36,10 @@ SystemInformation, SystemVariableData, ) -from hahomematic.exceptions import BaseHomematicException, NoConnection +from hahomematic.exceptions import BaseHomematicException, HaHomematicException, NoConnection from hahomematic.performance import measure_execution_time from hahomematic.platforms.device import HmDevice +from hahomematic.platforms.support import convert_value from hahomematic.support import ( build_headers, build_xml_rpc_uri, @@ -429,17 +432,28 @@ async def _set_value( value: Any, wait_for_callback: int | None, rx_mode: str | None = None, + check_against_pd: bool = False, ) -> set[ENTITY_KEY]: """Set single value on paramset VALUES.""" try: - _LOGGER.debug("SET_VALUE: %s, %s, %s", channel_address, parameter, value) + checked_value = ( + self._check_set_value( + channel_address=channel_address, + paramset_key=ParamsetKey.VALUES, + parameter=parameter, + value=value, + ) + if check_against_pd + else value + ) + _LOGGER.debug("SET_VALUE: %s, %s, %s", channel_address, parameter, checked_value) if rx_mode: - await self._proxy.setValue(channel_address, parameter, value, rx_mode) + await self._proxy.setValue(channel_address, parameter, checked_value, rx_mode) else: - await self._proxy.setValue(channel_address, parameter, value) + await self._proxy.setValue(channel_address, parameter, checked_value) # store the send value in the last_value_send_cache entity_keys = self._last_value_send_cache.add_set_value( - channel_address=channel_address, parameter=parameter, value=value + channel_address=channel_address, parameter=parameter, value=checked_value ) if wait_for_callback is not None and ( device := self.central.get_device( @@ -449,7 +463,7 @@ async def _set_value( await wait_for_state_change_or_timeout( device=device, entity_keys=entity_keys, - values={parameter: value}, + values={parameter: checked_value}, wait_for_callback=wait_for_callback, ) return entity_keys # noqa: TRY300 @@ -464,6 +478,18 @@ async def _set_value( ) return set() + def _check_set_value( + self, channel_address: str, paramset_key: str, parameter: str, value: Any + ) -> Any: + """Check set_value.""" + return self._convert_value( + channel_address=channel_address, + paramset_key=paramset_key, + parameter=parameter, + value=value, + operation=Operations.WRITE, + ) + async def set_value( self, channel_address: str, @@ -472,6 +498,7 @@ async def set_value( value: Any, wait_for_callback: int | None = WAIT_FOR_CALLBACK, rx_mode: str | None = None, + check_against_pd: bool = False, ) -> set[ENTITY_KEY]: """Set single value on paramset VALUES.""" if paramset_key == ParamsetKey.VALUES: @@ -481,6 +508,7 @@ async def set_value( value=value, wait_for_callback=wait_for_callback, rx_mode=rx_mode, + check_against_pd=check_against_pd, ) return await self.put_paramset( channel_address=channel_address, @@ -488,6 +516,7 @@ async def set_value( values={parameter: value}, wait_for_callback=wait_for_callback, rx_mode=rx_mode, + check_against_pd=check_against_pd, ) async def get_paramset(self, address: str, paramset_key: str) -> dict[str, Any]: @@ -522,6 +551,7 @@ async def put_paramset( values: dict[str, Any], wait_for_callback: int | None = WAIT_FOR_CALLBACK, rx_mode: str | None = None, + check_against_pd: bool = False, ) -> set[ENTITY_KEY]: """ Set paramsets manually. @@ -530,16 +560,27 @@ async def put_paramset( but for bidcos devices there is a master paramset at the device. """ try: - _LOGGER.debug("PUT_PARAMSET: %s, %s, %s", channel_address, paramset_key, values) + checked_values = ( + self._check_put_paramset( + channel_address=channel_address, paramset_key=paramset_key, values=values + ) + if check_against_pd + else values + ) + _LOGGER.debug( + "PUT_PARAMSET: %s, %s, %s", channel_address, paramset_key, checked_values + ) if rx_mode: - await self._proxy.putParamset(channel_address, paramset_key, values, rx_mode) + await self._proxy.putParamset( + channel_address, paramset_key, checked_values, rx_mode + ) else: - await self._proxy.putParamset(channel_address, paramset_key, values) + await self._proxy.putParamset(channel_address, paramset_key, checked_values) # store the send value in the last_value_send_cache entity_keys = self._last_value_send_cache.add_put_paramset( channel_address=channel_address, paramset_key=paramset_key, - values=values, + values=checked_values, ) if wait_for_callback is not None and ( device := self.central.get_device( @@ -549,7 +590,7 @@ async def put_paramset( await wait_for_state_change_or_timeout( device=device, entity_keys=entity_keys, - values=values, + values=checked_values, wait_for_callback=wait_for_callback, ) return entity_keys # noqa: TRY300 @@ -564,6 +605,52 @@ async def put_paramset( ) return set() + def _check_put_paramset( + self, channel_address: str, paramset_key: str, values: dict[str, Any] + ) -> dict[str, Any]: + """Check put_paramset.""" + checked_values: dict[str, Any] = {} + for param, value in values.items(): + checked_values[param] = self._convert_value( + channel_address=channel_address, + paramset_key=paramset_key, + parameter=param, + value=value, + operation=Operations.WRITE, + ) + return checked_values + + def _convert_value( + self, + channel_address: str, + paramset_key: str, + parameter: str, + value: Any, + operation: Operations, + ) -> Any: + """Check a single parameter against paramset descriptions.""" + if parameter_data := self.central.paramset_descriptions.get_parameter_data( + interface_id=self.interface_id, + channel_address=channel_address, + paramset_key=paramset_key, + parameter=parameter, + ): + pd_type = ParameterType(parameter_data[Description.TYPE]) + pd_value_list = ( + tuple(parameter_data[Description.VALUE_LIST]) + if Description.VALUE_LIST in parameter_data + else None + ) + if not bool(int(parameter_data[Description.OPERATIONS]) & operation): + raise HaHomematicException( + f"Parameter {parameter} does not support the requested operation {operation.value}" + ) + + return convert_value(value=value, target_type=pd_type, value_list=pd_value_list) + raise HaHomematicException( + f"Parameter {parameter} could not be found: {self.interface_id}/{channel_address}/{paramset_key}" + ) + async def fetch_paramset_description( self, channel_address: str, paramset_key: str, save_to_file: bool = True ) -> None: diff --git a/hahomematic_support/client_local.py b/hahomematic_support/client_local.py index a9299fe5..209aa305 100644 --- a/hahomematic_support/client_local.py +++ b/hahomematic_support/client_local.py @@ -199,6 +199,7 @@ async def set_value( value: Any, wait_for_callback: int | None = WAIT_FOR_CALLBACK, rx_mode: str | None = None, + check_against_pd: bool = False, ) -> set[ENTITY_KEY]: """Set single value on paramset VALUES.""" # store the send value in the last_value_send_cache @@ -253,6 +254,7 @@ async def put_paramset( values: Any, wait_for_callback: int | None = WAIT_FOR_CALLBACK, rx_mode: str | None = None, + check_against_pd: bool = False, ) -> set[ENTITY_KEY]: """ Set paramsets manually. diff --git a/pyproject.toml b/pyproject.toml index ca1da918..0de88596 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "hahomematic" -version = "2024.8.12" +version = "2024.8.13" license = {text = "MIT License"} description = "Homematic interface for Home Assistant running on Python 3." readme = "README.md" diff --git a/requirements_test.txt b/requirements_test.txt index 3f613d4c..5a780784 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,7 +12,7 @@ pylint-per-file-ignores==1.3.2 pylint-strict-informational==0.1 pylint==3.2.6 pytest-aiohttp==1.0.5 -pytest-asyncio==0.23.8 +pytest-asyncio==0.24.0 pytest-cov==5.0.0 pytest-rerunfailures==14.0 pytest-socket==0.7.0 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 736817e5..7e96877d 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,4 +1,4 @@ bandit==1.7.9 codespell==2.3.0 -ruff==0.6.1 +ruff==0.6.2 yamllint==1.35.1