Add Unifi outlet switches for PDU devices (#98320)
Updates the Unifi outlet switching feature to support PDU devices
This commit is contained in:
parent
ef6e75657a
commit
5b6a7edd8d
3 changed files with 214 additions and 22 deletions
|
@ -106,6 +106,15 @@ async def async_dpi_group_control_fn(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_outlet_supports_switching_fn(
|
||||||
|
controller: UniFiController, obj_id: str
|
||||||
|
) -> bool:
|
||||||
|
"""Determine if an outlet supports switching."""
|
||||||
|
outlet = controller.api.outlets[obj_id]
|
||||||
|
return outlet.has_relay or outlet.caps in (1, 3)
|
||||||
|
|
||||||
|
|
||||||
async def async_outlet_control_fn(
|
async def async_outlet_control_fn(
|
||||||
api: aiounifi.Controller, obj_id: str, target: bool
|
api: aiounifi.Controller, obj_id: str, target: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -210,7 +219,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSwitchEntityDescription, ...] = (
|
||||||
name_fn=lambda outlet: outlet.name,
|
name_fn=lambda outlet: outlet.name,
|
||||||
object_fn=lambda api, obj_id: api.outlets[obj_id],
|
object_fn=lambda api, obj_id: api.outlets[obj_id],
|
||||||
should_poll=False,
|
should_poll=False,
|
||||||
supported_fn=lambda c, obj_id: c.api.outlets[obj_id].has_relay,
|
supported_fn=async_outlet_supports_switching_fn,
|
||||||
unique_id_fn=lambda controller, obj_id: f"{obj_id.split('_', 1)[0]}-outlet-{obj_id.split('_', 1)[1]}",
|
unique_id_fn=lambda controller, obj_id: f"{obj_id.split('_', 1)[0]}-outlet-{obj_id.split('_', 1)[1]}",
|
||||||
),
|
),
|
||||||
UnifiSwitchEntityDescription[Ports, Port](
|
UnifiSwitchEntityDescription[Ports, Port](
|
||||||
|
|
|
@ -785,7 +785,7 @@ 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()) == 7
|
assert len(hass.states.async_all()) == 9
|
||||||
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3
|
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3
|
||||||
|
|
||||||
ent_reg = er.async_get(hass)
|
ent_reg = er.async_get(hass)
|
||||||
|
|
|
@ -4,6 +4,7 @@ from datetime import timedelta
|
||||||
|
|
||||||
from aiounifi.models.message import MessageKey
|
from aiounifi.models.message import MessageKey
|
||||||
from aiounifi.websocket import WebsocketState
|
from aiounifi.websocket import WebsocketState
|
||||||
|
import pytest
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.switch import (
|
from homeassistant.components.switch import (
|
||||||
|
@ -384,7 +385,7 @@ OUTLET_UP1 = {
|
||||||
"x_vwirekey": "2dabb7e23b048c88b60123456789",
|
"x_vwirekey": "2dabb7e23b048c88b60123456789",
|
||||||
"vwire_table": [],
|
"vwire_table": [],
|
||||||
"dot1x_portctrl_enabled": False,
|
"dot1x_portctrl_enabled": False,
|
||||||
"outlet_overrides": [],
|
"outlet_overrides": [{"index": 1, "name": "Outlet 1", "relay_state": True}],
|
||||||
"outlet_enabled": True,
|
"outlet_enabled": True,
|
||||||
"license_state": "registered",
|
"license_state": "registered",
|
||||||
"x_aes_gcm": True,
|
"x_aes_gcm": True,
|
||||||
|
@ -580,6 +581,152 @@ OUTLET_UP1 = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PDU_DEVICE_1 = {
|
||||||
|
"_id": "123456654321abcdef012345",
|
||||||
|
"required_version": "5.28.0",
|
||||||
|
"port_table": [],
|
||||||
|
"license_state": "registered",
|
||||||
|
"lcm_brightness_override": False,
|
||||||
|
"type": "usw",
|
||||||
|
"board_rev": 4,
|
||||||
|
"hw_caps": 136,
|
||||||
|
"reboot_duration": 70,
|
||||||
|
"snmp_contact": "",
|
||||||
|
"config_network": {"type": "dhcp", "bonding_enabled": False},
|
||||||
|
"outlet_table": [
|
||||||
|
{
|
||||||
|
"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": "73.827",
|
||||||
|
"outlet_power_factor": "0.659",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"model": "USPPDUP",
|
||||||
|
"manufacturer_id": 4,
|
||||||
|
"ip": "192.168.1.76",
|
||||||
|
"fw2_caps": 0,
|
||||||
|
"jumboframe_enabled": False,
|
||||||
|
"version": "6.5.59.14777",
|
||||||
|
"unsupported_reason": 0,
|
||||||
|
"adoption_completed": True,
|
||||||
|
"outlet_enabled": True,
|
||||||
|
"stp_version": "rstp",
|
||||||
|
"name": "Dummy USP-PDU-Pro",
|
||||||
|
"fw_caps": 1732968229,
|
||||||
|
"lcm_brightness": 80,
|
||||||
|
"internet": True,
|
||||||
|
"mgmt_network_id": "123456654321abcdef012347",
|
||||||
|
"gateway_mac": "01:02:03:04:05:06",
|
||||||
|
"stp_priority": "32768",
|
||||||
|
"lcm_night_mode_begins": "22:00",
|
||||||
|
"two_phase_adopt": False,
|
||||||
|
"connected_at": 1690626493,
|
||||||
|
"inform_ip": "192.168.1.1",
|
||||||
|
"cfgversion": "ba8f30a5a17aad64",
|
||||||
|
"mac": "01:02:03:04:05:ff",
|
||||||
|
"provisioned_at": 1690989511,
|
||||||
|
"inform_url": "http://192.168.1.1:8080/inform",
|
||||||
|
"upgrade_duration": 100,
|
||||||
|
"ethernet_table": [{"num_port": 1, "name": "eth0", "mac": "01:02:03:04:05:a1"}],
|
||||||
|
"flowctrl_enabled": False,
|
||||||
|
"unsupported": False,
|
||||||
|
"ble_caps": 0,
|
||||||
|
"sys_error_caps": 0,
|
||||||
|
"dot1x_portctrl_enabled": False,
|
||||||
|
"last_uplink": {},
|
||||||
|
"disconnected_at": 1690626452,
|
||||||
|
"architecture": "mips",
|
||||||
|
"x_aes_gcm": True,
|
||||||
|
"has_fan": False,
|
||||||
|
"outlet_overrides": [
|
||||||
|
{
|
||||||
|
"cycle_enabled": False,
|
||||||
|
"name": "USB Outlet 1",
|
||||||
|
"relay_state": True,
|
||||||
|
"index": 1,
|
||||||
|
},
|
||||||
|
{"cycle_enabled": False, "name": "Outlet 2", "relay_state": True, "index": 2},
|
||||||
|
],
|
||||||
|
"model_incompatible": False,
|
||||||
|
"satisfaction": 100,
|
||||||
|
"model_in_eol": False,
|
||||||
|
"anomalies": -1,
|
||||||
|
"has_temperature": False,
|
||||||
|
"switch_caps": {},
|
||||||
|
"adopted_by_client": "web",
|
||||||
|
"snmp_location": "",
|
||||||
|
"model_in_lts": False,
|
||||||
|
"kernel_version": "4.14.115",
|
||||||
|
"serial": "abc123",
|
||||||
|
"power_source_ctrl_enabled": False,
|
||||||
|
"lcm_night_mode_ends": "08:00",
|
||||||
|
"adopted": True,
|
||||||
|
"hash_id": "abcdef123456",
|
||||||
|
"device_id": "mock-pdu",
|
||||||
|
"uplink": {},
|
||||||
|
"state": 1,
|
||||||
|
"start_disconnected_millis": 1690626383386,
|
||||||
|
"credential_caps": 0,
|
||||||
|
"default": False,
|
||||||
|
"discovered_via": "l2",
|
||||||
|
"adopt_ip": "10.0.10.4",
|
||||||
|
"adopt_url": "http://192.168.1.1:8080/inform",
|
||||||
|
"last_seen": 1691518814,
|
||||||
|
"min_inform_interval_seconds": 10,
|
||||||
|
"upgradable": False,
|
||||||
|
"adoptable_when_upgraded": False,
|
||||||
|
"rollupgrade": False,
|
||||||
|
"known_cfgversion": "abcfde03929",
|
||||||
|
"uptime": 1193042,
|
||||||
|
"_uptime": 1193042,
|
||||||
|
"locating": False,
|
||||||
|
"start_connected_millis": 1690626493324,
|
||||||
|
"prev_non_busy_state": 5,
|
||||||
|
"next_interval": 47,
|
||||||
|
"sys_stats": {},
|
||||||
|
"system-stats": {"cpu": "1.4", "mem": "28.9", "uptime": "1193042"},
|
||||||
|
"ssh_session_table": [],
|
||||||
|
"lldp_table": [],
|
||||||
|
"displayable_version": "6.5.59",
|
||||||
|
"connection_network_id": "123456654321abcdef012349",
|
||||||
|
"connection_network_name": "Default",
|
||||||
|
"startup_timestamp": 1690325774,
|
||||||
|
"is_access_point": False,
|
||||||
|
"safe_for_autoupgrade": True,
|
||||||
|
"overheating": False,
|
||||||
|
"power_source": "0",
|
||||||
|
"total_max_power": 0,
|
||||||
|
"outlet_ac_power_budget": "1875.000",
|
||||||
|
"outlet_ac_power_consumption": "201.683",
|
||||||
|
"downlink_table": [],
|
||||||
|
"uplink_depth": 1,
|
||||||
|
"downlink_lldp_macs": [],
|
||||||
|
"dhcp_server_table": [],
|
||||||
|
"connect_request_ip": "10.0.10.4",
|
||||||
|
"connect_request_port": "57951",
|
||||||
|
"ipv4_lease_expiration_timestamp_seconds": 1691576686,
|
||||||
|
"stat": {},
|
||||||
|
"tx_bytes": 1426780,
|
||||||
|
"rx_bytes": 1435064,
|
||||||
|
"bytes": 2861844,
|
||||||
|
"num_sta": 0,
|
||||||
|
"user-num_sta": 0,
|
||||||
|
"guest-num_sta": 0,
|
||||||
|
"x_has_ssh_hostkey": True,
|
||||||
|
}
|
||||||
|
|
||||||
WLAN = {
|
WLAN = {
|
||||||
"_id": "012345678910111213141516",
|
"_id": "012345678910111213141516",
|
||||||
"bc_filter_enabled": False,
|
"bc_filter_enabled": False,
|
||||||
|
@ -960,56 +1107,92 @@ async def test_dpi_switches_add_second_app(
|
||||||
assert hass.states.get("switch.block_media_streaming").state == STATE_ON
|
assert hass.states.get("switch.block_media_streaming").state == STATE_ON
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("entity_id", "test_data", "outlet_index", "expected_switches"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"plug_outlet_1",
|
||||||
|
OUTLET_UP1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"dummy_usp_pdu_pro_usb_outlet_1",
|
||||||
|
PDU_DEVICE_1,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"dummy_usp_pdu_pro_outlet_2",
|
||||||
|
PDU_DEVICE_1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
async def test_outlet_switches(
|
async def test_outlet_switches(
|
||||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket
|
hass: HomeAssistant,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
|
mock_unifi_websocket,
|
||||||
|
entity_id: str,
|
||||||
|
test_data: any,
|
||||||
|
outlet_index: int,
|
||||||
|
expected_switches: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test the outlet entities."""
|
"""Test the outlet entities."""
|
||||||
config_entry = await setup_unifi_integration(
|
config_entry = await setup_unifi_integration(
|
||||||
hass, aioclient_mock, devices_response=[OUTLET_UP1]
|
hass, aioclient_mock, devices_response=[test_data]
|
||||||
)
|
)
|
||||||
controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
|
controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
|
||||||
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
|
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == expected_switches
|
||||||
|
|
||||||
# Validate state object
|
# Validate state object
|
||||||
switch_1 = hass.states.get("switch.plug_outlet_1")
|
switch_1 = hass.states.get(f"switch.{entity_id}")
|
||||||
assert switch_1 is not None
|
assert switch_1 is not None
|
||||||
assert switch_1.state == STATE_ON
|
assert switch_1.state == STATE_ON
|
||||||
assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.OUTLET
|
assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.OUTLET
|
||||||
|
|
||||||
# Update state object
|
# Update state object
|
||||||
device_1 = deepcopy(OUTLET_UP1)
|
device_1 = deepcopy(test_data)
|
||||||
device_1["outlet_table"][0]["relay_state"] = False
|
device_1["outlet_table"][outlet_index - 1]["relay_state"] = False
|
||||||
mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1)
|
mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert hass.states.get("switch.plug_outlet_1").state == STATE_OFF
|
assert hass.states.get(f"switch.{entity_id}").state == STATE_OFF
|
||||||
|
|
||||||
# Turn off outlet
|
# Turn off outlet
|
||||||
|
device_id = test_data["device_id"]
|
||||||
aioclient_mock.clear_requests()
|
aioclient_mock.clear_requests()
|
||||||
aioclient_mock.put(
|
aioclient_mock.put(
|
||||||
f"https://{controller.host}:1234/api/s/{controller.site}/rest/device/600c8356942a6ade50707b56",
|
f"https://{controller.host}:1234/api/s/{controller.site}/rest/device/{device_id}",
|
||||||
)
|
)
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
SWITCH_DOMAIN,
|
SWITCH_DOMAIN,
|
||||||
SERVICE_TURN_OFF,
|
SERVICE_TURN_OFF,
|
||||||
{ATTR_ENTITY_ID: "switch.plug_outlet_1"},
|
{ATTR_ENTITY_ID: f"switch.{entity_id}"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
expected_off_overrides = deepcopy(device_1["outlet_overrides"])
|
||||||
|
expected_off_overrides[outlet_index - 1]["relay_state"] = False
|
||||||
|
|
||||||
assert aioclient_mock.call_count == 1
|
assert aioclient_mock.call_count == 1
|
||||||
assert aioclient_mock.mock_calls[0][2] == {
|
assert aioclient_mock.mock_calls[0][2] == {
|
||||||
"outlet_overrides": [{"index": 1, "name": "Outlet 1", "relay_state": False}]
|
"outlet_overrides": expected_off_overrides
|
||||||
}
|
}
|
||||||
|
|
||||||
# Turn on outlet
|
# Turn on outlet
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
SWITCH_DOMAIN,
|
SWITCH_DOMAIN,
|
||||||
SERVICE_TURN_ON,
|
SERVICE_TURN_ON,
|
||||||
{ATTR_ENTITY_ID: "switch.plug_outlet_1"},
|
{ATTR_ENTITY_ID: f"switch.{entity_id}"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
expected_on_overrides = deepcopy(device_1["outlet_overrides"])
|
||||||
|
expected_on_overrides[outlet_index - 1]["relay_state"] = True
|
||||||
assert aioclient_mock.call_count == 2
|
assert aioclient_mock.call_count == 2
|
||||||
assert aioclient_mock.mock_calls[1][2] == {
|
assert aioclient_mock.mock_calls[1][2] == {
|
||||||
"outlet_overrides": [{"index": 1, "name": "Outlet 1", "relay_state": True}]
|
"outlet_overrides": expected_on_overrides
|
||||||
}
|
}
|
||||||
|
|
||||||
# Availability signalling
|
# Availability signalling
|
||||||
|
@ -1017,33 +1200,33 @@ async def test_outlet_switches(
|
||||||
# Controller disconnects
|
# Controller disconnects
|
||||||
mock_unifi_websocket(state=WebsocketState.DISCONNECTED)
|
mock_unifi_websocket(state=WebsocketState.DISCONNECTED)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert hass.states.get("switch.plug_outlet_1").state == STATE_UNAVAILABLE
|
assert hass.states.get(f"switch.{entity_id}").state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
# Controller reconnects
|
# Controller reconnects
|
||||||
mock_unifi_websocket(state=WebsocketState.RUNNING)
|
mock_unifi_websocket(state=WebsocketState.RUNNING)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert hass.states.get("switch.plug_outlet_1").state == STATE_OFF
|
assert hass.states.get(f"switch.{entity_id}").state == STATE_OFF
|
||||||
|
|
||||||
# Device gets disabled
|
# Device gets disabled
|
||||||
device_1["disabled"] = True
|
device_1["disabled"] = True
|
||||||
mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1)
|
mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert hass.states.get("switch.plug_outlet_1").state == STATE_UNAVAILABLE
|
assert hass.states.get(f"switch.{entity_id}").state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
# Device gets re-enabled
|
# Device gets re-enabled
|
||||||
device_1["disabled"] = False
|
device_1["disabled"] = False
|
||||||
mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1)
|
mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert hass.states.get("switch.plug_outlet_1").state == STATE_OFF
|
assert hass.states.get(f"switch.{entity_id}").state == STATE_OFF
|
||||||
|
|
||||||
# Unload config entry
|
# Unload config entry
|
||||||
await hass.config_entries.async_unload(config_entry.entry_id)
|
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||||
assert hass.states.get("switch.plug_outlet_1").state == STATE_UNAVAILABLE
|
assert hass.states.get(f"switch.{entity_id}").state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
# Remove config entry
|
# Remove config entry
|
||||||
await hass.config_entries.async_remove(config_entry.entry_id)
|
await hass.config_entries.async_remove(config_entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert hass.states.get("switch.plug_outlet_1") is None
|
assert hass.states.get(f"switch.{entity_id}") is None
|
||||||
|
|
||||||
|
|
||||||
async def test_new_client_discovered_on_block_control(
|
async def test_new_client_discovered_on_block_control(
|
||||||
|
|
Loading…
Add table
Reference in a new issue