Skip to content

Ensure third party callbacks on BLE notify do not cause disconnect #1212

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion aioesphomeapi/client_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,14 @@ def on_bluetooth_gatt_notify_data_response(
) -> None:
"""Handle a BluetoothGATTNotifyDataResponse message."""
if address == msg.address and handle == msg.handle:
on_bluetooth_gatt_notify(handle, bytearray(msg.data))
try:
on_bluetooth_gatt_notify(handle, bytearray(msg.data))
except Exception:
_LOGGER.exception(
"Unexpected error in Bluetooth GATT notify callback for address %s, handle %s",
address,
handle,
)


def on_bluetooth_scanner_state_response(
Expand Down
41 changes: 41 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1661,6 +1661,47 @@ def on_bluetooth_gatt_notify(handle: int, data: bytearray) -> None:
)


async def test_bluetooth_gatt_notify_callback_raises(
api_client: tuple[
APIClient, APIConnection, asyncio.Transport, APIPlaintextFrameHelper
],
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that exceptions in bluetooth gatt notify callbacks are caught."""
client, connection, transport, protocol = api_client

def on_bluetooth_gatt_notify(handle: int, data: bytearray) -> None:
raise ValueError("Test exception in notify callback")

notify_task = asyncio.create_task(
client.bluetooth_gatt_start_notify(1234, 1, on_bluetooth_gatt_notify)
)
await asyncio.sleep(0)
notify_response: message.Message = BluetoothGATTNotifyResponse(
address=1234, handle=1
)
mock_data_received(protocol, generate_plaintext_packet(notify_response))
await notify_task

# Clear any logs from the notify setup
caplog.clear()

# Send data that will trigger the exception
data_response: message.Message = BluetoothGATTNotifyDataResponse(
address=1234, handle=1, data=b"test_data"
)
mock_data_received(protocol, generate_plaintext_packet(data_response))
await asyncio.sleep(0)

# Verify the exception was caught and logged
assert "Unexpected error in Bluetooth GATT notify callback" in caplog.text
assert "ValueError: Test exception in notify callback" in caplog.text
assert "address 1234, handle 1" in caplog.text

# Verify the connection is still alive
assert connection.is_connected


async def test_subscribe_bluetooth_le_advertisements(
api_client: tuple[
APIClient, APIConnection, asyncio.Transport, APIPlaintextFrameHelper
Expand Down