mirror of https://github.com/home-assistant/core
1467 lines
42 KiB
Python
1467 lines
42 KiB
Python
"""Selectors for Home Assistant."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable, Mapping, Sequence
|
|
from enum import StrEnum
|
|
from functools import cache
|
|
import importlib
|
|
from typing import Any, Literal, Required, TypedDict, cast
|
|
from uuid import UUID
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT
|
|
from homeassistant.core import split_entity_id, valid_entity_id
|
|
from homeassistant.generated.countries import COUNTRIES
|
|
from homeassistant.util import decorator
|
|
from homeassistant.util.yaml import dumper
|
|
|
|
from . import config_validation as cv
|
|
|
|
SELECTORS: decorator.Registry[str, type[Selector]] = decorator.Registry()
|
|
|
|
|
|
def _get_selector_class(config: Any) -> type[Selector]:
|
|
"""Get selector class type."""
|
|
if not isinstance(config, dict):
|
|
raise vol.Invalid("Expected a dictionary")
|
|
|
|
if len(config) != 1:
|
|
raise vol.Invalid(f"Only one type can be specified. Found {', '.join(config)}")
|
|
|
|
selector_type: str = list(config)[0]
|
|
|
|
if (selector_class := SELECTORS.get(selector_type)) is None:
|
|
raise vol.Invalid(f"Unknown selector type {selector_type} found")
|
|
|
|
return selector_class
|
|
|
|
|
|
def selector(config: Any) -> Selector:
|
|
"""Instantiate a selector."""
|
|
selector_class = _get_selector_class(config)
|
|
selector_type = list(config)[0]
|
|
|
|
return selector_class(config[selector_type])
|
|
|
|
|
|
def validate_selector(config: Any) -> dict:
|
|
"""Validate a selector."""
|
|
selector_class = _get_selector_class(config)
|
|
selector_type = list(config)[0]
|
|
|
|
# Selectors can be empty
|
|
if config[selector_type] is None:
|
|
return {selector_type: {}}
|
|
|
|
return {
|
|
selector_type: cast(dict, selector_class.CONFIG_SCHEMA(config[selector_type]))
|
|
}
|
|
|
|
|
|
class Selector[_T: Mapping[str, Any]]:
|
|
"""Base class for selectors."""
|
|
|
|
CONFIG_SCHEMA: Callable
|
|
config: _T
|
|
selector_type: str
|
|
|
|
def __init__(self, config: Mapping[str, Any] | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
# Selectors can be empty
|
|
if config is None:
|
|
config = {}
|
|
|
|
self.config = self.CONFIG_SCHEMA(config)
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
"""Check equality."""
|
|
if not isinstance(other, Selector):
|
|
return NotImplemented
|
|
|
|
return self.selector_type == other.selector_type and self.config == other.config
|
|
|
|
def serialize(self) -> dict[str, dict[str, _T]]:
|
|
"""Serialize Selector for voluptuous_serialize."""
|
|
return {"selector": {self.selector_type: self.config}}
|
|
|
|
|
|
@cache
|
|
def _entity_feature_flag(domain: str, enum_name: str, feature_name: str) -> int:
|
|
"""Return a cached lookup of an entity feature enum.
|
|
|
|
This will import a module from disk and is run from an executor when
|
|
loading the services schema files.
|
|
"""
|
|
module = importlib.import_module(f"homeassistant.components.{domain}")
|
|
enum = getattr(module, enum_name)
|
|
feature = getattr(enum, feature_name)
|
|
return cast(int, feature.value)
|
|
|
|
|
|
def _validate_supported_feature(supported_feature: str) -> int:
|
|
"""Validate a supported feature and resolve an enum string to its value."""
|
|
|
|
try:
|
|
domain, 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 _entity_feature_flag(domain, enum, feature)
|
|
except (ModuleNotFoundError, AttributeError) as exc:
|
|
raise vol.Invalid(f"Unknown supported feature '{supported_feature}'") from exc
|
|
|
|
|
|
def _validate_supported_features(supported_features: int | list[str]) -> int:
|
|
"""Validate a supported feature and resolve an enum string to its value."""
|
|
|
|
if isinstance(supported_features, int):
|
|
return supported_features
|
|
|
|
feature_mask = 0
|
|
|
|
for supported_feature in supported_features:
|
|
feature_mask |= _validate_supported_feature(supported_feature)
|
|
|
|
return feature_mask
|
|
|
|
|
|
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.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(cv.ensure_list, [str], _validate_supported_features)
|
|
],
|
|
}
|
|
)
|
|
|
|
|
|
class EntityFilterSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent a single entity selector config."""
|
|
|
|
integration: str
|
|
domain: str | list[str]
|
|
device_class: str | list[str]
|
|
supported_features: list[str]
|
|
|
|
|
|
DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
# Integration linked to it with a config entry
|
|
vol.Optional("integration"): str,
|
|
# Manufacturer of device
|
|
vol.Optional("manufacturer"): str,
|
|
# Model of device
|
|
vol.Optional("model"): str,
|
|
# Device has to contain entities matching this selector
|
|
vol.Optional("entity"): vol.All(
|
|
cv.ensure_list, [ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA]
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
class DeviceFilterSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent a single device selector config."""
|
|
|
|
integration: str
|
|
manufacturer: str
|
|
model: str
|
|
|
|
|
|
class ActionSelectorConfig(TypedDict):
|
|
"""Class to represent an action selector config."""
|
|
|
|
|
|
@SELECTORS.register("action")
|
|
class ActionSelector(Selector[ActionSelectorConfig]):
|
|
"""Selector of an action sequence (script syntax)."""
|
|
|
|
selector_type = "action"
|
|
|
|
CONFIG_SCHEMA = vol.Schema({})
|
|
|
|
def __init__(self, config: ActionSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> Any:
|
|
"""Validate the passed selection."""
|
|
return data
|
|
|
|
|
|
class AddonSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent an addon selector config."""
|
|
|
|
name: str
|
|
slug: str
|
|
|
|
|
|
@SELECTORS.register("addon")
|
|
class AddonSelector(Selector[AddonSelectorConfig]):
|
|
"""Selector of a add-on."""
|
|
|
|
selector_type = "addon"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Optional("name"): str,
|
|
vol.Optional("slug"): str,
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: AddonSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str:
|
|
"""Validate the passed selection."""
|
|
addon: str = vol.Schema(str)(data)
|
|
return addon
|
|
|
|
|
|
class AreaSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent an area selector config."""
|
|
|
|
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
|
|
device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
|
|
multiple: bool
|
|
|
|
|
|
@SELECTORS.register("area")
|
|
class AreaSelector(Selector[AreaSelectorConfig]):
|
|
"""Selector of a single or list of areas."""
|
|
|
|
selector_type = "area"
|
|
|
|
CONFIG_SCHEMA = vol.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,
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: AreaSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str | list[str]:
|
|
"""Validate the passed selection."""
|
|
if not self.config["multiple"]:
|
|
area_id: str = vol.Schema(str)(data)
|
|
return area_id
|
|
if not isinstance(data, list):
|
|
raise vol.Invalid("Value should be a list")
|
|
return [vol.Schema(str)(val) for val in data]
|
|
|
|
|
|
class AssistPipelineSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent an assist pipeline selector config."""
|
|
|
|
|
|
@SELECTORS.register("assist_pipeline")
|
|
class AssistPipelineSelector(Selector[AssistPipelineSelectorConfig]):
|
|
"""Selector for an assist pipeline."""
|
|
|
|
selector_type = "assist_pipeline"
|
|
|
|
CONFIG_SCHEMA = vol.Schema({})
|
|
|
|
def __init__(self, config: AssistPipelineSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str:
|
|
"""Validate the passed selection."""
|
|
pipeline: str = vol.Schema(str)(data)
|
|
return pipeline
|
|
|
|
|
|
class AttributeSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent an attribute selector config."""
|
|
|
|
entity_id: Required[str]
|
|
hide_attributes: list[str]
|
|
|
|
|
|
@SELECTORS.register("attribute")
|
|
class AttributeSelector(Selector[AttributeSelectorConfig]):
|
|
"""Selector for an entity attribute."""
|
|
|
|
selector_type = "attribute"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required("entity_id"): cv.entity_id,
|
|
# hide_attributes is used to hide attributes in the frontend.
|
|
# A hidden attribute can still be provided manually.
|
|
vol.Optional("hide_attributes"): [str],
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: AttributeSelectorConfig) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str:
|
|
"""Validate the passed selection."""
|
|
attribute: str = vol.Schema(str)(data)
|
|
return attribute
|
|
|
|
|
|
class BackupLocationSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent a backup location selector config."""
|
|
|
|
|
|
@SELECTORS.register("backup_location")
|
|
class BackupLocationSelector(Selector[BackupLocationSelectorConfig]):
|
|
"""Selector of a backup location."""
|
|
|
|
selector_type = "backup_location"
|
|
|
|
CONFIG_SCHEMA = vol.Schema({})
|
|
|
|
def __init__(self, config: BackupLocationSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str:
|
|
"""Validate the passed selection."""
|
|
name: str = vol.Match(r"^(?:\/backup|\w+)$")(data)
|
|
return name
|
|
|
|
|
|
class BooleanSelectorConfig(TypedDict):
|
|
"""Class to represent a boolean selector config."""
|
|
|
|
|
|
@SELECTORS.register("boolean")
|
|
class BooleanSelector(Selector[BooleanSelectorConfig]):
|
|
"""Selector of a boolean value."""
|
|
|
|
selector_type = "boolean"
|
|
|
|
CONFIG_SCHEMA = vol.Schema({})
|
|
|
|
def __init__(self, config: BooleanSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> bool:
|
|
"""Validate the passed selection."""
|
|
value: bool = vol.Coerce(bool)(data)
|
|
return value
|
|
|
|
|
|
class ColorRGBSelectorConfig(TypedDict):
|
|
"""Class to represent a color RGB selector config."""
|
|
|
|
|
|
@SELECTORS.register("color_rgb")
|
|
class ColorRGBSelector(Selector[ColorRGBSelectorConfig]):
|
|
"""Selector of an RGB color value."""
|
|
|
|
selector_type = "color_rgb"
|
|
|
|
CONFIG_SCHEMA = vol.Schema({})
|
|
|
|
def __init__(self, config: ColorRGBSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> list[int]:
|
|
"""Validate the passed selection."""
|
|
value: list[int] = vol.All(list, vol.ExactSequence((cv.byte,) * 3))(data)
|
|
return value
|
|
|
|
|
|
class ColorTempSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent a color temp selector config."""
|
|
|
|
unit: ColorTempSelectorUnit
|
|
min: int
|
|
max: int
|
|
max_mireds: int
|
|
min_mireds: int
|
|
|
|
|
|
class ColorTempSelectorUnit(StrEnum):
|
|
"""Possible units for a color temperature selector."""
|
|
|
|
KELVIN = "kelvin"
|
|
MIRED = "mired"
|
|
|
|
|
|
@SELECTORS.register("color_temp")
|
|
class ColorTempSelector(Selector[ColorTempSelectorConfig]):
|
|
"""Selector of an color temperature."""
|
|
|
|
selector_type = "color_temp"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Optional("unit", default=ColorTempSelectorUnit.MIRED): vol.All(
|
|
vol.Coerce(ColorTempSelectorUnit), lambda val: val.value
|
|
),
|
|
vol.Optional("min"): vol.Coerce(int),
|
|
vol.Optional("max"): vol.Coerce(int),
|
|
vol.Optional("max_mireds"): vol.Coerce(int),
|
|
vol.Optional("min_mireds"): vol.Coerce(int),
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: ColorTempSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> int:
|
|
"""Validate the passed selection."""
|
|
range_min = self.config.get("min")
|
|
range_max = self.config.get("max")
|
|
|
|
if range_min is None:
|
|
range_min = self.config.get("min_mireds")
|
|
|
|
if range_max is None:
|
|
range_max = self.config.get("max_mireds")
|
|
|
|
value: int = vol.All(
|
|
vol.Coerce(float),
|
|
vol.Range(
|
|
min=range_min,
|
|
max=range_max,
|
|
),
|
|
)(data)
|
|
return value
|
|
|
|
|
|
class ConditionSelectorConfig(TypedDict):
|
|
"""Class to represent an condition selector config."""
|
|
|
|
|
|
@SELECTORS.register("condition")
|
|
class ConditionSelector(Selector[ConditionSelectorConfig]):
|
|
"""Selector of an condition sequence (script syntax)."""
|
|
|
|
selector_type = "condition"
|
|
|
|
CONFIG_SCHEMA = vol.Schema({})
|
|
|
|
def __init__(self, config: ConditionSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> Any:
|
|
"""Validate the passed selection."""
|
|
return vol.Schema(cv.CONDITIONS_SCHEMA)(data)
|
|
|
|
|
|
class ConfigEntrySelectorConfig(TypedDict, total=False):
|
|
"""Class to represent a config entry selector config."""
|
|
|
|
integration: str
|
|
|
|
|
|
@SELECTORS.register("config_entry")
|
|
class ConfigEntrySelector(Selector[ConfigEntrySelectorConfig]):
|
|
"""Selector of a config entry."""
|
|
|
|
selector_type = "config_entry"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Optional("integration"): str,
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: ConfigEntrySelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str:
|
|
"""Validate the passed selection."""
|
|
config: str = vol.Schema(str)(data)
|
|
return config
|
|
|
|
|
|
class ConstantSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent a constant selector config."""
|
|
|
|
label: str
|
|
translation_key: str
|
|
value: str | int | bool
|
|
|
|
|
|
@SELECTORS.register("constant")
|
|
class ConstantSelector(Selector[ConstantSelectorConfig]):
|
|
"""Constant selector."""
|
|
|
|
selector_type = "constant"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Optional("label"): str,
|
|
vol.Optional("translation_key"): cv.string,
|
|
vol.Required("value"): vol.Any(str, int, bool),
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: ConstantSelectorConfig) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> Any:
|
|
"""Validate the passed selection."""
|
|
vol.Schema(self.config["value"])(data)
|
|
return self.config["value"]
|
|
|
|
|
|
class QrErrorCorrectionLevel(StrEnum):
|
|
"""Possible error correction levels for QR code selector."""
|
|
|
|
LOW = "low"
|
|
MEDIUM = "medium"
|
|
QUARTILE = "quartile"
|
|
HIGH = "high"
|
|
|
|
|
|
class QrCodeSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent a QR code selector config."""
|
|
|
|
data: str
|
|
scale: int
|
|
error_correction_level: QrErrorCorrectionLevel
|
|
|
|
|
|
@SELECTORS.register("qr_code")
|
|
class QrCodeSelector(Selector[QrCodeSelectorConfig]):
|
|
"""QR code selector."""
|
|
|
|
selector_type = "qr_code"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required("data"): str,
|
|
vol.Optional("scale"): int,
|
|
vol.Optional("error_correction_level"): vol.All(
|
|
vol.Coerce(QrErrorCorrectionLevel), lambda val: val.value
|
|
),
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: QrCodeSelectorConfig) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> Any:
|
|
"""Validate the passed selection."""
|
|
vol.Schema(vol.Any(str, None))(data)
|
|
return self.config["data"]
|
|
|
|
|
|
class ConversationAgentSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent a conversation agent selector config."""
|
|
|
|
language: str
|
|
|
|
|
|
@SELECTORS.register("conversation_agent")
|
|
class ConversationAgentSelector(Selector[ConversationAgentSelectorConfig]):
|
|
"""Selector for a conversation agent."""
|
|
|
|
selector_type = "conversation_agent"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Optional("language"): str,
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: ConversationAgentSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str:
|
|
"""Validate the passed selection."""
|
|
agent: str = vol.Schema(str)(data)
|
|
return agent
|
|
|
|
|
|
class CountrySelectorConfig(TypedDict, total=False):
|
|
"""Class to represent a country selector config."""
|
|
|
|
countries: list[str]
|
|
no_sort: bool
|
|
|
|
|
|
@SELECTORS.register("country")
|
|
class CountrySelector(Selector[CountrySelectorConfig]):
|
|
"""Selector for a single-choice country select."""
|
|
|
|
selector_type = "country"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Optional("countries"): [str],
|
|
vol.Optional("no_sort", default=False): cv.boolean,
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: CountrySelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> Any:
|
|
"""Validate the passed selection."""
|
|
country: str = vol.Schema(str)(data)
|
|
if "countries" in self.config and (
|
|
country not in self.config["countries"] or country not in COUNTRIES
|
|
):
|
|
raise vol.Invalid(f"Value {country} is not a valid option")
|
|
return country
|
|
|
|
|
|
class DateSelectorConfig(TypedDict):
|
|
"""Class to represent a date selector config."""
|
|
|
|
|
|
@SELECTORS.register("date")
|
|
class DateSelector(Selector[DateSelectorConfig]):
|
|
"""Selector of a date."""
|
|
|
|
selector_type = "date"
|
|
|
|
CONFIG_SCHEMA = vol.Schema({})
|
|
|
|
def __init__(self, config: DateSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> Any:
|
|
"""Validate the passed selection."""
|
|
cv.date(data)
|
|
return data
|
|
|
|
|
|
class DateTimeSelectorConfig(TypedDict):
|
|
"""Class to represent a date time selector config."""
|
|
|
|
|
|
@SELECTORS.register("datetime")
|
|
class DateTimeSelector(Selector[DateTimeSelectorConfig]):
|
|
"""Selector of a datetime."""
|
|
|
|
selector_type = "datetime"
|
|
|
|
CONFIG_SCHEMA = vol.Schema({})
|
|
|
|
def __init__(self, config: DateTimeSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> Any:
|
|
"""Validate the passed selection."""
|
|
cv.datetime(data)
|
|
return data
|
|
|
|
|
|
class DeviceSelectorConfig(DeviceFilterSelectorConfig, total=False):
|
|
"""Class to represent a device selector config."""
|
|
|
|
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
|
|
multiple: bool
|
|
filter: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
|
|
|
|
|
|
@SELECTORS.register("device")
|
|
class DeviceSelector(Selector[DeviceSelectorConfig]):
|
|
"""Selector of a single or list of devices."""
|
|
|
|
selector_type = "device"
|
|
|
|
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:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str | list[str]:
|
|
"""Validate the passed selection."""
|
|
if not self.config["multiple"]:
|
|
device_id: str = vol.Schema(str)(data)
|
|
return device_id
|
|
if not isinstance(data, list):
|
|
raise vol.Invalid("Value should be a list")
|
|
return [vol.Schema(str)(val) for val in data]
|
|
|
|
|
|
class DurationSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent a duration selector config."""
|
|
|
|
enable_day: bool
|
|
enable_millisecond: bool
|
|
allow_negative: bool
|
|
|
|
|
|
@SELECTORS.register("duration")
|
|
class DurationSelector(Selector[DurationSelectorConfig]):
|
|
"""Selector for a duration."""
|
|
|
|
selector_type = "duration"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
# Enable day field in frontend. A selection with `days` set is allowed
|
|
# even if `enable_day` is not set
|
|
vol.Optional("enable_day"): cv.boolean,
|
|
# Enable millisecond field in frontend.
|
|
vol.Optional("enable_millisecond"): cv.boolean,
|
|
# Allow negative durations. Will default to False in HA Core 2025.6.0.
|
|
vol.Optional("allow_negative"): cv.boolean,
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: DurationSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> dict[str, float]:
|
|
"""Validate the passed selection."""
|
|
if self.config.get("allow_negative", True):
|
|
cv.time_period_dict(data)
|
|
else:
|
|
cv.positive_time_period_dict(data)
|
|
return cast(dict[str, float], data)
|
|
|
|
|
|
class EntitySelectorConfig(EntityFilterSelectorConfig, total=False):
|
|
"""Class to represent an entity selector config."""
|
|
|
|
exclude_entities: list[str]
|
|
include_entities: list[str]
|
|
multiple: bool
|
|
filter: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
|
|
|
|
|
|
@SELECTORS.register("entity")
|
|
class EntitySelector(Selector[EntitySelectorConfig]):
|
|
"""Selector of a single or list of entities."""
|
|
|
|
selector_type = "entity"
|
|
|
|
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],
|
|
),
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: EntitySelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str | list[str]:
|
|
"""Validate the passed selection."""
|
|
|
|
include_entities = self.config.get("include_entities")
|
|
exclude_entities = self.config.get("exclude_entities")
|
|
|
|
def validate(e_or_u: str) -> str:
|
|
e_or_u = cv.entity_id_or_uuid(e_or_u)
|
|
if not valid_entity_id(e_or_u):
|
|
return e_or_u
|
|
if allowed_domains := cv.ensure_list(self.config.get("domain")):
|
|
domain = split_entity_id(e_or_u)[0]
|
|
if domain not in allowed_domains:
|
|
raise vol.Invalid(
|
|
f"Entity {e_or_u} belongs to domain {domain}, "
|
|
f"expected {allowed_domains}"
|
|
)
|
|
if include_entities:
|
|
vol.In(include_entities)(e_or_u)
|
|
if exclude_entities:
|
|
vol.NotIn(exclude_entities)(e_or_u)
|
|
return e_or_u
|
|
|
|
if not self.config["multiple"]:
|
|
return validate(data)
|
|
if not isinstance(data, list):
|
|
raise vol.Invalid("Value should be a list")
|
|
return cast(list, vol.Schema([validate])(data)) # Output is a list
|
|
|
|
|
|
class FloorSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent an floor selector config."""
|
|
|
|
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
|
|
device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
|
|
multiple: bool
|
|
|
|
|
|
@SELECTORS.register("floor")
|
|
class FloorSelector(Selector[FloorSelectorConfig]):
|
|
"""Selector of a single or list of floors."""
|
|
|
|
selector_type = "floor"
|
|
|
|
CONFIG_SCHEMA = vol.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,
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: FloorSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str | list[str]:
|
|
"""Validate the passed selection."""
|
|
if not self.config["multiple"]:
|
|
floor_id: str = vol.Schema(str)(data)
|
|
return floor_id
|
|
if not isinstance(data, list):
|
|
raise vol.Invalid("Value should be a list")
|
|
return [vol.Schema(str)(val) for val in data]
|
|
|
|
|
|
class IconSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent an icon selector config."""
|
|
|
|
placeholder: str
|
|
|
|
|
|
@SELECTORS.register("icon")
|
|
class IconSelector(Selector[IconSelectorConfig]):
|
|
"""Selector for an icon."""
|
|
|
|
selector_type = "icon"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{vol.Optional("placeholder"): str}
|
|
# Frontend also has a fallbackPath option, this is not used by core
|
|
)
|
|
|
|
def __init__(self, config: IconSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str:
|
|
"""Validate the passed selection."""
|
|
icon: str = vol.Schema(str)(data)
|
|
return icon
|
|
|
|
|
|
class LabelSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent a label selector config."""
|
|
|
|
multiple: bool
|
|
|
|
|
|
@SELECTORS.register("label")
|
|
class LabelSelector(Selector[LabelSelectorConfig]):
|
|
"""Selector of a single or list of labels."""
|
|
|
|
selector_type = "label"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Optional("multiple", default=False): cv.boolean,
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: LabelSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str | list[str]:
|
|
"""Validate the passed selection."""
|
|
if not self.config["multiple"]:
|
|
label_id: str = vol.Schema(str)(data)
|
|
return label_id
|
|
if not isinstance(data, list):
|
|
raise vol.Invalid("Value should be a list")
|
|
return [vol.Schema(str)(val) for val in data]
|
|
|
|
|
|
class LanguageSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent an language selector config."""
|
|
|
|
languages: list[str]
|
|
native_name: bool
|
|
no_sort: bool
|
|
|
|
|
|
@SELECTORS.register("language")
|
|
class LanguageSelector(Selector[LanguageSelectorConfig]):
|
|
"""Selector for an language."""
|
|
|
|
selector_type = "language"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Optional("languages"): [str],
|
|
vol.Optional("native_name", default=False): cv.boolean,
|
|
vol.Optional("no_sort", default=False): cv.boolean,
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: LanguageSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str:
|
|
"""Validate the passed selection."""
|
|
language: str = vol.Schema(str)(data)
|
|
if "languages" in self.config and language not in self.config["languages"]:
|
|
raise vol.Invalid(f"Value {language} is not a valid option")
|
|
return language
|
|
|
|
|
|
class LocationSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent a location selector config."""
|
|
|
|
radius: bool
|
|
icon: str
|
|
|
|
|
|
@SELECTORS.register("location")
|
|
class LocationSelector(Selector[LocationSelectorConfig]):
|
|
"""Selector for a location."""
|
|
|
|
selector_type = "location"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{vol.Optional("radius"): bool, vol.Optional("icon"): str}
|
|
)
|
|
DATA_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required("latitude"): vol.Coerce(float),
|
|
vol.Required("longitude"): vol.Coerce(float),
|
|
vol.Optional("radius"): vol.Coerce(float),
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: LocationSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> dict[str, float]:
|
|
"""Validate the passed selection."""
|
|
location: dict[str, float] = self.DATA_SCHEMA(data)
|
|
return location
|
|
|
|
|
|
class MediaSelectorConfig(TypedDict):
|
|
"""Class to represent a media selector config."""
|
|
|
|
|
|
@SELECTORS.register("media")
|
|
class MediaSelector(Selector[MediaSelectorConfig]):
|
|
"""Selector for media."""
|
|
|
|
selector_type = "media"
|
|
|
|
CONFIG_SCHEMA = vol.Schema({})
|
|
DATA_SCHEMA = vol.Schema(
|
|
{
|
|
# Although marked as optional in frontend, this field is required
|
|
vol.Required("entity_id"): cv.entity_id_or_uuid,
|
|
# Although marked as optional in frontend, this field is required
|
|
vol.Required("media_content_id"): str,
|
|
# Although marked as optional in frontend, this field is required
|
|
vol.Required("media_content_type"): str,
|
|
vol.Remove("metadata"): dict,
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: MediaSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> dict[str, float]:
|
|
"""Validate the passed selection."""
|
|
media: dict[str, float] = self.DATA_SCHEMA(data)
|
|
return media
|
|
|
|
|
|
class NumberSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent a number selector config."""
|
|
|
|
min: float
|
|
max: float
|
|
step: float | Literal["any"]
|
|
unit_of_measurement: str
|
|
mode: NumberSelectorMode
|
|
|
|
|
|
class NumberSelectorMode(StrEnum):
|
|
"""Possible modes for a number selector."""
|
|
|
|
BOX = "box"
|
|
SLIDER = "slider"
|
|
|
|
|
|
def validate_slider(data: Any) -> Any:
|
|
"""Validate configuration."""
|
|
if data["mode"] == "box":
|
|
return data
|
|
|
|
if "min" not in data or "max" not in data:
|
|
raise vol.Invalid("min and max are required in slider mode")
|
|
|
|
return data
|
|
|
|
|
|
@SELECTORS.register("number")
|
|
class NumberSelector(Selector[NumberSelectorConfig]):
|
|
"""Selector of a numeric value."""
|
|
|
|
selector_type = "number"
|
|
|
|
CONFIG_SCHEMA = vol.All(
|
|
vol.Schema(
|
|
{
|
|
vol.Optional("min"): vol.Coerce(float),
|
|
vol.Optional("max"): vol.Coerce(float),
|
|
# Controls slider steps, and up/down keyboard binding for the box
|
|
# user input is not rounded
|
|
vol.Optional("step", default=1): vol.Any(
|
|
"any", vol.All(vol.Coerce(float), vol.Range(min=1e-3))
|
|
),
|
|
vol.Optional(CONF_UNIT_OF_MEASUREMENT): str,
|
|
vol.Optional(CONF_MODE, default=NumberSelectorMode.SLIDER): vol.All(
|
|
vol.Coerce(NumberSelectorMode), lambda val: val.value
|
|
),
|
|
}
|
|
),
|
|
validate_slider,
|
|
)
|
|
|
|
def __init__(self, config: NumberSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> float:
|
|
"""Validate the passed selection."""
|
|
value: float = vol.Coerce(float)(data)
|
|
|
|
if "min" in self.config and value < self.config["min"]:
|
|
raise vol.Invalid(f"Value {value} is too small")
|
|
|
|
if "max" in self.config and value > self.config["max"]:
|
|
raise vol.Invalid(f"Value {value} is too large")
|
|
|
|
return value
|
|
|
|
|
|
class ObjectSelectorConfig(TypedDict):
|
|
"""Class to represent an object selector config."""
|
|
|
|
|
|
@SELECTORS.register("object")
|
|
class ObjectSelector(Selector[ObjectSelectorConfig]):
|
|
"""Selector for an arbitrary object."""
|
|
|
|
selector_type = "object"
|
|
|
|
CONFIG_SCHEMA = vol.Schema({})
|
|
|
|
def __init__(self, config: ObjectSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> Any:
|
|
"""Validate the passed selection."""
|
|
return data
|
|
|
|
|
|
select_option = vol.All(
|
|
dict,
|
|
vol.Schema(
|
|
{
|
|
vol.Required("value"): str,
|
|
vol.Required("label"): str,
|
|
}
|
|
),
|
|
)
|
|
|
|
|
|
class SelectOptionDict(TypedDict):
|
|
"""Class to represent a select option dict."""
|
|
|
|
value: str
|
|
label: str
|
|
|
|
|
|
class SelectSelectorMode(StrEnum):
|
|
"""Possible modes for a number selector."""
|
|
|
|
LIST = "list"
|
|
DROPDOWN = "dropdown"
|
|
|
|
|
|
class SelectSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent a select selector config."""
|
|
|
|
options: Required[Sequence[SelectOptionDict] | Sequence[str]]
|
|
multiple: bool
|
|
custom_value: bool
|
|
mode: SelectSelectorMode
|
|
translation_key: str
|
|
sort: bool
|
|
|
|
|
|
@SELECTORS.register("select")
|
|
class SelectSelector(Selector[SelectSelectorConfig]):
|
|
"""Selector for an single-choice input select."""
|
|
|
|
selector_type = "select"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required("options"): vol.All(vol.Any([str], [select_option])),
|
|
vol.Optional("multiple", default=False): cv.boolean,
|
|
vol.Optional("custom_value", default=False): cv.boolean,
|
|
vol.Optional("mode"): vol.All(
|
|
vol.Coerce(SelectSelectorMode), lambda val: val.value
|
|
),
|
|
vol.Optional("translation_key"): cv.string,
|
|
vol.Optional("sort", default=False): cv.boolean,
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: SelectSelectorConfig) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> Any:
|
|
"""Validate the passed selection."""
|
|
options: Sequence[str] = []
|
|
if config_options := self.config["options"]:
|
|
if isinstance(config_options[0], str):
|
|
options = cast(Sequence[str], config_options)
|
|
else:
|
|
options = [
|
|
option["value"]
|
|
for option in cast(Sequence[SelectOptionDict], config_options)
|
|
]
|
|
|
|
parent_schema: vol.In | vol.Any = vol.In(options)
|
|
if self.config["custom_value"]:
|
|
parent_schema = vol.Any(parent_schema, str)
|
|
|
|
if not self.config["multiple"]:
|
|
return parent_schema(vol.Schema(str)(data))
|
|
if not isinstance(data, list):
|
|
raise vol.Invalid("Value should be a list")
|
|
return [parent_schema(vol.Schema(str)(val)) for val in data]
|
|
|
|
|
|
class TargetSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent a target selector config."""
|
|
|
|
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
|
|
device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
|
|
|
|
|
|
class StateSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent an state selector config."""
|
|
|
|
entity_id: Required[str]
|
|
|
|
|
|
@SELECTORS.register("state")
|
|
class StateSelector(Selector[StateSelectorConfig]):
|
|
"""Selector for an entity state."""
|
|
|
|
selector_type = "state"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required("entity_id"): cv.entity_id,
|
|
# The attribute to filter on, is currently deliberately not
|
|
# configurable/exposed. We are considering separating state
|
|
# selectors into two types: one for state and one for attribute.
|
|
# Limiting the public use, prevents breaking changes in the future.
|
|
# vol.Optional("attribute"): str,
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: StateSelectorConfig) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str:
|
|
"""Validate the passed selection."""
|
|
state: str = vol.Schema(str)(data)
|
|
return state
|
|
|
|
|
|
@SELECTORS.register("target")
|
|
class TargetSelector(Selector[TargetSelectorConfig]):
|
|
"""Selector of a target value (area ID, device ID, entity ID etc).
|
|
|
|
Value should follow cv.TARGET_SERVICE_FIELDS format.
|
|
"""
|
|
|
|
selector_type = "target"
|
|
|
|
CONFIG_SCHEMA = vol.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],
|
|
),
|
|
}
|
|
)
|
|
|
|
TARGET_SELECTION_SCHEMA = vol.Schema(cv.TARGET_SERVICE_FIELDS)
|
|
|
|
def __init__(self, config: TargetSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> dict[str, list[str]]:
|
|
"""Validate the passed selection."""
|
|
target: dict[str, list[str]] = self.TARGET_SELECTION_SCHEMA(data)
|
|
return target
|
|
|
|
|
|
class TemplateSelectorConfig(TypedDict):
|
|
"""Class to represent an template selector config."""
|
|
|
|
|
|
@SELECTORS.register("template")
|
|
class TemplateSelector(Selector[TemplateSelectorConfig]):
|
|
"""Selector for an template."""
|
|
|
|
selector_type = "template"
|
|
|
|
CONFIG_SCHEMA = vol.Schema({})
|
|
|
|
def __init__(self, config: TemplateSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str:
|
|
"""Validate the passed selection."""
|
|
template = cv.template(data)
|
|
return template.template
|
|
|
|
|
|
class TextSelectorConfig(TypedDict, total=False):
|
|
"""Class to represent a text selector config."""
|
|
|
|
multiline: bool
|
|
prefix: str
|
|
suffix: str
|
|
type: TextSelectorType
|
|
autocomplete: str
|
|
multiple: bool
|
|
|
|
|
|
class TextSelectorType(StrEnum):
|
|
"""Enum for text selector types."""
|
|
|
|
COLOR = "color"
|
|
DATE = "date"
|
|
DATETIME_LOCAL = "datetime-local"
|
|
EMAIL = "email"
|
|
MONTH = "month"
|
|
NUMBER = "number"
|
|
PASSWORD = "password"
|
|
SEARCH = "search"
|
|
TEL = "tel"
|
|
TEXT = "text"
|
|
TIME = "time"
|
|
URL = "url"
|
|
WEEK = "week"
|
|
|
|
|
|
@SELECTORS.register("text")
|
|
class TextSelector(Selector[TextSelectorConfig]):
|
|
"""Selector for a multi-line text string."""
|
|
|
|
selector_type = "text"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Optional("multiline", default=False): bool,
|
|
vol.Optional("prefix"): str,
|
|
vol.Optional("suffix"): str,
|
|
# The "type" controls the input field in the browser, the resulting
|
|
# data can be any string so we don't validate it.
|
|
vol.Optional("type"): vol.All(
|
|
vol.Coerce(TextSelectorType), lambda val: val.value
|
|
),
|
|
vol.Optional("autocomplete"): str,
|
|
vol.Optional("multiple", default=False): bool,
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: TextSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str | list[str]:
|
|
"""Validate the passed selection."""
|
|
if not self.config["multiple"]:
|
|
text: str = vol.Schema(str)(data)
|
|
return text
|
|
if not isinstance(data, list):
|
|
raise vol.Invalid("Value should be a list")
|
|
return [vol.Schema(str)(val) for val in data]
|
|
|
|
|
|
class ThemeSelectorConfig(TypedDict):
|
|
"""Class to represent a theme selector config."""
|
|
|
|
|
|
@SELECTORS.register("theme")
|
|
class ThemeSelector(Selector[ThemeSelectorConfig]):
|
|
"""Selector for an theme."""
|
|
|
|
selector_type = "theme"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Optional("include_default", default=False): cv.boolean,
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: ThemeSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str:
|
|
"""Validate the passed selection."""
|
|
theme: str = vol.Schema(str)(data)
|
|
return theme
|
|
|
|
|
|
class TimeSelectorConfig(TypedDict):
|
|
"""Class to represent a time selector config."""
|
|
|
|
|
|
@SELECTORS.register("time")
|
|
class TimeSelector(Selector[TimeSelectorConfig]):
|
|
"""Selector of a time value."""
|
|
|
|
selector_type = "time"
|
|
|
|
CONFIG_SCHEMA = vol.Schema({})
|
|
|
|
def __init__(self, config: TimeSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str:
|
|
"""Validate the passed selection."""
|
|
cv.time(data)
|
|
return cast(str, data)
|
|
|
|
|
|
class TriggerSelectorConfig(TypedDict):
|
|
"""Class to represent an trigger selector config."""
|
|
|
|
|
|
@SELECTORS.register("trigger")
|
|
class TriggerSelector(Selector[TriggerSelectorConfig]):
|
|
"""Selector of a trigger sequence (script syntax)."""
|
|
|
|
selector_type = "trigger"
|
|
|
|
CONFIG_SCHEMA = vol.Schema({})
|
|
|
|
def __init__(self, config: TriggerSelectorConfig | None = None) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> Any:
|
|
"""Validate the passed selection."""
|
|
return vol.Schema(cv.TRIGGER_SCHEMA)(data)
|
|
|
|
|
|
class FileSelectorConfig(TypedDict):
|
|
"""Class to represent a file selector config."""
|
|
|
|
accept: str # required
|
|
|
|
|
|
@SELECTORS.register("file")
|
|
class FileSelector(Selector[FileSelectorConfig]):
|
|
"""Selector of a file."""
|
|
|
|
selector_type = "file"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
# https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept
|
|
vol.Required("accept"): str,
|
|
}
|
|
)
|
|
|
|
def __init__(self, config: FileSelectorConfig) -> None:
|
|
"""Instantiate a selector."""
|
|
super().__init__(config)
|
|
|
|
def __call__(self, data: Any) -> str:
|
|
"""Validate the passed selection."""
|
|
if not isinstance(data, str):
|
|
raise vol.Invalid("Value should be a string")
|
|
|
|
UUID(data)
|
|
|
|
return data
|
|
|
|
|
|
dumper.add_representer(
|
|
Selector,
|
|
lambda dumper, value: dumper.represent_odict(
|
|
dumper, "tag:yaml.org,2002:map", value.serialize()
|
|
),
|
|
)
|