111 lines
3.6 KiB
Python
111 lines
3.6 KiB
Python
"""Support for Linksys Smart Wifi routers."""
|
|
from __future__ import annotations
|
|
|
|
from http import HTTPStatus
|
|
import logging
|
|
|
|
import requests
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.device_tracker import (
|
|
DOMAIN,
|
|
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
|
|
DeviceScanner,
|
|
)
|
|
from homeassistant.const import CONF_HOST
|
|
from homeassistant.core import HomeAssistant
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.typing import ConfigType
|
|
|
|
DEFAULT_TIMEOUT = 10
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string})
|
|
|
|
|
|
def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None:
|
|
"""Validate the configuration and return a Linksys AP scanner."""
|
|
try:
|
|
return LinksysSmartWifiDeviceScanner(config[DOMAIN])
|
|
except ConnectionError:
|
|
return None
|
|
|
|
|
|
class LinksysSmartWifiDeviceScanner(DeviceScanner):
|
|
"""This class queries a Linksys Access Point."""
|
|
|
|
def __init__(self, config):
|
|
"""Initialize the scanner."""
|
|
self.host = config[CONF_HOST]
|
|
self.last_results = {}
|
|
|
|
# Check if the access point is accessible
|
|
response = self._make_request()
|
|
if response.status_code != HTTPStatus.OK:
|
|
raise ConnectionError("Cannot connect to Linksys Access Point")
|
|
|
|
def scan_devices(self):
|
|
"""Scan for new devices and return a list with device IDs (MACs)."""
|
|
self._update_info()
|
|
|
|
return self.last_results.keys()
|
|
|
|
def get_device_name(self, device):
|
|
"""Return the name (if known) of the device."""
|
|
return self.last_results.get(device)
|
|
|
|
def _update_info(self):
|
|
"""Check for connected devices."""
|
|
_LOGGER.info("Checking Linksys Smart Wifi")
|
|
|
|
self.last_results = {}
|
|
response = self._make_request()
|
|
if response.status_code != HTTPStatus.OK:
|
|
_LOGGER.error(
|
|
"Got HTTP status code %d when getting device list", response.status_code
|
|
)
|
|
return False
|
|
try:
|
|
data = response.json()
|
|
result = data["responses"][0]
|
|
devices = result["output"]["devices"]
|
|
for device in devices:
|
|
if not (macs := device["knownMACAddresses"]):
|
|
_LOGGER.warning("Skipping device without known MAC address")
|
|
continue
|
|
mac = macs[-1]
|
|
if not device["connections"]:
|
|
_LOGGER.debug("Device %s is not connected", mac)
|
|
continue
|
|
|
|
name = None
|
|
for prop in device["properties"]:
|
|
if prop["name"] == "userDeviceName":
|
|
name = prop["value"]
|
|
if not name:
|
|
name = device.get("friendlyName", device["deviceID"])
|
|
|
|
_LOGGER.debug("Device %s is connected", mac)
|
|
self.last_results[mac] = name
|
|
except (KeyError, IndexError):
|
|
_LOGGER.exception("Router returned unexpected response")
|
|
return False
|
|
return True
|
|
|
|
def _make_request(self):
|
|
# Weirdly enough, this doesn't seem to require authentication
|
|
data = [
|
|
{
|
|
"request": {"sinceRevision": 0},
|
|
"action": "http://linksys.com/jnap/devicelist/GetDevices",
|
|
}
|
|
]
|
|
headers = {"X-JNAP-Action": "http://linksys.com/jnap/core/Transaction"}
|
|
return requests.post(
|
|
f"http://{self.host}/JNAP/",
|
|
timeout=DEFAULT_TIMEOUT,
|
|
headers=headers,
|
|
json=data,
|
|
)
|