Merge pull request #1159 from nickwaring/proximity

Proximity component
This commit is contained in:
Paulus Schoutsen 2016-02-07 00:54:47 -08:00
commit 98c6e56ea4
2 changed files with 898 additions and 0 deletions

View file

@ -0,0 +1,282 @@
"""
custom_components.proximity
~~~~~~~~~~~~~~~~~~~~~~~~~
Component to monitor the proximity of devices to a particular zone and the
direction of travel. The result is an entity created in HA which maintains
the proximity data
This component is useful to reduce the number of automation rules required
when wanting to perform automations based on locations outside a particular
zone. The standard HA zone and state based triggers allow similar control
but the number of rules grows exponentially when factors such as direction
of travel need to be taken into account. Some examples of its use include:
- Increase thermostat temperature as you near home
- Decrease temperature the further away from home you travel
The Proximity entity which is created has the following values:
state = distance from the monitored zone (in km)
dir_of_travel = direction of the closest device to the monitoed zone. Values
are:
'not set'
'arrived'
'towards'
'away_from'
'unknown'
'stationary'
dist_to_zone = distance from the monitored zone (in km)
Use configuration.yaml to enable the user to easily tune a number of settings:
- Zone: the zone to which this component is measuring the distance to. Default
is the home zone
- Ignored Zones: where proximity is not calculated for a device (either the
device being monitored or ones being compared (e.g. work or school)
- Devices: a list of devices to compare location against to check closeness to
the configured zone
- Tolerance: the tolerance used to calculate the direction of travel in metres
(to filter out small GPS co-ordinate changes
Logging levels debug, info and error are in use
Example configuration.yaml entry:
proximity:
zone: home
ignored_zones:
- twork
- elschool
devices:
- device_tracker.nwaring_nickmobile
- device_tracker.eleanorsiphone
- device_tracker.tsiphone
tolerance: 50
"""
import logging
from homeassistant.helpers.event import track_state_change
from homeassistant.helpers.entity import Entity
from homeassistant.util.location import distance
DEPENDENCIES = ['zone', 'device_tracker']
# domain for the component
DOMAIN = 'proximity'
# default tolerance
DEFAULT_TOLERANCE = 1
# default zone
DEFAULT_PROXIMITY_ZONE = 'home'
# entity attributes
ATTR_DIST_FROM = 'dist_to_zone'
ATTR_DIR_OF_TRAVEL = 'dir_of_travel'
ATTR_NEAREST = 'nearest'
ATTR_FRIENDLY_NAME = 'friendly_name'
# Shortcut for the logger
_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)
# get the devices from configuration.yaml
if 'devices' not in config[DOMAIN]:
_LOGGER.error('devices not found in config')
return False
proximity_devices = []
for variable in config[DOMAIN]['devices']:
proximity_devices.append(variable)
# get the direction of travel tolerance from configuration.yaml
tolerance = config[DOMAIN].get('tolerance', DEFAULT_TOLERANCE)
# get the zone to monitor proximity to from configuration.yaml
proximity_zone = config[DOMAIN].get('zone', DEFAULT_PROXIMITY_ZONE)
entity_id = DOMAIN + '.' + proximity_zone
proximity_zone = 'zone.' + proximity_zone
state = hass.states.get(proximity_zone)
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.update_ha_state()
# main command to monitor proximity of devices
track_state_change(hass, proximity_devices,
proximity.check_proximity_state_change)
# Tells the bootstrapper that the component was successfully initialized
return True
class Proximity(Entity): # pylint: disable=too-many-instance-attributes
""" Represents a Proximity in Home Assistant. """
def __init__(self, hass, zone_friendly_name, dist_to, dir_of_travel,
nearest, ignored_zones, proximity_devices, tolerance,
proximity_zone):
# pylint: disable=too-many-arguments
self.hass = hass
self.friendly_name = zone_friendly_name
self.dist_to = dist_to
self.dir_of_travel = dir_of_travel
self.nearest = nearest
self.ignored_zones = ignored_zones
self.proximity_devices = proximity_devices
self.tolerance = tolerance
self.proximity_zone = proximity_zone
@property
def state(self):
return self.dist_to
@property
def unit_of_measurement(self):
""" Unit of measurement of this entity """
return "km"
@property
def state_attributes(self):
return {
ATTR_DIR_OF_TRAVEL: self.dir_of_travel,
ATTR_NEAREST: self.nearest,
ATTR_FRIENDLY_NAME: self.friendly_name
}
def check_proximity_state_change(self, entity, old_state, new_state):
# pylint: disable=too-many-branches,too-many-statements,too-many-locals
""" Function to perform the proximity checking """
entity_name = new_state.name
devices_to_calculate = False
devices_in_zone = ''
zone_state = self.hass.states.get(self.proximity_zone)
proximity_latitude = zone_state.attributes.get('latitude')
proximity_longitude = zone_state.attributes.get('longitude')
# check for devices in the monitored zone
for device in self.proximity_devices:
device_state = self.hass.states.get(device)
if device_state.state not in self.ignored_zones:
devices_to_calculate = True
# check the location of all devices
if (device_state.state).lower() == (self.friendly_name).lower():
device_friendly = device_state.name
if devices_in_zone != '':
devices_in_zone = devices_in_zone + ', '
devices_in_zone = devices_in_zone + device_friendly
# no-one to track so reset the entity
if not devices_to_calculate:
self.dist_to = 'not set'
self.dir_of_travel = 'not set'
self.nearest = 'not set'
self.update_ha_state()
return
# at least one device is in the monitored zone so update the entity
if devices_in_zone != '':
self.dist_to = 0
self.dir_of_travel = 'arrived'
self.nearest = devices_in_zone
self.update_ha_state()
return
# we can't check proximity because latitude and longitude don't exist
if 'latitude' not in new_state.attributes:
return
# collect distances to the zone for all devices
distances_to_zone = {}
for device in self.proximity_devices:
# ignore devices in an ignored zone
device_state = self.hass.states.get(device)
if device_state.state in self.ignored_zones:
continue
# ignore devices if proximity cannot be calculated
if 'latitude' not in device_state.attributes:
continue
# calculate the distance to the proximity zone
dist_to_zone = distance(proximity_latitude,
proximity_longitude,
device_state.attributes['latitude'],
device_state.attributes['longitude'])
# add the device and distance to a dictionary
distances_to_zone[device] = round(dist_to_zone / 1000, 1)
# loop through each of the distances collected and work out the closest
closest_device = ''
dist_to_zone = 1000000
for device in distances_to_zone:
if distances_to_zone[device] < dist_to_zone:
closest_device = device
dist_to_zone = distances_to_zone[device]
# if the closest device is one of the other devices
if closest_device != entity:
self.dist_to = round(distances_to_zone[closest_device])
self.dir_of_travel = 'unknown'
device_state = self.hass.states.get(closest_device)
self.nearest = device_state.name
self.update_ha_state()
return
# stop if we cannot calculate the direction of travel (i.e. we don't
# have a previous state and a current LAT and LONG)
if old_state is None or 'latitude' not in old_state.attributes:
self.dist_to = round(distances_to_zone[entity])
self.dir_of_travel = 'unknown'
self.nearest = entity_name
self.update_ha_state()
return
# reset the variables
distance_travelled = 0
# calculate the distance travelled
old_distance = distance(proximity_latitude, proximity_longitude,
old_state.attributes['latitude'],
old_state.attributes['longitude'])
new_distance = distance(proximity_latitude, proximity_longitude,
new_state.attributes['latitude'],
new_state.attributes['longitude'])
distance_travelled = round(new_distance - old_distance, 1)
# check for tolerance
if distance_travelled < self.tolerance * -1:
direction_of_travel = 'towards'
elif distance_travelled > self.tolerance:
direction_of_travel = 'away_from'
else:
direction_of_travel = 'stationary'
# update the proximity entity
self.dist_to = round(dist_to_zone)
self.dir_of_travel = direction_of_travel
self.nearest = entity_name
self.update_ha_state()
_LOGGER.debug('proximity.%s update entity: distance=%s: direction=%s: '
'device=%s', self.friendly_name, round(dist_to_zone),
direction_of_travel, entity_name)
_LOGGER.info('%s: proximity calculation complete', entity_name)

View file

@ -0,0 +1,616 @@
"""
tests.components.proximity
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests proximity component.
"""
import homeassistant.core as ha
from homeassistant.components import proximity
class TestProximity:
""" Test the Proximity component. """
def setup_method(self, method):
self.hass = ha.HomeAssistant()
self.hass.states.set(
'zone.home', 'zoning',
{
'name': 'home',
'latitude': 2.1,
'longitude': 1.1,
'radius': 10
})
def teardown_method(self, method):
""" Stop down stuff we started. """
self.hass.stop()
def test_proximity(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
},
'tolerance': '1'
}
})
state = self.hass.states.get('proximity.home')
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.home', '0')
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.state == '0'
def test_no_devices_in_config(self):
assert not proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'tolerance': '1'
}
})
def test_no_tolerance_in_config(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
}
}
})
def test_no_ignored_zones_in_config(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'devices': {
'device_tracker.test1',
'device_tracker.test2'
},
'tolerance': '1'
}
})
def test_no_zone_in_config(self):
assert proximity.setup(self.hass, {
'proximity': {
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
},
'tolerance': '1'
}
})
def test_device_tracker_test1_in_zone(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1'
},
'tolerance': '1'
}
})
self.hass.states.set(
'device_tracker.test1', 'home',
{
'friendly_name': 'test1',
'latitude': 2.1,
'longitude': 1.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.state == '0'
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'arrived'
def test_device_trackers_in_zone(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
},
'tolerance': '1'
}
})
self.hass.states.set(
'device_tracker.test1', 'home',
{
'friendly_name': 'test1',
'latitude': 2.1,
'longitude': 1.1
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test2', 'home',
{
'friendly_name': 'test2',
'latitude': 2.1,
'longitude': 1.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.state == '0'
assert (state.attributes.get('nearest') == 'test1, test2') or (state.attributes.get('nearest') == 'test2, test1')
assert state.attributes.get('dir_of_travel') == 'arrived'
def test_device_tracker_test1_away(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1'
},
'tolerance': '1'
}
})
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 20.1,
'longitude': 10.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'
def test_device_tracker_test1_awayfurther(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1'
}
}
})
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 20.1,
'longitude': 10.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 40.1,
'longitude': 20.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'away_from'
def test_device_tracker_test1_awaycloser(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1'
}
}
})
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 40.1,
'longitude': 20.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 20.1,
'longitude': 10.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'towards'
def test_all_device_trackers_in_ignored_zone(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1'
}
}
})
self.hass.states.set(
'device_tracker.test1', 'work',
{
'friendly_name': 'test1'
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.state == 'not set'
assert state.attributes.get('nearest') == 'not set'
assert state.attributes.get('dir_of_travel') == 'not set'
def test_device_tracker_test1_no_coordinates(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1'
},
'tolerance': '1'
}
})
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1'
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'not set'
assert state.attributes.get('dir_of_travel') == 'not set'
def test_device_tracker_test1_awayfurther_than_test2_first_test1(self):
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1'
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test2', 'not_home',
{
'friendly_name': 'test2'
})
self.hass.pool.block_till_done()
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
}
}
})
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 20.1,
'longitude': 10.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'
self.hass.states.set(
'device_tracker.test2', 'not_home',
{
'friendly_name': 'test2',
'latitude': 40.1,
'longitude': 20.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'
def test_device_tracker_test1_awayfurther_than_test2_first_test2(self):
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1'
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test2', 'not_home',
{
'friendly_name': 'test2'
})
self.hass.pool.block_till_done()
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
}
}
})
self.hass.states.set(
'device_tracker.test2', 'not_home',
{
'friendly_name': 'test2',
'latitude': 40.1,
'longitude': 20.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test2'
assert state.attributes.get('dir_of_travel') == 'unknown'
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 20.1,
'longitude': 10.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'
def test_device_tracker_test1_awayfurther_test2_in_ignored_zone(self):
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1'
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test2', 'work',
{
'friendly_name': 'test2'
})
self.hass.pool.block_till_done()
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
}
}
})
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 20.1,
'longitude': 10.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'
def test_device_tracker_test1_awayfurther_than_test2_first_test1_than_test2_than_test1(self):
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1'
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test2', 'not_home',
{
'friendly_name': 'test2'
})
self.hass.pool.block_till_done()
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
}
}
})
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 10.1,
'longitude': 5.1
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test2', 'not_home',
{
'friendly_name': 'test2',
'latitude': 20.1,
'longitude': 10.1
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 40.1,
'longitude': 20.1
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 35.1,
'longitude': 15.1
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test1', 'work',
{
'friendly_name': 'test1'
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test2'
assert state.attributes.get('dir_of_travel') == 'unknown'
def test_device_tracker_test1_awayfurther_a_bit(self):
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1'
},
'tolerance': 1000
}
})
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 20.1000001,
'longitude': 10.1000001
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 20.1000002,
'longitude': 10.1000002
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'stationary'
def test_device_tracker_test1_nearest_after_test2_in_ignored_zone(self):
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1'
})
self.hass.pool.block_till_done()
self.hass.states.set(
'device_tracker.test2', 'not_home',
{
'friendly_name': 'test2'
})
self.hass.pool.block_till_done()
assert proximity.setup(self.hass, {
'proximity': {
'zone': 'home',
'ignored_zones': {
'work'
},
'devices': {
'device_tracker.test1',
'device_tracker.test2'
}
}
})
self.hass.states.set(
'device_tracker.test1', 'not_home',
{
'friendly_name': 'test1',
'latitude': 20.1,
'longitude': 10.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'
self.hass.states.set(
'device_tracker.test2', 'not_home',
{
'friendly_name': 'test2',
'latitude': 10.1,
'longitude': 5.1
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test2'
assert state.attributes.get('dir_of_travel') == 'unknown'
self.hass.states.set(
'device_tracker.test2', 'work',
{
'friendly_name': 'test2',
'latitude': 12.6,
'longitude': 7.6
})
self.hass.pool.block_till_done()
state = self.hass.states.get('proximity.home')
assert state.attributes.get('nearest') == 'test1'
assert state.attributes.get('dir_of_travel') == 'unknown'