2019-02-13 20:21:14 +00:00
|
|
|
"""Support for devices connected to UniFi POE."""
|
2018-10-16 08:35:35 +00:00
|
|
|
import asyncio
|
|
|
|
from datetime import timedelta
|
2019-03-21 05:56:46 +00:00
|
|
|
import logging
|
2018-10-16 08:35:35 +00:00
|
|
|
|
|
|
|
import async_timeout
|
|
|
|
|
|
|
|
from homeassistant.components import unifi
|
|
|
|
from homeassistant.components.switch import SwitchDevice
|
|
|
|
from homeassistant.const import CONF_HOST
|
|
|
|
from homeassistant.core import callback
|
|
|
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
|
|
|
|
2019-04-12 17:13:30 +00:00
|
|
|
from .const import CONF_CONTROLLER, CONF_SITE_ID, CONTROLLER_ID
|
2019-03-21 05:56:46 +00:00
|
|
|
|
2018-10-16 08:35:35 +00:00
|
|
|
SCAN_INTERVAL = timedelta(seconds=15)
|
|
|
|
|
|
|
|
LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
async def async_setup_platform(
|
|
|
|
hass, config, async_add_entities, discovery_info=None):
|
|
|
|
"""Component doesn't support configuration through configuration.yaml."""
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
|
|
|
"""Set up switches for UniFi component.
|
|
|
|
|
|
|
|
Switches are controlling network switch ports with Poe.
|
|
|
|
"""
|
|
|
|
controller_id = CONTROLLER_ID.format(
|
|
|
|
host=config_entry.data[CONF_CONTROLLER][CONF_HOST],
|
2019-02-13 20:21:14 +00:00
|
|
|
site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID],
|
2018-10-16 08:35:35 +00:00
|
|
|
)
|
|
|
|
controller = hass.data[unifi.DOMAIN][controller_id]
|
|
|
|
switches = {}
|
|
|
|
|
|
|
|
progress = None
|
|
|
|
update_progress = set()
|
|
|
|
|
|
|
|
async def request_update(object_id):
|
|
|
|
"""Request an update."""
|
|
|
|
nonlocal progress
|
|
|
|
update_progress.add(object_id)
|
|
|
|
|
|
|
|
if progress is not None:
|
|
|
|
return await progress
|
|
|
|
|
|
|
|
progress = asyncio.ensure_future(update_controller())
|
|
|
|
result = await progress
|
|
|
|
progress = None
|
|
|
|
update_progress.clear()
|
|
|
|
return result
|
|
|
|
|
|
|
|
async def update_controller():
|
|
|
|
"""Update the values of the controller."""
|
|
|
|
tasks = [async_update_items(
|
|
|
|
controller, async_add_entities, request_update,
|
|
|
|
switches, update_progress
|
|
|
|
)]
|
|
|
|
await asyncio.wait(tasks)
|
|
|
|
|
|
|
|
await update_controller()
|
|
|
|
|
|
|
|
|
|
|
|
async def async_update_items(controller, async_add_entities,
|
|
|
|
request_controller_update, switches,
|
|
|
|
progress_waiting):
|
|
|
|
"""Update POE port state from the controller."""
|
|
|
|
import aiounifi
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def update_switch_state():
|
|
|
|
"""Tell switches to reload state."""
|
|
|
|
for client_id, client in switches.items():
|
|
|
|
if client_id not in progress_waiting:
|
|
|
|
client.async_schedule_update_ha_state()
|
|
|
|
|
|
|
|
try:
|
|
|
|
with async_timeout.timeout(4):
|
|
|
|
await controller.api.clients.update()
|
|
|
|
await controller.api.devices.update()
|
|
|
|
|
|
|
|
except aiounifi.LoginRequired:
|
|
|
|
try:
|
|
|
|
with async_timeout.timeout(5):
|
|
|
|
await controller.api.login()
|
|
|
|
except (asyncio.TimeoutError, aiounifi.AiounifiException):
|
|
|
|
if controller.available:
|
|
|
|
controller.available = False
|
|
|
|
update_switch_state()
|
|
|
|
return
|
|
|
|
|
|
|
|
except (asyncio.TimeoutError, aiounifi.AiounifiException):
|
|
|
|
if controller.available:
|
|
|
|
LOGGER.error('Unable to reach controller %s', controller.host)
|
|
|
|
controller.available = False
|
|
|
|
update_switch_state()
|
|
|
|
return
|
|
|
|
|
|
|
|
if not controller.available:
|
|
|
|
LOGGER.info('Reconnected to controller %s', controller.host)
|
|
|
|
controller.available = True
|
|
|
|
|
|
|
|
new_switches = []
|
|
|
|
devices = controller.api.devices
|
|
|
|
for client_id in controller.api.clients:
|
|
|
|
|
|
|
|
if client_id in progress_waiting:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if client_id in switches:
|
|
|
|
LOGGER.debug("Updating UniFi switch %s (%s)",
|
|
|
|
switches[client_id].entity_id,
|
|
|
|
switches[client_id].client.mac)
|
|
|
|
switches[client_id].async_schedule_update_ha_state()
|
|
|
|
continue
|
|
|
|
|
|
|
|
client = controller.api.clients[client_id]
|
|
|
|
# Network device with active POE
|
|
|
|
if not client.is_wired or client.sw_mac not in devices or \
|
|
|
|
not devices[client.sw_mac].ports[client.sw_port].port_poe or \
|
2018-11-02 20:09:16 +00:00
|
|
|
not devices[client.sw_mac].ports[client.sw_port].poe_enable or \
|
|
|
|
controller.mac == client.mac:
|
2018-10-16 08:35:35 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
# Multiple POE-devices on same port means non UniFi POE driven switch
|
|
|
|
multi_clients_on_port = False
|
|
|
|
for client2 in controller.api.clients.values():
|
|
|
|
if client.mac != client2.mac and \
|
|
|
|
client.sw_mac == client2.sw_mac and \
|
|
|
|
client.sw_port == client2.sw_port:
|
|
|
|
multi_clients_on_port = True
|
|
|
|
break
|
|
|
|
|
|
|
|
if multi_clients_on_port:
|
|
|
|
continue
|
|
|
|
|
|
|
|
switches[client_id] = UniFiSwitch(
|
|
|
|
client, controller, request_controller_update)
|
|
|
|
new_switches.append(switches[client_id])
|
|
|
|
LOGGER.debug("New UniFi switch %s (%s)", client.hostname, client.mac)
|
|
|
|
|
|
|
|
if new_switches:
|
|
|
|
async_add_entities(new_switches)
|
|
|
|
|
|
|
|
|
|
|
|
class UniFiSwitch(SwitchDevice):
|
|
|
|
"""Representation of a client that uses POE."""
|
|
|
|
|
|
|
|
def __init__(self, client, controller, request_controller_update):
|
|
|
|
"""Set up switch."""
|
|
|
|
self.client = client
|
|
|
|
self.controller = controller
|
|
|
|
self.poe_mode = None
|
|
|
|
if self.port.poe_mode != 'off':
|
|
|
|
self.poe_mode = self.port.poe_mode
|
|
|
|
self.async_request_controller_update = request_controller_update
|
|
|
|
|
|
|
|
async def async_update(self):
|
|
|
|
"""Synchronize state with controller."""
|
|
|
|
await self.async_request_controller_update(self.client.mac)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Return the name of the switch."""
|
|
|
|
return self.client.hostname
|
|
|
|
|
|
|
|
@property
|
|
|
|
def unique_id(self):
|
|
|
|
"""Return a unique identifier for this switch."""
|
|
|
|
return 'poe-{}'.format(self.client.mac)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_on(self):
|
|
|
|
"""Return true if POE is active."""
|
|
|
|
return self.port.poe_mode != 'off'
|
|
|
|
|
|
|
|
@property
|
|
|
|
def available(self):
|
|
|
|
"""Return if switch is available."""
|
|
|
|
return self.controller.available or \
|
|
|
|
self.client.sw_mac in self.controller.api.devices
|
|
|
|
|
|
|
|
async def async_turn_on(self, **kwargs):
|
|
|
|
"""Enable POE for client."""
|
|
|
|
await self.device.async_set_port_poe_mode(
|
|
|
|
self.client.sw_port, self.poe_mode)
|
|
|
|
|
|
|
|
async def async_turn_off(self, **kwargs):
|
|
|
|
"""Disable POE for client."""
|
|
|
|
await self.device.async_set_port_poe_mode(self.client.sw_port, 'off')
|
|
|
|
|
|
|
|
@property
|
|
|
|
def device_state_attributes(self):
|
|
|
|
"""Return the device state attributes."""
|
|
|
|
attributes = {
|
|
|
|
'power': self.port.poe_power,
|
|
|
|
'received': self.client.wired_rx_bytes / 1000000,
|
|
|
|
'sent': self.client.wired_tx_bytes / 1000000,
|
|
|
|
'switch': self.client.sw_mac,
|
|
|
|
'port': self.client.sw_port,
|
|
|
|
'poe_mode': self.poe_mode
|
|
|
|
}
|
|
|
|
return attributes
|
|
|
|
|
|
|
|
@property
|
|
|
|
def device_info(self):
|
|
|
|
"""Return a device description for device registry."""
|
|
|
|
return {
|
|
|
|
'connections': {(CONNECTION_NETWORK_MAC, self.client.mac)}
|
|
|
|
}
|
|
|
|
|
|
|
|
@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."""
|
|
|
|
return self.device.ports[self.client.sw_port]
|