core/tests/helpers/test_template.py

691 lines
25 KiB
Python

"""Test Home Assistant template helper methods."""
from datetime import datetime
import unittest
from unittest.mock import patch
from homeassistant.components import group
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template
from homeassistant.util.unit_system import UnitSystem
from homeassistant.const import (
LENGTH_METERS,
TEMP_CELSIUS,
MASS_GRAMS,
VOLUME_LITERS,
MATCH_ALL,
)
import homeassistant.util.dt as dt_util
from tests.common import get_test_home_assistant
class TestHelpersTemplate(unittest.TestCase):
"""Test the Template."""
# pylint: disable=invalid-name
def setUp(self):
"""Setup the tests."""
self.hass = get_test_home_assistant()
self.hass.config.units = UnitSystem('custom', TEMP_CELSIUS,
LENGTH_METERS, VOLUME_LITERS,
MASS_GRAMS)
# pylint: disable=invalid-name
def tearDown(self):
"""Stop down stuff we started."""
self.hass.stop()
def test_referring_states_by_entity_id(self):
"""Test referring states by entity id."""
self.hass.states.set('test.object', 'happy')
self.assertEqual(
'happy',
template.Template(
'{{ states.test.object.state }}', self.hass).render())
def test_iterating_all_states(self):
"""Test iterating all states."""
self.hass.states.set('test.object', 'happy')
self.hass.states.set('sensor.temperature', 10)
self.assertEqual(
'10happy',
template.Template(
'{% for state in states %}{{ state.state }}{% endfor %}',
self.hass).render())
def test_iterating_domain_states(self):
"""Test iterating domain states."""
self.hass.states.set('test.object', 'happy')
self.hass.states.set('sensor.back_door', 'open')
self.hass.states.set('sensor.temperature', 10)
self.assertEqual(
'open10',
template.Template("""
{% for state in states.sensor %}{{ state.state }}{% endfor %}
""", self.hass).render())
def test_float(self):
"""Test float."""
self.hass.states.set('sensor.temperature', '12')
self.assertEqual(
'12.0',
template.Template(
'{{ float(states.sensor.temperature.state) }}',
self.hass).render())
self.assertEqual(
'True',
template.Template(
'{{ float(states.sensor.temperature.state) > 11 }}',
self.hass).render())
def test_rounding_value(self):
"""Test rounding value."""
self.hass.states.set('sensor.temperature', 12.78)
self.assertEqual(
'12.8',
template.Template(
'{{ states.sensor.temperature.state | round(1) }}',
self.hass).render())
self.assertEqual(
'128',
template.Template(
'{{ states.sensor.temperature.state | multiply(10) | round }}',
self.hass).render())
def test_rounding_value_get_original_value_on_error(self):
"""Test rounding value get original value on error."""
self.assertEqual(
'None',
template.Template('{{ None | round }}', self.hass).render())
self.assertEqual(
'no_number',
template.Template(
'{{ "no_number" | round }}', self.hass).render())
def test_multiply(self):
"""Test multiply."""
tests = {
None: 'None',
10: '100',
'"abcd"': 'abcd'
}
for inp, out in tests.items():
self.assertEqual(
out,
template.Template('{{ %s | multiply(10) | round }}' % inp,
self.hass).render())
def test_strptime(self):
"""Test the parse timestamp method."""
tests = [
('2016-10-19 15:22:05.588122 UTC',
'%Y-%m-%d %H:%M:%S.%f %Z', None),
('2016-10-19 15:22:05.588122+0100',
'%Y-%m-%d %H:%M:%S.%f%z', None),
('2016-10-19 15:22:05.588122',
'%Y-%m-%d %H:%M:%S.%f', None),
('2016-10-19', '%Y-%m-%d', None),
('2016', '%Y', None),
('15:22:05', '%H:%M:%S', None),
('1469119144', '%Y', '1469119144'),
('invalid', '%Y', 'invalid')
]
for inp, fmt, expected in tests:
if expected is None:
expected = datetime.strptime(inp, fmt)
temp = '{{ strptime(\'%s\', \'%s\') }}' % (inp, fmt)
self.assertEqual(
str(expected),
template.Template(temp, self.hass).render())
def test_timestamp_custom(self):
"""Test the timestamps to custom filter."""
tests = [
(None, None, None, 'None'),
(1469119144, None, True, '2016-07-21 16:39:04'),
(1469119144, '%Y', True, '2016'),
(1469119144, 'invalid', True, 'invalid'),
(dt_util.as_timestamp(dt_util.utcnow()), None, False,
dt_util.now().strftime('%Y-%m-%d %H:%M:%S'))
]
for inp, fmt, local, out in tests:
if fmt:
fil = 'timestamp_custom(\'{}\')'.format(fmt)
elif fmt and local:
fil = 'timestamp_custom(\'{0}\', {1})'.format(fmt, local)
else:
fil = 'timestamp_custom'
self.assertEqual(
out,
template.Template('{{ %s | %s }}' % (inp, fil),
self.hass).render())
def test_timestamp_local(self):
"""Test the timestamps to local filter."""
tests = {
None: 'None',
1469119144: '2016-07-21 16:39:04',
}
for inp, out in tests.items():
self.assertEqual(
out,
template.Template('{{ %s | timestamp_local }}' % inp,
self.hass).render())
def test_timestamp_utc(self):
"""Test the timestamps to local filter."""
tests = {
None: 'None',
1469119144: '2016-07-21 16:39:04',
dt_util.as_timestamp(dt_util.utcnow()):
dt_util.now().strftime('%Y-%m-%d %H:%M:%S')
}
for inp, out in tests.items():
self.assertEqual(
out,
template.Template('{{ %s | timestamp_utc }}' % inp,
self.hass).render())
def test_passing_vars_as_keywords(self):
"""Test passing variables as keywords."""
self.assertEqual(
'127',
template.Template('{{ hello }}', self.hass).render(hello=127))
def test_passing_vars_as_vars(self):
"""Test passing variables as variables."""
self.assertEqual(
'127',
template.Template('{{ hello }}', self.hass).render({'hello': 127}))
def test_render_with_possible_json_value_with_valid_json(self):
"""Render with possible JSON value with valid JSON."""
tpl = template.Template('{{ value_json.hello }}', self.hass)
self.assertEqual(
'world',
tpl.render_with_possible_json_value('{"hello": "world"}'))
def test_render_with_possible_json_value_with_invalid_json(self):
"""Render with possible JSON value with invalid JSON."""
tpl = template.Template('{{ value_json }}', self.hass)
self.assertEqual(
'',
tpl.render_with_possible_json_value('{ I AM NOT JSON }'))
def test_render_with_possible_json_value_with_template_error_value(self):
"""Render with possible JSON value with template error value."""
tpl = template.Template('{{ non_existing.variable }}', self.hass)
self.assertEqual(
'-',
tpl.render_with_possible_json_value('hello', '-'))
def test_render_with_possible_json_value_with_missing_json_value(self):
"""Render with possible JSON value with unknown JSON object."""
tpl = template.Template('{{ value_json.goodbye }}', self.hass)
self.assertEqual(
'',
tpl.render_with_possible_json_value('{"hello": "world"}'))
def test_render_with_possible_json_value_valid_with_is_defined(self):
"""Render with possible JSON value with known JSON object."""
tpl = template.Template('{{ value_json.hello|is_defined }}', self.hass)
self.assertEqual(
'world',
tpl.render_with_possible_json_value('{"hello": "world"}'))
def test_render_with_possible_json_value_undefined_json(self):
"""Render with possible JSON value with unknown JSON object."""
tpl = template.Template('{{ value_json.bye|is_defined }}', self.hass)
self.assertEqual(
'{"hello": "world"}',
tpl.render_with_possible_json_value('{"hello": "world"}'))
def test_render_with_possible_json_value_undefined_json_error_value(self):
"""Render with possible JSON value with unknown JSON object."""
tpl = template.Template('{{ value_json.bye|is_defined }}', self.hass)
self.assertEqual(
'',
tpl.render_with_possible_json_value('{"hello": "world"}', ''))
def test_raise_exception_on_error(self):
"""Test raising an exception on error."""
with self.assertRaises(TemplateError):
template.Template('{{ invalid_syntax').ensure_valid()
def test_if_state_exists(self):
"""Test if state exists works."""
self.hass.states.set('test.object', 'available')
tpl = template.Template(
'{% if states.test.object %}exists{% else %}not exists{% endif %}',
self.hass)
self.assertEqual('exists', tpl.render())
def test_is_state(self):
"""Test is_state method."""
self.hass.states.set('test.object', 'available')
tpl = template.Template("""
{% if is_state("test.object", "available") %}yes{% else %}no{% endif %}
""", self.hass)
self.assertEqual('yes', tpl.render())
def test_is_state_attr(self):
"""Test is_state_attr method."""
self.hass.states.set('test.object', 'available', {'mode': 'on'})
tpl = template.Template("""
{% if is_state_attr("test.object", "mode", "on") %}yes{% else %}no{% endif %}
""", self.hass)
self.assertEqual('yes', tpl.render())
def test_states_function(self):
"""Test using states as a function."""
self.hass.states.set('test.object', 'available')
tpl = template.Template('{{ states("test.object") }}', self.hass)
self.assertEqual('available', tpl.render())
tpl2 = template.Template('{{ states("test.object2") }}', self.hass)
self.assertEqual('unknown', tpl2.render())
@patch('homeassistant.helpers.template.TemplateEnvironment.'
'is_safe_callable', return_value=True)
def test_now(self, mock_is_safe):
"""Test now method."""
now = dt_util.now()
with patch.dict(template.ENV.globals, {'now': lambda: now}):
self.assertEqual(
now.isoformat(),
template.Template('{{ now().isoformat() }}',
self.hass).render())
@patch('homeassistant.helpers.template.TemplateEnvironment.'
'is_safe_callable', return_value=True)
def test_utcnow(self, mock_is_safe):
"""Test utcnow method."""
now = dt_util.utcnow()
with patch.dict(template.ENV.globals, {'utcnow': lambda: now}):
self.assertEqual(
now.isoformat(),
template.Template('{{ utcnow().isoformat() }}',
self.hass).render())
def test_distance_function_with_1_state(self):
"""Test distance function with 1 state."""
self.hass.states.set('test.object', 'happy', {
'latitude': 32.87336,
'longitude': -117.22943,
})
tpl = template.Template('{{ distance(states.test.object) | round }}',
self.hass)
self.assertEqual('187', tpl.render())
def test_distance_function_with_2_states(self):
"""Test distance function with 2 states."""
self.hass.states.set('test.object', 'happy', {
'latitude': 32.87336,
'longitude': -117.22943,
})
self.hass.states.set('test.object_2', 'happy', {
'latitude': self.hass.config.latitude,
'longitude': self.hass.config.longitude,
})
tpl = template.Template(
'{{ distance(states.test.object, states.test.object_2) | round }}',
self.hass)
self.assertEqual('187', tpl.render())
def test_distance_function_with_1_coord(self):
"""Test distance function with 1 coord."""
tpl = template.Template(
'{{ distance("32.87336", "-117.22943") | round }}', self.hass)
self.assertEqual(
'187',
tpl.render())
def test_distance_function_with_2_coords(self):
"""Test distance function with 2 coords."""
self.assertEqual(
'187',
template.Template(
'{{ distance("32.87336", "-117.22943", %s, %s) | round }}'
% (self.hass.config.latitude, self.hass.config.longitude),
self.hass).render())
def test_distance_function_with_1_state_1_coord(self):
"""Test distance function with 1 state 1 coord."""
self.hass.states.set('test.object_2', 'happy', {
'latitude': self.hass.config.latitude,
'longitude': self.hass.config.longitude,
})
tpl = template.Template(
'{{ distance("32.87336", "-117.22943", states.test.object_2) '
'| round }}', self.hass)
self.assertEqual('187', tpl.render())
tpl2 = template.Template(
'{{ distance(states.test.object_2, "32.87336", "-117.22943") '
'| round }}', self.hass)
self.assertEqual('187', tpl2.render())
def test_distance_function_return_None_if_invalid_state(self):
"""Test distance function return None if invalid state."""
self.hass.states.set('test.object_2', 'happy', {
'latitude': 10,
})
tpl = template.Template('{{ distance(states.test.object_2) | round }}',
self.hass)
self.assertEqual(
'None',
tpl.render())
def test_distance_function_return_None_if_invalid_coord(self):
"""Test distance function return None if invalid coord."""
self.assertEqual(
'None',
template.Template(
'{{ distance("123", "abc") }}', self.hass).render())
self.assertEqual(
'None',
template.Template('{{ distance("123") }}', self.hass).render())
self.hass.states.set('test.object_2', 'happy', {
'latitude': self.hass.config.latitude,
'longitude': self.hass.config.longitude,
})
tpl = template.Template('{{ distance("123", states.test_object_2) }}',
self.hass)
self.assertEqual(
'None',
tpl.render())
def test_closest_function_home_vs_domain(self):
"""Test closest function home vs domain."""
self.hass.states.set('test_domain.object', 'happy', {
'latitude': self.hass.config.latitude + 0.1,
'longitude': self.hass.config.longitude + 0.1,
})
self.hass.states.set('not_test_domain.but_closer', 'happy', {
'latitude': self.hass.config.latitude,
'longitude': self.hass.config.longitude,
})
self.assertEqual(
'test_domain.object',
template.Template('{{ closest(states.test_domain).entity_id }}',
self.hass).render())
def test_closest_function_home_vs_all_states(self):
"""Test closest function home vs all states."""
self.hass.states.set('test_domain.object', 'happy', {
'latitude': self.hass.config.latitude + 0.1,
'longitude': self.hass.config.longitude + 0.1,
})
self.hass.states.set('test_domain_2.and_closer', 'happy', {
'latitude': self.hass.config.latitude,
'longitude': self.hass.config.longitude,
})
self.assertEqual(
'test_domain_2.and_closer',
template.Template('{{ closest(states).entity_id }}',
self.hass).render())
def test_closest_function_home_vs_group_entity_id(self):
"""Test closest function home vs group entity id."""
self.hass.states.set('test_domain.object', 'happy', {
'latitude': self.hass.config.latitude + 0.1,
'longitude': self.hass.config.longitude + 0.1,
})
self.hass.states.set('not_in_group.but_closer', 'happy', {
'latitude': self.hass.config.latitude,
'longitude': self.hass.config.longitude,
})
group.Group.create_group(
self.hass, 'location group', ['test_domain.object'])
self.assertEqual(
'test_domain.object',
template.Template(
'{{ closest("group.location_group").entity_id }}',
self.hass).render())
def test_closest_function_home_vs_group_state(self):
"""Test closest function home vs group state."""
self.hass.states.set('test_domain.object', 'happy', {
'latitude': self.hass.config.latitude + 0.1,
'longitude': self.hass.config.longitude + 0.1,
})
self.hass.states.set('not_in_group.but_closer', 'happy', {
'latitude': self.hass.config.latitude,
'longitude': self.hass.config.longitude,
})
group.Group.create_group(
self.hass, 'location group', ['test_domain.object'])
self.assertEqual(
'test_domain.object',
template.Template(
'{{ closest(states.group.location_group).entity_id }}',
self.hass).render())
def test_closest_function_to_coord(self):
"""Test closest function to coord."""
self.hass.states.set('test_domain.closest_home', 'happy', {
'latitude': self.hass.config.latitude + 0.1,
'longitude': self.hass.config.longitude + 0.1,
})
self.hass.states.set('test_domain.closest_zone', 'happy', {
'latitude': self.hass.config.latitude + 0.2,
'longitude': self.hass.config.longitude + 0.2,
})
self.hass.states.set('zone.far_away', 'zoning', {
'latitude': self.hass.config.latitude + 0.3,
'longitude': self.hass.config.longitude + 0.3,
})
tpl = template.Template(
'{{ closest("%s", %s, states.test_domain).entity_id }}'
% (self.hass.config.latitude + 0.3,
self.hass.config.longitude + 0.3), self.hass)
self.assertEqual(
'test_domain.closest_zone',
tpl.render())
def test_closest_function_to_entity_id(self):
"""Test closest function to entity id."""
self.hass.states.set('test_domain.closest_home', 'happy', {
'latitude': self.hass.config.latitude + 0.1,
'longitude': self.hass.config.longitude + 0.1,
})
self.hass.states.set('test_domain.closest_zone', 'happy', {
'latitude': self.hass.config.latitude + 0.2,
'longitude': self.hass.config.longitude + 0.2,
})
self.hass.states.set('zone.far_away', 'zoning', {
'latitude': self.hass.config.latitude + 0.3,
'longitude': self.hass.config.longitude + 0.3,
})
self.assertEqual(
'test_domain.closest_zone',
template.Template(
'{{ closest("zone.far_away", '
'states.test_domain).entity_id }}', self.hass).render())
def test_closest_function_to_state(self):
"""Test closest function to state."""
self.hass.states.set('test_domain.closest_home', 'happy', {
'latitude': self.hass.config.latitude + 0.1,
'longitude': self.hass.config.longitude + 0.1,
})
self.hass.states.set('test_domain.closest_zone', 'happy', {
'latitude': self.hass.config.latitude + 0.2,
'longitude': self.hass.config.longitude + 0.2,
})
self.hass.states.set('zone.far_away', 'zoning', {
'latitude': self.hass.config.latitude + 0.3,
'longitude': self.hass.config.longitude + 0.3,
})
self.assertEqual(
'test_domain.closest_zone',
template.Template(
'{{ closest(states.zone.far_away, '
'states.test_domain).entity_id }}', self.hass).render())
def test_closest_function_invalid_state(self):
"""Test closest function invalid state."""
self.hass.states.set('test_domain.closest_home', 'happy', {
'latitude': self.hass.config.latitude + 0.1,
'longitude': self.hass.config.longitude + 0.1,
})
for state in ('states.zone.non_existing', '"zone.non_existing"'):
self.assertEqual(
'None',
template.Template('{{ closest(%s, states) }}' % state,
self.hass).render())
def test_closest_function_state_with_invalid_location(self):
"""Test closest function state with invalid location."""
self.hass.states.set('test_domain.closest_home', 'happy', {
'latitude': 'invalid latitude',
'longitude': self.hass.config.longitude + 0.1,
})
self.assertEqual(
'None',
template.Template(
'{{ closest(states.test_domain.closest_home, '
'states) }}', self.hass).render())
def test_closest_function_invalid_coordinates(self):
"""Test closest function invalid coordinates."""
self.hass.states.set('test_domain.closest_home', 'happy', {
'latitude': self.hass.config.latitude + 0.1,
'longitude': self.hass.config.longitude + 0.1,
})
self.assertEqual(
'None',
template.Template('{{ closest("invalid", "coord", states) }}',
self.hass).render())
def test_closest_function_no_location_states(self):
"""Test closest function without location states."""
self.assertEqual(
'None',
template.Template('{{ closest(states) }}', self.hass).render())
def test_extract_entities_none_exclude_stuff(self):
"""Test extract entities function with none or exclude stuff."""
self.assertEqual(MATCH_ALL, template.extract_entities(None))
self.assertEqual(
MATCH_ALL,
template.extract_entities(
'{{ closest(states.zone.far_away, '
'states.test_domain).entity_id }}'))
self.assertEqual(
MATCH_ALL,
template.extract_entities(
'{{ distance("123", states.test_object_2) }}'))
def test_extract_entities_no_match_entities(self):
"""Test extract entities function with none entities stuff."""
self.assertEqual(
MATCH_ALL,
template.extract_entities(
"{{ value_json.tst | timestamp_custom('%Y' True) }}"))
self.assertEqual(
MATCH_ALL,
template.extract_entities("""
{% for state in states.sensor %}
{{ state.entity_id }}={{ state.state }},
{% endfor %}
"""))
def test_extract_entities_match_entities(self):
"""Test extract entities function with entities stuff."""
self.assertListEqual(
['device_tracker.phone_1'],
template.extract_entities("""
{% if is_state('device_tracker.phone_1', 'home') %}
Ha, Hercules is home!
{% else %}
Hercules is at {{ states('device_tracker.phone_1') }}.
{% endif %}
"""))
self.assertListEqual(
['binary_sensor.garage_door'],
template.extract_entities("""
{{ as_timestamp(states.binary_sensor.garage_door.last_changed) }}
"""))
self.assertListEqual(
['binary_sensor.garage_door'],
template.extract_entities("""
{{ states("binary_sensor.garage_door") }}
"""))
self.assertListEqual(
['device_tracker.phone_2'],
template.extract_entities("""
is_state_attr('device_tracker.phone_2', 'battery', 40)
"""))
self.assertListEqual(
sorted([
'device_tracker.phone_1',
'device_tracker.phone_2',
]),
sorted(template.extract_entities("""
{% if is_state('device_tracker.phone_1', 'home') %}
Ha, Hercules is home!
{% elif states.device_tracker.phone_2.attributes.battery < 40 %}
Hercules you power goes done!.
{% endif %}
""")))
self.assertListEqual(
sorted([
'sensor.pick_humidity',
'sensor.pick_temperature',
]),
sorted(template.extract_entities("""
{{
states.sensor.pick_temperature.state ~ „°C (“ ~
states.sensor.pick_humidity.state ~ „ %
}}
""")))