Fix iBeacons with infrequent random mac address changes unexpectedly going unavailable (#82668)

fixes https://github.com/home-assistant/core/issues/79781
pull/82680/head
J. Nick Koston 2022-11-24 15:20:19 -07:00 committed by GitHub
parent 47cec8da8e
commit 09c3df7eb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 135 additions and 2 deletions

View File

@ -354,7 +354,25 @@ class IBeaconCoordinator:
for group_id in self._group_ids_random_macs
if group_id not in self._unavailable_group_ids
and (service_info := self._last_seen_by_group_id.get(group_id))
and now - service_info.time > UNAVAILABLE_TIMEOUT
and (
# We will not be callbacks for iBeacons with random macs
# that rotate infrequently since their advertisement data is
# does not change as the bluetooth.async_register_callback API
# suppresses callbacks for duplicate advertisements to avoid
# exposing integrations to the firehose of bluetooth advertisements.
#
# To solve this we need to ask for the latest service info for
# the address we last saw to get the latest timestamp.
#
# If there is no last service info for the address we know that
# the device is no longer advertising.
not (
latest_service_info := bluetooth.async_last_service_info(
self.hass, service_info.address, connectable=False
)
)
or now - latest_service_info.time > UNAVAILABLE_TIMEOUT
)
]
for group_id in gone_unavailable:
self._unavailable_group_ids.add(group_id)

View File

@ -7,8 +7,17 @@ from unittest.mock import patch
import pytest
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_ble_device_from_address,
async_last_service_info,
)
from homeassistant.components.bluetooth.const import UNAVAILABLE_TRACK_SECONDS
from homeassistant.components.ibeacon.const import DOMAIN, UNAVAILABLE_TIMEOUT
from homeassistant.components.ibeacon.const import (
DOMAIN,
UNAVAILABLE_TIMEOUT,
UPDATE_INTERVAL,
)
from homeassistant.const import (
ATTR_FRIENDLY_NAME,
STATE_HOME,
@ -27,6 +36,7 @@ from . import (
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.components.bluetooth import (
inject_bluetooth_service_info,
inject_bluetooth_service_info_bleak,
patch_all_discovered_devices,
)
@ -130,3 +140,108 @@ async def test_device_tracker_random_address(hass):
tracker_attributes = tracker.attributes
assert tracker.state == STATE_HOME
assert tracker_attributes[ATTR_FRIENDLY_NAME] == "RandomAddress_1234"
async def test_device_tracker_random_address_infrequent_changes(hass):
"""Test creating and updating device_tracker with a random mac that only changes once per day."""
entry = MockConfigEntry(
domain=DOMAIN,
)
entry.add_to_hass(hass)
start_time = time.monotonic()
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
for i in range(20):
inject_bluetooth_service_info(
hass,
replace(
BEACON_RANDOM_ADDRESS_SERVICE_INFO, address=f"AA:BB:CC:DD:EE:{i:02X}"
),
)
await hass.async_block_till_done()
tracker = hass.states.get("device_tracker.randomaddress_1234")
tracker_attributes = tracker.attributes
assert tracker.state == STATE_HOME
assert tracker_attributes[ATTR_FRIENDLY_NAME] == "RandomAddress_1234"
await hass.async_block_till_done()
with patch_all_discovered_devices([]), patch(
"homeassistant.components.ibeacon.coordinator.MONOTONIC_TIME",
return_value=start_time + UNAVAILABLE_TIMEOUT + 1,
):
async_fire_time_changed(
hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TIMEOUT)
)
await hass.async_block_till_done()
tracker = hass.states.get("device_tracker.randomaddress_1234")
assert tracker.state == STATE_NOT_HOME
inject_bluetooth_service_info(
hass, replace(BEACON_RANDOM_ADDRESS_SERVICE_INFO, address="AA:BB:CC:DD:EE:14")
)
await hass.async_block_till_done()
tracker = hass.states.get("device_tracker.randomaddress_1234")
tracker_attributes = tracker.attributes
assert tracker.state == STATE_HOME
assert tracker_attributes[ATTR_FRIENDLY_NAME] == "RandomAddress_1234"
inject_bluetooth_service_info(
hass, replace(BEACON_RANDOM_ADDRESS_SERVICE_INFO, address="AA:BB:CC:DD:EE:14")
)
device = async_ble_device_from_address(hass, "AA:BB:CC:DD:EE:14", False)
with patch_all_discovered_devices([device]), patch(
"homeassistant.components.ibeacon.coordinator.MONOTONIC_TIME",
return_value=start_time + UPDATE_INTERVAL.total_seconds() + 1,
):
async_fire_time_changed(hass, dt_util.utcnow() + UPDATE_INTERVAL)
await hass.async_block_till_done()
tracker = hass.states.get("device_tracker.randomaddress_1234")
tracker_attributes = tracker.attributes
assert tracker.state == STATE_HOME
assert tracker_attributes[ATTR_FRIENDLY_NAME] == "RandomAddress_1234"
one_day_future = start_time + 86400
previous_service_info = async_last_service_info(
hass, "AA:BB:CC:DD:EE:14", connectable=False
)
inject_bluetooth_service_info_bleak(
hass,
BluetoothServiceInfoBleak(
name="RandomAddress_1234",
address="AA:BB:CC:DD:EE:14",
rssi=-63,
service_data={},
manufacturer_data={76: b"\x02\x15RandCharmBeacons\x0e\xfe\x13U\xc5"},
service_uuids=[],
source="local",
time=one_day_future,
connectable=False,
device=device,
advertisement=previous_service_info.advertisement,
),
)
device = async_ble_device_from_address(hass, "AA:BB:CC:DD:EE:14", False)
assert (
async_last_service_info(hass, "AA:BB:CC:DD:EE:14", connectable=False).time
== one_day_future
)
with patch_all_discovered_devices([device]), patch(
"homeassistant.components.ibeacon.coordinator.MONOTONIC_TIME",
return_value=start_time + UNAVAILABLE_TIMEOUT + 1,
):
async_fire_time_changed(
hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TIMEOUT + 1)
)
await hass.async_block_till_done()
tracker = hass.states.get("device_tracker.randomaddress_1234")
tracker_attributes = tracker.attributes
assert tracker.state == STATE_HOME
assert tracker_attributes[ATTR_FRIENDLY_NAME] == "RandomAddress_1234"