From f49cfe866a6e0b41dc7cdb183c6dece55fc99129 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 27 Jan 2022 22:02:30 +0000 Subject: [PATCH] Support unpairing homekit accessories from homekit_controller (#65065) --- .../components/homekit_controller/__init__.py | 20 +++++++++++++++ .../homekit_controller/test_init.py | 24 ++++++++++++++++++ .../homekit_controller/test_storage.py | 25 ------------------- 3 files changed, 44 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index a26978f537a..0f231fa9303 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +import logging from typing import Any import aiohomekit @@ -26,6 +27,8 @@ from .connection import HKDevice, valid_serial_number from .const import CONTROLLER, ENTITY_MAP, KNOWN_DEVICES, TRIGGERS from .storage import EntityMapStorage +_LOGGER = logging.getLogger(__name__) + def escape_characteristic_name(char_name): """Escape any dash or dots in a characteristics name.""" @@ -248,4 +251,21 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Cleanup caches before removing config entry.""" hkid = entry.data["AccessoryPairingID"] + + # Remove cached type data from .storage/homekit_controller-entity-map hass.data[ENTITY_MAP].async_delete_map(hkid) + + # Remove the pairing on the device, making the device discoverable again. + # Don't reuse any objects in hass.data as they are already unloaded + async_zeroconf_instance = await zeroconf.async_get_async_instance(hass) + controller = aiohomekit.Controller(async_zeroconf_instance=async_zeroconf_instance) + controller.load_pairing(hkid, dict(entry.data)) + try: + await controller.remove_pairing(hkid) + except aiohomekit.AccessoryDisconnectedError: + _LOGGER.warning( + "Accessory %s was removed from HomeAssistant but was not reachable " + "to properly unpair. It may need resetting before you can use it with " + "HomeKit again", + entry.title, + ) diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index cd5662d73c9..d1b133468d5 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -4,8 +4,11 @@ from unittest.mock import patch from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes +from aiohomekit.testing import FakeController +from homeassistant.components.homekit_controller.const import ENTITY_MAP from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from tests.components.homekit_controller.common import setup_test_component @@ -27,3 +30,24 @@ async def test_unload_on_stop(hass, utcnow): await hass.async_block_till_done() assert async_unlock_mock.called + + +async def test_async_remove_entry(hass: HomeAssistant): + """Test unpairing a component.""" + helper = await setup_test_component(hass, create_motion_sensor_service) + + hkid = "00:00:00:00:00:00" + + with patch("aiohomekit.Controller") as controller_cls: + # Setup a fake controller with 1 pairing + controller = controller_cls.return_value = FakeController() + await controller.add_paired_device([helper.accessory], hkid) + assert len(controller.pairings) == 1 + + assert hkid in hass.data[ENTITY_MAP].storage_data + + # Remove it via config entry and number of pairings should go down + await helper.config_entry.async_remove(hass) + assert len(controller.pairings) == 0 + + assert hkid not in hass.data[ENTITY_MAP].storage_data diff --git a/tests/components/homekit_controller/test_storage.py b/tests/components/homekit_controller/test_storage.py index aa0a5e55057..b4ed617f901 100644 --- a/tests/components/homekit_controller/test_storage.py +++ b/tests/components/homekit_controller/test_storage.py @@ -2,8 +2,6 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from homeassistant import config_entries -from homeassistant.components.homekit_controller import async_remove_entry from homeassistant.components.homekit_controller.const import ENTITY_MAP from tests.common import flush_store @@ -79,26 +77,3 @@ async def test_storage_is_updated_on_add(hass, hass_storage, utcnow): # Is saved out to store? await flush_store(entity_map.store) assert hkid in hass_storage[ENTITY_MAP]["data"]["pairings"] - - -async def test_storage_is_removed_on_config_entry_removal(hass, utcnow): - """Test entity map storage is cleaned up on config entry removal.""" - await setup_test_component(hass, create_lightbulb_service) - - hkid = "00:00:00:00:00:00" - - pairing_data = {"AccessoryPairingID": hkid} - - entry = config_entries.ConfigEntry( - 1, - "homekit_controller", - "TestData", - pairing_data, - "test", - ) - - assert hkid in hass.data[ENTITY_MAP].storage_data - - await async_remove_entry(hass, entry) - - assert hkid not in hass.data[ENTITY_MAP].storage_data