From c8cce7607c5c6482f42a6ed0e91d209c9f56953e Mon Sep 17 00:00:00 2001 From: guillempages Date: Sat, 9 May 2020 22:18:35 +0200 Subject: [PATCH] Add battery information to BLE devices (#33222) * Add battery information to BLE devices * Check Bluetooth LE battery at most once a day --- .../bluetooth_le_tracker/device_tracker.py | 59 +++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) mode change 100644 => 100755 homeassistant/components/bluetooth_le_tracker/device_tracker.py diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py old mode 100644 new mode 100755 index 4957356d26a..47f7afe1e13 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -1,6 +1,8 @@ """Tracking for bluetooth low energy devices.""" import asyncio +from datetime import datetime, timedelta import logging +from uuid import UUID import pygatt # pylint: disable=import-error @@ -20,6 +22,11 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) +# Base UUID: 00000000-0000-1000-8000-00805F9B34FB +# Battery characteristic: 0x2a19 (https://www.bluetooth.com/specifications/gatt/characteristics/) +BATTERY_CHARACTERISTIC_UUID = UUID("00002a19-0000-1000-8000-00805f9b34fb") +CONF_TRACK_BATTERY = "track_battery" +DEFAULT_TRACK_BATTERY_INTERVAL = timedelta(days=1) DATA_BLE = "BLE" DATA_BLE_ADAPTER = "ADAPTER" BLE_PREFIX = "BLE_" @@ -42,7 +49,12 @@ def setup_scanner(hass, config, see, discovery_info=None): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop) - def see_device(address, name, new_device=False): + if config.get(CONF_TRACK_BATTERY): + battery_track_interval = DEFAULT_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") @@ -59,6 +71,10 @@ def setup_scanner(hass, config, see, discovery_info=None): 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} @@ -68,6 +84,7 @@ def setup_scanner(hass, config, see, discovery_info=None): mac=BLE_PREFIX + address, host_name=name, source_type=SOURCE_TYPE_BLUETOOTH_LE, + battery=battery, ) def discover_ble_devices(): @@ -88,6 +105,7 @@ def setup_scanner(hass, config, see, discovery_info=None): 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 @@ -97,12 +115,17 @@ def setup_scanner(hass, config, see, discovery_info=None): ).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(device.mac[4:]) + 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(device.mac[4:]) + devs_donot_track.append(address) # if track new devices is true discover new devices # on every scan. @@ -117,13 +140,41 @@ def setup_scanner(hass, config, see, discovery_info=None): 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 - see_device(mac, devs[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: