From 6ac54b20c7b36a50eec0bea432f9329ea32d5c77 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 20 Feb 2016 21:58:53 -0800 Subject: [PATCH] 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 }}'))