def setup_scanner(hass, config, see, discovery_info=None): """Set up the Bluetooth Scanner.""" # pylint: disable=import-error import bluetooth from bt_proximity import BluetoothRSSI def see_device(mac, name, rssi=None): """Mark a device as seen.""" attributes = {} if rssi is not None: attributes["rssi"] = rssi see( mac=f"{BT_PREFIX}{mac}", host_name=name, attributes=attributes, source_type=SOURCE_TYPE_BLUETOOTH, ) device_id = config.get(CONF_DEVICE_ID) def discover_devices(): """Discover Bluetooth devices.""" result = bluetooth.discover_devices( duration=8, lookup_names=True, flush_cache=True, lookup_class=False, device_id=device_id, ) _LOGGER.debug("Bluetooth devices discovered = %d", len(result)) return result yaml_path = hass.config.path(YAML_DEVICES) devs_to_track = [] devs_donot_track = [] # Load all known devices. # We just need the devices so set consider_home and home range # to 0 for device in run_coroutine_threadsafe( async_load_config(yaml_path, hass, 0), hass.loop).result(): # Check if device is a valid bluetooth device if device.mac and device.mac[:3].upper() == BT_PREFIX: if device.track: devs_to_track.append(device.mac[3:]) else: devs_donot_track.append(device.mac[3:]) # If track new devices is true discover new devices on startup. track_new = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) if track_new: for dev in discover_devices(): if dev[0] not in devs_to_track and dev[0] not in devs_donot_track: devs_to_track.append(dev[0]) see_device(dev[0], dev[1]) interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) request_rssi = config.get(CONF_REQUEST_RSSI, False) def update_bluetooth(_): """Update Bluetooth and set timer for the next update.""" update_bluetooth_once() track_point_in_utc_time(hass, update_bluetooth, dt_util.utcnow() + interval) def update_bluetooth_once(): """Lookup Bluetooth device and update status.""" try: if track_new: for dev in discover_devices(): if dev[0] not in devs_to_track and dev[ 0] not in devs_donot_track: devs_to_track.append(dev[0]) for mac in devs_to_track: _LOGGER.debug("Scanning %s", mac) result = bluetooth.lookup_name(mac, timeout=5) rssi = None if request_rssi: client = BluetoothRSSI(mac) rssi = client.request_rssi() client.close() if result is None: # Could not lookup device name continue see_device(mac, result, rssi) except bluetooth.BluetoothError: _LOGGER.exception("Error looking up Bluetooth device") def handle_update_bluetooth(call): """Update bluetooth devices on demand.""" update_bluetooth_once() update_bluetooth(dt_util.utcnow()) hass.services.register(DOMAIN, "bluetooth_tracker_update", handle_update_bluetooth) return True
def setup_scanner(hass, config, see, discovery_info=None): """Set up the Bluetooth LE Scanner.""" new_devices = {} hass.data.setdefault(DATA_BLE, {DATA_BLE_ADAPTER: None}) def handle_stop(event): """Try to shut down the bluetooth child process nicely.""" # These should never be unset at the point this runs, but just for # safety's sake, use `get`. adapter = hass.data.get(DATA_BLE, {}).get(DATA_BLE_ADAPTER) if adapter is not None: adapter.kill() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop) if config[CONF_TRACK_BATTERY]: battery_track_interval = config[CONF_TRACK_BATTERY_INTERVAL] else: battery_track_interval = timedelta(0) def see_device(address, name, new_device=False, battery=None): """Mark a device as seen.""" if name is not None: name = name.strip("\x00") if new_device: if address in new_devices: new_devices[address]["seen"] += 1 if name: new_devices[address]["name"] = name else: name = new_devices[address]["name"] _LOGGER.debug("Seen %s %s times", address, new_devices[address]["seen"]) if new_devices[address]["seen"] < MIN_SEEN_NEW: return _LOGGER.debug("Adding %s to tracked devices", address) devs_to_track.append(address) if battery_track_interval > timedelta(0): devs_track_battery[address] = dt_util.as_utc( datetime.fromtimestamp(0) ) else: _LOGGER.debug("Seen %s for the first time", address) new_devices[address] = {"seen": 1, "name": name} return see( mac=BLE_PREFIX + address, host_name=name, source_type=SOURCE_TYPE_BLUETOOTH_LE, battery=battery, ) def discover_ble_devices(): """Discover Bluetooth LE devices.""" _LOGGER.debug("Discovering Bluetooth LE devices") try: adapter = pygatt.GATTToolBackend() hass.data[DATA_BLE][DATA_BLE_ADAPTER] = adapter devs = adapter.scan() devices = {x["address"]: x["name"] for x in devs} _LOGGER.debug("Bluetooth LE devices discovered = %s", devices) except (RuntimeError, pygatt.exceptions.BLEError) as error: _LOGGER.error("Error during Bluetooth LE scan: %s", error) return {} return devices yaml_path = hass.config.path(YAML_DEVICES) devs_to_track = [] devs_donot_track = [] devs_track_battery = {} # Load all known devices. # We just need the devices so set consider_home and home range # to 0 for device in asyncio.run_coroutine_threadsafe( async_load_config(yaml_path, hass, 0), hass.loop ).result(): # check if device is a valid bluetooth device if device.mac and device.mac[:4].upper() == BLE_PREFIX: address = device.mac[4:] if device.track: _LOGGER.debug("Adding %s to BLE tracker", device.mac) devs_to_track.append(address) if battery_track_interval > timedelta(0): devs_track_battery[address] = dt_util.as_utc( datetime.fromtimestamp(0) ) else: _LOGGER.debug("Adding %s to BLE do not track", device.mac) devs_donot_track.append(address) # if track new devices is true discover new devices # on every scan. track_new = config.get(CONF_TRACK_NEW) if not devs_to_track and not track_new: _LOGGER.warning("No Bluetooth LE devices to track!") return False interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) def update_ble(now): """Lookup Bluetooth LE devices and update status.""" devs = discover_ble_devices() if devs_track_battery: adapter = hass.data[DATA_BLE][DATA_BLE_ADAPTER] for mac in devs_to_track: if mac not in devs: continue if devs[mac] is None: devs[mac] = mac battery = None if ( mac in devs_track_battery and now > devs_track_battery[mac] + battery_track_interval ): handle = None try: adapter.start(reset_on_start=True) _LOGGER.debug("Reading battery for Bluetooth LE device %s", mac) bt_device = adapter.connect(mac) # Try to get the handle; it will raise a BLEError exception if not available handle = bt_device.get_handle(BATTERY_CHARACTERISTIC_UUID) battery = ord(bt_device.char_read(BATTERY_CHARACTERISTIC_UUID)) devs_track_battery[mac] = now except pygatt.exceptions.NotificationTimeout: _LOGGER.warning("Timeout when trying to get battery status") except pygatt.exceptions.BLEError as err: _LOGGER.warning("Could not read battery status: %s", err) if handle is not None: # If the device does not offer battery information, there is no point in asking again later on. # Remove the device from the battery-tracked devices, so that their battery is not wasted # trying to get an unavailable information. del devs_track_battery[mac] finally: adapter.stop() see_device(mac, devs[mac], battery=battery) if track_new: for address in devs: if address not in devs_to_track and address not in devs_donot_track: _LOGGER.info("Discovered Bluetooth LE device %s", address) see_device(address, devs[address], new_device=True) track_point_in_utc_time(hass, update_ble, dt_util.utcnow() + interval) update_ble(dt_util.utcnow()) return True
def setup_scanner(hass, config, see, discovery_info=None): """Set up the Bluetooth LE Scanner.""" # pylint: disable=import-error import pygatt new_devices = {} hass.data.setdefault(DATA_BLE, {DATA_BLE_ADAPTER: None}) def handle_stop(event): """Try to shut down the bluetooth child process nicely.""" # These should never be unset at the point this runs, but just for # safety's sake, use `get`. adapter = hass.data.get(DATA_BLE, {}).get(DATA_BLE_ADAPTER) if adapter is not None: adapter.kill() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop) def see_device(address, name, new_device=False): """Mark a device as seen.""" if new_device: if address in new_devices: _LOGGER.debug("Seen %s %s times", address, new_devices[address]) new_devices[address] += 1 if new_devices[address] >= MIN_SEEN_NEW: _LOGGER.debug("Adding %s to tracked devices", address) devs_to_track.append(address) else: return else: _LOGGER.debug("Seen %s for the first time", address) new_devices[address] = 1 return if name is not None: name = name.strip("\x00") see( mac=BLE_PREFIX + address, host_name=name, source_type=SOURCE_TYPE_BLUETOOTH_LE, ) def discover_ble_devices(): """Discover Bluetooth LE devices.""" _LOGGER.debug("Discovering Bluetooth LE devices") try: adapter = pygatt.GATTToolBackend() hass.data[DATA_BLE][DATA_BLE_ADAPTER] = adapter devs = adapter.scan() devices = {x["address"]: x["name"] for x in devs} _LOGGER.debug("Bluetooth LE devices discovered = %s", devices) except RuntimeError as error: _LOGGER.error("Error during Bluetooth LE scan: %s", error) return {} return devices yaml_path = hass.config.path(YAML_DEVICES) devs_to_track = [] devs_donot_track = [] # Load all known devices. # We just need the devices so set consider_home and home range # to 0 for device in run_coroutine_threadsafe( async_load_config(yaml_path, hass, 0), hass.loop).result(): # check if device is a valid bluetooth device if device.mac and device.mac[:4].upper() == BLE_PREFIX: if device.track: _LOGGER.debug("Adding %s to BLE tracker", device.mac) devs_to_track.append(device.mac[4:]) else: _LOGGER.debug("Adding %s to BLE do not track", device.mac) devs_donot_track.append(device.mac[4:]) # if track new devices is true discover new devices # on every scan. track_new = config.get(CONF_TRACK_NEW) if not devs_to_track and not track_new: _LOGGER.warning("No Bluetooth LE devices to track!") return False interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) def update_ble(now): """Lookup Bluetooth LE devices and update status.""" devs = discover_ble_devices() for mac in devs_to_track: if mac not in devs: continue if devs[mac] is None: devs[mac] = mac see_device(mac, devs[mac]) if track_new: for address in devs: if address not in devs_to_track and address not in devs_donot_track: _LOGGER.info("Discovered Bluetooth LE device %s", address) see_device(address, devs[address], new_device=True) track_point_in_utc_time(hass, update_ble, dt_util.utcnow() + interval) update_ble(dt_util.utcnow()) return True