core/homeassistant/components/device_tracker/tplink.py

336 lines
12 KiB
Python
Raw Normal View History

2015-06-17 20:55:03 +00:00
"""
2016-03-07 20:18:53 +00:00
Support for TP-Link routers.
2015-06-17 20:55:03 +00:00
2015-10-13 18:51:30 +00:00
For more details about this platform, please refer to the documentation at
2015-11-09 12:12:18 +00:00
https://home-assistant.io/components/device_tracker.tplink/
2015-06-17 20:55:03 +00:00
"""
import base64
2016-02-28 23:56:13 +00:00
import hashlib
2015-06-17 20:55:03 +00:00
import logging
import re
import threading
2016-02-19 05:27:50 +00:00
from datetime import timedelta
2015-06-17 20:55:03 +00:00
import requests
import voluptuous as vol
2015-06-17 20:55:03 +00:00
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
2016-02-19 05:27:50 +00:00
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
2015-06-17 20:55:03 +00:00
from homeassistant.util import Throttle
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string
})
2015-06-17 20:55:03 +00:00
def get_scanner(hass, config):
2016-03-09 10:15:04 +00:00
"""Validate the configuration and return a TP-Link scanner."""
for cls in [Tplink4DeviceScanner, Tplink3DeviceScanner,
Tplink2DeviceScanner, TplinkDeviceScanner]:
scanner = cls(config[DOMAIN])
if scanner.success_init:
return scanner
2015-06-17 20:55:03 +00:00
return None
2015-06-17 20:55:03 +00:00
class TplinkDeviceScanner(DeviceScanner):
2016-03-07 20:18:53 +00:00
"""This class queries a wireless router running TP-Link firmware."""
2015-06-17 20:55:03 +00:00
def __init__(self, config):
2016-03-07 20:18:53 +00:00
"""Initialize the scanner."""
2015-06-17 20:55:03 +00:00
host = config[CONF_HOST]
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
2015-06-17 21:40:58 +00:00
self.parse_macs = re.compile('[0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2}-' +
2015-06-17 21:32:33 +00:00
'[0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2}')
2015-06-17 20:55:03 +00:00
self.host = host
self.username = username
self.password = password
self.last_results = {}
self.lock = threading.Lock()
self.success_init = self._update_info()
def scan_devices(self):
2016-03-07 20:18:53 +00:00
"""Scan for new devices and return a list with found device IDs."""
2015-06-17 20:55:03 +00:00
self._update_info()
return self.last_results
# pylint: disable=no-self-use
2015-06-17 20:55:03 +00:00
def get_device_name(self, device):
2016-03-07 20:18:53 +00:00
"""The firmware doesn't save the name of the wireless device."""
2015-06-17 20:55:03 +00:00
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
2016-03-07 20:18:53 +00:00
"""Ensure the information from the TP-Link router is up to date.
2015-06-17 20:55:03 +00:00
2016-03-07 20:18:53 +00:00
Return boolean if scanning successful.
"""
2015-06-17 20:55:03 +00:00
with self.lock:
_LOGGER.info("Loading wireless clients...")
url = 'http://{}/userRpm/WlanStationRpm.htm'.format(self.host)
referer = 'http://{}'.format(self.host)
2015-06-17 21:32:33 +00:00
page = requests.get(url, auth=(self.username, self.password),
headers={'referer': referer})
2015-06-17 20:55:03 +00:00
result = self.parse_macs.findall(page.text)
2015-06-17 21:32:33 +00:00
2015-06-17 20:55:03 +00:00
if result:
self.last_results = [mac.replace("-", ":") for mac in result]
2015-06-17 20:55:03 +00:00
return True
return False
class Tplink2DeviceScanner(TplinkDeviceScanner):
2016-03-07 20:18:53 +00:00
"""This class queries a router with newer version of TP-Link firmware."""
def scan_devices(self):
2016-03-07 20:18:53 +00:00
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
return self.last_results.keys()
# pylint: disable=no-self-use
def get_device_name(self, device):
2016-03-07 20:18:53 +00:00
"""The firmware doesn't save the name of the wireless device."""
return self.last_results.get(device)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
2016-03-07 20:18:53 +00:00
"""Ensure the information from the TP-Link router is up to date.
2016-03-07 20:18:53 +00:00
Return boolean if scanning successful.
"""
with self.lock:
_LOGGER.info("Loading wireless clients...")
2015-10-12 07:18:55 +00:00
url = 'http://{}/data/map_access_wireless_client_grid.json' \
.format(self.host)
referer = 'http://{}'.format(self.host)
# Router uses Authorization cookie instead of header
# Let's create the cookie
username_password = '{}:{}'.format(self.username, self.password)
b64_encoded_username_password = base64.b64encode(
username_password.encode('ascii')
).decode('ascii')
2015-10-12 07:18:55 +00:00
cookie = 'Authorization=Basic {}' \
.format(b64_encoded_username_password)
response = requests.post(url, headers={'referer': referer,
'cookie': cookie})
try:
result = response.json().get('data')
except ValueError:
_LOGGER.error("Router didn't respond with JSON. "
"Check if credentials are correct.")
return False
if result:
self.last_results = {
device['mac_addr'].replace('-', ':'): device['name']
for device in result
2015-10-12 07:18:55 +00:00
}
return True
return False
2015-10-12 07:18:55 +00:00
class Tplink3DeviceScanner(TplinkDeviceScanner):
2016-03-07 20:18:53 +00:00
"""This class queries the Archer C9 router with version 150811 or high."""
2015-10-12 07:18:55 +00:00
def __init__(self, config):
2016-03-07 20:18:53 +00:00
"""Initialize the scanner."""
2015-10-12 07:18:55 +00:00
self.stok = ''
self.sysauth = ''
super(Tplink3DeviceScanner, self).__init__(config)
2015-10-12 07:18:55 +00:00
def scan_devices(self):
2016-03-07 20:18:53 +00:00
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
return self.last_results.keys()
# pylint: disable=no-self-use
def get_device_name(self, device):
2016-03-07 20:18:53 +00:00
"""The firmware doesn't save the name of the wireless device.
We are forced to use the MAC address as name here.
"""
return self.last_results.get(device)
def _get_auth_tokens(self):
2016-03-07 20:18:53 +00:00
"""Retrieve auth tokens from the router."""
_LOGGER.info("Retrieving auth tokens...")
2015-10-12 07:18:55 +00:00
url = 'http://{}/cgi-bin/luci/;stok=/login?form=login' \
.format(self.host)
referer = 'http://{}/webpages/login.html'.format(self.host)
2016-03-07 20:18:53 +00:00
# If possible implement rsa encryption of password here.
2015-10-12 07:18:55 +00:00
response = requests.post(url,
params={'operation': 'login',
'username': self.username,
'password': self.password},
headers={'referer': referer})
try:
self.stok = response.json().get('data').get('stok')
_LOGGER.info(self.stok)
2015-10-12 07:18:55 +00:00
regex_result = re.search('sysauth=(.*);',
response.headers['set-cookie'])
self.sysauth = regex_result.group(1)
_LOGGER.info(self.sysauth)
return True
2015-10-12 07:18:55 +00:00
except ValueError:
_LOGGER.error("Couldn't fetch auth tokens!")
return False
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
2016-03-07 20:18:53 +00:00
"""Ensure the information from the TP-Link router is up to date.
2016-03-07 20:18:53 +00:00
Return boolean if scanning successful.
"""
with self.lock:
if (self.stok == '') or (self.sysauth == ''):
self._get_auth_tokens()
_LOGGER.info("Loading wireless clients...")
2016-01-13 05:53:27 +00:00
url = ('http://{}/cgi-bin/luci/;stok={}/admin/wireless?'
'form=statistics').format(self.host, self.stok)
referer = 'http://{}/webpages/index.html'.format(self.host)
2015-10-12 07:18:55 +00:00
response = requests.post(url,
params={'operation': 'load'},
headers={'referer': referer},
cookies={'sysauth': self.sysauth})
try:
json_response = response.json()
if json_response.get('success'):
result = response.json().get('data')
else:
if json_response.get('errorcode') == 'timeout':
2015-10-12 07:18:55 +00:00
_LOGGER.info("Token timed out. "
"Relogging on next scan.")
self.stok = ''
self.sysauth = ''
return False
else:
2015-10-12 07:18:55 +00:00
_LOGGER.error("An unknown error happened "
"while fetching data.")
return False
except ValueError:
_LOGGER.error("Router didn't respond with JSON. "
"Check if credentials are correct.")
return False
if result:
self.last_results = {
device['mac'].replace('-', ':'): device['mac']
for device in result
2015-10-12 07:18:55 +00:00
}
return True
return False
2016-02-28 23:56:13 +00:00
class Tplink4DeviceScanner(TplinkDeviceScanner):
2016-03-07 20:18:53 +00:00
"""This class queries an Archer C7 router with TP-Link firmware 150427."""
2016-02-28 23:56:13 +00:00
def __init__(self, config):
2016-03-07 20:18:53 +00:00
"""Initialize the scanner."""
2016-02-28 23:56:13 +00:00
self.credentials = ''
self.token = ''
super(Tplink4DeviceScanner, self).__init__(config)
def scan_devices(self):
2016-03-07 20:18:53 +00:00
"""Scan for new devices and return a list with found device IDs."""
2016-02-28 23:56:13 +00:00
self._update_info()
return self.last_results
# pylint: disable=no-self-use
def get_device_name(self, device):
2016-03-07 20:18:53 +00:00
"""The firmware doesn't save the name of the wireless device."""
2016-02-28 23:56:13 +00:00
return None
def _get_auth_tokens(self):
2016-03-07 20:18:53 +00:00
"""Retrieve auth tokens from the router."""
2016-02-28 23:56:13 +00:00
_LOGGER.info("Retrieving auth tokens...")
url = 'http://{}/userRpm/LoginRpm.htm?Save=Save'.format(self.host)
# Generate md5 hash of password. The C7 appears to use the first 15
# characters of the password only, so we truncate to remove additional
# characters from being hashed.
password = hashlib.md5(self.password.encode('utf')[:15]).hexdigest()
2016-02-28 23:56:13 +00:00
credentials = '{}:{}'.format(self.username, password).encode('utf')
2016-03-07 20:18:53 +00:00
# Encode the credentials to be sent as a cookie.
2016-02-28 23:56:13 +00:00
self.credentials = base64.b64encode(credentials).decode('utf')
2016-03-07 20:18:53 +00:00
# Create the authorization cookie.
2016-02-28 23:56:13 +00:00
cookie = 'Authorization=Basic {}'.format(self.credentials)
response = requests.get(url, headers={'cookie': cookie})
try:
2016-03-07 16:29:05 +00:00
result = re.search(r'window.parent.location.href = '
2016-02-28 23:56:13 +00:00
r'"https?:\/\/.*\/(.*)\/userRpm\/Index.htm";',
response.text)
2016-03-07 16:29:05 +00:00
if not result:
return False
2016-02-28 23:56:13 +00:00
self.token = result.group(1)
return True
except ValueError:
_LOGGER.error("Couldn't fetch auth tokens!")
return False
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
2016-03-07 20:18:53 +00:00
"""Ensure the information from the TP-Link router is up to date.
2016-02-28 23:56:13 +00:00
2016-03-07 20:18:53 +00:00
Return boolean if scanning successful.
"""
2016-02-28 23:56:13 +00:00
with self.lock:
if (self.credentials == '') or (self.token == ''):
self._get_auth_tokens()
_LOGGER.info("Loading wireless clients...")
mac_results = []
# Check both the 2.4GHz and 5GHz client list URLs
for clients_url in ('WlanStationRpm.htm', 'WlanStationRpm_5g.htm'):
url = 'http://{}/{}/userRpm/{}' \
.format(self.host, self.token, clients_url)
referer = 'http://{}'.format(self.host)
cookie = 'Authorization=Basic {}'.format(self.credentials)
page = requests.get(url, headers={
'cookie': cookie,
'referer': referer
})
mac_results.extend(self.parse_macs.findall(page.text))
if not mac_results:
2016-03-07 16:29:05 +00:00
return False
2016-02-28 23:56:13 +00:00
self.last_results = [mac.replace("-", ":") for mac in mac_results]
2016-03-07 16:29:05 +00:00
return True