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:
parent
ac70612ec5
commit
e95944bf9f
3 changed files with 170 additions and 31 deletions
|
@ -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],
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
}),
|
||||
|
|
|
@ -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"},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue