From cfd223ba0aa3503cacabd0c78086915a5fce847c Mon Sep 17 00:00:00 2001 From: Artur Pragacz Date: Mon, 4 Aug 2025 01:17:22 +0200 Subject: [PATCH 1/6] Improve validation of service fields --- .../components/websocket_api/commands.py | 7 +-- homeassistant/helpers/config_validation.py | 48 ++++++++++++------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index b63e5e1482068..1e306a4b57d66 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -254,11 +254,6 @@ async def handle_call_service( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Handle call service command.""" - # We do not support templates. - target = msg.get("target") - if template.is_complex(target): - raise vol.Invalid("Templates are not supported here") - try: context = connection.context(msg) response = await hass.services.async_call( @@ -267,7 +262,7 @@ async def handle_call_service( service_data=msg.get("service_data"), blocking=True, context=context, - target=target, + target=msg.get("target"), return_response=msg["return_response"], ) result: dict[str, Context | ServiceResponse] = {"context": context} diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index e3dda5d32f3b1..304aae0217ab2 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -732,7 +732,7 @@ def temperature_unit(value: Any) -> UnitOfTemperature: raise vol.Invalid("invalid temperature unit (expected C or F)") -def template(value: Any | None) -> template_helper.Template: +def template(value: Any) -> template_helper.Template: """Validate a jinja2 template.""" if value is None: raise vol.Invalid("template value is None") @@ -758,7 +758,7 @@ def template(value: Any | None) -> template_helper.Template: return template_value -def dynamic_template(value: Any | None) -> template_helper.Template: +def dynamic_template(value: Any) -> template_helper.Template: """Validate a dynamic (non static) jinja2 template.""" if value is None: raise vol.Invalid("template value is None") @@ -1313,34 +1313,38 @@ def platform_only_config_schema(domain: str) -> Callable[[dict], dict]: } ENTITY_SERVICE_FIELDS: VolDictType = { - # 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, dynamic_template, vol.All(list, template_complex) - ), + vol.Optional(ATTR_ENTITY_ID): comp_entity_ids, vol.Optional(ATTR_DEVICE_ID): vol.Any( - ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)]) + ENTITY_MATCH_NONE, + vol.All(ensure_list, [str]), ), vol.Optional(ATTR_AREA_ID): vol.Any( - ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)]) + ENTITY_MATCH_NONE, + vol.All(ensure_list, [str]), ), vol.Optional(ATTR_FLOOR_ID): vol.Any( - ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)]) + ENTITY_MATCH_NONE, + vol.All(ensure_list, [str]), ), vol.Optional(ATTR_LABEL_ID): vol.Any( - ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)]) + ENTITY_MATCH_NONE, + vol.All(ensure_list, [str]), ), } -TARGET_SERVICE_FIELDS = { - # Same as ENTITY_SERVICE_FIELDS but supports specifying entity by entity registry - # ID. +TARGET_SERVICE_FIELDS: VolDictType = { + # Same as ENTITY_SERVICE_FIELDS but supports specifying entity + # by entity registry ID. + **ENTITY_SERVICE_FIELDS, + vol.Optional(ATTR_ENTITY_ID): comp_entity_ids_or_uuids, +} + +_ENTITY_SERVICE_FIELDS_TEMPLATED: VolDictType = { # 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) + comp_entity_ids, 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)]) @@ -1356,6 +1360,14 @@ def platform_only_config_schema(domain: str) -> Callable[[dict], dict]: ), } +_TARGET_SERVICE_FIELDS_TEMPLATED: VolDictType = { + # Same as ENTITY_SERVICE_FIELDS_TEMPLATED but supports specifying entity + # by entity registry ID. + **_ENTITY_SERVICE_FIELDS_TEMPLATED, + vol.Optional(ATTR_ENTITY_ID): vol.Any( + comp_entity_ids_or_uuids, dynamic_template, vol.All(list, template_complex) + ), +} _HAS_ENTITY_SERVICE_FIELD = has_at_least_one_key(*ENTITY_SERVICE_FIELDS) @@ -1488,7 +1500,9 @@ def _backward_compat_service_schema(value: Any | None) -> Any: template, vol.All(dict, template_complex) ), vol.Optional(CONF_ENTITY_ID): comp_entity_ids, - vol.Optional(CONF_TARGET): vol.Any(TARGET_SERVICE_FIELDS, dynamic_template), + vol.Optional(CONF_TARGET): vol.Any( + _TARGET_SERVICE_FIELDS_TEMPLATED, dynamic_template + ), vol.Optional(CONF_RESPONSE_VARIABLE): str, # The frontend stores data here. Don't use in core. vol.Remove("metadata"): dict, From e3e54c0851fe88c828579a78666ae87514b811ee Mon Sep 17 00:00:00 2001 From: Artur Pragacz Date: Sat, 9 Aug 2025 01:23:55 +0200 Subject: [PATCH 2/6] Keep target selector templated --- homeassistant/helpers/config_validation.py | 4 ++-- homeassistant/helpers/selector.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 304aae0217ab2..760a755ba9255 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1360,7 +1360,7 @@ def platform_only_config_schema(domain: str) -> Callable[[dict], dict]: ), } -_TARGET_SERVICE_FIELDS_TEMPLATED: VolDictType = { +TARGET_SERVICE_FIELDS_TEMPLATED: VolDictType = { # Same as ENTITY_SERVICE_FIELDS_TEMPLATED but supports specifying entity # by entity registry ID. **_ENTITY_SERVICE_FIELDS_TEMPLATED, @@ -1501,7 +1501,7 @@ def _backward_compat_service_schema(value: Any | None) -> Any: ), vol.Optional(CONF_ENTITY_ID): comp_entity_ids, vol.Optional(CONF_TARGET): vol.Any( - _TARGET_SERVICE_FIELDS_TEMPLATED, dynamic_template + TARGET_SERVICE_FIELDS_TEMPLATED, dynamic_template ), vol.Optional(CONF_RESPONSE_VARIABLE): str, # The frontend stores data here. Don't use in core. diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 1003991ccec5e..0976886626c7b 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -1431,7 +1431,7 @@ class TargetSelector(Selector[TargetSelectorConfig]): } ) - TARGET_SELECTION_SCHEMA = vol.Schema(cv.TARGET_SERVICE_FIELDS) + TARGET_SELECTION_SCHEMA = vol.Schema(cv.TARGET_SERVICE_FIELDS_TEMPLATED) def __init__(self, config: TargetSelectorConfig | None = None) -> None: """Instantiate a selector.""" From 62f201c204f4b439a4085c343bc19716e9e470e9 Mon Sep 17 00:00:00 2001 From: Artur Pragacz Date: Thu, 4 Sep 2025 12:22:24 +0200 Subject: [PATCH 3/6] Format --- homeassistant/helpers/config_validation.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 760a755ba9255..919b3869a2255 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1344,7 +1344,9 @@ def platform_only_config_schema(domain: str) -> Callable[[dict], dict]: # 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, dynamic_template, vol.All(list, template_complex) + comp_entity_ids, + 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)]) @@ -1365,7 +1367,9 @@ def platform_only_config_schema(domain: str) -> Callable[[dict], dict]: # by entity registry ID. **_ENTITY_SERVICE_FIELDS_TEMPLATED, vol.Optional(ATTR_ENTITY_ID): vol.Any( - comp_entity_ids_or_uuids, dynamic_template, vol.All(list, template_complex) + comp_entity_ids_or_uuids, + dynamic_template, + vol.All(list, template_complex), ), } From f6397ac387f8554048935fd28d678e0b6472db3d Mon Sep 17 00:00:00 2001 From: Artur Pragacz Date: Thu, 30 Oct 2025 16:41:23 +0100 Subject: [PATCH 4/6] Address feedback --- homeassistant/helpers/config_validation.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 5371db2a13121..79dbc6e2ea9cb 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1345,10 +1345,14 @@ def platform_only_config_schema(domain: str) -> Callable[[dict], dict]: vol.Optional(ATTR_ENTITY_ID): comp_entity_ids_or_uuids, } -_ENTITY_SERVICE_FIELDS_TEMPLATED: VolDictType = { +TARGET_SERVICE_FIELDS_TEMPLATED: VolDictType = { # 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. + # Entity ID can be specified as either a user visible one or by entity registry ID. + # + # The schema supports templates as it is meant to be used in the initial validation + # before templates are automatically rendered by the core logic. vol.Optional(ATTR_ENTITY_ID): vol.Any( comp_entity_ids, dynamic_template, @@ -1366,12 +1370,6 @@ def platform_only_config_schema(domain: str) -> Callable[[dict], dict]: vol.Optional(ATTR_LABEL_ID): vol.Any( ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)]) ), -} - -TARGET_SERVICE_FIELDS_TEMPLATED: VolDictType = { - # Same as ENTITY_SERVICE_FIELDS_TEMPLATED but supports specifying entity - # by entity registry ID. - **_ENTITY_SERVICE_FIELDS_TEMPLATED, vol.Optional(ATTR_ENTITY_ID): vol.Any( comp_entity_ids_or_uuids, dynamic_template, From 2ee10579e3b31f1b130eae56d94e8b93200141d9 Mon Sep 17 00:00:00 2001 From: Artur Pragacz Date: Thu, 30 Oct 2025 17:04:58 +0100 Subject: [PATCH 5/6] Fix duplicate --- homeassistant/helpers/config_validation.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 79dbc6e2ea9cb..f7ac2e1c80027 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1354,7 +1354,7 @@ def platform_only_config_schema(domain: str) -> Callable[[dict], dict]: # The schema supports templates as it is meant to be used in the initial validation # before templates are automatically rendered by the core logic. vol.Optional(ATTR_ENTITY_ID): vol.Any( - comp_entity_ids, + comp_entity_ids_or_uuids, dynamic_template, vol.All(list, template_complex), ), @@ -1370,11 +1370,6 @@ def platform_only_config_schema(domain: str) -> Callable[[dict], dict]: vol.Optional(ATTR_LABEL_ID): vol.Any( ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)]) ), - vol.Optional(ATTR_ENTITY_ID): vol.Any( - comp_entity_ids_or_uuids, - dynamic_template, - vol.All(list, template_complex), - ), } _HAS_ENTITY_SERVICE_FIELD = has_at_least_one_key(*ENTITY_SERVICE_FIELDS) From b4bfdce31f6d0bb065e42c3fe685a025b6841802 Mon Sep 17 00:00:00 2001 From: Artur Pragacz Date: Thu, 30 Oct 2025 18:20:30 +0100 Subject: [PATCH 6/6] Make protected --- homeassistant/helpers/config_validation.py | 4 ++-- homeassistant/helpers/selector.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index f7ac2e1c80027..6a922835dcbeb 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1345,7 +1345,7 @@ def platform_only_config_schema(domain: str) -> Callable[[dict], dict]: vol.Optional(ATTR_ENTITY_ID): comp_entity_ids_or_uuids, } -TARGET_SERVICE_FIELDS_TEMPLATED: VolDictType = { +_TARGET_SERVICE_FIELDS_TEMPLATED: VolDictType = { # 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. @@ -1504,7 +1504,7 @@ def _backward_compat_service_schema(value: Any | None) -> Any: ), vol.Optional(CONF_ENTITY_ID): comp_entity_ids, vol.Optional(CONF_TARGET): vol.Any( - TARGET_SERVICE_FIELDS_TEMPLATED, dynamic_template + _TARGET_SERVICE_FIELDS_TEMPLATED, dynamic_template ), vol.Optional(CONF_RESPONSE_VARIABLE): str, # The frontend stores data here. Don't use in core. diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 2f1c61ec7da29..215a15b351271 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -1463,7 +1463,8 @@ class TargetSelector(Selector[TargetSelectorConfig]): } ) - TARGET_SELECTION_SCHEMA = vol.Schema(cv.TARGET_SERVICE_FIELDS_TEMPLATED) + # We want to transition to not including templates in the target selector. + TARGET_SELECTION_SCHEMA = vol.Schema(cv._TARGET_SERVICE_FIELDS_TEMPLATED) # noqa: SLF001 def __init__(self, config: TargetSelectorConfig | None = None) -> None: """Instantiate a selector."""