From d820efc4e377cf1f282de0a4b1bfb81f825f006b Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Fri, 11 Jan 2019 15:14:11 -0800 Subject: [PATCH] Split locative to a separate component (#19964) * Split locative to a separate component * Switch tests to use constants for http codes * Fix tests --- .../components/device_tracker/locative.py | 107 ++------------- homeassistant/components/locative/__init__.py | 120 +++++++++++++++++ tests/components/locative/__init__.py | 1 + .../test_init.py} | 127 ++++++++++-------- 4 files changed, 202 insertions(+), 153 deletions(-) create mode 100644 homeassistant/components/locative/__init__.py create mode 100644 tests/components/locative/__init__.py rename tests/components/{device_tracker/test_locative.py => locative/test_init.py} (52%) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index aa91f0d3d71..e7a63077a3a 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -4,106 +4,25 @@ Support for the Locative platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.locative/ """ -from functools import partial import logging -from homeassistant.const import ( - ATTR_LATITUDE, ATTR_LONGITUDE, STATE_NOT_HOME, HTTP_UNPROCESSABLE_ENTITY) -from homeassistant.components.http import HomeAssistantView -# pylint: disable=unused-import -from homeassistant.components.device_tracker import ( # NOQA - DOMAIN, PLATFORM_SCHEMA) +from homeassistant.components.locative import TRACKER_UPDATE +from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['http'] -URL = '/api/locative' +DEPENDENCIES = ['locative'] -def setup_scanner(hass, config, see, discovery_info=None): - """Set up an endpoint for the Locative application.""" - hass.http.register_view(LocativeView(see)) +async def async_setup_scanner(hass, config, async_see, discovery_info=None): + """Set up an endpoint for the Locative device tracker.""" + async def _set_location(device, gps_location, location_name): + """Fire HA event to set location.""" + await async_see( + dev_id=device, + gps=gps_location, + location_name=location_name + ) + async_dispatcher_connect(hass, TRACKER_UPDATE, _set_location) return True - - -class LocativeView(HomeAssistantView): - """View to handle Locative requests.""" - - url = URL - name = 'api:locative' - - def __init__(self, see): - """Initialize Locative URL endpoints.""" - self.see = see - - async def get(self, request): - """Locative message received as GET.""" - res = await self._handle(request.app['hass'], request.query) - return res - - async def post(self, request): - """Locative message received.""" - data = await request.post() - res = await self._handle(request.app['hass'], data) - return res - - async def _handle(self, hass, data): - """Handle locative request.""" - if 'latitude' not in data or 'longitude' not in data: - return ('Latitude and longitude not specified.', - HTTP_UNPROCESSABLE_ENTITY) - - if 'device' not in data: - _LOGGER.error('Device id not specified.') - return ('Device id not specified.', - HTTP_UNPROCESSABLE_ENTITY) - - if 'trigger' not in data: - _LOGGER.error('Trigger is not specified.') - return ('Trigger is not specified.', - HTTP_UNPROCESSABLE_ENTITY) - - if 'id' not in data and data['trigger'] != 'test': - _LOGGER.error('Location id not specified.') - return ('Location id not specified.', - HTTP_UNPROCESSABLE_ENTITY) - - device = data['device'].replace('-', '') - location_name = data.get('id', data['trigger']).lower() - direction = data['trigger'] - gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]) - - if direction == 'enter': - await hass.async_add_job( - partial(self.see, dev_id=device, location_name=location_name, - gps=gps_location)) - return 'Setting location to {}'.format(location_name) - - if direction == 'exit': - current_state = hass.states.get( - '{}.{}'.format(DOMAIN, device)) - - if current_state is None or current_state.state == location_name: - location_name = STATE_NOT_HOME - await hass.async_add_job( - partial(self.see, dev_id=device, - location_name=location_name, gps=gps_location)) - return 'Setting location to not home' - - # Ignore the message if it is telling us to exit a zone that we - # aren't currently in. This occurs when a zone is entered - # before the previous zone was exited. The enter message will - # be sent first, then the exit message will be sent second. - return 'Ignoring exit from {} (already in {})'.format( - location_name, current_state) - - if direction == 'test': - # In the app, a test message can be sent. Just return something to - # the user to let them know that it works. - return 'Received test message.' - - _LOGGER.error('Received unidentified message from Locative: %s', - direction) - return ('Received unidentified message: {}'.format(direction), - HTTP_UNPROCESSABLE_ENTITY) diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py new file mode 100644 index 00000000000..1f924987dd3 --- /dev/null +++ b/homeassistant/components/locative/__init__.py @@ -0,0 +1,120 @@ +""" +Support for Locative. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/locative/ +""" +import logging + +from homeassistant.components.device_tracker import \ + DOMAIN as DEVICE_TRACKER_DOMAIN +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, ATTR_LATITUDE, \ + ATTR_LONGITUDE, STATE_NOT_HOME +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.dispatcher import async_dispatcher_send + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'locative' +DEPENDENCIES = ['http'] + +URL = '/api/locative' + +TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN) + + +async def async_setup(hass, hass_config): + """Set up the Locative component.""" + hass.http.register_view(LocativeView) + hass.async_create_task( + async_load_platform(hass, 'device_tracker', DOMAIN, {}, hass_config) + ) + return True + + +class LocativeView(HomeAssistantView): + """View to handle Locative requests.""" + + url = URL + name = 'api:locative' + + def __init__(self): + """Initialize Locative URL endpoints.""" + + async def get(self, request): + """Locative message received as GET.""" + return await self._handle(request.app['hass'], request.query) + + async def post(self, request): + """Locative message received.""" + data = await request.post() + return await self._handle(request.app['hass'], data) + + async def _handle(self, hass, data): + """Handle locative request.""" + if 'latitude' not in data or 'longitude' not in data: + return ('Latitude and longitude not specified.', + HTTP_UNPROCESSABLE_ENTITY) + + if 'device' not in data: + _LOGGER.error('Device id not specified.') + return ('Device id not specified.', + HTTP_UNPROCESSABLE_ENTITY) + + if 'trigger' not in data: + _LOGGER.error('Trigger is not specified.') + return ('Trigger is not specified.', + HTTP_UNPROCESSABLE_ENTITY) + + if 'id' not in data and data['trigger'] != 'test': + _LOGGER.error('Location id not specified.') + return ('Location id not specified.', + HTTP_UNPROCESSABLE_ENTITY) + + device = data['device'].replace('-', '') + location_name = data.get('id', data['trigger']).lower() + direction = data['trigger'] + gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]) + + if direction == 'enter': + async_dispatcher_send( + hass, + TRACKER_UPDATE, + device, + gps_location, + location_name + ) + return 'Setting location to {}'.format(location_name) + + if direction == 'exit': + current_state = hass.states.get( + '{}.{}'.format(DEVICE_TRACKER_DOMAIN, device)) + + if current_state is None or current_state.state == location_name: + location_name = STATE_NOT_HOME + async_dispatcher_send( + hass, + TRACKER_UPDATE, + device, + gps_location, + location_name + ) + return 'Setting location to not home' + + # Ignore the message if it is telling us to exit a zone that we + # aren't currently in. This occurs when a zone is entered + # before the previous zone was exited. The enter message will + # be sent first, then the exit message will be sent second. + return 'Ignoring exit from {} (already in {})'.format( + location_name, current_state) + + if direction == 'test': + # In the app, a test message can be sent. Just return something to + # the user to let them know that it works. + return 'Received test message.' + + _LOGGER.error('Received unidentified message from Locative: %s', + direction) + return ('Received unidentified message: {}'.format(direction), + HTTP_UNPROCESSABLE_ENTITY) diff --git a/tests/components/locative/__init__.py b/tests/components/locative/__init__.py new file mode 100644 index 00000000000..8be6da6628d --- /dev/null +++ b/tests/components/locative/__init__.py @@ -0,0 +1 @@ +"""Tests for the Locative component.""" diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/locative/test_init.py similarity index 52% rename from tests/components/device_tracker/test_locative.py rename to tests/components/locative/test_init.py index a167a1e9fd4..50c72f468af 100644 --- a/tests/components/device_tracker/test_locative.py +++ b/tests/components/locative/test_init.py @@ -1,13 +1,13 @@ """The tests the for Locative device tracker platform.""" -import asyncio from unittest.mock import patch import pytest +from homeassistant.components.device_tracker import \ + DOMAIN as DEVICE_TRACKER_DOMAIN +from homeassistant.components.locative import URL, DOMAIN +from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.setup import async_setup_component -import homeassistant.components.device_tracker as device_tracker -from homeassistant.const import CONF_PLATFORM -from homeassistant.components.device_tracker.locative import URL def _url(data=None): @@ -18,22 +18,25 @@ def _url(data=None): return "{}?{}".format(URL, data) +@pytest.fixture(autouse=True) +def mock_dev_track(mock_device_tracker_conf): + """Mock device tracker config loading.""" + pass + + @pytest.fixture def locative_client(loop, hass, hass_client): """Locative mock client.""" assert loop.run_until_complete(async_setup_component( - hass, device_tracker.DOMAIN, { - device_tracker.DOMAIN: { - CONF_PLATFORM: 'locative' - } + hass, DOMAIN, { + DOMAIN: {} })) with patch('homeassistant.components.device_tracker.update_config'): yield loop.run_until_complete(hass_client()) -@asyncio.coroutine -def test_missing_data(locative_client): +async def test_missing_data(locative_client): """Test missing data.""" data = { 'latitude': 1.0, @@ -44,55 +47,54 @@ def test_missing_data(locative_client): } # No data - req = yield from locative_client.get(_url({})) - assert req.status == 422 + req = await locative_client.get(_url({})) + assert req.status == HTTP_UNPROCESSABLE_ENTITY # No latitude copy = data.copy() del copy['latitude'] - req = yield from locative_client.get(_url(copy)) - assert req.status == 422 + req = await locative_client.get(_url(copy)) + assert req.status == HTTP_UNPROCESSABLE_ENTITY # No device copy = data.copy() del copy['device'] - req = yield from locative_client.get(_url(copy)) - assert req.status == 422 + req = await locative_client.get(_url(copy)) + assert req.status == HTTP_UNPROCESSABLE_ENTITY # No location copy = data.copy() del copy['id'] - req = yield from locative_client.get(_url(copy)) - assert req.status == 422 + req = await locative_client.get(_url(copy)) + assert req.status == HTTP_UNPROCESSABLE_ENTITY # No trigger copy = data.copy() del copy['trigger'] - req = yield from locative_client.get(_url(copy)) - assert req.status == 422 + req = await locative_client.get(_url(copy)) + assert req.status == HTTP_UNPROCESSABLE_ENTITY # Test message copy = data.copy() copy['trigger'] = 'test' - req = yield from locative_client.get(_url(copy)) - assert req.status == 200 + req = await locative_client.get(_url(copy)) + assert req.status == HTTP_OK # Test message, no location copy = data.copy() copy['trigger'] = 'test' del copy['id'] - req = yield from locative_client.get(_url(copy)) - assert req.status == 200 + req = await locative_client.get(_url(copy)) + assert req.status == HTTP_OK # Unknown trigger copy = data.copy() copy['trigger'] = 'foobar' - req = yield from locative_client.get(_url(copy)) - assert req.status == 422 + req = await locative_client.get(_url(copy)) + assert req.status == HTTP_UNPROCESSABLE_ENTITY -@asyncio.coroutine -def test_enter_and_exit(hass, locative_client): +async def test_enter_and_exit(hass, locative_client): """Test when there is a known zone.""" data = { 'latitude': 40.7855, @@ -103,9 +105,10 @@ def test_enter_and_exit(hass, locative_client): } # Enter the Home - req = yield from locative_client.get(_url(data)) - assert req.status == 200 - state_name = hass.states.get('{}.{}'.format('device_tracker', + req = await locative_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state assert 'home' == state_name @@ -113,9 +116,10 @@ def test_enter_and_exit(hass, locative_client): data['trigger'] = 'exit' # Exit Home - req = yield from locative_client.get(_url(data)) - assert req.status == 200 - state_name = hass.states.get('{}.{}'.format('device_tracker', + req = await locative_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state assert 'not_home' == state_name @@ -123,18 +127,20 @@ def test_enter_and_exit(hass, locative_client): data['trigger'] = 'enter' # Enter Home again - req = yield from locative_client.get(_url(data)) - assert req.status == 200 - state_name = hass.states.get('{}.{}'.format('device_tracker', + req = await locative_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state assert 'home' == state_name data['trigger'] = 'exit' # Exit Home - req = yield from locative_client.get(_url(data)) - assert req.status == 200 - state_name = hass.states.get('{}.{}'.format('device_tracker', + req = await locative_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state assert 'not_home' == state_name @@ -142,15 +148,15 @@ def test_enter_and_exit(hass, locative_client): data['trigger'] = 'enter' # Enter Work - req = yield from locative_client.get(_url(data)) - assert req.status == 200 - state_name = hass.states.get('{}.{}'.format('device_tracker', + req = await locative_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state assert 'work' == state_name -@asyncio.coroutine -def test_exit_after_enter(hass, locative_client): +async def test_exit_after_enter(hass, locative_client): """Test when an exit message comes after an enter message.""" data = { 'latitude': 40.7855, @@ -161,20 +167,22 @@ def test_exit_after_enter(hass, locative_client): } # Enter Home - req = yield from locative_client.get(_url(data)) - assert req.status == 200 + req = await locative_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK - state = hass.states.get('{}.{}'.format('device_tracker', + state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])) assert state.state == 'home' data['id'] = 'Work' # Enter Work - req = yield from locative_client.get(_url(data)) - assert req.status == 200 + req = await locative_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK - state = hass.states.get('{}.{}'.format('device_tracker', + state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])) assert state.state == 'work' @@ -182,16 +190,16 @@ def test_exit_after_enter(hass, locative_client): data['trigger'] = 'exit' # Exit Home - req = yield from locative_client.get(_url(data)) - assert req.status == 200 + req = await locative_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK - state = hass.states.get('{}.{}'.format('device_tracker', + state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])) assert state.state == 'work' -@asyncio.coroutine -def test_exit_first(hass, locative_client): +async def test_exit_first(hass, locative_client): """Test when an exit message is sent first on a new device.""" data = { 'latitude': 40.7855, @@ -202,9 +210,10 @@ def test_exit_first(hass, locative_client): } # Exit Home - req = yield from locative_client.get(_url(data)) - assert req.status == 200 + req = await locative_client.get(_url(data)) + await hass.async_block_till_done() + assert req.status == HTTP_OK - state = hass.states.get('{}.{}'.format('device_tracker', + state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])) assert state.state == 'not_home'