Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 23 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
53 changes: 25 additions & 28 deletions homeassistant/components/fan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
SERVICE_TURN_ON,
STATE_ON,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
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
Expand Down Expand Up @@ -80,13 +80,7 @@ class FanEntityFeature(IntFlag):


class NotValidPresetModeError(ServiceValidationError):
"""Exception class when the preset_mode in not in the preset_modes list.

The use of this class is deprecated, and will be removed with
HA Core 2024.12
The fan entity component now has built-in validation and
raises a ServiceValidationError in case an invalid preset_mode is used.
"""
"""Exception class when the preset_mode in not in the preset_modes list."""
jbouwh marked this conversation as resolved.
Show resolved Hide resolved


@bind_hass
Expand Down Expand Up @@ -115,7 +109,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
),
vol.Optional(ATTR_PRESET_MODE): cv.string,
},
_async_turn_on,
"_async_turn_on",
)
component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off")
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle")
Expand Down Expand Up @@ -164,7 +158,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_set_preset_mode",
[FanEntityFeature.SET_SPEED, FanEntityFeature.PRESET_MODE],
)

Expand All @@ -183,21 +177,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return await component.async_unload_entry(entry)


async def _async_set_preset_mode(entity: FanEntity, service_call: ServiceCall) -> None:
"""Validate and set new preset mode."""
preset_mode: str = service_call.data["preset_mode"]
entity.valid_preset_mode_or_raise(preset_mode)
await entity.async_set_preset_mode(preset_mode)


async def _async_turn_on(entity: FanEntity, service_call: ServiceCall) -> None:
"""Validate and turn on the fan."""
preset_mode: str | None
if (preset_mode := service_call.data.get("preset_mode")) is not None:
entity.valid_preset_mode_or_raise(preset_mode)
await entity.async_turn_on(**service_call.data)


@dataclass
class FanEntityDescription(ToggleEntityDescription):
"""A class that describes fan entities."""
Expand Down Expand Up @@ -260,13 +239,18 @@ def set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
raise NotImplementedError()

async def _async_set_preset_mode(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)

@callback
def _valid_preset_mode_or_raise(self, preset_mode: str) -> None:
gjohansson-ST marked this conversation as resolved.
Show resolved Hide resolved
"""Raise ServiceValidationError on invalid preset_mode."""
"""Raise NotValidPresetModeError on invalid preset_mode."""
integration_platform: str = "unknown"
if (entry := self.registry_entry) is not None:
integration_platform = entry.platform
Expand All @@ -293,13 +277,14 @@ def _valid_preset_mode_or_raise(self, preset_mode: str) -> None:
)
self.valid_preset_mode_or_raise(preset_mode)

@final
@callback
def valid_preset_mode_or_raise(self, preset_mode: str) -> None:
jbouwh marked this conversation as resolved.
Show resolved Hide resolved
"""Raise ServiceValidationError on invalid preset_mode."""
"""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 ServiceValidationError(
raise NotValidPresetModeError(
f"The preset_mode {preset_mode} is not a valid preset_mode:"
f" {preset_modes}",
translation_domain=DOMAIN,
Expand Down Expand Up @@ -327,6 +312,18 @@ def turn_on(
"""Turn on the fan."""
raise NotImplementedError()

@final
async def _async_turn_on(
jbouwh marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand Down
7 changes: 4 additions & 3 deletions tests/components/bond/test_fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
SERVICE_SET_PERCENTAGE,
SERVICE_SET_PRESET_MODE,
FanEntityFeature,
NotValidPresetModeError,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
Expand All @@ -34,7 +35,7 @@
SERVICE_TURN_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util import utcnow

Expand Down Expand Up @@ -252,12 +253,12 @@ async def test_turn_on_fan_preset_mode_not_supported(hass: HomeAssistant) -> Non
)

with patch_bond_action(), patch_bond_device_state(), pytest.raises(
ServiceValidationError
NotValidPresetModeError
):
await turn_fan_on(hass, "fan.name_1", preset_mode=PRESET_MODE_BREEZE)

with patch_bond_action(), patch_bond_device_state(), pytest.raises(
ServiceValidationError
NotValidPresetModeError
):
await hass.services.async_call(
FAN_DOMAIN,
Expand Down
9 changes: 4 additions & 5 deletions tests/components/demo/test_fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.setup import async_setup_component

FULL_FAN_ENTITY_IDS = ["fan.living_room_fan", "fan.percentage_full_fan"]
Expand Down Expand Up @@ -183,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(ServiceValidationError) as exc:
with pytest.raises(fan.NotValidPresetModeError) as exc:
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
Expand Down Expand Up @@ -257,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(ServiceValidationError) as exc:
with pytest.raises(fan.NotValidPresetModeError) as exc:
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
Expand Down Expand Up @@ -356,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(ServiceValidationError) as exc:
with pytest.raises(fan.NotValidPresetModeError) as exc:
await hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_SET_PRESET_MODE,
Expand All @@ -367,7 +366,7 @@ async def test_set_preset_mode_invalid(hass: HomeAssistant, fan_entity_id) -> No
assert exc.value.translation_domain == fan.DOMAIN
assert exc.value.translation_key == "not_valid_preset_mode"

with pytest.raises(ServiceValidationError) as exc:
with pytest.raises(fan.NotValidPresetModeError) as exc:
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
Expand Down
8 changes: 4 additions & 4 deletions tests/components/fan/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
DOMAIN,
SERVICE_SET_PRESET_MODE,
FanEntity,
NotValidPresetModeError,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
import homeassistant.helpers.entity_registry as er
from homeassistant.setup import async_setup_component

Expand Down Expand Up @@ -130,7 +130,7 @@ async def test_preset_mode_validation(
state = hass.states.get("fan.support_fan_with_preset_mode_support")
assert state.attributes.get(ATTR_PRESET_MODE) == "eco"

with pytest.raises(ServiceValidationError) as exc:
with pytest.raises(NotValidPresetModeError) as exc:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_PRESET_MODE,
Expand All @@ -142,11 +142,11 @@ async def test_preset_mode_validation(
)
assert exc.value.translation_key == "not_valid_preset_mode"

with pytest.raises(ServiceValidationError) as exc:
with pytest.raises(NotValidPresetModeError) as exc:
await test_fan.valid_preset_mode_or_raise("invalid")
assert exc.value.translation_key == "not_valid_preset_mode"

with pytest.raises(ServiceValidationError) as exc:
with pytest.raises(NotValidPresetModeError) as exc:
await test_fan._valid_preset_mode_or_raise("invalid")
assert exc.value.translation_key == "not_valid_preset_mode"

Expand Down
26 changes: 13 additions & 13 deletions tests/components/mqtt/test_fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
ATTR_PERCENTAGE,
ATTR_PRESET_MODE,
ATTR_PRESET_MODES,
NotValidPresetModeError,
)
from homeassistant.components.mqtt.fan import (
CONF_DIRECTION_COMMAND_TOPIC,
Expand All @@ -34,7 +35,6 @@
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError

from .test_common import (
help_custom_config,
Expand Down Expand Up @@ -705,7 +705,7 @@ 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(ServiceValidationError) as exc:
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"

Expand Down Expand Up @@ -917,11 +917,11 @@ 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(ServiceValidationError) as exc:
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(ServiceValidationError) as exc:
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"

Expand Down Expand Up @@ -979,7 +979,7 @@ 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(ServiceValidationError) as exc:
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"

Expand Down Expand Up @@ -1082,11 +1082,11 @@ 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(ServiceValidationError) as exc:
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(ServiceValidationError) as exc:
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"

Expand Down Expand Up @@ -1146,7 +1146,7 @@ async def test_sending_mqtt_command_templates_(
assert state.state == STATE_ON
assert state.attributes.get(ATTR_ASSUMED_STATE)

with pytest.raises(ServiceValidationError) as exc:
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"

Expand Down Expand Up @@ -1183,7 +1183,7 @@ 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(ServiceValidationError) as exc:
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"

Expand Down Expand Up @@ -1284,7 +1284,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(
assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)

with pytest.raises(ServiceValidationError) as exc:
with pytest.raises(NotValidPresetModeError) as exc:
await common.async_turn_on(hass, "fan.test", preset_mode="auto")
assert exc.value.translation_key == "not_valid_preset_mode"
assert mqtt_mock.async_publish.call_count == 0
Expand Down Expand Up @@ -1435,11 +1435,11 @@ 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(ServiceValidationError) as exc:
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(ServiceValidationError) as exc:
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"

Expand All @@ -1461,7 +1461,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(
assert state.state == STATE_OFF
assert state.attributes.get(ATTR_ASSUMED_STATE)

with pytest.raises(ServiceValidationError) as exc:
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"

Expand Down
4 changes: 2 additions & 2 deletions tests/components/template/test_fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
DIRECTION_REVERSE,
DOMAIN,
FanEntityFeature,
NotValidPresetModeError,
)
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError

from tests.common import assert_setup_component
from tests.components.fan import common
Expand Down Expand Up @@ -491,7 +491,7 @@ async def test_preset_modes(hass: HomeAssistant, calls) -> None:
("invalid", "smart", 3),
]:
if extra != state:
with pytest.raises(ServiceValidationError):
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)
Expand Down
5 changes: 3 additions & 2 deletions tests/components/vallox/test_fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
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
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.exceptions import HomeAssistantError

from .conftest import patch_metrics, patch_metrics_set, patch_profile, patch_profile_set

Expand Down Expand Up @@ -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(ServiceValidationError) as exc:
with pytest.raises(NotValidPresetModeError) as exc:
await hass.services.async_call(
FAN_DOMAIN,
SERVICE_SET_PRESET_MODE,
Expand Down