Update binary_sensor template platform to use new template framework (#159650)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Petro31
2026-01-22 08:11:49 -05:00
committed by GitHub
parent 54be76f0ab
commit 9448f52d4a
18 changed files with 189 additions and 97 deletions

View File

@@ -355,16 +355,3 @@ class TriggerAlarmControlPanelEntity(TriggerEntity, AbstractTemplateAlarmControl
"""Restore last state."""
await super().async_added_to_hass()
await self._async_handle_restored_state()
@callback
def _handle_coordinator_update(self) -> None:
"""Handle update of the data."""
self._process_data()
if not self.available:
self.async_write_ha_state()
return
if self.handle_rendered_result(CONF_STATE):
self.async_set_context(self.coordinator.data["context"])
self.async_write_ha_state()

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from abc import abstractmethod
from dataclasses import dataclass
from datetime import datetime, timedelta
from functools import partial
@@ -183,8 +184,32 @@ class AbstractTemplateBinarySensor(
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
self._template: template.Template = config[CONF_STATE]
self._delay_on = None
self._delay_off = None
self._delay_cancel: CALLBACK_TYPE | None = None
self.setup_state_template(
CONF_STATE,
"_attr_is_on",
on_update=self._update_state,
)
self._delay_on = None
try:
self._delay_on = cv.positive_time_period(config.get(CONF_DELAY_ON))
except vol.Invalid:
self.setup_template(CONF_DELAY_ON, "_delay_on", cv.positive_time_period)
self._delay_off = None
try:
self._delay_off = cv.positive_time_period(config.get(CONF_DELAY_OFF))
except vol.Invalid:
self.setup_template(CONF_DELAY_OFF, "_delay_off", cv.positive_time_period)
@callback
@abstractmethod
def _update_state(self, result: Any) -> None:
"""Update the state."""
class StateBinarySensorEntity(TemplateEntity, AbstractTemplateBinarySensor):
"""A virtual binary sensor that triggers from another sensor."""
@@ -200,17 +225,15 @@ class StateBinarySensorEntity(TemplateEntity, AbstractTemplateBinarySensor):
"""Initialize the Template binary sensor."""
TemplateEntity.__init__(self, hass, config, unique_id)
AbstractTemplateBinarySensor.__init__(self, config)
self._delay_on = None
self._delay_on_template = config.get(CONF_DELAY_ON)
self._delay_off = None
self._delay_off_template = config.get(CONF_DELAY_OFF)
async def async_added_to_hass(self) -> None:
"""Restore state."""
if (
(
self._delay_on_template is not None
or self._delay_off_template is not None
CONF_DELAY_ON in self._templates
or CONF_DELAY_OFF in self._templates
or self._delay_on is not None
or self._delay_off is not None
)
and (last_state := await self.async_get_last_state()) is not None
and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
@@ -218,29 +241,6 @@ class StateBinarySensorEntity(TemplateEntity, AbstractTemplateBinarySensor):
self._attr_is_on = last_state.state == STATE_ON
await super().async_added_to_hass()
@callback
def _async_setup_templates(self) -> None:
"""Set up templates."""
self.add_template_attribute("_state", self._template, None, self._update_state)
if self._delay_on_template is not None:
try:
self._delay_on = cv.positive_time_period(self._delay_on_template)
except vol.Invalid:
self.add_template_attribute(
"_delay_on", self._delay_on_template, cv.positive_time_period
)
if self._delay_off_template is not None:
try:
self._delay_off = cv.positive_time_period(self._delay_off_template)
except vol.Invalid:
self.add_template_attribute(
"_delay_off", self._delay_off_template, cv.positive_time_period
)
super()._async_setup_templates()
@callback
def _update_state(self, result):
super()._update_state(result)
@@ -291,15 +291,11 @@ class TriggerBinarySensorEntity(TriggerEntity, AbstractTemplateBinarySensor):
TriggerEntity.__init__(self, hass, coordinator, config)
AbstractTemplateBinarySensor.__init__(self, config)
for key in (CONF_STATE, CONF_DELAY_ON, CONF_DELAY_OFF, CONF_AUTO_OFF):
if isinstance(config.get(key), template.Template):
self._to_render_simple.append(key)
self._parse_result.add(key)
self._last_delay_from: bool | None = None
self._last_delay_to: bool | None = None
self._auto_off_cancel: CALLBACK_TYPE | None = None
self._auto_off_time: datetime | None = None
self.setup_template(CONF_AUTO_OFF, "_auto_off_time", cv.positive_time_period)
async def async_added_to_hass(self) -> None:
"""Restore last state."""
@@ -329,26 +325,7 @@ class TriggerBinarySensorEntity(TriggerEntity, AbstractTemplateBinarySensor):
self._set_auto_off(auto_off_time)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle update of the data."""
self._process_data()
raw = self._rendered.get(CONF_STATE)
state: bool | None = None
if raw is not None:
state = template.result_as_boolean(raw)
key = CONF_DELAY_ON if state else CONF_DELAY_OFF
delay = self._rendered.get(key) or self._config.get(key)
if (
self._delay_cancel
and delay
and self._attr_is_on == self._last_delay_from
and state == self._last_delay_to
):
return
def _cancel_delays(self):
if self._delay_cancel:
self._delay_cancel()
self._delay_cancel = None
@@ -358,10 +335,27 @@ class TriggerBinarySensorEntity(TriggerEntity, AbstractTemplateBinarySensor):
self._auto_off_cancel = None
self._auto_off_time = None
if not self.available:
self.async_write_ha_state()
@callback
def _update_state(self, result):
state: bool | None = None
if result is not None:
state = template.result_as_boolean(result)
if state:
delay = self._rendered.get(CONF_DELAY_ON) or self._delay_on
else:
delay = self._rendered.get(CONF_DELAY_OFF) or self._delay_off
if (
self._delay_cancel
and delay
and self._attr_is_on == self._last_delay_from
and state == self._last_delay_to
):
return
self._cancel_delays()
# state without delay.
if self._attr_is_on == state or delay is None:
self._set_state(state)
@@ -371,6 +365,7 @@ class TriggerBinarySensorEntity(TriggerEntity, AbstractTemplateBinarySensor):
try:
delay = cv.positive_time_period(delay)
except vol.Invalid as err:
key = CONF_DELAY_ON if state else CONF_DELAY_OFF
logging.getLogger(__name__).warning(
"Error rendering %s template: %s", key, err
)
@@ -412,6 +407,14 @@ class TriggerBinarySensorEntity(TriggerEntity, AbstractTemplateBinarySensor):
auto_off_time = dt_util.utcnow() + auto_off_delay
self._set_auto_off(auto_off_time)
def _render_availability_template(self, variables):
available = super()._render_availability_template(variables)
if not available:
# Cancel any delay_on, delay_off, or auto_off when
# the entity goes unavailable
self._cancel_delays()
return available
def _set_auto_off(self, auto_off_time: datetime) -> None:
@callback
def _auto_off(_):

View File

@@ -500,7 +500,6 @@ class TriggerCoverEntity(TriggerEntity, AbstractTemplateCover):
self._process_data()
if not self.available:
self.async_write_ha_state()
return
write_ha_state = False

View File

@@ -96,6 +96,30 @@ class AbstractTemplateEntity(Entity):
) -> None:
"""Set up a template that manages the main state of the entity."""
@abstractmethod
def setup_template(
self,
option: str,
attribute: str,
validator: Callable[[Any], Any] | None = None,
on_update: Callable[[Any], None] | None = None,
) -> 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.
"""
def add_template(
self,
option: str,
@@ -109,7 +133,11 @@ class AbstractTemplateEntity(Entity):
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
attribute,
template,
validator,
on_update,
none_on_template_error,
)
return template

View File

@@ -224,7 +224,6 @@ class TriggerEventEntity(TriggerEntity, AbstractTemplateEvent, RestoreEntity):
self._process_data()
if not self.available:
self.async_write_ha_state()
return
for key, updater in (

View File

@@ -552,7 +552,6 @@ class TriggerFanEntity(TriggerEntity, AbstractTemplateFan):
self._process_data()
if not self.available:
self.async_write_ha_state()
return
write_ha_state = False

View File

@@ -180,3 +180,4 @@ class TriggerImageEntity(TriggerEntity, AbstractTemplateImage):
"""Process new data."""
super()._process_data()
self._handle_state(self._rendered.get(CONF_URL))
self.async_write_ha_state()

View File

@@ -1123,7 +1123,6 @@ class TriggerLightEntity(TriggerEntity, AbstractTemplateLight):
self._process_data()
if not self.available:
self.async_write_ha_state()
return
write_ha_state = False

View File

@@ -377,7 +377,6 @@ class TriggerLockEntity(TriggerEntity, AbstractTemplateLock):
self._process_data()
if not self.available:
self.async_write_ha_state()
return
write_ha_state = False

View File

@@ -236,7 +236,6 @@ class TriggerNumberEntity(TriggerEntity, AbstractTemplateNumber):
self._process_data()
if not self.available:
self.async_write_ha_state()
return
write_ha_state = False

View File

@@ -209,7 +209,6 @@ class TriggerSelectEntity(TriggerEntity, AbstractTemplateSelect):
self._process_data()
if not self.available:
self.async_write_ha_state()
return
write_ha_state = False

View File

@@ -323,3 +323,4 @@ class TriggerSensorEntity(TriggerEntity, AbstractTemplateSensor):
rendered = self._rendered.get(CONF_STATE)
self._handle_state(rendered)
self.async_write_ha_state()

View File

@@ -281,7 +281,6 @@ class TriggerSwitchEntity(TriggerEntity, AbstractTemplateSwitch):
self._process_data()
if not self.available:
self.async_write_ha_state()
return
write_ha_state = False

View File

@@ -303,6 +303,30 @@ class TemplateEntity(AbstractTemplateEntity):
self.add_template(option, attribute, on_update=_update_state)
def setup_template(
self,
option: str,
attribute: str,
validator: Callable[[Any], Any] | None = None,
on_update: Callable[[Any], None] | None = 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.
"""
self.add_template(option, attribute, validator, on_update, True)
def add_template_attribute(
self,
attribute: str,

View File

@@ -59,10 +59,33 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
on_update: Callable[[Any], None] | None = None,
) -> None:
"""Set up a template that manages the main state of the entity."""
if self._config.get(option):
self._to_render_simple.append(CONF_STATE)
self._parse_result.add(CONF_STATE)
self.add_template(option, attribute, validator, on_update)
if self.add_template(option, attribute, validator, on_update):
self._to_render_simple.append(option)
self._parse_result.add(option)
def setup_template(
self,
option: str,
attribute: str,
validator: Callable[[Any], Any] | None = None,
on_update: Callable[[Any], None] | None = None,
) -> 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.
"""
self.setup_state_template(option, attribute, validator, on_update)
@property
def referenced_blueprint(self) -> str | None:
@@ -103,21 +126,35 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
self._render_attributes(rendered, variables)
self._rendered = rendered
def handle_rendered_result(self, key: str) -> bool:
def _handle_rendered_results(self) -> bool:
"""Get a rendered result and return the value."""
if (rendered := self._rendered.get(key)) is not None:
if (entity_template := self._templates.get(key)) is not None:
# Handle any templates.
for option, entity_template in self._templates.items():
value = _SENTINEL
if (rendered := self._rendered.get(option)) is not None:
value = rendered
if entity_template.validator:
value = entity_template.validator(rendered)
if entity_template.on_update:
entity_template.on_update(value)
else:
setattr(self, entity_template.attribute, value)
if entity_template.validator:
value = entity_template.validator(rendered)
# Capture templates that did not render a result due to an exception and
# ensure the state object updates. _SENTINEL is used to differentiate
# templates that render None.
if value is _SENTINEL:
return True
if entity_template.on_update:
entity_template.on_update(value)
else:
setattr(self, entity_template.attribute, value)
return True
if len(self._rendered) > 0:
# In some cases, the entity may be state optimistic or
# attribute optimistic, in these scenarios the state needs
# to update.
return True
return False
@callback
@@ -136,13 +173,35 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
else:
self._rendered_entity_variables = coordinator_variables
variables = self._template_variables(self._rendered_entity_variables)
self.async_set_context(self.coordinator.data["context"])
if self._render_availability_template(variables):
self._render_templates(variables)
self.async_set_context(self.coordinator.data["context"])
write_state = False
# While transitioning platforms to the new framework, this
# if-statement is necessary for backward compatibility with existing
# trigger based platforms.
if self._templates:
# Handle any results that were rendered.
write_state = self._handle_rendered_results()
# Check availability after rendering the results because the state
# template could render the entity unavailable
if not self.available:
write_state = True
if write_state:
self.async_write_ha_state()
else:
self.async_write_ha_state()
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
"""Handle updated data from the coordinator.
While transitioning platforms to the new framework, this
function is necessary for backward compatibility with existing
trigger based platforms.
"""
self._process_data()
self.async_write_ha_state()

View File

@@ -438,7 +438,6 @@ class TriggerUpdateEntity(TriggerEntity, AbstractTemplateUpdate):
self._process_data()
if not self.available:
self.async_write_ha_state()
return
write_ha_state = False

View File

@@ -489,7 +489,6 @@ class TriggerVacuumEntity(TriggerEntity, AbstractTemplateVacuum):
self._process_data()
if not self.available:
self.async_write_ha_state()
return
write_ha_state = False

View File

@@ -747,7 +747,6 @@ class TriggerWeatherEntity(TriggerEntity, AbstractTemplateWeather, RestoreEntity
self._process_data()
if not self.available:
self.async_write_ha_state()
return
write_ha_state = False