"""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.option_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 allowed to connect.""" return not self.client.blocked @property def available(self): """Return if controller is available.""" return self.controller.available async def async_turn_on(self, **kwargs): """Turn on connectivity for client.""" await self.controller.api.clients.async_unblock(self.client.mac) async def async_turn_off(self, **kwargs): """Turn off connectivity for client.""" await self.controller.api.clients.async_block(self.client.mac)