mirror of https://github.com/home-assistant/core
477 lines
16 KiB
Python
477 lines
16 KiB
Python
"""aioasuswrt and pyasuswrt bridge classes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from collections import namedtuple
|
|
from collections.abc import Awaitable, Callable, Coroutine
|
|
from datetime import datetime
|
|
import functools
|
|
import logging
|
|
from typing import Any, cast
|
|
|
|
from aioasuswrt.asuswrt import AsusWrt as AsusWrtLegacy
|
|
from aiohttp import ClientSession
|
|
from pyasuswrt import AsusWrtError, AsusWrtHttp
|
|
from pyasuswrt.exceptions import AsusWrtNotAvailableInfoError
|
|
|
|
from homeassistant.const import (
|
|
CONF_HOST,
|
|
CONF_MODE,
|
|
CONF_PASSWORD,
|
|
CONF_PORT,
|
|
CONF_PROTOCOL,
|
|
CONF_USERNAME,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
from homeassistant.helpers.device_registry import format_mac
|
|
from homeassistant.helpers.update_coordinator import UpdateFailed
|
|
|
|
from .const import (
|
|
CONF_DNSMASQ,
|
|
CONF_INTERFACE,
|
|
CONF_REQUIRE_IP,
|
|
CONF_SSH_KEY,
|
|
DEFAULT_DNSMASQ,
|
|
DEFAULT_INTERFACE,
|
|
KEY_METHOD,
|
|
KEY_SENSORS,
|
|
PROTOCOL_HTTP,
|
|
PROTOCOL_HTTPS,
|
|
PROTOCOL_TELNET,
|
|
SENSORS_BYTES,
|
|
SENSORS_CPU,
|
|
SENSORS_LOAD_AVG,
|
|
SENSORS_MEMORY,
|
|
SENSORS_RATES,
|
|
SENSORS_TEMPERATURES,
|
|
SENSORS_TEMPERATURES_LEGACY,
|
|
SENSORS_UPTIME,
|
|
)
|
|
|
|
SENSORS_TYPE_BYTES = "sensors_bytes"
|
|
SENSORS_TYPE_COUNT = "sensors_count"
|
|
SENSORS_TYPE_CPU = "sensors_cpu"
|
|
SENSORS_TYPE_LOAD_AVG = "sensors_load_avg"
|
|
SENSORS_TYPE_MEMORY = "sensors_memory"
|
|
SENSORS_TYPE_RATES = "sensors_rates"
|
|
SENSORS_TYPE_TEMPERATURES = "sensors_temperatures"
|
|
SENSORS_TYPE_UPTIME = "sensors_uptime"
|
|
|
|
WrtDevice = namedtuple("WrtDevice", ["ip", "name", "connected_to"]) # noqa: PYI024
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
type _FuncType[_T] = Callable[[_T], Awaitable[list[Any] | tuple[Any] | dict[str, Any]]]
|
|
type _ReturnFuncType[_T] = Callable[[_T], Coroutine[Any, Any, dict[str, Any]]]
|
|
|
|
|
|
def handle_errors_and_zip[_AsusWrtBridgeT: AsusWrtBridge](
|
|
exceptions: type[Exception] | tuple[type[Exception], ...], keys: list[str] | None
|
|
) -> Callable[[_FuncType[_AsusWrtBridgeT]], _ReturnFuncType[_AsusWrtBridgeT]]:
|
|
"""Run library methods and zip results or manage exceptions."""
|
|
|
|
def _handle_errors_and_zip(
|
|
func: _FuncType[_AsusWrtBridgeT],
|
|
) -> _ReturnFuncType[_AsusWrtBridgeT]:
|
|
"""Run library methods and zip results or manage exceptions."""
|
|
|
|
@functools.wraps(func)
|
|
async def _wrapper(self: _AsusWrtBridgeT) -> dict[str, Any]:
|
|
try:
|
|
data = await func(self)
|
|
except exceptions as exc:
|
|
raise UpdateFailed(exc) from exc
|
|
|
|
if keys is None:
|
|
if not isinstance(data, dict):
|
|
raise UpdateFailed("Received invalid data type")
|
|
return data
|
|
|
|
if isinstance(data, dict):
|
|
return dict(zip(keys, list(data.values()), strict=False))
|
|
if not isinstance(data, (list, tuple)):
|
|
raise UpdateFailed("Received invalid data type")
|
|
return dict(zip(keys, data, strict=False))
|
|
|
|
return _wrapper
|
|
|
|
return _handle_errors_and_zip
|
|
|
|
|
|
class AsusWrtBridge(ABC):
|
|
"""The Base Bridge abstract class."""
|
|
|
|
@staticmethod
|
|
def get_bridge(
|
|
hass: HomeAssistant, conf: dict[str, Any], options: dict[str, Any] | None = None
|
|
) -> AsusWrtBridge:
|
|
"""Get Bridge instance."""
|
|
if conf[CONF_PROTOCOL] in (PROTOCOL_HTTPS, PROTOCOL_HTTP):
|
|
session = async_get_clientsession(hass)
|
|
return AsusWrtHttpBridge(conf, session)
|
|
return AsusWrtLegacyBridge(conf, options)
|
|
|
|
def __init__(self, host: str) -> None:
|
|
"""Initialize Bridge."""
|
|
self._host = host
|
|
self._firmware: str | None = None
|
|
self._label_mac: str | None = None
|
|
self._model: str | None = None
|
|
|
|
@property
|
|
def host(self) -> str:
|
|
"""Return hostname."""
|
|
return self._host
|
|
|
|
@property
|
|
def firmware(self) -> str | None:
|
|
"""Return firmware information."""
|
|
return self._firmware
|
|
|
|
@property
|
|
def label_mac(self) -> str | None:
|
|
"""Return label mac information."""
|
|
return self._label_mac
|
|
|
|
@property
|
|
def model(self) -> str | None:
|
|
"""Return model information."""
|
|
return self._model
|
|
|
|
@property
|
|
@abstractmethod
|
|
def is_connected(self) -> bool:
|
|
"""Get connected status."""
|
|
|
|
@abstractmethod
|
|
async def async_connect(self) -> None:
|
|
"""Connect to the device."""
|
|
|
|
@abstractmethod
|
|
async def async_disconnect(self) -> None:
|
|
"""Disconnect to the device."""
|
|
|
|
@abstractmethod
|
|
async def async_get_connected_devices(self) -> dict[str, WrtDevice]:
|
|
"""Get list of connected devices."""
|
|
|
|
@abstractmethod
|
|
async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]:
|
|
"""Return a dictionary of available sensors for this bridge."""
|
|
|
|
|
|
class AsusWrtLegacyBridge(AsusWrtBridge):
|
|
"""The Bridge that use legacy library."""
|
|
|
|
def __init__(
|
|
self, conf: dict[str, Any], options: dict[str, Any] | None = None
|
|
) -> None:
|
|
"""Initialize Bridge."""
|
|
super().__init__(conf[CONF_HOST])
|
|
self._protocol: str = conf[CONF_PROTOCOL]
|
|
self._api: AsusWrtLegacy = self._get_api(conf, options)
|
|
|
|
@staticmethod
|
|
def _get_api(
|
|
conf: dict[str, Any], options: dict[str, Any] | None = None
|
|
) -> AsusWrtLegacy:
|
|
"""Get the AsusWrtLegacy API."""
|
|
opt = options or {}
|
|
|
|
return AsusWrtLegacy(
|
|
conf[CONF_HOST],
|
|
conf.get(CONF_PORT),
|
|
conf[CONF_PROTOCOL] == PROTOCOL_TELNET,
|
|
conf[CONF_USERNAME],
|
|
conf.get(CONF_PASSWORD, ""),
|
|
conf.get(CONF_SSH_KEY, ""),
|
|
conf[CONF_MODE],
|
|
opt.get(CONF_REQUIRE_IP, True),
|
|
interface=opt.get(CONF_INTERFACE, DEFAULT_INTERFACE),
|
|
dnsmasq=opt.get(CONF_DNSMASQ, DEFAULT_DNSMASQ),
|
|
)
|
|
|
|
@property
|
|
def is_connected(self) -> bool:
|
|
"""Get connected status."""
|
|
return cast(bool, self._api.is_connected)
|
|
|
|
async def async_connect(self) -> None:
|
|
"""Connect to the device."""
|
|
await self._api.connection.async_connect()
|
|
|
|
# get main router properties
|
|
if self._label_mac is None:
|
|
await self._get_label_mac()
|
|
if self._firmware is None:
|
|
await self._get_firmware()
|
|
if self._model is None:
|
|
await self._get_model()
|
|
|
|
async def async_disconnect(self) -> None:
|
|
"""Disconnect to the device."""
|
|
if self._api is not None and self._protocol == PROTOCOL_TELNET:
|
|
self._api.connection.disconnect()
|
|
|
|
async def async_get_connected_devices(self) -> dict[str, WrtDevice]:
|
|
"""Get list of connected devices."""
|
|
api_devices = await self._api.async_get_connected_devices()
|
|
return {
|
|
format_mac(mac): WrtDevice(dev.ip, dev.name, None)
|
|
for mac, dev in api_devices.items()
|
|
}
|
|
|
|
async def _get_nvram_info(self, info_type: str) -> dict[str, Any]:
|
|
"""Get AsusWrt router info from nvram."""
|
|
info = {}
|
|
try:
|
|
info = await self._api.async_get_nvram(info_type)
|
|
except OSError as exc:
|
|
_LOGGER.warning(
|
|
"Error calling method async_get_nvram(%s): %s", info_type, exc
|
|
)
|
|
|
|
return info
|
|
|
|
async def _get_label_mac(self) -> None:
|
|
"""Get label mac information."""
|
|
label_mac = await self._get_nvram_info("LABEL_MAC")
|
|
if label_mac and "label_mac" in label_mac:
|
|
self._label_mac = format_mac(label_mac["label_mac"])
|
|
|
|
async def _get_firmware(self) -> None:
|
|
"""Get firmware information."""
|
|
firmware = await self._get_nvram_info("FIRMWARE")
|
|
if firmware and "firmver" in firmware:
|
|
firmver: str = firmware["firmver"]
|
|
if "buildno" in firmware:
|
|
firmver += f" (build {firmware['buildno']})"
|
|
self._firmware = firmver
|
|
|
|
async def _get_model(self) -> None:
|
|
"""Get model information."""
|
|
model = await self._get_nvram_info("MODEL")
|
|
if model and "model" in model:
|
|
self._model = model["model"]
|
|
|
|
async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]:
|
|
"""Return a dictionary of available sensors for this bridge."""
|
|
sensors_temperatures = await self._get_available_temperature_sensors()
|
|
return {
|
|
SENSORS_TYPE_BYTES: {
|
|
KEY_SENSORS: SENSORS_BYTES,
|
|
KEY_METHOD: self._get_bytes,
|
|
},
|
|
SENSORS_TYPE_LOAD_AVG: {
|
|
KEY_SENSORS: SENSORS_LOAD_AVG,
|
|
KEY_METHOD: self._get_load_avg,
|
|
},
|
|
SENSORS_TYPE_RATES: {
|
|
KEY_SENSORS: SENSORS_RATES,
|
|
KEY_METHOD: self._get_rates,
|
|
},
|
|
SENSORS_TYPE_TEMPERATURES: {
|
|
KEY_SENSORS: sensors_temperatures,
|
|
KEY_METHOD: self._get_temperatures,
|
|
},
|
|
}
|
|
|
|
async def _get_available_temperature_sensors(self) -> list[str]:
|
|
"""Check which temperature information is available on the router."""
|
|
availability = await self._api.async_find_temperature_commands()
|
|
return [SENSORS_TEMPERATURES_LEGACY[i] for i in range(3) if availability[i]]
|
|
|
|
@handle_errors_and_zip((IndexError, OSError, ValueError), SENSORS_BYTES)
|
|
async def _get_bytes(self) -> Any:
|
|
"""Fetch byte information from the router."""
|
|
return await self._api.async_get_bytes_total()
|
|
|
|
@handle_errors_and_zip((IndexError, OSError, ValueError), SENSORS_RATES)
|
|
async def _get_rates(self) -> Any:
|
|
"""Fetch rates information from the router."""
|
|
return await self._api.async_get_current_transfer_rates()
|
|
|
|
@handle_errors_and_zip((IndexError, OSError, ValueError), SENSORS_LOAD_AVG)
|
|
async def _get_load_avg(self) -> Any:
|
|
"""Fetch load average information from the router."""
|
|
return await self._api.async_get_loadavg()
|
|
|
|
@handle_errors_and_zip((OSError, ValueError), None)
|
|
async def _get_temperatures(self) -> Any:
|
|
"""Fetch temperatures information from the router."""
|
|
return await self._api.async_get_temperature()
|
|
|
|
|
|
class AsusWrtHttpBridge(AsusWrtBridge):
|
|
"""The Bridge that use HTTP library."""
|
|
|
|
def __init__(self, conf: dict[str, Any], session: ClientSession) -> None:
|
|
"""Initialize Bridge that use HTTP library."""
|
|
super().__init__(conf[CONF_HOST])
|
|
self._api: AsusWrtHttp = self._get_api(conf, session)
|
|
|
|
@staticmethod
|
|
def _get_api(conf: dict[str, Any], session: ClientSession) -> AsusWrtHttp:
|
|
"""Get the AsusWrtHttp API."""
|
|
return AsusWrtHttp(
|
|
conf[CONF_HOST],
|
|
conf[CONF_USERNAME],
|
|
conf.get(CONF_PASSWORD, ""),
|
|
use_https=conf[CONF_PROTOCOL] == PROTOCOL_HTTPS,
|
|
port=conf.get(CONF_PORT),
|
|
session=session,
|
|
)
|
|
|
|
@property
|
|
def is_connected(self) -> bool:
|
|
"""Get connected status."""
|
|
return cast(bool, self._api.is_connected)
|
|
|
|
async def async_connect(self) -> None:
|
|
"""Connect to the device."""
|
|
await self._api.async_connect()
|
|
|
|
# get main router properties
|
|
if mac := self._api.mac:
|
|
self._label_mac = format_mac(mac)
|
|
self._firmware = self._api.firmware
|
|
self._model = self._api.model
|
|
|
|
async def async_disconnect(self) -> None:
|
|
"""Disconnect to the device."""
|
|
await self._api.async_disconnect()
|
|
|
|
async def async_get_connected_devices(self) -> dict[str, WrtDevice]:
|
|
"""Get list of connected devices."""
|
|
api_devices = await self._api.async_get_connected_devices()
|
|
return {
|
|
format_mac(mac): WrtDevice(dev.ip, dev.name, dev.node)
|
|
for mac, dev in api_devices.items()
|
|
}
|
|
|
|
async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]:
|
|
"""Return a dictionary of available sensors for this bridge."""
|
|
sensors_cpu = await self._get_available_cpu_sensors()
|
|
sensors_temperatures = await self._get_available_temperature_sensors()
|
|
sensors_loadavg = await self._get_loadavg_sensors_availability()
|
|
return {
|
|
SENSORS_TYPE_BYTES: {
|
|
KEY_SENSORS: SENSORS_BYTES,
|
|
KEY_METHOD: self._get_bytes,
|
|
},
|
|
SENSORS_TYPE_CPU: {
|
|
KEY_SENSORS: sensors_cpu,
|
|
KEY_METHOD: self._get_cpu_usage,
|
|
},
|
|
SENSORS_TYPE_LOAD_AVG: {
|
|
KEY_SENSORS: sensors_loadavg,
|
|
KEY_METHOD: self._get_load_avg,
|
|
},
|
|
SENSORS_TYPE_MEMORY: {
|
|
KEY_SENSORS: SENSORS_MEMORY,
|
|
KEY_METHOD: self._get_memory_usage,
|
|
},
|
|
SENSORS_TYPE_RATES: {
|
|
KEY_SENSORS: SENSORS_RATES,
|
|
KEY_METHOD: self._get_rates,
|
|
},
|
|
SENSORS_TYPE_UPTIME: {
|
|
KEY_SENSORS: SENSORS_UPTIME,
|
|
KEY_METHOD: self._get_uptime,
|
|
},
|
|
SENSORS_TYPE_TEMPERATURES: {
|
|
KEY_SENSORS: sensors_temperatures,
|
|
KEY_METHOD: self._get_temperatures,
|
|
},
|
|
}
|
|
|
|
async def _get_available_cpu_sensors(self) -> list[str]:
|
|
"""Check which cpu information is available on the router."""
|
|
try:
|
|
available_cpu = await self._api.async_get_cpu_usage()
|
|
available_sensors = [t for t in SENSORS_CPU if t in available_cpu]
|
|
except AsusWrtError as exc:
|
|
_LOGGER.warning(
|
|
(
|
|
"Failed checking cpu sensor availability for ASUS router"
|
|
" %s. Exception: %s"
|
|
),
|
|
self.host,
|
|
exc,
|
|
)
|
|
return []
|
|
return available_sensors
|
|
|
|
async def _get_available_temperature_sensors(self) -> list[str]:
|
|
"""Check which temperature information is available on the router."""
|
|
try:
|
|
available_temps = await self._api.async_get_temperatures()
|
|
available_sensors = [
|
|
t for t in SENSORS_TEMPERATURES if t in available_temps
|
|
]
|
|
except AsusWrtError as exc:
|
|
_LOGGER.warning(
|
|
(
|
|
"Failed checking temperature sensor availability for ASUS router"
|
|
" %s. Exception: %s"
|
|
),
|
|
self.host,
|
|
exc,
|
|
)
|
|
return []
|
|
return available_sensors
|
|
|
|
async def _get_loadavg_sensors_availability(self) -> list[str]:
|
|
"""Check if load avg is available on the router."""
|
|
try:
|
|
await self._api.async_get_loadavg()
|
|
except AsusWrtNotAvailableInfoError:
|
|
return []
|
|
except AsusWrtError:
|
|
pass
|
|
return SENSORS_LOAD_AVG
|
|
|
|
@handle_errors_and_zip(AsusWrtError, SENSORS_BYTES)
|
|
async def _get_bytes(self) -> Any:
|
|
"""Fetch byte information from the router."""
|
|
return await self._api.async_get_traffic_bytes()
|
|
|
|
@handle_errors_and_zip(AsusWrtError, SENSORS_RATES)
|
|
async def _get_rates(self) -> Any:
|
|
"""Fetch rates information from the router."""
|
|
return await self._api.async_get_traffic_rates()
|
|
|
|
@handle_errors_and_zip(AsusWrtError, SENSORS_LOAD_AVG)
|
|
async def _get_load_avg(self) -> Any:
|
|
"""Fetch cpu load avg information from the router."""
|
|
return await self._api.async_get_loadavg()
|
|
|
|
@handle_errors_and_zip(AsusWrtError, None)
|
|
async def _get_temperatures(self) -> Any:
|
|
"""Fetch temperatures information from the router."""
|
|
return await self._api.async_get_temperatures()
|
|
|
|
@handle_errors_and_zip(AsusWrtError, None)
|
|
async def _get_cpu_usage(self) -> Any:
|
|
"""Fetch cpu information from the router."""
|
|
return await self._api.async_get_cpu_usage()
|
|
|
|
@handle_errors_and_zip(AsusWrtError, None)
|
|
async def _get_memory_usage(self) -> Any:
|
|
"""Fetch memory information from the router."""
|
|
return await self._api.async_get_memory_usage()
|
|
|
|
async def _get_uptime(self) -> dict[str, Any]:
|
|
"""Fetch uptime from the router."""
|
|
try:
|
|
uptimes = await self._api.async_get_uptime()
|
|
except AsusWrtError as exc:
|
|
raise UpdateFailed(exc) from exc
|
|
|
|
last_boot = datetime.fromisoformat(uptimes["last_boot"])
|
|
uptime = uptimes["uptime"]
|
|
|
|
return dict(zip(SENSORS_UPTIME, [last_boot, uptime], strict=False))
|