diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index aba5dc729c..9ef30081aa 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -248,6 +248,9 @@ bool Component::cancel_defer(const std::string &name) { // NOLINT void Component::defer(const std::string &name, std::function &&f) { // NOLINT App.scheduler.set_timeout(this, name, 0, std::move(f)); } +void Component::defer(const char *name, std::function &&f) { // NOLINT + App.scheduler.set_timeout(this, name, 0, std::move(f)); +} void Component::set_timeout(uint32_t timeout, std::function &&f) { // NOLINT App.scheduler.set_timeout(this, "", timeout, std::move(f)); } diff --git a/esphome/core/component.h b/esphome/core/component.h index ab30466e2d..3734473a02 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -380,6 +380,21 @@ class Component { */ void defer(const std::string &name, std::function &&f); // NOLINT + /** Defer a callback to the next loop() call with a const char* name. + * + * IMPORTANT: The provided name pointer must remain valid for the lifetime of the deferred task. + * This means the name should be: + * - A string literal (e.g., "update") + * - A static const char* variable + * - A pointer with lifetime >= the deferred execution + * + * For dynamic strings, use the std::string overload instead. + * + * @param name The name of the defer function (must have static lifetime) + * @param f The callback + */ + void defer(const char *name, std::function &&f); // NOLINT + /// Defer a callback to the next loop() call. void defer(std::function &&f); // NOLINT diff --git a/tests/integration/fixtures/scheduler_string_test.yaml b/tests/integration/fixtures/scheduler_string_test.yaml index 1188577e15..3dfe891370 100644 --- a/tests/integration/fixtures/scheduler_string_test.yaml +++ b/tests/integration/fixtures/scheduler_string_test.yaml @@ -75,20 +75,42 @@ script: App.scheduler.cancel_timeout(component1, "cancel_static_timeout"); ESP_LOGI("test", "Cancelled static timeout using const char*"); + // Test 6 & 7: Test defer with const char* overload using a test component + class TestDeferComponent : public Component { + public: + void test_static_defer() { + // Test 6: Static string literal with defer (const char* overload) + this->defer("static_defer_1", []() { + ESP_LOGI("test", "Static defer 1 fired"); + id(timeout_counter) += 1; + }); + + // Test 7: Static const char* with defer + static const char* DEFER_NAME = "static_defer_2"; + this->defer(DEFER_NAME, []() { + ESP_LOGI("test", "Static defer 2 fired"); + id(timeout_counter) += 1; + }); + } + }; + + static TestDeferComponent test_defer_component; + test_defer_component.test_static_defer(); + - id: test_dynamic_strings then: - logger.log: "Testing dynamic string timeouts and intervals" - lambda: |- auto *component2 = id(test_sensor2); - // Test 6: Dynamic string with set_timeout (std::string) + // Test 8: Dynamic string with set_timeout (std::string) std::string dynamic_name = "dynamic_timeout_" + std::to_string(id(dynamic_counter)++); App.scheduler.set_timeout(component2, dynamic_name, 100, []() { ESP_LOGI("test", "Dynamic timeout fired"); id(timeout_counter) += 1; }); - // Test 7: Dynamic string with set_interval + // Test 9: Dynamic string with set_interval std::string interval_name = "dynamic_interval_" + std::to_string(id(dynamic_counter)++); App.scheduler.set_interval(component2, interval_name, 250, [interval_name]() { ESP_LOGI("test", "Dynamic interval fired: %s", interval_name.c_str()); @@ -99,7 +121,7 @@ script: } }); - // Test 8: Cancel with different string object but same content + // Test 10: Cancel with different string object but same content std::string cancel_name = "cancel_test"; App.scheduler.set_timeout(component2, cancel_name, 2000, []() { ESP_LOGI("test", "This should be cancelled"); @@ -110,6 +132,21 @@ script: App.scheduler.cancel_timeout(component2, cancel_name_2); ESP_LOGI("test", "Cancelled timeout using different string object"); + // Test 11: Dynamic string with defer (using std::string overload) + class TestDynamicDeferComponent : public Component { + public: + void test_dynamic_defer() { + std::string defer_name = "dynamic_defer_" + std::to_string(id(dynamic_counter)++); + this->defer(defer_name, [defer_name]() { + ESP_LOGI("test", "Dynamic defer fired: %s", defer_name.c_str()); + id(timeout_counter) += 1; + }); + } + }; + + static TestDynamicDeferComponent test_dynamic_defer_component; + test_dynamic_defer_component.test_dynamic_defer(); + - id: report_results then: - lambda: |- diff --git a/tests/integration/test_scheduler_string_test.py b/tests/integration/test_scheduler_string_test.py index b5ca07f9db..f3a36b2db7 100644 --- a/tests/integration/test_scheduler_string_test.py +++ b/tests/integration/test_scheduler_string_test.py @@ -26,8 +26,11 @@ async def test_scheduler_string_test( static_interval_cancelled = asyncio.Event() empty_string_timeout_fired = asyncio.Event() static_timeout_cancelled = asyncio.Event() + static_defer_1_fired = asyncio.Event() + static_defer_2_fired = asyncio.Event() dynamic_timeout_fired = asyncio.Event() dynamic_interval_fired = asyncio.Event() + dynamic_defer_fired = asyncio.Event() cancel_test_done = asyncio.Event() final_results_logged = asyncio.Event() @@ -72,6 +75,15 @@ async def test_scheduler_string_test( elif "Cancelled static timeout using const char*" in clean_line: static_timeout_cancelled.set() + # Check for static defer tests + elif "Static defer 1 fired" in clean_line: + static_defer_1_fired.set() + timeout_count += 1 + + elif "Static defer 2 fired" in clean_line: + static_defer_2_fired.set() + timeout_count += 1 + # Check for dynamic string tests elif "Dynamic timeout fired" in clean_line: dynamic_timeout_fired.set() @@ -81,6 +93,11 @@ async def test_scheduler_string_test( dynamic_interval_count += 1 dynamic_interval_fired.set() + # Check for dynamic defer test + elif "Dynamic defer fired" in clean_line: + dynamic_defer_fired.set() + timeout_count += 1 + # Check for cancel test elif "Cancelled timeout using different string object" in clean_line: cancel_test_done.set() @@ -133,6 +150,17 @@ async def test_scheduler_string_test( "Static timeout should have been cancelled" ) + # Wait for static defer tests + try: + await asyncio.wait_for(static_defer_1_fired.wait(), timeout=0.5) + except asyncio.TimeoutError: + pytest.fail("Static defer 1 did not fire within 0.5 seconds") + + try: + await asyncio.wait_for(static_defer_2_fired.wait(), timeout=0.5) + except asyncio.TimeoutError: + pytest.fail("Static defer 2 did not fire within 0.5 seconds") + # Wait for dynamic string tests try: await asyncio.wait_for(dynamic_timeout_fired.wait(), timeout=1.0) @@ -144,6 +172,12 @@ async def test_scheduler_string_test( except asyncio.TimeoutError: pytest.fail("Dynamic interval did not fire within 1.5 seconds") + # Wait for dynamic defer test + try: + await asyncio.wait_for(dynamic_defer_fired.wait(), timeout=1.0) + except asyncio.TimeoutError: + pytest.fail("Dynamic defer did not fire within 1 second") + # Wait for cancel test try: await asyncio.wait_for(cancel_test_done.wait(), timeout=1.0) @@ -157,7 +191,9 @@ async def test_scheduler_string_test( pytest.fail("Final results were not logged within 4 seconds") # Verify results - assert timeout_count >= 3, f"Expected at least 3 timeouts, got {timeout_count}" + assert timeout_count >= 6, ( + f"Expected at least 6 timeouts (including defers), got {timeout_count}" + ) assert interval_count >= 3, ( f"Expected at least 3 interval fires, got {interval_count}" )