hass-core/tests/components/bluetooth/test_diagnostics.py
J. Nick Koston 2a5ffa9a5b
Fix timestamps for bluetooth scanners that bundle advertisements (#94511)
#94138 added support for raw/bundled advertisements. We should use the
same monotonic time for all advertisements in the bundle if not time
is passed, or calculate the timestamp and pass it if its known
2023-06-14 21:47:00 -04:00

648 lines
26 KiB
Python

"""Test bluetooth diagnostics."""
from unittest.mock import ANY, MagicMock, patch
from bleak.backends.scanner import AdvertisementData, BLEDevice
from bluetooth_adapters import DEFAULT_ADDRESS
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
MONOTONIC_TIME,
BaseHaRemoteScanner,
HaBluetoothConnector,
)
from homeassistant.core import HomeAssistant
from . import (
MockBleakClient,
_get_manager,
generate_advertisement_data,
generate_ble_device,
inject_advertisement,
)
from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator
async def test_diagnostics(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_bleak_scanner_start: MagicMock,
enable_bluetooth: None,
two_adapters: None,
) -> None:
"""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_and_advertisement_data",
{
"44:44:33:11:23:45": (
generate_ble_device(name="x", rssi=-127, address="44:44:33:11:23:45"),
generate_advertisement_data(local_name="x"),
)
},
), 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": {
"org.bluez.Adapter1": {
"Name": "BlueZ 5.63",
"Alias": "BlueZ 5.63",
"Modalias": "usb:v1D6Bp0246d0540",
"Discovering": False,
},
"org.bluez.AdvertisementMonitorManager1": {
"SupportedMonitorTypes": ["or_patterns"],
"SupportedFeatures": [],
},
}
}
},
):
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": "usb:v1D6Bp0246d053F",
"passive_scan": False,
"sw_version": "homeassistant",
"manufacturer": "ACME",
"product": "Bluetooth Adapter 5.0",
"product_id": "aa01",
"vendor_id": "cc01",
"connection_slots": 1,
},
"hci1": {
"address": "00:00:00:00:00:02",
"hw_version": "usb:v1D6Bp0246d053F",
"passive_scan": True,
"sw_version": "homeassistant",
"manufacturer": "ACME",
"product": "Bluetooth Adapter 5.0",
"product_id": "aa01",
"vendor_id": "cc01",
"connection_slots": 2,
},
},
"dbus": {
"org.bluez": {
"/org/bluez/hci0": {
"org.bluez.Adapter1": {
"Alias": "BlueZ 5.63",
"Discovering": False,
"Modalias": "usb:v1D6Bp0246d0540",
"Name": "BlueZ 5.63",
},
"org.bluez.AdvertisementMonitorManager1": {
"SupportedFeatures": [],
"SupportedMonitorTypes": ["or_patterns"],
},
}
}
},
"manager": {
"slot_manager": {
"adapter_slots": {"hci0": 5, "hci1": 2},
"allocations_by_adapter": {"hci0": [], "hci1": []},
"manager": False,
},
"adapters": {
"hci0": {
"address": "00:00:00:00:00:01",
"hw_version": "usb:v1D6Bp0246d053F",
"passive_scan": False,
"sw_version": "homeassistant",
"manufacturer": "ACME",
"product": "Bluetooth Adapter 5.0",
"product_id": "aa01",
"vendor_id": "cc01",
"connection_slots": 1,
},
"hci1": {
"address": "00:00:00:00:00:02",
"hw_version": "usb:v1D6Bp0246d053F",
"passive_scan": True,
"sw_version": "homeassistant",
"manufacturer": "ACME",
"product": "Bluetooth Adapter 5.0",
"product_id": "aa01",
"vendor_id": "cc01",
"connection_slots": 2,
},
},
"advertisement_tracker": {
"intervals": {},
"sources": {},
"timings": {},
},
"connectable_history": [],
"all_history": [],
"scanners": [
{
"adapter": "hci0",
"discovered_devices_and_advertisement_data": [
{
"address": "44:44:33:11:23:45",
"advertisement_data": [
"x",
{},
{},
[],
-127,
-127,
[[]],
],
"details": None,
"name": "x",
"rssi": -127,
}
],
"last_detection": ANY,
"monotonic_time": ANY,
"name": "hci0 (00:00:00:00:00:01)",
"scanning": True,
"source": "00:00:00:00:00:01",
"start_time": ANY,
"type": "HaScanner",
},
{
"adapter": "hci0",
"discovered_devices_and_advertisement_data": [
{
"address": "44:44:33:11:23:45",
"advertisement_data": [
"x",
{},
{},
[],
-127,
-127,
[[]],
],
"details": None,
"name": "x",
"rssi": -127,
}
],
"last_detection": ANY,
"monotonic_time": ANY,
"name": "hci0 (00:00:00:00:00:01)",
"scanning": True,
"source": "00:00:00:00:00:01",
"start_time": ANY,
"type": "HaScanner",
},
{
"adapter": "hci1",
"discovered_devices_and_advertisement_data": [
{
"address": "44:44:33:11:23:45",
"advertisement_data": [
"x",
{},
{},
[],
-127,
-127,
[[]],
],
"details": None,
"name": "x",
"rssi": -127,
}
],
"last_detection": ANY,
"monotonic_time": ANY,
"name": "hci1 (00:00:00:00:00:02)",
"scanning": True,
"source": "00:00:00:00:00:02",
"start_time": ANY,
"type": "HaScanner",
},
],
},
}
async def test_diagnostics_macos(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_bleak_scanner_start: MagicMock,
mock_bluetooth_adapters: None,
macos_adapter,
) -> None:
"""Test diagnostics for macos."""
# 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.
switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
switchbot_adv = generate_advertisement_data(
local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}
)
with patch(
"homeassistant.components.bluetooth.scanner.HaScanner.discovered_devices_and_advertisement_data",
{
"44:44:33:11:23:45": (
generate_ble_device(name="x", rssi=-127, address="44:44:33:11:23:45"),
switchbot_adv,
)
},
), patch(
"homeassistant.components.bluetooth.diagnostics.platform.system",
return_value="Darwin",
), patch(
"homeassistant.components.bluetooth.diagnostics.get_dbus_managed_objects",
return_value={},
):
entry1 = MockConfigEntry(
domain=bluetooth.DOMAIN,
data={},
title="Core Bluetooth",
unique_id=DEFAULT_ADDRESS,
)
entry1.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry1.entry_id)
await hass.async_block_till_done()
inject_advertisement(hass, switchbot_device, switchbot_adv)
diag = await get_diagnostics_for_config_entry(hass, hass_client, entry1)
assert diag == {
"adapters": {
"Core Bluetooth": {
"address": "00:00:00:00:00:00",
"passive_scan": False,
"sw_version": ANY,
"manufacturer": "Apple",
"product": "Unknown MacOS Model",
"product_id": "Unknown",
"vendor_id": "Unknown",
}
},
"manager": {
"slot_manager": {
"adapter_slots": {"Core Bluetooth": 5},
"allocations_by_adapter": {"Core Bluetooth": []},
"manager": False,
},
"adapters": {
"Core Bluetooth": {
"address": "00:00:00:00:00:00",
"passive_scan": False,
"sw_version": ANY,
"manufacturer": "Apple",
"product": "Unknown MacOS Model",
"product_id": "Unknown",
"vendor_id": "Unknown",
}
},
"advertisement_tracker": {
"intervals": {},
"sources": {"44:44:33:11:23:45": "local"},
"timings": {"44:44:33:11:23:45": [ANY]},
},
"connectable_history": [
{
"address": "44:44:33:11:23:45",
"advertisement": [
"wohand",
{"1": {"__type": "<class 'bytes'>", "repr": "b'\\x01'"}},
{},
[],
-127,
-127,
[[]],
],
"device": {
"__type": "<class 'bleak.backends.device.BLEDevice'>",
"repr": "BLEDevice(44:44:33:11:23:45, wohand)",
},
"connectable": True,
"manufacturer_data": {
"1": {"__type": "<class 'bytes'>", "repr": "b'\\x01'"}
},
"name": "wohand",
"rssi": -127,
"service_data": {},
"service_uuids": [],
"source": "local",
"time": ANY,
}
],
"all_history": [
{
"address": "44:44:33:11:23:45",
"advertisement": [
"wohand",
{"1": {"__type": "<class 'bytes'>", "repr": "b'\\x01'"}},
{},
[],
-127,
-127,
[[]],
],
"device": {
"__type": "<class 'bleak.backends.device.BLEDevice'>",
"repr": "BLEDevice(44:44:33:11:23:45, wohand)",
},
"connectable": True,
"manufacturer_data": {
"1": {"__type": "<class 'bytes'>", "repr": "b'\\x01'"}
},
"name": "wohand",
"rssi": -127,
"service_data": {},
"service_uuids": [],
"source": "local",
"time": ANY,
}
],
"scanners": [
{
"adapter": "Core Bluetooth",
"discovered_devices_and_advertisement_data": [
{
"address": "44:44:33:11:23:45",
"advertisement_data": [
"wohand",
{
"1": {
"__type": "<class 'bytes'>",
"repr": "b'\\x01'",
}
},
{},
[],
-127,
-127,
[[]],
],
"details": None,
"name": "x",
"rssi": -127,
}
],
"last_detection": ANY,
"monotonic_time": ANY,
"name": "Core Bluetooth",
"scanning": True,
"source": "Core Bluetooth",
"start_time": ANY,
"type": "HaScanner",
}
],
},
}
async def test_diagnostics_remote_adapter(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_bleak_scanner_start: MagicMock,
mock_bluetooth_adapters: None,
enable_bluetooth: None,
one_adapter: None,
) -> None:
"""Test diagnostics for remote adapter."""
manager = _get_manager()
switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand")
switchbot_adv = generate_advertisement_data(
local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}
)
class FakeScanner(BaseHaRemoteScanner):
def inject_advertisement(
self, device: BLEDevice, advertisement_data: AdvertisementData
) -> None:
"""Inject an advertisement."""
self._async_on_advertisement(
device.address,
advertisement_data.rssi,
device.name,
advertisement_data.service_uuids,
advertisement_data.service_data,
advertisement_data.manufacturer_data,
advertisement_data.tx_power,
{"scanner_specific_data": "test"},
MONOTONIC_TIME(),
)
with patch(
"homeassistant.components.bluetooth.diagnostics.platform.system",
return_value="Linux",
), patch(
"homeassistant.components.bluetooth.diagnostics.get_dbus_managed_objects",
return_value={},
):
entry1 = MockConfigEntry(
domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01"
)
entry1.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry1.entry_id)
await hass.async_block_till_done()
new_info_callback = manager.scanner_adv_received
connector = (
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
)
scanner = FakeScanner(
hass, "esp32", "esp32", new_info_callback, connector, False
)
unsetup = scanner.async_setup()
cancel = manager.async_register_scanner(scanner, True)
scanner.inject_advertisement(switchbot_device, switchbot_adv)
inject_advertisement(hass, switchbot_device, switchbot_adv)
diag = await get_diagnostics_for_config_entry(hass, hass_client, entry1)
assert diag == {
"adapters": {
"hci0": {
"address": "00:00:00:00:00:01",
"hw_version": "usb:v1D6Bp0246d053F",
"manufacturer": "ACME",
"passive_scan": False,
"product": "Bluetooth Adapter 5.0",
"product_id": "aa01",
"sw_version": "homeassistant",
"vendor_id": "cc01",
}
},
"dbus": {},
"manager": {
"slot_manager": {
"adapter_slots": {"hci0": 5},
"allocations_by_adapter": {"hci0": []},
"manager": False,
},
"adapters": {
"hci0": {
"address": "00:00:00:00:00:01",
"hw_version": "usb:v1D6Bp0246d053F",
"manufacturer": "ACME",
"passive_scan": False,
"product": "Bluetooth Adapter 5.0",
"product_id": "aa01",
"sw_version": "homeassistant",
"vendor_id": "cc01",
}
},
"advertisement_tracker": {
"intervals": {},
"sources": {"44:44:33:11:23:45": "esp32"},
"timings": {"44:44:33:11:23:45": [ANY]},
},
"all_history": [
{
"address": "44:44:33:11:23:45",
"advertisement": [
"wohand",
{"1": {"__type": "<class 'bytes'>", "repr": "b'\\x01'"}},
{},
[],
-127,
-127,
[],
],
"connectable": False,
"device": {
"__type": "<class 'bleak.backends.device.BLEDevice'>",
"repr": "BLEDevice(44:44:33:11:23:45, wohand)",
},
"manufacturer_data": {
"1": {"__type": "<class 'bytes'>", "repr": "b'\\x01'"}
},
"name": "wohand",
"rssi": -127,
"service_data": {},
"service_uuids": [],
"source": "esp32",
"time": ANY,
}
],
"connectable_history": [
{
"address": "44:44:33:11:23:45",
"advertisement": [
"wohand",
{"1": {"__type": "<class 'bytes'>", "repr": "b'\\x01'"}},
{},
[],
-127,
-127,
[[]],
],
"connectable": True,
"device": {
"__type": "<class 'bleak.backends.device.BLEDevice'>",
"repr": "BLEDevice(44:44:33:11:23:45, wohand)",
},
"manufacturer_data": {
"1": {"__type": "<class 'bytes'>", "repr": "b'\\x01'"}
},
"name": "wohand",
"rssi": -127,
"service_data": {},
"service_uuids": [],
"source": "local",
"time": ANY,
}
],
"scanners": [
{
"adapter": "hci0",
"discovered_devices_and_advertisement_data": [],
"last_detection": ANY,
"monotonic_time": ANY,
"name": "hci0 (00:00:00:00:00:01)",
"scanning": True,
"source": "00:00:00:00:00:01",
"start_time": ANY,
"type": "HaScanner",
},
{
"adapter": "hci0",
"discovered_devices_and_advertisement_data": [],
"last_detection": ANY,
"monotonic_time": ANY,
"name": "hci0 (00:00:00:00:00:01)",
"scanning": True,
"source": "00:00:00:00:00:01",
"start_time": ANY,
"type": "HaScanner",
},
{
"connectable": False,
"discovered_device_timestamps": {"44:44:33:11:23:45": ANY},
"time_since_last_device_detection": {"44:44:33:11:23:45": ANY},
"discovered_devices_and_advertisement_data": [
{
"address": "44:44:33:11:23:45",
"advertisement_data": [
"wohand",
{
"1": {
"__type": "<class 'bytes'>",
"repr": "b'\\x01'",
}
},
{},
[],
-127,
-127,
[],
],
"details": {
"scanner_specific_data": "test",
"source": "esp32",
},
"name": "wohand",
"rssi": -127,
}
],
"last_detection": ANY,
"monotonic_time": ANY,
"name": "esp32",
"scanning": True,
"source": "esp32",
"storage": None,
"type": "FakeScanner",
"start_time": ANY,
},
],
},
}
cancel()
unsetup()