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

* fix ble mac

* fixes

* fixes

* fixes

* restore deleted test
pull/125870/merge
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
figure out where the adapter is.
"""
address = details[ADAPTER_ADDRESS]
connections = {(dr.CONNECTION_BLUETOOTH, address)}
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(
config_entry_id=entry.entry_id,
name=adapter_human_name(adapter, details[ADAPTER_ADDRESS]),
connections={(dr.CONNECTION_BLUETOOTH, details[ADAPTER_ADDRESS])},
name=adapter_human_name(adapter, address),
connections=connections,
manufacturer=details[ADAPTER_MANUFACTURER],
model=adapter_model(details),
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
assert address is not None
assert source_entry is not None
source_domain = entry.data[CONF_SOURCE_DOMAIN]
if mac_manufacturer := await get_manufacturer_from_mac(address):
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."""
source = user_input[CONF_SOURCE]
await self.async_set_unique_id(source)
source_config_entry_id = user_input[CONF_SOURCE_CONFIG_ENTRY_ID]
data = {
CONF_SOURCE: source,
CONF_SOURCE_MODEL: user_input[CONF_SOURCE_MODEL],
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],
}
self._abort_if_unique_id_configured(updates=data)
manager = get_manager()
scanner = manager.async_scanner_by_source(source)
for entry in self._async_current_entries(include_ignore=False):
# 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
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()
cancel_scanner()
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(
"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
)