From 8f16b09751fb9ae010e6a42856c9ad89872d049a Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Wed, 3 Sep 2025 08:37:43 +0200 Subject: [PATCH] Accept None directly in the selector schemas (#151510) Co-authored-by: Erik Montnemery --- homeassistant/helpers/selector.py | 113 +++++++++++++++--------------- homeassistant/helpers/service.py | 2 +- homeassistant/helpers/trigger.py | 2 +- script/hassfest/services.py | 4 +- script/hassfest/triggers.py | 4 +- 5 files changed, 62 insertions(+), 63 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index c25a3b64562..6c162dc08fc 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -47,14 +47,7 @@ def selector(config: Any) -> Selector: def validate_selector(config: Any) -> dict: """Validate a selector.""" selector_type, selector_class = _get_selector_type_and_class(config) - - # Selectors can be empty - if config[selector_type] is None: - config = {selector_type: {}} - - return { - selector_type: cast(dict, selector_class.CONFIG_SCHEMA(config[selector_type])) - } + return {selector_type: selector_class.CONFIG_SCHEMA(config[selector_type])} class Selector[_T: Mapping[str, Any]]: @@ -66,10 +59,6 @@ class Selector[_T: Mapping[str, Any]]: def __init__(self, config: Mapping[str, Any] | None = None) -> None: """Instantiate a selector.""" - # Selectors can be empty - if config is None: - config = {} - self.config = self.CONFIG_SCHEMA(config) def __eq__(self, other: object) -> bool: @@ -125,11 +114,25 @@ def _validate_supported_features(supported_features: list[str]) -> int: return feature_mask -BASE_SELECTOR_CONFIG_SCHEMA = vol.Schema( - { - vol.Optional("read_only"): bool, - } -) +def make_selector_config_schema(schema_dict: dict | None = None) -> vol.Schema: + """Make selector config schema.""" + if schema_dict is None: + schema_dict = {} + + def none_to_empty_dict(value: Any) -> Any: + if value is None: + return {} + return value + + return vol.Schema( + vol.All( + none_to_empty_dict, + { + vol.Optional("read_only"): bool, + **schema_dict, + }, + ) + ) class BaseSelectorConfig(TypedDict, total=False): @@ -224,7 +227,7 @@ class ActionSelector(Selector[ActionSelectorConfig]): selector_type = "action" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA + CONFIG_SCHEMA = make_selector_config_schema() def __init__(self, config: ActionSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -248,7 +251,7 @@ class AddonSelector(Selector[AddonSelectorConfig]): selector_type = "addon" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Optional("name"): str, vol.Optional("slug"): str, @@ -279,7 +282,7 @@ class AreaSelector(Selector[AreaSelectorConfig]): selector_type = "area" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Optional("entity"): vol.All( cv.ensure_list, @@ -317,7 +320,7 @@ class AssistPipelineSelector(Selector[AssistPipelineSelectorConfig]): selector_type = "assist_pipeline" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA + CONFIG_SCHEMA = make_selector_config_schema() def __init__(self, config: AssistPipelineSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -342,7 +345,7 @@ class AttributeSelector(Selector[AttributeSelectorConfig]): selector_type = "attribute" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Required("entity_id"): cv.entity_id, # hide_attributes is used to hide attributes in the frontend. @@ -371,7 +374,7 @@ class BackupLocationSelector(Selector[BackupLocationSelectorConfig]): selector_type = "backup_location" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA + CONFIG_SCHEMA = make_selector_config_schema() def __init__(self, config: BackupLocationSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -393,7 +396,7 @@ class BooleanSelector(Selector[BooleanSelectorConfig]): selector_type = "boolean" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA + CONFIG_SCHEMA = make_selector_config_schema() def __init__(self, config: BooleanSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -415,7 +418,7 @@ class ColorRGBSelector(Selector[ColorRGBSelectorConfig]): selector_type = "color_rgb" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA + CONFIG_SCHEMA = make_selector_config_schema() def __init__(self, config: ColorRGBSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -450,7 +453,7 @@ class ColorTempSelector(Selector[ColorTempSelectorConfig]): selector_type = "color_temp" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Optional("unit", default=ColorTempSelectorUnit.MIRED): vol.All( vol.Coerce(ColorTempSelectorUnit), lambda val: val.value @@ -497,7 +500,7 @@ class ConditionSelector(Selector[ConditionSelectorConfig]): selector_type = "condition" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA + CONFIG_SCHEMA = make_selector_config_schema() def __init__(self, config: ConditionSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -520,7 +523,7 @@ class ConfigEntrySelector(Selector[ConfigEntrySelectorConfig]): selector_type = "config_entry" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Optional("integration"): str, } @@ -550,7 +553,7 @@ class ConstantSelector(Selector[ConstantSelectorConfig]): selector_type = "constant" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Optional("label"): str, vol.Optional("translation_key"): cv.string, @@ -580,7 +583,7 @@ class ConversationAgentSelector(Selector[ConversationAgentSelectorConfig]): selector_type = "conversation_agent" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Optional("language"): str, } @@ -609,7 +612,7 @@ class CountrySelector(Selector[CountrySelectorConfig]): selector_type = "country" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Optional("countries"): [str], vol.Optional("no_sort", default=False): cv.boolean, @@ -640,7 +643,7 @@ class DateSelector(Selector[DateSelectorConfig]): selector_type = "date" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA + CONFIG_SCHEMA = make_selector_config_schema() def __init__(self, config: DateSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -662,7 +665,7 @@ class DateTimeSelector(Selector[DateTimeSelectorConfig]): selector_type = "datetime" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA + CONFIG_SCHEMA = make_selector_config_schema() def __init__(self, config: DateTimeSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -688,7 +691,7 @@ class DeviceSelector(Selector[DeviceSelectorConfig]): selector_type = "device" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { **_LEGACY_DEVICE_SELECTOR_CONFIG_SCHEMA_DICT, # Device has to contain entities matching this selector @@ -731,7 +734,7 @@ class DurationSelector(Selector[DurationSelectorConfig]): selector_type = "duration" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { # Enable day field in frontend. A selection with `days` set is allowed # even if `enable_day` is not set @@ -772,7 +775,7 @@ class EntitySelector(Selector[EntitySelectorConfig]): selector_type = "entity" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { **_LEGACY_ENTITY_SELECTOR_CONFIG_SCHEMA_DICT, vol.Optional("exclude_entities"): [str], @@ -832,7 +835,7 @@ class FileSelector(Selector[FileSelectorConfig]): selector_type = "file" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { # https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept vol.Required("accept"): str, @@ -867,7 +870,7 @@ class FloorSelector(Selector[FloorSelectorConfig]): selector_type = "floor" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Optional("entity"): vol.All( cv.ensure_list, @@ -907,7 +910,7 @@ class IconSelector(Selector[IconSelectorConfig]): selector_type = "icon" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( {vol.Optional("placeholder"): str} # Frontend also has a fallbackPath option, this is not used by core ) @@ -934,7 +937,7 @@ class LabelSelector(Selector[LabelSelectorConfig]): selector_type = "label" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Optional("multiple", default=False): cv.boolean, } @@ -968,7 +971,7 @@ class LanguageSelector(Selector[LanguageSelectorConfig]): selector_type = "language" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Optional("languages"): [str], vol.Optional("native_name", default=False): cv.boolean, @@ -1001,7 +1004,7 @@ class LocationSelector(Selector[LocationSelectorConfig]): selector_type = "location" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( {vol.Optional("radius"): bool, vol.Optional("icon"): str} ) DATA_SCHEMA = vol.Schema( @@ -1034,7 +1037,7 @@ class MediaSelector(Selector[MediaSelectorConfig]): selector_type = "media" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Optional("accept"): [str], } @@ -1109,7 +1112,7 @@ class NumberSelector(Selector[NumberSelectorConfig]): selector_type = "number" CONFIG_SCHEMA = vol.All( - BASE_SELECTOR_CONFIG_SCHEMA.extend( + make_selector_config_schema( { vol.Optional("min"): vol.Coerce(float), vol.Optional("max"): vol.Coerce(float), @@ -1169,7 +1172,7 @@ class ObjectSelector(Selector[ObjectSelectorConfig]): selector_type = "object" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Optional("fields"): { str: { @@ -1217,7 +1220,7 @@ class QrCodeSelector(Selector[QrCodeSelectorConfig]): selector_type = "qr_code" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Required("data"): str, vol.Optional("scale"): int, @@ -1279,7 +1282,7 @@ class SelectSelector(Selector[SelectSelectorConfig]): selector_type = "select" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Required("options"): vol.All(vol.Any([str], [select_option])), vol.Optional("multiple", default=False): cv.boolean, @@ -1333,7 +1336,7 @@ class StateSelector(Selector[StateSelectorConfig]): selector_type = "state" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Optional("entity_id"): cv.entity_id, vol.Optional("hide_states"): [str], @@ -1372,7 +1375,7 @@ class StatisticSelector(Selector[StatisticSelectorConfig]): selector_type = "statistic" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Optional("multiple", default=False): cv.boolean, } @@ -1409,7 +1412,7 @@ class TargetSelector(Selector[TargetSelectorConfig]): selector_type = "target" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Optional("entity"): vol.All( cv.ensure_list, @@ -1444,7 +1447,7 @@ class TemplateSelector(Selector[TemplateSelectorConfig]): selector_type = "template" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA + CONFIG_SCHEMA = make_selector_config_schema() def __init__(self, config: TemplateSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -1491,7 +1494,7 @@ class TextSelector(Selector[TextSelectorConfig]): selector_type = "text" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Optional("multiline", default=False): bool, vol.Optional("prefix"): str, @@ -1530,7 +1533,7 @@ class ThemeSelector(Selector[ThemeSelectorConfig]): selector_type = "theme" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = make_selector_config_schema( { vol.Optional("include_default", default=False): cv.boolean, } @@ -1556,7 +1559,7 @@ class TimeSelector(Selector[TimeSelectorConfig]): selector_type = "time" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA + CONFIG_SCHEMA = make_selector_config_schema() def __init__(self, config: TimeSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -1578,7 +1581,7 @@ class TriggerSelector(Selector[TriggerSelectorConfig]): selector_type = "trigger" - CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA + CONFIG_SCHEMA = make_selector_config_schema() def __init__(self, config: TriggerSelectorConfig | None = None) -> None: """Instantiate a selector.""" diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index f9c846c60fa..a30d5c67cef 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -190,7 +190,7 @@ _SECTION_SCHEMA = vol.Schema( _SERVICE_SCHEMA = vol.Schema( { - vol.Optional("target"): vol.Any(TargetSelector.CONFIG_SCHEMA, None), + vol.Optional("target"): TargetSelector.CONFIG_SCHEMA, vol.Optional("fields"): vol.Schema( {str: vol.Any(_SECTION_SCHEMA, _FIELD_SCHEMA)} ), diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index 741fac3fcf7..2351ab9468b 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -83,7 +83,7 @@ _FIELD_SCHEMA = vol.Schema( _TRIGGER_SCHEMA = vol.Schema( { - vol.Optional("target"): vol.Any(TargetSelector.CONFIG_SCHEMA, None), + vol.Optional("target"): TargetSelector.CONFIG_SCHEMA, vol.Optional("fields"): vol.Schema({str: _FIELD_SCHEMA}), }, extra=vol.ALLOW_EXTRA, diff --git a/script/hassfest/services.py b/script/hassfest/services.py index 84d3aaefa88..844a8955470 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -119,9 +119,7 @@ def _service_schema(targeted: bool, custom: bool) -> vol.Schema: } if targeted: - schema_dict[vol.Required("target")] = vol.Any( - selector.TargetSelector.CONFIG_SCHEMA, None - ) + schema_dict[vol.Required("target")] = selector.TargetSelector.CONFIG_SCHEMA if custom: schema_dict |= CUSTOM_INTEGRATION_EXTRA_SCHEMA_DICT diff --git a/script/hassfest/triggers.py b/script/hassfest/triggers.py index 7406e6f98ea..4eb376c435f 100644 --- a/script/hassfest/triggers.py +++ b/script/hassfest/triggers.py @@ -38,9 +38,7 @@ FIELD_SCHEMA = vol.Schema( TRIGGER_SCHEMA = vol.Any( vol.Schema( { - vol.Optional("target"): vol.Any( - selector.TargetSelector.CONFIG_SCHEMA, None - ), + vol.Optional("target"): selector.TargetSelector.CONFIG_SCHEMA, vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}), } ),