mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 01:38:02 +00:00
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
@ -1416,9 +1416,6 @@ omit =
|
|||||||
homeassistant/components/upnp/__init__.py
|
homeassistant/components/upnp/__init__.py
|
||||||
homeassistant/components/upnp/device.py
|
homeassistant/components/upnp/device.py
|
||||||
homeassistant/components/upnp/sensor.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/vasttrafik/sensor.py
|
||||||
homeassistant/components/velbus/__init__.py
|
homeassistant/components/velbus/__init__.py
|
||||||
homeassistant/components/velbus/binary_sensor.py
|
homeassistant/components/velbus/binary_sensor.py
|
||||||
|
@ -8,8 +8,7 @@ import logging
|
|||||||
from typing import Any, NamedTuple, cast
|
from typing import Any, NamedTuple, cast
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from vallox_websocket_api import PROFILE as VALLOX_PROFILE, Vallox
|
from vallox_websocket_api import PROFILE as VALLOX_PROFILE, Vallox, ValloxApiException
|
||||||
from vallox_websocket_api.exceptions import ValloxApiException
|
|
||||||
from vallox_websocket_api.vallox import (
|
from vallox_websocket_api.vallox import (
|
||||||
get_model as _api_get_model,
|
get_model as _api_get_model,
|
||||||
get_next_filter_change_date as _api_get_next_filter_change_date,
|
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()
|
metric_cache = await client.fetch_metrics()
|
||||||
profile = await client.get_profile()
|
profile = await client.get_profile()
|
||||||
|
|
||||||
except (OSError, ValloxApiException) as err:
|
except ValloxApiException as err:
|
||||||
raise UpdateFailed("Error during state cache update") from err
|
raise UpdateFailed("Error during state cache update") from err
|
||||||
|
|
||||||
return ValloxState(metric_cache, profile)
|
return ValloxState(metric_cache, profile)
|
||||||
@ -262,7 +261,7 @@ class ValloxServiceHandler:
|
|||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except (OSError, ValloxApiException) as err:
|
except ValloxApiException as err:
|
||||||
_LOGGER.error("Error setting fan speed for Home profile: %s", err)
|
_LOGGER.error("Error setting fan speed for Home profile: %s", err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -278,7 +277,7 @@ class ValloxServiceHandler:
|
|||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except (OSError, ValloxApiException) as err:
|
except ValloxApiException as err:
|
||||||
_LOGGER.error("Error setting fan speed for Away profile: %s", err)
|
_LOGGER.error("Error setting fan speed for Away profile: %s", err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -294,7 +293,7 @@ class ValloxServiceHandler:
|
|||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except (OSError, ValloxApiException) as err:
|
except ValloxApiException as err:
|
||||||
_LOGGER.error("Error setting fan speed for Boost profile: %s", err)
|
_LOGGER.error("Error setting fan speed for Boost profile: %s", err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -4,8 +4,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from vallox_websocket_api import Vallox
|
from vallox_websocket_api import Vallox, ValloxApiException
|
||||||
from vallox_websocket_api.exceptions import ValloxApiException
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
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:
|
async def validate_host(hass: HomeAssistant, host: str) -> None:
|
||||||
"""Validate that the user input allows us to connect."""
|
"""Validate that the user input allows us to connect."""
|
||||||
@ -61,7 +55,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
except InvalidHost:
|
except InvalidHost:
|
||||||
_LOGGER.error("An invalid host is configured for Vallox: %s", host)
|
_LOGGER.error("An invalid host is configured for Vallox: %s", host)
|
||||||
reason = "invalid_host"
|
reason = "invalid_host"
|
||||||
except VALLOX_CONNECTION_EXCEPTIONS:
|
except ValloxApiException:
|
||||||
_LOGGER.error("Cannot connect to Vallox host %s", host)
|
_LOGGER.error("Cannot connect to Vallox host %s", host)
|
||||||
reason = "cannot_connect"
|
reason = "cannot_connect"
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
@ -98,7 +92,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
await validate_host(self.hass, host)
|
await validate_host(self.hass, host)
|
||||||
except InvalidHost:
|
except InvalidHost:
|
||||||
errors[CONF_HOST] = "invalid_host"
|
errors[CONF_HOST] = "invalid_host"
|
||||||
except VALLOX_CONNECTION_EXCEPTIONS:
|
except ValloxApiException:
|
||||||
errors[CONF_HOST] = "cannot_connect"
|
errors[CONF_HOST] = "cannot_connect"
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
@ -22,20 +22,20 @@ DEFAULT_FAN_SPEED_HOME = 50
|
|||||||
DEFAULT_FAN_SPEED_AWAY = 25
|
DEFAULT_FAN_SPEED_AWAY = 25
|
||||||
DEFAULT_FAN_SPEED_BOOST = 65
|
DEFAULT_FAN_SPEED_BOOST = 65
|
||||||
|
|
||||||
VALLOX_PROFILE_TO_STR_SETTABLE = {
|
VALLOX_PROFILE_TO_PRESET_MODE_SETTABLE = {
|
||||||
VALLOX_PROFILE.HOME: "Home",
|
VALLOX_PROFILE.HOME: "Home",
|
||||||
VALLOX_PROFILE.AWAY: "Away",
|
VALLOX_PROFILE.AWAY: "Away",
|
||||||
VALLOX_PROFILE.BOOST: "Boost",
|
VALLOX_PROFILE.BOOST: "Boost",
|
||||||
VALLOX_PROFILE.FIREPLACE: "Fireplace",
|
VALLOX_PROFILE.FIREPLACE: "Fireplace",
|
||||||
}
|
}
|
||||||
|
|
||||||
VALLOX_PROFILE_TO_STR_REPORTABLE = {
|
VALLOX_PROFILE_TO_PRESET_MODE_REPORTABLE = {
|
||||||
VALLOX_PROFILE.EXTRA: "Extra",
|
VALLOX_PROFILE.EXTRA: "Extra",
|
||||||
**VALLOX_PROFILE_TO_STR_SETTABLE,
|
**VALLOX_PROFILE_TO_PRESET_MODE_SETTABLE,
|
||||||
}
|
}
|
||||||
|
|
||||||
STR_TO_VALLOX_PROFILE_SETTABLE = {
|
PRESET_MODE_TO_VALLOX_PROFILE_SETTABLE = {
|
||||||
value: key for (key, value) in VALLOX_PROFILE_TO_STR_SETTABLE.items()
|
value: key for (key, value) in VALLOX_PROFILE_TO_PRESET_MODE_SETTABLE.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
VALLOX_CELL_STATE_TO_STR = {
|
VALLOX_CELL_STATE_TO_STR = {
|
||||||
|
@ -2,11 +2,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
import logging
|
|
||||||
from typing import Any, NamedTuple
|
from typing import Any, NamedTuple
|
||||||
|
|
||||||
from vallox_websocket_api import Vallox
|
from vallox_websocket_api import (
|
||||||
from vallox_websocket_api.exceptions import ValloxApiException
|
PROFILE_TO_SET_FAN_SPEED_METRIC_MAP,
|
||||||
|
Vallox,
|
||||||
|
ValloxApiException,
|
||||||
|
ValloxInvalidInputException,
|
||||||
|
)
|
||||||
|
|
||||||
from homeassistant.components.fan import (
|
from homeassistant.components.fan import (
|
||||||
FanEntity,
|
FanEntity,
|
||||||
@ -15,6 +18,7 @@ from homeassistant.components.fan import (
|
|||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
@ -27,12 +31,10 @@ from .const import (
|
|||||||
METRIC_KEY_PROFILE_FAN_SPEED_HOME,
|
METRIC_KEY_PROFILE_FAN_SPEED_HOME,
|
||||||
MODE_OFF,
|
MODE_OFF,
|
||||||
MODE_ON,
|
MODE_ON,
|
||||||
STR_TO_VALLOX_PROFILE_SETTABLE,
|
PRESET_MODE_TO_VALLOX_PROFILE_SETTABLE,
|
||||||
VALLOX_PROFILE_TO_STR_SETTABLE,
|
VALLOX_PROFILE_TO_PRESET_MODE_REPORTABLE,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ExtraStateAttributeDetails(NamedTuple):
|
class ExtraStateAttributeDetails(NamedTuple):
|
||||||
"""Extra state attribute details."""
|
"""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)):
|
if isinstance(value, (int, float)):
|
||||||
return int(value)
|
return int(value)
|
||||||
|
|
||||||
@ -68,7 +70,6 @@ async def async_setup_entry(
|
|||||||
data = hass.data[DOMAIN][entry.entry_id]
|
data = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
client = data["client"]
|
client = data["client"]
|
||||||
client.set_settable_address(METRIC_KEY_MODE, int)
|
|
||||||
|
|
||||||
device = ValloxFanEntity(
|
device = ValloxFanEntity(
|
||||||
data["name"],
|
data["name"],
|
||||||
@ -82,8 +83,8 @@ async def async_setup_entry(
|
|||||||
class ValloxFanEntity(ValloxEntity, FanEntity):
|
class ValloxFanEntity(ValloxEntity, FanEntity):
|
||||||
"""Representation of the fan."""
|
"""Representation of the fan."""
|
||||||
|
|
||||||
_attr_supported_features = FanEntityFeature.PRESET_MODE
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
|
_attr_supported_features = FanEntityFeature.PRESET_MODE | FanEntityFeature.SET_SPEED
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -97,12 +98,7 @@ class ValloxFanEntity(ValloxEntity, FanEntity):
|
|||||||
self._client = client
|
self._client = client
|
||||||
|
|
||||||
self._attr_unique_id = str(self._device_uuid)
|
self._attr_unique_id = str(self._device_uuid)
|
||||||
|
self._attr_preset_modes = list(PRESET_MODE_TO_VALLOX_PROFILE_SETTABLE)
|
||||||
@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())
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
@ -113,7 +109,18 @@ class ValloxFanEntity(ValloxEntity, FanEntity):
|
|||||||
def preset_mode(self) -> str | None:
|
def preset_mode(self) -> str | None:
|
||||||
"""Return the current preset mode."""
|
"""Return the current preset mode."""
|
||||||
vallox_profile = self.coordinator.data.profile
|
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
|
@property
|
||||||
def extra_state_attributes(self) -> Mapping[str, int | None]:
|
def extra_state_attributes(self) -> Mapping[str, int | None]:
|
||||||
@ -121,35 +128,10 @@ class ValloxFanEntity(ValloxEntity, FanEntity):
|
|||||||
data = self.coordinator.data
|
data = self.coordinator.data
|
||||||
|
|
||||||
return {
|
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
|
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:
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||||
"""Set new preset mode."""
|
"""Set new preset mode."""
|
||||||
update_needed = await self._async_set_preset_mode_internal(preset_mode)
|
update_needed = await self._async_set_preset_mode_internal(preset_mode)
|
||||||
@ -166,22 +148,16 @@ class ValloxFanEntity(ValloxEntity, FanEntity):
|
|||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
_LOGGER.debug("Turn on")
|
|
||||||
|
|
||||||
update_needed = False
|
update_needed = False
|
||||||
|
|
||||||
if preset_mode:
|
|
||||||
update_needed = await self._async_set_preset_mode_internal(preset_mode)
|
|
||||||
|
|
||||||
if not self.is_on:
|
if not self.is_on:
|
||||||
try:
|
update_needed |= await self._async_set_power(True)
|
||||||
await self._client.set_values({METRIC_KEY_MODE: MODE_ON})
|
|
||||||
|
|
||||||
except OSError as err:
|
if preset_mode:
|
||||||
_LOGGER.error("Error turning on: %s", err)
|
update_needed |= await self._async_set_preset_mode_internal(preset_mode)
|
||||||
|
|
||||||
else:
|
if percentage is not None:
|
||||||
update_needed = True
|
update_needed |= await self._async_set_percentage_internal(percentage)
|
||||||
|
|
||||||
if update_needed:
|
if update_needed:
|
||||||
# This state change affects other entities like sensors. Force an immediate update that
|
# 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:
|
if not self.is_on:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
update_needed = await self._async_set_power(False)
|
||||||
await self._client.set_values({METRIC_KEY_MODE: MODE_OFF})
|
|
||||||
|
|
||||||
except OSError as err:
|
if update_needed:
|
||||||
_LOGGER.error("Error turning off: %s", err)
|
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
|
return
|
||||||
|
|
||||||
# Same as for turn_on method.
|
update_needed = await self._async_set_percentage_internal(percentage)
|
||||||
|
|
||||||
|
if update_needed:
|
||||||
await self.coordinator.async_request_refresh()
|
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",
|
"domain": "vallox",
|
||||||
"name": "Vallox",
|
"name": "Vallox",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/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-"],
|
"codeowners": ["@andre-richter", "@slovdahl", "@viiru-"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
|
@ -29,7 +29,7 @@ from .const import (
|
|||||||
METRIC_KEY_MODE,
|
METRIC_KEY_MODE,
|
||||||
MODE_ON,
|
MODE_ON,
|
||||||
VALLOX_CELL_STATE_TO_STR,
|
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:
|
def native_value(self) -> StateType:
|
||||||
"""Return the value reported by the sensor."""
|
"""Return the value reported by the sensor."""
|
||||||
vallox_profile = self.coordinator.data.profile
|
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
|
# 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
|
uvcclient==0.11.0
|
||||||
|
|
||||||
# homeassistant.components.vallox
|
# homeassistant.components.vallox
|
||||||
vallox-websocket-api==2.12.0
|
vallox-websocket-api==3.0.0
|
||||||
|
|
||||||
# homeassistant.components.rdw
|
# homeassistant.components.rdw
|
||||||
vehicle==0.4.0
|
vehicle==0.4.0
|
||||||
|
@ -1741,7 +1741,7 @@ url-normalize==1.4.3
|
|||||||
uvcclient==0.11.0
|
uvcclient==0.11.0
|
||||||
|
|
||||||
# homeassistant.components.vallox
|
# homeassistant.components.vallox
|
||||||
vallox-websocket-api==2.12.0
|
vallox-websocket-api==3.0.0
|
||||||
|
|
||||||
# homeassistant.components.rdw
|
# homeassistant.components.rdw
|
||||||
vehicle==0.4.0
|
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():
|
def patch_metrics_set():
|
||||||
"""Patch the Vallox metrics set values."""
|
"""Patch the Vallox metrics set values."""
|
||||||
return patch("homeassistant.components.vallox.Vallox.set_values")
|
return patch("homeassistant.components.vallox.Vallox.set_values")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@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."""
|
"""Patch the Vallox profile response."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.vallox.Vallox.get_profile",
|
"homeassistant.components.vallox.Vallox.get_profile",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Test the Vallox integration config flow."""
|
"""Test the Vallox integration config flow."""
|
||||||
from unittest.mock import patch
|
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.components.vallox.const import DOMAIN
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
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(
|
with patch(
|
||||||
"homeassistant.components.vallox.config_flow.Vallox.get_info",
|
"homeassistant.components.vallox.config_flow.Vallox.get_info",
|
||||||
side_effect=OSError,
|
side_effect=ValloxWebsocketException,
|
||||||
):
|
):
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
init["flow_id"],
|
init["flow_id"],
|
||||||
@ -243,7 +243,7 @@ async def test_import_cannot_connect_os_error(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.vallox.config_flow.Vallox.get_info",
|
"homeassistant.components.vallox.config_flow.Vallox.get_info",
|
||||||
side_effect=OSError,
|
side_effect=ValloxWebsocketException,
|
||||||
):
|
):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
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…
x
Reference in New Issue
Block a user