Add bluetooth discovery to HomeKit Controller (#75333)
Co-authored-by: Jc2k <john.carr@unrouted.co.uk>pull/75359/head
parent
503b31fb15
commit
8d63f81821
|
@ -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":
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"] == {}
|
||||
|
|
Loading…
Reference in New Issue