From 84fd66c8a18330f74ab83cfacb99aa4c6163e2c4 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 19 Nov 2018 12:10:48 +0100 Subject: [PATCH] Template binary sensor to not track all state changes (#18573) --- .../components/binary_sensor/template.py | 32 +++-- .../components/binary_sensor/test_template.py | 109 ++++++++++++++---- 2 files changed, 111 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index 1f386fc2293..d5f8b16e0c1 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -58,10 +58,12 @@ async def async_setup_platform(hass, config, async_add_entities, entity_ids = set() manual_entity_ids = device_config.get(ATTR_ENTITY_ID) - for template in ( - value_template, - icon_template, - entity_picture_template, + invalid_templates = [] + + for tpl_name, template in ( + (CONF_VALUE_TEMPLATE, value_template), + (CONF_ICON_TEMPLATE, icon_template), + (CONF_ENTITY_PICTURE_TEMPLATE, entity_picture_template), ): if template is None: continue @@ -73,6 +75,8 @@ async def async_setup_platform(hass, config, async_add_entities, template_entity_ids = template.extract_entities() if template_entity_ids == MATCH_ALL: entity_ids = MATCH_ALL + # Cut off _template from name + invalid_templates.append(tpl_name[:-9]) elif entity_ids != MATCH_ALL: entity_ids |= set(template_entity_ids) @@ -81,6 +85,14 @@ async def async_setup_platform(hass, config, async_add_entities, elif entity_ids != MATCH_ALL: entity_ids = list(entity_ids) + if invalid_templates: + _LOGGER.warning( + 'Template binary sensor %s has no entity ids configured to' + ' track nor were we able to extract the entities to track' + ' from the %s template(s). This entity will only be able' + ' to be updated manually.', + device, ', '.join(invalid_templates)) + 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) @@ -132,10 +144,12 @@ class BinarySensorTemplate(BinarySensorDevice): @callback def template_bsensor_startup(event): """Update template on startup.""" - async_track_state_change( - self.hass, self._entities, template_bsensor_state_listener) + if self._entities != MATCH_ALL: + # Track state change only for valid templates + async_track_state_change( + self.hass, self._entities, template_bsensor_state_listener) - self.hass.async_add_job(self.async_check_state) + self.async_check_state() self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, template_bsensor_startup) @@ -233,3 +247,7 @@ class BinarySensorTemplate(BinarySensorDevice): async_track_same_state( self.hass, period, set_state, entity_ids=self._entities, async_check_same_func=lambda *args: self._async_render() == state) + + async def async_update(self): + """Force update of the state from the template.""" + self.async_check_state() diff --git a/tests/components/binary_sensor/test_template.py b/tests/components/binary_sensor/test_template.py index f448bcc47a2..a1f97398616 100644 --- a/tests/components/binary_sensor/test_template.py +++ b/tests/components/binary_sensor/test_template.py @@ -1,10 +1,9 @@ """The tests for the Template Binary sensor platform.""" -import asyncio from datetime import timedelta import unittest from unittest import mock -from homeassistant.const import MATCH_ALL +from homeassistant.const import MATCH_ALL, EVENT_HOMEASSISTANT_START from homeassistant import setup from homeassistant.components.binary_sensor import template from homeassistant.exceptions import TemplateError @@ -182,7 +181,7 @@ class TestBinarySensorTemplate(unittest.TestCase): self.hass.states.set('sensor.any_state', 'update') self.hass.block_till_done() - assert len(_async_render.mock_calls) > init_calls + assert len(_async_render.mock_calls) == init_calls def test_attributes(self): """Test the attributes.""" @@ -252,8 +251,7 @@ class TestBinarySensorTemplate(unittest.TestCase): run_callback_threadsafe(self.hass.loop, vs.async_check_state).result() -@asyncio.coroutine -def test_template_delay_on(hass): +async def test_template_delay_on(hass): """Test binary sensor template delay on.""" config = { 'binary_sensor': { @@ -269,51 +267,50 @@ def test_template_delay_on(hass): }, }, } - yield from setup.async_setup_component(hass, 'binary_sensor', config) - yield from hass.async_start() + await setup.async_setup_component(hass, 'binary_sensor', config) + await hass.async_start() hass.states.async_set('sensor.test_state', 'on') - yield from hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get('binary_sensor.test') assert state.state == 'off' future = dt_util.utcnow() + timedelta(seconds=5) async_fire_time_changed(hass, future) - yield from hass.async_block_till_done() + 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') - yield from hass.async_block_till_done() + 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') - yield from hass.async_block_till_done() + 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') - yield from hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get('binary_sensor.test') assert state.state == 'off' future = dt_util.utcnow() + timedelta(seconds=5) async_fire_time_changed(hass, future) - yield from hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get('binary_sensor.test') assert state.state == 'off' -@asyncio.coroutine -def test_template_delay_off(hass): +async def test_template_delay_off(hass): """Test binary sensor template delay off.""" config = { 'binary_sensor': { @@ -330,44 +327,110 @@ def test_template_delay_off(hass): }, } hass.states.async_set('sensor.test_state', 'on') - yield from setup.async_setup_component(hass, 'binary_sensor', config) - yield from hass.async_start() + await setup.async_setup_component(hass, 'binary_sensor', config) + await hass.async_start() hass.states.async_set('sensor.test_state', 'off') - yield from hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get('binary_sensor.test') assert state.state == 'on' future = dt_util.utcnow() + timedelta(seconds=5) async_fire_time_changed(hass, future) - yield from hass.async_block_till_done() + 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') - yield from hass.async_block_till_done() + 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') - yield from hass.async_block_till_done() + 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') - yield from hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get('binary_sensor.test') assert state.state == 'on' future = dt_util.utcnow() + timedelta(seconds=5) async_fire_time_changed(hass, future) - yield from hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get('binary_sensor.test') assert state.state == 'on' + + +async def test_no_update_template_match_all(hass, caplog): + """Test that we do not update sensors that match on all.""" + hass.states.async_set('binary_sensor.test_sensor', 'true') + + await setup.async_setup_component(hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'template', + 'sensors': { + 'all_state': { + 'value_template': '{{ "true" }}', + }, + 'all_icon': { + 'value_template': + '{{ states.binary_sensor.test_sensor.state }}', + 'icon_template': '{{ 1 + 1 }}', + }, + 'all_entity_picture': { + 'value_template': + '{{ states.binary_sensor.test_sensor.state }}', + 'entity_picture_template': '{{ 1 + 1 }}', + }, + } + } + }) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 4 + assert ('Template binary sensor all_state has no entity ids ' + 'configured to track nor were we able to extract the entities to ' + 'track from the value template') in caplog.text + assert ('Template binary sensor all_icon has no entity ids ' + 'configured to track nor were we able to extract the entities to ' + 'track from the icon template') in caplog.text + assert ('Template binary sensor all_entity_picture has no entity ids ' + 'configured to track nor were we able to extract the entities to ' + 'track from the entity_picture template') in caplog.text + + assert hass.states.get('binary_sensor.all_state').state == 'off' + assert hass.states.get('binary_sensor.all_icon').state == 'off' + assert hass.states.get('binary_sensor.all_entity_picture').state == 'off' + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert hass.states.get('binary_sensor.all_state').state == 'on' + assert hass.states.get('binary_sensor.all_icon').state == 'on' + assert hass.states.get('binary_sensor.all_entity_picture').state == 'on' + + hass.states.async_set('binary_sensor.test_sensor', 'false') + await hass.async_block_till_done() + + assert hass.states.get('binary_sensor.all_state').state == 'on' + assert hass.states.get('binary_sensor.all_icon').state == 'on' + assert hass.states.get('binary_sensor.all_entity_picture').state == 'on' + + await hass.helpers.entity_component.async_update_entity( + 'binary_sensor.all_state') + await hass.helpers.entity_component.async_update_entity( + 'binary_sensor.all_icon') + await hass.helpers.entity_component.async_update_entity( + 'binary_sensor.all_entity_picture') + + assert hass.states.get('binary_sensor.all_state').state == 'on' + assert hass.states.get('binary_sensor.all_icon').state == 'off' + assert hass.states.get('binary_sensor.all_entity_picture').state == 'off'