From 8d63f81821e726a44a5aa1ebe6bb9283cd45b037 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sun, 17 Jul 2022 08:19:05 -0500
Subject: [PATCH] Add bluetooth discovery to HomeKit Controller (#75333)

Co-authored-by: Jc2k <john.carr@unrouted.co.uk>
---
 .../homekit_controller/config_flow.py         | 114 ++++++++--
 .../homekit_controller/manifest.json          |   3 +-
 .../homekit_controller/strings.json           |   4 +-
 .../homekit_controller/translations/en.json   |   4 +-
 homeassistant/generated/bluetooth.py          |   5 +
 requirements_all.txt                          |   2 +-
 requirements_test_all.txt                     |   2 +-
 .../homekit_controller/test_config_flow.py    | 197 +++++++++++++++---
 8 files changed, 272 insertions(+), 59 deletions(-)

diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py
index 9b8b759f80e..d8b3fda2d06 100644
--- a/homeassistant/components/homekit_controller/config_flow.py
+++ b/homeassistant/components/homekit_controller/config_flow.py
@@ -1,13 +1,17 @@
 """Config flow to configure homekit_controller."""
 from __future__ import annotations
 
+from collections.abc import Awaitable
 import logging
 import re
-from typing import Any
+from typing import TYPE_CHECKING, Any, cast
 
 import aiohomekit
-from aiohomekit.controller.abstract import AbstractPairing
+from aiohomekit import Controller, const as aiohomekit_const
+from aiohomekit.controller.abstract import AbstractDiscovery, AbstractPairing
 from aiohomekit.exceptions import AuthenticationError
+from aiohomekit.model.categories import Categories
+from aiohomekit.model.status_flags import StatusFlags
 from aiohomekit.utils import domain_supported, domain_to_name
 import voluptuous as vol
 
@@ -16,6 +20,7 @@ from homeassistant.components import zeroconf
 from homeassistant.core import callback
 from homeassistant.data_entry_flow import AbortFlow, FlowResult
 from homeassistant.helpers import device_registry as dr
+from homeassistant.helpers.service_info import bluetooth
 
 from .connection import HKDevice
 from .const import DOMAIN, KNOWN_DEVICES
@@ -41,6 +46,8 @@ PIN_FORMAT = re.compile(r"^(\d{3})-{0,1}(\d{2})-{0,1}(\d{3})$")
 _LOGGER = logging.getLogger(__name__)
 
 
+BLE_DEFAULT_NAME = "Bluetooth device"
+
 INSECURE_CODES = {
     "00000000",
     "11111111",
@@ -62,6 +69,11 @@ def normalize_hkid(hkid: str) -> str:
     return hkid.lower()
 
 
+def formatted_category(category: Categories) -> str:
+    """Return a human readable category name."""
+    return str(category.name).replace("_", " ").title()
+
+
 @callback
 def find_existing_host(hass, serial: str) -> config_entries.ConfigEntry | None:
     """Return a set of the configured hosts."""
@@ -92,14 +104,15 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
 
     VERSION = 1
 
-    def __init__(self):
+    def __init__(self) -> None:
         """Initialize the homekit_controller flow."""
-        self.model = None
-        self.hkid = None
-        self.name = None
-        self.devices = {}
-        self.controller = None
-        self.finish_pairing = None
+        self.model: str | None = None
+        self.hkid: str | None = None
+        self.name: str | None = None
+        self.category: Categories | None = None
+        self.devices: dict[str, AbstractDiscovery] = {}
+        self.controller: Controller | None = None
+        self.finish_pairing: Awaitable[AbstractPairing] | None = None
 
     async def _async_setup_controller(self):
         """Create the controller."""
@@ -111,9 +124,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
 
         if user_input is not None:
             key = user_input["device"]
-            self.hkid = self.devices[key].description.id
-            self.model = self.devices[key].description.model
-            self.name = self.devices[key].description.name
+            discovery = self.devices[key]
+            self.category = discovery.description.category
+            self.hkid = discovery.description.id
+            self.model = getattr(discovery.description, "model", BLE_DEFAULT_NAME)
+            self.name = discovery.description.name or BLE_DEFAULT_NAME
 
             await self.async_set_unique_id(
                 normalize_hkid(self.hkid), raise_on_progress=False
@@ -138,7 +153,14 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
             step_id="user",
             errors=errors,
             data_schema=vol.Schema(
-                {vol.Required("device"): vol.In(self.devices.keys())}
+                {
+                    vol.Required("device"): vol.In(
+                        {
+                            key: f"{key} ({formatted_category(discovery.description.category)})"
+                            for key, discovery in self.devices.items()
+                        }
+                    )
+                }
             ),
         )
 
@@ -151,13 +173,14 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
             await self._async_setup_controller()
 
         try:
-            device = await self.controller.async_find(unique_id)
+            discovery = await self.controller.async_find(unique_id)
         except aiohomekit.AccessoryNotFoundError:
             return self.async_abort(reason="accessory_not_found_error")
 
-        self.name = device.description.name
-        self.model = device.description.model
-        self.hkid = device.description.id
+        self.name = discovery.description.name
+        self.model = discovery.description.model
+        self.category = discovery.description.category
+        self.hkid = discovery.description.id
 
         return self._async_step_pair_show_form()
 
@@ -213,6 +236,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
         model = properties["md"]
         name = domain_to_name(discovery_info.name)
         status_flags = int(properties["sf"])
+        category = Categories(int(properties.get("ci", 0)))
         paired = not status_flags & 0x01
 
         # The configuration number increases every time the characteristic map
@@ -326,6 +350,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
 
         self.name = name
         self.model = model
+        self.category = category
         self.hkid = hkid
 
         # We want to show the pairing form - but don't call async_step_pair
@@ -333,6 +358,55 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
         # pairing code)
         return self._async_step_pair_show_form()
 
+    async def async_step_bluetooth(
+        self, discovery_info: bluetooth.BluetoothServiceInfo
+    ) -> FlowResult:
+        """Handle the bluetooth discovery step."""
+        if not aiohomekit_const.BLE_TRANSPORT_SUPPORTED:
+            return self.async_abort(reason="ignored_model")
+
+        # Late imports in case BLE is not available
+        from aiohomekit.controller.ble.discovery import (  # pylint: disable=import-outside-toplevel
+            BleDiscovery,
+        )
+        from aiohomekit.controller.ble.manufacturer_data import (  # pylint: disable=import-outside-toplevel
+            HomeKitAdvertisement,
+        )
+
+        await self.async_set_unique_id(discovery_info.address)
+        self._abort_if_unique_id_configured()
+
+        mfr_data = discovery_info.manufacturer_data
+
+        try:
+            device = HomeKitAdvertisement.from_manufacturer_data(
+                discovery_info.name, discovery_info.address, mfr_data
+            )
+        except ValueError:
+            return self.async_abort(reason="ignored_model")
+
+        if not (device.status_flags & StatusFlags.UNPAIRED):
+            return self.async_abort(reason="already_paired")
+
+        if self.controller is None:
+            await self._async_setup_controller()
+            assert self.controller is not None
+
+        try:
+            discovery = await self.controller.async_find(device.id)
+        except aiohomekit.AccessoryNotFoundError:
+            return self.async_abort(reason="accessory_not_found_error")
+
+        if TYPE_CHECKING:
+            discovery = cast(BleDiscovery, discovery)
+
+        self.name = discovery.description.name
+        self.model = BLE_DEFAULT_NAME
+        self.category = discovery.description.category
+        self.hkid = discovery.description.id
+
+        return self._async_step_pair_show_form()
+
     async def async_step_pair(self, pair_info=None):
         """Pair with a new HomeKit accessory."""
         # If async_step_pair is called with no pairing code then we do the M1
@@ -453,8 +527,10 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
 
     @callback
     def _async_step_pair_show_form(self, errors=None):
-        placeholders = {"name": self.name}
-        self.context["title_placeholders"] = {"name": self.name}
+        placeholders = self.context["title_placeholders"] = {
+            "name": self.name,
+            "category": formatted_category(self.category),
+        }
 
         schema = {vol.Required("pairing_code"): vol.All(str, vol.Strip)}
         if errors and errors.get("pairing_code") == "insecure_setup_code":
diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json
index a5ca76fdc1e..a4f5350a167 100644
--- a/homeassistant/components/homekit_controller/manifest.json
+++ b/homeassistant/components/homekit_controller/manifest.json
@@ -3,8 +3,9 @@
   "name": "HomeKit Controller",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/homekit_controller",
-  "requirements": ["aiohomekit==1.1.1"],
+  "requirements": ["aiohomekit==1.1.4"],
   "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
+  "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_first_byte": 6 }],
   "after_dependencies": ["zeroconf"],
   "codeowners": ["@Jc2k", "@bdraco"],
   "iot_class": "local_push",
diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json
index 7ad868db3fc..2831dabc38d 100644
--- a/homeassistant/components/homekit_controller/strings.json
+++ b/homeassistant/components/homekit_controller/strings.json
@@ -1,7 +1,7 @@
 {
   "title": "HomeKit Controller",
   "config": {
-    "flow_title": "{name}",
+    "flow_title": "{name} ({category})",
     "step": {
       "user": {
         "title": "Device selection",
@@ -12,7 +12,7 @@
       },
       "pair": {
         "title": "Pair with a device via HomeKit Accessory Protocol",
-        "description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.",
+        "description": "HomeKit Controller communicates with {name} ({category}) over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.",
         "data": {
           "pairing_code": "Pairing Code",
           "allow_insecure_setup_codes": "Allow pairing with insecure setup codes."
diff --git a/homeassistant/components/homekit_controller/translations/en.json b/homeassistant/components/homekit_controller/translations/en.json
index 5de3a6c5334..2686e71d252 100644
--- a/homeassistant/components/homekit_controller/translations/en.json
+++ b/homeassistant/components/homekit_controller/translations/en.json
@@ -18,7 +18,7 @@
             "unable_to_pair": "Unable to pair, please try again.",
             "unknown_error": "Device reported an unknown error. Pairing failed."
         },
-        "flow_title": "{name}",
+        "flow_title": "{name} ({category})",
         "step": {
             "busy_error": {
                 "description": "Abort pairing on all controllers, or try restarting the device, then continue to resume pairing.",
@@ -33,7 +33,7 @@
                     "allow_insecure_setup_codes": "Allow pairing with insecure setup codes.",
                     "pairing_code": "Pairing Code"
                 },
-                "description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.",
+                "description": "HomeKit Controller communicates with {name} ({category}) over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.",
                 "title": "Pair with a device via HomeKit Accessory Protocol"
             },
             "protocol_error": {
diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py
index 49596c4773c..feac27af1f1 100644
--- a/homeassistant/generated/bluetooth.py
+++ b/homeassistant/generated/bluetooth.py
@@ -7,6 +7,11 @@ from __future__ import annotations
 # fmt: off
 
 BLUETOOTH: list[dict[str, str | int]] = [
+    {
+        "domain": "homekit_controller",
+        "manufacturer_id": 76,
+        "manufacturer_data_first_byte": 6
+    },
     {
         "domain": "switchbot",
         "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"
diff --git a/requirements_all.txt b/requirements_all.txt
index 6a4b30192fe..47ff7005c66 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -168,7 +168,7 @@ aioguardian==2022.03.2
 aioharmony==0.2.9
 
 # homeassistant.components.homekit_controller
-aiohomekit==1.1.1
+aiohomekit==1.1.4
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index d82b023b4f6..a9d015738f5 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -152,7 +152,7 @@ aioguardian==2022.03.2
 aioharmony==0.2.9
 
 # homeassistant.components.homekit_controller
-aiohomekit==1.1.1
+aiohomekit==1.1.4
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py
index 73bd159fd73..78d3c609a9c 100644
--- a/tests/components/homekit_controller/test_config_flow.py
+++ b/tests/components/homekit_controller/test_config_flow.py
@@ -1,6 +1,5 @@
 """Tests for homekit_controller config flow."""
 import asyncio
-from unittest import mock
 import unittest.mock
 from unittest.mock import AsyncMock, MagicMock, patch
 
@@ -15,8 +14,13 @@ from homeassistant import config_entries
 from homeassistant.components import zeroconf
 from homeassistant.components.homekit_controller import config_flow
 from homeassistant.components.homekit_controller.const import KNOWN_DEVICES
-from homeassistant.data_entry_flow import FlowResultType
+from homeassistant.data_entry_flow import (
+    RESULT_TYPE_ABORT,
+    RESULT_TYPE_FORM,
+    FlowResultType,
+)
 from homeassistant.helpers import device_registry
+from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
 
 from tests.common import MockConfigEntry, mock_device_registry
 
@@ -78,23 +82,55 @@ VALID_PAIRING_CODES = [
     "   98765432  ",
 ]
 
-
-def _setup_flow_handler(hass, pairing=None):
-    flow = config_flow.HomekitControllerFlowHandler()
-    flow.hass = hass
-    flow.context = {}
-
-    finish_pairing = unittest.mock.AsyncMock(return_value=pairing)
-
-    discovery = mock.Mock()
-    discovery.description.id = "00:00:00:00:00:00"
-    discovery.async_start_pairing = unittest.mock.AsyncMock(return_value=finish_pairing)
-
-    flow.controller = mock.Mock()
-    flow.controller.pairings = {}
-    flow.controller.async_find = unittest.mock.AsyncMock(return_value=discovery)
-
-    return flow
+NOT_HK_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo(
+    name="FakeAccessory",
+    address="AA:BB:CC:DD:EE:FF",
+    rssi=-81,
+    manufacturer_data={12: b"\x06\x12\x34"},
+    service_data={},
+    service_uuids=[],
+    source="local",
+)
+
+HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED = BluetoothServiceInfo(
+    name="Eve Energy Not Found",
+    address="AA:BB:CC:DD:EE:FF",
+    rssi=-81,
+    # ID is '9b:86:af:01:af:db'
+    manufacturer_data={
+        76: b"\x061\x01\x9b\x86\xaf\x01\xaf\xdb\x07\x00\x06\x00\x02\x02X\x19\xb1Q"
+    },
+    service_data={},
+    service_uuids=[],
+    source="local",
+)
+
+HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_UNPAIRED = BluetoothServiceInfo(
+    name="Eve Energy Found Unpaired",
+    address="AA:BB:CC:DD:EE:FF",
+    rssi=-81,
+    # ID is '00:00:00:00:00:00', pairing flag is byte 3
+    manufacturer_data={
+        76: b"\x061\x01\x00\x00\x00\x00\x00\x00\x07\x00\x06\x00\x02\x02X\x19\xb1Q"
+    },
+    service_data={},
+    service_uuids=[],
+    source="local",
+)
+
+
+HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_PAIRED = BluetoothServiceInfo(
+    name="Eve Energy Found Paired",
+    address="AA:BB:CC:DD:EE:FF",
+    rssi=-81,
+    # ID is '00:00:00:00:00:00', pairing flag is byte 3
+    manufacturer_data={
+        76: b"\x061\x00\x00\x00\x00\x00\x00\x00\x07\x00\x06\x00\x02\x02X\x19\xb1Q"
+    },
+    service_data={},
+    service_uuids=[],
+    source="local",
+)
 
 
 @pytest.mark.parametrize("pairing_code", INVALID_PAIRING_CODES)
@@ -151,7 +187,7 @@ def get_device_discovery_info(
             "c#": device.description.config_num,
             "s#": device.description.state_num,
             "ff": "0",
-            "ci": "0",
+            "ci": "7",
             "sf": "0" if paired else "1",
             "sh": "",
         },
@@ -208,7 +244,7 @@ async def test_discovery_works(hass, controller, upper_case_props, missing_cshar
     assert result["step_id"] == "pair"
     assert get_flow_context(hass, result) == {
         "source": config_entries.SOURCE_ZEROCONF,
-        "title_placeholders": {"name": "TestDevice"},
+        "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
         "unique_id": "00:00:00:00:00:00",
     }
 
@@ -592,7 +628,7 @@ async def test_pair_form_errors_on_start(hass, controller, exception, expected):
     )
 
     assert get_flow_context(hass, result) == {
-        "title_placeholders": {"name": "TestDevice"},
+        "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
         "unique_id": "00:00:00:00:00:00",
         "source": config_entries.SOURCE_ZEROCONF,
     }
@@ -607,7 +643,7 @@ async def test_pair_form_errors_on_start(hass, controller, exception, expected):
     assert result["errors"]["pairing_code"] == expected
 
     assert get_flow_context(hass, result) == {
-        "title_placeholders": {"name": "TestDevice"},
+        "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
         "unique_id": "00:00:00:00:00:00",
         "source": config_entries.SOURCE_ZEROCONF,
     }
@@ -640,7 +676,7 @@ async def test_pair_abort_errors_on_finish(hass, controller, exception, expected
     )
 
     assert get_flow_context(hass, result) == {
-        "title_placeholders": {"name": "TestDevice"},
+        "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
         "unique_id": "00:00:00:00:00:00",
         "source": config_entries.SOURCE_ZEROCONF,
     }
@@ -653,7 +689,7 @@ async def test_pair_abort_errors_on_finish(hass, controller, exception, expected
 
     assert result["type"] == "form"
     assert get_flow_context(hass, result) == {
-        "title_placeholders": {"name": "TestDevice"},
+        "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
         "unique_id": "00:00:00:00:00:00",
         "source": config_entries.SOURCE_ZEROCONF,
     }
@@ -680,7 +716,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected)
     )
 
     assert get_flow_context(hass, result) == {
-        "title_placeholders": {"name": "TestDevice"},
+        "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
         "unique_id": "00:00:00:00:00:00",
         "source": config_entries.SOURCE_ZEROCONF,
     }
@@ -693,7 +729,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected)
 
     assert result["type"] == "form"
     assert get_flow_context(hass, result) == {
-        "title_placeholders": {"name": "TestDevice"},
+        "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
         "unique_id": "00:00:00:00:00:00",
         "source": config_entries.SOURCE_ZEROCONF,
     }
@@ -706,7 +742,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected)
     assert result["errors"]["pairing_code"] == expected
 
     assert get_flow_context(hass, result) == {
-        "title_placeholders": {"name": "TestDevice"},
+        "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
         "unique_id": "00:00:00:00:00:00",
         "source": config_entries.SOURCE_ZEROCONF,
         "pairing": True,
@@ -737,7 +773,7 @@ async def test_user_works(hass, controller):
     assert get_flow_context(hass, result) == {
         "source": config_entries.SOURCE_USER,
         "unique_id": "00:00:00:00:00:00",
-        "title_placeholders": {"name": "TestDevice"},
+        "title_placeholders": {"name": "TestDevice", "category": "Other"},
     }
 
     result = await hass.config_entries.flow.async_configure(
@@ -772,7 +808,7 @@ async def test_user_pairing_with_insecure_setup_code(hass, controller):
     assert get_flow_context(hass, result) == {
         "source": config_entries.SOURCE_USER,
         "unique_id": "00:00:00:00:00:00",
-        "title_placeholders": {"name": "TestDevice"},
+        "title_placeholders": {"name": "TestDevice", "category": "Other"},
     }
 
     result = await hass.config_entries.flow.async_configure(
@@ -829,7 +865,7 @@ async def test_unignore_works(hass, controller):
     assert result["type"] == "form"
     assert result["step_id"] == "pair"
     assert get_flow_context(hass, result) == {
-        "title_placeholders": {"name": "TestDevice"},
+        "title_placeholders": {"name": "TestDevice", "category": "Other"},
         "unique_id": "00:00:00:00:00:00",
         "source": config_entries.SOURCE_UNIGNORE,
     }
@@ -917,7 +953,7 @@ async def test_mdns_update_to_paired_during_pairing(hass, controller):
     )
 
     assert get_flow_context(hass, result) == {
-        "title_placeholders": {"name": "TestDevice"},
+        "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
         "unique_id": "00:00:00:00:00:00",
         "source": config_entries.SOURCE_ZEROCONF,
     }
@@ -942,7 +978,7 @@ async def test_mdns_update_to_paired_during_pairing(hass, controller):
 
     assert result["type"] == "form"
     assert get_flow_context(hass, result) == {
-        "title_placeholders": {"name": "TestDevice"},
+        "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
         "unique_id": "00:00:00:00:00:00",
         "source": config_entries.SOURCE_ZEROCONF,
     }
@@ -967,3 +1003,98 @@ async def test_mdns_update_to_paired_during_pairing(hass, controller):
     assert result["type"] == FlowResultType.CREATE_ENTRY
     assert result["title"] == "Koogeek-LS1-20833F"
     assert result["data"] == {}
+
+
+async def test_discovery_no_bluetooth_support(hass, controller):
+    """Test discovery with bluetooth support not available."""
+    with patch(
+        "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED",
+        False,
+    ):
+        result = await hass.config_entries.flow.async_init(
+            "homekit_controller",
+            context={"source": config_entries.SOURCE_BLUETOOTH},
+            data=HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED,
+        )
+    assert result["type"] == RESULT_TYPE_ABORT
+    assert result["reason"] == "ignored_model"
+
+
+async def test_bluetooth_not_homekit(hass, controller):
+    """Test bluetooth discovery with a non-homekit device."""
+    with patch(
+        "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED",
+        True,
+    ):
+        result = await hass.config_entries.flow.async_init(
+            "homekit_controller",
+            context={"source": config_entries.SOURCE_BLUETOOTH},
+            data=NOT_HK_BLUETOOTH_SERVICE_INFO,
+        )
+    assert result["type"] == RESULT_TYPE_ABORT
+    assert result["reason"] == "ignored_model"
+
+
+async def test_bluetooth_valid_device_no_discovery(hass, controller):
+    """Test bluetooth discovery  with a homekit device and discovery fails."""
+    with patch(
+        "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED",
+        True,
+    ):
+        result = await hass.config_entries.flow.async_init(
+            "homekit_controller",
+            context={"source": config_entries.SOURCE_BLUETOOTH},
+            data=HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED,
+        )
+    assert result["type"] == RESULT_TYPE_ABORT
+    assert result["reason"] == "accessory_not_found_error"
+
+
+async def test_bluetooth_valid_device_discovery_paired(hass, controller):
+    """Test bluetooth discovery  with a homekit device and discovery works."""
+    setup_mock_accessory(controller)
+
+    with patch(
+        "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED",
+        True,
+    ):
+        result = await hass.config_entries.flow.async_init(
+            "homekit_controller",
+            context={"source": config_entries.SOURCE_BLUETOOTH},
+            data=HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_PAIRED,
+        )
+
+    assert result["type"] == RESULT_TYPE_ABORT
+    assert result["reason"] == "already_paired"
+
+
+async def test_bluetooth_valid_device_discovery_unpaired(hass, controller):
+    """Test bluetooth discovery with a homekit device and discovery works."""
+    setup_mock_accessory(controller)
+    with patch(
+        "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED",
+        True,
+    ):
+        result = await hass.config_entries.flow.async_init(
+            "homekit_controller",
+            context={"source": config_entries.SOURCE_BLUETOOTH},
+            data=HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_UNPAIRED,
+        )
+
+    assert result["type"] == RESULT_TYPE_FORM
+    assert result["step_id"] == "pair"
+
+    assert get_flow_context(hass, result) == {
+        "source": config_entries.SOURCE_BLUETOOTH,
+        "unique_id": "AA:BB:CC:DD:EE:FF",
+        "title_placeholders": {"name": "TestDevice", "category": "Other"},
+    }
+
+    result2 = await hass.config_entries.flow.async_configure(result["flow_id"])
+    assert result2["type"] == RESULT_TYPE_FORM
+    result3 = await hass.config_entries.flow.async_configure(
+        result2["flow_id"], user_input={"pairing_code": "111-22-333"}
+    )
+    assert result3["type"] == FlowResultType.CREATE_ENTRY
+    assert result3["title"] == "Koogeek-LS1-20833F"
+    assert result3["data"] == {}
-- 
GitLab