From a28aeeeca7805bcd22b12ba236be308b0ef5f9d2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Aug 2022 18:18:54 -0500 Subject: [PATCH] Hide bluetooth passive option if its not available on the host system (#77421) * Hide bluetooth passive option if its not available - We now have a way to determine in advance if passive scanning is supported by BlueZ * drop string --- .../components/bluetooth/config_flow.py | 4 +- homeassistant/components/bluetooth/const.py | 2 + homeassistant/components/bluetooth/manager.py | 6 +++ .../components/bluetooth/strings.json | 3 +- .../components/bluetooth/translations/en.json | 9 +--- homeassistant/components/bluetooth/util.py | 3 ++ tests/components/bluetooth/conftest.py | 12 ++++- .../components/bluetooth/test_config_flow.py | 50 ++++++++++++++----- .../components/bluetooth/test_diagnostics.py | 26 +++++++++- 9 files changed, 87 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index 0fa2304468f..324520a8b5b 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -1,7 +1,6 @@ """Config flow to configure the Bluetooth integration.""" from __future__ import annotations -import platform from typing import TYPE_CHECKING, Any, cast import voluptuous as vol @@ -11,6 +10,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.core import callback from homeassistant.helpers.typing import DiscoveryInfoType +from . import models from .const import ( ADAPTER_ADDRESS, CONF_ADAPTER, @@ -134,7 +134,7 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): @callback def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool: """Return options flow support for this handler.""" - return platform.system() == "Linux" + return bool(models.MANAGER and models.MANAGER.supports_passive_scan) class OptionsFlowHandler(OptionsFlow): diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index 540310e9747..3174603f08e 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -59,8 +59,10 @@ class AdapterDetails(TypedDict, total=False): address: str sw_version: str hw_version: str + passive_scan: bool ADAPTER_ADDRESS: Final = "address" ADAPTER_SW_VERSION: Final = "sw_version" ADAPTER_HW_VERSION: Final = "hw_version" +ADAPTER_PASSIVE_SCAN: Final = "passive_scan" diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 984d37d806d..b1193c47245 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -23,6 +23,7 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( ADAPTER_ADDRESS, + ADAPTER_PASSIVE_SCAN, STALE_ADVERTISEMENT_SECONDS, UNAVAILABLE_TRACK_SECONDS, AdapterDetails, @@ -147,6 +148,11 @@ class BluetoothManager: self._connectable_scanners: list[BaseHaScanner] = [] self._adapters: dict[str, AdapterDetails] = {} + @property + def supports_passive_scan(self) -> bool: + """Return if passive scan is supported.""" + return any(adapter[ADAPTER_PASSIVE_SCAN] for adapter in self._adapters.values()) + async def async_diagnostics(self) -> dict[str, Any]: """Diagnostics for the manager.""" scanner_diagnostics = await asyncio.gather( diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json index 1912242ea6a..f838cd97798 100644 --- a/homeassistant/components/bluetooth/strings.json +++ b/homeassistant/components/bluetooth/strings.json @@ -29,9 +29,8 @@ "options": { "step": { "init": { - "description": "Passive listening requires BlueZ 5.63 or later with experimental features enabled.", "data": { - "passive": "Passive listening" + "passive": "Passive scanning" } } } diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index a3a7b97260e..9fcd0e5e1ee 100644 --- a/homeassistant/components/bluetooth/translations/en.json +++ b/homeassistant/components/bluetooth/translations/en.json @@ -9,9 +9,6 @@ "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, - "enable_bluetooth": { - "description": "Do you want to setup Bluetooth?" - }, "multiple_adapters": { "data": { "adapter": "Adapter" @@ -33,10 +30,8 @@ "step": { "init": { "data": { - "adapter": "The Bluetooth Adapter to use for scanning", - "passive": "Passive listening" - }, - "description": "Passive listening requires BlueZ 5.63 or later with experimental features enabled." + "passive": "Passive scanning" + } } } } diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 450c1812483..3f6c862e53d 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -24,6 +24,7 @@ async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]: WINDOWS_DEFAULT_BLUETOOTH_ADAPTER: AdapterDetails( address=DEFAULT_ADDRESS, sw_version=platform.release(), + passive_scan=False, ) } if platform.system() == "Darwin": @@ -31,6 +32,7 @@ async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]: MACOS_DEFAULT_BLUETOOTH_ADAPTER: AdapterDetails( address=DEFAULT_ADDRESS, sw_version=platform.release(), + passive_scan=False, ) } from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel @@ -45,6 +47,7 @@ async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]: address=adapter1["Address"], sw_version=adapter1["Name"], # This is actually the BlueZ version hw_version=adapter1["Modalias"], + passive_scan="org.bluez.AdvertisementMonitorManager1" in details, ) return adapters diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 44b9a60d1b5..3447012ace5 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -42,7 +42,11 @@ def one_adapter_fixture(): "Address": "00:00:00:00:00:01", "Name": "BlueZ 4.63", "Modalias": "usbid:1234", - } + }, + "org.bluez.AdvertisementMonitorManager1": { + "SupportedMonitorTypes": ["or_patterns"], + "SupportedFeatures": [], + }, }, }, ): @@ -74,7 +78,11 @@ def two_adapters_fixture(): "Address": "00:00:00:00:00:02", "Name": "BlueZ 4.63", "Modalias": "usbid:1234", - } + }, + "org.bluez.AdvertisementMonitorManager1": { + "SupportedMonitorTypes": ["or_patterns"], + "SupportedFeatures": [], + }, }, }, ): diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index 61763eef257..aa40666c80a 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -17,6 +17,29 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry +async def test_options_flow_disabled_not_setup( + hass, hass_ws_client, mock_bleak_scanner_start, macos_adapter +): + """Test options are disabled if the integration has not been setup.""" + await async_setup_component(hass, "config", {}) + entry = MockConfigEntry( + domain=DOMAIN, data={}, options={}, unique_id=DEFAULT_ADDRESS + ) + entry.add_to_hass(hass) + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/get", + "domain": "bluetooth", + "type_filter": "integration", + } + ) + response = await ws_client.receive_json() + assert response["result"][0]["supports_options"] is False + + async def test_async_step_user_macos(hass, macos_adapter): """Test setting up manually with one adapter on MacOS.""" result = await hass.config_entries.flow.async_init( @@ -287,19 +310,18 @@ async def test_options_flow_linux(hass, mock_bleak_scanner_start, one_adapter): assert result["data"][CONF_PASSIVE] is False -@patch( - "homeassistant.components.bluetooth.config_flow.platform.system", - return_value="Darwin", -) -async def test_options_flow_disabled_macos(mock_system, hass, hass_ws_client): +async def test_options_flow_disabled_macos( + hass, hass_ws_client, mock_bleak_scanner_start, macos_adapter +): """Test options are disabled on MacOS.""" await async_setup_component(hass, "config", {}) entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={}, + 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( @@ -314,19 +336,21 @@ async def test_options_flow_disabled_macos(mock_system, hass, hass_ws_client): assert response["result"][0]["supports_options"] is False -@patch( - "homeassistant.components.bluetooth.config_flow.platform.system", - return_value="Linux", -) -async def test_options_flow_enabled_linux(mock_system, hass, hass_ws_client): +async def test_options_flow_enabled_linux( + hass, hass_ws_client, mock_bleak_scanner_start, one_adapter +): """Test options are enabled on Linux.""" await async_setup_component(hass, "config", {}) entry = MockConfigEntry( domain=DOMAIN, data={}, options={}, + unique_id="00:00:00:00:00:01", ) 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( diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index 6f5eeefa5b6..9059fcb9ab1 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -31,7 +31,16 @@ async def test_diagnostics( return_value={ "org.bluez": { "/org/bluez/hci0": { - "Interfaces": {"org.bluez.Adapter1": {"Discovering": False}} + "org.bluez.Adapter1": { + "Name": "BlueZ 5.63", + "Alias": "BlueZ 5.63", + "Modalias": "usb:v1D6Bp0246d0540", + "Discovering": False, + }, + "org.bluez.AdvertisementMonitorManager1": { + "SupportedMonitorTypes": ["or_patterns"], + "SupportedFeatures": [], + }, } } }, @@ -57,18 +66,29 @@ async def test_diagnostics( "hci0": { "address": "00:00:00:00:00:01", "hw_version": "usbid:1234", + "passive_scan": False, "sw_version": "BlueZ 4.63", }, "hci1": { "address": "00:00:00:00:00:02", "hw_version": "usbid:1234", + "passive_scan": True, "sw_version": "BlueZ 4.63", }, }, "dbus": { "org.bluez": { "/org/bluez/hci0": { - "Interfaces": {"org.bluez.Adapter1": {"Discovering": False}} + "org.bluez.Adapter1": { + "Alias": "BlueZ " "5.63", + "Discovering": False, + "Modalias": "usb:v1D6Bp0246d0540", + "Name": "BlueZ " "5.63", + }, + "org.bluez.AdvertisementMonitorManager1": { + "SupportedFeatures": [], + "SupportedMonitorTypes": ["or_patterns"], + }, } } }, @@ -77,11 +97,13 @@ async def test_diagnostics( "hci0": { "address": "00:00:00:00:00:01", "hw_version": "usbid:1234", + "passive_scan": False, "sw_version": "BlueZ 4.63", }, "hci1": { "address": "00:00:00:00:00:02", "hw_version": "usbid:1234", + "passive_scan": True, "sw_version": "BlueZ 4.63", }, },