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 intpull/82635/head^2
parent
c81a38effb
commit
9384ec18f8
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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}),
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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."""
|
||||
|
|
Loading…
Reference in New Issue