Skip to content

Commit 1341a68

Browse files
author
Kirill Rudovski
committed
bleak: raise BleakBluetoothNotAvailableError from BleakAdapter.get() on Linux and Windows
CoreBluetooth already raises BleakBluetoothNotAvailableError from BleakAdapter.get() via wait_until_ready() when Bluetooth is not powered on. This makes the BlueZ and WinRT backends do the same, so callers can rely on the exception across all platforms. - BlueZ: checks for adapter presence in the manager's cached properties and the Powered property; raises NO_BLUETOOTH or POWERED_OFF. - WinRT: fetches the Bluetooth radio via Radio.get_radios_async() and checks RadioState; raises NO_BLUETOOTH or POWERED_OFF.
1 parent ae2f589 commit 1341a68

5 files changed

Lines changed: 72 additions & 0 deletions

File tree

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ Added
1414
-----
1515
* Added ``BleakAdapter`` class with ``get_connected_devices()`` to retrieve BLE devices that are already connected to the system without scanning.
1616

17+
Changed
18+
-------
19+
* Changed ``BleakAdapter.get()`` to raise ``BleakBluetoothNotAvailableError`` on Linux and Windows when the local Bluetooth adapter is not powered on, matching the existing CoreBluetooth behaviour.
20+
1721
Fixed
1822
-----
1923
* Fixed handling empty notification payloads in BlueZ backend when using "AcquireNotify". Fixes #1982.

bleak/backends/bluezdbus/adapter.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
from bleak._compat import Self, override
1111
from bleak.args.bluez import BlueZAdapterArgs
1212
from bleak.backends.adapter import BaseBleakAdapter
13+
from bleak.backends.bluezdbus import defs
1314
from bleak.backends.bluezdbus.manager import get_global_bluez_manager
1415
from bleak.backends.device import BLEDevice
16+
from bleak.exc import BleakBluetoothNotAvailableError, BleakBluetoothNotAvailableReason
1517

1618

1719
class BleakAdapterBlueZDBus(BaseBleakAdapter):
@@ -28,6 +30,21 @@ async def get(cls, *, bluez: BlueZAdapterArgs = {}, **kwargs: Any) -> Self:
2830
adapter_path = (
2931
f"/org/bluez/{adapter}" if adapter else manager.get_default_adapter()
3032
)
33+
34+
adapter_props = manager._properties.get( # pyright: ignore[reportPrivateUsage]
35+
adapter_path, {}
36+
).get(defs.ADAPTER_INTERFACE)
37+
if adapter_props is None:
38+
raise BleakBluetoothNotAvailableError(
39+
f"Bluetooth adapter '{adapter_path}' is unavailable",
40+
BleakBluetoothNotAvailableReason.NO_BLUETOOTH,
41+
)
42+
if not adapter_props.get("Powered"):
43+
raise BleakBluetoothNotAvailableError(
44+
"Bluetooth adapter is not powered on",
45+
BleakBluetoothNotAvailableReason.POWERED_OFF,
46+
)
47+
3148
return cls(adapter_path)
3249

3350
@override

bleak/backends/winrt/adapter.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616
)
1717
from winrt.windows.devices.bluetooth.genericattributeprofile import GattDeviceService
1818
from winrt.windows.devices.enumeration import DeviceInformation
19+
from winrt.windows.devices.radios import Radio, RadioKind, RadioState
1920

2021
from bleak._compat import Self, override
2122
from bleak.backends.adapter import BaseBleakAdapter
2223
from bleak.backends.device import BLEDevice
2324
from bleak.backends.winrt.util import assert_mta
25+
from bleak.exc import BleakBluetoothNotAvailableError, BleakBluetoothNotAvailableReason
2426
from bleak.uuids import normalize_uuid_16
2527

2628

@@ -31,6 +33,23 @@ class BleakAdapterWinRT(BaseBleakAdapter):
3133
@override
3234
async def get(cls, **kwargs: Any) -> Self:
3335
await assert_mta()
36+
37+
radio = None
38+
for candidate in await Radio.get_radios_async():
39+
if candidate.kind == RadioKind.BLUETOOTH:
40+
radio = candidate
41+
break
42+
if radio is None:
43+
raise BleakBluetoothNotAvailableError(
44+
"No Bluetooth radio available",
45+
BleakBluetoothNotAvailableReason.NO_BLUETOOTH,
46+
)
47+
if radio.state != RadioState.ON:
48+
raise BleakBluetoothNotAvailableError(
49+
"Bluetooth adapter is not powered on",
50+
BleakBluetoothNotAvailableReason.POWERED_OFF,
51+
)
52+
3453
return cls()
3554

3655
@override

docs/api/adapter.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ On Linux, a specific adapter can be selected via the ``bluez`` argument::
2626

2727
adapter = await BleakAdapter.get(bluez={"adapter": "hci1"})
2828

29+
:meth:`BleakAdapter.get` raises :class:`~bleak.exc.BleakBluetoothNotAvailableError`
30+
if the local Bluetooth adapter is not currently powered on or is otherwise
31+
unavailable.
32+
2933
.. automethod:: bleak.BleakAdapter.get
3034

3135
-----------------------------

tests/integration/test_adapter.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import dataclasses
23
from collections.abc import AsyncGenerator
34

@@ -11,6 +12,7 @@
1112

1213
from bleak import BleakAdapter, BleakClient
1314
from bleak.backends.device import BLEDevice
15+
from bleak.exc import BleakBluetoothNotAvailableError, BleakBluetoothNotAvailableReason
1416
from tests.integration.conftest import (
1517
configure_and_power_on_bumble_peripheral,
1618
create_bumble_peripheral,
@@ -99,3 +101,29 @@ async def test_get_connected_devices_filters_by_service_uuid(
99101

100102
not_connected = await adapter.get_connected_devices([OTHER_SERVICE_UUID])
101103
assert not_connected == []
104+
105+
106+
@pytest.mark.asyncio(loop_scope="module")
107+
@pytest.mark.usefixtures("hci_transport")
108+
async def test_get_raises_when_powered_off() -> None:
109+
"""``BleakAdapter.get()`` raises ``BleakBluetoothNotAvailableError`` with
110+
reason ``POWERED_OFF`` when the local Bluetooth adapter is not powered on.
111+
112+
This test power-cycles the BlueZ adapter via ``bluetoothctl``, so it must
113+
be the last test in the module - it leaves the adapter back in
114+
``POWERED_ON`` but subsequent tests would race the recovery.
115+
"""
116+
proc = await asyncio.create_subprocess_exec("bluetoothctl", "power", "off")
117+
await proc.wait()
118+
# Allow the BlueZ PropertiesChanged signal to propagate to the manager's
119+
# cached properties before calling get().
120+
await asyncio.sleep(1.0)
121+
122+
try:
123+
with pytest.raises(BleakBluetoothNotAvailableError) as exc_info:
124+
await BleakAdapter.get()
125+
assert exc_info.value.reason == BleakBluetoothNotAvailableReason.POWERED_OFF
126+
finally:
127+
proc = await asyncio.create_subprocess_exec("bluetoothctl", "power", "on")
128+
await proc.wait()
129+
await asyncio.sleep(1.0)

0 commit comments

Comments
 (0)