Add Unifi device state for additional diagnostics (#105138)

* Add device state for additional diagnostics

* Add state test and fix existing tests

* Utilize IntEnum and dict for state lookup

* Update aiounifi to v68
This commit is contained in:
Joseph Block 2023-12-16 02:38:21 -05:00 committed by GitHub
parent 9c134c6b51
commit 1271f16385
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 95 additions and 9 deletions

View File

@ -2,6 +2,8 @@
import logging import logging
from aiounifi.models.device import DeviceState
from homeassistant.const import Platform from homeassistant.const import Platform
LOGGER = logging.getLogger(__package__) LOGGER = logging.getLogger(__package__)
@ -46,3 +48,19 @@ ATTR_MANUFACTURER = "Ubiquiti Networks"
BLOCK_SWITCH = "block" BLOCK_SWITCH = "block"
DPI_SWITCH = "dpi" DPI_SWITCH = "dpi"
OUTLET_SWITCH = "outlet" OUTLET_SWITCH = "outlet"
DEVICE_STATES = {
DeviceState.DISCONNECTED: "Disconnected",
DeviceState.CONNECTED: "Connected",
DeviceState.PENDING: "Pending",
DeviceState.FIRMWARE_MISMATCH: "Firmware Mismatch",
DeviceState.UPGRADING: "Upgrading",
DeviceState.PROVISIONING: "Provisioning",
DeviceState.HEARTBEAT_MISSED: "Heartbeat Missed",
DeviceState.ADOPTING: "Adopting",
DeviceState.DELETING: "Deleting",
DeviceState.INFORM_ERROR: "Inform Error",
DeviceState.ADOPTION_FALIED: "Adoption Failed",
DeviceState.ISOLATED: "Isolated",
DeviceState.UNKNOWN: "Unknown",
}

View File

@ -8,7 +8,7 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aiounifi"], "loggers": ["aiounifi"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["aiounifi==67"], "requirements": ["aiounifi==68"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "Ubiquiti Networks", "manufacturer": "Ubiquiti Networks",

View File

@ -36,6 +36,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .const import DEVICE_STATES
from .controller import UniFiController from .controller import UniFiController
from .entity import ( from .entity import (
HandlerT, HandlerT,
@ -138,6 +139,12 @@ class UnifiSensorEntityDescriptionMixin(Generic[HandlerT, ApiItemT]):
value_fn: Callable[[UniFiController, ApiItemT], datetime | float | str | None] value_fn: Callable[[UniFiController, ApiItemT], datetime | float | str | None]
@callback
def async_device_state_value_fn(controller: UniFiController, device: Device) -> str:
"""Retrieve the state of the device."""
return DEVICE_STATES[device.state]
@dataclass @dataclass
class UnifiSensorEntityDescription( class UnifiSensorEntityDescription(
SensorEntityDescription, SensorEntityDescription,
@ -343,6 +350,25 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = (
unique_id_fn=lambda controller, obj_id: f"device_temperature-{obj_id}", unique_id_fn=lambda controller, obj_id: f"device_temperature-{obj_id}",
value_fn=lambda ctrlr, device: device.general_temperature, value_fn=lambda ctrlr, device: device.general_temperature,
), ),
UnifiSensorEntityDescription[Devices, Device](
key="Device State",
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
has_entity_name=True,
allowed_fn=lambda controller, obj_id: True,
api_handler_fn=lambda api: api.devices,
available_fn=async_device_available_fn,
device_info_fn=async_device_device_info_fn,
event_is_on=None,
event_to_subscribe=None,
name_fn=lambda device: "State",
object_fn=lambda api, obj_id: api.devices[obj_id],
should_poll=False,
supported_fn=lambda controller, obj_id: True,
unique_id_fn=lambda controller, obj_id: f"device_state-{obj_id}",
value_fn=async_device_state_value_fn,
options=list(DEVICE_STATES.values()),
),
) )

View File

@ -377,7 +377,7 @@ aiosyncthing==0.5.1
aiotractive==0.5.6 aiotractive==0.5.6
# homeassistant.components.unifi # homeassistant.components.unifi
aiounifi==67 aiounifi==68
# homeassistant.components.vlc_telnet # homeassistant.components.vlc_telnet
aiovlc==0.1.0 aiovlc==0.1.0

View File

@ -350,7 +350,7 @@ aiosyncthing==0.5.1
aiotractive==0.5.6 aiotractive==0.5.6
# homeassistant.components.unifi # homeassistant.components.unifi
aiounifi==67 aiounifi==68
# homeassistant.components.vlc_telnet # homeassistant.components.vlc_telnet
aiovlc==0.1.0 aiovlc==0.1.0

View File

@ -3,6 +3,7 @@ from copy import deepcopy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest.mock import patch from unittest.mock import patch
from aiounifi.models.device import DeviceState
from aiounifi.models.message import MessageKey from aiounifi.models.message import MessageKey
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
@ -20,6 +21,7 @@ from homeassistant.components.unifi.const import (
CONF_ALLOW_UPTIME_SENSORS, CONF_ALLOW_UPTIME_SENSORS,
CONF_TRACK_CLIENTS, CONF_TRACK_CLIENTS,
CONF_TRACK_DEVICES, CONF_TRACK_DEVICES,
DEVICE_STATES,
) )
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, EntityCategory from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, EntityCategory
@ -584,7 +586,7 @@ async def test_poe_port_switches(
) -> None: ) -> None:
"""Test the update_items function with some clients.""" """Test the update_items function with some clients."""
await setup_unifi_integration(hass, aioclient_mock, devices_response=[DEVICE_1]) await setup_unifi_integration(hass, aioclient_mock, devices_response=[DEVICE_1])
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2
ent_reg = er.async_get(hass) ent_reg = er.async_get(hass)
ent_reg_entry = ent_reg.async_get("sensor.mock_name_port_1_poe_power") ent_reg_entry = ent_reg.async_get("sensor.mock_name_port_1_poe_power")
@ -807,8 +809,8 @@ async def test_outlet_power_readings(
"""Test the outlet power reporting on PDU devices.""" """Test the outlet power reporting on PDU devices."""
await setup_unifi_integration(hass, aioclient_mock, devices_response=[PDU_DEVICE_1]) await setup_unifi_integration(hass, aioclient_mock, devices_response=[PDU_DEVICE_1])
assert len(hass.states.async_all()) == 10 assert len(hass.states.async_all()) == 11
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 5
ent_reg = er.async_get(hass) ent_reg = er.async_get(hass)
ent_reg_entry = ent_reg.async_get(f"sensor.{entity_id}") ent_reg_entry = ent_reg.async_get(f"sensor.{entity_id}")
@ -856,7 +858,7 @@ async def test_device_uptime(
now = datetime(2021, 1, 1, 1, 1, 0, tzinfo=dt_util.UTC) now = datetime(2021, 1, 1, 1, 1, 0, tzinfo=dt_util.UTC)
with patch("homeassistant.util.dt.now", return_value=now): with patch("homeassistant.util.dt.now", return_value=now):
await setup_unifi_integration(hass, aioclient_mock, devices_response=[device]) await setup_unifi_integration(hass, aioclient_mock, devices_response=[device])
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2
assert hass.states.get("sensor.device_uptime").state == "2021-01-01T01:00:00+00:00" assert hass.states.get("sensor.device_uptime").state == "2021-01-01T01:00:00+00:00"
ent_reg = er.async_get(hass) ent_reg = er.async_get(hass)
@ -912,7 +914,7 @@ async def test_device_temperature(
} }
await setup_unifi_integration(hass, aioclient_mock, devices_response=[device]) await setup_unifi_integration(hass, aioclient_mock, devices_response=[device])
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3
assert hass.states.get("sensor.device_temperature").state == "30" assert hass.states.get("sensor.device_temperature").state == "30"
ent_reg = er.async_get(hass) ent_reg = er.async_get(hass)
@ -925,3 +927,43 @@ async def test_device_temperature(
device["general_temperature"] = 60 device["general_temperature"] = 60
mock_unifi_websocket(message=MessageKey.DEVICE, data=device) mock_unifi_websocket(message=MessageKey.DEVICE, data=device)
assert hass.states.get("sensor.device_temperature").state == "60" assert hass.states.get("sensor.device_temperature").state == "60"
async def test_device_state(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket
) -> None:
"""Verify that state sensors are working as expected."""
device = {
"board_rev": 3,
"device_id": "mock-id",
"general_temperature": 30,
"has_fan": True,
"has_temperature": True,
"fan_level": 0,
"ip": "10.0.1.1",
"last_seen": 1562600145,
"mac": "00:00:00:00:01:01",
"model": "US16P150",
"name": "Device",
"next_interval": 20,
"overheating": True,
"state": 1,
"type": "usw",
"upgradable": True,
"uptime": 60,
"version": "4.0.42.10433",
}
await setup_unifi_integration(hass, aioclient_mock, devices_response=[device])
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3
ent_reg = er.async_get(hass)
assert (
ent_reg.async_get("sensor.device_state").entity_category
is EntityCategory.DIAGNOSTIC
)
for i in list(map(int, DeviceState)):
device["state"] = i
mock_unifi_websocket(message=MessageKey.DEVICE, data=device)
assert hass.states.get("sensor.device_state").state == DEVICE_STATES[i]

View File

@ -117,7 +117,7 @@ async def test_device_updates(
# Simulate update finished # Simulate update finished
device_1["state"] = "0" device_1["state"] = 0
device_1["version"] = "4.3.17.11279" device_1["version"] = "4.3.17.11279"
device_1["upgradable"] = False device_1["upgradable"] = False
del device_1["upgrade_to_firmware"] del device_1["upgrade_to_firmware"]