Switch locative to use the webhook component

This commit is contained in:
Rohan Kapoor 2019-01-12 19:18:33 -08:00
parent 04636e9ba7
commit 4e020b90e1
5 changed files with 158 additions and 94 deletions

View File

@ -0,0 +1,18 @@
{
"config": {
"title": "Locative Webhook",
"step": {
"user": {
"title": "Set up the Locative Webhook",
"description": "Are you sure you want to set up the Locative Webhook?"
}
},
"abort": {
"one_instance_allowed": "Only a single instance is necessary.",
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency."
},
"create_entry": {
"default": "To send locations to Home Assistant, you will need to setup the webhook feature in the Locative app.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details."
}
}
}

View File

@ -8,51 +8,32 @@ import logging
from homeassistant.components.device_tracker import \ from homeassistant.components.device_tracker import \
DOMAIN as DEVICE_TRACKER_DOMAIN DOMAIN as DEVICE_TRACKER_DOMAIN
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, ATTR_LATITUDE, \ from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, ATTR_LATITUDE, \
ATTR_LONGITUDE, STATE_NOT_HOME ATTR_LONGITUDE, STATE_NOT_HOME, CONF_WEBHOOK_ID
from homeassistant.helpers import config_entry_flow
from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'locative' DOMAIN = 'locative'
DEPENDENCIES = ['http'] DEPENDENCIES = ['webhook']
URL = '/api/locative'
TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN) TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN)
async def async_setup(hass, hass_config): async def async_setup(hass, hass_config):
"""Set up the Locative component.""" """Set up the Locative component."""
hass.http.register_view(LocativeView)
hass.async_create_task( hass.async_create_task(
async_load_platform(hass, 'device_tracker', DOMAIN, {}, hass_config) async_load_platform(hass, 'device_tracker', DOMAIN, {}, hass_config)
) )
return True return True
class LocativeView(HomeAssistantView): async def handle_webhook(hass, webhook_id, request):
"""View to handle Locative requests.""" """Handle incoming webhook from Locative."""
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() 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: if 'latitude' not in data or 'longitude' not in data:
return ('Latitude and longitude not specified.', return ('Latitude and longitude not specified.',
HTTP_UNPROCESSABLE_ENTITY) HTTP_UNPROCESSABLE_ENTITY)
@ -118,3 +99,24 @@ class LocativeView(HomeAssistantView):
direction) direction)
return ('Received unidentified message: {}'.format(direction), return ('Received unidentified message: {}'.format(direction),
HTTP_UNPROCESSABLE_ENTITY) HTTP_UNPROCESSABLE_ENTITY)
async def async_setup_entry(hass, entry):
"""Configure based on config entry."""
hass.components.webhook.async_register(
DOMAIN, 'Locative', entry.data[CONF_WEBHOOK_ID], handle_webhook)
return True
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
return True
config_entry_flow.register_webhook_flow(
DOMAIN,
'Locative Webhook',
{
'docs_url': 'https://www.home-assistant.io/components/locative/'
}
)

View File

@ -0,0 +1,18 @@
{
"config": {
"title": "Locative Webhook",
"step": {
"user": {
"title": "Set up the Locative Webhook",
"description": "Are you sure you want to set up the Locative Webhook?"
}
},
"abort": {
"one_instance_allowed": "Only a single instance is necessary.",
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Geofency."
},
"create_entry": {
"default": "To send locations to Home Assistant, you will need to setup the webhook feature in the Locative app.\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nSee [the documentation]({docs_url}) for further details."
}
}
}

View File

@ -148,6 +148,7 @@ FLOWS = [
'ifttt', 'ifttt',
'ios', 'ios',
'lifx', 'lifx',
'locative',
'luftdaten', 'luftdaten',
'mailgun', 'mailgun',
'mqtt', 'mqtt',

View File

@ -1,11 +1,12 @@
"""The tests the for Locative device tracker platform.""" """The tests the for Locative device tracker platform."""
from unittest.mock import patch from unittest.mock import patch, Mock
import pytest import pytest
from homeassistant import data_entry_flow
from homeassistant.components.device_tracker import \ from homeassistant.components.device_tracker import \
DOMAIN as DEVICE_TRACKER_DOMAIN DOMAIN as DEVICE_TRACKER_DOMAIN
from homeassistant.components.locative import URL, DOMAIN from homeassistant.components.locative import DOMAIN
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -36,8 +37,26 @@ def locative_client(loop, hass, hass_client):
yield loop.run_until_complete(hass_client()) yield loop.run_until_complete(hass_client())
async def test_missing_data(locative_client): @pytest.fixture
async def webhook_id(hass, locative_client):
"""Initialize the Geofency component and get the webhook_id."""
hass.config.api = Mock(base_url='http://example.com')
result = await hass.config_entries.flow.async_init('locative', context={
'source': 'user'
})
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM, result
result = await hass.config_entries.flow.async_configure(
result['flow_id'], {})
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
return result['result'].data['webhook_id']
async def test_missing_data(locative_client, webhook_id):
"""Test missing data.""" """Test missing data."""
url = '/api/webhook/{}'.format(webhook_id)
data = { data = {
'latitude': 1.0, 'latitude': 1.0,
'longitude': 1.1, 'longitude': 1.1,
@ -47,55 +66,57 @@ async def test_missing_data(locative_client):
} }
# No data # No data
req = await locative_client.get(_url({})) req = await locative_client.post(url)
assert req.status == HTTP_UNPROCESSABLE_ENTITY assert req.status == HTTP_UNPROCESSABLE_ENTITY
# No latitude # No latitude
copy = data.copy() copy = data.copy()
del copy['latitude'] del copy['latitude']
req = await locative_client.get(_url(copy)) req = await locative_client.post(url, data=copy)
assert req.status == HTTP_UNPROCESSABLE_ENTITY assert req.status == HTTP_UNPROCESSABLE_ENTITY
# No device # No device
copy = data.copy() copy = data.copy()
del copy['device'] del copy['device']
req = await locative_client.get(_url(copy)) req = await locative_client.post(url, data=copy)
assert req.status == HTTP_UNPROCESSABLE_ENTITY assert req.status == HTTP_UNPROCESSABLE_ENTITY
# No location # No location
copy = data.copy() copy = data.copy()
del copy['id'] del copy['id']
req = await locative_client.get(_url(copy)) req = await locative_client.post(url, data=copy)
assert req.status == HTTP_UNPROCESSABLE_ENTITY assert req.status == HTTP_UNPROCESSABLE_ENTITY
# No trigger # No trigger
copy = data.copy() copy = data.copy()
del copy['trigger'] del copy['trigger']
req = await locative_client.get(_url(copy)) req = await locative_client.post(url, data=copy)
assert req.status == HTTP_UNPROCESSABLE_ENTITY assert req.status == HTTP_UNPROCESSABLE_ENTITY
# Test message # Test message
copy = data.copy() copy = data.copy()
copy['trigger'] = 'test' copy['trigger'] = 'test'
req = await locative_client.get(_url(copy)) req = await locative_client.post(url, data=copy)
assert req.status == HTTP_OK assert req.status == HTTP_OK
# Test message, no location # Test message, no location
copy = data.copy() copy = data.copy()
copy['trigger'] = 'test' copy['trigger'] = 'test'
del copy['id'] del copy['id']
req = await locative_client.get(_url(copy)) req = await locative_client.post(url, data=copy)
assert req.status == HTTP_OK assert req.status == HTTP_OK
# Unknown trigger # Unknown trigger
copy = data.copy() copy = data.copy()
copy['trigger'] = 'foobar' copy['trigger'] = 'foobar'
req = await locative_client.get(_url(copy)) req = await locative_client.post(url, data=copy)
assert req.status == HTTP_UNPROCESSABLE_ENTITY assert req.status == HTTP_UNPROCESSABLE_ENTITY
async def test_enter_and_exit(hass, locative_client): async def test_enter_and_exit(hass, locative_client, webhook_id):
"""Test when there is a known zone.""" """Test when there is a known zone."""
url = '/api/webhook/{}'.format(webhook_id)
data = { data = {
'latitude': 40.7855, 'latitude': 40.7855,
'longitude': -111.7367, 'longitude': -111.7367,
@ -105,7 +126,7 @@ async def test_enter_and_exit(hass, locative_client):
} }
# Enter the Home # Enter the Home
req = await locative_client.get(_url(data)) req = await locative_client.post(url, data=data)
await hass.async_block_till_done() await hass.async_block_till_done()
assert req.status == HTTP_OK assert req.status == HTTP_OK
state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN,
@ -116,7 +137,7 @@ async def test_enter_and_exit(hass, locative_client):
data['trigger'] = 'exit' data['trigger'] = 'exit'
# Exit Home # Exit Home
req = await locative_client.get(_url(data)) req = await locative_client.post(url, data=data)
await hass.async_block_till_done() await hass.async_block_till_done()
assert req.status == HTTP_OK assert req.status == HTTP_OK
state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN,
@ -127,7 +148,7 @@ async def test_enter_and_exit(hass, locative_client):
data['trigger'] = 'enter' data['trigger'] = 'enter'
# Enter Home again # Enter Home again
req = await locative_client.get(_url(data)) req = await locative_client.post(url, data=data)
await hass.async_block_till_done() await hass.async_block_till_done()
assert req.status == HTTP_OK assert req.status == HTTP_OK
state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN,
@ -137,7 +158,7 @@ async def test_enter_and_exit(hass, locative_client):
data['trigger'] = 'exit' data['trigger'] = 'exit'
# Exit Home # Exit Home
req = await locative_client.get(_url(data)) req = await locative_client.post(url, data=data)
await hass.async_block_till_done() await hass.async_block_till_done()
assert req.status == HTTP_OK assert req.status == HTTP_OK
state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN,
@ -148,7 +169,7 @@ async def test_enter_and_exit(hass, locative_client):
data['trigger'] = 'enter' data['trigger'] = 'enter'
# Enter Work # Enter Work
req = await locative_client.get(_url(data)) req = await locative_client.post(url, data=data)
await hass.async_block_till_done() await hass.async_block_till_done()
assert req.status == HTTP_OK assert req.status == HTTP_OK
state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN,
@ -156,8 +177,10 @@ async def test_enter_and_exit(hass, locative_client):
assert 'work' == state_name assert 'work' == state_name
async def test_exit_after_enter(hass, locative_client): async def test_exit_after_enter(hass, locative_client, webhook_id):
"""Test when an exit message comes after an enter message.""" """Test when an exit message comes after an enter message."""
url = '/api/webhook/{}'.format(webhook_id)
data = { data = {
'latitude': 40.7855, 'latitude': 40.7855,
'longitude': -111.7367, 'longitude': -111.7367,
@ -167,7 +190,7 @@ async def test_exit_after_enter(hass, locative_client):
} }
# Enter Home # Enter Home
req = await locative_client.get(_url(data)) req = await locative_client.post(url, data=data)
await hass.async_block_till_done() await hass.async_block_till_done()
assert req.status == HTTP_OK assert req.status == HTTP_OK
@ -178,7 +201,7 @@ async def test_exit_after_enter(hass, locative_client):
data['id'] = 'Work' data['id'] = 'Work'
# Enter Work # Enter Work
req = await locative_client.get(_url(data)) req = await locative_client.post(url, data=data)
await hass.async_block_till_done() await hass.async_block_till_done()
assert req.status == HTTP_OK assert req.status == HTTP_OK
@ -190,7 +213,7 @@ async def test_exit_after_enter(hass, locative_client):
data['trigger'] = 'exit' data['trigger'] = 'exit'
# Exit Home # Exit Home
req = await locative_client.get(_url(data)) req = await locative_client.post(url, data=data)
await hass.async_block_till_done() await hass.async_block_till_done()
assert req.status == HTTP_OK assert req.status == HTTP_OK
@ -199,8 +222,10 @@ async def test_exit_after_enter(hass, locative_client):
assert state.state == 'work' assert state.state == 'work'
async def test_exit_first(hass, locative_client): async def test_exit_first(hass, locative_client, webhook_id):
"""Test when an exit message is sent first on a new device.""" """Test when an exit message is sent first on a new device."""
url = '/api/webhook/{}'.format(webhook_id)
data = { data = {
'latitude': 40.7855, 'latitude': 40.7855,
'longitude': -111.7367, 'longitude': -111.7367,
@ -210,7 +235,7 @@ async def test_exit_first(hass, locative_client):
} }
# Exit Home # Exit Home
req = await locative_client.get(_url(data)) req = await locative_client.post(url, data=data)
await hass.async_block_till_done() await hass.async_block_till_done()
assert req.status == HTTP_OK assert req.status == HTTP_OK