2021-11-26 21:44:49 +00:00
|
|
|
"""Switch platform for UniFi Network integration.
|
2021-02-01 16:55:16 +00:00
|
|
|
|
|
|
|
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 deep packet inspection (DPI) restriction groups.
|
|
|
|
"""
|
2022-10-25 17:26:56 +00:00
|
|
|
from __future__ import annotations
|
2022-01-12 16:11:05 +00:00
|
|
|
|
|
|
|
import asyncio
|
2022-10-25 17:26:56 +00:00
|
|
|
from collections.abc import Callable
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from typing import Any, Generic, TypeVar
|
2018-10-16 08:35:35 +00:00
|
|
|
|
2022-10-19 21:10:01 +00:00
|
|
|
from aiounifi.interfaces.api_handlers import ItemEvent
|
2022-10-25 17:26:56 +00:00
|
|
|
from aiounifi.interfaces.dpi_restriction_groups import DPIRestrictionGroups
|
|
|
|
from aiounifi.interfaces.outlets import Outlets
|
|
|
|
from aiounifi.interfaces.ports import Ports
|
2022-10-19 21:10:01 +00:00
|
|
|
from aiounifi.models.api import SOURCE_EVENT
|
2022-09-25 18:08:56 +00:00
|
|
|
from aiounifi.models.client import ClientBlockRequest
|
|
|
|
from aiounifi.models.device import (
|
|
|
|
DeviceSetOutletRelayRequest,
|
|
|
|
DeviceSetPoePortModeRequest,
|
2020-05-08 20:19:27 +00:00
|
|
|
)
|
2022-09-25 18:08:56 +00:00
|
|
|
from aiounifi.models.dpi_restriction_app import DPIRestrictionAppEnableRequest
|
|
|
|
from aiounifi.models.event import EventKey
|
2020-05-08 20:19:27 +00:00
|
|
|
|
2022-01-25 06:49:02 +00:00
|
|
|
from homeassistant.components.switch import DOMAIN, SwitchDeviceClass, SwitchEntity
|
2021-12-27 22:42:24 +00:00
|
|
|
from homeassistant.config_entries import ConfigEntry
|
|
|
|
from homeassistant.core import HomeAssistant, callback
|
2022-05-17 14:40:45 +00:00
|
|
|
from homeassistant.helpers import entity_registry as er
|
2022-01-25 06:49:02 +00:00
|
|
|
from homeassistant.helpers.device_registry import (
|
|
|
|
CONNECTION_NETWORK_MAC,
|
|
|
|
DeviceEntryType,
|
|
|
|
)
|
2019-06-15 15:38:22 +00:00
|
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
2021-12-16 11:53:01 +00:00
|
|
|
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
|
2021-12-27 22:42:24 +00:00
|
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
2019-07-29 17:48:38 +00:00
|
|
|
from homeassistant.helpers.restore_state import RestoreEntity
|
2018-10-16 08:35:35 +00:00
|
|
|
|
2022-10-25 17:26:56 +00:00
|
|
|
from .const import (
|
|
|
|
ATTR_MANUFACTURER,
|
|
|
|
BLOCK_SWITCH,
|
|
|
|
DOMAIN as UNIFI_DOMAIN,
|
|
|
|
DPI_SWITCH,
|
|
|
|
OUTLET_SWITCH,
|
|
|
|
POE_SWITCH,
|
|
|
|
)
|
|
|
|
from .controller import UniFiController
|
2020-01-31 19:23:25 +00:00
|
|
|
from .unifi_client import UniFiClient
|
|
|
|
|
2022-09-25 18:08:56 +00:00
|
|
|
CLIENT_BLOCKED = (EventKey.WIRED_CLIENT_BLOCKED, EventKey.WIRELESS_CLIENT_BLOCKED)
|
|
|
|
CLIENT_UNBLOCKED = (EventKey.WIRED_CLIENT_UNBLOCKED, EventKey.WIRELESS_CLIENT_UNBLOCKED)
|
2020-05-08 20:19:27 +00:00
|
|
|
|
2022-10-25 17:26:56 +00:00
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class UnifiEntityLoader(Generic[T]):
|
|
|
|
"""Validate and load entities from different UniFi handlers."""
|
|
|
|
|
|
|
|
config_option_fn: Callable[[UniFiController], bool]
|
|
|
|
entity_cls: type[UnifiDPIRestrictionSwitch] | type[UnifiOutletSwitch] | type[
|
|
|
|
UnifiPoePortSwitch
|
|
|
|
] | type[UnifiDPIRestrictionSwitch]
|
|
|
|
handler_fn: Callable[[UniFiController], T]
|
|
|
|
value_fn: Callable[[T, str], bool | None]
|
|
|
|
|
2018-10-16 08:35:35 +00:00
|
|
|
|
2021-12-27 22:42:24 +00:00
|
|
|
async def async_setup_entry(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
config_entry: ConfigEntry,
|
|
|
|
async_add_entities: AddEntitiesCallback,
|
|
|
|
) -> None:
|
2021-11-26 21:44:49 +00:00
|
|
|
"""Set up switches for UniFi Network integration.
|
2018-10-16 08:35:35 +00:00
|
|
|
|
2020-01-31 19:23:25 +00:00
|
|
|
Switches are controlling network access and switch ports with POE.
|
2018-10-16 08:35:35 +00:00
|
|
|
"""
|
2022-10-25 17:26:56 +00:00
|
|
|
controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
|
2020-11-03 07:36:37 +00:00
|
|
|
controller.entities[DOMAIN] = {
|
|
|
|
BLOCK_SWITCH: set(),
|
|
|
|
POE_SWITCH: set(),
|
|
|
|
DPI_SWITCH: set(),
|
2022-01-25 06:49:02 +00:00
|
|
|
OUTLET_SWITCH: set(),
|
2020-11-03 07:36:37 +00:00
|
|
|
}
|
2019-07-14 19:57:09 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if controller.site_role != "admin":
|
2019-07-14 19:57:09 +00:00
|
|
|
return
|
|
|
|
|
2020-04-23 19:29:38 +00:00
|
|
|
# Store previously known POE control entities in case their POE are turned off.
|
2021-03-05 21:09:05 +00:00
|
|
|
known_poe_clients = []
|
2022-05-17 14:40:45 +00:00
|
|
|
entity_registry = er.async_get(hass)
|
|
|
|
for entry in er.async_entries_for_config_entry(
|
|
|
|
entity_registry, config_entry.entry_id
|
|
|
|
):
|
2019-07-29 17:48:38 +00:00
|
|
|
|
2021-03-05 21:09:05 +00:00
|
|
|
if not entry.unique_id.startswith(POE_SWITCH):
|
|
|
|
continue
|
|
|
|
|
|
|
|
mac = entry.unique_id.replace(f"{POE_SWITCH}-", "")
|
|
|
|
if mac not in controller.api.clients:
|
2020-04-23 19:29:38 +00:00
|
|
|
continue
|
2019-07-29 17:48:38 +00:00
|
|
|
|
2021-03-05 21:09:05 +00:00
|
|
|
known_poe_clients.append(mac)
|
2019-07-29 17:48:38 +00:00
|
|
|
|
2020-04-23 19:29:38 +00:00
|
|
|
for mac in controller.option_block_clients:
|
|
|
|
if mac not in controller.api.clients and mac in controller.api.clients_all:
|
|
|
|
client = controller.api.clients_all[mac]
|
|
|
|
controller.api.clients.process_raw([client.raw])
|
2018-10-16 08:35:35 +00:00
|
|
|
|
2019-06-15 15:38:22 +00:00
|
|
|
@callback
|
2020-05-04 17:29:49 +00:00
|
|
|
def items_added(
|
2020-11-03 07:36:37 +00:00
|
|
|
clients: set = controller.api.clients,
|
2022-10-23 20:42:24 +00:00
|
|
|
devices: set = controller.api.devices,
|
2020-05-04 17:29:49 +00:00
|
|
|
) -> None:
|
2018-10-16 08:35:35 +00:00
|
|
|
"""Update the values of the controller."""
|
2020-05-04 17:29:49 +00:00
|
|
|
if controller.option_block_clients:
|
|
|
|
add_block_entities(controller, async_add_entities, clients)
|
|
|
|
|
|
|
|
if controller.option_poe_clients:
|
2021-03-05 21:09:05 +00:00
|
|
|
add_poe_entities(controller, async_add_entities, clients, known_poe_clients)
|
2020-04-16 22:08:53 +00:00
|
|
|
|
2020-04-19 19:30:06 +00:00
|
|
|
for signal in (controller.signal_update, controller.signal_options_update):
|
2021-04-20 18:50:42 +00:00
|
|
|
config_entry.async_on_unload(
|
|
|
|
async_dispatcher_connect(hass, signal, items_added)
|
|
|
|
)
|
2020-03-05 05:55:56 +00:00
|
|
|
|
2020-04-16 22:08:53 +00:00
|
|
|
items_added()
|
2021-03-05 21:09:05 +00:00
|
|
|
known_poe_clients.clear()
|
2018-10-16 08:35:35 +00:00
|
|
|
|
2022-10-23 18:28:45 +00:00
|
|
|
@callback
|
2022-10-25 17:26:56 +00:00
|
|
|
def async_load_entities(loader: UnifiEntityLoader) -> None:
|
|
|
|
"""Load and subscribe to UniFi devices."""
|
|
|
|
entities: list[SwitchEntity] = []
|
|
|
|
api_handler = loader.handler_fn(controller)
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_create_entity(event: ItemEvent, obj_id: str) -> None:
|
|
|
|
"""Create UniFi entity."""
|
|
|
|
if not loader.config_option_fn(controller) or not loader.value_fn(
|
|
|
|
api_handler, obj_id
|
|
|
|
):
|
|
|
|
return
|
2022-10-23 20:42:24 +00:00
|
|
|
|
2022-10-25 17:26:56 +00:00
|
|
|
entity = loader.entity_cls(obj_id, controller)
|
|
|
|
if event == ItemEvent.ADDED:
|
|
|
|
async_add_entities(entities)
|
|
|
|
return
|
|
|
|
entities.append(entity)
|
2022-10-23 20:42:24 +00:00
|
|
|
|
2022-10-25 17:26:56 +00:00
|
|
|
for obj_id in api_handler:
|
|
|
|
async_create_entity(ItemEvent.CHANGED, obj_id)
|
|
|
|
async_add_entities(entities)
|
2022-10-19 17:54:40 +00:00
|
|
|
|
2022-10-25 17:26:56 +00:00
|
|
|
api_handler.subscribe(async_create_entity, ItemEvent.ADDED)
|
2022-10-19 17:54:40 +00:00
|
|
|
|
2022-10-25 17:26:56 +00:00
|
|
|
for unifi_loader in UNIFI_LOADERS:
|
|
|
|
async_load_entities(unifi_loader)
|
2022-10-19 17:54:40 +00:00
|
|
|
|
2018-10-16 08:35:35 +00:00
|
|
|
|
2019-06-15 15:38:22 +00:00
|
|
|
@callback
|
2020-05-04 17:29:49 +00:00
|
|
|
def add_block_entities(controller, async_add_entities, clients):
|
2020-01-31 19:23:25 +00:00
|
|
|
"""Add new switch entities from the controller."""
|
2020-04-19 19:30:06 +00:00
|
|
|
switches = []
|
2018-10-16 08:35:35 +00:00
|
|
|
|
2020-04-19 19:30:06 +00:00
|
|
|
for mac in controller.option_block_clients:
|
2020-05-04 17:29:49 +00:00
|
|
|
if mac in controller.entities[DOMAIN][BLOCK_SWITCH] or mac not in clients:
|
2019-07-25 14:56:56 +00:00
|
|
|
continue
|
|
|
|
|
2020-04-23 19:29:38 +00:00
|
|
|
client = controller.api.clients[mac]
|
2020-04-19 19:30:06 +00:00
|
|
|
switches.append(UniFiBlockClientSwitch(client, controller))
|
2019-07-25 14:56:56 +00:00
|
|
|
|
2022-10-17 19:11:58 +00:00
|
|
|
async_add_entities(switches)
|
2018-10-16 08:35:35 +00:00
|
|
|
|
|
|
|
|
2020-05-04 17:29:49 +00:00
|
|
|
@callback
|
2021-03-05 21:09:05 +00:00
|
|
|
def add_poe_entities(controller, async_add_entities, clients, known_poe_clients):
|
2020-05-04 17:29:49 +00:00
|
|
|
"""Add new switch entities from the controller."""
|
|
|
|
switches = []
|
2020-04-19 19:30:06 +00:00
|
|
|
|
2020-05-04 17:29:49 +00:00
|
|
|
devices = controller.api.devices
|
2018-10-16 08:35:35 +00:00
|
|
|
|
2020-05-04 17:29:49 +00:00
|
|
|
for mac in clients:
|
|
|
|
if mac in controller.entities[DOMAIN][POE_SWITCH]:
|
|
|
|
continue
|
2019-07-29 17:48:38 +00:00
|
|
|
|
2020-05-04 17:29:49 +00:00
|
|
|
client = controller.api.clients[mac]
|
|
|
|
|
2021-03-05 21:09:05 +00:00
|
|
|
# Try to identify new clients powered by POE.
|
|
|
|
# Known POE clients have been created in previous HASS sessions.
|
|
|
|
# If port_poe is None the port does not support POE
|
|
|
|
# If poe_enable is False we can't know if a POE client is available for control.
|
|
|
|
if mac not in known_poe_clients and (
|
2020-05-04 17:29:49 +00:00
|
|
|
mac in controller.wireless_clients
|
2022-01-24 07:50:08 +00:00
|
|
|
or client.switch_mac not in devices
|
|
|
|
or not devices[client.switch_mac].ports[client.switch_port].port_poe
|
|
|
|
or not devices[client.switch_mac].ports[client.switch_port].poe_enable
|
2020-05-04 17:29:49 +00:00
|
|
|
or controller.mac == client.mac
|
|
|
|
):
|
|
|
|
continue
|
2018-10-16 08:35:35 +00:00
|
|
|
|
2020-05-04 17:29:49 +00:00
|
|
|
# Multiple POE-devices on same port means non UniFi POE driven switch
|
|
|
|
multi_clients_on_port = False
|
|
|
|
for client2 in controller.api.clients.values():
|
2020-04-02 15:53:33 +00:00
|
|
|
|
2021-03-05 21:09:05 +00:00
|
|
|
if mac in known_poe_clients:
|
2020-05-04 17:29:49 +00:00
|
|
|
break
|
2020-04-02 15:53:33 +00:00
|
|
|
|
2020-05-04 17:29:49 +00:00
|
|
|
if (
|
|
|
|
client2.is_wired
|
|
|
|
and client.mac != client2.mac
|
2022-01-24 07:50:08 +00:00
|
|
|
and client.switch_mac == client2.switch_mac
|
|
|
|
and client.switch_port == client2.switch_port
|
2020-05-04 17:29:49 +00:00
|
|
|
):
|
|
|
|
multi_clients_on_port = True
|
|
|
|
break
|
2020-04-02 15:53:33 +00:00
|
|
|
|
2020-05-04 17:29:49 +00:00
|
|
|
if multi_clients_on_port:
|
|
|
|
continue
|
2018-10-16 08:35:35 +00:00
|
|
|
|
2020-05-04 17:29:49 +00:00
|
|
|
switches.append(UniFiPOEClientSwitch(client, controller))
|
2020-04-16 22:08:53 +00:00
|
|
|
|
2022-10-17 19:11:58 +00:00
|
|
|
async_add_entities(switches)
|
2020-04-16 22:08:53 +00:00
|
|
|
|
|
|
|
|
2020-04-26 16:50:37 +00:00
|
|
|
class UniFiPOEClientSwitch(UniFiClient, SwitchEntity, RestoreEntity):
|
2019-07-25 14:56:56 +00:00
|
|
|
"""Representation of a client that uses POE."""
|
|
|
|
|
2020-04-21 04:17:14 +00:00
|
|
|
DOMAIN = DOMAIN
|
2020-04-19 19:30:06 +00:00
|
|
|
TYPE = POE_SWITCH
|
|
|
|
|
2021-12-16 11:53:01 +00:00
|
|
|
_attr_entity_category = EntityCategory.CONFIG
|
2021-10-26 18:23:20 +00:00
|
|
|
|
2019-07-25 14:56:56 +00:00
|
|
|
def __init__(self, client, controller):
|
|
|
|
"""Set up POE switch."""
|
|
|
|
super().__init__(client, controller)
|
2020-01-31 19:23:25 +00:00
|
|
|
|
2019-07-25 14:56:56 +00:00
|
|
|
self.poe_mode = None
|
2022-01-24 07:50:08 +00:00
|
|
|
if client.switch_port and self.port.poe_mode != "off":
|
2019-07-25 14:56:56 +00:00
|
|
|
self.poe_mode = self.port.poe_mode
|
2018-10-16 08:35:35 +00:00
|
|
|
|
2022-09-06 11:37:00 +00:00
|
|
|
async def async_added_to_hass(self) -> None:
|
2019-07-29 17:48:38 +00:00
|
|
|
"""Call when entity about to be added to Home Assistant."""
|
2020-01-31 19:23:25 +00:00
|
|
|
await super().async_added_to_hass()
|
|
|
|
|
2021-03-05 21:09:05 +00:00
|
|
|
if self.poe_mode: # POE is enabled and client in a known state
|
|
|
|
return
|
|
|
|
|
|
|
|
if (state := await self.async_get_last_state()) is None:
|
2019-07-29 17:48:38 +00:00
|
|
|
return
|
|
|
|
|
2021-03-05 21:09:05 +00:00
|
|
|
self.poe_mode = state.attributes.get("poe_mode")
|
2019-07-29 17:48:38 +00:00
|
|
|
|
2022-01-24 07:50:08 +00:00
|
|
|
if not self.client.switch_mac:
|
2021-03-05 21:09:05 +00:00
|
|
|
self.client.raw["sw_mac"] = state.attributes.get("switch")
|
2019-07-29 17:48:38 +00:00
|
|
|
|
2022-01-24 07:50:08 +00:00
|
|
|
if not self.client.switch_port:
|
2021-03-05 21:09:05 +00:00
|
|
|
self.client.raw["sw_port"] = state.attributes.get("port")
|
2019-07-29 17:48:38 +00:00
|
|
|
|
2018-10-16 08:35:35 +00:00
|
|
|
@property
|
|
|
|
def is_on(self):
|
|
|
|
"""Return true if POE is active."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.port.poe_mode != "off"
|
2018-10-16 08:35:35 +00:00
|
|
|
|
|
|
|
@property
|
2022-09-06 11:37:00 +00:00
|
|
|
def available(self) -> bool:
|
2019-07-29 17:48:38 +00:00
|
|
|
"""Return if switch is available.
|
|
|
|
|
2021-03-05 21:09:05 +00:00
|
|
|
Poe_mode None means its POE state is unknown.
|
2019-07-29 17:48:38 +00:00
|
|
|
Sw_mac unavailable means restored client.
|
|
|
|
"""
|
2019-07-31 19:25:30 +00:00
|
|
|
return (
|
2021-03-05 21:09:05 +00:00
|
|
|
self.poe_mode is not None
|
|
|
|
and self.controller.available
|
2022-01-24 07:50:08 +00:00
|
|
|
and self.client.switch_port
|
|
|
|
and self.client.switch_mac
|
|
|
|
and self.client.switch_mac in self.controller.api.devices
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-10-16 08:35:35 +00:00
|
|
|
|
2022-09-06 11:37:00 +00:00
|
|
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
2018-10-16 08:35:35 +00:00
|
|
|
"""Enable POE for client."""
|
2022-09-25 18:08:56 +00:00
|
|
|
await self.controller.api.request(
|
|
|
|
DeviceSetPoePortModeRequest.create(
|
|
|
|
self.device, self.client.switch_port, self.poe_mode
|
|
|
|
)
|
|
|
|
)
|
2018-10-16 08:35:35 +00:00
|
|
|
|
2022-09-06 11:37:00 +00:00
|
|
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
2018-10-16 08:35:35 +00:00
|
|
|
"""Disable POE for client."""
|
2022-09-25 18:08:56 +00:00
|
|
|
await self.controller.api.request(
|
|
|
|
DeviceSetPoePortModeRequest.create(
|
|
|
|
self.device, self.client.switch_port, "off"
|
|
|
|
)
|
|
|
|
)
|
2018-10-16 08:35:35 +00:00
|
|
|
|
|
|
|
@property
|
2021-03-11 19:16:26 +00:00
|
|
|
def extra_state_attributes(self):
|
2018-10-16 08:35:35 +00:00
|
|
|
"""Return the device state attributes."""
|
|
|
|
attributes = {
|
2019-07-31 19:25:30 +00:00
|
|
|
"power": self.port.poe_power,
|
2022-01-24 07:50:08 +00:00
|
|
|
"switch": self.client.switch_mac,
|
|
|
|
"port": self.client.switch_port,
|
2019-07-31 19:25:30 +00:00
|
|
|
"poe_mode": self.poe_mode,
|
2018-10-16 08:35:35 +00:00
|
|
|
}
|
|
|
|
return attributes
|
|
|
|
|
|
|
|
@property
|
|
|
|
def device(self):
|
|
|
|
"""Shortcut to the switch that client is connected to."""
|
2022-01-24 07:50:08 +00:00
|
|
|
return self.controller.api.devices[self.client.switch_mac]
|
2018-10-16 08:35:35 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def port(self):
|
|
|
|
"""Shortcut to the switch port that client is connected to."""
|
2022-01-24 07:50:08 +00:00
|
|
|
return self.device.ports[self.client.switch_port]
|
2019-07-25 14:56:56 +00:00
|
|
|
|
2020-04-19 19:30:06 +00:00
|
|
|
async def options_updated(self) -> None:
|
|
|
|
"""Config entry options are updated, remove entity if option is disabled."""
|
|
|
|
if not self.controller.option_poe_clients:
|
2020-05-19 21:57:41 +00:00
|
|
|
await self.remove_item({self.client.mac})
|
2020-04-19 19:30:06 +00:00
|
|
|
|
2019-07-25 14:56:56 +00:00
|
|
|
|
2020-04-26 16:50:37 +00:00
|
|
|
class UniFiBlockClientSwitch(UniFiClient, SwitchEntity):
|
2019-07-25 14:56:56 +00:00
|
|
|
"""Representation of a blockable client."""
|
|
|
|
|
2020-04-21 04:17:14 +00:00
|
|
|
DOMAIN = DOMAIN
|
2020-04-19 19:30:06 +00:00
|
|
|
TYPE = BLOCK_SWITCH
|
2019-07-25 14:56:56 +00:00
|
|
|
|
2021-12-16 11:53:01 +00:00
|
|
|
_attr_entity_category = EntityCategory.CONFIG
|
2021-10-26 18:23:20 +00:00
|
|
|
|
2020-05-08 20:19:27 +00:00
|
|
|
def __init__(self, client, controller):
|
|
|
|
"""Set up block switch."""
|
|
|
|
super().__init__(client, controller)
|
|
|
|
|
2020-05-08 22:34:18 +00:00
|
|
|
self._is_blocked = client.blocked
|
2020-05-08 20:19:27 +00:00
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_update_callback(self) -> None:
|
|
|
|
"""Update the clients state."""
|
2021-03-27 09:54:59 +00:00
|
|
|
if (
|
|
|
|
self.client.last_updated == SOURCE_EVENT
|
2022-09-25 18:08:56 +00:00
|
|
|
and self.client.event.key in CLIENT_BLOCKED + CLIENT_UNBLOCKED
|
2021-03-27 09:54:59 +00:00
|
|
|
):
|
2022-09-25 18:08:56 +00:00
|
|
|
self._is_blocked = self.client.event.key in CLIENT_BLOCKED
|
2020-05-08 20:19:27 +00:00
|
|
|
|
|
|
|
super().async_update_callback()
|
|
|
|
|
2019-07-25 14:56:56 +00:00
|
|
|
@property
|
|
|
|
def is_on(self):
|
2019-08-04 14:57:36 +00:00
|
|
|
"""Return true if client is allowed to connect."""
|
2020-05-08 20:19:27 +00:00
|
|
|
return not self._is_blocked
|
2019-07-25 14:56:56 +00:00
|
|
|
|
2022-09-06 11:37:00 +00:00
|
|
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
2019-08-04 14:57:36 +00:00
|
|
|
"""Turn on connectivity for client."""
|
2022-09-25 18:08:56 +00:00
|
|
|
await self.controller.api.request(
|
|
|
|
ClientBlockRequest.create(self.client.mac, False)
|
|
|
|
)
|
2019-07-25 14:56:56 +00:00
|
|
|
|
2022-09-06 11:37:00 +00:00
|
|
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
2019-08-04 14:57:36 +00:00
|
|
|
"""Turn off connectivity for client."""
|
2022-09-25 18:08:56 +00:00
|
|
|
await self.controller.api.request(
|
|
|
|
ClientBlockRequest.create(self.client.mac, True)
|
|
|
|
)
|
2020-03-10 17:27:25 +00:00
|
|
|
|
|
|
|
@property
|
2022-09-06 11:37:00 +00:00
|
|
|
def icon(self) -> str:
|
2020-03-10 17:27:25 +00:00
|
|
|
"""Return the icon to use in the frontend."""
|
2020-05-08 20:19:27 +00:00
|
|
|
if self._is_blocked:
|
2020-03-10 17:27:25 +00:00
|
|
|
return "mdi:network-off"
|
|
|
|
return "mdi:network"
|
2020-04-19 19:30:06 +00:00
|
|
|
|
|
|
|
async def options_updated(self) -> None:
|
|
|
|
"""Config entry options are updated, remove entity if option is disabled."""
|
|
|
|
if self.client.mac not in self.controller.option_block_clients:
|
2020-05-19 21:57:41 +00:00
|
|
|
await self.remove_item({self.client.mac})
|
2020-11-03 07:36:37 +00:00
|
|
|
|
|
|
|
|
2022-10-23 20:42:24 +00:00
|
|
|
class UnifiDPIRestrictionSwitch(SwitchEntity):
|
2020-11-03 07:36:37 +00:00
|
|
|
"""Representation of a DPI restriction group."""
|
|
|
|
|
2021-12-16 11:53:01 +00:00
|
|
|
_attr_entity_category = EntityCategory.CONFIG
|
2021-10-26 18:23:20 +00:00
|
|
|
|
2022-10-23 20:42:24 +00:00
|
|
|
def __init__(self, obj_id: str, controller):
|
2022-01-12 16:11:05 +00:00
|
|
|
"""Set up dpi switch."""
|
2022-10-23 20:42:24 +00:00
|
|
|
controller.entities[DOMAIN][DPI_SWITCH].add(obj_id)
|
|
|
|
self._obj_id = obj_id
|
|
|
|
self.controller = controller
|
2022-01-12 16:11:05 +00:00
|
|
|
|
2022-10-23 20:42:24 +00:00
|
|
|
dpi_group = controller.api.dpi_groups[obj_id]
|
2022-01-12 16:11:05 +00:00
|
|
|
self._known_app_ids = dpi_group.dpiapp_ids
|
|
|
|
|
2022-10-23 20:42:24 +00:00
|
|
|
self._attr_available = controller.available
|
|
|
|
self._attr_is_on = self.calculate_enabled()
|
|
|
|
self._attr_name = dpi_group.name
|
|
|
|
self._attr_unique_id = dpi_group.id
|
|
|
|
self._attr_device_info = DeviceInfo(
|
|
|
|
entry_type=DeviceEntryType.SERVICE,
|
|
|
|
identifiers={(DOMAIN, f"unifi_controller_{obj_id}")},
|
|
|
|
manufacturer=ATTR_MANUFACTURER,
|
|
|
|
model="UniFi Network",
|
|
|
|
name="UniFi Network",
|
|
|
|
)
|
2020-11-03 07:36:37 +00:00
|
|
|
|
2022-01-12 16:11:05 +00:00
|
|
|
async def async_added_to_hass(self) -> None:
|
|
|
|
"""Register callback to known apps."""
|
2022-10-23 20:42:24 +00:00
|
|
|
self.async_on_remove(
|
|
|
|
self.controller.api.dpi_groups.subscribe(self.async_signalling_callback)
|
|
|
|
)
|
|
|
|
self.async_on_remove(
|
|
|
|
self.controller.api.dpi_apps.subscribe(
|
|
|
|
self.async_signalling_callback, ItemEvent.CHANGED
|
|
|
|
),
|
|
|
|
)
|
|
|
|
self.async_on_remove(
|
|
|
|
async_dispatcher_connect(
|
|
|
|
self.hass, self.controller.signal_remove, self.remove_item
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self.async_on_remove(
|
|
|
|
async_dispatcher_connect(
|
|
|
|
self.hass, self.controller.signal_options_update, self.options_updated
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self.async_on_remove(
|
|
|
|
async_dispatcher_connect(
|
|
|
|
self.hass,
|
|
|
|
self.controller.signal_reachable,
|
|
|
|
self.async_signal_reachable_callback,
|
|
|
|
)
|
|
|
|
)
|
2022-01-12 16:11:05 +00:00
|
|
|
|
|
|
|
async def async_will_remove_from_hass(self) -> None:
|
2022-10-23 20:42:24 +00:00
|
|
|
"""Disconnect object when removed."""
|
|
|
|
self.controller.entities[DOMAIN][DPI_SWITCH].remove(self._obj_id)
|
2022-01-12 16:11:05 +00:00
|
|
|
|
|
|
|
@callback
|
2022-10-23 20:42:24 +00:00
|
|
|
def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None:
|
|
|
|
"""Object has new event."""
|
|
|
|
if event == ItemEvent.DELETED:
|
|
|
|
self.hass.async_create_task(self.remove_item({self._obj_id}))
|
2022-01-12 16:11:05 +00:00
|
|
|
return
|
|
|
|
|
2022-10-23 20:42:24 +00:00
|
|
|
dpi_group = self.controller.api.dpi_groups[self._obj_id]
|
|
|
|
if not dpi_group.dpiapp_ids:
|
|
|
|
self.hass.async_create_task(self.remove_item({self._obj_id}))
|
|
|
|
return
|
2022-01-12 16:11:05 +00:00
|
|
|
|
2022-10-23 20:42:24 +00:00
|
|
|
self._attr_available = self.controller.available
|
|
|
|
self._attr_is_on = self.calculate_enabled()
|
|
|
|
self.async_write_ha_state()
|
2020-11-03 07:36:37 +00:00
|
|
|
|
2022-10-23 20:42:24 +00:00
|
|
|
@callback
|
|
|
|
def async_signal_reachable_callback(self) -> None:
|
|
|
|
"""Call when controller connection state change."""
|
|
|
|
self.async_signalling_callback(ItemEvent.ADDED, self._obj_id)
|
2020-11-03 07:36:37 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def icon(self):
|
|
|
|
"""Return the icon to use in the frontend."""
|
2022-10-23 20:42:24 +00:00
|
|
|
if self._attr_is_on:
|
2020-11-03 07:36:37 +00:00
|
|
|
return "mdi:network"
|
|
|
|
return "mdi:network-off"
|
|
|
|
|
2022-01-12 16:11:05 +00:00
|
|
|
def calculate_enabled(self) -> bool:
|
|
|
|
"""Calculate if all apps are enabled."""
|
2022-10-23 20:42:24 +00:00
|
|
|
dpi_group = self.controller.api.dpi_groups[self._obj_id]
|
2022-01-12 16:11:05 +00:00
|
|
|
return all(
|
|
|
|
self.controller.api.dpi_apps[app_id].enabled
|
2022-10-23 20:42:24 +00:00
|
|
|
for app_id in dpi_group.dpiapp_ids
|
2022-01-12 16:11:05 +00:00
|
|
|
if app_id in self.controller.api.dpi_apps
|
|
|
|
)
|
|
|
|
|
2022-09-06 11:37:00 +00:00
|
|
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
2022-01-12 16:11:05 +00:00
|
|
|
"""Restrict access of apps related to DPI group."""
|
2022-10-23 20:42:24 +00:00
|
|
|
dpi_group = self.controller.api.dpi_groups[self._obj_id]
|
2022-01-12 16:11:05 +00:00
|
|
|
return await asyncio.gather(
|
|
|
|
*[
|
2022-09-25 18:08:56 +00:00
|
|
|
self.controller.api.request(
|
|
|
|
DPIRestrictionAppEnableRequest.create(app_id, True)
|
|
|
|
)
|
2022-10-23 20:42:24 +00:00
|
|
|
for app_id in dpi_group.dpiapp_ids
|
2022-01-12 16:11:05 +00:00
|
|
|
]
|
|
|
|
)
|
2020-11-03 07:36:37 +00:00
|
|
|
|
2022-09-06 11:37:00 +00:00
|
|
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
2022-01-12 16:11:05 +00:00
|
|
|
"""Remove restriction of apps related to DPI group."""
|
2022-10-23 20:42:24 +00:00
|
|
|
dpi_group = self.controller.api.dpi_groups[self._obj_id]
|
2022-01-12 16:11:05 +00:00
|
|
|
return await asyncio.gather(
|
|
|
|
*[
|
2022-09-25 18:08:56 +00:00
|
|
|
self.controller.api.request(
|
|
|
|
DPIRestrictionAppEnableRequest.create(app_id, False)
|
|
|
|
)
|
2022-10-23 20:42:24 +00:00
|
|
|
for app_id in dpi_group.dpiapp_ids
|
2022-01-12 16:11:05 +00:00
|
|
|
]
|
|
|
|
)
|
2020-11-03 07:36:37 +00:00
|
|
|
|
|
|
|
async def options_updated(self) -> None:
|
|
|
|
"""Config entry options are updated, remove entity if option is disabled."""
|
|
|
|
if not self.controller.option_dpi_restrictions:
|
2022-10-23 20:42:24 +00:00
|
|
|
await self.remove_item({self._attr_unique_id})
|
2020-11-03 07:36:37 +00:00
|
|
|
|
2022-10-23 20:42:24 +00:00
|
|
|
async def remove_item(self, keys: set) -> None:
|
|
|
|
"""Remove entity if key is part of set."""
|
|
|
|
if self._attr_unique_id not in keys:
|
|
|
|
return
|
|
|
|
|
|
|
|
if self.registry_entry:
|
|
|
|
er.async_get(self.hass).async_remove(self.entity_id)
|
|
|
|
else:
|
|
|
|
await self.async_remove(force_remove=True)
|
2022-01-25 06:49:02 +00:00
|
|
|
|
|
|
|
|
2022-10-23 18:28:45 +00:00
|
|
|
class UnifiOutletSwitch(SwitchEntity):
|
2022-01-25 06:49:02 +00:00
|
|
|
"""Representation of a outlet relay."""
|
|
|
|
|
|
|
|
_attr_device_class = SwitchDeviceClass.OUTLET
|
2022-10-23 18:28:45 +00:00
|
|
|
_attr_has_entity_name = True
|
|
|
|
_attr_should_poll = False
|
|
|
|
|
|
|
|
def __init__(self, obj_id: str, controller) -> None:
|
|
|
|
"""Set up UniFi Network entity base."""
|
|
|
|
self._device_mac, index = obj_id.split("_", 1)
|
|
|
|
self._index = int(index)
|
|
|
|
self._obj_id = obj_id
|
|
|
|
self.controller = controller
|
2022-01-25 06:49:02 +00:00
|
|
|
|
2022-10-23 18:28:45 +00:00
|
|
|
outlet = self.controller.api.outlets[self._obj_id]
|
|
|
|
self._attr_name = outlet.name
|
|
|
|
self._attr_is_on = outlet.relay_state
|
|
|
|
self._attr_unique_id = f"{self._device_mac}-outlet-{index}"
|
2022-01-25 06:49:02 +00:00
|
|
|
|
2022-10-23 18:28:45 +00:00
|
|
|
device = self.controller.api.devices[self._device_mac]
|
|
|
|
self._attr_available = controller.available and not device.disabled
|
|
|
|
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,
|
|
|
|
hw_version=device.board_revision,
|
|
|
|
)
|
2022-01-25 06:49:02 +00:00
|
|
|
|
2022-10-23 18:28:45 +00:00
|
|
|
async def async_added_to_hass(self) -> None:
|
|
|
|
"""Entity created."""
|
|
|
|
self.async_on_remove(
|
|
|
|
self.controller.api.outlets.subscribe(self.async_signalling_callback)
|
|
|
|
)
|
|
|
|
self.async_on_remove(
|
|
|
|
async_dispatcher_connect(
|
|
|
|
self.hass,
|
|
|
|
self.controller.signal_reachable,
|
|
|
|
self.async_signal_reachable_callback,
|
|
|
|
)
|
|
|
|
)
|
2022-01-25 06:49:02 +00:00
|
|
|
|
2022-10-23 18:28:45 +00:00
|
|
|
@callback
|
|
|
|
def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None:
|
|
|
|
"""Object has new event."""
|
|
|
|
device = self.controller.api.devices[self._device_mac]
|
|
|
|
outlet = self.controller.api.outlets[self._obj_id]
|
|
|
|
self._attr_available = self.controller.available and not device.disabled
|
|
|
|
self._attr_is_on = outlet.relay_state
|
|
|
|
self.async_write_ha_state()
|
2022-01-25 06:49:02 +00:00
|
|
|
|
2022-10-23 18:28:45 +00:00
|
|
|
@callback
|
|
|
|
def async_signal_reachable_callback(self) -> None:
|
|
|
|
"""Call when controller connection state change."""
|
|
|
|
self.async_signalling_callback(ItemEvent.ADDED, self._obj_id)
|
2022-01-25 06:49:02 +00:00
|
|
|
|
2022-09-06 11:37:00 +00:00
|
|
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
2022-01-25 06:49:02 +00:00
|
|
|
"""Enable outlet relay."""
|
2022-10-23 18:28:45 +00:00
|
|
|
device = self.controller.api.devices[self._device_mac]
|
2022-09-25 18:08:56 +00:00
|
|
|
await self.controller.api.request(
|
2022-10-23 18:28:45 +00:00
|
|
|
DeviceSetOutletRelayRequest.create(device, self._index, True)
|
2022-09-25 18:08:56 +00:00
|
|
|
)
|
2022-01-25 06:49:02 +00:00
|
|
|
|
2022-09-06 11:37:00 +00:00
|
|
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
2022-01-25 06:49:02 +00:00
|
|
|
"""Disable outlet relay."""
|
2022-10-23 18:28:45 +00:00
|
|
|
device = self.controller.api.devices[self._device_mac]
|
2022-09-25 18:08:56 +00:00
|
|
|
await self.controller.api.request(
|
2022-10-23 18:28:45 +00:00
|
|
|
DeviceSetOutletRelayRequest.create(device, self._index, False)
|
2022-01-25 06:49:02 +00:00
|
|
|
)
|
|
|
|
|
2022-10-19 17:54:40 +00:00
|
|
|
|
|
|
|
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."""
|
2022-10-23 18:33:53 +00:00
|
|
|
self._device_mac, index = obj_id.split("_", 1)
|
|
|
|
self._index = int(index)
|
2022-10-19 17:54:40 +00:00
|
|
|
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"
|
2022-10-23 18:33:53 +00:00
|
|
|
self._attr_unique_id = f"{self._device_mac}-poe-{index}"
|
2022-10-19 17:54:40 +00:00
|
|
|
|
|
|
|
device = self.controller.api.devices[self._device_mac]
|
2022-10-23 18:33:53 +00:00
|
|
|
self._attr_available = controller.available and not device.disabled
|
2022-10-19 17:54:40 +00:00
|
|
|
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,
|
2022-10-23 18:28:45 +00:00
|
|
|
hw_version=device.board_revision,
|
2022-10-19 17:54:40 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
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(
|
2022-10-23 18:33:53 +00:00
|
|
|
DeviceSetPoePortModeRequest.create(device, self._index, "auto")
|
2022-10-19 17:54:40 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
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(
|
2022-10-23 18:33:53 +00:00
|
|
|
DeviceSetPoePortModeRequest.create(device, self._index, "off")
|
2022-10-19 17:54:40 +00:00
|
|
|
)
|
2022-10-25 17:26:56 +00:00
|
|
|
|
|
|
|
|
|
|
|
UNIFI_LOADERS: tuple[UnifiEntityLoader, ...] = (
|
|
|
|
UnifiEntityLoader[DPIRestrictionGroups](
|
|
|
|
config_option_fn=lambda controller: controller.option_dpi_restrictions,
|
|
|
|
entity_cls=UnifiDPIRestrictionSwitch,
|
|
|
|
handler_fn=lambda controller: controller.api.dpi_groups,
|
|
|
|
value_fn=lambda handler, index: bool(handler[index].dpiapp_ids),
|
|
|
|
),
|
|
|
|
UnifiEntityLoader[Outlets](
|
|
|
|
config_option_fn=lambda controller: True,
|
|
|
|
entity_cls=UnifiOutletSwitch,
|
|
|
|
handler_fn=lambda controller: controller.api.outlets,
|
|
|
|
value_fn=lambda handler, index: handler[index].has_relay,
|
|
|
|
),
|
|
|
|
UnifiEntityLoader[Ports](
|
|
|
|
config_option_fn=lambda controller: True,
|
|
|
|
entity_cls=UnifiPoePortSwitch,
|
|
|
|
handler_fn=lambda controller: controller.api.ports,
|
|
|
|
value_fn=lambda handler, index: handler[index].port_poe,
|
|
|
|
),
|
|
|
|
)
|