From 3aae9b629fc8ca0b8cbf8a2981f0bce20a78e16f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 1 Dec 2024 16:53:06 +0100 Subject: [PATCH] Add exception translation for entity action not supported (#131956) --- .../components/homeassistant/strings.json | 3 +++ homeassistant/exceptions.py | 19 +++++++++++++++++++ homeassistant/helpers/service.py | 5 ++--- tests/components/august/test_lock.py | 6 ++++-- tests/components/calendar/test_init.py | 11 ++++++++--- tests/components/google/test_init.py | 13 +++++++++---- tests/components/matter/test_vacuum.py | 10 +++++++--- .../components/samsungtv/test_media_player.py | 6 ++++-- tests/components/tedee/test_lock.py | 9 ++++++--- tests/components/tesla_fleet/test_climate.py | 13 ++++++++++--- tests/components/todo/test_init.py | 13 +++++++++---- tests/components/yale/test_lock.py | 13 ++++++++++--- tests/helpers/test_service.py | 8 +++++++- 13 files changed, 98 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index da8a1015d79..52b330bfbc8 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -224,6 +224,9 @@ "service_not_found": { "message": "Action {domain}.{service} not found." }, + "service_not_supported": { + "message": "Entity {entity_id} does not support action {domain}.{service}." + }, "service_does_not_support_response": { "message": "An action which does not return responses can't be called with {return_response}." }, diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index f308cbc5cd8..85fe55277fa 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -270,6 +270,25 @@ class ServiceNotFound(ServiceValidationError): self.generate_message = True +class ServiceNotSupported(ServiceValidationError): + """Raised when an entity action is not supported.""" + + def __init__(self, domain: str, service: str, entity_id: str) -> None: + """Initialize ServiceNotSupported exception.""" + super().__init__( + translation_domain="homeassistant", + translation_key="service_not_supported", + translation_placeholders={ + "domain": domain, + "service": service, + "entity_id": entity_id, + }, + ) + self.domain = domain + self.service = service + self.generate_message = True + + class MaxLengthExceeded(HomeAssistantError): """Raised when a property value has exceeded the max character length.""" diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 31b2e8e8ac8..35135010452 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -42,6 +42,7 @@ from homeassistant.core import ( ) from homeassistant.exceptions import ( HomeAssistantError, + ServiceNotSupported, TemplateError, Unauthorized, UnknownUser, @@ -986,9 +987,7 @@ async def entity_service_call( ): # If entity explicitly referenced, raise an error if referenced is not None and entity.entity_id in referenced.referenced: - raise HomeAssistantError( - f"Entity {entity.entity_id} does not support this service." - ) + raise ServiceNotSupported(call.domain, call.service, entity.entity_id) continue diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index 1b8c98e299c..eb177a35cfb 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -20,8 +20,9 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import ServiceNotSupported from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from .mocks import ( @@ -453,8 +454,9 @@ async def test_open_throws_hass_service_not_supported_error( hass: HomeAssistant, ) -> None: """Test open throws correct error on entity does not support this service error.""" + await async_setup_component(hass, "homeassistant", {}) mocked_lock_detail = await _mock_operative_august_lock_detail(hass) await _create_august_with_devices(hass, [mocked_lock_detail]) data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"} - with pytest.raises(HomeAssistantError, match="does not support this service"): + with pytest.raises(ServiceNotSupported, match="does not support action"): await hass.services.async_call(LOCK_DOMAIN, SERVICE_OPEN, data, blocking=True) diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 4ad5e11b8e4..36b102b933a 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -14,7 +14,8 @@ import voluptuous as vol from homeassistant.components.calendar import DOMAIN, SERVICE_GET_EVENTS from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceNotSupported +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from .conftest import MockCalendarEntity, MockConfigEntry @@ -214,8 +215,12 @@ async def test_unsupported_websocket( async def test_unsupported_create_event_service(hass: HomeAssistant) -> None: """Test unsupported service call.""" - - with pytest.raises(HomeAssistantError, match="does not support this service"): + await async_setup_component(hass, "homeassistant", {}) + with pytest.raises( + ServiceNotSupported, + match="Entity calendar.calendar_1 does not " + "support action calendar.create_event", + ): await hass.services.async_call( DOMAIN, "create_event", diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 536a1440958..ad43e341968 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -20,7 +20,8 @@ from homeassistant.components.google.const import CONF_CALENDAR_ACCESS from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF from homeassistant.core import HomeAssistant, State -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceNotSupported +from homeassistant.setup import async_setup_component from homeassistant.util.dt import UTC, utcnow from .conftest import ( @@ -593,7 +594,7 @@ async def test_unsupported_create_event( aioclient_mock: AiohttpClientMocker, ) -> None: """Test create event service call is unsupported for virtual calendars.""" - + await async_setup_component(hass, "homeassistant", {}) mock_calendars_list({"items": [test_api_calendar]}) mock_events_list({}) assert await component_setup() @@ -601,8 +602,12 @@ async def test_unsupported_create_event( start_datetime = datetime.datetime.now(tz=zoneinfo.ZoneInfo("America/Regina")) delta = datetime.timedelta(days=3, hours=3) end_datetime = start_datetime + delta + entity_id = "calendar.backyard_light" - with pytest.raises(HomeAssistantError, match="does not support this service"): + with pytest.raises( + ServiceNotSupported, + match=f"Entity {entity_id} does not support action google.create_event", + ): await hass.services.async_call( DOMAIN, "create_event", @@ -613,7 +618,7 @@ async def test_unsupported_create_event( "summary": TEST_EVENT_SUMMARY, "description": TEST_EVENT_DESCRIPTION, }, - target={"entity_id": "calendar.backyard_light"}, + target={"entity_id": entity_id}, blocking=True, ) diff --git a/tests/components/matter/test_vacuum.py b/tests/components/matter/test_vacuum.py index 86f7542395a..1b33f6a2fe2 100644 --- a/tests/components/matter/test_vacuum.py +++ b/tests/components/matter/test_vacuum.py @@ -8,8 +8,10 @@ import pytest from syrupy import SnapshotAssertion from homeassistant.const import Platform -from homeassistant.core import HomeAssistant, HomeAssistantError +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceNotSupported from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component from .common import ( set_node_attribute, @@ -35,6 +37,8 @@ async def test_vacuum_actions( matter_node: MatterNode, ) -> None: """Test vacuum entity actions.""" + # Fetch translations + await async_setup_component(hass, "homeassistant", {}) entity_id = "vacuum.mock_vacuum" state = hass.states.get(entity_id) assert state @@ -96,8 +100,8 @@ async def test_vacuum_actions( # test stop action # stop command is not supported by the vacuum fixture with pytest.raises( - HomeAssistantError, - match="Entity vacuum.mock_vacuum does not support this service.", + ServiceNotSupported, + match="Entity vacuum.mock_vacuum does not support action vacuum.stop", ): await hass.services.async_call( "vacuum", diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index ef7e58251e8..1a7c8713b17 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -76,7 +76,8 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import ServiceNotSupported +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from . import async_wait_config_entry_reload, setup_samsungtv_entry @@ -1021,8 +1022,9 @@ async def test_turn_on_wol(hass: HomeAssistant) -> None: async def test_turn_on_without_turnon(hass: HomeAssistant, remote: Mock) -> None: """Test turn on.""" + await async_setup_component(hass, "homeassistant", {}) await setup_samsungtv_entry(hass, MOCK_CONFIG) - with pytest.raises(HomeAssistantError, match="does not support this service"): + with pytest.raises(ServiceNotSupported, match="does not support action"): await hass.services.async_call( MP_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True ) diff --git a/tests/components/tedee/test_lock.py b/tests/components/tedee/test_lock.py index 45eae6e22d9..d84acb212ea 100644 --- a/tests/components/tedee/test_lock.py +++ b/tests/components/tedee/test_lock.py @@ -24,8 +24,9 @@ from homeassistant.components.lock import ( from homeassistant.components.webhook import async_generate_url from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceNotSupported from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component from .conftest import WEBHOOK_ID @@ -113,6 +114,8 @@ async def test_lock_without_pullspring( snapshot: SnapshotAssertion, ) -> None: """Test the tedee lock without pullspring.""" + # Fetch translations + await async_setup_component(hass, "homeassistant", {}) mock_tedee.lock.return_value = None mock_tedee.unlock.return_value = None mock_tedee.open.return_value = None @@ -131,8 +134,8 @@ async def test_lock_without_pullspring( assert device == snapshot with pytest.raises( - HomeAssistantError, - match="Entity lock.lock_2c3d does not support this service.", + ServiceNotSupported, + match=f"Entity lock.lock_2c3d does not support action {LOCK_DOMAIN}.{SERVICE_OPEN}", ): await hass.services.async_call( LOCK_DOMAIN, diff --git a/tests/components/tesla_fleet/test_climate.py b/tests/components/tesla_fleet/test_climate.py index b8cb7f1269b..b45e5259a5c 100644 --- a/tests/components/tesla_fleet/test_climate.py +++ b/tests/components/tesla_fleet/test_climate.py @@ -24,8 +24,13 @@ from homeassistant.components.climate import ( from homeassistant.components.tesla_fleet.coordinator import VEHICLE_INTERVAL from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.exceptions import ( + HomeAssistantError, + ServiceNotSupported, + ServiceValidationError, +) from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component from . import assert_entities, setup_platform from .const import ( @@ -391,6 +396,7 @@ async def test_climate_noscope( snapshot: SnapshotAssertion, ) -> None: """Tests with no command scopes.""" + await async_setup_component(hass, "homeassistant", {}) await setup_platform(hass, readonly_config_entry, [Platform.CLIMATE]) entity_id = "climate.test_climate" @@ -405,8 +411,9 @@ async def test_climate_noscope( ) with pytest.raises( - HomeAssistantError, - match="Entity climate.test_climate does not support this service.", + ServiceNotSupported, + match="Entity climate.test_climate does not " + "support action climate.set_temperature", ): await hass.services.async_call( CLIMATE_DOMAIN, diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index fd052a7f8a3..8e8c010f758 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -27,7 +27,11 @@ from homeassistant.components.todo import ( from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.exceptions import ( + HomeAssistantError, + ServiceNotSupported, + ServiceValidationError, +) from homeassistant.helpers import intent from homeassistant.setup import async_setup_component @@ -941,14 +945,15 @@ async def test_unsupported_service( payload: dict[str, Any] | None, ) -> None: """Test a To-do list that does not support features.""" - + # Fetch translations + await async_setup_component(hass, "homeassistant", "") entity1 = TodoListEntity() entity1.entity_id = "todo.entity1" await create_mock_platform(hass, [entity1]) with pytest.raises( - HomeAssistantError, - match="does not support this service", + ServiceNotSupported, + match=f"Entity todo.entity1 does not support action {DOMAIN}.{service_name}", ): await hass.services.async_call( DOMAIN, diff --git a/tests/components/yale/test_lock.py b/tests/components/yale/test_lock.py index f0fe018759c..f6b96120d0d 100644 --- a/tests/components/yale/test_lock.py +++ b/tests/components/yale/test_lock.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import ServiceNotSupported from homeassistant.helpers import device_registry as dr, entity_registry as er import homeassistant.util.dt as dt_util @@ -29,6 +29,7 @@ from .mocks import ( _mock_lock_from_fixture, _mock_lock_with_unlatch, _mock_operative_yale_lock_detail, + async_setup_component, ) from tests.common import async_fire_time_changed @@ -418,8 +419,14 @@ async def test_open_throws_hass_service_not_supported_error( hass: HomeAssistant, ) -> None: """Test open throws correct error on entity does not support this service error.""" + # Fetch translations + await async_setup_component(hass, "homeassistant", {}) mocked_lock_detail = await _mock_operative_yale_lock_detail(hass) await _create_yale_with_devices(hass, [mocked_lock_detail]) - data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"} - with pytest.raises(HomeAssistantError, match="does not support this service"): + entity_id = "lock.a6697750d607098bae8d6baa11ef8063_name" + data = {ATTR_ENTITY_ID: entity_id} + with pytest.raises( + ServiceNotSupported, + match=f"Entity {entity_id} does not support action {LOCK_DOMAIN}.{SERVICE_OPEN}", + ): await hass.services.async_call(LOCK_DOMAIN, SERVICE_OPEN, data, blocking=True) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index d0e1aa34340..e63cb69909c 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -1274,6 +1274,8 @@ async def test_register_with_mixed_case(hass: HomeAssistant) -> None: async def test_call_with_required_features(hass: HomeAssistant, mock_entities) -> None: """Test service calls invoked only if entity has required features.""" + # Set up homeassistant component to fetch the translations + await async_setup_component(hass, "homeassistant", {}) test_service_mock = AsyncMock(return_value=None) await service.entity_service_call( hass, @@ -1293,7 +1295,11 @@ async def test_call_with_required_features(hass: HomeAssistant, mock_entities) - # Test we raise if we target entity ID that does not support the service test_service_mock.reset_mock() - with pytest.raises(exceptions.HomeAssistantError): + with pytest.raises( + exceptions.ServiceNotSupported, + match="Entity light.living_room does not " + "support action test_domain.test_service", + ): await service.entity_service_call( hass, mock_entities,