Add config flow for platform switch in Template (#121639)
parent
f94b28f72d
commit
52454f5218
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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',
|
||||
})
|
||||
# ---
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue