Switch OwnTracks HTTP to use webhook component (#17034)

* Update OwnTracks_HTTP to use the webhook component

* Update owntracks_http.py

* Update owntracks_http.py
This commit is contained in:
Georgi Kirichkov 2018-11-06 17:10:17 +02:00 committed by Paulus Schoutsen
parent 589764900a
commit eb385515c8
2 changed files with 150 additions and 59 deletions

View File

@ -4,52 +4,79 @@ Device tracker platform that adds support for OwnTracks over HTTP.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.owntracks_http/ https://home-assistant.io/components/device_tracker.owntracks_http/
""" """
import json
import logging
import re import re
from aiohttp.web_exceptions import HTTPInternalServerError from aiohttp.web import Response
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView
# pylint: disable=unused-import # pylint: disable=unused-import
from .owntracks import ( # NOQA from homeassistant.components.device_tracker.owntracks import ( # NOQA
REQUIREMENTS, PLATFORM_SCHEMA, context_from_config, async_handle_message) PLATFORM_SCHEMA, REQUIREMENTS, async_handle_message, context_from_config)
from homeassistant.const import CONF_WEBHOOK_ID
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['webhook']
DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__)
EVENT_RECEIVED = 'owntracks_http_webhook_received'
EVENT_RESPONSE = 'owntracks_http_webhook_response_'
DOMAIN = 'device_tracker.owntracks_http'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_WEBHOOK_ID): cv.string
})
async def async_setup_scanner(hass, config, async_see, discovery_info=None): async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up an OwnTracks tracker.""" """Set up OwnTracks HTTP component."""
context = context_from_config(async_see, config) context = context_from_config(async_see, config)
hass.http.register_view(OwnTracksView(context)) subscription = context.mqtt_topic
topic = re.sub('/#$', '', subscription)
return True async def handle_webhook(hass, webhook_id, request):
"""Handle webhook callback."""
headers = request.headers
data = dict()
if 'X-Limit-U' in headers:
data['user'] = headers['X-Limit-U']
elif 'u' in request.query:
data['user'] = request.query['u']
else:
return Response(
body=json.dumps({'error': 'You need to supply username.'}),
content_type="application/json"
)
class OwnTracksView(HomeAssistantView): if 'X-Limit-D' in headers:
"""View to handle OwnTracks HTTP requests.""" data['device'] = headers['X-Limit-D']
elif 'd' in request.query:
url = '/api/owntracks/{user}/{device}' data['device'] = request.query['d']
name = 'api:owntracks' else:
return Response(
def __init__(self, context): body=json.dumps({'error': 'You need to supply device name.'}),
"""Initialize OwnTracks URL endpoints.""" content_type="application/json"
self.context = context )
async def post(self, request, user, device):
"""Handle an OwnTracks message."""
hass = request.app['hass']
subscription = self.context.mqtt_topic
topic = re.sub('/#$', '', subscription)
message = await request.json() message = await request.json()
message['topic'] = '{}/{}/{}'.format(topic, user, device)
message['topic'] = '{}/{}/{}'.format(topic, data['user'],
data['device'])
try: try:
await async_handle_message(hass, self.context, message) await async_handle_message(hass, context, message)
return self.json([]) return Response(body=json.dumps([]), status=200,
content_type="application/json")
except ValueError: except ValueError:
raise HTTPInternalServerError _LOGGER.error("Received invalid JSON")
return None
hass.components.webhook.async_register(
'owntracks', 'OwnTracks', config['webhook_id'], handle_webhook)
return True

View File

@ -2,11 +2,47 @@
import asyncio import asyncio
from unittest.mock import patch from unittest.mock import patch
import os
import pytest import pytest
from homeassistant.components import device_tracker
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import mock_coro, mock_component from tests.common import mock_component, mock_coro
MINIMAL_LOCATION_MESSAGE = {
'_type': 'location',
'lon': 45,
'lat': 90,
'p': 101.3977584838867,
'tid': 'test',
'tst': 1,
}
LOCATION_MESSAGE = {
'_type': 'location',
'acc': 60,
'alt': 27,
'batt': 92,
'cog': 248,
'lon': 45,
'lat': 90,
'p': 101.3977584838867,
'tid': 'test',
't': 'u',
'tst': 1,
'vac': 4,
'vel': 0
}
@pytest.fixture(autouse=True)
def owntracks_http_cleanup(hass):
"""Remove known_devices.yaml."""
try:
os.remove(hass.config.path(device_tracker.YAML_DEVICES))
except OSError:
pass
@pytest.fixture @pytest.fixture
@ -19,42 +55,70 @@ def mock_client(hass, aiohttp_client):
hass.loop.run_until_complete( hass.loop.run_until_complete(
async_setup_component(hass, 'device_tracker', { async_setup_component(hass, 'device_tracker', {
'device_tracker': { 'device_tracker': {
'platform': 'owntracks_http' 'platform': 'owntracks_http',
'webhook_id': 'owntracks_test'
} }
})) }))
return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) return hass.loop.run_until_complete(aiohttp_client(hass.http.app))
@pytest.fixture
def mock_handle_message():
"""Mock async_handle_message."""
with patch('homeassistant.components.device_tracker.'
'owntracks_http.async_handle_message') as mock:
mock.return_value = mock_coro(None)
yield mock
@asyncio.coroutine @asyncio.coroutine
def test_forward_message_correctly(mock_client, mock_handle_message): def test_handle_valid_message(mock_client):
"""Test that we forward messages correctly to OwnTracks handle message.""" """Test that we forward messages correctly to OwnTracks."""
resp = yield from mock_client.post('/api/owntracks/user/device', json={ resp = yield from mock_client.post('/api/webhook/owntracks_test?'
'_type': 'test' 'u=test&d=test',
}) json=LOCATION_MESSAGE)
assert resp.status == 200 assert resp.status == 200
assert len(mock_handle_message.mock_calls) == 1
data = mock_handle_message.mock_calls[0][1][2] json = yield from resp.json()
assert data == { assert json == []
'_type': 'test',
'topic': 'owntracks/user/device'
}
@asyncio.coroutine @asyncio.coroutine
def test_handle_value_error(mock_client, mock_handle_message): def test_handle_valid_minimal_message(mock_client):
"""Test that we handle errors from handle message correctly.""" """Test that we forward messages correctly to OwnTracks."""
mock_handle_message.side_effect = ValueError resp = yield from mock_client.post('/api/webhook/owntracks_test?'
resp = yield from mock_client.post('/api/owntracks/user/device', json={ 'u=test&d=test',
'_type': 'test' json=MINIMAL_LOCATION_MESSAGE)
})
assert resp.status == 500 assert resp.status == 200
json = yield from resp.json()
assert json == []
@asyncio.coroutine
def test_handle_value_error(mock_client):
"""Test we don't disclose that this is a valid webhook."""
resp = yield from mock_client.post('/api/webhook/owntracks_test'
'?u=test&d=test', json='')
assert resp.status == 200
json = yield from resp.text()
assert json == ""
@asyncio.coroutine
def test_returns_error_missing_username(mock_client):
"""Test that an error is returned when username is missing."""
resp = yield from mock_client.post('/api/webhook/owntracks_test?d=test',
json=LOCATION_MESSAGE)
assert resp.status == 200
json = yield from resp.json()
assert json == {'error': 'You need to supply username.'}
@asyncio.coroutine
def test_returns_error_missing_device(mock_client):
"""Test that an error is returned when device name is missing."""
resp = yield from mock_client.post('/api/webhook/owntracks_test?u=test',
json=LOCATION_MESSAGE)
assert resp.status == 200
json = yield from resp.json()
assert json == {'error': 'You need to supply device name.'}