Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update bluetooth_le_tracker to use Bleak #75013

Merged
merged 28 commits into from Jul 19, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
978c47f
Update bluetooth_le_tracker to use Bleak
bdraco Jul 11, 2022
118d263
Update homeassistant/components/bluetooth_le_tracker/device_tracker.py
bdraco Jul 12, 2022
98181ad
Update homeassistant/components/bluetooth_le_tracker/device_tracker.py
bdraco Jul 12, 2022
ba00372
Update homeassistant/components/bluetooth_le_tracker/device_tracker.py
bdraco Jul 12, 2022
1ca9457
Update homeassistant/components/bluetooth_le_tracker/device_tracker.py
bdraco Jul 12, 2022
17ad22f
Merge branch 'dev' into btle_tracker
bdraco Jul 12, 2022
a8a3059
set
bdraco Jul 12, 2022
b135bc0
still do interval updates to ensure everything stays home
bdraco Jul 12, 2022
e884965
still do interval updates to ensure everything stays home
bdraco Jul 12, 2022
e735762
still do interval updates to ensure everything stays home
bdraco Jul 12, 2022
61a3474
naming
bdraco Jul 12, 2022
a09fb78
naming
bdraco Jul 12, 2022
092e6df
adapt test
bdraco Jul 12, 2022
d6a287a
Add mock_bluetooth fixture
bdraco Jul 12, 2022
3bfbfcc
Merge branch 'bluetooth_fixtures' into btle_tracker
bdraco Jul 12, 2022
5ddc99b
mock bluetooth in test
bdraco Jul 12, 2022
aa972dc
mock bluetooth in test
bdraco Jul 12, 2022
376beac
Merge branch 'dev' into btle_tracker
bdraco Jul 14, 2022
41bdb66
Merge branch 'dev' into btle_tracker
bdraco Jul 14, 2022
b1e6c26
Merge branch 'dev' into btle_tracker
bdraco Jul 16, 2022
4c63aba
Add device and advertisement to BluetoothServiceInfoBleak
bdraco Jul 17, 2022
23db047
convert address to bledevice
bdraco Jul 17, 2022
0ec7531
convert address to bledevice
bdraco Jul 17, 2022
2cb7319
Merge branch 'service_info_with_bleak' into btle_tracker
bdraco Jul 18, 2022
d4ac77f
fixes
bdraco Jul 18, 2022
6f29ef2
cover
bdraco Jul 18, 2022
231c4e8
Update homeassistant/components/bluetooth_le_tracker/device_tracker.py
bdraco Jul 18, 2022
d0125c1
Merge branch 'dev' into btle_tracker
bdraco Jul 18, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
137 changes: 58 additions & 79 deletions homeassistant/components/bluetooth_le_tracker/device_tracker.py
Expand Up @@ -2,31 +2,29 @@
from __future__ import annotations

import asyncio
from collections.abc import Callable
from collections.abc import Awaitable, Callable
from datetime import datetime, timedelta
import logging
from uuid import UUID

import pygatt
from bleak import BleakClient, BleakError
import voluptuous as vol

from homeassistant.components import bluetooth
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
)
from homeassistant.components.device_tracker.const import (
CONF_SCAN_INTERVAL,
CONF_TRACK_NEW,
SCAN_INTERVAL,
SOURCE_TYPE_BLUETOOTH_LE,
)
from homeassistant.components.device_tracker.legacy import (
YAML_DEVICES,
async_load_config,
)
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.core import Event, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
import homeassistant.util.dt as dt_util

Expand All @@ -53,33 +51,23 @@
)


def setup_scanner( # noqa: C901
async def async_setup_scanner(
hass: HomeAssistant,
config: ConfigType,
see: Callable[..., None],
async_see: Callable[..., Awaitable[None]],
discovery_info: DiscoveryInfoType | None = None,
) -> bool:
"""Set up the Bluetooth LE Scanner."""

new_devices: dict[str, dict] = {}
hass.data.setdefault(DATA_BLE, {DATA_BLE_ADAPTER: None})
bdraco marked this conversation as resolved.
Show resolved Hide resolved

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):
async def async_see_device(address, name, new_device=False, battery=None):
"""Mark a device as seen."""
if name is not None:
name = name.strip("\x00")
Expand All @@ -105,28 +93,13 @@ def see_device(address, name, new_device=False, battery=None):
new_devices[address] = {"seen": 1, "name": name}
return

see(
await async_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 = []
bdraco marked this conversation as resolved.
Show resolved Hide resolved
devs_donot_track = []
Expand All @@ -135,9 +108,7 @@ def discover_ble_devices():
# 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, timedelta(0)), hass.loop
).result():
for device in await async_load_config(yaml_path, hass, timedelta(0)):
# check if device is a valid bluetooth device
if device.mac and device.mac[:4].upper() == BLE_PREFIX:
address = device.mac[4:]
Expand All @@ -160,54 +131,62 @@ def discover_ble_devices():
_LOGGER.warning("No Bluetooth LE devices to track!")
return False

interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)

def update_ble(now):
async def _async_see_update_ble_battery(
mac: str,
now: datetime,
service_info: bluetooth.BluetoothServiceInfo,
):
bdraco marked this conversation as resolved.
Show resolved Hide resolved
"""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
battery = None
try:
async with BleakClient(mac) as client:
bdraco marked this conversation as resolved.
Show resolved Hide resolved
bat_char = await client.read_gatt_char(BATTERY_CHARACTERISTIC_UUID)
battery = ord(bat_char)
# Try to get the handle; it will raise a BLEError exception if not available
except asyncio.TimeoutError:
_LOGGER.warning(
"Timeout when trying to get battery status for %s", service_info.name
)
except BleakError as err:
_LOGGER.debug("Could not read battery status: %s", err)
# 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]
if battery:
await async_see_device(mac, service_info.name, battery=battery)

@callback
def _async_update_ble(
service_info: bluetooth.BluetoothServiceInfo, change: bluetooth.BluetoothChange
):
bdraco marked this conversation as resolved.
Show resolved Hide resolved
"""Update from a ble callback."""
mac = service_info.address
if mac in devs_to_track:
now = dt_util.utcnow()
hass.async_create_task(async_see_device(mac, service_info.name))
if (
mac in devs_track_battery
and now > devs_track_battery[mac] + battery_track_interval
):
handle = None
try:
adapter.start(reset_on_start=False)
_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)
devs_track_battery[mac] = now
asyncio.create_task(
_async_see_update_ble_battery(mac, now, service_info)
)

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)
if mac not in devs_to_track and mac not in devs_donot_track:
_LOGGER.info("Discovered Bluetooth LE device %s", mac)
hass.async_create_task(
async_see_device(mac, service_info.name, new_device=True)
)

track_point_in_utc_time(hass, update_ble, dt_util.utcnow() + interval)
cancel = bluetooth.async_register_callback(hass, _async_update_ble, None)

def handle_stop(event: Event) -> None:
bdraco marked this conversation as resolved.
Show resolved Hide resolved
"""Cancel the callback."""
cancel()

hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop)

update_ble(dt_util.utcnow())
return True
6 changes: 3 additions & 3 deletions homeassistant/components/bluetooth_le_tracker/manifest.json
Expand Up @@ -2,8 +2,8 @@
"domain": "bluetooth_le_tracker",
"name": "Bluetooth LE Tracker",
"documentation": "https://www.home-assistant.io/integrations/bluetooth_le_tracker",
"requirements": ["pygatt[GATTTOOL]==4.0.5"],
"dependencies": ["bluetooth"],
"codeowners": [],
"iot_class": "local_polling",
"loggers": ["pygatt"]
"iot_class": "local_push",
"loggers": []
}
1 change: 0 additions & 1 deletion requirements_all.txt
Expand Up @@ -1527,7 +1527,6 @@ pyfronius==0.7.1
# homeassistant.components.ifttt
pyfttt==0.3

# homeassistant.components.bluetooth_le_tracker
# homeassistant.components.skybeacon
pygatt[GATTTOOL]==4.0.5

Expand Down
4 changes: 0 additions & 4 deletions requirements_test_all.txt
Expand Up @@ -1033,10 +1033,6 @@ pyfronius==0.7.1
# homeassistant.components.ifttt
pyfttt==0.3

# homeassistant.components.bluetooth_le_tracker
# homeassistant.components.skybeacon
pygatt[GATTTOOL]==4.0.5

# homeassistant.components.hvv_departures
pygti==0.9.2

Expand Down