Merge UniFi device tracker to config entry (#24367)

* Move device tracker to use config entry

* Remove monitored conditions attributes based on ADR0003

* Add support for import of device tracker config to be backwards compatible

* Remove unnecessary configuration options from device tracker

* Add component configuration support
This commit is contained in:
Robert Svensson 2019-07-14 21:57:09 +02:00 committed by GitHub
parent 3480e6229a
commit 01b890f426
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 493 additions and 449 deletions

View file

@ -1,172 +1,181 @@
"""Support for Unifi WAP controllers."""
import asyncio
import logging
from datetime import timedelta
import logging
import voluptuous as vol
import async_timeout
import aiounifi
from homeassistant import config_entries
from homeassistant.components import unifi
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
from homeassistant.components.device_tracker.config_entry import ScannerEntity
from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER
from homeassistant.core import callback
from homeassistant.const import (
CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_VERIFY_SSL)
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.const import CONF_VERIFY_SSL, CONF_MONITORED_CONDITIONS
import homeassistant.util.dt as dt_util
from .controller import get_controller
from .errors import AuthenticationRequired, CannotConnect
from .const import (
CONF_CONTROLLER, CONF_DETECTION_TIME, CONF_SITE_ID, CONF_SSID_FILTER,
CONTROLLER_ID, DOMAIN as UNIFI_DOMAIN)
_LOGGER = logging.getLogger(__name__)
CONF_PORT = 'port'
CONF_SITE_ID = 'site_id'
CONF_DETECTION_TIME = 'detection_time'
CONF_SSID_FILTER = 'ssid_filter'
LOGGER = logging.getLogger(__name__)
DEVICE_ATTRIBUTES = [
'_is_guest_by_uap', 'ap_mac', 'authorized', 'bssid', 'ccq',
'channel', 'essid', 'hostname', 'ip', 'is_11r', 'is_guest', 'is_wired',
'mac', 'name', 'noise', 'noted', 'oui', 'qos_policy_applied', 'radio',
'radio_proto', 'rssi', 'signal', 'site_id', 'vlan'
]
CONF_DT_SITE_ID = 'site_id'
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 8443
DEFAULT_VERIFY_SSL = True
DEFAULT_DETECTION_TIME = timedelta(seconds=300)
NOTIFICATION_ID = 'unifi_notification'
NOTIFICATION_TITLE = 'Unifi Device Tracker Setup'
AVAILABLE_ATTRS = [
'_id', '_is_guest_by_uap', '_last_seen_by_uap', '_uptime_by_uap',
'ap_mac', 'assoc_time', 'authorized', 'bssid', 'bytes-r', 'ccq',
'channel', 'essid', 'first_seen', 'hostname', 'idletime', 'ip',
'is_11r', 'is_guest', 'is_wired', 'last_seen', 'latest_assoc_time',
'mac', 'name', 'noise', 'noted', 'oui', 'powersave_enabled',
'qos_policy_applied', 'radio', 'radio_proto', 'rssi', 'rx_bytes',
'rx_bytes-r', 'rx_packets', 'rx_rate', 'signal', 'site_id',
'tx_bytes', 'tx_bytes-r', 'tx_packets', 'tx_power', 'tx_rate',
'uptime', 'user_id', 'usergroup_id', 'vlan'
]
TIMESTAMP_ATTRS = ['first_seen', 'last_seen', 'latest_assoc_time']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_SITE_ID, default='default'): cv.string,
vol.Optional(CONF_DT_SITE_ID, default='default'): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): vol.Any(
cv.boolean, cv.isfile),
vol.Optional(CONF_DETECTION_TIME, default=DEFAULT_DETECTION_TIME): vol.All(
cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_MONITORED_CONDITIONS):
vol.All(cv.ensure_list, [vol.In(AVAILABLE_ATTRS)]),
vol.Optional(CONF_SSID_FILTER): vol.All(cv.ensure_list, [cv.string])
})
cv.boolean, cv.isfile)
}, extra=vol.ALLOW_EXTRA)
async def async_get_scanner(hass, config):
"""Set up the Unifi device_tracker."""
host = config[DOMAIN].get(CONF_HOST)
username = config[DOMAIN].get(CONF_USERNAME)
password = config[DOMAIN].get(CONF_PASSWORD)
site_id = config[DOMAIN].get(CONF_SITE_ID)
port = config[DOMAIN].get(CONF_PORT)
verify_ssl = config[DOMAIN].get(CONF_VERIFY_SSL)
detection_time = config[DOMAIN].get(CONF_DETECTION_TIME)
monitored_conditions = config[DOMAIN].get(CONF_MONITORED_CONDITIONS)
ssid_filter = config[DOMAIN].get(CONF_SSID_FILTER)
async def async_setup_scanner(hass, config, sync_see, discovery_info):
"""Set up the Unifi integration."""
config[CONF_SITE_ID] = config.pop(CONF_DT_SITE_ID) # Current from legacy
try:
controller = await get_controller(
hass, host, username, password, port, site_id, verify_ssl)
await controller.initialize()
exist = False
except (AuthenticationRequired, CannotConnect) as ex:
_LOGGER.error("Failed to connect to Unifi: %s", ex)
hass.components.persistent_notification.create(
'Failed to connect to Unifi. '
'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
for entry in hass.config_entries.async_entries(UNIFI_DOMAIN):
if config[CONF_HOST] == entry.data[CONF_CONTROLLER][CONF_HOST] and \
config[CONF_SITE_ID] == \
entry.data[CONF_CONTROLLER][CONF_SITE_ID]:
exist = True
break
return UnifiScanner(
controller, detection_time, ssid_filter, monitored_conditions)
if not exist:
hass.async_create_task(hass.config_entries.flow.async_init(
UNIFI_DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
data=config
))
return True
class UnifiScanner(DeviceScanner):
"""Provide device_tracker support from Unifi WAP client data."""
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up device tracker for UniFi component."""
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]
tracked = {}
def __init__(self, controller, detection_time: timedelta,
ssid_filter, monitored_conditions) -> None:
"""Initialize the scanner."""
@callback
def update_controller():
"""Update the values of the controller."""
update_items(controller, async_add_entities, tracked)
async_dispatcher_connect(hass, controller.event_update, update_controller)
update_controller()
@callback
def update_items(controller, async_add_entities, tracked):
"""Update tracked device state from the controller."""
new_tracked = []
for client_id in controller.api.clients:
if client_id in tracked:
LOGGER.debug("Updating UniFi tracked device %s (%s)",
tracked[client_id].entity_id,
tracked[client_id].client.mac)
tracked[client_id].async_schedule_update_ha_state()
continue
client = controller.api.clients[client_id]
if not client.is_wired and \
CONF_SSID_FILTER in controller.unifi_config and \
client.essid not in controller.unifi_config[CONF_SSID_FILTER]:
continue
tracked[client_id] = UniFiClientTracker(client, controller)
new_tracked.append(tracked[client_id])
LOGGER.debug("New UniFi switch %s (%s)", client.hostname, client.mac)
if new_tracked:
async_add_entities(new_tracked)
class UniFiClientTracker(ScannerEntity):
"""Representation of a network device."""
def __init__(self, client, controller):
"""Set up tracked device."""
self.client = client
self.controller = controller
self._detection_time = detection_time
self._ssid_filter = ssid_filter
self._monitored_conditions = monitored_conditions
self._clients = {}
async def async_update(self):
"""Get the clients from the device."""
try:
await self.controller.clients.update()
clients = self.controller.clients.values()
"""Synchronize state with controller."""
await self.controller.request_update()
except aiounifi.LoginRequired:
try:
with async_timeout.timeout(5):
await self.controller.login()
except (asyncio.TimeoutError, aiounifi.AiounifiException):
clients = []
@property
def is_connected(self):
"""Return true if the device is connected to the network."""
detection_time = self.controller.unifi_config.get(
CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME)
except aiounifi.AiounifiException:
clients = []
if (dt_util.utcnow() - dt_util.utc_from_timestamp(float(
self.client.last_seen))) < detection_time:
return True
return False
# Filter clients to provided SSID list
if self._ssid_filter:
clients = [
client for client in clients
if client.essid in self._ssid_filter
]
@property
def source_type(self):
"""Return the source type of the device."""
return SOURCE_TYPE_ROUTER
self._clients = {
client.raw['mac']: client.raw
for client in clients
if (dt_util.utcnow() - dt_util.utc_from_timestamp(float(
client.last_seen))) < self._detection_time
@property
def name(self) -> str:
"""Return the name of the device."""
return self.client.name or self.client.hostname
@property
def unique_id(self) -> str:
"""Return a unique identifier for this client."""
return '{}-{}'.format(self.client.mac, self.controller.site)
@property
def available(self) -> bool:
"""Return if controller is available."""
return self.controller.available
@property
def device_info(self):
"""Return a device description for device registry."""
return {
'connections': {(CONNECTION_NETWORK_MAC, self.client.mac)}
}
async def async_scan_devices(self):
"""Scan for devices."""
await self.async_update()
return self._clients.keys()
def get_device_name(self, device):
"""Return the name (if known) of the device.
If a name has been set in Unifi, then return that, else
return the hostname if it has been detected.
"""
client = self._clients.get(device, {})
name = client.get('name') or client.get('hostname')
_LOGGER.debug("Device mac %s name %s", device, name)
return name
def get_extra_attributes(self, device):
"""Return the extra attributes of the device."""
if not self._monitored_conditions:
return {}
client = self._clients.get(device, {})
@property
def device_state_attributes(self):
"""Return the device state attributes."""
attributes = {}
for variable in self._monitored_conditions:
if variable in client:
if variable in TIMESTAMP_ATTRS:
attributes[variable] = dt_util.utc_from_timestamp(
float(client[variable])
)
else:
attributes[variable] = client[variable]
_LOGGER.debug("Device mac %s attributes %s", device, attributes)
for variable in DEVICE_ATTRIBUTES:
if variable in self.client.raw:
attributes[variable] = self.client.raw[variable]
return attributes