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 <nick@koston.org>
This commit is contained in:
G Johansson 2023-12-27 14:51:39 +01:00 committed by GitHub
parent e04fda3fad
commit 83f4d3af5c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 342 additions and 77 deletions

View file

@ -4,7 +4,7 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import functools as ft import functools as ft
import logging import logging
from typing import TYPE_CHECKING, Any, final from typing import TYPE_CHECKING, Any, Literal, final
import voluptuous as vol import voluptuous as vol
@ -19,7 +19,8 @@ from homeassistant.const import (
STATE_ON, STATE_ON,
UnitOfTemperature, 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 import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
@ -166,7 +167,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_SET_PRESET_MODE, SERVICE_SET_PRESET_MODE,
{vol.Required(ATTR_PRESET_MODE): cv.string}, {vol.Required(ATTR_PRESET_MODE): cv.string},
"async_set_preset_mode", "async_handle_set_preset_mode_service",
[ClimateEntityFeature.PRESET_MODE], [ClimateEntityFeature.PRESET_MODE],
) )
component.async_register_entity_service( component.async_register_entity_service(
@ -193,13 +194,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_SET_FAN_MODE, SERVICE_SET_FAN_MODE,
{vol.Required(ATTR_FAN_MODE): cv.string}, {vol.Required(ATTR_FAN_MODE): cv.string},
"async_set_fan_mode", "async_handle_set_fan_mode_service",
[ClimateEntityFeature.FAN_MODE], [ClimateEntityFeature.FAN_MODE],
) )
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_SET_SWING_MODE, SERVICE_SET_SWING_MODE,
{vol.Required(ATTR_SWING_MODE): cv.string}, {vol.Required(ATTR_SWING_MODE): cv.string},
"async_set_swing_mode", "async_handle_set_swing_mode_service",
[ClimateEntityFeature.SWING_MODE], [ClimateEntityFeature.SWING_MODE],
) )
@ -515,6 +516,35 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
""" """
return self._attr_swing_modes 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: def set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature.""" """Set new target temperature."""
raise NotImplementedError() raise NotImplementedError()
@ -533,6 +563,12 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Set new target humidity.""" """Set new target humidity."""
await self.hass.async_add_executor_job(self.set_humidity, 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: def set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode.""" """Set new target fan mode."""
raise NotImplementedError() raise NotImplementedError()
@ -549,6 +585,12 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Set new target hvac mode.""" """Set new target hvac mode."""
await self.hass.async_add_executor_job(self.set_hvac_mode, 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: def set_swing_mode(self, swing_mode: str) -> None:
"""Set new target swing operation.""" """Set new target swing operation."""
raise NotImplementedError() raise NotImplementedError()
@ -557,6 +599,12 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Set new target swing operation.""" """Set new target swing operation."""
await self.hass.async_add_executor_job(self.set_swing_mode, swing_mode) 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: def set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode.""" """Set new preset mode."""
raise NotImplementedError() raise NotImplementedError()

View file

@ -233,5 +233,16 @@
"heat": "Heat" "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}."
}
} }
} }

View file

@ -73,7 +73,7 @@ async def async_setup_entry(
target_temperature=None, target_temperature=None,
unit_of_measurement=UnitOfTemperature.CELSIUS, unit_of_measurement=UnitOfTemperature.CELSIUS,
preset="home", preset="home",
preset_modes=["home", "eco"], preset_modes=["home", "eco", "away"],
current_temperature=23, current_temperature=23,
fan_mode="Auto Low", fan_mode="Auto Low",
target_humidity=None, target_humidity=None,

View file

@ -26,6 +26,7 @@ from homeassistant.components.climate import (
) )
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, State from homeassistant.core import HomeAssistant, State
from homeassistant.exceptions import ServiceValidationError
from . import init_integration from . import init_integration
@ -146,7 +147,7 @@ async def test_spa_preset_modes(
assert state assert state
assert state.attributes[ATTR_PRESET_MODE] == mode 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) await common.async_set_preset_mode(hass, 2, ENTITY_CLIMATE)
# put it in RNR and test assertion # put it in RNR and test assertion

View file

@ -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

View file

@ -10,16 +10,36 @@ import voluptuous as vol
from homeassistant.components import climate from homeassistant.components import climate
from homeassistant.components.climate import ( from homeassistant.components.climate import (
DOMAIN,
SET_TEMPERATURE_SCHEMA, SET_TEMPERATURE_SCHEMA,
ClimateEntity, ClimateEntity,
HVACMode, 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.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from tests.common import ( from tests.common import (
MockConfigEntry,
MockEntity,
MockModule,
MockPlatform,
async_mock_service, async_mock_service,
import_and_test_deprecated_constant, import_and_test_deprecated_constant,
import_and_test_deprecated_constant_enum, 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 assert calls[-1].data == data
class MockClimateEntity(ClimateEntity): class MockClimateEntity(MockEntity, ClimateEntity):
"""Mock Climate device to use in tests.""" """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 @property
def hvac_mode(self) -> HVACMode: def hvac_mode(self) -> HVACMode:
"""Return hvac operation ie. heat, cool mode. """Return hvac operation ie. heat, cool mode.
@ -82,6 +115,18 @@ class MockClimateEntity(ClimateEntity):
def turn_off(self) -> None: def turn_off(self) -> None:
"""Turn off.""" """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: async def test_sync_turn_on(hass: HomeAssistant) -> None:
"""Test if async turn_on calls sync turn_on.""" """Test if async turn_on calls sync turn_on."""
@ -158,3 +203,133 @@ def test_deprecated_current_constants(
enum, enum,
"2025.1", "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"

View file

@ -41,6 +41,7 @@ from homeassistant.const import (
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from .test_gateway import ( from .test_gateway import (
DECONZ_WEB_REQUEST, DECONZ_WEB_REQUEST,
@ -602,7 +603,7 @@ async def test_climate_device_with_fan_support(
# Service set fan mode to unsupported value # Service set fan mode to unsupported value
with pytest.raises(ValueError): with pytest.raises(ServiceValidationError):
await hass.services.async_call( await hass.services.async_call(
CLIMATE_DOMAIN, CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE, SERVICE_SET_FAN_MODE,
@ -725,7 +726,7 @@ async def test_climate_device_with_preset(
# Service set preset to unsupported value # Service set preset to unsupported value
with pytest.raises(ValueError): with pytest.raises(ServiceValidationError):
await hass.services.async_call( await hass.services.async_call(
CLIMATE_DOMAIN, CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE, SERVICE_SET_PRESET_MODE,

View file

@ -278,12 +278,12 @@ async def test_set_fan_mode(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_SET_FAN_MODE, 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, blocking=True,
) )
state = hass.states.get(ENTITY_CLIMATE) 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: 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( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_SET_SWING_MODE, SERVICE_SET_SWING_MODE,
{ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_SWING_MODE: "Auto"}, {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_SWING_MODE: "auto"},
blocking=True, blocking=True,
) )
state = hass.states.get(ENTITY_CLIMATE) 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: async def test_set_hvac_bad_attr_and_state(hass: HomeAssistant) -> None:

View file

@ -41,6 +41,7 @@ from homeassistant.core import (
State, State,
callback, callback,
) )
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util 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") await common.async_set_preset_mode(hass, "none")
state = hass.states.get(ENTITY) state = hass.states.get(ENTITY)
assert state.attributes.get("preset_mode") == "none" assert state.attributes.get("preset_mode") == "none"
with pytest.raises(ValueError): with pytest.raises(ServiceValidationError):
await common.async_set_preset_mode(hass, "Sleep") await common.async_set_preset_mode(hass, "Sleep")
state = hass.states.get(ENTITY) state = hass.states.get(ENTITY)
assert state.attributes.get("preset_mode") == "none" assert state.attributes.get("preset_mode") == "none"

View file

@ -50,6 +50,7 @@ from homeassistant.const import (
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
import homeassistant.util.dt as dt_util 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.""" """Test for sending preset mode command to the device."""
await async_setup_gree(hass) await async_setup_gree(hass)
with pytest.raises(ValueError): with pytest.raises(ServiceValidationError):
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_SET_PRESET_MODE, SERVICE_SET_PRESET_MODE,
@ -699,7 +700,7 @@ async def test_send_invalid_fan_mode(
"""Test for sending fan mode command to the device.""" """Test for sending fan mode command to the device."""
await async_setup_gree(hass) await async_setup_gree(hass)
with pytest.raises(ValueError): with pytest.raises(ServiceValidationError):
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_SET_FAN_MODE, SERVICE_SET_FAN_MODE,
@ -780,7 +781,7 @@ async def test_send_invalid_swing_mode(
"""Test for sending swing mode command to the device.""" """Test for sending swing mode command to the device."""
await async_setup_gree(hass) await async_setup_gree(hass)
with pytest.raises(ValueError): with pytest.raises(ServiceValidationError):
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_SET_SWING_MODE, SERVICE_SET_SWING_MODE,

View file

@ -3,6 +3,7 @@ import datetime
from homematicip.base.enums import AbsenceType from homematicip.base.enums import AbsenceType
from homematicip.functionalHomes import IndoorClimateHome from homematicip.functionalHomes import IndoorClimateHome
import pytest
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE, ATTR_CURRENT_TEMPERATURE,
@ -23,6 +24,7 @@ from homeassistant.components.homematicip_cloud.climate import (
PERMANENT_END_TIME, PERMANENT_END_TIME,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from .helper import HAPID, async_manipulate_test_data, get_and_check_entity_basics 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_MODE] == "none"
assert ha_state.attributes[ATTR_PRESET_MODES] == [] assert ha_state.attributes[ATTR_PRESET_MODES] == []
await hass.services.async_call( with pytest.raises(ServiceValidationError):
"climate", await hass.services.async_call(
"set_preset_mode", "climate",
{"entity_id": entity_id, "preset_mode": "Cool2"}, "set_preset_mode",
blocking=True, {"entity_id": entity_id, "preset_mode": "Cool2"},
) blocking=True,
)
assert len(hmip_device.mock_calls) == service_call_counter + 12 assert len(hmip_device.mock_calls) == service_call_counter + 12
# fire_update_event shows that set_active_profile has not been called. # fire_update_event shows that set_active_profile has not been called.

View file

@ -50,6 +50,7 @@ from homeassistant.const import (
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.util import utcnow from homeassistant.util import utcnow
@ -370,7 +371,7 @@ async def test_thermostat_set_invalid_preset(
hass: HomeAssistant, cube: MaxCube, thermostat: MaxThermostat hass: HomeAssistant, cube: MaxCube, thermostat: MaxThermostat
) -> None: ) -> None:
"""Set hvac mode to heat.""" """Set hvac mode to heat."""
with pytest.raises(ValueError): with pytest.raises(ServiceValidationError):
await hass.services.async_call( await hass.services.async_call(
CLIMATE_DOMAIN, CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE, SERVICE_SET_PRESET_MODE,

View file

@ -33,6 +33,7 @@ from homeassistant.components.mqtt.climate import (
) )
from homeassistant.const import ATTR_TEMPERATURE, Platform, UnitOfTemperature from homeassistant.const import ATTR_TEMPERATURE, Platform, UnitOfTemperature
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from .test_common import ( from .test_common import (
help_custom_config, help_custom_config,
@ -1130,8 +1131,9 @@ async def test_set_preset_mode_optimistic(
state = hass.states.get(ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "comfort" assert state.attributes.get("preset_mode") == "comfort"
await common.async_set_preset_mode(hass, "invalid", ENTITY_CLIMATE) with pytest.raises(ServiceValidationError):
assert "'invalid' is not a valid preset mode" in caplog.text await common.async_set_preset_mode(hass, "invalid", ENTITY_CLIMATE)
assert "'invalid' is not a valid preset mode" in caplog.text
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -1187,8 +1189,9 @@ async def test_set_preset_mode_explicit_optimistic(
state = hass.states.get(ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get("preset_mode") == "comfort" assert state.attributes.get("preset_mode") == "comfort"
await common.async_set_preset_mode(hass, "invalid", ENTITY_CLIMATE) with pytest.raises(ServiceValidationError):
assert "'invalid' is not a valid preset mode" in caplog.text await common.async_set_preset_mode(hass, "invalid", ENTITY_CLIMATE)
assert "'invalid' is not a valid preset mode" in caplog.text
@pytest.mark.parametrize( @pytest.mark.parametrize(

View file

@ -39,7 +39,7 @@ from homeassistant.const import (
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from .common import ( from .common import (
DEVICE_COMMAND, 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_MODE] == FAN_ON
assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] 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 common.async_set_fan_mode(hass, FAN_LOW)
await hass.async_block_till_done() 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] assert thermostat.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_NONE]
# Set preset mode that is invalid # Set preset mode that is invalid
with pytest.raises(ValueError): with pytest.raises(ServiceValidationError):
await common.async_set_preset_mode(hass, PRESET_SLEEP) await common.async_set_preset_mode(hass, PRESET_SLEEP)
await hass.async_block_till_done() await hass.async_block_till_done()

View file

@ -33,6 +33,7 @@ from homeassistant.components.netatmo.const import (
) )
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_WEBHOOK_ID from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_WEBHOOK_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .common import selected_platforms, simulate_webhook 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.async_block_till_done()
await hass.services.async_call( with pytest.raises(ServiceValidationError):
CLIMATE_DOMAIN, await hass.services.async_call(
SERVICE_SET_PRESET_MODE, CLIMATE_DOMAIN,
{ATTR_ENTITY_ID: "climate.cocina", ATTR_PRESET_MODE: "invalid"}, SERVICE_SET_PRESET_MODE,
blocking=True, {ATTR_ENTITY_ID: "climate.cocina", ATTR_PRESET_MODE: "invalid"},
) blocking=True,
await hass.async_block_till_done() )
await hass.async_block_till_done()
assert "Preset mode 'invalid' not available" in caplog.text
async def test_valves_service_turn_off( async def test_valves_service_turn_off(

View file

@ -1330,10 +1330,7 @@ async def test_climate_fan_mode_and_swing_mode_not_supported(
with patch( with patch(
"homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property",
), pytest.raises( ), pytest.raises(ServiceValidationError):
HomeAssistantError,
match="Climate swing mode faulty_swing_mode is not supported by the integration, please open an issue",
):
await hass.services.async_call( await hass.services.async_call(
CLIMATE_DOMAIN, CLIMATE_DOMAIN,
SERVICE_SET_SWING_MODE, SERVICE_SET_SWING_MODE,
@ -1343,10 +1340,7 @@ async def test_climate_fan_mode_and_swing_mode_not_supported(
with patch( with patch(
"homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property",
), pytest.raises( ), pytest.raises(ServiceValidationError):
HomeAssistantError,
match="Climate fan mode faulty_fan_mode is not supported by the integration, please open an issue",
):
await hass.services.async_call( await hass.services.async_call(
CLIMATE_DOMAIN, CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE, SERVICE_SET_FAN_MODE,

View file

@ -25,7 +25,7 @@ from homeassistant.components.shelly.const import DOMAIN, MODEL_WALL_DISPLAY
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNAVAILABLE from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant, State 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 from homeassistant.helpers import entity_registry as er
import homeassistant.helpers.issue_registry as ir import homeassistant.helpers.issue_registry as ir
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM 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 assert hass.states.get(entity_id).state == HVACMode.HEAT
await hass.services.async_call( with pytest.raises(ServiceValidationError):
CLIMATE_DOMAIN, await hass.services.async_call(
SERVICE_SET_PRESET_MODE, CLIMATE_DOMAIN,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: "Profile1"}, SERVICE_SET_PRESET_MODE,
blocking=True, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: "Profile1"},
) blocking=True,
)
mock_block_device.http_request.assert_not_called() mock_block_device.http_request.assert_not_called()

View file

@ -25,7 +25,7 @@ from homeassistant.components.climate import (
) )
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNAVAILABLE from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.util import slugify from homeassistant.util import slugify
from . import init_integration from . import init_integration
@ -336,9 +336,8 @@ async def test_climate_control_errors(
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 24}, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 24},
blocking=True, blocking=True,
) )
# Test exception when trying set fan level # Test exception when trying set fan level
with pytest.raises(HomeAssistantError): with pytest.raises(ServiceValidationError):
await hass.services.async_call( await hass.services.async_call(
CLIMATE_DOMAIN, CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE, SERVICE_SET_FAN_MODE,
@ -347,7 +346,7 @@ async def test_climate_control_errors(
) )
# Test exception when trying set swing mode # Test exception when trying set swing mode
with pytest.raises(HomeAssistantError): with pytest.raises(ServiceValidationError):
await hass.services.async_call( await hass.services.async_call(
CLIMATE_DOMAIN, CLIMATE_DOMAIN,
SERVICE_SET_SWING_MODE, SERVICE_SET_SWING_MODE,

View file

@ -43,6 +43,7 @@ from homeassistant.const import (
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from . import init_integration from . import init_integration
@ -337,7 +338,7 @@ async def test_service_calls(
mock_instance.set_fanspeed.reset_mock() mock_instance.set_fanspeed.reset_mock()
# FAN_MIDDLE is not supported # FAN_MIDDLE is not supported
with pytest.raises(ValueError): with pytest.raises(ServiceValidationError):
await hass.services.async_call( await hass.services.async_call(
CLIMATE_DOMAIN, CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE, SERVICE_SET_FAN_MODE,

View file

@ -52,7 +52,7 @@ from homeassistant.const import (
Platform, Platform,
) )
from homeassistant.core import HomeAssistant 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 .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 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) state = hass.states.get(entity_id)
assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE
await hass.services.async_call( with pytest.raises(ServiceValidationError):
CLIMATE_DOMAIN, await hass.services.async_call(
SERVICE_SET_PRESET_MODE, CLIMATE_DOMAIN,
{ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "invalid_preset"}, SERVICE_SET_PRESET_MODE,
blocking=True, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "invalid_preset"},
) blocking=True,
)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE 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) entity_id = find_entity_id(Platform.CLIMATE, device_climate_fan, hass)
fan_cluster = device_climate_fan.device.endpoints[1].fan fan_cluster = device_climate_fan.device.endpoints[1].fan
await hass.services.async_call( with pytest.raises(ServiceValidationError):
CLIMATE_DOMAIN, await hass.services.async_call(
SERVICE_SET_FAN_MODE, CLIMATE_DOMAIN,
{ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_LOW}, SERVICE_SET_FAN_MODE,
blocking=True, {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_LOW},
) blocking=True,
assert fan_cluster.write_attributes.await_count == 0 )
assert fan_cluster.write_attributes.await_count == 0
async def test_set_fan_mode(hass: HomeAssistant, device_climate_fan) -> None: async def test_set_fan_mode(hass: HomeAssistant, device_climate_fan) -> None:

View file

@ -40,6 +40,7 @@ from homeassistant.const import (
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import issue_registry as ir from homeassistant.helpers import issue_registry as ir
from .common import ( from .common import (
@ -278,7 +279,7 @@ async def test_thermostat_v2(
client.async_send_command.reset_mock() client.async_send_command.reset_mock()
# Test setting invalid fan mode # Test setting invalid fan mode
with pytest.raises(ValueError): with pytest.raises(ServiceValidationError):
await hass.services.async_call( await hass.services.async_call(
CLIMATE_DOMAIN, CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE, 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_TEMPERATURE] is None
assert state.attributes[ATTR_PRESET_MODE] == "Full power" assert state.attributes[ATTR_PRESET_MODE] == "Full power"
with pytest.raises(ValueError): with pytest.raises(ServiceValidationError):
# Test setting invalid preset mode # Test setting invalid preset mode
await hass.services.async_call( await hass.services.async_call(
CLIMATE_DOMAIN, CLIMATE_DOMAIN,