From 162b734be756d48e5e36ec828e6703110b7e2f59 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Fri, 12 Jul 2024 12:50:02 -0300 Subject: [PATCH] Add config flow for select platform in Template (#121809) --- .../components/template/config_flow.py | 17 ++++- homeassistant/components/template/select.py | 60 ++++++++++++--- .../components/template/strings.json | 27 +++++++ .../template/snapshots/test_select.ambr | 19 +++++ tests/components/template/test_config_flow.py | 32 ++++++++ tests/components/template/test_init.py | 12 +++ tests/components/template/test_select.py | 74 ++++++++++++++++++- 7 files changed, 227 insertions(+), 14 deletions(-) create mode 100644 tests/components/template/snapshots/test_select.ambr diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index e6b7a436084..c52a890c1f7 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -41,6 +41,7 @@ from homeassistant.helpers.schema_config_entry_flow import ( from .binary_sensor import async_create_preview_binary_sensor from .const import CONF_PRESS, CONF_TURN_OFF, CONF_TURN_ON, DOMAIN +from .select import CONF_OPTIONS, CONF_SELECT_OPTION from .sensor import async_create_preview_sensor from .switch import async_create_preview_switch from .template_entity import TemplateEntity @@ -91,7 +92,12 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema: schema |= { vol.Required(CONF_URL): selector.TemplateSelector(), vol.Optional(CONF_VERIFY_SSL, default=True): selector.BooleanSelector(), - vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(), + } + + if domain == Platform.SELECT: + schema |= _SCHEMA_STATE | { + vol.Required(CONF_OPTIONS): selector.TemplateSelector(), + vol.Optional(CONF_SELECT_OPTION): selector.ActionSelector(), } if domain == Platform.SENSOR: @@ -232,6 +238,7 @@ TEMPLATE_TYPES = [ "binary_sensor", "button", "image", + "select", "sensor", "switch", ] @@ -251,6 +258,10 @@ CONFIG_FLOW = { config_schema(Platform.IMAGE), validate_user_input=validate_user_input(Platform.IMAGE), ), + Platform.SELECT: SchemaFlowFormStep( + config_schema(Platform.SELECT), + validate_user_input=validate_user_input(Platform.SELECT), + ), Platform.SENSOR: SchemaFlowFormStep( config_schema(Platform.SENSOR), preview="template", @@ -279,6 +290,10 @@ OPTIONS_FLOW = { options_schema(Platform.IMAGE), validate_user_input=validate_user_input(Platform.IMAGE), ), + Platform.SELECT: SchemaFlowFormStep( + options_schema(Platform.SELECT), + validate_user_input=validate_user_input(Platform.SELECT), + ), Platform.SENSOR: SchemaFlowFormStep( options_schema(Platform.SENSOR), preview="template", diff --git a/homeassistant/components/template/select.py b/homeassistant/components/template/select.py index 650b236faee..bd37ca1015c 100644 --- a/homeassistant/components/template/select.py +++ b/homeassistant/components/template/select.py @@ -13,9 +13,17 @@ from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SelectEntity, ) -from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, CONF_UNIQUE_ID +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_NAME, + CONF_OPTIMISTIC, + CONF_STATE, + CONF_UNIQUE_ID, +) from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, selector +from homeassistant.helpers.device import async_device_info_to_link_from_device_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -31,6 +39,7 @@ from .trigger_entity import TriggerEntity _LOGGER = logging.getLogger(__name__) +CONF_OPTIONS = "options" CONF_SELECT_OPTION = "select_option" DEFAULT_NAME = "Template Select" @@ -52,6 +61,17 @@ SELECT_SCHEMA = ( ) +SELECT_CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.template, + vol.Required(CONF_STATE): cv.template, + vol.Required(CONF_OPTIONS): cv.template, + vol.Optional(CONF_SELECT_OPTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(), + } +) + + async def _async_create_entities( hass: HomeAssistant, definitions: list[dict[str, Any]], unique_id_prefix: str | None ) -> list[TemplateSelect]: @@ -92,6 +112,18 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize config entry.""" + _options = dict(config_entry.options) + _options.pop("template_type") + validated_config = SELECT_CONFIG_SCHEMA(_options) + async_add_entities([TemplateSelect(hass, validated_config, config_entry.entry_id)]) + + class TemplateSelect(TemplateEntity, SelectEntity): """Representation of a template select.""" @@ -107,13 +139,18 @@ class TemplateSelect(TemplateEntity, SelectEntity): super().__init__(hass, config=config, unique_id=unique_id) assert self._attr_name is not None self._value_template = config[CONF_STATE] - self._command_select_option = Script( - hass, config[CONF_SELECT_OPTION], self._attr_name, DOMAIN - ) + if (selection_option := config.get(CONF_SELECT_OPTION)) is not None: + self._command_select_option = Script( + hass, selection_option, self._attr_name, DOMAIN + ) self._options_template = config[ATTR_OPTIONS] - self._attr_assumed_state = self._optimistic = config[CONF_OPTIMISTIC] + self._attr_assumed_state = self._optimistic = config.get(CONF_OPTIMISTIC, False) self._attr_options = [] self._attr_current_option = None + self._attr_device_info = async_device_info_to_link_from_device_id( + hass, + config.get(CONF_DEVICE_ID), + ) @callback def _async_setup_templates(self) -> None: @@ -137,11 +174,12 @@ class TemplateSelect(TemplateEntity, SelectEntity): if self._optimistic: self._attr_current_option = option self.async_write_ha_state() - await self.async_run_script( - self._command_select_option, - run_variables={ATTR_OPTION: option}, - context=self._context, - ) + if self._command_select_option: + await self.async_run_script( + self._command_select_option, + run_variables={ATTR_OPTION: option}, + context=self._context, + ) class TriggerSelectEntity(TriggerEntity, SelectEntity): diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 2c0a1cdf501..f004c342eab 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -37,6 +37,19 @@ }, "title": "Template image" }, + "select": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "name": "[%key:common::config_flow::data::name%]", + "state": "[%key:component::template::config::step::sensor::data::state%]", + "select_option": "Actions on select", + "options": "Available options" + }, + "data_description": { + "device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" + }, + "title": "Template select" + }, "sensor": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -57,6 +70,7 @@ "binary_sensor": "Template a binary sensor", "button": "Template a button", "image": "Template a image", + "select": "Template a select", "sensor": "Template a sensor", "switch": "Template a switch" }, @@ -110,6 +124,19 @@ }, "title": "[%key:component::template::config::step::image::title%]" }, + "select": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "name": "[%key:common::config_flow::data::name%]", + "state": "[%key:component::template::config::step::sensor::data::state%]", + "select_option": "[%key:component::template::config::step::select::data::select_option%]", + "options": "[%key:component::template::config::step::select::data::options%]" + }, + "data_description": { + "device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" + }, + "title": "[%key:component::template::config::step::select::title%]" + }, "sensor": { "data": { "device_id": "[%key:common::config_flow::data::device%]", diff --git a/tests/components/template/snapshots/test_select.ambr b/tests/components/template/snapshots/test_select.ambr new file mode 100644 index 00000000000..d4cabb2900f --- /dev/null +++ b/tests/components/template/snapshots/test_select.ambr @@ -0,0 +1,19 @@ +# serializer version: 1 +# name: test_setup_config_entry + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'My template', + 'options': Wrapper([ + 'off', + 'on', + 'auto', + ]), + }), + 'context': <ANY>, + 'entity_id': 'select.my_template', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': 'on', + }) +# --- \ No newline at end of file diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 14276bb355c..ff5db52d667 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -91,6 +91,16 @@ from tests.typing import WebSocketGenerator {"verify_ssl": True}, {}, ), + ( + "select", + {"state": "{{ states('select.one') }}"}, + "on", + {"one": "on", "two": "off"}, + {}, + {"options": "{{ ['off', 'on', 'auto'] }}"}, + {"options": "{{ ['off', 'on', 'auto'] }}"}, + {}, + ), ( "switch", {"value_template": "{{ states('switch.one') }}"}, @@ -216,6 +226,12 @@ async def test_config_flow( {"verify_ssl": True}, {"verify_ssl": True}, ), + ( + "select", + {"state": "{{ states('select.one') }}"}, + {"options": "{{ ['off', 'on', 'auto'] }}"}, + {"options": "{{ ['off', 'on', 'auto'] }}"}, + ), ], ) async def test_config_flow_device( @@ -386,6 +402,16 @@ def get_suggested(schema, key): }, "url", ), + ( + "select", + {"state": "{{ states('select.one') }}"}, + {"state": "{{ states('select.two') }}"}, + ["on", "off"], + {"one": "on", "two": "off"}, + {"options": "{{ ['off', 'on', 'auto'] }}"}, + {"options": "{{ ['off', 'on', 'auto'] }}"}, + "state", + ), ( "switch", {"value_template": "{{ states('switch.one') }}"}, @@ -1130,6 +1156,12 @@ async def test_option_flow_sensor_preview_config_entry_removed( {}, {}, ), + ( + "select", + {"state": "{{ states('select.one') }}"}, + {"options": "{{ ['off', 'on', 'auto'] }}"}, + {"options": "{{ ['off', 'on', 'auto'] }}"}, + ), ( "switch", {"value_template": "{{ false }}"}, diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index f1e5fe7f920..1face4bfda0 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -314,6 +314,18 @@ async def async_yaml_patch_helper(hass, filename): }, {}, ), + ( + { + "template_type": "select", + "name": "My template", + "state": "{{ 'on' }}", + "options": "{{ ['off', 'on', 'auto'] }}", + }, + { + "state": "{{ 'on' }}", + "options": "{{ ['off', 'on', 'auto'] }}", + }, + ), ( { "template_type": "switch", diff --git a/tests/components/template/test_select.py b/tests/components/template/test_select.py index 4106abdd469..2268c0840aa 100644 --- a/tests/components/template/test_select.py +++ b/tests/components/template/test_select.py @@ -1,5 +1,7 @@ """The tests for the Template select platform.""" +from syrupy.assertion import SnapshotAssertion + from homeassistant import setup from homeassistant.components.input_select import ( ATTR_OPTION as INPUT_SELECT_ATTR_OPTION, @@ -14,17 +16,45 @@ from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION as SELECT_SERVICE_SELECT_OPTION, ) +from homeassistant.components.template import DOMAIN from homeassistant.const import ATTR_ICON, CONF_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import Context, HomeAssistant, ServiceCall -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er -from tests.common import assert_setup_component, async_capture_events +from tests.common import MockConfigEntry, assert_setup_component, async_capture_events _TEST_SELECT = "select.template_select" # Represent for select's current_option _OPTION_INPUT_SELECT = "input_select.option" +async def test_setup_config_entry( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Test the config flow.""" + + template_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My template", + "template_type": "select", + "state": "{{ 'on' }}", + "options": "{{ ['off', 'on', 'auto'] }}", + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("select.my_template") + assert state is not None + assert state == snapshot + + async def test_missing_optional_config(hass: HomeAssistant) -> None: """Test: missing optional template is ok.""" with assert_setup_component(1, "template"): @@ -428,3 +458,43 @@ async def test_template_icon_with_trigger(hass: HomeAssistant) -> None: state = hass.states.get(_TEST_SELECT) assert state.state == "a" assert state.attributes[ATTR_ICON] == "mdi:greater" + + +async def test_device_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test for device for select template.""" + + device_config_entry = MockConfigEntry() + device_config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=device_config_entry.entry_id, + identifiers={("test", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + await hass.async_block_till_done() + assert device_entry is not None + assert device_entry.id is not None + + template_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My template", + "template_type": "select", + "state": "{{ 'on' }}", + "options": "{{ ['off', 'on', 'auto'] }}", + "device_id": device_entry.id, + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + template_entity = entity_registry.async_get("select.my_template") + assert template_entity is not None + assert template_entity.device_id == device_entry.id -- GitLab