mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Add diagnostics to bluetooth (#77393)
This commit is contained in:
parent
15ad10643a
commit
8e88e039f7
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."""
|
"""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:
|
||||||
|
@ -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"],
|
||||||
|
@ -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."""
|
||||||
|
@ -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]
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
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…
x
Reference in New Issue
Block a user