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 ", ] +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", +) -def _setup_flow_handler(hass, pairing=None): - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass - flow.context = {} +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", +) - finish_pairing = unittest.mock.AsyncMock(return_value=pairing) +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", +) - 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 +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"] == {}