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: comments:
level: error level: error
require-starting-space: true require-starting-space: true
min-spaces-from-content: 2 min-spaces-from-content: 1
comments-indentation: comments-indentation:
level: error level: error
document-end: document-end:

View File

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

View File

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

View File

@ -2,6 +2,8 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable, Mapping, Sequence 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 typing import Any, Generic, Literal, TypedDict, TypeVar, cast
from uuid import UUID from uuid import UUID
@ -79,6 +81,69 @@ class Selector(Generic[_T]):
return {"selector": {self.selector_type: self.config}} 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( ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema(
{ {
# Integration that provided the entity # 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]), vol.Optional("domain"): vol.All(cv.ensure_list, [str]),
# Device class of the entity # Device class of the entity
vol.Optional("device_class"): vol.All(cv.ensure_list, [str]), 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 integration: str
domain: str | list[str] domain: str | list[str]
device_class: str | list[str] device_class: str | list[str]
supported_features: list[str]
DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema( DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema(

View File

@ -4,9 +4,11 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Awaitable, Callable, Iterable from collections.abc import Awaitable, Callable, Iterable
import dataclasses import dataclasses
from functools import partial, wraps from enum import Enum
from functools import cache, partial, wraps
import logging 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 import voluptuous as vol
@ -42,6 +44,7 @@ from . import (
entity_registry, entity_registry,
template, template,
) )
from .selector import TargetSelector
from .typing import ConfigType, TemplateVarsType from .typing import ConfigType, TemplateVarsType
if TYPE_CHECKING: if TYPE_CHECKING:
@ -58,6 +61,112 @@ _LOGGER = logging.getLogger(__name__)
SERVICE_DESCRIPTION_CACHE = "service_description_cache" 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): class ServiceParams(TypedDict):
"""Type for service call parameters.""" """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: def _load_services_file(hass: HomeAssistant, integration: Integration) -> JSON_TYPE:
"""Load services file for an integration.""" """Load services file for an integration."""
try: 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: except FileNotFoundError:
_LOGGER.warning( _LOGGER.warning(
"Unable to find services.yaml for the %s integration", integration.domain "Unable to find services.yaml for the %s integration", integration.domain
) )
return {} return {}
except HomeAssistantError: except (HomeAssistantError, vol.Invalid):
_LOGGER.warning( _LOGGER.warning(
"Unable to parse services.yaml for the %s integration", integration.domain "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.const import CONF_SELECTOR
from homeassistant.exceptions import HomeAssistantError 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 homeassistant.util.yaml import load_yaml
from .model import Config, Integration from .model import Config, Integration
@ -33,6 +33,14 @@ FIELD_SCHEMA = vol.Schema(
vol.Optional("required"): bool, vol.Optional("required"): bool,
vol.Optional("advanced"): bool, vol.Optional("advanced"): bool,
vol.Optional(CONF_SELECTOR): selector.validate_selector, 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.Required("description"): str,
vol.Optional("name"): str, vol.Optional("name"): str,
vol.Optional("target"): vol.Any( vol.Optional("target"): vol.Any(selector.TargetSelector.CONFIG_SCHEMA, None),
selector.TargetSelector.CONFIG_SCHEMA, None # pylint: disable=no-member
),
vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}), vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}),
} }
) )

View File

@ -69,10 +69,9 @@ def _test_selector(
# Serialize selector # Serialize selector
selector_instance = selector.selector({selector_type: schema}) selector_instance = selector.selector({selector_type: schema})
assert ( assert selector_instance.serialize() == {
selector.selector(selector_instance.serialize()["selector"]).config "selector": {selector_type: selector_instance.config}
== selector_instance.config }
)
# Test serialized selector can be dumped to YAML # Test serialized selector can be dumped to YAML
yaml.dump(selector_instance.serialize()) 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), ("light.abc123", "binary_sensor.abc123", FAKE_UUID),
(None,), (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: 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) _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( @pytest.mark.parametrize(
("schema", "valid_selections", "invalid_selections"), ("schema", "valid_selections", "invalid_selections"),
( (
@ -359,7 +400,7 @@ def test_addon_selector_schema(schema, valid_selections, invalid_selections) ->
@pytest.mark.parametrize( @pytest.mark.parametrize(
("schema", "valid_selections", "invalid_selections"), ("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: def test_boolean_selector_schema(schema, valid_selections, invalid_selections) -> None:
"""Test boolean selector.""" """Test boolean selector."""