Proximity unit of measure (#2659)

* Allow multiple proximities

* Distance conversion

* Add unit of measurement and conversion to proximity

* Shorten attribute name

* Fix get unit of measurement

* Fix the km <-> m conversion

* Add type check and errors

* first path unit test around distance utility

* Fix numeric type check

* Fix conversion type-os

* Actually set the exception thrown flag

* Test for exact conversion

* More descriptive variable names

* Update method invocation to match change in method name

* Missed a couple variables

* Line continuation

* Fix linting too many return issue

* Break out proximity setup for list of proximity and for single proximity device

* Pass hass to setup function

* Check if setup succeeded for each proximity component

* Change variable name

* Break out branches in convert to avoid too many branches linting error

* Remove disable lint line

* Variables for default properties

* Combine logic

* Test loading multiple proximities for 100% code coverage on proximity component

* Unit test to reach 100%
Fail to configure proximities missing devices

* Fail first before processing

* Combine return statements

* lstrip = bad Teagan

* Utilize string formating instead of concatenation

* Fix variable reference

* Typeo

* Clean up conversion to reduce complexity

* Update unit tests to match code changes on distance util

* Test non numeric value

* Private methods, value type has already been checked.
pull/2679/head
Teagan Glenn 2016-07-31 11:20:56 -06:00 committed by Paulus Schoutsen
parent de7e27c92c
commit 122581da7f
4 changed files with 269 additions and 26 deletions

View File

@ -12,17 +12,33 @@ import logging
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_state_change
from homeassistant.util.location import distance
from homeassistant.util.distance import convert
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT
DEPENDENCIES = ['zone', 'device_tracker']
DOMAIN = 'proximity'
NOT_SET = 'not set'
# Default tolerance
DEFAULT_TOLERANCE = 1
# Default zone
DEFAULT_PROXIMITY_ZONE = 'home'
# Default unit of measure
DEFAULT_UNIT_OF_MEASUREMENT = 'km'
# Default distance to zone
DEFAULT_DIST_TO_ZONE = NOT_SET
# Default direction of travel
DEFAULT_DIR_OF_TRAVEL = NOT_SET
# Default nearest device
DEFAULT_NEAREST = NOT_SET
# Entity attributes
ATTR_DIST_FROM = 'dist_to_zone'
ATTR_DIR_OF_TRAVEL = 'dir_of_travel'
@ -31,43 +47,41 @@ ATTR_NEAREST = 'nearest'
_LOGGER = logging.getLogger(__name__)
def setup(hass, config): # pylint: disable=too-many-locals,too-many-statements
"""Get the zones and offsets from configuration.yaml."""
ignored_zones = []
if 'ignored_zones' in config[DOMAIN]:
for variable in config[DOMAIN]['ignored_zones']:
ignored_zones.append(variable)
def setup_proximity_component(hass, config):
"""Set up individual proximity component."""
# Get the devices from configuration.yaml.
if 'devices' not in config[DOMAIN]:
if 'devices' not in config:
_LOGGER.error('devices not found in config')
return False
ignored_zones = []
if 'ignored_zones' in config:
for variable in config['ignored_zones']:
ignored_zones.append(variable)
proximity_devices = []
for variable in config[DOMAIN]['devices']:
for variable in config['devices']:
proximity_devices.append(variable)
# Get the direction of travel tolerance from configuration.yaml.
tolerance = config[DOMAIN].get('tolerance', DEFAULT_TOLERANCE)
tolerance = config.get('tolerance', DEFAULT_TOLERANCE)
# Get the zone to monitor proximity to from configuration.yaml.
proximity_zone = config[DOMAIN].get('zone', DEFAULT_PROXIMITY_ZONE)
proximity_zone = config.get('zone', DEFAULT_PROXIMITY_ZONE)
entity_id = DOMAIN + '.' + proximity_zone
proximity_zone = 'zone.' + proximity_zone
# Get the unit of measurement from configuration.yaml.
unit_of_measure = config.get(ATTR_UNIT_OF_MEASUREMENT,
DEFAULT_UNIT_OF_MEASUREMENT)
state = hass.states.get(proximity_zone)
zone_id = 'zone.{}'.format(proximity_zone)
state = hass.states.get(zone_id)
zone_friendly_name = (state.name).lower()
# Set the default values.
dist_to_zone = 'not set'
dir_of_travel = 'not set'
nearest = 'not set'
proximity = Proximity(hass, zone_friendly_name, dist_to_zone,
dir_of_travel, nearest, ignored_zones,
proximity_devices, tolerance, proximity_zone)
proximity.entity_id = entity_id
proximity = Proximity(hass, zone_friendly_name, DEFAULT_DIST_TO_ZONE,
DEFAULT_DIR_OF_TRAVEL, DEFAULT_NEAREST,
ignored_zones, proximity_devices, tolerance,
zone_id, unit_of_measure)
proximity.entity_id = '{}.{}'.format(DOMAIN, proximity_zone)
proximity.update_ha_state()
@ -78,13 +92,26 @@ def setup(hass, config): # pylint: disable=too-many-locals,too-many-statements
return True
def setup(hass, config):
"""Get the zones and offsets from configuration.yaml."""
result = True
if isinstance(config[DOMAIN], list):
for proximity_config in config[DOMAIN]:
if not setup_proximity_component(hass, proximity_config):
result = False
elif not setup_proximity_component(hass, config[DOMAIN]):
result = False
return result
class Proximity(Entity): # pylint: disable=too-many-instance-attributes
"""Representation of a Proximity."""
# pylint: disable=too-many-arguments
def __init__(self, hass, zone_friendly_name, dist_to, dir_of_travel,
nearest, ignored_zones, proximity_devices, tolerance,
proximity_zone):
proximity_zone, unit_of_measure):
"""Initialize the proximity."""
self.hass = hass
self.friendly_name = zone_friendly_name
@ -95,6 +122,7 @@ class Proximity(Entity): # pylint: disable=too-many-instance-attributes
self.proximity_devices = proximity_devices
self.tolerance = tolerance
self.proximity_zone = proximity_zone
self.unit_of_measure = unit_of_measure
@property
def name(self):
@ -109,7 +137,7 @@ class Proximity(Entity): # pylint: disable=too-many-instance-attributes
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return "km"
return self.unit_of_measure
@property
def state_attributes(self):
@ -183,7 +211,8 @@ class Proximity(Entity): # pylint: disable=too-many-instance-attributes
device_state.attributes['longitude'])
# Add the device and distance to a dictionary.
distances_to_zone[device] = round(dist_to_zone / 1000, 1)
distances_to_zone[device] = round(
convert(dist_to_zone, 'm', self.unit_of_measure), 1)
# Loop through each of the distances collected and work out the
# closest.

View File

@ -0,0 +1,84 @@
"""Distance util functions."""
import logging
from numbers import Number
_LOGGER = logging.getLogger(__name__)
KILOMETERS_SYMBOL = 'km'
METERS_SYMBOL = 'm'
FEET_SYMBOL = 'ft'
MILES_SYMBOL = 'mi'
VALID_UNITS = [
KILOMETERS_SYMBOL,
METERS_SYMBOL,
FEET_SYMBOL,
MILES_SYMBOL,
]
def convert(value, unit_1, unit_2):
"""Convert one unit of measurement to another."""
if not isinstance(value, Number):
raise TypeError(str(value) + ' is not of numeric type')
if unit_1 == unit_2:
return value
if unit_1 not in VALID_UNITS:
_LOGGER.error('Unknown unit of measure: ' + str(unit_1))
raise ValueError('Unknown unit of measure: ' + str(unit_1))
elif unit_2 not in VALID_UNITS:
_LOGGER.error('Unknown unit of measure: ' + str(unit_2))
raise ValueError('Unknown unit of measure: ' + str(unit_2))
meters = value
if unit_1 == MILES_SYMBOL:
meters = __miles_to_meters(value)
elif unit_1 == FEET_SYMBOL:
meters = __feet_to_meters(value)
elif unit_1 == KILOMETERS_SYMBOL:
meters = __kilometers_to_meters(value)
result = meters
if unit_2 == MILES_SYMBOL:
result = __meters_to_miles(meters)
elif unit_2 == FEET_SYMBOL:
result = __meters_to_feet(meters)
elif unit_2 == KILOMETERS_SYMBOL:
result = __meters_to_kilometers(meters)
return result
def __miles_to_meters(miles):
"""Convert miles to meters."""
return miles * 1609.344
def __feet_to_meters(feet):
"""Convert feet to meters."""
return feet * 0.3048
def __kilometers_to_meters(kilometers):
"""Convert kilometers to meters."""
return kilometers * 1000
def __meters_to_miles(meters):
"""Convert meters to miles."""
return meters * 0.000621371
def __meters_to_feet(meters):
"""Convert meters to feet."""
return meters * 3.28084
def __meters_to_kilometers(meters):
"""Convert meters to kilometers."""
return meters * 0.001

View File

@ -18,11 +18,73 @@ class TestProximity:
'longitude': 1.1,
'radius': 10
})
self.hass.states.set(
'zone.work', 'zoning',
{
'name': 'work',
'latitude': 2.3,
'longitude': 1.3,
'radius': 10
})
def teardown_method(self, method):
"""Stop everything that was started."""
self.hass.stop()
def test_proximities(self):
"""Test a list of proximities."""
assert proximity.setup(self.hass, {
'proximity': [{
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
},
'tolerance': '1'
}, {
'zone': 'work',
'devices': {
'device_tracker.test1'
},
'tolerance': '1'
}]
})
proximities = ['home', 'work']
for prox in proximities:
state = self.hass.states.get('proximity.' + prox)
assert state.state == 'not set'
assert state.attributes.get('nearest') == 'not set'
assert state.attributes.get('dir_of_travel') == 'not set'
self.hass.states.set('proximity.' + prox, '0')
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.' + prox)
assert state.state == '0'
def test_proximities_missing_devices(self):
"""Test a list of proximities with one missing devices."""
assert not proximity.setup(self.hass, {
'proximity': [{
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
},
'tolerance': '1'
}, {
'zone': 'work',
'tolerance': '1'
}]
})
def test_proximity(self):
"""Test the proximity."""
assert proximity.setup(self.hass, {

View File

@ -0,0 +1,68 @@
"""Test homeasssitant distance utility functions."""
import unittest
import homeassistant.util.distance as distance_util
KILOMETERS = distance_util.KILOMETERS_SYMBOL
METERS = distance_util.METERS_SYMBOL
FEET = distance_util.FEET_SYMBOL
MILES = distance_util.MILES_SYMBOL
INVALID_SYMBOL = 'bob'
VALID_SYMBOL = KILOMETERS
class TestDistanceUtil(unittest.TestCase):
"""Test the distance utility functions."""
def test_convert_same_unit(self):
"""Test conversion from any unit to same unit."""
self.assertEqual(5, distance_util.convert(5, KILOMETERS, KILOMETERS))
self.assertEqual(2, distance_util.convert(2, METERS, METERS))
self.assertEqual(10, distance_util.convert(10, MILES, MILES))
self.assertEqual(9, distance_util.convert(9, FEET, FEET))
def test_convert_invalid_unit(self):
"""Test exception is thrown for invalid units."""
with self.assertRaises(ValueError):
distance_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL)
with self.assertRaises(ValueError):
distance_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL)
def test_convert_nonnumeric_value(self):
"""Test exception is thrown for nonnumeric type."""
with self.assertRaises(TypeError):
distance_util.convert('a', KILOMETERS, METERS)
def test_convert_from_miles(self):
"""Test conversion from miles to other units."""
miles = 5
self.assertEqual(distance_util.convert(miles, MILES, KILOMETERS),
8.04672)
self.assertEqual(distance_util.convert(miles, MILES, METERS), 8046.72)
self.assertEqual(distance_util.convert(miles, MILES, FEET),
26400.0008448)
def test_convert_from_feet(self):
"""Test conversion from feet to other units."""
feet = 5000
self.assertEqual(distance_util.convert(feet, FEET, KILOMETERS), 1.524)
self.assertEqual(distance_util.convert(feet, FEET, METERS), 1524)
self.assertEqual(distance_util.convert(feet, FEET, MILES),
0.9469694040000001)
def test_convert_from_kilometers(self):
"""Test conversion from kilometers to other units."""
km = 5
self.assertEqual(distance_util.convert(km, KILOMETERS, FEET), 16404.2)
self.assertEqual(distance_util.convert(km, KILOMETERS, METERS), 5000)
self.assertEqual(distance_util.convert(km, KILOMETERS, MILES),
3.106855)
def test_convert_from_meters(self):
"""Test conversion from meters to other units."""
m = 5000
self.assertEqual(distance_util.convert(m, METERS, FEET), 16404.2)
self.assertEqual(distance_util.convert(m, METERS, KILOMETERS), 5)
self.assertEqual(distance_util.convert(m, METERS, MILES), 3.106855)