Add config flow to template lock platform (#149449)

This commit is contained in:
Petro31 2025-07-30 11:04:39 -04:00 committed by GitHub
parent d481a694f1
commit 6306baa3c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 217 additions and 4 deletions

View File

@ -89,6 +89,7 @@ from .light import (
CONF_TEMPERATURE_ACTION, CONF_TEMPERATURE_ACTION,
async_create_preview_light, async_create_preview_light,
) )
from .lock import CONF_LOCK, CONF_OPEN, CONF_UNLOCK, async_create_preview_lock
from .number import ( from .number import (
CONF_MAX, CONF_MAX,
CONF_MIN, CONF_MIN,
@ -233,6 +234,14 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema:
vol.Optional(CONF_TEMPERATURE_ACTION): selector.ActionSelector(), vol.Optional(CONF_TEMPERATURE_ACTION): selector.ActionSelector(),
} }
if domain == Platform.LOCK:
schema |= _SCHEMA_STATE | {
vol.Required(CONF_LOCK): selector.ActionSelector(),
vol.Required(CONF_UNLOCK): selector.ActionSelector(),
vol.Optional(CONF_CODE_FORMAT): selector.TemplateSelector(),
vol.Optional(CONF_OPEN): selector.ActionSelector(),
}
if domain == Platform.NUMBER: if domain == Platform.NUMBER:
schema |= { schema |= {
vol.Required(CONF_STATE): selector.TemplateSelector(), vol.Required(CONF_STATE): selector.TemplateSelector(),
@ -435,6 +444,7 @@ TEMPLATE_TYPES = [
Platform.FAN, Platform.FAN,
Platform.IMAGE, Platform.IMAGE,
Platform.LIGHT, Platform.LIGHT,
Platform.LOCK,
Platform.NUMBER, Platform.NUMBER,
Platform.SELECT, Platform.SELECT,
Platform.SENSOR, Platform.SENSOR,
@ -478,6 +488,11 @@ CONFIG_FLOW = {
preview="template", preview="template",
validate_user_input=validate_user_input(Platform.LIGHT), validate_user_input=validate_user_input(Platform.LIGHT),
), ),
Platform.LOCK: SchemaFlowFormStep(
config_schema(Platform.LOCK),
preview="template",
validate_user_input=validate_user_input(Platform.LOCK),
),
Platform.NUMBER: SchemaFlowFormStep( Platform.NUMBER: SchemaFlowFormStep(
config_schema(Platform.NUMBER), config_schema(Platform.NUMBER),
preview="template", preview="template",
@ -542,6 +557,11 @@ OPTIONS_FLOW = {
preview="template", preview="template",
validate_user_input=validate_user_input(Platform.LIGHT), validate_user_input=validate_user_input(Platform.LIGHT),
), ),
Platform.LOCK: SchemaFlowFormStep(
options_schema(Platform.LOCK),
preview="template",
validate_user_input=validate_user_input(Platform.LOCK),
),
Platform.NUMBER: SchemaFlowFormStep( Platform.NUMBER: SchemaFlowFormStep(
options_schema(Platform.NUMBER), options_schema(Platform.NUMBER),
preview="template", preview="template",
@ -578,6 +598,7 @@ CREATE_PREVIEW_ENTITY: dict[
Platform.COVER: async_create_preview_cover, Platform.COVER: async_create_preview_cover,
Platform.FAN: async_create_preview_fan, Platform.FAN: async_create_preview_fan,
Platform.LIGHT: async_create_preview_light, Platform.LIGHT: async_create_preview_light,
Platform.LOCK: async_create_preview_lock,
Platform.NUMBER: async_create_preview_number, Platform.NUMBER: async_create_preview_number,
Platform.SELECT: async_create_preview_select, Platform.SELECT: async_create_preview_select,
Platform.SENSOR: async_create_preview_sensor, Platform.SENSOR: async_create_preview_sensor,

View File

@ -15,6 +15,7 @@ from homeassistant.components.lock import (
LockEntityFeature, LockEntityFeature,
LockState, LockState,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_CODE, ATTR_CODE,
CONF_NAME, CONF_NAME,
@ -26,15 +27,23 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError, TemplateError from homeassistant.exceptions import ServiceValidationError, TemplateError
from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN from .const import DOMAIN
from .coordinator import TriggerUpdateCoordinator from .coordinator import TriggerUpdateCoordinator
from .entity import AbstractTemplateEntity from .entity import AbstractTemplateEntity
from .helpers import async_setup_template_platform from .helpers import (
async_setup_template_entry,
async_setup_template_platform,
async_setup_template_preview,
)
from .template_entity import ( from .template_entity import (
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY, TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY,
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA,
TemplateEntity, TemplateEntity,
make_template_entity_common_modern_schema, make_template_entity_common_modern_schema,
@ -82,6 +91,10 @@ PLATFORM_SCHEMA = LOCK_PLATFORM_SCHEMA.extend(
} }
).extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY.schema) ).extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY.schema)
LOCK_CONFIG_ENTRY_SCHEMA = LOCK_COMMON_SCHEMA.extend(
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema
)
async def async_setup_platform( async def async_setup_platform(
hass: HomeAssistant, hass: HomeAssistant,
@ -102,6 +115,35 @@ async def async_setup_platform(
) )
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize config entry."""
await async_setup_template_entry(
hass,
config_entry,
async_add_entities,
StateLockEntity,
LOCK_CONFIG_ENTRY_SCHEMA,
)
@callback
def async_create_preview_lock(
hass: HomeAssistant, name: str, config: dict[str, Any]
) -> StateLockEntity:
"""Create a preview."""
return async_setup_template_preview(
hass,
name,
config,
StateLockEntity,
LOCK_CONFIG_ENTRY_SCHEMA,
)
class AbstractTemplateLock(AbstractTemplateEntity, LockEntity): class AbstractTemplateLock(AbstractTemplateEntity, LockEntity):
"""Representation of a template lock features.""" """Representation of a template lock features."""

View File

@ -188,6 +188,29 @@
}, },
"title": "Template light" "title": "Template light"
}, },
"lock": {
"data": {
"device_id": "[%key:common::config_flow::data::device%]",
"name": "[%key:common::config_flow::data::name%]",
"state": "[%key:component::template::common::state%]",
"lock": "Actions on lock",
"unlock": "Actions on unlock",
"code_format": "[%key:component::template::common::code_format%]",
"open": "Actions on open"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
}
}
},
"title": "Template lock"
},
"number": { "number": {
"data": { "data": {
"device_id": "[%key:common::config_flow::data::device%]", "device_id": "[%key:common::config_flow::data::device%]",
@ -265,6 +288,7 @@
"fan": "Template a fan", "fan": "Template a fan",
"image": "Template an image", "image": "Template an image",
"light": "Template a light", "light": "Template a light",
"lock": "Template a lock",
"number": "Template a number", "number": "Template a number",
"select": "Template a select", "select": "Template a select",
"sensor": "Template a sensor", "sensor": "Template a sensor",
@ -495,6 +519,28 @@
}, },
"title": "[%key:component::template::config::step::light::title%]" "title": "[%key:component::template::config::step::light::title%]"
}, },
"lock": {
"data": {
"device_id": "[%key:common::config_flow::data::device%]",
"state": "[%key:component::template::common::state%]",
"lock": "[%key:component::template::config::step::lock::data::lock%]",
"unlock": "[%key:component::template::config::step::lock::data::unlock%]",
"code_format": "[%key:component::template::common::code_format%]",
"open": "[%key:component::template::config::step::lock::data::open%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
}
}
},
"title": "[%key:component::template::config::step::lock::title%]"
},
"number": { "number": {
"data": { "data": {
"device_id": "[%key:common::config_flow::data::device%]", "device_id": "[%key:common::config_flow::data::device%]",

View File

@ -0,0 +1,15 @@
# serializer version: 1
# name: test_setup_config_entry
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'My template',
'supported_features': <LockEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'lock.my_template',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'locked',
})
# ---

View File

@ -179,6 +179,16 @@ BINARY_SENSOR_OPTIONS = {
{"turn_on": [], "turn_off": []}, {"turn_on": [], "turn_off": []},
{}, {},
), ),
(
"lock",
{"state": "{{ states('lock.one') }}"},
"locked",
{"one": "locked", "two": "unlocked"},
{},
{"lock": [], "unlock": []},
{"lock": [], "unlock": []},
{},
),
( (
"number", "number",
{"state": "{{ states('number.one') }}"}, {"state": "{{ states('number.one') }}"},
@ -372,6 +382,12 @@ async def test_config_flow(
{"turn_on": [], "turn_off": []}, {"turn_on": [], "turn_off": []},
{"turn_on": [], "turn_off": []}, {"turn_on": [], "turn_off": []},
), ),
(
"lock",
{"state": "{{ states('lock.one') }}"},
{"lock": [], "unlock": []},
{"lock": [], "unlock": []},
),
( (
"number", "number",
{"state": "{{ states('number.one') }}"}, {"state": "{{ states('number.one') }}"},
@ -603,6 +619,16 @@ async def test_config_flow_device(
{"turn_on": [], "turn_off": []}, {"turn_on": [], "turn_off": []},
"state", "state",
), ),
(
"lock",
{"state": "{{ states('lock.one') }}"},
{"state": "{{ states('lock.two') }}"},
["locked", "unlocked"],
{"one": "locked", "two": "unlocked"},
{"lock": [], "unlock": []},
{"lock": [], "unlock": []},
"state",
),
( (
"number", "number",
{"state": "{{ states('number.one') }}"}, {"state": "{{ states('number.one') }}"},
@ -1464,6 +1490,12 @@ async def test_option_flow_sensor_preview_config_entry_removed(
{"turn_on": [], "turn_off": []}, {"turn_on": [], "turn_off": []},
{"turn_on": [], "turn_off": []}, {"turn_on": [], "turn_off": []},
), ),
(
"lock",
{"state": "{{ states('lock.one') }}"},
{"lock": [], "unlock": []},
{"lock": [], "unlock": []},
),
( (
"number", "number",
{"state": "{{ states('number.one') }}"}, {"state": "{{ states('number.one') }}"},

View File

@ -3,6 +3,7 @@
from typing import Any from typing import Any
import pytest import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant import setup from homeassistant import setup
from homeassistant.components import lock, template from homeassistant.components import lock, template
@ -19,9 +20,10 @@ from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from .conftest import ConfigurationStyle from .conftest import ConfigurationStyle, async_get_flow_preview_state
from tests.common import assert_setup_component from tests.common import MockConfigEntry, assert_setup_component
from tests.typing import WebSocketGenerator
TEST_OBJECT_ID = "test_template_lock" TEST_OBJECT_ID = "test_template_lock"
TEST_ENTITY_ID = f"lock.{TEST_OBJECT_ID}" TEST_ENTITY_ID = f"lock.{TEST_OBJECT_ID}"
@ -1186,3 +1188,58 @@ async def test_optimistic(hass: HomeAssistant) -> None:
state = hass.states.get(TEST_ENTITY_ID) state = hass.states.get(TEST_ENTITY_ID)
assert state.state == LockState.UNLOCKED assert state.state == LockState.UNLOCKED
async def test_setup_config_entry(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
) -> None:
"""Tests creating a lock from a config entry."""
hass.states.async_set(
"sensor.test_state",
LockState.LOCKED,
{},
)
template_config_entry = MockConfigEntry(
data={},
domain=template.DOMAIN,
options={
"name": "My template",
"state": "{{ states('sensor.test_state') }}",
"lock": [],
"unlock": [],
"template_type": lock.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("lock.my_template")
assert state is not None
assert state == snapshot
async def test_flow_preview(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test the config flow preview."""
state = await async_get_flow_preview_state(
hass,
hass_ws_client,
lock.DOMAIN,
{
"name": "My template",
"state": "{{ 'locked' }}",
"lock": [],
"unlock": [],
},
)
assert state["state"] == LockState.LOCKED