From 865b3a66469b02420a98207fabb5065b27006eab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Aug 2025 02:44:15 -0500 Subject: [PATCH] Add raw advertisement data to Bluetooth WebSocket API (#150358) --- .../components/bluetooth/websocket_api.py | 9 ++++++++- tests/components/bluetooth/__init__.py | 2 ++ tests/components/bluetooth/test_websocket_api.py | 14 ++++++++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth/websocket_api.py b/homeassistant/components/bluetooth/websocket_api.py index d21b11b050f..9022d98bf06 100644 --- a/homeassistant/components/bluetooth/websocket_api.py +++ b/homeassistant/components/bluetooth/websocket_api.py @@ -39,7 +39,13 @@ def async_setup(hass: HomeAssistant) -> None: def serialize_service_info( service_info: BluetoothServiceInfoBleak, time_diff: float ) -> dict[str, Any]: - """Serialize a BluetoothServiceInfoBleak object.""" + """Serialize a BluetoothServiceInfoBleak object. + + The raw field is included for: + 1. Debugging - to see the actual advertisement packet + 2. Data freshness - manufacturer_data and service_data are aggregated + across multiple advertisements, raw shows the latest packet only + """ return { "name": service_info.name, "address": service_info.address, @@ -57,6 +63,7 @@ def serialize_service_info( "connectable": service_info.connectable, "time": service_info.time + time_diff, "tx_power": service_info.tx_power, + "raw": service_info.raw.hex() if service_info.raw else None, } diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index d439f46bb71..6951a2ce4cc 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -145,6 +145,7 @@ def inject_advertisement_with_time_and_source_connectable( time: float, source: str, connectable: bool, + raw: bytes | None = None, ) -> None: """Inject an advertisement into the manager from a specific source at a time and connectable status.""" async_get_advertisement_callback(hass)( @@ -161,6 +162,7 @@ def inject_advertisement_with_time_and_source_connectable( connectable=connectable, time=time, tx_power=adv.tx_power, + raw=raw, ) ) diff --git a/tests/components/bluetooth/test_websocket_api.py b/tests/components/bluetooth/test_websocket_api.py index 2e613932f3c..f12d77913a9 100644 --- a/tests/components/bluetooth/test_websocket_api.py +++ b/tests/components/bluetooth/test_websocket_api.py @@ -22,6 +22,7 @@ from . import ( generate_advertisement_data, generate_ble_device, inject_advertisement_with_source, + inject_advertisement_with_time_and_source_connectable, ) from tests.common import MockConfigEntry, async_fire_time_changed @@ -72,6 +73,7 @@ async def test_subscribe_advertisements( "source": HCI0_SOURCE_ADDRESS, "time": ANY, "tx_power": -127, + "raw": None, } ] } @@ -83,8 +85,15 @@ async def test_subscribe_advertisements( service_uuids=[], rssi=-80, ) - inject_advertisement_with_source( - hass, switchbot_device_signal_100, switchbot_adv_signal_100, HCI1_SOURCE_ADDRESS + # Inject with raw bytes data + inject_advertisement_with_time_and_source_connectable( + hass, + switchbot_device_signal_100, + switchbot_adv_signal_100, + time.monotonic(), + HCI1_SOURCE_ADDRESS, + True, + raw=b"\x02\x01\x06\x03\x03\x0f\x18", ) async with asyncio.timeout(1): response = await client.receive_json() @@ -101,6 +110,7 @@ async def test_subscribe_advertisements( "source": HCI1_SOURCE_ADDRESS, "time": ANY, "tx_power": -127, + "raw": "02010603030f18", } ] }