diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 6668229c0cf..1652e2110f4 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -84,6 +84,136 @@ SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA = vol.Schema( } ) +DEVICE_SELECTOR_CONFIG_SCHEMA = vol.Schema( + { + # Integration linked to it with a config entry + vol.Optional("integration"): str, + # Manufacturer of device + vol.Optional("manufacturer"): str, + # Model of device + vol.Optional("model"): str, + # Device has to contain entities matching this selector + vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, + vol.Optional("multiple", default=False): cv.boolean, + } +) + + +@SELECTORS.register("action") +class ActionSelector(Selector): + """Selector of an action sequence (script syntax).""" + + selector_type = "action" + + CONFIG_SCHEMA = vol.Schema({}) + + def __call__(self, data: Any) -> Any: + """Validate the passed selection.""" + return data + + +@SELECTORS.register("addon") +class AddonSelector(Selector): + """Selector of a add-on.""" + + selector_type = "addon" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("name"): str, + vol.Optional("slug"): str, + } + ) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + addon: str = vol.Schema(str)(data) + return addon + + +@SELECTORS.register("area") +class AreaSelector(Selector): + """Selector of a single or list of areas.""" + + selector_type = "area" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, + vol.Optional("device"): DEVICE_SELECTOR_CONFIG_SCHEMA, + vol.Optional("multiple", default=False): cv.boolean, + } + ) + + def __call__(self, data: Any) -> str | list[str]: + """Validate the passed selection.""" + if not self.config["multiple"]: + area_id: str = vol.Schema(str)(data) + return area_id + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return [vol.Schema(str)(val) for val in data] + + +@SELECTORS.register("attribute") +class AttributeSelector(Selector): + """Selector for an entity attribute.""" + + selector_type = "attribute" + + CONFIG_SCHEMA = vol.Schema({vol.Required("entity_id"): cv.entity_id}) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + attribute: str = vol.Schema(str)(data) + return attribute + + +@SELECTORS.register("boolean") +class BooleanSelector(Selector): + """Selector of a boolean value.""" + + selector_type = "boolean" + + CONFIG_SCHEMA = vol.Schema({}) + + def __call__(self, data: Any) -> bool: + """Validate the passed selection.""" + value: bool = vol.Coerce(bool)(data) + return value + + +@SELECTORS.register("device") +class DeviceSelector(Selector): + """Selector of a single or list of devices.""" + + selector_type = "device" + + CONFIG_SCHEMA = DEVICE_SELECTOR_CONFIG_SCHEMA + + def __call__(self, data: Any) -> str | list[str]: + """Validate the passed selection.""" + if not self.config["multiple"]: + device_id: str = vol.Schema(str)(data) + return device_id + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return [vol.Schema(str)(val) for val in data] + + +@SELECTORS.register("duration") +class DurationSelector(Selector): + """Selector for a duration.""" + + selector_type = "duration" + + CONFIG_SCHEMA = vol.Schema({}) + + def __call__(self, data: Any) -> dict[str, float]: + """Validate the passed selection.""" + cv.time_period_dict(data) + return cast(dict[str, float], data) + @SELECTORS.register("entity") class EntitySelector(Selector): @@ -118,58 +248,69 @@ class EntitySelector(Selector): return cast(list, vol.Schema([validate])(data)) # Output is a list -@SELECTORS.register("device") -class DeviceSelector(Selector): - """Selector of a single or list of devices.""" +@SELECTORS.register("icon") +class IconSelector(Selector): + """Selector for an icon.""" - selector_type = "device" + selector_type = "icon" CONFIG_SCHEMA = vol.Schema( + {vol.Optional("placeholder"): str} + # Frontend also has a fallbackPath option, this is not used by core + ) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + icon: str = vol.Schema(str)(data) + return icon + + +@SELECTORS.register("location") +class LocationSelector(Selector): + """Selector for a location.""" + + selector_type = "location" + + CONFIG_SCHEMA = vol.Schema( + {vol.Optional("radius"): bool, vol.Optional("icon"): str} + ) + DATA_SCHEMA = vol.Schema( { - # Integration linked to it with a config entry - vol.Optional("integration"): str, - # Manufacturer of device - vol.Optional("manufacturer"): str, - # Model of device - vol.Optional("model"): str, - # Device has to contain entities matching this selector - vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, - vol.Optional("multiple", default=False): cv.boolean, + vol.Required("latitude"): float, + vol.Required("longitude"): float, + vol.Optional("radius"): float, } ) - def __call__(self, data: Any) -> str | list[str]: + def __call__(self, data: Any) -> dict[str, float]: """Validate the passed selection.""" - if not self.config["multiple"]: - device_id: str = vol.Schema(str)(data) - return device_id - if not isinstance(data, list): - raise vol.Invalid("Value should be a list") - return [vol.Schema(str)(val) for val in data] + location: dict[str, float] = self.DATA_SCHEMA(data) + return location -@SELECTORS.register("area") -class AreaSelector(Selector): - """Selector of a single or list of areas.""" +@SELECTORS.register("media") +class MediaSelector(Selector): + """Selector for media.""" - selector_type = "area" + selector_type = "media" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = vol.Schema({}) + DATA_SCHEMA = vol.Schema( { - vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, - vol.Optional("device"): DeviceSelector.CONFIG_SCHEMA, - vol.Optional("multiple", default=False): cv.boolean, + # Although marked as optional in frontend, this field is required + vol.Required("entity_id"): cv.entity_id_or_uuid, + # Although marked as optional in frontend, this field is required + vol.Required("media_content_id"): str, + # Although marked as optional in frontend, this field is required + vol.Required("media_content_type"): str, + vol.Remove("metadata"): dict, } ) - def __call__(self, data: Any) -> str | list[str]: + def __call__(self, data: Any) -> dict[str, float]: """Validate the passed selection.""" - if not self.config["multiple"]: - area_id: str = vol.Schema(str)(data) - return area_id - if not isinstance(data, list): - raise vol.Invalid("Value should be a list") - return [vol.Schema(str)(val) for val in data] + media: dict[str, float] = self.DATA_SCHEMA(data) + return media def has_min_max_if_slider(data: Any) -> Any: @@ -219,90 +360,6 @@ class NumberSelector(Selector): return value -@SELECTORS.register("addon") -class AddonSelector(Selector): - """Selector of a add-on.""" - - selector_type = "addon" - - CONFIG_SCHEMA = vol.Schema( - { - vol.Optional("name"): str, - vol.Optional("slug"): str, - } - ) - - def __call__(self, data: Any) -> str: - """Validate the passed selection.""" - addon: str = vol.Schema(str)(data) - return addon - - -@SELECTORS.register("boolean") -class BooleanSelector(Selector): - """Selector of a boolean value.""" - - selector_type = "boolean" - - CONFIG_SCHEMA = vol.Schema({}) - - def __call__(self, data: Any) -> bool: - """Validate the passed selection.""" - value: bool = vol.Coerce(bool)(data) - return value - - -@SELECTORS.register("time") -class TimeSelector(Selector): - """Selector of a time value.""" - - selector_type = "time" - - CONFIG_SCHEMA = vol.Schema({}) - - def __call__(self, data: Any) -> str: - """Validate the passed selection.""" - cv.time(data) - return cast(str, data) - - -@SELECTORS.register("target") -class TargetSelector(Selector): - """Selector of a target value (area ID, device ID, entity ID etc). - - Value should follow cv.TARGET_SERVICE_FIELDS format. - """ - - selector_type = "target" - - CONFIG_SCHEMA = vol.Schema( - { - vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, - vol.Optional("device"): DeviceSelector.CONFIG_SCHEMA, - } - ) - - TARGET_SELECTION_SCHEMA = vol.Schema(cv.TARGET_SERVICE_FIELDS) - - 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 - - -@SELECTORS.register("action") -class ActionSelector(Selector): - """Selector of an action sequence (script syntax).""" - - selector_type = "action" - - CONFIG_SCHEMA = vol.Schema({}) - - def __call__(self, data: Any) -> Any: - """Validate the passed selection.""" - return data - - @SELECTORS.register("object") class ObjectSelector(Selector): """Selector for an arbitrary object.""" @@ -316,6 +373,40 @@ class ObjectSelector(Selector): return data +select_option = vol.All( + dict, + vol.Schema( + { + vol.Required("value"): str, + vol.Required("label"): str, + } + ), +) + + +@SELECTORS.register("select") +class SelectSelector(Selector): + """Selector for an single-choice input select.""" + + selector_type = "select" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Required("options"): vol.All( + vol.Any([str], [select_option]), vol.Length(min=1) + ) + } + ) + + def __call__(self, data: Any) -> Any: + """Validate the passed selection.""" + if isinstance(self.config["options"][0], str): + options = self.config["options"] + else: + options = [option["value"] for option in self.config["options"]] + return vol.In(options)(vol.Schema(str)(data)) + + @SELECTORS.register("text") class StringSelector(Selector): """Selector for a multi-line text string.""" @@ -353,83 +444,28 @@ class StringSelector(Selector): return text -select_option = vol.All( - dict, - vol.Schema( - { - vol.Required("value"): str, - vol.Required("label"): str, - } - ), -) +@SELECTORS.register("target") +class TargetSelector(Selector): + """Selector of a target value (area ID, device ID, entity ID etc). + Value should follow cv.TARGET_SERVICE_FIELDS format. + """ -@SELECTORS.register("select") -class SelectSelector(Selector): - """Selector for an single-choice input select.""" - - selector_type = "select" + selector_type = "target" CONFIG_SCHEMA = vol.Schema( { - vol.Required("options"): vol.All( - vol.Any([str], [select_option]), vol.Length(min=1) - ) + vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, + vol.Optional("device"): DEVICE_SELECTOR_CONFIG_SCHEMA, } ) - def __call__(self, data: Any) -> Any: + TARGET_SELECTION_SCHEMA = vol.Schema(cv.TARGET_SERVICE_FIELDS) + + def __call__(self, data: Any) -> dict[str, list[str]]: """Validate the passed selection.""" - if isinstance(self.config["options"][0], str): - options = self.config["options"] - else: - options = [option["value"] for option in self.config["options"]] - return vol.In(options)(vol.Schema(str)(data)) - - -@SELECTORS.register("attribute") -class AttributeSelector(Selector): - """Selector for an entity attribute.""" - - selector_type = "attribute" - - CONFIG_SCHEMA = vol.Schema({vol.Required("entity_id"): cv.entity_id}) - - def __call__(self, data: Any) -> str: - """Validate the passed selection.""" - attribute: str = vol.Schema(str)(data) - return attribute - - -@SELECTORS.register("duration") -class DurationSelector(Selector): - """Selector for a duration.""" - - selector_type = "duration" - - CONFIG_SCHEMA = vol.Schema({}) - - def __call__(self, data: Any) -> dict[str, float]: - """Validate the passed selection.""" - cv.time_period_dict(data) - return cast(dict[str, float], data) - - -@SELECTORS.register("icon") -class IconSelector(Selector): - """Selector for an icon.""" - - selector_type = "icon" - - CONFIG_SCHEMA = vol.Schema( - {vol.Optional("placeholder"): str} - # Frontend also has a fallbackPath option, this is not used by core - ) - - def __call__(self, data: Any) -> str: - """Validate the passed selection.""" - icon: str = vol.Schema(str)(data) - return icon + target: dict[str, list[str]] = self.TARGET_SELECTION_SCHEMA(data) + return target @SELECTORS.register("theme") @@ -446,49 +482,15 @@ class ThemeSelector(Selector): return theme -@SELECTORS.register("media") -class MediaSelector(Selector): - """Selector for media.""" +@SELECTORS.register("time") +class TimeSelector(Selector): + """Selector of a time value.""" - selector_type = "media" + selector_type = "time" CONFIG_SCHEMA = vol.Schema({}) - DATA_SCHEMA = vol.Schema( - { - # Although marked as optional in frontend, this field is required - vol.Required("entity_id"): cv.entity_id_or_uuid, - # Although marked as optional in frontend, this field is required - vol.Required("media_content_id"): str, - # Although marked as optional in frontend, this field is required - vol.Required("media_content_type"): str, - vol.Remove("metadata"): dict, - } - ) - def __call__(self, data: Any) -> dict[str, float]: + def __call__(self, data: Any) -> str: """Validate the passed selection.""" - media: dict[str, float] = self.DATA_SCHEMA(data) - return media - - -@SELECTORS.register("location") -class LocationSelector(Selector): - """Selector for a location.""" - - selector_type = "location" - - CONFIG_SCHEMA = vol.Schema( - {vol.Optional("radius"): bool, vol.Optional("icon"): str} - ) - DATA_SCHEMA = vol.Schema( - { - vol.Required("latitude"): float, - vol.Required("longitude"): float, - vol.Optional("radius"): float, - } - ) - - def __call__(self, data: Any) -> dict[str, float]: - """Validate the passed selection.""" - location: dict[str, float] = self.DATA_SCHEMA(data) - return location + cv.time(data) + return cast(str, data)