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:
parent
a3c4996291
commit
d62bdbb9ff
12 changed files with 401 additions and 92 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
259
tests/components/vallox/test_fan.py
Normal file
259
tests/components/vallox/test_fan.py
Normal 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,
|
||||
)
|
Loading…
Add table
Reference in a new issue