diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index a7e4dbfdc14..b2e5fa51540 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -297,11 +297,15 @@ def process_ha_core_config(hass, config): else: _LOGGER.error('Received invalid time zone %s', time_zone_str) - for key, attr in ((CONF_LATITUDE, 'latitude'), - (CONF_LONGITUDE, 'longitude'), - (CONF_NAME, 'location_name')): + for key, attr, typ in ((CONF_LATITUDE, 'latitude', float), + (CONF_LONGITUDE, 'longitude', float), + (CONF_NAME, 'location_name', str)): if key in config: - setattr(hac, attr, config[key]) + try: + setattr(hac, attr, typ(config[key])) + except ValueError: + _LOGGER.error('Received invalid %s value for %s: %s', + typ.__name__, key, attr) set_time_zone(config.get(CONF_TIME_ZONE)) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 97c3d769715..bb0ac890d1a 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -17,6 +17,9 @@ device_tracker: # New found devices auto found track_new_devices: yes + + # Maximum distance from home we consider people home + range_home: 100 """ import csv from datetime import timedelta @@ -52,7 +55,7 @@ CONF_TRACK_NEW = "track_new_devices" DEFAULT_CONF_TRACK_NEW = True CONF_CONSIDER_HOME = 'consider_home' -DEFAULT_CONF_CONSIDER_HOME = 180 # seconds +DEFAULT_CONSIDER_HOME = 180 # seconds CONF_SCAN_INTERVAL = "interval_seconds" DEFAULT_SCAN_INTERVAL = 12 @@ -60,6 +63,9 @@ DEFAULT_SCAN_INTERVAL = 12 CONF_AWAY_HIDE = 'hide_if_away' DEFAULT_AWAY_HIDE = False +CONF_HOME_RANGE = 'home_range' +DEFAULT_HOME_RANGE = 100 + SERVICE_SEE = 'see' ATTR_LATITUDE = 'latitude' @@ -106,13 +112,17 @@ def setup(hass, config): os.remove(csv_path) conf = config.get(DOMAIN, {}) - consider_home = util.convert(conf.get(CONF_CONSIDER_HOME), int, - DEFAULT_CONF_CONSIDER_HOME) + consider_home = timedelta( + seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int, + DEFAULT_CONSIDER_HOME)) track_new = util.convert(conf.get(CONF_TRACK_NEW), bool, DEFAULT_CONF_TRACK_NEW) + home_range = util.convert(conf.get(CONF_HOME_RANGE), int, + DEFAULT_HOME_RANGE) - devices = load_config(yaml_path, hass, timedelta(seconds=consider_home)) - tracker = DeviceTracker(hass, consider_home, track_new, devices) + devices = load_config(yaml_path, hass, consider_home, home_range) + tracker = DeviceTracker(hass, consider_home, track_new, home_range, + devices) def setup_platform(p_type, p_config, disc_info=None): """ Setup a device tracker platform. """ @@ -168,12 +178,13 @@ def setup(hass, config): class DeviceTracker(object): """ Track devices """ - def __init__(self, hass, consider_home, track_new, devices): + def __init__(self, hass, consider_home, track_new, home_range, devices): self.hass = hass self.devices = {dev.dev_id: dev for dev in devices} self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac} - self.consider_home = timedelta(seconds=consider_home) + self.consider_home = consider_home self.track_new = track_new + self.home_range = home_range self.lock = threading.Lock() for device in devices: @@ -205,8 +216,8 @@ class DeviceTracker(object): # If no device can be found, create it device = Device( - self.hass, self.consider_home, self.track_new, dev_id, mac, - (host_name or dev_id).replace('_', ' ')) + self.hass, self.consider_home, self.home_range, self.track_new, + dev_id, mac, (host_name or dev_id).replace('_', ' ')) self.devices[dev_id] = device if mac is not None: self.mac_to_dev[mac] = device @@ -250,8 +261,8 @@ class Device(Entity): last_update_home = False _state = STATE_NOT_HOME - def __init__(self, hass, consider_home, track, dev_id, mac, name=None, - picture=None, away_hide=False): + def __init__(self, hass, consider_home, home_range, track, dev_id, mac, + name=None, picture=None, away_hide=False): self.hass = hass self.entity_id = ENTITY_ID_FORMAT.format(dev_id) @@ -259,6 +270,8 @@ class Device(Entity): # detected anymore. self.consider_home = consider_home + # Distance in meters + self.home_range = home_range # Device ID self.dev_id = dev_id self.mac = mac @@ -273,6 +286,12 @@ class Device(Entity): self.config_picture = picture self.away_hide = away_hide + @property + def gps_home(self): + """ Return if device is within range of home. """ + return (self.gps is not None and + self.hass.config.distance(*self.gps) < self.home_range) + @property def name(self): """ Returns the name of the entity. """ @@ -307,7 +326,15 @@ class Device(Entity): self.last_seen = dt_util.utcnow() self.host_name = host_name self.location_name = location_name - self.gps = gps + if gps is None: + self.gps = None + else: + try: + self.gps = tuple(float(val) for val in gps) + except ValueError: + _LOGGER.warning('Could not parse gps value for %s: %s', + self.dev_id, gps) + self.gps = None self.update() def stale(self, now=None): @@ -321,6 +348,8 @@ class Device(Entity): return elif self.location_name: self._state = self.location_name + elif self.gps is not None: + self._state = STATE_HOME if self.gps_home else STATE_NOT_HOME elif self.stale(): self._state = STATE_NOT_HOME self.last_update_home = False @@ -338,18 +367,18 @@ def convert_csv_config(csv_path, yaml_path): (util.slugify(row['name']) or DEVICE_DEFAULT_NAME).lower(), used_ids) used_ids.add(dev_id) - device = Device(None, None, row['track'] == '1', dev_id, + device = Device(None, None, None, row['track'] == '1', dev_id, row['device'], row['name'], row['picture']) update_config(yaml_path, dev_id, device) return True -def load_config(path, hass, consider_home): +def load_config(path, hass, consider_home, home_range): """ Load devices from YAML config file. """ if not os.path.isfile(path): return [] return [ - Device(hass, consider_home, device.get('track', False), + Device(hass, consider_home, home_range, device.get('track', False), str(dev_id).lower(), str(device.get('mac')).upper(), device.get('name'), device.get('picture'), device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE)) diff --git a/homeassistant/core.py b/homeassistant/core.py index df18d7e7902..d0494e070f6 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -26,6 +26,7 @@ from homeassistant.exceptions import ( HomeAssistantError, InvalidEntityFormatError) import homeassistant.util as util import homeassistant.util.dt as date_util +import homeassistant.util.location as location import homeassistant.helpers.temperature as temp_helper from homeassistant.config import get_default_config_dir @@ -676,6 +677,10 @@ class Config(object): # Directory that holds the configuration self.config_dir = get_default_config_dir() + def distance(self, lat, lon): + """ Calculate distance from Home Assistant in meters. """ + return location.distance(self.latitude, self.longitude, lat, lon) + def path(self, *path): """ Returns path to the file within the config dir. """ return os.path.join(self.config_dir, *path) diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 8cc008613cb..acb5deb675b 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -1,5 +1,6 @@ """Module with location helpers.""" import collections +from math import radians, cos, sin, asin, sqrt import requests @@ -28,3 +29,22 @@ def detect_location_info(): 'BS', 'BZ', 'KY', 'PW', 'US', 'AS', 'VI') return LocationInfo(**data) + + +# From: http://stackoverflow.com/a/4913653/646416 +def distance(lon1, lat1, lon2, lat2): + """ + Calculate the great circle distance in meters between two points specified + in decimal degrees on the earth using the Haversine algorithm. + """ + # convert decimal degrees to radians + lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2]) + + # haversine formula + dlon = lon2 - lon1 + dlat = lat2 - lat1 + a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2 + c = 2 * asin(sqrt(a)) + # Radius of earth in meters. + radius = 6371000 + return c * radius