Home Assistant Unofficial Reference 2024.12.1
device_tracker.py
Go to the documentation of this file.
1 """Support for Cisco IOS Routers."""
2 
3 from __future__ import annotations
4 
5 import logging
6 import re
7 
8 from pexpect import pxssh
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_PORT, 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 = vol.All(
24  DEVICE_TRACKER_PLATFORM_SCHEMA.extend(
25  {
26  vol.Required(CONF_HOST): cv.string,
27  vol.Required(CONF_USERNAME): cv.string,
28  vol.Optional(CONF_PASSWORD, default=""): cv.string,
29  vol.Optional(CONF_PORT): cv.port,
30  }
31  )
32 )
33 
34 
35 def get_scanner(hass: HomeAssistant, config: ConfigType) -> CiscoDeviceScanner | None:
36  """Validate the configuration and return a Cisco scanner."""
37  scanner = CiscoDeviceScanner(config[DEVICE_TRACKER_DOMAIN])
38 
39  return scanner if scanner.success_init else None
40 
41 
42 class CiscoDeviceScanner(DeviceScanner):
43  """Class which queries a wireless router running Cisco IOS firmware."""
44 
45  def __init__(self, config):
46  """Initialize the scanner."""
47  self.hosthost = config[CONF_HOST]
48  self.usernameusername = config[CONF_USERNAME]
49  self.portport = config.get(CONF_PORT)
50  self.passwordpassword = config[CONF_PASSWORD]
51 
52  self.last_resultslast_results = {}
53 
54  self.success_initsuccess_init = self._update_info_update_info()
55 
56  async def async_get_device_name(self, device: str) -> str | None:
57  """Get the firmware doesn't save the name of the wireless device."""
58  return None
59 
60  def scan_devices(self):
61  """Scan for new devices and return a list with found device IDs."""
62  self._update_info_update_info()
63 
64  return self.last_resultslast_results
65 
66  def _update_info(self):
67  """Ensure the information from the Cisco router is up to date.
68 
69  Returns boolean if scanning successful.
70  """
71  if string_result := self._get_arp_data_get_arp_data():
72  self.last_resultslast_results = []
73  last_results = []
74 
75  lines_result = string_result.splitlines()
76 
77  # Remove the first two lines, as they contains the arp command
78  # and the arp table titles e.g.
79  # show ip arp
80  # Protocol Address | Age (min) | Hardware Addr | Type | Interface
81  lines_result = lines_result[2:]
82 
83  for line in lines_result:
84  parts = line.split()
85  if len(parts) != 6:
86  continue
87 
88  # ['Internet', '10.10.11.1', '-', '0027.d32d.0123', 'ARPA',
89  # 'GigabitEthernet0']
90  age = parts[2]
91  hw_addr = parts[3]
92 
93  if age != "-":
94  mac = _parse_cisco_mac_address(hw_addr)
95  age = int(age)
96  if age < 1:
97  last_results.append(mac)
98 
99  self.last_resultslast_results = last_results
100  return True
101 
102  return False
103 
104  def _get_arp_data(self):
105  """Open connection to the router and get arp entries."""
106 
107  try:
108  cisco_ssh = pxssh.pxssh()
109  cisco_ssh.login(
110  self.hosthost,
111  self.usernameusername,
112  self.passwordpassword,
113  port=self.portport,
114  auto_prompt_reset=False,
115  )
116 
117  # Find the hostname
118  initial_line = cisco_ssh.before.decode("utf-8").splitlines()
119  router_hostname = initial_line[len(initial_line) - 1]
120  router_hostname += "#"
121  # Set the discovered hostname as prompt
122  regex_expression = f"(?i)^{router_hostname}".encode()
123  cisco_ssh.PROMPT = re.compile(regex_expression, re.MULTILINE)
124  # Allow full arp table to print at once
125  cisco_ssh.sendline("terminal length 0")
126  cisco_ssh.prompt(1)
127 
128  cisco_ssh.sendline("show ip arp")
129  cisco_ssh.prompt(1)
130 
131  devices_result = cisco_ssh.before
132 
133  return devices_result.decode("utf-8")
134  except pxssh.ExceptionPxssh as px_e:
135  _LOGGER.error("Failed to login via pxssh: %s", px_e)
136 
137  return None
138 
139 
140 def _parse_cisco_mac_address(cisco_hardware_addr):
141  """Parse a Cisco formatted HW address to normal MAC.
142 
143  e.g. convert
144  001d.ec02.07ab
145 
146  to:
147  00:1D:EC:02:07:AB
148 
149  Takes in cisco_hwaddr: HWAddr String from Cisco ARP table
150  Returns a regular standard MAC address
151  """
152  cisco_hardware_addr = cisco_hardware_addr.replace(".", "")
153  blocks = [
154  cisco_hardware_addr[x : x + 2] for x in range(0, len(cisco_hardware_addr), 2)
155  ]
156 
157  return ":".join(blocks).upper()
CiscoDeviceScanner|None get_scanner(HomeAssistant hass, ConfigType config)