Move optimistic platform logic to AbstractTemplateEntity base class (#149245)

This commit is contained in:
Petro31 2025-07-23 09:52:25 -04:00 committed by GitHub
parent 58ddf4ea95
commit fad5f7a47b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 43 additions and 36 deletions

View File

@ -182,7 +182,7 @@ class StateBinarySensorEntity(TemplateEntity, BinarySensorEntity, RestoreEntity)
TemplateEntity.__init__(self, hass, config, unique_id) TemplateEntity.__init__(self, hass, config, unique_id)
self._attr_device_class = config.get(CONF_DEVICE_CLASS) 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_cancel = None
self._delay_on = None self._delay_on = None
self._delay_on_raw = config.get(CONF_DELAY_ON) self._delay_on_raw = config.get(CONF_DELAY_ON)

View File

@ -24,7 +24,6 @@ from homeassistant.const import (
CONF_ENTITY_ID, CONF_ENTITY_ID,
CONF_FRIENDLY_NAME, CONF_FRIENDLY_NAME,
CONF_NAME, CONF_NAME,
CONF_OPTIMISTIC,
CONF_STATE, CONF_STATE,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE,
@ -41,6 +40,7 @@ from .entity import AbstractTemplateEntity
from .helpers import async_setup_template_platform from .helpers import async_setup_template_platform
from .template_entity import ( from .template_entity import (
TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY, TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY,
TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA,
TemplateEntity, TemplateEntity,
make_template_entity_common_modern_schema, 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(CLOSE_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA,
vol.Inclusive(OPEN_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_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_POSITION): cv.template, vol.Optional(CONF_POSITION): cv.template,
vol.Optional(CONF_STATE): cv.template, vol.Optional(CONF_STATE): cv.template,
vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean, 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(STOP_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(TILT_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), 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_POSITION_TEMPLATE): cv.template,
vol.Optional(CONF_TILT_TEMPLATE): cv.template, vol.Optional(CONF_TILT_TEMPLATE): cv.template,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean,
vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(TILT_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_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_UNIQUE_ID): cv.string, 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), cv.has_at_least_one_key(OPEN_ACTION, POSITION_ACTION),
) )
@ -162,21 +164,17 @@ class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity):
"""Representation of a template cover features.""" """Representation of a template cover features."""
_entity_id_format = ENTITY_ID_FORMAT _entity_id_format = ENTITY_ID_FORMAT
_optimistic_entity = True
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__. # 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. # 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, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called
"""Initialize the features.""" """Initialize the features."""
self._template = config.get(CONF_STATE)
self._position_template = config.get(CONF_POSITION) self._position_template = config.get(CONF_POSITION)
self._tilt_template = config.get(CONF_TILT) self._tilt_template = config.get(CONF_TILT)
self._attr_device_class = config.get(CONF_DEVICE_CLASS) 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) tilt_optimistic = config.get(CONF_TILT_OPTIMISTIC)
self._tilt_optimistic = tilt_optimistic or not self._tilt_template self._tilt_optimistic = tilt_optimistic or not self._tilt_template
self._position: int | None = None self._position: int | None = None
@ -318,7 +316,7 @@ class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity):
run_variables={"position": 100}, run_variables={"position": 100},
context=self._context, context=self._context,
) )
if self._optimistic: if self._attr_assumed_state:
self._position = 100 self._position = 100
self.async_write_ha_state() self.async_write_ha_state()
@ -332,7 +330,7 @@ class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity):
run_variables={"position": 0}, run_variables={"position": 0},
context=self._context, context=self._context,
) )
if self._optimistic: if self._attr_assumed_state:
self._position = 0 self._position = 0
self.async_write_ha_state() self.async_write_ha_state()
@ -349,7 +347,7 @@ class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity):
run_variables={"position": self._position}, run_variables={"position": self._position},
context=self._context, context=self._context,
) )
if self._optimistic: if self._attr_assumed_state:
self.async_write_ha_state() self.async_write_ha_state()
async def async_open_cover_tilt(self, **kwargs: Any) -> None: async def async_open_cover_tilt(self, **kwargs: Any) -> None:
@ -493,10 +491,10 @@ class TriggerCoverEntity(TriggerEntity, AbstractTemplateCover):
updater(rendered) updater(rendered)
write_ha_state = True write_ha_state = True
if not self._optimistic: if not self._attr_assumed_state:
self.async_set_context(self.coordinator.data["context"]) self.async_set_context(self.coordinator.data["context"])
write_ha_state = True 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 # In case any non optimistic template
write_ha_state = True write_ha_state = True

View File

@ -4,12 +4,12 @@ from abc import abstractmethod
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any 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.core import Context, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity import Entity, async_generate_entity_id
from homeassistant.helpers.script import Script, _VarsType 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 homeassistant.helpers.typing import ConfigType
from .const import CONF_OBJECT_ID from .const import CONF_OBJECT_ID
@ -19,13 +19,26 @@ class AbstractTemplateEntity(Entity):
"""Actions linked to a template entity.""" """Actions linked to a template entity."""
_entity_id_format: str _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.""" """Initialize the entity."""
self.hass = hass self.hass = hass
self._action_scripts: dict[str, Script] = {} 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: if (object_id := config.get(CONF_OBJECT_ID)) is not None:
self.entity_id = async_generate_entity_id( self.entity_id = async_generate_entity_id(
self._entity_id_format, object_id, hass=self.hass self._entity_id_format, object_id, hass=self.hass

View File

@ -15,7 +15,7 @@ from homeassistant.components.select import (
SelectEntity, SelectEntity,
) )
from homeassistant.config_entries import ConfigEntry 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.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import ( from homeassistant.helpers.entity_platform import (
@ -34,6 +34,7 @@ from .helpers import (
) )
from .template_entity import ( from .template_entity import (
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA,
TemplateEntity, TemplateEntity,
make_template_entity_common_modern_schema, make_template_entity_common_modern_schema,
) )
@ -45,7 +46,6 @@ CONF_OPTIONS = "options"
CONF_SELECT_OPTION = "select_option" CONF_SELECT_OPTION = "select_option"
DEFAULT_NAME = "Template Select" DEFAULT_NAME = "Template Select"
DEFAULT_OPTIMISTIC = False
SELECT_COMMON_SCHEMA = vol.Schema( SELECT_COMMON_SCHEMA = vol.Schema(
{ {
@ -55,15 +55,9 @@ SELECT_COMMON_SCHEMA = vol.Schema(
} }
) )
SELECT_YAML_SCHEMA = ( SELECT_YAML_SCHEMA = SELECT_COMMON_SCHEMA.extend(
vol.Schema( TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA
{ ).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).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_CONFIG_ENTRY_SCHEMA = SELECT_COMMON_SCHEMA.extend( SELECT_CONFIG_ENTRY_SCHEMA = SELECT_COMMON_SCHEMA.extend(
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema
@ -117,24 +111,20 @@ class AbstractTemplateSelect(AbstractTemplateEntity, SelectEntity):
"""Representation of a template select features.""" """Representation of a template select features."""
_entity_id_format = ENTITY_ID_FORMAT _entity_id_format = ENTITY_ID_FORMAT
_optimistic_entity = True
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__. # 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. # 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, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called
"""Initialize the features.""" """Initialize the features."""
self._template = config.get(CONF_STATE)
self._options_template = config[ATTR_OPTIONS] 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_options = []
self._attr_current_option = None self._attr_current_option = None
async def async_select_option(self, option: str) -> None: async def async_select_option(self, option: str) -> None:
"""Change the selected option.""" """Change the selected option."""
if self._optimistic: if self._attr_assumed_state:
self._attr_current_option = option self._attr_current_option = option
self.async_write_ha_state() self.async_write_ha_state()
if select_option := self._action_scripts.get(CONF_SELECT_OPTION): if select_option := self._action_scripts.get(CONF_SELECT_OPTION):

View File

@ -17,6 +17,7 @@ from homeassistant.const import (
CONF_ICON, CONF_ICON,
CONF_ICON_TEMPLATE, CONF_ICON_TEMPLATE,
CONF_NAME, CONF_NAME,
CONF_OPTIMISTIC,
CONF_PATH, CONF_PATH,
CONF_VARIABLES, CONF_VARIABLES,
STATE_UNKNOWN, STATE_UNKNOWN,
@ -100,6 +101,11 @@ TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA = vol.Schema(
).extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.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( def make_template_entity_common_modern_schema(
default_name: str, default_name: str,
) -> vol.Schema: ) -> vol.Schema: