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 datetime import datetime, timedelta
|
||||||
|
from logging import getLogger
|
||||||
from queue import Queue
|
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.device import BLEDevice
|
||||||
from bleak.backends.scanner import AdvertisementData
|
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 (
|
from platypush.message.event.bluetooth import (
|
||||||
BluetoothDeviceConnectedEvent,
|
BluetoothDeviceConnectedEvent,
|
||||||
BluetoothDeviceDisconnectedEvent,
|
BluetoothDeviceDisconnectedEvent,
|
||||||
|
@ -14,18 +17,35 @@ from platypush.message.event.bluetooth import (
|
||||||
BluetoothDeviceEvent,
|
BluetoothDeviceEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
from platypush.context import get_bus
|
|
||||||
|
|
||||||
from .._cache import EntityCache
|
from .._cache import EntityCache
|
||||||
|
from .._model import ServiceClass
|
||||||
from ._cache import DeviceCache
|
from ._cache import DeviceCache
|
||||||
from ._mappers import device_to_entity
|
from ._mappers import device_to_entity
|
||||||
|
|
||||||
_rssi_update_interval: Final[float] = 30.0
|
_rssi_update_interval: Final[float] = 30.0
|
||||||
""" How often to trigger RSSI update events for a device. """
|
""" 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(
|
def _has_changed(
|
||||||
old: Optional[BluetoothDevice], new: BluetoothDevice, attr: str
|
old: Optional[BluetoothDevice],
|
||||||
|
new: BluetoothDevice,
|
||||||
|
attr: str,
|
||||||
|
tolerance: Optional[float] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns True if the given attribute has changed on the device.
|
Returns True if the given attribute has changed on the device.
|
||||||
|
@ -35,6 +55,8 @@ def _has_changed(
|
||||||
|
|
||||||
old_value = getattr(old, attr)
|
old_value = getattr(old, attr)
|
||||||
new_value = getattr(new, 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
|
return old_value != new_value
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,13 +82,17 @@ event_matchers: Dict[
|
||||||
old, new, 'connected', True
|
old, new, 'connected', True
|
||||||
),
|
),
|
||||||
BluetoothDeviceDisconnectedEvent: lambda old, new: old is not None
|
BluetoothDeviceDisconnectedEvent: lambda old, new: old is not None
|
||||||
and old.connected
|
and bool(old.connected)
|
||||||
and _has_been_set(old, new, 'connected', False),
|
and _has_been_set(old, new, 'connected', False),
|
||||||
BluetoothDeviceFoundEvent: lambda old, new: old is None
|
BluetoothDeviceFoundEvent: lambda old, new: old is None
|
||||||
or (old.reachable is False and new.reachable is True),
|
or (old.reachable is False and new.reachable is True),
|
||||||
BluetoothDeviceSignalUpdateEvent: lambda old, new: (
|
BluetoothDeviceSignalUpdateEvent: lambda old, new: (
|
||||||
(new.rssi is not None or new.tx_power is not None)
|
(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 (
|
and (
|
||||||
not (old and old.updated_at)
|
not (old and old.updated_at)
|
||||||
or datetime.utcnow() - old.updated_at
|
or datetime.utcnow() - old.updated_at
|
||||||
|
@ -88,15 +114,18 @@ class EventHandler:
|
||||||
device_queue: Queue,
|
device_queue: Queue,
|
||||||
device_cache: DeviceCache,
|
device_cache: DeviceCache,
|
||||||
entity_cache: EntityCache,
|
entity_cache: EntityCache,
|
||||||
|
exclude_known_noisy_beacons: bool,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
:param device_queue: Queue used to publish updated devices upstream.
|
:param device_queue: Queue used to publish updated devices upstream.
|
||||||
:param device_cache: Device cache.
|
:param device_cache: Device cache.
|
||||||
:param entity_cache: Entity cache.
|
:param entity_cache: Entity cache.
|
||||||
|
:param exclude_known_noisy_beacons: Exclude known noisy beacons.
|
||||||
"""
|
"""
|
||||||
self._device_queue = device_queue
|
self._device_queue = device_queue
|
||||||
self._device_cache = device_cache
|
self._device_cache = device_cache
|
||||||
self._entity_cache = entity_cache
|
self._entity_cache = entity_cache
|
||||||
|
self._exclude_known_noisy_beacons = exclude_known_noisy_beacons
|
||||||
|
|
||||||
def __call__(self, device: BLEDevice, data: AdvertisementData):
|
def __call__(self, device: BLEDevice, data: AdvertisementData):
|
||||||
"""
|
"""
|
||||||
|
@ -117,10 +146,16 @@ class EventHandler:
|
||||||
:param data: The advertised data.
|
: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] = []
|
events: List[BluetoothDeviceEvent] = []
|
||||||
existing_entity = self._entity_cache.get(device.address)
|
existing_entity = self._entity_cache.get(device.address)
|
||||||
new_entity = device_to_entity(device, data)
|
|
||||||
|
|
||||||
events += [
|
events += [
|
||||||
event_type.from_device(new_entity)
|
event_type.from_device(new_entity)
|
||||||
for event_type, matcher in event_matchers.items()
|
for event_type, matcher in event_matchers.items()
|
||||||
|
@ -133,3 +168,38 @@ class EventHandler:
|
||||||
|
|
||||||
if events:
|
if events:
|
||||||
self._device_queue.put_nowait(new_entity)
|
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