Home Assistant Unofficial Reference 2024.12.1
device_tracker.py
Go to the documentation of this file.
1 """Support for Xiaomi Mi routers."""
2 
3 from __future__ import annotations
4 
5 from http import HTTPStatus
6 import logging
7 
8 import requests
9 import voluptuous as vol
10 
12  DOMAIN as DEVICE_TRACKER_DOMAIN,
13  PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA,
14  DeviceScanner,
15 )
16 from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
17 from homeassistant.core import HomeAssistant
19 from homeassistant.helpers.typing import ConfigType
20 
21 _LOGGER = logging.getLogger(__name__)
22 
23 PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend(
24  {
25  vol.Required(CONF_HOST): cv.string,
26  vol.Required(CONF_USERNAME, default="admin"): cv.string,
27  vol.Required(CONF_PASSWORD): cv.string,
28  }
29 )
30 
31 
32 def get_scanner(hass: HomeAssistant, config: ConfigType) -> XiaomiDeviceScanner | None:
33  """Validate the configuration and return a Xiaomi Device Scanner."""
34  scanner = XiaomiDeviceScanner(config[DEVICE_TRACKER_DOMAIN])
35 
36  return scanner if scanner.success_init else None
37 
38 
39 class XiaomiDeviceScanner(DeviceScanner):
40  """Class which queries a Xiaomi Mi router.
41 
42  Adapted from Luci scanner.
43  """
44 
45  def __init__(self, config):
46  """Initialize the scanner."""
47  self.hosthost = config[CONF_HOST]
48  self.usernameusername = config[CONF_USERNAME]
49  self.passwordpassword = config[CONF_PASSWORD]
50 
51  self.last_resultslast_results = {}
52  self.tokentoken = _get_token(self.hosthost, self.usernameusername, self.passwordpassword)
53 
54  self.mac2namemac2name = None
55  self.success_initsuccess_init = self.tokentoken is not None
56 
57  def scan_devices(self):
58  """Scan for new devices and return a list with found device IDs."""
59  self._update_info_update_info()
60  return self.last_resultslast_results
61 
62  def get_device_name(self, device):
63  """Return the name of the given device or None if we don't know."""
64  if self.mac2namemac2name is None:
65  result = self._retrieve_list_with_retry_retrieve_list_with_retry()
66  if result:
67  hosts = [x for x in result if "mac" in x and "name" in x]
68  mac2name_list = [(x["mac"].upper(), x["name"]) for x in hosts]
69  self.mac2namemac2name = dict(mac2name_list)
70  else:
71  # Error, handled in the _retrieve_list_with_retry
72  return None
73  return self.mac2namemac2name.get(device.upper(), None)
74 
75  def _update_info(self):
76  """Ensure the information from the router are up to date.
77 
78  Returns true if scanning successful.
79  """
80  if not self.success_initsuccess_init:
81  return False
82 
83  result = self._retrieve_list_with_retry_retrieve_list_with_retry()
84  if result:
85  self._store_result_store_result(result)
86  return True
87  return False
88 
90  """Retrieve the device list with a retry if token is invalid.
91 
92  Return the list if successful.
93  """
94  _LOGGER.debug("Refreshing device list")
95  result = _retrieve_list(self.hosthost, self.tokentoken)
96  if result:
97  return result
98 
99  _LOGGER.debug("Refreshing token and retrying device list refresh")
100  self.tokentoken = _get_token(self.hosthost, self.usernameusername, self.passwordpassword)
101  return _retrieve_list(self.hosthost, self.tokentoken)
102 
103  def _store_result(self, result):
104  """Extract and store the device list in self.last_results."""
105  self.last_resultslast_results = []
106  for device_entry in result:
107  # Check if the device is marked as connected
108  if int(device_entry["online"]) == 1:
109  self.last_resultslast_results.append(device_entry["mac"])
110 
111 
112 def _retrieve_list(host, token, **kwargs):
113  """Get device list for the given host."""
114  url = "http://{}/cgi-bin/luci/;stok={}/api/misystem/devicelist"
115  url = url.format(host, token)
116  try:
117  res = requests.get(url, timeout=10, **kwargs)
118  except requests.exceptions.Timeout:
119  _LOGGER.exception("Connection to the router timed out at URL %s", url)
120  return None
121  if res.status_code != HTTPStatus.OK:
122  _LOGGER.exception("Connection failed with http code %s", res.status_code)
123  return None
124  try:
125  result = res.json()
126  except ValueError:
127  # If json decoder could not parse the response
128  _LOGGER.exception("Failed to parse response from mi router")
129  return None
130  try:
131  xiaomi_code = result["code"]
132  except KeyError:
133  _LOGGER.exception("No field code in response from mi router. %s", result)
134  return None
135  if xiaomi_code == 0:
136  try:
137  return result["list"]
138  except KeyError:
139  _LOGGER.exception("No list in response from mi router. %s", result)
140  return None
141  else:
142  _LOGGER.warning(
143  "Receive wrong Xiaomi code %s, expected 0 in response %s",
144  xiaomi_code,
145  result,
146  )
147  return None
148 
149 
150 def _get_token(host, username, password):
151  """Get authentication token for the given host+username+password."""
152  url = f"http://{host}/cgi-bin/luci/api/xqsystem/login"
153  data = {"username": username, "password": password}
154  try:
155  res = requests.post(url, data=data, timeout=5)
156  except requests.exceptions.Timeout:
157  _LOGGER.exception("Connection to the router timed out")
158  return None
159  if res.status_code == HTTPStatus.OK:
160  try:
161  result = res.json()
162  except ValueError:
163  # If JSON decoder could not parse the response
164  _LOGGER.exception("Failed to parse response from mi router")
165  return None
166  try:
167  return result["token"]
168  except KeyError:
169  error_message = (
170  "Xiaomi token cannot be refreshed, response from "
171  "url: [%s] \nwith parameter: [%s] \nwas: [%s]"
172  )
173  _LOGGER.exception(error_message, url, data, result)
174  return None
175 
176  _LOGGER.error("Invalid response: [%s] at url: [%s] with data [%s]", res, url, data)
177  return None
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
def _get_token(host, username, password)
XiaomiDeviceScanner|None get_scanner(HomeAssistant hass, ConfigType config)