From f23e48f373402e68baf479d06095150ac3434c77 Mon Sep 17 00:00:00 2001 From: Kim de Vos Date: Mon, 8 Apr 2024 19:29:54 +0200 Subject: [PATCH] Add sensor for CPU and memory utilization for unifi device (#114986) Add system stats to unifi device sensors --- homeassistant/components/unifi/sensor.py | 41 +++++++++++++++++++- tests/components/unifi/test_sensor.py | 48 +++++++++++++++++++++++- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 54ecc2ea763..360f40384c9 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -10,6 +10,7 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import date, datetime, timedelta from decimal import Decimal +from functools import partial from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.clients import Clients @@ -32,7 +33,7 @@ from homeassistant.components.sensor import ( UnitOfTemperature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory, UnitOfDataRate, UnitOfPower +from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfDataRate, UnitOfPower from homeassistant.core import Event as core_Event, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -140,6 +141,16 @@ def async_device_outlet_supported_fn(hub: UnifiHub, obj_id: str) -> bool: return hub.api.devices[obj_id].outlet_ac_power_budget is not None +def device_system_stats_supported_fn( + stat_index: int, hub: UnifiHub, obj_id: str +) -> bool: + """Determine if a device supports reading item at index in system stats.""" + return ( + "system-stats" in hub.api.devices[obj_id].raw + and hub.api.devices[obj_id].system_stats[stat_index] != "" + ) + + @callback def async_client_is_connected_fn(hub: UnifiHub, obj_id: str) -> bool: """Check if client was last seen recently.""" @@ -352,6 +363,34 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( unique_id_fn=lambda hub, obj_id: f"password-{obj_id}", value_fn=lambda hub, obj: obj.x_passphrase, ), + UnifiSensorEntityDescription[Devices, Device]( + key="Device CPU utilization", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + api_handler_fn=lambda api: api.devices, + available_fn=async_device_available_fn, + device_info_fn=async_device_device_info_fn, + name_fn=lambda device: "CPU utilization", + object_fn=lambda api, obj_id: api.devices[obj_id], + supported_fn=partial(device_system_stats_supported_fn, 0), + unique_id_fn=lambda hub, obj_id: f"cpu_utilization-{obj_id}", + value_fn=lambda hub, device: device.system_stats[0], + ), + UnifiSensorEntityDescription[Devices, Device]( + key="Device memory utilization", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + api_handler_fn=lambda api: api.devices, + available_fn=async_device_available_fn, + device_info_fn=async_device_device_info_fn, + name_fn=lambda device: "Memory utilization", + object_fn=lambda api, obj_id: api.devices[obj_id], + supported_fn=partial(device_system_stats_supported_fn, 1), + unique_id_fn=lambda hub, obj_id: f"memory_utilization-{obj_id}", + value_fn=lambda hub, device: device.system_stats[1], + ), ) diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 239707aa4c9..e8f9f763409 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -835,8 +835,8 @@ async def test_outlet_power_readings( """Test the outlet power reporting on PDU devices.""" await setup_unifi_integration(hass, aioclient_mock, devices_response=[PDU_DEVICE_1]) - assert len(hass.states.async_all()) == 11 - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 5 + assert len(hass.states.async_all()) == 13 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 7 ent_reg_entry = entity_registry.async_get(f"sensor.{entity_id}") assert ent_reg_entry.unique_id == expected_unique_id @@ -1069,3 +1069,47 @@ async def test_wlan_password( mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan_1) await hass.async_block_till_done() assert hass.states.get(sensor_password).state == password + + +async def test_device_system_stats( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + aioclient_mock: AiohttpClientMocker, + mock_unifi_websocket, +) -> None: + """Verify that device stats sensors are working as expected.""" + + device = { + "device_id": "mock-id", + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device", + "state": 1, + "version": "4.0.42.10433", + "system-stats": {"cpu": 5.8, "mem": 31.1, "uptime": 7316}, + } + + await setup_unifi_integration(hass, aioclient_mock, devices_response=[device]) + + assert len(hass.states.async_all()) == 8 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4 + + assert hass.states.get("sensor.device_cpu_utilization").state == "5.8" + assert hass.states.get("sensor.device_memory_utilization").state == "31.1" + + assert ( + entity_registry.async_get("sensor.device_cpu_utilization").entity_category + is EntityCategory.DIAGNOSTIC + ) + + assert ( + entity_registry.async_get("sensor.device_memory_utilization").entity_category + is EntityCategory.DIAGNOSTIC + ) + + # Verify new event change system-stats + device["system-stats"] = {"cpu": 7.7, "mem": 33.3, "uptime": 7316} + mock_unifi_websocket(message=MessageKey.DEVICE, data=device) + + assert hass.states.get("sensor.device_cpu_utilization").state == "7.7" + assert hass.states.get("sensor.device_memory_utilization").state == "33.3"