Switch asuswrt http(s) library to asusrouter package (#150426)

This commit is contained in:
Yevhenii Vaskivskyi
2025-08-12 20:45:39 +02:00
committed by GitHub
parent 4d426c31f9
commit 9fdc632780
13 changed files with 414 additions and 171 deletions

4
CODEOWNERS generated
View File

@@ -156,8 +156,8 @@ build.json @home-assistant/supervisor
/tests/components/assist_pipeline/ @balloob @synesthesiam /tests/components/assist_pipeline/ @balloob @synesthesiam
/homeassistant/components/assist_satellite/ @home-assistant/core @synesthesiam /homeassistant/components/assist_satellite/ @home-assistant/core @synesthesiam
/tests/components/assist_satellite/ @home-assistant/core @synesthesiam /tests/components/assist_satellite/ @home-assistant/core @synesthesiam
/homeassistant/components/asuswrt/ @kennedyshead @ollo69 /homeassistant/components/asuswrt/ @kennedyshead @ollo69 @Vaskivskyi
/tests/components/asuswrt/ @kennedyshead @ollo69 /tests/components/asuswrt/ @kennedyshead @ollo69 @Vaskivskyi
/homeassistant/components/atag/ @MatsNL /homeassistant/components/atag/ @MatsNL
/tests/components/atag/ @MatsNL /tests/components/atag/ @MatsNL
/homeassistant/components/aten_pe/ @mtdcr /homeassistant/components/aten_pe/ @mtdcr

View File

@@ -5,15 +5,16 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections import namedtuple from collections import namedtuple
from collections.abc import Awaitable, Callable, Coroutine from collections.abc import Awaitable, Callable, Coroutine
from datetime import datetime
import functools import functools
import logging import logging
from typing import Any, cast from typing import Any, cast
from aioasuswrt.asuswrt import AsusWrt as AsusWrtLegacy from aioasuswrt.asuswrt import AsusWrt as AsusWrtLegacy
from aiohttp import ClientSession from aiohttp import ClientSession
from pyasuswrt import AsusWrtError, AsusWrtHttp from asusrouter import AsusRouter, AsusRouterError
from pyasuswrt.exceptions import AsusWrtNotAvailableInfoError from asusrouter.modules.client import AsusClient
from asusrouter.modules.data import AsusData
from asusrouter.modules.homeassistant import convert_to_ha_data, convert_to_ha_sensors
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
@@ -41,14 +42,13 @@ from .const import (
PROTOCOL_HTTPS, PROTOCOL_HTTPS,
PROTOCOL_TELNET, PROTOCOL_TELNET,
SENSORS_BYTES, SENSORS_BYTES,
SENSORS_CPU,
SENSORS_LOAD_AVG, SENSORS_LOAD_AVG,
SENSORS_MEMORY, SENSORS_MEMORY,
SENSORS_RATES, SENSORS_RATES,
SENSORS_TEMPERATURES,
SENSORS_TEMPERATURES_LEGACY, SENSORS_TEMPERATURES_LEGACY,
SENSORS_UPTIME, SENSORS_UPTIME,
) )
from .helpers import clean_dict, translate_to_legacy
SENSORS_TYPE_BYTES = "sensors_bytes" SENSORS_TYPE_BYTES = "sensors_bytes"
SENSORS_TYPE_COUNT = "sensors_count" SENSORS_TYPE_COUNT = "sensors_count"
@@ -310,16 +310,16 @@ class AsusWrtHttpBridge(AsusWrtBridge):
def __init__(self, conf: dict[str, Any], session: ClientSession) -> None: def __init__(self, conf: dict[str, Any], session: ClientSession) -> None:
"""Initialize Bridge that use HTTP library.""" """Initialize Bridge that use HTTP library."""
super().__init__(conf[CONF_HOST]) super().__init__(conf[CONF_HOST])
self._api: AsusWrtHttp = self._get_api(conf, session) self._api = self._get_api(conf, session)
@staticmethod @staticmethod
def _get_api(conf: dict[str, Any], session: ClientSession) -> AsusWrtHttp: def _get_api(conf: dict[str, Any], session: ClientSession) -> AsusRouter:
"""Get the AsusWrtHttp API.""" """Get the AsusRouter API."""
return AsusWrtHttp( return AsusRouter(
conf[CONF_HOST], hostname=conf[CONF_HOST],
conf[CONF_USERNAME], username=conf[CONF_USERNAME],
conf.get(CONF_PASSWORD, ""), password=conf.get(CONF_PASSWORD, ""),
use_https=conf[CONF_PROTOCOL] == PROTOCOL_HTTPS, use_ssl=conf[CONF_PROTOCOL] == PROTOCOL_HTTPS,
port=conf.get(CONF_PORT), port=conf.get(CONF_PORT),
session=session, session=session,
) )
@@ -327,46 +327,90 @@ class AsusWrtHttpBridge(AsusWrtBridge):
@property @property
def is_connected(self) -> bool: def is_connected(self) -> bool:
"""Get connected status.""" """Get connected status."""
return cast(bool, self._api.is_connected) return self._api.connected
async def async_connect(self) -> None: async def async_connect(self) -> None:
"""Connect to the device.""" """Connect to the device."""
await self._api.async_connect() await self._api.async_connect()
# Collect the identity
_identity = await self._api.async_get_identity()
# get main router properties # get main router properties
if mac := self._api.mac: if mac := _identity.mac:
self._label_mac = format_mac(mac) self._label_mac = format_mac(mac)
self._firmware = self._api.firmware self._firmware = str(_identity.firmware)
self._model = self._api.model self._model = _identity.model
async def async_disconnect(self) -> None: async def async_disconnect(self) -> None:
"""Disconnect to the device.""" """Disconnect to the device."""
await self._api.async_disconnect() await self._api.async_disconnect()
async def _get_data(
self,
datatype: AsusData,
force: bool = False,
) -> dict[str, Any]:
"""Get data from the device.
This is a generic method which automatically converts to
the Home Assistant-compatible format.
"""
try:
raw = await self._api.async_get_data(datatype, force=force)
return translate_to_legacy(clean_dict(convert_to_ha_data(raw)))
except AsusRouterError as ex:
raise UpdateFailed(ex) from ex
async def _get_sensors(self, datatype: AsusData) -> list[str]:
"""Get the available sensors.
This is a generic method which automatically converts to
the Home Assistant-compatible format.
"""
sensors = []
try:
data = await self._api.async_get_data(datatype)
# Get the list of sensors from the raw data
# and translate in to the legacy format
sensors = translate_to_legacy(convert_to_ha_sensors(data, datatype))
_LOGGER.debug("Available `%s` sensors: %s", datatype.value, sensors)
except AsusRouterError as ex:
_LOGGER.warning(
"Cannot get available `%s` sensors with exception: %s",
datatype.value,
ex,
)
return sensors
async def async_get_connected_devices(self) -> dict[str, WrtDevice]: async def async_get_connected_devices(self) -> dict[str, WrtDevice]:
"""Get list of connected devices.""" """Get list of connected devices."""
api_devices = await self._api.async_get_connected_devices() api_devices: dict[str, AsusClient] = await self._api.async_get_data(
AsusData.CLIENTS, force=True
)
return { return {
format_mac(mac): WrtDevice(dev.ip, dev.name, dev.node) format_mac(mac): WrtDevice(
dev.connection.ip_address, dev.description.name, dev.connection.node
)
for mac, dev in api_devices.items() for mac, dev in api_devices.items()
if dev.connection is not None
and dev.description is not None
and dev.connection.ip_address is not None
} }
async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]: async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]:
"""Return a dictionary of available sensors for this bridge.""" """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 { return {
SENSORS_TYPE_BYTES: { SENSORS_TYPE_BYTES: {
KEY_SENSORS: SENSORS_BYTES, KEY_SENSORS: SENSORS_BYTES,
KEY_METHOD: self._get_bytes, KEY_METHOD: self._get_bytes,
}, },
SENSORS_TYPE_CPU: { SENSORS_TYPE_CPU: {
KEY_SENSORS: sensors_cpu, KEY_SENSORS: await self._get_sensors(AsusData.CPU),
KEY_METHOD: self._get_cpu_usage, KEY_METHOD: self._get_cpu_usage,
}, },
SENSORS_TYPE_LOAD_AVG: { SENSORS_TYPE_LOAD_AVG: {
KEY_SENSORS: sensors_loadavg, KEY_SENSORS: await self._get_sensors(AsusData.SYSINFO),
KEY_METHOD: self._get_load_avg, KEY_METHOD: self._get_load_avg,
}, },
SENSORS_TYPE_MEMORY: { SENSORS_TYPE_MEMORY: {
@@ -382,95 +426,44 @@ class AsusWrtHttpBridge(AsusWrtBridge):
KEY_METHOD: self._get_uptime, KEY_METHOD: self._get_uptime,
}, },
SENSORS_TYPE_TEMPERATURES: { SENSORS_TYPE_TEMPERATURES: {
KEY_SENSORS: sensors_temperatures, KEY_SENSORS: await self._get_sensors(AsusData.TEMPERATURE),
KEY_METHOD: self._get_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: async def _get_bytes(self) -> Any:
"""Fetch byte information from the router.""" """Fetch byte information from the router."""
return await self._api.async_get_traffic_bytes() return await self._get_data(AsusData.NETWORK)
@handle_errors_and_zip(AsusWrtError, SENSORS_RATES)
async def _get_rates(self) -> Any: async def _get_rates(self) -> Any:
"""Fetch rates information from the router.""" """Fetch rates information from the router."""
return await self._api.async_get_traffic_rates() data = await self._get_data(AsusData.NETWORK)
# Convert from bits/s to Bytes/s for compatibility with legacy sensors
return {
key: (
value / 8
if key in SENSORS_RATES and isinstance(value, (int, float))
else value
)
for key, value in data.items()
}
@handle_errors_and_zip(AsusWrtError, SENSORS_LOAD_AVG)
async def _get_load_avg(self) -> Any: async def _get_load_avg(self) -> Any:
"""Fetch cpu load avg information from the router.""" """Fetch cpu load avg information from the router."""
return await self._api.async_get_loadavg() return await self._get_data(AsusData.SYSINFO)
@handle_errors_and_zip(AsusWrtError, None)
async def _get_temperatures(self) -> Any: async def _get_temperatures(self) -> Any:
"""Fetch temperatures information from the router.""" """Fetch temperatures information from the router."""
return await self._api.async_get_temperatures() return await self._get_data(AsusData.TEMPERATURE)
@handle_errors_and_zip(AsusWrtError, None)
async def _get_cpu_usage(self) -> Any: async def _get_cpu_usage(self) -> Any:
"""Fetch cpu information from the router.""" """Fetch cpu information from the router."""
return await self._api.async_get_cpu_usage() return await self._get_data(AsusData.CPU)
@handle_errors_and_zip(AsusWrtError, None)
async def _get_memory_usage(self) -> Any: async def _get_memory_usage(self) -> Any:
"""Fetch memory information from the router.""" """Fetch memory information from the router."""
return await self._api.async_get_memory_usage() return await self._get_data(AsusData.RAM)
async def _get_uptime(self) -> dict[str, Any]: async def _get_uptime(self) -> dict[str, Any]:
"""Fetch uptime from the router.""" """Fetch uptime from the router."""
try: return await self._get_data(AsusData.BOOTTIME)
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))

View File

@@ -7,7 +7,7 @@ import os
import socket import socket
from typing import Any, cast from typing import Any, cast
from pyasuswrt import AsusWrtError from asusrouter import AsusRouterError
import voluptuous as vol import voluptuous as vol
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
@@ -189,7 +189,7 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN):
try: try:
await api.async_connect() await api.async_connect()
except (AsusWrtError, OSError): except (AsusRouterError, OSError):
_LOGGER.error( _LOGGER.error(
"Error connecting to the AsusWrt router at %s using protocol %s", "Error connecting to the AsusWrt router at %s using protocol %s",
host, host,

View File

@@ -0,0 +1,56 @@
"""Helpers for AsusWRT integration."""
from __future__ import annotations
from typing import Any, TypeVar
T = TypeVar("T", dict[str, Any], list[Any], None)
TRANSLATION_MAP = {
"wan_rx": "sensor_rx_bytes",
"wan_tx": "sensor_tx_bytes",
"total_usage": "cpu_total_usage",
"usage": "mem_usage_perc",
"free": "mem_free",
"used": "mem_used",
"wan_rx_speed": "sensor_rx_rates",
"wan_tx_speed": "sensor_tx_rates",
"2ghz": "2.4GHz",
"5ghz": "5.0GHz",
"5ghz2": "5.0GHz_2",
"6ghz": "6.0GHz",
"cpu": "CPU",
"datetime": "sensor_last_boot",
"uptime": "sensor_uptime",
**{f"{num}_usage": f"cpu{num}_usage" for num in range(1, 9)},
**{f"load_avg_{load}": f"sensor_load_avg{load}" for load in ("1", "5", "15")},
}
def clean_dict(raw: dict[str, Any]) -> dict[str, Any]:
"""Cleans dictionary from None values.
The `state` key is always preserved regardless of its value.
"""
return {k: v for k, v in raw.items() if v is not None or k.endswith("state")}
def translate_to_legacy(raw: T) -> T:
"""Translate raw data to legacy format for dicts and lists."""
if raw is None:
return None
if isinstance(raw, dict):
return {TRANSLATION_MAP.get(k, k): v for k, v in raw.items()}
if isinstance(raw, list):
return [
TRANSLATION_MAP[item]
if isinstance(item, str) and item in TRANSLATION_MAP
else item
for item in raw
]
return raw

View File

@@ -1,11 +1,11 @@
{ {
"domain": "asuswrt", "domain": "asuswrt",
"name": "ASUSWRT", "name": "ASUSWRT",
"codeowners": ["@kennedyshead", "@ollo69"], "codeowners": ["@kennedyshead", "@ollo69", "@Vaskivskyi"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/asuswrt", "documentation": "https://www.home-assistant.io/integrations/asuswrt",
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioasuswrt", "asyncssh"], "loggers": ["aioasuswrt", "asyncssh"],
"requirements": ["aioasuswrt==1.4.0", "pyasuswrt==0.1.21"] "requirements": ["aioasuswrt==1.4.0", "asusrouter==1.18.1"]
} }

View File

@@ -7,7 +7,7 @@ from datetime import datetime, timedelta
import logging import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from pyasuswrt import AsusWrtError from asusrouter import AsusRouterError
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
CONF_CONSIDER_HOME, CONF_CONSIDER_HOME,
@@ -229,7 +229,7 @@ class AsusWrtRouter:
"""Set up a AsusWrt router.""" """Set up a AsusWrt router."""
try: try:
await self._api.async_connect() await self._api.async_connect()
except (AsusWrtError, OSError) as exc: except (AsusRouterError, OSError) as exc:
raise ConfigEntryNotReady from exc raise ConfigEntryNotReady from exc
if not self._api.is_connected: if not self._api.is_connected:
raise ConfigEntryNotReady raise ConfigEntryNotReady
@@ -284,7 +284,7 @@ class AsusWrtRouter:
_LOGGER.debug("Checking devices for ASUS router %s", self.host) _LOGGER.debug("Checking devices for ASUS router %s", self.host)
try: try:
wrt_devices = await self._api.async_get_connected_devices() wrt_devices = await self._api.async_get_connected_devices()
except (OSError, AsusWrtError) as exc: except (OSError, AsusRouterError) as exc:
if not self._connect_error: if not self._connect_error:
self._connect_error = True self._connect_error = True
_LOGGER.error( _LOGGER.error(

6
requirements_all.txt generated
View File

@@ -527,6 +527,9 @@ arris-tg2492lg==2.2.0
# homeassistant.components.ampio # homeassistant.components.ampio
asmog==0.0.6 asmog==0.0.6
# homeassistant.components.asuswrt
asusrouter==1.18.1
# homeassistant.components.dlna_dmr # homeassistant.components.dlna_dmr
# homeassistant.components.dlna_dms # homeassistant.components.dlna_dms
# homeassistant.components.samsungtv # homeassistant.components.samsungtv
@@ -1839,9 +1842,6 @@ pyairvisual==2023.08.1
# homeassistant.components.aprilaire # homeassistant.components.aprilaire
pyaprilaire==0.9.1 pyaprilaire==0.9.1
# homeassistant.components.asuswrt
pyasuswrt==0.1.21
# homeassistant.components.atag # homeassistant.components.atag
pyatag==0.3.5.3 pyatag==0.3.5.3

View File

@@ -491,6 +491,9 @@ aranet4==2.5.1
# homeassistant.components.arcam_fmj # homeassistant.components.arcam_fmj
arcam-fmj==1.8.2 arcam-fmj==1.8.2
# homeassistant.components.asuswrt
asusrouter==1.18.1
# homeassistant.components.dlna_dmr # homeassistant.components.dlna_dmr
# homeassistant.components.dlna_dms # homeassistant.components.dlna_dms
# homeassistant.components.samsungtv # homeassistant.components.samsungtv
@@ -1544,9 +1547,6 @@ pyairvisual==2023.08.1
# homeassistant.components.aprilaire # homeassistant.components.aprilaire
pyaprilaire==0.9.1 pyaprilaire==0.9.1
# homeassistant.components.asuswrt
pyasuswrt==0.1.21
# homeassistant.components.atag # homeassistant.components.atag
pyatag==0.3.5.3 pyatag==0.3.5.3

View File

@@ -1,7 +1,8 @@
"""Test code shared between test files.""" """Test code shared between test files."""
from unittest.mock import MagicMock
from aioasuswrt.asuswrt import Device as LegacyDevice from aioasuswrt.asuswrt import Device as LegacyDevice
from pyasuswrt.asuswrt import Device as HttpDevice
from homeassistant.components.asuswrt.const import ( from homeassistant.components.asuswrt.const import (
CONF_SSH_KEY, CONF_SSH_KEY,
@@ -59,8 +60,22 @@ MOCK_MACS = [
] ]
def new_device(protocol, mac, ip, name): def make_client(mac, ip, name, node):
"""Create a modern mock client."""
connection = MagicMock()
connection.ip_address = ip
connection.node = node
description = MagicMock()
description.name = name
description.mac = mac
client = MagicMock()
client.connection = connection
client.description = description
return client
def new_device(protocol, mac, ip, name, node=None):
"""Return a new device for specific protocol.""" """Return a new device for specific protocol."""
if protocol in [PROTOCOL_HTTP, PROTOCOL_HTTPS]: if protocol in [PROTOCOL_HTTP, PROTOCOL_HTTPS]:
return HttpDevice(mac, ip, name, ROUTER_MAC_ADDR, None) return make_client(mac, ip, name, node)
return LegacyDevice(mac, ip, name) return LegacyDevice(mac, ip, name)

View File

@@ -1,17 +1,25 @@
"""Fixtures for Asuswrt component.""" """Fixtures for Asuswrt component."""
from datetime import datetime
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from aioasuswrt.asuswrt import AsusWrt as AsusWrtLegacy from aioasuswrt.asuswrt import AsusWrt as AsusWrtLegacy
from aioasuswrt.connection import TelnetConnection from aioasuswrt.connection import TelnetConnection
from pyasuswrt.asuswrt import AsusWrtError, AsusWrtHttp from asusrouter import AsusRouter, AsusRouterError
from asusrouter.modules.data import AsusData
from asusrouter.modules.identity import AsusDevice
import pytest import pytest
from homeassistant.components.asuswrt.const import PROTOCOL_HTTP, PROTOCOL_SSH from .common import (
ASUSWRT_BASE,
MOCK_MACS,
PROTOCOL_HTTP,
PROTOCOL_SSH,
ROUTER_MAC_ADDR,
new_device,
)
from .common import ASUSWRT_BASE, MOCK_MACS, ROUTER_MAC_ADDR, new_device ASUSWRT_HTTP_LIB = f"{ASUSWRT_BASE}.bridge.AsusRouter"
ASUSWRT_HTTP_LIB = f"{ASUSWRT_BASE}.bridge.AsusWrtHttp"
ASUSWRT_LEGACY_LIB = f"{ASUSWRT_BASE}.bridge.AsusWrtLegacy" ASUSWRT_LEGACY_LIB = f"{ASUSWRT_BASE}.bridge.AsusWrtLegacy"
MOCK_BYTES_TOTAL = 60000000000, 50000000000 MOCK_BYTES_TOTAL = 60000000000, 50000000000
@@ -29,8 +37,20 @@ MOCK_CPU_USAGE = {
} }
MOCK_CURRENT_TRANSFER_RATES = 20000000, 10000000 MOCK_CURRENT_TRANSFER_RATES = 20000000, 10000000
MOCK_CURRENT_TRANSFER_RATES_HTTP = dict(enumerate(MOCK_CURRENT_TRANSFER_RATES)) MOCK_CURRENT_TRANSFER_RATES_HTTP = dict(enumerate(MOCK_CURRENT_TRANSFER_RATES))
# Mock for AsusData.NETWORK return of both rates and total bytes
MOCK_CURRENT_NETWORK = {
"sensor_rx_rates": MOCK_CURRENT_TRANSFER_RATES[0] * 8, # AR works with bits
"sensor_tx_rates": MOCK_CURRENT_TRANSFER_RATES[1] * 8, # AR works with bits
"sensor_rx_bytes": MOCK_BYTES_TOTAL[0],
"sensor_tx_bytes": MOCK_BYTES_TOTAL[1],
}
MOCK_LOAD_AVG_HTTP = {"load_avg_1": 1.1, "load_avg_5": 1.2, "load_avg_15": 1.3} MOCK_LOAD_AVG_HTTP = {"load_avg_1": 1.1, "load_avg_5": 1.2, "load_avg_15": 1.3}
MOCK_LOAD_AVG = list(MOCK_LOAD_AVG_HTTP.values()) MOCK_LOAD_AVG = list(MOCK_LOAD_AVG_HTTP.values())
MOCK_SYSINFO = {
"sensor_load_avg1": MOCK_LOAD_AVG[0],
"sensor_load_avg5": MOCK_LOAD_AVG[1],
"sensor_load_avg15": MOCK_LOAD_AVG[2],
}
MOCK_MEMORY_USAGE = { MOCK_MEMORY_USAGE = {
"mem_usage_perc": 52.4, "mem_usage_perc": 52.4,
"mem_total": 1048576, "mem_total": 1048576,
@@ -40,6 +60,10 @@ MOCK_MEMORY_USAGE = {
MOCK_TEMPERATURES = {"2.4GHz": 40.2, "5.0GHz": 0, "CPU": 71.2} MOCK_TEMPERATURES = {"2.4GHz": 40.2, "5.0GHz": 0, "CPU": 71.2}
MOCK_TEMPERATURES_HTTP = {**MOCK_TEMPERATURES, "5.0GHz_2": 40.3, "6.0GHz": 40.4} MOCK_TEMPERATURES_HTTP = {**MOCK_TEMPERATURES, "5.0GHz_2": 40.3, "6.0GHz": 40.4}
MOCK_UPTIME = {"last_boot": "2024-08-02T00:47:00+00:00", "uptime": 1625927} MOCK_UPTIME = {"last_boot": "2024-08-02T00:47:00+00:00", "uptime": 1625927}
MOCK_BOOTTIME = {
"sensor_last_boot": datetime.fromisoformat(MOCK_UPTIME["last_boot"]),
"sensor_uptime": MOCK_UPTIME["uptime"],
}
@pytest.fixture(name="patch_setup_entry") @pytest.fixture(name="patch_setup_entry")
@@ -62,10 +86,14 @@ def mock_devices_legacy_fixture():
@pytest.fixture(name="mock_devices_http") @pytest.fixture(name="mock_devices_http")
def mock_devices_http_fixture(): def mock_devices_http_fixture():
"""Mock a list of devices.""" """Mock a list of AsusRouter client devices for HTTP backend."""
return { return {
MOCK_MACS[0]: new_device(PROTOCOL_HTTP, MOCK_MACS[0], "192.168.1.2", "Test"), MOCK_MACS[0]: new_device(
MOCK_MACS[1]: new_device(PROTOCOL_HTTP, MOCK_MACS[1], "192.168.1.3", "TestTwo"), PROTOCOL_HTTP, MOCK_MACS[0], "192.168.1.2", "Test", "node1"
),
MOCK_MACS[1]: new_device(
PROTOCOL_HTTP, MOCK_MACS[1], "192.168.1.3", "TestTwo", "node2"
),
} }
@@ -121,57 +149,90 @@ def mock_controller_connect_legacy_sens_fail(connect_legacy):
@pytest.fixture(name="connect_http") @pytest.fixture(name="connect_http")
def mock_controller_connect_http(mock_devices_http): def mock_controller_connect_http(mock_devices_http):
"""Mock a successful connection with http library.""" """Mock a successful connection with http library."""
with patch(ASUSWRT_HTTP_LIB, spec_set=AsusWrtHttp) as service_mock: with patch(ASUSWRT_HTTP_LIB, spec_set=AsusRouter) as service_mock:
service_mock.return_value.is_connected = True instance = service_mock.return_value
service_mock.return_value.mac = ROUTER_MAC_ADDR
service_mock.return_value.model = "FAKE_MODEL" # Simulate connection status
service_mock.return_value.firmware = "FAKE_FIRMWARE" instance.connected = True
service_mock.return_value.async_get_connected_devices.return_value = (
mock_devices_http # Identity
instance.async_get_identity.return_value = AsusDevice(
mac=ROUTER_MAC_ADDR,
model="FAKE_MODEL",
firmware="FAKE_FIRMWARE",
) )
service_mock.return_value.async_get_traffic_bytes.return_value = (
MOCK_BYTES_TOTAL_HTTP # Data fetches via async_get_data
) instance.async_get_data.side_effect = lambda datatype, *args, **kwargs: {
service_mock.return_value.async_get_traffic_rates.return_value = ( AsusData.CLIENTS: mock_devices_http,
MOCK_CURRENT_TRANSFER_RATES_HTTP AsusData.NETWORK: MOCK_CURRENT_NETWORK,
) AsusData.SYSINFO: MOCK_SYSINFO,
service_mock.return_value.async_get_loadavg.return_value = MOCK_LOAD_AVG_HTTP AsusData.TEMPERATURE: {
service_mock.return_value.async_get_temperatures.return_value = { k: v for k, v in MOCK_TEMPERATURES_HTTP.items() if k != "5.0GHz"
k: v for k, v in MOCK_TEMPERATURES_HTTP.items() if k != "5.0GHz" },
} AsusData.CPU: MOCK_CPU_USAGE,
service_mock.return_value.async_get_cpu_usage.return_value = MOCK_CPU_USAGE AsusData.RAM: MOCK_MEMORY_USAGE,
service_mock.return_value.async_get_memory_usage.return_value = ( AsusData.BOOTTIME: MOCK_BOOTTIME,
MOCK_MEMORY_USAGE }[datatype]
)
service_mock.return_value.async_get_uptime.return_value = MOCK_UPTIME
yield service_mock yield service_mock
def make_async_get_data_side_effect(fail_types=None):
"""Return a side effect for async_get_data that fails for specified AsusData types."""
fail_types = set(fail_types or [])
def side_effect(datatype, *args, **kwargs):
if datatype in fail_types:
raise AsusRouterError(f"{datatype} unavailable")
# Return valid mock data for other types
if datatype == AsusData.CLIENTS:
return {}
if datatype == AsusData.NETWORK:
return {}
if datatype == AsusData.SYSINFO:
return {}
if datatype == AsusData.TEMPERATURE:
return {}
if datatype == AsusData.CPU:
return {}
if datatype == AsusData.RAM:
return {}
if datatype == AsusData.BOOTTIME:
return {}
return {}
return side_effect
@pytest.fixture(name="connect_http_sens_fail") @pytest.fixture(name="connect_http_sens_fail")
def mock_controller_connect_http_sens_fail(connect_http): def mock_controller_connect_http_sens_fail(connect_http):
"""Mock a successful connection using http library with sensors fail.""" """Universal fixture to fail specified AsusData types."""
connect_http.return_value.mac = None
connect_http.return_value.async_get_connected_devices.side_effect = AsusWrtError def _set_fail_types(fail_types):
connect_http.return_value.async_get_traffic_bytes.side_effect = AsusWrtError connect_http.return_value.async_get_data.side_effect = (
connect_http.return_value.async_get_traffic_rates.side_effect = AsusWrtError make_async_get_data_side_effect(fail_types)
connect_http.return_value.async_get_loadavg.side_effect = AsusWrtError )
connect_http.return_value.async_get_temperatures.side_effect = AsusWrtError return connect_http
connect_http.return_value.async_get_cpu_usage.side_effect = AsusWrtError
connect_http.return_value.async_get_memory_usage.side_effect = AsusWrtError return _set_fail_types
connect_http.return_value.async_get_uptime.side_effect = AsusWrtError
@pytest.fixture(name="connect_http_sens_detect") @pytest.fixture(name="connect_http_sens_detect")
def mock_controller_connect_http_sens_detect(): def mock_controller_connect_http_sens_detect():
"""Mock a successful sensor detection using http library.""" """Mock a successful sensor detection using http library."""
with (
patch( def _get_sensors_side_effect(datatype):
f"{ASUSWRT_BASE}.bridge.AsusWrtHttpBridge._get_available_temperature_sensors", if datatype == AsusData.TEMPERATURE:
return_value=[*MOCK_TEMPERATURES_HTTP], return list(MOCK_TEMPERATURES_HTTP)
) as mock_sens_temp_detect, if datatype == AsusData.CPU:
patch( return list(MOCK_CPU_USAGE)
f"{ASUSWRT_BASE}.bridge.AsusWrtHttpBridge._get_available_cpu_sensors", if datatype == AsusData.SYSINFO:
return_value=[*MOCK_CPU_USAGE], return list(MOCK_SYSINFO)
) as mock_sens_cpu_detect, return []
):
yield mock_sens_temp_detect, mock_sens_cpu_detect with patch(
f"{ASUSWRT_BASE}.bridge.AsusWrtHttpBridge._get_sensors",
side_effect=_get_sensors_side_effect,
) as mock_sens_detect:
yield mock_sens_detect

View File

@@ -3,7 +3,8 @@
from socket import gaierror from socket import gaierror
from unittest.mock import patch from unittest.mock import patch
from pyasuswrt import AsusWrtError from asusrouter import AsusRouterError
from asusrouter.modules.identity import AsusDevice
import pytest import pytest
from homeassistant.components.asuswrt.const import ( from homeassistant.components.asuswrt.const import (
@@ -128,7 +129,11 @@ async def test_user_http(
assert flow_result["type"] is FlowResultType.FORM assert flow_result["type"] is FlowResultType.FORM
assert flow_result["step_id"] == "user" assert flow_result["step_id"] == "user"
connect_http.return_value.mac = unique_id connect_http.return_value.async_get_identity.return_value = AsusDevice(
mac=unique_id,
model="FAKE_MODEL",
firmware="FAKE_FIRMWARE",
)
# test with all provided # test with all provided
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
@@ -297,7 +302,7 @@ async def test_on_connect_legacy_failed(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("side_effect", "error"), ("side_effect", "error"),
[ [
(AsusWrtError, "cannot_connect"), (AsusRouterError, "cannot_connect"),
(TypeError, "unknown"), (TypeError, "unknown"),
(None, "cannot_connect"), (None, "cannot_connect"),
], ],
@@ -311,7 +316,7 @@ async def test_on_connect_http_failed(
context={"source": SOURCE_USER, "show_advanced_options": True}, context={"source": SOURCE_USER, "show_advanced_options": True},
) )
connect_http.return_value.is_connected = False connect_http.return_value.connected = False
connect_http.return_value.async_connect.side_effect = side_effect connect_http.return_value.async_connect.side_effect = side_effect
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(

View File

@@ -0,0 +1,95 @@
"""Tests for AsusWRT helpers."""
from typing import Any
import pytest
from homeassistant.components.asuswrt.helpers import clean_dict, translate_to_legacy
DICT_TO_CLEAN = {
"key1": "value1",
"key2": None,
"key3_state": "value3",
"key4_state": None,
"state": None,
}
DICT_CLEAN = {
"key1": "value1",
"key3_state": "value3",
"key4_state": None,
"state": None,
}
TRANSLATE_0_INPUT = {
"usage": "value1",
"cpu": "value2",
}
TRANSLATE_0_OUTPUT = {
"mem_usage_perc": "value1",
"CPU": "value2",
}
TRANSLATE_1_INPUT = {
"wan_rx": "value1",
"wan_rrx": "value2",
}
TRANSLATE_1_OUTPUT = {
"sensor_rx_bytes": "value1",
"wan_rrx": "value2",
}
TRANSLATE_2_INPUT = [
"free",
"used",
]
TRANSLATE_2_OUTPUT = [
"mem_free",
"mem_used",
]
TRANSLATE_3_INPUT = [
"2ghz",
"2ghz2",
]
TRANSLATE_3_OUTPUT = [
"2.4GHz",
"2ghz2",
]
def test_clean_dict() -> None:
"""Test clean_dict method."""
assert clean_dict(DICT_TO_CLEAN) == DICT_CLEAN
@pytest.mark.parametrize(
("input", "expected"),
[
# Case set 0: None as input -> None on output
(None, None),
# Case set 1: Dict structure should stay intact or translated
({"key1": "value1", "key2": None}, {"key1": "value1", "key2": None}),
(TRANSLATE_0_INPUT, TRANSLATE_0_OUTPUT),
(TRANSLATE_1_INPUT, TRANSLATE_1_OUTPUT),
({}, {}),
# Case set 2: List structure should stay intact or translated
(["key1", "key2"], ["key1", "key2"]),
(TRANSLATE_2_INPUT, TRANSLATE_2_OUTPUT),
(TRANSLATE_3_INPUT, TRANSLATE_3_OUTPUT),
([], []),
# Case set 3: Anything else should be simply returned
(123, 123),
("string", "string"),
(3.1415926535, 3.1415926535),
],
)
def test_translate(input: Any, expected: Any) -> None:
"""Test translate method."""
assert translate_to_legacy(input) == expected

View File

@@ -2,8 +2,9 @@
from datetime import timedelta from datetime import timedelta
from asusrouter import AsusRouterError
from asusrouter.modules.data import AsusData
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
from pyasuswrt.exceptions import AsusWrtError, AsusWrtNotAvailableInfoError
import pytest import pytest
from homeassistant.components import device_tracker, sensor from homeassistant.components import device_tracker, sensor
@@ -39,6 +40,7 @@ from .common import (
ROUTER_MAC_ADDR, ROUTER_MAC_ADDR,
new_device, new_device,
) )
from .conftest import make_async_get_data_side_effect
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
@@ -260,8 +262,8 @@ async def test_loadavg_sensors_unaivalable_http(
config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA_HTTP, SENSORS_LOAD_AVG) config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA_HTTP, SENSORS_LOAD_AVG)
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
connect_http.return_value.async_get_loadavg.side_effect = ( connect_http.return_value.async_get_data.side_effect = (
AsusWrtNotAvailableInfoError make_async_get_data_side_effect([AsusData.SYSINFO])
) )
# initial devices setup # initial devices setup
@@ -281,6 +283,7 @@ async def test_temperature_sensors_http_fail(
hass: HomeAssistant, connect_http_sens_fail hass: HomeAssistant, connect_http_sens_fail
) -> None: ) -> None:
"""Test fail creating AsusWRT temperature sensors.""" """Test fail creating AsusWRT temperature sensors."""
_ = connect_http_sens_fail([AsusData.TEMPERATURE])
config_entry, sensor_prefix = _setup_entry( config_entry, sensor_prefix = _setup_entry(
hass, CONFIG_DATA_HTTP, SENSORS_TEMPERATURES hass, CONFIG_DATA_HTTP, SENSORS_TEMPERATURES
) )
@@ -347,6 +350,7 @@ async def test_cpu_sensors_http_fail(
hass: HomeAssistant, connect_http_sens_fail hass: HomeAssistant, connect_http_sens_fail
) -> None: ) -> None:
"""Test fail creating AsusWRT cpu sensors.""" """Test fail creating AsusWRT cpu sensors."""
_ = connect_http_sens_fail([AsusData.CPU])
config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA_HTTP, SENSORS_CPU) config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA_HTTP, SENSORS_CPU)
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
@@ -367,9 +371,13 @@ async def test_cpu_sensors_http_fail(
async def test_cpu_sensors_http( async def test_cpu_sensors_http(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, connect_http hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
connect_http,
connect_http_sens_detect,
) -> None: ) -> None:
"""Test creating AsusWRT cpu sensors.""" """Test creating AsusWRT cpu sensors."""
connect_http_sens_detect(AsusData.CPU)
config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA_HTTP, SENSORS_CPU) config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA_HTTP, SENSORS_CPU)
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
@@ -461,7 +469,7 @@ async def test_connect_fail_legacy(
@pytest.mark.parametrize( @pytest.mark.parametrize(
"side_effect", "side_effect",
[AsusWrtError, None], [AsusRouterError, None],
) )
async def test_connect_fail_http( async def test_connect_fail_http(
hass: HomeAssistant, connect_http, side_effect hass: HomeAssistant, connect_http, side_effect
@@ -476,7 +484,7 @@ async def test_connect_fail_http(
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
connect_http.return_value.async_connect.side_effect = side_effect connect_http.return_value.async_connect.side_effect = side_effect
connect_http.return_value.is_connected = False connect_http.return_value.connected = False
# initial setup fail # initial setup fail
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
@@ -524,6 +532,16 @@ async def test_sensors_polling_fails_http(
connect_http_sens_detect, connect_http_sens_detect,
) -> None: ) -> None:
"""Test AsusWRT sensors are unavailable when polling fails.""" """Test AsusWRT sensors are unavailable when polling fails."""
# Fail all relevant AsusData types for HTTP sensors
fail_types = [
AsusData.NETWORK,
AsusData.CPU,
AsusData.SYSINFO,
AsusData.RAM,
AsusData.TEMPERATURE,
AsusData.BOOTTIME,
]
_ = connect_http_sens_fail(fail_types)
await _test_sensors_polling_fails(hass, freezer, CONFIG_DATA_HTTP, SENSORS_ALL_HTTP) await _test_sensors_polling_fails(hass, freezer, CONFIG_DATA_HTTP, SENSORS_ALL_HTTP)