diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 387615cdc29..24eab5c9a5c 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -8,14 +8,24 @@ import platform from typing import TYPE_CHECKING, cast import async_timeout +from awesomeversion import AwesomeVersion from homeassistant.components import usb -from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.config_entries import ( + SOURCE_IGNORE, + SOURCE_INTEGRATION_DISCOVERY, + ConfigEntry, +) +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, discovery_flow from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.loader import async_get_bluetooth from . import models @@ -71,6 +81,8 @@ __all__ = [ _LOGGER = logging.getLogger(__name__) +RECOMMENDED_MIN_HAOS_VERSION = AwesomeVersion("9.0.dev0") + def _get_manager(hass: HomeAssistant) -> BluetoothManager: """Get the bluetooth manager.""" @@ -223,6 +235,43 @@ async def async_get_adapter_from_address( return await _get_manager(hass).async_get_adapter_from_address(address) +@hass_callback +def _async_haos_is_new_enough(hass: HomeAssistant) -> bool: + """Check if the version of Home Assistant Operating System is new enough.""" + # Only warn if a USB adapter is plugged in + if not any( + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.source != SOURCE_IGNORE + ): + return True + if ( + not hass.components.hassio.is_hassio() + or not (os_info := hass.components.hassio.get_os_info()) + or not (haos_version := os_info.get("version")) + or AwesomeVersion(haos_version) >= RECOMMENDED_MIN_HAOS_VERSION + ): + return True + return False + + +@hass_callback +def _async_check_haos(hass: HomeAssistant) -> None: + """Create or delete an the haos_outdated issue.""" + if _async_haos_is_new_enough(hass): + async_delete_issue(hass, DOMAIN, "haos_outdated") + return + async_create_issue( + hass, + DOMAIN, + "haos_outdated", + is_fixable=False, + severity=IssueSeverity.WARNING, + learn_more_url="/config/updates", + translation_key="haos_outdated", + ) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) @@ -261,6 +310,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: EVENT_HOMEASSISTANT_STOP, hass_callback(lambda event: cancel()) ) + # Wait to check until after start to make sure + # that the system info is available. + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, + hass_callback(lambda event: _async_check_haos(hass)), + ) + return True diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 1c4a8a2d2fc..7a7cfbae007 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -3,6 +3,7 @@ "name": "Bluetooth", "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["usb"], + "after_dependencies": ["hassio"], "quality_scale": "internal", "requirements": [ "bleak==0.17.0", diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json index f838cd97798..cfde1b90cd8 100644 --- a/homeassistant/components/bluetooth/strings.json +++ b/homeassistant/components/bluetooth/strings.json @@ -1,4 +1,10 @@ { + "issues": { + "haos_outdated": { + "title": "Update to Home Assistant Operating System 9.0 or later", + "description": "To improve Bluetooth reliability and performance, we highly recommend you update to version 9.0 or later of the Home Assistant Operating System." + } + }, "config": { "flow_title": "{name}", "step": { diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index 7d76740602d..beefb842204 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" @@ -29,14 +26,18 @@ } } }, + "issues": { + "haos_outdated": { + "description": "To improve Bluetooth reliability and performance, we highly recommend you update to version 9.0 or later of the Home Assistant Operating System.", + "title": "Update to Home Assistant Operating System 9.0 or later" + } + }, "options": { "step": { "init": { "data": { - "adapter": "The Bluetooth Adapter to use for scanning", "passive": "Passive scanning" - }, - "description": "Passive listening requires BlueZ 5.63 or later with experimental features enabled." + } } } } diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 2c0e2e6eb9c..4c78f063780 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -5,6 +5,44 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +@pytest.fixture(name="operating_system_85") +def mock_operating_system_85(): + """Mock running Home Assistant Operating system 8.5.""" + with patch("homeassistant.components.hassio.is_hassio", return_value=True), patch( + "homeassistant.components.hassio.get_os_info", + return_value={ + "version": "8.5", + "version_latest": "10.0.dev20220912", + "update_available": False, + "board": "odroid-n2", + "boot": "B", + "data_disk": "/dev/mmcblk1p4", + }, + ), patch("homeassistant.components.hassio.get_info", return_value={}), patch( + "homeassistant.components.hassio.get_host_info", return_value={} + ): + yield + + +@pytest.fixture(name="operating_system_90") +def mock_operating_system_90(): + """Mock running Home Assistant Operating system 9.0.""" + with patch("homeassistant.components.hassio.is_hassio", return_value=True), patch( + "homeassistant.components.hassio.get_os_info", + return_value={ + "version": "9.0.dev20220912", + "version_latest": "10.0.dev20220912", + "update_available": False, + "board": "odroid-n2", + "boot": "B", + "data_disk": "/dev/mmcblk1p4", + }, + ), patch("homeassistant.components.hassio.get_info", return_value={}), patch( + "homeassistant.components.hassio.get_host_info", return_value={} + ): + yield + + @pytest.fixture(name="bluez_dbus_mock") def bluez_dbus_mock(): """Fixture that mocks out the bluez dbus calls.""" @@ -39,6 +77,23 @@ def windows_adapter(): yield +@pytest.fixture(name="no_adapters") +def no_adapter_fixture(bluez_dbus_mock): + """Fixture that mocks no adapters on Linux.""" + with patch( + "homeassistant.components.bluetooth.platform.system", return_value="Linux" + ), patch( + "homeassistant.components.bluetooth.scanner.platform.system", + return_value="Linux", + ), patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.get_bluetooth_adapter_details", + return_value={}, + ): + yield + + @pytest.fixture(name="one_adapter") def one_adapter_fixture(bluez_dbus_mock): """Fixture that mocks one adapter on Linux.""" diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index a3045291286..7ee1a9840db 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -37,6 +37,7 @@ from homeassistant.components.bluetooth.match import ( from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.issue_registry import async_get as async_get_issue_registry from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -2680,3 +2681,51 @@ async def test_discover_new_usb_adapters(hass, mock_bleak_scanner_start, one_ada await hass.async_block_till_done() assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 1 + + +async def test_issue_outdated_haos( + hass, mock_bleak_scanner_start, one_adapter, operating_system_85 +): + """Test we create an issue on outdated haos.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01" + ) + entry.add_to_hass(hass) + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + registry = async_get_issue_registry(hass) + issue = registry.async_get_issue(DOMAIN, "haos_outdated") + assert issue is not None + + +async def test_issue_outdated_haos_no_adapters( + hass, mock_bleak_scanner_start, no_adapters, operating_system_85 +): + """Test we do not create an issue on outdated haos if there are no adapters.""" + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + registry = async_get_issue_registry(hass) + issue = registry.async_get_issue(DOMAIN, "haos_outdated") + assert issue is None + + +async def test_haos_9_or_later( + hass, mock_bleak_scanner_start, one_adapter, operating_system_90 +): + """Test we do not create issues for haos 9.x or later.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01" + ) + entry.add_to_hass(hass) + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + registry = async_get_issue_registry(hass) + issue = registry.async_get_issue(DOMAIN, "haos_outdated") + assert issue is None