Skip to content
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

fix: handle devices listed before adapter in dbus managed objects #174

14 changes: 9 additions & 5 deletions src/bluetooth_adapters/dbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,15 @@ def _adapters_from_managed_objects(
adapters: dict[str, dict[str, Any]] = {}
for path, unpacked_data in managed_objects.items():
path_str = str(path)
if path_str.startswith("/org/bluez/hci"):
split_path = path_str.split("/")
adapter = split_path[3]
if adapter not in adapters:
adapters[adapter] = unpacked_data
# check that path is exactly /org/bluez/hci<integer>
if not path_str.startswith("/org/bluez/hci"):
continue
if not path_str[14:].isdigit():
continue
split_path = path_str.split("/")
adapter = split_path[3]
if adapter not in adapters:
adapters[adapter] = unpacked_data
return adapters


Expand Down
278 changes: 278 additions & 0 deletions tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,284 @@ def setup(self, *args, **kwargs):
}


@pytest.mark.asyncio
@pytest.mark.skipif(
MessageType is None or get_dbus_managed_objects is None,
reason="dbus_fast is not available",
)
async def test_get_adapters_linux_device_listed_before_adapter():
"""Test get_adapters. List a device entry before the adapter entry to ensure
the adapter is retrieved and not the device."""

class MockMessageBus:
def __init__(self, *args, **kwargs):
pass

async def connect(self):
return AsyncMock(
disconnect=MagicMock(),
call=AsyncMock(
return_value=MagicMock(
body=[
{
"/other": {},
"/org/bluez/hci0/dev_54_D2_72_AB_35_95": {
"org.freedesktop.DBus.Introspectable": {},
"org.bluez.Device1": {
"Address": "54:D2:72:AB:35:95",
"AddressType": "public",
"Name": "Nuki_1EAB3595",
"Alias": "Nuki_1EAB3595",
"Paired": False,
"Trusted": False,
"Blocked": False,
"LegacyPairing": False,
"RSSI": -78,
"Connected": False,
"UUIDs": [],
"Adapter": "/org/bluez/hci0",
"ManufacturerData": {
"76": b"\\x02\\x15\\xa9.\\xe2\\x00U\\x01\\x11\\xe4\\x91l\\x08\\x00 \\x0c\\x9af\\x1e\\xab5\\x95\\xc4"
},
"ServicesResolved": False,
"AdvertisingFlags": {
"__type": "<class 'bytearray'>",
"repr": "bytearray(b'\\x06')",
},
},
"org.freedesktop.DBus.Properties": {},
},
"/org/bluez/hci0": {
"org.bluez.Adapter1": {
"Address": "00:1A:7D:DA:71:04",
"AddressType": "public",
"Alias": "homeassistant",
"Class": 2883584,
"Discoverable": False,
"DiscoverableTimeout": 180,
"Discovering": True,
"Modalias": "usb:v1D6Bp0246d053F",
"Name": "homeassistant",
"Pairable": False,
"PairableTimeout": 0,
"Powered": True,
"Roles": ["central", "peripheral"],
"UUIDs": [
"0000110e-0000-1000-8000-00805f9b34fb",
"0000110a-0000-1000-8000-00805f9b34fb",
"00001200-0000-1000-8000-00805f9b34fb",
"0000110b-0000-1000-8000-00805f9b34fb",
"00001108-0000-1000-8000-00805f9b34fb",
"0000110c-0000-1000-8000-00805f9b34fb",
"00001800-0000-1000-8000-00805f9b34fb",
"00001801-0000-1000-8000-00805f9b34fb",
"0000180a-0000-1000-8000-00805f9b34fb",
"00001112-0000-1000-8000-00805f9b34fb",
],
},
"org.bluez.GattManager1": {},
"org.bluez.LEAdvertisingManager1": {
"ActiveInstances": 0,
"SupportedIncludes": [
"tx-power",
"appearance",
"local-name",
],
"SupportedInstances": 5,
},
"org.bluez.Media1": {},
"org.bluez.NetworkServer1": {},
"org.freedesktop.DBus.Introspectable": {},
"org.freedesktop.DBus.Properties": {},
},
"/org/bluez/hci1": {},
"/org/bluez/hci2": {
"org.bluez.Adapter1": {
"Address": "00:00:00:00:00:00",
"AddressType": "public",
"Alias": "homeassistant",
"Class": 2883584,
"Discoverable": False,
"DiscoverableTimeout": 180,
"Discovering": True,
"Modalias": "usb:v1D6Bp0246d053F",
"Name": "homeassistant",
"Pairable": False,
"PairableTimeout": 0,
"Powered": True,
"Roles": ["central", "peripheral"],
"UUIDs": [
"0000110e-0000-1000-8000-00805f9b34fb",
"0000110a-0000-1000-8000-00805f9b34fb",
"00001200-0000-1000-8000-00805f9b34fb",
"0000110b-0000-1000-8000-00805f9b34fb",
"00001108-0000-1000-8000-00805f9b34fb",
"0000110c-0000-1000-8000-00805f9b34fb",
"00001800-0000-1000-8000-00805f9b34fb",
"00001801-0000-1000-8000-00805f9b34fb",
"0000180a-0000-1000-8000-00805f9b34fb",
"00001112-0000-1000-8000-00805f9b34fb",
],
},
"org.bluez.GattManager1": {},
"org.bluez.LEAdvertisingManager1": {
"ActiveInstances": 0,
"SupportedIncludes": [
"tx-power",
"appearance",
"local-name",
],
"SupportedInstances": 5,
},
"org.bluez.Media1": {},
"org.bluez.NetworkServer1": {},
"org.freedesktop.DBus.Introspectable": {},
"org.freedesktop.DBus.Properties": {},
},
"/org/bluez/hci3": {
"org.bluez.Adapter1": {
"Address": "00:1A:7D:DA:71:05",
"AddressType": "public",
"Alias": "homeassistant",
"Class": 2883584,
"Discoverable": False,
"DiscoverableTimeout": 180,
"Discovering": True,
"Modalias": "usb:v1D6Bp0246d053F",
"Name": "homeassistant",
"Pairable": False,
"PairableTimeout": 0,
"Powered": True,
"Roles": ["central", "peripheral"],
"UUIDs": [
"0000110e-0000-1000-8000-00805f9b34fb",
"0000110a-0000-1000-8000-00805f9b34fb",
"00001200-0000-1000-8000-00805f9b34fb",
"0000110b-0000-1000-8000-00805f9b34fb",
"00001108-0000-1000-8000-00805f9b34fb",
"0000110c-0000-1000-8000-00805f9b34fb",
"00001800-0000-1000-8000-00805f9b34fb",
"00001801-0000-1000-8000-00805f9b34fb",
"0000180a-0000-1000-8000-00805f9b34fb",
"00001112-0000-1000-8000-00805f9b34fb",
],
},
"org.bluez.GattManager1": {},
"org.bluez.LEAdvertisingManager1": {
"ActiveInstances": 0,
"SupportedIncludes": [
"tx-power",
"appearance",
"local-name",
],
"SupportedInstances": 5,
},
"org.bluez.Media1": {},
"org.bluez.NetworkServer1": {},
"org.freedesktop.DBus.Introspectable": {},
"org.freedesktop.DBus.Properties": {},
},
"/org/bluez/hci1/any": {},
"/org/bluez/hci1/dev_54_D2_72_AB_35_95": {
"org.freedesktop.DBus.Introspectable": {},
"org.bluez.Device1": {
"Address": "54:D2:72:AB:35:95",
"AddressType": "public",
"Name": "Nuki_1EAB3595",
"Alias": "Nuki_1EAB3595",
"Paired": False,
"Trusted": False,
"Blocked": False,
"LegacyPairing": False,
"RSSI": -100,
"Connected": False,
"UUIDs": [],
"Adapter": "/org/bluez/hci0",
"ManufacturerData": {
"76": b"\\x02\\x15\\xa9.\\xe2\\x00U\\x01\\x11\\xe4\\x91l\\x08\\x00 \\x0c\\x9af\\x1e\\xab5\\x95\\xc4"
},
"ServicesResolved": False,
"AdvertisingFlags": {
"__type": "<class 'bytearray'>",
"repr": "bytearray(b'\\x06')",
},
},
"org.freedesktop.DBus.Properties": {},
},
}
],
message_type=MessageType.METHOD_RETURN,
)
),
)

class MockUSBDevice(USBDevice):
def __init__(self, *args, **kwargs):
self.manufacturer = "XTech"
self.product = "Bluetooth 4.0 USB Adapter"
self.vendor_id = "0a12"
self.product_id = "0001"
pass

class MockBluetoothDevice(USBBluetoothDevice):
def __init__(self, *args, **kwargs):
self.usb_device = MockUSBDevice()
pass

def setup(self, *args, **kwargs):
pass

with patch("platform.system", return_value="Linux"), patch(
"bluetooth_adapters.dbus.MessageBus", MockMessageBus
), patch(
"bluetooth_adapters.systems.linux.USBBluetoothDevice", MockBluetoothDevice
):
bluetooth_adapters = get_adapters()
await bluetooth_adapters.refresh()
assert bluetooth_adapters.default_adapter == "hci0"
assert bluetooth_adapters.history == {
"54:D2:72:AB:35:95": AdvertisementHistory(
device=ANY, advertisement_data=ANY, source="hci0"
)
}
# hci0 should show
# hci1 is empty so it should not be in the list
# hci2 should not show as 00:00:00:00:00:00 are filtered downstream now
# hci3 should show
assert bluetooth_adapters.adapters == {
"hci0": {
"address": "00:1A:7D:DA:71:04",
"hw_version": "usb:v1D6Bp0246d053F",
"manufacturer": "XTech",
"passive_scan": False,
"product": "Bluetooth 4.0 USB Adapter",
"product_id": "0001",
"sw_version": "homeassistant",
"vendor_id": "0a12",
},
"hci2": {
"address": "00:00:00:00:00:00",
"hw_version": "usb:v1D6Bp0246d053F",
"manufacturer": "XTech",
"passive_scan": False,
"product": "Bluetooth 4.0 USB Adapter",
"product_id": "0001",
"sw_version": "homeassistant",
"vendor_id": "0a12",
},
"hci3": {
"address": "00:1A:7D:DA:71:05",
"hw_version": "usb:v1D6Bp0246d053F",
"manufacturer": "XTech",
"passive_scan": False,
"product": "Bluetooth 4.0 USB Adapter",
"product_id": "0001",
"sw_version": "homeassistant",
"vendor_id": "0a12",
},
}


@pytest.mark.asyncio
@pytest.mark.skipif(
MessageType is None or get_dbus_managed_objects is None,
Expand Down
Loading