async def test_switching_adapters_based_on_stale(hass, enable_bluetooth): """Test switching adapters based on the previous advertisement being stale.""" address = "44:44:33:11:23:41" start_time_monotonic = 50.0 switchbot_device_poor_signal_hci0 = BLEDevice(address, "wohand_poor_signal_hci0", rssi=-100) switchbot_adv_poor_signal_hci0 = AdvertisementData( local_name="wohand_poor_signal_hci0", service_uuids=[]) inject_advertisement_with_time_and_source( hass, switchbot_device_poor_signal_hci0, switchbot_adv_poor_signal_hci0, start_time_monotonic, "hci0", ) assert (bluetooth.async_ble_device_from_address(hass, address) is switchbot_device_poor_signal_hci0) switchbot_device_poor_signal_hci1 = BLEDevice(address, "wohand_poor_signal_hci1", rssi=-99) switchbot_adv_poor_signal_hci1 = AdvertisementData( local_name="wohand_poor_signal_hci1", service_uuids=[]) inject_advertisement_with_time_and_source( hass, switchbot_device_poor_signal_hci1, switchbot_adv_poor_signal_hci1, start_time_monotonic, "hci1", ) # Should not switch adapters until the advertisement is stale assert (bluetooth.async_ble_device_from_address(hass, address) is switchbot_device_poor_signal_hci0) # Should switch to hci1 since the previous advertisement is stale # even though the signal is poor because the device is now # likely unreachable via hci0 inject_advertisement_with_time_and_source( hass, switchbot_device_poor_signal_hci1, switchbot_adv_poor_signal_hci1, start_time_monotonic + STALE_ADVERTISEMENT_SECONDS + 1, "hci1", ) assert (bluetooth.async_ble_device_from_address(hass, address) is switchbot_device_poor_signal_hci1)
async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth): """Test switching adapters based on rssi.""" address = "44:44:33:11:23:45" switchbot_device_poor_signal = BLEDevice(address, "wohand_poor_signal", rssi=-100) switchbot_adv_poor_signal = AdvertisementData( local_name="wohand_poor_signal", service_uuids=[]) inject_advertisement_with_source(hass, switchbot_device_poor_signal, switchbot_adv_poor_signal, "hci0") assert (bluetooth.async_ble_device_from_address(hass, address) is switchbot_device_poor_signal) switchbot_device_good_signal = BLEDevice(address, "wohand_good_signal", rssi=-60) switchbot_adv_good_signal = AdvertisementData( local_name="wohand_good_signal", service_uuids=[]) inject_advertisement_with_source(hass, switchbot_device_good_signal, switchbot_adv_good_signal, "hci1") assert (bluetooth.async_ble_device_from_address(hass, address) is switchbot_device_good_signal) inject_advertisement_with_source(hass, switchbot_device_good_signal, switchbot_adv_poor_signal, "hci0") assert (bluetooth.async_ble_device_from_address(hass, address) is switchbot_device_good_signal) # We should not switch adapters unless the signal hits the threshold switchbot_device_similar_signal = BLEDevice(address, "wohand_similar_signal", rssi=-62) switchbot_adv_similar_signal = AdvertisementData( local_name="wohand_similar_signal", service_uuids=[]) inject_advertisement_with_source(hass, switchbot_device_similar_signal, switchbot_adv_similar_signal, "hci0") assert (bluetooth.async_ble_device_from_address(hass, address) is switchbot_device_good_signal)
async def test_advertisements_do_not_switch_adapters_for_no_reason( hass, enable_bluetooth): """Test we only switch adapters when needed.""" address = "44:44:33:11:23:12" switchbot_device_signal_100 = BLEDevice(address, "wohand_signal_100", rssi=-100) switchbot_adv_signal_100 = AdvertisementData( local_name="wohand_signal_100", service_uuids=[]) inject_advertisement_with_source(hass, switchbot_device_signal_100, switchbot_adv_signal_100, "hci0") assert (bluetooth.async_ble_device_from_address(hass, address) is switchbot_device_signal_100) switchbot_device_signal_99 = BLEDevice(address, "wohand_signal_99", rssi=-99) switchbot_adv_signal_99 = AdvertisementData(local_name="wohand_signal_99", service_uuids=[]) inject_advertisement_with_source(hass, switchbot_device_signal_99, switchbot_adv_signal_99, "hci0") assert (bluetooth.async_ble_device_from_address(hass, address) is switchbot_device_signal_99) switchbot_device_signal_98 = BLEDevice(address, "wohand_good_signal", rssi=-98) switchbot_adv_signal_98 = AdvertisementData( local_name="wohand_good_signal", service_uuids=[]) inject_advertisement_with_source(hass, switchbot_device_signal_98, switchbot_adv_signal_98, "hci1") # should not switch to hci1 assert (bluetooth.async_ble_device_from_address(hass, address) is switchbot_device_signal_99)
async def test_preserve_new_tracked_device_name(hass, mock_bluetooth, mock_device_tracker_conf): """Test preserving tracked device name across new seens.""" address = "DE:AD:BE:EF:13:37" name = "Mock device name" entity_id = f"{DOMAIN}.{slugify(name)}" with patch( "homeassistant.components.bluetooth.async_discovered_service_info" ) as mock_async_discovered_service_info, patch.object( device_tracker, "MIN_SEEN_NEW", 3): device = BluetoothServiceInfoBleak( name=name, address=address, rssi=-19, manufacturer_data={}, service_data={}, service_uuids=[], source="local", device=BLEDevice(address, None), advertisement=AdvertisementData(local_name="empty"), time=0, connectable=False, ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] config = { CONF_PLATFORM: "bluetooth_le_tracker", CONF_SCAN_INTERVAL: timedelta(minutes=1), CONF_TRACK_NEW: True, } result = await async_setup_component(hass, DOMAIN, {DOMAIN: config}) assert result # Seen once here; return without name when seen subsequent times device = BluetoothServiceInfoBleak( name=None, address=address, rssi=-19, manufacturer_data={}, service_data={}, service_uuids=[], source="local", device=BLEDevice(address, None), advertisement=AdvertisementData(local_name="empty"), time=0, connectable=False, ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] # Tick until device seen enough times for to be registered for tracking for _ in range(device_tracker.MIN_SEEN_NEW - 1): async_fire_time_changed( hass, dt_util.utcnow() + config[CONF_SCAN_INTERVAL] + timedelta(seconds=1), ) await hass.async_block_till_done() state = hass.states.get(entity_id) assert state assert state.name == name
async def test_tracking_battery_successful(hass, mock_bluetooth, mock_device_tracker_conf): """Test tracking the battery gets a value.""" address = "DE:AD:BE:EF:13:37" name = "Mock device name" entity_id = f"{DOMAIN}.{slugify(name)}" with patch( "homeassistant.components.bluetooth.async_discovered_service_info" ) as mock_async_discovered_service_info, patch.object( device_tracker, "MIN_SEEN_NEW", 3): device = BluetoothServiceInfoBleak( name=name, address=address, rssi=-19, manufacturer_data={}, service_data={}, service_uuids=[], source="local", device=BLEDevice(address, None), advertisement=AdvertisementData(local_name="empty"), time=0, connectable=True, ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] config = { CONF_PLATFORM: "bluetooth_le_tracker", CONF_SCAN_INTERVAL: timedelta(minutes=1), CONF_TRACK_BATTERY: True, CONF_TRACK_BATTERY_INTERVAL: timedelta(minutes=2), CONF_TRACK_NEW: True, } result = await async_setup_component(hass, DOMAIN, {DOMAIN: config}) assert result # Tick until device seen enough times for to be registered for tracking for _ in range(device_tracker.MIN_SEEN_NEW - 1): async_fire_time_changed( hass, dt_util.utcnow() + config[CONF_SCAN_INTERVAL] + timedelta(seconds=1), ) await hass.async_block_till_done() with patch( "homeassistant.components.bluetooth_le_tracker.device_tracker.BleakClient", MockBleakClientBattery5, ): # Wait for the battery scan async_fire_time_changed( hass, dt_util.utcnow() + config[CONF_SCAN_INTERVAL] + timedelta(seconds=1) + timedelta(minutes=2), ) await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.name == name assert state.attributes["battery"] == 5
async def test_recovery_from_dbus_restart(hass, one_adapter): """Test we can recover when DBus gets restarted out from under us.""" called_start = 0 called_stop = 0 _callback = None mock_discovered = [] class MockBleakScanner: async def start(self, *args, **kwargs): """Mock Start.""" nonlocal called_start called_start += 1 async def stop(self, *args, **kwargs): """Mock Start.""" nonlocal called_stop called_stop += 1 @property def discovered_devices(self): """Mock discovered_devices.""" nonlocal mock_discovered return mock_discovered def register_detection_callback(self, callback: AdvertisementDataCallback): """Mock Register Detection Callback.""" nonlocal _callback _callback = callback scanner = MockBleakScanner() with patch( "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", return_value=scanner, ): await async_setup_with_one_adapter(hass) assert called_start == 1 start_time_monotonic = 1000 scanner = _get_manager() mock_discovered = [MagicMock()] # Ensure we don't restart the scanner if we don't need to with patch( "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", return_value=start_time_monotonic + 10, ): async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) await hass.async_block_till_done() assert called_start == 1 # Fire a callback to reset the timer with patch( "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", return_value=start_time_monotonic, ): _callback( BLEDevice("44:44:33:11:23:42", "any_name"), AdvertisementData(local_name="any_name"), ) # Ensure we don't restart the scanner if we don't need to with patch( "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", return_value=start_time_monotonic + 20, ): async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) await hass.async_block_till_done() assert called_start == 1 # We hit the timer, so we restart the scanner with patch( "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", return_value=start_time_monotonic + SCANNER_WATCHDOG_TIMEOUT, ): async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) await hass.async_block_till_done() assert called_start == 2
async def test_diagnostics( hass, hass_client, mock_bleak_scanner_start, enable_bluetooth, two_adapters ): """Test we can setup and unsetup bluetooth with multiple adapters.""" # Normally we do not want to patch our classes, but since bleak will import # a different scanner based on the operating system, we need to patch here # because we cannot import the scanner class directly without it throwing an # error if the test is not running on linux since we won't have the correct # deps installed when testing on MacOS. with patch( "homeassistant.components.bluetooth.scanner.HaScanner.discovered_devices", [BLEDevice(name="x", rssi=-60, address="44:44:33:11:23:45")], ), patch( "homeassistant.components.bluetooth.diagnostics.platform.system", return_value="Linux", ), patch( "homeassistant.components.bluetooth.diagnostics.get_dbus_managed_objects", return_value={ "org.bluez": { "/org/bluez/hci0": { "org.bluez.Adapter1": { "Name": "BlueZ 5.63", "Alias": "BlueZ 5.63", "Modalias": "usb:v1D6Bp0246d0540", "Discovering": False, }, "org.bluez.AdvertisementMonitorManager1": { "SupportedMonitorTypes": ["or_patterns"], "SupportedFeatures": [], }, } } }, ): entry1 = MockConfigEntry( domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01" ) entry1.add_to_hass(hass) entry2 = MockConfigEntry( domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:02" ) entry2.add_to_hass(hass) assert await hass.config_entries.async_setup(entry1.entry_id) await hass.async_block_till_done() assert await hass.config_entries.async_setup(entry2.entry_id) await hass.async_block_till_done() diag = await get_diagnostics_for_config_entry(hass, hass_client, entry1) assert diag == { "adapters": { "hci0": { "address": "00:00:00:00:00:01", "hw_version": "usbid:1234", "passive_scan": False, "sw_version": "BlueZ 4.63", }, "hci1": { "address": "00:00:00:00:00:02", "hw_version": "usbid:1234", "passive_scan": True, "sw_version": "BlueZ 4.63", }, }, "dbus": { "org.bluez": { "/org/bluez/hci0": { "org.bluez.Adapter1": { "Alias": "BlueZ " "5.63", "Discovering": False, "Modalias": "usb:v1D6Bp0246d0540", "Name": "BlueZ " "5.63", }, "org.bluez.AdvertisementMonitorManager1": { "SupportedFeatures": [], "SupportedMonitorTypes": ["or_patterns"], }, } } }, "manager": { "adapters": { "hci0": { "address": "00:00:00:00:00:01", "hw_version": "usbid:1234", "passive_scan": False, "sw_version": "BlueZ 4.63", }, "hci1": { "address": "00:00:00:00:00:02", "hw_version": "usbid:1234", "passive_scan": True, "sw_version": "BlueZ 4.63", }, }, "connectable_history": [], "history": [], "scanners": [ { "adapter": "hci0", "discovered_devices": [ {"address": "44:44:33:11:23:45", "name": "x", "rssi": -60} ], "last_detection": ANY, "name": "hci0 (00:00:00:00:00:01)", "source": "hci0", "start_time": ANY, "type": "HaScanner", }, { "adapter": "hci0", "discovered_devices": [ {"address": "44:44:33:11:23:45", "name": "x", "rssi": -60} ], "last_detection": ANY, "name": "hci0 (00:00:00:00:00:01)", "source": "hci0", "start_time": ANY, "type": "HaScanner", }, { "adapter": "hci1", "discovered_devices": [ {"address": "44:44:33:11:23:45", "name": "x", "rssi": -60} ], "last_detection": ANY, "name": "hci1 (00:00:00:00:00:02)", "source": "hci1", "start_time": ANY, "type": "HaScanner", }, ], }, }