Update template lock platform to new template entity framework (#162493)

This commit is contained in:
Petro31
2026-02-08 17:03:55 -05:00
committed by GitHub
parent 3e44d15fc1
commit a4204bf11e
5 changed files with 46 additions and 120 deletions

View File

@@ -103,6 +103,7 @@ class AbstractTemplateEntity(Entity):
attribute: str,
validator: Callable[[Any], Any] | None = None,
on_update: Callable[[Any], None] | None = None,
**kwargs,
) -> None:
"""Set up a template that manages any property or attribute of the entity.

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Generator, Sequence
from typing import TYPE_CHECKING, Any
import voluptuous as vol
@@ -26,13 +25,14 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError, TemplateError
from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import validators as template_validators
from .const import DOMAIN
from .coordinator import TriggerUpdateCoordinator
from .entity import AbstractTemplateEntity
@@ -152,26 +152,41 @@ class AbstractTemplateLock(AbstractTemplateEntity, LockEntity):
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
def __init__(self, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called
def __init__(self, name: str, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called
"""Initialize the features."""
self._code_format_template = config.get(CONF_CODE_FORMAT)
self._code_format_template_error: TemplateError | None = None
# Legacy behavior, create all locks as Unlocked.
self._set_state(LockState.UNLOCKED)
self.setup_state_template(
CONF_STATE,
"_lock_state",
template_validators.strenum(
self, CONF_STATE, LockState, LockState.LOCKED, LockState.UNLOCKED
),
self._set_state,
)
self.setup_template(
CONF_CODE_FORMAT,
"_attr_code_format",
None,
self._update_code_format,
none_on_template_error=False,
)
def _iterate_scripts(
self, config: dict[str, Any]
) -> Generator[tuple[str, Sequence[dict[str, Any]], LockEntityFeature | int]]:
for action_id, supported_feature in (
(CONF_LOCK, 0),
(CONF_UNLOCK, 0),
(CONF_OPEN, LockEntityFeature.OPEN),
):
if (action_config := config.get(action_id)) is not None:
yield (action_id, action_config, supported_feature)
self.add_script(action_id, action_config, name, DOMAIN)
self._attr_supported_features |= supported_feature
def _set_state(self, state: LockState | None) -> None:
if state is None:
self._attr_is_locked = None
return
self._attr_is_jammed = state == LockState.JAMMED
self._attr_is_opening = state == LockState.OPENING
self._attr_is_locking = state == LockState.LOCKING
@@ -179,33 +194,6 @@ class AbstractTemplateLock(AbstractTemplateEntity, LockEntity):
self._attr_is_unlocking = state == LockState.UNLOCKING
self._attr_is_locked = state == LockState.LOCKED
def _handle_state(self, result: Any) -> None:
if isinstance(result, bool):
self._set_state(LockState.LOCKED if result else LockState.UNLOCKED)
return
if isinstance(result, str):
if result.lower() in (
"true",
"on",
"locked",
):
self._set_state(LockState.LOCKED)
elif result.lower() in (
"false",
"off",
"unlocked",
):
self._set_state(LockState.UNLOCKED)
else:
try:
self._set_state(LockState(result.lower()))
except ValueError:
self._set_state(None)
return
self._set_state(None)
@callback
def _update_code_format(self, render: str | TemplateError | None):
"""Update code format from the template."""
@@ -281,7 +269,7 @@ class AbstractTemplateLock(AbstractTemplateEntity, LockEntity):
translation_key="code_format_template_error",
translation_placeholders={
"entity_id": self.entity_id,
"code_format_template": self._code_format_template.template,
"code_format_template": self._templates[CONF_CODE_FORMAT].template,
"cause": str(self._code_format_template_error),
},
)
@@ -300,45 +288,10 @@ class StateLockEntity(TemplateEntity, AbstractTemplateLock):
) -> None:
"""Initialize the lock."""
TemplateEntity.__init__(self, hass, config, unique_id)
AbstractTemplateLock.__init__(self, config)
name = self._attr_name
if TYPE_CHECKING:
assert name is not None
for action_id, action_config, supported_feature in self._iterate_scripts(
config
):
self.add_script(action_id, action_config, name, DOMAIN)
self._attr_supported_features |= supported_feature
@callback
def _update_state(self, result: str | TemplateError) -> None:
"""Update the state from the template."""
super()._update_state(result)
if isinstance(result, TemplateError):
self._attr_is_locked = None
return
self._handle_state(result)
@callback
def _async_setup_templates(self) -> None:
"""Set up templates."""
if self._template is not None:
self.add_template_attribute(
"_attr_is_locked",
self._template,
None,
self._update_state,
)
if self._code_format_template:
self.add_template_attribute(
"_attr_code_format",
self._code_format_template,
None,
self._update_code_format,
)
super()._async_setup_templates()
AbstractTemplateLock.__init__(self, name, config)
class TriggerLockEntity(TriggerEntity, AbstractTemplateLock):
@@ -354,45 +307,5 @@ class TriggerLockEntity(TriggerEntity, AbstractTemplateLock):
) -> None:
"""Initialize the entity."""
TriggerEntity.__init__(self, hass, coordinator, config)
AbstractTemplateLock.__init__(self, config)
self._attr_name = name = self._rendered.get(CONF_NAME, DEFAULT_NAME)
if CONF_STATE in config:
self._to_render_simple.append(CONF_STATE)
if isinstance(config.get(CONF_CODE_FORMAT), template.Template):
self._to_render_simple.append(CONF_CODE_FORMAT)
self._parse_result.add(CONF_CODE_FORMAT)
for action_id, action_config, supported_feature in self._iterate_scripts(
config
):
self.add_script(action_id, action_config, name, DOMAIN)
self._attr_supported_features |= supported_feature
@callback
def _handle_coordinator_update(self) -> None:
"""Handle update of the data."""
self._process_data()
if not self.available:
return
write_ha_state = False
for key, updater in (
(CONF_STATE, self._handle_state),
(CONF_CODE_FORMAT, self._update_code_format),
):
if (rendered := self._rendered.get(key)) is not None:
updater(rendered)
write_ha_state = True
if not self._attr_assumed_state:
write_ha_state = True
elif self._attr_assumed_state and len(self._rendered) > 0:
# In case any non optimistic template
write_ha_state = True
if write_ha_state:
self.async_write_ha_state()
AbstractTemplateLock.__init__(self, name, config)

View File

@@ -313,6 +313,7 @@ class TemplateEntity(AbstractTemplateEntity):
attribute: str,
validator: Callable[[Any], Any] | None = None,
on_update: Callable[[Any], None] | None = None,
**kwargs,
):
"""Set up a template that manages any property or attribute of the entity.
@@ -329,7 +330,10 @@ class TemplateEntity(AbstractTemplateEntity):
Called to store the template result rather than storing it
the supplied attribute. Passed the result of the validator.
"""
self.add_template(option, attribute, validator, on_update, True)
none_on_template_error = kwargs.get("none_on_template_error", True)
self.add_template(
option, attribute, validator, on_update, none_on_template_error
)
def add_template_attribute(
self,

View File

@@ -69,6 +69,7 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
attribute: str,
validator: Callable[[Any], Any] | None = None,
on_update: Callable[[Any], None] | None = None,
**kwargs,
) -> None:
"""Set up a template that manages any property or attribute of the entity.

View File

@@ -15,6 +15,7 @@ from homeassistant.const import (
STATE_ON,
STATE_OPEN,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import entity_registry as er
@@ -383,6 +384,10 @@ async def test_template_state_boolean_on(hass: HomeAssistant) -> None:
@pytest.mark.usefixtures("setup_state_lock")
async def test_template_state_boolean_off(hass: HomeAssistant) -> None:
"""Test the setting of the state with off."""
# Ensure the trigger executes for trigger configurations
hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON)
await hass.async_block_till_done()
state = hass.states.get(TEST_ENTITY_ID)
assert state.state == LockState.UNLOCKED
@@ -437,9 +442,6 @@ async def test_template_code_template_syntax_error(hass: HomeAssistant) -> None:
@pytest.mark.usefixtures("setup_state_lock")
async def test_template_static(hass: HomeAssistant) -> None:
"""Test that we allow static templates."""
state = hass.states.get(TEST_ENTITY_ID)
assert state.state == LockState.UNLOCKED
hass.states.async_set(TEST_ENTITY_ID, LockState.LOCKED)
await hass.async_block_till_done()
state = hass.states.get(TEST_ENTITY_ID)
@@ -457,6 +459,7 @@ async def test_template_static(hass: HomeAssistant) -> None:
("{{ True }}", LockState.LOCKED),
("{{ False }}", LockState.UNLOCKED),
("{{ x - 12 }}", STATE_UNAVAILABLE),
("{{ None }}", STATE_UNKNOWN),
],
)
@pytest.mark.usefixtures("setup_state_lock")
@@ -1163,7 +1166,7 @@ async def test_optimistic(hass: HomeAssistant) -> None:
"""Test configuration with optimistic state."""
state = hass.states.get(TEST_ENTITY_ID)
assert state.state == LockState.UNLOCKED
assert state.state == STATE_UNKNOWN
# Ensure Trigger template entities update.
hass.states.async_set(TEST_STATE_ENTITY_ID, "anything")
@@ -1219,6 +1222,10 @@ async def test_not_optimistic(hass: HomeAssistant) -> None:
blocking=True,
)
# Ensure Trigger template entities update.
hass.states.async_set(TEST_AVAILABILITY_ENTITY_ID, "anything")
await hass.async_block_till_done()
state = hass.states.get(TEST_ENTITY_ID)
assert state.state == LockState.UNLOCKED