diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 2c69512db9d..7e98fc40910 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -8,12 +8,19 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.core import callback +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + async_get_registry as async_get_device_registry, +) from .connection import get_accessory_name, get_bridge_information from .const import DOMAIN, KNOWN_DEVICES -HOMEKIT_IGNORE = ["Home Assistant Bridge"] HOMEKIT_DIR = ".homekit" +HOMEKIT_BRIDGE_DOMAIN = "homekit" +HOMEKIT_BRIDGE_SERIAL_NUMBER = "homekit.bridge" +HOMEKIT_BRIDGE_MODEL = "Home Assistant HomeKit Bridge" + PAIRING_FILE = "pairing.json" PIN_FORMAT = re.compile(r"^(\d{3})-{0,1}(\d{2})-{0,1}(\d{3})$") @@ -141,6 +148,17 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): return self.async_abort(reason="no_devices") + async def _hkid_is_homekit_bridge(self, hkid): + """Determine if the device is a homekit bridge.""" + dev_reg = await async_get_device_registry(self.hass) + device = dev_reg.async_get_device( + identifiers=set(), connections={(CONNECTION_NETWORK_MAC, hkid)} + ) + + if device is None: + return False + return device.model == HOMEKIT_BRIDGE_MODEL + async def async_step_zeroconf(self, discovery_info): """Handle a discovered HomeKit accessory. @@ -153,6 +171,12 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): key.lower(): value for (key, value) in discovery_info["properties"].items() } + if "id" not in properties: + _LOGGER.warning( + "HomeKit device %s: id not exposed, in violation of spec", properties + ) + return self.async_abort(reason="invalid_properties") + # The hkid is a unique random number that looks like a pairing code. # It changes if a device is factory reset. hkid = properties["id"] @@ -208,7 +232,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): # Devices in HOMEKIT_IGNORE have native local integrations - users # should be encouraged to use native integration and not confused # by alternative HK API. - if model in HOMEKIT_IGNORE: + if await self._hkid_is_homekit_bridge(hkid): return self.async_abort(reason="ignored_model") self.model = model diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index bc07b71fa75..e685a46e144 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -44,6 +44,7 @@ "already_configured": "Accessory is already configured with this controller.", "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting configuration entry for it in Home Assistant that must first be removed.", "accessory_not_found_error": "Cannot add pairing as device can no longer be found.", + "invalid_properties": "Invalid properties announced by device.", "already_in_progress": "Config flow for device is already in progress." } } diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 09c823ff498..e5c8e381a5f 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -8,10 +8,11 @@ from aiohomekit.model.services import ServicesTypes import pytest from homeassistant.components.homekit_controller import config_flow +from homeassistant.helpers import device_registry import tests.async_mock from tests.async_mock import patch -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_device_registry PAIRING_START_FORM_ERRORS = [ (KeyError, "pairing_failed"), @@ -233,11 +234,45 @@ async def test_pair_already_paired_1(hass, controller): assert result["reason"] == "already_paired" +async def test_id_missing(hass, controller): + """Test id is missing.""" + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) + + # Remove id from device + del discovery_info["properties"]["id"] + + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + ) + assert result["type"] == "abort" + assert result["reason"] == "invalid_properties" + + async def test_discovery_ignored_model(hass, controller): """Already paired.""" device = setup_mock_accessory(controller) discovery_info = get_device_discovery_info(device) - discovery_info["properties"]["md"] = config_flow.HOMEKIT_IGNORE[0] + + config_entry = MockConfigEntry(domain=config_flow.HOMEKIT_BRIDGE_DOMAIN, data={}) + formatted_mac = device_registry.format_mac("AA:BB:CC:DD:EE:FF") + + dev_reg = mock_device_registry(hass) + dev_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={ + ( + config_flow.HOMEKIT_BRIDGE_DOMAIN, + config_entry.entry_id, + config_flow.HOMEKIT_BRIDGE_SERIAL_NUMBER, + ) + }, + connections={(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)}, + model=config_flow.HOMEKIT_BRIDGE_MODEL, + ) + + discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF" # Device is discovered result = await hass.config_entries.flow.async_init(