Add diagnostics to bluetooth (#77393)

This commit is contained in:
J. Nick Koston 2022-08-27 16:41:49 -05:00 committed by GitHub
parent 15ad10643a
commit 8e88e039f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 213 additions and 5 deletions

View File

@ -0,0 +1,28 @@
"""Diagnostics support for bluetooth."""
from __future__ import annotations
import platform
from typing import Any
from bluetooth_adapters import get_dbus_managed_objects
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from . import _get_manager
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
manager = _get_manager(hass)
manager_diagnostics = await manager.async_diagnostics()
adapters = await manager.async_get_bluetooth_adapters()
diagnostics = {
"manager": manager_diagnostics,
"adapters": adapters,
}
if platform.system() == "Linux":
diagnostics["dbus"] = await get_dbus_managed_objects()
return diagnostics

View File

@ -1,11 +1,13 @@
"""The bluetooth integration.""" """The bluetooth integration."""
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
from dataclasses import asdict
from datetime import datetime, timedelta from datetime import datetime, timedelta
import itertools import itertools
import logging import logging
from typing import TYPE_CHECKING, Final from typing import TYPE_CHECKING, Any, Final
from bleak.backends.scanner import AdvertisementDataCallback from bleak.backends.scanner import AdvertisementDataCallback
@ -145,6 +147,28 @@ class BluetoothManager:
self._connectable_scanners: list[BaseHaScanner] = [] self._connectable_scanners: list[BaseHaScanner] = []
self._adapters: dict[str, AdapterDetails] = {} self._adapters: dict[str, AdapterDetails] = {}
async def async_diagnostics(self) -> dict[str, Any]:
"""Diagnostics for the manager."""
scanner_diagnostics = await asyncio.gather(
*[
scanner.async_diagnostics()
for scanner in itertools.chain(
self._scanners, self._connectable_scanners
)
]
)
return {
"adapters": self._adapters,
"scanners": scanner_diagnostics,
"connectable_history": [
asdict(service_info)
for service_info in self._connectable_history.values()
],
"history": [
asdict(service_info) for service_info in self._history.values()
],
}
def _find_adapter_by_address(self, address: str) -> str | None: def _find_adapter_by_address(self, address: str) -> str | None:
for adapter, details in self._adapters.items(): for adapter, details in self._adapters.items():
if details[ADAPTER_ADDRESS] == address: if details[ADAPTER_ADDRESS] == address:

View File

@ -6,7 +6,7 @@
"quality_scale": "internal", "quality_scale": "internal",
"requirements": [ "requirements": [
"bleak==0.15.1", "bleak==0.15.1",
"bluetooth-adapters==0.2.0", "bluetooth-adapters==0.3.2",
"bluetooth-auto-recovery==0.2.2" "bluetooth-auto-recovery==0.2.2"
], ],
"codeowners": ["@bdraco"], "codeowners": ["@bdraco"],

View File

@ -70,6 +70,20 @@ class BaseHaScanner:
def discovered_devices(self) -> list[BLEDevice]: def discovered_devices(self) -> list[BLEDevice]:
"""Return a list of discovered devices.""" """Return a list of discovered devices."""
async def async_diagnostics(self) -> dict[str, Any]:
"""Return diagnostic information about the scanner."""
return {
"type": self.__class__.__name__,
"discovered_devices": [
{
"name": device.name,
"address": device.address,
"rssi": device.rssi,
}
for device in self.discovered_devices
],
}
class HaBleakScannerWrapper(BaseBleakScanner): class HaBleakScannerWrapper(BaseBleakScanner):
"""A wrapper that uses the single instance.""" """A wrapper that uses the single instance."""

View File

@ -146,6 +146,17 @@ class HaScanner(BaseHaScanner):
"""Return a list of discovered devices.""" """Return a list of discovered devices."""
return self.scanner.discovered_devices return self.scanner.discovered_devices
async def async_diagnostics(self) -> dict[str, Any]:
"""Return diagnostic information about the scanner."""
base_diag = await super().async_diagnostics()
return base_diag | {
"adapter": self.adapter,
"source": self.source,
"name": self.name,
"last_detection": self._last_detection,
"start_time": self._start_time,
}
@hass_callback @hass_callback
def async_register_callback( def async_register_callback(
self, callback: Callable[[BluetoothServiceInfoBleak], None] self, callback: Callable[[BluetoothServiceInfoBleak], None]

View File

@ -11,7 +11,7 @@ attrs==21.2.0
awesomeversion==22.6.0 awesomeversion==22.6.0
bcrypt==3.1.7 bcrypt==3.1.7
bleak==0.15.1 bleak==0.15.1
bluetooth-adapters==0.2.0 bluetooth-adapters==0.3.2
bluetooth-auto-recovery==0.2.2 bluetooth-auto-recovery==0.2.2
certifi>=2021.5.30 certifi>=2021.5.30
ciso8601==2.2.0 ciso8601==2.2.0

View File

@ -424,7 +424,7 @@ blockchain==1.4.4
# bluepy==1.3.0 # bluepy==1.3.0
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-adapters==0.2.0 bluetooth-adapters==0.3.2
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-auto-recovery==0.2.2 bluetooth-auto-recovery==0.2.2

View File

@ -335,7 +335,7 @@ blebox_uniapi==2.0.2
blinkpy==0.19.0 blinkpy==0.19.0
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-adapters==0.2.0 bluetooth-adapters==0.3.2
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bluetooth-auto-recovery==0.2.2 bluetooth-auto-recovery==0.2.2

View File

@ -53,6 +53,11 @@ def one_adapter_fixture():
def two_adapters_fixture(): def two_adapters_fixture():
"""Fixture that mocks two adapters on Linux.""" """Fixture that mocks two adapters on Linux."""
with patch( with patch(
"homeassistant.components.bluetooth.platform.system", return_value="Linux"
), patch(
"homeassistant.components.bluetooth.scanner.platform.system",
return_value="Linux",
), patch(
"homeassistant.components.bluetooth.util.platform.system", return_value="Linux" "homeassistant.components.bluetooth.util.platform.system", return_value="Linux"
), patch( ), patch(
"bluetooth_adapters.get_bluetooth_adapter_details", "bluetooth_adapters.get_bluetooth_adapter_details",

View File

@ -0,0 +1,126 @@
"""Test bluetooth diagnostics."""
from unittest.mock import ANY, patch
from bleak.backends.scanner import BLEDevice
from homeassistant.components import bluetooth
from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
async def test_diagnostics(
hass, hass_client, mock_bleak_scanner_start, enable_bluetooth, two_adapters
):
"""Test we can setup and unsetup bluetooth with multiple adapters."""
# Normally we do not want to patch our classes, but since bleak will import
# a different scanner based on the operating system, we need to patch here
# because we cannot import the scanner class directly without it throwing an
# error if the test is not running on linux since we won't have the correct
# deps installed when testing on MacOS.
with patch(
"homeassistant.components.bluetooth.scanner.HaScanner.discovered_devices",
[BLEDevice(name="x", rssi=-60, address="44:44:33:11:23:45")],
), patch(
"homeassistant.components.bluetooth.diagnostics.platform.system",
return_value="Linux",
), patch(
"homeassistant.components.bluetooth.diagnostics.get_dbus_managed_objects",
return_value={
"org.bluez": {
"/org/bluez/hci0": {
"Interfaces": {"org.bluez.Adapter1": {"Discovering": False}}
}
}
},
):
entry1 = MockConfigEntry(
domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01"
)
entry1.add_to_hass(hass)
entry2 = MockConfigEntry(
domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:02"
)
entry2.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry1.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_setup(entry2.entry_id)
await hass.async_block_till_done()
diag = await get_diagnostics_for_config_entry(hass, hass_client, entry1)
assert diag == {
"adapters": {
"hci0": {
"address": "00:00:00:00:00:01",
"hw_version": "usbid:1234",
"sw_version": "BlueZ 4.63",
},
"hci1": {
"address": "00:00:00:00:00:02",
"hw_version": "usbid:1234",
"sw_version": "BlueZ 4.63",
},
},
"dbus": {
"org.bluez": {
"/org/bluez/hci0": {
"Interfaces": {"org.bluez.Adapter1": {"Discovering": False}}
}
}
},
"manager": {
"adapters": {
"hci0": {
"address": "00:00:00:00:00:01",
"hw_version": "usbid:1234",
"sw_version": "BlueZ 4.63",
},
"hci1": {
"address": "00:00:00:00:00:02",
"hw_version": "usbid:1234",
"sw_version": "BlueZ 4.63",
},
},
"connectable_history": [],
"history": [],
"scanners": [
{
"adapter": "hci0",
"discovered_devices": [
{"address": "44:44:33:11:23:45", "name": "x", "rssi": -60}
],
"last_detection": ANY,
"name": "hci0 (00:00:00:00:00:01)",
"source": "hci0",
"start_time": ANY,
"type": "HaScanner",
},
{
"adapter": "hci0",
"discovered_devices": [
{"address": "44:44:33:11:23:45", "name": "x", "rssi": -60}
],
"last_detection": ANY,
"name": "hci0 (00:00:00:00:00:01)",
"source": "hci0",
"start_time": ANY,
"type": "HaScanner",
},
{
"adapter": "hci1",
"discovered_devices": [
{"address": "44:44:33:11:23:45", "name": "x", "rssi": -60}
],
"last_detection": ANY,
"name": "hci1 (00:00:00:00:00:02)",
"source": "hci1",
"start_time": ANY,
"type": "HaScanner",
},
],
},
}