From f5e4b138140a52d5caf13634a26e92f9574cfd5e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen <balloob@gmail.com> Date: Thu, 29 Apr 2021 09:25:34 -0700 Subject: [PATCH] Add auto_off to binary sensor template entity (#49615) --- .../components/template/binary_sensor.py | 73 +++++++++++++------ .../components/template/test_binary_sensor.py | 20 ++++- 2 files changed, 71 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 2e1d2f71590..4d316388eae 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from functools import partial import logging import voluptuous as vol @@ -49,6 +50,7 @@ from .trigger_entity import TriggerEntity CONF_DELAY_ON = "delay_on" CONF_DELAY_OFF = "delay_off" +CONF_AUTO_OFF = "auto_off" CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" LEGACY_FIELDS = { @@ -74,6 +76,7 @@ BINARY_SENSOR_SCHEMA = vol.Schema( vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_DELAY_ON): vol.Any(cv.positive_time_period, cv.template), vol.Optional(CONF_DELAY_OFF): vol.Any(cv.positive_time_period, cv.template), + vol.Optional(CONF_AUTO_OFF): vol.Any(cv.positive_time_period, cv.template), } ) @@ -353,15 +356,13 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity): """Initialize the entity.""" super().__init__(hass, coordinator, config) - if isinstance(config.get(CONF_DELAY_ON), template.Template): - self._to_render.append(CONF_DELAY_ON) - self._parse_result.add(CONF_DELAY_ON) - - if isinstance(config.get(CONF_DELAY_OFF), template.Template): - self._to_render.append(CONF_DELAY_OFF) - self._parse_result.add(CONF_DELAY_OFF) + for key in (CONF_DELAY_ON, CONF_DELAY_OFF, CONF_AUTO_OFF): + if isinstance(config.get(key), template.Template): + self._to_render.append(key) + self._parse_result.add(key) self._delay_cancel = None + self._auto_off_cancel = None self._state = False @property @@ -378,22 +379,23 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity): self._delay_cancel() self._delay_cancel = None + if self._auto_off_cancel: + self._auto_off_cancel() + self._auto_off_cancel = None + if not self.available: + self.async_write_ha_state() return raw = self._rendered.get(CONF_STATE) state = template.result_as_boolean(raw) - if state == self._state: - return - key = CONF_DELAY_ON if state else CONF_DELAY_OFF delay = self._rendered.get(key) or self._config.get(key) # state without delay. None means rendering failed. - if state is None or delay is None: - self._state = state - self.async_write_ha_state() + if self._state == state or state is None or delay is None: + self._set_state(state) return if not isinstance(delay, timedelta): @@ -405,14 +407,43 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity): ) return - @callback - def _set_state(_): - """Set state of template binary sensor.""" - self._state = state - self.async_set_context(self.coordinator.data["context"]) - self.async_write_ha_state() - # state with delay. Cancelled if new trigger received self._delay_cancel = async_call_later( - self.hass, delay.total_seconds(), _set_state + self.hass, delay.total_seconds(), partial(self._set_state, state) + ) + + @callback + def _set_state(self, state, _=None): + """Set up auto off.""" + self._state = state + self.async_set_context(self.coordinator.data["context"]) + self.async_write_ha_state() + + if not state: + return + + auto_off_time = self._rendered.get(CONF_AUTO_OFF) or self._config.get( + CONF_AUTO_OFF + ) + + if auto_off_time is None: + return + + if not isinstance(auto_off_time, timedelta): + try: + auto_off_time = cv.positive_time_period(auto_off_time) + except vol.Invalid as err: + logging.getLogger(__name__).warning( + "Error rendering %s template: %s", CONF_AUTO_OFF, err + ) + return + + @callback + def _auto_off(_): + """Set state of template binary sensor.""" + self._state = False + self.async_write_ha_state() + + self._auto_off_cancel = async_call_later( + self.hass, auto_off_time.total_seconds(), _auto_off ) diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 70356405867..ccadef5aa96 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -965,7 +965,8 @@ async def test_trigger_entity(hass): "picture": "{{ '/local/dogs.png' }}", "icon": "{{ 'mdi:pirate' }}", "attributes": { - "plus_one": "{{ trigger.event.data.beer + 1 }}" + "plus_one": "{{ trigger.event.data.beer + 1 }}", + "another": "{{ trigger.event.data.uno_mas or 1 }}", }, } ], @@ -1021,8 +1022,16 @@ async def test_trigger_entity(hass): assert state.attributes.get("icon") == "mdi:pirate" assert state.attributes.get("entity_picture") == "/local/dogs.png" assert state.attributes.get("plus_one") == 3 + assert state.attributes.get("another") == 1 assert state.context is context + # Even if state itself didn't change, attributes might have changed + hass.bus.async_fire("test_event", {"beer": 2, "uno_mas": "si"}) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.via_list") + assert state.state == "on" + assert state.attributes.get("another") == "si" + async def test_template_with_trigger_templated_delay_on(hass): """Test binary sensor template with template delay on.""" @@ -1034,6 +1043,7 @@ async def test_template_with_trigger_templated_delay_on(hass): "state": "{{ trigger.event.data.beer == 2 }}", "device_class": "motion", "delay_on": '{{ ({ "seconds": 6 / 2 }) }}', + "auto_off": '{{ ({ "seconds": 1 + 1 }) }}', }, } } @@ -1054,3 +1064,11 @@ async def test_template_with_trigger_templated_delay_on(hass): state = hass.states.get("binary_sensor.test") assert state.state == "on" + + # Now wait for the auto-off + future = dt_util.utcnow() + timedelta(seconds=2) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off"