Raise ServiceValidationError on invalid select option (#106350)
* Raise ServiceValidationError on invalid select option * Fix tests * Correct place holders * More test fixes
This commit is contained in:
parent
fbcb31b103
commit
c7eab49c70
11 changed files with 60 additions and 27 deletions
|
@ -8,7 +8,8 @@ from typing import TYPE_CHECKING, Any, final
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.config_validation import (
|
from homeassistant.helpers.config_validation import (
|
||||||
PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA,
|
||||||
|
@ -90,7 +91,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
component.async_register_entity_service(
|
component.async_register_entity_service(
|
||||||
SERVICE_SELECT_OPTION,
|
SERVICE_SELECT_OPTION,
|
||||||
{vol.Required(ATTR_OPTION): cv.string},
|
{vol.Required(ATTR_OPTION): cv.string},
|
||||||
async_select_option,
|
SelectEntity.async_handle_select_option.__name__,
|
||||||
)
|
)
|
||||||
|
|
||||||
component.async_register_entity_service(
|
component.async_register_entity_service(
|
||||||
|
@ -102,14 +103,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_select_option(entity: SelectEntity, service_call: ServiceCall) -> None:
|
|
||||||
"""Service call wrapper to set a new value."""
|
|
||||||
option = service_call.data[ATTR_OPTION]
|
|
||||||
if option not in entity.options:
|
|
||||||
raise ValueError(f"Option {option} not valid for {entity.entity_id}")
|
|
||||||
await entity.async_select_option(option)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up a config entry."""
|
"""Set up a config entry."""
|
||||||
component: EntityComponent[SelectEntity] = hass.data[DOMAIN]
|
component: EntityComponent[SelectEntity] = hass.data[DOMAIN]
|
||||||
|
@ -177,6 +170,30 @@ class SelectEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||||
"""Return the selected entity option to represent the entity state."""
|
"""Return the selected entity option to represent the entity state."""
|
||||||
return self._attr_current_option
|
return self._attr_current_option
|
||||||
|
|
||||||
|
@final
|
||||||
|
@callback
|
||||||
|
def _valid_option_or_raise(self, option: str) -> None:
|
||||||
|
"""Raise ServiceValidationError on invalid option."""
|
||||||
|
options = self.options
|
||||||
|
if not options or option not in options:
|
||||||
|
friendly_options: str = ", ".join(options or [])
|
||||||
|
raise ServiceValidationError(
|
||||||
|
f"Option {option} is not valid for {self.entity_id}",
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="not_valid_option",
|
||||||
|
translation_placeholders={
|
||||||
|
"entity_id": self.entity_id,
|
||||||
|
"option": option,
|
||||||
|
"options": friendly_options,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@final
|
||||||
|
async def async_handle_select_option(self, option: str) -> None:
|
||||||
|
"""Service call wrapper to set a new value."""
|
||||||
|
self._valid_option_or_raise(option)
|
||||||
|
await self.async_select_option(option)
|
||||||
|
|
||||||
def select_option(self, option: str) -> None:
|
def select_option(self, option: str) -> None:
|
||||||
"""Change the selected option."""
|
"""Change the selected option."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
|
@ -64,5 +64,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"not_valid_option": {
|
||||||
|
"message": "Option {option} is not valid for entity {entity_id}, valid options are: {options}."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import pytest
|
||||||
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
|
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, SERVICE_SELECT_OPTION
|
from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, SERVICE_SELECT_OPTION
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
|
|
||||||
from .util import async_init_integration
|
from .util import async_init_integration
|
||||||
|
|
||||||
|
@ -85,7 +86,7 @@ async def test_airzone_select_sleep(hass: HomeAssistant) -> None:
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ServiceValidationError):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
SELECT_DOMAIN,
|
SELECT_DOMAIN,
|
||||||
SERVICE_SELECT_OPTION,
|
SERVICE_SELECT_OPTION,
|
||||||
|
|
|
@ -8,7 +8,7 @@ import respx
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
|
|
||||||
from . import check_remote_service_call, setup_mocked_integration
|
from . import check_remote_service_call, setup_mocked_integration
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ async def test_service_call_invalid_input(
|
||||||
old_value = hass.states.get(entity_id).state
|
old_value = hass.states.get(entity_id).state
|
||||||
|
|
||||||
# Test
|
# Test
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ServiceValidationError):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"select",
|
"select",
|
||||||
"select_option",
|
"select_option",
|
||||||
|
@ -108,7 +108,7 @@ async def test_service_call_invalid_input(
|
||||||
[
|
[
|
||||||
(MyBMWRemoteServiceError, HomeAssistantError),
|
(MyBMWRemoteServiceError, HomeAssistantError),
|
||||||
(MyBMWAPIError, HomeAssistantError),
|
(MyBMWAPIError, HomeAssistantError),
|
||||||
(ValueError, ValueError),
|
(ServiceValidationError, ServiceValidationError),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_service_call_fail(
|
async def test_service_call_fail(
|
||||||
|
|
|
@ -11,6 +11,7 @@ from homeassistant.components.select import (
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||||
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
|
||||||
|
|
||||||
ENTITY_SPEED = "select.speed"
|
ENTITY_SPEED = "select.speed"
|
||||||
|
@ -51,7 +52,7 @@ async def test_select_option_bad_attr(hass: HomeAssistant) -> None:
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "ridiculous_speed"
|
assert state.state == "ridiculous_speed"
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ServiceValidationError):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_SELECT_OPTION,
|
SERVICE_SELECT_OPTION,
|
||||||
|
|
|
@ -14,6 +14,7 @@ from homeassistant.components.flux_led.const import CONF_WHITE_CHANNEL_TYPE, DOM
|
||||||
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
|
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, CONF_HOST, CONF_NAME
|
from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, CONF_HOST, CONF_NAME
|
||||||
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.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
@ -133,7 +134,7 @@ async def test_select_addressable_strip_config(hass: HomeAssistant) -> None:
|
||||||
state = hass.states.get(ic_type_entity_id)
|
state = hass.states.get(ic_type_entity_id)
|
||||||
assert state.state == "WS2812B"
|
assert state.state == "WS2812B"
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ServiceValidationError):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
SELECT_DOMAIN,
|
SELECT_DOMAIN,
|
||||||
"select_option",
|
"select_option",
|
||||||
|
@ -149,7 +150,7 @@ async def test_select_addressable_strip_config(hass: HomeAssistant) -> None:
|
||||||
bulb.async_set_device_config.assert_called_once_with(wiring="GRBW")
|
bulb.async_set_device_config.assert_called_once_with(wiring="GRBW")
|
||||||
bulb.async_set_device_config.reset_mock()
|
bulb.async_set_device_config.reset_mock()
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ServiceValidationError):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
SELECT_DOMAIN,
|
SELECT_DOMAIN,
|
||||||
"select_option",
|
"select_option",
|
||||||
|
@ -191,7 +192,7 @@ async def test_select_mutable_0x25_strip_config(hass: HomeAssistant) -> None:
|
||||||
state = hass.states.get(operating_mode_entity_id)
|
state = hass.states.get(operating_mode_entity_id)
|
||||||
assert state.state == "RGBWW"
|
assert state.state == "RGBWW"
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ServiceValidationError):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
SELECT_DOMAIN,
|
SELECT_DOMAIN,
|
||||||
"select_option",
|
"select_option",
|
||||||
|
@ -226,7 +227,7 @@ async def test_select_24ghz_remote_config(hass: HomeAssistant) -> None:
|
||||||
state = hass.states.get(remote_config_entity_id)
|
state = hass.states.get(remote_config_entity_id)
|
||||||
assert state.state == "Open"
|
assert state.state == "Open"
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ServiceValidationError):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
SELECT_DOMAIN,
|
SELECT_DOMAIN,
|
||||||
"select_option",
|
"select_option",
|
||||||
|
@ -275,7 +276,7 @@ async def test_select_white_channel_type(hass: HomeAssistant) -> None:
|
||||||
state = hass.states.get(operating_mode_entity_id)
|
state = hass.states.get(operating_mode_entity_id)
|
||||||
assert state.state == WhiteChannelType.WARM.name.title()
|
assert state.state == WhiteChannelType.WARM.name.title()
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ServiceValidationError):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
SELECT_DOMAIN,
|
SELECT_DOMAIN,
|
||||||
"select_option",
|
"select_option",
|
||||||
|
|
|
@ -11,6 +11,7 @@ from homeassistant.components.knx.const import (
|
||||||
from homeassistant.components.knx.schema import SelectSchema
|
from homeassistant.components.knx.schema import SelectSchema
|
||||||
from homeassistant.const import CONF_NAME, CONF_PAYLOAD, STATE_UNKNOWN
|
from homeassistant.const import CONF_NAME, CONF_PAYLOAD, STATE_UNKNOWN
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HomeAssistant, State
|
||||||
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
|
|
||||||
from .conftest import KNXTestKit
|
from .conftest import KNXTestKit
|
||||||
|
|
||||||
|
@ -76,7 +77,7 @@ async def test_select_dpt_2_simple(hass: HomeAssistant, knx: KNXTestKit) -> None
|
||||||
assert state.state is STATE_UNKNOWN
|
assert state.state is STATE_UNKNOWN
|
||||||
|
|
||||||
# select invalid option
|
# select invalid option
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ServiceValidationError):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"select",
|
"select",
|
||||||
"select_option",
|
"select_option",
|
||||||
|
|
|
@ -12,6 +12,7 @@ from homeassistant.components.select import (
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, EntityCategory
|
from homeassistant.const import ATTR_ENTITY_ID, EntityCategory
|
||||||
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 .conftest import setup_integration
|
from .conftest import setup_integration
|
||||||
|
@ -59,7 +60,7 @@ async def test_invalid_wait_time_select(hass: HomeAssistant, mock_account) -> No
|
||||||
|
|
||||||
data = {ATTR_ENTITY_ID: SELECT_ENTITY_ID, ATTR_OPTION: "10"}
|
data = {ATTR_ENTITY_ID: SELECT_ENTITY_ID, ATTR_OPTION: "10"}
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ServiceValidationError):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
PLATFORM_DOMAIN,
|
PLATFORM_DOMAIN,
|
||||||
SERVICE_SELECT_OPTION,
|
SERVICE_SELECT_OPTION,
|
||||||
|
|
|
@ -15,6 +15,7 @@ from homeassistant.const import (
|
||||||
EntityCategory,
|
EntityCategory,
|
||||||
)
|
)
|
||||||
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.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
@ -84,7 +85,7 @@ async def test_select_invalid_option(hass: HomeAssistant) -> None:
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "60"
|
assert state.state == "60"
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ServiceValidationError):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
SELECT_DOMAIN,
|
SELECT_DOMAIN,
|
||||||
SERVICE_SELECT_OPTION,
|
SERVICE_SELECT_OPTION,
|
||||||
|
|
|
@ -17,6 +17,7 @@ from homeassistant.components.select import (
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, STATE_UNKNOWN
|
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, STATE_UNKNOWN
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
@ -111,8 +112,8 @@ async def test_custom_integration_and_validation(
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert hass.states.get("select.select_1").state == "option 2"
|
assert hass.states.get("select.select_1").state == "option 2"
|
||||||
|
|
||||||
# test ValueError trigger
|
# test ServiceValidationError trigger
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ServiceValidationError) as exc:
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_SELECT_OPTION,
|
SERVICE_SELECT_OPTION,
|
||||||
|
@ -120,11 +121,14 @@ async def test_custom_integration_and_validation(
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
assert exc.value.translation_domain == DOMAIN
|
||||||
|
assert exc.value.translation_key == "not_valid_option"
|
||||||
|
|
||||||
assert hass.states.get("select.select_1").state == "option 2"
|
assert hass.states.get("select.select_1").state == "option 2"
|
||||||
|
|
||||||
assert hass.states.get("select.select_2").state == STATE_UNKNOWN
|
assert hass.states.get("select.select_2").state == STATE_UNKNOWN
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ServiceValidationError):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_SELECT_OPTION,
|
SERVICE_SELECT_OPTION,
|
||||||
|
|
|
@ -31,6 +31,7 @@ from homeassistant.const import (
|
||||||
Platform,
|
Platform,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
|
|
||||||
from . import TEST_MAC
|
from . import TEST_MAC
|
||||||
|
|
||||||
|
@ -77,7 +78,7 @@ async def test_select_bad_attr(hass: HomeAssistant) -> None:
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "forward"
|
assert state.state == "forward"
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ServiceValidationError):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"select",
|
"select",
|
||||||
SERVICE_SELECT_OPTION,
|
SERVICE_SELECT_OPTION,
|
||||||
|
|
Loading…
Add table
Reference in a new issue