hass-core/homeassistant/components/unifi/switch.py
Robert Svensson dc722adbb5
UniFi POE control restore clients (#25558)
* Restore POE controls on restart
2019-07-29 19:48:38 +02:00

273 lines
8.8 KiB
Python

"""Support for devices connected to UniFi POE."""
import logging
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 import entity_registry
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.restore_state import RestoreEntity
from .const import CONF_CONTROLLER, CONF_SITE_ID, CONTROLLER_ID
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],
site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID],
)
controller = hass.data[unifi.DOMAIN][controller_id]
if controller.site_role != 'admin':
return
switches = {}
switches_off = []
registry = await entity_registry.async_get_registry(hass)
# Restore clients that is not a part of active clients list.
for entity in registry.entities.values():
if entity.config_entry_id == config_entry.entry_id and \
entity.unique_id.startswith('poe-'):
_, mac = entity.unique_id.split('-', 1)
if mac in controller.api.clients or \
mac not in controller.api.clients_all:
continue
client = controller.api.clients_all[mac]
controller.api.clients.process_raw([client.raw])
switches_off.append(entity.unique_id)
@callback
def update_controller():
"""Update the values of the controller."""
update_items(controller, async_add_entities, switches, switches_off)
async_dispatcher_connect(hass, controller.event_update, update_controller)
update_controller()
switches_off.clear()
@callback
def update_items(controller, async_add_entities, switches, switches_off):
"""Update POE port state from the controller."""
new_switches = []
devices = controller.api.devices
# block client
for client_id in controller.block_clients:
block_client_id = 'block-{}'.format(client_id)
if block_client_id in switches:
LOGGER.debug("Updating UniFi block switch %s (%s)",
switches[block_client_id].entity_id,
switches[block_client_id].client.mac)
switches[block_client_id].async_schedule_update_ha_state()
continue
if client_id not in controller.api.clients_all:
continue
client = controller.api.clients_all[client_id]
switches[block_client_id] = UniFiBlockClientSwitch(client, controller)
new_switches.append(switches[block_client_id])
LOGGER.debug(
"New UniFi Block switch %s (%s)", client.hostname, client.mac)
# control poe
for client_id in controller.api.clients:
poe_client_id = 'poe-{}'.format(client_id)
if poe_client_id in switches:
LOGGER.debug("Updating UniFi POE switch %s (%s)",
switches[poe_client_id].entity_id,
switches[poe_client_id].client.mac)
switches[poe_client_id].async_schedule_update_ha_state()
continue
client = controller.api.clients[client_id]
if poe_client_id in switches_off:
pass
# Network device with active POE
elif not client.is_wired or client.sw_mac not in devices or \
not devices[client.sw_mac].ports[client.sw_port].port_poe or \
controller.mac == client.mac:
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 poe_client_id in switches_off:
break
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
if multi_clients_on_port:
continue
switches[poe_client_id] = UniFiPOEClientSwitch(client, controller)
new_switches.append(switches[poe_client_id])
LOGGER.debug(
"New UniFi POE switch %s (%s)", client.hostname, client.mac)
if new_switches:
async_add_entities(new_switches)
class UniFiClient:
"""Base class for UniFi switches."""
def __init__(self, client, controller):
"""Set up switch."""
self.client = client
self.controller = controller
async def async_update(self):
"""Synchronize state with controller."""
await self.controller.request_update()
@property
def name(self):
"""Return the name of the client."""
return self.client.name or self.client.hostname
@property
def device_info(self):
"""Return a device description for device registry."""
return {
'connections': {(CONNECTION_NETWORK_MAC, self.client.mac)}
}
class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity):
"""Representation of a client that uses POE."""
def __init__(self, client, controller):
"""Set up POE switch."""
super().__init__(client, controller)
self.poe_mode = None
if self.client.sw_port and self.port.poe_mode != 'off':
self.poe_mode = self.port.poe_mode
async def async_added_to_hass(self):
"""Call when entity about to be added to Home Assistant."""
state = await self.async_get_last_state()
if state is None:
return
if self.poe_mode is None:
self.poe_mode = state.attributes['poe_mode']
if not self.client.sw_mac:
self.client.raw['sw_mac'] = state.attributes['switch']
if not self.client.sw_port:
self.client.raw['sw_port'] = state.attributes['port']
@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.
Poe_mode None means its poe state is unknown.
Sw_mac unavailable means restored client.
"""
return self.poe_mode is None or self.client.sw_mac and (
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(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]
class UniFiBlockClientSwitch(UniFiClient, SwitchDevice):
"""Representation of a blockable client."""
@property
def unique_id(self):
"""Return a unique identifier for this switch."""
return 'block-{}'.format(self.client.mac)
@property
def is_on(self):
"""Return true if client is blocked."""
return self.client.blocked
@property
def available(self):
"""Return if controller is available."""
return self.controller.available
async def async_turn_on(self, **kwargs):
"""Block client."""
await self.controller.api.clients.async_block(self.client.mac)
async def async_turn_off(self, **kwargs):
"""Unblock client."""
await self.controller.api.clients.async_unblock(self.client.mac)