From 99cfd247a5136bd544672d6e420fc68ea1ee2702 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 22 Mar 2023 15:35:02 +0100 Subject: [PATCH] A more effective logic to exclude noisy BLE beacons. This includes BLE beacons sent from all Google/Apple/Microsoft/Samsung beacon networks in all of their variants. --- .../plugins/bluetooth/_ble/_event_handler.py | 88 +++++++++++++++++-- 1 file changed, 79 insertions(+), 9 deletions(-) diff --git a/platypush/plugins/bluetooth/_ble/_event_handler.py b/platypush/plugins/bluetooth/_ble/_event_handler.py index b4c20d390a..00af5d10f7 100644 --- a/platypush/plugins/bluetooth/_ble/_event_handler.py +++ b/platypush/plugins/bluetooth/_ble/_event_handler.py @@ -1,11 +1,14 @@ from datetime import datetime, timedelta +from logging import getLogger from queue import Queue -from typing import Callable, Dict, Final, List, Optional, Type +from typing import Callable, Collection, Dict, Final, List, Optional, Type +from uuid import UUID from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData -from platypush.entities.bluetooth import BluetoothDevice +from platypush.context import get_bus +from platypush.entities.bluetooth import BluetoothDevice, BluetoothService from platypush.message.event.bluetooth import ( BluetoothDeviceConnectedEvent, BluetoothDeviceDisconnectedEvent, @@ -14,18 +17,35 @@ from platypush.message.event.bluetooth import ( BluetoothDeviceEvent, ) -from platypush.context import get_bus - from .._cache import EntityCache +from .._model import ServiceClass from ._cache import DeviceCache from ._mappers import device_to_entity _rssi_update_interval: Final[float] = 30.0 """ How often to trigger RSSI update events for a device. """ +_excluded_manufacturers: Final[Collection[str]] = { + 'Apple, Inc.', + 'GAEN', + 'Google', + 'Google Inc.', + 'Microsoft', + 'Samsung Electronics Co., Ltd', +} +""" +Exclude beacons from these device manufacturers by default (main offenders +when it comes to Bluetooth device space pollution). +""" + +logger = getLogger(__name__) + def _has_changed( - old: Optional[BluetoothDevice], new: BluetoothDevice, attr: str + old: Optional[BluetoothDevice], + new: BluetoothDevice, + attr: str, + tolerance: Optional[float] = None, ) -> bool: """ Returns True if the given attribute has changed on the device. @@ -35,6 +55,8 @@ def _has_changed( old_value = getattr(old, attr) new_value = getattr(new, attr) + if tolerance and (old_value is not None and new_value is not None): + return abs(new_value - old_value) < tolerance return old_value != new_value @@ -60,13 +82,17 @@ event_matchers: Dict[ old, new, 'connected', True ), BluetoothDeviceDisconnectedEvent: lambda old, new: old is not None - and old.connected + and bool(old.connected) and _has_been_set(old, new, 'connected', False), BluetoothDeviceFoundEvent: lambda old, new: old is None or (old.reachable is False and new.reachable is True), BluetoothDeviceSignalUpdateEvent: lambda old, new: ( (new.rssi is not None or new.tx_power is not None) - and (_has_changed(old, new, 'rssi') or _has_changed(old, new, 'tx_power')) + and (old and old.rssi) + and ( + _has_changed(old, new, 'rssi', tolerance=5) + or _has_changed(old, new, 'tx_power', tolerance=5) + ) and ( not (old and old.updated_at) or datetime.utcnow() - old.updated_at @@ -88,15 +114,18 @@ class EventHandler: device_queue: Queue, device_cache: DeviceCache, entity_cache: EntityCache, + exclude_known_noisy_beacons: bool, ): """ :param device_queue: Queue used to publish updated devices upstream. :param device_cache: Device cache. :param entity_cache: Entity cache. + :param exclude_known_noisy_beacons: Exclude known noisy beacons. """ self._device_queue = device_queue self._device_cache = device_cache self._entity_cache = entity_cache + self._exclude_known_noisy_beacons = exclude_known_noisy_beacons def __call__(self, device: BLEDevice, data: AdvertisementData): """ @@ -117,10 +146,16 @@ class EventHandler: :param data: The advertised data. """ + new_entity = device_to_entity(device, data) + if self._exclude_known_noisy_beacons and self._is_noisy_beacon(new_entity): + logger.info( + 'exclude_known_noisy_beacons is set to True: skipping beacon from device %s', + device.address, + ) + return + events: List[BluetoothDeviceEvent] = [] existing_entity = self._entity_cache.get(device.address) - new_entity = device_to_entity(device, data) - events += [ event_type.from_device(new_entity) for event_type, matcher in event_matchers.items() @@ -133,3 +168,38 @@ class EventHandler: if events: self._device_queue.put_nowait(new_entity) + + @staticmethod + def _is_noisy_beacon(device: BluetoothDevice) -> bool: + """ + Check if the beacon received from the given device should be skipped. + """ + # "Noisy" beacon devices usually have no associated friendly name. If a + # device has a valid name, we should probably include it. + if ( + device.name + and device.name.replace('-', ':').lower() != device.address.lower() + ): + return False + + # If the manufacturer is in the excluded list, we should skip it + if device.manufacturer in _excluded_manufacturers: + return True + + # If the device has any children other than services, don't skip it + if any(not isinstance(child, BluetoothService) for child in device.children): + return False + + mapped_uuids = [ + int(str(srv.uuid).split('-')[0], 16) & 0xFFFF + if isinstance(srv.uuid, UUID) + else srv.uuid + for srv in device.services + ] + + # If any of the services matches the blacklisted manufacturers, skip + # the event. + return any( + str(ServiceClass.get(uuid)) in _excluded_manufacturers + for uuid in mapped_uuids + )