[api] Add description and example metadata to user-defined actions (#18881)

Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Brandon Harvey
2026-08-30 13:52:51 -05:00
committed by GitHub
co-authored by J. Nick Koston
parent 0607c228f5
commit fb65096ea3
22 changed files with 591 additions and 56 deletions
@@ -0,0 +1,26 @@
esphome:
name: api-action-metadata-test
host:
api:
batch_delay: 0ms
actions:
- action: play_buzzer
description: Play an RTTTL melody on the buzzer
variables:
song_str:
type: string
description: RTTTL melody string
example: "two_short:d=4,o=5,b=100:16e6,16e6"
volume:
type: int
then:
- logger.log:
format: "Buzzer: %s"
args: [song_str.c_str()]
- action: plain_action
variables:
value: int
then:
- logger.log: "Plain action called"
logger:
@@ -0,0 +1,65 @@
"""Integration test for user-defined action field metadata."""
from __future__ import annotations
import asyncio
import re
import pytest
from esphome.helpers import fnv1_hash
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_api_action_metadata(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Action and argument metadata reach the client and the actions still run."""
loop = asyncio.get_running_loop()
buzzer_called = loop.create_future()
plain_called = loop.create_future()
buzzer_pattern = re.compile(r"Buzzer: two_short")
plain_pattern = re.compile(r"Plain action called")
def check_output(line: str) -> None:
if not buzzer_called.done() and buzzer_pattern.search(line):
buzzer_called.set_result(True)
elif not plain_called.done() and plain_pattern.search(line):
plain_called.set_result(True)
async with (
run_compiled(yaml_config, line_callback=check_output),
api_client_connected() as client,
):
_, services = await client.list_entities_services()
by_name = {service.name: service for service in services}
assert set(by_name) == {"play_buzzer", "plain_action"}
# Keys are hashed at codegen time and must match what the client expects
for name, service in by_name.items():
assert service.key == fnv1_hash(name), name
buzzer = by_name["play_buzzer"]
assert buzzer.description == "Play an RTTTL melody on the buzzer"
args = {arg.name: arg for arg in buzzer.args}
assert args["song_str"].description == "RTTTL melody string"
assert args["song_str"].example == "two_short:d=4,o=5,b=100:16e6,16e6"
# An arg without metadata sends empty strings
assert args["volume"].description == ""
assert args["volume"].example == ""
# An action without metadata sends empty strings
plain = by_name["plain_action"]
assert plain.description == ""
assert plain.args[0].description == ""
await client.execute_service(
buzzer, {"song_str": "two_short:d=4,o=5,b=100:16e6,16e6", "volume": 3}
)
await client.execute_service(plain, {"value": 1})
await asyncio.wait_for(buzzer_called, timeout=5.0)
await asyncio.wait_for(plain_called, timeout=5.0)