Home Assistant Unofficial Reference 2024.12.1
bridge.py
Go to the documentation of this file.
1 """aioasuswrt and pyasuswrt bridge classes."""
2 
3 from __future__ import annotations
4 
5 from abc import ABC, abstractmethod
6 from collections import namedtuple
7 from collections.abc import Awaitable, Callable, Coroutine
8 from datetime import datetime
9 import functools
10 import logging
11 from typing import Any, cast
12 
13 from aioasuswrt.asuswrt import AsusWrt as AsusWrtLegacy
14 from aiohttp import ClientSession
15 from pyasuswrt import AsusWrtError, AsusWrtHttp
16 from pyasuswrt.exceptions import AsusWrtNotAvailableInfoError
17 
18 from homeassistant.const import (
19  CONF_HOST,
20  CONF_MODE,
21  CONF_PASSWORD,
22  CONF_PORT,
23  CONF_PROTOCOL,
24  CONF_USERNAME,
25 )
26 from homeassistant.core import HomeAssistant
27 from homeassistant.helpers.aiohttp_client import async_get_clientsession
28 from homeassistant.helpers.device_registry import format_mac
29 from homeassistant.helpers.update_coordinator import UpdateFailed
30 
31 from .const import (
32  CONF_DNSMASQ,
33  CONF_INTERFACE,
34  CONF_REQUIRE_IP,
35  CONF_SSH_KEY,
36  DEFAULT_DNSMASQ,
37  DEFAULT_INTERFACE,
38  KEY_METHOD,
39  KEY_SENSORS,
40  PROTOCOL_HTTP,
41  PROTOCOL_HTTPS,
42  PROTOCOL_TELNET,
43  SENSORS_BYTES,
44  SENSORS_CPU,
45  SENSORS_LOAD_AVG,
46  SENSORS_MEMORY,
47  SENSORS_RATES,
48  SENSORS_TEMPERATURES,
49  SENSORS_TEMPERATURES_LEGACY,
50  SENSORS_UPTIME,
51 )
52 
53 SENSORS_TYPE_BYTES = "sensors_bytes"
54 SENSORS_TYPE_COUNT = "sensors_count"
55 SENSORS_TYPE_CPU = "sensors_cpu"
56 SENSORS_TYPE_LOAD_AVG = "sensors_load_avg"
57 SENSORS_TYPE_MEMORY = "sensors_memory"
58 SENSORS_TYPE_RATES = "sensors_rates"
59 SENSORS_TYPE_TEMPERATURES = "sensors_temperatures"
60 SENSORS_TYPE_UPTIME = "sensors_uptime"
61 
62 WrtDevice = namedtuple("WrtDevice", ["ip", "name", "connected_to"]) # noqa: PYI024
63 
64 _LOGGER = logging.getLogger(__name__)
65 
66 type _FuncType[_T] = Callable[[_T], Awaitable[list[Any] | tuple[Any] | dict[str, Any]]]
67 type _ReturnFuncType[_T] = Callable[[_T], Coroutine[Any, Any, dict[str, Any]]]
68 
69 
70 def handle_errors_and_zip[_AsusWrtBridgeT: AsusWrtBridge](
71  exceptions: type[Exception] | tuple[type[Exception], ...], keys: list[str] | None
72 ) -> Callable[[_FuncType[_AsusWrtBridgeT]], _ReturnFuncType[_AsusWrtBridgeT]]:
73  """Run library methods and zip results or manage exceptions."""
74 
75  def _handle_errors_and_zip(
76  func: _FuncType[_AsusWrtBridgeT],
77  ) -> _ReturnFuncType[_AsusWrtBridgeT]:
78  """Run library methods and zip results or manage exceptions."""
79 
80  @functools.wraps(func)
81  async def _wrapper(self: _AsusWrtBridgeT) -> dict[str, Any]:
82  try:
83  data = await func(self)
84  except exceptions as exc:
85  raise UpdateFailed(exc) from exc
86 
87  if keys is None:
88  if not isinstance(data, dict):
89  raise UpdateFailed("Received invalid data type")
90  return data
91 
92  if isinstance(data, dict):
93  return dict(zip(keys, list(data.values()), strict=False))
94  if not isinstance(data, (list, tuple)):
95  raise UpdateFailed("Received invalid data type")
96  return dict(zip(keys, data, strict=False))
97 
98  return _wrapper
99 
100  return _handle_errors_and_zip
101 
102 
103 class AsusWrtBridge(ABC):
104  """The Base Bridge abstract class."""
105 
106  @staticmethod
108  hass: HomeAssistant, conf: dict[str, Any], options: dict[str, Any] | None = None
109  ) -> AsusWrtBridge:
110  """Get Bridge instance."""
111  if conf[CONF_PROTOCOL] in (PROTOCOL_HTTPS, PROTOCOL_HTTP):
112  session = async_get_clientsession(hass)
113  return AsusWrtHttpBridge(conf, session)
114  return AsusWrtLegacyBridge(conf, options)
115 
116  def __init__(self, host: str) -> None:
117  """Initialize Bridge."""
118  self._host_host = host
119  self._firmware: str | None = None
120  self._label_mac: str | None = None
121  self._model: str | None = None
122 
123  @property
124  def host(self) -> str:
125  """Return hostname."""
126  return self._host_host
127 
128  @property
129  def firmware(self) -> str | None:
130  """Return firmware information."""
131  return self._firmware
132 
133  @property
134  def label_mac(self) -> str | None:
135  """Return label mac information."""
136  return self._label_mac
137 
138  @property
139  def model(self) -> str | None:
140  """Return model information."""
141  return self._model
142 
143  @property
144  @abstractmethod
145  def is_connected(self) -> bool:
146  """Get connected status."""
147 
148  @abstractmethod
149  async def async_connect(self) -> None:
150  """Connect to the device."""
151 
152  @abstractmethod
153  async def async_disconnect(self) -> None:
154  """Disconnect to the device."""
155 
156  @abstractmethod
157  async def async_get_connected_devices(self) -> dict[str, WrtDevice]:
158  """Get list of connected devices."""
159 
160  @abstractmethod
161  async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]:
162  """Return a dictionary of available sensors for this bridge."""
163 
164 
165 class AsusWrtLegacyBridge(AsusWrtBridge):
166  """The Bridge that use legacy library."""
167 
168  def __init__(
169  self, conf: dict[str, Any], options: dict[str, Any] | None = None
170  ) -> None:
171  """Initialize Bridge."""
172  super().__init__(conf[CONF_HOST])
173  self._protocol_protocol: str = conf[CONF_PROTOCOL]
174  self._api: AsusWrtLegacy = self._get_api_get_api(conf, options)
175 
176  @staticmethod
177  def _get_api(
178  conf: dict[str, Any], options: dict[str, Any] | None = None
179  ) -> AsusWrtLegacy:
180  """Get the AsusWrtLegacy API."""
181  opt = options or {}
182 
183  return AsusWrtLegacy(
184  conf[CONF_HOST],
185  conf.get(CONF_PORT),
186  conf[CONF_PROTOCOL] == PROTOCOL_TELNET,
187  conf[CONF_USERNAME],
188  conf.get(CONF_PASSWORD, ""),
189  conf.get(CONF_SSH_KEY, ""),
190  conf[CONF_MODE],
191  opt.get(CONF_REQUIRE_IP, True),
192  interface=opt.get(CONF_INTERFACE, DEFAULT_INTERFACE),
193  dnsmasq=opt.get(CONF_DNSMASQ, DEFAULT_DNSMASQ),
194  )
195 
196  @property
197  def is_connected(self) -> bool:
198  """Get connected status."""
199  return cast(bool, self._api.is_connected)
200 
201  async def async_connect(self) -> None:
202  """Connect to the device."""
203  await self._api.connection.async_connect()
204 
205  # get main router properties
206  if self._label_mac_label_mac is None:
207  await self._get_label_mac_get_label_mac()
208  if self._firmware_firmware is None:
209  await self._get_firmware_get_firmware()
210  if self._model_model is None:
211  await self._get_model_get_model()
212 
213  async def async_disconnect(self) -> None:
214  """Disconnect to the device."""
215  if self._api is not None and self._protocol_protocol == PROTOCOL_TELNET:
216  self._api.connection.disconnect()
217 
218  async def async_get_connected_devices(self) -> dict[str, WrtDevice]:
219  """Get list of connected devices."""
220  api_devices = await self._api.async_get_connected_devices()
221  return {
222  format_mac(mac): WrtDevice(dev.ip, dev.name, None)
223  for mac, dev in api_devices.items()
224  }
225 
226  async def _get_nvram_info(self, info_type: str) -> dict[str, Any]:
227  """Get AsusWrt router info from nvram."""
228  info = {}
229  try:
230  info = await self._api.async_get_nvram(info_type)
231  except OSError as exc:
232  _LOGGER.warning(
233  "Error calling method async_get_nvram(%s): %s", info_type, exc
234  )
235 
236  return info
237 
238  async def _get_label_mac(self) -> None:
239  """Get label mac information."""
240  label_mac = await self._get_nvram_info_get_nvram_info("LABEL_MAC")
241  if label_mac and "label_mac" in label_mac:
242  self._label_mac_label_mac = format_mac(label_mac["label_mac"])
243 
244  async def _get_firmware(self) -> None:
245  """Get firmware information."""
246  firmware = await self._get_nvram_info_get_nvram_info("FIRMWARE")
247  if firmware and "firmver" in firmware:
248  firmver: str = firmware["firmver"]
249  if "buildno" in firmware:
250  firmver += f" (build {firmware['buildno']})"
251  self._firmware_firmware = firmver
252 
253  async def _get_model(self) -> None:
254  """Get model information."""
255  model = await self._get_nvram_info_get_nvram_info("MODEL")
256  if model and "model" in model:
257  self._model_model = model["model"]
258 
259  async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]:
260  """Return a dictionary of available sensors for this bridge."""
261  sensors_temperatures = await self._get_available_temperature_sensors_get_available_temperature_sensors()
262  return {
263  SENSORS_TYPE_BYTES: {
264  KEY_SENSORS: SENSORS_BYTES,
265  KEY_METHOD: self._get_bytes_get_bytes,
266  },
267  SENSORS_TYPE_LOAD_AVG: {
268  KEY_SENSORS: SENSORS_LOAD_AVG,
269  KEY_METHOD: self._get_load_avg_get_load_avg,
270  },
271  SENSORS_TYPE_RATES: {
272  KEY_SENSORS: SENSORS_RATES,
273  KEY_METHOD: self._get_rates_get_rates,
274  },
275  SENSORS_TYPE_TEMPERATURES: {
276  KEY_SENSORS: sensors_temperatures,
277  KEY_METHOD: self._get_temperatures_get_temperatures,
278  },
279  }
280 
281  async def _get_available_temperature_sensors(self) -> list[str]:
282  """Check which temperature information is available on the router."""
283  availability = await self._api.async_find_temperature_commands()
284  return [SENSORS_TEMPERATURES_LEGACY[i] for i in range(3) if availability[i]]
285 
286  @handle_errors_and_zip((IndexError, OSError, ValueError), SENSORS_BYTES)
287  async def _get_bytes(self) -> Any:
288  """Fetch byte information from the router."""
289  return await self._api.async_get_bytes_total()
290 
291  @handle_errors_and_zip((IndexError, OSError, ValueError), SENSORS_RATES)
292  async def _get_rates(self) -> Any:
293  """Fetch rates information from the router."""
294  return await self._api.async_get_current_transfer_rates()
295 
296  @handle_errors_and_zip((IndexError, OSError, ValueError), SENSORS_LOAD_AVG)
297  async def _get_load_avg(self) -> Any:
298  """Fetch load average information from the router."""
299  return await self._api.async_get_loadavg()
300 
301  @handle_errors_and_zip((OSError, ValueError), None)
302  async def _get_temperatures(self) -> Any:
303  """Fetch temperatures information from the router."""
304  return await self._api.async_get_temperature()
305 
306 
308  """The Bridge that use HTTP library."""
309 
310  def __init__(self, conf: dict[str, Any], session: ClientSession) -> None:
311  """Initialize Bridge that use HTTP library."""
312  super().__init__(conf[CONF_HOST])
313  self._api: AsusWrtHttp = self._get_api_get_api(conf, session)
314 
315  @staticmethod
316  def _get_api(conf: dict[str, Any], session: ClientSession) -> AsusWrtHttp:
317  """Get the AsusWrtHttp API."""
318  return AsusWrtHttp(
319  conf[CONF_HOST],
320  conf[CONF_USERNAME],
321  conf.get(CONF_PASSWORD, ""),
322  use_https=conf[CONF_PROTOCOL] == PROTOCOL_HTTPS,
323  port=conf.get(CONF_PORT),
324  session=session,
325  )
326 
327  @property
328  def is_connected(self) -> bool:
329  """Get connected status."""
330  return cast(bool, self._api.is_connected)
331 
332  async def async_connect(self) -> None:
333  """Connect to the device."""
334  await self._api.async_connect()
335 
336  # get main router properties
337  if mac := self._api.mac:
338  self._label_mac_label_mac = format_mac(mac)
339  self._firmware_firmware = self._api.firmware
340  self._model_model = self._api.model
341 
342  async def async_disconnect(self) -> None:
343  """Disconnect to the device."""
344  await self._api.async_disconnect()
345 
346  async def async_get_connected_devices(self) -> dict[str, WrtDevice]:
347  """Get list of connected devices."""
348  api_devices = await self._api.async_get_connected_devices()
349  return {
350  format_mac(mac): WrtDevice(dev.ip, dev.name, dev.node)
351  for mac, dev in api_devices.items()
352  }
353 
354  async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]:
355  """Return a dictionary of available sensors for this bridge."""
356  sensors_cpu = await self._get_available_cpu_sensors_get_available_cpu_sensors()
357  sensors_temperatures = await self._get_available_temperature_sensors_get_available_temperature_sensors()
358  sensors_loadavg = await self._get_loadavg_sensors_availability_get_loadavg_sensors_availability()
359  return {
360  SENSORS_TYPE_BYTES: {
361  KEY_SENSORS: SENSORS_BYTES,
362  KEY_METHOD: self._get_bytes_get_bytes,
363  },
364  SENSORS_TYPE_CPU: {
365  KEY_SENSORS: sensors_cpu,
366  KEY_METHOD: self._get_cpu_usage_get_cpu_usage,
367  },
368  SENSORS_TYPE_LOAD_AVG: {
369  KEY_SENSORS: sensors_loadavg,
370  KEY_METHOD: self._get_load_avg_get_load_avg,
371  },
372  SENSORS_TYPE_MEMORY: {
373  KEY_SENSORS: SENSORS_MEMORY,
374  KEY_METHOD: self._get_memory_usage_get_memory_usage,
375  },
376  SENSORS_TYPE_RATES: {
377  KEY_SENSORS: SENSORS_RATES,
378  KEY_METHOD: self._get_rates_get_rates,
379  },
380  SENSORS_TYPE_UPTIME: {
381  KEY_SENSORS: SENSORS_UPTIME,
382  KEY_METHOD: self._get_uptime_get_uptime,
383  },
384  SENSORS_TYPE_TEMPERATURES: {
385  KEY_SENSORS: sensors_temperatures,
386  KEY_METHOD: self._get_temperatures_get_temperatures,
387  },
388  }
389 
390  async def _get_available_cpu_sensors(self) -> list[str]:
391  """Check which cpu information is available on the router."""
392  try:
393  available_cpu = await self._api.async_get_cpu_usage()
394  available_sensors = [t for t in SENSORS_CPU if t in available_cpu]
395  except AsusWrtError as exc:
396  _LOGGER.warning(
397  (
398  "Failed checking cpu sensor availability for ASUS router"
399  " %s. Exception: %s"
400  ),
401  self.hosthost,
402  exc,
403  )
404  return []
405  return available_sensors
406 
407  async def _get_available_temperature_sensors(self) -> list[str]:
408  """Check which temperature information is available on the router."""
409  try:
410  available_temps = await self._api.async_get_temperatures()
411  available_sensors = [
412  t for t in SENSORS_TEMPERATURES if t in available_temps
413  ]
414  except AsusWrtError as exc:
415  _LOGGER.warning(
416  (
417  "Failed checking temperature sensor availability for ASUS router"
418  " %s. Exception: %s"
419  ),
420  self.hosthost,
421  exc,
422  )
423  return []
424  return available_sensors
425 
426  async def _get_loadavg_sensors_availability(self) -> list[str]:
427  """Check if load avg is available on the router."""
428  try:
429  await self._api.async_get_loadavg()
430  except AsusWrtNotAvailableInfoError:
431  return []
432  except AsusWrtError:
433  pass
434  return SENSORS_LOAD_AVG
435 
436  @handle_errors_and_zip(AsusWrtError, SENSORS_BYTES)
437  async def _get_bytes(self) -> Any:
438  """Fetch byte information from the router."""
439  return await self._api.async_get_traffic_bytes()
440 
441  @handle_errors_and_zip(AsusWrtError, SENSORS_RATES)
442  async def _get_rates(self) -> Any:
443  """Fetch rates information from the router."""
444  return await self._api.async_get_traffic_rates()
445 
446  @handle_errors_and_zip(AsusWrtError, SENSORS_LOAD_AVG)
447  async def _get_load_avg(self) -> Any:
448  """Fetch cpu load avg information from the router."""
449  return await self._api.async_get_loadavg()
450 
451  @handle_errors_and_zip(AsusWrtError, None)
452  async def _get_temperatures(self) -> Any:
453  """Fetch temperatures information from the router."""
454  return await self._api.async_get_temperatures()
455 
456  @handle_errors_and_zip(AsusWrtError, None)
457  async def _get_cpu_usage(self) -> Any:
458  """Fetch cpu information from the router."""
459  return await self._api.async_get_cpu_usage()
460 
461  @handle_errors_and_zip(AsusWrtError, None)
462  async def _get_memory_usage(self) -> Any:
463  """Fetch memory information from the router."""
464  return await self._api.async_get_memory_usage()
465 
466  async def _get_uptime(self) -> dict[str, Any]:
467  """Fetch uptime from the router."""
468  try:
469  uptimes = await self._api.async_get_uptime()
470  except AsusWrtError as exc:
471  raise UpdateFailed(exc) from exc
472 
473  last_boot = datetime.fromisoformat(uptimes["last_boot"])
474  uptime = uptimes["uptime"]
475 
476  return dict(zip(SENSORS_UPTIME, [last_boot, uptime], strict=False))
dict[str, WrtDevice] async_get_connected_devices(self)
Definition: bridge.py:157
dict[str, dict[str, Any]] async_get_available_sensors(self)
Definition: bridge.py:161
AsusWrtBridge get_bridge(HomeAssistant hass, dict[str, Any] conf, dict[str, Any]|None options=None)
Definition: bridge.py:109
None __init__(self, dict[str, Any] conf, ClientSession session)
Definition: bridge.py:310
dict[str, dict[str, Any]] async_get_available_sensors(self)
Definition: bridge.py:354
dict[str, WrtDevice] async_get_connected_devices(self)
Definition: bridge.py:346
AsusWrtHttp _get_api(dict[str, Any] conf, ClientSession session)
Definition: bridge.py:316
dict[str, Any] _get_nvram_info(self, str info_type)
Definition: bridge.py:226
None __init__(self, dict[str, Any] conf, dict[str, Any]|None options=None)
Definition: bridge.py:170
dict[str, dict[str, Any]] async_get_available_sensors(self)
Definition: bridge.py:259
AsusWrtLegacy _get_api(dict[str, Any] conf, dict[str, Any]|None options=None)
Definition: bridge.py:179
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)