diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index e0fc867720010c..ea0efdd0559324 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -14,6 +14,7 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, + CONF_AVAILABILITY_TEMPLATE, CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE, @@ -38,6 +39,7 @@ 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 @@ def __init__( value_template, icon_template, entity_picture_template, + availability_template, entity_ids, delay_on, delay_off, @@ -156,12 +162,14 @@ def __init__( 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 @@ def should_poll(self): """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,10 +253,22 @@ def _async_render(self): return _LOGGER.error("Could not render template %s: %s", self._name, ex) - templates = { - "_icon": self._icon_template, - "_entity_picture": self._entity_picture_template, - } + if self._availability_template is not None: + try: + self._available = ( + self._availability_template.async_render().lower() == "true" + ) + except TemplateError as ex: + if ex.args and ex.args[0].startswith( + "UndefinedError: 'None' has no attribute" + ): + # Common during HA startup - so just a warning + _LOGGER.warning( + "Could not render template %s, " "the state is unknown", + self._name, + ) + return + _LOGGER.error("Could not render template %s: %s", self._name, ex) attrs = {} if self._attribute_templates is not None: @@ -254,6 +279,11 @@ def _async_render(self): _LOGGER.error("Error rendering attribute %s: %s", key, err) self._attributes = attrs + templates = { + "_icon": self._icon_template, + "_entity_picture": self._entity_picture_template, + } + for property_name, template in templates.items(): if template is None: continue diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 51b9a523b3bbe2..31c39fcb6e456e 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -25,6 +25,7 @@ CONF_ENTITY_ID, EVENT_HOMEASSISTANT_START, MATCH_ALL, + CONF_AVAILABILITY_TEMPLATE, CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE, CONF_DEVICE_CLASS, @@ -74,6 +75,7 @@ vol.Exclusive( CONF_VALUE_TEMPLATE, CONF_VALUE_OR_POSITION_TEMPLATE ): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_POSITION_TEMPLATE): cv.template, vol.Optional(CONF_TILT_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, @@ -103,6 +105,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= position_template = device_config.get(CONF_POSITION_TEMPLATE) tilt_template = device_config.get(CONF_TILT_TEMPLATE) icon_template = device_config.get(CONF_ICON_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) device_class = device_config.get(CONF_DEVICE_CLASS) open_action = device_config.get(OPEN_ACTION) @@ -144,6 +147,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if str(temp_ids) != MATCH_ALL: template_entity_ids |= set(temp_ids) + if availability_template is not None: + temp_ids = availability_template.extract_entities() + if str(temp_ids) != MATCH_ALL: + template_entity_ids |= set(temp_ids) + if not template_entity_ids: template_entity_ids = MATCH_ALL @@ -160,6 +168,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= tilt_template, icon_template, entity_picture_template, + availability_template, open_action, close_action, stop_action, @@ -192,6 +201,7 @@ def __init__( tilt_template, icon_template, entity_picture_template, + availability_template, open_action, close_action, stop_action, @@ -213,6 +223,7 @@ def __init__( self._icon_template = icon_template self._device_class = device_class self._entity_picture_template = entity_picture_template + self._availability_template = availability_template self._open_script = None if open_action is not None: self._open_script = Script(hass, open_action) @@ -235,6 +246,7 @@ def __init__( self._position = None self._tilt_value = None self._entities = entity_ids + self._available = True if self._template is not None: self._template.hass = self.hass @@ -246,6 +258,8 @@ def __init__( self._icon_template.hass = self.hass if self._entity_picture_template is not None: self._entity_picture_template.hass = self.hass + if self._availability_template is not None: + self._availability_template.hass = self.hass async def async_added_to_hass(self): """Register callbacks.""" @@ -332,6 +346,11 @@ def should_poll(self): """Return the polling state.""" return False + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_open_cover(self, **kwargs): """Move the cover up.""" if self._open_script: @@ -430,11 +449,8 @@ async def async_update(self): ) else: self._position = state - except TemplateError as ex: - _LOGGER.error(ex) - self._position = None - except ValueError as ex: - _LOGGER.error(ex) + except (TemplateError, ValueError) as err: + _LOGGER.error(err) self._position = None if self._tilt_template is not None: try: @@ -447,12 +463,16 @@ async def async_update(self): ) else: self._tilt_value = state - except TemplateError as ex: - _LOGGER.error(ex) - self._tilt_value = None - except ValueError as ex: - _LOGGER.error(ex) + except (TemplateError, ValueError) as err: + _LOGGER.error(err) self._tilt_value = None + if self._availability_template is not None: + try: + result = self._availability_template.async_render() + self._available = result == "true" + except (TemplateError, ValueError) as err: + _LOGGER.error(err) + self._available = True for property_name, template in ( ("_icon", self._icon_template), diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 7fd8c4d9b3cea6..3ba09aa6d5a419 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -21,6 +21,7 @@ ) from homeassistant.const import ( CONF_FRIENDLY_NAME, + CONF_AVAILABILITY_TEMPLATE, CONF_VALUE_TEMPLATE, CONF_ENTITY_ID, STATE_ON, @@ -58,6 +59,7 @@ vol.Optional(CONF_SPEED_TEMPLATE): cv.template, vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, vol.Optional(CONF_DIRECTION_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA, @@ -86,6 +88,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= speed_template = device_config.get(CONF_SPEED_TEMPLATE) oscillating_template = device_config.get(CONF_OSCILLATING_TEMPLATE) direction_template = device_config.get(CONF_DIRECTION_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] @@ -103,6 +106,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= speed_template, oscillating_template, direction_template, + availability_template, ): if template is None: continue @@ -131,6 +135,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= speed_template, oscillating_template, direction_template, + availability_template, on_action, off_action, set_speed_action, @@ -156,6 +161,7 @@ def __init__( speed_template, oscillating_template, direction_template, + availability_template, on_action, off_action, set_speed_action, @@ -175,6 +181,8 @@ def __init__( self._speed_template = speed_template self._oscillating_template = oscillating_template self._direction_template = direction_template + self._availability_template = availability_template + self._available = True self._supported_features = 0 self._on_script = Script(hass, on_action) @@ -207,6 +215,8 @@ def __init__( if self._direction_template: self._direction_template.hass = self.hass self._supported_features |= SUPPORT_DIRECTION + if self._availability_template: + self._availability_template.hass = self.hass self._entities = entity_ids # List of valid speeds @@ -252,6 +262,11 @@ def should_poll(self): """Return the polling state.""" return False + @property + def available(self): + """Return availability of Device.""" + return self._available + # pylint: disable=arguments-differ async def async_turn_on(self, speed: str = None) -> None: """Turn on the fan.""" @@ -422,3 +437,12 @@ async def async_update(self): ", ".join(_VALID_DIRECTIONS), ) self._direction = None + + # Update Availability if 'availability_template' is defined + if self._availability_template is not None: + try: + result = self._availability_template.async_render() + self._available = result == "true" + except (TemplateError, ValueError) as err: + _LOGGER.error(err) + self._available = True diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 320dcd2e22feb1..554d9a7527eb5f 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -14,6 +14,7 @@ CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE, + CONF_AVAILABILITY_TEMPLATE, CONF_ENTITY_ID, CONF_FRIENDLY_NAME, STATE_ON, @@ -44,6 +45,7 @@ vol.Optional(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_LEVEL_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_LEVEL_TEMPLATE): cv.template, vol.Optional(CONF_FRIENDLY_NAME): cv.string, @@ -65,6 +67,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template = device_config.get(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) on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] level_action = device_config.get(CONF_LEVEL_ACTION) @@ -92,6 +95,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if str(temp_ids) != MATCH_ALL: template_entity_ids |= set(temp_ids) + if availability_template is not None: + temp_ids = availability_template.extract_entities() + if str(temp_ids) != MATCH_ALL: + template_entity_ids |= set(temp_ids) + if not template_entity_ids: template_entity_ids = MATCH_ALL @@ -105,6 +113,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template, icon_template, entity_picture_template, + availability_template, on_action, off_action, level_action, @@ -132,6 +141,7 @@ def __init__( state_template, icon_template, entity_picture_template, + availability_template, on_action, off_action, level_action, @@ -147,6 +157,7 @@ def __init__( self._template = state_template self._icon_template = icon_template self._entity_picture_template = entity_picture_template + self._availability_template = availability_template self._on_script = Script(hass, on_action) self._off_script = Script(hass, off_action) self._level_script = None @@ -159,6 +170,7 @@ def __init__( self._entity_picture = None self._brightness = None self._entities = entity_ids + self._available = True if self._template is not None: self._template.hass = self.hass @@ -168,6 +180,8 @@ def __init__( self._icon_template.hass = self.hass if self._entity_picture_template is not None: self._entity_picture_template.hass = self.hass + if self._availability_template is not None: + self._availability_template.hass = self.hass @property def brightness(self): @@ -207,6 +221,11 @@ def entity_picture(self): """Return the entity picture to use in the frontend, if any.""" return self._entity_picture + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_added_to_hass(self): """Register callbacks.""" @@ -218,7 +237,11 @@ def template_light_state_listener(entity, old_state, new_state): @callback def template_light_startup(event): """Update template on startup.""" - if self._template is not None or self._level_template is not None: + if ( + self._template is not None + or self._level_template is not None + or self._availability_template is not None + ): async_track_state_change( self.hass, self._entities, template_light_state_listener ) @@ -326,3 +349,10 @@ async def async_update(self): self._name, ex, ) + if self._availability_template is not None: + try: + result = self._availability_template.async_render() + self._available = result == "true" + except (TemplateError, ValueError) as err: + _LOGGER.error(err) + self._available = True diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index d7f501987f9527..b2cc44a8aedfcf 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -11,6 +11,7 @@ CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, + CONF_AVAILABILITY_TEMPLATE, EVENT_HOMEASSISTANT_START, STATE_ON, STATE_LOCKED, @@ -34,6 +35,7 @@ vol.Required(CONF_LOCK): cv.SCRIPT_SCHEMA, vol.Required(CONF_UNLOCK): cv.SCRIPT_SCHEMA, vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, } ) @@ -48,21 +50,32 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=N if value_template_entity_ids == MATCH_ALL: _LOGGER.warning( - "Template lock %s has no entity ids configured to track nor " - "were we able to extract the entities to track from the %s " + "Template lock '%s' has no entity ids configured to track nor " + "were we able to extract the entities to track from the '%s' " "template. This entity will only be able to be updated " "manually.", name, CONF_VALUE_TEMPLATE, ) + template_entity_ids = set() + template_entity_ids |= set(value_template_entity_ids) + + availability_template = config.get(CONF_AVAILABILITY_TEMPLATE) + if availability_template is not None: + availability_template.hass = hass + temp_ids = availability_template.extract_entities() + if str(temp_ids) != MATCH_ALL: + template_entity_ids |= set(temp_ids) + async_add_devices( [ TemplateLock( hass, name, value_template, - value_template_entity_ids, + availability_template, + template_entity_ids, config.get(CONF_LOCK), config.get(CONF_UNLOCK), config.get(CONF_OPTIMISTIC), @@ -79,6 +92,7 @@ def __init__( hass, name, value_template, + availability_template, entity_ids, command_lock, command_unlock, @@ -89,10 +103,12 @@ def __init__( self._hass = hass self._name = name self._state_template = value_template + self._availability_template = availability_template self._state_entities = entity_ids self._command_lock = Script(hass, command_lock) self._command_unlock = Script(hass, command_unlock) self._optimistic = optimistic + self._available = True async def async_added_to_hass(self): """Register callbacks.""" @@ -136,6 +152,11 @@ def is_locked(self): """Return true if lock is locked.""" return self._state + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_update(self): """Update the state from the template.""" try: @@ -148,6 +169,14 @@ async def async_update(self): self._state = None _LOGGER.error("Could not render template %s: %s", self._name, ex) + if self._availability_template is not None: + try: + result = self._availability_template.async_render() + self._available = result == "true" + except (TemplateError, ValueError) as err: + _LOGGER.error(err) + self._available = True + async def async_lock(self, **kwargs): """Lock the device.""" if self._optimistic: diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index b77528e0c324a7..57217581665378 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -17,6 +17,7 @@ CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE, + CONF_AVAILABILITY_TEMPLATE, ATTR_ENTITY_ID, CONF_SENSORS, EVENT_HOMEASSISTANT_START, @@ -39,6 +40,7 @@ vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, vol.Optional(CONF_FRIENDLY_NAME_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_ATTRIBUTE_TEMPLATES, default={}): vol.Schema( {cv.string: cv.template} ), @@ -62,6 +64,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_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) friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE) unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT) @@ -77,6 +80,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_ICON_TEMPLATE: icon_template, CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, CONF_FRIENDLY_NAME_TEMPLATE: friendly_name_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, } for tpl_name, template in chain(templates.items(), attribute_templates.items()): @@ -120,6 +124,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template, icon_template, entity_picture_template, + availability_template, entity_ids, device_class, attribute_templates, @@ -146,6 +151,7 @@ def __init__( state_template, icon_template, entity_picture_template, + availability_template, entity_ids, device_class, attribute_templates, @@ -162,10 +168,12 @@ def __init__( self._state = None self._icon_template = icon_template self._entity_picture_template = entity_picture_template + self._availability_template = availability_template self._icon = None self._entity_picture = None self._entities = entity_ids self._device_class = device_class + self._available = True self._attribute_templates = attribute_templates self._attributes = {} @@ -222,6 +230,11 @@ def unit_of_measurement(self): """Return the unit_of_measurement of the device.""" return self._unit_of_measurement + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + @property def device_state_attributes(self): """Return the state attributes.""" @@ -291,3 +304,10 @@ async def async_update(self): self._name, ex, ) + if self._availability_template is not None: + try: + result = self._availability_template.async_render() + self._available = result == "true" + except (TemplateError, ValueError) as err: + _LOGGER.error(err) + self._available = True diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index c77a90c1f8b6ee..578b0ae966b4ba 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -14,11 +14,13 @@ CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE, + CONF_AVAILABILITY_TEMPLATE, STATE_OFF, STATE_ON, ATTR_ENTITY_ID, CONF_SWITCHES, EVENT_HOMEASSISTANT_START, + MATCH_ALL, ) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv @@ -37,6 +39,7 @@ 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.Required(ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(ATTR_FRIENDLY_NAME): cv.string, @@ -58,6 +61,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_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) on_action = device_config[ON_ACTION] off_action = device_config[OFF_ACTION] entity_ids = ( @@ -72,6 +76,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if entity_picture_template is not None: entity_picture_template.hass = hass + if availability_template is not None: + availability_template.hass = hass + temp_ids = state_template.extract_entities() + if str(temp_ids) != MATCH_ALL: + entity_ids |= set(temp_ids) + switches.append( SwitchTemplate( hass, @@ -80,6 +90,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template, icon_template, entity_picture_template, + availability_template, on_action, off_action, entity_ids, @@ -104,6 +115,7 @@ def __init__( state_template, icon_template, entity_picture_template, + availability_template, on_action, off_action, entity_ids, @@ -120,9 +132,11 @@ def __init__( self._state = False self._icon_template = icon_template self._entity_picture_template = entity_picture_template + self._availability_template = availability_template self._icon = None self._entity_picture = None self._entities = entity_ids + self._available = True async def async_added_to_hass(self): """Register callbacks.""" @@ -160,11 +174,6 @@ def should_poll(self): """Return the polling state.""" return False - @property - def available(self): - """If switch is available.""" - return self._state is not None - @property def icon(self): """Return the icon to use in the frontend, if any.""" @@ -175,6 +184,11 @@ def entity_picture(self): """Return the entity_picture to use in the frontend, if any.""" return self._entity_picture + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_turn_on(self, **kwargs): """Fire the on action.""" await self._on_script.async_run(context=self._context) @@ -233,3 +247,10 @@ async def async_update(self): self._name, ex, ) + if self._availability_template is not None: + try: + result = self._availability_template.async_render() + self._available = result == "true" + except (TemplateError, ValueError) as err: + _LOGGER.error(err) + self._available = True diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 5374247daccd70..ea9ebc59e13cd8 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -34,6 +34,7 @@ from homeassistant.const import ( CONF_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, + CONF_AVAILABILITY_TEMPLATE, CONF_ENTITY_ID, MATCH_ALL, EVENT_HOMEASSISTANT_START, @@ -67,6 +68,7 @@ vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_BATTERY_LEVEL_TEMPLATE): cv.template, vol.Optional(CONF_FAN_SPEED_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Required(SERVICE_START): cv.SCRIPT_SCHEMA, vol.Optional(SERVICE_PAUSE): cv.SCRIPT_SCHEMA, vol.Optional(SERVICE_STOP): cv.SCRIPT_SCHEMA, @@ -94,6 +96,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template = device_config.get(CONF_VALUE_TEMPLATE) battery_level_template = device_config.get(CONF_BATTERY_LEVEL_TEMPLATE) fan_speed_template = device_config.get(CONF_FAN_SPEED_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) start_action = device_config[SERVICE_START] pause_action = device_config.get(SERVICE_PAUSE) @@ -113,6 +116,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= (CONF_VALUE_TEMPLATE, state_template), (CONF_BATTERY_LEVEL_TEMPLATE, battery_level_template), (CONF_FAN_SPEED_TEMPLATE, fan_speed_template), + (CONF_AVAILABILITY_TEMPLATE, availability_template), ): if template is None: continue @@ -152,6 +156,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template, battery_level_template, fan_speed_template, + availability_template, start_action, pause_action, stop_action, @@ -178,6 +183,7 @@ def __init__( state_template, battery_level_template, fan_speed_template, + availability_template, start_action, pause_action, stop_action, @@ -198,6 +204,7 @@ def __init__( self._template = state_template self._battery_level_template = battery_level_template self._fan_speed_template = fan_speed_template + self._availability_template = availability_template self._supported_features = SUPPORT_START self._start_script = Script(hass, start_action) @@ -235,6 +242,7 @@ def __init__( self._state = None self._battery_level = None self._fan_speed = None + self._available = True if self._template: self._supported_features |= SUPPORT_STATE @@ -280,6 +288,11 @@ def should_poll(self): """Return the polling state.""" return False + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_start(self): """Start or resume the cleaning task.""" await self._start_script.async_run(context=self._context) @@ -421,3 +434,11 @@ async def async_update(self): self._fan_speed_list, ) self._fan_speed = None + # Update availability if availability template is defined + if self._availability_template is not None: + try: + result = self._availability_template.async_render() + self._available = result == "true" + except (TemplateError, ValueError) as err: + _LOGGER.error(err) + self._available = True diff --git a/homeassistant/const.py b/homeassistant/const.py index 4cfd16b8c9f0cc..5eb351916190ba 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -35,6 +35,7 @@ CONF_AUTHENTICATION = "authentication" CONF_AUTH_MFA_MODULES = "auth_mfa_modules" CONF_AUTH_PROVIDERS = "auth_providers" +CONF_AVAILABILITY_TEMPLATE = "availability_template" CONF_BASE = "base" CONF_BEFORE = "before" CONF_BELOW = "below" diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index c8cec168d6e6cd..5a3a44b53651e5 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -238,6 +238,7 @@ def test_attributes(self): template_hlpr.Template("{{ 1 > 1 }}", self.hass), None, None, + None, MATCH_ALL, None, None, @@ -298,6 +299,7 @@ def test_update_template_error(self, mock_render): template_hlpr.Template("{{ 1 > 1 }}", self.hass), None, None, + None, MATCH_ALL, None, None, @@ -428,6 +430,67 @@ 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() + + state = hass.states.get("binary_sensor.test") + assert 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": "{% if states.sensor.test_state.state == 'on' %}" + "true" + "{% else %}" + "false" + "{% endif %}", + } + }, + } + } + hass.states.async_set("sensor.test_state", "on") + await setup.async_setup_component(hass, "binary_sensor", config) + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.async_set("sensor.test_state", "off") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == "unavailable" + + hass.states.async_set("sensor.test_state", "on") + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert 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") diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 247ee25027c94f..36208ac215ab40 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -16,6 +16,7 @@ SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, STATE_CLOSED, + STATE_UNAVAILABLE, STATE_OPEN, ) @@ -839,6 +840,86 @@ async def test_entity_picture_template(hass, calls): assert state.attributes["entity_picture"] == "/local/cover.png" +async def test_availability_template(hass, calls): + """Test availability template.""" + with assert_setup_component(1, "cover"): + assert await setup.async_setup_component( + hass, + "cover", + { + "cover": { + "platform": "template", + "covers": { + "test_template_cover": { + "value_template": "open", + "open_cover": { + "service": "cover.open_cover", + "entity_id": "cover.test_state", + }, + "close_cover": { + "service": "cover.close_cover", + "entity_id": "cover.test_state", + }, + "availability_template": "{% if states.cover.test_state.state == 'open' %}" + "true" + "{% else %}" + "false" + "{% endif %}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.async_set("cover.test_state", STATE_CLOSED) + await hass.async_block_till_done() + + state = hass.states.get("cover.test_template_cover") + assert state.state == STATE_UNAVAILABLE + + state = hass.states.async_set("cover.test_state", STATE_OPEN) + await hass.async_block_till_done() + + state = hass.states.get("cover.test_template_cover") + assert state.state != STATE_UNAVAILABLE + + +async def test_availability_without_availability_template(hass, calls): + """Test that component is availble if there is no.""" + with assert_setup_component(1, "cover"): + assert await setup.async_setup_component( + hass, + "cover", + { + "cover": { + "platform": "template", + "covers": { + "test_template_cover": { + "value_template": "open", + "open_cover": { + "service": "cover.open_cover", + "entity_id": "cover.test_state", + }, + "close_cover": { + "service": "cover.close_cover", + "entity_id": "cover.test_state", + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("cover.test_template_cover") + assert state.state != STATE_UNAVAILABLE + + async def test_device_class(hass, calls): """Test device class.""" with assert_setup_component(1, "cover"): diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index b80522b37e24c5..992a209118dcf0 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant import setup -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from homeassistant.components.fan import ( ATTR_SPEED, ATTR_OSCILLATING, @@ -26,6 +26,8 @@ _TEST_FAN = "fan.test_fan" # Represent for fan's state _STATE_INPUT_BOOLEAN = "input_boolean.state" +# Represent for fan's state +_STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" # Represent for fan's speed _SPEED_INPUT_SELECT = "input_select.speed" # Represent for fan's oscillating @@ -214,6 +216,57 @@ async def test_templates_with_entities(hass, calls): _verify(hass, STATE_ON, SPEED_MEDIUM, True, DIRECTION_FORWARD) +async def test_availability_template_with_entities(hass, calls): + """Test availability tempalates with values from other entities.""" + availability_template = """ + {% if is_state('availability_boolean.state', 'True') %} + {{ 'true' }} + {% else %} + {{ 'false' }} + {% endif %} + """ + with assert_setup_component(1, "fan"): + assert await setup.async_setup_component( + hass, + "fan", + { + "fan": { + "platform": "template", + "fans": { + "test_fan": { + "availability_template": availability_template, + "value_template": "{{ 'on' }}", + "speed_template": "{{ 'medium' }}", + "oscillating_template": "{{ 1 == 1 }}", + "direction_template": "{{ 'forward' }}", + "turn_on": {"service": "script.fan_on"}, + "turn_off": {"service": "script.fan_off"}, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + # When template returns true.. + hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, True) + await hass.async_block_till_done() + + # Device State should not be unavailable + state = hass.states.get(_TEST_FAN) + assert state.state != STATE_UNAVAILABLE + + # When Availability template returns false + hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, False) + await hass.async_block_till_done() + + # device state should be unavailable + state = hass.states.get(_TEST_FAN) + assert state.state == STATE_UNAVAILABLE + + async def test_templates_with_valid_values(hass, calls): """Test templates with valid values.""" with assert_setup_component(1, "fan"): diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 87fd8cd4db3f34..aeb93d75630540 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -4,13 +4,16 @@ from homeassistant.core import callback from homeassistant import setup from homeassistant.components.light import ATTR_BRIGHTNESS -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from tests.common import get_test_home_assistant, assert_setup_component from tests.components.light import common _LOGGER = logging.getLogger(__name__) +# Represent for light's availability +_STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" + class TestTemplateLight: """Test the Template light.""" @@ -774,3 +777,60 @@ def test_entity_picture_template(self): state = self.hass.states.get("light.test_template_light") assert state.attributes["entity_picture"] == "/local/light.png" + + def test_available_template_with_entities(self): + """Test availability templates with values from other entities.""" + availability_template = """ + {% if is_state('availability_boolean.state', 'True') %} + {{ 'true' }} + {% else %} + {{ 'false' }} + {% endif %} + """ + assert setup.setup_component( + self.hass, + "light", + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "availability_template": availability_template, + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + }, + }, + } + }, + } + }, + ) + self.hass.start() + self.hass.block_till_done() + + # When template returns true.. + self.hass.states.set(_STATE_AVAILABILITY_BOOLEAN, True) + self.hass.block_till_done() + + # Device State should not be unavailable + state = self.hass.states.get("light.test_template_light") + assert state.state != STATE_UNAVAILABLE + + # When Availability template returns false + self.hass.states.set(_STATE_AVAILABILITY_BOOLEAN, False) + self.hass.block_till_done() + + # device state should be unavailable + state = self.hass.states.get("light.test_template_light") + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index 24cde24051a69a..347933574f65a4 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -5,7 +5,7 @@ from homeassistant import setup from homeassistant.components import lock from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from tests.common import get_test_home_assistant, assert_setup_component @@ -254,9 +254,9 @@ def test_no_template_match_all(self, caplog): assert state.state == lock.STATE_UNLOCKED assert ( - "Template lock Template Lock has no entity ids configured " + "Template lock 'Template Lock' has no entity ids configured " "to track nor were we able to extract the entities to track " - "from the value_template template. This entity will only " + "from the 'value_template' template. This entity will only " "be able to be updated manually." ) in caplog.text @@ -332,3 +332,52 @@ def test_unlock_action(self): self.hass.block_till_done() assert len(self.calls) == 1 + + def test_available_template_with_entities(self): + """Test availability templates with values from other entities.""" + availability_template = """ + {% if is_state('availability_boolean.state', 'True') %} + {{ 'true' }} + {% else %} + {{ 'false' }} + {% endif %} + """ + + assert setup.setup_component( + self.hass, + "lock", + { + "lock": { + "platform": "template", + "value_template": "{{ 'on' }}", + "lock": { + "service": "switch.turn_on", + "entity_id": "switch.test_state", + }, + "unlock": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, + "availability_template": availability_template, + } + }, + ) + + self.hass.start() + self.hass.block_till_done() + + # When template returns true.. + self.hass.states.set("availability_boolean.state", True) + self.hass.block_till_done() + + # Device State should not be unavailable + state = self.hass.states.get("lock.template_lock") + assert state.state != STATE_UNAVAILABLE + + # When Availability template returns false + self.hass.states.set("availability_boolean.state", False) + self.hass.block_till_done() + + # device state should be unavailable + state = self.hass.states.get("lock.template_lock") + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 9223399bee7af1..91e67e99e8a0b4 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -3,6 +3,7 @@ from homeassistant.setup import setup_component, async_setup_component from tests.common import get_test_home_assistant, assert_setup_component +from homeassistant.const import STATE_UNAVAILABLE class TestTemplateSensor: @@ -376,6 +377,52 @@ def test_setup_valid_device_class(self): state = self.hass.states.get("sensor.test2") assert "device_class" not in state.attributes + def test_available_template_with_entities(self): + """Test availability tempalates with values from other entities.""" + availability_template = """ + {% if is_state('availability_boolean.state', 'True') %} + {{ 'true' }} + {% else %} + {{ 'false' }} + {% endif %} + """ + + with assert_setup_component(1): + assert setup_component( + self.hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "test_template_sensor": { + "value_template": "{{ states.sensor.test_state.state }}", + "availability_template": availability_template, + } + }, + } + }, + ) + + self.hass.start() + self.hass.block_till_done() + + # When template returns true.. + self.hass.states.set("availability_boolean.state", True) + self.hass.block_till_done() + + # Device State should not be unavailable + state = self.hass.states.get("sensor.test_template_sensor") + assert state.state != STATE_UNAVAILABLE + + # When Availability template returns false + self.hass.states.set("availability_boolean.state", False) + self.hass.block_till_done() + + # device state should be unavailable + state = self.hass.states.get("sensor.test_template_sensor") + assert state.state == STATE_UNAVAILABLE + async def test_invalid_attribute_template(hass, caplog): """Test that errors are logged if rendering template fails.""" diff --git a/tests/components/template/test_switch.py b/tests/components/template/test_switch.py index 9a07a935d12546..5842329ed1a557 100644 --- a/tests/components/template/test_switch.py +++ b/tests/components/template/test_switch.py @@ -1,7 +1,7 @@ """The tests for the Template switch platform.""" from homeassistant.core import callback from homeassistant import setup -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from tests.common import get_test_home_assistant, assert_setup_component from tests.components.switch import common @@ -135,6 +135,55 @@ def test_template_state_boolean_off(self): state = self.hass.states.get("switch.test_template_switch") assert state.state == STATE_OFF + def test_available_template_with_entities(self): + """Test availability templates with values from other entities.""" + availability_template = """ + {% if is_state('availability_boolean.state', 'True') %} + {{ 'true' }} + {% else %} + {{ 'false' }} + {% endif %} + """ + with assert_setup_component(1, "switch"): + assert setup.setup_component( + self.hass, + "switch", + { + "switch": { + "platform": "template", + "switches": { + "test_template_switch": { + "value_template": "{{ 1 == 1 }}", + "turn_on": { + "service": "switch.turn_on", + "entity_id": "switch.test_state", + }, + "turn_off": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, + "availability_template": availability_template, + } + }, + } + }, + ) + + self.hass.start() + self.hass.block_till_done() + + self.hass.states.set("availability_boolean.state", True) + self.hass.block_till_done() + + state = self.hass.states.get("switch.test_template_switch") + assert state.state != STATE_UNAVAILABLE + + self.hass.states.set("availability_boolean.state", False) + self.hass.block_till_done() + + state = self.hass.states.get("switch.test_template_switch") + assert state.state == STATE_UNAVAILABLE + def test_icon_template(self): """Test icon template.""" with assert_setup_component(1, "switch"): diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index 9e3c535f136d3c..45fd0b4caf4c2a 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -3,7 +3,7 @@ import pytest from homeassistant import setup -from homeassistant.const import STATE_ON, STATE_UNKNOWN +from homeassistant.const import STATE_ON, STATE_UNKNOWN, STATE_UNAVAILABLE from homeassistant.components.vacuum import ( ATTR_BATTERY_LEVEL, STATE_CLEANING, @@ -210,6 +210,51 @@ async def test_invalid_templates(hass, calls): _verify(hass, STATE_UNKNOWN, None) +async def test_available_template_with_entities(hass, calls): + """Test availability templates with values from other entities.""" + availability_template = """ + {% if is_state('availability_boolean.state', 'True') %} + {{ 'true' }} + {% else %} + {{ 'false' }} + {% endif %} + """ + with assert_setup_component(1, "vacuum"): + assert await setup.async_setup_component( + hass, + "vacuum", + { + "vacuum": { + "platform": "template", + "vacuums": { + "test_template_vacuum": { + "availability_template": availability_template, + "start": {"service": "script.vacuum_start"}, + } + }, + } + }, + ) + await hass.async_start() + await hass.async_block_till_done() + + # When template returns true.. + hass.states.async_set("availability_boolean.state", True) + await hass.async_block_till_done() + + # Device State should not be unavailable + state = hass.states.get("vacuum.test_template_vacuum") + assert state.state != STATE_UNAVAILABLE + + # When Availability template returns false + hass.states.async_set("availability_boolean.state", False) + await hass.async_block_till_done() + + # device state should be unavailable + state = hass.states.get("vacuum.test_template_vacuum") + assert state.state == STATE_UNAVAILABLE + + # End of template tests #