diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index e0fc8677200..d5ade703c97 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -26,6 +26,7 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change, async_track_same_state +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -38,6 +39,7 @@ 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_AVAILABILITY_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, @@ -60,6 +62,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= value_template = device_config[CONF_VALUE_TEMPLATE] icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) entity_ids = set() manual_entity_ids = device_config.get(ATTR_ENTITY_ID) attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {}) @@ -70,6 +73,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_VALUE_TEMPLATE: value_template, CONF_ICON_TEMPLATE: icon_template, CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, } for tpl_name, template in chain(templates.items(), attribute_templates.items()): @@ -117,6 +121,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= value_template, icon_template, entity_picture_template, + availability_template, entity_ids, delay_on, delay_off, @@ -143,6 +148,7 @@ class BinarySensorTemplate(BinarySensorDevice): value_template, icon_template, entity_picture_template, + availability_template, entity_ids, delay_on, delay_off, @@ -156,12 +162,14 @@ class BinarySensorTemplate(BinarySensorDevice): self._template = value_template self._state = None self._icon_template = icon_template + self._availability_template = availability_template self._entity_picture_template = entity_picture_template self._icon = None self._entity_picture = None self._entities = entity_ids self._delay_on = delay_on self._delay_off = delay_off + self._available = True self._attribute_templates = attribute_templates self._attributes = {} @@ -223,6 +231,11 @@ class BinarySensorTemplate(BinarySensorDevice): """No polling needed.""" return False + @property + def available(self): + """Availability indicator.""" + return self._available + @callback def _async_render(self): """Get the state of template.""" @@ -240,11 +253,6 @@ class BinarySensorTemplate(BinarySensorDevice): return _LOGGER.error("Could not render template %s: %s", self._name, ex) - 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(): @@ -254,12 +262,21 @@ class BinarySensorTemplate(BinarySensorDevice): _LOGGER.error("Error rendering attribute %s: %s", key, err) self._attributes = attrs + templates = { + "_icon": self._icon_template, + "_entity_picture": self._entity_picture_template, + "_available": self._availability_template, + } + for property_name, template in templates.items(): if template is None: continue try: - setattr(self, property_name, template.async_render()) + value = template.async_render() + if property_name == "_available": + value = value.lower() == "true" + setattr(self, property_name, value) except TemplateError as ex: friendly_property_name = property_name[1:].replace("_", " ") if ex.args and ex.args[0].startswith( diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index c8cec168d6e..143811da209 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -3,7 +3,13 @@ from datetime import timedelta import unittest from unittest import mock -from homeassistant.const import MATCH_ALL, EVENT_HOMEASSISTANT_START +from homeassistant.const import ( + MATCH_ALL, + EVENT_HOMEASSISTANT_START, + STATE_UNAVAILABLE, + STATE_ON, + STATE_OFF, +) from homeassistant import setup from homeassistant.components.template import binary_sensor as template from homeassistant.exceptions import TemplateError @@ -238,6 +244,7 @@ class TestBinarySensorTemplate(unittest.TestCase): template_hlpr.Template("{{ 1 > 1 }}", self.hass), None, None, + None, MATCH_ALL, None, None, @@ -298,6 +305,7 @@ class TestBinarySensorTemplate(unittest.TestCase): template_hlpr.Template("{{ 1 > 1 }}", self.hass), None, None, + None, MATCH_ALL, None, None, @@ -428,6 +436,59 @@ async def test_template_delay_off(hass): assert state.state == "on" +async def test_available_without_availability_template(hass): + """Ensure availability is true without an availability_template.""" + config = { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "true", + "device_class": "motion", + "delay_off": 5, + } + }, + } + } + await setup.async_setup_component(hass, "binary_sensor", config) + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test").state != STATE_UNAVAILABLE + + +async def test_availability_template(hass): + """Test availability template.""" + config = { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "true", + "device_class": "motion", + "delay_off": 5, + "availability_template": "{{ is_state('sensor.test_state','on') }}", + } + }, + } + } + await setup.async_setup_component(hass, "binary_sensor", config) + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("sensor.test_state", STATE_OFF) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test").state == STATE_UNAVAILABLE + + hass.states.async_set("sensor.test_state", STATE_ON) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test").state != STATE_UNAVAILABLE + + 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") @@ -458,6 +519,32 @@ async def test_invalid_attribute_template(hass, caplog): assert ("Error rendering attribute test_attribute") in caplog.text +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + + await setup.async_setup_component( + hass, + "binary_sensor", + { + "binary_sensor": { + "platform": "template", + "sensors": { + "my_sensor": { + "value_template": "{{ states.binary_sensor.test_sensor }}", + "availability_template": "{{ x - 12 }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.my_sensor").state != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") 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")