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:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user