diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 8c55bbc2cba..d3785e0eae6 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -15,7 +15,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MODE, CONF_UNIT_OF_MEASUREMENT, UnitOfTemperature from homeassistant.core import HomeAssistant, ServiceCall, async_get_hass, callback -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.config_validation import ( PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -30,6 +30,7 @@ from .const import ( # noqa: F401 ATTR_MAX, ATTR_MIN, ATTR_STEP, + ATTR_STEP_VALIDATION, ATTR_VALUE, DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE, @@ -99,10 +100,17 @@ async def async_set_value(entity: NumberEntity, service_call: ServiceCall) -> No """Service call wrapper to set a new value.""" value = service_call.data["value"] if value < entity.min_value or value > entity.max_value: - raise ValueError( - f"Value {value} for {entity.entity_id} is outside valid range" - f" {entity.min_value} - {entity.max_value}" + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="out_of_range", + translation_placeholders={ + "value": value, + "entity_id": entity.entity_id, + "min_value": str(entity.min_value), + "max_value": str(entity.max_value), + }, ) + try: native_value = entity.convert_to_native_value(value) # Clamp to the native range @@ -174,7 +182,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Representation of a Number entity.""" _entity_component_unrecorded_attributes = frozenset( - {ATTR_MIN, ATTR_MAX, ATTR_STEP, ATTR_MODE} + {ATTR_MIN, ATTR_MAX, ATTR_STEP, ATTR_STEP_VALIDATION, ATTR_MODE} ) entity_description: NumberEntityDescription diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 89829adcc50..f279ffb72a8 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -54,6 +54,7 @@ ATTR_VALUE = "value" ATTR_MIN = "min" ATTR_MAX = "max" ATTR_STEP = "step" +ATTR_STEP_VALIDATION = "step_validation" DEFAULT_MIN_VALUE = 0.0 DEFAULT_MAX_VALUE = 100.0 diff --git a/homeassistant/components/number/strings.json b/homeassistant/components/number/strings.json index ffddc0c2b3c..502b2b4affd 100644 --- a/homeassistant/components/number/strings.json +++ b/homeassistant/components/number/strings.json @@ -161,6 +161,11 @@ "name": "[%key:component::sensor::entity_component::wind_speed::name%]" } }, + "exceptions": { + "out_of_range": { + "message": "Value {value} for {entity_id} is outside valid range {min_value} - {max_value}." + } + }, "services": { "set_value": { "name": "Set", diff --git a/tests/components/deconz/test_number.py b/tests/components/deconz/test_number.py index 3f86182e032..655ae2f42e2 100644 --- a/tests/components/deconz/test_number.py +++ b/tests/components/deconz/test_number.py @@ -11,6 +11,7 @@ from homeassistant.components.number import ( ) from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, EntityCategory from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import device_registry as dr, entity_registry as er from .test_gateway import ( @@ -186,7 +187,7 @@ async def test_number_entities( # Service set value beyond the supported range - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, diff --git a/tests/components/demo/test_number.py b/tests/components/demo/test_number.py index 3c41b98a3fa..20e3ce8fc11 100644 --- a/tests/components/demo/test_number.py +++ b/tests/components/demo/test_number.py @@ -16,6 +16,7 @@ from homeassistant.components.number import ( ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.setup import async_setup_component ENTITY_VOLUME = "number.volume" @@ -97,7 +98,7 @@ async def test_set_value_bad_range(hass: HomeAssistant) -> None: state = hass.states.get(ENTITY_VOLUME) assert state.state == "42.0" - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( DOMAIN, SERVICE_SET_VALUE, diff --git a/tests/components/flux_led/test_number.py b/tests/components/flux_led/test_number.py index 455bad05029..2ed0d34989f 100644 --- a/tests/components/flux_led/test_number.py +++ b/tests/components/flux_led/test_number.py @@ -21,7 +21,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -292,7 +292,7 @@ async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None: assert state.state == "4" await hass.async_block_till_done(wait_background_tasks=True) - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -314,7 +314,7 @@ async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None: bulb.async_set_device_config.assert_called_with(pixels_per_segment=100) bulb.async_set_device_config.reset_mock() - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -335,7 +335,7 @@ async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None: bulb.async_set_device_config.assert_called_with(music_pixels_per_segment=100) bulb.async_set_device_config.reset_mock() - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -356,7 +356,7 @@ async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None: bulb.async_set_device_config.assert_called_with(segments=5) bulb.async_set_device_config.reset_mock() - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, diff --git a/tests/components/knx/test_number.py b/tests/components/knx/test_number.py index be3fe070c10..5eec4530d4e 100644 --- a/tests/components/knx/test_number.py +++ b/tests/components/knx/test_number.py @@ -6,6 +6,7 @@ from homeassistant.components.knx.const import CONF_RESPOND_TO_READ, KNX_ADDRESS from homeassistant.components.knx.schema import NumberSchema from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import ServiceValidationError from .conftest import KNXTestKit @@ -37,14 +38,14 @@ async def test_number_set_value(hass: HomeAssistant, knx: KNXTestKit) -> None: assert state.attributes.get("unit_of_measurement") == "%" # set value out of range - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( "number", "set_value", {"entity_id": "number.test", "value": 101.0}, blocking=True, ) - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( "number", "set_value", diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 07d2baf4926..96ad4b4d2d4 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -36,6 +36,7 @@ from homeassistant.const import ( UnitOfVolumeFlowRate, ) from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY @@ -359,14 +360,20 @@ async def test_set_value( state = hass.states.get("number.test") assert state.state == "60.0" - # test ValueError trigger - with pytest.raises(ValueError): + # test range validation + with pytest.raises(ServiceValidationError) as exc: await hass.services.async_call( DOMAIN, SERVICE_SET_VALUE, {ATTR_VALUE: 110.0, ATTR_ENTITY_ID: "number.test"}, blocking=True, ) + assert exc.value.translation_domain == DOMAIN + assert exc.value.translation_key == "out_of_range" + assert ( + str(exc.value) + == "Value 110.0 for number.test is outside valid range 0.0 - 100.0" + ) await hass.async_block_till_done() state = hass.states.get("number.test") diff --git a/tests/components/rituals_perfume_genie/test_number.py b/tests/components/rituals_perfume_genie/test_number.py index f88bcc6d0cb..ddca70649b5 100644 --- a/tests/components/rituals_perfume_genie/test_number.py +++ b/tests/components/rituals_perfume_genie/test_number.py @@ -14,6 +14,7 @@ from homeassistant.components.number import ( ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -86,7 +87,7 @@ async def test_set_number_value_out_of_range(hass: HomeAssistant) -> None: assert state assert state.state == "2" - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -105,7 +106,7 @@ async def test_set_number_value_out_of_range(hass: HomeAssistant) -> None: assert state assert state.state == "2" - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE,