Add exception translation for entity action not supported (#131956)

This commit is contained in:
Jan Bouwhuis 2024-12-01 16:53:06 +01:00 committed by GitHub
parent c55a4e9584
commit 3aae9b629f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 98 additions and 31 deletions

View File

@ -224,6 +224,9 @@
"service_not_found": { "service_not_found": {
"message": "Action {domain}.{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": { "service_does_not_support_response": {
"message": "An action which does not return responses can't be called with {return_response}." "message": "An action which does not return responses can't be called with {return_response}."
}, },

View File

@ -270,6 +270,25 @@ class ServiceNotFound(ServiceValidationError):
self.generate_message = True 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): class MaxLengthExceeded(HomeAssistantError):
"""Raised when a property value has exceeded the max character length.""" """Raised when a property value has exceeded the max character length."""

View File

@ -42,6 +42,7 @@ from homeassistant.core import (
) )
from homeassistant.exceptions import ( from homeassistant.exceptions import (
HomeAssistantError, HomeAssistantError,
ServiceNotSupported,
TemplateError, TemplateError,
Unauthorized, Unauthorized,
UnknownUser, UnknownUser,
@ -986,9 +987,7 @@ async def entity_service_call(
): ):
# If entity explicitly referenced, raise an error # If entity explicitly referenced, raise an error
if referenced is not None and entity.entity_id in referenced.referenced: if referenced is not None and entity.entity_id in referenced.referenced:
raise HomeAssistantError( raise ServiceNotSupported(call.domain, call.service, entity.entity_id)
f"Entity {entity.entity_id} does not support this service."
)
continue continue

View File

@ -20,8 +20,9 @@ from homeassistant.const import (
STATE_UNKNOWN, STATE_UNKNOWN,
) )
from homeassistant.core import HomeAssistant 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.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .mocks import ( from .mocks import (
@ -453,8 +454,9 @@ async def test_open_throws_hass_service_not_supported_error(
hass: HomeAssistant, hass: HomeAssistant,
) -> None: ) -> None:
"""Test open throws correct error on entity does not support this service error.""" """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) mocked_lock_detail = await _mock_operative_august_lock_detail(hass)
await _create_august_with_devices(hass, [mocked_lock_detail]) await _create_august_with_devices(hass, [mocked_lock_detail])
data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"} 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) await hass.services.async_call(LOCK_DOMAIN, SERVICE_OPEN, data, blocking=True)

View File

@ -14,7 +14,8 @@ import voluptuous as vol
from homeassistant.components.calendar import DOMAIN, SERVICE_GET_EVENTS from homeassistant.components.calendar import DOMAIN, SERVICE_GET_EVENTS
from homeassistant.core import HomeAssistant 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 import homeassistant.util.dt as dt_util
from .conftest import MockCalendarEntity, MockConfigEntry from .conftest import MockCalendarEntity, MockConfigEntry
@ -214,8 +215,12 @@ async def test_unsupported_websocket(
async def test_unsupported_create_event_service(hass: HomeAssistant) -> None: async def test_unsupported_create_event_service(hass: HomeAssistant) -> None:
"""Test unsupported service call.""" """Test unsupported service call."""
await async_setup_component(hass, "homeassistant", {})
with pytest.raises(HomeAssistantError, match="does not support this service"): with pytest.raises(
ServiceNotSupported,
match="Entity calendar.calendar_1 does not "
"support action calendar.create_event",
):
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
"create_event", "create_event",

View File

@ -20,7 +20,8 @@ from homeassistant.components.google.const import CONF_CALENDAR_ACCESS
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF
from homeassistant.core import HomeAssistant, State 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 homeassistant.util.dt import UTC, utcnow
from .conftest import ( from .conftest import (
@ -593,7 +594,7 @@ async def test_unsupported_create_event(
aioclient_mock: AiohttpClientMocker, aioclient_mock: AiohttpClientMocker,
) -> None: ) -> None:
"""Test create event service call is unsupported for virtual calendars.""" """Test create event service call is unsupported for virtual calendars."""
await async_setup_component(hass, "homeassistant", {})
mock_calendars_list({"items": [test_api_calendar]}) mock_calendars_list({"items": [test_api_calendar]})
mock_events_list({}) mock_events_list({})
assert await component_setup() assert await component_setup()
@ -601,8 +602,12 @@ async def test_unsupported_create_event(
start_datetime = datetime.datetime.now(tz=zoneinfo.ZoneInfo("America/Regina")) start_datetime = datetime.datetime.now(tz=zoneinfo.ZoneInfo("America/Regina"))
delta = datetime.timedelta(days=3, hours=3) delta = datetime.timedelta(days=3, hours=3)
end_datetime = start_datetime + delta 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( await hass.services.async_call(
DOMAIN, DOMAIN,
"create_event", "create_event",
@ -613,7 +618,7 @@ async def test_unsupported_create_event(
"summary": TEST_EVENT_SUMMARY, "summary": TEST_EVENT_SUMMARY,
"description": TEST_EVENT_DESCRIPTION, "description": TEST_EVENT_DESCRIPTION,
}, },
target={"entity_id": "calendar.backyard_light"}, target={"entity_id": entity_id},
blocking=True, blocking=True,
) )

View File

@ -8,8 +8,10 @@ import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
from homeassistant.const import Platform 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.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from .common import ( from .common import (
set_node_attribute, set_node_attribute,
@ -35,6 +37,8 @@ async def test_vacuum_actions(
matter_node: MatterNode, matter_node: MatterNode,
) -> None: ) -> None:
"""Test vacuum entity actions.""" """Test vacuum entity actions."""
# Fetch translations
await async_setup_component(hass, "homeassistant", {})
entity_id = "vacuum.mock_vacuum" entity_id = "vacuum.mock_vacuum"
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
@ -96,8 +100,8 @@ async def test_vacuum_actions(
# test stop action # test stop action
# stop command is not supported by the vacuum fixture # stop command is not supported by the vacuum fixture
with pytest.raises( with pytest.raises(
HomeAssistantError, ServiceNotSupported,
match="Entity vacuum.mock_vacuum does not support this service.", match="Entity vacuum.mock_vacuum does not support action vacuum.stop",
): ):
await hass.services.async_call( await hass.services.async_call(
"vacuum", "vacuum",

View File

@ -76,7 +76,8 @@ 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 ServiceNotSupported
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from . import async_wait_config_entry_reload, setup_samsungtv_entry 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: async def test_turn_on_without_turnon(hass: HomeAssistant, remote: Mock) -> None:
"""Test turn on.""" """Test turn on."""
await async_setup_component(hass, "homeassistant", {})
await setup_samsungtv_entry(hass, MOCK_CONFIG) 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( await hass.services.async_call(
MP_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True MP_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True
) )

View File

@ -24,8 +24,9 @@ from homeassistant.components.lock import (
from homeassistant.components.webhook import async_generate_url from homeassistant.components.webhook import async_generate_url
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant 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.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from .conftest import WEBHOOK_ID from .conftest import WEBHOOK_ID
@ -113,6 +114,8 @@ async def test_lock_without_pullspring(
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Test the tedee lock without pullspring.""" """Test the tedee lock without pullspring."""
# Fetch translations
await async_setup_component(hass, "homeassistant", {})
mock_tedee.lock.return_value = None mock_tedee.lock.return_value = None
mock_tedee.unlock.return_value = None mock_tedee.unlock.return_value = None
mock_tedee.open.return_value = None mock_tedee.open.return_value = None
@ -131,8 +134,8 @@ async def test_lock_without_pullspring(
assert device == snapshot assert device == snapshot
with pytest.raises( with pytest.raises(
HomeAssistantError, ServiceNotSupported,
match="Entity lock.lock_2c3d does not support this service.", match=f"Entity lock.lock_2c3d does not support action {LOCK_DOMAIN}.{SERVICE_OPEN}",
): ):
await hass.services.async_call( await hass.services.async_call(
LOCK_DOMAIN, LOCK_DOMAIN,

View File

@ -24,8 +24,13 @@ from homeassistant.components.climate import (
from homeassistant.components.tesla_fleet.coordinator import VEHICLE_INTERVAL from homeassistant.components.tesla_fleet.coordinator import VEHICLE_INTERVAL
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 HomeAssistantError, ServiceValidationError from homeassistant.exceptions import (
HomeAssistantError,
ServiceNotSupported,
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 . import assert_entities, setup_platform from . import assert_entities, setup_platform
from .const import ( from .const import (
@ -391,6 +396,7 @@ async def test_climate_noscope(
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Tests with no command scopes.""" """Tests with no command scopes."""
await async_setup_component(hass, "homeassistant", {})
await setup_platform(hass, readonly_config_entry, [Platform.CLIMATE]) await setup_platform(hass, readonly_config_entry, [Platform.CLIMATE])
entity_id = "climate.test_climate" entity_id = "climate.test_climate"
@ -405,8 +411,9 @@ async def test_climate_noscope(
) )
with pytest.raises( with pytest.raises(
HomeAssistantError, ServiceNotSupported,
match="Entity climate.test_climate does not support this service.", match="Entity climate.test_climate does not "
"support action climate.set_temperature",
): ):
await hass.services.async_call( await hass.services.async_call(
CLIMATE_DOMAIN, CLIMATE_DOMAIN,

View File

@ -27,7 +27,11 @@ from homeassistant.components.todo import (
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES
from homeassistant.core import HomeAssistant 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.helpers import intent
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -941,14 +945,15 @@ async def test_unsupported_service(
payload: dict[str, Any] | None, payload: dict[str, Any] | None,
) -> None: ) -> None:
"""Test a To-do list that does not support features.""" """Test a To-do list that does not support features."""
# Fetch translations
await async_setup_component(hass, "homeassistant", "")
entity1 = TodoListEntity() entity1 = TodoListEntity()
entity1.entity_id = "todo.entity1" entity1.entity_id = "todo.entity1"
await create_mock_platform(hass, [entity1]) await create_mock_platform(hass, [entity1])
with pytest.raises( with pytest.raises(
HomeAssistantError, ServiceNotSupported,
match="does not support this service", match=f"Entity todo.entity1 does not support action {DOMAIN}.{service_name}",
): ):
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,

View File

@ -18,7 +18,7 @@ from homeassistant.const import (
STATE_UNKNOWN, STATE_UNKNOWN,
) )
from homeassistant.core import HomeAssistant 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.helpers import device_registry as dr, entity_registry as er
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -29,6 +29,7 @@ from .mocks import (
_mock_lock_from_fixture, _mock_lock_from_fixture,
_mock_lock_with_unlatch, _mock_lock_with_unlatch,
_mock_operative_yale_lock_detail, _mock_operative_yale_lock_detail,
async_setup_component,
) )
from tests.common import async_fire_time_changed from tests.common import async_fire_time_changed
@ -418,8 +419,14 @@ async def test_open_throws_hass_service_not_supported_error(
hass: HomeAssistant, hass: HomeAssistant,
) -> None: ) -> None:
"""Test open throws correct error on entity does not support this service error.""" """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) mocked_lock_detail = await _mock_operative_yale_lock_detail(hass)
await _create_yale_with_devices(hass, [mocked_lock_detail]) await _create_yale_with_devices(hass, [mocked_lock_detail])
data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"} entity_id = "lock.a6697750d607098bae8d6baa11ef8063_name"
with pytest.raises(HomeAssistantError, match="does not support this service"): 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) await hass.services.async_call(LOCK_DOMAIN, SERVICE_OPEN, data, blocking=True)

View File

@ -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: async def test_call_with_required_features(hass: HomeAssistant, mock_entities) -> None:
"""Test service calls invoked only if entity has required features.""" """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) test_service_mock = AsyncMock(return_value=None)
await service.entity_service_call( await service.entity_service_call(
hass, 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 we raise if we target entity ID that does not support the service
test_service_mock.reset_mock() 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( await service.entity_service_call(
hass, hass,
mock_entities, mock_entities,