Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Nov 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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