diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 2af9ec7c853..68397a80f83 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -311,6 +311,12 @@ comp_entity_ids = vol.Any( ) +comp_entity_ids_or_uuids = vol.Any( + vol.All(vol.Lower, vol.Any(ENTITY_MATCH_ALL, ENTITY_MATCH_NONE)), + entity_ids_or_uuids, +) + + def entity_domain(domain: str | list[str]) -> Callable[[Any], str]: """Validate that entity belong to domain.""" ent_domain = entities_domain(domain) @@ -972,6 +978,23 @@ ENTITY_SERVICE_FIELDS = { ), } +TARGET_SERVICE_FIELDS = { + # Same as ENTITY_SERVICE_FIELDS but supports specifying entity by entity registry + # ID. + # Either accept static entity IDs, a single dynamic template or a mixed list + # of static and dynamic templates. While this could be solved with a single + # complex template, handling it like this, keeps config validation useful. + vol.Optional(ATTR_ENTITY_ID): vol.Any( + comp_entity_ids_or_uuids, dynamic_template, vol.All(list, template_complex) + ), + vol.Optional(ATTR_DEVICE_ID): vol.Any( + ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)]) + ), + vol.Optional(ATTR_AREA_ID): vol.Any( + ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)]) + ), +} + def make_entity_service_schema( schema: dict, *, extra: int = vol.PREVENT_EXTRA @@ -1034,7 +1057,7 @@ SERVICE_SCHEMA = vol.All( template, vol.All(dict, template_complex) ), vol.Optional(CONF_ENTITY_ID): comp_entity_ids, - vol.Optional(CONF_TARGET): vol.Any(ENTITY_SERVICE_FIELDS, dynamic_template), + vol.Optional(CONF_TARGET): vol.Any(TARGET_SERVICE_FIELDS, dynamic_template), } ), has_at_least_one_key(CONF_SERVICE, CONF_SERVICE_TEMPLATE), diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index bad233c006a..be8e316878f 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -218,7 +218,10 @@ def async_prepare_call_from_config( target.update(template.render_complex(conf, variables)) if CONF_ENTITY_ID in target: - target[CONF_ENTITY_ID] = cv.comp_entity_ids(target[CONF_ENTITY_ID]) + registry = entity_registry.async_get(hass) + target[CONF_ENTITY_ID] = entity_registry.async_resolve_entity_ids( + registry, cv.comp_entity_ids_or_uuids(target[CONF_ENTITY_ID]) + ) except TemplateError as ex: raise HomeAssistantError( f"Error rendering service target template: {ex}" diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 16f7ad4825a..54507d9b3bd 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -29,6 +29,7 @@ from homeassistant.setup import async_setup_component from tests.common import ( MockEntity, + async_mock_service, get_test_home_assistant, mock_device_registry, mock_registry, @@ -375,6 +376,27 @@ class TestServiceHelpers(unittest.TestCase): assert mock_log.call_count == 3 +async def test_service_call_entry_id(hass): + """Test service call with entity specified by entity registry ID.""" + registry = ent_reg.async_get(hass) + calls = async_mock_service(hass, "test_domain", "test_service") + entry = registry.async_get_or_create( + "hello", "hue", "1234", suggested_object_id="world" + ) + + assert entry.entity_id == "hello.world" + + config = { + "service": "test_domain.test_service", + "target": {"entity_id": entry.id}, + } + + await service.async_call_from_config(hass, config) + await hass.async_block_till_done() + + assert dict(calls[0].data) == {"entity_id": ["hello.world"]} + + async def test_extract_entity_ids(hass): """Test extract_entity_ids method.""" hass.states.async_set("light.Bowl", STATE_ON)