Files
core/tests/components/asuswrt/test_sensor.py
2025-08-13 18:33:02 +02:00

632 lines
22 KiB
Python

"""Tests for the AsusWrt sensor."""
from datetime import timedelta
from asusrouter import AsusRouterError
from asusrouter.modules.data import AsusData
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components import device_tracker, sensor
from homeassistant.components.asuswrt.const import (
CONF_INTERFACE,
DOMAIN,
SENSORS_BYTES,
SENSORS_CPU,
SENSORS_LOAD_AVG,
SENSORS_MEMORY,
SENSORS_RATES,
SENSORS_TEMPERATURES,
SENSORS_TEMPERATURES_LEGACY,
SENSORS_UPTIME,
)
from homeassistant.components.device_tracker import CONF_CONSIDER_HOME
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
CONF_PROTOCOL,
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 .common import (
CONFIG_DATA_HTTP,
CONFIG_DATA_TELNET,
HOST,
MOCK_MACS,
ROUTER_MAC_ADDR,
new_device,
)
from .conftest import make_async_get_data_side_effect
from tests.common import MockConfigEntry, async_fire_time_changed
SENSORS_DEFAULT = [*SENSORS_BYTES, *SENSORS_RATES]
SENSORS_ALL_LEGACY = [*SENSORS_DEFAULT, *SENSORS_LOAD_AVG, *SENSORS_TEMPERATURES_LEGACY]
SENSORS_ALL_HTTP = [
*SENSORS_DEFAULT,
*SENSORS_CPU,
*SENSORS_LOAD_AVG,
*SENSORS_MEMORY,
*SENSORS_TEMPERATURES,
*SENSORS_UPTIME,
]
@pytest.fixture(name="create_device_registry_devices")
def create_device_registry_devices_fixture(
hass: HomeAssistant, device_registry: dr.DeviceRegistry
):
"""Create device registry devices so the device tracker entities are enabled when added."""
config_entry = MockConfigEntry(domain="something_else")
config_entry.add_to_hass(hass)
for idx, device in enumerate((MOCK_MACS[2], MOCK_MACS[3])):
device_registry.async_get_or_create(
name=f"Device {idx}",
config_entry_id=config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, dr.format_mac(device))},
)
def _setup_entry(hass: HomeAssistant, 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,
)
config_entry.add_to_hass(hass)
# init variable
obj_prefix = slugify(HOST)
sensor_prefix = f"{sensor.DOMAIN}.{obj_prefix}"
unique_id_prefix = slugify(unique_id or config_entry.entry_id)
# Pre-enable the status sensor
for sensor_key in sensors:
sensor_id = slugify(sensor_key)
entity_reg.async_get_or_create(
sensor.DOMAIN,
DOMAIN,
f"{unique_id_prefix}_{sensor_id}",
suggested_object_id=f"{obj_prefix}_{sensor_id}",
config_entry=config_entry,
disabled_by=None,
)
return config_entry, sensor_prefix
async def _test_sensors(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_devices,
config,
entry_unique_id,
) -> None:
"""Test creating AsusWRT default sensors and tracker."""
config_entry, sensor_prefix = _setup_entry(
hass, config, 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_MACS[0]: "test",
dr.format_mac(MOCK_MACS[1]): "testtwo",
MOCK_MACS[1]: "testremove",
}.items():
entity_reg.async_get_or_create(
device_tracker.DOMAIN,
DOMAIN,
mac,
suggested_object_id=name,
config_entry=config_entry,
disabled_by=None,
)
# initial devices setup
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
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}_sensor_rx_rates").state == "160.0"
assert hass.states.get(f"{sensor_prefix}_sensor_rx_bytes").state == "60.0"
assert hass.states.get(f"{sensor_prefix}_sensor_tx_rates").state == "80.0"
assert hass.states.get(f"{sensor_prefix}_sensor_tx_bytes").state == "50.0"
assert hass.states.get(f"{sensor_prefix}_devices_connected").state == "2"
# remove first tracked device
mock_devices.pop(MOCK_MACS[0])
freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
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_MACS[2]] = new_device(
config[CONF_PROTOCOL], MOCK_MACS[2], "192.168.1.4", "TestThree"
)
mock_devices[MOCK_MACS[3]] = new_device(
config[CONF_PROTOCOL], MOCK_MACS[3], "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()
freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
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"
@pytest.mark.parametrize(
"entry_unique_id",
[None, ROUTER_MAC_ADDR],
)
async def test_sensors_legacy(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_devices_legacy,
entry_unique_id,
connect_legacy,
create_device_registry_devices,
) -> None:
"""Test creating AsusWRT default sensors and tracker with legacy protocol."""
await _test_sensors(
hass, freezer, mock_devices_legacy, CONFIG_DATA_TELNET, entry_unique_id
)
@pytest.mark.parametrize(
"entry_unique_id",
[None, ROUTER_MAC_ADDR],
)
async def test_sensors_http(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_devices_http,
entry_unique_id,
connect_http,
create_device_registry_devices,
) -> None:
"""Test creating AsusWRT default sensors and tracker with http protocol."""
await _test_sensors(
hass, freezer, mock_devices_http, CONFIG_DATA_HTTP, entry_unique_id
)
async def _test_loadavg_sensors(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, config
) -> None:
"""Test creating an AsusWRT load average sensors."""
config_entry, sensor_prefix = _setup_entry(hass, config, SENSORS_LOAD_AVG)
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()
freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done()
# assert temperature sensor available
assert hass.states.get(f"{sensor_prefix}_sensor_load_avg1").state == "1.1"
assert hass.states.get(f"{sensor_prefix}_sensor_load_avg5").state == "1.2"
assert hass.states.get(f"{sensor_prefix}_sensor_load_avg15").state == "1.3"
async def test_loadavg_sensors_legacy(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, connect_legacy
) -> None:
"""Test creating an AsusWRT load average sensors."""
await _test_loadavg_sensors(hass, freezer, CONFIG_DATA_TELNET)
async def test_loadavg_sensors_http(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, connect_http
) -> None:
"""Test creating an AsusWRT load average sensors."""
await _test_loadavg_sensors(hass, freezer, CONFIG_DATA_HTTP)
async def test_loadavg_sensors_unaivalable_http(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, connect_http
) -> None:
"""Test load average sensors no available using http."""
config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA_HTTP, SENSORS_LOAD_AVG)
config_entry.add_to_hass(hass)
connect_http.return_value.async_get_data.side_effect = (
make_async_get_data_side_effect([AsusData.SYSINFO])
)
# initial devices setup
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done()
# assert load average sensors not available
assert not hass.states.get(f"{sensor_prefix}_sensor_load_avg1")
assert not hass.states.get(f"{sensor_prefix}_sensor_load_avg5")
assert not hass.states.get(f"{sensor_prefix}_sensor_load_avg15")
async def test_temperature_sensors_http_fail(
hass: HomeAssistant, connect_http_sens_fail
) -> None:
"""Test fail creating AsusWRT temperature sensors."""
_ = connect_http_sens_fail([AsusData.TEMPERATURE])
config_entry, sensor_prefix = _setup_entry(
hass, CONFIG_DATA_HTTP, SENSORS_TEMPERATURES
)
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()
# assert temperature availability exception is handled correctly
assert not hass.states.get(f"{sensor_prefix}_2_4ghz")
assert not hass.states.get(f"{sensor_prefix}_5_0ghz")
assert not hass.states.get(f"{sensor_prefix}_cpu")
assert not hass.states.get(f"{sensor_prefix}_5_0ghz_2")
assert not hass.states.get(f"{sensor_prefix}_6_0ghz")
async def _test_temperature_sensors(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, config, sensors
) -> str:
"""Test creating a AsusWRT temperature sensors."""
config_entry, sensor_prefix = _setup_entry(hass, config, sensors)
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()
freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done()
return sensor_prefix
async def test_temperature_sensors_legacy(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, connect_legacy
) -> None:
"""Test creating a AsusWRT temperature sensors."""
sensor_prefix = await _test_temperature_sensors(
hass, freezer, CONFIG_DATA_TELNET, SENSORS_TEMPERATURES_LEGACY
)
# assert temperature sensor available
assert hass.states.get(f"{sensor_prefix}_2_4ghz").state == "40.2"
assert hass.states.get(f"{sensor_prefix}_cpu").state == "71.2"
assert not hass.states.get(f"{sensor_prefix}_5_0ghz")
async def test_temperature_sensors_http(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, connect_http
) -> None:
"""Test creating a AsusWRT temperature sensors."""
sensor_prefix = await _test_temperature_sensors(
hass, freezer, CONFIG_DATA_HTTP, SENSORS_TEMPERATURES
)
# assert temperature sensor available
assert hass.states.get(f"{sensor_prefix}_2_4ghz").state == "40.2"
assert hass.states.get(f"{sensor_prefix}_cpu").state == "71.2"
assert hass.states.get(f"{sensor_prefix}_5_0ghz_2").state == "40.3"
assert hass.states.get(f"{sensor_prefix}_6_0ghz").state == "40.4"
assert not hass.states.get(f"{sensor_prefix}_5_0ghz")
async def test_cpu_sensors_http_fail(
hass: HomeAssistant, connect_http_sens_fail
) -> None:
"""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.add_to_hass(hass)
# initial devices setup
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# assert cpu availability exception is handled correctly
assert not hass.states.get(f"{sensor_prefix}_cpu1_usage")
assert not hass.states.get(f"{sensor_prefix}_cpu2_usage")
assert not hass.states.get(f"{sensor_prefix}_cpu3_usage")
assert not hass.states.get(f"{sensor_prefix}_cpu4_usage")
assert not hass.states.get(f"{sensor_prefix}_cpu5_usage")
assert not hass.states.get(f"{sensor_prefix}_cpu6_usage")
assert not hass.states.get(f"{sensor_prefix}_cpu7_usage")
assert not hass.states.get(f"{sensor_prefix}_cpu8_usage")
assert not hass.states.get(f"{sensor_prefix}_cpu_total_usage")
async def test_cpu_sensors_http(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
connect_http,
connect_http_sens_detect,
) -> None:
"""Test creating AsusWRT cpu sensors."""
config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA_HTTP, SENSORS_CPU)
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()
freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done()
# assert cpu sensors available
assert hass.states.get(f"{sensor_prefix}_cpu1_usage").state == "0.1"
assert hass.states.get(f"{sensor_prefix}_cpu2_usage").state == "0.2"
assert hass.states.get(f"{sensor_prefix}_cpu3_usage").state == "0.3"
assert hass.states.get(f"{sensor_prefix}_cpu4_usage").state == "0.4"
assert hass.states.get(f"{sensor_prefix}_cpu5_usage").state == "0.5"
assert hass.states.get(f"{sensor_prefix}_cpu6_usage").state == "0.6"
assert hass.states.get(f"{sensor_prefix}_cpu7_usage").state == "0.7"
assert hass.states.get(f"{sensor_prefix}_cpu8_usage").state == "0.8"
assert hass.states.get(f"{sensor_prefix}_cpu_total_usage").state == "0.9"
async def test_memory_sensors_http(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, connect_http
) -> None:
"""Test creating AsusWRT memory sensors."""
config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA_HTTP, SENSORS_MEMORY)
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()
freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done()
# assert memory sensors available
assert hass.states.get(f"{sensor_prefix}_mem_usage_perc").state == "52.4"
assert hass.states.get(f"{sensor_prefix}_mem_free").state == "384.0"
assert hass.states.get(f"{sensor_prefix}_mem_used").state == "640.0"
async def test_uptime_sensors_http(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, connect_http
) -> None:
"""Test creating AsusWRT uptime sensors."""
config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA_HTTP, SENSORS_UPTIME)
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()
freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done()
# assert uptime sensors available
assert (
hass.states.get(f"{sensor_prefix}_sensor_last_boot").state
== "2024-08-02T00:47:00+00:00"
)
assert hass.states.get(f"{sensor_prefix}_sensor_uptime").state == "1625927"
@pytest.mark.parametrize(
"side_effect",
[OSError, None],
)
async def test_connect_fail_legacy(
hass: HomeAssistant, connect_legacy, side_effect
) -> None:
"""Test AsusWRT connect fail."""
# init config entry
config_entry = MockConfigEntry(
domain=DOMAIN,
data=CONFIG_DATA_TELNET,
)
config_entry.add_to_hass(hass)
connect_legacy.return_value.connection.async_connect.side_effect = side_effect
connect_legacy.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
@pytest.mark.parametrize(
"side_effect",
[AsusRouterError, None],
)
async def test_connect_fail_http(
hass: HomeAssistant, connect_http, side_effect
) -> None:
"""Test AsusWRT connect fail."""
# init config entry
config_entry = MockConfigEntry(
domain=DOMAIN,
data=CONFIG_DATA_HTTP,
)
config_entry.add_to_hass(hass)
connect_http.return_value.async_connect.side_effect = side_effect
connect_http.return_value.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, freezer: FrozenDateTimeFactory, config, sensors
) -> None:
"""Test AsusWRT sensors are unavailable when polling fails."""
config_entry, sensor_prefix = _setup_entry(hass, config, sensors)
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()
freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done()
for sensor_name in sensors:
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_sensors_polling_fails_legacy(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
connect_legacy_sens_fail,
) -> None:
"""Test AsusWRT sensors are unavailable when polling fails."""
await _test_sensors_polling_fails(
hass, freezer, CONFIG_DATA_TELNET, SENSORS_ALL_LEGACY
)
async def test_sensors_polling_fails_http(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
connect_http_sens_fail,
connect_http_sens_detect,
) -> None:
"""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)
async def test_options_reload(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, connect_legacy
) -> None:
"""Test AsusWRT integration is reload changing an options that require this."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data=CONFIG_DATA_TELNET,
unique_id=ROUTER_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()
assert connect_legacy.return_value.connection.async_connect.call_count == 1
freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done()
# 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 config_entry.state is ConfigEntryState.LOADED
assert connect_legacy.return_value.connection.async_connect.call_count == 2
async def test_unique_id_migration(
hass: HomeAssistant, entity_registry: er.EntityRegistry, connect_legacy
) -> None:
"""Test AsusWRT entities unique id format migration."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data=CONFIG_DATA_TELNET,
unique_id=ROUTER_MAC_ADDR,
)
config_entry.add_to_hass(hass)
obj_entity_id = slugify(f"{HOST} Upload")
entity_registry.async_get_or_create(
sensor.DOMAIN,
DOMAIN,
f"{DOMAIN} {ROUTER_MAC_ADDR} Upload",
suggested_object_id=obj_entity_id,
config_entry=config_entry,
disabled_by=None,
)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
migr_entity = entity_registry.async_get(f"{sensor.DOMAIN}.{obj_entity_id}")
assert migr_entity is not None
assert migr_entity.unique_id == slugify(f"{ROUTER_MAC_ADDR}_sensor_tx_bytes")
async def test_decorator_errors(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
connect_legacy,
mock_available_temps,
) -> None:
"""Test AsusWRT sensors are unavailable on decorator type check error."""
sensors = [*SENSORS_BYTES, *SENSORS_TEMPERATURES_LEGACY]
config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA_TELNET, sensors)
config_entry.add_to_hass(hass)
mock_available_temps[1] = True
connect_legacy.return_value.async_get_bytes_total.return_value = "bad_response"
connect_legacy.return_value.async_get_temperature.return_value = "bad_response"
# initial devices setup
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done()
for sensor_name in sensors:
assert (
hass.states.get(f"{sensor_prefix}_{slugify(sensor_name)}").state
== STATE_UNAVAILABLE
)