Skip to content

Commit

Permalink
Meraki AP Device tracker (home-assistant#10971)
Browse files Browse the repository at this point in the history
* Device tracker for meraki AP

* styles fix

* fix again

* again

* and again :)

* fix hide if away

* docs and optimization

* tests and fixes

* styles

* styles

* styles

* styles

* styles fix. Hope last

* clear track new

* changes

* fix accuracy error and requested changes

* remove meraki from .coveragerc

* tests and minor changes

* remove location
  • Loading branch information
masarliev authored and MartinHjelmare committed Dec 6, 2017
1 parent c13b510 commit e66268d
Show file tree
Hide file tree
Showing 2 changed files with 255 additions and 0 deletions.
116 changes: 116 additions & 0 deletions homeassistant/components/device_tracker/meraki.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""
Support for the Meraki CMX location service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.meraki/
"""
import asyncio
import logging
import json

import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (HTTP_BAD_REQUEST, HTTP_UNPROCESSABLE_ENTITY)
from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, SOURCE_TYPE_ROUTER)

CONF_VALIDATOR = 'validator'
CONF_SECRET = 'secret'
DEPENDENCIES = ['http']
URL = '/api/meraki'
VERSION = '2.0'


_LOGGER = logging.getLogger(__name__)


PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_VALIDATOR): cv.string,
vol.Required(CONF_SECRET): cv.string
})


@asyncio.coroutine
def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up an endpoint for the Meraki tracker."""
hass.http.register_view(
MerakiView(config, async_see))

return True


class MerakiView(HomeAssistantView):
"""View to handle Meraki requests."""

url = URL
name = 'api:meraki'

def __init__(self, config, async_see):
"""Initialize Meraki URL endpoints."""
self.async_see = async_see
self.validator = config[CONF_VALIDATOR]
self.secret = config[CONF_SECRET]

@asyncio.coroutine
def get(self, request):
"""Meraki message received as GET."""
return self.validator

@asyncio.coroutine
def post(self, request):
"""Meraki CMX message received."""
try:
data = yield from request.json()
except ValueError:
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
_LOGGER.debug("Meraki Data from Post: %s", json.dumps(data))
if not data.get('secret', False):
_LOGGER.error("secret invalid")
return self.json_message('No secret', HTTP_UNPROCESSABLE_ENTITY)
if data['secret'] != self.secret:
_LOGGER.error("Invalid Secret received from Meraki")
return self.json_message('Invalid secret',
HTTP_UNPROCESSABLE_ENTITY)
elif data['version'] != VERSION:
_LOGGER.error("Invalid API version: %s", data['version'])
return self.json_message('Invalid version',
HTTP_UNPROCESSABLE_ENTITY)
else:
_LOGGER.debug('Valid Secret')
if data['type'] not in ('DevicesSeen', 'BluetoothDevicesSeen'):
_LOGGER.error("Unknown Device %s", data['type'])
return self.json_message('Invalid device type',
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.debug("Processing %s", data['type'])
if len(data["data"]["observations"]) == 0:
_LOGGER.debug("No observations found")
return
self._handle(request.app['hass'], data)

@callback
def _handle(self, hass, data):
for i in data["data"]["observations"]:
data["data"]["secret"] = "hidden"
mac = i["clientMac"]
_LOGGER.debug("clientMac: %s", mac)
attrs = {}
if i.get('os', False):
attrs['os'] = i['os']
if i.get('manufacturer', False):
attrs['manufacturer'] = i['manufacturer']
if i.get('ipv4', False):
attrs['ipv4'] = i['ipv4']
if i.get('ipv6', False):
attrs['ipv6'] = i['ipv6']
if i.get('seenTime', False):
attrs['seenTime'] = i['seenTime']
if i.get('ssid', False):
attrs['ssid'] = i['ssid']
hass.async_add_job(self.async_see(
mac=mac,
source_type=SOURCE_TYPE_ROUTER,
attributes=attrs
))
139 changes: 139 additions & 0 deletions tests/components/device_tracker/test_meraki.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""The tests the for Meraki device tracker."""
import asyncio
import json
from unittest.mock import patch
import pytest
from homeassistant.components.device_tracker.meraki import (
CONF_VALIDATOR, CONF_SECRET)
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.meraki import URL


@pytest.fixture
def meraki_client(loop, hass, test_client):
"""Meraki mock client."""
assert loop.run_until_complete(async_setup_component(
hass, device_tracker.DOMAIN, {
device_tracker.DOMAIN: {
CONF_PLATFORM: 'meraki',
CONF_VALIDATOR: 'validator',
CONF_SECRET: 'secret'

}
}))

with patch('homeassistant.components.device_tracker.update_config'):
yield loop.run_until_complete(test_client(hass.http.app))


@asyncio.coroutine
def test_invalid_or_missing_data(meraki_client):
"""Test validator with invalid or missing data."""
req = yield from meraki_client.get(URL)
text = yield from req.text()
assert req.status == 200
assert text == 'validator'

req = yield from meraki_client.post(URL, data=b"invalid")
text = yield from req.json()
assert req.status == 400
assert text['message'] == 'Invalid JSON'

req = yield from meraki_client.post(URL, data=b"{}")
text = yield from req.json()
assert req.status == 422
assert text['message'] == 'No secret'

data = {
"version": "1.0",
"secret": "secret"
}
req = yield from meraki_client.post(URL, data=json.dumps(data))
text = yield from req.json()
assert req.status == 422
assert text['message'] == 'Invalid version'

data = {
"version": "2.0",
"secret": "invalid"
}
req = yield from meraki_client.post(URL, data=json.dumps(data))
text = yield from req.json()
assert req.status == 422
assert text['message'] == 'Invalid secret'

data = {
"version": "2.0",
"secret": "secret",
"type": "InvalidType"
}
req = yield from meraki_client.post(URL, data=json.dumps(data))
text = yield from req.json()
assert req.status == 422
assert text['message'] == 'Invalid device type'

data = {
"version": "2.0",
"secret": "secret",
"type": "BluetoothDevicesSeen",
"data": {
"observations": []
}
}
req = yield from meraki_client.post(URL, data=json.dumps(data))
assert req.status == 200


@asyncio.coroutine
def test_data_will_be_saved(hass, meraki_client):
"""Test with valid data."""
data = {
"version": "2.0",
"secret": "secret",
"type": "DevicesSeen",
"data": {
"observations": [
{
"location": {
"lat": "51.5355157",
"lng": "21.0699035",
"unc": "46.3610585",
},
"seenTime": "2016-09-12T16:23:13Z",
"ssid": 'ssid',
"os": 'HA',
"ipv6": '2607:f0d0:1002:51::4/64',
"clientMac": "00:26:ab:b8:a9:a4",
"seenEpoch": "147369739",
"rssi": "20",
"manufacturer": "Seiko Epson"
},
{
"location": {
"lat": "51.5355357",
"lng": "21.0699635",
"unc": "46.3610585",
},
"seenTime": "2016-09-12T16:21:13Z",
"ssid": 'ssid',
"os": 'HA',
"ipv4": '192.168.0.1',
"clientMac": "00:26:ab:b8:a9:a5",
"seenEpoch": "147369750",
"rssi": "20",
"manufacturer": "Seiko Epson"
}
]
}
}
req = yield from meraki_client.post(URL, data=json.dumps(data))
assert req.status == 200
state_name = hass.states.get('{}.{}'.format('device_tracker',
'0026abb8a9a4')).state
assert 'home' == state_name

state_name = hass.states.get('{}.{}'.format('device_tracker',
'0026abb8a9a5')).state
assert 'home' == state_name

0 comments on commit e66268d

Please sign in to comment.