diff --git a/esphome/automation.py b/esphome/automation.py index fad344fe1f..366d23e882 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -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, diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 37db181584..e7e96b6da5 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -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]) diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 0a9e289511..4ff3d27ceb 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -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]) diff --git a/esphome/util.py b/esphome/util.py index 9fb7ef6227..c4d82fbd92 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -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) diff --git a/tests/unit_tests/test_automation.py b/tests/unit_tests/test_automation.py index b1170c74bb..98e0b1eea7 100644 --- a/tests/unit_tests/test_automation.py +++ b/tests/unit_tests/test_automation.py @@ -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: