Switch OwnTracks HTTP to use webhook component (#17034)

* Update OwnTracks_HTTP to use the webhook component

* Update owntracks_http.py

* Update owntracks_http.py
pull/18272/head
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
https://home-assistant.io/components/device_tracker.owntracks_http/
"""
import json
import logging
import re
from aiohttp.web_exceptions import HTTPInternalServerError
from homeassistant.components.http import HomeAssistantView
from aiohttp.web import Response
import voluptuous as vol
# pylint: disable=unused-import
from .owntracks import ( # NOQA
REQUIREMENTS, PLATFORM_SCHEMA, context_from_config, async_handle_message)
from homeassistant.components.device_tracker.owntracks import ( # NOQA
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):
"""Set up an OwnTracks tracker."""
"""Set up OwnTracks HTTP component."""
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):
"""View to handle OwnTracks HTTP requests."""
url = '/api/owntracks/{user}/{device}'
name = 'api:owntracks'
def __init__(self, context):
"""Initialize OwnTracks URL endpoints."""
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)
if 'X-Limit-D' in headers:
data['device'] = headers['X-Limit-D']
elif 'd' in request.query:
data['device'] = request.query['d']
else:
return Response(
body=json.dumps({'error': 'You need to supply device name.'}),
content_type="application/json"
)
message = await request.json()
message['topic'] = '{}/{}/{}'.format(topic, user, device)
message['topic'] = '{}/{}/{}'.format(topic, data['user'],
data['device'])
try:
await async_handle_message(hass, self.context, message)
return self.json([])
await async_handle_message(hass, context, message)
return Response(body=json.dumps([]), status=200,
content_type="application/json")
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
from unittest.mock import patch
import os
import pytest
from homeassistant.components import device_tracker
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
@ -19,42 +55,70 @@ def mock_client(hass, aiohttp_client):
hass.loop.run_until_complete(
async_setup_component(hass, '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))
@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
def test_forward_message_correctly(mock_client, mock_handle_message):
"""Test that we forward messages correctly to OwnTracks handle message."""
resp = yield from mock_client.post('/api/owntracks/user/device', json={
'_type': 'test'
})
def test_handle_valid_message(mock_client):
"""Test that we forward messages correctly to OwnTracks."""
resp = yield from mock_client.post('/api/webhook/owntracks_test?'
'u=test&d=test',
json=LOCATION_MESSAGE)
assert resp.status == 200
assert len(mock_handle_message.mock_calls) == 1
data = mock_handle_message.mock_calls[0][1][2]
assert data == {
'_type': 'test',
'topic': 'owntracks/user/device'
}
json = yield from resp.json()
assert json == []
@asyncio.coroutine
def test_handle_value_error(mock_client, mock_handle_message):
"""Test that we handle errors from handle message correctly."""
mock_handle_message.side_effect = ValueError
resp = yield from mock_client.post('/api/owntracks/user/device', json={
'_type': 'test'
})
assert resp.status == 500
def test_handle_valid_minimal_message(mock_client):
"""Test that we forward messages correctly to OwnTracks."""
resp = yield from mock_client.post('/api/webhook/owntracks_test?'
'u=test&d=test',
json=MINIMAL_LOCATION_MESSAGE)
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.'}