mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 09:17:10 +00:00
Add Bluetooth WebSocket API to subscribe to scanner details (#136750)
This commit is contained in:
parent
29a3f0a271
commit
68dbe34b89
@ -7,7 +7,12 @@ from functools import lru_cache, partial
|
|||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from habluetooth import BluetoothScanningMode, HaBluetoothSlotAllocations
|
from habluetooth import (
|
||||||
|
BluetoothScanningMode,
|
||||||
|
HaBluetoothSlotAllocations,
|
||||||
|
HaScannerRegistration,
|
||||||
|
HaScannerRegistrationEvent,
|
||||||
|
)
|
||||||
from home_assistant_bluetooth import BluetoothServiceInfoBleak
|
from home_assistant_bluetooth import BluetoothServiceInfoBleak
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -16,6 +21,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.json import json_bytes
|
from homeassistant.helpers.json import json_bytes
|
||||||
|
|
||||||
from .api import _get_manager, async_register_callback
|
from .api import _get_manager, async_register_callback
|
||||||
|
from .const import DOMAIN
|
||||||
from .match import BluetoothCallbackMatcher
|
from .match import BluetoothCallbackMatcher
|
||||||
from .models import BluetoothChange
|
from .models import BluetoothChange
|
||||||
from .util import InvalidConfigEntryID, InvalidSource, config_entry_id_to_source
|
from .util import InvalidConfigEntryID, InvalidSource, config_entry_id_to_source
|
||||||
@ -26,6 +32,7 @@ def async_setup(hass: HomeAssistant) -> None:
|
|||||||
"""Set up the bluetooth websocket API."""
|
"""Set up the bluetooth websocket API."""
|
||||||
websocket_api.async_register_command(hass, ws_subscribe_advertisements)
|
websocket_api.async_register_command(hass, ws_subscribe_advertisements)
|
||||||
websocket_api.async_register_command(hass, ws_subscribe_connection_allocations)
|
websocket_api.async_register_command(hass, ws_subscribe_connection_allocations)
|
||||||
|
websocket_api.async_register_command(hass, ws_subscribe_scanner_details)
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1024)
|
@lru_cache(maxsize=1024)
|
||||||
@ -191,3 +198,58 @@ async def ws_subscribe_connection_allocations(
|
|||||||
connection.send_message(
|
connection.send_message(
|
||||||
json_bytes(websocket_api.event_message(ws_msg_id, current_allocations))
|
json_bytes(websocket_api.event_message(ws_msg_id, current_allocations))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.require_admin
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "bluetooth/subscribe_scanner_details",
|
||||||
|
vol.Optional("config_entry_id"): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.async_response
|
||||||
|
async def ws_subscribe_scanner_details(
|
||||||
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Handle subscribe scanner details websocket command."""
|
||||||
|
ws_msg_id = msg["id"]
|
||||||
|
source: str | None = None
|
||||||
|
if config_entry_id := msg.get("config_entry_id"):
|
||||||
|
if (
|
||||||
|
not (entry := hass.config_entries.async_get_entry(config_entry_id))
|
||||||
|
or entry.domain != DOMAIN
|
||||||
|
):
|
||||||
|
connection.send_error(
|
||||||
|
ws_msg_id,
|
||||||
|
"invalid_config_entry_id",
|
||||||
|
f"Invalid config entry id: {config_entry_id}",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
source = entry.unique_id
|
||||||
|
assert source is not None
|
||||||
|
|
||||||
|
def _async_event_message(message: dict[str, Any]) -> None:
|
||||||
|
connection.send_message(
|
||||||
|
json_bytes(websocket_api.event_message(ws_msg_id, message))
|
||||||
|
)
|
||||||
|
|
||||||
|
def _async_registration_changed(registration: HaScannerRegistration) -> None:
|
||||||
|
added_event = HaScannerRegistrationEvent.ADDED
|
||||||
|
event_type = "add" if registration.event == added_event else "remove"
|
||||||
|
_async_event_message({event_type: [registration.scanner.details]})
|
||||||
|
|
||||||
|
manager = _get_manager(hass)
|
||||||
|
connection.subscriptions[ws_msg_id] = (
|
||||||
|
manager.async_register_scanner_registration_callback(
|
||||||
|
_async_registration_changed, source
|
||||||
|
)
|
||||||
|
)
|
||||||
|
connection.send_message(json_bytes(websocket_api.result_message(ws_msg_id)))
|
||||||
|
if (scanners := manager.async_current_scanners()) and (
|
||||||
|
matching_scanners := [
|
||||||
|
scanner.details
|
||||||
|
for scanner in scanners
|
||||||
|
if source is None or scanner.source == source
|
||||||
|
]
|
||||||
|
):
|
||||||
|
_async_event_message({"add": matching_scanners})
|
||||||
|
@ -17,6 +17,7 @@ from . import (
|
|||||||
HCI0_SOURCE_ADDRESS,
|
HCI0_SOURCE_ADDRESS,
|
||||||
HCI1_SOURCE_ADDRESS,
|
HCI1_SOURCE_ADDRESS,
|
||||||
NON_CONNECTABLE_REMOTE_SOURCE_ADDRESS,
|
NON_CONNECTABLE_REMOTE_SOURCE_ADDRESS,
|
||||||
|
FakeScanner,
|
||||||
_get_manager,
|
_get_manager,
|
||||||
generate_advertisement_data,
|
generate_advertisement_data,
|
||||||
generate_ble_device,
|
generate_ble_device,
|
||||||
@ -123,7 +124,7 @@ async def test_subscribe_advertisements(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("enable_bluetooth")
|
@pytest.mark.usefixtures("enable_bluetooth")
|
||||||
async def test_subscribe_subscribe_connection_allocations(
|
async def test_subscribe_connection_allocations(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
register_hci0_scanner: None,
|
register_hci0_scanner: None,
|
||||||
register_hci1_scanner: None,
|
register_hci1_scanner: None,
|
||||||
@ -201,7 +202,7 @@ async def test_subscribe_subscribe_connection_allocations(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("enable_bluetooth")
|
@pytest.mark.usefixtures("enable_bluetooth")
|
||||||
async def test_subscribe_subscribe_connection_allocations_specific_scanner(
|
async def test_subscribe_connection_allocations_specific_scanner(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
register_non_connectable_scanner: None,
|
register_non_connectable_scanner: None,
|
||||||
hass_ws_client: WebSocketGenerator,
|
hass_ws_client: WebSocketGenerator,
|
||||||
@ -237,7 +238,7 @@ async def test_subscribe_subscribe_connection_allocations_specific_scanner(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("enable_bluetooth")
|
@pytest.mark.usefixtures("enable_bluetooth")
|
||||||
async def test_subscribe_subscribe_connection_allocations_invalid_config_entry_id(
|
async def test_subscribe_connection_allocations_invalid_config_entry_id(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_ws_client: WebSocketGenerator,
|
hass_ws_client: WebSocketGenerator,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -258,7 +259,7 @@ async def test_subscribe_subscribe_connection_allocations_invalid_config_entry_i
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("enable_bluetooth")
|
@pytest.mark.usefixtures("enable_bluetooth")
|
||||||
async def test_subscribe_subscribe_connection_allocations_invalid_scanner(
|
async def test_subscribe_connection_allocations_invalid_scanner(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_ws_client: WebSocketGenerator,
|
hass_ws_client: WebSocketGenerator,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -278,3 +279,136 @@ async def test_subscribe_subscribe_connection_allocations_invalid_scanner(
|
|||||||
assert not response["success"]
|
assert not response["success"]
|
||||||
assert response["error"]["code"] == "invalid_source"
|
assert response["error"]["code"] == "invalid_source"
|
||||||
assert response["error"]["message"] == "Source invalid not found"
|
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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user