From ac26c2378badf0f7bb13099924214a00f8a51879 Mon Sep 17 00:00:00 2001 From: Chen-IL <18098431+Chen-IL@users.noreply.github.com> Date: Fri, 3 Dec 2021 21:27:17 +0200 Subject: [PATCH] Add temperature sensors for Asuswrt (#58303) Co-authored-by: Paulus Schoutsen --- homeassistant/components/asuswrt/const.py | 1 + homeassistant/components/asuswrt/router.py | 35 ++++++++++++++++++ homeassistant/components/asuswrt/sensor.py | 41 +++++++++++++++++++++- tests/components/asuswrt/test_sensor.py | 39 ++++++++++++++++++-- 4 files changed, 113 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/asuswrt/const.py b/homeassistant/components/asuswrt/const.py index e41d683a7df..95e93e0ff25 100644 --- a/homeassistant/components/asuswrt/const.py +++ b/homeassistant/components/asuswrt/const.py @@ -25,3 +25,4 @@ SENSORS_BYTES = ["sensor_rx_bytes", "sensor_tx_bytes"] SENSORS_CONNECTED_DEVICE = ["sensor_connected_device"] SENSORS_LOAD_AVG = ["sensor_load_avg1", "sensor_load_avg5", "sensor_load_avg15"] SENSORS_RATES = ["sensor_rx_rates", "sensor_tx_rates"] +SENSORS_TEMPERATURES = ["2.4GHz", "5.0GHz", "CPU"] diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index 986ba828466..da314b12b65 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -47,6 +47,7 @@ from .const import ( SENSORS_CONNECTED_DEVICE, SENSORS_LOAD_AVG, SENSORS_RATES, + SENSORS_TEMPERATURES, ) 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_LOAD_AVG = "sensors_load_avg" SENSORS_TYPE_RATES = "sensors_rates" +SENSORS_TYPE_TEMPERATURES = "sensors_temperatures" _LOGGER = logging.getLogger(__name__) @@ -114,6 +116,15 @@ class AsusWrtSensorDataHandler: 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): """Update connected devices attribute.""" if self._connected_devices == conn_devices: @@ -131,6 +142,8 @@ class AsusWrtSensorDataHandler: method = self._get_load_avg elif sensor_type == SENSORS_TYPE_RATES: method = self._get_rates + elif sensor_type == SENSORS_TYPE_TEMPERATURES: + method = self._get_temperatures else: raise RuntimeError(f"Invalid sensor type: {sensor_type}") @@ -349,9 +362,14 @@ class AsusWrtRouter: SENSORS_TYPE_COUNT: SENSORS_CONNECTED_DEVICE, SENSORS_TYPE_LOAD_AVG: SENSORS_LOAD_AVG, SENSORS_TYPE_RATES: SENSORS_RATES, + SENSORS_TYPE_TEMPERATURES: SENSORS_TEMPERATURES, } 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( sensor_type, sensor_type != SENSORS_TYPE_COUNT ) @@ -370,6 +388,23 @@ class AsusWrtRouter: if self._sensors_data_handler.update_device_count(self._connected_devices): 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: """Close the connection.""" if self._api is not None and self._protocol == PROTOCOL_TELNET: diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 7c375ed3c20..2c3b022cb9d 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -5,12 +5,17 @@ from dataclasses import dataclass from numbers import Real from homeassistant.components.sensor import ( + SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) 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.helpers.entity import EntityCategory from homeassistant.helpers.update_coordinator import ( @@ -25,6 +30,7 @@ from .const import ( SENSORS_CONNECTED_DEVICE, SENSORS_LOAD_AVG, SENSORS_RATES, + SENSORS_TEMPERATURES, ) from .router import KEY_COORDINATOR, KEY_SENSORS, AsusWrtRouter @@ -114,6 +120,39 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( factor=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, + ), ) diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index 19c27777c2a..b8537a5e6a6 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -40,6 +40,7 @@ CONFIG_DATA = { 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, "5.0GHz": 0, "CPU": 71.2} SENSOR_NAMES = [ "Devices Connected", @@ -50,6 +51,9 @@ SENSOR_NAMES = [ "Load Avg (1m)", "Load Avg (5m)", "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") -def mock_controller_connect(mock_devices): +def mock_controller_connect(mock_devices, mock_available_temps): """Mock a successful connection.""" with patch("homeassistant.components.asuswrt.router.AsusWrt") as service_mock: 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( 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 -async def test_sensors(hass, connect, mock_devices): +async def test_sensors(hass, connect, mock_devices, mock_available_temps): """Test creating an AsusWRT sensor.""" 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}_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 mock_devices.pop("a1:b1:c1:d1:e1:f1") 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 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"