Add filters to climate and light service descriptions (#86162)

* Add filters to climate and light service descriptions

* Allow specifying enums directly

* Update service descriptions

* Adjust test

* Cache entity features

* Lint

* Improve error handling, add list of known base components

* Don't allow specifying an entity feature as int
This commit is contained in:
Erik Montnemery 2023-03-16 15:59:51 +01:00 committed by GitHub
parent c81a38effb
commit 9384ec18f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 467 additions and 14 deletions

View File

@ -25,7 +25,7 @@ rules:
comments:
level: error
require-starting-space: true
min-spaces-from-content: 2
min-spaces-from-content: 1
comments-indentation:
level: error
document-end:

View File

@ -6,6 +6,8 @@ set_aux_heat:
target:
entity:
domain: climate
supported_features:
- climate.ClimateEntityFeature.AUX_HEAT
fields:
aux_heat:
name: Auxiliary heating
@ -20,6 +22,8 @@ set_preset_mode:
target:
entity:
domain: climate
supported_features:
- climate.ClimateEntityFeature.PRESET_MODE
fields:
preset_mode:
name: Preset mode
@ -35,10 +39,16 @@ set_temperature:
target:
entity:
domain: climate
supported_features:
- climate.ClimateEntityFeature.TARGET_TEMPERATURE
- climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
fields:
temperature:
name: Temperature
description: New target temperature for HVAC.
filter:
supported_features:
- climate.ClimateEntityFeature.TARGET_TEMPERATURE
selector:
number:
min: 0
@ -48,6 +58,9 @@ set_temperature:
target_temp_high:
name: Target temperature high
description: New target high temperature for HVAC.
filter:
supported_features:
- climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
advanced: true
selector:
number:
@ -58,6 +71,9 @@ set_temperature:
target_temp_low:
name: Target temperature low
description: New target low temperature for HVAC.
filter:
supported_features:
- climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
advanced: true
selector:
number:
@ -92,6 +108,8 @@ set_humidity:
target:
entity:
domain: climate
supported_features:
- climate.ClimateEntityFeature.TARGET_HUMIDITY
fields:
humidity:
name: Humidity
@ -109,6 +127,8 @@ set_fan_mode:
target:
entity:
domain: climate
supported_features:
- climate.ClimateEntityFeature.FAN_MODE
fields:
fan_mode:
name: Fan mode
@ -152,6 +172,8 @@ set_swing_mode:
target:
entity:
domain: climate
supported_features:
- climate.ClimateEntityFeature.SWING_MODE
fields:
swing_mode:
name: Swing mode

View File

@ -12,6 +12,9 @@ turn_on:
transition:
name: Transition
description: Duration it takes to get to next state.
filter:
supported_features:
- light.LightEntityFeature.TRANSITION
selector:
number:
min: 0
@ -20,11 +23,27 @@ turn_on:
rgb_color:
name: Color
description: The color for the light (based on RGB - red, green, blue).
filter:
attribute:
supported_color_modes:
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
selector:
color_rgb:
rgbw_color:
name: RGBW-color
description: A list containing four integers between 0 and 255 representing the RGBW (red, green, blue, white) color for the light.
filter:
attribute:
supported_color_modes:
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
advanced: true
example: "[255, 100, 100, 50]"
selector:
@ -32,6 +51,14 @@ turn_on:
rgbww_color:
name: RGBWW-color
description: A list containing five integers between 0 and 255 representing the RGBWW (red, green, blue, cold white, warm white) color for the light.
filter:
attribute:
supported_color_modes:
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
advanced: true
example: "[255, 100, 100, 50, 70]"
selector:
@ -39,6 +66,14 @@ turn_on:
color_name:
name: Color name
description: A human readable color name.
filter:
attribute:
supported_color_modes:
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
advanced: true
selector:
select:
@ -195,6 +230,14 @@ turn_on:
hs_color:
name: Hue/Sat color
description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100.
filter:
attribute:
supported_color_modes:
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
advanced: true
example: "[300, 70]"
selector:
@ -202,6 +245,14 @@ turn_on:
xy_color:
name: XY-color
description: Color for the light in XY-format.
filter:
attribute:
supported_color_modes:
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
advanced: true
example: "[0.52, 0.43]"
selector:
@ -209,6 +260,15 @@ turn_on:
color_temp:
name: Color temperature
description: Color temperature for the light in mireds.
filter:
attribute:
supported_color_modes:
- light.ColorMode.COLOR_TEMP
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
selector:
color_temp:
min_mireds: 153
@ -216,6 +276,15 @@ turn_on:
kelvin:
name: Color temperature (Kelvin)
description: Color temperature for the light in Kelvin.
filter:
attribute:
supported_color_modes:
- light.ColorMode.COLOR_TEMP
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
advanced: true
selector:
number:
@ -228,6 +297,16 @@ turn_on:
description: Number indicating brightness, where 0 turns the light
off, 1 is the minimum brightness and 255 is the maximum brightness
supported by the light.
filter:
attribute:
supported_color_modes:
- light.ColorMode.BRIGHTNESS
- light.ColorMode.COLOR_TEMP
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
advanced: true
selector:
number:
@ -238,6 +317,16 @@ turn_on:
description: Number indicating percentage of full brightness, where 0
turns the light off, 1 is the minimum brightness and 100 is the maximum
brightness supported by the light.
filter:
attribute:
supported_color_modes:
- light.ColorMode.BRIGHTNESS
- light.ColorMode.COLOR_TEMP
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
selector:
number:
min: 0
@ -246,6 +335,16 @@ turn_on:
brightness_step:
name: Brightness step value
description: Change brightness by an amount.
filter:
attribute:
supported_color_modes:
- light.ColorMode.BRIGHTNESS
- light.ColorMode.COLOR_TEMP
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
advanced: true
selector:
number:
@ -254,6 +353,16 @@ turn_on:
brightness_step_pct:
name: Brightness step
description: Change brightness by a percentage.
filter:
attribute:
supported_color_modes:
- light.ColorMode.BRIGHTNESS
- light.ColorMode.COLOR_TEMP
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
selector:
number:
min: -100
@ -265,6 +374,10 @@ turn_on:
Set the light to white mode and change its brightness, where 0 turns
the light off, 1 is the minimum brightness and 255 is the maximum
brightness supported by the light.
filter:
attribute:
supported_color_modes:
- light.ColorMode.WHITE
advanced: true
selector:
number:
@ -280,6 +393,9 @@ turn_on:
flash:
name: Flash
description: If the light should flash.
filter:
supported_features:
- light.LightEntityFeature.FLASH
advanced: true
selector:
select:
@ -291,6 +407,9 @@ turn_on:
effect:
name: Effect
description: Light effect.
filter:
supported_features:
- light.LightEntityFeature.EFFECT
selector:
text:
@ -304,6 +423,9 @@ turn_off:
transition:
name: Transition
description: Duration it takes to get to next state.
filter:
supported_features:
- light.LightEntityFeature.TRANSITION
selector:
number:
min: 0
@ -312,6 +434,9 @@ turn_off:
flash:
name: Flash
description: If the light should flash.
filter:
supported_features:
- light.LightEntityFeature.FLASH
advanced: true
selector:
select:
@ -333,6 +458,9 @@ toggle:
transition:
name: Transition
description: Duration it takes to get to next state.
filter:
supported_features:
- light.LightEntityFeature.TRANSITION
selector:
number:
min: 0
@ -341,6 +469,14 @@ toggle:
rgb_color:
name: RGB-color
description: Color for the light in RGB-format.
filter:
attribute:
supported_color_modes:
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
advanced: true
example: "[255, 100, 100]"
selector:
@ -348,6 +484,14 @@ toggle:
color_name:
name: Color name
description: A human readable color name.
filter:
attribute:
supported_color_modes:
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
advanced: true
selector:
select:
@ -504,6 +648,14 @@ toggle:
hs_color:
name: Hue/Sat color
description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100.
filter:
attribute:
supported_color_modes:
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
advanced: true
example: "[300, 70]"
selector:
@ -511,6 +663,14 @@ toggle:
xy_color:
name: XY-color
description: Color for the light in XY-format.
filter:
attribute:
supported_color_modes:
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
advanced: true
example: "[0.52, 0.43]"
selector:
@ -518,12 +678,30 @@ toggle:
color_temp:
name: Color temperature (mireds)
description: Color temperature for the light in mireds.
filter:
attribute:
supported_color_modes:
- light.ColorMode.COLOR_TEMP
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
advanced: true
selector:
color_temp:
kelvin:
name: Color temperature (Kelvin)
description: Color temperature for the light in Kelvin.
filter:
attribute:
supported_color_modes:
- light.ColorMode.COLOR_TEMP
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
advanced: true
selector:
number:
@ -536,6 +714,16 @@ toggle:
description: Number indicating brightness, where 0 turns the light
off, 1 is the minimum brightness and 255 is the maximum brightness
supported by the light.
filter:
attribute:
supported_color_modes:
- light.ColorMode.BRIGHTNESS
- light.ColorMode.COLOR_TEMP
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
advanced: true
selector:
number:
@ -546,6 +734,16 @@ toggle:
description: Number indicating percentage of full brightness, where 0
turns the light off, 1 is the minimum brightness and 100 is the maximum
brightness supported by the light.
filter:
attribute:
supported_color_modes:
- light.ColorMode.BRIGHTNESS
- light.ColorMode.COLOR_TEMP
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
selector:
number:
min: 0
@ -561,6 +759,9 @@ toggle:
flash:
name: Flash
description: If the light should flash.
filter:
supported_features:
- light.LightEntityFeature.FLASH
advanced: true
selector:
select:
@ -572,5 +773,8 @@ toggle:
effect:
name: Effect
description: Light effect.
filter:
supported_features:
- light.LightEntityFeature.EFFECT
selector:
text:

View File

@ -2,6 +2,8 @@
from __future__ import annotations
from collections.abc import Callable, Mapping, Sequence
from enum import IntFlag
from functools import cache
from typing import Any, Generic, Literal, TypedDict, TypeVar, cast
from uuid import UUID
@ -79,6 +81,69 @@ class Selector(Generic[_T]):
return {"selector": {self.selector_type: self.config}}
@cache
def _entity_features() -> dict[str, type[IntFlag]]:
"""Return a cached lookup of entity feature enums."""
# pylint: disable=import-outside-toplevel
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntityFeature,
)
from homeassistant.components.calendar import CalendarEntityFeature
from homeassistant.components.camera import CameraEntityFeature
from homeassistant.components.climate import ClimateEntityFeature
from homeassistant.components.cover import CoverEntityFeature
from homeassistant.components.fan import FanEntityFeature
from homeassistant.components.humidifier import HumidifierEntityFeature
from homeassistant.components.light import LightEntityFeature
from homeassistant.components.lock import LockEntityFeature
from homeassistant.components.media_player import MediaPlayerEntityFeature
from homeassistant.components.remote import RemoteEntityFeature
from homeassistant.components.siren import SirenEntityFeature
from homeassistant.components.update import UpdateEntityFeature
from homeassistant.components.vacuum import VacuumEntityFeature
from homeassistant.components.water_heater import WaterHeaterEntityFeature
return {
"AlarmControlPanelEntityFeature": AlarmControlPanelEntityFeature,
"CalendarEntityFeature": CalendarEntityFeature,
"CameraEntityFeature": CameraEntityFeature,
"ClimateEntityFeature": ClimateEntityFeature,
"CoverEntityFeature": CoverEntityFeature,
"FanEntityFeature": FanEntityFeature,
"HumidifierEntityFeature": HumidifierEntityFeature,
"LightEntityFeature": LightEntityFeature,
"LockEntityFeature": LockEntityFeature,
"MediaPlayerEntityFeature": MediaPlayerEntityFeature,
"RemoteEntityFeature": RemoteEntityFeature,
"SirenEntityFeature": SirenEntityFeature,
"UpdateEntityFeature": UpdateEntityFeature,
"VacuumEntityFeature": VacuumEntityFeature,
"WaterHeaterEntityFeature": WaterHeaterEntityFeature,
}
def _validate_supported_feature(supported_feature: int | str) -> int:
"""Validate a supported feature and resolve an enum string to its value."""
if isinstance(supported_feature, int):
return supported_feature
known_entity_features = _entity_features()
try:
_, enum, feature = supported_feature.split(".", 2)
except ValueError as exc:
raise vol.Invalid(
f"Invalid supported feature '{supported_feature}', expected "
"<domain>.<enum>.<member>"
) from exc
try:
return cast(int, getattr(known_entity_features[enum], feature).value)
except (AttributeError, KeyError) as exc:
raise vol.Invalid(f"Unknown supported feature '{supported_feature}'") from exc
ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema(
{
# Integration that provided the entity
@ -87,6 +152,8 @@ ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema(
vol.Optional("domain"): vol.All(cv.ensure_list, [str]),
# Device class of the entity
vol.Optional("device_class"): vol.All(cv.ensure_list, [str]),
# Features supported by the entity
vol.Optional("supported_features"): [vol.All(str, _validate_supported_feature)],
}
)
@ -97,6 +164,7 @@ class EntityFilterSelectorConfig(TypedDict, total=False):
integration: str
domain: str | list[str]
device_class: str | list[str]
supported_features: list[str]
DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema(

View File

@ -4,9 +4,11 @@ from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable, Iterable
import dataclasses
from functools import partial, wraps
from enum import Enum
from functools import cache, partial, wraps
import logging
from typing import TYPE_CHECKING, Any, TypedDict, TypeGuard, TypeVar
from types import ModuleType
from typing import TYPE_CHECKING, Any, TypedDict, TypeGuard, TypeVar, cast
import voluptuous as vol
@ -42,6 +44,7 @@ from . import (
entity_registry,
template,
)
from .selector import TargetSelector
from .typing import ConfigType, TemplateVarsType
if TYPE_CHECKING:
@ -58,6 +61,112 @@ _LOGGER = logging.getLogger(__name__)
SERVICE_DESCRIPTION_CACHE = "service_description_cache"
@cache
def _base_components() -> dict[str, ModuleType]:
"""Return a cached lookup of base components."""
# pylint: disable=import-outside-toplevel
from homeassistant.components import (
alarm_control_panel,
calendar,
camera,
climate,
cover,
fan,
humidifier,
light,
lock,
media_player,
remote,
siren,
update,
vacuum,
water_heater,
)
return {
"alarm_control_panel": alarm_control_panel,
"calendar": calendar,
"camera": camera,
"climate": climate,
"cover": cover,
"fan": fan,
"humidifier": humidifier,
"light": light,
"lock": lock,
"media_player": media_player,
"remote": remote,
"siren": siren,
"update": update,
"vacuum": vacuum,
"water_heater": water_heater,
}
def _validate_option_or_feature(option_or_feature: str, label: str) -> Any:
"""Validate attribute option or supported feature."""
try:
domain, enum, option = option_or_feature.split(".", 2)
except ValueError as exc:
raise vol.Invalid(
f"Invalid {label} '{option_or_feature}', expected "
"<domain>.<enum>.<member>"
) from exc
base_components = _base_components()
if not (base_component := base_components.get(domain)):
raise vol.Invalid(f"Unknown base component '{domain}'")
try:
attribute_enum = getattr(base_component, enum)
except AttributeError as exc:
raise vol.Invalid(f"Unknown {label} enum '{domain}.{enum}'") from exc
if not issubclass(attribute_enum, Enum):
raise vol.Invalid(f"Expected {label} '{domain}.{enum}' to be an enum")
try:
return getattr(attribute_enum, option).value
except AttributeError as exc:
raise vol.Invalid(f"Unknown {label} '{enum}.{option}'") from exc
def validate_attribute_option(attribute_option: str) -> Any:
"""Validate attribute option."""
return _validate_option_or_feature(attribute_option, "attribute option")
def validate_supported_feature(supported_feature: str) -> Any:
"""Validate supported feature."""
return _validate_option_or_feature(supported_feature, "supported feature")
# Basic schemas which translate attribute and supported feature enum names
# to their values. Full validation is done by hassfest.services
_FIELD_SCHEMA = vol.Schema(
{
vol.Optional("filter"): {
vol.Optional("attribute"): {
vol.Required(str): [vol.All(str, validate_attribute_option)],
},
vol.Optional("supported_features"): [
vol.All(str, validate_supported_feature)
],
},
},
extra=vol.ALLOW_EXTRA,
)
_SERVICE_SCHEMA = vol.Schema(
{
vol.Optional("target"): vol.Any(TargetSelector.CONFIG_SCHEMA, None),
vol.Optional("fields"): vol.Schema({str: _FIELD_SCHEMA}),
},
extra=vol.ALLOW_EXTRA,
)
_SERVICES_SCHEMA = vol.Schema({cv.slug: _SERVICE_SCHEMA})
class ServiceParams(TypedDict):
"""Type for service call parameters."""
@ -421,13 +530,16 @@ async def async_extract_config_entry_ids(
def _load_services_file(hass: HomeAssistant, integration: Integration) -> JSON_TYPE:
"""Load services file for an integration."""
try:
return load_yaml(str(integration.file_path / "services.yaml"))
return cast(
JSON_TYPE,
_SERVICES_SCHEMA(load_yaml(str(integration.file_path / "services.yaml"))),
)
except FileNotFoundError:
_LOGGER.warning(
"Unable to find services.yaml for the %s integration", integration.domain
)
return {}
except HomeAssistantError:
except (HomeAssistantError, vol.Invalid):
_LOGGER.warning(
"Unable to parse services.yaml for the %s integration", integration.domain
)

View File

@ -10,7 +10,7 @@ from voluptuous.humanize import humanize_error
from homeassistant.const import CONF_SELECTOR
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, selector
from homeassistant.helpers import config_validation as cv, selector, service
from homeassistant.util.yaml import load_yaml
from .model import Config, Integration
@ -33,6 +33,14 @@ FIELD_SCHEMA = vol.Schema(
vol.Optional("required"): bool,
vol.Optional("advanced"): bool,
vol.Optional(CONF_SELECTOR): selector.validate_selector,
vol.Optional("filter"): {
vol.Optional("attribute"): {
vol.Required(str): [vol.All(str, service.validate_attribute_option)],
},
vol.Optional("supported_features"): [
vol.All(str, service.validate_supported_feature)
],
},
}
)
@ -40,9 +48,7 @@ SERVICE_SCHEMA = vol.Schema(
{
vol.Required("description"): str,
vol.Optional("name"): str,
vol.Optional("target"): vol.Any(
selector.TargetSelector.CONFIG_SCHEMA, None # pylint: disable=no-member
),
vol.Optional("target"): vol.Any(selector.TargetSelector.CONFIG_SCHEMA, None),
vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}),
}
)

View File

@ -69,10 +69,9 @@ def _test_selector(
# Serialize selector
selector_instance = selector.selector({selector_type: schema})
assert (
selector.selector(selector_instance.serialize()["selector"]).config
== selector_instance.config
)
assert selector_instance.serialize() == {
"selector": {selector_type: selector_instance.config}
}
# Test serialized selector can be dumped to YAML
yaml.dump(selector_instance.serialize())
@ -227,6 +226,29 @@ def test_device_selector_schema(schema, valid_selections, invalid_selections) ->
("light.abc123", "binary_sensor.abc123", FAKE_UUID),
(None,),
),
(
{
"filter": [
{"supported_features": ["light.LightEntityFeature.EFFECT"]},
]
},
("light.abc123", "blah.blah", FAKE_UUID),
(None,),
),
(
{
"filter": [
{
"supported_features": [
"light.LightEntityFeature.EFFECT",
"light.LightEntityFeature.TRANSITION",
]
},
]
},
("light.abc123", "blah.blah", FAKE_UUID),
(None,),
),
),
)
def test_entity_selector_schema(schema, valid_selections, invalid_selections) -> None:
@ -234,6 +256,25 @@ def test_entity_selector_schema(schema, valid_selections, invalid_selections) ->
_test_selector("entity", schema, valid_selections, invalid_selections)
@pytest.mark.parametrize(
"schema",
(
# Feature should be string specifying an enum member, not an int
{"filter": [{"supported_features": [1]}]},
# Invalid feature
{"filter": [{"supported_features": ["blah"]}]},
# Unknown feature enum
{"filter": [{"supported_features": ["blah.FooEntityFeature.blah"]}]},
# Unknown feature enum member
{"filter": [{"supported_features": ["light.LightEntityFeature.blah"]}]},
),
)
def test_entity_selector_schema_error(schema) -> None:
"""Test number selector."""
with pytest.raises(vol.Invalid):
selector.validate_selector({"entity": schema})
@pytest.mark.parametrize(
("schema", "valid_selections", "invalid_selections"),
(
@ -359,7 +400,7 @@ def test_addon_selector_schema(schema, valid_selections, invalid_selections) ->
@pytest.mark.parametrize(
("schema", "valid_selections", "invalid_selections"),
(({}, (1, "one", None), ()),), # Everything can be coarced to bool
(({}, (1, "one", None), ()),), # Everything can be coerced to bool
)
def test_boolean_selector_schema(schema, valid_selections, invalid_selections) -> None:
"""Test boolean selector."""