diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 35d4b625942..560fb0663a8 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -196,6 +196,17 @@ async def _async_start_adapter_discovery( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" + if platform.system() == "Linux": + # Remove any config entries that are using the default address + # that were created from discovering adapters in a crashed state + # + # DEFAULT_ADDRESS is perfectly valid on MacOS but on + # Linux it means the adapter is not yet configured + # or crashed + for entry in list(hass.config_entries.async_entries(DOMAIN)): + if entry.unique_id == DEFAULT_ADDRESS: + await hass.config_entries.async_remove(entry.entry_id) + bluetooth_adapters = get_adapters() bluetooth_storage = BluetoothStorage(hass) slot_manager = BleakSlotManager() @@ -257,13 +268,19 @@ async def async_discover_adapters( adapters: dict[str, AdapterDetails], ) -> None: """Discover adapters and start flows.""" - if platform.system() == "Windows": + system = platform.system() + if system == "Windows": # We currently do not have a good way to detect if a bluetooth device is # available on Windows. We will just assume that it is not unless they # actively add it. return for adapter, details in adapters.items(): + if system == "Linux" and details[ADAPTER_ADDRESS] == DEFAULT_ADDRESS: + # DEFAULT_ADDRESS is perfectly valid on MacOS but on + # Linux it means the adapter is not yet configured + # or crashed so we should not try to start a flow for it. + continue discovery_flow.async_create_flow( hass, DOMAIN, diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index 6802bdc37c0..87038d48151 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -2,11 +2,13 @@ from __future__ import annotations +import platform from typing import Any, cast from bluetooth_adapters import ( ADAPTER_ADDRESS, ADAPTER_MANUFACTURER, + DEFAULT_ADDRESS, AdapterDetails, adapter_human_name, adapter_model, @@ -133,10 +135,15 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): bluetooth_adapters = get_adapters() await bluetooth_adapters.refresh() self._adapters = bluetooth_adapters.adapters + system = platform.system() unconfigured_adapters = [ adapter for adapter, details in self._adapters.items() if details[ADAPTER_ADDRESS] not in configured_addresses + # DEFAULT_ADDRESS is perfectly valid on MacOS but on + # Linux it means the adapter is not yet configured + # or crashed + and not (system == "Linux" and details[ADAPTER_ADDRESS] == DEFAULT_ADDRESS) ] if not unconfigured_adapters: ignored_adapters = len( diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index c1e040ccd49..d4056c1e38e 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -219,6 +219,45 @@ def two_adapters_fixture(): yield +@pytest.fixture(name="crashed_adapter") +def crashed_adapter_fixture(): + """Fixture that mocks one crashed adapter on Linux.""" + with ( + patch( + "homeassistant.components.bluetooth.platform.system", + return_value="Linux", + ), + patch( + "habluetooth.scanner.platform.system", + return_value="Linux", + ), + patch( + "bluetooth_adapters.systems.platform.system", + return_value="Linux", + ), + patch("habluetooth.scanner.SYSTEM", "Linux"), + patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.refresh", + ), + patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + { + "hci0": { + "address": "00:00:00:00:00:00", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": True, + "sw_version": "homeassistant", + "manufacturer": None, + "product": None, + "product_id": None, + "vendor_id": None, + }, + }, + ), + ): + yield + + @pytest.fixture(name="one_adapter_old_bluez") def one_adapter_old_bluez(): """Fixture that mocks two adapters on Linux.""" diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index f9bbbcd2d0e..d044be76e6d 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -32,6 +32,9 @@ async def test_options_flow_disabled_not_setup( domain=DOMAIN, data={}, options={}, unique_id=DEFAULT_ADDRESS ) entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + ws_client = await hass_ws_client(hass) await ws_client.send_json( @@ -103,6 +106,19 @@ async def test_async_step_user_linux_one_adapter( assert len(mock_setup_entry.mock_calls) == 1 +async def test_async_step_user_linux_crashed_adapter( + hass: HomeAssistant, crashed_adapter: None +) -> None: + """Test setting up manually with one crashed adapter on Linux.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_adapters" + + async def test_async_step_user_linux_two_adapters( hass: HomeAssistant, two_adapters: None ) -> None: diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index e68ccc94d19..82fa0341966 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -2807,6 +2807,19 @@ async def test_can_unsetup_bluetooth_single_adapter_macos( await hass.async_block_till_done() +async def test_default_address_config_entries_removed_linux( + hass: HomeAssistant, + mock_bleak_scanner_start: MagicMock, + one_adapter: None, +) -> None: + """Test default address entries are removed on linux.""" + entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}, unique_id=DEFAULT_ADDRESS) + entry.add_to_hass(hass) + await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + + async def test_can_unsetup_bluetooth_single_adapter_linux( hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, @@ -2889,6 +2902,16 @@ async def test_auto_detect_bluetooth_adapters_linux_multiple( assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 2 +async def test_auto_detect_bluetooth_adapters_skips_crashed( + hass: HomeAssistant, crashed_adapter: None +) -> None: + """Test we skip crashed adapters on linux.""" + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 0 + + async def test_auto_detect_bluetooth_adapters_linux_none_found( hass: HomeAssistant, ) -> None: