Add optimistic option to fan yaml (#149390)

This commit is contained in:
Petro31 2025-07-28 10:58:46 -04:00 committed by GitHub
parent d3f18c1678
commit 49bd15718c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 76 additions and 27 deletions

View File

@ -43,6 +43,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_AVAILABILITY_SCHEMA_LEGACY, TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY,
TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA,
TemplateEntity, TemplateEntity,
make_template_entity_common_modern_schema, make_template_entity_common_modern_schema,
) )
@ -81,8 +82,7 @@ LEGACY_FIELDS = {
DEFAULT_NAME = "Template Fan" DEFAULT_NAME = "Template Fan"
FAN_YAML_SCHEMA = vol.All( FAN_COMMON_SCHEMA = vol.Schema(
vol.Schema(
{ {
vol.Optional(CONF_DIRECTION): cv.template, vol.Optional(CONF_DIRECTION): cv.template,
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
@ -98,7 +98,10 @@ FAN_YAML_SCHEMA = vol.All(
vol.Optional(CONF_SPEED_COUNT): vol.Coerce(int), vol.Optional(CONF_SPEED_COUNT): vol.Coerce(int),
vol.Optional(CONF_STATE): cv.template, vol.Optional(CONF_STATE): cv.template,
} }
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema) )
FAN_YAML_SCHEMA = FAN_COMMON_SCHEMA.extend(TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA).extend(
make_template_entity_common_modern_schema(DEFAULT_NAME).schema
) )
FAN_LEGACY_YAML_SCHEMA = vol.All( FAN_LEGACY_YAML_SCHEMA = vol.All(
@ -154,13 +157,12 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
"""Representation of a template fan features.""" """Representation of a template fan 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._percentage_template = config.get(CONF_PERCENTAGE) self._percentage_template = config.get(CONF_PERCENTAGE)
self._preset_mode_template = config.get(CONF_PRESET_MODE) self._preset_mode_template = config.get(CONF_PRESET_MODE)
self._oscillating_template = config.get(CONF_OSCILLATING) self._oscillating_template = config.get(CONF_OSCILLATING)
@ -177,7 +179,6 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
# List of valid preset modes # List of valid preset modes
self._preset_modes: list[str] | None = config.get(CONF_PRESET_MODES) self._preset_modes: list[str] | None = config.get(CONF_PRESET_MODES)
self._attr_assumed_state = self._template is None
self._attr_supported_features |= ( self._attr_supported_features |= (
FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON
@ -339,7 +340,7 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
if percentage is not None: if percentage is not None:
await self.async_set_percentage(percentage) await self.async_set_percentage(percentage)
if self._template is None: if self._attr_assumed_state:
self._state = True self._state = True
self.async_write_ha_state() self.async_write_ha_state()
@ -349,7 +350,7 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
self._action_scripts[CONF_OFF_ACTION], context=self._context self._action_scripts[CONF_OFF_ACTION], context=self._context
) )
if self._template is None: if self._attr_assumed_state:
self._state = False self._state = False
self.async_write_ha_state() self.async_write_ha_state()
@ -364,10 +365,10 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
context=self._context, context=self._context,
) )
if self._template is None: if self._attr_assumed_state:
self._state = percentage != 0 self._state = percentage != 0
if self._template is None or self._percentage_template is None: if self._attr_assumed_state or self._percentage_template is None:
self.async_write_ha_state() self.async_write_ha_state()
async def async_set_preset_mode(self, preset_mode: str) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None:
@ -381,10 +382,10 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
context=self._context, context=self._context,
) )
if self._template is None: if self._attr_assumed_state:
self._state = True self._state = True
if self._template is None or self._preset_mode_template is None: if self._attr_assumed_state or self._preset_mode_template is None:
self.async_write_ha_state() self.async_write_ha_state()
async def async_oscillate(self, oscillating: bool) -> None: async def async_oscillate(self, oscillating: bool) -> None:

View File

@ -1833,3 +1833,51 @@ async def test_nested_unique_id(
entry = entity_registry.async_get("fan.test_b") entry = entity_registry.async_get("fan.test_b")
assert entry assert entry
assert entry.unique_id == "x-b" assert entry.unique_id == "x-b"
@pytest.mark.parametrize(
("count", "fan_config"),
[
(
1,
{
"name": TEST_OBJECT_ID,
"state": "{{ is_state('sensor.test_sensor', 'on') }}",
"turn_on": [],
"turn_off": [],
"optimistic": True,
},
)
],
)
@pytest.mark.parametrize(
"style",
[ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
)
@pytest.mark.usefixtures("setup_fan")
async def test_optimistic_option(hass: HomeAssistant) -> None:
"""Test optimistic yaml option."""
hass.states.async_set(_STATE_TEST_SENSOR, STATE_OFF)
await hass.async_block_till_done()
state = hass.states.get(TEST_ENTITY_ID)
assert state.state == STATE_OFF
await hass.services.async_call(
fan.DOMAIN,
"turn_on",
{"entity_id": TEST_ENTITY_ID},
blocking=True,
)
state = hass.states.get(TEST_ENTITY_ID)
assert state.state == STATE_ON
hass.states.async_set(_STATE_TEST_SENSOR, STATE_ON)
await hass.async_block_till_done()
hass.states.async_set(_STATE_TEST_SENSOR, STATE_OFF)
await hass.async_block_till_done()
state = hass.states.get(TEST_ENTITY_ID)
assert state.state == STATE_OFF