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