Add UniFi power stats for PDU overall AC outlet metrics (#98217)

This commit is contained in:
Chris 2023-08-12 09:12:59 -07:00 committed by GitHub
parent ae8f9dcb77
commit 87753bdb82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 126 additions and 19 deletions

View File

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

View File

@ -12,11 +12,13 @@ from typing import Generic
from aiounifi.interfaces.api_handlers import ItemEvent
from aiounifi.interfaces.clients import Clients
from aiounifi.interfaces.devices import Devices
from aiounifi.interfaces.outlets import Outlets
from aiounifi.interfaces.ports import Ports
from aiounifi.interfaces.wlans import Wlans
from aiounifi.models.api import ApiItemT
from aiounifi.models.client import Client
from aiounifi.models.device import Device
from aiounifi.models.outlet import Outlet
from aiounifi.models.port import Port
from aiounifi.models.wlan import Wlan
@ -96,6 +98,12 @@ def async_device_outlet_power_supported_fn(
return controller.api.outlets[obj_id].caps == 3
@callback
def async_device_outlet_supported_fn(controller: UniFiController, obj_id: str) -> bool:
"""Determine if a device supports reading overall power metrics."""
return controller.api.devices[obj_id].outlet_ac_power_budget is not None
@dataclass
class UnifiSensorEntityDescriptionMixin(Generic[HandlerT, ApiItemT]):
"""Validate and load entities from different UniFi handlers."""
@ -224,6 +232,46 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = (
unique_id_fn=lambda controller, obj_id: f"outlet_power-{obj_id}",
value_fn=lambda _, obj: obj.power if obj.relay_state else "0",
),
UnifiSensorEntityDescription[Devices, Device](
key="SmartPower AC power budget",
device_class=SensorDeviceClass.POWER,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfPower.WATT,
suggested_display_precision=1,
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: "AC Power Budget",
object_fn=lambda api, obj_id: api.devices[obj_id],
should_poll=False,
supported_fn=async_device_outlet_supported_fn,
unique_id_fn=lambda controller, obj_id: f"ac_power_budget-{obj_id}",
value_fn=lambda controller, device: device.outlet_ac_power_budget,
),
UnifiSensorEntityDescription[Devices, Device](
key="SmartPower AC power consumption",
device_class=SensorDeviceClass.POWER,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfPower.WATT,
suggested_display_precision=1,
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: "AC Power Consumption",
object_fn=lambda api, obj_id: api.devices[obj_id],
should_poll=False,
supported_fn=async_device_outlet_supported_fn,
unique_id_fn=lambda controller, obj_id: f"ac_power_conumption-{obj_id}",
value_fn=lambda controller, device: device.outlet_ac_power_consumption,
),
)

View File

@ -360,7 +360,7 @@ aiosyncthing==0.5.1
aiotractive==0.5.5
# homeassistant.components.unifi
aiounifi==52
aiounifi==53
# homeassistant.components.vlc_telnet
aiovlc==0.1.0

View File

@ -335,7 +335,7 @@ aiosyncthing==0.5.1
aiotractive==0.5.5
# homeassistant.components.unifi
aiounifi==52
aiounifi==53
# homeassistant.components.vlc_telnet
aiovlc==0.1.0

View File

@ -278,6 +278,27 @@ PDU_DEVICE_1 = {
"x_has_ssh_hostkey": True,
}
PDU_OUTLETS_UPDATE_DATA = [
{
"index": 1,
"relay_state": True,
"cycle_enabled": False,
"name": "USB Outlet 1",
"outlet_caps": 1,
},
{
"index": 2,
"relay_state": True,
"cycle_enabled": False,
"name": "Outlet 2",
"outlet_caps": 3,
"outlet_voltage": "119.644",
"outlet_current": "0.935",
"outlet_power": "123.45",
"outlet_power_factor": "0.659",
},
]
async def test_no_clients(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
@ -719,31 +740,69 @@ async def test_wlan_client_sensors(
assert hass.states.get("sensor.ssid_1").state == "0"
@pytest.mark.parametrize(
(
"entity_id",
"expected_unique_id",
"expected_value",
"changed_data",
"expected_update_value",
),
[
(
"dummy_usp_pdu_pro_outlet_2_outlet_power",
"outlet_power-01:02:03:04:05:ff_2",
"73.827",
{"outlet_table": PDU_OUTLETS_UPDATE_DATA},
"123.45",
),
(
"dummy_usp_pdu_pro_ac_power_budget",
"ac_power_budget-01:02:03:04:05:ff",
"1875.000",
None,
None,
),
(
"dummy_usp_pdu_pro_ac_power_consumption",
"ac_power_conumption-01:02:03:04:05:ff",
"201.683",
{"outlet_ac_power_consumption": "456.78"},
"456.78",
),
],
)
async def test_outlet_power_readings(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
mock_unifi_websocket,
entity_id: str,
expected_unique_id: str,
expected_value: any,
changed_data: dict | None,
expected_update_value: any,
) -> None:
"""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()) == 5
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1
assert len(hass.states.async_all()) == 7
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3
ent_reg = er.async_get(hass)
ent_reg_entry = ent_reg.async_get("sensor.dummy_usp_pdu_pro_outlet_2_outlet_power")
assert ent_reg_entry.unique_id == "outlet_power-01:02:03:04:05:ff_2"
ent_reg_entry = ent_reg.async_get(f"sensor.{entity_id}")
assert ent_reg_entry.unique_id == expected_unique_id
assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC
outlet_2 = hass.states.get("sensor.dummy_usp_pdu_pro_outlet_2_outlet_power")
assert outlet_2.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER
assert outlet_2.state == "73.827"
sensor_data = hass.states.get(f"sensor.{entity_id}")
assert sensor_data.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER
assert sensor_data.state == expected_value
# Verify state update
pdu_device_state_update = deepcopy(PDU_DEVICE_1)
if changed_data is not None:
updated_device_data = deepcopy(PDU_DEVICE_1)
updated_device_data.update(changed_data)
pdu_device_state_update["outlet_table"][1]["outlet_power"] = "123.45"
mock_unifi_websocket(message=MessageKey.DEVICE, data=updated_device_data)
await hass.async_block_till_done()
mock_unifi_websocket(message=MessageKey.DEVICE, data=pdu_device_state_update)
await hass.async_block_till_done()
outlet_2 = hass.states.get("sensor.dummy_usp_pdu_pro_outlet_2_outlet_power")
assert outlet_2.state == "123.45"
sensor_data = hass.states.get(f"sensor.{entity_id}")
assert sensor_data.state == expected_update_value