async def _async_poll(service_info: BluetoothServiceInfoBleak): # BluetoothServiceInfoBleak is defined in HA, otherwise would just pass it # directly to the Xiaomi code # Make sure the device we have is one that we can connect with # in case its coming from a passive scanner if service_info.connectable: connectable_device = service_info.device elif device := async_ble_device_from_address( hass, service_info.device.address, True): connectable_device = device
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 _async_see_update_ble_battery( mac: str, now: datetime, service_info: bluetooth.BluetoothServiceInfoBleak, ) -> None: """Lookup Bluetooth LE devices and update status.""" battery = None # We need one we can connect to since the tracker will # accept devices from non-connectable sources if service_info.connectable: device = service_info.device elif connectable_device := bluetooth.async_ble_device_from_address( hass, service_info.device.address, True): device = connectable_device
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 async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Switchbot from a config entry.""" assert entry.unique_id is not None hass.data.setdefault(DOMAIN, {}) if CONF_ADDRESS not in entry.data and CONF_MAC in entry.data: # Bleak uses addresses not mac addresses which are are actually # UUIDs on some platforms (MacOS). mac = entry.data[CONF_MAC] if "-" not in mac: mac = dr.format_mac(mac) hass.config_entries.async_update_entry( entry, data={ **entry.data, CONF_ADDRESS: mac }, ) if not entry.options: hass.config_entries.async_update_entry( entry, options={CONF_RETRY_COUNT: DEFAULT_RETRY_COUNT}, ) sensor_type: str = entry.data[CONF_SENSOR_TYPE] switchbot_model = HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL[sensor_type] # connectable means we can make connections to the device connectable = switchbot_model in CONNECTABLE_SUPPORTED_MODEL_TYPES address: str = entry.data[CONF_ADDRESS] ble_device = bluetooth.async_ble_device_from_address( hass, address.upper(), connectable) if not ble_device: raise ConfigEntryNotReady( f"Could not find Switchbot {sensor_type} with address {address}") cls = CLASS_BY_DEVICE.get(sensor_type, switchbot.SwitchbotDevice) device = cls( device=ble_device, password=entry.data.get(CONF_PASSWORD), retry_count=entry.options[CONF_RETRY_COUNT], ) coordinator = hass.data[DOMAIN][ entry.entry_id] = SwitchbotDataUpdateCoordinator( hass, _LOGGER, ble_device, device, entry.unique_id, entry.data.get(CONF_NAME, entry.title), connectable, switchbot_model, ) entry.async_on_unload(coordinator.async_start()) if not await coordinator.async_wait_ready(): raise ConfigEntryNotReady( f"Switchbot {sensor_type} with {address} not ready") entry.async_on_unload(entry.add_update_listener(_async_update_listener)) await hass.config_entries.async_forward_entry_setups( entry, PLATFORMS_BY_TYPE[sensor_type]) return True
async def async_connect_and_update(self) -> AsyncIterator[Device]: """Provide an up to date device for use during connections.""" if ble_device := async_ble_device_from_address(self.hass, self.device.address): self.device.device = ble_device
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up LED BLE from a config entry.""" address: str = entry.data[CONF_ADDRESS] ble_device = bluetooth.async_ble_device_from_address( hass, address.upper(), True) if not ble_device: raise ConfigEntryNotReady( f"Could not find LED BLE device with address {address}") led_ble = LEDBLE(ble_device) @callback def _async_update_ble( service_info: bluetooth.BluetoothServiceInfoBleak, change: bluetooth.BluetoothChange, ) -> None: """Update from a ble callback.""" led_ble.set_ble_device(service_info.device) entry.async_on_unload( bluetooth.async_register_callback( hass, _async_update_ble, BluetoothCallbackMatcher({ADDRESS: address}), bluetooth.BluetoothScanningMode.PASSIVE, )) async def _async_update(): """Update the device state.""" try: await led_ble.update() except BLEAK_EXCEPTIONS as ex: raise UpdateFailed(str(ex)) from ex startup_event = asyncio.Event() cancel_first_update = led_ble.register_callback( lambda *_: startup_event.set()) coordinator = DataUpdateCoordinator( hass, _LOGGER, name=led_ble.name, update_method=_async_update, update_interval=timedelta(seconds=UPDATE_SECONDS), ) try: await coordinator.async_config_entry_first_refresh() except ConfigEntryNotReady: cancel_first_update() raise try: async with async_timeout.timeout(DEVICE_TIMEOUT): await startup_event.wait() except asyncio.TimeoutError as ex: raise ConfigEntryNotReady( "Unable to communicate with the device; " f"Try moving the Bluetooth adapter closer to {led_ble.name}" ) from ex finally: cancel_first_update() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = LEDBLEData( entry.title, led_ble, coordinator) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) async def _async_stop(event: Event) -> None: """Close the connection.""" await led_ble.stop() entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop)) return True