From 5722b4a1ce34b91f3c7ea293d3e3bb66b33cbefa Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Wed, 20 Dec 2023 13:36:37 -1000
Subject: [PATCH] Break out the ESPHome Bluetooth scanner connection logic into
 bleak-esphome (#105908)

---
 homeassistant/components/esphome/bluetooth.py |  44 ++++++
 .../components/esphome/bluetooth/__init__.py  | 129 ------------------
 .../test_init.py => test_bluetooth.py}        |   2 +-
 3 files changed, 45 insertions(+), 130 deletions(-)
 create mode 100644 homeassistant/components/esphome/bluetooth.py
 delete mode 100644 homeassistant/components/esphome/bluetooth/__init__.py
 rename tests/components/esphome/{bluetooth/test_init.py => test_bluetooth.py} (97%)

diff --git a/homeassistant/components/esphome/bluetooth.py b/homeassistant/components/esphome/bluetooth.py
new file mode 100644
index 00000000000..9534074b678
--- /dev/null
+++ b/homeassistant/components/esphome/bluetooth.py
@@ -0,0 +1,44 @@
+"""Bluetooth support for esphome."""
+from __future__ import annotations
+
+from functools import partial
+from typing import TYPE_CHECKING
+
+from aioesphomeapi import APIClient, DeviceInfo
+from bleak_esphome import connect_scanner
+from bleak_esphome.backend.cache import ESPHomeBluetoothCache
+
+from homeassistant.components.bluetooth import async_register_scanner
+from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
+
+from .entry_data import RuntimeEntryData
+
+
+@hass_callback
+def _async_unload(unload_callbacks: list[CALLBACK_TYPE]) -> None:
+    """Cancel all the callbacks on unload."""
+    for callback in unload_callbacks:
+        callback()
+
+
+async def async_connect_scanner(
+    hass: HomeAssistant,
+    entry_data: RuntimeEntryData,
+    cli: APIClient,
+    device_info: DeviceInfo,
+    cache: ESPHomeBluetoothCache,
+) -> CALLBACK_TYPE:
+    """Connect scanner."""
+    client_data = await connect_scanner(cli, device_info, cache, entry_data.available)
+    entry_data.bluetooth_device = client_data.bluetooth_device
+    client_data.disconnect_callbacks = entry_data.disconnect_callbacks
+    scanner = client_data.scanner
+    if TYPE_CHECKING:
+        assert scanner is not None
+    return partial(
+        _async_unload,
+        [
+            async_register_scanner(hass, scanner, scanner.connectable),
+            scanner.async_setup(),
+        ],
+    )
diff --git a/homeassistant/components/esphome/bluetooth/__init__.py b/homeassistant/components/esphome/bluetooth/__init__.py
deleted file mode 100644
index 88f47fe601d..00000000000
--- a/homeassistant/components/esphome/bluetooth/__init__.py
+++ /dev/null
@@ -1,129 +0,0 @@
-"""Bluetooth support for esphome."""
-from __future__ import annotations
-
-import asyncio
-from collections.abc import Coroutine
-from functools import partial
-import logging
-from typing import TYPE_CHECKING, Any
-
-from aioesphomeapi import APIClient, BluetoothProxyFeature, DeviceInfo
-from bleak_esphome.backend.cache import ESPHomeBluetoothCache
-from bleak_esphome.backend.client import ESPHomeClient, ESPHomeClientData
-from bleak_esphome.backend.device import ESPHomeBluetoothDevice
-from bleak_esphome.backend.scanner import ESPHomeScanner
-
-from homeassistant.components.bluetooth import (
-    HaBluetoothConnector,
-    async_register_scanner,
-)
-from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
-
-from ..entry_data import RuntimeEntryData
-
-_LOGGER = logging.getLogger(__name__)
-
-
-def _async_can_connect(bluetooth_device: ESPHomeBluetoothDevice, source: str) -> bool:
-    """Check if a given source can make another connection."""
-    can_connect = bool(
-        bluetooth_device.available and bluetooth_device.ble_connections_free
-    )
-    _LOGGER.debug(
-        (
-            "%s [%s]: Checking can connect, available=%s, ble_connections_free=%s"
-            " result=%s"
-        ),
-        bluetooth_device.name,
-        source,
-        bluetooth_device.available,
-        bluetooth_device.ble_connections_free,
-        can_connect,
-    )
-    return can_connect
-
-
-@hass_callback
-def _async_unload(unload_callbacks: list[CALLBACK_TYPE]) -> None:
-    """Cancel all the callbacks on unload."""
-    for callback in unload_callbacks:
-        callback()
-
-
-async def async_connect_scanner(
-    hass: HomeAssistant,
-    entry_data: RuntimeEntryData,
-    cli: APIClient,
-    device_info: DeviceInfo,
-    cache: ESPHomeBluetoothCache,
-) -> CALLBACK_TYPE:
-    """Connect scanner."""
-    source = device_info.mac_address
-    name = device_info.name
-    if TYPE_CHECKING:
-        assert cli.api_version is not None
-    feature_flags = device_info.bluetooth_proxy_feature_flags_compat(cli.api_version)
-    connectable = bool(feature_flags & BluetoothProxyFeature.ACTIVE_CONNECTIONS)
-    bluetooth_device = ESPHomeBluetoothDevice(
-        name, device_info.mac_address, available=entry_data.available
-    )
-    entry_data.bluetooth_device = bluetooth_device
-    _LOGGER.debug(
-        "%s [%s]: Connecting scanner feature_flags=%s, connectable=%s",
-        name,
-        source,
-        feature_flags,
-        connectable,
-    )
-    client_data = ESPHomeClientData(
-        bluetooth_device=bluetooth_device,
-        cache=cache,
-        client=cli,
-        device_info=device_info,
-        api_version=cli.api_version,
-        title=name,
-        scanner=None,
-        disconnect_callbacks=entry_data.disconnect_callbacks,
-    )
-    connector = HaBluetoothConnector(
-        # MyPy doesn't like partials, but this is correct
-        # https://github.com/python/mypy/issues/1484
-        client=partial(ESPHomeClient, client_data=client_data),  # type: ignore[arg-type]
-        source=source,
-        can_connect=partial(_async_can_connect, bluetooth_device, source),
-    )
-    scanner = ESPHomeScanner(source, name, connector, connectable)
-    client_data.scanner = scanner
-    coros: list[Coroutine[Any, Any, CALLBACK_TYPE]] = []
-    # These calls all return a callback that can be used to unsubscribe
-    # but we never unsubscribe so we don't care about the return value
-
-    if connectable:
-        # If its connectable be sure not to register the scanner
-        # until we know the connection is fully setup since otherwise
-        # there is a race condition where the connection can fail
-        coros.append(
-            cli.subscribe_bluetooth_connections_free(
-                bluetooth_device.async_update_ble_connection_limits
-            )
-        )
-
-    if feature_flags & BluetoothProxyFeature.RAW_ADVERTISEMENTS:
-        coros.append(
-            cli.subscribe_bluetooth_le_raw_advertisements(
-                scanner.async_on_raw_advertisements
-            )
-        )
-    else:
-        coros.append(
-            cli.subscribe_bluetooth_le_advertisements(scanner.async_on_advertisement)
-        )
-
-    await asyncio.gather(*coros)
-    return partial(
-        _async_unload,
-        [
-            async_register_scanner(hass, scanner, connectable),
-            scanner.async_setup(),
-        ],
-    )
diff --git a/tests/components/esphome/bluetooth/test_init.py b/tests/components/esphome/test_bluetooth.py
similarity index 97%
rename from tests/components/esphome/bluetooth/test_init.py
rename to tests/components/esphome/test_bluetooth.py
index d9d6f1947c9..a576c82c944 100644
--- a/tests/components/esphome/bluetooth/test_init.py
+++ b/tests/components/esphome/test_bluetooth.py
@@ -3,7 +3,7 @@
 from homeassistant.components import bluetooth
 from homeassistant.core import HomeAssistant
 
-from ..conftest import MockESPHomeDevice
+from .conftest import MockESPHomeDevice
 
 
 async def test_bluetooth_connect_with_raw_adv(
-- 
GitLab