Add SelectorType enum and TypedDicts for each selector's data (#68399)

* rebase off current

* rearrange

* Overload selector function

* Update/fix all selector references

* better typing?

* remove extra option

* move things around

* Switch to Sequence type to avoid ignoring mypy error

* Get rid of ...'s

* Improve typing to reduce number of ignores

* Remove all typing ignores

* Make config optional for selectors that don't need a config

* add missing unit prefixes

* Rename TypedDicts

* Update homeassistant/helpers/selector.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* review feedback

* remove peta from integration integration

* Fix min_max

* Revert change to selector function

* Fix logic

* Add typing for selector classes

* Update selector.py

* Fix indent

Co-authored-by: Erik Montnemery <erik@montnemery.com>
pull/69338/merge
Raman Gupta 2022-04-11 03:20:56 -04:00 committed by GitHub
parent e996142592
commit b325c112b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 525 additions and 239 deletions

View File

@ -9,7 +9,6 @@ import voluptuous as vol
from homeassistant.const import (
CONF_NAME,
CONF_SOURCE,
CONF_UNIT_OF_MEASUREMENT,
TIME_DAYS,
TIME_HOURS,
TIME_MINUTES,
@ -31,50 +30,48 @@ from .const import (
)
UNIT_PREFIXES = [
{"value": "none", "label": "none"},
{"value": "n", "label": "n (nano)"},
{"value": "µ", "label": "µ (micro)"},
{"value": "m", "label": "m (milli)"},
{"value": "k", "label": "k (kilo)"},
{"value": "M", "label": "M (mega)"},
{"value": "G", "label": "G (giga)"},
{"value": "T", "label": "T (tera)"},
{"value": "P", "label": "P (peta)"},
selector.SelectOptionDict(value="none", label="none"),
selector.SelectOptionDict(value="n", label="n (nano)"),
selector.SelectOptionDict(value="µ", label="µ (micro)"),
selector.SelectOptionDict(value="m", label="m (milli)"),
selector.SelectOptionDict(value="k", label="k (kilo)"),
selector.SelectOptionDict(value="M", label="M (mega)"),
selector.SelectOptionDict(value="G", label="G (giga)"),
selector.SelectOptionDict(value="T", label="T (tera)"),
selector.SelectOptionDict(value="P", label="P (peta)"),
]
TIME_UNITS = [
{"value": TIME_SECONDS, "label": "Seconds"},
{"value": TIME_MINUTES, "label": "Minutes"},
{"value": TIME_HOURS, "label": "Hours"},
{"value": TIME_DAYS, "label": "Days"},
selector.SelectOptionDict(value=TIME_SECONDS, label="Seconds"),
selector.SelectOptionDict(value=TIME_MINUTES, label="Minutes"),
selector.SelectOptionDict(value=TIME_HOURS, label="Hours"),
selector.SelectOptionDict(value=TIME_DAYS, label="Days"),
]
OPTIONS_SCHEMA = vol.Schema(
{
vol.Required(CONF_ROUND_DIGITS, default=2): selector.selector(
{
"number": {
"min": 0,
"max": 6,
"mode": "box",
CONF_UNIT_OF_MEASUREMENT: "decimals",
}
}
vol.Required(CONF_ROUND_DIGITS, default=2): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0,
max=6,
mode=selector.NumberSelectorMode.BOX,
unit_of_measurement="decimals",
),
),
vol.Required(CONF_TIME_WINDOW): selector.selector({"duration": {}}),
vol.Required(CONF_UNIT_PREFIX, default="none"): selector.selector(
{"select": {"options": UNIT_PREFIXES}}
vol.Required(CONF_TIME_WINDOW): selector.DurationSelector(),
vol.Required(CONF_UNIT_PREFIX, default="none"): selector.SelectSelector(
selector.SelectSelectorConfig(options=UNIT_PREFIXES),
),
vol.Required(CONF_UNIT_TIME, default=TIME_HOURS): selector.selector(
{"select": {"options": TIME_UNITS}}
vol.Required(CONF_UNIT_TIME, default=TIME_HOURS): selector.SelectSelector(
selector.SelectSelectorConfig(options=TIME_UNITS),
),
}
)
CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): selector.selector({"text": {}}),
vol.Required(CONF_SOURCE): selector.selector(
{"entity": {"domain": "sensor"}},
vol.Required(CONF_NAME): selector.TextSelector(),
vol.Required(CONF_SOURCE): selector.EntitySelector(
selector.EntitySelectorConfig(domain="sensor"),
),
}
).extend(OPTIONS_SCHEMA.schema)

View File

@ -33,11 +33,9 @@ def basic_group_options_schema(
return vol.Schema(
{
vol.Required(CONF_ENTITIES): entity_selector_without_own_entities(
handler, {"domain": domain, "multiple": True}
),
vol.Required(CONF_HIDE_MEMBERS, default=False): selector.selector(
{"boolean": {}}
handler, selector.EntitySelectorConfig(domain=domain, multiple=True)
),
vol.Required(CONF_HIDE_MEMBERS, default=False): selector.BooleanSelector(),
}
)
@ -46,13 +44,11 @@ def basic_group_config_schema(domain: str) -> vol.Schema:
"""Generate config schema."""
return vol.Schema(
{
vol.Required("name"): selector.selector({"text": {}}),
vol.Required(CONF_ENTITIES): selector.selector(
{"entity": {"domain": domain, "multiple": True}}
),
vol.Required(CONF_HIDE_MEMBERS, default=False): selector.selector(
{"boolean": {}}
vol.Required("name"): selector.TextSelector(),
vol.Required(CONF_ENTITIES): selector.EntitySelector(
selector.EntitySelectorConfig(domain=domain, multiple=True),
),
vol.Required(CONF_HIDE_MEMBERS, default=False): selector.BooleanSelector(),
}
)
@ -64,14 +60,14 @@ def binary_sensor_options_schema(
"""Generate options schema."""
return basic_group_options_schema("binary_sensor", handler, options).extend(
{
vol.Required(CONF_ALL, default=False): selector.selector({"boolean": {}}),
vol.Required(CONF_ALL, default=False): selector.BooleanSelector(),
}
)
BINARY_SENSOR_CONFIG_SCHEMA = basic_group_config_schema("binary_sensor").extend(
{
vol.Required(CONF_ALL, default=False): selector.selector({"boolean": {}}),
vol.Required(CONF_ALL, default=False): selector.BooleanSelector(),
}
)
@ -86,7 +82,7 @@ def light_switch_options_schema(
{
vol.Required(
CONF_ALL, default=False, description={"advanced": True}
): selector.selector({"boolean": {}}),
): selector.BooleanSelector(),
}
)

View File

@ -9,7 +9,6 @@ import voluptuous as vol
from homeassistant.const import (
CONF_METHOD,
CONF_NAME,
CONF_UNIT_OF_MEASUREMENT,
TIME_DAYS,
TIME_HOURS,
TIME_MINUTES,
@ -34,56 +33,58 @@ from .const import (
)
UNIT_PREFIXES = [
{"value": "none", "label": "none"},
{"value": "k", "label": "k (kilo)"},
{"value": "M", "label": "M (mega)"},
{"value": "G", "label": "G (giga)"},
{"value": "T", "label": "T (tera)"},
selector.SelectOptionDict(value="none", label="none"),
selector.SelectOptionDict(value="k", label="k (kilo)"),
selector.SelectOptionDict(value="M", label="M (mega)"),
selector.SelectOptionDict(value="G", label="G (giga)"),
selector.SelectOptionDict(value="T", label="T (tera)"),
]
TIME_UNITS = [
{"value": TIME_SECONDS, "label": "s (seconds)"},
{"value": TIME_MINUTES, "label": "min (minutes)"},
{"value": TIME_HOURS, "label": "h (hours)"},
{"value": TIME_DAYS, "label": "d (days)"},
selector.SelectOptionDict(value=TIME_SECONDS, label="s (seconds)"),
selector.SelectOptionDict(value=TIME_MINUTES, label="min (minutes)"),
selector.SelectOptionDict(value=TIME_HOURS, label="h (hours)"),
selector.SelectOptionDict(value=TIME_DAYS, label="d (days)"),
]
INTEGRATION_METHODS = [
{"value": METHOD_TRAPEZOIDAL, "label": "Trapezoidal rule"},
{"value": METHOD_LEFT, "label": "Left Riemann sum"},
{"value": METHOD_RIGHT, "label": "Right Riemann sum"},
selector.SelectOptionDict(value=METHOD_TRAPEZOIDAL, label="Trapezoidal rule"),
selector.SelectOptionDict(value=METHOD_LEFT, label="Left Riemann sum"),
selector.SelectOptionDict(value=METHOD_RIGHT, label="Right Riemann sum"),
]
OPTIONS_SCHEMA = vol.Schema(
{
vol.Required(CONF_ROUND_DIGITS, default=2): selector.selector(
{"number": {"min": 0, "max": 6, "mode": "box"}}
vol.Required(CONF_ROUND_DIGITS, default=2): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=6, mode=selector.NumberSelectorMode.BOX
),
),
}
)
CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): selector.selector({"text": {}}),
vol.Required(CONF_SOURCE_SENSOR): selector.selector(
{"entity": {"domain": "sensor"}},
vol.Required(CONF_NAME): selector.TextSelector(),
vol.Required(CONF_SOURCE_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(domain="sensor")
),
vol.Required(CONF_METHOD, default=METHOD_TRAPEZOIDAL): selector.selector(
{"select": {"options": INTEGRATION_METHODS}}
vol.Required(CONF_METHOD, default=METHOD_TRAPEZOIDAL): selector.SelectSelector(
selector.SelectSelectorConfig(options=INTEGRATION_METHODS),
),
vol.Required(CONF_ROUND_DIGITS, default=2): selector.selector(
{
"number": {
"min": 0,
"max": 6,
"mode": "box",
CONF_UNIT_OF_MEASUREMENT: "decimals",
}
}
vol.Required(CONF_ROUND_DIGITS, default=2): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0,
max=6,
mode=selector.NumberSelectorMode.BOX,
unit_of_measurement="decimals",
),
),
vol.Required(CONF_UNIT_PREFIX, default="none"): selector.selector(
{"select": {"options": UNIT_PREFIXES}}
vol.Required(CONF_UNIT_PREFIX, default="none"): selector.SelectSelector(
selector.SelectSelectorConfig(options=UNIT_PREFIXES),
),
vol.Required(CONF_UNIT_TIME, default=TIME_HOURS): selector.selector(
{"select": {"options": TIME_UNITS, "mode": "dropdown"}}
vol.Required(CONF_UNIT_TIME, default=TIME_HOURS): selector.SelectSelector(
selector.SelectSelectorConfig(
options=TIME_UNITS, mode=selector.SelectSelectorMode.DROPDOWN
),
),
}
)

View File

@ -63,10 +63,14 @@ CONF_KNX_LABEL_TUNNELING_TCP_SECURE: Final = "TCP with IP Secure"
CONF_KNX_LABEL_TUNNELING_UDP: Final = "UDP"
CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK: Final = "UDP with route back / NAT mode"
_IA_SELECTOR = selector.selector({"text": {}})
_IP_SELECTOR = selector.selector({"text": {}})
_IA_SELECTOR = selector.TextSelector()
_IP_SELECTOR = selector.TextSelector()
_PORT_SELECTOR = vol.All(
selector.selector({"number": {"min": 1, "max": 65535, "mode": "box"}}),
selector.NumberSelector(
selector.NumberSelectorConfig(
min=1, max=65535, mode=selector.NumberSelectorMode.BOX
),
),
vol.Coerce(int),
)
@ -254,14 +258,18 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
fields = {
vol.Required(CONF_KNX_SECURE_USER_ID, default=2): vol.All(
selector.selector({"number": {"min": 1, "max": 127, "mode": "box"}}),
selector.NumberSelector(
selector.NumberSelectorConfig(
min=1, max=127, mode=selector.NumberSelectorMode.BOX
),
),
vol.Coerce(int),
),
vol.Required(CONF_KNX_SECURE_USER_PASSWORD): selector.selector(
{"text": {"type": "password"}}
vol.Required(CONF_KNX_SECURE_USER_PASSWORD): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD),
),
vol.Required(CONF_KNX_SECURE_DEVICE_AUTHENTICATION): selector.selector(
{"text": {"type": "password"}}
vol.Required(CONF_KNX_SECURE_DEVICE_AUTHENTICATION): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD),
),
}
@ -301,8 +309,8 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
)
fields = {
vol.Required(CONF_KNX_KNXKEY_FILENAME): selector.selector({"text": {}}),
vol.Required(CONF_KNX_KNXKEY_PASSWORD): selector.selector({"text": {}}),
vol.Required(CONF_KNX_KNXKEY_FILENAME): selector.TextSelector(),
vol.Required(CONF_KNX_KNXKEY_PASSWORD): selector.TextSelector(),
}
return self.async_show_form(
@ -405,7 +413,7 @@ class KNXOptionsFlowHandler(OptionsFlow):
vol.Required(
CONF_KNX_INDIVIDUAL_ADDRESS,
default=self.current_config[CONF_KNX_INDIVIDUAL_ADDRESS],
): selector.selector({"text": {}}),
): selector.TextSelector(),
vol.Required(
CONF_KNX_MCAST_GRP,
default=self.current_config.get(CONF_KNX_MCAST_GRP, DEFAULT_MCAST_GRP),
@ -438,7 +446,7 @@ class KNXOptionsFlowHandler(OptionsFlow):
CONF_KNX_DEFAULT_STATE_UPDATER,
),
)
] = selector.selector({"boolean": {}})
] = selector.BooleanSelector()
data_schema[
vol.Required(
CONF_KNX_RATE_LIMIT,
@ -448,14 +456,12 @@ class KNXOptionsFlowHandler(OptionsFlow):
),
)
] = vol.All(
selector.selector(
{
"number": {
"min": 1,
"max": CONF_MAX_RATE_LIMIT,
"mode": "box",
}
}
selector.NumberSelector(
selector.NumberSelectorConfig(
min=0,
max=CONF_MAX_RATE_LIMIT,
mode=selector.NumberSelectorMode.BOX,
),
),
vol.Coerce(int),
)

View File

@ -17,31 +17,33 @@ from homeassistant.helpers.schema_config_entry_flow import (
from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN
_STATISTIC_MEASURES = [
{"value": "min", "label": "Minimum"},
{"value": "max", "label": "Maximum"},
{"value": "mean", "label": "Arithmetic mean"},
{"value": "median", "label": "Median"},
{"value": "last", "label": "Most recently updated"},
selector.SelectOptionDict(value="min", label="Minimum"),
selector.SelectOptionDict(value="max", label="Maximum"),
selector.SelectOptionDict(value="mean", label="Arithmetic mean"),
selector.SelectOptionDict(value="median", label="Median"),
selector.SelectOptionDict(value="last", label="Most recently updated"),
]
OPTIONS_SCHEMA = vol.Schema(
{
vol.Required(CONF_ENTITY_IDS): selector.selector(
{"entity": {"domain": "sensor", "multiple": True}}
vol.Required(CONF_ENTITY_IDS): selector.EntitySelector(
selector.EntitySelectorConfig(domain="sensor", multiple=True),
),
vol.Required(CONF_TYPE): selector.selector(
{"select": {"options": _STATISTIC_MEASURES}}
vol.Required(CONF_TYPE): selector.SelectSelector(
selector.SelectSelectorConfig(options=_STATISTIC_MEASURES),
),
vol.Required(CONF_ROUND_DIGITS, default=2): selector.selector(
{"number": {"min": 0, "max": 6, "mode": "box"}}
vol.Required(CONF_ROUND_DIGITS, default=2): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=6, mode=selector.NumberSelectorMode.BOX
),
),
}
)
CONFIG_SCHEMA = vol.Schema(
{
vol.Required("name"): selector.selector({"text": {}}),
vol.Required("name"): selector.TextSelector(),
}
).extend(OPTIONS_SCHEMA.schema)

View File

@ -9,7 +9,7 @@ from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_ZONE
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.selector import selector
from homeassistant.helpers.selector import EntitySelector, EntitySelectorConfig
from .const import DOMAIN
@ -37,8 +37,8 @@ class OpenMeteoFlowHandler(ConfigFlow, domain=DOMAIN):
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_ZONE): selector(
{"entity": {"domain": ZONE_DOMAIN}}
vol.Required(CONF_ZONE): EntitySelector(
EntitySelectorConfig(domain=ZONE_DOMAIN),
),
}
),

View File

@ -17,25 +17,23 @@ from homeassistant.helpers.schema_config_entry_flow import (
from .const import CONF_TARGET_DOMAIN, DOMAIN
TARGET_DOMAIN_OPTIONS = [
selector.SelectOptionDict(value=Platform.COVER, label="Cover"),
selector.SelectOptionDict(value=Platform.FAN, label="Fan"),
selector.SelectOptionDict(value=Platform.LIGHT, label="Light"),
selector.SelectOptionDict(value=Platform.LOCK, label="Lock"),
selector.SelectOptionDict(value=Platform.SIREN, label="Siren"),
]
CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
"user": SchemaFlowFormStep(
vol.Schema(
{
vol.Required(CONF_ENTITY_ID): selector.selector(
{"entity": {"domain": Platform.SWITCH}}
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
selector.EntitySelectorConfig(domain=Platform.SWITCH),
),
vol.Required(CONF_TARGET_DOMAIN): selector.selector(
{
"select": {
"options": [
{"value": Platform.COVER, "label": "Cover"},
{"value": Platform.FAN, "label": "Fan"},
{"value": Platform.LIGHT, "label": "Light"},
{"value": Platform.LOCK, "label": "Lock"},
{"value": Platform.SIREN, "label": "Siren"},
]
}
}
vol.Required(CONF_TARGET_DOMAIN): selector.SelectSelector(
selector.SelectSelectorConfig(options=TARGET_DOMAIN_OPTIONS),
),
}
)

View File

@ -15,13 +15,16 @@ from homeassistant.const import (
CONF_NAME,
CONF_RADIUS,
CONF_SHOW_ON_MAP,
CONF_UNIT_OF_MEASUREMENT,
LENGTH_KILOMETERS,
)
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.selector import selector
from homeassistant.helpers.selector import (
LocationSelector,
NumberSelector,
NumberSelectorConfig,
)
from .const import CONF_FUEL_TYPES, CONF_STATIONS, DEFAULT_RADIUS, DOMAIN, FUEL_TYPES
@ -154,18 +157,16 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"longitude": self.hass.config.longitude,
},
),
): selector({"location": {}}),
): LocationSelector(),
vol.Required(
CONF_RADIUS, default=user_input.get(CONF_RADIUS, DEFAULT_RADIUS)
): selector(
{
"number": {
"min": 0.1,
"max": 25,
"step": 0.1,
CONF_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS,
}
}
): NumberSelector(
NumberSelectorConfig(
min=0.1,
max=25,
step=0.1,
unit_of_measurement=LENGTH_KILOMETERS,
),
),
}
),

View File

@ -27,19 +27,25 @@ def _validate_mode(data: Any) -> Any:
OPTIONS_SCHEMA = vol.Schema(
{
vol.Required(CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS): selector.selector(
{"number": {"mode": "box"}}
vol.Required(
CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS
): selector.NumberSelector(
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
),
vol.Optional(CONF_LOWER): selector.NumberSelector(
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
),
vol.Optional(CONF_UPPER): selector.NumberSelector(
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
),
vol.Optional(CONF_LOWER): selector.selector({"number": {"mode": "box"}}),
vol.Optional(CONF_UPPER): selector.selector({"number": {"mode": "box"}}),
}
)
CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): selector.selector({"text": {}}),
vol.Required(CONF_ENTITY_ID): selector.selector(
{"entity": {"domain": "sensor"}}
vol.Required(CONF_NAME): selector.TextSelector(),
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
selector.EntitySelectorConfig(domain="sensor")
),
}
).extend(OPTIONS_SCHEMA.schema)

View File

@ -18,14 +18,14 @@ from .const import CONF_AFTER_TIME, CONF_BEFORE_TIME, DOMAIN
OPTIONS_SCHEMA = vol.Schema(
{
vol.Required(CONF_AFTER_TIME): selector.selector({"time": {}}),
vol.Required(CONF_BEFORE_TIME): selector.selector({"time": {}}),
vol.Required(CONF_AFTER_TIME): selector.TimeSelector(),
vol.Required(CONF_BEFORE_TIME): selector.TimeSelector(),
}
)
CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): selector.selector({"text": {}}),
vol.Required(CONF_NAME): selector.TextSelector(),
}
).extend(OPTIONS_SCHEMA.schema)

View File

@ -27,7 +27,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import selector
from homeassistant.helpers.selector import LocationSelector, LocationSelectorConfig
from .const import (
AUTO_MIGRATION_MESSAGE,
@ -78,7 +78,7 @@ def _get_config_schema(
vol.Required(
CONF_LOCATION,
default=default_location,
): selector({"location": {"radius": False}}),
): LocationSelector(LocationSelectorConfig(radius=False)),
},
)

View File

@ -6,7 +6,7 @@ from typing import Any, cast
import voluptuous as vol
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT
from homeassistant.const import CONF_NAME
from homeassistant.helpers import selector
from homeassistant.helpers.schema_config_entry_flow import (
SchemaConfigFlowHandler,
@ -34,15 +34,15 @@ from .const import (
)
METER_TYPES = [
{"value": "none", "label": "No cycle"},
{"value": QUARTER_HOURLY, "label": "Every 15 minutes"},
{"value": HOURLY, "label": "Hourly"},
{"value": DAILY, "label": "Daily"},
{"value": WEEKLY, "label": "Weekly"},
{"value": MONTHLY, "label": "Monthly"},
{"value": BIMONTHLY, "label": "Every two months"},
{"value": QUARTERLY, "label": "Quarterly"},
{"value": YEARLY, "label": "Yearly"},
selector.SelectOptionDict(value="none", label="No cycle"),
selector.SelectOptionDict(value=QUARTER_HOURLY, label="Every 15 minutes"),
selector.SelectOptionDict(value=HOURLY, label="Hourly"),
selector.SelectOptionDict(value=DAILY, label="Daily"),
selector.SelectOptionDict(value=WEEKLY, label="Weekly"),
selector.SelectOptionDict(value=MONTHLY, label="Monthly"),
selector.SelectOptionDict(value=BIMONTHLY, label="Every two months"),
selector.SelectOptionDict(value=QUARTERLY, label="Quarterly"),
selector.SelectOptionDict(value=YEARLY, label="Yearly"),
]
@ -58,40 +58,38 @@ def _validate_config(data: Any) -> Any:
OPTIONS_SCHEMA = vol.Schema(
{
vol.Required(CONF_SOURCE_SENSOR): selector.selector(
{"entity": {"domain": "sensor"}},
vol.Required(CONF_SOURCE_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(domain="sensor"),
),
}
)
CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): selector.selector({"text": {}}),
vol.Required(CONF_SOURCE_SENSOR): selector.selector(
{"entity": {"domain": "sensor"}},
vol.Required(CONF_NAME): selector.TextSelector(),
vol.Required(CONF_SOURCE_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(domain="sensor"),
),
vol.Required(CONF_METER_TYPE): selector.selector(
{"select": {"options": METER_TYPES}}
vol.Required(CONF_METER_TYPE): selector.SelectSelector(
selector.SelectSelectorConfig(options=METER_TYPES),
),
vol.Required(CONF_METER_OFFSET, default=0): selector.selector(
{
"number": {
"min": 0,
"max": 28,
"mode": "box",
CONF_UNIT_OF_MEASUREMENT: "days",
}
}
vol.Required(CONF_METER_OFFSET, default=0): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0,
max=28,
mode=selector.NumberSelectorMode.BOX,
unit_of_measurement="days",
),
),
vol.Required(CONF_TARIFFS, default=[]): selector.selector(
{"select": {"options": [], "custom_value": True, "multiple": True}}
),
vol.Required(CONF_METER_NET_CONSUMPTION, default=False): selector.selector(
{"boolean": {}}
),
vol.Required(CONF_METER_DELTA_VALUES, default=False): selector.selector(
{"boolean": {}}
vol.Required(CONF_TARIFFS, default=[]): selector.SelectSelector(
selector.SelectSelectorConfig(options=[], custom_value=True, multiple=True),
),
vol.Required(
CONF_METER_NET_CONSUMPTION, default=False
): selector.BooleanSelector(),
vol.Required(
CONF_METER_DELTA_VALUES, default=False
): selector.BooleanSelector(),
}
)

View File

@ -371,7 +371,7 @@ def wrapped_entity_config_entry_title(
@callback
def entity_selector_without_own_entities(
handler: SchemaOptionsFlowHandler,
entity_selector_config: dict[str, Any],
entity_selector_config: selector.EntitySelectorConfig,
) -> vol.Schema:
"""Return an entity selector which excludes own entities."""
entity_registry = er.async_get(handler.hass)
@ -381,6 +381,7 @@ def entity_selector_without_own_entities(
)
entity_ids = [ent.entity_id for ent in entities]
return selector.selector(
{"entity": {**entity_selector_config, "exclude_entities": entity_ids}}
)
final_selector_config = entity_selector_config.copy()
final_selector_config["exclude_entities"] = entity_ids
return selector.EntitySelector(final_selector_config)

View File

@ -1,11 +1,12 @@
"""Selectors for Home Assistant."""
from __future__ import annotations
from collections.abc import Callable
from typing import Any, cast
from collections.abc import Callable, Sequence
from typing import Any, TypedDict, cast
import voluptuous as vol
from homeassistant.backports.enum import StrEnum
from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT
from homeassistant.core import split_entity_id, valid_entity_id
from homeassistant.util import decorator
@ -36,11 +37,7 @@ def selector(config: Any) -> Selector:
selector_class = _get_selector_class(config)
selector_type = list(config)[0]
# Selectors can be empty
if config[selector_type] is None:
return selector_class({selector_type: {}})
return selector_class(config)
return selector_class(config[selector_type])
def validate_selector(config: Any) -> dict:
@ -64,9 +61,13 @@ class Selector:
config: Any
selector_type: str
def __init__(self, config: Any) -> None:
def __init__(self, config: Any = None) -> None:
"""Instantiate a selector."""
self.config = self.CONFIG_SCHEMA(config[self.selector_type])
# Selectors can be empty
if config is None:
config = {}
self.config = self.CONFIG_SCHEMA(config)
def serialize(self) -> Any:
"""Serialize Selector for voluptuous_serialize."""
@ -84,6 +85,15 @@ SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA = vol.Schema(
}
)
class SingleEntitySelectorConfig(TypedDict, total=False):
"""Class to represent a single entity selector config."""
integration: str
domain: str
device_class: str
SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA = vol.Schema(
{
# Integration linked to it with a config entry
@ -98,6 +108,19 @@ SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA = vol.Schema(
)
class SingleDeviceSelectorConfig(TypedDict, total=False):
"""Class to represent a single device selector config."""
integration: str
manufacturer: str
model: str
entity: SingleEntitySelectorConfig
class ActionSelectorConfig(TypedDict):
"""Class to represent an action selector config."""
@SELECTORS.register("action")
class ActionSelector(Selector):
"""Selector of an action sequence (script syntax)."""
@ -106,11 +129,22 @@ class ActionSelector(Selector):
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):
"""Selector of a add-on."""
@ -124,12 +158,24 @@ class AddonSelector(Selector):
}
)
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: SingleEntitySelectorConfig
device: SingleDeviceSelectorConfig
multiple: bool
@SELECTORS.register("area")
class AreaSelector(Selector):
"""Selector of a single or list of areas."""
@ -144,6 +190,10 @@ class AreaSelector(Selector):
}
)
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"]:
@ -154,6 +204,12 @@ class AreaSelector(Selector):
return [vol.Schema(str)(val) for val in data]
class AttributeSelectorConfig(TypedDict):
"""Class to represent an attribute selector config."""
entity_id: str
@SELECTORS.register("attribute")
class AttributeSelector(Selector):
"""Selector for an entity attribute."""
@ -162,12 +218,20 @@ class AttributeSelector(Selector):
CONFIG_SCHEMA = vol.Schema({vol.Required("entity_id"): cv.entity_id})
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 BooleanSelectorConfig(TypedDict):
"""Class to represent a boolean selector config."""
@SELECTORS.register("boolean")
class BooleanSelector(Selector):
"""Selector of a boolean value."""
@ -176,12 +240,20 @@ class BooleanSelector(Selector):
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):
"""Selector of an RGB color value."""
@ -190,12 +262,23 @@ class ColorRGBSelector(Selector):
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."""
max_mireds: int
min_mireds: int
@SELECTORS.register("color_temp")
class ColorTempSelector(Selector):
"""Selector of an color temperature."""
@ -209,6 +292,10 @@ class ColorTempSelector(Selector):
}
)
def __init__(self, config: ColorTempSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
super().__init__(config)
def __call__(self, data: Any) -> int:
"""Validate the passed selection."""
value: int = vol.All(
@ -221,6 +308,10 @@ class ColorTempSelector(Selector):
return value
class DateSelectorConfig(TypedDict):
"""Class to represent a date selector config."""
@SELECTORS.register("date")
class DateSelector(Selector):
"""Selector of a date."""
@ -229,12 +320,20 @@ class DateSelector(Selector):
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):
"""Selector of a datetime."""
@ -243,12 +342,26 @@ class DateTimeSelector(Selector):
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(TypedDict, total=False):
"""Class to represent a device selector config."""
integration: str
manufacturer: str
model: str
entity: SingleEntitySelectorConfig
multiple: bool
@SELECTORS.register("device")
class DeviceSelector(Selector):
"""Selector of a single or list of devices."""
@ -259,6 +372,10 @@ class DeviceSelector(Selector):
{vol.Optional("multiple", default=False): cv.boolean}
)
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"]:
@ -269,6 +386,12 @@ class DeviceSelector(Selector):
return [vol.Schema(str)(val) for val in data]
class DurationSelectorConfig(TypedDict, total=False):
"""Class to represent a duration selector config."""
enable_day: bool
@SELECTORS.register("duration")
class DurationSelector(Selector):
"""Selector for a duration."""
@ -283,12 +406,24 @@ class DurationSelector(Selector):
}
)
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."""
cv.time_period_dict(data)
return cast(dict[str, float], data)
class EntitySelectorConfig(SingleEntitySelectorConfig, total=False):
"""Class to represent an entity selector config."""
exclude_entities: list[str]
include_entities: list[str]
multiple: bool
@SELECTORS.register("entity")
class EntitySelector(Selector):
"""Selector of a single or list of entities."""
@ -303,6 +438,10 @@ class EntitySelector(Selector):
}
)
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."""
@ -333,6 +472,12 @@ class EntitySelector(Selector):
return cast(list, vol.Schema([validate])(data)) # Output is a list
class IconSelectorConfig(TypedDict, total=False):
"""Class to represent an icon selector config."""
placeholder: str
@SELECTORS.register("icon")
class IconSelector(Selector):
"""Selector for an icon."""
@ -344,12 +489,23 @@ class IconSelector(Selector):
# 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 LocationSelectorConfig(TypedDict, total=False):
"""Class to represent a location selector config."""
radius: bool
icon: str
@SELECTORS.register("location")
class LocationSelector(Selector):
"""Selector for a location."""
@ -367,12 +523,20 @@ class LocationSelector(Selector):
}
)
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):
"""Selector for media."""
@ -392,12 +556,33 @@ class MediaSelector(Selector):
}
)
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
unit_of_measurement: str
mode: NumberSelectorMode
class NumberSelectorMode(StrEnum):
"""Possible modes for a number selector."""
BOX = "box"
SLIDER = "slider"
def has_min_max_if_slider(data: Any) -> Any:
"""Validate configuration."""
if data["mode"] == "box":
@ -426,12 +611,18 @@ class NumberSelector(Selector):
vol.Coerce(float), vol.Range(min=1e-3)
),
vol.Optional(CONF_UNIT_OF_MEASUREMENT): str,
vol.Optional(CONF_MODE, default="slider"): vol.In(["box", "slider"]),
vol.Optional(CONF_MODE, default=NumberSelectorMode.SLIDER): vol.Coerce(
NumberSelectorMode
),
}
),
has_min_max_if_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)
@ -445,6 +636,10 @@ class NumberSelector(Selector):
return value
class ObjectSelectorConfig(TypedDict):
"""Class to represent an object selector config."""
@SELECTORS.register("object")
class ObjectSelector(Selector):
"""Selector for an arbitrary object."""
@ -453,6 +648,10 @@ class ObjectSelector(Selector):
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
@ -469,9 +668,32 @@ select_option = vol.All(
)
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: Sequence[SelectOptionDict] | Sequence[str] # required
multiple: bool
custom_value: bool
mode: SelectSelectorMode
@SELECTORS.register("select")
class SelectSelector(Selector):
"""Selector for an single or multi-choice input select."""
"""Selector for an single-choice input select."""
selector_type = "select"
@ -480,10 +702,14 @@ class SelectSelector(Selector):
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.In(("list", "dropdown")),
vol.Optional("mode"): vol.Coerce(SelectSelectorMode),
}
)
def __init__(self, config: SelectSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
super().__init__(config)
def __call__(self, data: Any) -> Any:
"""Validate the passed selection."""
options = []
@ -504,41 +730,11 @@ class SelectSelector(Selector):
return [parent_schema(vol.Schema(str)(val)) for val in data]
@SELECTORS.register("text")
class StringSelector(Selector):
"""Selector for a multi-line text string."""
class TargetSelectorConfig(TypedDict, total=False):
"""Class to represent a target selector config."""
selector_type = "text"
STRING_TYPES = [
"number",
"text",
"search",
"tel",
"url",
"email",
"password",
"date",
"month",
"week",
"time",
"datetime-local",
"color",
]
CONFIG_SCHEMA = vol.Schema(
{
vol.Optional("multiline", default=False): bool,
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.In(STRING_TYPES),
}
)
def __call__(self, data: Any) -> str:
"""Validate the passed selection."""
text: str = vol.Schema(str)(data)
return text
entity: SingleEntitySelectorConfig
device: SingleDeviceSelectorConfig
@SELECTORS.register("target")
@ -559,12 +755,72 @@ class TargetSelector(Selector):
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 TextSelectorConfig(TypedDict, total=False):
"""Class to represent a text selector config."""
multiline: bool
suffix: str
type: TextSelectorType
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):
"""Selector for a multi-line text string."""
selector_type = "text"
CONFIG_SCHEMA = vol.Schema(
{
vol.Optional("multiline", default=False): bool,
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.Coerce(TextSelectorType),
}
)
def __init__(self, config: TextSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
super().__init__(config)
def __call__(self, data: Any) -> str:
"""Validate the passed selection."""
text: str = vol.Schema(str)(data)
return text
class ThemeSelectorConfig(TypedDict):
"""Class to represent a theme selector config."""
@SELECTORS.register("theme")
class ThemeSelector(Selector):
"""Selector for an theme."""
@ -573,12 +829,20 @@ class ThemeSelector(Selector):
CONFIG_SCHEMA = vol.Schema({})
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):
"""Selector of a time value."""
@ -587,6 +851,10 @@ class TimeSelector(Selector):
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)

View File

@ -18,15 +18,15 @@ from .const import DOMAIN
OPTIONS_SCHEMA = vol.Schema(
{
vol.Required(CONF_ENTITY_ID): selector.selector(
{"entity": {"domain": "sensor"}}
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
selector.EntitySelectorConfig(domain="sensor")
),
}
)
CONFIG_SCHEMA = vol.Schema(
{
vol.Required("name"): selector.selector({"text": {}}),
vol.Required("name"): selector.TextSelector(),
}
).extend(OPTIONS_SCHEMA.schema)

View File

@ -266,7 +266,13 @@ def test_addon_selector_schema(schema, valid_selections, invalid_selections):
)
def test_boolean_selector_schema(schema, valid_selections, invalid_selections):
"""Test boolean selector."""
_test_selector("boolean", schema, valid_selections, invalid_selections, bool)
_test_selector(
"boolean",
schema,
valid_selections,
invalid_selections,
bool,
)
@pytest.mark.parametrize(
@ -512,7 +518,13 @@ def test_media_selector_schema(schema, valid_selections, invalid_selections):
data.pop("metadata", None)
return data
_test_selector("media", schema, valid_selections, invalid_selections, drop_metadata)
_test_selector(
"media",
schema,
valid_selections,
invalid_selections,
drop_metadata,
)
@pytest.mark.parametrize(