Fix bluetooth integration matching with service_data_uuids and service_uuids (#75687)

* Fix bluetooth integration with service_data and service_uuids

We would only dispatch a new flow when the address was seen for
the first time or the manufacturer_data appeared in a followup
advertisement. Its also possible for the service_data and
service_uuids to appear in a followup advertisement so we
need to track these as well

* improve logging to avoid overly large messages

* improve logging to avoid overly large messages

* adjust

* adjsut

* split

* coverage

* coverage

* coverage

* coverage

* fix matcher

* more coverage

* more coverage

* more coverage

* revert switchbot changes and move to seperate PR
This commit is contained in:
J. Nick Koston 2022-07-24 16:39:53 -05:00 committed by GitHub
parent d890598da7
commit bbb9443b00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 398 additions and 103 deletions

View file

@ -222,7 +222,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start):
assert mock_config_flow.mock_calls[0][1][0] == "switchbot"
async def test_discovery_match_by_manufacturer_id_and_first_byte(
async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start(
hass, mock_bleak_scanner_start
):
"""Test bluetooth discovery match by manufacturer_id and manufacturer_data_start."""
@ -248,20 +248,33 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte(
assert len(mock_bleak_scanner_start.mock_calls) == 1
hkc_device = BLEDevice("44:44:33:11:23:45", "lock")
hkc_adv_no_mfr_data = AdvertisementData(
local_name="lock",
service_uuids=[],
manufacturer_data={},
)
hkc_adv = AdvertisementData(
local_name="lock",
service_uuids=[],
manufacturer_data={76: b"\x06\x02\x03\x99"},
)
# 1st discovery with no manufacturer data
# should not trigger config flow
_get_underlying_scanner()._callback(hkc_device, hkc_adv_no_mfr_data)
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 0
mock_config_flow.reset_mock()
# 2nd discovery with manufacturer data
# should trigger a config flow
_get_underlying_scanner()._callback(hkc_device, hkc_adv)
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 1
assert mock_config_flow.mock_calls[0][1][0] == "homekit_controller"
mock_config_flow.reset_mock()
# 2nd discovery should not generate another flow
# 3rd discovery should not generate another flow
_get_underlying_scanner()._callback(hkc_device, hkc_adv)
await hass.async_block_till_done()
@ -288,6 +301,230 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte(
assert len(mock_config_flow.mock_calls) == 0
async def test_discovery_match_by_service_data_uuid_then_others(
hass, mock_bleak_scanner_start
):
"""Test bluetooth discovery match by service_data_uuid and then other fields."""
mock_bt = [
{
"domain": "my_domain",
"service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb",
},
{
"domain": "my_domain",
"service_uuid": "0000fd3d-0000-1000-8000-00805f9b34fc",
},
{
"domain": "other_domain",
"manufacturer_id": 323,
},
]
with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
):
assert await async_setup_component(
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow:
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert len(mock_bleak_scanner_start.mock_calls) == 1
device = BLEDevice("44:44:33:11:23:45", "lock")
adv_without_service_data_uuid = AdvertisementData(
local_name="lock",
service_uuids=[],
manufacturer_data={},
)
adv_with_mfr_data = AdvertisementData(
local_name="lock",
service_uuids=[],
manufacturer_data={323: b"\x01\x02\x03"},
service_data={},
)
adv_with_service_data_uuid = AdvertisementData(
local_name="lock",
service_uuids=[],
manufacturer_data={},
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"},
)
adv_with_service_data_uuid_and_mfr_data = AdvertisementData(
local_name="lock",
service_uuids=[],
manufacturer_data={323: b"\x01\x02\x03"},
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"},
)
adv_with_service_data_uuid_and_mfr_data_and_service_uuid = AdvertisementData(
local_name="lock",
manufacturer_data={323: b"\x01\x02\x03"},
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"},
service_uuids=["0000fd3d-0000-1000-8000-00805f9b34fd"],
)
adv_with_service_uuid = AdvertisementData(
local_name="lock",
manufacturer_data={},
service_data={},
service_uuids=["0000fd3d-0000-1000-8000-00805f9b34fd"],
)
# 1st discovery should not generate a flow because the
# service_data_uuid is not in the advertisement
_get_underlying_scanner()._callback(device, adv_without_service_data_uuid)
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 0
mock_config_flow.reset_mock()
# 2nd discovery should not generate a flow because the
# service_data_uuid is not in the advertisement
_get_underlying_scanner()._callback(device, adv_without_service_data_uuid)
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 0
mock_config_flow.reset_mock()
# 3rd discovery should generate a flow because the
# manufacturer_data is in the advertisement
_get_underlying_scanner()._callback(device, adv_with_mfr_data)
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 1
assert mock_config_flow.mock_calls[0][1][0] == "other_domain"
mock_config_flow.reset_mock()
# 4th discovery should generate a flow because the
# service_data_uuid is in the advertisement and
# we never saw a service_data_uuid before
_get_underlying_scanner()._callback(device, adv_with_service_data_uuid)
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 1
assert mock_config_flow.mock_calls[0][1][0] == "my_domain"
mock_config_flow.reset_mock()
# 5th discovery should not generate a flow because the
# we already saw an advertisement with the service_data_uuid
_get_underlying_scanner()._callback(device, adv_with_service_data_uuid)
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 0
# 6th discovery should not generate a flow because the
# manufacturer_data is in the advertisement
# and we saw manufacturer_data before
_get_underlying_scanner()._callback(
device, adv_with_service_data_uuid_and_mfr_data
)
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 0
mock_config_flow.reset_mock()
# 7th discovery should generate a flow because the
# service_uuids is in the advertisement
# and we never saw service_uuids before
_get_underlying_scanner()._callback(
device, adv_with_service_data_uuid_and_mfr_data_and_service_uuid
)
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 2
assert {
mock_config_flow.mock_calls[0][1][0],
mock_config_flow.mock_calls[1][1][0],
} == {"my_domain", "other_domain"}
mock_config_flow.reset_mock()
# 8th discovery should not generate a flow
# since all fields have been seen at this point
_get_underlying_scanner()._callback(
device, adv_with_service_data_uuid_and_mfr_data_and_service_uuid
)
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 0
mock_config_flow.reset_mock()
# 9th discovery should not generate a flow
# since all fields have been seen at this point
_get_underlying_scanner()._callback(device, adv_with_service_uuid)
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 0
# 10th discovery should not generate a flow
# since all fields have been seen at this point
_get_underlying_scanner()._callback(device, adv_with_service_data_uuid)
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 0
# 11th discovery should not generate a flow
# since all fields have been seen at this point
_get_underlying_scanner()._callback(device, adv_without_service_data_uuid)
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 0
async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id(
hass, mock_bleak_scanner_start
):
"""Test bluetooth discovery matches twice for service_uuid and then manufacturer_id."""
mock_bt = [
{
"domain": "my_domain",
"manufacturer_id": 76,
},
{
"domain": "my_domain",
"service_uuid": "0000fd3d-0000-1000-8000-00805f9b34fc",
},
]
with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
):
assert await async_setup_component(
hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}
)
await hass.async_block_till_done()
with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow:
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert len(mock_bleak_scanner_start.mock_calls) == 1
device = BLEDevice("44:44:33:11:23:45", "lock")
adv_service_uuids = AdvertisementData(
local_name="lock",
service_uuids=["0000fd3d-0000-1000-8000-00805f9b34fc"],
manufacturer_data={},
)
adv_manufacturer_data = AdvertisementData(
local_name="lock",
service_uuids=[],
manufacturer_data={76: b"\x06\x02\x03\x99"},
)
# 1st discovery with matches service_uuid
# should trigger config flow
_get_underlying_scanner()._callback(device, adv_service_uuids)
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 1
assert mock_config_flow.mock_calls[0][1][0] == "my_domain"
mock_config_flow.reset_mock()
# 2nd discovery with manufacturer data
# should trigger a config flow
_get_underlying_scanner()._callback(device, adv_manufacturer_data)
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 1
assert mock_config_flow.mock_calls[0][1][0] == "my_domain"
mock_config_flow.reset_mock()
# 3rd discovery should not generate another flow
_get_underlying_scanner()._callback(device, adv_service_uuids)
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 0
# 4th discovery should not generate another flow
_get_underlying_scanner()._callback(device, adv_manufacturer_data)
await hass.async_block_till_done()
assert len(mock_config_flow.mock_calls) == 0
async def test_async_discovered_device_api(hass, mock_bleak_scanner_start):
"""Test the async_discovered_device API."""
mock_bt = []