From 75e6ed87d6f60ab5419cdfe5ac4ec71295be3b02 Mon Sep 17 00:00:00 2001 From: NMA Date: Fri, 12 Aug 2016 14:48:28 +0530 Subject: [PATCH 1/2] Backend support for importing waypoints from owntracks as HA zones --- .../components/device_tracker/owntracks.py | 69 +++++++++++++------ homeassistant/components/zone.py | 23 +++++-- 2 files changed, 66 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 00ba8c68556..13cc918a436 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -12,6 +12,7 @@ from collections import defaultdict import homeassistant.components.mqtt as mqtt from homeassistant.const import STATE_HOME from homeassistant.util import convert, slugify +from homeassistant.components import zone DEPENDENCIES = ['mqtt'] @@ -22,17 +23,19 @@ BEACON_DEV_ID = 'beacon' LOCATION_TOPIC = 'owntracks/+/+' EVENT_TOPIC = 'owntracks/+/+/event' +WAYPOINT_TOPIC = 'owntracks/{}/+/waypoint' _LOGGER = logging.getLogger(__name__) LOCK = threading.Lock() CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy' - +CONF_WAYPOINT_IMPORT_USER = 'waypoint_import_user' def setup_scanner(hass, config, see): """Setup an OwnTracks tracker.""" max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY) + waypoint_import_user = config.get(CONF_WAYPOINT_IMPORT_USER) def validate_payload(payload, data_type): """Validate OwnTracks payload.""" @@ -47,17 +50,18 @@ def setup_scanner(hass, config, see): 'because of missing or malformatted data: %s', data_type, data) return None - if max_gps_accuracy is not None and \ - convert(data.get('acc'), float, 0.0) > max_gps_accuracy: - _LOGGER.debug('Skipping %s update because expected GPS ' - 'accuracy %s is not met: %s', - data_type, max_gps_accuracy, data) - return None - if convert(data.get('acc'), float, 1.0) == 0.0: - _LOGGER.debug('Skipping %s update because GPS accuracy' - 'is zero', - data_type) - return None + if data_type != 'waypoints': + if max_gps_accuracy is not None and \ + convert(data.get('acc'), float, 0.0) > max_gps_accuracy: + _LOGGER.debug('Skipping %s update because expected GPS ' + 'accuracy %s is not met: %s', + data_type, max_gps_accuracy, data) + return None + if convert(data.get('acc'), float, 1.0) == 0.0: + _LOGGER.debug('Skipping %s update because GPS accuracy' + 'is zero', + data_type) + return None return data @@ -105,9 +109,9 @@ def setup_scanner(hass, config, see): def enter_event(): """Execute enter event.""" - zone = hass.states.get("zone.{}".format(location)) + _zone = hass.states.get("zone.{}".format(location)) with LOCK: - if zone is None and data.get('t') == 'b': + if _zone is None and data.get('t') == 'b': # Not a HA zone, and a beacon so assume mobile beacons = MOBILE_BEACONS_ACTIVE[dev_id] if location not in beacons: @@ -119,7 +123,7 @@ def setup_scanner(hass, config, see): if location not in regions: regions.append(location) _LOGGER.info("Enter region %s", location) - _set_gps_from_zone(kwargs, location, zone) + _set_gps_from_zone(kwargs, location, _zone) see(**kwargs) see_beacons(dev_id, kwargs) @@ -134,8 +138,8 @@ def setup_scanner(hass, config, see): if new_region: # Exit to previous region - zone = hass.states.get("zone.{}".format(new_region)) - _set_gps_from_zone(kwargs, new_region, zone) + _zone = hass.states.get("zone.{}".format(new_region)) + _set_gps_from_zone(kwargs, new_region, _zone) _LOGGER.info("Exit to %s", new_region) see(**kwargs) see_beacons(dev_id, kwargs) @@ -167,6 +171,23 @@ def setup_scanner(hass, config, see): data['event']) return + def owntracks_waypoint_update(topic, payload, qos): + """List of waypoints published by a user.""" + # Docs on available data: + # http://owntracks.org/booklet/tech/json/#_typewaypoints + data = validate_payload(payload, 'waypoints') + if not data: + return + + wayps = data['waypoints'] + _LOGGER.info("Got %d waypoints from %s", len(wayps), topic) + for wayp in wayps: + name = wayp['desc'] + lat = wayp['lat'] + lon = wayp['lon'] + rad = wayp['rad'] + zone.add_zone(hass, name, lat, lon, rad) + def see_beacons(dev_id, kwargs_param): """Set active beacons to the current location.""" kwargs = kwargs_param.copy() @@ -180,6 +201,10 @@ def setup_scanner(hass, config, see): mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1) mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1) + if waypoint_import_user is not None: + mqtt.subscribe(hass, WAYPOINT_TOPIC.format(waypoint_import_user), + owntracks_waypoint_update, 1) + return True @@ -200,12 +225,12 @@ def _parse_see_args(topic, data): return dev_id, kwargs -def _set_gps_from_zone(kwargs, location, zone): +def _set_gps_from_zone(kwargs, location, _zone): """Set the see parameters from the zone parameters.""" - if zone is not None: + if _zone is not None: kwargs['gps'] = ( - zone.attributes['latitude'], - zone.attributes['longitude']) - kwargs['gps_accuracy'] = zone.attributes['radius'] + _zone.attributes['latitude'], + _zone.attributes['longitude']) + kwargs['gps_accuracy'] = _zone.attributes['radius'] kwargs['location_name'] = location return kwargs diff --git a/homeassistant/components/zone.py b/homeassistant/components/zone.py index db57b387c9f..ee4ff8a48a6 100644 --- a/homeassistant/components/zone.py +++ b/homeassistant/components/zone.py @@ -27,7 +27,10 @@ ATTR_PASSIVE = 'passive' DEFAULT_PASSIVE = False ICON_HOME = 'mdi:home' +ICON_IMPORT = 'mdi:import' +entities = set() +_LOGGER = logging.getLogger(__name__) def active_zone(hass, latitude, longitude, radius=0): """Find the active zone for given latitude, longitude.""" @@ -70,7 +73,6 @@ def in_zone(zone, latitude, longitude, radius=0): def setup(hass, config): """Setup zone.""" - entities = set() for key in extract_domain_configs(config, DOMAIN): entries = config[key] @@ -90,7 +92,7 @@ def setup(hass, config): 'Each zone needs a latitude and longitude.') continue - zone = Zone(hass, name, latitude, longitude, radius, icon, passive) + zone = Zone(hass, name, latitude, longitude, radius, icon, passive, False) zone.entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, entities) zone.update_ha_state() @@ -98,18 +100,30 @@ def setup(hass, config): if ENTITY_ID_HOME not in entities: zone = Zone(hass, hass.config.location_name, hass.config.latitude, - hass.config.longitude, DEFAULT_RADIUS, ICON_HOME, False) + hass.config.longitude, DEFAULT_RADIUS, ICON_HOME, False, False) zone.entity_id = ENTITY_ID_HOME zone.update_ha_state() return True +# Add a zone to the existing set +def add_zone(hass, name, latitude, longitude, radius): + _LOGGER.info("Adding new zone %s", name) + if name not in entities: + zone = Zone(hass, name, latitude, longitude, radius, ICON_IMPORT, + False, True) + zone.entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, + entities) + zone.update_ha_state() + entities.add(zone.entity_id) + else: + _LOGGER.info("Zone already exists") class Zone(Entity): """Representation of a Zone.""" # pylint: disable=too-many-arguments, too-many-instance-attributes - def __init__(self, hass, name, latitude, longitude, radius, icon, passive): + def __init__(self, hass, name, latitude, longitude, radius, icon, passive, imported): """Initialize the zone.""" self.hass = hass self._name = name @@ -118,6 +132,7 @@ class Zone(Entity): self._radius = radius self._icon = icon self._passive = passive + self._imported = imported @property def name(self): From 2bea5a484f462da496b5e8a6c16a92721d46f487 Mon Sep 17 00:00:00 2001 From: NMA Date: Thu, 25 Aug 2016 16:47:34 +0530 Subject: [PATCH 2/2] Added test for Owntracks waypoints import --- .../components/device_tracker/owntracks.py | 3 +- .../device_tracker/test_owntracks.py | 37 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 13cc918a436..9f505685721 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -32,6 +32,7 @@ LOCK = threading.Lock() CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy' CONF_WAYPOINT_IMPORT_USER = 'waypoint_import_user' + def setup_scanner(hass, config, see): """Setup an OwnTracks tracker.""" max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY) @@ -203,7 +204,7 @@ def setup_scanner(hass, config, see): if waypoint_import_user is not None: mqtt.subscribe(hass, WAYPOINT_TOPIC.format(waypoint_import_user), - owntracks_waypoint_update, 1) + owntracks_waypoint_update, 1) return True diff --git a/tests/components/device_tracker/test_owntracks.py b/tests/components/device_tracker/test_owntracks.py index 16fb1c4a4ce..f6f1fc58147 100644 --- a/tests/components/device_tracker/test_owntracks.py +++ b/tests/components/device_tracker/test_owntracks.py @@ -17,6 +17,7 @@ DEVICE = 'phone' LOCATION_TOPIC = "owntracks/{}/{}".format(USER, DEVICE) EVENT_TOPIC = "owntracks/{}/{}/event".format(USER, DEVICE) +WAYPOINT_TOPIC = 'owntracks/{}/{}/waypoint'.format(USER, DEVICE) DEVICE_TRACKER_STATE = "device_tracker.{}_{}".format(USER, DEVICE) @@ -24,6 +25,7 @@ IBEACON_DEVICE = 'keys' REGION_TRACKER_STATE = "device_tracker.beacon_{}".format(IBEACON_DEVICE) CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy' +CONF_WAYPOINT_IMPORT_USER = 'waypoint_import_user' LOCATION_MESSAGE = { 'batt': 92, @@ -107,6 +109,28 @@ REGION_LEAVE_INACCURATE_MESSAGE = { 'lat': 20.0, '_type': 'transition'} +WAYPOINTS_EXPORTED_MESSAGE = { + "_type": "waypoints", + "_creator": "test", + "waypoints": [ + { + "_type": "waypoint", + "tst": 3, + "lat": 47, + "lon": 9, + "rad": 10, + "desc": "exp_wayp1" + }, + { + "_type": "waypoint", + "tst": 4, + "lat": 3, + "lon": 9, + "rad": 500, + "desc": "exp_wayp2" + } + ] +} class TestDeviceTrackerOwnTracks(unittest.TestCase): """Test the OwnTrack sensor.""" @@ -118,7 +142,8 @@ class TestDeviceTrackerOwnTracks(unittest.TestCase): self.assertTrue(device_tracker.setup(self.hass, { device_tracker.DOMAIN: { CONF_PLATFORM: 'owntracks', - CONF_MAX_GPS_ACCURACY: 200 + CONF_MAX_GPS_ACCURACY: 200, + CONF_WAYPOINT_IMPORT_USER: USER }})) self.hass.states.set( @@ -486,3 +511,13 @@ class TestDeviceTrackerOwnTracks(unittest.TestCase): self.send_message(EVENT_TOPIC, exit_message) self.assertEqual(owntracks.MOBILE_BEACONS_ACTIVE['greg_phone'], []) + + def test_waypoint_import_simple(self): + """Test a simple import of list of waypoints.""" + waypoints_message = WAYPOINTS_EXPORTED_MESSAGE.copy() + self.send_message(WAYPOINT_TOPIC, waypoints_message) + # Check if it made it into states + wayp = self.hass.states.get('zone.exp_wayp1') + self.assertTrue(wayp != None) + wayp = self.hass.states.get('zone.exp_wayp2') + self.assertTrue(wayp != None)