esphome: name: scheduler-string-test on_boot: priority: -100 then: - logger.log: "Starting scheduler string tests" debug_scheduler: true # Enable scheduler debug logging host: api: logger: level: VERBOSE globals: - id: timeout_counter type: int initial_value: '0' - id: interval_counter type: int initial_value: '0' - id: dynamic_counter type: int initial_value: '0' - id: static_tests_done type: bool initial_value: 'false' - id: dynamic_tests_done type: bool initial_value: 'false' - id: results_reported type: bool initial_value: 'false' - id: edge_tests_done type: bool initial_value: 'false' - id: empty_cancel_failed type: bool initial_value: 'false' script: - id: test_static_strings then: - logger.log: "Testing static string timeouts and intervals" - lambda: |- auto *component1 = id(test_sensor1); // Test 1: Static string literals with set_timeout App.scheduler.set_timeout(component1, "static_timeout_1", 50, []() { ESP_LOGI("test", "Static timeout 1 fired"); id(timeout_counter) += 1; }); // Test 2: Static const char* with set_timeout static const char* TIMEOUT_NAME = "static_timeout_2"; App.scheduler.set_timeout(component1, TIMEOUT_NAME, 100, []() { ESP_LOGI("test", "Static timeout 2 fired"); id(timeout_counter) += 1; }); // Test 3: Static string literal with set_interval App.scheduler.set_interval(component1, "static_interval_1", 200, []() { ESP_LOGI("test", "Static interval 1 fired, count: %d", id(interval_counter)); id(interval_counter) += 1; if (id(interval_counter) >= 3) { App.scheduler.cancel_interval(id(test_sensor1), "static_interval_1"); ESP_LOGI("test", "Cancelled static interval 1"); } }); // Test 4: Empty string (should be handled safely) App.scheduler.set_timeout(component1, "", 150, []() { ESP_LOGI("test", "Empty string timeout fired"); }); // Test 5: Cancel timeout with const char* literal App.scheduler.set_timeout(component1, "cancel_static_timeout", 5000, []() { ESP_LOGI("test", "This static timeout should be cancelled"); }); // Cancel using const char* directly 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 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 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()); id(interval_counter) += 1; if (id(interval_counter) >= 6) { App.scheduler.cancel_interval(id(test_sensor2), interval_name); ESP_LOGI("test", "Cancelled dynamic interval"); } }); // 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"); }); // Cancel using a different string object std::string cancel_name_2 = "cancel_test"; 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: test_cancellation_edge_cases then: - logger.log: "Testing cancellation edge cases" - lambda: |- auto *component1 = id(test_sensor1); // Use a different component for empty string tests to avoid interference auto *component2 = id(test_sensor2); // Test 12: Cancel with empty string - regression test for issue #9599 // First create a timeout with empty name on component2 to avoid interference App.scheduler.set_timeout(component2, "", 500, []() { ESP_LOGE("test", "ERROR: Empty name timeout fired - it should have been cancelled!"); id(empty_cancel_failed) = true; }); // Now cancel it - this should work after our fix bool cancelled_empty = App.scheduler.cancel_timeout(component2, ""); ESP_LOGI("test", "Cancel empty string result: %s (should be true)", cancelled_empty ? "true" : "false"); if (!cancelled_empty) { ESP_LOGE("test", "ERROR: Failed to cancel empty string timeout!"); id(empty_cancel_failed) = true; } // Test 13: Cancel non-existent timeout bool cancelled_nonexistent = App.scheduler.cancel_timeout(component1, "does_not_exist"); ESP_LOGI("test", "Cancel non-existent timeout result: %s", cancelled_nonexistent ? "true (unexpected!)" : "false (expected)"); // Test 14: Multiple timeouts with same name - only last should execute for (int i = 0; i < 5; i++) { App.scheduler.set_timeout(component1, "duplicate_timeout", 200 + i*10, [i]() { ESP_LOGI("test", "Duplicate timeout %d fired", i); id(timeout_counter) += 1; }); } ESP_LOGI("test", "Created 5 timeouts with same name 'duplicate_timeout'"); // Test 15: Multiple intervals with same name - only last should run for (int i = 0; i < 3; i++) { App.scheduler.set_interval(component1, "duplicate_interval", 300, [i]() { ESP_LOGI("test", "Duplicate interval %d fired", i); id(interval_counter) += 10; // Large increment to detect multiple // Cancel after first execution App.scheduler.cancel_interval(id(test_sensor1), "duplicate_interval"); }); } ESP_LOGI("test", "Created 3 intervals with same name 'duplicate_interval'"); // Test 16: Cancel with nullptr protection (via empty const char*) const char* null_name = ""; App.scheduler.set_timeout(component2, null_name, 600, []() { ESP_LOGE("test", "ERROR: Const char* empty timeout fired - should have been cancelled!"); id(empty_cancel_failed) = true; }); bool cancelled_const_empty = App.scheduler.cancel_timeout(component2, null_name); ESP_LOGI("test", "Cancel const char* empty result: %s (should be true)", cancelled_const_empty ? "true" : "false"); if (!cancelled_const_empty) { ESP_LOGE("test", "ERROR: Failed to cancel const char* empty timeout!"); id(empty_cancel_failed) = true; } // Test 17: Rapid create/cancel/create with same name App.scheduler.set_timeout(component1, "rapid_test", 5000, []() { ESP_LOGI("test", "First rapid timeout - should not fire"); id(timeout_counter) += 100; }); App.scheduler.cancel_timeout(component1, "rapid_test"); App.scheduler.set_timeout(component1, "rapid_test", 250, []() { ESP_LOGI("test", "Second rapid timeout - should fire"); id(timeout_counter) += 1; }); // Test 18: Cancel all with a specific name (multiple instances) // Create multiple with same name App.scheduler.set_timeout(component1, "multi_cancel", 300, []() { ESP_LOGI("test", "Multi-cancel timeout 1"); }); App.scheduler.set_timeout(component1, "multi_cancel", 350, []() { ESP_LOGI("test", "Multi-cancel timeout 2"); }); App.scheduler.set_timeout(component1, "multi_cancel", 400, []() { ESP_LOGI("test", "Multi-cancel timeout 3 - only this should fire"); id(timeout_counter) += 1; }); // Note: Each set_timeout with same name cancels the previous one automatically - id: report_results then: - lambda: |- ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d", id(timeout_counter), id(interval_counter)); // Check if empty string cancellation test passed if (id(empty_cancel_failed)) { ESP_LOGE("test", "ERROR: Empty string cancellation test FAILED!"); } else { ESP_LOGI("test", "Empty string cancellation test PASSED"); } sensor: - platform: template name: Test Sensor 1 id: test_sensor1 lambda: return 1.0; update_interval: never - platform: template name: Test Sensor 2 id: test_sensor2 lambda: return 2.0; update_interval: never interval: # Run static string tests after boot - using script to run once - interval: 0.1s then: - if: condition: lambda: 'return id(static_tests_done) == false;' then: - lambda: 'id(static_tests_done) = true;' - script.execute: test_static_strings - logger.log: "Started static string tests" # Run dynamic string tests after static tests - interval: 0.2s then: - if: condition: lambda: 'return id(static_tests_done) && !id(dynamic_tests_done);' then: - lambda: 'id(dynamic_tests_done) = true;' - delay: 0.2s - script.execute: test_dynamic_strings # Run cancellation edge case tests after dynamic tests - interval: 0.2s then: - if: condition: lambda: 'return id(dynamic_tests_done) && !id(edge_tests_done);' then: - lambda: 'id(edge_tests_done) = true;' - delay: 0.5s - script.execute: test_cancellation_edge_cases # Report results after all tests - interval: 0.2s then: - if: condition: lambda: 'return id(edge_tests_done) && !id(results_reported);' then: - lambda: 'id(results_reported) = true;' - delay: 1s - script.execute: report_results