-
-
Notifications
You must be signed in to change notification settings - Fork 38.3k
Refactor Bluetooth Tracker to async #26614
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’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
d973b4d
8da6554
2c9009d
5d807f2
038dd9f
c9e72f1
0f11d61
d55dc55
8f2b78b
9e1615b
59b897e
a6b59a1
8336a68
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 @@ | |
| 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,103 +65,133 @@ 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( | ||
| mac=f"{BT_PREFIX}{mac}", | ||
| host_name=device_name, | ||
| attributes=attributes, | ||
| source_type=SOURCE_TYPE_BLUETOOTH, | ||
|
|
||
| await 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): | ||
| 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: | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
| 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) | ||
| see_device(see, mac, device_name) | ||
|
|
||
| interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) | ||
| if mac in devices_to_track: | ||
| await see_device(hass, async_see, mac, device_name) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could optimize this by using tasks = []
for mac, device_name in devices:
...
tasks.append(see_device(hass, async_see, mac, device_name))
if tasks:
await asyncio.wait(tasks)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice 👏 |
||
|
|
||
| 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.""" | ||
|
|
||
| def update_bluetooth_once(): | ||
| """Lookup Bluetooth device and update status.""" | ||
| _LOGGER.debug("Performing Bluetooth devices discovery and update") | ||
| 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) | ||
| device_name = await hass.async_add_executor_job(lookup_name, mac) | ||
| if device_name is None: | ||
| # Could not lookup device name | ||
| continue | ||
|
|
||
| rssi = None | ||
| if request_rssi: | ||
| client = BluetoothRSSI(mac) | ||
| rssi = client.request_rssi() | ||
| rssi = await hass.async_add_executor_job(client.request_rssi) | ||
| client.close() | ||
| if device_name is None: | ||
| # Could not lookup device name | ||
| continue | ||
| see_device(see, mac, device_name, rssi) | ||
|
|
||
| await see_device(hass, async_see, mac, device_name, rssi) | ||
|
|
||
| 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.info( | ||
| "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.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 | ||
Uh oh!
There was an error while loading. Please reload this page.