Skip to content

Commit 94d015e

Browse files
authored
Fix Bluetooth discovery for devices with alternating advertisement names (home-assistant#154347)
1 parent f185ffd commit 94d015e

File tree

2 files changed

+71
-2
lines changed

2 files changed

+71
-2
lines changed

homeassistant/components/bluetooth/match.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,17 @@ class IntegrationMatchHistory:
6868
manufacturer_data: bool
6969
service_data: set[str]
7070
service_uuids: set[str]
71+
name: str
7172

7273

7374
def seen_all_fields(
74-
previous_match: IntegrationMatchHistory, advertisement_data: AdvertisementData
75+
previous_match: IntegrationMatchHistory,
76+
advertisement_data: AdvertisementData,
77+
name: str,
7578
) -> bool:
7679
"""Return if we have seen all fields."""
80+
if previous_match.name != name:
81+
return False
7782
if not previous_match.manufacturer_data and advertisement_data.manufacturer_data:
7883
return False
7984
if advertisement_data.service_data and (
@@ -122,10 +127,11 @@ def match_domains(self, service_info: BluetoothServiceInfoBleak) -> set[str]:
122127
device = service_info.device
123128
advertisement_data = service_info.advertisement
124129
connectable = service_info.connectable
130+
name = service_info.name
125131
matched = self._matched_connectable if connectable else self._matched
126132
matched_domains: set[str] = set()
127133
if (previous_match := matched.get(device.address)) and seen_all_fields(
128-
previous_match, advertisement_data
134+
previous_match, advertisement_data, name
129135
):
130136
# We have seen all fields so we can skip the rest of the matchers
131137
return matched_domains
@@ -140,11 +146,13 @@ def match_domains(self, service_info: BluetoothServiceInfoBleak) -> set[str]:
140146
)
141147
previous_match.service_data |= set(advertisement_data.service_data)
142148
previous_match.service_uuids |= set(advertisement_data.service_uuids)
149+
previous_match.name = name
143150
else:
144151
matched[device.address] = IntegrationMatchHistory(
145152
manufacturer_data=bool(advertisement_data.manufacturer_data),
146153
service_data=set(advertisement_data.service_data),
147154
service_uuids=set(advertisement_data.service_uuids),
155+
name=name,
148156
)
149157
return matched_domains
150158

tests/components/bluetooth/test_init.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,67 @@ async def test_discovery_match_by_local_name(
704704
assert mock_config_flow.mock_calls[0][1][0] == "switchbot"
705705

706706

707+
@pytest.mark.usefixtures("enable_bluetooth")
708+
async def test_discovery_match_by_service_uuid_when_name_changes_from_mac(
709+
hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
710+
) -> None:
711+
"""Test bluetooth discovery still matches when name changes from MAC address to real name."""
712+
mock_bt = [
713+
{
714+
"domain": "improv_ble",
715+
"service_uuid": "00467768-6228-2272-4663-277478268000",
716+
}
717+
]
718+
with (
719+
patch(
720+
"homeassistant.components.bluetooth.async_get_bluetooth",
721+
return_value=mock_bt,
722+
),
723+
patch.object(hass.config_entries.flow, "async_init") as mock_config_flow,
724+
):
725+
await async_setup_with_default_adapter(hass)
726+
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
727+
await hass.async_block_till_done()
728+
729+
assert len(mock_bleak_scanner_start.mock_calls) == 1
730+
731+
# First advertisement: name is MAC address, with service UUID
732+
# This should trigger discovery
733+
device_mac_name = generate_ble_device("64:E8:33:7E:0D:9E", "64:E8:33:7E:0D:9E")
734+
adv_mac_name = generate_advertisement_data(
735+
local_name="64:E8:33:7E:0D:9E",
736+
service_uuids=["00467768-6228-2272-4663-277478268000"],
737+
service_data={
738+
"00004677-0000-1000-8000-00805f9b34fb": b"\x02\x00\x00\x00\x00\x00"
739+
},
740+
)
741+
742+
inject_advertisement(hass, device_mac_name, adv_mac_name)
743+
await hass.async_block_till_done()
744+
745+
assert len(mock_config_flow.mock_calls) == 1
746+
assert mock_config_flow.mock_calls[0][1][0] == "improv_ble"
747+
mock_config_flow.reset_mock()
748+
749+
# Second advertisement: name changes to real name, same service UUID
750+
# This should trigger discovery again because the name changed
751+
device_real_name = generate_ble_device("64:E8:33:7E:0D:9E", "improvtest")
752+
adv_real_name = generate_advertisement_data(
753+
local_name="improvtest",
754+
service_uuids=["00467768-6228-2272-4663-277478268000"],
755+
service_data={
756+
"00004677-0000-1000-8000-00805f9b34fb": b"\x02\x00\x00\x00\x00\x00"
757+
},
758+
)
759+
760+
inject_advertisement(hass, device_real_name, adv_real_name)
761+
await hass.async_block_till_done()
762+
763+
# Should still match improv_ble even though name changed
764+
assert len(mock_config_flow.mock_calls) == 1
765+
assert mock_config_flow.mock_calls[0][1][0] == "improv_ble"
766+
767+
707768
@pytest.mark.usefixtures("macos_adapter")
708769
async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start(
709770
hass: HomeAssistant, mock_bleak_scanner_start: MagicMock

0 commit comments

Comments
 (0)