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

View file

@ -33,11 +33,9 @@ def basic_group_options_schema(
return vol.Schema( return vol.Schema(
{ {
vol.Required(CONF_ENTITIES): entity_selector_without_own_entities( vol.Required(CONF_ENTITIES): entity_selector_without_own_entities(
handler, {"domain": domain, "multiple": True} handler, selector.EntitySelectorConfig(domain=domain, multiple=True)
),
vol.Required(CONF_HIDE_MEMBERS, default=False): selector.selector(
{"boolean": {}}
), ),
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.""" """Generate config schema."""
return vol.Schema( return vol.Schema(
{ {
vol.Required("name"): selector.selector({"text": {}}), vol.Required("name"): selector.TextSelector(),
vol.Required(CONF_ENTITIES): selector.selector( vol.Required(CONF_ENTITIES): selector.EntitySelector(
{"entity": {"domain": domain, "multiple": True}} selector.EntitySelectorConfig(domain=domain, multiple=True),
),
vol.Required(CONF_HIDE_MEMBERS, default=False): selector.selector(
{"boolean": {}}
), ),
vol.Required(CONF_HIDE_MEMBERS, default=False): selector.BooleanSelector(),
} }
) )
@ -64,14 +60,14 @@ def binary_sensor_options_schema(
"""Generate options schema.""" """Generate options schema."""
return basic_group_options_schema("binary_sensor", handler, options).extend( 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( 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( vol.Required(
CONF_ALL, default=False, description={"advanced": True} 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 ( from homeassistant.const import (
CONF_METHOD, CONF_METHOD,
CONF_NAME, CONF_NAME,
CONF_UNIT_OF_MEASUREMENT,
TIME_DAYS, TIME_DAYS,
TIME_HOURS, TIME_HOURS,
TIME_MINUTES, TIME_MINUTES,
@ -34,56 +33,58 @@ from .const import (
) )
UNIT_PREFIXES = [ UNIT_PREFIXES = [
{"value": "none", "label": "none"}, selector.SelectOptionDict(value="none", label="none"),
{"value": "k", "label": "k (kilo)"}, selector.SelectOptionDict(value="k", label="k (kilo)"),
{"value": "M", "label": "M (mega)"}, selector.SelectOptionDict(value="M", label="M (mega)"),
{"value": "G", "label": "G (giga)"}, selector.SelectOptionDict(value="G", label="G (giga)"),
{"value": "T", "label": "T (tera)"}, selector.SelectOptionDict(value="T", label="T (tera)"),
] ]
TIME_UNITS = [ TIME_UNITS = [
{"value": TIME_SECONDS, "label": "s (seconds)"}, selector.SelectOptionDict(value=TIME_SECONDS, label="s (seconds)"),
{"value": TIME_MINUTES, "label": "min (minutes)"}, selector.SelectOptionDict(value=TIME_MINUTES, label="min (minutes)"),
{"value": TIME_HOURS, "label": "h (hours)"}, selector.SelectOptionDict(value=TIME_HOURS, label="h (hours)"),
{"value": TIME_DAYS, "label": "d (days)"}, selector.SelectOptionDict(value=TIME_DAYS, label="d (days)"),
] ]
INTEGRATION_METHODS = [ INTEGRATION_METHODS = [
{"value": METHOD_TRAPEZOIDAL, "label": "Trapezoidal rule"}, selector.SelectOptionDict(value=METHOD_TRAPEZOIDAL, label="Trapezoidal rule"),
{"value": METHOD_LEFT, "label": "Left Riemann sum"}, selector.SelectOptionDict(value=METHOD_LEFT, label="Left Riemann sum"),
{"value": METHOD_RIGHT, "label": "Right Riemann sum"}, selector.SelectOptionDict(value=METHOD_RIGHT, label="Right Riemann sum"),
] ]
OPTIONS_SCHEMA = vol.Schema( OPTIONS_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_ROUND_DIGITS, default=2): selector.selector( vol.Required(CONF_ROUND_DIGITS, default=2): selector.NumberSelector(
{"number": {"min": 0, "max": 6, "mode": "box"}} selector.NumberSelectorConfig(
min=0, max=6, mode=selector.NumberSelectorMode.BOX
),
), ),
} }
) )
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_NAME): selector.selector({"text": {}}), vol.Required(CONF_NAME): selector.TextSelector(),
vol.Required(CONF_SOURCE_SENSOR): selector.selector( vol.Required(CONF_SOURCE_SENSOR): selector.EntitySelector(
{"entity": {"domain": "sensor"}}, selector.EntitySelectorConfig(domain="sensor")
), ),
vol.Required(CONF_METHOD, default=METHOD_TRAPEZOIDAL): selector.selector( vol.Required(CONF_METHOD, default=METHOD_TRAPEZOIDAL): selector.SelectSelector(
{"select": {"options": INTEGRATION_METHODS}} selector.SelectSelectorConfig(options=INTEGRATION_METHODS),
), ),
vol.Required(CONF_ROUND_DIGITS, default=2): selector.selector( vol.Required(CONF_ROUND_DIGITS, default=2): selector.NumberSelector(
{ selector.NumberSelectorConfig(
"number": { min=0,
"min": 0, max=6,
"max": 6, mode=selector.NumberSelectorMode.BOX,
"mode": "box", unit_of_measurement="decimals",
CONF_UNIT_OF_MEASUREMENT: "decimals", ),
}
}
), ),
vol.Required(CONF_UNIT_PREFIX, default="none"): selector.selector( vol.Required(CONF_UNIT_PREFIX, default="none"): selector.SelectSelector(
{"select": {"options": UNIT_PREFIXES}} selector.SelectSelectorConfig(options=UNIT_PREFIXES),
), ),
vol.Required(CONF_UNIT_TIME, default=TIME_HOURS): selector.selector( vol.Required(CONF_UNIT_TIME, default=TIME_HOURS): selector.SelectSelector(
{"select": {"options": TIME_UNITS, "mode": "dropdown"}} 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: Final = "UDP"
CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK: Final = "UDP with route back / NAT mode" CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK: Final = "UDP with route back / NAT mode"
_IA_SELECTOR = selector.selector({"text": {}}) _IA_SELECTOR = selector.TextSelector()
_IP_SELECTOR = selector.selector({"text": {}}) _IP_SELECTOR = selector.TextSelector()
_PORT_SELECTOR = vol.All( _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), vol.Coerce(int),
) )
@ -254,14 +258,18 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
fields = { fields = {
vol.Required(CONF_KNX_SECURE_USER_ID, default=2): vol.All( 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.Coerce(int),
), ),
vol.Required(CONF_KNX_SECURE_USER_PASSWORD): selector.selector( vol.Required(CONF_KNX_SECURE_USER_PASSWORD): selector.TextSelector(
{"text": {"type": "password"}} selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD),
), ),
vol.Required(CONF_KNX_SECURE_DEVICE_AUTHENTICATION): selector.selector( vol.Required(CONF_KNX_SECURE_DEVICE_AUTHENTICATION): selector.TextSelector(
{"text": {"type": "password"}} selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD),
), ),
} }
@ -301,8 +309,8 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
) )
fields = { fields = {
vol.Required(CONF_KNX_KNXKEY_FILENAME): selector.selector({"text": {}}), vol.Required(CONF_KNX_KNXKEY_FILENAME): selector.TextSelector(),
vol.Required(CONF_KNX_KNXKEY_PASSWORD): selector.selector({"text": {}}), vol.Required(CONF_KNX_KNXKEY_PASSWORD): selector.TextSelector(),
} }
return self.async_show_form( return self.async_show_form(
@ -405,7 +413,7 @@ class KNXOptionsFlowHandler(OptionsFlow):
vol.Required( vol.Required(
CONF_KNX_INDIVIDUAL_ADDRESS, CONF_KNX_INDIVIDUAL_ADDRESS,
default=self.current_config[CONF_KNX_INDIVIDUAL_ADDRESS], default=self.current_config[CONF_KNX_INDIVIDUAL_ADDRESS],
): selector.selector({"text": {}}), ): selector.TextSelector(),
vol.Required( vol.Required(
CONF_KNX_MCAST_GRP, CONF_KNX_MCAST_GRP,
default=self.current_config.get(CONF_KNX_MCAST_GRP, DEFAULT_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, CONF_KNX_DEFAULT_STATE_UPDATER,
), ),
) )
] = selector.selector({"boolean": {}}) ] = selector.BooleanSelector()
data_schema[ data_schema[
vol.Required( vol.Required(
CONF_KNX_RATE_LIMIT, CONF_KNX_RATE_LIMIT,
@ -448,14 +456,12 @@ class KNXOptionsFlowHandler(OptionsFlow):
), ),
) )
] = vol.All( ] = vol.All(
selector.selector( selector.NumberSelector(
{ selector.NumberSelectorConfig(
"number": { min=0,
"min": 1, max=CONF_MAX_RATE_LIMIT,
"max": CONF_MAX_RATE_LIMIT, mode=selector.NumberSelectorMode.BOX,
"mode": "box", ),
}
}
), ),
vol.Coerce(int), 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 from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN
_STATISTIC_MEASURES = [ _STATISTIC_MEASURES = [
{"value": "min", "label": "Minimum"}, selector.SelectOptionDict(value="min", label="Minimum"),
{"value": "max", "label": "Maximum"}, selector.SelectOptionDict(value="max", label="Maximum"),
{"value": "mean", "label": "Arithmetic mean"}, selector.SelectOptionDict(value="mean", label="Arithmetic mean"),
{"value": "median", "label": "Median"}, selector.SelectOptionDict(value="median", label="Median"),
{"value": "last", "label": "Most recently updated"}, selector.SelectOptionDict(value="last", label="Most recently updated"),
] ]
OPTIONS_SCHEMA = vol.Schema( OPTIONS_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_ENTITY_IDS): selector.selector( vol.Required(CONF_ENTITY_IDS): selector.EntitySelector(
{"entity": {"domain": "sensor", "multiple": True}} selector.EntitySelectorConfig(domain="sensor", multiple=True),
), ),
vol.Required(CONF_TYPE): selector.selector( vol.Required(CONF_TYPE): selector.SelectSelector(
{"select": {"options": _STATISTIC_MEASURES}} selector.SelectSelectorConfig(options=_STATISTIC_MEASURES),
), ),
vol.Required(CONF_ROUND_DIGITS, default=2): selector.selector( vol.Required(CONF_ROUND_DIGITS, default=2): selector.NumberSelector(
{"number": {"min": 0, "max": 6, "mode": "box"}} selector.NumberSelectorConfig(
min=0, max=6, mode=selector.NumberSelectorMode.BOX
),
), ),
} }
) )
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
vol.Required("name"): selector.selector({"text": {}}), vol.Required("name"): selector.TextSelector(),
} }
).extend(OPTIONS_SCHEMA.schema) ).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.config_entries import ConfigFlow
from homeassistant.const import CONF_ZONE from homeassistant.const import CONF_ZONE
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.selector import selector from homeassistant.helpers.selector import EntitySelector, EntitySelectorConfig
from .const import DOMAIN from .const import DOMAIN
@ -37,8 +37,8 @@ class OpenMeteoFlowHandler(ConfigFlow, domain=DOMAIN):
step_id="user", step_id="user",
data_schema=vol.Schema( data_schema=vol.Schema(
{ {
vol.Required(CONF_ZONE): selector( vol.Required(CONF_ZONE): EntitySelector(
{"entity": {"domain": ZONE_DOMAIN}} 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 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] = { CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
"user": SchemaFlowFormStep( "user": SchemaFlowFormStep(
vol.Schema( vol.Schema(
{ {
vol.Required(CONF_ENTITY_ID): selector.selector( vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
{"entity": {"domain": Platform.SWITCH}} selector.EntitySelectorConfig(domain=Platform.SWITCH),
), ),
vol.Required(CONF_TARGET_DOMAIN): selector.selector( vol.Required(CONF_TARGET_DOMAIN): selector.SelectSelector(
{ selector.SelectSelectorConfig(options=TARGET_DOMAIN_OPTIONS),
"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"},
]
}
}
), ),
} }
) )

View file

@ -15,13 +15,16 @@ from homeassistant.const import (
CONF_NAME, CONF_NAME,
CONF_RADIUS, CONF_RADIUS,
CONF_SHOW_ON_MAP, CONF_SHOW_ON_MAP,
CONF_UNIT_OF_MEASUREMENT,
LENGTH_KILOMETERS, LENGTH_KILOMETERS,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv 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 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, "longitude": self.hass.config.longitude,
}, },
), ),
): selector({"location": {}}), ): LocationSelector(),
vol.Required( vol.Required(
CONF_RADIUS, default=user_input.get(CONF_RADIUS, DEFAULT_RADIUS) CONF_RADIUS, default=user_input.get(CONF_RADIUS, DEFAULT_RADIUS)
): selector( ): NumberSelector(
{ NumberSelectorConfig(
"number": { min=0.1,
"min": 0.1, max=25,
"max": 25, step=0.1,
"step": 0.1, unit_of_measurement=LENGTH_KILOMETERS,
CONF_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, ),
}
}
), ),
} }
), ),

View file

@ -27,19 +27,25 @@ def _validate_mode(data: Any) -> Any:
OPTIONS_SCHEMA = vol.Schema( OPTIONS_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS): selector.selector( vol.Required(
{"number": {"mode": "box"}} 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( CONFIG_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_NAME): selector.selector({"text": {}}), vol.Required(CONF_NAME): selector.TextSelector(),
vol.Required(CONF_ENTITY_ID): selector.selector( vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
{"entity": {"domain": "sensor"}} selector.EntitySelectorConfig(domain="sensor")
), ),
} }
).extend(OPTIONS_SCHEMA.schema) ).extend(OPTIONS_SCHEMA.schema)

View file

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

View file

@ -27,7 +27,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession 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 ( from .const import (
AUTO_MIGRATION_MESSAGE, AUTO_MIGRATION_MESSAGE,
@ -78,7 +78,7 @@ def _get_config_schema(
vol.Required( vol.Required(
CONF_LOCATION, CONF_LOCATION,
default=default_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 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 import selector
from homeassistant.helpers.schema_config_entry_flow import ( from homeassistant.helpers.schema_config_entry_flow import (
SchemaConfigFlowHandler, SchemaConfigFlowHandler,
@ -34,15 +34,15 @@ from .const import (
) )
METER_TYPES = [ METER_TYPES = [
{"value": "none", "label": "No cycle"}, selector.SelectOptionDict(value="none", label="No cycle"),
{"value": QUARTER_HOURLY, "label": "Every 15 minutes"}, selector.SelectOptionDict(value=QUARTER_HOURLY, label="Every 15 minutes"),
{"value": HOURLY, "label": "Hourly"}, selector.SelectOptionDict(value=HOURLY, label="Hourly"),
{"value": DAILY, "label": "Daily"}, selector.SelectOptionDict(value=DAILY, label="Daily"),
{"value": WEEKLY, "label": "Weekly"}, selector.SelectOptionDict(value=WEEKLY, label="Weekly"),
{"value": MONTHLY, "label": "Monthly"}, selector.SelectOptionDict(value=MONTHLY, label="Monthly"),
{"value": BIMONTHLY, "label": "Every two months"}, selector.SelectOptionDict(value=BIMONTHLY, label="Every two months"),
{"value": QUARTERLY, "label": "Quarterly"}, selector.SelectOptionDict(value=QUARTERLY, label="Quarterly"),
{"value": YEARLY, "label": "Yearly"}, selector.SelectOptionDict(value=YEARLY, label="Yearly"),
] ]
@ -58,40 +58,38 @@ def _validate_config(data: Any) -> Any:
OPTIONS_SCHEMA = vol.Schema( OPTIONS_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_SOURCE_SENSOR): selector.selector( vol.Required(CONF_SOURCE_SENSOR): selector.EntitySelector(
{"entity": {"domain": "sensor"}}, selector.EntitySelectorConfig(domain="sensor"),
), ),
} }
) )
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_NAME): selector.selector({"text": {}}), vol.Required(CONF_NAME): selector.TextSelector(),
vol.Required(CONF_SOURCE_SENSOR): selector.selector( vol.Required(CONF_SOURCE_SENSOR): selector.EntitySelector(
{"entity": {"domain": "sensor"}}, selector.EntitySelectorConfig(domain="sensor"),
), ),
vol.Required(CONF_METER_TYPE): selector.selector( vol.Required(CONF_METER_TYPE): selector.SelectSelector(
{"select": {"options": METER_TYPES}} selector.SelectSelectorConfig(options=METER_TYPES),
), ),
vol.Required(CONF_METER_OFFSET, default=0): selector.selector( vol.Required(CONF_METER_OFFSET, default=0): selector.NumberSelector(
{ selector.NumberSelectorConfig(
"number": { min=0,
"min": 0, max=28,
"max": 28, mode=selector.NumberSelectorMode.BOX,
"mode": "box", unit_of_measurement="days",
CONF_UNIT_OF_MEASUREMENT: "days", ),
}
}
), ),
vol.Required(CONF_TARIFFS, default=[]): selector.selector( vol.Required(CONF_TARIFFS, default=[]): selector.SelectSelector(
{"select": {"options": [], "custom_value": True, "multiple": True}} selector.SelectSelectorConfig(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_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 @callback
def entity_selector_without_own_entities( def entity_selector_without_own_entities(
handler: SchemaOptionsFlowHandler, handler: SchemaOptionsFlowHandler,
entity_selector_config: dict[str, Any], entity_selector_config: selector.EntitySelectorConfig,
) -> vol.Schema: ) -> vol.Schema:
"""Return an entity selector which excludes own entities.""" """Return an entity selector which excludes own entities."""
entity_registry = er.async_get(handler.hass) 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] entity_ids = [ent.entity_id for ent in entities]
return selector.selector( final_selector_config = entity_selector_config.copy()
{"entity": {**entity_selector_config, "exclude_entities": entity_ids}} final_selector_config["exclude_entities"] = entity_ids
)
return selector.EntitySelector(final_selector_config)

View file

@ -1,11 +1,12 @@
"""Selectors for Home Assistant.""" """Selectors for Home Assistant."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable, Sequence
from typing import Any, cast from typing import Any, TypedDict, cast
import voluptuous as vol import voluptuous as vol
from homeassistant.backports.enum import StrEnum
from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT
from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.core import split_entity_id, valid_entity_id
from homeassistant.util import decorator from homeassistant.util import decorator
@ -36,11 +37,7 @@ def selector(config: Any) -> Selector:
selector_class = _get_selector_class(config) selector_class = _get_selector_class(config)
selector_type = list(config)[0] selector_type = list(config)[0]
# Selectors can be empty return selector_class(config[selector_type])
if config[selector_type] is None:
return selector_class({selector_type: {}})
return selector_class(config)
def validate_selector(config: Any) -> dict: def validate_selector(config: Any) -> dict:
@ -64,9 +61,13 @@ class Selector:
config: Any config: Any
selector_type: str selector_type: str
def __init__(self, config: Any) -> None: def __init__(self, config: Any = None) -> None:
"""Instantiate a selector.""" """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: def serialize(self) -> Any:
"""Serialize Selector for voluptuous_serialize.""" """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( SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA = vol.Schema(
{ {
# Integration linked to it with a config entry # 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") @SELECTORS.register("action")
class ActionSelector(Selector): class ActionSelector(Selector):
"""Selector of an action sequence (script syntax).""" """Selector of an action sequence (script syntax)."""
@ -106,11 +129,22 @@ class ActionSelector(Selector):
CONFIG_SCHEMA = vol.Schema({}) CONFIG_SCHEMA = vol.Schema({})
def __init__(self, config: ActionSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
super().__init__(config)
def __call__(self, data: Any) -> Any: def __call__(self, data: Any) -> Any:
"""Validate the passed selection.""" """Validate the passed selection."""
return data return data
class AddonSelectorConfig(TypedDict, total=False):
"""Class to represent an addon selector config."""
name: str
slug: str
@SELECTORS.register("addon") @SELECTORS.register("addon")
class AddonSelector(Selector): class AddonSelector(Selector):
"""Selector of a add-on.""" """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: def __call__(self, data: Any) -> str:
"""Validate the passed selection.""" """Validate the passed selection."""
addon: str = vol.Schema(str)(data) addon: str = vol.Schema(str)(data)
return addon return addon
class AreaSelectorConfig(TypedDict, total=False):
"""Class to represent an area selector config."""
entity: SingleEntitySelectorConfig
device: SingleDeviceSelectorConfig
multiple: bool
@SELECTORS.register("area") @SELECTORS.register("area")
class AreaSelector(Selector): class AreaSelector(Selector):
"""Selector of a single or list of areas.""" """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]: def __call__(self, data: Any) -> str | list[str]:
"""Validate the passed selection.""" """Validate the passed selection."""
if not self.config["multiple"]: if not self.config["multiple"]:
@ -154,6 +204,12 @@ class AreaSelector(Selector):
return [vol.Schema(str)(val) for val in data] 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") @SELECTORS.register("attribute")
class AttributeSelector(Selector): class AttributeSelector(Selector):
"""Selector for an entity attribute.""" """Selector for an entity attribute."""
@ -162,12 +218,20 @@ class AttributeSelector(Selector):
CONFIG_SCHEMA = vol.Schema({vol.Required("entity_id"): cv.entity_id}) 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: def __call__(self, data: Any) -> str:
"""Validate the passed selection.""" """Validate the passed selection."""
attribute: str = vol.Schema(str)(data) attribute: str = vol.Schema(str)(data)
return attribute return attribute
class BooleanSelectorConfig(TypedDict):
"""Class to represent a boolean selector config."""
@SELECTORS.register("boolean") @SELECTORS.register("boolean")
class BooleanSelector(Selector): class BooleanSelector(Selector):
"""Selector of a boolean value.""" """Selector of a boolean value."""
@ -176,12 +240,20 @@ class BooleanSelector(Selector):
CONFIG_SCHEMA = vol.Schema({}) CONFIG_SCHEMA = vol.Schema({})
def __init__(self, config: BooleanSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
super().__init__(config)
def __call__(self, data: Any) -> bool: def __call__(self, data: Any) -> bool:
"""Validate the passed selection.""" """Validate the passed selection."""
value: bool = vol.Coerce(bool)(data) value: bool = vol.Coerce(bool)(data)
return value return value
class ColorRGBSelectorConfig(TypedDict):
"""Class to represent a color RGB selector config."""
@SELECTORS.register("color_rgb") @SELECTORS.register("color_rgb")
class ColorRGBSelector(Selector): class ColorRGBSelector(Selector):
"""Selector of an RGB color value.""" """Selector of an RGB color value."""
@ -190,12 +262,23 @@ class ColorRGBSelector(Selector):
CONFIG_SCHEMA = vol.Schema({}) 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]: def __call__(self, data: Any) -> list[int]:
"""Validate the passed selection.""" """Validate the passed selection."""
value: list[int] = vol.All(list, vol.ExactSequence((cv.byte,) * 3))(data) value: list[int] = vol.All(list, vol.ExactSequence((cv.byte,) * 3))(data)
return value 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") @SELECTORS.register("color_temp")
class ColorTempSelector(Selector): class ColorTempSelector(Selector):
"""Selector of an color temperature.""" """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: def __call__(self, data: Any) -> int:
"""Validate the passed selection.""" """Validate the passed selection."""
value: int = vol.All( value: int = vol.All(
@ -221,6 +308,10 @@ class ColorTempSelector(Selector):
return value return value
class DateSelectorConfig(TypedDict):
"""Class to represent a date selector config."""
@SELECTORS.register("date") @SELECTORS.register("date")
class DateSelector(Selector): class DateSelector(Selector):
"""Selector of a date.""" """Selector of a date."""
@ -229,12 +320,20 @@ class DateSelector(Selector):
CONFIG_SCHEMA = vol.Schema({}) CONFIG_SCHEMA = vol.Schema({})
def __init__(self, config: DateSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
super().__init__(config)
def __call__(self, data: Any) -> Any: def __call__(self, data: Any) -> Any:
"""Validate the passed selection.""" """Validate the passed selection."""
cv.date(data) cv.date(data)
return data return data
class DateTimeSelectorConfig(TypedDict):
"""Class to represent a date time selector config."""
@SELECTORS.register("datetime") @SELECTORS.register("datetime")
class DateTimeSelector(Selector): class DateTimeSelector(Selector):
"""Selector of a datetime.""" """Selector of a datetime."""
@ -243,12 +342,26 @@ class DateTimeSelector(Selector):
CONFIG_SCHEMA = vol.Schema({}) CONFIG_SCHEMA = vol.Schema({})
def __init__(self, config: DateTimeSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
super().__init__(config)
def __call__(self, data: Any) -> Any: def __call__(self, data: Any) -> Any:
"""Validate the passed selection.""" """Validate the passed selection."""
cv.datetime(data) cv.datetime(data)
return 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") @SELECTORS.register("device")
class DeviceSelector(Selector): class DeviceSelector(Selector):
"""Selector of a single or list of devices.""" """Selector of a single or list of devices."""
@ -259,6 +372,10 @@ class DeviceSelector(Selector):
{vol.Optional("multiple", default=False): cv.boolean} {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]: def __call__(self, data: Any) -> str | list[str]:
"""Validate the passed selection.""" """Validate the passed selection."""
if not self.config["multiple"]: if not self.config["multiple"]:
@ -269,6 +386,12 @@ class DeviceSelector(Selector):
return [vol.Schema(str)(val) for val in data] 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") @SELECTORS.register("duration")
class DurationSelector(Selector): class DurationSelector(Selector):
"""Selector for a duration.""" """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]: def __call__(self, data: Any) -> dict[str, float]:
"""Validate the passed selection.""" """Validate the passed selection."""
cv.time_period_dict(data) cv.time_period_dict(data)
return cast(dict[str, float], 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") @SELECTORS.register("entity")
class EntitySelector(Selector): class EntitySelector(Selector):
"""Selector of a single or list of entities.""" """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]: def __call__(self, data: Any) -> str | list[str]:
"""Validate the passed selection.""" """Validate the passed selection."""
@ -333,6 +472,12 @@ class EntitySelector(Selector):
return cast(list, vol.Schema([validate])(data)) # Output is a list 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") @SELECTORS.register("icon")
class IconSelector(Selector): class IconSelector(Selector):
"""Selector for an icon.""" """Selector for an icon."""
@ -344,12 +489,23 @@ class IconSelector(Selector):
# Frontend also has a fallbackPath option, this is not used by core # 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: def __call__(self, data: Any) -> str:
"""Validate the passed selection.""" """Validate the passed selection."""
icon: str = vol.Schema(str)(data) icon: str = vol.Schema(str)(data)
return icon return icon
class LocationSelectorConfig(TypedDict, total=False):
"""Class to represent a location selector config."""
radius: bool
icon: str
@SELECTORS.register("location") @SELECTORS.register("location")
class LocationSelector(Selector): class LocationSelector(Selector):
"""Selector for a location.""" """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]: def __call__(self, data: Any) -> dict[str, float]:
"""Validate the passed selection.""" """Validate the passed selection."""
location: dict[str, float] = self.DATA_SCHEMA(data) location: dict[str, float] = self.DATA_SCHEMA(data)
return location return location
class MediaSelectorConfig(TypedDict):
"""Class to represent a media selector config."""
@SELECTORS.register("media") @SELECTORS.register("media")
class MediaSelector(Selector): class MediaSelector(Selector):
"""Selector for media.""" """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]: def __call__(self, data: Any) -> dict[str, float]:
"""Validate the passed selection.""" """Validate the passed selection."""
media: dict[str, float] = self.DATA_SCHEMA(data) media: dict[str, float] = self.DATA_SCHEMA(data)
return media 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: def has_min_max_if_slider(data: Any) -> Any:
"""Validate configuration.""" """Validate configuration."""
if data["mode"] == "box": if data["mode"] == "box":
@ -426,12 +611,18 @@ class NumberSelector(Selector):
vol.Coerce(float), vol.Range(min=1e-3) vol.Coerce(float), vol.Range(min=1e-3)
), ),
vol.Optional(CONF_UNIT_OF_MEASUREMENT): str, 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, 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: def __call__(self, data: Any) -> float:
"""Validate the passed selection.""" """Validate the passed selection."""
value: float = vol.Coerce(float)(data) value: float = vol.Coerce(float)(data)
@ -445,6 +636,10 @@ class NumberSelector(Selector):
return value return value
class ObjectSelectorConfig(TypedDict):
"""Class to represent an object selector config."""
@SELECTORS.register("object") @SELECTORS.register("object")
class ObjectSelector(Selector): class ObjectSelector(Selector):
"""Selector for an arbitrary object.""" """Selector for an arbitrary object."""
@ -453,6 +648,10 @@ class ObjectSelector(Selector):
CONFIG_SCHEMA = vol.Schema({}) CONFIG_SCHEMA = vol.Schema({})
def __init__(self, config: ObjectSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
super().__init__(config)
def __call__(self, data: Any) -> Any: def __call__(self, data: Any) -> Any:
"""Validate the passed selection.""" """Validate the passed selection."""
return data 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") @SELECTORS.register("select")
class SelectSelector(Selector): class SelectSelector(Selector):
"""Selector for an single or multi-choice input select.""" """Selector for an single-choice input select."""
selector_type = "select" selector_type = "select"
@ -480,10 +702,14 @@ class SelectSelector(Selector):
vol.Required("options"): vol.All(vol.Any([str], [select_option])), vol.Required("options"): vol.All(vol.Any([str], [select_option])),
vol.Optional("multiple", default=False): cv.boolean, vol.Optional("multiple", default=False): cv.boolean,
vol.Optional("custom_value", 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: def __call__(self, data: Any) -> Any:
"""Validate the passed selection.""" """Validate the passed selection."""
options = [] options = []
@ -504,41 +730,11 @@ class SelectSelector(Selector):
return [parent_schema(vol.Schema(str)(val)) for val in data] return [parent_schema(vol.Schema(str)(val)) for val in data]
@SELECTORS.register("text") class TargetSelectorConfig(TypedDict, total=False):
class StringSelector(Selector): """Class to represent a target selector config."""
"""Selector for a multi-line text string."""
selector_type = "text" entity: SingleEntitySelectorConfig
device: SingleDeviceSelectorConfig
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
@SELECTORS.register("target") @SELECTORS.register("target")
@ -559,12 +755,72 @@ class TargetSelector(Selector):
TARGET_SELECTION_SCHEMA = vol.Schema(cv.TARGET_SERVICE_FIELDS) 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]]: def __call__(self, data: Any) -> dict[str, list[str]]:
"""Validate the passed selection.""" """Validate the passed selection."""
target: dict[str, list[str]] = self.TARGET_SELECTION_SCHEMA(data) target: dict[str, list[str]] = self.TARGET_SELECTION_SCHEMA(data)
return target 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") @SELECTORS.register("theme")
class ThemeSelector(Selector): class ThemeSelector(Selector):
"""Selector for an theme.""" """Selector for an theme."""
@ -573,12 +829,20 @@ class ThemeSelector(Selector):
CONFIG_SCHEMA = vol.Schema({}) CONFIG_SCHEMA = vol.Schema({})
def __init__(self, config: ThemeSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
super().__init__(config)
def __call__(self, data: Any) -> str: def __call__(self, data: Any) -> str:
"""Validate the passed selection.""" """Validate the passed selection."""
theme: str = vol.Schema(str)(data) theme: str = vol.Schema(str)(data)
return theme return theme
class TimeSelectorConfig(TypedDict):
"""Class to represent a time selector config."""
@SELECTORS.register("time") @SELECTORS.register("time")
class TimeSelector(Selector): class TimeSelector(Selector):
"""Selector of a time value.""" """Selector of a time value."""
@ -587,6 +851,10 @@ class TimeSelector(Selector):
CONFIG_SCHEMA = vol.Schema({}) CONFIG_SCHEMA = vol.Schema({})
def __init__(self, config: TimeSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
super().__init__(config)
def __call__(self, data: Any) -> str: def __call__(self, data: Any) -> str:
"""Validate the passed selection.""" """Validate the passed selection."""
cv.time(data) cv.time(data)

View file

@ -18,15 +18,15 @@ from .const import DOMAIN
OPTIONS_SCHEMA = vol.Schema( OPTIONS_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_ENTITY_ID): selector.selector( vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
{"entity": {"domain": "sensor"}} selector.EntitySelectorConfig(domain="sensor")
), ),
} }
) )
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
vol.Required("name"): selector.selector({"text": {}}), vol.Required("name"): selector.TextSelector(),
} }
).extend(OPTIONS_SCHEMA.schema) ).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): def test_boolean_selector_schema(schema, valid_selections, invalid_selections):
"""Test boolean selector.""" """Test boolean selector."""
_test_selector("boolean", schema, valid_selections, invalid_selections, bool) _test_selector(
"boolean",
schema,
valid_selections,
invalid_selections,
bool,
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -512,7 +518,13 @@ def test_media_selector_schema(schema, valid_selections, invalid_selections):
data.pop("metadata", None) data.pop("metadata", None)
return data 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( @pytest.mark.parametrize(