Home Assistant Unofficial Reference 2024.12.1
device_tracker.py
Go to the documentation of this file.
1 """Support for DD-WRT routers."""
2 
3 from __future__ import annotations
4 
5 from http import HTTPStatus
6 import logging
7 import re
8 
9 import requests
10 import voluptuous as vol
11 
13  DOMAIN as DEVICE_TRACKER_DOMAIN,
14  PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA,
15  DeviceScanner,
16 )
17 from homeassistant.const import (
18  CONF_HOST,
19  CONF_PASSWORD,
20  CONF_SSL,
21  CONF_USERNAME,
22  CONF_VERIFY_SSL,
23 )
24 from homeassistant.core import HomeAssistant
26 from homeassistant.helpers.typing import ConfigType
27 
28 _LOGGER = logging.getLogger(__name__)
29 
30 _DDWRT_DATA_REGEX = re.compile(r"\{(\w+)::([^\}]*)\}")
31 _MAC_REGEX = re.compile(r"(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})")
32 
33 DEFAULT_SSL = False
34 DEFAULT_VERIFY_SSL = True
35 CONF_WIRELESS_ONLY = "wireless_only"
36 DEFAULT_WIRELESS_ONLY = True
37 
38 PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend(
39  {
40  vol.Required(CONF_HOST): cv.string,
41  vol.Required(CONF_PASSWORD): cv.string,
42  vol.Required(CONF_USERNAME): cv.string,
43  vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
44  vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
45  vol.Optional(CONF_WIRELESS_ONLY, default=DEFAULT_WIRELESS_ONLY): cv.boolean,
46  }
47 )
48 
49 
50 def get_scanner(hass: HomeAssistant, config: ConfigType) -> DdWrtDeviceScanner | None:
51  """Validate the configuration and return a DD-WRT scanner."""
52  try:
53  return DdWrtDeviceScanner(config[DEVICE_TRACKER_DOMAIN])
54  except ConnectionError:
55  return None
56 
57 
58 class DdWrtDeviceScanner(DeviceScanner):
59  """Class which queries a wireless router running DD-WRT firmware."""
60 
61  def __init__(self, config):
62  """Initialize the DD-WRT scanner."""
63  self.protocolprotocol = "https" if config[CONF_SSL] else "http"
64  self.verify_sslverify_ssl = config[CONF_VERIFY_SSL]
65  self.hosthost = config[CONF_HOST]
66  self.usernameusername = config[CONF_USERNAME]
67  self.passwordpassword = config[CONF_PASSWORD]
68  self.wireless_onlywireless_only = config[CONF_WIRELESS_ONLY]
69 
70  self.last_resultslast_results = {}
71  self.mac2namemac2name = {}
72 
73  # Test the router is accessible
74  url = f"{self.protocol}://{self.host}/Status_Wireless.live.asp"
75  if not self.get_ddwrt_dataget_ddwrt_data(url):
76  raise ConnectionError("Cannot connect to DD-Wrt router")
77 
78  def scan_devices(self):
79  """Scan for new devices and return a list with found device IDs."""
80  self._update_info_update_info()
81 
82  return self.last_resultslast_results
83 
84  def get_device_name(self, device):
85  """Return the name of the given device or None if we don't know."""
86  # If not initialised and not already scanned and not found.
87  if device not in self.mac2namemac2name:
88  url = f"{self.protocol}://{self.host}/Status_Lan.live.asp"
89 
90  if not (data := self.get_ddwrt_dataget_ddwrt_data(url)):
91  return None
92 
93  if not (dhcp_leases := data.get("dhcp_leases")):
94  return None
95 
96  # Remove leading and trailing quotes and spaces
97  cleaned_str = dhcp_leases.replace('"', "").replace("'", "").replace(" ", "")
98  elements = cleaned_str.split(",")
99  num_clients = int(len(elements) / 5)
100  self.mac2namemac2name = {}
101  for idx in range(num_clients):
102  # The data is a single array
103  # every 5 elements represents one host, the MAC
104  # is the third element and the name is the first.
105  mac_index = (idx * 5) + 2
106  if mac_index < len(elements):
107  mac = elements[mac_index]
108  self.mac2namemac2name[mac] = elements[idx * 5]
109 
110  return self.mac2namemac2name.get(device)
111 
112  def _update_info(self):
113  """Ensure the information from the DD-WRT router is up to date.
114 
115  Return boolean if scanning successful.
116  """
117  _LOGGER.debug("Checking ARP")
118 
119  endpoint = "Wireless" if self.wireless_onlywireless_only else "Lan"
120  url = f"{self.protocol}://{self.host}/Status_{endpoint}.live.asp"
121 
122  if not (data := self.get_ddwrt_dataget_ddwrt_data(url)):
123  return False
124 
125  self.last_resultslast_results = []
126 
127  if self.wireless_onlywireless_only:
128  active_clients = data.get("active_wireless")
129  else:
130  active_clients = data.get("arp_table")
131  if not active_clients:
132  return False
133 
134  # The DD-WRT UI uses its own data format and then
135  # regex's out values so this is done here too
136  # Remove leading and trailing single quotes.
137  clean_str = active_clients.strip().strip("'")
138  elements = clean_str.split("','")
139 
140  self.last_resultslast_results.extend(item for item in elements if _MAC_REGEX.match(item))
141 
142  return True
143 
144  def get_ddwrt_data(self, url):
145  """Retrieve data from DD-WRT and return parsed result."""
146  try:
147  response = requests.get(
148  url,
149  auth=(self.usernameusername, self.passwordpassword),
150  timeout=4,
151  verify=self.verify_sslverify_ssl,
152  )
153  except requests.exceptions.Timeout:
154  _LOGGER.exception("Connection to the router timed out")
155  return None
156  if response.status_code == HTTPStatus.OK:
157  return _parse_ddwrt_response(response.text)
158  if response.status_code == HTTPStatus.UNAUTHORIZED:
159  # Authentication error
160  _LOGGER.exception(
161  "Failed to authenticate, check your username and password"
162  )
163  return None
164  _LOGGER.error("Invalid response from DD-WRT: %s", response)
165  return None
166 
167 
168 def _parse_ddwrt_response(data_str):
169  """Parse the DD-WRT data format."""
170  return dict(_DDWRT_DATA_REGEX.findall(data_str))
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
DdWrtDeviceScanner|None get_scanner(HomeAssistant hass, ConfigType config)