Add config flow for platform switch in Template (#121639)

pull/118833/head
dougiteixeira 2024-07-11 05:11:31 -03:00 committed by GitHub
parent f94b28f72d
commit 52454f5218
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 264 additions and 21 deletions

View File

@ -25,6 +25,7 @@ from homeassistant.const import (
CONF_STATE, CONF_STATE,
CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_OF_MEASUREMENT,
CONF_URL, CONF_URL,
CONF_VALUE_TEMPLATE,
CONF_VERIFY_SSL, CONF_VERIFY_SSL,
Platform, Platform,
) )
@ -39,8 +40,9 @@ from homeassistant.helpers.schema_config_entry_flow import (
) )
from .binary_sensor import async_create_preview_binary_sensor from .binary_sensor import async_create_preview_binary_sensor
from .const import CONF_PRESS, DOMAIN from .const import CONF_PRESS, CONF_TURN_OFF, CONF_TURN_ON, DOMAIN
from .sensor import async_create_preview_sensor from .sensor import async_create_preview_sensor
from .switch import async_create_preview_switch
from .template_entity import TemplateEntity from .template_entity import TemplateEntity
_SCHEMA_STATE: dict[vol.Marker, Any] = { _SCHEMA_STATE: dict[vol.Marker, Any] = {
@ -132,6 +134,13 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema:
), ),
} }
if domain == Platform.SWITCH:
schema |= {
vol.Required(CONF_VALUE_TEMPLATE): selector.TemplateSelector(),
vol.Optional(CONF_TURN_ON): selector.ActionSelector(),
vol.Optional(CONF_TURN_OFF): selector.ActionSelector(),
}
schema[vol.Optional(CONF_DEVICE_ID)] = selector.DeviceSelector() schema[vol.Optional(CONF_DEVICE_ID)] = selector.DeviceSelector()
return vol.Schema(schema) return vol.Schema(schema)
@ -224,6 +233,7 @@ TEMPLATE_TYPES = [
"button", "button",
"image", "image",
"sensor", "sensor",
"switch",
] ]
CONFIG_FLOW = { CONFIG_FLOW = {
@ -246,6 +256,11 @@ CONFIG_FLOW = {
preview="template", preview="template",
validate_user_input=validate_user_input(Platform.SENSOR), validate_user_input=validate_user_input(Platform.SENSOR),
), ),
Platform.SWITCH: SchemaFlowFormStep(
config_schema(Platform.SWITCH),
preview="template",
validate_user_input=validate_user_input(Platform.SWITCH),
),
} }
@ -269,6 +284,11 @@ OPTIONS_FLOW = {
preview="template", preview="template",
validate_user_input=validate_user_input(Platform.SENSOR), validate_user_input=validate_user_input(Platform.SENSOR),
), ),
Platform.SWITCH: SchemaFlowFormStep(
options_schema(Platform.SWITCH),
preview="template",
validate_user_input=validate_user_input(Platform.SWITCH),
),
} }
CREATE_PREVIEW_ENTITY: dict[ CREATE_PREVIEW_ENTITY: dict[
@ -277,6 +297,7 @@ CREATE_PREVIEW_ENTITY: dict[
] = { ] = {
"binary_sensor": async_create_preview_binary_sensor, "binary_sensor": async_create_preview_binary_sensor,
"sensor": async_create_preview_sensor, "sensor": async_create_preview_sensor,
"switch": async_create_preview_switch,
} }

View File

@ -34,3 +34,5 @@ CONF_ATTRIBUTE_TEMPLATES = "attribute_templates"
CONF_PICTURE = "picture" CONF_PICTURE = "picture"
CONF_PRESS = "press" CONF_PRESS = "press"
CONF_OBJECT_ID = "object_id" CONF_OBJECT_ID = "object_id"
CONF_TURN_OFF = "turn_off"
CONF_TURN_ON = "turn_on"

View File

@ -57,9 +57,23 @@
"binary_sensor": "Template a binary sensor", "binary_sensor": "Template a binary sensor",
"button": "Template a button", "button": "Template a button",
"image": "Template a image", "image": "Template a image",
"sensor": "Template a sensor" "sensor": "Template a sensor",
"switch": "Template a switch"
}, },
"title": "Template helper" "title": "Template helper"
},
"switch": {
"data": {
"device_id": "[%key:common::config_flow::data::device%]",
"name": "[%key:common::config_flow::data::name%]",
"turn_off": "Actions on turn off",
"turn_on": "Actions on turn on",
"state": "[%key:component::template::config::step::sensor::data::state%]"
},
"data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]"
},
"title": "Template switch"
} }
} }
}, },
@ -108,6 +122,19 @@
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" "device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]"
}, },
"title": "[%key:component::template::config::step::sensor::title%]" "title": "[%key:component::template::config::step::sensor::title%]"
},
"switch": {
"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%]",
"turn_off": "[%key:component::template::config::step::switch::data::turn_off%]",
"turn_on": "[%key:component::template::config::step::switch::data::turn_on%]"
},
"data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]"
},
"title": "[%key:component::template::config::step::switch::title%]"
} }
} }
}, },

View File

@ -11,9 +11,12 @@ from homeassistant.components.switch import (
PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA, PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA,
SwitchEntity, SwitchEntity,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME, ATTR_FRIENDLY_NAME,
CONF_DEVICE_ID,
CONF_NAME,
CONF_SWITCHES, CONF_SWITCHES,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE,
@ -22,14 +25,15 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
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 import async_generate_entity_id from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.script import Script from homeassistant.helpers.script import Script
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN from .const import CONF_TURN_OFF, CONF_TURN_ON, DOMAIN
from .template_entity import ( from .template_entity import (
TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY, TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY,
TemplateEntity, TemplateEntity,
@ -38,16 +42,13 @@ from .template_entity import (
_VALID_STATES = [STATE_ON, STATE_OFF, "true", "false"] _VALID_STATES = [STATE_ON, STATE_OFF, "true", "false"]
ON_ACTION = "turn_on"
OFF_ACTION = "turn_off"
SWITCH_SCHEMA = vol.All( SWITCH_SCHEMA = vol.All(
cv.deprecated(ATTR_ENTITY_ID), cv.deprecated(ATTR_ENTITY_ID),
vol.Schema( vol.Schema(
{ {
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Required(ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
vol.Required(OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string,
@ -59,6 +60,16 @@ PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend(
{vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_SCHEMA)} {vol.Required(CONF_SWITCHES): cv.schema_with_slug_keys(SWITCH_SCHEMA)}
) )
SWICTH_CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.template,
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_TURN_ON): selector.ActionSelector(),
vol.Optional(CONF_TURN_OFF): selector.ActionSelector(),
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
}
)
async def _async_create_entities(hass, config): async def _async_create_entities(hass, config):
"""Create the Template switches.""" """Create the Template switches."""
@ -90,6 +101,29 @@ async def async_setup_platform(
async_add_entities(await _async_create_entities(hass, config)) async_add_entities(await _async_create_entities(hass, config))
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 = SWICTH_CONFIG_SCHEMA(_options)
async_add_entities(
[SwitchTemplate(hass, None, validated_config, config_entry.entry_id)]
)
@callback
def async_create_preview_switch(
hass: HomeAssistant, name: str, config: dict[str, Any]
) -> SwitchTemplate:
"""Create a preview switch."""
validated_config = SWICTH_CONFIG_SCHEMA(config | {CONF_NAME: name})
return SwitchTemplate(hass, None, validated_config, None)
class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity):
"""Representation of a Template switch.""" """Representation of a Template switch."""
@ -106,15 +140,28 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity):
super().__init__( super().__init__(
hass, config=config, fallback_name=object_id, unique_id=unique_id hass, config=config, fallback_name=object_id, unique_id=unique_id
) )
if object_id is not None:
self.entity_id = async_generate_entity_id( self.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, object_id, hass=hass ENTITY_ID_FORMAT, object_id, hass=hass
) )
friendly_name = self._attr_name friendly_name = self._attr_name
self._template = config.get(CONF_VALUE_TEMPLATE) self._template = config.get(CONF_VALUE_TEMPLATE)
self._on_script = Script(hass, config[ON_ACTION], friendly_name, DOMAIN) self._on_script = (
self._off_script = Script(hass, config[OFF_ACTION], friendly_name, DOMAIN) Script(hass, config.get(CONF_TURN_ON), friendly_name, DOMAIN)
if config.get(CONF_TURN_ON) is not None
else None
)
self._off_script = (
Script(hass, config.get(CONF_TURN_OFF), friendly_name, DOMAIN)
if config.get(CONF_TURN_OFF) is not None
else None
)
self._state: bool | None = False self._state: bool | None = False
self._attr_assumed_state = self._template is None self._attr_assumed_state = self._template is None
self._attr_device_info = async_device_info_to_link_from_device_id(
hass,
config.get(CONF_DEVICE_ID),
)
@callback @callback
def _update_state(self, result): def _update_state(self, result):
@ -159,6 +206,7 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity):
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Fire the on action.""" """Fire the on action."""
if self._on_script:
await self.async_run_script(self._on_script, context=self._context) await self.async_run_script(self._on_script, context=self._context)
if self._template is None: if self._template is None:
self._state = True self._state = True
@ -166,6 +214,7 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity):
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Fire the off action.""" """Fire the off action."""
if self._off_script:
await self.async_run_script(self._off_script, context=self._context) await self.async_run_script(self._off_script, context=self._context)
if self._template is None: if self._template is None:
self._state = False self._state = False

View File

@ -0,0 +1,14 @@
# serializer version: 1
# name: test_setup_config_entry
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'My template',
}),
'context': <ANY>,
'entity_id': 'switch.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}, {"verify_ssl": True},
{}, {},
), ),
(
"switch",
{"value_template": "{{ states('switch.one') }}"},
"on",
{"one": "on", "two": "off"},
{},
{},
{},
{},
),
], ],
) )
@pytest.mark.freeze_time("2024-07-09 00:00:00+00:00") @pytest.mark.freeze_time("2024-07-09 00:00:00+00:00")
@ -186,6 +196,12 @@ async def test_config_flow(
{}, {},
{}, {},
), ),
(
"switch",
{"value_template": "{{ false }}"},
{},
{},
),
( (
"button", "button",
{}, {},
@ -295,6 +311,7 @@ def get_suggested(schema, key):
"input_states", "input_states",
"extra_options", "extra_options",
"options_options", "options_options",
"key_template",
), ),
[ [
( (
@ -309,6 +326,7 @@ def get_suggested(schema, key):
{"one": "on", "two": "off"}, {"one": "on", "two": "off"},
{}, {},
{}, {},
"state",
), ),
( (
"sensor", "sensor",
@ -322,6 +340,7 @@ def get_suggested(schema, key):
{"one": "30.0", "two": "20.0"}, {"one": "30.0", "two": "20.0"},
{}, {},
{}, {},
"state",
), ),
( (
"button", "button",
@ -348,6 +367,7 @@ def get_suggested(schema, key):
} }
], ],
}, },
"state",
), ),
( (
"image", "image",
@ -364,6 +384,17 @@ def get_suggested(schema, key):
"url": "{{ states('sensor.two') }}", "url": "{{ states('sensor.two') }}",
"verify_ssl": True, "verify_ssl": True,
}, },
"url",
),
(
"switch",
{"value_template": "{{ states('switch.one') }}"},
{"value_template": "{{ states('switch.two') }}"},
["on", "off"],
{"one": "on", "two": "off"},
{},
{},
"value_template",
), ),
], ],
) )
@ -377,6 +408,7 @@ async def test_options(
input_states, input_states,
extra_options, extra_options,
options_options, options_options,
key_template,
) -> None: ) -> None:
"""Test reconfiguring.""" """Test reconfiguring."""
input_entities = ["one", "two"] input_entities = ["one", "two"]
@ -411,13 +443,16 @@ async def test_options(
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == template_type assert result["step_id"] == template_type
assert get_suggested( assert get_suggested(
result["data_schema"].schema, "state" result["data_schema"].schema, key_template
) == old_state_template.get("state") ) == old_state_template.get(key_template)
assert "name" not in result["data_schema"].schema assert "name" not in result["data_schema"].schema
result = await hass.config_entries.options.async_configure( result = await hass.config_entries.options.async_configure(
result["flow_id"], result["flow_id"],
user_input={**new_state_template, **options_options}, user_input={
**new_state_template,
**options_options,
},
) )
assert result["type"] is FlowResultType.CREATE_ENTRY assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == { assert result["data"] == {
@ -455,7 +490,7 @@ async def test_options(
assert result["step_id"] == template_type assert result["step_id"] == template_type
assert get_suggested(result["data_schema"].schema, "name") is None assert get_suggested(result["data_schema"].schema, "name") is None
assert get_suggested(result["data_schema"].schema, "state") is None assert get_suggested(result["data_schema"].schema, key_template) is None
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -1095,6 +1130,12 @@ async def test_option_flow_sensor_preview_config_entry_removed(
{}, {},
{}, {},
), ),
(
"switch",
{"value_template": "{{ false }}"},
{},
{},
),
], ],
) )
async def test_options_flow_change_device( async def test_options_flow_change_device(

View File

@ -314,6 +314,16 @@ async def async_yaml_patch_helper(hass, filename):
}, },
{}, {},
), ),
(
{
"template_type": "switch",
"name": "My template",
"value_template": "{{ true }}",
},
{
"value_template": "{{ true }}",
},
),
], ],
) )
async def test_change_device( async def test_change_device(

View File

@ -1,8 +1,10 @@
"""The tests for the Template switch platform.""" """The tests for the Template switch platform."""
import pytest import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant import setup from homeassistant import setup
from homeassistant.components import template
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
@ -13,9 +15,15 @@ from homeassistant.const import (
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
) )
from homeassistant.core import CoreState, HomeAssistant, ServiceCall, State from homeassistant.core import CoreState, HomeAssistant, ServiceCall, State
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import assert_setup_component, mock_component, mock_restore_cache from tests.common import (
MockConfigEntry,
assert_setup_component,
mock_component,
mock_restore_cache,
)
OPTIMISTIC_SWITCH_CONFIG = { OPTIMISTIC_SWITCH_CONFIG = {
"turn_on": { "turn_on": {
@ -35,6 +43,38 @@ OPTIMISTIC_SWITCH_CONFIG = {
} }
async def test_setup_config_entry(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
) -> None:
"""Test the config flow."""
hass.states.async_set(
"switch.one",
"on",
{},
)
template_config_entry = MockConfigEntry(
data={},
domain=template.DOMAIN,
options={
"name": "My template",
"value_template": "{{ states('switch.one') }}",
"template_type": SWITCH_DOMAIN,
},
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("switch.my_template")
assert state is not None
assert state == snapshot
async def test_template_state_text(hass: HomeAssistant) -> None: async def test_template_state_text(hass: HomeAssistant) -> None:
"""Test the state text of a template.""" """Test the state text of a template."""
with assert_setup_component(1, "switch"): with assert_setup_component(1, "switch"):
@ -655,3 +695,42 @@ async def test_unique_id(hass: HomeAssistant) -> None:
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_all("switch")) == 1 assert len(hass.states.async_all("switch")) == 1
async def test_device_id(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test for device for 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=template.DOMAIN,
options={
"name": "My template",
"value_template": "{{ true }}",
"template_type": "switch",
"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("switch.my_template")
assert template_entity is not None
assert template_entity.device_id == device_entry.id