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_UNIT_OF_MEASUREMENT,
CONF_URL,
CONF_VALUE_TEMPLATE,
CONF_VERIFY_SSL,
Platform,
)
@ -39,8 +40,9 @@ from homeassistant.helpers.schema_config_entry_flow import (
)
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 .switch import async_create_preview_switch
from .template_entity import TemplateEntity
_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()
return vol.Schema(schema)
@ -224,6 +233,7 @@ TEMPLATE_TYPES = [
"button",
"image",
"sensor",
"switch",
]
CONFIG_FLOW = {
@ -246,6 +256,11 @@ CONFIG_FLOW = {
preview="template",
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",
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[
@ -277,6 +297,7 @@ CREATE_PREVIEW_ENTITY: dict[
] = {
"binary_sensor": async_create_preview_binary_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_PRESS = "press"
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",
"button": "Template a button",
"image": "Template a image",
"sensor": "Template a sensor"
"sensor": "Template a sensor",
"switch": "Template a switch"
},
"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%]"
},
"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,
SwitchEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
CONF_DEVICE_ID,
CONF_NAME,
CONF_SWITCHES,
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
@ -22,14 +25,15 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
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_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.script import Script
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 (
TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY,
TemplateEntity,
@ -38,16 +42,13 @@ from .template_entity import (
_VALID_STATES = [STATE_ON, STATE_OFF, "true", "false"]
ON_ACTION = "turn_on"
OFF_ACTION = "turn_off"
SWITCH_SCHEMA = vol.All(
cv.deprecated(ATTR_ENTITY_ID),
vol.Schema(
{
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Required(ON_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(OFF_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
vol.Required(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
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)}
)
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):
"""Create the Template switches."""
@ -90,6 +101,29 @@ async def async_setup_platform(
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):
"""Representation of a Template switch."""
@ -106,15 +140,28 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity):
super().__init__(
hass, config=config, fallback_name=object_id, unique_id=unique_id
)
self.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, object_id, hass=hass
)
if object_id is not None:
self.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, object_id, hass=hass
)
friendly_name = self._attr_name
self._template = config.get(CONF_VALUE_TEMPLATE)
self._on_script = Script(hass, config[ON_ACTION], friendly_name, DOMAIN)
self._off_script = Script(hass, config[OFF_ACTION], friendly_name, DOMAIN)
self._on_script = (
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._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
def _update_state(self, result):
@ -159,14 +206,16 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Fire the on action."""
await self.async_run_script(self._on_script, context=self._context)
if self._on_script:
await self.async_run_script(self._on_script, context=self._context)
if self._template is None:
self._state = True
self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Fire the off action."""
await self.async_run_script(self._off_script, context=self._context)
if self._off_script:
await self.async_run_script(self._off_script, context=self._context)
if self._template is None:
self._state = False
self.async_write_ha_state()

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},
{},
),
(
"switch",
{"value_template": "{{ states('switch.one') }}"},
"on",
{"one": "on", "two": "off"},
{},
{},
{},
{},
),
],
)
@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",
{},
@ -295,6 +311,7 @@ def get_suggested(schema, key):
"input_states",
"extra_options",
"options_options",
"key_template",
),
[
(
@ -309,6 +326,7 @@ def get_suggested(schema, key):
{"one": "on", "two": "off"},
{},
{},
"state",
),
(
"sensor",
@ -322,6 +340,7 @@ def get_suggested(schema, key):
{"one": "30.0", "two": "20.0"},
{},
{},
"state",
),
(
"button",
@ -348,6 +367,7 @@ def get_suggested(schema, key):
}
],
},
"state",
),
(
"image",
@ -364,6 +384,17 @@ def get_suggested(schema, key):
"url": "{{ states('sensor.two') }}",
"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,
extra_options,
options_options,
key_template,
) -> None:
"""Test reconfiguring."""
input_entities = ["one", "two"]
@ -411,13 +443,16 @@ async def test_options(
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == template_type
assert get_suggested(
result["data_schema"].schema, "state"
) == old_state_template.get("state")
result["data_schema"].schema, key_template
) == old_state_template.get(key_template)
assert "name" not in result["data_schema"].schema
result = await hass.config_entries.options.async_configure(
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["data"] == {
@ -455,7 +490,7 @@ async def test_options(
assert result["step_id"] == template_type
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(
@ -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(

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(

View File

@ -1,8 +1,10 @@
"""The tests for the Template switch platform."""
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant import setup
from homeassistant.components import template
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
@ -13,9 +15,15 @@ from homeassistant.const import (
STATE_UNAVAILABLE,
)
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 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 = {
"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:
"""Test the state text of a template."""
with assert_setup_component(1, "switch"):
@ -655,3 +695,42 @@ async def test_unique_id(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
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