Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add availability_template to Template Sensor platform #26516

Merged
merged 15 commits into from Sep 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions homeassistant/components/template/const.py
@@ -0,0 +1,3 @@
"""Constants for the Template Platform Components."""

CONF_AVAILABILITY_TEMPLATE = "availability_template"
38 changes: 27 additions & 11 deletions homeassistant/components/template/sensor.py
Expand Up @@ -24,10 +24,12 @@
MATCH_ALL,
CONF_DEVICE_CLASS,
)

from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity, async_generate_entity_id
from homeassistant.helpers.event import async_track_state_change
from .const import CONF_AVAILABILITY_TEMPLATE

CONF_ATTRIBUTE_TEMPLATES = "attribute_templates"

Expand All @@ -39,6 +41,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}
),
Expand All @@ -62,6 +65,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)
Expand All @@ -77,6 +81,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()):
Expand Down Expand Up @@ -120,15 +125,12 @@ 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,
)
)
if not sensors:
_LOGGER.error("No sensors added")
return False

async_add_entities(sensors)
return True

Expand All @@ -146,6 +148,7 @@ def __init__(
state_template,
icon_template,
entity_picture_template,
availability_template,
entity_ids,
device_class,
attribute_templates,
Expand All @@ -162,10 +165,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 = {}

Expand Down Expand Up @@ -222,6 +227,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."""
Expand All @@ -236,7 +246,9 @@ async def async_update(self):
"""Update the state from the template."""
try:
self._state = self._template.async_render()
self._available = True
except TemplateError as ex:
self._available = False
if ex.args and ex.args[0].startswith(
"UndefinedError: 'None' has no attribute"
):
Expand All @@ -248,12 +260,6 @@ async def async_update(self):
self._state = None
_LOGGER.error("Could not render template %s: %s", self._name, ex)

templates = {
"_icon": self._icon_template,
"_entity_picture": self._entity_picture_template,
"_name": self._friendly_name_template,
}

attrs = {}
for key, value in self._attribute_templates.items():
try:
Expand All @@ -263,12 +269,22 @@ async def async_update(self):

self._attributes = attrs

templates = {
"_icon": self._icon_template,
"_entity_picture": self._entity_picture_template,
"_name": self._friendly_name_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(
Expand Down
67 changes: 66 additions & 1 deletion tests/components/template/test_sensor.py
Expand Up @@ -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, STATE_ON, STATE_OFF


class TestTemplateSensor:
Expand Down Expand Up @@ -251,7 +252,7 @@ def test_template_attribute_missing(self):
self.hass.block_till_done()

state = self.hass.states.get("sensor.test_template_sensor")
assert state.state == "unknown"
assert state.state == STATE_UNAVAILABLE

def test_invalid_name_does_not_create(self):
"""Test invalid name."""
Expand Down Expand Up @@ -377,6 +378,44 @@ def test_setup_valid_device_class(self):
assert "device_class" not in state.attributes


async def test_available_template_with_entities(hass):
"""Test availability tempalates with values from other entities."""
hass.states.async_set("sensor.availability_sensor", STATE_OFF)
with assert_setup_component(1, "sensor"):
assert await async_setup_component(
hass,
"sensor",
{
"sensor": {
"platform": "template",
"sensors": {
"test_template_sensor": {
"value_template": "{{ states.sensor.test_sensor.state }}",
"availability_template": "{{ is_state('sensor.availability_sensor', 'on') }}",
}
},
}
},
)

await hass.async_start()
await hass.async_block_till_done()

# When template returns true..
hass.states.async_set("sensor.availability_sensor", STATE_ON)
await hass.async_block_till_done()

# Device State should not be unavailable
assert hass.states.get("sensor.test_template_sensor").state != STATE_UNAVAILABLE

# When Availability template returns false
hass.states.async_set("sensor.availability_sensor", STATE_OFF)
await hass.async_block_till_done()

# device state should be unavailable
assert hass.states.get("sensor.test_template_sensor").state == STATE_UNAVAILABLE


async def test_invalid_attribute_template(hass, caplog):
"""Test that errors are logged if rendering template fails."""
hass.states.async_set("sensor.test_sensor", "startup")
Expand Down Expand Up @@ -405,6 +444,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 async_setup_component(
hass,
"sensor",
{
"sensor": {
"platform": "template",
"sensors": {
"my_sensor": {
"value_template": "{{ states.sensor.test_state.state }}",
"availability_template": "{{ x - 12 }}",
}
},
}
},
)

await hass.async_start()
grillp marked this conversation as resolved.
Show resolved Hide resolved
await hass.async_block_till_done()

assert hass.states.get("sensor.my_sensor").state != STATE_UNAVAILABLE
assert ("UndefinedError: 'x' is undefined") in caplog.text


async def test_no_template_match_all(hass, caplog):
"""Test that we do not allow sensors that match on all."""
hass.states.async_set("sensor.test_sensor", "startup")
Expand Down