Home Assistant Unofficial Reference 2024.12.1
device_tracker.py
Go to the documentation of this file.
1 """Support for Actiontec MI424WR (Verizon FIOS) routers."""
2 
3 from __future__ import annotations
4 
5 import logging
6 import telnetlib # pylint: disable=deprecated-module
7 from typing import Final
8 
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 from .const import LEASES_REGEX
22 from .model import Device
23 
24 _LOGGER: Final = logging.getLogger(__name__)
25 
26 PLATFORM_SCHEMA: Final = DEVICE_TRACKER_PLATFORM_SCHEMA.extend(
27  {
28  vol.Required(CONF_HOST): cv.string,
29  vol.Required(CONF_PASSWORD): cv.string,
30  vol.Required(CONF_USERNAME): cv.string,
31  }
32 )
33 
34 
36  hass: HomeAssistant, config: ConfigType
37 ) -> ActiontecDeviceScanner | None:
38  """Validate the configuration and return an Actiontec scanner."""
39  scanner = ActiontecDeviceScanner(config[DEVICE_TRACKER_DOMAIN])
40  return scanner if scanner.success_init else None
41 
42 
43 class ActiontecDeviceScanner(DeviceScanner):
44  """Class which queries an actiontec router for connected devices."""
45 
46  def __init__(self, config: ConfigType) -> None:
47  """Initialize the scanner."""
48  self.host: str = config[CONF_HOST]
49  self.username: str = config[CONF_USERNAME]
50  self.password: str = config[CONF_PASSWORD]
51  self.last_resultslast_results: list[Device] = []
52  data = self.get_actiontec_dataget_actiontec_data()
53  self.success_initsuccess_init = data is not None
54 
55  def scan_devices(self) -> list[str]:
56  """Scan for new devices and return a list with found device IDs."""
57  self._update_info_update_info()
58  return [client.mac_address for client in self.last_resultslast_results]
59 
60  def get_device_name(self, device: str) -> str | None:
61  """Return the name of the given device or None if we don't know."""
62  for client in self.last_resultslast_results:
63  if client.mac_address == device:
64  return client.ip_address
65  return None
66 
67  def _update_info(self) -> bool:
68  """Ensure the information from the router is up to date.
69 
70  Return boolean if scanning successful.
71  """
72  _LOGGER.debug("Scanning")
73  if not self.success_initsuccess_init:
74  return False
75 
76  if (actiontec_data := self.get_actiontec_dataget_actiontec_data()) is None:
77  return False
78  self.last_resultslast_results = [
79  device for device in actiontec_data if device.timevalid > -60
80  ]
81  _LOGGER.debug("Scan successful")
82  return True
83 
84  def get_actiontec_data(self) -> list[Device] | None:
85  """Retrieve data from Actiontec MI424WR and return parsed result."""
86  try:
87  telnet = telnetlib.Telnet(self.host)
88  telnet.read_until(b"Username: ")
89  telnet.write((f"{self.username}\n").encode("ascii"))
90  telnet.read_until(b"Password: ")
91  telnet.write((f"{self.password}\n").encode("ascii"))
92  prompt = telnet.read_until(b"Wireless Broadband Router> ").split(b"\n")[-1]
93  telnet.write(b"firewall mac_cache_dump\n")
94  telnet.write(b"\n")
95  telnet.read_until(prompt)
96  leases_result = telnet.read_until(prompt).split(b"\n")[1:-1]
97  telnet.write(b"exit\n")
98  except EOFError:
99  _LOGGER.exception("Unexpected response from router")
100  return None
101  except ConnectionRefusedError:
102  _LOGGER.exception("Connection refused by router. Telnet enabled?")
103  return None
104 
105  devices: list[Device] = []
106  for lease in leases_result:
107  match = LEASES_REGEX.search(lease.decode("utf-8"))
108  if match is not None:
109  devices.append(
110  Device(
111  match.group("ip"),
112  match.group("mac").upper(),
113  int(match.group("timevalid")),
114  )
115  )
116  return devices
ActiontecDeviceScanner|None get_scanner(HomeAssistant hass, ConfigType config)