core/tests/components/bluetooth/test_websocket_api.py
2025-01-31 18:13:27 -06:00

433 lines
13 KiB
Python

"""The tests for the bluetooth WebSocket API."""
import asyncio
from datetime import timedelta
import time
from unittest.mock import ANY, patch
from bleak_retry_connector import Allocations
from freezegun import freeze_time
import pytest
from homeassistant.components.bluetooth import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.util.dt import utcnow
from . import (
HCI0_SOURCE_ADDRESS,
HCI1_SOURCE_ADDRESS,
NON_CONNECTABLE_REMOTE_SOURCE_ADDRESS,
FakeScanner,
_get_manager,
generate_advertisement_data,
generate_ble_device,
inject_advertisement_with_source,
)
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.typing import WebSocketGenerator
@pytest.mark.usefixtures("enable_bluetooth")
async def test_subscribe_advertisements(
hass: HomeAssistant,
register_hci0_scanner: None,
register_hci1_scanner: None,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test bluetooth subscribe_advertisements."""
address = "44:44:33:11:23:12"
switchbot_device_signal_100 = generate_ble_device(
address, "wohand_signal_100", rssi=-100
)
switchbot_adv_signal_100 = generate_advertisement_data(
local_name="wohand_signal_100", service_uuids=[]
)
inject_advertisement_with_source(
hass, switchbot_device_signal_100, switchbot_adv_signal_100, HCI0_SOURCE_ADDRESS
)
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "bluetooth/subscribe_advertisements",
}
)
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["success"]
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["event"] == {
"add": [
{
"address": "44:44:33:11:23:12",
"connectable": True,
"manufacturer_data": {},
"name": "wohand_signal_100",
"rssi": -127,
"service_data": {},
"service_uuids": [],
"source": HCI0_SOURCE_ADDRESS,
"time": ANY,
"tx_power": -127,
}
]
}
adv_time = response["event"]["add"][0]["time"]
switchbot_adv_signal_100 = generate_advertisement_data(
local_name="wohand_signal_100",
manufacturer_data={123: b"abc"},
service_uuids=[],
rssi=-80,
)
inject_advertisement_with_source(
hass, switchbot_device_signal_100, switchbot_adv_signal_100, HCI1_SOURCE_ADDRESS
)
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["event"] == {
"add": [
{
"address": "44:44:33:11:23:12",
"connectable": True,
"manufacturer_data": {"123": "616263"},
"name": "wohand_signal_100",
"rssi": -80,
"service_data": {},
"service_uuids": [],
"source": HCI1_SOURCE_ADDRESS,
"time": ANY,
"tx_power": -127,
}
]
}
new_time = response["event"]["add"][0]["time"]
assert new_time > adv_time
future_time = utcnow() + timedelta(seconds=3600)
future_monotonic_time = time.monotonic() + 3600
with (
freeze_time(future_time),
patch(
"habluetooth.manager.monotonic_time_coarse",
return_value=future_monotonic_time,
),
):
async_fire_time_changed(hass, future_time)
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["event"] == {"remove": [{"address": "44:44:33:11:23:12"}]}
@pytest.mark.usefixtures("enable_bluetooth")
async def test_subscribe_connection_allocations(
hass: HomeAssistant,
register_hci0_scanner: None,
register_hci1_scanner: None,
register_non_connectable_scanner: None,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test bluetooth subscribe_connection_allocations."""
address = "44:44:33:11:23:12"
switchbot_device_signal_100 = generate_ble_device(
address, "wohand_signal_100", rssi=-100
)
switchbot_adv_signal_100 = generate_advertisement_data(
local_name="wohand_signal_100", service_uuids=[]
)
inject_advertisement_with_source(
hass, switchbot_device_signal_100, switchbot_adv_signal_100, HCI0_SOURCE_ADDRESS
)
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "bluetooth/subscribe_connection_allocations",
}
)
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["success"]
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["event"] == [
{
"allocated": [],
"free": 5,
"slots": 5,
"source": "00:00:00:00:00:01",
},
{
"allocated": [],
"free": 5,
"slots": 5,
"source": HCI0_SOURCE_ADDRESS,
},
{
"allocated": [],
"free": 5,
"slots": 5,
"source": HCI1_SOURCE_ADDRESS,
},
{
"allocated": [],
"free": 0,
"slots": 0,
"source": NON_CONNECTABLE_REMOTE_SOURCE_ADDRESS,
},
]
manager = _get_manager()
manager.async_on_allocation_changed(
Allocations(
adapter="hci1", # Will be translated to source
slots=5,
free=4,
allocated=["AA:BB:CC:DD:EE:EE"],
)
)
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["event"] == [
{
"allocated": ["AA:BB:CC:DD:EE:EE"],
"free": 4,
"slots": 5,
"source": "AA:BB:CC:DD:EE:11",
},
]
manager.async_on_allocation_changed(
Allocations(
adapter="hci1", # Will be translated to source
slots=5,
free=5,
allocated=[],
)
)
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["event"] == [
{"allocated": [], "free": 5, "slots": 5, "source": HCI1_SOURCE_ADDRESS}
]
@pytest.mark.usefixtures("enable_bluetooth")
async def test_subscribe_connection_allocations_specific_scanner(
hass: HomeAssistant,
register_non_connectable_scanner: None,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test bluetooth subscribe_connection_allocations for a specific source address."""
entry = MockConfigEntry(
domain=DOMAIN, unique_id=NON_CONNECTABLE_REMOTE_SOURCE_ADDRESS
)
entry.add_to_hass(hass)
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "bluetooth/subscribe_connection_allocations",
"config_entry_id": entry.entry_id,
}
)
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["success"]
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["event"] == [
{
"allocated": [],
"free": 0,
"slots": 0,
"source": NON_CONNECTABLE_REMOTE_SOURCE_ADDRESS,
}
]
@pytest.mark.usefixtures("enable_bluetooth")
async def test_subscribe_connection_allocations_invalid_config_entry_id(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test bluetooth subscribe_connection_allocations for an invalid config entry id."""
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "bluetooth/subscribe_connection_allocations",
"config_entry_id": "non_existent",
}
)
async with asyncio.timeout(1):
response = await client.receive_json()
assert not response["success"]
assert response["error"]["code"] == "invalid_config_entry_id"
assert response["error"]["message"] == "Config entry non_existent not found"
@pytest.mark.usefixtures("enable_bluetooth")
async def test_subscribe_connection_allocations_invalid_scanner(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test bluetooth subscribe_connection_allocations for an invalid source address."""
entry = MockConfigEntry(domain=DOMAIN, unique_id="invalid")
entry.add_to_hass(hass)
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "bluetooth/subscribe_connection_allocations",
"config_entry_id": entry.entry_id,
}
)
async with asyncio.timeout(1):
response = await client.receive_json()
assert not response["success"]
assert response["error"]["code"] == "invalid_source"
assert response["error"]["message"] == "Source invalid not found"
@pytest.mark.usefixtures("enable_bluetooth")
async def test_subscribe_scanner_details(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test bluetooth subscribe_connection_allocations."""
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "bluetooth/subscribe_scanner_details",
}
)
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["success"]
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["event"] == {
"add": [
{
"adapter": "hci0",
"connectable": False,
"name": "hci0 (00:00:00:00:00:01)",
"source": "00:00:00:00:00:01",
}
]
}
manager = _get_manager()
hci3_scanner = FakeScanner("AA:BB:CC:DD:EE:33", "hci3")
cancel_hci3 = manager.async_register_hass_scanner(hci3_scanner)
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["event"] == {
"add": [
{
"adapter": "hci3",
"connectable": False,
"name": "hci3 (AA:BB:CC:DD:EE:33)",
"source": "AA:BB:CC:DD:EE:33",
}
]
}
cancel_hci3()
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["event"] == {
"remove": [
{
"adapter": "hci3",
"connectable": False,
"name": "hci3 (AA:BB:CC:DD:EE:33)",
"source": "AA:BB:CC:DD:EE:33",
}
]
}
@pytest.mark.usefixtures("enable_bluetooth")
async def test_subscribe_scanner_details_specific_scanner(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test bluetooth subscribe_scanner_details for a specific source address."""
entry = MockConfigEntry(domain=DOMAIN, unique_id="AA:BB:CC:DD:EE:33")
entry.add_to_hass(hass)
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "bluetooth/subscribe_scanner_details",
"config_entry_id": entry.entry_id,
}
)
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["success"]
manager = _get_manager()
hci3_scanner = FakeScanner("AA:BB:CC:DD:EE:33", "hci3")
cancel_hci3 = manager.async_register_hass_scanner(hci3_scanner)
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["event"] == {
"add": [
{
"adapter": "hci3",
"connectable": False,
"name": "hci3 (AA:BB:CC:DD:EE:33)",
"source": "AA:BB:CC:DD:EE:33",
}
]
}
cancel_hci3()
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["event"] == {
"remove": [
{
"adapter": "hci3",
"connectable": False,
"name": "hci3 (AA:BB:CC:DD:EE:33)",
"source": "AA:BB:CC:DD:EE:33",
}
]
}
@pytest.mark.usefixtures("enable_bluetooth")
async def test_subscribe_scanner_details_invalid_config_entry_id(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test bluetooth subscribe_scanner_details for an invalid config entry id."""
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "bluetooth/subscribe_scanner_details",
"config_entry_id": "non_existent",
}
)
async with asyncio.timeout(1):
response = await client.receive_json()
assert not response["success"]
assert response["error"]["code"] == "invalid_config_entry_id"
assert response["error"]["message"] == "Invalid config entry id: non_existent"