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
parent
e996142592
commit
b325c112b4
|
@ -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)
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
}
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
}
|
||||
),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue