From 20073797012be0141608195a02f35fd376d0ab45 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Mar 2022 00:27:39 +0100 Subject: [PATCH] Restore state of template binary sensor with on or off delay (#67546) --- .../components/template/binary_sensor.py | 15 +++- .../components/template/test_binary_sensor.py | 73 ++++++++++++++++++- 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 4c080c736d0..f416454f388 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -30,6 +30,9 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import TemplateError @@ -38,6 +41,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import TriggerUpdateCoordinator @@ -186,7 +190,7 @@ async def async_setup_platform( ) -class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): +class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity): """A virtual binary sensor that triggers from another sensor.""" def __init__( @@ -212,7 +216,14 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): self._delay_off_raw = config.get(CONF_DELAY_OFF) async def async_added_to_hass(self): - """Register callbacks.""" + """Restore state and register callbacks.""" + if ( + (self._delay_on_raw is not None or self._delay_off_raw is not None) + and (last_state := await self.async_get_last_state()) is not None + and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) + ): + self._state = last_state.state == STATE_ON + self.add_template_attribute("_state", self._template, None, self._update_state) if self._delay_on_raw is not None: diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 8f76caa2cb9..c3773e8eb13 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -15,11 +15,16 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Context, CoreState +from homeassistant.core import Context, CoreState, State from homeassistant.helpers import entity_registry +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import ( + assert_setup_component, + async_fire_time_changed, + mock_restore_cache, +) ON = "on" OFF = "off" @@ -914,6 +919,70 @@ async def test_availability_icon_picture(hass, start_ha, entity_id): } +@pytest.mark.parametrize("count,domain", [(1, "template")]) +@pytest.mark.parametrize( + "config", + [ + { + "template": { + "binary_sensor": { + "name": "test", + "state": "{{ states.sensor.test_state.state == 'on' }}", + }, + }, + }, + ], +) +@pytest.mark.parametrize( + "extra_config, restored_state, initial_state", + [ + ({}, ON, OFF), + ({}, OFF, OFF), + ({}, STATE_UNAVAILABLE, OFF), + ({}, STATE_UNKNOWN, OFF), + ({"delay_off": 5}, ON, ON), + ({"delay_off": 5}, OFF, OFF), + ({"delay_off": 5}, STATE_UNAVAILABLE, STATE_UNKNOWN), + ({"delay_off": 5}, STATE_UNKNOWN, STATE_UNKNOWN), + ({"delay_on": 5}, ON, ON), + ({"delay_on": 5}, OFF, OFF), + ({"delay_on": 5}, STATE_UNAVAILABLE, STATE_UNKNOWN), + ({"delay_on": 5}, STATE_UNKNOWN, STATE_UNKNOWN), + ], +) +async def test_restore_state( + hass, count, domain, config, extra_config, restored_state, initial_state +): + """Test restoring template binary sensor.""" + + fake_state = State( + "binary_sensor.test", + restored_state, + {}, + ) + mock_restore_cache(hass, (fake_state,)) + config = dict(config) + config["template"]["binary_sensor"].update(**extra_config) + with assert_setup_component(count, domain): + assert await async_setup_component( + hass, + domain, + config, + ) + + await hass.async_block_till_done() + + context = Context() + hass.bus.async_fire("test_event", {"beer": 2}, context=context) + await hass.async_block_till_done() + + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == initial_state + + @pytest.mark.parametrize("count,domain", [(2, "template")]) @pytest.mark.parametrize( "config",