From 9bcb48985be9d9498ed37b1c54cd2b57cc8ca307 Mon Sep 17 00:00:00 2001 From: Steven Rollason <2099542+gadgetchnnel@users.noreply.github.com> Date: Wed, 21 Aug 2019 22:07:27 +0100 Subject: [PATCH] Template binary sensor attributes (#22664) * Added attribute support to template binary sensor with tests Added attribute support to template binary sensor with tests * fix dictionary update fix dictionary update * Fixed whitespace and line length issues * Fixed indentation * Simplify applying of attribute templates based on feedback * Syntax and whitespace fixes * Black formatting * Black formatting on tests * Check attribute_templates is not None * Fixed test * Added test for failure to render template * Test fix * Updated test * Removed whitespace and applied Black formatting * Fixed test assertion * Updated test * Code improvements folloing comments Using chain to iterate over templates and attribute_templates Replacing dict() with {} Rmoving unused constant * Applied Black formatting * Fixed removed code * Default attribute_templates to empty dict * Black formatting * Fixed imports --- .../components/template/binary_sensor.py | 46 ++++++++--- .../components/template/test_binary_sensor.py | 82 ++++++++++++++++++- 2 files changed, 117 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 8b354f4eeb2..e0fc8677200 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -1,5 +1,6 @@ """Support for exposing a templated binary sensor.""" import logging +from itertools import chain import voluptuous as vol @@ -30,12 +31,14 @@ _LOGGER = logging.getLogger(__name__) CONF_DELAY_ON = "delay_on" CONF_DELAY_OFF = "delay_off" +CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" SENSOR_SCHEMA = vol.Schema( { vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, + vol.Optional(CONF_ATTRIBUTE_TEMPLATES): vol.Schema({cv.string: cv.template}), vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, @@ -59,14 +62,17 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) entity_ids = set() manual_entity_ids = device_config.get(ATTR_ENTITY_ID) + attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {}) invalid_templates = [] - for tpl_name, template in ( - (CONF_VALUE_TEMPLATE, value_template), - (CONF_ICON_TEMPLATE, icon_template), - (CONF_ENTITY_PICTURE_TEMPLATE, entity_picture_template), - ): + templates = { + CONF_VALUE_TEMPLATE: value_template, + CONF_ICON_TEMPLATE: icon_template, + CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + } + + for tpl_name, template in chain(templates.items(), attribute_templates.items()): if template is None: continue template.hass = hass @@ -78,7 +84,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if template_entity_ids == MATCH_ALL: entity_ids = MATCH_ALL # Cut off _template from name - invalid_templates.append(tpl_name[:-9]) + invalid_templates.append(tpl_name.replace("_template", "")) elif entity_ids != MATCH_ALL: entity_ids |= set(template_entity_ids) @@ -114,6 +120,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entity_ids, delay_on, delay_off, + attribute_templates, ) ) if not sensors: @@ -139,6 +146,7 @@ class BinarySensorTemplate(BinarySensorDevice): entity_ids, delay_on, delay_off, + attribute_templates, ): """Initialize the Template binary sensor.""" self.hass = hass @@ -154,6 +162,8 @@ class BinarySensorTemplate(BinarySensorDevice): self._entities = entity_ids self._delay_on = delay_on self._delay_off = delay_off + self._attribute_templates = attribute_templates + self._attributes = {} async def async_added_to_hass(self): """Register callbacks.""" @@ -203,6 +213,11 @@ class BinarySensorTemplate(BinarySensorDevice): """Return the sensor class of the sensor.""" return self._device_class + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + @property def should_poll(self): """No polling needed.""" @@ -225,10 +240,21 @@ class BinarySensorTemplate(BinarySensorDevice): return _LOGGER.error("Could not render template %s: %s", self._name, ex) - for property_name, template in ( - ("_icon", self._icon_template), - ("_entity_picture", self._entity_picture_template), - ): + templates = { + "_icon": self._icon_template, + "_entity_picture": self._entity_picture_template, + } + + attrs = {} + if self._attribute_templates is not None: + for key, value in self._attribute_templates.items(): + try: + attrs[key] = value.async_render() + except TemplateError as err: + _LOGGER.error("Error rendering attribute %s: %s", key, err) + self._attributes = attrs + + for property_name, template in templates.items(): if template is None: continue diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index c0b73f9c559..c8cec168d6e 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -166,6 +166,38 @@ class TestBinarySensorTemplate(unittest.TestCase): state = self.hass.states.get("binary_sensor.test_template_sensor") assert state.attributes["entity_picture"] == "/local/sensor.png" + def test_attribute_templates(self): + """Test attribute_templates template.""" + with assert_setup_component(1): + assert setup.setup_component( + self.hass, + "binary_sensor", + { + "binary_sensor": { + "platform": "template", + "sensors": { + "test_template_sensor": { + "value_template": "{{ states.sensor.xyz.state }}", + "attribute_templates": { + "test_attribute": "It {{ states.sensor.test_state.state }}." + }, + } + }, + } + }, + ) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get("binary_sensor.test_template_sensor") + assert state.attributes.get("test_attribute") == "It ." + + self.hass.states.set("sensor.test_state", "Works") + self.hass.block_till_done() + state = self.hass.states.get("binary_sensor.test_template_sensor") + assert state.attributes["test_attribute"] == "It Works." + @mock.patch( "homeassistant.components.template.binary_sensor." "BinarySensorTemplate._async_render" @@ -209,6 +241,7 @@ class TestBinarySensorTemplate(unittest.TestCase): MATCH_ALL, None, None, + None, ).result() assert not vs.should_poll assert "motion" == vs.device_class @@ -268,6 +301,7 @@ class TestBinarySensorTemplate(unittest.TestCase): MATCH_ALL, None, None, + None, ).result() mock_render.side_effect = TemplateError("foo") run_callback_threadsafe(self.hass.loop, vs.async_check_state).result() @@ -394,6 +428,36 @@ async def test_template_delay_off(hass): assert state.state == "on" +async def test_invalid_attribute_template(hass, caplog): + """Test that errors are logged if rendering template fails.""" + hass.states.async_set("binary_sensor.test_sensor", "true") + + await setup.async_setup_component( + hass, + "binary_sensor", + { + "binary_sensor": { + "platform": "template", + "sensors": { + "invalid_template": { + "value_template": "{{ states.binary_sensor.test_sensor }}", + "attribute_templates": { + "test_attribute": "{{ states.binary_sensor.unknown.attributes.picture }}" + }, + } + }, + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + await hass.helpers.entity_component.async_update_entity( + "binary_sensor.invalid_template" + ) + + assert ("Error rendering attribute test_attribute") in caplog.text + + 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") @@ -414,12 +478,16 @@ async def test_no_update_template_match_all(hass, caplog): "value_template": "{{ states.binary_sensor.test_sensor.state }}", "entity_picture_template": "{{ 1 + 1 }}", }, + "all_attribute": { + "value_template": "{{ states.binary_sensor.test_sensor.state }}", + "attribute_templates": {"test_attribute": "{{ 1 + 1 }}"}, + }, }, } }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 assert ( "Template binary sensor all_state has no entity ids " "configured to track nor were we able to extract the entities to " @@ -435,10 +503,16 @@ async def test_no_update_template_match_all(hass, caplog): "configured to track nor were we able to extract the entities to " "track from the entity_picture template" ) in caplog.text + assert ( + "Template binary sensor all_attribute has no entity ids " + "configured to track nor were we able to extract the entities to " + "track from the test_attribute 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" + assert hass.states.get("binary_sensor.all_attribute").state == "off" hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -446,6 +520,7 @@ async def test_no_update_template_match_all(hass, caplog): 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" + assert hass.states.get("binary_sensor.all_attribute").state == "on" hass.states.async_set("binary_sensor.test_sensor", "false") await hass.async_block_till_done() @@ -453,13 +528,18 @@ async def test_no_update_template_match_all(hass, caplog): 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" + assert hass.states.get("binary_sensor.all_attribute").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" ) + await hass.helpers.entity_component.async_update_entity( + "binary_sensor.all_attribute" + ) 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" + assert hass.states.get("binary_sensor.all_attribute").state == "off"