Fix bluetooth adapters with missing firmware patch files not being discovered (#81926)

This commit is contained in:
J. Nick Koston 2022-11-10 14:14:37 -06:00 committed by GitHub
parent 2813101418
commit 78180b2ad8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 110 additions and 1 deletions

View File

@ -3,6 +3,7 @@ from __future__ import annotations
from asyncio import Future from asyncio import Future
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
import datetime
import logging import logging
import platform import platform
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING, cast
@ -21,6 +22,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_ca
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, discovery_flow from homeassistant.helpers import device_registry as dr, discovery_flow
from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.issue_registry import ( from homeassistant.helpers.issue_registry import (
IssueSeverity, IssueSeverity,
async_create_issue, async_create_issue,
@ -33,6 +35,7 @@ from .const import (
ADAPTER_ADDRESS, ADAPTER_ADDRESS,
ADAPTER_HW_VERSION, ADAPTER_HW_VERSION,
ADAPTER_SW_VERSION, ADAPTER_SW_VERSION,
BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
CONF_ADAPTER, CONF_ADAPTER,
CONF_DETAILS, CONF_DETAILS,
CONF_PASSIVE, CONF_PASSIVE,
@ -40,6 +43,7 @@ from .const import (
DEFAULT_ADDRESS, DEFAULT_ADDRESS,
DOMAIN, DOMAIN,
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
SOURCE_LOCAL, SOURCE_LOCAL,
AdapterDetails, AdapterDetails,
) )
@ -298,9 +302,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
await async_discover_adapters(hass, discovered_adapters) await async_discover_adapters(hass, discovered_adapters)
discovery_debouncer = Debouncer( discovery_debouncer = Debouncer(
hass, _LOGGER, cooldown=5, immediate=False, function=_async_rediscover_adapters hass,
_LOGGER,
cooldown=BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
immediate=False,
function=_async_rediscover_adapters,
) )
async def _async_call_debouncer(now: datetime.datetime) -> None:
"""Call the debouncer at a later time."""
await discovery_debouncer.async_call()
def _async_trigger_discovery() -> None: def _async_trigger_discovery() -> None:
# There are so many bluetooth adapter models that # There are so many bluetooth adapter models that
# we check the bus whenever a usb device is plugged in # we check the bus whenever a usb device is plugged in
@ -310,6 +322,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
# present. # present.
_LOGGER.debug("Triggering bluetooth usb discovery") _LOGGER.debug("Triggering bluetooth usb discovery")
hass.async_create_task(discovery_debouncer.async_call()) hass.async_create_task(discovery_debouncer.async_call())
# Because it can take 120s for the firmware loader
# fallback to timeout we need to wait that plus
# the debounce time to ensure we do not miss the
# adapter becoming available to DBus since otherwise
# we will never see the new adapter until
# Home Assistant is restarted
async_call_later(
hass,
BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS + LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
_async_call_debouncer,
)
cancel = usb.async_register_scan_request_callback(hass, _async_trigger_discovery) cancel = usb.async_register_scan_request_callback(hass, _async_trigger_discovery)
hass.bus.async_listen_once( hass.bus.async_listen_once(

View File

@ -59,6 +59,15 @@ SCANNER_WATCHDOG_TIMEOUT: Final = 90
SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=30) SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=30)
# When the linux kernel is configured with
# CONFIG_FW_LOADER_USER_HELPER_FALLBACK it
# can take up to 120s before the USB device
# is available if the firmware files
# are not present
LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS = 120
BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS = 5
class AdapterDetails(TypedDict, total=False): class AdapterDetails(TypedDict, total=False):
"""Adapter details.""" """Adapter details."""

View File

@ -20,9 +20,11 @@ from homeassistant.components.bluetooth import (
scanner, scanner,
) )
from homeassistant.components.bluetooth.const import ( from homeassistant.components.bluetooth.const import (
BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
CONF_PASSIVE, CONF_PASSIVE,
DEFAULT_ADDRESS, DEFAULT_ADDRESS,
DOMAIN, DOMAIN,
LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
SOURCE_LOCAL, SOURCE_LOCAL,
UNAVAILABLE_TRACK_SECONDS, UNAVAILABLE_TRACK_SECONDS,
) )
@ -2737,6 +2739,81 @@ async def test_discover_new_usb_adapters(hass, mock_bleak_scanner_start, one_ada
assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 1 assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 1
async def test_discover_new_usb_adapters_with_firmware_fallback_delay(
hass, mock_bleak_scanner_start, one_adapter
):
"""Test we can discover new usb adapters with a firmware fallback delay."""
entry = MockConfigEntry(
domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01"
)
entry.add_to_hass(hass)
saved_callback = None
def _async_register_scan_request_callback(_hass, _callback):
nonlocal saved_callback
saved_callback = _callback
return lambda: None
with patch(
"homeassistant.components.bluetooth.usb.async_register_scan_request_callback",
_async_register_scan_request_callback,
):
assert await async_setup_component(hass, bluetooth.DOMAIN, {})
await hass.async_block_till_done()
assert not hass.config_entries.flow.async_progress(DOMAIN)
saved_callback()
assert not hass.config_entries.flow.async_progress(DOMAIN)
with patch(
"homeassistant.components.bluetooth.util.platform.system", return_value="Linux"
), patch(
"bluetooth_adapters.get_bluetooth_adapter_details",
return_value={},
):
async_fire_time_changed(
hass, dt_util.utcnow() + timedelta(BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS * 2)
)
await hass.async_block_till_done()
assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 0
with patch(
"homeassistant.components.bluetooth.util.platform.system", return_value="Linux"
), patch(
"bluetooth_adapters.get_bluetooth_adapter_details",
return_value={
"hci0": {
"org.bluez.Adapter1": {
"Address": "00:00:00:00:00:01",
"Name": "BlueZ 4.63",
"Modalias": "usbid:1234",
}
},
"hci1": {
"org.bluez.Adapter1": {
"Address": "00:00:00:00:00:02",
"Name": "BlueZ 4.63",
"Modalias": "usbid:1234",
}
},
},
):
async_fire_time_changed(
hass,
dt_util.utcnow()
+ timedelta(
seconds=LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS
+ (BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS * 2)
),
)
await hass.async_block_till_done()
assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 1
async def test_issue_outdated_haos( async def test_issue_outdated_haos(
hass, mock_bleak_scanner_start, one_adapter, operating_system_85 hass, mock_bleak_scanner_start, one_adapter, operating_system_85
): ):