From ef4062a565b1f2f102b4331ebce3c670a9e60062 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Dec 2025 10:37:19 +0100 Subject: [PATCH] Improve helpers.trigger.async_subscribe_platform_events (#157709) --- homeassistant/helpers/trigger.py | 17 ++++- tests/helpers/test_trigger.py | 119 +++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index bbdea72486f..033ebd93a29 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -169,7 +169,12 @@ def async_subscribe_platform_events( async def _register_trigger_platform( hass: HomeAssistant, integration_domain: str, platform: TriggerProtocol ) -> None: - """Register a trigger platform.""" + """Register a trigger platform and notify listeners. + + If the trigger platform does not provide any triggers, or it is disabled, + listeners will not be notified. + """ + from homeassistant.components import automation # noqa: PLC0415 new_triggers: set[str] = set() @@ -178,6 +183,12 @@ async def _register_trigger_platform( trigger_key = get_absolute_description_key(integration_domain, trigger_key) hass.data[TRIGGERS][trigger_key] = integration_domain new_triggers.add(trigger_key) + if not new_triggers: + _LOGGER.debug( + "Integration %s returned no triggers in async_get_triggers", + integration_domain, + ) + return elif hasattr(platform, "async_validate_trigger_config") or hasattr( platform, "TRIGGER_SCHEMA" ): @@ -190,6 +201,10 @@ async def _register_trigger_platform( ) return + if automation.is_disabled_experimental_trigger(hass, integration_domain): + _LOGGER.debug("Triggers for integration %s are disabled", integration_domain) + return + # We don't use gather here because gather adds additional overhead # when wrapping each coroutine in a task, and we expect our listeners # to call trigger.async_get_all_descriptions which will only yield diff --git a/tests/helpers/test_trigger.py b/tests/helpers/test_trigger.py index 936fecf85a7..608da382e5e 100644 --- a/tests/helpers/test_trigger.py +++ b/tests/helpers/test_trigger.py @@ -1006,3 +1006,122 @@ async def test_subscribe_triggers( assert await async_setup_component(hass, "sun", {}) assert trigger_events == [{"sun"}] assert "Error while notifying trigger platform listener" in caplog.text + + +@patch("annotatedyaml.loader.load_yaml") +@patch.object(Integration, "has_triggers", return_value=True) +@pytest.mark.parametrize( + ("new_triggers_conditions_enabled", "expected_events"), + [ + (True, [{"light.turned_off", "light.turned_on"}]), + (False, []), + ], +) +async def test_subscribe_triggers_experimental_triggers( + mock_has_triggers: Mock, + mock_load_yaml: Mock, + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + caplog: pytest.LogCaptureFixture, + new_triggers_conditions_enabled: bool, + expected_events: list[set[str]], +) -> None: + """Test trigger.async_subscribe_platform_events doesn't send events for disabled triggers.""" + # Return empty triggers.yaml for light integration, the actual trigger descriptions + # are irrelevant for this test + light_trigger_descriptions = "" + + def _load_yaml(fname, secrets=None): + if fname.endswith("light/triggers.yaml"): + trigger_descriptions = light_trigger_descriptions + else: + raise FileNotFoundError + with io.StringIO(trigger_descriptions) as file: + return parse_yaml(file) + + mock_load_yaml.side_effect = _load_yaml + + trigger_events = [] + + async def good_subscriber(new_triggers: set[str]): + """Simulate a working subscriber.""" + trigger_events.append(new_triggers) + + ws_client = await hass_ws_client(hass) + + assert await async_setup_component(hass, "labs", {}) + await ws_client.send_json_auto_id( + { + "type": "labs/update", + "domain": "automation", + "preview_feature": "new_triggers_conditions", + "enabled": new_triggers_conditions_enabled, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + trigger.async_subscribe_platform_events(hass, good_subscriber) + + assert await async_setup_component(hass, "light", {}) + await hass.async_block_till_done() + assert trigger_events == expected_events + + +@patch("annotatedyaml.loader.load_yaml") +@patch.object(Integration, "has_triggers", return_value=True) +@patch( + "homeassistant.components.light.trigger.async_get_triggers", + new=AsyncMock(return_value={}), +) +async def test_subscribe_triggers_no_triggers( + mock_has_triggers: Mock, + mock_load_yaml: Mock, + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test trigger.async_subscribe_platform_events doesn't send events for platforms without triggers.""" + # Return empty triggers.yaml for light integration, the actual trigger descriptions + # are irrelevant for this test + light_trigger_descriptions = "" + + def _load_yaml(fname, secrets=None): + if fname.endswith("light/triggers.yaml"): + trigger_descriptions = light_trigger_descriptions + else: + raise FileNotFoundError + with io.StringIO(trigger_descriptions) as file: + return parse_yaml(file) + + mock_load_yaml.side_effect = _load_yaml + + trigger_events = [] + + async def good_subscriber(new_triggers: set[str]): + """Simulate a working subscriber.""" + trigger_events.append(new_triggers) + + ws_client = await hass_ws_client(hass) + + assert await async_setup_component(hass, "labs", {}) + await ws_client.send_json_auto_id( + { + "type": "labs/update", + "domain": "automation", + "preview_feature": "new_triggers_conditions", + "enabled": True, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + trigger.async_subscribe_platform_events(hass, good_subscriber) + + assert await async_setup_component(hass, "light", {}) + await hass.async_block_till_done() + assert trigger_events == []