1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

Add repair issue for Bluetooth adapter passive mode fallback (#152076)

This commit is contained in:
J. Nick Koston
2025-09-11 07:52:57 -05:00
committed by GitHub
parent 46463ea4f8
commit e8c1d3dc3c
3 changed files with 198 additions and 4 deletions

View File

@@ -8,8 +8,19 @@ import itertools
import logging
from bleak_retry_connector import BleakSlotManager
from bluetooth_adapters import BluetoothAdapters, adapter_human_name, adapter_model
from habluetooth import BaseHaRemoteScanner, BaseHaScanner, BluetoothManager, HaScanner
from bluetooth_adapters import (
ADAPTER_TYPE,
BluetoothAdapters,
adapter_human_name,
adapter_model,
)
from habluetooth import (
BaseHaRemoteScanner,
BaseHaScanner,
BluetoothManager,
BluetoothScanningMode,
HaScanner,
)
from homeassistant import config_entries
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, EVENT_LOGGING_CHANGED
@@ -326,7 +337,53 @@ class HomeAssistantBluetoothManager(BluetoothManager):
# Only handle repair issues for local adapters (HaScanner instances)
if not isinstance(scanner, HaScanner):
return
self.async_check_degraded_mode(scanner)
self.async_check_scanning_mode(scanner)
@hass_callback
def async_check_scanning_mode(self, scanner: HaScanner) -> None:
"""Check if the scanner is running in passive mode when active mode is requested."""
passive_mode_issue_id = f"bluetooth_adapter_passive_mode_{scanner.source}"
# Check if scanner is NOT in passive mode when active mode was requested
if not (
scanner.requested_mode is BluetoothScanningMode.ACTIVE
and scanner.current_mode is BluetoothScanningMode.PASSIVE
):
# Delete passive mode issue if it exists and we're not in passive fallback
ir.async_delete_issue(self.hass, DOMAIN, passive_mode_issue_id)
return
# Create repair issue for passive mode fallback
adapter_name = adapter_human_name(
scanner.adapter, scanner.mac_address or "00:00:00:00:00:00"
)
adapter_details = self._bluetooth_adapters.adapters.get(scanner.adapter)
model = adapter_model(adapter_details) if adapter_details else None
# Determine adapter type for specific instructions
# Default to USB for any other type or unknown
if adapter_details and adapter_details.get(ADAPTER_TYPE) == "uart":
translation_key = "bluetooth_adapter_passive_mode_uart"
else:
translation_key = "bluetooth_adapter_passive_mode_usb"
ir.async_create_issue(
self.hass,
DOMAIN,
passive_mode_issue_id,
is_fixable=False, # Requires a reboot or unplug
severity=ir.IssueSeverity.WARNING,
translation_key=translation_key,
translation_placeholders={
"adapter": adapter_name,
"model": model or "Unknown",
},
)
@hass_callback
def async_check_degraded_mode(self, scanner: HaScanner) -> None:
"""Check if we are in degraded mode and create/delete repair issues."""
issue_id = f"bluetooth_adapter_missing_permissions_{scanner.source}"
# Delete any existing issue if not in degraded mode

View File

@@ -43,6 +43,14 @@
"bluetooth_adapter_missing_permissions": {
"title": "Bluetooth adapter requires additional permissions",
"description": "The Bluetooth adapter **{adapter}** ({model}) is operating in degraded mode because your container needs additional permissions to fully access Bluetooth hardware.\n\nPlease follow the instructions in our documentation to add the required permissions:\n[Bluetooth permissions for Docker]({docs_url})\n\nAfter adding these permissions, restart your Home Assistant container for the changes to take effect."
},
"bluetooth_adapter_passive_mode_usb": {
"title": "Bluetooth USB adapter requires manual power cycle",
"description": "The Bluetooth adapter **{adapter}** ({model}) is stuck in passive scanning mode despite requesting active scanning mode. **Automatic recovery was attempted but failed.** This is likely a kernel, firmware, or operating system issue, and the adapter requires a manual power cycle to recover.\n\nIn passive mode, the adapter can only receive advertisements but cannot request additional data from devices, which will affect device discovery and functionality.\n\n**Manual intervention required:**\n1. **Unplug the USB adapter**\n2. Wait 5 seconds\n3. **Plug it back in**\n4. Wait for Home Assistant to detect the adapter\n\nIf the issue persists after power cycling:\n- Try a different USB port\n- Check for kernel/firmware updates\n- Consider using a different Bluetooth adapter"
},
"bluetooth_adapter_passive_mode_uart": {
"title": "Bluetooth adapter requires system power cycle",
"description": "The Bluetooth adapter **{adapter}** ({model}) is stuck in passive scanning mode despite requesting active scanning mode. **Automatic recovery was attempted but failed.** This is likely a kernel, firmware, or operating system issue, and the system requires a complete power cycle to recover the adapter.\n\nIn passive mode, the adapter can only receive advertisements but cannot request additional data from devices, which will affect device discovery and functionality.\n\n**Manual intervention required:**\n1. **Shut down the system completely** (not just a reboot)\n2. **Remove power** (unplug or turn off at the switch)\n3. Wait 10 seconds\n4. Restore power and boot the system\n\nIf the issue persists after power cycling:\n- Check for kernel/firmware updates\n- The onboard Bluetooth adapter may have hardware issues"
}
}
}

View File

@@ -8,7 +8,7 @@ from unittest.mock import patch
from bleak.backends.scanner import AdvertisementData, BLEDevice
from bluetooth_adapters import AdvertisementHistory
from freezegun import freeze_time
from habluetooth import HaScanner
from habluetooth import BluetoothScanningMode, HaScanner
# pylint: disable-next=no-name-in-module
from habluetooth.advertisement_tracker import TRACKER_BUFFERING_WOBBLE_SECONDS
@@ -21,7 +21,6 @@ from homeassistant.components.bluetooth import (
MONOTONIC_TIME,
BaseHaRemoteScanner,
BluetoothChange,
BluetoothScanningMode,
BluetoothServiceInfo,
BluetoothServiceInfoBleak,
HaBluetoothConnector,
@@ -1911,3 +1910,133 @@ async def test_no_repair_issue_for_remote_scanner(
and "bluetooth_adapter_missing_permissions" in issue.issue_id
]
assert len(issues) == 0
@pytest.mark.usefixtures("one_adapter")
async def test_repair_issue_created_for_passive_mode_fallback(
hass: HomeAssistant,
) -> None:
"""Test repair issue is created when scanner falls back to passive mode."""
assert await async_setup_component(hass, bluetooth.DOMAIN, {})
await hass.async_block_till_done()
manager = _get_manager()
scanner = HaScanner(
mode=BluetoothScanningMode.ACTIVE,
adapter="hci0",
address="00:11:22:33:44:55",
)
scanner.async_setup()
cancel = manager.async_register_scanner(scanner, connection_slots=1)
# Set scanner to passive mode when active was requested
scanner.set_requested_mode(BluetoothScanningMode.ACTIVE)
scanner.set_current_mode(BluetoothScanningMode.PASSIVE)
manager.on_scanner_start(scanner)
# Check repair issue is created
issue_id = f"bluetooth_adapter_passive_mode_{scanner.source}"
registry = ir.async_get(hass)
issue = registry.async_get_issue(bluetooth.DOMAIN, issue_id)
assert issue is not None
assert issue.severity == ir.IssueSeverity.WARNING
# Should default to USB translation key when adapter type is unknown
assert issue.translation_key == "bluetooth_adapter_passive_mode_usb"
assert not issue.is_fixable
cancel()
async def test_repair_issue_created_for_passive_mode_fallback_uart(
hass: HomeAssistant,
) -> None:
"""Test repair issue is created with UART-specific message for UART adapters."""
with patch(
"bluetooth_adapters.systems.linux.LinuxAdapters.adapters",
{
"hci0": {
"address": "00:11:22:33:44:55",
"sw_version": "homeassistant",
"hw_version": "uart:bcm2711",
"passive_scan": False,
"manufacturer": "Raspberry Pi",
"product": "BCM2711",
"adapter_type": "uart", # UART adapter type
}
},
):
assert await async_setup_component(hass, bluetooth.DOMAIN, {})
await hass.async_block_till_done()
manager = _get_manager()
scanner = HaScanner(
mode=BluetoothScanningMode.ACTIVE,
adapter="hci0",
address="00:11:22:33:44:55",
)
scanner.async_setup()
cancel = manager.async_register_scanner(scanner, connection_slots=1)
# Set scanner to passive mode when active was requested
scanner.set_requested_mode(BluetoothScanningMode.ACTIVE)
scanner.set_current_mode(BluetoothScanningMode.PASSIVE)
manager.on_scanner_start(scanner)
# Check repair issue is created with UART-specific translation key
issue_id = f"bluetooth_adapter_passive_mode_{scanner.source}"
registry = ir.async_get(hass)
issue = registry.async_get_issue(bluetooth.DOMAIN, issue_id)
assert issue is not None
assert issue.severity == ir.IssueSeverity.WARNING
assert issue.translation_key == "bluetooth_adapter_passive_mode_uart"
assert not issue.is_fixable
cancel()
@pytest.mark.usefixtures("one_adapter")
async def test_repair_issue_deleted_when_passive_mode_resolved(
hass: HomeAssistant,
) -> None:
"""Test repair issue is deleted when scanner no longer in passive mode."""
assert await async_setup_component(hass, bluetooth.DOMAIN, {})
await hass.async_block_till_done()
manager = _get_manager()
scanner = HaScanner(
mode=BluetoothScanningMode.ACTIVE,
adapter="hci0",
address="00:11:22:33:44:55",
)
scanner.async_setup()
cancel = manager.async_register_scanner(scanner, connection_slots=1)
# Initially set scanner to passive mode when active was requested
scanner.set_requested_mode(BluetoothScanningMode.ACTIVE)
scanner.set_current_mode(BluetoothScanningMode.PASSIVE)
manager.on_scanner_start(scanner)
# Check repair issue is created
issue_id = f"bluetooth_adapter_passive_mode_{scanner.source}"
registry = ir.async_get(hass)
issue = registry.async_get_issue(bluetooth.DOMAIN, issue_id)
assert issue is not None
# Now simulate scanner recovering to active mode
scanner.set_current_mode(BluetoothScanningMode.ACTIVE)
manager.on_scanner_start(scanner)
# Check repair issue is deleted
issue = registry.async_get_issue(bluetooth.DOMAIN, issue_id)
assert issue is None
cancel()