From d21d8977666695d6d3cd363747b08118055a67d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 21:12:31 -0600 Subject: [PATCH] deferred --- esphome/automation.py | 6 +-- esphome/components/api/__init__.py | 2 +- esphome/core/string_ref.h | 2 + tests/unit_tests/test_automation.py | 80 +++++++++++++++-------------- 4 files changed, 47 insertions(+), 43 deletions(-) diff --git a/esphome/automation.py b/esphome/automation.py index 14a716cdff3..99260437112 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -601,7 +601,7 @@ async def build_condition_list( return conditions -def has_deferred_actions(actions: ConfigType) -> bool: +def has_non_synchronous_actions(actions: ConfigType) -> bool: """Check if a validated action list contains any non-synchronous actions. Non-synchronous actions (delay, wait_until, script.wait, etc.) store @@ -609,13 +609,13 @@ def has_deferred_actions(actions: ConfigType) -> bool: unsafe. Actions that haven't been audited default to non-synchronous. """ if isinstance(actions, list): - return any(has_deferred_actions(item) for item in actions) + return any(has_non_synchronous_actions(item) for item in actions) if isinstance(actions, dict): for key in actions: if key in ACTION_REGISTRY and not ACTION_REGISTRY[key].synchronous: return True return any( - has_deferred_actions(v) + has_non_synchronous_actions(v) for v in actions.values() if isinstance(v, (list, dict)) ) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 8d92d219e81..0d60ce1cabb 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -382,7 +382,7 @@ async def to_code(config: ConfigType) -> None: # Check if action chain has non-synchronous actions that would make # non-owning StringRef dangle (rx_buf_ reused after delay) - has_non_synchronous = automation.has_deferred_actions( + has_non_synchronous = automation.has_non_synchronous_actions( conf.get(CONF_THEN, []) ) diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index 60e5fc76b10..3a66f3d9d59 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -84,6 +84,8 @@ class StringRef { /// Write a null terminator at base_[len_] in-place. /// Caller must guarantee that the byte at base_[len_] is writable memory /// (e.g., the RX_BUF_NULL_TERMINATOR byte reserved by frame helpers after decode). + /// Marked const because StringRef itself is not modified; the underlying buffer + /// (owned by frame helper rx_buf_) is mutated via const_cast. void null_terminate_in_place() const { const_cast(base_)[len_] = '\0'; } /// Find first occurrence of substring, returns std::string::npos if not found. diff --git a/tests/unit_tests/test_automation.py b/tests/unit_tests/test_automation.py index 33a8134f788..61fef8201d3 100644 --- a/tests/unit_tests/test_automation.py +++ b/tests/unit_tests/test_automation.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest -from esphome.automation import has_deferred_actions +from esphome.automation import has_non_synchronous_actions from esphome.util import RegistryEntry @@ -33,82 +33,84 @@ def mock_registry() -> Generator[dict[str, RegistryEntry]]: yield registry -def test_has_deferred_actions_empty_list( +def test_has_non_synchronous_actions_empty_list( mock_registry: dict[str, RegistryEntry], ) -> None: - assert has_deferred_actions([]) is False + assert has_non_synchronous_actions([]) is False -def test_has_deferred_actions_empty_dict( +def test_has_non_synchronous_actions_empty_dict( mock_registry: dict[str, RegistryEntry], ) -> None: - assert has_deferred_actions({}) is False + assert has_non_synchronous_actions({}) is False -def test_has_deferred_actions_non_dict_non_list( +def test_has_non_synchronous_actions_non_dict_non_list( mock_registry: dict[str, RegistryEntry], ) -> None: - assert has_deferred_actions("string") is False - assert has_deferred_actions(42) is False - assert has_deferred_actions(None) is False + assert has_non_synchronous_actions("string") is False + assert has_non_synchronous_actions(42) is False + assert has_non_synchronous_actions(None) is False -def test_has_deferred_actions_delay(mock_registry: dict[str, RegistryEntry]) -> None: - assert has_deferred_actions([{"delay": "1s"}]) is True - - -def test_has_deferred_actions_wait_until( +def test_has_non_synchronous_actions_delay( mock_registry: dict[str, RegistryEntry], ) -> None: - assert has_deferred_actions([{"wait_until": {"condition": {}}}]) is True + assert has_non_synchronous_actions([{"delay": "1s"}]) is True -def test_has_deferred_actions_script_wait( +def test_has_non_synchronous_actions_wait_until( mock_registry: dict[str, RegistryEntry], ) -> None: - assert has_deferred_actions([{"script.wait": "script_id"}]) is True + assert has_non_synchronous_actions([{"wait_until": {"condition": {}}}]) is True -def test_has_deferred_actions_non_deferred( +def test_has_non_synchronous_actions_script_wait( mock_registry: dict[str, RegistryEntry], ) -> None: - assert has_deferred_actions([{"logger.log": "hello"}]) is False + assert has_non_synchronous_actions([{"script.wait": "script_id"}]) is True -def test_has_deferred_actions_unknown_not_in_registry( +def test_has_non_synchronous_actions_synchronous( + mock_registry: dict[str, RegistryEntry], +) -> None: + assert has_non_synchronous_actions([{"logger.log": "hello"}]) is False + + +def test_has_non_synchronous_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 + assert has_non_synchronous_actions([{"unknown.action": "value"}]) is False -def test_has_deferred_actions_default_non_synchronous( +def test_has_non_synchronous_actions_default_non_synchronous( mock_registry: dict[str, RegistryEntry], ) -> None: """Actions registered without explicit synchronous=True default to non-synchronous.""" mock_registry["some.action"] = RegistryEntry( "some.action", lambda: None, None, None ) - assert has_deferred_actions([{"some.action": "value"}]) is True + assert has_non_synchronous_actions([{"some.action": "value"}]) is True -def test_has_deferred_actions_nested_in_then( +def test_has_non_synchronous_actions_nested_in_then( mock_registry: dict[str, RegistryEntry], ) -> None: - """Deferred action nested inside a non-deferred action's then block.""" + """Non-synchronous action nested inside a synchronous action's then block.""" actions: list[dict[str, object]] = [ { "logger.log": "first", "then": [{"delay": "1s"}], } ] - assert has_deferred_actions(actions) is True + assert has_non_synchronous_actions(actions) is True -def test_has_deferred_actions_deeply_nested( +def test_has_non_synchronous_actions_deeply_nested( mock_registry: dict[str, RegistryEntry], ) -> None: - """Deferred action deeply nested in action structure.""" + """Non-synchronous action deeply nested in action structure.""" actions: list[dict[str, object]] = [ { "if": { @@ -119,13 +121,13 @@ def test_has_deferred_actions_deeply_nested( } } ] - assert has_deferred_actions(actions) is True + assert has_non_synchronous_actions(actions) is True -def test_has_deferred_actions_no_deferred_in_nested( +def test_has_non_synchronous_actions_none_in_nested( mock_registry: dict[str, RegistryEntry], ) -> None: - """No deferred actions even with nesting.""" + """No non-synchronous actions even with nesting.""" actions: list[dict[str, object]] = [ { "if": { @@ -135,14 +137,14 @@ def test_has_deferred_actions_no_deferred_in_nested( } } ] - assert has_deferred_actions(actions) is False + assert has_non_synchronous_actions(actions) is False -def test_has_deferred_actions_multiple_one_deferred( +def test_has_non_synchronous_actions_multiple_one_non_synchronous( mock_registry: dict[str, RegistryEntry], ) -> None: assert ( - has_deferred_actions( + has_non_synchronous_actions( [ {"logger.log": "first"}, {"delay": "1s"}, @@ -153,11 +155,11 @@ def test_has_deferred_actions_multiple_one_deferred( ) -def test_has_deferred_actions_multiple_none_deferred( +def test_has_non_synchronous_actions_multiple_all_synchronous( mock_registry: dict[str, RegistryEntry], ) -> None: assert ( - has_deferred_actions( + has_non_synchronous_actions( [ {"logger.log": "first"}, {"logger.log": "second"}, @@ -167,9 +169,9 @@ def test_has_deferred_actions_multiple_none_deferred( ) -def test_has_deferred_actions_dict_input( +def test_has_non_synchronous_actions_dict_input( mock_registry: dict[str, RegistryEntry], ) -> None: """Direct dict input (single action).""" - assert has_deferred_actions({"delay": "1s"}) is True - assert has_deferred_actions({"logger.log": "hello"}) is False + assert has_non_synchronous_actions({"delay": "1s"}) is True + assert has_non_synchronous_actions({"logger.log": "hello"}) is False