diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 388d22664ea2..952496bdce62 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -9,6 +9,7 @@ from yarl import URL from zha.application.const import BAUD_RATES, RadioType from zha.application.gateway import Gateway from zha.application.helpers import ZHAData +from zha.quirks import DEVICE_REGISTRY from zha.zigbee.device import get_device_automation_triggers from zigpy.config import CONF_DATABASE, CONF_DEVICE, CONF_DEVICE_PATH from zigpy.exceptions import NetworkSettingsInconsistent, TransientConnectionError @@ -158,11 +159,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b zha_gateway = await Gateway.async_from_config(zha_lib_data) - # Load and cache device trigger information early + # Load and cache device trigger information early. Quirks were registered by + # `Gateway.async_from_config` above, so pass the resolver to quirk devices + # and surface quirk-defined triggers (e.g. remote button presses). device_registry = dr.async_get(hass) radio_mgr = ZhaRadioManager.from_config_entry(hass, config_entry) - async with radio_mgr.create_zigpy_app(connect=False) as app: + async with radio_mgr.create_zigpy_app( + connect=False, device_resolver=DEVICE_REGISTRY.resolve + ) as app: for dev in app.devices.values(): dev_entry = device_registry.async_get_device( identifiers={(DOMAIN, str(dev.ieee))}, diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index cdae1558e8e1..6ef2e4013880 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -1,7 +1,7 @@ """ZHA radio manager.""" import asyncio -from collections.abc import AsyncGenerator +from collections.abc import AsyncGenerator, Callable import contextlib from contextlib import suppress import copy @@ -22,6 +22,7 @@ from zigpy.config import ( CONF_NWK_BACKUP_ENABLED, SCHEMA_DEVICE, ) +import zigpy.device from zigpy.exceptions import NetworkNotFormed from homeassistant import config_entries @@ -174,9 +175,17 @@ class ZhaRadioManager: @contextlib.asynccontextmanager async def create_zigpy_app( - self, *, connect: bool = True + self, + *, + connect: bool = True, + device_resolver: Callable[[zigpy.device.Device], zigpy.device.Device] + | None = None, ) -> AsyncGenerator[ControllerApplication]: - """Connect to the radio with the current config and then clean up.""" + """Connect to the radio with the current config and then clean up. + + `device_resolver` is forwarded to zigpy so devices loaded from the + database are quirk-resolved to get quirk-defined device triggers. + """ assert self.radio_type is not None config = get_zha_data(self.hass).yaml_config @@ -201,7 +210,10 @@ class ZhaRadioManager: app_config[CONF_USE_THREAD] = False app = await self.radio_type.controller.new( - app_config, auto_form=False, start_radio=False + app_config, + auto_form=False, + start_radio=False, + device_resolver=device_resolver, ) try: diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 0839bbfdcb9a..6afe6d063931 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -1,10 +1,11 @@ """ZHA device automation trigger tests.""" from collections.abc import Callable, Coroutine -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest -from zha.application.const import ATTR_ENDPOINT_ID +from zha.application.const import ATTR_ENDPOINT_ID, RadioType +from zha.quirks import DEVICE_REGISTRY from zigpy.application import ControllerApplication from zigpy.device import Device as ZigpyDevice import zigpy.profiles.zha @@ -558,3 +559,28 @@ async def test_validate_trigger_config_unloaded_bad_info( ) assert "Unable to find trigger" in caplog.text + + +async def test_device_trigger_cache_built_with_quirk_resolver( + zigpy_app_controller: ControllerApplication, + setup_zha: Callable[..., Coroutine[None]], +) -> None: + """Test the early device trigger cache is built with quirk resolution. + + Regression test: without quirk resolution, quirk-defined triggers (e.g. + remote button presses) are missing whenever the cache is used as a + fallback (i.e. before ZHA has finished loading). + """ + with patch.object( + RadioType.ezsp.controller, + "new", + AsyncMock(return_value=zigpy_app_controller), + ) as mock_new: + await setup_zha() + + # Both the trigger cache app and the gateway app must quirk-resolve devices + assert len(mock_new.await_args_list) == 2 + assert all( + call.kwargs.get("device_resolver") == DEVICE_REGISTRY.resolve + for call in mock_new.await_args_list + )