From b325c112b4319681cb052fbec16aa3b23fe1e883 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 11 Apr 2022 03:20:56 -0400 Subject: [PATCH] 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 * 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 --- .../components/derivative/config_flow.py | 59 ++- homeassistant/components/group/config_flow.py | 22 +- .../components/integration/config_flow.py | 67 ++-- homeassistant/components/knx/config_flow.py | 46 ++- .../components/min_max/config_flow.py | 26 +- .../components/open_meteo/config_flow.py | 6 +- .../components/switch_as_x/config_flow.py | 26 +- .../components/tankerkoenig/config_flow.py | 25 +- .../components/threshold/config_flow.py | 20 +- homeassistant/components/tod/config_flow.py | 6 +- .../components/tomorrowio/config_flow.py | 4 +- .../components/utility_meter/config_flow.py | 66 ++-- .../helpers/schema_config_entry_flow.py | 9 +- homeassistant/helpers/selector.py | 360 +++++++++++++++--- .../integration/config_flow.py | 6 +- tests/helpers/test_selector.py | 16 +- 16 files changed, 525 insertions(+), 239 deletions(-) diff --git a/homeassistant/components/derivative/config_flow.py b/homeassistant/components/derivative/config_flow.py index 6249e9fd1cc..eea2b303a12 100644 --- a/homeassistant/components/derivative/config_flow.py +++ b/homeassistant/components/derivative/config_flow.py @@ -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) diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 8ddee492834..f47dbcb3b44 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -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(), } ) diff --git a/homeassistant/components/integration/config_flow.py b/homeassistant/components/integration/config_flow.py index 3f7d39554e8..aab5671f64b 100644 --- a/homeassistant/components/integration/config_flow.py +++ b/homeassistant/components/integration/config_flow.py @@ -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 + ), ), } ) diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 2e3036e9a32..d6516d1d4ef 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -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), ) diff --git a/homeassistant/components/min_max/config_flow.py b/homeassistant/components/min_max/config_flow.py index 6dab7cc000c..2114a5406d0 100644 --- a/homeassistant/components/min_max/config_flow.py +++ b/homeassistant/components/min_max/config_flow.py @@ -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) diff --git a/homeassistant/components/open_meteo/config_flow.py b/homeassistant/components/open_meteo/config_flow.py index 7f63b834bb9..7a603f887f0 100644 --- a/homeassistant/components/open_meteo/config_flow.py +++ b/homeassistant/components/open_meteo/config_flow.py @@ -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), ), } ), diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index a70e0a371e8..991f4f33a6b 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -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), ), } ) diff --git a/homeassistant/components/tankerkoenig/config_flow.py b/homeassistant/components/tankerkoenig/config_flow.py index 3f3449c26e4..65c367d1ba4 100644 --- a/homeassistant/components/tankerkoenig/config_flow.py +++ b/homeassistant/components/tankerkoenig/config_flow.py @@ -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, + ), ), } ), diff --git a/homeassistant/components/threshold/config_flow.py b/homeassistant/components/threshold/config_flow.py index c77d4b57115..1e6236259bd 100644 --- a/homeassistant/components/threshold/config_flow.py +++ b/homeassistant/components/threshold/config_flow.py @@ -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) diff --git a/homeassistant/components/tod/config_flow.py b/homeassistant/components/tod/config_flow.py index bd1712d1db5..5155d15561b 100644 --- a/homeassistant/components/tod/config_flow.py +++ b/homeassistant/components/tod/config_flow.py @@ -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) diff --git a/homeassistant/components/tomorrowio/config_flow.py b/homeassistant/components/tomorrowio/config_flow.py index bde49ed3fe5..a0fa7b0b34c 100644 --- a/homeassistant/components/tomorrowio/config_flow.py +++ b/homeassistant/components/tomorrowio/config_flow.py @@ -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)), }, ) diff --git a/homeassistant/components/utility_meter/config_flow.py b/homeassistant/components/utility_meter/config_flow.py index ed12b3038b6..cabd90ba8bd 100644 --- a/homeassistant/components/utility_meter/config_flow.py +++ b/homeassistant/components/utility_meter/config_flow.py @@ -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(), } ) diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 341ae605025..604ab8d9200 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -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) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index b4d01ef52e0..4666be51d4c 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -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) diff --git a/script/scaffold/templates/config_flow_helper/integration/config_flow.py b/script/scaffold/templates/config_flow_helper/integration/config_flow.py index b8a048e9dba..015aac2ca91 100644 --- a/script/scaffold/templates/config_flow_helper/integration/config_flow.py +++ b/script/scaffold/templates/config_flow_helper/integration/config_flow.py @@ -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) diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index bef95b056b4..326b9f3081e 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -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(