Remove iBeacon devices that rotate their major,minor and mac (#79338)
parent
ca0cd19dc9
commit
6694d06b37
|
@ -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"
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue