Remove iBeacon devices that rotate their major,minor and mac (#79338)

pull/79376/head
J. Nick Koston 2022-09-30 02:46:45 -10:00 committed by GitHub
parent ca0cd19dc9
commit 6694d06b37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 117 additions and 4 deletions

View File

@ -27,4 +27,9 @@ UPDATE_INTERVAL = timedelta(seconds=60)
# we will add it to the ignore list since its garbage data.
MAX_IDS = 10
# If a device broadcasts this many major minors for the same uuid
# we will add it to the ignore list since its garbage data.
MAX_IDS_PER_UUID = 50
CONF_IGNORE_ADDRESSES = "ignore_addresses"
CONF_IGNORE_UUIDS = "ignore_uuids"

View File

@ -23,8 +23,10 @@ from homeassistant.helpers.event import async_track_time_interval
from .const import (
CONF_IGNORE_ADDRESSES,
CONF_IGNORE_UUIDS,
DOMAIN,
MAX_IDS,
MAX_IDS_PER_UUID,
SIGNAL_IBEACON_DEVICE_NEW,
SIGNAL_IBEACON_DEVICE_SEEN,
SIGNAL_IBEACON_DEVICE_UNAVAILABLE,
@ -115,6 +117,9 @@ class IBeaconCoordinator:
self._ignore_addresses: set[str] = set(
entry.data.get(CONF_IGNORE_ADDRESSES, [])
)
# iBeacon devices that do not follow the spec
# and broadcast custom data in the major and minor fields
self._ignore_uuids: set[str] = set(entry.data.get(CONF_IGNORE_UUIDS, []))
# iBeacons with fixed MAC addresses
self._last_ibeacon_advertisement_by_unique_id: dict[
@ -131,6 +136,9 @@ class IBeaconCoordinator:
self._last_seen_by_group_id: dict[str, bluetooth.BluetoothServiceInfoBleak] = {}
self._unavailable_group_ids: set[str] = set()
# iBeacons with random MAC addresses, fixed UUID, random major/minor
self._major_minor_by_uuid: dict[str, set[tuple[int, int]]] = {}
@callback
def _async_handle_unavailable(
self, service_info: bluetooth.BluetoothServiceInfoBleak
@ -146,6 +154,25 @@ class IBeaconCoordinator:
"""Cancel unavailable tracking for an address."""
self._unavailable_trackers.pop(address)()
@callback
def _async_ignore_uuid(self, uuid: str) -> None:
"""Ignore an UUID that does not follow the spec and any entities created by it."""
self._ignore_uuids.add(uuid)
major_minor_by_uuid = self._major_minor_by_uuid.pop(uuid)
unique_ids_to_purge = set()
for major, minor in major_minor_by_uuid:
group_id = f"{uuid}_{major}_{minor}"
if unique_ids := self._unique_ids_by_group_id.pop(group_id, None):
unique_ids_to_purge.update(unique_ids)
for address in self._addresses_by_group_id.pop(group_id, []):
self._async_cancel_unavailable_tracker(address)
self._unique_ids_by_address.pop(address)
self._group_ids_by_address.pop(address)
self._async_purge_untrackable_entities(unique_ids_to_purge)
entry_data = self._entry.data
new_data = entry_data | {CONF_IGNORE_UUIDS: list(self._ignore_uuids)}
self.hass.config_entries.async_update_entry(self._entry, data=new_data)
@callback
def _async_ignore_address(self, address: str) -> None:
"""Ignore an address that does not follow the spec and any entities created by it."""
@ -203,7 +230,20 @@ class IBeaconCoordinator:
return
if not (ibeacon_advertisement := parse(service_info)):
return
group_id = f"{ibeacon_advertisement.uuid}_{ibeacon_advertisement.major}_{ibeacon_advertisement.minor}"
uuid_str = str(ibeacon_advertisement.uuid)
if uuid_str in self._ignore_uuids:
return
major = ibeacon_advertisement.major
minor = ibeacon_advertisement.minor
major_minor_by_uuid = self._major_minor_by_uuid.setdefault(uuid_str, set())
if len(major_minor_by_uuid) + 1 > MAX_IDS_PER_UUID:
self._async_ignore_uuid(uuid_str)
return
major_minor_by_uuid.add((major, minor))
group_id = f"{uuid_str}_{major}_{minor}"
if group_id in self._group_ids_random_macs:
self._async_update_ibeacon_with_random_mac(

View File

@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/ibeacon",
"dependencies": ["bluetooth"],
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [2, 21] }],
"requirements": ["ibeacon_ble==0.7.1"],
"requirements": ["ibeacon_ble==0.7.2"],
"codeowners": ["@bdraco"],
"iot_class": "local_push",
"loggers": ["bleak"],

View File

@ -901,7 +901,7 @@ iammeter==0.1.7
iaqualink==0.4.1
# homeassistant.components.ibeacon
ibeacon_ble==0.7.1
ibeacon_ble==0.7.2
# homeassistant.components.watson_tts
ibm-watson==5.2.2

View File

@ -672,7 +672,7 @@ hyperion-py==0.7.5
iaqualink==0.4.1
# homeassistant.components.ibeacon
ibeacon_ble==0.7.1
ibeacon_ble==0.7.2
# homeassistant.components.ping
icmplib==3.0

View File

@ -127,3 +127,71 @@ async def test_ignore_default_name(hass):
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids()) == before_entity_count
async def test_rotating_major_minor_and_mac(hass):
"""Test the different uuid, major, minor from many addresses removes all associated entities."""
entry = MockConfigEntry(
domain=DOMAIN,
)
entry.add_to_hass(hass)
before_entity_count = len(hass.states.async_entity_ids("device_tracker"))
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
for i in range(100):
service_info = BluetoothServiceInfo(
name="BlueCharm_177999",
address=f"AA:BB:CC:DD:EE:{i:02X}",
rssi=-63,
service_data={},
manufacturer_data={
76: b"\x02\x15BlueCharmBeacons"
+ bytearray([i])
+ b"\xfe"
+ bytearray([i])
+ b"U\xc5"
},
service_uuids=[],
source="local",
)
inject_bluetooth_service_info(hass, service_info)
await hass.async_block_till_done()
await hass.async_block_till_done()
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids("device_tracker")) == before_entity_count
async def test_rotating_major_minor_and_mac_no_name(hass):
"""Test no-name devices with different uuid, major, minor from many addresses removes all associated entities."""
entry = MockConfigEntry(
domain=DOMAIN,
)
entry.add_to_hass(hass)
before_entity_count = len(hass.states.async_entity_ids("device_tracker"))
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
for i in range(51):
service_info = BluetoothServiceInfo(
name=f"AA:BB:CC:DD:EE:{i:02X}",
address=f"AA:BB:CC:DD:EE:{i:02X}",
rssi=-63,
service_data={},
manufacturer_data={
76: b"\x02\x15BlueCharmBeacons"
+ bytearray([i])
+ b"\xfe"
+ bytearray([i])
+ b"U\xc5"
},
service_uuids=[],
source="local",
)
inject_bluetooth_service_info(hass, service_info)
await hass.async_block_till_done()
await hass.async_block_till_done()
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids("device_tracker")) == before_entity_count