"""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 ~ „ %“ }} """)))