1
mirror of https://github.com/home-assistant/core synced 2024-08-28 03:36:46 +02:00

Use ServiceValidationError for invalid fan preset_mode and move check to fan entity component (#104560)

* Use ServiceValidationError for fan preset_mode

* Use _valid_preset_mode_or_raise to raise

* Move preset_mode validation to entity component

* Fix bond fan and comments

* Fixes baf, fjaraskupan and template

* More integration adjustments

* Add custom components mock and test code

* Make NotValidPresetModeError subclass

* Update homeassistant/components/fan/strings.json

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

* Keep bond has_action validation

* Move demo test asserts outside context block

* Follow up comment

* Update homeassistant/components/fan/strings.json

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Fix demo tests

* Remove pylint disable

* Remove unreachable code

* Update homeassistant/components/fan/__init__.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Use NotValidPresetModeError, Final methods

* Address comments

* Correct docst

* Follow up comments

* Update homeassistant/components/fan/__init__.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Jan Bouwhuis 2023-11-29 13:56:51 +01:00 committed by GitHub
parent 49381cefa3
commit 953a212dd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 260 additions and 118 deletions

View File

@ -93,8 +93,6 @@ class BAFFan(BAFEntity, FanEntity):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan."""
if preset_mode != PRESET_MODE_AUTO:
raise ValueError(f"Invalid preset mode: {preset_mode}")
self._device.fan_mode = OffOnAuto.AUTO
async def async_set_direction(self, direction: str) -> None:

View File

@ -199,10 +199,6 @@ class BondFan(BondEntity, FanEntity):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan."""
if preset_mode != PRESET_MODE_BREEZE or not self._device.has_action(
Action.BREEZE_ON
):
raise ValueError(f"Invalid preset mode: {preset_mode}")
await self._hub.bond.action(self._device.device_id, Action(Action.BREEZE_ON))
async def async_turn_off(self, **kwargs: Any) -> None:

View File

@ -161,12 +161,9 @@ class DemoPercentageFan(BaseDemoFan, FanEntity):
def set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
if self.preset_modes and preset_mode in self.preset_modes:
self._preset_mode = preset_mode
self._percentage = None
self.schedule_update_ha_state()
else:
raise ValueError(f"Invalid preset mode: {preset_mode}")
self._preset_mode = preset_mode
self._percentage = None
self.schedule_update_ha_state()
def turn_on(
self,
@ -230,10 +227,6 @@ class AsyncDemoPercentageFan(BaseDemoFan, FanEntity):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
if self.preset_modes is None or preset_mode not in self.preset_modes:
raise ValueError(
f"{preset_mode} is not a valid preset_mode: {self.preset_modes}"
)
self._preset_mode = preset_mode
self._percentage = None
self.async_write_ha_state()

View File

@ -18,7 +18,8 @@ from homeassistant.const import (
SERVICE_TURN_ON,
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
@ -77,8 +78,19 @@ ATTR_PRESET_MODES = "preset_modes"
# mypy: disallow-any-generics
class NotValidPresetModeError(ValueError):
"""Exception class when the preset_mode in not in the preset_modes list."""
class NotValidPresetModeError(ServiceValidationError):
"""Raised when the preset_mode is not in the preset_modes list."""
def __init__(
self, *args: object, translation_placeholders: dict[str, str] | None = None
) -> None:
"""Initialize the exception."""
super().__init__(
*args,
translation_domain=DOMAIN,
translation_key="not_valid_preset_mode",
translation_placeholders=translation_placeholders,
)
@bind_hass
@ -107,7 +119,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
),
vol.Optional(ATTR_PRESET_MODE): cv.string,
},
"async_turn_on",
"async_handle_turn_on_service",
)
component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off")
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle")
@ -156,7 +168,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
component.async_register_entity_service(
SERVICE_SET_PRESET_MODE,
{vol.Required(ATTR_PRESET_MODE): cv.string},
"async_set_preset_mode",
"async_handle_set_preset_mode_service",
[FanEntityFeature.SET_SPEED, FanEntityFeature.PRESET_MODE],
)
@ -237,17 +249,30 @@ class FanEntity(ToggleEntity):
"""Set new preset mode."""
raise NotImplementedError()
@final
async def async_handle_set_preset_mode_service(self, preset_mode: str) -> None:
"""Validate and set new preset mode."""
self._valid_preset_mode_or_raise(preset_mode)
await self.async_set_preset_mode(preset_mode)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
await self.hass.async_add_executor_job(self.set_preset_mode, preset_mode)
@final
@callback
def _valid_preset_mode_or_raise(self, preset_mode: str) -> None:
"""Raise NotValidPresetModeError on invalid preset_mode."""
preset_modes = self.preset_modes
if not preset_modes or preset_mode not in preset_modes:
preset_modes_str: str = ", ".join(preset_modes or [])
raise NotValidPresetModeError(
f"The preset_mode {preset_mode} is not a valid preset_mode:"
f" {preset_modes}"
f" {preset_modes}",
translation_placeholders={
"preset_mode": preset_mode,
"preset_modes": preset_modes_str,
},
)
def set_direction(self, direction: str) -> None:
@ -267,6 +292,18 @@ class FanEntity(ToggleEntity):
"""Turn on the fan."""
raise NotImplementedError()
@final
async def async_handle_turn_on_service(
self,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any,
) -> None:
"""Validate and turn on the fan."""
if preset_mode is not None:
self._valid_preset_mode_or_raise(preset_mode)
await self.async_turn_on(percentage, preset_mode, **kwargs)
async def async_turn_on(
self,
percentage: int | None = None,

View File

@ -144,5 +144,10 @@
"reverse": "Reverse"
}
}
},
"exceptions": {
"not_valid_preset_mode": {
"message": "Preset mode {preset_mode} is not valid, valid preset modes are: {preset_modes}."
}
}
}

View File

@ -131,11 +131,9 @@ class Fan(CoordinatorEntity[FjaraskupanCoordinator], FanEntity):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
if command := PRESET_TO_COMMAND.get(preset_mode):
async with self.coordinator.async_connect_and_update() as device:
await device.send_command(command)
else:
raise UnsupportedPreset(f"The preset {preset_mode} is unsupported")
command = PRESET_TO_COMMAND[preset_mode]
async with self.coordinator.async_connect_and_update() as device:
await device.send_command(command)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""

View File

@ -553,8 +553,6 @@ class MqttFan(MqttEntity, FanEntity):
This method is a coroutine.
"""
self._valid_preset_mode_or_raise(preset_mode)
mqtt_payload = self._command_templates[ATTR_PRESET_MODE](preset_mode)
await self.async_publish(

View File

@ -282,15 +282,6 @@ class TemplateFan(TemplateEntity, FanEntity):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset_mode of the fan."""
if self.preset_modes and preset_mode not in self.preset_modes:
_LOGGER.error(
"Received invalid preset_mode: %s for entity %s. Expected: %s",
preset_mode,
self.entity_id,
self.preset_modes,
)
return
self._preset_mode = preset_mode
if self._set_preset_mode_script:

View File

@ -119,8 +119,7 @@ class TradfriAirPurifierFan(TradfriBaseEntity, FanEntity):
if not self._device_control:
return
if not preset_mode == ATTR_AUTO:
raise ValueError("Preset must be 'Auto'.")
# Preset must be 'Auto'
await self._api(self._device_control.turn_on_auto_mode())

View File

@ -11,11 +11,7 @@ from vallox_websocket_api import (
ValloxInvalidInputException,
)
from homeassistant.components.fan import (
FanEntity,
FanEntityFeature,
NotValidPresetModeError,
)
from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@ -200,12 +196,6 @@ class ValloxFanEntity(ValloxEntity, FanEntity):
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

View File

@ -530,9 +530,6 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier):
This method is a coroutine.
"""
if preset_mode not in self.preset_modes:
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
return
if await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode,
@ -623,9 +620,6 @@ class XiaomiAirPurifierMB4(XiaomiGenericAirPurifier):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan."""
if preset_mode not in self.preset_modes:
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
return
if await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode,
@ -721,9 +715,6 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier):
This method is a coroutine.
"""
if preset_mode not in self.preset_modes:
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
return
if await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode,
@ -809,9 +800,6 @@ class XiaomiAirFreshA1(XiaomiGenericAirPurifier):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan. This method is a coroutine."""
if preset_mode not in self.preset_modes:
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
return
if await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode,
@ -958,10 +946,6 @@ class XiaomiFan(XiaomiGenericFan):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan."""
if preset_mode not in self.preset_modes:
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
return
if preset_mode == ATTR_MODE_NATURE:
await self._try_command(
"Setting natural fan speed percentage of the miio device failed.",
@ -1034,9 +1018,6 @@ class XiaomiFanP5(XiaomiGenericFan):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan."""
if preset_mode not in self.preset_modes:
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
return
await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode,
@ -1093,9 +1074,6 @@ class XiaomiFanMiot(XiaomiGenericFan):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan."""
if preset_mode not in self.preset_modes:
_LOGGER.warning("'%s'is not a valid preset mode", preset_mode)
return
await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode,

View File

@ -13,7 +13,6 @@ from homeassistant.components.fan import (
ATTR_PRESET_MODE,
FanEntity,
FanEntityFeature,
NotValidPresetModeError,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_UNAVAILABLE, Platform
@ -131,11 +130,6 @@ class BaseFan(FanEntity):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode for the fan."""
if preset_mode not in self.preset_modes:
raise NotValidPresetModeError(
f"The preset_mode {preset_mode} is not a valid preset_mode:"
f" {self.preset_modes}"
)
await self._async_set_fan_mode(self.preset_name_to_mode[preset_mode])
@abstractmethod

View File

@ -18,7 +18,6 @@ from homeassistant.components.fan import (
DOMAIN as FAN_DOMAIN,
FanEntity,
FanEntityFeature,
NotValidPresetModeError,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
@ -181,11 +180,6 @@ class ValueMappingZwaveFan(ZwaveFan):
await self._async_set_value(self._target_value, zwave_value)
return
raise NotValidPresetModeError(
f"The preset_mode {preset_mode} is not a valid preset_mode:"
f" {self.preset_modes}"
)
@property
def available(self) -> bool:
"""Return whether the entity is available."""

View File

@ -26,6 +26,7 @@ from homeassistant.components.fan import (
SERVICE_SET_PERCENTAGE,
SERVICE_SET_PRESET_MODE,
FanEntityFeature,
NotValidPresetModeError,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
@ -251,10 +252,14 @@ async def test_turn_on_fan_preset_mode_not_supported(hass: HomeAssistant) -> Non
props={"max_speed": 6},
)
with patch_bond_action(), patch_bond_device_state(), pytest.raises(ValueError):
with patch_bond_action(), patch_bond_device_state(), pytest.raises(
NotValidPresetModeError
):
await turn_fan_on(hass, "fan.name_1", preset_mode=PRESET_MODE_BREEZE)
with patch_bond_action(), patch_bond_device_state(), pytest.raises(ValueError):
with patch_bond_action(), patch_bond_device_state(), pytest.raises(
NotValidPresetModeError
):
await hass.services.async_call(
FAN_DOMAIN,
SERVICE_SET_PRESET_MODE,

View File

@ -182,7 +182,7 @@ async def test_turn_on_with_preset_mode_only(
assert state.state == STATE_OFF
assert state.attributes[fan.ATTR_PRESET_MODE] is None
with pytest.raises(ValueError):
with pytest.raises(fan.NotValidPresetModeError) as exc:
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
@ -190,6 +190,12 @@ async def test_turn_on_with_preset_mode_only(
blocking=True,
)
await hass.async_block_till_done()
assert exc.value.translation_domain == fan.DOMAIN
assert exc.value.translation_key == "not_valid_preset_mode"
assert exc.value.translation_placeholders == {
"preset_mode": "invalid",
"preset_modes": "auto, smart, sleep, on",
}
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
@ -250,7 +256,7 @@ async def test_turn_on_with_preset_mode_and_speed(
assert state.attributes[fan.ATTR_PERCENTAGE] == 0
assert state.attributes[fan.ATTR_PRESET_MODE] is None
with pytest.raises(ValueError):
with pytest.raises(fan.NotValidPresetModeError) as exc:
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
@ -258,6 +264,12 @@ async def test_turn_on_with_preset_mode_and_speed(
blocking=True,
)
await hass.async_block_till_done()
assert exc.value.translation_domain == fan.DOMAIN
assert exc.value.translation_key == "not_valid_preset_mode"
assert exc.value.translation_placeholders == {
"preset_mode": "invalid",
"preset_modes": "auto, smart, sleep, on",
}
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
@ -343,7 +355,7 @@ async def test_set_preset_mode_invalid(hass: HomeAssistant, fan_entity_id) -> No
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
with pytest.raises(ValueError):
with pytest.raises(fan.NotValidPresetModeError) as exc:
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_SET_PRESET_MODE,
@ -351,8 +363,10 @@ async def test_set_preset_mode_invalid(hass: HomeAssistant, fan_entity_id) -> No
blocking=True,
)
await hass.async_block_till_done()
assert exc.value.translation_domain == fan.DOMAIN
assert exc.value.translation_key == "not_valid_preset_mode"
with pytest.raises(ValueError):
with pytest.raises(fan.NotValidPresetModeError) as exc:
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
@ -360,6 +374,8 @@ async def test_set_preset_mode_invalid(hass: HomeAssistant, fan_entity_id) -> No
blocking=True,
)
await hass.async_block_till_done()
assert exc.value.translation_domain == fan.DOMAIN
assert exc.value.translation_key == "not_valid_preset_mode"
@pytest.mark.parametrize("fan_entity_id", FULL_FAN_ENTITY_IDS)

View File

@ -1,8 +1,19 @@
"""Tests for fan platforms."""
import pytest
from homeassistant.components.fan import FanEntity
from homeassistant.components.fan import (
ATTR_PRESET_MODE,
ATTR_PRESET_MODES,
DOMAIN,
SERVICE_SET_PRESET_MODE,
FanEntity,
NotValidPresetModeError,
)
from homeassistant.core import HomeAssistant
import homeassistant.helpers.entity_registry as er
from homeassistant.setup import async_setup_component
from tests.testing_config.custom_components.test.fan import MockFan
class BaseFan(FanEntity):
@ -82,3 +93,55 @@ def test_fanentity_attributes(attribute_name, attribute_value) -> None:
fan = BaseFan()
setattr(fan, f"_attr_{attribute_name}", attribute_value)
assert getattr(fan, attribute_name) == attribute_value
async def test_preset_mode_validation(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
entity_registry: er.EntityRegistry,
enable_custom_integrations: None,
) -> None:
"""Test preset mode validation."""
await hass.async_block_till_done()
platform = getattr(hass.components, "test.fan")
platform.init(empty=False)
assert await async_setup_component(hass, "fan", {"fan": {"platform": "test"}})
await hass.async_block_till_done()
test_fan: MockFan = platform.ENTITIES["support_preset_mode"]
await hass.async_block_till_done()
state = hass.states.get("fan.support_fan_with_preset_mode_support")
assert state.attributes.get(ATTR_PRESET_MODES) == ["auto", "eco"]
await hass.services.async_call(
DOMAIN,
SERVICE_SET_PRESET_MODE,
{
"entity_id": "fan.support_fan_with_preset_mode_support",
"preset_mode": "eco",
},
blocking=True,
)
state = hass.states.get("fan.support_fan_with_preset_mode_support")
assert state.attributes.get(ATTR_PRESET_MODE) == "eco"
with pytest.raises(NotValidPresetModeError) as exc:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_PRESET_MODE,
{
"entity_id": "fan.support_fan_with_preset_mode_support",
"preset_mode": "invalid",
},
blocking=True,
)
assert exc.value.translation_key == "not_valid_preset_mode"
with pytest.raises(NotValidPresetModeError) as exc:
await test_fan._valid_preset_mode_or_raise("invalid")
assert exc.value.translation_key == "not_valid_preset_mode"

View File

@ -705,8 +705,9 @@ async def test_sending_mqtt_commands_and_optimistic(
assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0
assert state.attributes.get(ATTR_ASSUMED_STATE)
with pytest.raises(NotValidPresetModeError):
with pytest.raises(NotValidPresetModeError) as exc:
await common.async_set_preset_mode(hass, "fan.test", "low")
assert exc.value.translation_key == "not_valid_preset_mode"
await common.async_set_preset_mode(hass, "fan.test", "whoosh")
mqtt_mock.async_publish.assert_called_once_with(
@ -916,11 +917,13 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(
assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0
assert state.attributes.get(ATTR_ASSUMED_STATE)
with pytest.raises(NotValidPresetModeError):
with pytest.raises(NotValidPresetModeError) as exc:
await common.async_set_preset_mode(hass, "fan.test", "low")
assert exc.value.translation_key == "not_valid_preset_mode"
with pytest.raises(NotValidPresetModeError):
with pytest.raises(NotValidPresetModeError) as exc:
await common.async_set_preset_mode(hass, "fan.test", "auto")
assert exc.value.translation_key == "not_valid_preset_mode"
await common.async_set_preset_mode(hass, "fan.test", "whoosh")
mqtt_mock.async_publish.assert_called_once_with(
@ -976,8 +979,9 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(
assert state.state == STATE_ON
assert state.attributes.get(ATTR_ASSUMED_STATE)
with pytest.raises(NotValidPresetModeError):
with pytest.raises(NotValidPresetModeError) as exc:
await common.async_turn_on(hass, "fan.test", preset_mode="freaking-high")
assert exc.value.translation_key == "not_valid_preset_mode"
@pytest.mark.parametrize(
@ -1078,11 +1082,13 @@ async def test_sending_mqtt_command_templates_(
assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0
assert state.attributes.get(ATTR_ASSUMED_STATE)
with pytest.raises(NotValidPresetModeError):
with pytest.raises(NotValidPresetModeError) as exc:
await common.async_set_preset_mode(hass, "fan.test", "low")
assert exc.value.translation_key == "not_valid_preset_mode"
with pytest.raises(NotValidPresetModeError):
with pytest.raises(NotValidPresetModeError) as exc:
await common.async_set_preset_mode(hass, "fan.test", "medium")
assert exc.value.translation_key == "not_valid_preset_mode"
await common.async_set_preset_mode(hass, "fan.test", "whoosh")
mqtt_mock.async_publish.assert_called_once_with(
@ -1140,8 +1146,9 @@ async def test_sending_mqtt_command_templates_(
assert state.state == STATE_ON
assert state.attributes.get(ATTR_ASSUMED_STATE)
with pytest.raises(NotValidPresetModeError):
with pytest.raises(NotValidPresetModeError) as exc:
await common.async_turn_on(hass, "fan.test", preset_mode="low")
assert exc.value.translation_key == "not_valid_preset_mode"
@pytest.mark.parametrize(
@ -1176,8 +1183,9 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic(
assert state.state == STATE_UNKNOWN
assert state.attributes.get(ATTR_ASSUMED_STATE)
with pytest.raises(NotValidPresetModeError):
with pytest.raises(NotValidPresetModeError) as exc:
await common.async_set_preset_mode(hass, "fan.test", "medium")
assert exc.value.translation_key == "not_valid_preset_mode"
await common.async_set_preset_mode(hass, "fan.test", "whoosh")
mqtt_mock.async_publish.assert_called_once_with(
@ -1276,11 +1284,10 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(
assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
with pytest.raises(NotValidPresetModeError):
with pytest.raises(NotValidPresetModeError) as exc:
await common.async_turn_on(hass, "fan.test", preset_mode="auto")
assert mqtt_mock.async_publish.call_count == 1
# We can turn on, but the invalid preset mode will raise
mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False)
assert exc.value.translation_key == "not_valid_preset_mode"
assert mqtt_mock.async_publish.call_count == 0
mqtt_mock.async_publish.reset_mock()
await common.async_turn_on(hass, "fan.test", preset_mode="whoosh")
@ -1428,11 +1435,13 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(
with pytest.raises(MultipleInvalid):
await common.async_set_percentage(hass, "fan.test", 101)
with pytest.raises(NotValidPresetModeError):
with pytest.raises(NotValidPresetModeError) as exc:
await common.async_set_preset_mode(hass, "fan.test", "low")
assert exc.value.translation_key == "not_valid_preset_mode"
with pytest.raises(NotValidPresetModeError):
with pytest.raises(NotValidPresetModeError) as exc:
await common.async_set_preset_mode(hass, "fan.test", "medium")
assert exc.value.translation_key == "not_valid_preset_mode"
await common.async_set_preset_mode(hass, "fan.test", "whoosh")
mqtt_mock.async_publish.assert_called_once_with(
@ -1452,8 +1461,9 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(
assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)
with pytest.raises(NotValidPresetModeError):
with pytest.raises(NotValidPresetModeError) as exc:
await common.async_set_preset_mode(hass, "fan.test", "freaking-high")
assert exc.value.translation_key == "not_valid_preset_mode"
mqtt_mock.async_publish.reset_mock()
state = hass.states.get("fan.test")

View File

@ -12,6 +12,7 @@ from homeassistant.components.fan import (
DIRECTION_REVERSE,
DOMAIN,
FanEntityFeature,
NotValidPresetModeError,
)
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
@ -489,7 +490,11 @@ async def test_preset_modes(hass: HomeAssistant, calls) -> None:
("smart", "smart", 3),
("invalid", "smart", 3),
]:
await common.async_set_preset_mode(hass, _TEST_FAN, extra)
if extra != state:
with pytest.raises(NotValidPresetModeError):
await common.async_set_preset_mode(hass, _TEST_FAN, extra)
else:
await common.async_set_preset_mode(hass, _TEST_FAN, extra)
assert hass.states.get(_PRESET_MODE_INPUT_SELECT).state == state
assert len(calls) == expected_calls
assert calls[-1].data["action"] == "set_preset_mode"
@ -550,6 +555,7 @@ async def test_no_value_template(hass: HomeAssistant, calls) -> None:
with assert_setup_component(1, "fan"):
test_fan_config = {
"preset_mode_template": "{{ states('input_select.preset_mode') }}",
"preset_modes": ["auto"],
"percentage_template": "{{ states('input_number.percentage') }}",
"oscillating_template": "{{ states('input_select.osc') }}",
"direction_template": "{{ states('input_select.direction') }}",
@ -625,18 +631,18 @@ async def test_no_value_template(hass: HomeAssistant, calls) -> None:
await hass.async_block_till_done()
await common.async_turn_on(hass, _TEST_FAN)
_verify(hass, STATE_ON, 0, None, None, None)
_verify(hass, STATE_ON, 0, None, None, "auto")
await common.async_turn_off(hass, _TEST_FAN)
_verify(hass, STATE_OFF, 0, None, None, None)
_verify(hass, STATE_OFF, 0, None, None, "auto")
percent = 100
await common.async_set_percentage(hass, _TEST_FAN, percent)
assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == percent
_verify(hass, STATE_ON, percent, None, None, None)
_verify(hass, STATE_ON, percent, None, None, "auto")
await common.async_turn_off(hass, _TEST_FAN)
_verify(hass, STATE_OFF, percent, None, None, None)
_verify(hass, STATE_OFF, percent, None, None, "auto")
preset = "auto"
await common.async_set_preset_mode(hass, _TEST_FAN, preset)

View File

@ -10,6 +10,7 @@ from homeassistant.components.fan import (
DOMAIN as FAN_DOMAIN,
SERVICE_SET_PERCENTAGE,
SERVICE_SET_PRESET_MODE,
NotValidPresetModeError,
)
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
from homeassistant.core import HomeAssistant
@ -179,7 +180,7 @@ async def test_set_invalid_preset_mode(
"""Test set preset mode."""
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
with pytest.raises(ValueError):
with pytest.raises(NotValidPresetModeError) as exc:
await hass.services.async_call(
FAN_DOMAIN,
SERVICE_SET_PRESET_MODE,
@ -189,6 +190,7 @@ async def test_set_invalid_preset_mode(
},
blocking=True,
)
assert exc.value.translation_key == "not_valid_preset_mode"
async def test_set_preset_mode_exception(

View File

@ -222,10 +222,11 @@ async def test_fan(
# set invalid preset_mode from HA
cluster.write_attributes.reset_mock()
with pytest.raises(NotValidPresetModeError):
with pytest.raises(NotValidPresetModeError) as exc:
await async_set_preset_mode(
hass, entity_id, preset_mode="invalid does not exist"
)
assert exc.value.translation_key == "not_valid_preset_mode"
assert len(cluster.write_attributes.mock_calls) == 0
# test adding new fan to the network and HA
@ -624,10 +625,11 @@ async def test_fan_ikea(
# set invalid preset_mode from HA
cluster.write_attributes.reset_mock()
with pytest.raises(NotValidPresetModeError):
with pytest.raises(NotValidPresetModeError) as exc:
await async_set_preset_mode(
hass, entity_id, preset_mode="invalid does not exist"
)
assert exc.value.translation_key == "not_valid_preset_mode"
assert len(cluster.write_attributes.mock_calls) == 0
# test adding new fan to the network and HA
@ -813,8 +815,9 @@ async def test_fan_kof(
# set invalid preset_mode from HA
cluster.write_attributes.reset_mock()
with pytest.raises(NotValidPresetModeError):
with pytest.raises(NotValidPresetModeError) as exc:
await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_AUTO)
assert exc.value.translation_key == "not_valid_preset_mode"
assert len(cluster.write_attributes.mock_calls) == 0
# test adding new fan to the network and HA

View File

@ -536,13 +536,14 @@ async def test_inovelli_lzw36(
assert args["value"] == 1
client.async_send_command.reset_mock()
with pytest.raises(NotValidPresetModeError):
with pytest.raises(NotValidPresetModeError) as exc:
await hass.services.async_call(
"fan",
"turn_on",
{"entity_id": entity_id, "preset_mode": "wheeze"},
blocking=True,
)
assert exc.value.translation_key == "not_valid_preset_mode"
assert len(client.async_send_command.call_args_list) == 0
@ -675,13 +676,14 @@ async def test_thermostat_fan(
client.async_send_command.reset_mock()
# Test setting unknown preset mode
with pytest.raises(ValueError):
with pytest.raises(NotValidPresetModeError) as exc:
await hass.services.async_call(
FAN_DOMAIN,
SERVICE_SET_PRESET_MODE,
{ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "Turbo"},
blocking=True,
)
assert exc.value.translation_key == "not_valid_preset_mode"
client.async_send_command.reset_mock()

View File

@ -0,0 +1,64 @@
"""Provide a mock fan platform.
Call init before using it in your tests to ensure clean test data.
"""
from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from tests.common import MockEntity
ENTITIES = {}
def init(empty=False):
"""Initialize the platform with entities."""
global ENTITIES
ENTITIES = (
{}
if empty
else {
"support_preset_mode": MockFan(
name="Support fan with preset_mode support",
supported_features=FanEntityFeature.PRESET_MODE,
unique_id="unique_support_preset_mode",
preset_modes=["auto", "eco"],
)
}
)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities_callback: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
):
"""Return mock entities."""
async_add_entities_callback(list(ENTITIES.values()))
class MockFan(MockEntity, FanEntity):
"""Mock Fan class."""
@property
def preset_mode(self) -> str | None:
"""Return preset mode."""
return self._handle("preset_mode")
@property
def preset_modes(self) -> list[str] | None:
"""Return preset mode."""
return self._handle("preset_modes")
@property
def supported_features(self):
"""Return the class of this fan."""
return self._handle("supported_features")
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set preset mode."""
self._attr_preset_mode = preset_mode
await self.async_update_ha_state()