From 2dca165869ea6a14e9d9e84e59f7ffdff1a2782b Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 19 Jun 2025 15:54:37 +0200 Subject: [PATCH] Exclude triggers without description from WS command response --- .../components/websocket_api/commands.py | 14 ++++++-- homeassistant/helpers/trigger.py | 21 +++++++----- .../components/websocket_api/test_commands.py | 34 +++++++++++++++++-- tests/helpers/test_trigger.py | 10 ++---- 4 files changed, 57 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 6df008fff00..fde52d662d0 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -536,7 +536,13 @@ async def _async_get_all_trigger_descriptions_json(hass: HomeAssistant) -> bytes # If the descriptions are the same, return the cached JSON payload if cached_descriptions is descriptions: return cast(bytes, cached_json_payload) - json_payload = json_bytes(descriptions) + json_payload = json_bytes( + { + trigger: description + for trigger, description in descriptions.items() + if description is not None + } + ) hass.data[ALL_TRIGGER_DESCRIPTIONS_JSON_CACHE] = (descriptions, json_payload) return json_payload @@ -553,8 +559,10 @@ async def handle_subscribe_trigger_platforms( descriptions = await async_get_all_trigger_descriptions(hass) new_trigger_descriptions = {} for trigger in new_triggers: - if trigger in descriptions: - new_trigger_descriptions[trigger] = descriptions[trigger] + if (description := descriptions[trigger]) is not None: + new_trigger_descriptions[trigger] = description + if not new_trigger_descriptions: + return connection.send_event(msg["id"], new_trigger_descriptions) connection.subscriptions[msg["id"]] = async_subscribe_trigger_platform_events( diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index 1665212d711..bbe85c5c8d2 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -60,7 +60,7 @@ DATA_PLUGGABLE_ACTIONS: HassKey[defaultdict[tuple, PluggableActionsEntry]] = Has "pluggable_actions" ) -TRIGGER_DESCRIPTION_CACHE: HassKey[dict[str, dict[str, Any]]] = HassKey( +TRIGGER_DESCRIPTION_CACHE: HassKey[dict[str, dict[str, Any] | None]] = HassKey( "trigger_description_cache" ) TRIGGER_PLATFORM_SUBSCRIPTIONS: HassKey[ @@ -550,7 +550,7 @@ def _load_triggers_files( async def async_get_all_descriptions( hass: HomeAssistant, -) -> dict[str, dict[str, Any]]: +) -> dict[str, dict[str, Any] | None]: """Return descriptions (i.e. user documentation) for all triggers.""" descriptions_cache = hass.data[TRIGGER_DESCRIPTION_CACHE] @@ -599,12 +599,17 @@ async def async_get_all_descriptions( for missing_trigger in missing_triggers: domain = triggers[missing_trigger] - # Cache missing descriptions - domain_yaml = new_triggers_descriptions.get(domain) or {} - - yaml_description = ( - domain_yaml.get(missing_trigger) or {} # type: ignore[union-attr] - ) + if ( + yaml_description := new_triggers_descriptions.get(domain, {}).get( # type: ignore[union-attr] + missing_trigger + ) + ) is None: + _LOGGER.debug( + "No trigger descriptions found for trigger %s, skipping", + missing_trigger, + ) + new_descriptions_cache[missing_trigger] = None + continue description = {"fields": yaml_description.get("fields", {})} diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 71f0ffb5941..7836a2708dd 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -2,6 +2,7 @@ import asyncio from copy import deepcopy +import io import logging from typing import Any from unittest.mock import ANY, AsyncMock, Mock, patch @@ -28,9 +29,10 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.loader import async_get_integration +from homeassistant.loader import Integration, async_get_integration from homeassistant.setup import async_set_domains_to_be_loaded, async_setup_component from homeassistant.util.json import json_loads +from homeassistant.util.yaml.loader import parse_yaml from tests.common import ( MockConfigEntry, @@ -680,10 +682,34 @@ async def test_get_services( assert msg["result"].keys() == hass.services.async_services().keys() +@patch("annotatedyaml.loader.load_yaml") +@patch.object(Integration, "has_triggers", return_value=True) async def test_subscribe_triggers( - hass: HomeAssistant, websocket_client: MockHAClientWebSocket + mock_has_triggers: Mock, + mock_load_yaml: Mock, + hass: HomeAssistant, + websocket_client: MockHAClientWebSocket, ) -> None: """Test get_triggers command.""" + sun_service_descriptions = """ + sun: {} + """ + tag_service_descriptions = """ + tag: {} + """ + + def _load_yaml(fname, secrets=None): + if fname.endswith("sun/triggers.yaml"): + service_descriptions = sun_service_descriptions + elif fname.endswith("tag/triggers.yaml"): + service_descriptions = tag_service_descriptions + else: + raise FileNotFoundError + with io.StringIO(service_descriptions) as file: + return parse_yaml(file) + + mock_load_yaml.side_effect = _load_yaml + assert await async_setup_component(hass, "sun", {}) assert await async_setup_component(hass, "system_health", {}) await hass.async_block_till_done() @@ -700,8 +726,10 @@ async def test_subscribe_triggers( old_cache = hass.data[ALL_TRIGGER_DESCRIPTIONS_JSON_CACHE] - # Test we receive an event when a new platform is loaded + # Test we receive an event when a new platform is loaded, if it has descriptions + assert await async_setup_component(hass, "calendar", {}) assert await async_setup_component(hass, "tag", {}) + await hass.async_block_till_done() msg = await websocket_client.receive_json() assert msg == { "event": {"tag": {"fields": {}}}, diff --git a/tests/helpers/test_trigger.py b/tests/helpers/test_trigger.py index dba6f885fcd..91454ce44da 100644 --- a/tests/helpers/test_trigger.py +++ b/tests/helpers/test_trigger.py @@ -478,8 +478,6 @@ async def test_async_get_all_descriptions( hass: HomeAssistant, sun_service_descriptions: str ) -> None: """Test async_get_all_descriptions.""" - await trigger.async_setup(hass) # Move to hass fixture - assert await async_setup_component(hass, DOMAIN_SUN, {}) assert await async_setup_component(hass, DOMAIN_SYSTEM_HEALTH, {}) await hass.async_block_till_done() @@ -546,8 +544,6 @@ async def test_async_get_all_descriptions_with_yaml_error( expected_message: str, ) -> None: """Test async_get_all_descriptions.""" - await trigger.async_setup(hass) # Move to hass fixture - assert await async_setup_component(hass, DOMAIN_SUN, {}) await hass.async_block_till_done() @@ -563,7 +559,7 @@ async def test_async_get_all_descriptions_with_yaml_error( ): descriptions = await trigger.async_get_all_descriptions(hass) - assert descriptions == {DOMAIN_SUN: {"fields": {}}} + assert descriptions == {DOMAIN_SUN: None} assert expected_message in caplog.text @@ -578,8 +574,6 @@ async def test_async_get_all_descriptions_with_bad_description( fields: not_a_dict """ - await trigger.async_setup(hass) # Move to hass fixture - assert await async_setup_component(hass, DOMAIN_SUN, {}) await hass.async_block_till_done() @@ -596,7 +590,7 @@ async def test_async_get_all_descriptions_with_bad_description( ): descriptions = await trigger.async_get_all_descriptions(hass) - assert descriptions == {DOMAIN_SUN: {"fields": {}}} + assert descriptions == {DOMAIN_SUN: None} assert ( "Unable to parse triggers.yaml for the sun integration: "