Skip to content

Commit

Permalink
Fix bluetooth adapters with missing firmware patch files not being di…
Browse files Browse the repository at this point in the history
…scovered (#81926)
  • Loading branch information
bdraco authored Nov 10, 2022
1 parent 2813101 commit 78180b2
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 1 deletion.
25 changes: 24 additions & 1 deletion homeassistant/components/bluetooth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from asyncio import Future
from collections.abc import Callable, Iterable
import datetime
import logging
import platform
from typing import TYPE_CHECKING, cast
Expand All @@ -21,6 +22,7 @@
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, discovery_flow
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
Expand All @@ -33,13 +35,15 @@
ADAPTER_ADDRESS,
ADAPTER_HW_VERSION,
ADAPTER_SW_VERSION,
BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
CONF_ADAPTER,
CONF_DETAILS,
CONF_PASSIVE,
DATA_MANAGER,
DEFAULT_ADDRESS,
DOMAIN,
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
SOURCE_LOCAL,
AdapterDetails,
)
Expand Down Expand Up @@ -298,9 +302,17 @@ async def _async_rediscover_adapters() -> None:
await async_discover_adapters(hass, discovered_adapters)

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:
# There are so many bluetooth adapter models that
# we check the bus whenever a usb device is plugged in
Expand All @@ -310,6 +322,17 @@ def _async_trigger_discovery() -> None:
# present.
_LOGGER.debug("Triggering bluetooth usb discovery")
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)
hass.bus.async_listen_once(
Expand Down
9 changes: 9 additions & 0 deletions homeassistant/components/bluetooth/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@
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):
"""Adapter details."""

Expand Down
77 changes: 77 additions & 0 deletions tests/components/bluetooth/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
scanner,
)
from homeassistant.components.bluetooth.const import (
BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
CONF_PASSIVE,
DEFAULT_ADDRESS,
DOMAIN,
LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
SOURCE_LOCAL,
UNAVAILABLE_TRACK_SECONDS,
)
Expand Down Expand Up @@ -2737,6 +2739,81 @@ def _async_register_scan_request_callback(_hass, _callback):
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(
hass, mock_bleak_scanner_start, one_adapter, operating_system_85
):
Expand Down

0 comments on commit 78180b2

Please sign in to comment.