Add controls to enable and disable a UniFi WLAN (#97204)

This commit is contained in:
Robert Svensson 2023-07-25 14:01:57 +02:00 committed by GitHub
parent a0b61a1188
commit 8d6c4e3306
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 161 additions and 21 deletions

View File

@ -18,11 +18,14 @@ from aiounifi.models.event import Event, EventKey
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceEntryType,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
from .const import ATTR_MANUFACTURER from .const import ATTR_MANUFACTURER, DOMAIN
if TYPE_CHECKING: if TYPE_CHECKING:
from .controller import UniFiController from .controller import UniFiController
@ -58,6 +61,19 @@ def async_device_device_info_fn(api: aiounifi.Controller, obj_id: str) -> Device
) )
@callback
def async_wlan_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo:
"""Create device registry entry for WLAN."""
wlan = api.wlans[obj_id]
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, wlan.id)},
manufacturer=ATTR_MANUFACTURER,
model="UniFi WLAN",
name=wlan.name,
)
@dataclass @dataclass
class UnifiDescription(Generic[HandlerT, ApiItemT]): class UnifiDescription(Generic[HandlerT, ApiItemT]):
"""Validate and load entities from different UniFi handlers.""" """Validate and load entities from different UniFi handlers."""

View File

@ -8,24 +8,26 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Generic from typing import Generic
import aiounifi
from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.api_handlers import ItemEvent
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.wlan import Wlan from aiounifi.models.wlan import Wlan
from homeassistant.components.image import DOMAIN, ImageEntity, ImageEntityDescription from homeassistant.components.image import ImageEntity, ImageEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
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 ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN from .const import DOMAIN as UNIFI_DOMAIN
from .controller import UniFiController from .controller import UniFiController
from .entity import HandlerT, UnifiEntity, UnifiEntityDescription from .entity import (
HandlerT,
UnifiEntity,
UnifiEntityDescription,
async_wlan_device_info_fn,
)
@callback @callback
@ -34,19 +36,6 @@ def async_wlan_qr_code_image_fn(controller: UniFiController, wlan: Wlan) -> byte
return controller.api.wlans.generate_wlan_qr_code(wlan) return controller.api.wlans.generate_wlan_qr_code(wlan)
@callback
def async_wlan_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo:
"""Create device registry entry for WLAN."""
wlan = api.wlans[obj_id]
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, wlan.id)},
manufacturer=ATTR_MANUFACTURER,
model="UniFi Network",
name=wlan.name,
)
@dataclass @dataclass
class UnifiImageEntityDescriptionMixin(Generic[HandlerT, ApiItemT]): class UnifiImageEntityDescriptionMixin(Generic[HandlerT, ApiItemT]):
"""Validate and load entities from different UniFi handlers.""" """Validate and load entities from different UniFi handlers."""

View File

@ -3,6 +3,7 @@
Support for controlling power supply of clients which are powered over Ethernet (POE). Support for controlling power supply of clients which are powered over Ethernet (POE).
Support for controlling network access of clients selected in option flow. Support for controlling network access of clients selected in option flow.
Support for controlling deep packet inspection (DPI) restriction groups. Support for controlling deep packet inspection (DPI) restriction groups.
Support for controlling WLAN availability.
""" """
from __future__ import annotations from __future__ import annotations
@ -17,6 +18,7 @@ from aiounifi.interfaces.clients import Clients
from aiounifi.interfaces.dpi_restriction_groups import DPIRestrictionGroups from aiounifi.interfaces.dpi_restriction_groups import DPIRestrictionGroups
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.models.api import ApiItemT from aiounifi.models.api import ApiItemT
from aiounifi.models.client import Client, ClientBlockRequest from aiounifi.models.client import Client, ClientBlockRequest
from aiounifi.models.device import ( from aiounifi.models.device import (
@ -28,6 +30,7 @@ from aiounifi.models.dpi_restriction_group import DPIRestrictionGroup
from aiounifi.models.event import Event, EventKey from aiounifi.models.event import Event, EventKey
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, WlanEnableRequest
from homeassistant.components.switch import ( from homeassistant.components.switch import (
DOMAIN, DOMAIN,
@ -54,6 +57,7 @@ from .entity import (
UnifiEntityDescription, UnifiEntityDescription,
async_device_available_fn, async_device_available_fn,
async_device_device_info_fn, async_device_device_info_fn,
async_wlan_device_info_fn,
) )
CLIENT_BLOCKED = (EventKey.WIRED_CLIENT_BLOCKED, EventKey.WIRELESS_CLIENT_BLOCKED) CLIENT_BLOCKED = (EventKey.WIRED_CLIENT_BLOCKED, EventKey.WIRELESS_CLIENT_BLOCKED)
@ -137,6 +141,13 @@ async def async_poe_port_control_fn(
await api.request(DeviceSetPoePortModeRequest.create(device, int(index), state)) await api.request(DeviceSetPoePortModeRequest.create(device, int(index), state))
async def async_wlan_control_fn(
api: aiounifi.Controller, obj_id: str, target: bool
) -> None:
"""Control outlet relay."""
await api.request(WlanEnableRequest.create(obj_id, target))
@dataclass @dataclass
class UnifiSwitchEntityDescriptionMixin(Generic[HandlerT, ApiItemT]): class UnifiSwitchEntityDescriptionMixin(Generic[HandlerT, ApiItemT]):
"""Validate and load entities from different UniFi handlers.""" """Validate and load entities from different UniFi handlers."""
@ -233,6 +244,25 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSwitchEntityDescription, ...] = (
supported_fn=lambda controller, obj_id: controller.api.ports[obj_id].port_poe, supported_fn=lambda controller, obj_id: controller.api.ports[obj_id].port_poe,
unique_id_fn=lambda controller, obj_id: f"{obj_id.split('_', 1)[0]}-poe-{obj_id.split('_', 1)[1]}", unique_id_fn=lambda controller, obj_id: f"{obj_id.split('_', 1)[0]}-poe-{obj_id.split('_', 1)[1]}",
), ),
UnifiSwitchEntityDescription[Wlans, Wlan](
key="WLAN control",
device_class=SwitchDeviceClass.SWITCH,
entity_category=EntityCategory.CONFIG,
has_entity_name=True,
icon="mdi:wifi-check",
allowed_fn=lambda controller, obj_id: True,
api_handler_fn=lambda api: api.wlans,
available_fn=lambda controller, _: controller.available,
control_fn=async_wlan_control_fn,
device_info_fn=async_wlan_device_info_fn,
event_is_on=None,
event_to_subscribe=None,
is_on_fn=lambda controller, wlan: wlan.enabled,
name_fn=lambda wlan: None,
object_fn=lambda api, obj_id: api.wlans[obj_id],
supported_fn=lambda controller, obj_id: True,
unique_id_fn=lambda controller, obj_id: f"wlan-{obj_id}",
),
) )

View File

@ -580,6 +580,43 @@ OUTLET_UP1 = {
} }
WLAN = {
"_id": "012345678910111213141516",
"bc_filter_enabled": False,
"bc_filter_list": [],
"dtim_mode": "default",
"dtim_na": 1,
"dtim_ng": 1,
"enabled": True,
"group_rekey": 3600,
"mac_filter_enabled": False,
"mac_filter_list": [],
"mac_filter_policy": "allow",
"minrate_na_advertising_rates": False,
"minrate_na_beacon_rate_kbps": 6000,
"minrate_na_data_rate_kbps": 6000,
"minrate_na_enabled": False,
"minrate_na_mgmt_rate_kbps": 6000,
"minrate_ng_advertising_rates": False,
"minrate_ng_beacon_rate_kbps": 1000,
"minrate_ng_data_rate_kbps": 1000,
"minrate_ng_enabled": False,
"minrate_ng_mgmt_rate_kbps": 1000,
"name": "SSID 1",
"no2ghz_oui": False,
"schedule": [],
"security": "wpapsk",
"site_id": "5a32aa4ee4b0412345678910",
"usergroup_id": "012345678910111213141518",
"wep_idx": 1,
"wlangroup_id": "012345678910111213141519",
"wpa_enc": "ccmp",
"wpa_mode": "wpa2",
"x_iapp_key": "01234567891011121314151617181920",
"x_passphrase": "password",
}
async def test_no_clients( async def test_no_clients(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None: ) -> None:
@ -1230,3 +1267,71 @@ async def test_remove_poe_client_switches(
for entry in ent_reg.entities.values() for entry in ent_reg.entities.values()
if entry.config_entry_id == config_entry.entry_id if entry.config_entry_id == config_entry.entry_id
] ]
async def test_wlan_switches(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket
) -> None:
"""Test control of UniFi WLAN availability."""
config_entry = await setup_unifi_integration(
hass, aioclient_mock, wlans_response=[WLAN]
)
controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
ent_reg = er.async_get(hass)
ent_reg_entry = ent_reg.async_get("switch.ssid_1")
assert ent_reg_entry.unique_id == "wlan-012345678910111213141516"
assert ent_reg_entry.entity_category is EntityCategory.CONFIG
# Validate state object
switch_1 = hass.states.get("switch.ssid_1")
assert switch_1 is not None
assert switch_1.state == STATE_ON
assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.SWITCH
# Update state object
wlan = deepcopy(WLAN)
wlan["enabled"] = False
mock_unifi_websocket(message=MessageKey.WLAN_CONF_UPDATED, data=wlan)
await hass.async_block_till_done()
assert hass.states.get("switch.ssid_1").state == STATE_OFF
# Disable WLAN
aioclient_mock.clear_requests()
aioclient_mock.put(
f"https://{controller.host}:1234/api/s/{controller.site}"
+ f"/rest/wlanconf/{WLAN['_id']}",
)
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_off",
{"entity_id": "switch.ssid_1"},
blocking=True,
)
assert aioclient_mock.call_count == 1
assert aioclient_mock.mock_calls[0][2] == {"enabled": False}
# Enable WLAN
await hass.services.async_call(
SWITCH_DOMAIN,
"turn_on",
{"entity_id": "switch.ssid_1"},
blocking=True,
)
assert aioclient_mock.call_count == 2
assert aioclient_mock.mock_calls[1][2] == {"enabled": True}
# Availability signalling
# Controller disconnects
mock_unifi_websocket(state=WebsocketState.DISCONNECTED)
await hass.async_block_till_done()
assert hass.states.get("switch.ssid_1").state == STATE_UNAVAILABLE
# Controller reconnects
mock_unifi_websocket(state=WebsocketState.RUNNING)
await hass.async_block_till_done()
assert hass.states.get("switch.ssid_1").state == STATE_OFF