Add config flow for select platform in Template (#121809)

This commit is contained in:
dougiteixeira 2024-07-12 12:50:02 -03:00 committed by GitHub
parent a2fab98358
commit 162b734be7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 227 additions and 14 deletions

View File

@ -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",

View File

@ -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):

View File

@ -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%]",

View File

@ -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',
})
# ---

View File

@ -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 }}"},

View File

@ -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",

View File

@ -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