2022-07-08 23:55:31 +00:00
|
|
|
"""Models for bluetooth."""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
2022-08-22 18:02:26 +00:00
|
|
|
from abc import abstractmethod
|
2022-07-08 23:55:31 +00:00
|
|
|
import asyncio
|
2022-08-17 00:52:53 +00:00
|
|
|
from collections.abc import Callable
|
2022-07-08 23:55:31 +00:00
|
|
|
import contextlib
|
2022-08-17 00:52:53 +00:00
|
|
|
from dataclasses import dataclass
|
|
|
|
from enum import Enum
|
2022-07-08 23:55:31 +00:00
|
|
|
import logging
|
2022-08-01 15:54:06 +00:00
|
|
|
from typing import TYPE_CHECKING, Any, Final
|
2022-07-08 23:55:31 +00:00
|
|
|
|
2022-08-17 21:42:12 +00:00
|
|
|
from bleak import BleakClient, BleakError
|
|
|
|
from bleak.backends.device import BLEDevice
|
2022-07-11 15:14:00 +00:00
|
|
|
from bleak.backends.scanner import (
|
|
|
|
AdvertisementData,
|
|
|
|
AdvertisementDataCallback,
|
|
|
|
BaseBleakScanner,
|
|
|
|
)
|
2022-07-08 23:55:31 +00:00
|
|
|
|
2022-08-17 20:51:56 +00:00
|
|
|
from homeassistant.core import CALLBACK_TYPE
|
2022-08-17 21:42:12 +00:00
|
|
|
from homeassistant.helpers.frame import report
|
2022-08-17 00:52:53 +00:00
|
|
|
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
|
2022-07-08 23:55:31 +00:00
|
|
|
|
2022-08-01 15:54:06 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
2022-08-17 20:51:56 +00:00
|
|
|
from .manager import BluetoothManager
|
|
|
|
|
2022-08-01 15:54:06 +00:00
|
|
|
|
2022-07-08 23:55:31 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
FILTER_UUIDS: Final = "UUIDs"
|
|
|
|
|
2022-08-17 20:51:56 +00:00
|
|
|
MANAGER: BluetoothManager | None = None
|
2022-07-08 23:55:31 +00:00
|
|
|
|
|
|
|
|
2022-08-17 00:52:53 +00:00
|
|
|
@dataclass
|
|
|
|
class BluetoothServiceInfoBleak(BluetoothServiceInfo):
|
|
|
|
"""BluetoothServiceInfo with bleak data.
|
|
|
|
|
|
|
|
Integrations may need BLEDevice and AdvertisementData
|
|
|
|
to connect to the device without having bleak trigger
|
|
|
|
another scan to translate the address to the system's
|
|
|
|
internal details.
|
|
|
|
"""
|
|
|
|
|
|
|
|
device: BLEDevice
|
|
|
|
advertisement: AdvertisementData
|
2022-08-22 18:02:26 +00:00
|
|
|
connectable: bool
|
|
|
|
time: float
|
2022-08-17 00:52:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
class BluetoothScanningMode(Enum):
|
|
|
|
"""The mode of scanning for bluetooth devices."""
|
|
|
|
|
|
|
|
PASSIVE = "passive"
|
|
|
|
ACTIVE = "active"
|
|
|
|
|
|
|
|
|
|
|
|
BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT")
|
|
|
|
BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None]
|
|
|
|
ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool]
|
|
|
|
|
|
|
|
|
2022-08-22 18:02:26 +00:00
|
|
|
class BaseHaScanner:
|
|
|
|
"""Base class for Ha Scanners."""
|
|
|
|
|
|
|
|
@property
|
|
|
|
@abstractmethod
|
|
|
|
def discovered_devices(self) -> list[BLEDevice]:
|
|
|
|
"""Return a list of discovered devices."""
|
|
|
|
|
|
|
|
|
2022-07-30 00:53:33 +00:00
|
|
|
class HaBleakScannerWrapper(BaseBleakScanner):
|
2022-07-08 23:55:31 +00:00
|
|
|
"""A wrapper that uses the single instance."""
|
|
|
|
|
2022-07-30 00:53:33 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
*args: Any,
|
|
|
|
detection_callback: AdvertisementDataCallback | None = None,
|
|
|
|
service_uuids: list[str] | None = None,
|
|
|
|
**kwargs: Any,
|
|
|
|
) -> None:
|
2022-07-08 23:55:31 +00:00
|
|
|
"""Initialize the BleakScanner."""
|
|
|
|
self._detection_cancel: CALLBACK_TYPE | None = None
|
|
|
|
self._mapped_filters: dict[str, set[str]] = {}
|
2022-08-22 18:02:26 +00:00
|
|
|
self._advertisement_data_callback: AdvertisementDataCallback | None = None
|
2022-07-30 00:53:33 +00:00
|
|
|
remapped_kwargs = {
|
|
|
|
"detection_callback": detection_callback,
|
|
|
|
"service_uuids": service_uuids or [],
|
|
|
|
**kwargs,
|
|
|
|
}
|
|
|
|
self._map_filters(*args, **remapped_kwargs)
|
|
|
|
super().__init__(
|
|
|
|
detection_callback=detection_callback, service_uuids=service_uuids or []
|
|
|
|
)
|
2022-07-08 23:55:31 +00:00
|
|
|
|
|
|
|
async def stop(self, *args: Any, **kwargs: Any) -> None:
|
|
|
|
"""Stop scanning for devices."""
|
|
|
|
|
|
|
|
async def start(self, *args: Any, **kwargs: Any) -> None:
|
|
|
|
"""Start scanning for devices."""
|
2022-07-11 15:14:00 +00:00
|
|
|
|
|
|
|
def _map_filters(self, *args: Any, **kwargs: Any) -> bool:
|
|
|
|
"""Map the filters."""
|
|
|
|
mapped_filters = {}
|
|
|
|
if filters := kwargs.get("filters"):
|
2022-07-17 21:13:12 +00:00
|
|
|
if filter_uuids := filters.get(FILTER_UUIDS):
|
|
|
|
mapped_filters[FILTER_UUIDS] = set(filter_uuids)
|
|
|
|
else:
|
2022-07-11 15:14:00 +00:00
|
|
|
_LOGGER.warning("Only %s filters are supported", FILTER_UUIDS)
|
|
|
|
if service_uuids := kwargs.get("service_uuids"):
|
|
|
|
mapped_filters[FILTER_UUIDS] = set(service_uuids)
|
|
|
|
if mapped_filters == self._mapped_filters:
|
|
|
|
return False
|
|
|
|
self._mapped_filters = mapped_filters
|
|
|
|
return True
|
|
|
|
|
|
|
|
def set_scanning_filter(self, *args: Any, **kwargs: Any) -> None:
|
|
|
|
"""Set the filters to use."""
|
|
|
|
if self._map_filters(*args, **kwargs):
|
|
|
|
self._setup_detection_callback()
|
2022-07-08 23:55:31 +00:00
|
|
|
|
|
|
|
def _cancel_callback(self) -> None:
|
|
|
|
"""Cancel callback."""
|
|
|
|
if self._detection_cancel:
|
|
|
|
self._detection_cancel()
|
|
|
|
self._detection_cancel = None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def discovered_devices(self) -> list[BLEDevice]:
|
|
|
|
"""Return a list of discovered devices."""
|
2022-08-17 20:51:56 +00:00
|
|
|
assert MANAGER is not None
|
2022-08-22 18:02:26 +00:00
|
|
|
return list(MANAGER.async_discovered_devices(True))
|
2022-07-08 23:55:31 +00:00
|
|
|
|
2022-07-30 00:53:33 +00:00
|
|
|
def register_detection_callback(
|
|
|
|
self, callback: AdvertisementDataCallback | None
|
|
|
|
) -> None:
|
2022-07-08 23:55:31 +00:00
|
|
|
"""Register a callback that is called when a device is discovered or has a property changed.
|
|
|
|
|
|
|
|
This method takes the callback and registers it with the long running
|
|
|
|
scanner.
|
|
|
|
"""
|
2022-08-22 18:02:26 +00:00
|
|
|
self._advertisement_data_callback = callback
|
2022-07-11 15:14:00 +00:00
|
|
|
self._setup_detection_callback()
|
|
|
|
|
|
|
|
def _setup_detection_callback(self) -> None:
|
|
|
|
"""Set up the detection callback."""
|
2022-08-22 18:02:26 +00:00
|
|
|
if self._advertisement_data_callback is None:
|
2022-07-11 15:14:00 +00:00
|
|
|
return
|
2022-07-08 23:55:31 +00:00
|
|
|
self._cancel_callback()
|
2022-08-22 18:02:26 +00:00
|
|
|
super().register_detection_callback(self._advertisement_data_callback)
|
2022-08-17 20:51:56 +00:00
|
|
|
assert MANAGER is not None
|
2022-07-30 00:53:33 +00:00
|
|
|
assert self._callback is not None
|
2022-08-17 20:51:56 +00:00
|
|
|
self._detection_cancel = MANAGER.async_register_bleak_callback(
|
2022-07-08 23:55:31 +00:00
|
|
|
self._callback, self._mapped_filters
|
|
|
|
)
|
|
|
|
|
|
|
|
def __del__(self) -> None:
|
|
|
|
"""Delete the BleakScanner."""
|
|
|
|
if self._detection_cancel:
|
|
|
|
# Nothing to do if event loop is already closed
|
|
|
|
with contextlib.suppress(RuntimeError):
|
|
|
|
asyncio.get_running_loop().call_soon_threadsafe(self._detection_cancel)
|
2022-08-17 21:42:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
class HaBleakClientWrapper(BleakClient):
|
|
|
|
"""Wrap the BleakClient to ensure it does not shutdown our scanner.
|
|
|
|
|
|
|
|
If an address is passed into BleakClient instead of a BLEDevice,
|
|
|
|
bleak will quietly start a new scanner under the hood to resolve
|
|
|
|
the address. This can cause a conflict with our scanner. We need
|
|
|
|
to handle translating the address to the BLEDevice in this case
|
|
|
|
to avoid the whole stack from getting stuck in an in progress state
|
|
|
|
when an integration does this.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self, address_or_ble_device: str | BLEDevice, *args: Any, **kwargs: Any
|
|
|
|
) -> None:
|
|
|
|
"""Initialize the BleakClient."""
|
|
|
|
if isinstance(address_or_ble_device, BLEDevice):
|
|
|
|
super().__init__(address_or_ble_device, *args, **kwargs)
|
|
|
|
return
|
|
|
|
report(
|
|
|
|
"attempted to call BleakClient with an address instead of a BLEDevice",
|
|
|
|
exclude_integrations={"bluetooth"},
|
|
|
|
error_if_core=False,
|
|
|
|
)
|
|
|
|
assert MANAGER is not None
|
2022-08-22 18:02:26 +00:00
|
|
|
ble_device = MANAGER.async_ble_device_from_address(address_or_ble_device, True)
|
2022-08-17 21:42:12 +00:00
|
|
|
if ble_device is None:
|
|
|
|
raise BleakError(f"No device found for address {address_or_ble_device}")
|
|
|
|
super().__init__(ble_device, *args, **kwargs)
|