Skip to content

Commit

Permalink
Add exception translation for entity action not supported (home-assis…
Browse files Browse the repository at this point in the history
  • Loading branch information
jbouwh authored Dec 1, 2024
1 parent c55a4e9 commit 3aae9b6
Show file tree
Hide file tree
Showing 13 changed files with 98 additions and 31 deletions.
3 changes: 3 additions & 0 deletions homeassistant/components/homeassistant/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}."
},
Expand Down
19 changes: 19 additions & 0 deletions homeassistant/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,25 @@ def __init__(self, domain: str, service: str) -> None:
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."""

Expand Down
5 changes: 2 additions & 3 deletions homeassistant/helpers/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
)
from homeassistant.exceptions import (
HomeAssistantError,
ServiceNotSupported,
TemplateError,
Unauthorized,
UnknownUser,
Expand Down Expand Up @@ -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

Expand Down
6 changes: 4 additions & 2 deletions tests/components/august/test_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
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 (
Expand Down Expand Up @@ -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)
11 changes: 8 additions & 3 deletions tests/components/calendar/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

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
Expand Down Expand Up @@ -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",
Expand Down
13 changes: 9 additions & 4 deletions tests/components/google/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
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 (
Expand Down Expand Up @@ -593,16 +594,20 @@ 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()

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",
Expand All @@ -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,
)

Expand Down
10 changes: 7 additions & 3 deletions tests/components/matter/test_vacuum.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
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,
Expand All @@ -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
Expand Down Expand Up @@ -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",
Expand Down
6 changes: 4 additions & 2 deletions tests/components/samsungtv/test_media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@
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
Expand Down Expand Up @@ -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
)
Expand Down
9 changes: 6 additions & 3 deletions tests/components/tedee/test_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@
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

Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
13 changes: 10 additions & 3 deletions tests/components/tesla_fleet/test_climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@
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 (
Expand Down Expand Up @@ -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"

Expand All @@ -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,
Expand Down
13 changes: 9 additions & 4 deletions tests/components/todo/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
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

Expand Down Expand Up @@ -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,
Expand Down
13 changes: 10 additions & 3 deletions tests/components/yale/test_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
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

Expand All @@ -29,6 +29,7 @@
_mock_lock_from_fixture,
_mock_lock_with_unlatch,
_mock_operative_yale_lock_detail,
async_setup_component,
)

from tests.common import async_fire_time_changed
Expand Down Expand Up @@ -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)
8 changes: 7 additions & 1 deletion tests/helpers/test_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down

0 comments on commit 3aae9b6

Please sign in to comment.