From d62bdbb9ffa035d5246f3d54619640f4e5616b4f Mon Sep 17 00:00:00 2001 From: Jevgeni Kiski Date: Tue, 6 Dec 2022 12:00:59 +0200 Subject: [PATCH] Add vallox fan speed control (#82548) * fan.set_percentage + tests * let's see what is not yet covered * Apply suggestions from code review Co-authored-by: Martin Hjelmare * tests fix * vallox_websocket_api 3.0.0 * more coverage * test coverage * Update tests/components/vallox/test_fan.py Co-authored-by: Martin Hjelmare * raise exceptions on user input * Supported features are different per preset mode. * Test fixes * Static supported features is back. Co-authored-by: Martin Hjelmare --- .coveragerc | 3 - homeassistant/components/vallox/__init__.py | 11 +- .../components/vallox/config_flow.py | 12 +- homeassistant/components/vallox/const.py | 10 +- homeassistant/components/vallox/fan.py | 157 +++++++---- homeassistant/components/vallox/manifest.json | 2 +- homeassistant/components/vallox/sensor.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/vallox/conftest.py | 25 +- tests/components/vallox/test_config_flow.py | 6 +- tests/components/vallox/test_fan.py | 259 ++++++++++++++++++ 12 files changed, 401 insertions(+), 92 deletions(-) create mode 100644 tests/components/vallox/test_fan.py diff --git a/.coveragerc b/.coveragerc index 1f1c80269f1..54430c5de96 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1416,9 +1416,6 @@ omit = homeassistant/components/upnp/__init__.py homeassistant/components/upnp/device.py homeassistant/components/upnp/sensor.py - homeassistant/components/vallox/__init__.py - homeassistant/components/vallox/fan.py - homeassistant/components/vallox/sensor.py homeassistant/components/vasttrafik/sensor.py homeassistant/components/velbus/__init__.py homeassistant/components/velbus/binary_sensor.py diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index f393342dfd5..dceffb47a21 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -8,8 +8,7 @@ import logging from typing import Any, NamedTuple, cast from uuid import UUID -from vallox_websocket_api import PROFILE as VALLOX_PROFILE, Vallox -from vallox_websocket_api.exceptions import ValloxApiException +from vallox_websocket_api import PROFILE as VALLOX_PROFILE, Vallox, ValloxApiException from vallox_websocket_api.vallox import ( get_model as _api_get_model, get_next_filter_change_date as _api_get_next_filter_change_date, @@ -191,7 +190,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: metric_cache = await client.fetch_metrics() profile = await client.get_profile() - except (OSError, ValloxApiException) as err: + except ValloxApiException as err: raise UpdateFailed("Error during state cache update") from err return ValloxState(metric_cache, profile) @@ -262,7 +261,7 @@ class ValloxServiceHandler: ) return True - except (OSError, ValloxApiException) as err: + except ValloxApiException as err: _LOGGER.error("Error setting fan speed for Home profile: %s", err) return False @@ -278,7 +277,7 @@ class ValloxServiceHandler: ) return True - except (OSError, ValloxApiException) as err: + except ValloxApiException as err: _LOGGER.error("Error setting fan speed for Away profile: %s", err) return False @@ -294,7 +293,7 @@ class ValloxServiceHandler: ) return True - except (OSError, ValloxApiException) as err: + except ValloxApiException as err: _LOGGER.error("Error setting fan speed for Boost profile: %s", err) return False diff --git a/homeassistant/components/vallox/config_flow.py b/homeassistant/components/vallox/config_flow.py index d30c8641d2c..b9d29b17689 100644 --- a/homeassistant/components/vallox/config_flow.py +++ b/homeassistant/components/vallox/config_flow.py @@ -4,8 +4,7 @@ from __future__ import annotations import logging from typing import Any -from vallox_websocket_api import Vallox -from vallox_websocket_api.exceptions import ValloxApiException +from vallox_websocket_api import Vallox, ValloxApiException import voluptuous as vol from homeassistant import config_entries @@ -25,11 +24,6 @@ STEP_USER_DATA_SCHEMA = vol.Schema( } ) -VALLOX_CONNECTION_EXCEPTIONS = ( - OSError, - ValloxApiException, -) - async def validate_host(hass: HomeAssistant, host: str) -> None: """Validate that the user input allows us to connect.""" @@ -61,7 +55,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except InvalidHost: _LOGGER.error("An invalid host is configured for Vallox: %s", host) reason = "invalid_host" - except VALLOX_CONNECTION_EXCEPTIONS: + except ValloxApiException: _LOGGER.error("Cannot connect to Vallox host %s", host) reason = "cannot_connect" except Exception: # pylint: disable=broad-except @@ -98,7 +92,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await validate_host(self.hass, host) except InvalidHost: errors[CONF_HOST] = "invalid_host" - except VALLOX_CONNECTION_EXCEPTIONS: + except ValloxApiException: errors[CONF_HOST] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") diff --git a/homeassistant/components/vallox/const.py b/homeassistant/components/vallox/const.py index aba10188bde..ef6115a2894 100644 --- a/homeassistant/components/vallox/const.py +++ b/homeassistant/components/vallox/const.py @@ -22,20 +22,20 @@ DEFAULT_FAN_SPEED_HOME = 50 DEFAULT_FAN_SPEED_AWAY = 25 DEFAULT_FAN_SPEED_BOOST = 65 -VALLOX_PROFILE_TO_STR_SETTABLE = { +VALLOX_PROFILE_TO_PRESET_MODE_SETTABLE = { VALLOX_PROFILE.HOME: "Home", VALLOX_PROFILE.AWAY: "Away", VALLOX_PROFILE.BOOST: "Boost", VALLOX_PROFILE.FIREPLACE: "Fireplace", } -VALLOX_PROFILE_TO_STR_REPORTABLE = { +VALLOX_PROFILE_TO_PRESET_MODE_REPORTABLE = { VALLOX_PROFILE.EXTRA: "Extra", - **VALLOX_PROFILE_TO_STR_SETTABLE, + **VALLOX_PROFILE_TO_PRESET_MODE_SETTABLE, } -STR_TO_VALLOX_PROFILE_SETTABLE = { - value: key for (key, value) in VALLOX_PROFILE_TO_STR_SETTABLE.items() +PRESET_MODE_TO_VALLOX_PROFILE_SETTABLE = { + value: key for (key, value) in VALLOX_PROFILE_TO_PRESET_MODE_SETTABLE.items() } VALLOX_CELL_STATE_TO_STR = { diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index be713e34e25..50c30927fde 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -2,11 +2,14 @@ from __future__ import annotations from collections.abc import Mapping -import logging from typing import Any, NamedTuple -from vallox_websocket_api import Vallox -from vallox_websocket_api.exceptions import ValloxApiException +from vallox_websocket_api import ( + PROFILE_TO_SET_FAN_SPEED_METRIC_MAP, + Vallox, + ValloxApiException, + ValloxInvalidInputException, +) from homeassistant.components.fan import ( FanEntity, @@ -15,6 +18,7 @@ from homeassistant.components.fan import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -27,12 +31,10 @@ from .const import ( METRIC_KEY_PROFILE_FAN_SPEED_HOME, MODE_OFF, MODE_ON, - STR_TO_VALLOX_PROFILE_SETTABLE, - VALLOX_PROFILE_TO_STR_SETTABLE, + PRESET_MODE_TO_VALLOX_PROFILE_SETTABLE, + VALLOX_PROFILE_TO_PRESET_MODE_REPORTABLE, ) -_LOGGER = logging.getLogger(__name__) - class ExtraStateAttributeDetails(NamedTuple): """Extra state attribute details.""" @@ -54,7 +56,7 @@ EXTRA_STATE_ATTRIBUTES = ( ) -def _convert_fan_speed_value(value: StateType) -> int | None: +def _convert_to_int(value: StateType) -> int | None: if isinstance(value, (int, float)): return int(value) @@ -68,7 +70,6 @@ async def async_setup_entry( data = hass.data[DOMAIN][entry.entry_id] client = data["client"] - client.set_settable_address(METRIC_KEY_MODE, int) device = ValloxFanEntity( data["name"], @@ -82,8 +83,8 @@ async def async_setup_entry( class ValloxFanEntity(ValloxEntity, FanEntity): """Representation of the fan.""" - _attr_supported_features = FanEntityFeature.PRESET_MODE _attr_has_entity_name = True + _attr_supported_features = FanEntityFeature.PRESET_MODE | FanEntityFeature.SET_SPEED def __init__( self, @@ -97,12 +98,7 @@ class ValloxFanEntity(ValloxEntity, FanEntity): self._client = client self._attr_unique_id = str(self._device_uuid) - - @property - def preset_modes(self) -> list[str]: - """Return a list of available preset modes.""" - # Use the Vallox profile names for the preset names. - return list(STR_TO_VALLOX_PROFILE_SETTABLE.keys()) + self._attr_preset_modes = list(PRESET_MODE_TO_VALLOX_PROFILE_SETTABLE) @property def is_on(self) -> bool: @@ -113,7 +109,18 @@ class ValloxFanEntity(ValloxEntity, FanEntity): def preset_mode(self) -> str | None: """Return the current preset mode.""" vallox_profile = self.coordinator.data.profile - return VALLOX_PROFILE_TO_STR_SETTABLE.get(vallox_profile) + return VALLOX_PROFILE_TO_PRESET_MODE_REPORTABLE.get(vallox_profile) + + @property + def percentage(self) -> int | None: + """Return the current speed as a percentage.""" + + vallox_profile = self.coordinator.data.profile + metric_key = PROFILE_TO_SET_FAN_SPEED_METRIC_MAP.get(vallox_profile) + if not metric_key: + return None + + return _convert_to_int(self.coordinator.data.get_metric(metric_key)) @property def extra_state_attributes(self) -> Mapping[str, int | None]: @@ -121,35 +128,10 @@ class ValloxFanEntity(ValloxEntity, FanEntity): data = self.coordinator.data return { - attr.description: _convert_fan_speed_value(data.get_metric(attr.metric_key)) + attr.description: _convert_to_int(data.get_metric(attr.metric_key)) for attr in EXTRA_STATE_ATTRIBUTES } - async def _async_set_preset_mode_internal(self, preset_mode: str) -> bool: - """ - Set new preset mode. - - Returns true if the mode has been changed, false otherwise. - """ - try: - self._valid_preset_mode_or_raise(preset_mode) - - except NotValidPresetModeError as err: - _LOGGER.error(err) - return False - - if preset_mode == self.preset_mode: - return False - - try: - await self._client.set_profile(STR_TO_VALLOX_PROFILE_SETTABLE[preset_mode]) - - except (OSError, ValloxApiException) as err: - _LOGGER.error("Error setting preset: %s", err) - return False - - return True - async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" update_needed = await self._async_set_preset_mode_internal(preset_mode) @@ -166,22 +148,16 @@ class ValloxFanEntity(ValloxEntity, FanEntity): **kwargs: Any, ) -> None: """Turn the device on.""" - _LOGGER.debug("Turn on") - update_needed = False - if preset_mode: - update_needed = await self._async_set_preset_mode_internal(preset_mode) - if not self.is_on: - try: - await self._client.set_values({METRIC_KEY_MODE: MODE_ON}) + update_needed |= await self._async_set_power(True) - except OSError as err: - _LOGGER.error("Error turning on: %s", err) + if preset_mode: + update_needed |= await self._async_set_preset_mode_internal(preset_mode) - else: - update_needed = True + if percentage is not None: + update_needed |= await self._async_set_percentage_internal(percentage) if update_needed: # This state change affects other entities like sensors. Force an immediate update that @@ -193,12 +169,73 @@ class ValloxFanEntity(ValloxEntity, FanEntity): if not self.is_on: return - try: - await self._client.set_values({METRIC_KEY_MODE: MODE_OFF}) + update_needed = await self._async_set_power(False) - except OSError as err: - _LOGGER.error("Error turning off: %s", err) + if update_needed: + await self.coordinator.async_request_refresh() + + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed of the fan, as a percentage.""" + if percentage == 0: + await self.async_turn_off() return - # Same as for turn_on method. - await self.coordinator.async_request_refresh() + update_needed = await self._async_set_percentage_internal(percentage) + + if update_needed: + await self.coordinator.async_request_refresh() + + async def _async_set_power(self, mode: bool) -> bool: + try: + await self._client.set_values( + {METRIC_KEY_MODE: MODE_ON if mode else MODE_OFF} + ) + except ValloxApiException as err: + raise HomeAssistantError("Failed to set power mode") from err + + return True + + async def _async_set_preset_mode_internal(self, preset_mode: str) -> bool: + """ + Set new preset mode. + + Returns true if the mode has been changed, false otherwise. + """ + try: + self._valid_preset_mode_or_raise(preset_mode) + + except NotValidPresetModeError as err: + raise ValueError(f"Not valid preset mode: {preset_mode}") from err + + if preset_mode == self.preset_mode: + return False + + try: + profile = PRESET_MODE_TO_VALLOX_PROFILE_SETTABLE[preset_mode] + await self._client.set_profile(profile) + self.coordinator.data.profile = profile + + except ValloxApiException as err: + raise HomeAssistantError(f"Failed to set profile: {preset_mode}") from err + + return True + + async def _async_set_percentage_internal(self, percentage: int) -> bool: + """ + Set fan speed percentage for current profile. + + Returns true if speed has been changed, false otherwise. + """ + vallox_profile = self.coordinator.data.profile + + try: + await self._client.set_fan_speed(vallox_profile, percentage) + except ValloxInvalidInputException as err: + # This can happen if current profile does not support setting the fan speed. + raise ValueError( + f"{vallox_profile} profile does not support setting the fan speed" + ) from err + except ValloxApiException as err: + raise HomeAssistantError("Failed to set fan speed") from err + + return True diff --git a/homeassistant/components/vallox/manifest.json b/homeassistant/components/vallox/manifest.json index 5c862562fc1..1e2783a5b9c 100644 --- a/homeassistant/components/vallox/manifest.json +++ b/homeassistant/components/vallox/manifest.json @@ -2,7 +2,7 @@ "domain": "vallox", "name": "Vallox", "documentation": "https://www.home-assistant.io/integrations/vallox", - "requirements": ["vallox-websocket-api==2.12.0"], + "requirements": ["vallox-websocket-api==3.0.0"], "codeowners": ["@andre-richter", "@slovdahl", "@viiru-"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index c349107a3f3..c0cc5150ef2 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -29,7 +29,7 @@ from .const import ( METRIC_KEY_MODE, MODE_ON, VALLOX_CELL_STATE_TO_STR, - VALLOX_PROFILE_TO_STR_REPORTABLE, + VALLOX_PROFILE_TO_PRESET_MODE_REPORTABLE, ) @@ -76,7 +76,7 @@ class ValloxProfileSensor(ValloxSensorEntity): def native_value(self) -> StateType: """Return the value reported by the sensor.""" vallox_profile = self.coordinator.data.profile - return VALLOX_PROFILE_TO_STR_REPORTABLE.get(vallox_profile) + return VALLOX_PROFILE_TO_PRESET_MODE_REPORTABLE.get(vallox_profile) # There is a quirk with respect to the fan speed reporting. The device keeps on reporting the last diff --git a/requirements_all.txt b/requirements_all.txt index de60083c59b..ff1310cc64b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2507,7 +2507,7 @@ url-normalize==1.4.3 uvcclient==0.11.0 # homeassistant.components.vallox -vallox-websocket-api==2.12.0 +vallox-websocket-api==3.0.0 # homeassistant.components.rdw vehicle==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d0cf652b85..8e4397402ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1741,7 +1741,7 @@ url-normalize==1.4.3 uvcclient==0.11.0 # homeassistant.components.vallox -vallox-websocket-api==2.12.0 +vallox-websocket-api==3.0.0 # homeassistant.components.rdw vehicle==0.4.0 diff --git a/tests/components/vallox/conftest.py b/tests/components/vallox/conftest.py index 0c14f359b5f..60d8bfc0562 100644 --- a/tests/components/vallox/conftest.py +++ b/tests/components/vallox/conftest.py @@ -39,13 +39,36 @@ def patch_metrics(metrics: dict[str, Any]): ) +def patch_profile(profile: PROFILE): + """Patch the Vallox metrics response.""" + return patch( + "homeassistant.components.vallox.Vallox.get_profile", + return_value=profile, + ) + + +def patch_profile_set(): + """Patch the Vallox metrics set values.""" + return patch("homeassistant.components.vallox.Vallox.set_profile") + + def patch_metrics_set(): """Patch the Vallox metrics set values.""" return patch("homeassistant.components.vallox.Vallox.set_values") @pytest.fixture(autouse=True) -def patch_profile_home(): +def patch_empty_metrics(): + """Patch the Vallox profile response.""" + with patch( + "homeassistant.components.vallox.Vallox.fetch_metrics", + return_value={}, + ): + yield + + +@pytest.fixture(autouse=True) +def patch_default_profile(): """Patch the Vallox profile response.""" with patch( "homeassistant.components.vallox.Vallox.get_profile", diff --git a/tests/components/vallox/test_config_flow.py b/tests/components/vallox/test_config_flow.py index b0c951383b8..39de026bdbb 100644 --- a/tests/components/vallox/test_config_flow.py +++ b/tests/components/vallox/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Vallox integration config flow.""" from unittest.mock import patch -from vallox_websocket_api.exceptions import ValloxApiException +from vallox_websocket_api import ValloxApiException, ValloxWebsocketException from homeassistant.components.vallox.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER @@ -95,7 +95,7 @@ async def test_form_os_error_cannot_connect(hass: HomeAssistant) -> None: with patch( "homeassistant.components.vallox.config_flow.Vallox.get_info", - side_effect=OSError, + side_effect=ValloxWebsocketException, ): result = await hass.config_entries.flow.async_configure( init["flow_id"], @@ -243,7 +243,7 @@ async def test_import_cannot_connect_os_error(hass: HomeAssistant) -> None: with patch( "homeassistant.components.vallox.config_flow.Vallox.get_info", - side_effect=OSError, + side_effect=ValloxWebsocketException, ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/vallox/test_fan.py b/tests/components/vallox/test_fan.py new file mode 100644 index 00000000000..beb9a2647ef --- /dev/null +++ b/tests/components/vallox/test_fan.py @@ -0,0 +1,259 @@ +"""Tests for Vallox fan platform.""" +from unittest.mock import call + +import pytest +from vallox_websocket_api import PROFILE, ValloxApiException + +from homeassistant.components.fan import ( + ATTR_PERCENTAGE, + ATTR_PRESET_MODE, + DOMAIN as FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, + SERVICE_SET_PRESET_MODE, +) +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from .conftest import patch_metrics, patch_metrics_set, patch_profile, patch_profile_set + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "metrics, expected_state", [({"A_CYC_MODE": 0}, "on"), ({"A_CYC_MODE": 5}, "off")] +) +async def test_fan_state( + metrics: dict[str, int], + expected_state: str, + mock_entry: MockConfigEntry, + hass: HomeAssistant, +) -> None: + """Test fan on/off state.""" + + # Act + with patch_metrics(metrics=metrics): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Assert + sensor = hass.states.get("fan.vallox") + assert sensor + assert sensor.state == expected_state + + +@pytest.mark.parametrize( + "profile, expected_preset", + [ + (PROFILE.HOME, "Home"), + (PROFILE.AWAY, "Away"), + (PROFILE.BOOST, "Boost"), + (PROFILE.FIREPLACE, "Fireplace"), + ], +) +async def test_fan_profile( + profile: PROFILE, + expected_preset: str, + mock_entry: MockConfigEntry, + hass: HomeAssistant, +) -> None: + """Test fan profile.""" + + # Act + with patch_profile(profile): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Assert + sensor = hass.states.get("fan.vallox") + assert sensor + assert sensor.attributes["preset_mode"] == expected_preset + + +@pytest.mark.parametrize( + "service, initial_metrics, expected_called_with", + [ + (SERVICE_TURN_ON, {"A_CYC_MODE": 5}, {"A_CYC_MODE": 0}), + (SERVICE_TURN_OFF, {"A_CYC_MODE": 0}, {"A_CYC_MODE": 5}), + ], +) +async def test_turn_on_off( + service: str, + initial_metrics: dict[str, int], + expected_called_with: dict[str, int], + mock_entry: MockConfigEntry, + hass: HomeAssistant, +) -> None: + """Test turn on/off.""" + with patch_metrics(metrics=initial_metrics), patch_metrics_set() as metrics_set: + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + await hass.services.async_call( + FAN_DOMAIN, + service, + service_data={ATTR_ENTITY_ID: "fan.vallox"}, + blocking=True, + ) + metrics_set.assert_called_once_with(expected_called_with) + + +@pytest.mark.parametrize( + "initial_metrics, expected_call_args_list", + [ + ( + {"A_CYC_MODE": 5}, + [ + call({"A_CYC_MODE": 0}), + call({"A_CYC_AWAY_SPEED_SETTING": 15}), + ], + ), + ( + {"A_CYC_MODE": 0}, + [ + call({"A_CYC_AWAY_SPEED_SETTING": 15}), + ], + ), + ], +) +async def test_turn_on_with_parameters( + initial_metrics: dict[str, int], + expected_call_args_list: list[tuple], + mock_entry: MockConfigEntry, + hass: HomeAssistant, +) -> None: + """Test turn on/off.""" + with patch_metrics( + metrics=initial_metrics + ), patch_metrics_set() as metrics_set, patch_profile_set() as profile_set: + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + service_data={ + ATTR_ENTITY_ID: "fan.vallox", + ATTR_PERCENTAGE: "15", + ATTR_PRESET_MODE: "Away", + }, + blocking=True, + ) + assert metrics_set.call_args_list == expected_call_args_list + profile_set.assert_called_once_with(PROFILE.AWAY) + + +@pytest.mark.parametrize( + "preset, initial_profile, expected_call_args_list", + [ + ("Home", PROFILE.AWAY, [call(PROFILE.HOME)]), + ("Away", PROFILE.HOME, [call(PROFILE.AWAY)]), + ("Boost", PROFILE.HOME, [call(PROFILE.BOOST)]), + ("Fireplace", PROFILE.HOME, [call(PROFILE.FIREPLACE)]), + ("Home", PROFILE.HOME, []), + ], +) +async def test_set_preset_mode( + preset: str, + initial_profile: PROFILE, + expected_call_args_list: list[tuple], + mock_entry: MockConfigEntry, + hass: HomeAssistant, +) -> None: + """Test set preset mode.""" + with patch_profile(initial_profile), patch_profile_set() as profile_set: + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PRESET_MODE, + service_data={ATTR_ENTITY_ID: "fan.vallox", ATTR_PRESET_MODE: preset}, + blocking=True, + ) + assert profile_set.call_args_list == expected_call_args_list + + +async def test_set_invalid_preset_mode( + mock_entry: MockConfigEntry, + hass: HomeAssistant, +) -> None: + """Test set preset mode.""" + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + with pytest.raises(ValueError): + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PRESET_MODE, + service_data={ + ATTR_ENTITY_ID: "fan.vallox", + ATTR_PRESET_MODE: "Invalid", + }, + blocking=True, + ) + + +async def test_set_preset_mode_exception( + mock_entry: MockConfigEntry, + hass: HomeAssistant, +) -> None: + """Test set preset mode.""" + with patch_profile_set() as profile_set: + profile_set.side_effect = ValloxApiException("Fake exception") + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PRESET_MODE, + service_data={ATTR_ENTITY_ID: "fan.vallox", ATTR_PRESET_MODE: "Away"}, + blocking=True, + ) + + +@pytest.mark.parametrize( + "profile, percentage, expected_call_args_list", + [ + (PROFILE.HOME, 40, [call({"A_CYC_HOME_SPEED_SETTING": 40})]), + (PROFILE.AWAY, 30, [call({"A_CYC_AWAY_SPEED_SETTING": 30})]), + (PROFILE.BOOST, 60, [call({"A_CYC_BOOST_SPEED_SETTING": 60})]), + (PROFILE.HOME, 0, [call({"A_CYC_MODE": 5})]), + ], +) +async def test_set_fan_speed( + profile: PROFILE, + percentage: int, + expected_call_args_list: list[tuple], + mock_entry: MockConfigEntry, + hass: HomeAssistant, +) -> None: + """Test set fan speed percentage.""" + with patch_profile(profile), patch_metrics_set() as metrics_set, patch_metrics( + {"A_CYC_MODE": 0} + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, + service_data={ATTR_ENTITY_ID: "fan.vallox", ATTR_PERCENTAGE: percentage}, + blocking=True, + ) + assert metrics_set.call_args_list == expected_call_args_list + + +async def test_set_fan_speed_exception( + mock_entry: MockConfigEntry, + hass: HomeAssistant, +) -> None: + """Test set fan speed percentage.""" + with patch_metrics_set() as metrics_set, patch_metrics( + {"A_CYC_MODE": 0, "A_CYC_HOME_SPEED_SETTING": 30} + ): + metrics_set.side_effect = ValloxApiException("Fake failure") + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, + service_data={ATTR_ENTITY_ID: "fan.vallox", ATTR_PERCENTAGE: 5}, + blocking=True, + )