Add bluetooth discovery to HomeKit Controller (#75333)

Co-authored-by: Jc2k <john.carr@unrouted.co.uk>
This commit is contained in:
J. Nick Koston 2022-07-17 08:19:05 -05:00 committed by GitHub
parent 503b31fb15
commit 8d63f81821
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 268 additions and 55 deletions

View File

@ -1,13 +1,17 @@
"""Config flow to configure homekit_controller.""" """Config flow to configure homekit_controller."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Awaitable
import logging import logging
import re import re
from typing import Any from typing import TYPE_CHECKING, Any, cast
import aiohomekit 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.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 from aiohomekit.utils import domain_supported, domain_to_name
import voluptuous as vol import voluptuous as vol
@ -16,6 +20,7 @@ from homeassistant.components import zeroconf
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.data_entry_flow import AbortFlow, FlowResult
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.service_info import bluetooth
from .connection import HKDevice from .connection import HKDevice
from .const import DOMAIN, KNOWN_DEVICES 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__) _LOGGER = logging.getLogger(__name__)
BLE_DEFAULT_NAME = "Bluetooth device"
INSECURE_CODES = { INSECURE_CODES = {
"00000000", "00000000",
"11111111", "11111111",
@ -62,6 +69,11 @@ def normalize_hkid(hkid: str) -> str:
return hkid.lower() return hkid.lower()
def formatted_category(category: Categories) -> str:
"""Return a human readable category name."""
return str(category.name).replace("_", " ").title()
@callback @callback
def find_existing_host(hass, serial: str) -> config_entries.ConfigEntry | None: def find_existing_host(hass, serial: str) -> config_entries.ConfigEntry | None:
"""Return a set of the configured hosts.""" """Return a set of the configured hosts."""
@ -92,14 +104,15 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
def __init__(self): def __init__(self) -> None:
"""Initialize the homekit_controller flow.""" """Initialize the homekit_controller flow."""
self.model = None self.model: str | None = None
self.hkid = None self.hkid: str | None = None
self.name = None self.name: str | None = None
self.devices = {} self.category: Categories | None = None
self.controller = None self.devices: dict[str, AbstractDiscovery] = {}
self.finish_pairing = None self.controller: Controller | None = None
self.finish_pairing: Awaitable[AbstractPairing] | None = None
async def _async_setup_controller(self): async def _async_setup_controller(self):
"""Create the controller.""" """Create the controller."""
@ -111,9 +124,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None: if user_input is not None:
key = user_input["device"] key = user_input["device"]
self.hkid = self.devices[key].description.id discovery = self.devices[key]
self.model = self.devices[key].description.model self.category = discovery.description.category
self.name = self.devices[key].description.name 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( await self.async_set_unique_id(
normalize_hkid(self.hkid), raise_on_progress=False normalize_hkid(self.hkid), raise_on_progress=False
@ -138,7 +153,14 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
step_id="user", step_id="user",
errors=errors, errors=errors,
data_schema=vol.Schema( 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() await self._async_setup_controller()
try: try:
device = await self.controller.async_find(unique_id) discovery = await self.controller.async_find(unique_id)
except aiohomekit.AccessoryNotFoundError: except aiohomekit.AccessoryNotFoundError:
return self.async_abort(reason="accessory_not_found_error") return self.async_abort(reason="accessory_not_found_error")
self.name = device.description.name self.name = discovery.description.name
self.model = device.description.model self.model = discovery.description.model
self.hkid = device.description.id self.category = discovery.description.category
self.hkid = discovery.description.id
return self._async_step_pair_show_form() return self._async_step_pair_show_form()
@ -213,6 +236,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
model = properties["md"] model = properties["md"]
name = domain_to_name(discovery_info.name) name = domain_to_name(discovery_info.name)
status_flags = int(properties["sf"]) status_flags = int(properties["sf"])
category = Categories(int(properties.get("ci", 0)))
paired = not status_flags & 0x01 paired = not status_flags & 0x01
# The configuration number increases every time the characteristic map # The configuration number increases every time the characteristic map
@ -326,6 +350,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self.name = name self.name = name
self.model = model self.model = model
self.category = category
self.hkid = hkid self.hkid = hkid
# We want to show the pairing form - but don't call async_step_pair # 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) # pairing code)
return self._async_step_pair_show_form() 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): async def async_step_pair(self, pair_info=None):
"""Pair with a new HomeKit accessory.""" """Pair with a new HomeKit accessory."""
# If async_step_pair is called with no pairing code then we do the M1 # 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 @callback
def _async_step_pair_show_form(self, errors=None): def _async_step_pair_show_form(self, errors=None):
placeholders = {"name": self.name} placeholders = self.context["title_placeholders"] = {
self.context["title_placeholders"] = {"name": self.name} "name": self.name,
"category": formatted_category(self.category),
}
schema = {vol.Required("pairing_code"): vol.All(str, vol.Strip)} schema = {vol.Required("pairing_code"): vol.All(str, vol.Strip)}
if errors and errors.get("pairing_code") == "insecure_setup_code": if errors and errors.get("pairing_code") == "insecure_setup_code":

View File

@ -3,8 +3,9 @@
"name": "HomeKit Controller", "name": "HomeKit Controller",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller", "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."], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_first_byte": 6 }],
"after_dependencies": ["zeroconf"], "after_dependencies": ["zeroconf"],
"codeowners": ["@Jc2k", "@bdraco"], "codeowners": ["@Jc2k", "@bdraco"],
"iot_class": "local_push", "iot_class": "local_push",

View File

@ -1,7 +1,7 @@
{ {
"title": "HomeKit Controller", "title": "HomeKit Controller",
"config": { "config": {
"flow_title": "{name}", "flow_title": "{name} ({category})",
"step": { "step": {
"user": { "user": {
"title": "Device selection", "title": "Device selection",
@ -12,7 +12,7 @@
}, },
"pair": { "pair": {
"title": "Pair with a device via HomeKit Accessory Protocol", "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": { "data": {
"pairing_code": "Pairing Code", "pairing_code": "Pairing Code",
"allow_insecure_setup_codes": "Allow pairing with insecure setup codes." "allow_insecure_setup_codes": "Allow pairing with insecure setup codes."

View File

@ -18,7 +18,7 @@
"unable_to_pair": "Unable to pair, please try again.", "unable_to_pair": "Unable to pair, please try again.",
"unknown_error": "Device reported an unknown error. Pairing failed." "unknown_error": "Device reported an unknown error. Pairing failed."
}, },
"flow_title": "{name}", "flow_title": "{name} ({category})",
"step": { "step": {
"busy_error": { "busy_error": {
"description": "Abort pairing on all controllers, or try restarting the device, then continue to resume pairing.", "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.", "allow_insecure_setup_codes": "Allow pairing with insecure setup codes.",
"pairing_code": "Pairing Code" "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" "title": "Pair with a device via HomeKit Accessory Protocol"
}, },
"protocol_error": { "protocol_error": {

View File

@ -7,6 +7,11 @@ from __future__ import annotations
# fmt: off # fmt: off
BLUETOOTH: list[dict[str, str | int]] = [ BLUETOOTH: list[dict[str, str | int]] = [
{
"domain": "homekit_controller",
"manufacturer_id": 76,
"manufacturer_data_first_byte": 6
},
{ {
"domain": "switchbot", "domain": "switchbot",
"service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"

View File

@ -168,7 +168,7 @@ aioguardian==2022.03.2
aioharmony==0.2.9 aioharmony==0.2.9
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==1.1.1 aiohomekit==1.1.4
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http

View File

@ -152,7 +152,7 @@ aioguardian==2022.03.2
aioharmony==0.2.9 aioharmony==0.2.9
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit==1.1.1 aiohomekit==1.1.4
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http

View File

@ -1,6 +1,5 @@
"""Tests for homekit_controller config flow.""" """Tests for homekit_controller config flow."""
import asyncio import asyncio
from unittest import mock
import unittest.mock import unittest.mock
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
@ -15,8 +14,13 @@ from homeassistant import config_entries
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller import config_flow
from homeassistant.components.homekit_controller.const import KNOWN_DEVICES 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 import device_registry
from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
from tests.common import MockConfigEntry, mock_device_registry from tests.common import MockConfigEntry, mock_device_registry
@ -78,23 +82,55 @@ VALID_PAIRING_CODES = [
" 98765432 ", " 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): HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED = BluetoothServiceInfo(
flow = config_flow.HomekitControllerFlowHandler() name="Eve Energy Not Found",
flow.hass = hass address="AA:BB:CC:DD:EE:FF",
flow.context = {} 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() HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_PAIRED = BluetoothServiceInfo(
flow.controller.pairings = {} name="Eve Energy Found Paired",
flow.controller.async_find = unittest.mock.AsyncMock(return_value=discovery) address="AA:BB:CC:DD:EE:FF",
rssi=-81,
return flow # 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) @pytest.mark.parametrize("pairing_code", INVALID_PAIRING_CODES)
@ -151,7 +187,7 @@ def get_device_discovery_info(
"c#": device.description.config_num, "c#": device.description.config_num,
"s#": device.description.state_num, "s#": device.description.state_num,
"ff": "0", "ff": "0",
"ci": "0", "ci": "7",
"sf": "0" if paired else "1", "sf": "0" if paired else "1",
"sh": "", "sh": "",
}, },
@ -208,7 +244,7 @@ async def test_discovery_works(hass, controller, upper_case_props, missing_cshar
assert result["step_id"] == "pair" assert result["step_id"] == "pair"
assert get_flow_context(hass, result) == { assert get_flow_context(hass, result) == {
"source": config_entries.SOURCE_ZEROCONF, "source": config_entries.SOURCE_ZEROCONF,
"title_placeholders": {"name": "TestDevice"}, "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
"unique_id": "00:00:00:00:00:00", "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) == { assert get_flow_context(hass, result) == {
"title_placeholders": {"name": "TestDevice"}, "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
"unique_id": "00:00:00:00:00:00", "unique_id": "00:00:00:00:00:00",
"source": config_entries.SOURCE_ZEROCONF, "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 result["errors"]["pairing_code"] == expected
assert get_flow_context(hass, result) == { assert get_flow_context(hass, result) == {
"title_placeholders": {"name": "TestDevice"}, "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
"unique_id": "00:00:00:00:00:00", "unique_id": "00:00:00:00:00:00",
"source": config_entries.SOURCE_ZEROCONF, "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) == { assert get_flow_context(hass, result) == {
"title_placeholders": {"name": "TestDevice"}, "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
"unique_id": "00:00:00:00:00:00", "unique_id": "00:00:00:00:00:00",
"source": config_entries.SOURCE_ZEROCONF, "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 result["type"] == "form"
assert get_flow_context(hass, result) == { assert get_flow_context(hass, result) == {
"title_placeholders": {"name": "TestDevice"}, "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
"unique_id": "00:00:00:00:00:00", "unique_id": "00:00:00:00:00:00",
"source": config_entries.SOURCE_ZEROCONF, "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) == { assert get_flow_context(hass, result) == {
"title_placeholders": {"name": "TestDevice"}, "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
"unique_id": "00:00:00:00:00:00", "unique_id": "00:00:00:00:00:00",
"source": config_entries.SOURCE_ZEROCONF, "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 result["type"] == "form"
assert get_flow_context(hass, result) == { assert get_flow_context(hass, result) == {
"title_placeholders": {"name": "TestDevice"}, "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
"unique_id": "00:00:00:00:00:00", "unique_id": "00:00:00:00:00:00",
"source": config_entries.SOURCE_ZEROCONF, "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 result["errors"]["pairing_code"] == expected
assert get_flow_context(hass, result) == { assert get_flow_context(hass, result) == {
"title_placeholders": {"name": "TestDevice"}, "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
"unique_id": "00:00:00:00:00:00", "unique_id": "00:00:00:00:00:00",
"source": config_entries.SOURCE_ZEROCONF, "source": config_entries.SOURCE_ZEROCONF,
"pairing": True, "pairing": True,
@ -737,7 +773,7 @@ async def test_user_works(hass, controller):
assert get_flow_context(hass, result) == { assert get_flow_context(hass, result) == {
"source": config_entries.SOURCE_USER, "source": config_entries.SOURCE_USER,
"unique_id": "00:00:00:00:00:00", "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( 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) == { assert get_flow_context(hass, result) == {
"source": config_entries.SOURCE_USER, "source": config_entries.SOURCE_USER,
"unique_id": "00:00:00:00:00:00", "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( 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["type"] == "form"
assert result["step_id"] == "pair" assert result["step_id"] == "pair"
assert get_flow_context(hass, result) == { assert get_flow_context(hass, result) == {
"title_placeholders": {"name": "TestDevice"}, "title_placeholders": {"name": "TestDevice", "category": "Other"},
"unique_id": "00:00:00:00:00:00", "unique_id": "00:00:00:00:00:00",
"source": config_entries.SOURCE_UNIGNORE, "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) == { assert get_flow_context(hass, result) == {
"title_placeholders": {"name": "TestDevice"}, "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
"unique_id": "00:00:00:00:00:00", "unique_id": "00:00:00:00:00:00",
"source": config_entries.SOURCE_ZEROCONF, "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 result["type"] == "form"
assert get_flow_context(hass, result) == { assert get_flow_context(hass, result) == {
"title_placeholders": {"name": "TestDevice"}, "title_placeholders": {"name": "TestDevice", "category": "Outlet"},
"unique_id": "00:00:00:00:00:00", "unique_id": "00:00:00:00:00:00",
"source": config_entries.SOURCE_ZEROCONF, "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["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "Koogeek-LS1-20833F" assert result["title"] == "Koogeek-LS1-20833F"
assert result["data"] == {} 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"] == {}