diff --git a/homeassistant/components/unifi/.translations/en.json b/homeassistant/components/unifi/.translations/en.json index d42a647c82fe2c..ac43e5dcb31873 100644 --- a/homeassistant/components/unifi/.translations/en.json +++ b/homeassistant/components/unifi/.translations/en.json @@ -41,6 +41,7 @@ "device_tracker": { "data": { "detection_time": "Time in seconds from last seen until considered away", + "ignore_wired_bug": "Disable UniFi wired bug logic", "ssid_filter": "Select SSIDs to track wireless clients on", "track_clients": "Track network clients", "track_devices": "Track network devices (Ubiquiti devices)", diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index e9f534360d7257..e3225a2d210992 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -7,7 +7,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .config_flow import get_controller_id_from_config_entry -from .const import ATTR_MANUFACTURER, DOMAIN, UNIFI_WIRELESS_CLIENTS +from .const import ATTR_MANUFACTURER, DOMAIN, LOGGER, UNIFI_WIRELESS_CLIENTS from .controller import UniFiController SAVE_DELAY = 10 @@ -42,6 +42,8 @@ async def async_setup_entry(hass, config_entry): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, controller.shutdown) + LOGGER.debug("UniFi config options %s", config_entry.options) + if controller.mac is None: return True diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 781fcdeae828e8..78389c6a2d1b74 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -19,6 +19,7 @@ CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, + CONF_IGNORE_WIRED_BUG, CONF_POE_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, @@ -216,6 +217,10 @@ async def async_step_device_tracker(self, user_input=None): self.controller.option_detection_time.total_seconds() ), ): int, + vol.Optional( + CONF_IGNORE_WIRED_BUG, + default=self.controller.option_ignore_wired_bug, + ): bool, } ), ) diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index 4acf75fbdd9f56..803a892647fe0e 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -14,6 +14,7 @@ CONF_ALLOW_BANDWIDTH_SENSORS = "allow_bandwidth_sensors" CONF_BLOCK_CLIENT = "block_client" CONF_DETECTION_TIME = "detection_time" +CONF_IGNORE_WIRED_BUG = "ignore_wired_bug" CONF_POE_CLIENTS = "poe_clients" CONF_TRACK_CLIENTS = "track_clients" CONF_TRACK_DEVICES = "track_devices" @@ -21,6 +22,7 @@ CONF_SSID_FILTER = "ssid_filter" DEFAULT_ALLOW_BANDWIDTH_SENSORS = False +DEFAULT_IGNORE_WIRED_BUG = False DEFAULT_POE_CLIENTS = True DEFAULT_TRACK_CLIENTS = True DEFAULT_TRACK_DEVICES = True diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 864d131d287f31..a9c8827be6923c 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -24,6 +24,7 @@ CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, + CONF_IGNORE_WIRED_BUG, CONF_POE_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, @@ -33,6 +34,7 @@ CONTROLLER_ID, DEFAULT_ALLOW_BANDWIDTH_SENSORS, DEFAULT_DETECTION_TIME, + DEFAULT_IGNORE_WIRED_BUG, DEFAULT_POE_CLIENTS, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, @@ -89,32 +91,20 @@ def site_role(self): return self._site_role @property - def option_allow_bandwidth_sensors(self): - """Config entry option to allow bandwidth sensors.""" - return self.config_entry.options.get( - CONF_ALLOW_BANDWIDTH_SENSORS, DEFAULT_ALLOW_BANDWIDTH_SENSORS - ) - - @property - def option_block_clients(self): - """Config entry option with list of clients to control network access.""" - return self.config_entry.options.get(CONF_BLOCK_CLIENT, []) + def mac(self): + """Return the mac address of this controller.""" + for client in self.api.clients.values(): + if self.host == client.ip: + return client.mac + return None - @property - def option_poe_clients(self): - """Config entry option to control poe clients.""" - return self.config_entry.options.get(CONF_POE_CLIENTS, DEFAULT_POE_CLIENTS) + # Device tracker options @property def option_track_clients(self): """Config entry option to not track clients.""" return self.config_entry.options.get(CONF_TRACK_CLIENTS, DEFAULT_TRACK_CLIENTS) - @property - def option_track_devices(self): - """Config entry option to not track devices.""" - return self.config_entry.options.get(CONF_TRACK_DEVICES, DEFAULT_TRACK_DEVICES) - @property def option_track_wired_clients(self): """Config entry option to not track wired clients.""" @@ -122,6 +112,16 @@ def option_track_wired_clients(self): CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS ) + @property + def option_track_devices(self): + """Config entry option to not track devices.""" + return self.config_entry.options.get(CONF_TRACK_DEVICES, DEFAULT_TRACK_DEVICES) + + @property + def option_ssid_filter(self): + """Config entry option listing what SSIDs are being used to track clients.""" + return self.config_entry.options.get(CONF_SSID_FILTER, []) + @property def option_detection_time(self): """Config entry option defining number of seconds from last seen to away.""" @@ -132,17 +132,32 @@ def option_detection_time(self): ) @property - def option_ssid_filter(self): - """Config entry option listing what SSIDs are being used to track clients.""" - return self.config_entry.options.get(CONF_SSID_FILTER, []) + def option_ignore_wired_bug(self): + """Config entry option to ignore wired bug.""" + return self.config_entry.options.get( + CONF_IGNORE_WIRED_BUG, DEFAULT_IGNORE_WIRED_BUG + ) + + # Client control options @property - def mac(self): - """Return the mac address of this controller.""" - for client in self.api.clients.values(): - if self.host == client.ip: - return client.mac - return None + def option_poe_clients(self): + """Config entry option to control poe clients.""" + return self.config_entry.options.get(CONF_POE_CLIENTS, DEFAULT_POE_CLIENTS) + + @property + def option_block_clients(self): + """Config entry option with list of clients to control network access.""" + return self.config_entry.options.get(CONF_BLOCK_CLIENT, []) + + # Statistics sensor options + + @property + def option_allow_bandwidth_sensors(self): + """Config entry option to allow bandwidth sensors.""" + return self.config_entry.options.get( + CONF_ALLOW_BANDWIDTH_SENSORS, DEFAULT_ALLOW_BANDWIDTH_SENSORS + ) @callback def async_unifi_signalling_callback(self, signal, data): diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index c5aa74706a1cbd..d0e30039bd3db0 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -67,6 +67,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): client = controller.api.clients_all[mac] controller.api.clients.process_raw([client.raw]) + LOGGER.debug( + "Restore disconnected client %s (%s)", entity.entity_id, client.mac, + ) @callback def update_controller(): @@ -121,20 +124,21 @@ def options_updated(): remove.add(mac) if option_ssid_filter != controller.option_ssid_filter: - option_ssid_filter = controller.option_ssid_filter update = True - for mac, entity in tracked.items(): - if ( - isinstance(entity, UniFiClientTracker) - and not entity.is_wired - and entity.client.essid not in option_ssid_filter - ): - remove.add(mac) + if controller.option_ssid_filter: + for mac, entity in tracked.items(): + if ( + isinstance(entity, UniFiClientTracker) + and not entity.is_wired + and entity.client.essid not in controller.option_ssid_filter + ): + remove.add(mac) option_track_clients = controller.option_track_clients option_track_devices = controller.option_track_devices option_track_wired_clients = controller.option_track_wired_clients + option_ssid_filter = controller.option_ssid_filter for mac in remove: entity = tracked.pop(mac) @@ -304,13 +308,12 @@ def __init__(self, device, controller): """Set up tracked device.""" self.device = device self.controller = controller - self.listeners = [] async def async_added_to_hass(self): """Subscribe to device events.""" - LOGGER.debug("New UniFi device tracker %s (%s)", self.name, self.device.mac) + LOGGER.debug("New device %s (%s)", self.entity_id, self.device.mac) self.device.register_callback(self.async_update_callback) - self.listeners.append( + self.async_on_remove( async_dispatcher_connect( self.hass, self.controller.signal_reachable, self.async_update_callback ) @@ -319,13 +322,11 @@ async def async_added_to_hass(self): async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" self.device.remove_callback(self.async_update_callback) - for unsub_dispatcher in self.listeners: - unsub_dispatcher() @callback def async_update_callback(self): """Update the sensor's state.""" - LOGGER.debug("Updating UniFi tracked device %s", self.entity_id) + LOGGER.debug("Updating device %s (%s)", self.entity_id, self.device.mac) self.async_write_ha_state() diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 2e82ecb4f6f849..255e95f401189a 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -98,7 +98,7 @@ class UniFiRxBandwidthSensor(UniFiClient): @property def state(self): """Return the state of the sensor.""" - if self.is_wired: + if self._is_wired: return self.client.wired_rx_bytes / 1000000 return self.client.raw.get("rx_bytes", 0) / 1000000 @@ -125,7 +125,7 @@ class UniFiTxBandwidthSensor(UniFiRxBandwidthSensor): @property def state(self): """Return the state of the sensor.""" - if self.is_wired: + if self._is_wired: return self.client.wired_tx_bytes / 1000000 return self.client.raw.get("tx_bytes", 0) / 1000000 diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index 881e97bc9cad72..0bbcb1bbca182c 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -32,6 +32,7 @@ "device_tracker": { "data": { "detection_time": "Time in seconds from last seen until considered away", + "ignore_wired_bug": "Disable UniFi wired bug logic", "ssid_filter": "Select SSIDs to track wireless clients on", "track_clients": "Track network clients", "track_devices": "Track network devices (Ubiquiti devices)", @@ -61,4 +62,4 @@ "error": { "unknown_client_mac": "No client available in UniFi on that MAC address" } -} +} \ No newline at end of file diff --git a/homeassistant/components/unifi/unifi_client.py b/homeassistant/components/unifi/unifi_client.py index 644b0856bb49dd..a30dc21854df8c 100644 --- a/homeassistant/components/unifi/unifi_client.py +++ b/homeassistant/components/unifi/unifi_client.py @@ -39,18 +39,17 @@ def __init__(self, client, controller) -> None: """Set up client.""" self.client = client self.controller = controller - self.listeners = [] - self.is_wired = self.client.mac not in controller.wireless_clients + self._is_wired = self.client.mac not in controller.wireless_clients self.is_blocked = self.client.blocked self.wired_connection = None self.wireless_connection = None async def async_added_to_hass(self) -> None: """Client entity created.""" - LOGGER.debug("New UniFi client %s (%s)", self.name, self.client.mac) + LOGGER.debug("New client %s (%s)", self.entity_id, self.client.mac) self.client.register_callback(self.async_update_callback) - self.listeners.append( + self.async_on_remove( async_dispatcher_connect( self.hass, self.controller.signal_reachable, self.async_update_callback ) @@ -59,17 +58,14 @@ async def async_added_to_hass(self) -> None: async def async_will_remove_from_hass(self) -> None: """Disconnect client object when removed.""" self.client.remove_callback(self.async_update_callback) - for unsub_dispatcher in self.listeners: - unsub_dispatcher() @callback def async_update_callback(self) -> None: """Update the clients state.""" - if self.is_wired and self.client.mac in self.controller.wireless_clients: - self.is_wired = False + if self._is_wired and self.client.mac in self.controller.wireless_clients: + self._is_wired = False if self.client.last_updated == SOURCE_EVENT: - if self.client.event.event in WIRELESS_CLIENT: self.wireless_connection = self.client.event.event in ( WIRELESS_CLIENT_CONNECTED, @@ -84,9 +80,19 @@ def async_update_callback(self) -> None: elif self.client.event.event in CLIENT_BLOCKED + CLIENT_UNBLOCKED: self.is_blocked = self.client.event.event in CLIENT_BLOCKED - LOGGER.debug("Updating client %s %s", self.entity_id, self.client.mac) + LOGGER.debug("Updating client %s (%s)", self.entity_id, self.client.mac) self.async_write_ha_state() + @property + def is_wired(self): + """Return if the client is wired. + + Allows disabling logic to keep track of clients affected by UniFi wired bug marking wireless devices as wired. This is useful when running a network not only containing UniFi APs. + """ + if self.controller.option_ignore_wired_bug: + return self.client.is_wired + return self._is_wired + @property def name(self) -> str: """Return the name of the client.""" diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 3366ec1641d101..09b16440f94769 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -11,6 +11,7 @@ CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, + CONF_IGNORE_WIRED_BUG, CONF_POE_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, @@ -338,9 +339,10 @@ async def test_option_flow(hass): CONF_TRACK_CLIENTS: False, CONF_TRACK_WIRED_CLIENTS: False, CONF_TRACK_DEVICES: False, - CONF_DETECTION_TIME: 100, CONF_SSID_FILTER: ["SSID 1"], - CONF_BLOCK_CLIENT: ["00:00:00:00:00:01"], + CONF_DETECTION_TIME: 100, + CONF_IGNORE_WIRED_BUG: False, CONF_POE_CLIENTS: False, + CONF_BLOCK_CLIENT: ["00:00:00:00:00:01"], CONF_ALLOW_BANDWIDTH_SENSORS: True, } diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index cfb4637a6c4fda..e207cd51763a27 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -11,6 +11,7 @@ import homeassistant.components.device_tracker as device_tracker from homeassistant.components.unifi.const import ( CONF_BLOCK_CLIENT, + CONF_IGNORE_WIRED_BUG, CONF_SSID_FILTER, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, @@ -347,12 +348,18 @@ async def test_option_track_devices(hass): async def test_option_ssid_filter(hass): """Test the SSID filter works.""" - controller = await setup_unifi_integration( - hass, options={CONF_SSID_FILTER: ["ssid"]}, clients_response=[CLIENT_3], + controller = await setup_unifi_integration(hass, clients_response=[CLIENT_3]) + assert len(hass.states.async_entity_ids("device_tracker")) == 1 + + client_3 = hass.states.get("device_tracker.client_3") + assert client_3 + + # Set SSID filter + hass.config_entries.async_update_entry( + controller.config_entry, options={CONF_SSID_FILTER: ["ssid"]}, ) - assert len(hass.states.async_entity_ids("device_tracker")) == 0 + await hass.async_block_till_done() - # SSID filter active client_3 = hass.states.get("device_tracker.client_3") assert not client_3 @@ -374,7 +381,6 @@ async def test_option_ssid_filter(hass): controller.api.message_handler(event) await hass.async_block_till_done() - # SSID no longer filtered client_3 = hass.states.get("device_tracker.client_3") assert client_3.state == "home" @@ -393,6 +399,7 @@ async def test_wireless_client_go_wired_issue(hass): client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None assert client_1.state == "home" + assert client_1.attributes["is_wired"] is False client_1_client["is_wired"] = True client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) @@ -402,6 +409,7 @@ async def test_wireless_client_go_wired_issue(hass): client_1 = hass.states.get("device_tracker.client_1") assert client_1.state == "home" + assert client_1.attributes["is_wired"] is False with patch.object( unifi.device_tracker.dt_util, @@ -414,6 +422,43 @@ async def test_wireless_client_go_wired_issue(hass): client_1 = hass.states.get("device_tracker.client_1") assert client_1.state == "not_home" + assert client_1.attributes["is_wired"] is False + + client_1_client["is_wired"] = False + client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) + event = {"meta": {"message": "sta:sync"}, "data": [client_1_client]} + controller.api.message_handler(event) + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "home" + assert client_1.attributes["is_wired"] is False + + +async def test_option_ignore_wired_bug(hass): + """Test option to ignore wired bug.""" + client_1_client = copy(CLIENT_1) + client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) + + controller = await setup_unifi_integration( + hass, options={CONF_IGNORE_WIRED_BUG: True}, clients_response=[client_1_client] + ) + assert len(hass.states.async_entity_ids("device_tracker")) == 1 + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 is not None + assert client_1.state == "home" + assert client_1.attributes["is_wired"] is False + + client_1_client["is_wired"] = True + client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) + event = {"meta": {"message": "sta:sync"}, "data": [client_1_client]} + controller.api.message_handler(event) + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "home" + assert client_1.attributes["is_wired"] is True client_1_client["is_wired"] = False client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) @@ -423,6 +468,7 @@ async def test_wireless_client_go_wired_issue(hass): client_1 = hass.states.get("device_tracker.client_1") assert client_1.state == "home" + assert client_1.attributes["is_wired"] is False async def test_restoring_client(hass):