mirror of
https://github.com/home-assistant/core.git
synced 2026-04-27 05:57:53 +00:00
Update template lock platform to new template entity framework (#162493)
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user