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.
|
# we will add it to the ignore list since its garbage data.
|
||||||
MAX_IDS = 10
|
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_ADDRESSES = "ignore_addresses"
|
||||||
|
CONF_IGNORE_UUIDS = "ignore_uuids"
|
||||||
|
|
|
@ -23,8 +23,10 @@ from homeassistant.helpers.event import async_track_time_interval
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_IGNORE_ADDRESSES,
|
CONF_IGNORE_ADDRESSES,
|
||||||
|
CONF_IGNORE_UUIDS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
MAX_IDS,
|
MAX_IDS,
|
||||||
|
MAX_IDS_PER_UUID,
|
||||||
SIGNAL_IBEACON_DEVICE_NEW,
|
SIGNAL_IBEACON_DEVICE_NEW,
|
||||||
SIGNAL_IBEACON_DEVICE_SEEN,
|
SIGNAL_IBEACON_DEVICE_SEEN,
|
||||||
SIGNAL_IBEACON_DEVICE_UNAVAILABLE,
|
SIGNAL_IBEACON_DEVICE_UNAVAILABLE,
|
||||||
|
@ -115,6 +117,9 @@ class IBeaconCoordinator:
|
||||||
self._ignore_addresses: set[str] = set(
|
self._ignore_addresses: set[str] = set(
|
||||||
entry.data.get(CONF_IGNORE_ADDRESSES, [])
|
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
|
# iBeacons with fixed MAC addresses
|
||||||
self._last_ibeacon_advertisement_by_unique_id: dict[
|
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._last_seen_by_group_id: dict[str, bluetooth.BluetoothServiceInfoBleak] = {}
|
||||||
self._unavailable_group_ids: set[str] = set()
|
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
|
@callback
|
||||||
def _async_handle_unavailable(
|
def _async_handle_unavailable(
|
||||||
self, service_info: bluetooth.BluetoothServiceInfoBleak
|
self, service_info: bluetooth.BluetoothServiceInfoBleak
|
||||||
|
@ -146,6 +154,25 @@ class IBeaconCoordinator:
|
||||||
"""Cancel unavailable tracking for an address."""
|
"""Cancel unavailable tracking for an address."""
|
||||||
self._unavailable_trackers.pop(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
|
@callback
|
||||||
def _async_ignore_address(self, address: str) -> None:
|
def _async_ignore_address(self, address: str) -> None:
|
||||||
"""Ignore an address that does not follow the spec and any entities created by it."""
|
"""Ignore an address that does not follow the spec and any entities created by it."""
|
||||||
|
@ -203,7 +230,20 @@ class IBeaconCoordinator:
|
||||||
return
|
return
|
||||||
if not (ibeacon_advertisement := parse(service_info)):
|
if not (ibeacon_advertisement := parse(service_info)):
|
||||||
return
|
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:
|
if group_id in self._group_ids_random_macs:
|
||||||
self._async_update_ibeacon_with_random_mac(
|
self._async_update_ibeacon_with_random_mac(
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ibeacon",
|
"documentation": "https://www.home-assistant.io/integrations/ibeacon",
|
||||||
"dependencies": ["bluetooth"],
|
"dependencies": ["bluetooth"],
|
||||||
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [2, 21] }],
|
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [2, 21] }],
|
||||||
"requirements": ["ibeacon_ble==0.7.1"],
|
"requirements": ["ibeacon_ble==0.7.2"],
|
||||||
"codeowners": ["@bdraco"],
|
"codeowners": ["@bdraco"],
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["bleak"],
|
"loggers": ["bleak"],
|
||||||
|
|
|
@ -901,7 +901,7 @@ iammeter==0.1.7
|
||||||
iaqualink==0.4.1
|
iaqualink==0.4.1
|
||||||
|
|
||||||
# homeassistant.components.ibeacon
|
# homeassistant.components.ibeacon
|
||||||
ibeacon_ble==0.7.1
|
ibeacon_ble==0.7.2
|
||||||
|
|
||||||
# homeassistant.components.watson_tts
|
# homeassistant.components.watson_tts
|
||||||
ibm-watson==5.2.2
|
ibm-watson==5.2.2
|
||||||
|
|
|
@ -672,7 +672,7 @@ hyperion-py==0.7.5
|
||||||
iaqualink==0.4.1
|
iaqualink==0.4.1
|
||||||
|
|
||||||
# homeassistant.components.ibeacon
|
# homeassistant.components.ibeacon
|
||||||
ibeacon_ble==0.7.1
|
ibeacon_ble==0.7.2
|
||||||
|
|
||||||
# homeassistant.components.ping
|
# homeassistant.components.ping
|
||||||
icmplib==3.0
|
icmplib==3.0
|
||||||
|
|
|
@ -127,3 +127,71 @@ async def test_ignore_default_name(hass):
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(hass.states.async_entity_ids()) == before_entity_count
|
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