diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 2d0d06f86a1..381d58a8a9c 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -43,6 +43,7 @@ from .entity import AbstractTemplateEntity from .helpers import async_setup_template_platform from .template_entity import ( TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY, + TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TemplateEntity, make_template_entity_common_modern_schema, ) @@ -81,24 +82,26 @@ LEGACY_FIELDS = { DEFAULT_NAME = "Template Fan" -FAN_YAML_SCHEMA = vol.All( - vol.Schema( - { - vol.Optional(CONF_DIRECTION): cv.template, - vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, - vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_OSCILLATING): cv.template, - vol.Optional(CONF_PERCENTAGE): cv.template, - vol.Optional(CONF_PRESET_MODE): cv.template, - vol.Optional(CONF_PRESET_MODES): cv.ensure_list, - vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_SET_PERCENTAGE_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_SET_PRESET_MODE_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_SPEED_COUNT): vol.Coerce(int), - vol.Optional(CONF_STATE): cv.template, - } - ).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema) +FAN_COMMON_SCHEMA = vol.Schema( + { + vol.Optional(CONF_DIRECTION): cv.template, + vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, + vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_OSCILLATING): cv.template, + vol.Optional(CONF_PERCENTAGE): cv.template, + vol.Optional(CONF_PRESET_MODE): cv.template, + vol.Optional(CONF_PRESET_MODES): cv.ensure_list, + vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_PERCENTAGE_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_PRESET_MODE_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SPEED_COUNT): vol.Coerce(int), + vol.Optional(CONF_STATE): cv.template, + } +) + +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( @@ -154,13 +157,12 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity): """Representation of a template fan 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._percentage_template = config.get(CONF_PERCENTAGE) self._preset_mode_template = config.get(CONF_PRESET_MODE) self._oscillating_template = config.get(CONF_OSCILLATING) @@ -177,7 +179,6 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity): # List of valid 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 |= ( FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON @@ -339,7 +340,7 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity): if percentage is not None: await self.async_set_percentage(percentage) - if self._template is None: + if self._attr_assumed_state: self._state = True self.async_write_ha_state() @@ -349,7 +350,7 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity): self._action_scripts[CONF_OFF_ACTION], context=self._context ) - if self._template is None: + if self._attr_assumed_state: self._state = False self.async_write_ha_state() @@ -364,10 +365,10 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity): context=self._context, ) - if self._template is None: + if self._attr_assumed_state: 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() async def async_set_preset_mode(self, preset_mode: str) -> None: @@ -381,10 +382,10 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity): context=self._context, ) - if self._template is None: + if self._attr_assumed_state: 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() async def async_oscillate(self, oscillating: bool) -> None: diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index 708ad6bdecd..c0af18166df 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -1833,3 +1833,51 @@ async def test_nested_unique_id( entry = entity_registry.async_get("fan.test_b") assert entry 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