From 6a5546afc1832ffa38d58b426dfe91cd1a3fe97e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Wilczy=C5=84ski?= Date: Tue, 17 Nov 2020 18:24:04 +0100 Subject: [PATCH] Add an option to template delay_on/off in template binary sensor (#43259) --- .../components/template/binary_sensor.py | 38 ++- .../components/template/test_binary_sensor.py | 263 ++++++++++++++++++ 2 files changed, 291 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 0fd3205f20c..f996b91a61e 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -46,8 +46,8 @@ SENSOR_SCHEMA = vol.All( vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_DELAY_ON): cv.positive_time_period, - vol.Optional(CONF_DELAY_OFF): cv.positive_time_period, + 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_UNIQUE_ID): cv.string, } ), @@ -71,8 +71,8 @@ async def _async_create_entities(hass, config): friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) device_class = device_config.get(CONF_DEVICE_CLASS) - delay_on = device_config.get(CONF_DELAY_ON) - delay_off = device_config.get(CONF_DELAY_OFF) + delay_on_raw = device_config.get(CONF_DELAY_ON) + delay_off_raw = device_config.get(CONF_DELAY_OFF) unique_id = device_config.get(CONF_UNIQUE_ID) sensors.append( @@ -85,8 +85,8 @@ async def _async_create_entities(hass, config): icon_template, entity_picture_template, availability_template, - delay_on, - delay_off, + delay_on_raw, + delay_off_raw, attribute_templates, unique_id, ) @@ -115,8 +115,8 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): icon_template, entity_picture_template, availability_template, - delay_on, - delay_off, + delay_on_raw, + delay_off_raw, attribute_templates, unique_id, ): @@ -133,8 +133,10 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): self._template = value_template self._state = None self._delay_cancel = None - self._delay_on = delay_on - self._delay_off = delay_off + self._delay_on = None + self._delay_on_raw = delay_on_raw + self._delay_off = None + self._delay_off_raw = delay_off_raw self._unique_id = unique_id async def async_added_to_hass(self): @@ -142,6 +144,22 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): self.add_template_attribute("_state", self._template, None, self._update_state) + if self._delay_on_raw is not None: + try: + self._delay_on = cv.positive_time_period(self._delay_on_raw) + except vol.Invalid: + self.add_template_attribute( + "_delay_on", self._delay_on_raw, cv.positive_time_period + ) + + if self._delay_off_raw is not None: + try: + self._delay_off = cv.positive_time_period(self._delay_off_raw) + except vol.Invalid: + self.add_template_attribute( + "_delay_off", self._delay_off_raw, cv.positive_time_period + ) + await super().async_added_to_hass() @callback diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index a6aa253e746..e8ff4c83f8d 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -383,6 +383,269 @@ async def test_template_delay_off(hass): assert state.state == "on" +async def test_template_with_templated_delay_on(hass): + """Test binary sensor template with template delay on.""" + config = { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "{{ states.sensor.test_state.state == 'on' }}", + "device_class": "motion", + "delay_on": '{{ ({ "seconds": 6 / 2 }) }}', + } + }, + } + } + await setup.async_setup_component(hass, binary_sensor.DOMAIN, config) + await hass.async_block_till_done() + await hass.async_start() + + hass.states.async_set("sensor.test_state", "on") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + future = dt_util.utcnow() + timedelta(seconds=3) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + # check with time changes + hass.states.async_set("sensor.test_state", "off") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + hass.states.async_set("sensor.test_state", "on") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + hass.states.async_set("sensor.test_state", "off") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + future = dt_util.utcnow() + timedelta(seconds=3) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + +async def test_template_with_templated_delay_off(hass): + """Test binary sensor template with template delay off.""" + config = { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "{{ states.sensor.test_state.state == 'on' }}", + "device_class": "motion", + "delay_off": '{{ ({ "seconds": 6 / 2 }) }}', + } + }, + } + } + hass.states.async_set("sensor.test_state", "on") + await setup.async_setup_component(hass, binary_sensor.DOMAIN, config) + await hass.async_block_till_done() + await hass.async_start() + + hass.states.async_set("sensor.test_state", "off") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + future = dt_util.utcnow() + timedelta(seconds=3) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + # check with time changes + hass.states.async_set("sensor.test_state", "on") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + hass.states.async_set("sensor.test_state", "off") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + hass.states.async_set("sensor.test_state", "on") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + future = dt_util.utcnow() + timedelta(seconds=3) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + +async def test_template_with_delay_on_based_on_input(hass): + """Test binary sensor template with template delay on based on input number.""" + config = { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "{{ states.sensor.test_state.state == 'on' }}", + "device_class": "motion", + "delay_on": '{{ ({ "seconds": states("input_number.delay")|int }) }}', + } + }, + } + } + await setup.async_setup_component(hass, binary_sensor.DOMAIN, config) + await hass.async_block_till_done() + await hass.async_start() + + hass.states.async_set("sensor.test_state", "off") + await hass.async_block_till_done() + + hass.states.async_set("input_number.delay", 3) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + hass.states.async_set("sensor.test_state", "on") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + future = dt_util.utcnow() + timedelta(seconds=3) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + # set input to 4 seconds + hass.states.async_set("sensor.test_state", "off") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + hass.states.async_set("input_number.delay", 4) + await hass.async_block_till_done() + + hass.states.async_set("sensor.test_state", "on") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "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" + + future = dt_util.utcnow() + timedelta(seconds=4) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + +async def test_template_with_delay_off_based_on_input(hass): + """Test binary sensor template with template delay off based on input number.""" + config = { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "{{ states.sensor.test_state.state == 'on' }}", + "device_class": "motion", + "delay_off": '{{ ({ "seconds": states("input_number.delay")|int }) }}', + } + }, + } + } + await setup.async_setup_component(hass, binary_sensor.DOMAIN, config) + await hass.async_block_till_done() + await hass.async_start() + + hass.states.async_set("sensor.test_state", "on") + await hass.async_block_till_done() + + hass.states.async_set("input_number.delay", 3) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + hass.states.async_set("sensor.test_state", "off") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + future = dt_util.utcnow() + timedelta(seconds=3) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + # set input to 4 seconds + hass.states.async_set("sensor.test_state", "on") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + hass.states.async_set("input_number.delay", 4) + await hass.async_block_till_done() + + hass.states.async_set("sensor.test_state", "off") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "on" + + 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 == "on" + + future = dt_util.utcnow() + timedelta(seconds=4) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "off" + + async def test_available_without_availability_template(hass): """Ensure availability is true without an availability_template.""" config = {