2019-02-13 20:21:14 +00:00
|
|
|
"""Support for devices connected to UniFi POE."""
|
2019-03-21 05:56:46 +00:00
|
|
|
import logging
|
2018-10-16 08:35:35 +00:00
|
|
|
|
2020-05-08 20:19:27 +00:00
|
|
|
from aiounifi.api import SOURCE_EVENT
|
|
|
|
from aiounifi.events import (
|
|
|
|
WIRED_CLIENT_BLOCKED,
|
|
|
|
WIRED_CLIENT_UNBLOCKED,
|
|
|
|
WIRELESS_CLIENT_BLOCKED,
|
|
|
|
WIRELESS_CLIENT_UNBLOCKED,
|
|
|
|
)
|
|
|
|
|
2020-04-26 16:50:37 +00:00
|
|
|
from homeassistant.components.switch import DOMAIN, SwitchEntity
|
2018-10-16 08:35:35 +00:00
|
|
|
from homeassistant.core import callback
|
2019-06-15 15:38:22 +00:00
|
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
2019-07-29 17:48:38 +00:00
|
|
|
from homeassistant.helpers.restore_state import RestoreEntity
|
2018-10-16 08:35:35 +00:00
|
|
|
|
2020-04-23 14:48:24 +00:00
|
|
|
from .const import DOMAIN as UNIFI_DOMAIN
|
2020-01-31 19:23:25 +00:00
|
|
|
from .unifi_client import UniFiClient
|
|
|
|
|
2018-10-16 08:35:35 +00:00
|
|
|
LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2020-04-19 19:30:06 +00:00
|
|
|
BLOCK_SWITCH = "block"
|
|
|
|
POE_SWITCH = "poe"
|
|
|
|
|
2020-05-08 20:19:27 +00:00
|
|
|
CLIENT_BLOCKED = (WIRED_CLIENT_BLOCKED, WIRELESS_CLIENT_BLOCKED)
|
|
|
|
CLIENT_UNBLOCKED = (WIRED_CLIENT_UNBLOCKED, WIRELESS_CLIENT_UNBLOCKED)
|
|
|
|
|
2018-10-16 08:35:35 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
2018-10-16 08:35:35 +00:00
|
|
|
"""Component doesn't support configuration through configuration.yaml."""
|
|
|
|
|
|
|
|
|
|
|
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
|
|
|
"""Set up switches for UniFi component.
|
|
|
|
|
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
|
|
|
"""
|
2020-04-23 14:48:24 +00:00
|
|
|
controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
|
2020-04-19 19:30:06 +00:00
|
|
|
controller.entities[DOMAIN] = {BLOCK_SWITCH: set(), POE_SWITCH: set()}
|
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.
|
|
|
|
previously_known_poe_clients = []
|
2020-04-19 19:30:06 +00:00
|
|
|
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
2020-03-05 05:55:56 +00:00
|
|
|
for entity in entity_registry.entities.values():
|
2019-07-29 17:48:38 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if (
|
2020-04-23 19:29:38 +00:00
|
|
|
entity.config_entry_id != config_entry.entry_id
|
|
|
|
or not entity.unique_id.startswith(POE_SWITCH)
|
2019-07-31 19:25:30 +00:00
|
|
|
):
|
2020-04-23 19:29:38 +00:00
|
|
|
continue
|
2019-07-29 17:48:38 +00:00
|
|
|
|
2020-04-23 19:29:38 +00:00
|
|
|
mac = entity.unique_id.replace(f"{POE_SWITCH}-", "")
|
|
|
|
if mac in controller.api.clients or mac in controller.api.clients_all:
|
|
|
|
previously_known_poe_clients.append(entity.unique_id)
|
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(
|
|
|
|
clients: set = controller.api.clients, devices: set = controller.api.devices
|
|
|
|
) -> 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:
|
|
|
|
add_poe_entities(
|
|
|
|
controller, async_add_entities, clients, previously_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):
|
|
|
|
controller.listeners.append(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()
|
2020-04-23 19:29:38 +00:00
|
|
|
previously_known_poe_clients.clear()
|
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
|
|
|
|
2020-05-04 17:29:49 +00:00
|
|
|
if switches:
|
|
|
|
async_add_entities(switches)
|
2018-10-16 08:35:35 +00:00
|
|
|
|
|
|
|
|
2020-05-04 17:29:49 +00:00
|
|
|
@callback
|
|
|
|
def add_poe_entities(
|
|
|
|
controller, async_add_entities, clients, previously_known_poe_clients
|
|
|
|
):
|
|
|
|
"""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
|
|
|
poe_client_id = f"{POE_SWITCH}-{mac}"
|
|
|
|
client = controller.api.clients[mac]
|
|
|
|
|
|
|
|
if poe_client_id not in previously_known_poe_clients and (
|
|
|
|
mac in controller.wireless_clients
|
|
|
|
or client.sw_mac not in devices
|
|
|
|
or not devices[client.sw_mac].ports[client.sw_port].port_poe
|
|
|
|
or not devices[client.sw_mac].ports[client.sw_port].poe_enable
|
|
|
|
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
|
|
|
|
2020-05-04 17:29:49 +00:00
|
|
|
if poe_client_id in previously_known_poe_clients:
|
|
|
|
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
|
|
|
|
and client.sw_mac == client2.sw_mac
|
|
|
|
and client.sw_port == client2.sw_port
|
|
|
|
):
|
|
|
|
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
|
|
|
|
2020-04-19 19:30:06 +00:00
|
|
|
if switches:
|
|
|
|
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
|
|
|
|
|
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
|
2020-05-08 22:34:18 +00:00
|
|
|
if client.sw_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
|
|
|
|
2019-07-29 17:48:38 +00:00
|
|
|
async def async_added_to_hass(self):
|
|
|
|
"""Call when entity about to be added to Home Assistant."""
|
2020-01-31 19:23:25 +00:00
|
|
|
await super().async_added_to_hass()
|
|
|
|
|
2019-07-29 17:48:38 +00:00
|
|
|
state = await self.async_get_last_state()
|
|
|
|
if state is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
if self.poe_mode is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
self.poe_mode = state.attributes["poe_mode"]
|
2019-07-29 17:48:38 +00:00
|
|
|
|
|
|
|
if not self.client.sw_mac:
|
2019-07-31 19:25:30 +00:00
|
|
|
self.client.raw["sw_mac"] = state.attributes["switch"]
|
2019-07-29 17:48:38 +00:00
|
|
|
|
|
|
|
if not self.client.sw_port:
|
2019-07-31 19:25:30 +00:00
|
|
|
self.client.raw["sw_port"] = state.attributes["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
|
|
|
|
def available(self):
|
2019-07-29 17:48:38 +00:00
|
|
|
"""Return if switch is available.
|
|
|
|
|
|
|
|
Poe_mode None means its poe state is unknown.
|
|
|
|
Sw_mac unavailable means restored client.
|
|
|
|
"""
|
2019-07-31 19:25:30 +00:00
|
|
|
return (
|
|
|
|
self.poe_mode is None
|
|
|
|
or self.client.sw_mac
|
|
|
|
and (
|
|
|
|
self.controller.available
|
2019-08-31 20:04:04 +00:00
|
|
|
and self.client.sw_mac in self.controller.api.devices
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
|
|
|
)
|
2018-10-16 08:35:35 +00:00
|
|
|
|
|
|
|
async def async_turn_on(self, **kwargs):
|
|
|
|
"""Enable POE for client."""
|
2019-07-31 19:25:30 +00:00
|
|
|
await self.device.async_set_port_poe_mode(self.client.sw_port, self.poe_mode)
|
2018-10-16 08:35:35 +00:00
|
|
|
|
|
|
|
async def async_turn_off(self, **kwargs):
|
|
|
|
"""Disable POE for client."""
|
2019-07-31 19:25:30 +00:00
|
|
|
await self.device.async_set_port_poe_mode(self.client.sw_port, "off")
|
2018-10-16 08:35:35 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def device_state_attributes(self):
|
|
|
|
"""Return the device state attributes."""
|
|
|
|
attributes = {
|
2019-07-31 19:25:30 +00:00
|
|
|
"power": self.port.poe_power,
|
|
|
|
"switch": self.client.sw_mac,
|
|
|
|
"port": self.client.sw_port,
|
|
|
|
"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."""
|
|
|
|
return self.controller.api.devices[self.client.sw_mac]
|
|
|
|
|
|
|
|
@property
|
|
|
|
def port(self):
|
|
|
|
"""Shortcut to the switch port that client is connected to."""
|
2020-02-01 19:48:23 +00:00
|
|
|
try:
|
|
|
|
return self.device.ports[self.client.sw_port]
|
2020-03-10 17:27:25 +00:00
|
|
|
except (AttributeError, KeyError, TypeError):
|
2020-02-01 19:48:23 +00:00
|
|
|
LOGGER.warning(
|
|
|
|
"Entity %s reports faulty device %s or port %s",
|
|
|
|
self.entity_id,
|
|
|
|
self.client.sw_mac,
|
|
|
|
self.client.sw_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
|
|
|
|
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."""
|
|
|
|
if self.client.last_updated == SOURCE_EVENT:
|
|
|
|
|
|
|
|
if self.client.event.event in CLIENT_BLOCKED + CLIENT_UNBLOCKED:
|
|
|
|
self._is_blocked = self.client.event.event in CLIENT_BLOCKED
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
async def async_turn_on(self, **kwargs):
|
2019-08-04 14:57:36 +00:00
|
|
|
"""Turn on connectivity for client."""
|
|
|
|
await self.controller.api.clients.async_unblock(self.client.mac)
|
2019-07-25 14:56:56 +00:00
|
|
|
|
|
|
|
async def async_turn_off(self, **kwargs):
|
2019-08-04 14:57:36 +00:00
|
|
|
"""Turn off connectivity for client."""
|
|
|
|
await self.controller.api.clients.async_block(self.client.mac)
|
2020-03-10 17:27:25 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def icon(self):
|
|
|
|
"""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})
|