diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 8f01036da75..6a26775b0a8 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -1,6 +1,7 @@ """Tracking for bluetooth devices.""" +import asyncio import logging -from typing import List, Set, Tuple +from typing import List, Set, Tuple, Optional # pylint: disable=import-error import bluetooth @@ -21,10 +22,9 @@ from homeassistant.components.device_tracker.legacy import ( 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__) @@ -65,12 +65,15 @@ def discover_devices(device_id: int) -> List[Tuple[str, str]]: return result -def see_device(see, mac: str, device_name: str, rssi=None) -> None: +async 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( + + await async_see( mac=f"{BT_PREFIX}{mac}", host_name=device_name, attributes=attributes, @@ -78,90 +81,111 @@ def see_device(see, mac: str, device_name: str, rssi=None) -> None: ) -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 + devices = await async_load_config(yaml_path, hass, 0) + bluetooth_devices = [device for device in devices if is_bluetooth_device(device)] - normalized_mac: str = device.mac[3:] - if device.track: - devices_to_track.add(normalized_mac) - else: - devices_to_not_track.add(normalized_mac) + 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): +def lookup_name(mac: str) -> Optional[str]: + """Lookup a Bluetooth device name.""" + _LOGGER.debug("Scanning %s", mac) + return bluetooth.lookup_name(mac, timeout=5) + + +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) + _LOGGER.debug("Tracking new devices is set to %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: - 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) - - 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) + async def perform_bluetooth_update(): + """Discover Bluetooth devices and update status.""" + + _LOGGER.debug("Performing Bluetooth devices discovery and update") + tasks = [] - def update_bluetooth_once(): - """Lookup Bluetooth device and update status.""" try: if track_new: - for mac, device_name in discover_devices(device_id): + devices = await hass.async_add_executor_job(discover_devices, device_id) + for mac, device_name in devices: 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() + device_name = await hass.async_add_executor_job(lookup_name, mac) if device_name is None: # Could not lookup device name continue - see_device(see, mac, device_name, rssi) + + rssi = None + if request_rssi: + client = BluetoothRSSI(mac) + rssi = await hass.async_add_executor_job(client.request_rssi) + client.close() + + tasks.append(see_device(hass, async_see, mac, device_name, rssi)) + + if tasks: + await asyncio.wait(tasks) + except bluetooth.BluetoothError: _LOGGER.exception("Error looking up Bluetooth device") - def handle_update_bluetooth(call): + 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.debug( + "Previous execution of update_bluetooth is taking longer than the scheduled update of interval %s", + interval, + ) + return + + async with update_bluetooth_lock: + await perform_bluetooth_update() + + async def handle_manual_update_bluetooth(call): """Update bluetooth devices on demand.""" - update_bluetooth_once() - update_bluetooth(dt_util.utcnow()) + await update_bluetooth() - hass.services.register(DOMAIN, "bluetooth_tracker_update", handle_update_bluetooth) + hass.async_create_task(update_bluetooth()) + async_track_time_interval(hass, update_bluetooth, interval) + + hass.services.async_register( + DOMAIN, "bluetooth_tracker_update", handle_manual_update_bluetooth + ) return True