Expose UniFi PoE ports as individual switches (#80566)

* Add simple PoE control switches

* Add basic tests

* Complete testing

* Dont use port.up as part of available

* Bump aiounifi to v40
pull/80630/head
Robert Svensson 2022-10-19 19:54:40 +02:00 committed by GitHub
parent 2b2275dfb3
commit 04cdcad7f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 213 additions and 6 deletions

View File

@ -3,7 +3,7 @@
"name": "UniFi Network",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/unifi",
"requirements": ["aiounifi==39"],
"requirements": ["aiounifi==40"],
"codeowners": ["@Kane610"],
"quality_scale": "platinum",
"ssdp": [

View File

@ -8,7 +8,7 @@ Support for controlling deep packet inspection (DPI) restriction groups.
import asyncio
from typing import Any
from aiounifi.interfaces.api_handlers import SOURCE_EVENT
from aiounifi.interfaces.api_handlers import SOURCE_EVENT, ItemEvent
from aiounifi.models.client import ClientBlockRequest
from aiounifi.models.device import (
DeviceSetOutletRelayRequest,
@ -111,6 +111,18 @@ async def async_setup_entry(
items_added()
known_poe_clients.clear()
@callback
def async_add_poe_switch(_: ItemEvent, obj_id: str) -> None:
"""Add port PoE switch from UniFi controller."""
if not controller.api.ports[obj_id].port_poe:
return
async_add_entities([UnifiPoePortSwitch(obj_id, controller)])
controller.api.ports.subscribe(async_add_poe_switch, ItemEvent.ADDED)
for port_idx in controller.api.ports:
async_add_poe_switch(ItemEvent.ADDED, port_idx)
@callback
def add_block_entities(controller, async_add_entities, clients):
@ -550,3 +562,76 @@ class UniFiOutletSwitch(UniFiBase, SwitchEntity):
async def options_updated(self) -> None:
"""Config entry options are updated, no options to act on."""
class UnifiPoePortSwitch(SwitchEntity):
"""Representation of a Power-over-Ethernet source port on an UniFi device."""
_attr_device_class = SwitchDeviceClass.OUTLET
_attr_entity_category = EntityCategory.CONFIG
_attr_entity_registry_enabled_default = False
_attr_has_entity_name = True
_attr_icon = "mdi:ethernet"
_attr_should_poll = False
def __init__(self, obj_id: str, controller) -> None:
"""Set up UniFi Network entity base."""
self._attr_unique_id = f"{obj_id}-PoE"
self._device_mac, port_idx = obj_id.split("_", 1)
self._port_idx = int(port_idx)
self._obj_id = obj_id
self.controller = controller
port = self.controller.api.ports[self._obj_id]
self._attr_name = f"{port.name} PoE"
self._attr_is_on = port.poe_mode != "off"
device = self.controller.api.devices[self._device_mac]
self._attr_device_info = DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, device.mac)},
manufacturer=ATTR_MANUFACTURER,
model=device.model,
name=device.name or None,
sw_version=device.version,
)
async def async_added_to_hass(self) -> None:
"""Entity created."""
self.async_on_remove(
self.controller.api.ports.subscribe(self.async_signalling_callback)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
self.controller.signal_reachable,
self.async_signal_reachable_callback,
)
)
@callback
def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None:
"""Object has new event."""
device = self.controller.api.devices[self._device_mac]
port = self.controller.api.ports[self._obj_id]
self._attr_available = self.controller.available and not device.disabled
self._attr_is_on = port.poe_mode != "off"
self.async_write_ha_state()
@callback
def async_signal_reachable_callback(self) -> None:
"""Call when controller connection state change."""
self.async_signalling_callback(ItemEvent.ADDED, self._obj_id)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Enable POE for client."""
device = self.controller.api.devices[self._device_mac]
await self.controller.api.request(
DeviceSetPoePortModeRequest.create(device, self._port_idx, "auto")
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Disable POE for client."""
device = self.controller.api.devices[self._device_mac]
await self.controller.api.request(
DeviceSetPoePortModeRequest.create(device, self._port_idx, "off")
)

View File

@ -276,7 +276,7 @@ aiosyncthing==0.5.1
aiotractive==0.5.4
# homeassistant.components.unifi
aiounifi==39
aiounifi==40
# homeassistant.components.vlc_telnet
aiovlc==0.1.0

View File

@ -251,7 +251,7 @@ aiosyncthing==0.5.1
aiotractive==0.5.4
# homeassistant.components.unifi
aiounifi==39
aiounifi==40
# homeassistant.components.vlc_telnet
aiovlc==0.1.0

View File

@ -1,13 +1,18 @@
"""UniFi Network switch platform tests."""
from copy import deepcopy
from datetime import timedelta
from aiounifi.controller import MESSAGE_CLIENT_REMOVED, MESSAGE_DEVICE, MESSAGE_EVENT
from aiounifi.models.message import MessageKey
from aiounifi.websocket import WebsocketState
from homeassistant import config_entries, core
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
SwitchDeviceClass,
)
from homeassistant.components.unifi.const import (
CONF_BLOCK_CLIENT,
@ -18,10 +23,19 @@ from homeassistant.components.unifi.const import (
DOMAIN as UNIFI_DOMAIN,
)
from homeassistant.components.unifi.switch import POE_SWITCH
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
)
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_registry import RegistryEntryDisabler
from homeassistant.util import dt
from .test_controller import (
CONTROLLER_HOST,
@ -31,7 +45,7 @@ from .test_controller import (
setup_unifi_integration,
)
from tests.common import mock_restore_cache
from tests.common import async_fire_time_changed, mock_restore_cache
CLIENT_1 = {
"hostname": "client_1",
@ -1373,3 +1387,111 @@ async def test_restore_client_no_old_state(hass, aioclient_mock):
poe_client = hass.states.get("switch.poe_client")
assert poe_client.state == "unavailable" # self.poe_mode is None
async def test_poe_port_switches(hass, aioclient_mock, mock_unifi_websocket):
"""Test the update_items function with some clients."""
config_entry = await setup_unifi_integration(
hass, aioclient_mock, devices_response=[DEVICE_1]
)
controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
ent_reg = er.async_get(hass)
ent_reg_entry = ent_reg.async_get("switch.mock_name_port_1_poe")
assert ent_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION
assert ent_reg_entry.entity_category is EntityCategory.CONFIG
# Enable entity
ent_reg.async_update_entity(
entity_id="switch.mock_name_port_1_poe", disabled_by=None
)
await hass.async_block_till_done()
async_fire_time_changed(
hass,
dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
)
await hass.async_block_till_done()
# Validate state object
switch_1 = hass.states.get("switch.mock_name_port_1_poe")
assert switch_1 is not None
assert switch_1.state == STATE_ON
assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.OUTLET
# Update state object
device_1 = deepcopy(DEVICE_1)
device_1["port_table"][0]["poe_mode"] = "off"
mock_unifi_websocket(
data={
"meta": {"message": MessageKey.DEVICE.value},
"data": [device_1],
}
)
await hass.async_block_till_done()
assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_OFF
# Turn off PoE
aioclient_mock.clear_requests()
aioclient_mock.put(
f"https://{controller.host}:1234/api/s/{controller.site}/rest/device/mock-id",
)
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_off",
{"entity_id": "switch.mock_name_port_1_poe"},
blocking=True,
)
assert aioclient_mock.call_count == 1
assert aioclient_mock.mock_calls[0][2] == {
"port_overrides": [{"poe_mode": "off", "port_idx": 1, "portconf_id": "1a1"}]
}
# Turn on PoE
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_on",
{"entity_id": "switch.mock_name_port_1_poe"},
blocking=True,
)
assert aioclient_mock.call_count == 2
assert aioclient_mock.mock_calls[1][2] == {
"port_overrides": [{"poe_mode": "auto", "port_idx": 1, "portconf_id": "1a1"}]
}
# Availability signalling
# Controller disconnects
mock_unifi_websocket(state=WebsocketState.DISCONNECTED)
await hass.async_block_till_done()
assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_UNAVAILABLE
# Controller reconnects
mock_unifi_websocket(state=WebsocketState.RUNNING)
await hass.async_block_till_done()
assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_OFF
# Device gets disabled
device_1["disabled"] = True
mock_unifi_websocket(
data={
"meta": {"message": MessageKey.DEVICE.value},
"data": [device_1],
}
)
await hass.async_block_till_done()
assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_UNAVAILABLE
# Device gets re-enabled
device_1["disabled"] = False
mock_unifi_websocket(
data={
"meta": {"message": MessageKey.DEVICE.value},
"data": [device_1],
}
)
await hass.async_block_till_done()
assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_OFF