Skip to content
146 changes: 84 additions & 62 deletions homeassistant/components/bluetooth_tracker/device_tracker.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tracking for bluetooth devices."""
import asyncio
import logging
from typing import List, Set, Tuple

Expand All @@ -21,10 +22,9 @@
async_load_config,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.async_ import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util


_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -65,103 +65,125 @@ def discover_devices(device_id: int) -> List[Tuple[str, str]]:
return result


def see_device(see, mac: str, device_name: str, rssi=None) -> None:
def see_device(
hass: HomeAssistantType, async_see, mac: str, device_name: str, rssi=None
) -> None:
"""Mark a device as seen."""
attributes = {}
if rssi is not None:
attributes["rssi"] = rssi
see(
mac=f"{BT_PREFIX}{mac}",
host_name=device_name,
attributes=attributes,
source_type=SOURCE_TYPE_BLUETOOTH,

hass.async_create_task(
async_see(
mac=f"{BT_PREFIX}{mac}",
host_name=device_name,
attributes=attributes,
source_type=SOURCE_TYPE_BLUETOOTH,
)
)


def get_tracking_devices(hass: HomeAssistantType) -> Tuple[Set[str], Set[str]]:
async def get_tracking_devices(hass: HomeAssistantType) -> Tuple[Set[str], Set[str]]:
"""
Load all known devices.

We just need the devices so set consider_home and home range to 0
"""
yaml_path: str = hass.config.path(YAML_DEVICES)
devices_to_track: Set[str] = set()
devices_to_not_track: Set[str] = set()

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 not is_bluetooth_device(device):
continue

normalized_mac: str = device.mac[3:]
if device.track:
devices_to_track.add(normalized_mac)
else:
devices_to_not_track.add(normalized_mac)

devices = await async_load_config(yaml_path, hass, 0)
bluetooth_devices = [device for device in devices if is_bluetooth_device(device)]

devices_to_track: Set[str] = {
device.mac[3:] for device in bluetooth_devices if device.track
}
devices_to_not_track: Set[str] = {
device.mac[3:] for device in bluetooth_devices if not device.track
}

return devices_to_track, devices_to_not_track


def setup_scanner(hass: HomeAssistantType, config: dict, see, discovery_info=None):
async def async_setup_scanner(
hass: HomeAssistantType, config: dict, async_see, discovery_info=None
):
"""Set up the Bluetooth Scanner."""
device_id: int = config.get(CONF_DEVICE_ID)
devices_to_track, devices_to_not_track = get_tracking_devices(hass)
interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
request_rssi = config.get(CONF_REQUEST_RSSI, False)
update_bluetooth_lock = asyncio.Lock()

# If track new devices is true discover new devices on startup.
track_new: bool = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
_LOGGER.debug("Tracking new devices = %s", track_new)

devices_to_track, devices_to_not_track = await get_tracking_devices(hass)

if not devices_to_track and not track_new:
_LOGGER.debug("No Bluetooth devices to track and not tracking new devices")

if track_new:

@pgilad pgilad Sep 13, 2019

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MartinHjelmare What do you think about removing this? I'm talking about this block. We call update_bluetooth immediately on bootstrap (https://github.com/home-assistant/home-assistant/pull/26614/files#diff-a1148ba3a37cfd77fa0c986c69f86b2dR1980) which is a much more complete solution

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the difference is that the bluetooth update takes longer time? But it looks like we can remove it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They both run at startup one after the other, the 2nd run being a more complete discovery and update, so I would say the 1st was redundant

for mac, device_name in discover_devices(device_id):
if mac not in devices_to_track and mac not in devices_to_not_track:
devices_to_track.add(mac)
see_device(see, mac, device_name)

interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
if mac in devices_to_track:
see_device(hass, async_see, mac, device_name)

request_rssi = config.get(CONF_REQUEST_RSSI, False)
if request_rssi:
_LOGGER.debug("Detecting RSSI for devices")

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 mac, device_name in discover_devices(device_id):
if mac not in devices_to_track and mac not in devices_to_not_track:
devices_to_track.add(mac)

for mac in devices_to_track:
_LOGGER.debug("Scanning %s", mac)
device_name = bluetooth.lookup_name(mac, timeout=5)
rssi = None
if request_rssi:
client = BluetoothRSSI(mac)
rssi = client.request_rssi()
client.close()
if device_name is None:
# Could not lookup device name
continue
see_device(see, mac, device_name, rssi)
except bluetooth.BluetoothError:
_LOGGER.exception("Error looking up Bluetooth device")

def handle_update_bluetooth(call):
def perform_bluetooth_update():
"""Discover Bluetooth devices and update status."""

if track_new:
for mac, device_name in discover_devices(device_id):
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated
if mac not in devices_to_track and mac not in devices_to_not_track:
devices_to_track.add(mac)

for mac in devices_to_track:
_LOGGER.debug("Scanning %s", mac)
device_name = bluetooth.lookup_name(mac, timeout=5)
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated
if device_name is None:
# Could not lookup device name
continue

rssi = None
if request_rssi:
client = BluetoothRSSI(mac)
rssi = client.request_rssi()
client.close()
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated

see_device(hass, async_see, mac, device_name, rssi)

async def update_bluetooth(now=None):
"""Lookup Bluetooth devices and update status."""

# If an update is in progress, we don't do anything
if update_bluetooth_lock.locked():
_LOGGER.warning(
"Updating %s took longer than the scheduled update of interval %s",
DOMAIN,
interval,
)
return

async with update_bluetooth_lock:
try:
perform_bluetooth_update()
except bluetooth.BluetoothError:
_LOGGER.exception("Error looking up Bluetooth device")

async def handle_manual_update_bluetooth(call):
"""Update bluetooth devices on demand."""
update_bluetooth_once()

update_bluetooth(dt_util.utcnow())
await update_bluetooth()

hass.async_create_task(update_bluetooth())
async_track_time_interval(hass, update_bluetooth, interval)

hass.services.register(DOMAIN, "bluetooth_tracker_update", handle_update_bluetooth)
hass.services.async_register(
DOMAIN, "bluetooth_tracker_update", handle_manual_update_bluetooth
)

return True