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)
def async_on_advertisement(self, adv: BluetoothLEAdvertisement) -> None: """Call the registered callback.""" now = time.monotonic() address = ":".join(TWO_CHAR.findall("%012X" % adv.address)) # must be upper advertisement_data = AdvertisementData( # type: ignore[no-untyped-call] local_name=None if adv.name == "" else adv.name, manufacturer_data=adv.manufacturer_data, service_data=adv.service_data, service_uuids=adv.service_uuids, ) device = BLEDevice( # type: ignore[no-untyped-call] address=address, name=adv.name, details={}, rssi=adv.rssi, ) self._discovered_devices[address] = device self._discovered_device_timestamps[address] = now self._new_info_callback( BluetoothServiceInfoBleak( name=advertisement_data.local_name or device.name or device.address, address=device.address, rssi=device.rssi, manufacturer_data=advertisement_data.manufacturer_data, service_data=advertisement_data.service_data, service_uuids=advertisement_data.service_uuids, source=self._source, device=device, advertisement=advertisement_data, connectable=False, time=now, ))
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)
def _received_handler( self, sender: BluetoothLEAdvertisementWatcher, event_args: BluetoothLEAdvertisementReceivedEventArgs, ): if sender == self.watcher: logger.debug("Received {0}.".format( _format_event_args(event_args))) if (event_args.AdvertisementType == BluetoothLEAdvertisementType.ScanResponse): if event_args.BluetoothAddress not in self._scan_responses: self._scan_responses[ event_args.BluetoothAddress] = event_args else: if event_args.BluetoothAddress not in self._devices: self._devices[event_args.BluetoothAddress] = event_args if self._callback is None: return # Get a "BLEDevice" from parse_event args device = self.parse_eventargs(event_args) # Decode service data service_data = {} # 0x16 is service data with 16-bit UUID for section in event_args.Advertisement.GetSectionsByType(0x16): with BleakDataReader(section.Data) as reader: data = reader.read() service_data[ f"0000{data[1]:02x}{data[0]:02x}-0000-1000-8000-00805f9b34fb"] = data[ 2:] # 0x20 is service data with 32-bit UUID for section in event_args.Advertisement.GetSectionsByType(0x20): with BleakDataReader(section.Data) as reader: data = reader.read() service_data[ f"{data[3]:02x}{data[2]:02x}{data[1]:02x}{data[0]:02x}-0000-1000-8000-00805f9b34fb"] = data[ 4:] # 0x21 is service data with 128-bit UUID for section in event_args.Advertisement.GetSectionsByType(0x21): with BleakDataReader(section.Data) as reader: data = reader.read() service_data[str(UUID(bytes=data[15::-1]))] = data[16:] # Use the BLEDevice to populate all the fields for the advertisement data to return advertisement_data = AdvertisementData( local_name=event_args.Advertisement.LocalName, manufacturer_data=device.metadata["manufacturer_data"], service_data=service_data, service_uuids=device.metadata["uuids"], platform_data=(sender, event_args), ) self._callback(device, advertisement_data)
def _received_handler( self, sender: BluetoothLEAdvertisementWatcher, event_args: BluetoothLEAdvertisementReceivedEventArgs, ): """Callback for AdvertisementWatcher.Received""" # TODO: Cannot check for if sender == self.watcher in winrt? logger.debug("Received {0}.".format(_format_event_args(event_args))) if event_args.advertisement_type == BluetoothLEAdvertisementType.SCAN_RESPONSE: if event_args.bluetooth_address not in self._scan_responses: self._scan_responses[event_args.bluetooth_address] = event_args else: if event_args.bluetooth_address not in self._devices: self._devices[event_args.bluetooth_address] = event_args if self._callback is None: return # Get a "BLEDevice" from parse_event args device = self._parse_event_args(event_args) # Decode service data service_data = {} # 0x16 is service data with 16-bit UUID for section in event_args.advertisement.get_sections_by_type(0x16): data = bytearray( CryptographicBuffer.copy_to_byte_array(section.data)) service_data[ f"0000{data[1]:02x}{data[0]:02x}-0000-1000-8000-00805f9b34fb"] = data[ 2:] # 0x20 is service data with 32-bit UUID for section in event_args.advertisement.get_sections_by_type(0x20): data = bytearray( CryptographicBuffer.copy_to_byte_array(section.data)) service_data[ f"{data[3]:02x}{data[2]:02x}{data[1]:02x}{data[0]:02x}-0000-1000-8000-00805f9b34fb"] = data[ 4:] # 0x21 is service data with 128-bit UUID for section in event_args.advertisement.get_sections_by_type(0x21): data = bytearray( CryptographicBuffer.copy_to_byte_array(section.data)) service_data[str(UUID(bytes=bytes(data[15::-1])))] = data[16:] # Use the BLEDevice to populate all the fields for the advertisement data to return advertisement_data = AdvertisementData( local_name=event_args.advertisement.local_name, manufacturer_data=device.metadata["manufacturer_data"], service_data=service_data, service_uuids=device.metadata["uuids"], platform_data=(sender, event_args), ) self._callback(device, advertisement_data)
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)
def callback(p: CBPeripheral, a: Dict[str, Any], r: int) -> None: # update identifiers for scanned device self._identifiers.setdefault(p.identifier(), {}).update(a) if not self._callback: return # Process service data service_data_dict_raw = a.get("kCBAdvDataServiceData", {}) service_data = { cb_uuid_to_str(k): bytes(v) for k, v in service_data_dict_raw.items() } # Process manufacturer data into a more friendly format manufacturer_binary_data = a.get("kCBAdvDataManufacturerData") manufacturer_data = {} if manufacturer_binary_data: manufacturer_id = int.from_bytes( manufacturer_binary_data[0:2], byteorder="little" ) manufacturer_value = bytes(manufacturer_binary_data[2:]) manufacturer_data[manufacturer_id] = manufacturer_value service_uuids = [ cb_uuid_to_str(u) for u in a.get("kCBAdvDataServiceUUIDs", []) ] advertisement_data = AdvertisementData( local_name=p.name(), manufacturer_data=manufacturer_data, service_data=service_data, service_uuids=service_uuids, platform_data=(p, a, r), ) device = BLEDevice( p.identifier().UUIDString(), p.name(), p, r, uuids=service_uuids, manufacturer_data=manufacturer_data, service_data=service_data, delegate=self._manager.central_manager.delegate(), ) self._callback(device, advertisement_data)
def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfoBleak: """Make a dummy advertisement.""" return BluetoothServiceInfoBleak( name="Test Device", address=address, device=BLEDevice(address, None), rssi=-56, manufacturer_data={}, service_data={ "0000181c-0000-1000-8000-00805f9b34fb": payload, }, service_uuids=["0000181c-0000-1000-8000-00805f9b34fb"], source="local", advertisement=AdvertisementData(local_name="Test Device"), time=0, connectable=False, )
def _invoke_callback(self, path: str, message: Message) -> None: """Invokes the advertising data callback. Args: message: The D-Bus message that triggered the callback. """ if self._callback is None: return props = self._devices[path] # Get all the information wanted to pack in the advertisement data _local_name = props.get("Name") _manufacturer_data = { k: bytes(v) for k, v in props.get("ManufacturerData", {}).items() } _service_data = { k: bytes(v) for k, v in props.get("ServiceData", {}).items() } _service_uuids = props.get("UUIDs", []) # Pack the advertisement data advertisement_data = AdvertisementData( local_name=_local_name, manufacturer_data=_manufacturer_data, service_data=_service_data, service_uuids=_service_uuids, platform_data=(props, message), ) device = BLEDevice( props["Address"], props["Alias"], { "path": path, "props": props }, props.get("RSSI", 0), ) self._callback(device, advertisement_data)
def _callback(device: BLEDevice, adv: AdvertisementData): adv = process_advertisement(device, adv) if adv: for callback in self._callbacks: callback(adv.dict())
await hass.async_block_till_done() return entry WOHAND_SERVICE_INFO = BluetoothServiceInfoBleak( name="WoHand", manufacturer_data={89: b"\xfd`0U\x92W"}, service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"}, service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], address="aa:bb:cc:dd:ee:ff", rssi=-60, source="local", advertisement=AdvertisementData( local_name="WoHand", manufacturer_data={89: b"\xfd`0U\x92W"}, service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"}, service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], ), device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoHand"), time=0, connectable=True, ) WOHAND_SERVICE_INFO_NOT_CONNECTABLE = BluetoothServiceInfoBleak( name="WoHand", manufacturer_data={89: b"\xfd`0U\x92W"}, service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"}, service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], address="aa:bb:cc:dd:ee:ff", rssi=-60, source="local",
def parse_msg(self, message): if message.member == "InterfacesAdded": msg_path = message.body[0] try: device_interface = message.body[1].get(defs.DEVICE_INTERFACE, {}) except Exception as e: raise e self._devices[msg_path] = ({ **self._devices[msg_path], **device_interface } if msg_path in self._devices else device_interface) elif message.member == "PropertiesChanged": iface, changed, invalidated = message.body if iface != defs.DEVICE_INTERFACE: return msg_path = message.path # the PropertiesChanged signal only sends changed properties, so we # need to get remaining properties from cached_devices. However, we # don't want to add all cached_devices to the devices dict since # they may not actually be nearby or powered on. if msg_path not in self._devices and msg_path in self._cached_devices: self._devices[msg_path] = self._cached_devices[msg_path] self._devices[msg_path] = ({ **self._devices[msg_path], **changed } if msg_path in self._devices else changed) if self._callback is None: return props = self._devices[msg_path] # Get all the information wanted to pack in the advertisement data _local_name = props.get("Name") _manufacturer_data = { k: bytes(v) for k, v in props.get("ManufacturerData", {}).items() } _service_data = { k: bytes(v) for k, v in props.get("ServiceData", {}).items() } _service_uuids = props.get("UUIDs", []) # Pack the advertisement data advertisement_data = AdvertisementData( local_name=_local_name, manufacturer_data=_manufacturer_data, service_data=_service_data, service_uuids=_service_uuids, platform_data=(props, message), ) device = BLEDevice(props["Address"], props["Alias"], props, props.get("RSSI", 0)) self._callback(device, advertisement_data) elif (message.member == "InterfacesRemoved" and message.body[1][0] == defs.BATTERY_INTERFACE): logger.debug("{0}, {1} ({2}): {3}".format(message.member, message.interface, message.path, message.body)) return else: msg_path = message.path logger.debug("{0}, {1} ({2}): {3}".format(message.member, message.interface, message.path, message.body)) logger.debug("{0}, {1} ({2} dBm), Object Path: {3}".format( *_device_info(msg_path, self._devices.get(msg_path))))
async def start(self): """Start scanning for devices.""" for device in devices: self._callback(device, AdvertisementData())
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
from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData from homeassistant.components.bluetooth import BluetoothServiceInfoBleak NOT_SENSOR_PUSH_SERVICE_INFO = BluetoothServiceInfoBleak( name="Not it", address="00:00:00:00:00:00", device=BLEDevice("00:00:00:00:00:00", None), rssi=-63, manufacturer_data={3234: b"\x00\x01"}, service_data={}, service_uuids=[], source="local", advertisement=AdvertisementData(local_name="Not it"), time=0, connectable=False, ) LYWSDCGQ_SERVICE_INFO = BluetoothServiceInfoBleak( name="LYWSDCGQ", address="58:2D:34:35:93:21", device=BLEDevice("00:00:00:00:00:00", None), rssi=-63, manufacturer_data={}, service_data={ "0000fe95-0000-1000-8000-00805f9b34fb": b"P \xaa\x01\xda!\x9354-X\r\x10\x04\xfe\x00H\x02" }, service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"],
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
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak YALE_ACCESS_LOCK_DISCOVERY_INFO = BluetoothServiceInfoBleak( name="M1012LU", address="AA:BB:CC:DD:EE:FF", rssi=-60, manufacturer_data={ 465: b"\x00\x00\xd1\xf0b;\xd8\x1dE\xd6\xba\xeeL\xdd]\xf5\xb2\xe9", 76: b"\x061\x00Z\x8f\x93\xb2\xec\x85\x06\x00i\x00\x02\x02Q\xed\x1d\xf0", }, service_uuids=[], service_data={}, source="local", device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="M1012LU"), advertisement=AdvertisementData(), time=0, connectable=True, ) LOCK_DISCOVERY_INFO_UUID_ADDRESS = BluetoothServiceInfoBleak( name="M1012LU", address="61DE521B-F0BF-9F44-64D4-75BBE1738105", rssi=-60, manufacturer_data={ 465: b"\x00\x00\xd1\xf0b;\xd8\x1dE\xd6\xba\xeeL\xdd]\xf5\xb2\xe9", 76: b"\x061\x00Z\x8f\x93\xb2\xec\x85\x06\x00i\x00\x02\x02Q\xed\x1d\xf0", }, service_uuids=[], service_data={},
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