From 0db160e37235351877fccb2b39f91566ef58988a Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 24 Jul 2021 07:03:44 +0100 Subject: [PATCH] Handle homekit accessories where the pairing flag is wrong (#53385) --- .../homekit_controller/config_flow.py | 25 ++++++++- .../homekit_controller/test_config_flow.py | 56 +++++++++++++++++-- 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index e8357a4001d..a19c1d0c107 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -3,6 +3,7 @@ import logging import re import aiohomekit +from aiohomekit.exceptions import AuthenticationError import voluptuous as vol from homeassistant import config_entries @@ -270,7 +271,29 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # invalid. Remove it automatically. existing = find_existing_host(self.hass, hkid) if not paired and existing: - await self.hass.config_entries.async_remove(existing.entry_id) + if self.controller is None: + await self._async_setup_controller() + + pairing = self.controller.load_pairing( + existing.data["AccessoryPairingID"], dict(existing.data) + ) + try: + await pairing.list_accessories_and_characteristics() + _LOGGER.debug( + "%s (%s - %s) claims to be unpaired but isn't. It's implementation of HomeKit is defective or a zeroconf relay is broadcasting stale data", + name, + model, + hkid, + ) + return self.async_abort(reason="already_paired") + except AuthenticationError: + _LOGGER.debug( + "%s (%s - %s) is unpaired. Removing invalid pairing for this device", + name, + model, + hkid, + ) + await self.hass.config_entries.async_remove(existing.entry_id) # Set unique-id and error out if it's already configured self._abort_if_unique_id_configured(updates=updated_ip_port) diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 52685334500..b08659bf77b 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -4,6 +4,7 @@ import unittest.mock from unittest.mock import AsyncMock, patch import aiohomekit +from aiohomekit.exceptions import AuthenticationError from aiohomekit.model import Accessories, Accessory from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes @@ -351,8 +352,48 @@ async def test_discovery_does_not_ignore_non_homekit(hass, controller): assert result["type"] == "form" +async def test_discovery_broken_pairing_flag(hass, controller): + """ + There is already a config entry for the pairing and its pairing flag is wrong in zeroconf. + + We have seen this particular implementation error in 2 different devices. + """ + await controller.add_paired_device(Accessories(), "00:00:00:00:00:00") + + MockConfigEntry( + domain="homekit_controller", + data={"AccessoryPairingID": "00:00:00:00:00:00"}, + unique_id="00:00:00:00:00:00", + ).add_to_hass(hass) + + # We just added a mock config entry so it must be visible in hass + assert len(hass.config_entries.async_entries()) == 1 + + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) + + # Make sure that we are pairable + assert discovery_info["properties"]["sf"] != 0x0 + + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, + ) + + # Should still be paired. + config_entry_count = len(hass.config_entries.async_entries()) + assert config_entry_count == 1 + + # Even though discovered as pairable, we bail out as already paired. + assert result["reason"] == "already_paired" + + async def test_discovery_invalid_config_entry(hass, controller): """There is already a config entry for the pairing id but it's invalid.""" + pairing = await controller.add_paired_device(Accessories(), "00:00:00:00:00:00") + MockConfigEntry( domain="homekit_controller", data={"AccessoryPairingID": "00:00:00:00:00:00"}, @@ -366,11 +407,16 @@ async def test_discovery_invalid_config_entry(hass, controller): discovery_info = get_device_discovery_info(device) # Device is discovered - result = await hass.config_entries.flow.async_init( - "homekit_controller", - context={"source": config_entries.SOURCE_ZEROCONF}, - data=discovery_info, - ) + with patch.object( + pairing, + "list_accessories_and_characteristics", + side_effect=AuthenticationError("Invalid pairing keys"), + ): + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, + ) # Discovery of a HKID that is in a pairable state but for which there is # already a config entry - in that case the stale config entry is