Files

421 lines
14 KiB
Python

"""Tests for the AsusWrt sensor."""
from datetime import timedelta
from unittest.mock import AsyncMock, Mock, patch
from aioasuswrt.asuswrt import Device
import pytest
from homeassistant.components import device_tracker, sensor
from homeassistant.components.asuswrt.const import (
CONF_INTERFACE,
DOMAIN,
PROTOCOL_TELNET,
)
from homeassistant.components.asuswrt.router import DEFAULT_NAME
from homeassistant.components.device_tracker import CONF_CONSIDER_HOME
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
CONF_HOST,
CONF_MODE,
CONF_PASSWORD,
CONF_PORT,
CONF_PROTOCOL,
CONF_USERNAME,
STATE_HOME,
STATE_NOT_HOME,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util import slugify
from homeassistant.util.dt import utcnow
from tests.common import MockConfigEntry, async_fire_time_changed
ASUSWRT_LIB = "homeassistant.components.asuswrt.router.AsusWrt"
HOST = "myrouter.asuswrt.com"
IP_ADDRESS = "192.168.1.1"
CONFIG_DATA = {
CONF_HOST: HOST,
CONF_PORT: 22,
CONF_PROTOCOL: PROTOCOL_TELNET,
CONF_USERNAME: "user",
CONF_PASSWORD: "pwd",
CONF_MODE: "router",
}
MAC_ADDR = "a1:b2:c3:d4:e5:f6"
MOCK_BYTES_TOTAL = [60000000000, 50000000000]
MOCK_CURRENT_TRANSFER_RATES = [20000000, 10000000]
MOCK_LOAD_AVG = [1.1, 1.2, 1.3]
MOCK_TEMPERATURES = {"2.4GHz": 40.0, "5.0GHz": 0.0, "CPU": 71.2}
MOCK_MAC_1 = "A1:B1:C1:D1:E1:F1"
MOCK_MAC_2 = "A2:B2:C2:D2:E2:F2"
MOCK_MAC_3 = "A3:B3:C3:D3:E3:F3"
MOCK_MAC_4 = "A4:B4:C4:D4:E4:F4"
SENSORS_DEFAULT = [
"Download Speed",
"Download",
"Upload Speed",
"Upload",
]
SENSORS_LOADAVG = [
"Load Avg (1m)",
"Load Avg (5m)",
"Load Avg (15m)",
]
SENSORS_TEMP = [
"2.4GHz Temperature",
"5GHz Temperature",
"CPU Temperature",
]
SENSORS_ALL = [*SENSORS_DEFAULT, *SENSORS_LOADAVG, *SENSORS_TEMP]
PATCH_SETUP_ENTRY = patch(
"homeassistant.components.asuswrt.async_setup_entry",
return_value=True,
)
def new_device(mac, ip, name):
"""Return a new device for specific protocol."""
return Device(mac, ip, name)
@pytest.fixture(name="mock_devices")
def mock_devices_fixture():
"""Mock a list of devices."""
return {
MOCK_MAC_1: Device(MOCK_MAC_1, "192.168.1.2", "Test"),
MOCK_MAC_2: Device(MOCK_MAC_2, "192.168.1.3", "TestTwo"),
}
@pytest.fixture(name="mock_available_temps")
def mock_available_temps_fixture():
"""Mock a list of available temperature sensors."""
return [True, False, True]
@pytest.fixture(name="create_device_registry_devices")
def create_device_registry_devices_fixture(hass):
"""Create device registry devices so the device tracker entities are enabled when added."""
dev_reg = dr.async_get(hass)
config_entry = MockConfigEntry(domain="something_else")
for idx, device in enumerate(
(
MOCK_MAC_3,
MOCK_MAC_4,
)
):
dev_reg.async_get_or_create(
name=f"Device {idx}",
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, dr.format_mac(device))},
)
@pytest.fixture(name="connect")
def mock_controller_connect(mock_devices, mock_available_temps):
"""Mock a successful connection with AsusWrt library."""
with patch(ASUSWRT_LIB) as service_mock:
service_mock.return_value.connection.async_connect = AsyncMock()
service_mock.return_value.is_connected = True
service_mock.return_value.connection.disconnect = Mock()
service_mock.return_value.async_get_nvram = AsyncMock(
return_value={
"label_mac": MAC_ADDR,
"model": "abcd",
"firmver": "efg",
"buildno": "123",
}
)
service_mock.return_value.async_get_connected_devices = AsyncMock(
return_value=mock_devices
)
service_mock.return_value.async_get_bytes_total = AsyncMock(
return_value=MOCK_BYTES_TOTAL
)
service_mock.return_value.async_get_current_transfer_rates = AsyncMock(
return_value=MOCK_CURRENT_TRANSFER_RATES
)
service_mock.return_value.async_get_loadavg = AsyncMock(
return_value=MOCK_LOAD_AVG
)
service_mock.return_value.async_get_temperature = AsyncMock(
return_value=MOCK_TEMPERATURES
)
service_mock.return_value.async_find_temperature_commands = AsyncMock(
return_value=mock_available_temps
)
yield service_mock
@pytest.fixture(name="connect_sens_fail")
def mock_controller_connect_sens_fail():
"""Mock a successful connection using AsusWrt library with sensors failing."""
with patch(ASUSWRT_LIB) as service_mock:
service_mock.return_value.connection.async_connect = AsyncMock()
service_mock.return_value.is_connected = True
service_mock.return_value.connection.disconnect = Mock()
service_mock.return_value.async_get_nvram = AsyncMock(side_effect=OSError)
service_mock.return_value.async_get_connected_devices = AsyncMock(
side_effect=OSError
)
service_mock.return_value.async_get_bytes_total = AsyncMock(side_effect=OSError)
service_mock.return_value.async_get_current_transfer_rates = AsyncMock(
side_effect=OSError
)
service_mock.return_value.async_get_loadavg = AsyncMock(side_effect=OSError)
service_mock.return_value.async_get_temperature = AsyncMock(side_effect=OSError)
service_mock.return_value.async_find_temperature_commands = AsyncMock(
return_value=[True, True, True]
)
yield service_mock
def _setup_entry(hass, config, sensors, unique_id=None):
"""Create mock config entry with enabled sensors."""
entity_reg = er.async_get(hass)
# init config entry
config_entry = MockConfigEntry(
domain=DOMAIN,
data=config,
options={CONF_CONSIDER_HOME: 60},
unique_id=unique_id,
)
# init variable
obj_prefix = slugify(HOST if unique_id else DEFAULT_NAME)
sensor_prefix = f"{sensor.DOMAIN}.{obj_prefix}"
# Pre-enable the status sensor
for sensor_name in sensors:
sensor_id = slugify(sensor_name)
entity_reg.async_get_or_create(
sensor.DOMAIN,
DOMAIN,
f"{DOMAIN} {unique_id or DEFAULT_NAME} {sensor_name}",
suggested_object_id=f"{obj_prefix}_{sensor_id}",
config_entry=config_entry,
disabled_by=None,
)
return config_entry, sensor_prefix
@pytest.mark.parametrize(
"entry_unique_id",
[None, MAC_ADDR],
)
async def test_sensors(
hass: HomeAssistant,
connect,
mock_devices,
create_device_registry_devices,
entry_unique_id,
) -> None:
"""Test creating AsusWRT default sensors and tracker."""
config_entry, sensor_prefix = _setup_entry(
hass, CONFIG_DATA, SENSORS_DEFAULT, entry_unique_id
)
# Create the first device tracker to test mac conversion
entity_reg = er.async_get(hass)
for mac, name in {
MOCK_MAC_1: "test",
dr.format_mac(MOCK_MAC_2): "testtwo",
MOCK_MAC_2: "testremove",
}.items():
entity_reg.async_get_or_create(
device_tracker.DOMAIN,
DOMAIN,
mac,
suggested_object_id=name,
config_entry=config_entry,
disabled_by=None,
)
config_entry.add_to_hass(hass)
# initial devices setup
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
await hass.async_block_till_done()
assert hass.states.get(f"{device_tracker.DOMAIN}.test").state == STATE_HOME
assert hass.states.get(f"{device_tracker.DOMAIN}.testtwo").state == STATE_HOME
assert hass.states.get(f"{sensor_prefix}_download_speed").state == "160.0"
assert hass.states.get(f"{sensor_prefix}_download").state == "60.0"
assert hass.states.get(f"{sensor_prefix}_upload_speed").state == "80.0"
assert hass.states.get(f"{sensor_prefix}_upload").state == "50.0"
assert hass.states.get(f"{sensor_prefix}_devices_connected").state == "2"
# remove first tracked device
mock_devices.pop(MOCK_MAC_1)
async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
await hass.async_block_till_done()
# consider home option set, all devices still home but only 1 device connected
assert hass.states.get(f"{device_tracker.DOMAIN}.test").state == STATE_HOME
assert hass.states.get(f"{device_tracker.DOMAIN}.testtwo").state == STATE_HOME
assert hass.states.get(f"{sensor_prefix}_devices_connected").state == "1"
# add 2 new devices, one unnamed that should be ignored but counted
mock_devices[MOCK_MAC_3] = new_device(MOCK_MAC_3, "192.168.1.4", "TestThree")
mock_devices[MOCK_MAC_4] = new_device(MOCK_MAC_4, "192.168.1.5", None)
# change consider home settings to have status not home of removed tracked device
hass.config_entries.async_update_entry(
config_entry, options={CONF_CONSIDER_HOME: 0}
)
await hass.async_block_till_done()
async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
await hass.async_block_till_done()
# consider home option set to 0, device "test" not home
assert hass.states.get(f"{device_tracker.DOMAIN}.test").state == STATE_NOT_HOME
assert hass.states.get(f"{device_tracker.DOMAIN}.testtwo").state == STATE_HOME
assert hass.states.get(f"{device_tracker.DOMAIN}.testthree").state == STATE_HOME
assert hass.states.get(f"{sensor_prefix}_devices_connected").state == "3"
async def test_loadavg_sensors(
hass: HomeAssistant,
connect,
) -> None:
"""Test creating an AsusWRT load average sensors."""
config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA, SENSORS_LOADAVG)
config_entry.add_to_hass(hass)
# initial devices setup
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
await hass.async_block_till_done()
# assert temperature sensor available
assert hass.states.get(f"{sensor_prefix}_load_avg_1m").state == "1.1"
assert hass.states.get(f"{sensor_prefix}_load_avg_5m").state == "1.2"
assert hass.states.get(f"{sensor_prefix}_load_avg_15m").state == "1.3"
async def test_temperature_sensors_fail(
hass: HomeAssistant,
connect,
mock_available_temps,
) -> None:
"""Test fail creating AsusWRT temperature sensors."""
config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA, SENSORS_TEMP)
config_entry.add_to_hass(hass)
# Only length of 3 booleans is valid. Checking the exception handling.
mock_available_temps.pop(2)
# initial devices setup
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# assert temperature availability exception is handled correctly
assert not hass.states.get(f"{sensor_prefix}_2_4ghz_temperature")
assert not hass.states.get(f"{sensor_prefix}_5ghz_temperature")
assert not hass.states.get(f"{sensor_prefix}_cpu_temperature")
async def test_temperature_sensors(
hass: HomeAssistant,
connect,
) -> None:
"""Test creating a AsusWRT temperature sensors."""
config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA, SENSORS_TEMP)
config_entry.add_to_hass(hass)
# initial devices setup
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
await hass.async_block_till_done()
# assert temperature sensor available
assert hass.states.get(f"{sensor_prefix}_2_4ghz_temperature").state == "40.0"
assert not hass.states.get(f"{sensor_prefix}_5ghz_temperature")
assert hass.states.get(f"{sensor_prefix}_cpu_temperature").state == "71.2"
@pytest.mark.parametrize(
"side_effect",
[OSError, None],
)
async def test_connect_fail(hass: HomeAssistant, side_effect) -> None:
"""Test AsusWRT connect fail."""
# init config entry
config_entry = MockConfigEntry(
domain=DOMAIN,
data=CONFIG_DATA,
)
config_entry.add_to_hass(hass)
with patch(ASUSWRT_LIB) as asus_wrt:
asus_wrt.return_value.connection.async_connect = AsyncMock(
side_effect=side_effect
)
asus_wrt.return_value.async_get_nvram = AsyncMock()
asus_wrt.return_value.is_connected = False
# initial setup fail
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_sensors_polling_fails(hass: HomeAssistant, connect_sens_fail) -> None:
"""Test AsusWRT sensors are unavailable when polling fails."""
config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA, SENSORS_ALL)
config_entry.add_to_hass(hass)
# initial devices setup
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
await hass.async_block_till_done()
for sensor_name in SENSORS_ALL:
assert (
hass.states.get(f"{sensor_prefix}_{slugify(sensor_name)}").state
== STATE_UNAVAILABLE
)
assert hass.states.get(f"{sensor_prefix}_devices_connected").state == "0"
async def test_options_reload(hass: HomeAssistant, connect) -> None:
"""Test AsusWRT integration is reload changing an options that require this."""
config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_DATA, unique_id=MAC_ADDR)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
await hass.async_block_till_done()
with PATCH_SETUP_ENTRY as setup_entry_call:
# change an option that requires integration reload
hass.config_entries.async_update_entry(
config_entry, options={CONF_INTERFACE: "eth1"}
)
await hass.async_block_till_done()
assert setup_entry_call.called
assert config_entry.state is ConfigEntryState.LOADED