From fad5f7a47b5d93bb1ff5ad4262727db3ca2a429d Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 23 Jul 2025 09:52:25 -0400 Subject: [PATCH] Move optimistic platform logic to AbstractTemplateEntity base class (#149245) --- .../components/template/binary_sensor.py | 2 +- homeassistant/components/template/cover.py | 28 +++++++++---------- homeassistant/components/template/entity.py | 19 +++++++++++-- homeassistant/components/template/select.py | 24 +++++----------- .../components/template/template_entity.py | 6 ++++ 5 files changed, 43 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index e8b8efbda0a..567e9e3a110 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -182,7 +182,7 @@ class StateBinarySensorEntity(TemplateEntity, BinarySensorEntity, RestoreEntity) TemplateEntity.__init__(self, hass, config, unique_id) self._attr_device_class = config.get(CONF_DEVICE_CLASS) - self._template = config[CONF_STATE] + self._template: template.Template = config[CONF_STATE] self._delay_cancel = None self._delay_on = None self._delay_on_raw = config.get(CONF_DELAY_ON) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 0bbc6b77f57..8f88baea091 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -24,7 +24,6 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_FRIENDLY_NAME, CONF_NAME, - CONF_OPTIMISTIC, CONF_STATE, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, @@ -41,6 +40,7 @@ from .entity import AbstractTemplateEntity from .helpers import async_setup_template_platform from .template_entity import ( TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY, + TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TemplateEntity, make_template_entity_common_modern_schema, ) @@ -97,7 +97,6 @@ COVER_YAML_SCHEMA = vol.All( vol.Inclusive(CLOSE_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA, vol.Inclusive(OPEN_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_OPTIMISTIC): cv.boolean, vol.Optional(CONF_POSITION): cv.template, vol.Optional(CONF_STATE): cv.template, vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean, @@ -106,7 +105,9 @@ COVER_YAML_SCHEMA = vol.All( vol.Optional(STOP_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA, } - ).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema), + ) + .extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema) + .extend(TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA), cv.has_at_least_one_key(OPEN_ACTION, POSITION_ACTION), ) @@ -121,7 +122,6 @@ COVER_LEGACY_YAML_SCHEMA = vol.All( vol.Optional(CONF_POSITION_TEMPLATE): cv.template, vol.Optional(CONF_TILT_TEMPLATE): cv.template, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_OPTIMISTIC): cv.boolean, vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean, vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA, @@ -129,7 +129,9 @@ COVER_LEGACY_YAML_SCHEMA = vol.All( vol.Optional(CONF_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_UNIQUE_ID): cv.string, } - ).extend(TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY.schema), + ) + .extend(TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY.schema) + .extend(TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA), cv.has_at_least_one_key(OPEN_ACTION, POSITION_ACTION), ) @@ -162,21 +164,17 @@ class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity): """Representation of a template cover features.""" _entity_id_format = ENTITY_ID_FORMAT + _optimistic_entity = True # 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 """Initialize the features.""" - self._template = config.get(CONF_STATE) self._position_template = config.get(CONF_POSITION) self._tilt_template = config.get(CONF_TILT) self._attr_device_class = config.get(CONF_DEVICE_CLASS) - optimistic = config.get(CONF_OPTIMISTIC) - self._optimistic = optimistic or ( - optimistic is None and not self._template and not self._position_template - ) tilt_optimistic = config.get(CONF_TILT_OPTIMISTIC) self._tilt_optimistic = tilt_optimistic or not self._tilt_template self._position: int | None = None @@ -318,7 +316,7 @@ class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity): run_variables={"position": 100}, context=self._context, ) - if self._optimistic: + if self._attr_assumed_state: self._position = 100 self.async_write_ha_state() @@ -332,7 +330,7 @@ class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity): run_variables={"position": 0}, context=self._context, ) - if self._optimistic: + if self._attr_assumed_state: self._position = 0 self.async_write_ha_state() @@ -349,7 +347,7 @@ class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity): run_variables={"position": self._position}, context=self._context, ) - if self._optimistic: + if self._attr_assumed_state: self.async_write_ha_state() async def async_open_cover_tilt(self, **kwargs: Any) -> None: @@ -493,10 +491,10 @@ class TriggerCoverEntity(TriggerEntity, AbstractTemplateCover): updater(rendered) write_ha_state = True - if not self._optimistic: + if not self._attr_assumed_state: self.async_set_context(self.coordinator.data["context"]) write_ha_state = True - elif self._optimistic and len(self._rendered) > 0: + elif self._attr_assumed_state and len(self._rendered) > 0: # In case any non optimistic template write_ha_state = True diff --git a/homeassistant/components/template/entity.py b/homeassistant/components/template/entity.py index 31c48917a1f..e9a630594d7 100644 --- a/homeassistant/components/template/entity.py +++ b/homeassistant/components/template/entity.py @@ -4,12 +4,12 @@ from abc import abstractmethod from collections.abc import Sequence from typing import Any -from homeassistant.const import CONF_DEVICE_ID +from homeassistant.const import CONF_DEVICE_ID, CONF_OPTIMISTIC, CONF_STATE from homeassistant.core import Context, HomeAssistant, 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 TemplateStateFromEntityId +from homeassistant.helpers.template import Template, TemplateStateFromEntityId from homeassistant.helpers.typing import ConfigType from .const import CONF_OBJECT_ID @@ -19,13 +19,26 @@ class AbstractTemplateEntity(Entity): """Actions linked to a template entity.""" _entity_id_format: str + _optimistic_entity: bool = False + _template: Template | None = None - def __init__(self, hass: HomeAssistant, config: ConfigType) -> None: + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + ) -> None: """Initialize the entity.""" self.hass = hass self._action_scripts: dict[str, Script] = {} + if self._optimistic_entity: + self._template = config.get(CONF_STATE) + + self._attr_assumed_state = self._template is None or config.get( + CONF_OPTIMISTIC, False + ) + if (object_id := config.get(CONF_OBJECT_ID)) is not None: self.entity_id = async_generate_entity_id( self._entity_id_format, object_id, hass=self.hass diff --git a/homeassistant/components/template/select.py b/homeassistant/components/template/select.py index 0ad99cd6ae8..8e298c28539 100644 --- a/homeassistant/components/template/select.py +++ b/homeassistant/components/template/select.py @@ -15,7 +15,7 @@ from homeassistant.components.select import ( SelectEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_STATE +from homeassistant.const import CONF_NAME, CONF_STATE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import ( @@ -34,6 +34,7 @@ from .helpers import ( ) from .template_entity import ( TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, + TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TemplateEntity, make_template_entity_common_modern_schema, ) @@ -45,7 +46,6 @@ CONF_OPTIONS = "options" CONF_SELECT_OPTION = "select_option" DEFAULT_NAME = "Template Select" -DEFAULT_OPTIMISTIC = False SELECT_COMMON_SCHEMA = vol.Schema( { @@ -55,15 +55,9 @@ SELECT_COMMON_SCHEMA = vol.Schema( } ) -SELECT_YAML_SCHEMA = ( - vol.Schema( - { - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - } - ) - .extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema) - .extend(SELECT_COMMON_SCHEMA.schema) -) +SELECT_YAML_SCHEMA = SELECT_COMMON_SCHEMA.extend( + TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA +).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema) SELECT_CONFIG_ENTRY_SCHEMA = SELECT_COMMON_SCHEMA.extend( TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema @@ -117,24 +111,20 @@ class AbstractTemplateSelect(AbstractTemplateEntity, SelectEntity): """Representation of a template select features.""" _entity_id_format = ENTITY_ID_FORMAT + _optimistic_entity = True # 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 """Initialize the features.""" - self._template = config.get(CONF_STATE) - self._options_template = config[ATTR_OPTIONS] - self._attr_assumed_state = self._optimistic = ( - self._template is None or config.get(CONF_OPTIMISTIC, DEFAULT_OPTIMISTIC) - ) self._attr_options = [] self._attr_current_option = None async def async_select_option(self, option: str) -> None: """Change the selected option.""" - if self._optimistic: + if self._attr_assumed_state: self._attr_current_option = option self.async_write_ha_state() if select_option := self._action_scripts.get(CONF_SELECT_OPTION): diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index ae473854502..1bc49bceafd 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_ICON, CONF_ICON_TEMPLATE, CONF_NAME, + CONF_OPTIMISTIC, CONF_PATH, CONF_VARIABLES, STATE_UNKNOWN, @@ -100,6 +101,11 @@ TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA = vol.Schema( ).extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema) +TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA = { + vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, +} + + def make_template_entity_common_modern_schema( default_name: str, ) -> vol.Schema: