Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,6 @@ omit =
homeassistant/components/device_tracker/keenetic_ndms2.py
homeassistant/components/device_tracker/linksys_ap.py
homeassistant/components/device_tracker/linksys_smart.py
homeassistant/components/device_tracker/luci.py
homeassistant/components/device_tracker/mikrotik.py
homeassistant/components/device_tracker/netgear.py
homeassistant/components/device_tracker/nmap_tracker.py
Expand Down
67 changes: 59 additions & 8 deletions homeassistant/components/device_tracker/luci.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ class InvalidLuciTokenError(HomeAssistantError):
pass


class LuciRpcMethodNotFoundError(HomeAssistantError):
"""When an invalid method is called."""

pass


def get_scanner(hass, config):
"""Validate the configuration and return a Luci scanner."""
scanner = LuciDeviceScanner(config[DOMAIN])
Expand All @@ -60,6 +66,7 @@ def __init__(self, config):
self.refresh_token()
self.mac2name = None
self.success_init = self.token is not None
self.use_ip_neighbors_table = False

def refresh_token(self):
"""Get a new token."""
Expand Down Expand Up @@ -98,11 +105,28 @@ def _update_info(self):

_LOGGER.info("Checking ARP")

url = '{}/cgi-bin/luci/rpc/sys'.format(self.origin)
sys_url = '{}/cgi-bin/luci/rpc/sys'.format(self.origin)
ip_url = '{}/cgi-bin/luci/rpc/ip'.format(self.origin)

try:
result = _req_json_rpc(
url, 'net.arptable', params={'auth': self.token})
if self.use_ip_neighbors_table:
# Newer OpenWRT releases (18.06+)
result = _req_json_rpc(
ip_url, 'neighbors', {"family": 4},
params={'auth': self.token})
else:
try:
# Older OpenWRT releases (pre-18.06)
result = _req_json_rpc(
sys_url, 'net.arptable', params={
'auth': self.token})
except LuciRpcMethodNotFoundError:
# This is normal for newer OpenWRT (18.06+)
_LOGGER.error('method net.arptable not found. '
'Will try ip neighbors instead')
self.use_ip_neighbors_table = True
self._update_info()
return False
except InvalidLuciTokenError:
_LOGGER.info("Refreshing token")
self.refresh_token()
Expand All @@ -111,10 +135,24 @@ def _update_info(self):
if result:
self.last_results = []
for device_entry in result:
# Check if the Flags for each device contain
# NUD_REACHABLE and if so, add it to last_results
if int(device_entry['Flags'], 16) & 0x2:
self.last_results.append(device_entry['HW address'])
_LOGGER.debug('device_entry. %s', device_entry)

if "Flags" in device_entry:
# Older OpenWRT releases (pre-18.06)
#
# Check if the Flags for each device contain
# NUD_REACHABLE and if so, add it to last_results
if device_entry['Flags'] == "0x2":
self.last_results.append(device_entry['HW address'])
elif "mac" in device_entry:
# Newer OpenWRT releases (18.06+)
#
# Do not use `reachable` or `stale` values
# as they flap constantly even
# when the device is inside the network.
# The very existence of the mac in the results
# is enough to determine the "device is home"
self.last_results.append(device_entry['mac'])

return True

Expand All @@ -138,7 +176,20 @@ def _req_json_rpc(url, method, *args, **kwargs):
_LOGGER.exception("Failed to parse response from luci")
return
try:
return result['result']
result_value = result['result']

if result_value is not None:
return result_value

# On 18.06, we want to check for error 'Method not Found'
error_message = result['error']['message']
error_code = result['error']['code']
_LOGGER.error(
"method: '%s' returned an "
"error '%s' (code: '%s).",
method, error_message, error_code)
if error_code == -32601:
raise LuciRpcMethodNotFoundError
except KeyError:
_LOGGER.exception("No result in response from luci")
return
Expand Down
109 changes: 109 additions & 0 deletions tests/components/device_tracker/test_luci.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""The tests for Efergy sensor platform."""
import unittest
import requests_mock
from homeassistant.components import device_tracker

from homeassistant.setup import setup_component
from homeassistant.const import (
CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_SSL)

from tests.common import load_fixture, get_test_home_assistant

TOKEN = 'bf13be9ca4cea446c49410963492282a'

LUCI_CONFIG = {
'platform': 'luci',
CONF_HOST: "127.0.0.1",
CONF_USERNAME: 'blahuser',
CONF_PASSWORD: "blahpass",
CONF_SSL: False
}


def mock_responses_version_pre_18(mock):
"""Mock responses for Luci RPC version pre-18."""
base_url = 'http://{}'.format(LUCI_CONFIG[CONF_HOST])
mock.post(
'{}/cgi-bin/luci/rpc/auth'.format(base_url),
text=load_fixture('luci_auth.json'))
mock.post(
'{}/cgi-bin/luci/rpc/sys?auth={}'.format(base_url, TOKEN),
text=load_fixture('luci_arptable_legacy.json'))
mock.post(
'{}/cgi-bin/luci/rpc/uci?auth={}'.format(base_url, TOKEN),
text=load_fixture('luci_dhcp.json'))


def mock_responses_version_v18(mock):
"""Mock responses for Luci RPC version v18."""
base_url = 'http://{}'.format(LUCI_CONFIG[CONF_HOST])
mock.post(
'{}/cgi-bin/luci/rpc/auth'.format(base_url),
text=load_fixture('luci_auth.json'))
mock.post(
'{}/cgi-bin/luci/rpc/sys?auth={}'.format(base_url, TOKEN),
text=load_fixture('luci_arptable_v18.json'))
mock.post(
'{}/cgi-bin/luci/rpc/ip?auth={}'.format(base_url, TOKEN),
text=load_fixture('luci_neighbors.json'))
mock.post(
'{}/cgi-bin/luci/rpc/uci?auth={}'.format(base_url, TOKEN),
text=load_fixture('luci_dhcp.json'))


class TestLuciDeviceScanner(unittest.TestCase):
"""Tests the Luci device scanner platform."""

DEVICES = []

def setUp(self):
"""Initialize values for this test case class."""
self.hass = get_test_home_assistant()
self.config = LUCI_CONFIG

def tearDown(self): # pylint: disable=invalid-name
"""Stop everything that was started."""
self.hass.stop()

def setup_luci_component(self):
"""Set up with basic config."""
self.assertTrue(
setup_component(self.hass,
device_tracker.DOMAIN, {
device_tracker.DOMAIN: LUCI_CONFIG
}))
self.hass.block_till_done()

@requests_mock.Mocker()
def test_pre_v18_success(self, mock):
"""Test for platform on pre-v18."""
mock_responses_version_pre_18(mock)

self.setup_luci_component()

self.assertEqual('home', self.hass.states.get(
'device_tracker.raspberrypi3').state)
self.assertIsNone(self.hass.states.get(
'device_tracker.b8e8995c6e12'))
self.assertIsNone(self.hass.states.get(
'device_tracker.b8e8995c6e10'))

self.assertEqual('home', self.hass.states.get(
'device_tracker.b8e8995c6e11').state)

@requests_mock.Mocker()
def test_v18_or_newer_success(self, mock):
"""Test for platform on v18.06+."""
mock_responses_version_v18(mock)

self.setup_luci_component()

self.assertIsNone(self.hass.states.get(
'device_tracker.188b400814b2'))
self.assertEqual('home', self.hass.states.get(
'device_tracker.mydevicename').state)

self.assertEqual('home', self.hass.states.get(
'device_tracker.b8e8995c6e15').state)
self.assertEqual('home', self.hass.states.get(
'device_tracker.b827eb117c22').state)
30 changes: 30 additions & 0 deletions tests/fixtures/luci_arptable_legacy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"id": null,
"result": [
{
"IP address": "10.10.10.249",
"HW address": "B8:E8:99:5C:6E:10",
"HW type": "0x1",
"Flags": "0x6",
"Mask": "*",
"Device": "br-lan"
},
{
"IP address": "10.10.10.250",
"HW address": "B8:E8:99:5C:6E:11",
"HW type": "0x1",
"Flags": "0x2",
"Mask": "*",
"Device": "br-lan"
},
{
"IP address": "10.10.10.251",
"HW address": "B8:E8:99:5C:6E:12",
"HW type": "0x1",
"Flags": "0x2",
"Mask": "*",
"Device": "br-lan"
}
],
"error": null
}
8 changes: 8 additions & 0 deletions tests/fixtures/luci_arptable_v18.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"id": null,
"result": null,
"error": {
"message": "Method not found.",
"code": -32601
}
}
5 changes: 5 additions & 0 deletions tests/fixtures/luci_auth.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"id": null,
"result": "bf13be9ca4cea446c49410963492282a",
"error": null
}
26 changes: 26 additions & 0 deletions tests/fixtures/luci_dhcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"id": null,
"result": {
"cfg21fe63": {
".name": "cfg21fe63",
".type": "host",
"name": "raspberrypi3",
".index": 32,
"mac": "b8:e8:99:5c:6e:12",
"dns": "1",
".anonymous": true,
"ip": "10.10.10.251"
},
"cfg21fe62": {
".name": "cfg21fe62",
".type": "host",
"name": "mydevicename",
".index": 32,
"mac": "18:8b:40:08:14:b2",
"dns": "1",
".anonymous": true,
"ip": "10.10.10.25"
}
},
"error": null
}
Loading