diff --git a/homeassistant/components/homekit_controller/button.py b/homeassistant/components/homekit_controller/button.py index d71ffa9539e..b065f4a5a27 100644 --- a/homeassistant/components/homekit_controller/button.py +++ b/homeassistant/components/homekit_controller/button.py @@ -5,6 +5,7 @@ characteristics that don't map to a Home Assistant feature. """ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass import logging @@ -32,6 +33,7 @@ _LOGGER = logging.getLogger(__name__) class HomeKitButtonEntityDescription(ButtonEntityDescription): """Describes Homekit button.""" + probe: Callable[[Characteristic], bool] | None = None write_value: int | str | None = None @@ -71,13 +73,19 @@ async def async_setup_entry( @callback def async_add_characteristic(char: Characteristic) -> bool: - entities: list[HomeKitButton | HomeKitEcobeeClearHoldButton] = [] + entities: list[CharacteristicEntity] = [] info = {"aid": char.service.accessory.aid, "iid": char.service.iid} if description := BUTTON_ENTITIES.get(char.type): entities.append(HomeKitButton(conn, info, char, description)) elif entity_type := BUTTON_ENTITY_CLASSES.get(char.type): entities.append(entity_type(conn, info, char)) + elif char.type == CharacteristicsTypes.THREAD_CONTROL_POINT: + if not conn.is_unprovisioned_thread_device: + return False + entities.append( + HomeKitProvisionPreferredThreadCredentials(conn, info, char) + ) else: return False @@ -92,7 +100,11 @@ async def async_setup_entry( conn.add_char_factory(async_add_characteristic) -class HomeKitButton(CharacteristicEntity, ButtonEntity): +class BaseHomeKitButton(CharacteristicEntity, ButtonEntity): + """Base class for all HomeKit buttons.""" + + +class HomeKitButton(BaseHomeKitButton): """Representation of a Button control on a homekit accessory.""" entity_description: HomeKitButtonEntityDescription @@ -126,7 +138,7 @@ class HomeKitButton(CharacteristicEntity, ButtonEntity): await self.async_put_characteristics({key: val}) -class HomeKitEcobeeClearHoldButton(CharacteristicEntity, ButtonEntity): +class HomeKitEcobeeClearHoldButton(BaseHomeKitButton): """Representation of a Button control for Ecobee clear hold request.""" def get_characteristic_types(self) -> list[str]: @@ -155,7 +167,7 @@ class HomeKitEcobeeClearHoldButton(CharacteristicEntity, ButtonEntity): await self.async_put_characteristics({key: val}) -class HomeKitProvisionPreferredThreadCredentials(CharacteristicEntity, ButtonEntity): +class HomeKitProvisionPreferredThreadCredentials(BaseHomeKitButton): """A button users can press to migrate their HomeKit BLE device to Thread.""" _attr_entity_category = EntityCategory.CONFIG @@ -179,5 +191,4 @@ class HomeKitProvisionPreferredThreadCredentials(CharacteristicEntity, ButtonEnt BUTTON_ENTITY_CLASSES: dict[str, type] = { CharacteristicsTypes.VENDOR_ECOBEE_CLEAR_HOLD: HomeKitEcobeeClearHoldButton, - CharacteristicsTypes.THREAD_CONTROL_POINT: HomeKitProvisionPreferredThreadCredentials, } diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 299b01e5b00..96fba8090f9 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -18,7 +18,7 @@ from aiohomekit.exceptions import ( EncryptionError, ) from aiohomekit.model import Accessories, Accessory, Transport -from aiohomekit.model.characteristics import Characteristic +from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.thread.dataset_store import async_get_preferred_dataset @@ -544,6 +544,10 @@ class HKDevice: current_unique_id.add((accessory.aid, service.iid, None)) for char in service.characteristics: + if self.pairing.transport != Transport.BLE: + if char.type == CharacteristicsTypes.THREAD_CONTROL_POINT: + continue + current_unique_id.add( ( accessory.aid, diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 2505f4de8e3..d5aa44fb163 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -70,44 +70,6 @@ 'state': 'unknown', }), }), - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.airversa_ap2_1808_provision_preferred_thread_credentials', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Airversa AP2 1808 Provision Preferred Thread Credentials', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_1_112_119', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'friendly_name': 'Airversa AP2 1808 Provision Preferred Thread Credentials', - }), - 'entity_id': 'button.airversa_ap2_1808_provision_preferred_thread_credentials', - 'state': 'unknown', - }), - }), dict({ 'entry': dict({ 'aliases': list([ @@ -14200,44 +14162,6 @@ 'state': 'unknown', }), }), - dict({ - 'entry': dict({ - 'aliases': list([ - ]), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': 'TestData', - 'device_class': None, - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.nanoleaf_strip_3b32_provision_preferred_thread_credentials', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'labels': list([ - ]), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Nanoleaf Strip 3B32 Provision Preferred Thread Credentials', - 'platform': 'homekit_controller', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00:00:00:00:00:00_1_31_119', - 'unit_of_measurement': None, - }), - 'state': dict({ - 'attributes': dict({ - 'friendly_name': 'Nanoleaf Strip 3B32 Provision Preferred Thread Credentials', - }), - 'entity_id': 'button.nanoleaf_strip_3b32_provision_preferred_thread_credentials', - 'state': 'unknown', - }), - }), dict({ 'entry': dict({ 'aliases': list([ diff --git a/tests/components/homekit_controller/test_connection.py b/tests/components/homekit_controller/test_connection.py index 35b88c1abbe..d8382bdde86 100644 --- a/tests/components/homekit_controller/test_connection.py +++ b/tests/components/homekit_controller/test_connection.py @@ -14,7 +14,7 @@ from homeassistant.components.homekit_controller.const import ( from homeassistant.components.thread import async_add_dataset, dataset_store from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from .common import setup_accessories_from_file, setup_platform, setup_test_accessories @@ -216,7 +216,9 @@ async def test_thread_provision_no_creds(hass: HomeAssistant) -> None: ) -async def test_thread_provision(hass: HomeAssistant) -> None: +async def test_thread_provision( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: """Test that a when a thread provision works the config entry is updated.""" await async_add_dataset( hass, @@ -256,6 +258,13 @@ async def test_thread_provision(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + assert hass.states.get( + "button.nanoleaf_strip_3b32_provision_preferred_thread_credentials" + ) + assert entity_registry.async_get( + "button.nanoleaf_strip_3b32_provision_preferred_thread_credentials" + ) + await hass.services.async_call( "button", "press", @@ -267,6 +276,13 @@ async def test_thread_provision(hass: HomeAssistant) -> None: assert config_entry.data["Connection"] == "CoAP" + assert not hass.states.get( + "button.nanoleaf_strip_3b32_provision_preferred_thread_credentials" + ) + assert not entity_registry.async_get( + "button.nanoleaf_strip_3b32_provision_preferred_thread_credentials" + ) + async def test_thread_provision_migration_failed(hass: HomeAssistant) -> None: """Test that when a device 'migrates' but doesn't show up in CoAP, we remain in BLE mode."""