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."""
from __future__ import annotations
import asyncio
from collections.abc import Callable, Iterable
from dataclasses import asdict
from datetime import datetime, timedelta
import itertools
import logging
from typing import TYPE_CHECKING, Final
from typing import TYPE_CHECKING, Any, Final
from bleak.backends.scanner import AdvertisementDataCallback
@ -145,6 +147,28 @@ class BluetoothManager:
self._connectable_scanners: list[BaseHaScanner] = []
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:
for adapter, details in self._adapters.items():
if details[ADAPTER_ADDRESS] == address:

View file

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

View file

@ -70,6 +70,20 @@ class BaseHaScanner:
def discovered_devices(self) -> list[BLEDevice]:
"""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):
"""A wrapper that uses the single instance."""

View file

@ -146,6 +146,17 @@ class HaScanner(BaseHaScanner):
"""Return a list of 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
def async_register_callback(
self, callback: Callable[[BluetoothServiceInfoBleak], None]

View file

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

View file

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

View file

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

View file

@ -53,6 +53,11 @@ def one_adapter_fixture():
def two_adapters_fixture():
"""Fixture that mocks two adapters on Linux."""
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"
), patch(
"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",
},
],
},
}