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 <marhje52@gmail.com>

* tests fix

* vallox_websocket_api 3.0.0

* more coverage

* test coverage

* Update tests/components/vallox/test_fan.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* raise exceptions on user input

* Supported features are different per preset mode.

* Test fixes

* Static supported features is back.

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Jevgeni Kiski 2022-12-06 12:00:59 +02:00 committed by GitHub
parent a3c4996291
commit d62bdbb9ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 401 additions and 92 deletions

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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 = {

View file

@ -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

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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,

View file

@ -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,
)