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
parent
de7e27c92c
commit
122581da7f
|
@ -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.
|
||||
|
|
|
@ -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
|
|
@ -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, {
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue