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:
parent
3480e6229a
commit
01b890f426
11 changed files with 493 additions and 449 deletions
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue