Add filter options to entity and device selectors (#87536)

* Add support for multiple device classes

* Add support for entity filter selector

* Add support for device filter selector

* Apply suggestions

* Fix wrong property name

* Update snapshot

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Paul Bottein 2023-02-27 16:38:18 +01:00 committed by GitHub
parent ac70612ec5
commit e95944bf9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 170 additions and 31 deletions

View file

@ -79,27 +79,27 @@ class Selector(Generic[_T]):
return {"selector": {self.selector_type: self.config}}
SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA = vol.Schema(
ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema(
{
# Integration that provided the entity
vol.Optional("integration"): str,
# Domain the entity belongs to
vol.Optional("domain"): vol.Any(str, [str]),
vol.Optional("domain"): vol.All(cv.ensure_list, [str]),
# Device class of the entity
vol.Optional("device_class"): str,
vol.Optional("device_class"): vol.All(cv.ensure_list, [str]),
}
)
class SingleEntitySelectorConfig(TypedDict, total=False):
class EntityFilterSelectorConfig(TypedDict, total=False):
"""Class to represent a single entity selector config."""
integration: str
domain: str | list[str]
device_class: str
device_class: str | list[str]
SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA = vol.Schema(
DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema(
{
# Integration linked to it with a config entry
vol.Optional("integration"): str,
@ -108,18 +108,21 @@ SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA = vol.Schema(
# Model of device
vol.Optional("model"): str,
# Device has to contain entities matching this selector
vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA,
vol.Optional("entity"): vol.All(
cv.ensure_list, [ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA]
),
}
)
class SingleDeviceSelectorConfig(TypedDict, total=False):
class DeviceFilterSelectorConfig(TypedDict, total=False):
"""Class to represent a single device selector config."""
integration: str
manufacturer: str
model: str
entity: SingleEntitySelectorConfig
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
filter: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
class ActionSelectorConfig(TypedDict):
@ -176,8 +179,8 @@ class AddonSelector(Selector[AddonSelectorConfig]):
class AreaSelectorConfig(TypedDict, total=False):
"""Class to represent an area selector config."""
entity: SingleEntitySelectorConfig
device: SingleDeviceSelectorConfig
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
multiple: bool
@ -189,8 +192,14 @@ class AreaSelector(Selector[AreaSelectorConfig]):
CONFIG_SCHEMA = vol.Schema(
{
vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA,
vol.Optional("device"): SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA,
vol.Optional("entity"): vol.All(
cv.ensure_list,
[ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA],
),
vol.Optional("device"): vol.All(
cv.ensure_list,
[DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA],
),
vol.Optional("multiple", default=False): cv.boolean,
}
)
@ -399,7 +408,7 @@ class DeviceSelectorConfig(TypedDict, total=False):
integration: str
manufacturer: str
model: str
entity: SingleEntitySelectorConfig
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
multiple: bool
@ -409,8 +418,14 @@ class DeviceSelector(Selector[DeviceSelectorConfig]):
selector_type = "device"
CONFIG_SCHEMA = SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA.extend(
{vol.Optional("multiple", default=False): cv.boolean}
CONFIG_SCHEMA = DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA.extend(
{
vol.Optional("multiple", default=False): cv.boolean,
vol.Optional("filter"): vol.All(
cv.ensure_list,
[DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA],
),
},
)
def __init__(self, config: DeviceSelectorConfig | None = None) -> None:
@ -457,7 +472,7 @@ class DurationSelector(Selector[DurationSelectorConfig]):
return cast(dict[str, float], data)
class EntitySelectorConfig(SingleEntitySelectorConfig, total=False):
class EntitySelectorConfig(EntityFilterSelectorConfig, total=False):
"""Class to represent an entity selector config."""
exclude_entities: list[str]
@ -471,11 +486,15 @@ class EntitySelector(Selector[EntitySelectorConfig]):
selector_type = "entity"
CONFIG_SCHEMA = SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA.extend(
{
vol.Optional("exclude_entities"): [str],
vol.Optional("include_entities"): [str],
vol.Optional("multiple", default=False): cv.boolean,
vol.Optional("filter"): vol.All(
cv.ensure_list,
[ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA],
),
}
)
@ -784,8 +803,8 @@ class SelectSelector(Selector[SelectSelectorConfig]):
class TargetSelectorConfig(TypedDict, total=False):
"""Class to represent a target selector config."""
entity: SingleEntitySelectorConfig
device: SingleDeviceSelectorConfig
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
class StateSelectorConfig(TypedDict, total=False):
@ -832,8 +851,14 @@ class TargetSelector(Selector[TargetSelectorConfig]):
CONFIG_SCHEMA = vol.Schema(
{
vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA,
vol.Optional("device"): SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA,
vol.Optional("entity"): vol.All(
cv.ensure_list,
[ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA],
),
vol.Optional("device"): vol.All(
cv.ensure_list,
[DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA],
),
}
)

View file

@ -18,9 +18,13 @@
'description': 'The light(s) to control',
'selector': dict({
'target': OrderedDict({
'entity': OrderedDict({
'domain': 'light',
'entity': list([
OrderedDict({
'domain': list([
'light',
]),
}),
]),
}),
}),
}),
@ -111,9 +115,13 @@
'description': 'The light(s) to control',
'selector': dict({
'target': OrderedDict({
'entity': OrderedDict({
'domain': 'light',
'entity': list([
OrderedDict({
'domain': list([
'light',
]),
}),
]),
}),
}),
}),
@ -191,8 +199,12 @@
'name': 'Motion Sensor',
'selector': dict({
'entity': OrderedDict({
'domain': 'binary_sensor',
'device_class': 'motion',
'domain': list([
'binary_sensor',
]),
'device_class': list([
'motion',
]),
'multiple': False,
}),
}),
@ -201,7 +213,9 @@
'name': 'Light',
'selector': dict({
'entity': OrderedDict({
'domain': 'light',
'domain': list([
'light',
]),
'multiple': False,
}),
}),

View file

@ -92,6 +92,17 @@ def _test_selector(
(None,),
),
({"entity": {"device_class": "motion"}}, ("abc123",), (None,)),
({"entity": {"device_class": ["motion", "temperature"]}}, ("abc123",), (None,)),
(
{
"entity": [
{"domain": "light"},
{"domain": "binary_sensor", "device_class": "motion"},
]
},
("abc123",),
(None,),
),
(
{
"integration": "zha",
@ -107,6 +118,35 @@ def _test_selector(
(["abc123", "def456"],),
("abc123", None, ["abc123", None]),
),
(
{
"filter": {
"integration": "zha",
"manufacturer": "mock-manuf",
"model": "mock-model",
}
},
("abc123",),
(None,),
),
(
{
"filter": [
{
"integration": "zha",
"manufacturer": "mock-manuf",
"model": "mock-model",
},
{
"integration": "matter",
"manufacturer": "other-mock-manuf",
"model": "other-mock-model",
},
]
},
("abc123",),
(None,),
),
),
)
def test_device_selector_schema(schema, valid_selections, invalid_selections) -> None:
@ -126,6 +166,11 @@ def test_device_selector_schema(schema, valid_selections, invalid_selections) ->
(None, "dog.abc123"),
),
({"device_class": "motion"}, ("sensor.abc123", FAKE_UUID), (None, "abc123")),
(
{"device_class": ["motion", "temperature"]},
("sensor.abc123", FAKE_UUID),
(None, "abc123"),
),
(
{"integration": "zha", "domain": "light"},
("light.abc123", FAKE_UUID),
@ -167,6 +212,21 @@ def test_device_selector_schema(schema, valid_selections, invalid_selections) ->
["sensor.abc123", "sensor.ghi789"],
),
),
(
{"filter": {"domain": "light"}},
("light.abc123", FAKE_UUID),
(None,),
),
(
{
"filter": [
{"domain": "light"},
{"domain": "binary_sensor", "device_class": "motion"},
]
},
("light.abc123", "binary_sensor.abc123", FAKE_UUID),
(None,),
),
),
)
def test_entity_selector_schema(schema, valid_selections, invalid_selections) -> None:
@ -196,11 +256,31 @@ def test_entity_selector_schema(schema, valid_selections, invalid_selections) ->
("abc123",),
(None,),
),
(
{
"entity": [
{"domain": "light"},
{"domain": "binary_sensor", "device_class": "motion"},
]
},
("abc123",),
(None,),
),
(
{"device": {"integration": "demo", "model": "mock-model"}},
("abc123",),
(None,),
),
(
{
"device": [
{"integration": "demo", "model": "mock-model"},
{"integration": "other-demo", "model": "other-mock-model"},
]
},
("abc123",),
(None,),
),
(
{
"entity": {"domain": "binary_sensor", "device_class": "motion"},
@ -345,6 +425,16 @@ def test_state_selector_schema(schema, valid_selections, invalid_selections) ->
({"entity": {}}, (), ()),
({"entity": {"domain": "light"}}, (), ()),
({"entity": {"domain": "binary_sensor", "device_class": "motion"}}, (), ()),
(
{
"entity": [
{"domain": "light"},
{"domain": "binary_sensor", "device_class": "motion"},
]
},
(),
(),
),
(
{
"entity": {
@ -357,6 +447,16 @@ def test_state_selector_schema(schema, valid_selections, invalid_selections) ->
(),
),
({"device": {"integration": "demo", "model": "mock-model"}}, (), ()),
(
{
"device": [
{"integration": "demo", "model": "mock-model"},
{"integration": "other-demo", "model": "other-mock-model"},
],
},
(),
(),
),
(
{
"entity": {"domain": "binary_sensor", "device_class": "motion"},