Add UniFi power stats for PDU overall AC outlet metrics (#98217)
parent
ae8f9dcb77
commit
87753bdb82
|
@ -8,7 +8,7 @@
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aiounifi"],
|
"loggers": ["aiounifi"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["aiounifi==52"],
|
"requirements": ["aiounifi==53"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "Ubiquiti Networks",
|
"manufacturer": "Ubiquiti Networks",
|
||||||
|
|
|
@ -12,11 +12,13 @@ from typing import Generic
|
||||||
|
|
||||||
from aiounifi.interfaces.api_handlers import ItemEvent
|
from aiounifi.interfaces.api_handlers import ItemEvent
|
||||||
from aiounifi.interfaces.clients import Clients
|
from aiounifi.interfaces.clients import Clients
|
||||||
|
from aiounifi.interfaces.devices import Devices
|
||||||
from aiounifi.interfaces.outlets import Outlets
|
from aiounifi.interfaces.outlets import Outlets
|
||||||
from aiounifi.interfaces.ports import Ports
|
from aiounifi.interfaces.ports import Ports
|
||||||
from aiounifi.interfaces.wlans import Wlans
|
from aiounifi.interfaces.wlans import Wlans
|
||||||
from aiounifi.models.api import ApiItemT
|
from aiounifi.models.api import ApiItemT
|
||||||
from aiounifi.models.client import Client
|
from aiounifi.models.client import Client
|
||||||
|
from aiounifi.models.device import Device
|
||||||
from aiounifi.models.outlet import Outlet
|
from aiounifi.models.outlet import Outlet
|
||||||
from aiounifi.models.port import Port
|
from aiounifi.models.port import Port
|
||||||
from aiounifi.models.wlan import Wlan
|
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
|
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
|
@dataclass
|
||||||
class UnifiSensorEntityDescriptionMixin(Generic[HandlerT, ApiItemT]):
|
class UnifiSensorEntityDescriptionMixin(Generic[HandlerT, ApiItemT]):
|
||||||
"""Validate and load entities from different UniFi handlers."""
|
"""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}",
|
unique_id_fn=lambda controller, obj_id: f"outlet_power-{obj_id}",
|
||||||
value_fn=lambda _, obj: obj.power if obj.relay_state else "0",
|
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,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -360,7 +360,7 @@ aiosyncthing==0.5.1
|
||||||
aiotractive==0.5.5
|
aiotractive==0.5.5
|
||||||
|
|
||||||
# homeassistant.components.unifi
|
# homeassistant.components.unifi
|
||||||
aiounifi==52
|
aiounifi==53
|
||||||
|
|
||||||
# homeassistant.components.vlc_telnet
|
# homeassistant.components.vlc_telnet
|
||||||
aiovlc==0.1.0
|
aiovlc==0.1.0
|
||||||
|
|
|
@ -335,7 +335,7 @@ aiosyncthing==0.5.1
|
||||||
aiotractive==0.5.5
|
aiotractive==0.5.5
|
||||||
|
|
||||||
# homeassistant.components.unifi
|
# homeassistant.components.unifi
|
||||||
aiounifi==52
|
aiounifi==53
|
||||||
|
|
||||||
# homeassistant.components.vlc_telnet
|
# homeassistant.components.vlc_telnet
|
||||||
aiovlc==0.1.0
|
aiovlc==0.1.0
|
||||||
|
|
|
@ -278,6 +278,27 @@ PDU_DEVICE_1 = {
|
||||||
"x_has_ssh_hostkey": True,
|
"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(
|
async def test_no_clients(
|
||||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||||
|
@ -719,31 +740,69 @@ async def test_wlan_client_sensors(
|
||||||
assert hass.states.get("sensor.ssid_1").state == "0"
|
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(
|
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:
|
) -> None:
|
||||||
"""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()) == 5
|
assert len(hass.states.async_all()) == 7
|
||||||
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1
|
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3
|
||||||
|
|
||||||
ent_reg = er.async_get(hass)
|
ent_reg = er.async_get(hass)
|
||||||
ent_reg_entry = ent_reg.async_get("sensor.dummy_usp_pdu_pro_outlet_2_outlet_power")
|
ent_reg_entry = ent_reg.async_get(f"sensor.{entity_id}")
|
||||||
assert ent_reg_entry.unique_id == "outlet_power-01:02:03:04:05:ff_2"
|
assert ent_reg_entry.unique_id == expected_unique_id
|
||||||
assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC
|
assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC
|
||||||
|
|
||||||
outlet_2 = hass.states.get("sensor.dummy_usp_pdu_pro_outlet_2_outlet_power")
|
sensor_data = hass.states.get(f"sensor.{entity_id}")
|
||||||
assert outlet_2.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER
|
assert sensor_data.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER
|
||||||
assert outlet_2.state == "73.827"
|
assert sensor_data.state == expected_value
|
||||||
|
|
||||||
# Verify state update
|
if changed_data is not None:
|
||||||
pdu_device_state_update = deepcopy(PDU_DEVICE_1)
|
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)
|
sensor_data = hass.states.get(f"sensor.{entity_id}")
|
||||||
await hass.async_block_till_done()
|
assert sensor_data.state == expected_update_value
|
||||||
|
|
||||||
outlet_2 = hass.states.get("sensor.dummy_usp_pdu_pro_outlet_2_outlet_power")
|
|
||||||
assert outlet_2.state == "123.45"
|
|
||||||
|
|
Loading…
Reference in New Issue