forked from platypush/platypush
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.
This commit is contained in:
parent
01d323fad0
commit
99cfd247a5
1 changed files with 79 additions and 9 deletions
|
@ -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
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue