mirror of
https://github.com/home-assistant/core.git
synced 2025-07-29 08:07:45 +00:00
Add optimistic option to switch yaml (#149402)
This commit is contained in:
parent
ee2cf961f6
commit
1895db0ddd
@ -38,6 +38,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .const import CONF_TURN_OFF, CONF_TURN_ON, DOMAIN
|
||||
from .entity import AbstractTemplateEntity
|
||||
from .helpers import (
|
||||
async_setup_template_entry,
|
||||
async_setup_template_platform,
|
||||
@ -46,6 +47,7 @@ from .helpers import (
|
||||
from .template_entity import (
|
||||
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
|
||||
TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY,
|
||||
TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA,
|
||||
TemplateEntity,
|
||||
make_template_entity_common_modern_schema,
|
||||
)
|
||||
@ -68,8 +70,8 @@ SWITCH_COMMON_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
SWITCH_YAML_SCHEMA = SWITCH_COMMON_SCHEMA.extend(
|
||||
make_template_entity_common_modern_schema(DEFAULT_NAME).schema
|
||||
)
|
||||
TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
|
||||
SWITCH_LEGACY_YAML_SCHEMA = vol.All(
|
||||
cv.deprecated(ATTR_ENTITY_ID),
|
||||
@ -145,11 +147,38 @@ def async_create_preview_switch(
|
||||
)
|
||||
|
||||
|
||||
class StateSwitchEntity(TemplateEntity, SwitchEntity, RestoreEntity):
|
||||
class AbstractTemplateSwitch(AbstractTemplateEntity, SwitchEntity, RestoreEntity):
|
||||
"""Representation of a template switch 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."""
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Fire the on action."""
|
||||
if on_script := self._action_scripts.get(CONF_TURN_ON):
|
||||
await self.async_run_script(on_script, context=self._context)
|
||||
if self._attr_assumed_state:
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Fire the off action."""
|
||||
if off_script := self._action_scripts.get(CONF_TURN_OFF):
|
||||
await self.async_run_script(off_script, context=self._context)
|
||||
if self._attr_assumed_state:
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class StateSwitchEntity(TemplateEntity, AbstractTemplateSwitch):
|
||||
"""Representation of a Template switch."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -158,12 +187,12 @@ class StateSwitchEntity(TemplateEntity, SwitchEntity, RestoreEntity):
|
||||
unique_id: str | None,
|
||||
) -> None:
|
||||
"""Initialize the Template switch."""
|
||||
super().__init__(hass, config, unique_id)
|
||||
TemplateEntity.__init__(self, hass, config, unique_id)
|
||||
AbstractTemplateSwitch.__init__(self, config)
|
||||
|
||||
name = self._attr_name
|
||||
if TYPE_CHECKING:
|
||||
assert name is not None
|
||||
self._template = config.get(CONF_STATE)
|
||||
|
||||
# Scripts can be an empty list, therefore we need to check for None
|
||||
if (on_action := config.get(CONF_TURN_ON)) is not None:
|
||||
@ -171,25 +200,22 @@ class StateSwitchEntity(TemplateEntity, SwitchEntity, RestoreEntity):
|
||||
if (off_action := config.get(CONF_TURN_OFF)) is not None:
|
||||
self.add_script(CONF_TURN_OFF, off_action, name, DOMAIN)
|
||||
|
||||
self._state: bool | None = False
|
||||
self._attr_assumed_state = self._template is None
|
||||
|
||||
@callback
|
||||
def _update_state(self, result):
|
||||
super()._update_state(result)
|
||||
if isinstance(result, TemplateError):
|
||||
self._state = None
|
||||
self._attr_is_on = None
|
||||
return
|
||||
|
||||
if isinstance(result, bool):
|
||||
self._state = result
|
||||
self._attr_is_on = result
|
||||
return
|
||||
|
||||
if isinstance(result, str):
|
||||
self._state = result.lower() in ("true", STATE_ON)
|
||||
self._attr_is_on = result.lower() in ("true", STATE_ON)
|
||||
return
|
||||
|
||||
self._state = False
|
||||
self._attr_is_on = False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
@ -197,7 +223,7 @@ class StateSwitchEntity(TemplateEntity, SwitchEntity, RestoreEntity):
|
||||
# restore state after startup
|
||||
await super().async_added_to_hass()
|
||||
if state := await self.async_get_last_state():
|
||||
self._state = state.state == STATE_ON
|
||||
self._attr_is_on = state.state == STATE_ON
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
@ -205,37 +231,15 @@ class StateSwitchEntity(TemplateEntity, SwitchEntity, RestoreEntity):
|
||||
"""Set up templates."""
|
||||
if self._template is not None:
|
||||
self.add_template_attribute(
|
||||
"_state", self._template, None, self._update_state
|
||||
"_attr_is_on", self._template, None, self._update_state
|
||||
)
|
||||
|
||||
super()._async_setup_templates()
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if device is on."""
|
||||
return self._state
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Fire the on action."""
|
||||
if on_script := self._action_scripts.get(CONF_TURN_ON):
|
||||
await self.async_run_script(on_script, context=self._context)
|
||||
if self._template is None:
|
||||
self._state = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Fire the off action."""
|
||||
if off_script := self._action_scripts.get(CONF_TURN_OFF):
|
||||
await self.async_run_script(off_script, context=self._context)
|
||||
if self._template is None:
|
||||
self._state = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class TriggerSwitchEntity(TriggerEntity, SwitchEntity, RestoreEntity):
|
||||
class TriggerSwitchEntity(TriggerEntity, AbstractTemplateSwitch):
|
||||
"""Switch entity based on trigger data."""
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
domain = SWITCH_DOMAIN
|
||||
|
||||
def __init__(
|
||||
@ -245,17 +249,16 @@ class TriggerSwitchEntity(TriggerEntity, SwitchEntity, RestoreEntity):
|
||||
config: ConfigType,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(hass, coordinator, config)
|
||||
TriggerEntity.__init__(self, hass, coordinator, config)
|
||||
AbstractTemplateSwitch.__init__(self, config)
|
||||
|
||||
name = self._rendered.get(CONF_NAME, DEFAULT_NAME)
|
||||
self._template = config.get(CONF_STATE)
|
||||
if on_action := config.get(CONF_TURN_ON):
|
||||
self.add_script(CONF_TURN_ON, on_action, name, DOMAIN)
|
||||
if off_action := config.get(CONF_TURN_OFF):
|
||||
self.add_script(CONF_TURN_OFF, off_action, name, DOMAIN)
|
||||
|
||||
self._attr_assumed_state = self._template is None
|
||||
if not self._attr_assumed_state:
|
||||
if CONF_STATE in config:
|
||||
self._to_render_simple.append(CONF_STATE)
|
||||
self._parse_result.add(CONF_STATE)
|
||||
|
||||
@ -281,28 +284,15 @@ class TriggerSwitchEntity(TriggerEntity, SwitchEntity, RestoreEntity):
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
if not self._attr_assumed_state:
|
||||
raw = self._rendered.get(CONF_STATE)
|
||||
self._attr_is_on = template.result_as_boolean(raw)
|
||||
write_ha_state = False
|
||||
if (state := self._rendered.get(CONF_STATE)) is not None:
|
||||
self._attr_is_on = template.result_as_boolean(state)
|
||||
write_ha_state = True
|
||||
|
||||
self.async_write_ha_state()
|
||||
elif self._attr_assumed_state and len(self._rendered) > 0:
|
||||
elif len(self._rendered) > 0:
|
||||
# In case name, icon, or friendly name have a template but
|
||||
# states does not
|
||||
self.async_write_ha_state()
|
||||
write_ha_state = True
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Fire the on action."""
|
||||
if on_script := self._action_scripts.get(CONF_TURN_ON):
|
||||
await self.async_run_script(on_script, context=self._context)
|
||||
if self._template is None:
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Fire the off action."""
|
||||
if off_script := self._action_scripts.get(CONF_TURN_OFF):
|
||||
await self.async_run_script(off_script, context=self._context)
|
||||
if self._template is None:
|
||||
self._attr_is_on = False
|
||||
if write_ha_state:
|
||||
self.async_write_ha_state()
|
||||
|
@ -34,8 +34,13 @@ TEST_ENTITY_ID = f"switch.{TEST_OBJECT_ID}"
|
||||
TEST_STATE_ENTITY_ID = "switch.test_state"
|
||||
|
||||
TEST_EVENT_TRIGGER = {
|
||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||
"variables": {"type": "{{ trigger.event.data.type }}"},
|
||||
"triggers": [
|
||||
{"trigger": "event", "event_type": "test_event"},
|
||||
{"trigger": "state", "entity_id": [TEST_STATE_ENTITY_ID]},
|
||||
],
|
||||
"variables": {
|
||||
"type": "{{ trigger.event.data.type if trigger.event is defined else trigger.entity_id }}"
|
||||
},
|
||||
"action": [{"event": "action_event", "event_data": {"type": "{{ type }}"}}],
|
||||
}
|
||||
|
||||
@ -1211,3 +1216,54 @@ async def test_empty_action_config(hass: HomeAssistant, setup_switch) -> None:
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "switch_config"),
|
||||
[
|
||||
(
|
||||
1,
|
||||
{
|
||||
"name": TEST_OBJECT_ID,
|
||||
"state": "{{ is_state('switch.test_state', 'on') }}",
|
||||
"turn_on": [],
|
||||
"turn_off": [],
|
||||
"optimistic": True,
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"style",
|
||||
[
|
||||
ConfigurationStyle.MODERN,
|
||||
ConfigurationStyle.TRIGGER,
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_switch")
|
||||
async def test_optimistic_option(hass: HomeAssistant) -> None:
|
||||
"""Test optimistic yaml option."""
|
||||
hass.states.async_set(TEST_STATE_ENTITY_ID, 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(
|
||||
switch.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(TEST_STATE_ENTITY_ID, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
assert state.state == STATE_OFF
|
||||
|
Loading…
x
Reference in New Issue
Block a user