diff --git a/homeassistant/components/device_tracker/unifi_direct.py b/homeassistant/components/device_tracker/unifi_direct.py new file mode 100644 index 0000000000000000000000000000000000000000..57a0186a2e215aae9a95490a8b7339c5ab997c9b --- /dev/null +++ b/homeassistant/components/device_tracker/unifi_direct.py @@ -0,0 +1,134 @@ +""" +Support for Unifi AP direct access. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.unifi_direct/ +""" +import logging +import json + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import ( + DOMAIN, PLATFORM_SCHEMA, DeviceScanner) +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_USERNAME, + CONF_PORT) + +REQUIREMENTS = ['pexpect==4.0.1'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_SSH_PORT = 22 +UNIFI_COMMAND = 'mca-dump | tr -d "\n"' +UNIFI_SSID_TABLE = "vap_table" +UNIFI_CLIENT_TABLE = "sta_table" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port +}) + + +# pylint: disable=unused-argument +def get_scanner(hass, config): + """Validate the configuration and return a Unifi direct scanner.""" + scanner = UnifiDeviceScanner(config[DOMAIN]) + if not scanner.connected: + return False + return scanner + + +class UnifiDeviceScanner(DeviceScanner): + """This class queries Unifi wireless access point.""" + + def __init__(self, config): + """Initialize the scanner.""" + self.host = config[CONF_HOST] + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] + self.port = config[CONF_PORT] + self.ssh = None + self.connected = False + self.last_results = {} + self._connect() + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + result = _response_to_json(self._get_update()) + if result: + self.last_results = result + return self.last_results.keys() + + def get_device_name(self, device): + """Return the name of the given device or None if we don't know.""" + hostname = next(( + value.get('hostname') for key, value in self.last_results.items() + if key.upper() == device.upper()), None) + if hostname is not None: + hostname = str(hostname) + return hostname + + def _connect(self): + """Connect to the Unifi AP SSH server.""" + from pexpect import pxssh, exceptions + + self.ssh = pxssh.pxssh() + try: + self.ssh.login(self.host, self.username, + password=self.password, port=self.port) + self.connected = True + except exceptions.EOF: + _LOGGER.error("Connection refused. SSH enabled?") + self._disconnect() + + def _disconnect(self): + """Disconnect the current SSH connection.""" + # pylint: disable=broad-except + try: + self.ssh.logout() + except Exception: + pass + finally: + self.ssh = None + + self.connected = False + + def _get_update(self): + from pexpect import pxssh + + try: + if not self.connected: + self._connect() + self.ssh.sendline(UNIFI_COMMAND) + self.ssh.prompt() + return self.ssh.before + except pxssh.ExceptionPxssh as err: + _LOGGER.error("Unexpected SSH error: %s", str(err)) + self._disconnect() + return None + except AssertionError as err: + _LOGGER.error("Connection to AP unavailable: %s", str(err)) + self._disconnect() + return None + + +def _response_to_json(response): + try: + json_response = json.loads(str(response)[31:-1].replace("\\", "")) + _LOGGER.debug(str(json_response)) + ssid_table = json_response.get(UNIFI_SSID_TABLE) + active_clients = {} + + for ssid in ssid_table: + client_table = ssid.get(UNIFI_CLIENT_TABLE) + for client in client_table: + active_clients[client.get("mac")] = client + + return active_clients + except ValueError: + _LOGGER.error("Failed to decode response from AP.") + return {} diff --git a/requirements_all.txt b/requirements_all.txt index 5ad07d15ef7838925cda064c644a99f495ea931a..39136d59db464eac07dcca2dbf40eac9cf6b9fb8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -523,6 +523,7 @@ pdunehd==1.3 # homeassistant.components.device_tracker.aruba # homeassistant.components.device_tracker.asuswrt # homeassistant.components.device_tracker.cisco_ios +# homeassistant.components.device_tracker.unifi_direct # homeassistant.components.media_player.pandora pexpect==4.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 32c2feb7427cc498be75571aa391be576c8c5e26..4331b7d11e6882f22ced86a2dee2dc98fb8dad30 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -101,6 +101,7 @@ paho-mqtt==1.3.1 # homeassistant.components.device_tracker.aruba # homeassistant.components.device_tracker.asuswrt # homeassistant.components.device_tracker.cisco_ios +# homeassistant.components.device_tracker.unifi_direct # homeassistant.components.media_player.pandora pexpect==4.0.1 diff --git a/tests/components/device_tracker/test_unifi_direct.py b/tests/components/device_tracker/test_unifi_direct.py new file mode 100644 index 0000000000000000000000000000000000000000..0e22758d07ecb847dbdc17e82988947adfdf6762 --- /dev/null +++ b/tests/components/device_tracker/test_unifi_direct.py @@ -0,0 +1,172 @@ +"""The tests for the Unifi direct device tracker platform.""" +import os +from datetime import timedelta +import unittest +from unittest import mock +from unittest.mock import patch + +import pytest +import voluptuous as vol + +from homeassistant.setup import setup_component +from homeassistant.components import device_tracker +from homeassistant.components.device_tracker import ( + CONF_CONSIDER_HOME, CONF_TRACK_NEW) +from homeassistant.components.device_tracker.unifi_direct import ( + DOMAIN, CONF_PORT, PLATFORM_SCHEMA, _response_to_json, get_scanner) +from homeassistant.const import (CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME, + CONF_HOST) + +from tests.common import ( + get_test_home_assistant, assert_setup_component, + mock_component, load_fixture) + + +class TestComponentsDeviceTrackerUnifiDirect(unittest.TestCase): + """Tests for the Unifi direct device tracker platform.""" + + hass = None + scanner_path = 'homeassistant.components.device_tracker.' + \ + 'unifi_direct.UnifiDeviceScanner' + + def setup_method(self, _): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + mock_component(self.hass, 'zone') + + def teardown_method(self, _): + """Stop everything that was started.""" + self.hass.stop() + try: + os.remove(self.hass.config.path(device_tracker.YAML_DEVICES)) + except FileNotFoundError: + pass + + @mock.patch(scanner_path, + return_value=mock.MagicMock()) + def test_get_scanner(self, unifi_mock): \ + # pylint: disable=invalid-name + """Test creating an Unifi direct scanner with a password.""" + conf_dict = { + DOMAIN: { + CONF_PLATFORM: 'unifi_direct', + CONF_HOST: 'fake_host', + CONF_USERNAME: 'fake_user', + CONF_PASSWORD: 'fake_pass', + CONF_TRACK_NEW: True, + CONF_CONSIDER_HOME: timedelta(seconds=180) + } + } + + with assert_setup_component(1, DOMAIN): + assert setup_component(self.hass, DOMAIN, conf_dict) + + conf_dict[DOMAIN][CONF_PORT] = 22 + self.assertEqual(unifi_mock.call_args, mock.call(conf_dict[DOMAIN])) + + @patch('pexpect.pxssh.pxssh') + def test_get_device_name(self, mock_ssh): + """"Testing MAC matching.""" + conf_dict = { + DOMAIN: { + CONF_PLATFORM: 'unifi_direct', + CONF_HOST: 'fake_host', + CONF_USERNAME: 'fake_user', + CONF_PASSWORD: 'fake_pass', + CONF_PORT: 22, + CONF_TRACK_NEW: True, + CONF_CONSIDER_HOME: timedelta(seconds=180) + } + } + mock_ssh.return_value.before = load_fixture('unifi_direct.txt') + scanner = get_scanner(self.hass, conf_dict) + devices = scanner.scan_devices() + self.assertEqual(23, len(devices)) + self.assertEqual("iPhone", + scanner.get_device_name("98:00:c6:56:34:12")) + self.assertEqual("iPhone", + scanner.get_device_name("98:00:C6:56:34:12")) + + @patch('pexpect.pxssh.pxssh.logout') + @patch('pexpect.pxssh.pxssh.login') + def test_failed_to_log_in(self, mock_login, mock_logout): + """"Testing exception at login results in False.""" + from pexpect import exceptions + + conf_dict = { + DOMAIN: { + CONF_PLATFORM: 'unifi_direct', + CONF_HOST: 'fake_host', + CONF_USERNAME: 'fake_user', + CONF_PASSWORD: 'fake_pass', + CONF_PORT: 22, + CONF_TRACK_NEW: True, + CONF_CONSIDER_HOME: timedelta(seconds=180) + } + } + + mock_login.side_effect = exceptions.EOF("Test") + scanner = get_scanner(self.hass, conf_dict) + self.assertFalse(scanner) + + @patch('pexpect.pxssh.pxssh.logout') + @patch('pexpect.pxssh.pxssh.login', autospec=True) + @patch('pexpect.pxssh.pxssh.prompt') + @patch('pexpect.pxssh.pxssh.sendline') + def test_to_get_update(self, mock_sendline, mock_prompt, mock_login, + mock_logout): + """"Testing exception in get_update matching.""" + conf_dict = { + DOMAIN: { + CONF_PLATFORM: 'unifi_direct', + CONF_HOST: 'fake_host', + CONF_USERNAME: 'fake_user', + CONF_PASSWORD: 'fake_pass', + CONF_PORT: 22, + CONF_TRACK_NEW: True, + CONF_CONSIDER_HOME: timedelta(seconds=180) + } + } + + scanner = get_scanner(self.hass, conf_dict) + # mock_sendline.side_effect = AssertionError("Test") + mock_prompt.side_effect = AssertionError("Test") + devices = scanner._get_update() # pylint: disable=protected-access + self.assertTrue(devices is None) + + def test_good_reponse_parses(self): + """Test that the response form the AP parses to JSON correctly.""" + response = _response_to_json(load_fixture('unifi_direct.txt')) + self.assertTrue(response != {}) + + def test_bad_reponse_returns_none(self): + """Test that a bad response form the AP parses to JSON correctly.""" + self.assertTrue(_response_to_json("{(}") == {}) + + +def test_config_error(): + """Test for configuration errors.""" + with pytest.raises(vol.Invalid): + PLATFORM_SCHEMA({ + # no username + CONF_PASSWORD: 'password', + CONF_PLATFORM: DOMAIN, + CONF_HOST: 'myhost', + 'port': 123, + }) + with pytest.raises(vol.Invalid): + PLATFORM_SCHEMA({ + # no password + CONF_USERNAME: 'foo', + CONF_PLATFORM: DOMAIN, + CONF_HOST: 'myhost', + 'port': 123, + }) + with pytest.raises(vol.Invalid): + PLATFORM_SCHEMA({ + CONF_PLATFORM: DOMAIN, + CONF_USERNAME: 'foo', + CONF_PASSWORD: 'password', + CONF_HOST: 'myhost', + 'port': 'foo', # bad port! + }) diff --git a/tests/fixtures/unifi_direct.txt b/tests/fixtures/unifi_direct.txt new file mode 100644 index 0000000000000000000000000000000000000000..fcb58070fcca629d99cc0acf2d4bf168277657c2 --- /dev/null +++ b/tests/fixtures/unifi_direct.txt @@ -0,0 +1 @@ +b'mca-dump | tr -d "\r\n> "\r\n{ "board_rev": 16, "bootrom_version": "unifi-v1.6.7.249-gb74e0282", "cfgversion": "63b505a1c328fd9c", "country_code": 840, "default": false, "discovery_response": true, "fw_caps": 855, "guest_token": "E6BAE04FD72C", "has_eth1": false, "has_speaker": false, "hostname": "UBNT", "if_table": [ { "full_duplex": true, "ip": "0.0.0.0", "mac": "80:2a:a8:56:34:12", "name": "eth0", "netmask": "0.0.0.0", "num_port": 1, "rx_bytes": 3879332085, "rx_dropped": 0, "rx_errors": 0, "rx_multicast": 0, "rx_packets": 4093520, "speed": 1000, "tx_bytes": 1745140940, "tx_dropped": 0, "tx_errors": 0, "tx_packets": 3105586, "up": true } ], "inform_url": "?", "ip": "192.168.1.2", "isolated": false, "last_error": "", "locating": false, "mac": "80:2a:a8:56:34:12", "model": "U7LR", "model_display": "UAP-AC-LR", "netmask": "255.255.255.0", "port_table": [ { "media": "GE", "poe_caps": 0, "port_idx": 0, "port_poe": false } ], "radio_table": [ { "athstats": { "ast_ath_reset": 0, "ast_be_xmit": 1098121, "ast_cst": 225, "ast_deadqueue_reset": 0, "ast_fullqueue_stop": 0, "ast_txto": 151, "cu_self_rx": 8, "cu_self_tx": 4, "cu_total": 12, "n_rx_aggr": 3915695, "n_rx_pkts": 6518082, "n_tx_bawadv": 1205430, "n_tx_bawretries": 70257, "n_tx_pkts": 1813368, "n_tx_queue": 1024366, "n_tx_retries": 70273, "n_tx_xretries": 897, "n_txaggr_compgood": 616173, "n_txaggr_compretries": 71170, "n_txaggr_compxretry": 0, "n_txaggr_prepends": 21240, "name": "wifi0" }, "builtin_ant_gain": 0, "builtin_antenna": true, "max_txpower": 24, "min_txpower": 6, "name": "wifi0", "nss": 3, "radio": "ng", "scan_table": [ { "age": 2, "bssid": "28:56:5a:34:23:12", "bw": 20, "center_freq": 2462, "channel": 11, "essid": "someones_wifi", "freq": 2462, "is_adhoc": false, "is_ubnt": false, "rssi": 8, "rssi_age": 2, "security": "secured" }, { "age": 37, "bssid": "00:60:0f:45:34:12", "bw": 20, "center_freq": 2462, "channel": 11, "essid": "", "freq": 2462, "is_adhoc": false, "is_ubnt": false, "rssi": 10, "rssi_age": 37, "security": "secured" }, { "age": 29, "bssid": "b0:93:5b:7a:35:23", "bw": 20, "center_freq": 2462, "channel": 11, "essid": "ARRIS-CB55", "freq": 2462, "is_adhoc": false, "is_ubnt": false, "rssi": 10, "rssi_age": 29, "security": "secured" }, { "age": 0, "bssid": "e0:46:9a:e1:ea:7d", "bw": 20, "center_freq": 2462, "channel": 11, "essid": "Darjeeling", "freq": 2462, "is_adhoc": false, "is_ubnt": false, "rssi": 9, "rssi_age": 0, "security": "secured" }, { "age": 1, "bssid": "00:60:0f:e1:ea:7e", "bw": 20, "center_freq": 2462, "channel": 11, "essid": "", "freq": 2462, "is_adhoc": false, "is_ubnt": false, "rssi": 10, "rssi_age": 1, "security": "secured" }, { "age": 0, "bssid": "7c:d1:c3:cd:e5:f4", "bw": 20, "center_freq": 2462, "channel": 11, "essid": "Chris\'s Wi-Fi Network", "freq": 2462, "is_adhoc": false, "is_ubnt": false, "rssi": 17, "rssi_age": 0, "security": "secured" } ] }, { "athstats": { "ast_ath_reset": 14, "ast_be_xmit": 1097310, "ast_cst": 0, "ast_deadqueue_reset": 41, "ast_fullqueue_stop": 0, "ast_txto": 0, "cu_self_rx": 0, "cu_self_tx": 0, "cu_total": 0, "n_rx_aggr": 106804, "n_rx_pkts": 2453041, "n_tx_bawadv": 557298, "n_tx_bawretries": 0, "n_tx_pkts": 1080, "n_tx_queue": 0, "n_tx_retries": 1, "n_tx_xretries": 44046, "n_txaggr_compgood": 0, "n_txaggr_compretries": 0, "n_txaggr_compxretry": 0, "n_txaggr_prepends": 0, "name": "wifi1" }, "builtin_ant_gain": 0, "builtin_antenna": true, "has_dfs": true, "has_fccdfs": true, "is_11ac": true, "max_txpower": 22, "min_txpower": 4, "name": "wifi1", "nss": 2, "radio": "na", "scan_table": [] } ], "required_version": "3.4.1", "selfrun_beacon": false, "serial": "802AA896363C", "spectrum_scanning": false, "ssh_session_table": [], "state": 0, "stream_token": "", "sys_stats": { "loadavg_1": "0.03", "loadavg_15": "0.06", "loadavg_5": "0.06", "mem_buffer": 0, "mem_total": 129310720, "mem_used": 75800576 }, "system-stats": { "cpu": "8.4", "mem": "58.6", "uptime": "112391" }, "time": 1508795154, "uplink": "eth0", "uptime": 112391, "vap_table": [ { "bssid": "80:2a:a8:97:36:3c", "ccq": 914, "channel": 11, "essid": "220", "id": "55b19c7e50e4e11e798e84c7", "name": "ath0", "num_sta": 20, "radio": "ng", "rx_bytes": 1155345354, "rx_crypts": 5491, "rx_dropped": 5540, "rx_errors": 5540, "rx_frags": 0, "rx_nwids": 647001, "rx_packets": 1840967, "sta_table": [ { "auth_time": 4294967206, "authorized": true, "ccq": 991, "dhcpend_time": 660, "dhcpstart_time": 660, "hostname": "amazon-device", "idletime": 0, "ip": "192.168.1.45", "is_11n": true, "mac": "44:65:0d:12:34:56", "noise": -114, "rssi": 59, "rx_bytes": 1176121, "rx_mcast": 0, "rx_packets": 20927, "rx_rate": 24000, "rx_retries": 0, "signal": -55, "state": 15, "state_ht": true, "state_pwrmgt": false, "tx_bytes": 364495, "tx_packets": 2183, "tx_power": 48, "tx_rate": 72222, "tx_retries": 589, "uptime": 7031, "vlan_id": 0 }, { "auth_time": 4294967266, "authorized": true, "ccq": 991, "dhcpend_time": 290, "dhcpstart_time": 290, "hostname": "iPhone", "idletime": 9, "ip": "192.168.1.209", "is_11n": true, "mac": "98:00:c6:56:34:12", "noise": -114, "rssi": 40, "rx_bytes": 5862172, "rx_mcast": 0, "rx_packets": 30977, "rx_rate": 24000, "rx_retries": 0, "signal": -74, "state": 31, "state_ht": true, "state_pwrmgt": true, "tx_bytes": 31707361, "tx_packets": 27775, "tx_power": 48, "tx_rate": 140637, "tx_retries": 1213, "uptime": 15556, "vlan_id": 0 }, { "auth_time": 4294967276, "authorized": true, "ccq": 991, "dhcpend_time": 630, "dhcpstart_time": 630, "hostname": "android", "idletime": 0, "ip": "192.168.1.10", "is_11n": true, "mac": "b4:79:a7:45:34:12", "noise": -114, "rssi": 60, "rx_bytes": 13694423, "rx_mcast": 0, "rx_packets": 110909, "rx_rate": 1000, "rx_retries": 0, "signal": -54, "state": 15, "state_ht": true, "state_pwrmgt": false, "tx_bytes": 7988429, "tx_packets": 28863, "tx_power": 48, "tx_rate": 72222, "tx_retries": 1254, "uptime": 19052, "vlan_id": 0 }, { "auth_time": 4294967176, "authorized": true, "ccq": 991, "dhcpend_time": 4480, "dhcpstart_time": 4480, "hostname": "wink", "idletime": 0, "ip": "192.168.1.3", "is_11n": true, "mac": "b4:79:a7:56:34:12", "noise": -114, "rssi": 38, "rx_bytes": 18705870, "rx_mcast": 0, "rx_packets": 78794, "rx_rate": 72109, "rx_retries": 0, "signal": -76, "state": 15, "state_ht": true, "state_pwrmgt": false, "tx_bytes": 4416534, "tx_packets": 58304, "tx_power": 48, "tx_rate": 72222, "tx_retries": 1978, "uptime": 51648, "vlan_id": 0 }, { "auth_time": 4294966266, "authorized": true, "ccq": 981, "dhcpend_time": 1530, "dhcpstart_time": 1530, "hostname": "Chromecast", "idletime": 0, "ip": "192.168.1.30", "is_11n": true, "mac": "80:d2:1d:56:34:12", "noise": -114, "rssi": 37, "rx_bytes": 29377621, "rx_mcast": 0, "rx_packets": 105806, "rx_rate": 72109, "rx_retries": 0, "signal": -77, "state": 15, "state_ht": true, "state_pwrmgt": false, "tx_bytes": 122681792, "tx_packets": 145339, "tx_power": 48, "tx_rate": 72222, "tx_retries": 2980, "uptime": 53658, "vlan_id": 0 }, { "auth_time": 4294967176, "authorized": true, "ccq": 991, "dhcpend_time": 370, "dhcpstart_time": 360, "idletime": 2, "ip": "192.168.1.51", "is_11n": false, "mac": "48:02:2d:56:34:12", "noise": -114, "rssi": 56, "rx_bytes": 48148926, "rx_mcast": 0, "rx_packets": 59462, "rx_rate": 1000, "rx_retries": 0, "signal": -58, "state": 16391, "state_ht": false, "state_pwrmgt": false, "tx_bytes": 7075470, "tx_packets": 33047, "tx_power": 48, "tx_rate": 54000, "tx_retries": 2833, "uptime": 63850, "vlan_id": 0 }, { "auth_time": 4294967276, "authorized": true, "ccq": 971, "dhcpend_time": 30, "dhcpstart_time": 30, "hostname": "ESP_1C2F8D", "idletime": 0, "ip": "192.168.1.54", "is_11n": true, "mac": "a0:20:a6:45:35:12", "noise": -114, "rssi": 51, "rx_bytes": 4684699, "rx_mcast": 0, "rx_packets": 137798, "rx_rate": 2000, "rx_retries": 0, "signal": -63, "state": 31, "state_ht": true, "state_pwrmgt": true, "tx_bytes": 355735, "tx_packets": 6977, "tx_power": 48, "tx_rate": 72222, "tx_retries": 590, "uptime": 78427, "vlan_id": 0 }, { "auth_time": 4294967176, "authorized": true, "ccq": 991, "dhcpend_time": 220, "dhcpstart_time": 220, "hostname": "HF-LPB100-ZJ200", "idletime": 2, "ip": "192.168.1.53", "is_11n": true, "mac": "f0:fe:6b:56:34:12", "noise": -114, "rssi": 29, "rx_bytes": 1415840, "rx_mcast": 0, "rx_packets": 22821, "rx_rate": 1000, "rx_retries": 0, "signal": -85, "state": 31, "state_ht": true, "state_pwrmgt": true, "tx_bytes": 402439, "tx_packets": 7779, "tx_power": 48, "tx_rate": 72222, "tx_retries": 891, "uptime": 111944, "vlan_id": 0 }, { "auth_time": 4294967276, "authorized": true, "ccq": 991, "dhcpend_time": 1620, "dhcpstart_time": 1620, "idletime": 0, "ip": "192.168.1.33", "is_11n": false, "mac": "94:10:3e:45:34:12", "noise": -114, "rssi": 48, "rx_bytes": 47843953, "rx_mcast": 0, "rx_packets": 79456, "rx_rate": 54000, "rx_retries": 0, "signal": -66, "state": 16391, "state_ht": false, "state_pwrmgt": false, "tx_bytes": 4357955, "tx_packets": 60958, "tx_power": 48, "tx_rate": 54000, "tx_retries": 4598, "uptime": 112316, "vlan_id": 0 }, { "auth_time": 4294967266, "authorized": true, "ccq": 991, "dhcpend_time": 540, "dhcpstart_time": 540, "hostname": "amazon-device", "idletime": 0, "ip": "192.168.1.46", "is_11n": true, "mac": "ac:63:be:56:34:12", "noise": -114, "rssi": 30, "rx_bytes": 14607810, "rx_mcast": 0, "rx_packets": 326158, "rx_rate": 24000, "rx_retries": 0, "signal": -84, "state": 15, "state_ht": true, "state_pwrmgt": false, "tx_bytes": 3238319, "tx_packets": 25605, "tx_power": 48, "tx_rate": 72222, "tx_retries": 2465, "uptime": 112364, "vlan_id": 0 }, { "auth_time": 4294966266, "authorized": true, "ccq": 941, "dhcpend_time": 1060, "dhcpstart_time": 1060, "hostname": "Broadlink_RMMINI-56-34-12", "idletime": 12, "ip": "192.168.1.52", "is_11n": true, "mac": "34:ea:34:56:34:12", "noise": -114, "rssi": 43, "rx_bytes": 625268, "rx_mcast": 0, "rx_packets": 4711, "rx_rate": 65000, "rx_retries": 0, "signal": -71, "state": 15, "state_ht": true, "state_pwrmgt": false, "tx_bytes": 420763, "tx_packets": 4620, "tx_power": 48, "tx_rate": 65000, "tx_retries": 783, "uptime": 112368, "vlan_id": 0 }, { "auth_time": 4294967256, "authorized": true, "ccq": 930, "dhcpend_time": 3360, "dhcpstart_time": 3360, "hostname": "garage", "idletime": 2, "ip": "192.168.1.28", "is_11n": true, "mac": "00:13:ef:45:34:12", "noise": -114, "rssi": 28, "rx_bytes": 11639474, "rx_mcast": 0, "rx_packets": 102103, "rx_rate": 24000, "rx_retries": 0, "signal": -86, "state": 31, "state_ht": true, "state_pwrmgt": true, "tx_bytes": 6282728, "tx_packets": 85279, "tx_power": 48, "tx_rate": 58500, "tx_retries": 21185, "uptime": 112369, "vlan_id": 0 }, { "auth_time": 4294967286, "authorized": true, "ccq": 991, "dhcpend_time": 30, "dhcpstart_time": 30, "hostname": "keurig", "idletime": 0, "ip": "192.168.1.48", "is_11n": true, "mac": "18:fe:34:56:34:12", "noise": -114, "rssi": 52, "rx_bytes": 17781940, "rx_mcast": 0, "rx_packets": 432172, "rx_rate": 6000, "rx_retries": 0, "signal": -62, "state": 31, "state_ht": true, "state_pwrmgt": true, "tx_bytes": 4143184, "tx_packets": 53751, "tx_power": 48, "tx_rate": 72222, "tx_retries": 3781, "uptime": 112369, "vlan_id": 0 }, { "auth_time": 4294967276, "authorized": true, "ccq": 940, "dhcpend_time": 50, "dhcpstart_time": 50, "hostname": "freezer", "idletime": 0, "ip": "192.168.1.26", "is_11n": true, "mac": "5c:cf:7f:07:5a:a4", "noise": -114, "rssi": 47, "rx_bytes": 13613265, "rx_mcast": 0, "rx_packets": 411785, "rx_rate": 2000, "rx_retries": 0, "signal": -67, "state": 31, "state_ht": true, "state_pwrmgt": true, "tx_bytes": 1411127, "tx_packets": 17492, "tx_power": 48, "tx_rate": 65000, "tx_retries": 5869, "uptime": 112370, "vlan_id": 0 }, { "auth_time": 4294967276, "authorized": true, "ccq": 778, "dhcpend_time": 50, "dhcpstart_time": 50, "hostname": "fan", "idletime": 0, "ip": "192.168.1.34", "is_11n": true, "mac": "5c:cf:7f:02:09:4e", "noise": -114, "rssi": 45, "rx_bytes": 15377230, "rx_mcast": 0, "rx_packets": 417435, "rx_rate": 6000, "rx_retries": 0, "signal": -69, "state": 31, "state_ht": true, "state_pwrmgt": true, "tx_bytes": 2974258, "tx_packets": 36175, "tx_power": 48, "tx_rate": 58500, "tx_retries": 18552, "uptime": 112372, "vlan_id": 0 }, { "auth_time": 4294966266, "authorized": true, "ccq": 991, "dhcpend_time": 1070, "dhcpstart_time": 1070, "hostname": "Broadlink_RMPROPLUS-45-34-12", "idletime": 1, "ip": "192.168.1.9", "is_11n": true, "mac": "b4:43:0d:45:56:56", "noise": -114, "rssi": 57, "rx_bytes": 1792908, "rx_mcast": 0, "rx_packets": 8528, "rx_rate": 72109, "rx_retries": 0, "signal": -57, "state": 15, "state_ht": true, "state_pwrmgt": false, "tx_bytes": 770834, "tx_packets": 8443, "tx_power": 48, "tx_rate": 65000, "tx_retries": 5258, "uptime": 112373, "vlan_id": 0 }, { "auth_time": 4294967276, "authorized": true, "ccq": 991, "dhcpend_time": 210, "dhcpstart_time": 210, "idletime": 49, "ip": "192.168.1.40", "is_11n": true, "mac": "0c:2a:69:02:3e:3b", "noise": -114, "rssi": 36, "rx_bytes": 427418, "rx_mcast": 0, "rx_packets": 2824, "rx_rate": 65000, "rx_retries": 0, "signal": -78, "state": 15, "state_ht": true, "state_pwrmgt": false, "tx_bytes": 176039, "tx_packets": 2872, "tx_power": 48, "tx_rate": 65000, "tx_retries": 87, "uptime": 112373, "vlan_id": 0 }, { "auth_time": 4294966266, "authorized": true, "ccq": 991, "dhcpend_time": 5030, "dhcpstart_time": 5030, "hostname": "HP2C27D78D9F3E", "idletime": 268, "ip": "192.168.1.44", "is_11n": true, "mac": "2c:27:d7:8d:9f:3e", "noise": -114, "rssi": 41, "rx_bytes": 172927, "rx_mcast": 0, "rx_packets": 781, "rx_rate": 72109, "rx_retries": 0, "signal": -73, "state": 15, "state_ht": true, "state_pwrmgt": false, "tx_bytes": 41924, "tx_packets": 453, "tx_power": 48, "tx_rate": 66610, "tx_retries": 66, "uptime": 112373, "vlan_id": 0 }, { "auth_time": 4294967266, "authorized": true, "ccq": 991, "dhcpend_time": 110, "dhcpstart_time": 110, "idletime": 4, "ip": "192.168.1.55", "is_11n": true, "mac": "0c:2a:69:04:e6:ac", "noise": -114, "rssi": 51, "rx_bytes": 300741, "rx_mcast": 0, "rx_packets": 2443, "rx_rate": 65000, "rx_retries": 0, "signal": -63, "state": 15, "state_ht": true, "state_pwrmgt": false, "tx_bytes": 159980, "tx_packets": 2526, "tx_power": 48, "tx_rate": 65000, "tx_retries": 47, "uptime": 112373, "vlan_id": 0 }, { "auth_time": 4294967256, "authorized": true, "ccq": 991, "dhcpend_time": 1570, "dhcpstart_time": 1560, "idletime": 1, "ip": "192.168.1.37", "is_11n": true, "mac": "0c:2a:69:03:df:37", "noise": -114, "rssi": 42, "rx_bytes": 304567, "rx_mcast": 0, "rx_packets": 2468, "rx_rate": 65000, "rx_retries": 0, "signal": -72, "state": 15, "state_ht": true, "state_pwrmgt": false, "tx_bytes": 164382, "tx_packets": 2553, "tx_power": 48, "tx_rate": 65000, "tx_retries": 48, "uptime": 112373, "vlan_id": 0 } ], "state": "RUN", "tx_bytes": 1190129336, "tx_dropped": 7, "tx_errors": 0, "tx_packets": 1907093, "tx_power": 24, "tx_retries": 29927, "up": true, "usage": "user" }, { "bssid": "ff:ff:ff:ff:ff:ff", "ccq": 914, "channel": 157, "essid": "", "extchannel": 1, "id": "user", "name": "ath1", "num_sta": 0, "radio": "na", "rx_bytes": 0, "rx_crypts": 0, "rx_dropped": 0, "rx_errors": 0, "rx_frags": 0, "rx_nwids": 0, "rx_packets": 0, "sta_table": [], "state": "INIT", "tx_bytes": 0, "tx_dropped": 0, "tx_errors": 0, "tx_packets": 0, "tx_power": 22, "tx_retries": 0, "up": false, "usage": "uplink" }, { "bssid": "82:2a:a8:98:36:3c", "ccq": 482, "channel": 157, "essid": "220 5ghz", "extchannel": 1, "id": "55b19c7e50e4e11e798e84c7", "name": "ath2", "num_sta": 3, "radio": "na", "rx_bytes": 250435644, "rx_crypts": 4071, "rx_dropped": 4071, "rx_errors": 4071, "rx_frags": 0, "rx_nwids": 6660, "rx_packets": 1123263, "sta_table": [ { "auth_time": 4294967246, "authorized": true, "ccq": 631, "dhcpend_time": 190, "dhcpstart_time": 190, "hostname": "android-f4aaefc31d5d2f78", "idletime": 26, "ip": "192.168.1.15", "is_11a": true, "is_11ac": true, "is_11n": true, "mac": "c0:ee:fb:24:ef:a0", "noise": -105, "rssi": 16, "rx_bytes": 3188995, "rx_mcast": 0, "rx_packets": 37243, "rx_rate": 81000, "rx_retries": 0, "signal": -89, "state": 11, "state_ht": true, "state_pwrmgt": false, "tx_bytes": 89051905, "tx_packets": 64756, "tx_power": 44, "tx_rate": 108000, "tx_retries": 0, "uptime": 5494, "vlan_id": 0 }, { "auth_time": 4294967286, "authorized": true, "ccq": 333, "dhcpend_time": 10, "dhcpstart_time": 10, "hostname": "mac_book_air", "idletime": 1, "ip": "192.168.1.12", "is_11a": true, "is_11n": true, "mac": "00:88:65:56:34:12", "noise": -105, "rssi": 52, "rx_bytes": 106902966, "rx_mcast": 0, "rx_packets": 270845, "rx_rate": 300000, "rx_retries": 0, "signal": -53, "state": 11, "state_ht": true, "state_pwrmgt": false, "tx_bytes": 289588466, "tx_packets": 339466, "tx_power": 44, "tx_rate": 300000, "tx_retries": 0, "uptime": 15312, "vlan_id": 0 }, { "auth_time": 4294967266, "authorized": true, "ccq": 333, "dhcpend_time": 160, "dhcpstart_time": 160, "hostname": "Chromecast", "idletime": 0, "ip": "192.168.1.29", "is_11a": true, "is_11ac": true, "is_11n": true, "mac": "f4:f5:d8:11:57:6a", "noise": -105, "rssi": 40, "rx_bytes": 50958412, "rx_mcast": 0, "rx_packets": 339563, "rx_rate": 200000, "rx_retries": 0, "signal": -65, "state": 11, "state_ht": true, "state_pwrmgt": false, "tx_bytes": 1186178689, "tx_packets": 890384, "tx_power": 44, "tx_rate": 150000, "tx_retries": 0, "uptime": 56493, "vlan_id": 0 } ], "state": "RUN", "tx_bytes": 2766849222, "tx_dropped": 119, "tx_errors": 23508, "tx_packets": 2247859, "tx_power": 22, "tx_retries": 0, "up": true, "usage": "user" } ], "version": "3.7.58.6385", "wifi_caps": 1909}' \ No newline at end of file