From 6847dac582eb6c5f6b5363a69a00d641e54df698 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 20 Feb 2016 20:58:01 -0800 Subject: [PATCH 1/8] Expose current time in templates Fixes #1282 --- homeassistant/util/template.py | 5 ++++- tests/util/test_template.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/template.py b/homeassistant/util/template.py index d9b1990a252..3774e9bb898 100644 --- a/homeassistant/util/template.py +++ b/homeassistant/util/template.py @@ -12,6 +12,7 @@ from jinja2.sandbox import ImmutableSandboxedEnvironment from homeassistant.const import STATE_UNKNOWN from homeassistant.exceptions import TemplateError +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) _SENTINEL = object() @@ -45,7 +46,9 @@ def render(hass, template, variables=None, **kwargs): return ENV.from_string(template, { 'states': AllStates(hass), 'is_state': hass.states.is_state, - 'is_state_attr': hass.states.is_state_attr + 'is_state_attr': hass.states.is_state_attr, + 'now': dt_util.now, + 'utcnow': dt_util.utcnow, }).render(kwargs).strip() except jinja2.TemplateError as err: raise TemplateError(err) diff --git a/tests/util/test_template.py b/tests/util/test_template.py index 09e0e154888..a3b680c71eb 100644 --- a/tests/util/test_template.py +++ b/tests/util/test_template.py @@ -6,8 +6,11 @@ Tests Home Assistant template util methods. """ # pylint: disable=too-many-public-methods import unittest +from unittest.mock import patch + from homeassistant.exceptions import TemplateError from homeassistant.util import template +import homeassistant.util.dt as dt_util from tests.common import get_test_home_assistant @@ -143,3 +146,19 @@ class TestUtilTemplate(unittest.TestCase): self.assertEqual( 'unknown', template.render(self.hass, '{{ states("test.object2") }}')) + + @patch('homeassistant.core.dt_util.now', return_value=dt_util.now()) + @patch('homeassistant.util.template.TemplateEnvironment.is_safe_callable', + return_value=True) + def test_now_function(self, mock_is_safe, mock_now): + self.assertEqual( + dt_util.now().isoformat(), + template.render(self.hass, '{{ now().isoformat() }}')) + + @patch('homeassistant.core.dt_util.utcnow', return_value=dt_util.utcnow()) + @patch('homeassistant.util.template.TemplateEnvironment.is_safe_callable', + return_value=True) + def test_utcnow_function(self, mock_is_safe, mock_utcnow): + self.assertEqual( + dt_util.utcnow().isoformat(), + template.render(self.hass, '{{ utcnow().isoformat() }}')) From 6ac54b20c7b36a50eec0bea432f9329ea32d5c77 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 20 Feb 2016 21:58:53 -0800 Subject: [PATCH 2/8] Add template distance helper --- homeassistant/util/template.py | 65 +++++++++++++++++++++++++-- tests/util/test_template.py | 81 ++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 3 deletions(-) diff --git a/homeassistant/util/template.py b/homeassistant/util/template.py index 3774e9bb898..3006a426d43 100644 --- a/homeassistant/util/template.py +++ b/homeassistant/util/template.py @@ -10,9 +10,10 @@ import logging import jinja2 from jinja2.sandbox import ImmutableSandboxedEnvironment -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import STATE_UNKNOWN, ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import State from homeassistant.exceptions import TemplateError -import homeassistant.util.dt as dt_util +from homeassistant.util import convert, dt as dt_util, location _LOGGER = logging.getLogger(__name__) _SENTINEL = object() @@ -42,11 +43,14 @@ def render(hass, template, variables=None, **kwargs): if variables is not None: kwargs.update(variables) + location_helper = LocationHelpers(hass) + try: return ENV.from_string(template, { - 'states': AllStates(hass), + 'distance': location_helper.distance, 'is_state': hass.states.is_state, 'is_state_attr': hass.states.is_state_attr, + 'states': AllStates(hass), 'now': dt_util.now, 'utcnow': dt_util.utcnow, }).render(kwargs).strip() @@ -88,6 +92,61 @@ class DomainStates(object): key=lambda state: state.entity_id)) +class LocationHelpers(object): + """Class to expose distance helpers to templates.""" + + def __init__(self, hass): + """Initialize distance helpers.""" + self._hass = hass + + def distance(self, *args): + """Calculate distance. + + Will calculate distance from home to a point or between points. + Points can be passed in using state objects or lat/lng coordinates. + """ + locations = [] + + to_process = list(args) + + while to_process: + value = to_process.pop(0) + + if isinstance(value, State): + latitude = value.attributes.get(ATTR_LATITUDE) + longitude = value.attributes.get(ATTR_LONGITUDE) + + if latitude is None or longitude is None: + _LOGGER.warning( + 'Distance:State does not contains a location: %s', + value) + return None + + else: + # We expect this and next value to be lat&lng + if not to_process: + _LOGGER.warning( + 'Distance:Expected latitude and longitude, got %s', + value) + return None + + value_2 = to_process.pop(0) + latitude = convert(value, float) + longitude = convert(value_2, float) + + if latitude is None or longitude is None: + _LOGGER.warning('Distance:Unable to process latitude and ' + 'longitude: %s, %s', value, value_2) + return None + + locations.append((latitude, longitude)) + + if len(locations) == 1: + return self._hass.config.distance(*locations[0]) + + return location.distance(*locations[0] + locations[1]) + + def forgiving_round(value, precision=0): """ Rounding method that accepts strings. """ try: diff --git a/tests/util/test_template.py b/tests/util/test_template.py index a3b680c71eb..cac5978393d 100644 --- a/tests/util/test_template.py +++ b/tests/util/test_template.py @@ -162,3 +162,84 @@ class TestUtilTemplate(unittest.TestCase): self.assertEqual( dt_util.utcnow().isoformat(), template.render(self.hass, '{{ utcnow().isoformat() }}')) + + def test_distance_function_with_1_state(self): + self.hass.states.set('test.object', 'happy', { + 'latitude': 32.87336, + 'longitude': -117.22943, + }) + + self.assertEqual( + '187', + template.render( + self.hass, '{{ distance(states.test.object) | round }}')) + + def test_distance_function_with_2_states(self): + 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, + }) + + self.assertEqual( + '187', + template.render( + self.hass, + '{{ distance(states.test.object, states.test.object_2)' + '| round }}')) + + def test_distance_function_with_1_coord(self): + self.assertEqual( + '187', + template.render( + self.hass, '{{ distance("32.87336", "-117.22943") | round }}')) + + def test_distance_function_with_2_coords(self): + self.assertEqual( + '187', + template.render( + self.hass, + '{{ distance("32.87336", "-117.22943", %s, %s) | round }}' + % (self.hass.config.latitude, self.hass.config.longitude))) + + def test_distance_function_with_1_state_1_coord(self): + self.hass.states.set('test.object_2', 'happy', { + 'latitude': self.hass.config.latitude, + 'longitude': self.hass.config.longitude, + }) + + self.assertEqual( + '187', + template.render( + self.hass, + '{{ distance("32.87336", "-117.22943", states.test.object_2) ' + '| round }}')) + + self.assertEqual( + '187', + template.render( + self.hass, + '{{ distance(states.test.object_2, "32.87336", "-117.22943") ' + '| round }}')) + + def test_distance_function_return_None_if_invalid_state(self): + self.hass.states.set('test.object_2', 'happy', { + 'latitude': 10, + }) + + self.assertEqual( + 'None', + template.render( + self.hass, + '{{ distance(states.test.object_2) | round }}')) + + def test_distance_function_return_None_if_invalid_coord(self): + self.assertEqual( + 'None', + template.render( + self.hass, + '{{ distance("123", "abc") | round }}')) From 9ad2cf7b7a470ae724a0b97ee8b04b36f96c128d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 20 Feb 2016 21:59:16 -0800 Subject: [PATCH 3/8] Adjust template rounding tests --- homeassistant/util/template.py | 9 +++++---- tests/util/test_template.py | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/homeassistant/util/template.py b/homeassistant/util/template.py index 3006a426d43..0cbddb2aa16 100644 --- a/homeassistant/util/template.py +++ b/homeassistant/util/template.py @@ -148,17 +148,17 @@ class LocationHelpers(object): def forgiving_round(value, precision=0): - """ Rounding method that accepts strings. """ + """Rounding filter that accepts strings.""" try: value = round(float(value), precision) return int(value) if precision == 0 else value - except ValueError: + except (ValueError, TypeError): # If value can't be converted to float return value def multiply(value, amount): - """ Converts to float and multiplies value. """ + """Filter to convert value to float and multiply it.""" try: return float(value) * amount except ValueError: @@ -167,9 +167,10 @@ def multiply(value, amount): class TemplateEnvironment(ImmutableSandboxedEnvironment): - """ Home Assistant template environment. """ + """Home Assistant template environment.""" def is_safe_callable(self, obj): + """Test if callback is safe.""" return isinstance(obj, AllStates) or super().is_safe_callable(obj) ENV = TemplateEnvironment() diff --git a/tests/util/test_template.py b/tests/util/test_template.py index cac5978393d..737bb87fce9 100644 --- a/tests/util/test_template.py +++ b/tests/util/test_template.py @@ -62,9 +62,6 @@ class TestUtilTemplate(unittest.TestCase): self.hass, '{{ states.sensor.temperature.state | round(1) }}')) - def test_rounding_value2(self): - self.hass.states.set('sensor.temperature', 12.78) - self.assertEqual( '128', template.render( @@ -72,6 +69,21 @@ class TestUtilTemplate(unittest.TestCase): '{{ states.sensor.temperature.state | multiply(10) | round }}' )) + def test_rounding_value_get_original_value_on_error(self): + self.assertEqual( + 'None', + template.render( + self.hass, + '{{ None | round }}' + )) + + self.assertEqual( + 'no_number', + template.render( + self.hass, + '{{ "no_number" | round }}' + )) + def test_passing_vars_as_keywords(self): self.assertEqual( '127', template.render(self.hass, '{{ hello }}', hello=127)) From 7805d2800c6e417549f064bf1662ced93ff1809b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 21 Feb 2016 09:32:43 -0800 Subject: [PATCH 4/8] Expose current time object instead of method to retrieve (thanks @fabaff) --- homeassistant/util/template.py | 5 +++-- tests/util/test_template.py | 17 +++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/homeassistant/util/template.py b/homeassistant/util/template.py index 0cbddb2aa16..6fa4b6d55bf 100644 --- a/homeassistant/util/template.py +++ b/homeassistant/util/template.py @@ -44,6 +44,7 @@ def render(hass, template, variables=None, **kwargs): kwargs.update(variables) location_helper = LocationHelpers(hass) + utcnow = dt_util.utcnow() try: return ENV.from_string(template, { @@ -51,8 +52,8 @@ def render(hass, template, variables=None, **kwargs): 'is_state': hass.states.is_state, 'is_state_attr': hass.states.is_state_attr, 'states': AllStates(hass), - 'now': dt_util.now, - 'utcnow': dt_util.utcnow, + 'now': dt_util.as_local(utcnow), + 'utcnow': utcnow, }).render(kwargs).strip() except jinja2.TemplateError as err: raise TemplateError(err) diff --git a/tests/util/test_template.py b/tests/util/test_template.py index 737bb87fce9..d6882c9a219 100644 --- a/tests/util/test_template.py +++ b/tests/util/test_template.py @@ -159,21 +159,26 @@ class TestUtilTemplate(unittest.TestCase): 'unknown', template.render(self.hass, '{{ states("test.object2") }}')) - @patch('homeassistant.core.dt_util.now', return_value=dt_util.now()) + @patch('homeassistant.core.dt_util.utcnow', return_value=dt_util.utcnow()) @patch('homeassistant.util.template.TemplateEnvironment.is_safe_callable', return_value=True) - def test_now_function(self, mock_is_safe, mock_now): + def test_now(self, mock_is_safe, mock_utcnow): self.assertEqual( - dt_util.now().isoformat(), - template.render(self.hass, '{{ now().isoformat() }}')) + dt_util.utcnow().isoformat(), + template.render(self.hass, '{{ now.isoformat() }}')) @patch('homeassistant.core.dt_util.utcnow', return_value=dt_util.utcnow()) @patch('homeassistant.util.template.TemplateEnvironment.is_safe_callable', return_value=True) - def test_utcnow_function(self, mock_is_safe, mock_utcnow): + def test_utcnow(self, mock_is_safe, mock_utcnow): self.assertEqual( dt_util.utcnow().isoformat(), - template.render(self.hass, '{{ utcnow().isoformat() }}')) + template.render(self.hass, '{{ utcnow.isoformat() }}')) + + def test_utcnow_is_exactly_now(self): + self.assertEqual( + 'True', + template.render(self.hass, '{{ utcnow == now }}')) def test_distance_function_with_1_state(self): self.hass.states.set('test.object', 'happy', { From 9f5f13644a2d296f52a7fd09fda1a79ce4d96fb6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 21 Feb 2016 11:12:37 -0800 Subject: [PATCH 5/8] Add template multiply test --- homeassistant/util/template.py | 2 +- tests/util/test_template.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/template.py b/homeassistant/util/template.py index 6fa4b6d55bf..30b90f85144 100644 --- a/homeassistant/util/template.py +++ b/homeassistant/util/template.py @@ -162,7 +162,7 @@ def multiply(value, amount): """Filter to convert value to float and multiply it.""" try: return float(value) * amount - except ValueError: + except (ValueError, TypeError): # If value can't be converted to float return value diff --git a/tests/util/test_template.py b/tests/util/test_template.py index d6882c9a219..c6c5f18f01c 100644 --- a/tests/util/test_template.py +++ b/tests/util/test_template.py @@ -84,6 +84,19 @@ class TestUtilTemplate(unittest.TestCase): '{{ "no_number" | round }}' )) + def test_multiply(self): + tests = { + None: 'None', + 10: '100', + '"abcd"': 'abcd' + } + + for inp, out in tests.items(): + self.assertEqual( + out, + template.render(self.hass, + '{{ %s | multiply(10) | round }}' % inp)) + def test_passing_vars_as_keywords(self): self.assertEqual( '127', template.render(self.hass, '{{ hello }}', hello=127)) From c3bb6d32aa308f2b5cd8cf3d4b44da676adb01ab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 21 Feb 2016 11:13:40 -0800 Subject: [PATCH 6/8] Add closest template helper --- homeassistant/helpers/location.py | 27 ++++ homeassistant/util/template.py | 85 ++++++++++++- tests/helpers/test_location.py | 53 ++++++++ tests/util/test_template.py | 202 +++++++++++++++++++++++++++++- 4 files changed, 360 insertions(+), 7 deletions(-) create mode 100644 homeassistant/helpers/location.py create mode 100644 tests/helpers/test_location.py diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py new file mode 100644 index 00000000000..a3cdc348a24 --- /dev/null +++ b/homeassistant/helpers/location.py @@ -0,0 +1,27 @@ +"""Location helpers for Home Assistant.""" + +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import State +from homeassistant.util import location as loc_util + + +def has_location(state): + """Test if state contains a valid location.""" + return (isinstance(state, State) and + isinstance(state.attributes.get(ATTR_LATITUDE), float) and + isinstance(state.attributes.get(ATTR_LONGITUDE), float)) + + +def closest(latitude, longitude, states): + """Return closest state to point.""" + with_location = [state for state in states if has_location(state)] + + if not with_location: + return None + + return min( + with_location, + key=lambda state: loc_util.distance( + latitude, longitude, state.attributes.get(ATTR_LATITUDE), + state.attributes.get(ATTR_LONGITUDE)) + ) diff --git a/homeassistant/util/template.py b/homeassistant/util/template.py index 30b90f85144..25d65855ef6 100644 --- a/homeassistant/util/template.py +++ b/homeassistant/util/template.py @@ -10,10 +10,12 @@ import logging import jinja2 from jinja2.sandbox import ImmutableSandboxedEnvironment +from homeassistant.components import group from homeassistant.const import STATE_UNKNOWN, ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import State from homeassistant.exceptions import TemplateError -from homeassistant.util import convert, dt as dt_util, location +from homeassistant.helpers import location as loc_helper +from homeassistant.util import convert, dt as dt_util, location as loc_util _LOGGER = logging.getLogger(__name__) _SENTINEL = object() @@ -43,16 +45,17 @@ def render(hass, template, variables=None, **kwargs): if variables is not None: kwargs.update(variables) - location_helper = LocationHelpers(hass) + location_methods = LocationMethods(hass) utcnow = dt_util.utcnow() try: return ENV.from_string(template, { - 'distance': location_helper.distance, + 'closest': location_methods.closest, + 'distance': location_methods.distance, 'is_state': hass.states.is_state, 'is_state_attr': hass.states.is_state_attr, - 'states': AllStates(hass), 'now': dt_util.as_local(utcnow), + 'states': AllStates(hass), 'utcnow': utcnow, }).render(kwargs).strip() except jinja2.TemplateError as err: @@ -93,13 +96,75 @@ class DomainStates(object): key=lambda state: state.entity_id)) -class LocationHelpers(object): +class LocationMethods(object): """Class to expose distance helpers to templates.""" def __init__(self, hass): """Initialize distance helpers.""" self._hass = hass + def closest(self, *args): + """Find closest entity. + + Closest to home: + closest(states) + closest(states.device_tracker) + closest('group.children') + closest(states.group.children) + + Closest to a point: + closest(23.456, 23.456, 'group.children') + closest('zone.school', 'group.children') + closest(states.zone.school, 'group.children') + """ + + if len(args) == 1: + latitude = self._hass.config.latitude + longitude = self._hass.config.longitude + entities = args[0] + + elif len(args) == 2: + point_state = self._resolve_state(args[0]) + + if point_state is None: + _LOGGER.warning('Closest:Unable to find state %s', args[0]) + return None + elif not loc_helper.has_location(point_state): + _LOGGER.warning( + 'Closest:State does not contain valid location: %s', + point_state) + return None + + latitude = point_state.attributes.get(ATTR_LATITUDE) + longitude = point_state.attributes.get(ATTR_LONGITUDE) + + entities = args[1] + + else: + latitude = convert(args[0], float) + longitude = convert(args[1], float) + + if latitude is None or longitude is None: + _LOGGER.warning( + 'Closest:Received invalid coordinates: %s, %s', + args[0], args[1]) + return None + + entities = args[2] + + if isinstance(entities, (AllStates, DomainStates)): + states = list(entities) + else: + if isinstance(entities, State): + entity_id = entities.entity_id + else: + entity_id = str(entities) + + states = [self._hass.states.get(entity_id) for entity_id + in group.expand_entity_ids(self._hass, [entity_id])] + + return loc_helper.closest(latitude, longitude, states) + def distance(self, *args): """Calculate distance. @@ -145,7 +210,15 @@ class LocationHelpers(object): if len(locations) == 1: return self._hass.config.distance(*locations[0]) - return location.distance(*locations[0] + locations[1]) + return loc_util.distance(*locations[0] + locations[1]) + + def _resolve_state(self, entity_id_or_state): + """Return state or entity_id if given.""" + if isinstance(entity_id_or_state, State): + return entity_id_or_state + elif isinstance(entity_id_or_state, str): + return self._hass.states.get(entity_id_or_state) + return None def forgiving_round(value, precision=0): diff --git a/tests/helpers/test_location.py b/tests/helpers/test_location.py new file mode 100644 index 00000000000..dfd57f642bd --- /dev/null +++ b/tests/helpers/test_location.py @@ -0,0 +1,53 @@ +"""Tests Home Assistant location helpers.""" +# pylint: disable=too-many-public-methods +import unittest + +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import State +from homeassistant.helpers import location + + +class TestHelpersLocation(unittest.TestCase): + def test_has_location_with_invalid_states(self): + for state in (None, 1, "hello", object): + self.assertFalse(location.has_location(state)) + + def test_has_location_with_states_with_invalid_locations(self): + state = State('hello.world', 'invalid', { + ATTR_LATITUDE: 'no number', + ATTR_LONGITUDE: 123.12 + }) + self.assertFalse(location.has_location(state)) + + def test_has_location_with_states_with_valid_location(self): + state = State('hello.world', 'invalid', { + ATTR_LATITUDE: 123.12, + ATTR_LONGITUDE: 123.12 + }) + self.assertTrue(location.has_location(state)) + + def test_closest_with_no_states_with_location(self): + state = State('light.test', 'on') + state2 = State('light.test', 'on', { + ATTR_LATITUDE: 'invalid', + ATTR_LONGITUDE: 123.45, + }) + state3 = State('light.test', 'on', { + ATTR_LONGITUDE: 123.45, + }) + + self.assertIsNone( + location.closest(123.45, 123.45, [state, state2, state3])) + + def test_closest_returns_closest(self): + state = State('light.test', 'on', { + ATTR_LATITUDE: 124.45, + ATTR_LONGITUDE: 124.45, + }) + state2 = State('light.test', 'on', { + ATTR_LATITUDE: 125.45, + ATTR_LONGITUDE: 125.45, + }) + + self.assertEqual( + state, location.closest(123.45, 123.45, [state, state2])) diff --git a/tests/util/test_template.py b/tests/util/test_template.py index c6c5f18f01c..426d90bd4d9 100644 --- a/tests/util/test_template.py +++ b/tests/util/test_template.py @@ -8,6 +8,7 @@ Tests Home Assistant template util methods. import unittest from unittest.mock import patch +from homeassistant.components import group from homeassistant.exceptions import TemplateError from homeassistant.util import template import homeassistant.util.dt as dt_util @@ -272,4 +273,203 @@ class TestUtilTemplate(unittest.TestCase): 'None', template.render( self.hass, - '{{ distance("123", "abc") | round }}')) + '{{ distance("123", "abc") }}')) + + self.assertEqual( + 'None', + template.render( + self.hass, + '{{ distance("123") }}')) + + self.hass.states.set('test.object_2', 'happy', { + 'latitude': self.hass.config.latitude, + 'longitude': self.hass.config.longitude, + }) + + self.assertEqual( + 'None', + template.render( + self.hass, + '{{ distance("123", states.test_object_2) }}')) + + + def test_closest_function_home_vs_domain(self): + 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.render(self.hass, + '{{ closest(states.test_domain).entity_id }}')) + + def test_closest_function_home_vs_all_states(self): + 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.render(self.hass, + '{{ closest(states).entity_id }}')) + + def test_closest_function_home_vs_group_entity_id(self): + 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(self.hass, 'location group', ['test_domain.object']) + + self.assertEqual( + 'test_domain.object', + template.render(self.hass, + '{{ closest("group.location_group").entity_id }}')) + + def test_closest_function_home_vs_group_state(self): + 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(self.hass, 'location group', ['test_domain.object']) + + self.assertEqual( + 'test_domain.object', + template.render( + self.hass, + '{{ closest(states.group.location_group).entity_id }}')) + + def test_closest_function_to_coord(self): + 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.render( + self.hass, + '{{ closest("%s", %s, states.test_domain).entity_id }}' + % (self.hass.config.latitude + 0.3, + self.hass.config.longitude + 0.3)) + ) + + def test_closest_function_to_entity_id(self): + 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.render( + self.hass, + '{{ closest("zone.far_away", states.test_domain).entity_id }}') + ) + + def test_closest_function_to_state(self): + 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.render( + self.hass, + '{{ closest(states.zone.far_away, ' + 'states.test_domain).entity_id }}') + ) + + def test_closest_function_invalid_state(self): + 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.render( + self.hass, '{{ closest(%s, states) }}' % state)) + + def test_closest_function_state_with_invalid_location(self): + self.hass.states.set('test_domain.closest_home', 'happy', { + 'latitude': 'invalid latitude', + 'longitude': self.hass.config.longitude + 0.1, + }) + + self.assertEqual( + 'None', + template.render( + self.hass, + '{{ closest(states.test_domain.closest_home, ' + 'states) }}')) + + def test_closest_function_invalid_coordinates(self): + 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.render(self.hass, + '{{ closest("invalid", "coord", states) }}')) + + def test_closest_function_no_location_states(self): + self.assertEqual('None', + template.render(self.hass, '{{ closest(states) }}')) From 7c6dcdb082f228a8bb9a4861883cc2f125541496 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 21 Feb 2016 11:23:16 -0800 Subject: [PATCH 7/8] Catch an extra error that could break util.convert --- homeassistant/util/__init__.py | 2 +- tests/util/test_init.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 16650fe549d..7a396b44d3b 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -57,7 +57,7 @@ def convert(value, to_type, default=None): """ Converts value to to_type, returns default if fails. """ try: return default if value is None else to_type(value) - except ValueError: + except (ValueError, TypeError): # If value could not be converted return default diff --git a/tests/util/test_init.py b/tests/util/test_init.py index bd546d4e5e1..1a80d74b065 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -53,6 +53,7 @@ class TestUtil(unittest.TestCase): self.assertEqual(True, util.convert("True", bool)) self.assertEqual(1, util.convert("NOT A NUMBER", int, 1)) self.assertEqual(1, util.convert(None, int, 1)) + self.assertEqual(1, util.convert(object, int, 1)) def test_ensure_unique_string(self): """ Test ensure_unique_string. """ From 3d8e9b4261e8ce6cf2ee3d6c930729f2f816bd5d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 21 Feb 2016 13:09:49 -0800 Subject: [PATCH 8/8] Lint fixes --- homeassistant/util/template.py | 6 +++--- tests/util/test_template.py | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/util/template.py b/homeassistant/util/template.py index 25d65855ef6..7b00fb6dcc1 100644 --- a/homeassistant/util/template.py +++ b/homeassistant/util/template.py @@ -156,12 +156,12 @@ class LocationMethods(object): states = list(entities) else: if isinstance(entities, State): - entity_id = entities.entity_id + gr_entity_id = entities.entity_id else: - entity_id = str(entities) + gr_entity_id = str(entities) states = [self._hass.states.get(entity_id) for entity_id - in group.expand_entity_ids(self._hass, [entity_id])] + in group.expand_entity_ids(self._hass, [gr_entity_id])] return loc_helper.closest(latitude, longitude, states) diff --git a/tests/util/test_template.py b/tests/util/test_template.py index 426d90bd4d9..7a0b990a04a 100644 --- a/tests/util/test_template.py +++ b/tests/util/test_template.py @@ -292,7 +292,6 @@ class TestUtilTemplate(unittest.TestCase): self.hass, '{{ distance("123", states.test_object_2) }}')) - def test_closest_function_home_vs_domain(self): self.hass.states.set('test_domain.object', 'happy', { 'latitude': self.hass.config.latitude + 0.1,