Update Bluetooth remote config entries if the MAC is corrected (#139457)

* fix ble mac

* fixes

* fixes

* fixes

* restore deleted test
This commit is contained in:
J. Nick Koston 2025-02-28 19:49:31 +00:00 committed by GitHub
parent 6ce48eab45
commit 5a6ffe1901
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 117 additions and 6 deletions

View File

@ -311,11 +311,24 @@ async def async_update_device(
update the device with the new location so they can update the device with the new location so they can
figure out where the adapter is. figure out where the adapter is.
""" """
address = details[ADAPTER_ADDRESS]
connections = {(dr.CONNECTION_BLUETOOTH, address)}
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
# We only have one device for the config entry
# so if the address has been corrected, make
# sure the device entry reflects the correct
# address
for device in dr.async_entries_for_config_entry(device_registry, entry.entry_id):
for conn_type, conn_value in device.connections:
if conn_type == dr.CONNECTION_BLUETOOTH and conn_value != address:
device_registry.async_update_device(
device.id, new_connections=connections
)
break
device_entry = device_registry.async_get_or_create( device_entry = device_registry.async_get_or_create(
config_entry_id=entry.entry_id, config_entry_id=entry.entry_id,
name=adapter_human_name(adapter, details[ADAPTER_ADDRESS]), name=adapter_human_name(adapter, address),
connections={(dr.CONNECTION_BLUETOOTH, details[ADAPTER_ADDRESS])}, connections=connections,
manufacturer=details[ADAPTER_MANUFACTURER], manufacturer=details[ADAPTER_MANUFACTURER],
model=adapter_model(details), model=adapter_model(details),
sw_version=details.get(ADAPTER_SW_VERSION), sw_version=details.get(ADAPTER_SW_VERSION),
@ -342,9 +355,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
) )
) )
) )
return True
address = entry.unique_id address = entry.unique_id
assert address is not None assert address is not None
assert source_entry is not None
source_domain = entry.data[CONF_SOURCE_DOMAIN] source_domain = entry.data[CONF_SOURCE_DOMAIN]
if mac_manufacturer := await get_manufacturer_from_mac(address): if mac_manufacturer := await get_manufacturer_from_mac(address):
manufacturer = f"{mac_manufacturer} ({source_domain})" manufacturer = f"{mac_manufacturer} ({source_domain})"

View File

@ -186,16 +186,28 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a flow initialized by an external scanner.""" """Handle a flow initialized by an external scanner."""
source = user_input[CONF_SOURCE] source = user_input[CONF_SOURCE]
await self.async_set_unique_id(source) await self.async_set_unique_id(source)
source_config_entry_id = user_input[CONF_SOURCE_CONFIG_ENTRY_ID]
data = { data = {
CONF_SOURCE: source, CONF_SOURCE: source,
CONF_SOURCE_MODEL: user_input[CONF_SOURCE_MODEL], CONF_SOURCE_MODEL: user_input[CONF_SOURCE_MODEL],
CONF_SOURCE_DOMAIN: user_input[CONF_SOURCE_DOMAIN], CONF_SOURCE_DOMAIN: user_input[CONF_SOURCE_DOMAIN],
CONF_SOURCE_CONFIG_ENTRY_ID: user_input[CONF_SOURCE_CONFIG_ENTRY_ID], CONF_SOURCE_CONFIG_ENTRY_ID: source_config_entry_id,
CONF_SOURCE_DEVICE_ID: user_input[CONF_SOURCE_DEVICE_ID], CONF_SOURCE_DEVICE_ID: user_input[CONF_SOURCE_DEVICE_ID],
} }
self._abort_if_unique_id_configured(updates=data) self._abort_if_unique_id_configured(updates=data)
manager = get_manager() for entry in self._async_current_entries(include_ignore=False):
scanner = manager.async_scanner_by_source(source) # If the mac address needs to be corrected, migrate
# the config entry to the new mac address
if (
entry.data.get(CONF_SOURCE_CONFIG_ENTRY_ID) == source_config_entry_id
and entry.unique_id != source
):
self.hass.config_entries.async_update_entry(
entry, unique_id=source, data={**entry.data, **data}
)
self.hass.config_entries.async_schedule_reload(entry.entry_id)
return self.async_abort(reason="already_configured")
scanner = get_manager().async_scanner_by_source(source)
assert scanner is not None assert scanner is not None
return self.async_create_entry(title=scanner.name, data=data) return self.async_create_entry(title=scanner.name, data=data)

View File

@ -608,3 +608,40 @@ async def test_async_step_integration_discovery_remote_adapter(
await hass.async_block_till_done() await hass.async_block_till_done()
cancel_scanner() cancel_scanner()
await hass.async_block_till_done() await hass.async_block_till_done()
@pytest.mark.usefixtures("enable_bluetooth")
async def test_async_step_integration_discovery_remote_adapter_mac_fix(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
area_registry: ar.AreaRegistry,
) -> None:
"""Test remote adapter corrects mac address via integration discovery."""
entry = MockConfigEntry(domain="test")
entry.add_to_hass(hass)
bluetooth_entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_SOURCE: "AA:BB:CC:DD:EE:FF",
CONF_SOURCE_DOMAIN: "test",
CONF_SOURCE_MODEL: "test",
CONF_SOURCE_CONFIG_ENTRY_ID: entry.entry_id,
CONF_SOURCE_DEVICE_ID: None,
},
)
bluetooth_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
data={
CONF_SOURCE: "AA:AA:AA:AA:AA:AA",
CONF_SOURCE_DOMAIN: "test",
CONF_SOURCE_MODEL: "test",
CONF_SOURCE_CONFIG_ENTRY_ID: entry.entry_id,
CONF_SOURCE_DEVICE_ID: None,
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert bluetooth_entry.unique_id == "AA:AA:AA:AA:AA:AA"
assert bluetooth_entry.data[CONF_SOURCE] == "AA:AA:AA:AA:AA:AA"

View File

@ -3300,3 +3300,52 @@ async def test_cleanup_orphened_remote_scanner_config_entry(
assert not hass.config_entries.async_entry_for_domain_unique_id( assert not hass.config_entries.async_entry_for_domain_unique_id(
"bluetooth", scanner.source "bluetooth", scanner.source
) )
@pytest.mark.usefixtures("enable_bluetooth")
async def test_fix_incorrect_mac_remote_scanner_config_entry(
hass: HomeAssistant,
) -> None:
"""Test the remote scanner config entries can replace a incorrect mac."""
source_entry = MockConfigEntry(domain="test")
source_entry.add_to_hass(hass)
connector = (
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
)
scanner = FakeRemoteScanner("AA:BB:CC:DD:EE:FF", "esp32", connector, True)
assert scanner.source == "AA:BB:CC:DD:EE:FF"
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_SOURCE: scanner.source,
CONF_SOURCE_DOMAIN: "test",
CONF_SOURCE_MODEL: "test",
CONF_SOURCE_CONFIG_ENTRY_ID: source_entry.entry_id,
},
unique_id=scanner.source,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert hass.config_entries.async_entry_for_domain_unique_id(
"bluetooth", scanner.source
)
await hass.config_entries.async_unload(entry.entry_id)
new_scanner = FakeRemoteScanner("AA:BB:CC:DD:EE:AA", "esp32", connector, True)
assert new_scanner.source == "AA:BB:CC:DD:EE:AA"
hass.config_entries.async_update_entry(
entry,
data={**entry.data, CONF_SOURCE: new_scanner.source},
unique_id=new_scanner.source,
)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert hass.config_entries.async_entry_for_domain_unique_id(
"bluetooth", new_scanner.source
)
# Incorrect connection should be removed
assert not hass.config_entries.async_entry_for_domain_unique_id(
"bluetooth", scanner.source
)