Fix entity and device selectors (#148580)

This commit is contained in:
Artur Pragacz 2025-07-15 18:58:42 +02:00 committed by GitHub
parent 8bd51a7fd1
commit 3e0628cec2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 65 additions and 14 deletions

View File

@ -15,9 +15,10 @@ generate_data:
required: false required: false
selector: selector:
entity: entity:
domain: ai_task filter:
supported_features: domain: ai_task
- ai_task.AITaskEntityFeature.GENERATE_DATA supported_features:
- ai_task.AITaskEntityFeature.GENERATE_DATA
structure: structure:
advanced: true advanced: true
required: false required: false

View File

@ -68,9 +68,10 @@ ask_question:
required: true required: true
selector: selector:
entity: entity:
domain: assist_satellite filter:
supported_features: domain: assist_satellite
- assist_satellite.AssistSatelliteEntityFeature.START_CONVERSATION supported_features:
- assist_satellite.AssistSatelliteEntityFeature.START_CONVERSATION
question: question:
required: false required: false
example: "What kind of music would you like to play?" example: "What kind of music would you like to play?"

View File

@ -160,6 +160,22 @@ ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema(
) )
# Legacy entity selector config schema used directly under entity selectors
# is provided for backwards compatibility and remains feature frozen.
# New filtering features should be added under the `filter` key instead.
# https://github.com/home-assistant/frontend/pull/15302
LEGACY_ENTITY_SELECTOR_CONFIG_SCHEMA = vol.Schema(
{
# Integration that provided the entity
vol.Optional("integration"): str,
# Domain the entity belongs to
vol.Optional("domain"): vol.All(cv.ensure_list, [str]),
# Device class of the entity
vol.Optional("device_class"): vol.All(cv.ensure_list, [str]),
}
)
class EntityFilterSelectorConfig(TypedDict, total=False): class EntityFilterSelectorConfig(TypedDict, total=False):
"""Class to represent a single entity selector config.""" """Class to represent a single entity selector config."""
@ -179,10 +195,22 @@ DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema(
vol.Optional("model"): str, vol.Optional("model"): str,
# Model ID of device # Model ID of device
vol.Optional("model_id"): str, vol.Optional("model_id"): str,
# Device has to contain entities matching this selector }
vol.Optional("entity"): vol.All( )
cv.ensure_list, [ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA]
),
# Legacy device selector config schema used directly under device selectors
# is provided for backwards compatibility and remains feature frozen.
# New filtering features should be added under the `filter` key instead.
# https://github.com/home-assistant/frontend/pull/15302
LEGACY_DEVICE_SELECTOR_CONFIG_SCHEMA = vol.Schema(
{
# Integration linked to it with a config entry
vol.Optional("integration"): str,
# Manufacturer of device
vol.Optional("manufacturer"): str,
# Model of device
vol.Optional("model"): str,
} }
) )
@ -714,9 +742,13 @@ class DeviceSelector(Selector[DeviceSelectorConfig]):
selector_type = "device" selector_type = "device"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA.schema LEGACY_DEVICE_SELECTOR_CONFIG_SCHEMA.schema
).extend( ).extend(
{ {
# Device has to contain entities matching this selector
vol.Optional("entity"): vol.All(
cv.ensure_list, [ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA]
),
vol.Optional("multiple", default=False): cv.boolean, vol.Optional("multiple", default=False): cv.boolean,
vol.Optional("filter"): vol.All( vol.Optional("filter"): vol.All(
cv.ensure_list, cv.ensure_list,
@ -794,7 +826,7 @@ class EntitySelector(Selector[EntitySelectorConfig]):
selector_type = "entity" selector_type = "entity"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA.schema LEGACY_ENTITY_SELECTOR_CONFIG_SCHEMA.schema
).extend( ).extend(
{ {
vol.Optional("exclude_entities"): [str], vol.Optional("exclude_entities"): [str],

View File

@ -88,7 +88,6 @@ def _test_selector(
({"integration": "zha"}, ("abc123",), (None,)), ({"integration": "zha"}, ("abc123",), (None,)),
({"manufacturer": "mock-manuf"}, ("abc123",), (None,)), ({"manufacturer": "mock-manuf"}, ("abc123",), (None,)),
({"model": "mock-model"}, ("abc123",), (None,)), ({"model": "mock-model"}, ("abc123",), (None,)),
({"model_id": "mock-model_id"}, ("abc123",), (None,)),
({"manufacturer": "mock-manuf", "model": "mock-model"}, ("abc123",), (None,)), ({"manufacturer": "mock-manuf", "model": "mock-model"}, ("abc123",), (None,)),
( (
{"integration": "zha", "manufacturer": "mock-manuf", "model": "mock-model"}, {"integration": "zha", "manufacturer": "mock-manuf", "model": "mock-model"},
@ -128,6 +127,7 @@ def _test_selector(
"integration": "zha", "integration": "zha",
"manufacturer": "mock-manuf", "manufacturer": "mock-manuf",
"model": "mock-model", "model": "mock-model",
"model_id": "mock-model_id",
} }
}, },
("abc123",), ("abc123",),
@ -140,11 +140,13 @@ def _test_selector(
"integration": "zha", "integration": "zha",
"manufacturer": "mock-manuf", "manufacturer": "mock-manuf",
"model": "mock-model", "model": "mock-model",
"model_id": "mock-model_id",
}, },
{ {
"integration": "matter", "integration": "matter",
"manufacturer": "other-mock-manuf", "manufacturer": "other-mock-manuf",
"model": "other-mock-model", "model": "other-mock-model",
"model_id": "other-mock-model_id",
}, },
] ]
}, },
@ -158,6 +160,19 @@ def test_device_selector_schema(schema, valid_selections, invalid_selections) ->
_test_selector("device", schema, valid_selections, invalid_selections) _test_selector("device", schema, valid_selections, invalid_selections)
@pytest.mark.parametrize(
"schema",
[
# model_id should be used under the filter key
{"model_id": "mock-model_id"},
],
)
def test_device_selector_schema_error(schema) -> None:
"""Test device selector."""
with pytest.raises(vol.Invalid):
selector.validate_selector({"device": schema})
@pytest.mark.parametrize( @pytest.mark.parametrize(
("schema", "valid_selections", "invalid_selections"), ("schema", "valid_selections", "invalid_selections"),
[ [
@ -290,10 +305,12 @@ def test_entity_selector_schema(schema, valid_selections, invalid_selections) ->
{"filter": [{"supported_features": ["light.FooEntityFeature.blah"]}]}, {"filter": [{"supported_features": ["light.FooEntityFeature.blah"]}]},
# Unknown feature enum member # Unknown feature enum member
{"filter": [{"supported_features": ["light.LightEntityFeature.blah"]}]}, {"filter": [{"supported_features": ["light.LightEntityFeature.blah"]}]},
# supported_features should be used under the filter key
{"supported_features": ["light.LightEntityFeature.EFFECT"]},
], ],
) )
def test_entity_selector_schema_error(schema) -> None: def test_entity_selector_schema_error(schema) -> None:
"""Test number selector.""" """Test entity selector."""
with pytest.raises(vol.Invalid): with pytest.raises(vol.Invalid):
selector.validate_selector({"entity": schema}) selector.validate_selector({"entity": schema})