2022-11-20 08:44:28 -06:00
|
|
|
"""Tests for the Bluetooth base scanner models."""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
from datetime import timedelta
|
|
|
|
import time
|
|
|
|
from unittest.mock import patch
|
|
|
|
|
|
|
|
from bleak.backends.device import BLEDevice
|
|
|
|
from bleak.backends.scanner import AdvertisementData
|
|
|
|
|
|
|
|
from homeassistant.components.bluetooth import BaseHaRemoteScanner, HaBluetoothConnector
|
|
|
|
from homeassistant.components.bluetooth.const import (
|
|
|
|
CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
|
|
|
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
|
|
|
)
|
|
|
|
import homeassistant.util.dt as dt_util
|
|
|
|
|
|
|
|
from . import MockBleakClient, _get_manager, generate_advertisement_data
|
|
|
|
|
|
|
|
from tests.common import async_fire_time_changed
|
|
|
|
|
|
|
|
|
2022-11-21 20:23:07 -06:00
|
|
|
async def test_remote_scanner(hass, enable_bluetooth):
|
2022-11-20 08:44:28 -06:00
|
|
|
"""Test the remote scanner base class merges advertisement_data."""
|
|
|
|
manager = _get_manager()
|
|
|
|
|
|
|
|
switchbot_device = BLEDevice(
|
|
|
|
"44:44:33:11:23:45",
|
|
|
|
"wohand",
|
|
|
|
{},
|
|
|
|
rssi=-100,
|
|
|
|
)
|
|
|
|
switchbot_device_adv = generate_advertisement_data(
|
|
|
|
local_name="wohand",
|
|
|
|
service_uuids=["050a021a-0000-1000-8000-00805f9b34fb"],
|
|
|
|
service_data={"050a021a-0000-1000-8000-00805f9b34fb": b"\n\xff"},
|
|
|
|
manufacturer_data={1: b"\x01"},
|
|
|
|
rssi=-100,
|
|
|
|
)
|
|
|
|
switchbot_device_2 = BLEDevice(
|
|
|
|
"44:44:33:11:23:45",
|
|
|
|
"w",
|
|
|
|
{},
|
|
|
|
rssi=-100,
|
|
|
|
)
|
|
|
|
switchbot_device_adv_2 = generate_advertisement_data(
|
|
|
|
local_name="wohand",
|
|
|
|
service_uuids=["00000001-0000-1000-8000-00805f9b34fb"],
|
|
|
|
service_data={"00000001-0000-1000-8000-00805f9b34fb": b"\n\xff"},
|
|
|
|
manufacturer_data={1: b"\x01", 2: b"\x02"},
|
|
|
|
rssi=-100,
|
|
|
|
)
|
|
|
|
|
|
|
|
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,
|
2022-11-28 18:54:53 -10:00
|
|
|
{"scanner_specific_data": "test"},
|
2022-11-20 08:44:28 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
new_info_callback = manager.scanner_adv_received
|
|
|
|
connector = (
|
|
|
|
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
|
|
|
|
)
|
2022-11-21 20:23:07 -06:00
|
|
|
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, True)
|
2022-11-20 08:44:28 -06:00
|
|
|
scanner.async_setup()
|
|
|
|
cancel = manager.async_register_scanner(scanner, True)
|
|
|
|
|
|
|
|
scanner.inject_advertisement(switchbot_device, switchbot_device_adv)
|
|
|
|
|
|
|
|
data = scanner.discovered_devices_and_advertisement_data
|
|
|
|
discovered_device, discovered_adv_data = data[switchbot_device.address]
|
|
|
|
assert discovered_device.address == switchbot_device.address
|
|
|
|
assert discovered_device.name == switchbot_device.name
|
|
|
|
assert (
|
|
|
|
discovered_adv_data.manufacturer_data == switchbot_device_adv.manufacturer_data
|
|
|
|
)
|
|
|
|
assert discovered_adv_data.service_data == switchbot_device_adv.service_data
|
|
|
|
assert discovered_adv_data.service_uuids == switchbot_device_adv.service_uuids
|
|
|
|
scanner.inject_advertisement(switchbot_device_2, switchbot_device_adv_2)
|
|
|
|
|
|
|
|
data = scanner.discovered_devices_and_advertisement_data
|
|
|
|
discovered_device, discovered_adv_data = data[switchbot_device.address]
|
|
|
|
assert discovered_device.address == switchbot_device.address
|
|
|
|
assert discovered_device.name == switchbot_device.name
|
|
|
|
assert discovered_adv_data.manufacturer_data == {1: b"\x01", 2: b"\x02"}
|
|
|
|
assert discovered_adv_data.service_data == {
|
|
|
|
"050a021a-0000-1000-8000-00805f9b34fb": b"\n\xff",
|
|
|
|
"00000001-0000-1000-8000-00805f9b34fb": b"\n\xff",
|
|
|
|
}
|
|
|
|
assert set(discovered_adv_data.service_uuids) == {
|
|
|
|
"050a021a-0000-1000-8000-00805f9b34fb",
|
|
|
|
"00000001-0000-1000-8000-00805f9b34fb",
|
|
|
|
}
|
|
|
|
|
|
|
|
cancel()
|
|
|
|
|
|
|
|
|
2022-11-21 20:23:07 -06:00
|
|
|
async def test_remote_scanner_expires_connectable(hass, enable_bluetooth):
|
2022-11-20 08:44:28 -06:00
|
|
|
"""Test the remote scanner expires stale connectable data."""
|
|
|
|
manager = _get_manager()
|
|
|
|
|
|
|
|
switchbot_device = BLEDevice(
|
|
|
|
"44:44:33:11:23:45",
|
|
|
|
"wohand",
|
|
|
|
{},
|
|
|
|
rssi=-100,
|
|
|
|
)
|
|
|
|
switchbot_device_adv = generate_advertisement_data(
|
|
|
|
local_name="wohand",
|
|
|
|
service_uuids=[],
|
|
|
|
manufacturer_data={1: b"\x01"},
|
|
|
|
rssi=-100,
|
|
|
|
)
|
|
|
|
|
|
|
|
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,
|
2022-11-28 18:54:53 -10:00
|
|
|
{"scanner_specific_data": "test"},
|
2022-11-20 08:44:28 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
new_info_callback = manager.scanner_adv_received
|
|
|
|
connector = (
|
|
|
|
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
|
|
|
|
)
|
2022-11-21 20:23:07 -06:00
|
|
|
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, True)
|
2022-11-20 08:44:28 -06:00
|
|
|
scanner.async_setup()
|
|
|
|
cancel = manager.async_register_scanner(scanner, True)
|
|
|
|
|
|
|
|
start_time_monotonic = time.monotonic()
|
|
|
|
scanner.inject_advertisement(switchbot_device, switchbot_device_adv)
|
|
|
|
|
|
|
|
devices = scanner.discovered_devices
|
|
|
|
assert len(scanner.discovered_devices) == 1
|
|
|
|
assert len(scanner.discovered_devices_and_advertisement_data) == 1
|
|
|
|
assert devices[0].name == "wohand"
|
|
|
|
|
|
|
|
expire_monotonic = (
|
|
|
|
start_time_monotonic
|
|
|
|
+ CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
|
|
|
|
+ 1
|
|
|
|
)
|
|
|
|
expire_utc = dt_util.utcnow() + timedelta(
|
|
|
|
seconds=CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1
|
|
|
|
)
|
|
|
|
with patch(
|
|
|
|
"homeassistant.components.bluetooth.base_scanner.MONOTONIC_TIME",
|
|
|
|
return_value=expire_monotonic,
|
|
|
|
):
|
|
|
|
async_fire_time_changed(hass, expire_utc)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
|
|
|
|
devices = scanner.discovered_devices
|
|
|
|
assert len(scanner.discovered_devices) == 0
|
|
|
|
assert len(scanner.discovered_devices_and_advertisement_data) == 0
|
|
|
|
|
|
|
|
cancel()
|
|
|
|
|
|
|
|
|
2022-11-21 20:23:07 -06:00
|
|
|
async def test_remote_scanner_expires_non_connectable(hass, enable_bluetooth):
|
2022-11-20 08:44:28 -06:00
|
|
|
"""Test the remote scanner expires stale non connectable data."""
|
|
|
|
manager = _get_manager()
|
|
|
|
|
|
|
|
switchbot_device = BLEDevice(
|
|
|
|
"44:44:33:11:23:45",
|
|
|
|
"wohand",
|
|
|
|
{},
|
|
|
|
rssi=-100,
|
|
|
|
)
|
|
|
|
switchbot_device_adv = generate_advertisement_data(
|
|
|
|
local_name="wohand",
|
|
|
|
service_uuids=[],
|
|
|
|
manufacturer_data={1: b"\x01"},
|
|
|
|
rssi=-100,
|
|
|
|
)
|
|
|
|
|
|
|
|
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,
|
2022-11-28 18:54:53 -10:00
|
|
|
{"scanner_specific_data": "test"},
|
2022-11-20 08:44:28 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
new_info_callback = manager.scanner_adv_received
|
|
|
|
connector = (
|
|
|
|
HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False),
|
|
|
|
)
|
2022-11-21 20:23:07 -06:00
|
|
|
scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False)
|
2022-11-20 08:44:28 -06:00
|
|
|
scanner.async_setup()
|
|
|
|
cancel = manager.async_register_scanner(scanner, True)
|
|
|
|
|
|
|
|
start_time_monotonic = time.monotonic()
|
|
|
|
scanner.inject_advertisement(switchbot_device, switchbot_device_adv)
|
|
|
|
|
|
|
|
devices = scanner.discovered_devices
|
|
|
|
assert len(scanner.discovered_devices) == 1
|
|
|
|
assert len(scanner.discovered_devices_and_advertisement_data) == 1
|
|
|
|
assert devices[0].name == "wohand"
|
|
|
|
|
|
|
|
assert (
|
|
|
|
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
|
|
|
|
> CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
|
|
|
|
)
|
|
|
|
|
|
|
|
# The connectable timeout is not used for non connectable devices
|
|
|
|
expire_monotonic = (
|
|
|
|
start_time_monotonic
|
|
|
|
+ CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
|
|
|
|
+ 1
|
|
|
|
)
|
|
|
|
expire_utc = dt_util.utcnow() + timedelta(
|
|
|
|
seconds=CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1
|
|
|
|
)
|
|
|
|
with patch(
|
|
|
|
"homeassistant.components.bluetooth.base_scanner.MONOTONIC_TIME",
|
|
|
|
return_value=expire_monotonic,
|
|
|
|
):
|
|
|
|
async_fire_time_changed(hass, expire_utc)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
|
|
|
|
assert len(scanner.discovered_devices) == 1
|
|
|
|
assert len(scanner.discovered_devices_and_advertisement_data) == 1
|
|
|
|
|
|
|
|
# The non connectable timeout is used for non connectable devices
|
|
|
|
# which is always longer than the connectable timeout
|
|
|
|
expire_monotonic = (
|
|
|
|
start_time_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1
|
|
|
|
)
|
|
|
|
expire_utc = dt_util.utcnow() + timedelta(
|
|
|
|
seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1
|
|
|
|
)
|
|
|
|
with patch(
|
|
|
|
"homeassistant.components.bluetooth.base_scanner.MONOTONIC_TIME",
|
|
|
|
return_value=expire_monotonic,
|
|
|
|
):
|
|
|
|
async_fire_time_changed(hass, expire_utc)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
|
|
|
|
assert len(scanner.discovered_devices) == 0
|
|
|
|
assert len(scanner.discovered_devices_and_advertisement_data) == 0
|
|
|
|
|
|
|
|
cancel()
|
2022-11-21 20:23:07 -06:00
|
|
|
|
|
|
|
|
|
|
|
async def test_base_scanner_connecting_behavior(hass, enable_bluetooth):
|
|
|
|
"""Test that the default behavior is to mark the scanner as not scanning when connecting."""
|
|
|
|
manager = _get_manager()
|
|
|
|
|
|
|
|
switchbot_device = BLEDevice(
|
|
|
|
"44:44:33:11:23:45",
|
|
|
|
"wohand",
|
|
|
|
{},
|
|
|
|
rssi=-100,
|
|
|
|
)
|
|
|
|
switchbot_device_adv = generate_advertisement_data(
|
|
|
|
local_name="wohand",
|
|
|
|
service_uuids=[],
|
|
|
|
manufacturer_data={1: b"\x01"},
|
|
|
|
rssi=-100,
|
|
|
|
)
|
|
|
|
|
|
|
|
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,
|
2022-11-28 18:54:53 -10:00
|
|
|
{"scanner_specific_data": "test"},
|
2022-11-21 20:23:07 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
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)
|
|
|
|
scanner.async_setup()
|
|
|
|
cancel = manager.async_register_scanner(scanner, True)
|
|
|
|
|
|
|
|
with scanner.connecting():
|
|
|
|
assert scanner.scanning is False
|
|
|
|
|
|
|
|
# We should still accept new advertisements while connecting
|
|
|
|
# since advertisements are delivered asynchronously and
|
|
|
|
# we don't want to miss any even when we are willing to
|
|
|
|
# accept advertisements from another scanner in the brief window
|
|
|
|
# between when we start connecting and when we stop scanning
|
|
|
|
scanner.inject_advertisement(switchbot_device, switchbot_device_adv)
|
|
|
|
|
|
|
|
devices = scanner.discovered_devices
|
|
|
|
assert len(scanner.discovered_devices) == 1
|
|
|
|
assert len(scanner.discovered_devices_and_advertisement_data) == 1
|
|
|
|
assert devices[0].name == "wohand"
|
|
|
|
|
|
|
|
cancel()
|