From 41ef6228be15cdd2639673c7c6573d866a0c6c48 Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Sat, 7 Jan 2017 21:09:07 +0000 Subject: [PATCH] [device_tracker] Use home zone GPS coordinates for router based devices. (#4852) --- .../components/device_tracker/__init__.py | 52 +++++++++++---- tests/components/device_tracker/test_init.py | 64 +++++++++++++++++++ 2 files changed, 105 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 9b5556d7ace..f08a9badb6f 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -70,6 +70,10 @@ ATTR_LOCATION_NAME = 'location_name' ATTR_GPS = 'gps' ATTR_BATTERY = 'battery' ATTR_ATTRIBUTES = 'attributes' +ATTR_SOURCE_TYPE = 'source_type' + +SOURCE_TYPE_GPS = 'gps' +SOURCE_TYPE_ROUTER = 'router' PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SCAN_INTERVAL): cv.time_period, @@ -234,17 +238,19 @@ class DeviceTracker(object): def see(self, mac: str=None, dev_id: str=None, host_name: str=None, location_name: str=None, gps: GPSType=None, gps_accuracy=None, - battery: str=None, attributes: dict=None): + battery: str=None, attributes: dict=None, + source_type: str=SOURCE_TYPE_GPS): """Notify the device tracker that you see a device.""" self.hass.add_job( self.async_see(mac, dev_id, host_name, location_name, gps, - gps_accuracy, battery, attributes) + gps_accuracy, battery, attributes, source_type) ) @asyncio.coroutine def async_see(self, mac: str=None, dev_id: str=None, host_name: str=None, location_name: str=None, gps: GPSType=None, - gps_accuracy=None, battery: str=None, attributes: dict=None): + gps_accuracy=None, battery: str=None, attributes: dict=None, + source_type: str=SOURCE_TYPE_GPS): """Notify the device tracker that you see a device. This method is a coroutine. @@ -262,7 +268,8 @@ class DeviceTracker(object): if device: yield from device.async_seen(host_name, location_name, gps, - gps_accuracy, battery, attributes) + gps_accuracy, battery, attributes, + source_type) if device.track: yield from device.async_update_ha_state() return @@ -277,7 +284,8 @@ class DeviceTracker(object): self.mac_to_dev[mac] = device yield from device.async_seen(host_name, location_name, gps, - gps_accuracy, battery, attributes) + gps_accuracy, battery, attributes, + source_type) if device.track: yield from device.async_update_ha_state() @@ -381,6 +389,9 @@ class Device(Entity): self.away_hide = hide_if_away self.vendor = vendor + + self.source_type = None + self._attributes = {} @property @@ -401,7 +412,9 @@ class Device(Entity): @property def state_attributes(self): """Return the device state attributes.""" - attr = {} + attr = { + ATTR_SOURCE_TYPE: self.source_type + } if self.gps: attr[ATTR_LATITUDE] = self.gps[0] @@ -426,12 +439,13 @@ class Device(Entity): @asyncio.coroutine def async_seen(self, host_name: str=None, location_name: str=None, gps: GPSType=None, gps_accuracy=0, battery: str=None, - attributes: dict=None): + attributes: dict=None, source_type: str=SOURCE_TYPE_GPS): """Mark the device as seen.""" + self.source_type = source_type self.last_seen = dt_util.utcnow() self.host_name = host_name self.location_name = location_name - self.gps_accuracy = gps_accuracy or 0 + if battery: self.battery = battery if attributes: @@ -442,7 +456,10 @@ class Device(Entity): if gps is not None: try: self.gps = float(gps[0]), float(gps[1]) + self.gps_accuracy = gps_accuracy or 0 except (ValueError, TypeError, IndexError): + self.gps = None + self.gps_accuracy = 0 _LOGGER.warning('Could not parse gps value for %s: %s', self.dev_id, gps) @@ -467,7 +484,7 @@ class Device(Entity): return elif self.location_name: self._state = self.location_name - elif self.gps is not None: + elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS: zone_state = zone.async_active_zone( self.hass, self.gps[0], self.gps[1], self.gps_accuracy) if zone_state is None: @@ -476,9 +493,9 @@ class Device(Entity): self._state = STATE_HOME else: self._state = zone_state.name - elif self.stale(): self._state = STATE_NOT_HOME + self.gps = None self.last_update_home = False else: self._state = STATE_HOME @@ -637,7 +654,20 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, else: host_name = yield from scanner.async_get_device_name(mac) seen.add(mac) - hass.async_add_job(async_see_device(mac=mac, host_name=host_name)) + + kwargs = { + 'mac': mac, + 'host_name': host_name, + 'source_type': SOURCE_TYPE_ROUTER + } + + zone_home = hass.states.get(zone.ENTITY_ID_HOME) + if zone_home: + kwargs['gps'] = [zone_home.attributes[ATTR_LATITUDE], + zone_home.attributes[ATTR_LONGITUDE]] + kwargs['gps_accuracy'] = 0 + + hass.async_add_job(async_see_device(**kwargs)) async_track_time_interval(hass, async_device_tracker_scan, interval) hass.async_add_job(async_device_tracker_scan, None) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index ffcb1e8acd6..c083557294b 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -8,6 +8,7 @@ from unittest.mock import call, patch from datetime import datetime, timedelta import os +from homeassistant.components import zone from homeassistant.core import callback from homeassistant.bootstrap import setup_component from homeassistant.loader import get_component @@ -541,8 +542,71 @@ class TestComponentsDeviceTracker(unittest.TestCase): self.assertEqual(attrs['longitude'], 0.8) self.assertEqual(attrs['test'], 'test') self.assertEqual(attrs['gps_accuracy'], 1) + self.assertEqual(attrs['source_type'], 'gps') self.assertEqual(attrs['number'], 1) + def test_see_passive_zone_state(self): + """Test that the device tracker sets gps for passive trackers.""" + register_time = datetime(2015, 9, 15, 23, tzinfo=dt_util.UTC) + scan_time = datetime(2015, 9, 15, 23, 1, tzinfo=dt_util.UTC) + + with assert_setup_component(1, zone.DOMAIN): + zone_info = { + 'name': 'Home', + 'latitude': 1, + 'longitude': 2, + 'radius': 250, + 'passive': False + } + + setup_component(self.hass, zone.DOMAIN, { + 'zone': zone_info + }) + + scanner = get_component('device_tracker.test').SCANNER + scanner.reset() + scanner.come_home('dev1') + + with patch('homeassistant.components.device_tracker.dt_util.utcnow', + return_value=register_time): + with assert_setup_component(1, device_tracker.DOMAIN): + assert setup_component(self.hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'test', + device_tracker.CONF_CONSIDER_HOME: 59, + }}) + + state = self.hass.states.get('device_tracker.dev1') + attrs = state.attributes + self.assertEqual(STATE_HOME, state.state) + self.assertEqual(state.object_id, 'dev1') + self.assertEqual(state.name, 'dev1') + self.assertEqual(attrs.get('friendly_name'), 'dev1') + self.assertEqual(attrs.get('latitude'), 1) + self.assertEqual(attrs.get('longitude'), 2) + self.assertEqual(attrs.get('gps_accuracy'), 0) + self.assertEqual(attrs.get('source_type'), + device_tracker.SOURCE_TYPE_ROUTER) + + scanner.leave_home('dev1') + + with patch('homeassistant.components.device_tracker.dt_util.utcnow', + return_value=scan_time): + fire_time_changed(self.hass, scan_time) + self.hass.block_till_done() + + state = self.hass.states.get('device_tracker.dev1') + attrs = state.attributes + self.assertEqual(STATE_NOT_HOME, state.state) + self.assertEqual(state.object_id, 'dev1') + self.assertEqual(state.name, 'dev1') + self.assertEqual(attrs.get('friendly_name'), 'dev1') + self.assertEqual(attrs.get('latitude'), None) + self.assertEqual(attrs.get('longitude'), None) + self.assertEqual(attrs.get('gps_accuracy'), None) + self.assertEqual(attrs.get('source_type'), + device_tracker.SOURCE_TYPE_ROUTER) + @patch('homeassistant.components.device_tracker._LOGGER.warning') def test_see_failures(self, mock_warning): """Test that the device tracker see failures."""