From 83f4d3af5c6f95b33fa98313fb493844bfd4b101 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 27 Dec 2023 14:51:39 +0100 Subject: [PATCH] Implement mode validation in Climate entity component (#105745) * Implement mode validation in Climate entity component * Fix some tests * more tests * Fix translations * fix deconz tests * Fix switcher_kis tests * not None * Fix homematicip_cloud test * Always validate * Fix shelly * reverse logic in validation * modes_str --------- Co-authored-by: J. Nick Koston --- homeassistant/components/climate/__init__.py | 58 +++++- homeassistant/components/climate/strings.json | 11 ++ homeassistant/components/demo/climate.py | 2 +- tests/components/balboa/test_climate.py | 3 +- tests/components/climate/conftest.py | 22 +++ tests/components/climate/test_init.py | 177 +++++++++++++++++- tests/components/deconz/test_climate.py | 5 +- tests/components/demo/test_climate.py | 8 +- .../generic_thermostat/test_climate.py | 3 +- tests/components/gree/test_climate.py | 7 +- .../homematicip_cloud/test_climate.py | 15 +- .../maxcube/test_maxcube_climate.py | 3 +- tests/components/mqtt/test_climate.py | 11 +- tests/components/nest/test_climate.py | 6 +- tests/components/netatmo/test_climate.py | 18 +- tests/components/sensibo/test_climate.py | 10 +- tests/components/shelly/test_climate.py | 15 +- tests/components/switcher_kis/test_climate.py | 7 +- tests/components/whirlpool/test_climate.py | 3 +- tests/components/zha/test_climate.py | 30 +-- tests/components/zwave_js/test_climate.py | 5 +- 21 files changed, 342 insertions(+), 77 deletions(-) create mode 100644 tests/components/climate/conftest.py diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index f7d168bfa4a..4815b7a1cbb 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta import functools as ft import logging -from typing import TYPE_CHECKING, Any, final +from typing import TYPE_CHECKING, Any, Literal, final import voluptuous as vol @@ -19,7 +19,8 @@ from homeassistant.const import ( STATE_ON, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import ServiceValidationError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -166,7 +167,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", [ClimateEntityFeature.PRESET_MODE], ) component.async_register_entity_service( @@ -193,13 +194,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_SET_FAN_MODE, {vol.Required(ATTR_FAN_MODE): cv.string}, - "async_set_fan_mode", + "async_handle_set_fan_mode_service", [ClimateEntityFeature.FAN_MODE], ) component.async_register_entity_service( SERVICE_SET_SWING_MODE, {vol.Required(ATTR_SWING_MODE): cv.string}, - "async_set_swing_mode", + "async_handle_set_swing_mode_service", [ClimateEntityFeature.SWING_MODE], ) @@ -515,6 +516,35 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """ return self._attr_swing_modes + @final + @callback + def _valid_mode_or_raise( + self, + mode_type: Literal["preset", "swing", "fan"], + mode: str, + modes: list[str] | None, + ) -> None: + """Raise ServiceValidationError on invalid modes.""" + if modes and mode in modes: + return + modes_str: str = ", ".join(modes) if modes else "" + if mode_type == "preset": + translation_key = "not_valid_preset_mode" + elif mode_type == "swing": + translation_key = "not_valid_swing_mode" + elif mode_type == "fan": + translation_key = "not_valid_fan_mode" + raise ServiceValidationError( + f"The {mode_type}_mode {mode} is not a valid {mode_type}_mode:" + f" {modes_str}", + translation_domain=DOMAIN, + translation_key=translation_key, + translation_placeholders={ + "mode": mode, + "modes": modes_str, + }, + ) + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" raise NotImplementedError() @@ -533,6 +563,12 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Set new target humidity.""" await self.hass.async_add_executor_job(self.set_humidity, humidity) + @final + async def async_handle_set_fan_mode_service(self, fan_mode: str) -> None: + """Validate and set new preset mode.""" + self._valid_mode_or_raise("fan", fan_mode, self.fan_modes) + await self.async_set_fan_mode(fan_mode) + def set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" raise NotImplementedError() @@ -549,6 +585,12 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Set new target hvac mode.""" await self.hass.async_add_executor_job(self.set_hvac_mode, hvac_mode) + @final + async def async_handle_set_swing_mode_service(self, swing_mode: str) -> None: + """Validate and set new preset mode.""" + self._valid_mode_or_raise("swing", swing_mode, self.swing_modes) + await self.async_set_swing_mode(swing_mode) + def set_swing_mode(self, swing_mode: str) -> None: """Set new target swing operation.""" raise NotImplementedError() @@ -557,6 +599,12 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Set new target swing operation.""" await self.hass.async_add_executor_job(self.set_swing_mode, swing_mode) + @final + async def async_handle_set_preset_mode_service(self, preset_mode: str) -> None: + """Validate and set new preset mode.""" + self._valid_mode_or_raise("preset", preset_mode, self.preset_modes) + await self.async_set_preset_mode(preset_mode) + def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" raise NotImplementedError() diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index 55ccef2bc76..ef87f287430 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -233,5 +233,16 @@ "heat": "Heat" } } + }, + "exceptions": { + "not_valid_preset_mode": { + "message": "Preset mode {mode} is not valid. Valid preset modes are: {modes}." + }, + "not_valid_swing_mode": { + "message": "Swing mode {mode} is not valid. Valid swing modes are: {modes}." + }, + "not_valid_fan_mode": { + "message": "Fan mode {mode} is not valid. Valid fan modes are: {modes}." + } } } diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index 1e585b12acd..0eaa7d5f41f 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -73,7 +73,7 @@ async def async_setup_entry( target_temperature=None, unit_of_measurement=UnitOfTemperature.CELSIUS, preset="home", - preset_modes=["home", "eco"], + preset_modes=["home", "eco", "away"], current_temperature=23, fan_mode="Auto Low", target_humidity=None, diff --git a/tests/components/balboa/test_climate.py b/tests/components/balboa/test_climate.py index 90ef6c75e5f..6ba0661ae55 100644 --- a/tests/components/balboa/test_climate.py +++ b/tests/components/balboa/test_climate.py @@ -26,6 +26,7 @@ from homeassistant.components.climate import ( ) from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import ServiceValidationError from . import init_integration @@ -146,7 +147,7 @@ async def test_spa_preset_modes( assert state assert state.attributes[ATTR_PRESET_MODE] == mode - with pytest.raises(KeyError): + with pytest.raises(ServiceValidationError): await common.async_set_preset_mode(hass, 2, ENTITY_CLIMATE) # put it in RNR and test assertion diff --git a/tests/components/climate/conftest.py b/tests/components/climate/conftest.py new file mode 100644 index 00000000000..2db96a20a0b --- /dev/null +++ b/tests/components/climate/conftest.py @@ -0,0 +1,22 @@ +"""Fixtures for Climate platform tests.""" +from collections.abc import Generator + +import pytest + +from homeassistant.config_entries import ConfigFlow +from homeassistant.core import HomeAssistant + +from tests.common import mock_config_flow, mock_platform + + +class MockFlow(ConfigFlow): + """Test flow.""" + + +@pytest.fixture +def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: + """Mock config flow.""" + mock_platform(hass, "test.config_flow") + + with mock_config_flow("test", MockFlow): + yield diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index 1181a432ea2..f46e0902c66 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -10,16 +10,36 @@ import voluptuous as vol from homeassistant.components import climate from homeassistant.components.climate import ( + DOMAIN, SET_TEMPERATURE_SCHEMA, ClimateEntity, HVACMode, ) +from homeassistant.components.climate.const import ( + ATTR_FAN_MODE, + ATTR_PRESET_MODE, + ATTR_SWING_MODE, + SERVICE_SET_FAN_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_SWING_MODE, + ClimateEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers.entity_platform import AddEntitiesCallback from tests.common import ( + MockConfigEntry, + MockEntity, + MockModule, + MockPlatform, async_mock_service, import_and_test_deprecated_constant, import_and_test_deprecated_constant_enum, + mock_integration, + mock_platform, ) @@ -57,9 +77,22 @@ async def test_set_temp_schema( assert calls[-1].data == data -class MockClimateEntity(ClimateEntity): +class MockClimateEntity(MockEntity, ClimateEntity): """Mock Climate device to use in tests.""" + _attr_supported_features = ( + ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.SWING_MODE + ) + _attr_preset_mode = "home" + _attr_preset_modes = ["home", "away"] + _attr_fan_mode = "auto" + _attr_fan_modes = ["auto", "off"] + _attr_swing_mode = "auto" + _attr_swing_modes = ["auto", "off"] + _attr_temperature_unit = UnitOfTemperature.CELSIUS + @property def hvac_mode(self) -> HVACMode: """Return hvac operation ie. heat, cool mode. @@ -82,6 +115,18 @@ class MockClimateEntity(ClimateEntity): def turn_off(self) -> None: """Turn off.""" + def set_preset_mode(self, preset_mode: str) -> None: + """Set preset mode.""" + self._attr_preset_mode = preset_mode + + def set_fan_mode(self, fan_mode: str) -> None: + """Set fan mode.""" + self._attr_fan_mode = fan_mode + + def set_swing_mode(self, swing_mode: str) -> None: + """Set swing mode.""" + self._attr_swing_mode = swing_mode + async def test_sync_turn_on(hass: HomeAssistant) -> None: """Test if async turn_on calls sync turn_on.""" @@ -158,3 +203,133 @@ def test_deprecated_current_constants( enum, "2025.1", ) + + +async def test_preset_mode_validation( + hass: HomeAssistant, config_flow_fixture: None +) -> None: + """Test mode validation for fan, swing and preset.""" + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) + return True + + async def async_setup_entry_climate_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test climate platform via config entry.""" + async_add_entities([MockClimateEntity(name="test", entity_id="climate.test")]) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=async_setup_entry_init, + ), + built_in=False, + ) + mock_platform( + hass, + "test.climate", + MockPlatform(async_setup_entry=async_setup_entry_climate_platform), + ) + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("climate.test") + assert state.attributes.get(ATTR_PRESET_MODE) == "home" + assert state.attributes.get(ATTR_FAN_MODE) == "auto" + assert state.attributes.get(ATTR_SWING_MODE) == "auto" + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + { + "entity_id": "climate.test", + "preset_mode": "away", + }, + blocking=True, + ) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_SWING_MODE, + { + "entity_id": "climate.test", + "swing_mode": "off", + }, + blocking=True, + ) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + { + "entity_id": "climate.test", + "fan_mode": "off", + }, + blocking=True, + ) + state = hass.states.get("climate.test") + assert state.attributes.get(ATTR_PRESET_MODE) == "away" + assert state.attributes.get(ATTR_FAN_MODE) == "off" + assert state.attributes.get(ATTR_SWING_MODE) == "off" + + with pytest.raises( + ServiceValidationError, + match="The preset_mode invalid is not a valid preset_mode: home, away", + ) as exc: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + { + "entity_id": "climate.test", + "preset_mode": "invalid", + }, + blocking=True, + ) + assert ( + str(exc.value) + == "The preset_mode invalid is not a valid preset_mode: home, away" + ) + assert exc.value.translation_key == "not_valid_preset_mode" + + with pytest.raises( + ServiceValidationError, + match="The swing_mode invalid is not a valid swing_mode: auto, off", + ) as exc: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_SWING_MODE, + { + "entity_id": "climate.test", + "swing_mode": "invalid", + }, + blocking=True, + ) + assert ( + str(exc.value) == "The swing_mode invalid is not a valid swing_mode: auto, off" + ) + assert exc.value.translation_key == "not_valid_swing_mode" + + with pytest.raises( + ServiceValidationError, + match="The fan_mode invalid is not a valid fan_mode: auto, off", + ) as exc: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + { + "entity_id": "climate.test", + "fan_mode": "invalid", + }, + blocking=True, + ) + assert str(exc.value) == "The fan_mode invalid is not a valid fan_mode: auto, off" + assert exc.value.translation_key == "not_valid_fan_mode" diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 5a3952e16db..dd0de559ba8 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -41,6 +41,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from .test_gateway import ( DECONZ_WEB_REQUEST, @@ -602,7 +603,7 @@ async def test_climate_device_with_fan_support( # Service set fan mode to unsupported value - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, @@ -725,7 +726,7 @@ async def test_climate_device_with_preset( # Service set preset to unsupported value - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, diff --git a/tests/components/demo/test_climate.py b/tests/components/demo/test_climate.py index 69e385ce242..97b436ea2b0 100644 --- a/tests/components/demo/test_climate.py +++ b/tests/components/demo/test_climate.py @@ -278,12 +278,12 @@ async def test_set_fan_mode(hass: HomeAssistant) -> None: await hass.services.async_call( DOMAIN, SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_FAN_MODE: "On Low"}, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_FAN_MODE: "on_low"}, blocking=True, ) state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get(ATTR_FAN_MODE) == "On Low" + assert state.attributes.get(ATTR_FAN_MODE) == "on_low" async def test_set_swing_mode_bad_attr(hass: HomeAssistant) -> None: @@ -311,12 +311,12 @@ async def test_set_swing(hass: HomeAssistant) -> None: await hass.services.async_call( DOMAIN, SERVICE_SET_SWING_MODE, - {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_SWING_MODE: "Auto"}, + {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_SWING_MODE: "auto"}, blocking=True, ) state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get(ATTR_SWING_MODE) == "Auto" + assert state.attributes.get(ATTR_SWING_MODE) == "auto" async def test_set_hvac_bad_attr_and_state(hass: HomeAssistant) -> None: diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 47a3cdc30af..9196de8b096 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -41,6 +41,7 @@ from homeassistant.core import ( State, callback, ) +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -388,7 +389,7 @@ async def test_set_preset_mode_invalid(hass: HomeAssistant, setup_comp_2) -> Non await common.async_set_preset_mode(hass, "none") state = hass.states.get(ENTITY) assert state.attributes.get("preset_mode") == "none" - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await common.async_set_preset_mode(hass, "Sleep") state = hass.states.get(ENTITY) assert state.attributes.get("preset_mode") == "none" diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index f5af1f403c3..5b261fa266b 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -50,6 +50,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util @@ -538,7 +539,7 @@ async def test_send_invalid_preset_mode( """Test for sending preset mode command to the device.""" await async_setup_gree(hass) - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( DOMAIN, SERVICE_SET_PRESET_MODE, @@ -699,7 +700,7 @@ async def test_send_invalid_fan_mode( """Test for sending fan mode command to the device.""" await async_setup_gree(hass) - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( DOMAIN, SERVICE_SET_FAN_MODE, @@ -780,7 +781,7 @@ async def test_send_invalid_swing_mode( """Test for sending swing mode command to the device.""" await async_setup_gree(hass) - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( DOMAIN, SERVICE_SET_SWING_MODE, diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index b042e3daa6c..20193d91239 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -3,6 +3,7 @@ import datetime from homematicip.base.enums import AbsenceType from homematicip.functionalHomes import IndoorClimateHome +import pytest from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, @@ -23,6 +24,7 @@ from homeassistant.components.homematicip_cloud.climate import ( PERMANENT_END_TIME, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.setup import async_setup_component from .helper import HAPID, async_manipulate_test_data, get_and_check_entity_basics @@ -340,12 +342,13 @@ async def test_hmip_heating_group_cool( assert ha_state.attributes[ATTR_PRESET_MODE] == "none" assert ha_state.attributes[ATTR_PRESET_MODES] == [] - await hass.services.async_call( - "climate", - "set_preset_mode", - {"entity_id": entity_id, "preset_mode": "Cool2"}, - blocking=True, - ) + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": "Cool2"}, + blocking=True, + ) assert len(hmip_device.mock_calls) == service_call_counter + 12 # fire_update_event shows that set_active_profile has not been called. diff --git a/tests/components/maxcube/test_maxcube_climate.py b/tests/components/maxcube/test_maxcube_climate.py index f279f049ac3..3f2b325330e 100644 --- a/tests/components/maxcube/test_maxcube_climate.py +++ b/tests/components/maxcube/test_maxcube_climate.py @@ -50,6 +50,7 @@ from homeassistant.const import ( ATTR_TEMPERATURE, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er from homeassistant.util import utcnow @@ -370,7 +371,7 @@ async def test_thermostat_set_invalid_preset( hass: HomeAssistant, cube: MaxCube, thermostat: MaxThermostat ) -> None: """Set hvac mode to heat.""" - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 6d6c7475366..9bb5c8b2585 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -33,6 +33,7 @@ from homeassistant.components.mqtt.climate import ( ) from homeassistant.const import ATTR_TEMPERATURE, Platform, UnitOfTemperature from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from .test_common import ( help_custom_config, @@ -1130,8 +1131,9 @@ async def test_set_preset_mode_optimistic( state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "comfort" - await common.async_set_preset_mode(hass, "invalid", ENTITY_CLIMATE) - assert "'invalid' is not a valid preset mode" in caplog.text + with pytest.raises(ServiceValidationError): + await common.async_set_preset_mode(hass, "invalid", ENTITY_CLIMATE) + assert "'invalid' is not a valid preset mode" in caplog.text @pytest.mark.parametrize( @@ -1187,8 +1189,9 @@ async def test_set_preset_mode_explicit_optimistic( state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "comfort" - await common.async_set_preset_mode(hass, "invalid", ENTITY_CLIMATE) - assert "'invalid' is not a valid preset mode" in caplog.text + with pytest.raises(ServiceValidationError): + await common.async_set_preset_mode(hass, "invalid", ENTITY_CLIMATE) + assert "'invalid' is not a valid preset mode" in caplog.text @pytest.mark.parametrize( diff --git a/tests/components/nest/test_climate.py b/tests/components/nest/test_climate.py index c920eb5717d..e1c3cc187db 100644 --- a/tests/components/nest/test_climate.py +++ b/tests/components/nest/test_climate.py @@ -39,7 +39,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from .common import ( DEVICE_COMMAND, @@ -1192,7 +1192,7 @@ async def test_thermostat_invalid_fan_mode( assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await common.async_set_fan_mode(hass, FAN_LOW) await hass.async_block_till_done() @@ -1474,7 +1474,7 @@ async def test_thermostat_invalid_set_preset_mode( assert thermostat.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_NONE] # Set preset mode that is invalid - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await common.async_set_preset_mode(hass, PRESET_SLEEP) await hass.async_block_till_done() diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index 848aad331bd..11e2077f859 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -33,6 +33,7 @@ from homeassistant.components.netatmo.const import ( ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.util import dt as dt_util from .common import selected_platforms, simulate_webhook @@ -879,15 +880,14 @@ async def test_service_preset_mode_invalid( await hass.async_block_till_done() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: "climate.cocina", ATTR_PRESET_MODE: "invalid"}, - blocking=True, - ) - await hass.async_block_till_done() - - assert "Preset mode 'invalid' not available" in caplog.text + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: "climate.cocina", ATTR_PRESET_MODE: "invalid"}, + blocking=True, + ) + await hass.async_block_till_done() async def test_valves_service_turn_off( diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index 71680733098..bf0113cb22b 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -1330,10 +1330,7 @@ async def test_climate_fan_mode_and_swing_mode_not_supported( with patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - ), pytest.raises( - HomeAssistantError, - match="Climate swing mode faulty_swing_mode is not supported by the integration, please open an issue", - ): + ), pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_SWING_MODE, @@ -1343,10 +1340,7 @@ async def test_climate_fan_mode_and_swing_mode_not_supported( with patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - ), pytest.raises( - HomeAssistantError, - match="Climate fan mode faulty_fan_mode is not supported by the integration, please open an issue", - ): + ), pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index f52b542b389..980981de754 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -25,7 +25,7 @@ from homeassistant.components.shelly.const import DOMAIN, MODEL_WALL_DISPLAY from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant, State -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er import homeassistant.helpers.issue_registry as ir from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM @@ -382,12 +382,13 @@ async def test_block_restored_climate_set_preset_before_online( assert hass.states.get(entity_id).state == HVACMode.HEAT - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: "Profile1"}, - blocking=True, - ) + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: "Profile1"}, + blocking=True, + ) mock_block_device.http_request.assert_not_called() diff --git a/tests/components/switcher_kis/test_climate.py b/tests/components/switcher_kis/test_climate.py index f998dbe294b..1919261109e 100644 --- a/tests/components/switcher_kis/test_climate.py +++ b/tests/components/switcher_kis/test_climate.py @@ -25,7 +25,7 @@ from homeassistant.components.climate import ( ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.util import slugify from . import init_integration @@ -336,9 +336,8 @@ async def test_climate_control_errors( {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 24}, blocking=True, ) - # Test exception when trying set fan level - with pytest.raises(HomeAssistantError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, @@ -347,7 +346,7 @@ async def test_climate_control_errors( ) # Test exception when trying set swing mode - with pytest.raises(HomeAssistantError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_SWING_MODE, diff --git a/tests/components/whirlpool/test_climate.py b/tests/components/whirlpool/test_climate.py index fe2f9f17504..8607a49b42c 100644 --- a/tests/components/whirlpool/test_climate.py +++ b/tests/components/whirlpool/test_climate.py @@ -43,6 +43,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er from . import init_integration @@ -337,7 +338,7 @@ async def test_service_calls( mock_instance.set_fanspeed.reset_mock() # FAN_MIDDLE is not supported - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index 145aba799ca..b693c034199 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -52,7 +52,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from .common import async_enable_traffic, find_entity_id, send_attributes_report from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @@ -860,12 +860,13 @@ async def test_preset_setting_invalid( state = hass.states.get(entity_id) assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "invalid_preset"}, - blocking=True, - ) + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "invalid_preset"}, + blocking=True, + ) state = hass.states.get(entity_id) assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE @@ -1251,13 +1252,14 @@ async def test_set_fan_mode_not_supported( entity_id = find_entity_id(Platform.CLIMATE, device_climate_fan, hass) fan_cluster = device_climate_fan.device.endpoints[1].fan - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_LOW}, - blocking=True, - ) - assert fan_cluster.write_attributes.await_count == 0 + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_LOW}, + blocking=True, + ) + assert fan_cluster.write_attributes.await_count == 0 async def test_set_fan_mode(hass: HomeAssistant, device_climate_fan) -> None: diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index d5619ff014c..e4550b7f961 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -40,6 +40,7 @@ from homeassistant.const import ( ATTR_TEMPERATURE, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import issue_registry as ir from .common import ( @@ -278,7 +279,7 @@ async def test_thermostat_v2( client.async_send_command.reset_mock() # Test setting invalid fan mode - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, @@ -692,7 +693,7 @@ async def test_preset_and_no_setpoint( assert state.attributes[ATTR_TEMPERATURE] is None assert state.attributes[ATTR_PRESET_MODE] == "Full power" - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): # Test setting invalid preset mode await hass.services.async_call( CLIMATE_DOMAIN,