Add temperature sensors for Asuswrt (#58303)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>pull/60936/head
parent
0dfc86956b
commit
ac26c2378b
|
@ -25,3 +25,4 @@ SENSORS_BYTES = ["sensor_rx_bytes", "sensor_tx_bytes"]
|
||||||
SENSORS_CONNECTED_DEVICE = ["sensor_connected_device"]
|
SENSORS_CONNECTED_DEVICE = ["sensor_connected_device"]
|
||||||
SENSORS_LOAD_AVG = ["sensor_load_avg1", "sensor_load_avg5", "sensor_load_avg15"]
|
SENSORS_LOAD_AVG = ["sensor_load_avg1", "sensor_load_avg5", "sensor_load_avg15"]
|
||||||
SENSORS_RATES = ["sensor_rx_rates", "sensor_tx_rates"]
|
SENSORS_RATES = ["sensor_rx_rates", "sensor_tx_rates"]
|
||||||
|
SENSORS_TEMPERATURES = ["2.4GHz", "5.0GHz", "CPU"]
|
||||||
|
|
|
@ -47,6 +47,7 @@ from .const import (
|
||||||
SENSORS_CONNECTED_DEVICE,
|
SENSORS_CONNECTED_DEVICE,
|
||||||
SENSORS_LOAD_AVG,
|
SENSORS_LOAD_AVG,
|
||||||
SENSORS_RATES,
|
SENSORS_RATES,
|
||||||
|
SENSORS_TEMPERATURES,
|
||||||
)
|
)
|
||||||
|
|
||||||
CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]
|
CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]
|
||||||
|
@ -60,6 +61,7 @@ SENSORS_TYPE_BYTES = "sensors_bytes"
|
||||||
SENSORS_TYPE_COUNT = "sensors_count"
|
SENSORS_TYPE_COUNT = "sensors_count"
|
||||||
SENSORS_TYPE_LOAD_AVG = "sensors_load_avg"
|
SENSORS_TYPE_LOAD_AVG = "sensors_load_avg"
|
||||||
SENSORS_TYPE_RATES = "sensors_rates"
|
SENSORS_TYPE_RATES = "sensors_rates"
|
||||||
|
SENSORS_TYPE_TEMPERATURES = "sensors_temperatures"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -114,6 +116,15 @@ class AsusWrtSensorDataHandler:
|
||||||
|
|
||||||
return _get_dict(SENSORS_LOAD_AVG, avg)
|
return _get_dict(SENSORS_LOAD_AVG, avg)
|
||||||
|
|
||||||
|
async def _get_temperatures(self):
|
||||||
|
"""Fetch temperatures information from the router."""
|
||||||
|
try:
|
||||||
|
temperatures = await self._api.async_get_temperature()
|
||||||
|
except (OSError, ValueError) as exc:
|
||||||
|
raise UpdateFailed(exc) from exc
|
||||||
|
|
||||||
|
return temperatures
|
||||||
|
|
||||||
def update_device_count(self, conn_devices: int):
|
def update_device_count(self, conn_devices: int):
|
||||||
"""Update connected devices attribute."""
|
"""Update connected devices attribute."""
|
||||||
if self._connected_devices == conn_devices:
|
if self._connected_devices == conn_devices:
|
||||||
|
@ -131,6 +142,8 @@ class AsusWrtSensorDataHandler:
|
||||||
method = self._get_load_avg
|
method = self._get_load_avg
|
||||||
elif sensor_type == SENSORS_TYPE_RATES:
|
elif sensor_type == SENSORS_TYPE_RATES:
|
||||||
method = self._get_rates
|
method = self._get_rates
|
||||||
|
elif sensor_type == SENSORS_TYPE_TEMPERATURES:
|
||||||
|
method = self._get_temperatures
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(f"Invalid sensor type: {sensor_type}")
|
raise RuntimeError(f"Invalid sensor type: {sensor_type}")
|
||||||
|
|
||||||
|
@ -349,9 +362,14 @@ class AsusWrtRouter:
|
||||||
SENSORS_TYPE_COUNT: SENSORS_CONNECTED_DEVICE,
|
SENSORS_TYPE_COUNT: SENSORS_CONNECTED_DEVICE,
|
||||||
SENSORS_TYPE_LOAD_AVG: SENSORS_LOAD_AVG,
|
SENSORS_TYPE_LOAD_AVG: SENSORS_LOAD_AVG,
|
||||||
SENSORS_TYPE_RATES: SENSORS_RATES,
|
SENSORS_TYPE_RATES: SENSORS_RATES,
|
||||||
|
SENSORS_TYPE_TEMPERATURES: SENSORS_TEMPERATURES,
|
||||||
}
|
}
|
||||||
|
|
||||||
for sensor_type, sensor_names in sensors_types.items():
|
for sensor_type, sensor_names in sensors_types.items():
|
||||||
|
if sensor_type == SENSORS_TYPE_TEMPERATURES:
|
||||||
|
sensor_names = await self._get_available_temperature_sensors()
|
||||||
|
if not sensor_names:
|
||||||
|
continue
|
||||||
coordinator = await self._sensors_data_handler.get_coordinator(
|
coordinator = await self._sensors_data_handler.get_coordinator(
|
||||||
sensor_type, sensor_type != SENSORS_TYPE_COUNT
|
sensor_type, sensor_type != SENSORS_TYPE_COUNT
|
||||||
)
|
)
|
||||||
|
@ -370,6 +388,23 @@ class AsusWrtRouter:
|
||||||
if self._sensors_data_handler.update_device_count(self._connected_devices):
|
if self._sensors_data_handler.update_device_count(self._connected_devices):
|
||||||
await coordinator.async_refresh()
|
await coordinator.async_refresh()
|
||||||
|
|
||||||
|
async def _get_available_temperature_sensors(self):
|
||||||
|
"""Check which temperature information is available on the router."""
|
||||||
|
try:
|
||||||
|
availability = await self._api.async_find_temperature_commands()
|
||||||
|
available_sensors = [
|
||||||
|
SENSORS_TEMPERATURES[i] for i in range(3) if availability[i]
|
||||||
|
]
|
||||||
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Failed checking temperature sensor availability for ASUS router %s. Exception: %s",
|
||||||
|
self._host,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
|
return available_sensors
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
"""Close the connection."""
|
"""Close the connection."""
|
||||||
if self._api is not None and self._protocol == PROTOCOL_TELNET:
|
if self._api is not None and self._protocol == PROTOCOL_TELNET:
|
||||||
|
|
|
@ -5,12 +5,17 @@ from dataclasses import dataclass
|
||||||
from numbers import Real
|
from numbers import Real
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND
|
from homeassistant.const import (
|
||||||
|
DATA_GIGABYTES,
|
||||||
|
DATA_RATE_MEGABITS_PER_SECOND,
|
||||||
|
TEMP_CELSIUS,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity import EntityCategory
|
from homeassistant.helpers.entity import EntityCategory
|
||||||
from homeassistant.helpers.update_coordinator import (
|
from homeassistant.helpers.update_coordinator import (
|
||||||
|
@ -25,6 +30,7 @@ from .const import (
|
||||||
SENSORS_CONNECTED_DEVICE,
|
SENSORS_CONNECTED_DEVICE,
|
||||||
SENSORS_LOAD_AVG,
|
SENSORS_LOAD_AVG,
|
||||||
SENSORS_RATES,
|
SENSORS_RATES,
|
||||||
|
SENSORS_TEMPERATURES,
|
||||||
)
|
)
|
||||||
from .router import KEY_COORDINATOR, KEY_SENSORS, AsusWrtRouter
|
from .router import KEY_COORDINATOR, KEY_SENSORS, AsusWrtRouter
|
||||||
|
|
||||||
|
@ -114,6 +120,39 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
|
||||||
factor=1,
|
factor=1,
|
||||||
precision=1,
|
precision=1,
|
||||||
),
|
),
|
||||||
|
AsusWrtSensorEntityDescription(
|
||||||
|
key=SENSORS_TEMPERATURES[0],
|
||||||
|
name="2.4GHz Temperature",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
native_unit_of_measurement=TEMP_CELSIUS,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
factor=1,
|
||||||
|
precision=1,
|
||||||
|
),
|
||||||
|
AsusWrtSensorEntityDescription(
|
||||||
|
key=SENSORS_TEMPERATURES[1],
|
||||||
|
name="5GHz Temperature",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
native_unit_of_measurement=TEMP_CELSIUS,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
factor=1,
|
||||||
|
precision=1,
|
||||||
|
),
|
||||||
|
AsusWrtSensorEntityDescription(
|
||||||
|
key=SENSORS_TEMPERATURES[2],
|
||||||
|
name="CPU Temperature",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
native_unit_of_measurement=TEMP_CELSIUS,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
factor=1,
|
||||||
|
precision=1,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ CONFIG_DATA = {
|
||||||
MOCK_BYTES_TOTAL = [60000000000, 50000000000]
|
MOCK_BYTES_TOTAL = [60000000000, 50000000000]
|
||||||
MOCK_CURRENT_TRANSFER_RATES = [20000000, 10000000]
|
MOCK_CURRENT_TRANSFER_RATES = [20000000, 10000000]
|
||||||
MOCK_LOAD_AVG = [1.1, 1.2, 1.3]
|
MOCK_LOAD_AVG = [1.1, 1.2, 1.3]
|
||||||
|
MOCK_TEMPERATURES = {"2.4GHz": 40, "5.0GHz": 0, "CPU": 71.2}
|
||||||
|
|
||||||
SENSOR_NAMES = [
|
SENSOR_NAMES = [
|
||||||
"Devices Connected",
|
"Devices Connected",
|
||||||
|
@ -50,6 +51,9 @@ SENSOR_NAMES = [
|
||||||
"Load Avg (1m)",
|
"Load Avg (1m)",
|
||||||
"Load Avg (5m)",
|
"Load Avg (5m)",
|
||||||
"Load Avg (15m)",
|
"Load Avg (15m)",
|
||||||
|
"2.4GHz Temperature",
|
||||||
|
"5GHz Temperature",
|
||||||
|
"CPU Temperature",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,8 +66,16 @@ def mock_devices_fixture():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="mock_available_temps")
|
||||||
|
def mock_available_temps_list():
|
||||||
|
"""Mock a list of available temperature sensors."""
|
||||||
|
|
||||||
|
# Only length of 3 booleans is valid. First checking the exception handling.
|
||||||
|
return [True, False]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="connect")
|
@pytest.fixture(name="connect")
|
||||||
def mock_controller_connect(mock_devices):
|
def mock_controller_connect(mock_devices, mock_available_temps):
|
||||||
"""Mock a successful connection."""
|
"""Mock a successful connection."""
|
||||||
with patch("homeassistant.components.asuswrt.router.AsusWrt") as service_mock:
|
with patch("homeassistant.components.asuswrt.router.AsusWrt") as service_mock:
|
||||||
service_mock.return_value.connection.async_connect = AsyncMock()
|
service_mock.return_value.connection.async_connect = AsyncMock()
|
||||||
|
@ -88,10 +100,16 @@ def mock_controller_connect(mock_devices):
|
||||||
service_mock.return_value.async_get_loadavg = AsyncMock(
|
service_mock.return_value.async_get_loadavg = AsyncMock(
|
||||||
return_value=MOCK_LOAD_AVG
|
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
|
yield service_mock
|
||||||
|
|
||||||
|
|
||||||
async def test_sensors(hass, connect, mock_devices):
|
async def test_sensors(hass, connect, mock_devices, mock_available_temps):
|
||||||
"""Test creating an AsusWRT sensor."""
|
"""Test creating an AsusWRT sensor."""
|
||||||
entity_reg = er.async_get(hass)
|
entity_reg = er.async_get(hass)
|
||||||
|
|
||||||
|
@ -137,6 +155,11 @@ async def test_sensors(hass, connect, mock_devices):
|
||||||
assert hass.states.get(f"{sensor_prefix}_load_avg_15m").state == "1.3"
|
assert hass.states.get(f"{sensor_prefix}_load_avg_15m").state == "1.3"
|
||||||
assert hass.states.get(f"{sensor_prefix}_devices_connected").state == "2"
|
assert hass.states.get(f"{sensor_prefix}_devices_connected").state == "2"
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
|
||||||
# add one device and remove another
|
# add one device and remove another
|
||||||
mock_devices.pop("a1:b1:c1:d1:e1:f1")
|
mock_devices.pop("a1:b1:c1:d1:e1:f1")
|
||||||
mock_devices["a3:b3:c3:d3:e3:f3"] = Device(
|
mock_devices["a3:b3:c3:d3:e3:f3"] = Device(
|
||||||
|
@ -161,3 +184,15 @@ async def test_sensors(hass, connect, mock_devices):
|
||||||
|
|
||||||
# consider home option not set, device "test" not home
|
# consider home option not set, 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}.test").state == STATE_NOT_HOME
|
||||||
|
|
||||||
|
# checking temperature sensors without exceptions
|
||||||
|
mock_available_temps.append(True)
|
||||||
|
await hass.config_entries.async_reload(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"{sensor_prefix}_load_avg_15m").state == "1.3"
|
||||||
|
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"
|
||||||
|
|
Loading…
Reference in New Issue