Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 50 additions & 5 deletions homeassistant/components/unifi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
"""Support for devices connected to UniFi POE."""
import voluptuous as vol

from homeassistant.components.unifi.config_flow import (
get_controller_id_from_config_entry,
)
from homeassistant.const import CONF_HOST
from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC

import homeassistant.helpers.config_validation as cv

from .config_flow import get_controller_id_from_config_entry
from .const import (
ATTR_MANUFACTURER,
CONF_BLOCK_CLIENT,
Expand All @@ -20,9 +19,14 @@
CONF_SSID_FILTER,
DOMAIN,
UNIFI_CONFIG,
UNIFI_WIRELESS_CLIENTS,
)
from .controller import UniFiController

SAVE_DELAY = 10
STORAGE_KEY = "unifi_data"
STORAGE_VERSION = 1

CONF_CONTROLLERS = "controllers"

CONTROLLER_SCHEMA = vol.Schema(
Expand Down Expand Up @@ -61,6 +65,9 @@ async def async_setup(hass, config):
if DOMAIN in config:
hass.data[UNIFI_CONFIG] = config[DOMAIN][CONF_CONTROLLERS]

hass.data[UNIFI_WIRELESS_CLIENTS] = wireless_clients = UnifiWirelessClients(hass)
await wireless_clients.async_load()

return True


Expand All @@ -70,9 +77,7 @@ async def async_setup_entry(hass, config_entry):
hass.data[DOMAIN] = {}

controller = UniFiController(hass, config_entry)

controller_id = get_controller_id_from_config_entry(config_entry)

hass.data[DOMAIN][controller_id] = controller

if not await controller.async_setup():
Expand All @@ -99,3 +104,43 @@ async def async_unload_entry(hass, config_entry):
controller_id = get_controller_id_from_config_entry(config_entry)
controller = hass.data[DOMAIN].pop(controller_id)
return await controller.async_reset()


class UnifiWirelessClients:
"""Class to store clients known to be wireless.

This is needed since wireless devices going offline might get marked as wired by UniFi.
"""

def __init__(self, hass):
"""Set up client storage."""
self.hass = hass
self.data = {}
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)

async def async_load(self):
"""Load data from file."""
data = await self._store.async_load()

if data is not None:
self.data = data

@callback
def get_data(self, config_entry):
"""Get data related to a specific controller."""
controller_id = get_controller_id_from_config_entry(config_entry)
data = self.data.get(controller_id, {"wireless_devices": []})
return set(data["wireless_devices"])

@callback
def update_data(self, data, config_entry):
"""Update data and schedule to save to file."""
controller_id = get_controller_id_from_config_entry(config_entry)
self.data[controller_id] = {"wireless_devices": list(data)}

self._store.async_delay_save(self._data_to_save, SAVE_DELAY)

@callback
def _data_to_save(self):
"""Return data of UniFi wireless clients to store in a file."""
return self.data
1 change: 1 addition & 0 deletions homeassistant/components/unifi/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
CONF_SITE_ID = "site"

UNIFI_CONFIG = "unifi_config"
UNIFI_WIRELESS_CLIENTS = "unifi_wireless_clients"

CONF_BLOCK_CLIENT = "block_client"
CONF_DETECTION_TIME = "detection_time"
Expand Down
24 changes: 24 additions & 0 deletions homeassistant/components/unifi/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
DOMAIN,
LOGGER,
UNIFI_CONFIG,
UNIFI_WIRELESS_CLIENTS,
)
from .errors import AuthenticationRequired, CannotConnect

Expand All @@ -50,6 +51,7 @@ def __init__(self, hass, config_entry):
self.available = True
self.api = None
self.progress = None
self.wireless_clients = None

self._site_name = None
self._site_role = None
Expand Down Expand Up @@ -128,6 +130,22 @@ def signal_options_update(self):
"""Event specific per UniFi entry to signal new options."""
return f"unifi-options-{CONTROLLER_ID.format(host=self.host, site=self.site)}"

def update_wireless_clients(self):
"""Update set of known to be wireless clients."""
new_wireless_clients = set()

for client_id in self.api.clients:
if (
client_id not in self.wireless_clients
and not self.api.clients[client_id].is_wired
):
new_wireless_clients.add(client_id)

if new_wireless_clients:
self.wireless_clients |= new_wireless_clients
unifi_wireless_clients = self.hass.data[UNIFI_WIRELESS_CLIENTS]
unifi_wireless_clients.update_data(self.wireless_clients, self.config_entry)

async def request_update(self):
"""Request an update."""
if self.progress is not None:
Expand Down Expand Up @@ -170,6 +188,8 @@ async def async_update(self):
LOGGER.info("Reconnected to controller %s", self.host)
self.available = True

self.update_wireless_clients()

async_dispatcher_send(self.hass, self.signal_update)

async def async_setup(self):
Expand Down Expand Up @@ -197,6 +217,10 @@ async def async_setup(self):
LOGGER.error("Unknown error connecting with UniFi controller: %s", err)
return False

wireless_clients = hass.data[UNIFI_WIRELESS_CLIENTS]
self.wireless_clients = wireless_clients.get_data(self.config_entry)
self.update_wireless_clients()

self.import_configuration()

self.config_entry.add_update_listener(self.async_options_updated)
Expand Down
31 changes: 23 additions & 8 deletions homeassistant/components/unifi/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
"ip",
"is_11r",
"is_guest",
"is_wired",
"mac",
"name",
"noted",
Expand Down Expand Up @@ -121,6 +120,7 @@ def __init__(self, client, controller):
"""Set up tracked client."""
self.client = client
self.controller = controller
self.is_wired = self.client.mac not in controller.wireless_clients

@property
def entity_registry_enabled_default(self):
Expand All @@ -129,13 +129,13 @@ def entity_registry_enabled_default(self):
return False

if (
not self.client.is_wired
not self.is_wired
and self.controller.option_ssid_filter
and self.client.essid not in self.controller.option_ssid_filter
):
return False

if not self.controller.option_track_wired_clients and self.client.is_wired:
if not self.controller.option_track_wired_clients and self.is_wired:
return False

return True
Expand All @@ -145,18 +145,31 @@ async def async_added_to_hass(self):
LOGGER.debug("New UniFi client tracker %s (%s)", self.name, self.client.mac)

async def async_update(self):
"""Synchronize state with controller."""
"""Synchronize state with controller.

Make sure to update self.is_wired if client is wireless, there is an issue when clients go offline that they get marked as wired.
"""
LOGGER.debug(
"Updating UniFi tracked client %s (%s)", self.entity_id, self.client.mac
)
await self.controller.request_update()

if self.is_wired and self.client.mac in self.controller.wireless_clients:
self.is_wired = False

@property
def is_connected(self):
"""Return true if the client is connected to the network."""
if (
dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.client.last_seen))
) < self.controller.option_detection_time:
"""Return true if the client is connected to the network.

If is_wired and client.is_wired differ it means that the device is offline and UniFi bug shows device as wired.
"""
if self.is_wired == self.client.is_wired and (
(
dt_util.utcnow()
- dt_util.utc_from_timestamp(float(self.client.last_seen))
)
< self.controller.option_detection_time
):
return True

return False
Expand Down Expand Up @@ -195,6 +208,8 @@ def device_state_attributes(self):
if variable in self.client.raw:
attributes[variable] = self.client.raw[variable]

attributes["is_wired"] = self.is_wired

return attributes


Expand Down
5 changes: 3 additions & 2 deletions homeassistant/components/unifi/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def update_items(controller, async_add_entities, switches, switches_off):
new_switches.append(switches[block_client_id])
LOGGER.debug("New UniFi Block switch %s (%s)", client.hostname, client.mac)

# control poe
# control POE
for client_id in controller.api.clients:

poe_client_id = f"poe-{client_id}"
Expand All @@ -108,9 +108,10 @@ def update_items(controller, async_add_entities, switches, switches_off):
pass
# Network device with active POE
elif (
not client.is_wired
client_id in controller.wireless_clients
or client.sw_mac not in devices
or not devices[client.sw_mac].ports[client.sw_port].port_poe
or not devices[client.sw_mac].ports[client.sw_port].poe_enable
or controller.mac == client.mac
):
continue
Expand Down
14 changes: 10 additions & 4 deletions tests/components/unifi/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
CONF_CONTROLLER,
CONF_SITE_ID,
UNIFI_CONFIG,
UNIFI_WIRELESS_CLIENTS,
)
from homeassistant.const import (
CONF_HOST,
Expand Down Expand Up @@ -49,14 +50,16 @@ async def test_controller_setup():
controller.CONF_DETECTION_TIME: 30,
controller.CONF_SSID_FILTER: ["ssid"],
}
]
],
UNIFI_WIRELESS_CLIENTS: Mock(),
}
entry = Mock()
entry.data = ENTRY_CONFIG
entry.options = {}
api = Mock()
api.initialize.return_value = mock_coro(True)
api.sites.return_value = mock_coro(CONTROLLER_SITES)
api.clients = []

unifi_controller = controller.UniFiController(hass, entry)

Expand Down Expand Up @@ -100,7 +103,8 @@ async def test_controller_site():
async def test_controller_mac():
"""Test that it is possible to identify controller mac."""
hass = Mock()
hass.data = {UNIFI_CONFIG: {}}
hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()}
hass.data[UNIFI_WIRELESS_CLIENTS].get_data.return_value = set()
entry = Mock()
entry.data = ENTRY_CONFIG
entry.options = {}
Expand All @@ -123,7 +127,7 @@ async def test_controller_mac():
async def test_controller_no_mac():
"""Test that it works to not find the controllers mac."""
hass = Mock()
hass.data = {UNIFI_CONFIG: {}}
hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()}
entry = Mock()
entry.data = ENTRY_CONFIG
entry.options = {}
Expand All @@ -133,6 +137,7 @@ async def test_controller_no_mac():
api.initialize.return_value = mock_coro(True)
api.clients = {"client1": client}
api.sites.return_value = mock_coro(CONTROLLER_SITES)
api.clients = {}

unifi_controller = controller.UniFiController(hass, entry)

Expand Down Expand Up @@ -195,13 +200,14 @@ async def test_reset_if_entry_had_wrong_auth():
async def test_reset_unloads_entry_if_setup():
"""Calling reset when the entry has been setup."""
hass = Mock()
hass.data = {UNIFI_CONFIG: {}}
hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()}
entry = Mock()
entry.data = ENTRY_CONFIG
entry.options = {}
api = Mock()
api.initialize.return_value = mock_coro(True)
api.sites.return_value = mock_coro(CONTROLLER_SITES)
api.clients = []

unifi_controller = controller.UniFiController(hass, entry)

Expand Down
Loading