diff --git a/src/bluetooth_adapters/dbus.py b/src/bluetooth_adapters/dbus.py index 9cde2c6..e538e47 100644 --- a/src/bluetooth_adapters/dbus.py +++ b/src/bluetooth_adapters/dbus.py @@ -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 + 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 diff --git a/tests/test_init.py b/tests/test_init.py index 1c710e6..c82c753 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -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": "", + "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": "", + "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,