This commit is contained in:
J. Nick Koston
2026-02-21 20:49:49 -06:00
parent b85878e1ee
commit fa343aa1ba
5 changed files with 47 additions and 17 deletions

View File

@@ -62,14 +62,16 @@ def register_action(
action_type: MockObjClass,
schema: cv.Schema,
*,
deferred: bool = False,
deferred: bool = True,
):
"""Register an action type.
Set ``deferred=True`` if this action stores trigger arguments for later
execution (e.g. delay, wait_until, script.wait). This tells the code
generator to use owning types (std::string) instead of non-owning views
(StringRef) for string arguments, preventing dangling references.
Actions default to ``deferred=True`` (safe default), meaning string
arguments use owning std::string to prevent dangling references.
Set ``deferred=False`` only for actions that complete synchronously
and never store trigger arguments for later execution. This allows
the code generator to use non-owning StringRef for zero-copy access.
"""
return ACTION_REGISTRY.register(name, action_type, schema, deferred=deferred)
@@ -351,7 +353,6 @@ async def component_is_idle_condition_to_code(
"delay",
DelayAction,
cv.templatable(cv.positive_time_period_milliseconds),
deferred=True,
)
async def delay_action_to_code(
config: ConfigType,
@@ -382,6 +383,7 @@ async def delay_action_to_code(
cv.has_at_least_one_key(CONF_THEN, CONF_ELSE),
cv.has_at_least_one_key(CONF_CONDITION, CONF_ANY, CONF_ALL),
),
deferred=False,
)
async def if_action_to_code(
config: ConfigType,
@@ -410,6 +412,7 @@ async def if_action_to_code(
cv.Required(CONF_THEN): validate_action_list,
}
),
deferred=False,
)
async def while_action_to_code(
config: ConfigType,
@@ -433,6 +436,7 @@ async def while_action_to_code(
cv.Required(CONF_THEN): validate_action_list,
}
),
deferred=False,
)
async def repeat_action_to_code(
config: ConfigType,
@@ -461,7 +465,7 @@ _validate_wait_until = cv.maybe_simple_value(
)
@register_action("wait_until", WaitUntilAction, _validate_wait_until, deferred=True)
@register_action("wait_until", WaitUntilAction, _validate_wait_until)
async def wait_until_action_to_code(
config: ConfigType,
action_id: ID,
@@ -477,7 +481,7 @@ async def wait_until_action_to_code(
return var
@register_action("lambda", LambdaAction, cv.lambda_)
@register_action("lambda", LambdaAction, cv.lambda_, deferred=False)
async def lambda_action_to_code(
config: ConfigType,
action_id: ID,
@@ -496,6 +500,7 @@ async def lambda_action_to_code(
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
}
),
deferred=False,
)
async def component_update_action_to_code(
config: ConfigType,
@@ -515,6 +520,7 @@ async def component_update_action_to_code(
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
}
),
deferred=False,
)
async def component_suspend_action_to_code(
config: ConfigType,
@@ -537,6 +543,7 @@ async def component_suspend_action_to_code(
),
}
),
deferred=False,
)
async def component_resume_action_to_code(
config: ConfigType,

View File

@@ -172,7 +172,9 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema(
@automation.register_action(
"ble_client.disconnect", BLEDisconnectAction, BLE_CONNECT_ACTION_SCHEMA
"ble_client.disconnect",
BLEDisconnectAction,
BLE_CONNECT_ACTION_SCHEMA,
)
async def ble_disconnect_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID])
@@ -180,7 +182,9 @@ async def ble_disconnect_to_code(config, action_id, template_arg, args):
@automation.register_action(
"ble_client.connect", BLEConnectAction, BLE_CONNECT_ACTION_SCHEMA
"ble_client.connect",
BLEConnectAction,
BLE_CONNECT_ACTION_SCHEMA,
)
async def ble_connect_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID])
@@ -188,7 +192,9 @@ async def ble_connect_to_code(config, action_id, template_arg, args):
@automation.register_action(
"ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA
"ble_client.ble_write",
BLEWriteAction,
BLE_WRITE_ACTION_SCHEMA,
)
async def ble_write_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID])

View File

@@ -160,6 +160,7 @@ async def to_code(config):
cv.Optional(validate_parameter_name): cv.templatable(cv.valid),
},
),
deferred=False,
)
async def script_execute_action_to_code(config, action_id, template_arg, args):
def convert(type: str):
@@ -208,6 +209,7 @@ async def script_execute_action_to_code(config, action_id, template_arg, args):
"script.stop",
ScriptStopAction,
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}),
deferred=False,
)
async def script_stop_action_to_code(config, action_id, template_arg, args):
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
@@ -219,7 +221,6 @@ async def script_stop_action_to_code(config, action_id, template_arg, args):
"script.wait",
ScriptWaitAction,
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}),
deferred=True,
)
async def script_wait_action_to_code(config, action_id, template_arg, args):
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])

View File

@@ -25,7 +25,7 @@ class RegistryEntry:
type_id: "MockObjClass",
schema: "Schema",
*,
deferred: bool = False,
deferred: bool = True,
):
self.name = name
self.fun = fun
@@ -58,7 +58,7 @@ class Registry(dict[str, RegistryEntry]):
type_id: "MockObjClass",
schema: "Schema",
*,
deferred: bool = False,
deferred: bool = True,
):
def decorator(fun: Callable[..., Any]):
self[name] = RegistryEntry(name, fun, type_id, schema, deferred=deferred)

View File

@@ -10,10 +10,13 @@ from esphome.util import RegistryEntry
def _make_registry(deferred_actions: set[str]) -> dict[str, RegistryEntry]:
"""Create a mock ACTION_REGISTRY with specified deferred actions."""
"""Create a mock ACTION_REGISTRY with specified deferred actions.
Uses the default deferred=True, matching the real registry behavior.
"""
registry: dict[str, RegistryEntry] = {}
for name in deferred_actions:
registry[name] = RegistryEntry(name, lambda: None, None, None, deferred=True)
registry[name] = RegistryEntry(name, lambda: None, None, None)
return registry
@@ -72,10 +75,23 @@ def test_has_deferred_actions_non_deferred(
assert has_deferred_actions([{"logger.log": "hello"}]) is False
def test_has_deferred_actions_unknown(mock_registry: dict[str, RegistryEntry]) -> None:
def test_has_deferred_actions_unknown_not_in_registry(
mock_registry: dict[str, RegistryEntry],
) -> None:
"""Unknown actions not in registry are not flagged (only registered actions count)."""
assert has_deferred_actions([{"unknown.action": "value"}]) is False
def test_has_deferred_actions_default_deferred(
mock_registry: dict[str, RegistryEntry],
) -> None:
"""Actions registered without explicit deferred=False default to deferred=True."""
mock_registry["some.action"] = RegistryEntry(
"some.action", lambda: None, None, None
)
assert has_deferred_actions([{"some.action": "value"}]) is True
def test_has_deferred_actions_nested_in_then(
mock_registry: dict[str, RegistryEntry],
) -> None: