mirror of
https://github.com/home-assistant/core.git
synced 2026-07-05 21:55:36 +01:00
308 lines
10 KiB
Python
308 lines
10 KiB
Python
"""Template entity base class."""
|
|
|
|
from abc import abstractmethod
|
|
from collections.abc import Callable, Sequence
|
|
from dataclasses import dataclass
|
|
from typing import Any, override
|
|
|
|
from homeassistant.const import (
|
|
ATTR_ENTITY_PICTURE,
|
|
ATTR_FRIENDLY_NAME,
|
|
ATTR_ICON,
|
|
CONF_DEVICE_ID,
|
|
CONF_ICON,
|
|
CONF_NAME,
|
|
CONF_OPTIMISTIC,
|
|
STATE_UNAVAILABLE,
|
|
STATE_UNKNOWN,
|
|
)
|
|
from homeassistant.core import Context, HomeAssistant, State, callback
|
|
from homeassistant.helpers import device_registry as dr
|
|
from homeassistant.helpers.entity import Entity, async_generate_entity_id
|
|
from homeassistant.helpers.script import Script, _VarsType
|
|
from homeassistant.helpers.template import Template, TemplateStateFromEntityId
|
|
from homeassistant.helpers.typing import ConfigType
|
|
|
|
from .const import CONF_ATTRIBUTES, CONF_DEFAULT_ENTITY_ID, CONF_PICTURE
|
|
|
|
_SENTINEL = object()
|
|
|
|
|
|
@dataclass
|
|
class EntityTemplate:
|
|
"""Information class for properly handling template results."""
|
|
|
|
attribute: str
|
|
template: Template
|
|
validator: Callable[[Any], Any] | None
|
|
on_update: Callable[[Any], None] | None
|
|
none_on_template_error: bool
|
|
|
|
|
|
class AbstractTemplateEntity(Entity):
|
|
"""Actions linked to a template entity."""
|
|
|
|
_entity_id_format: str
|
|
_optimistic_entity: bool = False
|
|
_extra_optimistic_options: tuple[str, ...] | None = None
|
|
_state_option: str | None = None
|
|
_restore_state_extra_data: Any | None = None
|
|
|
|
# Restore state properties. The state will be restored if set to None.
|
|
# If a tuple is supplied, all properties must be None for the state to restore.
|
|
_restore_state_properties: tuple[str, ...] | None = None
|
|
|
|
def __init__(
|
|
self,
|
|
hass: HomeAssistant,
|
|
config: ConfigType,
|
|
) -> None:
|
|
"""Initialize the entity."""
|
|
|
|
self.hass = hass
|
|
self._config = config
|
|
self._templates: dict[str, EntityTemplate] = {}
|
|
self._action_scripts: dict[str, Script] = {}
|
|
self._attr_extra_state_attributes = {}
|
|
self._attribute_templates: dict[str, Template] | None = config.get(
|
|
CONF_ATTRIBUTES
|
|
)
|
|
|
|
if self._optimistic_entity:
|
|
optimistic = config.get(CONF_OPTIMISTIC)
|
|
|
|
if self._state_option is not None:
|
|
assumed_optimistic = config.get(self._state_option) is None
|
|
if self._extra_optimistic_options:
|
|
assumed_optimistic = assumed_optimistic and all(
|
|
config.get(option) is None
|
|
for option in self._extra_optimistic_options
|
|
)
|
|
|
|
self._attr_assumed_state = optimistic or (
|
|
optimistic is None and assumed_optimistic
|
|
)
|
|
|
|
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
|
|
_, _, object_id = default_entity_id.partition(".")
|
|
self.entity_id = async_generate_entity_id(
|
|
self._entity_id_format, object_id, hass=self.hass
|
|
)
|
|
|
|
device_registry = dr.async_get(hass)
|
|
if (device_id := config.get(CONF_DEVICE_ID)) is not None:
|
|
self.device_entry = device_registry.async_get(device_id)
|
|
|
|
@property
|
|
@abstractmethod
|
|
def referenced_blueprint(self) -> str | None:
|
|
"""Return referenced blueprint or None."""
|
|
|
|
@callback
|
|
@abstractmethod
|
|
def _render_script_variables(self) -> dict:
|
|
"""Render configured variables."""
|
|
|
|
@abstractmethod
|
|
def setup_state_template(
|
|
self,
|
|
attribute: str,
|
|
validator: Callable[[Any], Any] | None = None,
|
|
on_update: Callable[[Any], None] | None = None,
|
|
) -> None:
|
|
"""Set up a template that manages the main state of the entity.
|
|
|
|
Requires _state_option to be set on the inheriting
|
|
class. _state_option represents the configuration
|
|
option that derives the state. E.g. Template weather
|
|
entities main state option is 'condition', where
|
|
switch is 'state'.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def setup_template(
|
|
self,
|
|
option: str,
|
|
attribute: str,
|
|
validator: Callable[[Any], Any] | None = None,
|
|
on_update: Callable[[Any], None] | None = None,
|
|
render_complex: bool = False,
|
|
none_on_template_error: bool = True,
|
|
) -> None:
|
|
"""Set up a template that manages any property or attribute of the entity.
|
|
|
|
Parameters
|
|
----------
|
|
option
|
|
The configuration key provided by ConfigFlow or the yaml option
|
|
attribute
|
|
The name of the attribute to link to. This attribute must exist
|
|
unless a custom on_update method is supplied.
|
|
validator:
|
|
Optional function that validates the rendered result.
|
|
on_update:
|
|
Called to store the template result rather than storing it
|
|
the supplied attribute. Passed the result of the validator.
|
|
render_complex (default=False):
|
|
This signals trigger based template entities to render the template
|
|
as a complex result. State based template entities always render
|
|
complex results.
|
|
none_on_template_error (default=True)
|
|
If set to false, template errors will be supplied in the result to
|
|
on_update.
|
|
"""
|
|
|
|
def add_template(
|
|
self,
|
|
option: str,
|
|
attribute: str,
|
|
validator: Callable[[Any], Any] | None = None,
|
|
on_update: Callable[[Any], None] | None = None,
|
|
none_on_template_error: bool = False,
|
|
add_if_static: bool = True,
|
|
) -> Template | None:
|
|
"""Add a template."""
|
|
if (template := self._config.get(option)) and isinstance(template, Template):
|
|
if add_if_static or (not template.is_static):
|
|
self._templates[option] = EntityTemplate(
|
|
attribute,
|
|
template,
|
|
validator,
|
|
on_update,
|
|
none_on_template_error,
|
|
)
|
|
return template
|
|
|
|
return None
|
|
|
|
def add_script(
|
|
self,
|
|
script_id: str,
|
|
config: Sequence[dict[str, Any]],
|
|
name: str,
|
|
domain: str,
|
|
):
|
|
"""Add an action script."""
|
|
|
|
self._action_scripts[script_id] = Script(
|
|
self.hass,
|
|
config,
|
|
f"{name} {script_id}",
|
|
domain,
|
|
)
|
|
|
|
@override
|
|
async def async_will_remove_from_hass(self) -> None:
|
|
"""Clean up scripts when removing from Home Assistant."""
|
|
if not self.registry_entry or self.registry_entry.entity_id == self.entity_id:
|
|
# Entity ID not changed, unload scripts as they will not be reused.
|
|
for action_script in self._action_scripts.values():
|
|
await action_script.async_unload()
|
|
else:
|
|
# Entity ID changed, just stop scripts
|
|
for action_script in self._action_scripts.values():
|
|
await action_script.async_stop()
|
|
|
|
async def async_run_script(
|
|
self,
|
|
script: Script,
|
|
*,
|
|
run_variables: _VarsType | None = None,
|
|
context: Context | None = None,
|
|
) -> None:
|
|
"""Run an action script."""
|
|
if run_variables is None:
|
|
run_variables = {}
|
|
await script.async_run(
|
|
run_variables={
|
|
"this": TemplateStateFromEntityId(self.hass, self.entity_id),
|
|
**self._render_script_variables(),
|
|
**run_variables,
|
|
},
|
|
context=context,
|
|
)
|
|
|
|
async def _async_get_last_template_data(
|
|
self,
|
|
) -> Any | None:
|
|
"""Get the last template data."""
|
|
if self._restore_state_extra_data is None or not hasattr(
|
|
self, "async_get_last_extra_data"
|
|
):
|
|
return _SENTINEL
|
|
|
|
if (restored_last_extra_data := await self.async_get_last_extra_data()) is None:
|
|
return None
|
|
|
|
return self._restore_state_extra_data.from_dict(
|
|
restored_last_extra_data.as_dict()
|
|
)
|
|
|
|
def restore_extra_data(self, extra_data: Any) -> None:
|
|
"""Restore extra data from the last state."""
|
|
|
|
async def async_restore_last_state(self) -> None:
|
|
"""Restore the state from the last state."""
|
|
if not hasattr(self, "async_get_last_state"):
|
|
return
|
|
|
|
last_state: State | None = await self.async_get_last_state()
|
|
if last_state is None:
|
|
return
|
|
|
|
# Handle extra data.
|
|
extra_data = _SENTINEL
|
|
if self._restore_state_extra_data is not None:
|
|
extra_data = await self._async_get_last_template_data()
|
|
|
|
if (
|
|
extra_data is None
|
|
or last_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
|
or (
|
|
self._restore_state_properties is not None
|
|
and any(
|
|
getattr(self, attr) is not None
|
|
for attr in self._restore_state_properties
|
|
)
|
|
)
|
|
):
|
|
return
|
|
|
|
if not self.restore_last_state_state(last_state):
|
|
return
|
|
|
|
self.restore_last_state_attributes(last_state)
|
|
|
|
# Extra data should be loaded last
|
|
if extra_data is not _SENTINEL:
|
|
self.restore_extra_data(extra_data)
|
|
|
|
def restore_last_state_state(self, last_state: State) -> bool:
|
|
"""Restore the state from the last state."""
|
|
return True
|
|
|
|
@abstractmethod
|
|
def restore_attribute(self, conf_attr: str, attr: str, restored_value: Any) -> None:
|
|
"""Restore an attribute from the last value."""
|
|
|
|
def restore_last_state_attributes(self, last_state: State) -> None:
|
|
"""Restore attributes from the last state."""
|
|
# Restore built-in attributes from templates
|
|
for conf_key, attr, _attr in (
|
|
(CONF_ICON, ATTR_ICON, "_attr_icon"),
|
|
(CONF_NAME, ATTR_FRIENDLY_NAME, "_attr_name"),
|
|
(CONF_PICTURE, ATTR_ENTITY_PICTURE, "_attr_entity_picture"),
|
|
):
|
|
if conf_key not in self._config or attr not in last_state.attributes:
|
|
continue
|
|
value = last_state.attributes[attr]
|
|
self.restore_attribute(conf_key, _attr, value)
|
|
|
|
self._attr_extra_state_attributes = {}
|
|
# Restore attributes from template attributes
|
|
if self._attribute_templates:
|
|
for attr in self._config[CONF_ATTRIBUTES]:
|
|
if attr not in last_state.attributes:
|
|
continue
|
|
self._attr_extra_state_attributes[attr] = last_state.attributes[attr]
|