forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Meraki AP Device tracker (home-assistant#10971)
* 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
1 parent
c13b510
commit e66268d
Showing
2 changed files
with
255 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |