Add diagnostics to bluetooth (#77393)
This commit is contained in:
parent
15ad10643a
commit
8e88e039f7
10 changed files with 213 additions and 5 deletions
28
homeassistant/components/bluetooth/diagnostics.py
Normal file
28
homeassistant/components/bluetooth/diagnostics.py
Normal 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
|
|
@ -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:
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
126
tests/components/bluetooth/test_diagnostics.py
Normal file
126
tests/components/bluetooth/test_diagnostics.py
Normal 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",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
Loading…
Add table
Reference in a new issue