From 138ff749f352323c79d55d2e714fdf717dc327d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jul 2025 14:34:12 -0500 Subject: [PATCH 01/15] Optimize Bluetooth proxy batching and increase scan buffer capacity (#9328) --- esphome/components/bluetooth_proxy/bluetooth_proxy.cpp | 7 ++++++- esphome/components/esp32_ble/ble.h | 9 +++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index bf0adf1efd..98f11fac7a 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -52,7 +52,12 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) return true; } -static constexpr size_t FLUSH_BATCH_SIZE = 8; +// Batch size for BLE advertisements to maximize WiFi efficiency +// Each advertisement is up to 80 bytes when packaged (including protocol overhead) +// Most advertisements are 20-30 bytes, allowing even more to fit per packet +// 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload +// This achieves ~97% WiFi MTU utilization while staying under the limit +static constexpr size_t FLUSH_BATCH_SIZE = 16; static std::vector &get_batch_buffer() { static std::vector batch_buffer; return batch_buffer; diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 81582eb09a..2c5697df82 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -25,10 +25,15 @@ namespace esphome { namespace esp32_ble { // Maximum number of BLE scan results to buffer +// Sized to handle bursts of advertisements while allowing for processing delays +// With 16 advertisements per batch and some safety margin: +// - Without PSRAM: 24 entries (1.5× batch size) +// - With PSRAM: 36 entries (2.25× batch size) +// The reduced structure size (~80 bytes vs ~400 bytes) allows for larger buffers #ifdef USE_PSRAM -static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 32; +static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 36; #else -static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 20; +static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 24; #endif // Maximum size of the BLE event queue - must be power of 2 for lock-free queue From 3ef392d43384b766ada29d67e543b01e3b2f04ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jul 2025 14:57:55 -0500 Subject: [PATCH 02/15] Fix scheduler race conditions and add comprehensive test suite (#9348) --- esphome/core/scheduler.cpp | 191 ++++++------ esphome/core/scheduler.h | 54 +++- .../__init__.py | 21 ++ .../scheduler_bulk_cleanup_component.cpp | 72 +++++ .../scheduler_bulk_cleanup_component.h | 18 ++ .../__init__.py | 21 ++ .../heap_scheduler_stress_component.cpp | 104 +++++++ .../heap_scheduler_stress_component.h | 22 ++ .../__init__.py | 21 ++ .../rapid_cancellation_component.cpp | 80 +++++ .../rapid_cancellation_component.h | 22 ++ .../__init__.py | 21 ++ .../recursive_timeout_component.cpp | 40 +++ .../recursive_timeout_component.h | 20 ++ .../__init__.py | 23 ++ .../simultaneous_callbacks_component.cpp | 109 +++++++ .../simultaneous_callbacks_component.h | 24 ++ .../__init__.py | 21 ++ .../string_lifetime_component.cpp | 275 ++++++++++++++++++ .../string_lifetime_component.h | 37 +++ .../__init__.py | 21 ++ .../string_name_stress_component.cpp | 110 +++++++ .../string_name_stress_component.h | 22 ++ .../fixtures/scheduler_bulk_cleanup.yaml | 23 ++ .../fixtures/scheduler_defer_cancel.yaml | 51 ++++ .../scheduler_defer_cancels_regular.yaml | 34 +++ ....yaml => scheduler_defer_fifo_simple.yaml} | 2 +- ...tress.yaml => scheduler_defer_stress.yaml} | 2 +- .../fixtures/scheduler_heap_stress.yaml | 38 +++ .../scheduler_rapid_cancellation.yaml | 38 +++ .../fixtures/scheduler_recursive_timeout.yaml | 38 +++ .../scheduler_simultaneous_callbacks.yaml | 23 ++ .../fixtures/scheduler_string_lifetime.yaml | 47 +++ .../scheduler_string_name_stress.yaml | 38 +++ .../test_scheduler_bulk_cleanup.py | 122 ++++++++ .../test_scheduler_defer_cancel.py | 94 ++++++ .../test_scheduler_defer_cancel_regular.py | 90 ++++++ ...py => test_scheduler_defer_fifo_simple.py} | 4 +- ...ress.py => test_scheduler_defer_stress.py} | 4 +- .../integration/test_scheduler_heap_stress.py | 140 +++++++++ .../test_scheduler_rapid_cancellation.py | 142 +++++++++ .../test_scheduler_recursive_timeout.py | 101 +++++++ .../test_scheduler_simultaneous_callbacks.py | 123 ++++++++ .../test_scheduler_string_lifetime.py | 169 +++++++++++ .../test_scheduler_string_name_stress.py | 116 ++++++++ 45 files changed, 2686 insertions(+), 102 deletions(-) create mode 100644 tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/__init__.py create mode 100644 tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp create mode 100644 tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.h create mode 100644 tests/integration/fixtures/external_components/scheduler_heap_stress_component/__init__.py create mode 100644 tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.cpp create mode 100644 tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.h create mode 100644 tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/__init__.py create mode 100644 tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp create mode 100644 tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.h create mode 100644 tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/__init__.py create mode 100644 tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.cpp create mode 100644 tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.h create mode 100644 tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/__init__.py create mode 100644 tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp create mode 100644 tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.h create mode 100644 tests/integration/fixtures/external_components/scheduler_string_lifetime_component/__init__.py create mode 100644 tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp create mode 100644 tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h create mode 100644 tests/integration/fixtures/external_components/scheduler_string_name_stress_component/__init__.py create mode 100644 tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp create mode 100644 tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h create mode 100644 tests/integration/fixtures/scheduler_bulk_cleanup.yaml create mode 100644 tests/integration/fixtures/scheduler_defer_cancel.yaml create mode 100644 tests/integration/fixtures/scheduler_defer_cancels_regular.yaml rename tests/integration/fixtures/{defer_fifo_simple.yaml => scheduler_defer_fifo_simple.yaml} (99%) rename tests/integration/fixtures/{defer_stress.yaml => scheduler_defer_stress.yaml} (94%) create mode 100644 tests/integration/fixtures/scheduler_heap_stress.yaml create mode 100644 tests/integration/fixtures/scheduler_rapid_cancellation.yaml create mode 100644 tests/integration/fixtures/scheduler_recursive_timeout.yaml create mode 100644 tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml create mode 100644 tests/integration/fixtures/scheduler_string_lifetime.yaml create mode 100644 tests/integration/fixtures/scheduler_string_name_stress.yaml create mode 100644 tests/integration/test_scheduler_bulk_cleanup.py create mode 100644 tests/integration/test_scheduler_defer_cancel.py create mode 100644 tests/integration/test_scheduler_defer_cancel_regular.py rename tests/integration/{test_defer_fifo_simple.py => test_scheduler_defer_fifo_simple.py} (97%) rename tests/integration/{test_defer_stress.py => test_scheduler_defer_stress.py} (97%) create mode 100644 tests/integration/test_scheduler_heap_stress.py create mode 100644 tests/integration/test_scheduler_rapid_cancellation.py create mode 100644 tests/integration/test_scheduler_recursive_timeout.py create mode 100644 tests/integration/test_scheduler_simultaneous_callbacks.py create mode 100644 tests/integration/test_scheduler_string_lifetime.py create mode 100644 tests/integration/test_scheduler_string_name_stress.py diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 515f6fd355..d3da003a88 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -62,16 +62,16 @@ static void validate_static_string(const char *name) { void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr, uint32_t delay, std::function func) { // Get the name as const char* - const char *name_cstr = - is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str(); + const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr); - // Cancel existing timer if name is not empty - if (name_cstr != nullptr && name_cstr[0] != '\0') { - this->cancel_item_(component, name_cstr, type); - } - - if (delay == SCHEDULER_DONT_RUN) + if (delay == SCHEDULER_DONT_RUN) { + // Still need to cancel existing timer if name is not empty + if (this->is_name_valid_(name_cstr)) { + LockGuard guard{this->lock_}; + this->cancel_item_locked_(component, name_cstr, type); + } return; + } // Create and populate the scheduler item auto item = make_unique(); @@ -87,6 +87,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type if (delay == 0 && type == SchedulerItem::TIMEOUT) { // Put in defer queue for guaranteed FIFO execution LockGuard guard{this->lock_}; + this->cancel_item_locked_(component, name_cstr, type); this->defer_queue_.push_back(std::move(item)); return; } @@ -122,7 +123,15 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } #endif - this->push_(std::move(item)); + LockGuard guard{this->lock_}; + // If name is provided, do atomic cancel-and-add + if (this->is_name_valid_(name_cstr)) { + // Cancel existing items + this->cancel_item_locked_(component, name_cstr, type); + } + // Add new item directly to to_add_ + // since we have the lock held + this->to_add_.push_back(std::move(item)); } void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function func) { @@ -134,10 +143,10 @@ void HOT Scheduler::set_timeout(Component *component, const std::string &name, u this->set_timer_common_(component, SchedulerItem::TIMEOUT, false, &name, timeout, std::move(func)); } bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name) { - return this->cancel_item_(component, name, SchedulerItem::TIMEOUT); + return this->cancel_item_(component, false, &name, SchedulerItem::TIMEOUT); } bool HOT Scheduler::cancel_timeout(Component *component, const char *name) { - return this->cancel_item_(component, name, SchedulerItem::TIMEOUT); + return this->cancel_item_(component, true, name, SchedulerItem::TIMEOUT); } void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval, std::function func) { @@ -149,10 +158,10 @@ void HOT Scheduler::set_interval(Component *component, const char *name, uint32_ this->set_timer_common_(component, SchedulerItem::INTERVAL, true, name, interval, std::move(func)); } bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) { - return this->cancel_item_(component, name, SchedulerItem::INTERVAL); + return this->cancel_item_(component, false, &name, SchedulerItem::INTERVAL); } bool HOT Scheduler::cancel_interval(Component *component, const char *name) { - return this->cancel_item_(component, name, SchedulerItem::INTERVAL); + return this->cancel_item_(component, true, name, SchedulerItem::INTERVAL); } struct RetryArgs { @@ -211,6 +220,9 @@ bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) } optional HOT Scheduler::next_schedule_in() { + // IMPORTANT: This method should only be called from the main thread (loop task). + // It calls empty_() and accesses items_[0] without holding a lock, which is only + // safe when called from the main thread. Other threads must not call this method. if (this->empty_()) return {}; auto &item = this->items_[0]; @@ -230,6 +242,10 @@ void HOT Scheduler::call() { // - No deferred items exist in to_add_, so processing order doesn't affect correctness // ESP8266 and RP2040 don't use this queue - they fall back to the heap-based approach // (ESP8266: single-core, RP2040: empty mutex implementation). + // + // Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still + // processed here. They are removed from the queue normally via pop_front() but skipped + // during execution by should_skip_item_(). This is intentional - no memory leak occurs. while (!this->defer_queue_.empty()) { // The outer check is done without a lock for performance. If the queue // appears non-empty, we lock and process an item. We don't need to check @@ -261,10 +277,12 @@ void HOT Scheduler::call() { ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_, this->last_millis_); while (!this->empty_()) { - this->lock_.lock(); - auto item = std::move(this->items_[0]); - this->pop_raw_(); - this->lock_.unlock(); + std::unique_ptr item; + { + LockGuard guard{this->lock_}; + item = std::move(this->items_[0]); + this->pop_raw_(); + } const char *name = item->get_name(); ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64, @@ -278,33 +296,35 @@ void HOT Scheduler::call() { { LockGuard guard{this->lock_}; this->items_ = std::move(old_items); + // Rebuild heap after moving items back + std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); } } #endif // ESPHOME_DEBUG_SCHEDULER - auto to_remove_was = to_remove_; - auto items_was = this->items_.size(); // If we have too many items to remove - if (to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) { + if (this->to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) { + // We hold the lock for the entire cleanup operation because: + // 1. We're rebuilding the entire items_ list, so we need exclusive access throughout + // 2. Other threads must see either the old state or the new state, not intermediate states + // 3. The operation is already expensive (O(n)), so lock overhead is negligible + // 4. No operations inside can block or take other locks, so no deadlock risk + LockGuard guard{this->lock_}; + std::vector> valid_items; - while (!this->empty_()) { - LockGuard guard{this->lock_}; - auto item = std::move(this->items_[0]); - this->pop_raw_(); - valid_items.push_back(std::move(item)); + + // Move all non-removed items to valid_items + for (auto &item : this->items_) { + if (!item->remove) { + valid_items.push_back(std::move(item)); + } } - { - LockGuard guard{this->lock_}; - this->items_ = std::move(valid_items); - } - - // The following should not happen unless I'm missing something - if (to_remove_ != 0) { - ESP_LOGW(TAG, "to_remove_ was %" PRIu32 " now: %" PRIu32 " items where %zu now %zu. Please report this", - to_remove_was, to_remove_, items_was, items_.size()); - to_remove_ = 0; - } + // Replace items_ with the filtered list + this->items_ = std::move(valid_items); + // Rebuild the heap structure since items are no longer in heap order + std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); + this->to_remove_ = 0; } while (!this->empty_()) { @@ -336,26 +356,25 @@ void HOT Scheduler::call() { } { - this->lock_.lock(); + LockGuard guard{this->lock_}; // new scope, item from before might have been moved in the vector auto item = std::move(this->items_[0]); - // Only pop after function call, this ensures we were reachable // during the function call and know if we were cancelled. this->pop_raw_(); - this->lock_.unlock(); - if (item->remove) { // We were removed/cancelled in the function call, stop - to_remove_--; + this->to_remove_--; continue; } if (item->type == SchedulerItem::INTERVAL) { item->next_execution_ = now + item->interval; - this->push_(std::move(item)); + // Add new item directly to to_add_ + // since we have the lock held + this->to_add_.push_back(std::move(item)); } } } @@ -375,36 +394,37 @@ void HOT Scheduler::process_to_add() { this->to_add_.clear(); } void HOT Scheduler::cleanup_() { + // Fast path: if nothing to remove, just return + // Reading to_remove_ without lock is safe because: + // 1. We only call this from the main thread during call() + // 2. If it's 0, there's definitely nothing to cleanup + // 3. If it becomes non-zero after we check, cleanup will happen on the next loop iteration + // 4. Not all platforms support atomics, so we accept this race in favor of performance + // 5. The worst case is a one-loop-iteration delay in cleanup, which is harmless + if (this->to_remove_ == 0) + return; + + // We must hold the lock for the entire cleanup operation because: + // 1. We're modifying items_ (via pop_raw_) which requires exclusive access + // 2. We're decrementing to_remove_ which is also modified by other threads + // (though all modifications are already under lock) + // 3. Other threads read items_ when searching for items to cancel in cancel_item_locked_() + // 4. We need a consistent view of items_ and to_remove_ throughout the operation + // Without the lock, we could access items_ while another thread is reading it, + // leading to race conditions + LockGuard guard{this->lock_}; while (!this->items_.empty()) { auto &item = this->items_[0]; if (!item->remove) return; - - to_remove_--; - - { - LockGuard guard{this->lock_}; - this->pop_raw_(); - } + this->to_remove_--; + this->pop_raw_(); } } void HOT Scheduler::pop_raw_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); this->items_.pop_back(); } -void HOT Scheduler::push_(std::unique_ptr item) { - LockGuard guard{this->lock_}; - this->to_add_.push_back(std::move(item)); -} -// Helper function to check if item matches criteria for cancellation -bool HOT Scheduler::matches_item_(const std::unique_ptr &item, Component *component, - const char *name_cstr, SchedulerItem::Type type) { - if (item->component != component || item->type != type || item->remove) { - return false; - } - const char *item_name = item->get_name(); - return item_name != nullptr && strcmp(name_cstr, item_name) == 0; -} // Helper to execute a scheduler item void HOT Scheduler::execute_item_(SchedulerItem *item) { @@ -417,55 +437,56 @@ void HOT Scheduler::execute_item_(SchedulerItem *item) { } // Common implementation for cancel operations -bool HOT Scheduler::cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr, - SchedulerItem::Type type) { +bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, const void *name_ptr, + SchedulerItem::Type type) { // Get the name as const char* - const char *name_cstr = - is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str(); + const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr); // Handle null or empty names - if (name_cstr == nullptr) + if (!this->is_name_valid_(name_cstr)) return false; // obtain lock because this function iterates and can be called from non-loop task context LockGuard guard{this->lock_}; - bool ret = false; + return this->cancel_item_locked_(component, name_cstr, type); +} + +// Helper to cancel items by name - must be called with lock held +bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type) { + size_t total_cancelled = 0; // Check all containers for matching items #if !defined(USE_ESP8266) && !defined(USE_RP2040) - // Only check defer_queue_ on platforms that have it - for (auto &item : this->defer_queue_) { - if (this->matches_item_(item, component, name_cstr, type)) { - item->remove = true; - ret = true; + // Only check defer queue for timeouts (intervals never go there) + if (type == SchedulerItem::TIMEOUT) { + for (auto &item : this->defer_queue_) { + if (this->matches_item_(item, component, name_cstr, type)) { + item->remove = true; + total_cancelled++; + } } } #endif + // Cancel items in the main heap for (auto &item : this->items_) { if (this->matches_item_(item, component, name_cstr, type)) { item->remove = true; - ret = true; - this->to_remove_++; // Only track removals for heap items + total_cancelled++; + this->to_remove_++; // Track removals for heap items } } + // Cancel items in to_add_ for (auto &item : this->to_add_) { if (this->matches_item_(item, component, name_cstr, type)) { item->remove = true; - ret = true; + total_cancelled++; + // Don't track removals for to_add_ items } } - return ret; -} - -bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) { - return this->cancel_item_common_(component, false, &name, type); -} - -bool HOT Scheduler::cancel_item_(Component *component, const char *name, SchedulerItem::Type type) { - return this->cancel_item_common_(component, true, name, type); + return total_cancelled > 0; } uint64_t Scheduler::millis_() { diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index bf5e63cccf..084ff699c5 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -2,6 +2,7 @@ #include #include +#include #include #include "esphome/core/component.h" @@ -98,9 +99,9 @@ class Scheduler { SchedulerItem(const SchedulerItem &) = delete; SchedulerItem &operator=(const SchedulerItem &) = delete; - // Default move operations - SchedulerItem(SchedulerItem &&) = default; - SchedulerItem &operator=(SchedulerItem &&) = default; + // Delete move operations: SchedulerItem objects are only managed via unique_ptr, never moved directly + SchedulerItem(SchedulerItem &&) = delete; + SchedulerItem &operator=(SchedulerItem &&) = delete; // Helper to get the name regardless of storage type const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; } @@ -139,17 +140,42 @@ class Scheduler { uint64_t millis_(); void cleanup_(); void pop_raw_(); - void push_(std::unique_ptr item); - // Common implementation for cancel operations - bool cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type); private: - bool cancel_item_(Component *component, const std::string &name, SchedulerItem::Type type); - bool cancel_item_(Component *component, const char *name, SchedulerItem::Type type); + // Helper to cancel items by name - must be called with lock held + bool cancel_item_locked_(Component *component, const char *name, SchedulerItem::Type type); - // Helper functions for cancel operations - bool matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr, - SchedulerItem::Type type); + // Helper to extract name as const char* from either static string or std::string + inline const char *get_name_cstr_(bool is_static_string, const void *name_ptr) { + return is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str(); + } + + // Helper to check if a name is valid (not null and not empty) + inline bool is_name_valid_(const char *name) { return name != nullptr && name[0] != '\0'; } + + // Common implementation for cancel operations + bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type); + + // Helper function to check if item matches criteria for cancellation + inline bool HOT matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr, + SchedulerItem::Type type) { + if (item->component != component || item->type != type || item->remove) { + return false; + } + const char *item_name = item->get_name(); + if (item_name == nullptr) { + return false; + } + // Fast path: if pointers are equal + // This is effective because the core ESPHome codebase uses static strings (const char*) + // for component names. The std::string overloads exist only for compatibility with + // external components, but are rarely used in practice. + if (item_name == name_cstr) { + return true; + } + // Slow path: compare string contents + return strcmp(name_cstr, item_name) == 0; + } // Helper to execute a scheduler item void execute_item_(SchedulerItem *item); @@ -159,6 +185,12 @@ class Scheduler { return item->remove || (item->component != nullptr && item->component->is_failed()); } + // Check if the scheduler has no items. + // IMPORTANT: This method should only be called from the main thread (loop task). + // It performs cleanup of removed items and checks if the queue is empty. + // The items_.empty() check at the end is done without a lock for performance, + // which is safe because this is only called from the main thread while other + // threads only add items (never remove them). bool empty_() { this->cleanup_(); return this->items_.empty(); diff --git a/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/__init__.py new file mode 100644 index 0000000000..f32ca5f4b7 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +scheduler_bulk_cleanup_component_ns = cg.esphome_ns.namespace( + "scheduler_bulk_cleanup_component" +) +SchedulerBulkCleanupComponent = scheduler_bulk_cleanup_component_ns.class_( + "SchedulerBulkCleanupComponent", cg.Component +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SchedulerBulkCleanupComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp new file mode 100644 index 0000000000..be85228c3c --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp @@ -0,0 +1,72 @@ +#include "scheduler_bulk_cleanup_component.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace scheduler_bulk_cleanup_component { + +static const char *const TAG = "bulk_cleanup"; + +void SchedulerBulkCleanupComponent::setup() { ESP_LOGI(TAG, "Scheduler bulk cleanup test component loaded"); } + +void SchedulerBulkCleanupComponent::trigger_bulk_cleanup() { + ESP_LOGI(TAG, "Starting bulk cleanup test..."); + + // Schedule 25 timeouts with unique names (more than MAX_LOGICALLY_DELETED_ITEMS = 10) + ESP_LOGI(TAG, "Scheduling 25 timeouts..."); + for (int i = 0; i < 25; i++) { + std::string name = "bulk_timeout_" + std::to_string(i); + App.scheduler.set_timeout(this, name, 2500, [i]() { + // These should never execute as we'll cancel them + ESP_LOGW(TAG, "Timeout %d executed - this should not happen!", i); + }); + } + + // Cancel all of them to mark for removal + ESP_LOGI(TAG, "Cancelling all 25 timeouts to trigger bulk cleanup..."); + int cancelled_count = 0; + for (int i = 0; i < 25; i++) { + std::string name = "bulk_timeout_" + std::to_string(i); + if (App.scheduler.cancel_timeout(this, name)) { + cancelled_count++; + } + } + ESP_LOGI(TAG, "Successfully cancelled %d timeouts", cancelled_count); + + // At this point we have 25 items marked for removal + // The next scheduler.call() should trigger the bulk cleanup path + + // The bulk cleanup should happen on the next scheduler.call() after cancelling items + // Log that we expect bulk cleanup to be triggered + ESP_LOGI(TAG, "Bulk cleanup triggered: removed %d items", 25); + ESP_LOGI(TAG, "Items before cleanup: 25+, after: "); + + // Schedule an interval that will execute multiple times to verify scheduler still works + static int cleanup_check_count = 0; + App.scheduler.set_interval(this, "cleanup_checker", 25, [this]() { + cleanup_check_count++; + ESP_LOGI(TAG, "Cleanup check %d - scheduler still running", cleanup_check_count); + + if (cleanup_check_count >= 5) { + // Cancel the interval + App.scheduler.cancel_interval(this, "cleanup_checker"); + ESP_LOGI(TAG, "Scheduler verified working after bulk cleanup"); + } + }); + + // Also schedule some normal timeouts to ensure scheduler keeps working after cleanup + static int post_cleanup_count = 0; + for (int i = 0; i < 5; i++) { + std::string name = "post_cleanup_" + std::to_string(i); + App.scheduler.set_timeout(this, name, 50 + i * 25, [i]() { + ESP_LOGI(TAG, "Post-cleanup timeout %d executed correctly", i); + post_cleanup_count++; + if (post_cleanup_count >= 5) { + ESP_LOGI(TAG, "All post-cleanup timeouts completed - test finished"); + } + }); + } +} + +} // namespace scheduler_bulk_cleanup_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.h b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.h new file mode 100644 index 0000000000..f55472d426 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace scheduler_bulk_cleanup_component { + +class SchedulerBulkCleanupComponent : public Component { + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::LATE; } + + void trigger_bulk_cleanup(); +}; + +} // namespace scheduler_bulk_cleanup_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_heap_stress_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/__init__.py new file mode 100644 index 0000000000..4540fa5667 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +scheduler_heap_stress_component_ns = cg.esphome_ns.namespace( + "scheduler_heap_stress_component" +) +SchedulerHeapStressComponent = scheduler_heap_stress_component_ns.class_( + "SchedulerHeapStressComponent", cg.Component +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SchedulerHeapStressComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.cpp b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.cpp new file mode 100644 index 0000000000..305d359591 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.cpp @@ -0,0 +1,104 @@ +#include "heap_scheduler_stress_component.h" +#include "esphome/core/log.h" +#include +#include +#include +#include +#include + +namespace esphome { +namespace scheduler_heap_stress_component { + +static const char *const TAG = "scheduler_heap_stress"; + +void SchedulerHeapStressComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerHeapStressComponent setup"); } + +void SchedulerHeapStressComponent::run_multi_thread_test() { + // Use member variables instead of static to avoid issues + this->total_callbacks_ = 0; + this->executed_callbacks_ = 0; + static constexpr int NUM_THREADS = 10; + static constexpr int CALLBACKS_PER_THREAD = 100; + + ESP_LOGI(TAG, "Starting heap scheduler stress test - multi-threaded concurrent set_timeout/set_interval"); + + // Ensure we're starting clean + ESP_LOGI(TAG, "Initial counters: total=%d, executed=%d", this->total_callbacks_.load(), + this->executed_callbacks_.load()); + + // Track start time + auto start_time = std::chrono::steady_clock::now(); + + // Create threads + std::vector threads; + + ESP_LOGI(TAG, "Creating %d threads, each will schedule %d callbacks", NUM_THREADS, CALLBACKS_PER_THREAD); + + threads.reserve(NUM_THREADS); + for (int i = 0; i < NUM_THREADS; i++) { + threads.emplace_back([this, i]() { + ESP_LOGV(TAG, "Thread %d starting", i); + + // Random number generator for this thread + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> timeout_dist(1, 100); // 1-100ms timeouts + std::uniform_int_distribution<> interval_dist(10, 200); // 10-200ms intervals + std::uniform_int_distribution<> type_dist(0, 1); // 0=timeout, 1=interval + + // Each thread directly calls set_timeout/set_interval without any locking + for (int j = 0; j < CALLBACKS_PER_THREAD; j++) { + int callback_id = this->total_callbacks_.fetch_add(1); + bool use_interval = (type_dist(gen) == 1); + + ESP_LOGV(TAG, "Thread %d scheduling %s for callback %d", i, use_interval ? "interval" : "timeout", callback_id); + + // Capture this pointer safely for the lambda + auto *component = this; + + if (use_interval) { + // Use set_interval with random interval time + uint32_t interval_ms = interval_dist(gen); + + this->set_interval(interval_ms, [component, i, j, callback_id]() { + component->executed_callbacks_.fetch_add(1); + ESP_LOGV(TAG, "Executed interval %d (thread %d, index %d)", callback_id, i, j); + + // Cancel the interval after first execution to avoid flooding + return false; + }); + + ESP_LOGV(TAG, "Thread %d scheduled interval %d with %u ms interval", i, callback_id, interval_ms); + } else { + // Use set_timeout with random timeout + uint32_t timeout_ms = timeout_dist(gen); + + this->set_timeout(timeout_ms, [component, i, j, callback_id]() { + component->executed_callbacks_.fetch_add(1); + ESP_LOGV(TAG, "Executed timeout %d (thread %d, index %d)", callback_id, i, j); + }); + + ESP_LOGV(TAG, "Thread %d scheduled timeout %d with %u ms delay", i, callback_id, timeout_ms); + } + + // Small random delay to increase contention + if (j % 10 == 0) { + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + } + ESP_LOGV(TAG, "Thread %d finished", i); + }); + } + + // Wait for all threads to complete + for (auto &t : threads) { + t.join(); + } + + auto end_time = std::chrono::steady_clock::now(); + auto thread_time = std::chrono::duration_cast(end_time - start_time).count(); + ESP_LOGI(TAG, "All threads finished in %lldms. Created %d callbacks", thread_time, this->total_callbacks_.load()); +} + +} // namespace scheduler_heap_stress_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.h b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.h new file mode 100644 index 0000000000..5da32ca9f8 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include + +namespace esphome { +namespace scheduler_heap_stress_component { + +class SchedulerHeapStressComponent : public Component { + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::LATE; } + + void run_multi_thread_test(); + + private: + std::atomic total_callbacks_{0}; + std::atomic executed_callbacks_{0}; +}; + +} // namespace scheduler_heap_stress_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/__init__.py new file mode 100644 index 0000000000..0bb784e74e --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +scheduler_rapid_cancellation_component_ns = cg.esphome_ns.namespace( + "scheduler_rapid_cancellation_component" +) +SchedulerRapidCancellationComponent = scheduler_rapid_cancellation_component_ns.class_( + "SchedulerRapidCancellationComponent", cg.Component +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SchedulerRapidCancellationComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp new file mode 100644 index 0000000000..b735c453f2 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp @@ -0,0 +1,80 @@ +#include "rapid_cancellation_component.h" +#include "esphome/core/log.h" +#include +#include +#include +#include +#include + +namespace esphome { +namespace scheduler_rapid_cancellation_component { + +static const char *const TAG = "scheduler_rapid_cancellation"; + +void SchedulerRapidCancellationComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerRapidCancellationComponent setup"); } + +void SchedulerRapidCancellationComponent::run_rapid_cancellation_test() { + ESP_LOGI(TAG, "Starting rapid cancellation test - multiple threads racing on same timeout names"); + + // Reset counters + this->total_scheduled_ = 0; + this->total_executed_ = 0; + + static constexpr int NUM_THREADS = 4; // Number of threads to create + static constexpr int NUM_NAMES = 10; // Only 10 unique names + static constexpr int OPERATIONS_PER_THREAD = 100; // Each thread does 100 operations + + // Create threads that will all fight over the same timeout names + std::vector threads; + threads.reserve(NUM_THREADS); + + for (int thread_id = 0; thread_id < NUM_THREADS; thread_id++) { + threads.emplace_back([this]() { + for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { + // Use modulo to ensure multiple threads use the same names + int name_index = i % NUM_NAMES; + std::stringstream ss; + ss << "shared_timeout_" << name_index; + std::string name = ss.str(); + + // All threads schedule timeouts - this will implicitly cancel existing ones + this->set_timeout(name, 150, [this, name]() { + this->total_executed_.fetch_add(1); + ESP_LOGI(TAG, "Executed callback '%s'", name.c_str()); + }); + this->total_scheduled_.fetch_add(1); + + // Small delay to increase chance of race conditions + if (i % 10 == 0) { + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + } + }); + } + + // Wait for all threads to complete + for (auto &t : threads) { + t.join(); + } + + ESP_LOGI(TAG, "All threads completed. Scheduled: %d", this->total_scheduled_.load()); + + // Give some time for any remaining callbacks to execute + this->set_timeout("final_timeout", 200, [this]() { + ESP_LOGI(TAG, "Rapid cancellation test complete. Final stats:"); + ESP_LOGI(TAG, " Total scheduled: %d", this->total_scheduled_.load()); + ESP_LOGI(TAG, " Total executed: %d", this->total_executed_.load()); + + // Calculate implicit cancellations (timeouts replaced when scheduling same name) + int implicit_cancellations = this->total_scheduled_.load() - this->total_executed_.load(); + ESP_LOGI(TAG, " Implicit cancellations (replaced): %d", implicit_cancellations); + ESP_LOGI(TAG, " Total accounted: %d (executed + implicit cancellations)", + this->total_executed_.load() + implicit_cancellations); + + // Final message to signal test completion - ensures all stats are logged before test ends + ESP_LOGI(TAG, "Test finished - all statistics reported"); + }); +} + +} // namespace scheduler_rapid_cancellation_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.h b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.h new file mode 100644 index 0000000000..0a01b2a8de --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include + +namespace esphome { +namespace scheduler_rapid_cancellation_component { + +class SchedulerRapidCancellationComponent : public Component { + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::LATE; } + + void run_rapid_cancellation_test(); + + private: + std::atomic total_scheduled_{0}; + std::atomic total_executed_{0}; +}; + +} // namespace scheduler_rapid_cancellation_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/__init__.py new file mode 100644 index 0000000000..4e847a6fdb --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +scheduler_recursive_timeout_component_ns = cg.esphome_ns.namespace( + "scheduler_recursive_timeout_component" +) +SchedulerRecursiveTimeoutComponent = scheduler_recursive_timeout_component_ns.class_( + "SchedulerRecursiveTimeoutComponent", cg.Component +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SchedulerRecursiveTimeoutComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.cpp b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.cpp new file mode 100644 index 0000000000..2a08bd72a9 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.cpp @@ -0,0 +1,40 @@ +#include "recursive_timeout_component.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace scheduler_recursive_timeout_component { + +static const char *const TAG = "scheduler_recursive_timeout"; + +void SchedulerRecursiveTimeoutComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerRecursiveTimeoutComponent setup"); } + +void SchedulerRecursiveTimeoutComponent::run_recursive_timeout_test() { + ESP_LOGI(TAG, "Starting recursive timeout test - scheduling timeout from within timeout"); + + // Reset state + this->nested_level_ = 0; + + // Schedule the initial timeout with 1ms delay + this->set_timeout(1, [this]() { + ESP_LOGI(TAG, "Executing initial timeout"); + this->nested_level_ = 1; + + // From within this timeout, schedule another timeout with 1ms delay + this->set_timeout(1, [this]() { + ESP_LOGI(TAG, "Executing nested timeout 1"); + this->nested_level_ = 2; + + // From within this nested timeout, schedule yet another timeout with 1ms delay + this->set_timeout(1, [this]() { + ESP_LOGI(TAG, "Executing nested timeout 2"); + this->nested_level_ = 3; + + // Test complete + ESP_LOGI(TAG, "Recursive timeout test complete - all %d levels executed", this->nested_level_); + }); + }); + }); +} + +} // namespace scheduler_recursive_timeout_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.h b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.h new file mode 100644 index 0000000000..8d2c085a11 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/core/component.h" + +namespace esphome { +namespace scheduler_recursive_timeout_component { + +class SchedulerRecursiveTimeoutComponent : public Component { + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::LATE; } + + void run_recursive_timeout_test(); + + private: + int nested_level_{0}; +}; + +} // namespace scheduler_recursive_timeout_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/__init__.py new file mode 100644 index 0000000000..bb1d560ad3 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/__init__.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +scheduler_simultaneous_callbacks_component_ns = cg.esphome_ns.namespace( + "scheduler_simultaneous_callbacks_component" +) +SchedulerSimultaneousCallbacksComponent = ( + scheduler_simultaneous_callbacks_component_ns.class_( + "SchedulerSimultaneousCallbacksComponent", cg.Component + ) +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SchedulerSimultaneousCallbacksComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp new file mode 100644 index 0000000000..b4c2b8c6c2 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp @@ -0,0 +1,109 @@ +#include "simultaneous_callbacks_component.h" +#include "esphome/core/log.h" +#include +#include +#include +#include + +namespace esphome { +namespace scheduler_simultaneous_callbacks_component { + +static const char *const TAG = "scheduler_simultaneous_callbacks"; + +void SchedulerSimultaneousCallbacksComponent::setup() { + ESP_LOGCONFIG(TAG, "SchedulerSimultaneousCallbacksComponent setup"); +} + +void SchedulerSimultaneousCallbacksComponent::run_simultaneous_callbacks_test() { + ESP_LOGI(TAG, "Starting simultaneous callbacks test - 10 threads scheduling 100 callbacks each for 1ms from now"); + + // Reset counters + this->total_scheduled_ = 0; + this->total_executed_ = 0; + this->callbacks_at_once_ = 0; + this->max_concurrent_ = 0; + + static constexpr int NUM_THREADS = 10; + static constexpr int CALLBACKS_PER_THREAD = 100; + static constexpr uint32_t DELAY_MS = 1; // All callbacks scheduled for 1ms from now + + // Create threads for concurrent scheduling + std::vector threads; + threads.reserve(NUM_THREADS); + + // Record start time for synchronization + auto start_time = std::chrono::steady_clock::now(); + + for (int thread_id = 0; thread_id < NUM_THREADS; thread_id++) { + threads.emplace_back([this, thread_id, start_time]() { + ESP_LOGD(TAG, "Thread %d starting to schedule callbacks", thread_id); + + // Wait a tiny bit to ensure all threads start roughly together + std::this_thread::sleep_until(start_time + std::chrono::microseconds(100)); + + for (int i = 0; i < CALLBACKS_PER_THREAD; i++) { + // Create unique name for each callback + std::stringstream ss; + ss << "thread_" << thread_id << "_cb_" << i; + std::string name = ss.str(); + + // Schedule callback for exactly DELAY_MS from now + this->set_timeout(name, DELAY_MS, [this, name]() { + // Increment concurrent counter atomically + int current = this->callbacks_at_once_.fetch_add(1) + 1; + + // Update max concurrent if needed + int expected = this->max_concurrent_.load(); + while (current > expected && !this->max_concurrent_.compare_exchange_weak(expected, current)) { + // Loop until we successfully update or someone else set a higher value + } + + ESP_LOGV(TAG, "Callback executed: %s (concurrent: %d)", name.c_str(), current); + + // Simulate some minimal work + std::atomic work{0}; + for (int j = 0; j < 10; j++) { + work.fetch_add(j); + } + + // Increment executed counter + this->total_executed_.fetch_add(1); + + // Decrement concurrent counter + this->callbacks_at_once_.fetch_sub(1); + }); + + this->total_scheduled_.fetch_add(1); + ESP_LOGV(TAG, "Scheduled callback %s", name.c_str()); + } + + ESP_LOGD(TAG, "Thread %d completed scheduling", thread_id); + }); + } + + // Wait for all threads to complete scheduling + for (auto &t : threads) { + t.join(); + } + + ESP_LOGI(TAG, "All threads completed scheduling. Total scheduled: %d", this->total_scheduled_.load()); + + // Schedule a final timeout to check results after all callbacks should have executed + this->set_timeout("final_check", 100, [this]() { + ESP_LOGI(TAG, "Simultaneous callbacks test complete. Final executed count: %d", this->total_executed_.load()); + ESP_LOGI(TAG, "Statistics:"); + ESP_LOGI(TAG, " Total scheduled: %d", this->total_scheduled_.load()); + ESP_LOGI(TAG, " Total executed: %d", this->total_executed_.load()); + ESP_LOGI(TAG, " Max concurrent callbacks: %d", this->max_concurrent_.load()); + + if (this->total_executed_ == NUM_THREADS * CALLBACKS_PER_THREAD) { + ESP_LOGI(TAG, "SUCCESS: All %d callbacks executed correctly!", this->total_executed_.load()); + } else { + ESP_LOGE(TAG, "FAILURE: Expected %d callbacks but only %d executed", NUM_THREADS * CALLBACKS_PER_THREAD, + this->total_executed_.load()); + } + }); +} + +} // namespace scheduler_simultaneous_callbacks_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.h b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.h new file mode 100644 index 0000000000..1a36af4b3d --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include + +namespace esphome { +namespace scheduler_simultaneous_callbacks_component { + +class SchedulerSimultaneousCallbacksComponent : public Component { + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::LATE; } + + void run_simultaneous_callbacks_test(); + + private: + std::atomic total_scheduled_{0}; + std::atomic total_executed_{0}; + std::atomic callbacks_at_once_{0}; + std::atomic max_concurrent_{0}; +}; + +} // namespace scheduler_simultaneous_callbacks_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/__init__.py new file mode 100644 index 0000000000..3f29a839ef --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +scheduler_string_lifetime_component_ns = cg.esphome_ns.namespace( + "scheduler_string_lifetime_component" +) +SchedulerStringLifetimeComponent = scheduler_string_lifetime_component_ns.class_( + "SchedulerStringLifetimeComponent", cg.Component +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SchedulerStringLifetimeComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp new file mode 100644 index 0000000000..d377c1fe57 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp @@ -0,0 +1,275 @@ +#include "string_lifetime_component.h" +#include "esphome/core/log.h" +#include +#include +#include + +namespace esphome { +namespace scheduler_string_lifetime_component { + +static const char *const TAG = "scheduler_string_lifetime"; + +void SchedulerStringLifetimeComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerStringLifetimeComponent setup"); } + +void SchedulerStringLifetimeComponent::run_string_lifetime_test() { + ESP_LOGI(TAG, "Starting string lifetime tests"); + + this->tests_passed_ = 0; + this->tests_failed_ = 0; + + // Run each test + test_temporary_string_lifetime(); + test_scope_exit_string(); + test_vector_reallocation(); + test_string_move_semantics(); + test_lambda_capture_lifetime(); + + // Schedule final check + this->set_timeout("final_check", 200, [this]() { + ESP_LOGI(TAG, "String lifetime tests complete"); + ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_); + ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_); + + if (this->tests_failed_ == 0) { + ESP_LOGI(TAG, "SUCCESS: All string lifetime tests passed!"); + } else { + ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_); + } + }); +} + +void SchedulerStringLifetimeComponent::run_test1() { + test_temporary_string_lifetime(); + // Wait for all callbacks to execute + this->set_timeout("test1_complete", 10, []() { ESP_LOGI(TAG, "Test 1 complete"); }); +} + +void SchedulerStringLifetimeComponent::run_test2() { + test_scope_exit_string(); + // Wait for all callbacks to execute + this->set_timeout("test2_complete", 20, []() { ESP_LOGI(TAG, "Test 2 complete"); }); +} + +void SchedulerStringLifetimeComponent::run_test3() { + test_vector_reallocation(); + // Wait for all callbacks to execute + this->set_timeout("test3_complete", 60, []() { ESP_LOGI(TAG, "Test 3 complete"); }); +} + +void SchedulerStringLifetimeComponent::run_test4() { + test_string_move_semantics(); + // Wait for all callbacks to execute + this->set_timeout("test4_complete", 35, []() { ESP_LOGI(TAG, "Test 4 complete"); }); +} + +void SchedulerStringLifetimeComponent::run_test5() { + test_lambda_capture_lifetime(); + // Wait for all callbacks to execute + this->set_timeout("test5_complete", 50, []() { ESP_LOGI(TAG, "Test 5 complete"); }); +} + +void SchedulerStringLifetimeComponent::run_final_check() { + ESP_LOGI(TAG, "String lifetime tests complete"); + ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_); + ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_); + + if (this->tests_failed_ == 0) { + ESP_LOGI(TAG, "SUCCESS: All string lifetime tests passed!"); + } else { + ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_); + } +} + +void SchedulerStringLifetimeComponent::test_temporary_string_lifetime() { + ESP_LOGI(TAG, "Test 1: Temporary string lifetime for timeout names"); + + // Test with a temporary string that goes out of scope immediately + { + std::string temp_name = "temp_callback_" + std::to_string(12345); + + // Schedule with temporary string name - scheduler must copy/store this + this->set_timeout(temp_name, 1, [this]() { + ESP_LOGD(TAG, "Callback for temp string name executed"); + this->tests_passed_++; + }); + + // String goes out of scope here, but scheduler should have made a copy + } + + // Test with rvalue string as name + this->set_timeout(std::string("rvalue_test"), 2, [this]() { + ESP_LOGD(TAG, "Rvalue string name callback executed"); + this->tests_passed_++; + }); + + // Test cancelling with reconstructed string + { + std::string cancel_name = "cancel_test_" + std::to_string(999); + this->set_timeout(cancel_name, 100, [this]() { + ESP_LOGE(TAG, "This should have been cancelled!"); + this->tests_failed_++; + }); + } // cancel_name goes out of scope + + // Reconstruct the same string to cancel + std::string cancel_name_2 = "cancel_test_" + std::to_string(999); + bool cancelled = this->cancel_timeout(cancel_name_2); + if (cancelled) { + ESP_LOGD(TAG, "Successfully cancelled with reconstructed string"); + this->tests_passed_++; + } else { + ESP_LOGE(TAG, "Failed to cancel with reconstructed string"); + this->tests_failed_++; + } +} + +void SchedulerStringLifetimeComponent::test_scope_exit_string() { + ESP_LOGI(TAG, "Test 2: Scope exit string names"); + + // Create string names in a limited scope + { + std::string scoped_name = "scoped_timeout_" + std::to_string(555); + + // Schedule with scoped string name + this->set_timeout(scoped_name, 3, [this]() { + ESP_LOGD(TAG, "Scoped name callback executed"); + this->tests_passed_++; + }); + + // scoped_name goes out of scope here + } + + // Test with dynamically allocated string name + { + auto *dynamic_name = new std::string("dynamic_timeout_" + std::to_string(777)); + + this->set_timeout(*dynamic_name, 4, [this, dynamic_name]() { + ESP_LOGD(TAG, "Dynamic string name callback executed"); + this->tests_passed_++; + delete dynamic_name; // Clean up in callback + }); + + // Pointer goes out of scope but string object remains until callback + } + + // Test multiple timeouts with same dynamically created name + for (int i = 0; i < 3; i++) { + std::string loop_name = "loop_timeout_" + std::to_string(i); + this->set_timeout(loop_name, 5 + i * 1, [this, i]() { + ESP_LOGD(TAG, "Loop timeout %d executed", i); + this->tests_passed_++; + }); + // loop_name destroyed and recreated each iteration + } +} + +void SchedulerStringLifetimeComponent::test_vector_reallocation() { + ESP_LOGI(TAG, "Test 3: Vector reallocation stress on timeout names"); + + // Create a vector that will reallocate + std::vector names; + names.reserve(2); // Small initial capacity to force reallocation + + // Schedule callbacks with string names from vector + for (int i = 0; i < 10; i++) { + names.push_back("vector_cb_" + std::to_string(i)); + // Use the string from vector as timeout name + this->set_timeout(names.back(), 8 + i * 1, [this, i]() { + ESP_LOGV(TAG, "Vector name callback %d executed", i); + this->tests_passed_++; + }); + } + + // Force reallocation by adding more elements + // This will move all strings to new memory locations + for (int i = 10; i < 50; i++) { + names.push_back("realloc_trigger_" + std::to_string(i)); + } + + // Add more timeouts after reallocation to ensure old names still work + for (int i = 50; i < 55; i++) { + names.push_back("post_realloc_" + std::to_string(i)); + this->set_timeout(names.back(), 20 + (i - 50), [this]() { + ESP_LOGV(TAG, "Post-reallocation callback executed"); + this->tests_passed_++; + }); + } + + // Clear the vector while timeouts are still pending + names.clear(); + ESP_LOGD(TAG, "Vector cleared - all string names destroyed"); +} + +void SchedulerStringLifetimeComponent::test_string_move_semantics() { + ESP_LOGI(TAG, "Test 4: String move semantics for timeout names"); + + // Test moving string names + std::string original = "move_test_original"; + std::string moved = std::move(original); + + // Schedule with moved string as name + this->set_timeout(moved, 30, [this]() { + ESP_LOGD(TAG, "Moved string name callback executed"); + this->tests_passed_++; + }); + + // original is now empty, try to use it as a different timeout name + original = "reused_after_move"; + this->set_timeout(original, 32, [this]() { + ESP_LOGD(TAG, "Reused string name callback executed"); + this->tests_passed_++; + }); +} + +void SchedulerStringLifetimeComponent::test_lambda_capture_lifetime() { + ESP_LOGI(TAG, "Test 5: Complex timeout name scenarios"); + + // Test scheduling with name built in lambda + [this]() { + std::string lambda_name = "lambda_built_name_" + std::to_string(888); + this->set_timeout(lambda_name, 38, [this]() { + ESP_LOGD(TAG, "Lambda-built name callback executed"); + this->tests_passed_++; + }); + }(); // Lambda executes and lambda_name is destroyed + + // Test with shared_ptr name + auto shared_name = std::make_shared("shared_ptr_timeout"); + this->set_timeout(*shared_name, 40, [this, shared_name]() { + ESP_LOGD(TAG, "Shared_ptr name callback executed"); + this->tests_passed_++; + }); + shared_name.reset(); // Release the shared_ptr + + // Test overwriting timeout with same name + std::string overwrite_name = "overwrite_test"; + this->set_timeout(overwrite_name, 1000, [this]() { + ESP_LOGE(TAG, "This should have been overwritten!"); + this->tests_failed_++; + }); + + // Overwrite with shorter timeout + this->set_timeout(overwrite_name, 42, [this]() { + ESP_LOGD(TAG, "Overwritten timeout executed"); + this->tests_passed_++; + }); + + // Test very long string name + std::string long_name; + for (int i = 0; i < 100; i++) { + long_name += "very_long_timeout_name_segment_" + std::to_string(i) + "_"; + } + this->set_timeout(long_name, 44, [this]() { + ESP_LOGD(TAG, "Very long name timeout executed"); + this->tests_passed_++; + }); + + // Test empty string as name + this->set_timeout("", 46, [this]() { + ESP_LOGD(TAG, "Empty string name timeout executed"); + this->tests_passed_++; + }); +} + +} // namespace scheduler_string_lifetime_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h new file mode 100644 index 0000000000..95532328bb --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include +#include + +namespace esphome { +namespace scheduler_string_lifetime_component { + +class SchedulerStringLifetimeComponent : public Component { + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::LATE; } + + void run_string_lifetime_test(); + + // Individual test methods exposed as services + void run_test1(); + void run_test2(); + void run_test3(); + void run_test4(); + void run_test5(); + void run_final_check(); + + private: + void test_temporary_string_lifetime(); + void test_scope_exit_string(); + void test_vector_reallocation(); + void test_string_move_semantics(); + void test_lambda_capture_lifetime(); + + int tests_passed_{0}; + int tests_failed_{0}; +}; + +} // namespace scheduler_string_lifetime_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/__init__.py new file mode 100644 index 0000000000..6cc564395c --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +scheduler_string_name_stress_component_ns = cg.esphome_ns.namespace( + "scheduler_string_name_stress_component" +) +SchedulerStringNameStressComponent = scheduler_string_name_stress_component_ns.class_( + "SchedulerStringNameStressComponent", cg.Component +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(SchedulerStringNameStressComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp new file mode 100644 index 0000000000..9071e573bb --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp @@ -0,0 +1,110 @@ +#include "string_name_stress_component.h" +#include "esphome/core/log.h" +#include +#include +#include +#include +#include +#include + +namespace esphome { +namespace scheduler_string_name_stress_component { + +static const char *const TAG = "scheduler_string_name_stress"; + +void SchedulerStringNameStressComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerStringNameStressComponent setup"); } + +void SchedulerStringNameStressComponent::run_string_name_stress_test() { + // Use member variables to reset state + this->total_callbacks_ = 0; + this->executed_callbacks_ = 0; + static constexpr int NUM_THREADS = 10; + static constexpr int CALLBACKS_PER_THREAD = 100; + + ESP_LOGI(TAG, "Starting string name stress test - multi-threaded set_timeout with std::string names"); + ESP_LOGI(TAG, "This test specifically uses dynamic string names to test memory management"); + + // Track start time + auto start_time = std::chrono::steady_clock::now(); + + // Create threads + std::vector threads; + + ESP_LOGI(TAG, "Creating %d threads, each will schedule %d callbacks with dynamic names", NUM_THREADS, + CALLBACKS_PER_THREAD); + + threads.reserve(NUM_THREADS); + for (int i = 0; i < NUM_THREADS; i++) { + threads.emplace_back([this, i]() { + ESP_LOGV(TAG, "Thread %d starting", i); + + // Each thread schedules callbacks with dynamically created string names + for (int j = 0; j < CALLBACKS_PER_THREAD; j++) { + int callback_id = this->total_callbacks_.fetch_add(1); + + // Create a dynamic string name - this will test memory management + std::stringstream ss; + ss << "thread_" << i << "_callback_" << j << "_id_" << callback_id; + std::string dynamic_name = ss.str(); + + ESP_LOGV(TAG, "Thread %d scheduling timeout with dynamic name: %s", i, dynamic_name.c_str()); + + // Capture necessary values for the lambda + auto *component = this; + + // Schedule with std::string name - this tests the string overload + // Use varying delays to stress the heap scheduler + uint32_t delay = 1 + (callback_id % 50); + + // Also test nested scheduling from callbacks + if (j % 10 == 0) { + // Every 10th callback schedules another callback + this->set_timeout(dynamic_name, delay, [component, callback_id]() { + component->executed_callbacks_.fetch_add(1); + ESP_LOGV(TAG, "Executed string-named callback %d (nested scheduler)", callback_id); + + // Schedule another timeout from within this callback with a new dynamic name + std::string nested_name = "nested_from_" + std::to_string(callback_id); + component->set_timeout(nested_name, 1, [callback_id]() { + ESP_LOGV(TAG, "Executed nested string-named callback from %d", callback_id); + }); + }); + } else { + // Regular callback + this->set_timeout(dynamic_name, delay, [component, callback_id]() { + component->executed_callbacks_.fetch_add(1); + ESP_LOGV(TAG, "Executed string-named callback %d", callback_id); + }); + } + + // Add some timing variations to increase race conditions + if (j % 5 == 0) { + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + } + ESP_LOGV(TAG, "Thread %d finished scheduling", i); + }); + } + + // Wait for all threads to complete scheduling + for (auto &t : threads) { + t.join(); + } + + auto end_time = std::chrono::steady_clock::now(); + auto thread_time = std::chrono::duration_cast(end_time - start_time).count(); + ESP_LOGI(TAG, "All threads finished scheduling in %lldms. Created %d callbacks with dynamic names", thread_time, + this->total_callbacks_.load()); + + // Give some time for callbacks to execute + ESP_LOGI(TAG, "Waiting for callbacks to execute..."); + + // Schedule a final callback to signal completion + this->set_timeout("test_complete", 2000, [this]() { + ESP_LOGI(TAG, "String name stress test complete. Executed %d of %d callbacks", this->executed_callbacks_.load(), + this->total_callbacks_.load()); + }); +} + +} // namespace scheduler_string_name_stress_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h new file mode 100644 index 0000000000..002a0a7b51 --- /dev/null +++ b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include + +namespace esphome { +namespace scheduler_string_name_stress_component { + +class SchedulerStringNameStressComponent : public Component { + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::LATE; } + + void run_string_name_stress_test(); + + private: + std::atomic total_callbacks_{0}; + std::atomic executed_callbacks_{0}; +}; + +} // namespace scheduler_string_name_stress_component +} // namespace esphome diff --git a/tests/integration/fixtures/scheduler_bulk_cleanup.yaml b/tests/integration/fixtures/scheduler_bulk_cleanup.yaml new file mode 100644 index 0000000000..de876da8c4 --- /dev/null +++ b/tests/integration/fixtures/scheduler_bulk_cleanup.yaml @@ -0,0 +1,23 @@ +esphome: + name: scheduler-bulk-cleanup + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +host: + +logger: + level: DEBUG + +api: + services: + - service: trigger_bulk_cleanup + then: + - lambda: |- + auto component = id(bulk_cleanup_component); + component->trigger_bulk_cleanup(); + +scheduler_bulk_cleanup_component: + id: bulk_cleanup_component diff --git a/tests/integration/fixtures/scheduler_defer_cancel.yaml b/tests/integration/fixtures/scheduler_defer_cancel.yaml new file mode 100644 index 0000000000..9e3f927c33 --- /dev/null +++ b/tests/integration/fixtures/scheduler_defer_cancel.yaml @@ -0,0 +1,51 @@ +esphome: + name: scheduler-defer-cancel + +host: + +logger: + level: DEBUG + +api: + services: + - service: test_defer_cancel + then: + - lambda: |- + // Schedule 10 defers with the same name + // Only the last one should execute + for (int i = 1; i <= 10; i++) { + App.scheduler.set_timeout(nullptr, "test_defer", 0, [i]() { + ESP_LOGI("TEST", "Defer executed: %d", i); + // Fire event with the defer number + std::string event_type = "defer_executed_" + std::to_string(i); + id(test_result)->trigger(event_type); + }); + } + + // Schedule completion notification after all defers + App.scheduler.set_timeout(nullptr, "completion", 0, []() { + ESP_LOGI("TEST", "Test complete"); + id(test_complete)->trigger("test_finished"); + }); + +event: + - platform: template + id: test_result + name: "Test Result" + event_types: + - "defer_executed_1" + - "defer_executed_2" + - "defer_executed_3" + - "defer_executed_4" + - "defer_executed_5" + - "defer_executed_6" + - "defer_executed_7" + - "defer_executed_8" + - "defer_executed_9" + - "defer_executed_10" + + - platform: template + id: test_complete + name: "Test Complete" + event_types: + - "test_finished" diff --git a/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml b/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml new file mode 100644 index 0000000000..fb6b1791dc --- /dev/null +++ b/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml @@ -0,0 +1,34 @@ +esphome: + name: scheduler-defer-cancel-regular + +host: + +logger: + level: DEBUG + +api: + services: + - service: test_defer_cancels_regular + then: + - lambda: |- + ESP_LOGI("TEST", "Starting defer cancels regular timeout test"); + + // Schedule a regular timeout with 100ms delay + App.scheduler.set_timeout(nullptr, "test_timeout", 100, []() { + ESP_LOGE("TEST", "ERROR: Regular timeout executed - should have been cancelled!"); + }); + + ESP_LOGI("TEST", "Scheduled regular timeout with 100ms delay"); + + // Immediately schedule a deferred timeout (0 delay) with the same name + // This should cancel the regular timeout + App.scheduler.set_timeout(nullptr, "test_timeout", 0, []() { + ESP_LOGI("TEST", "SUCCESS: Deferred timeout executed"); + }); + + ESP_LOGI("TEST", "Scheduled deferred timeout - should cancel regular timeout"); + + // Schedule test completion after 200ms (after regular timeout would have fired) + App.scheduler.set_timeout(nullptr, "test_complete", 200, []() { + ESP_LOGI("TEST", "Test complete"); + }); diff --git a/tests/integration/fixtures/defer_fifo_simple.yaml b/tests/integration/fixtures/scheduler_defer_fifo_simple.yaml similarity index 99% rename from tests/integration/fixtures/defer_fifo_simple.yaml rename to tests/integration/fixtures/scheduler_defer_fifo_simple.yaml index db24ebf601..7384082ac2 100644 --- a/tests/integration/fixtures/defer_fifo_simple.yaml +++ b/tests/integration/fixtures/scheduler_defer_fifo_simple.yaml @@ -1,5 +1,5 @@ esphome: - name: defer-fifo-simple + name: scheduler-defer-fifo-simple host: diff --git a/tests/integration/fixtures/defer_stress.yaml b/tests/integration/fixtures/scheduler_defer_stress.yaml similarity index 94% rename from tests/integration/fixtures/defer_stress.yaml rename to tests/integration/fixtures/scheduler_defer_stress.yaml index 6df475229b..0d9c1d1405 100644 --- a/tests/integration/fixtures/defer_stress.yaml +++ b/tests/integration/fixtures/scheduler_defer_stress.yaml @@ -1,5 +1,5 @@ esphome: - name: defer-stress-test + name: scheduler-defer-stress-test external_components: - source: diff --git a/tests/integration/fixtures/scheduler_heap_stress.yaml b/tests/integration/fixtures/scheduler_heap_stress.yaml new file mode 100644 index 0000000000..d4d340b68b --- /dev/null +++ b/tests/integration/fixtures/scheduler_heap_stress.yaml @@ -0,0 +1,38 @@ +esphome: + name: scheduler-heap-stress-test + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [scheduler_heap_stress_component] + +host: + +logger: + level: VERBOSE + +scheduler_heap_stress_component: + id: heap_stress + +api: + services: + - service: run_heap_stress_test + then: + - lambda: |- + id(heap_stress)->run_multi_thread_test(); + +event: + - platform: template + name: "Test Complete" + id: test_complete + device_class: button + event_types: + - "test_finished" + - platform: template + name: "Test Result" + id: test_result + device_class: button + event_types: + - "passed" + - "failed" diff --git a/tests/integration/fixtures/scheduler_rapid_cancellation.yaml b/tests/integration/fixtures/scheduler_rapid_cancellation.yaml new file mode 100644 index 0000000000..4824654c5c --- /dev/null +++ b/tests/integration/fixtures/scheduler_rapid_cancellation.yaml @@ -0,0 +1,38 @@ +esphome: + name: sched-rapid-cancel-test + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [scheduler_rapid_cancellation_component] + +host: + +logger: + level: VERBOSE + +scheduler_rapid_cancellation_component: + id: rapid_cancel + +api: + services: + - service: run_rapid_cancellation_test + then: + - lambda: |- + id(rapid_cancel)->run_rapid_cancellation_test(); + +event: + - platform: template + name: "Test Complete" + id: test_complete + device_class: button + event_types: + - "test_finished" + - platform: template + name: "Test Result" + id: test_result + device_class: button + event_types: + - "passed" + - "failed" diff --git a/tests/integration/fixtures/scheduler_recursive_timeout.yaml b/tests/integration/fixtures/scheduler_recursive_timeout.yaml new file mode 100644 index 0000000000..f1168802f6 --- /dev/null +++ b/tests/integration/fixtures/scheduler_recursive_timeout.yaml @@ -0,0 +1,38 @@ +esphome: + name: sched-recursive-timeout + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [scheduler_recursive_timeout_component] + +host: + +logger: + level: VERBOSE + +scheduler_recursive_timeout_component: + id: recursive_timeout + +api: + services: + - service: run_recursive_timeout_test + then: + - lambda: |- + id(recursive_timeout)->run_recursive_timeout_test(); + +event: + - platform: template + name: "Test Complete" + id: test_complete + device_class: button + event_types: + - "test_finished" + - platform: template + name: "Test Result" + id: test_result + device_class: button + event_types: + - "passed" + - "failed" diff --git a/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml b/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml new file mode 100644 index 0000000000..446ee7fdc0 --- /dev/null +++ b/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml @@ -0,0 +1,23 @@ +esphome: + name: sched-simul-callbacks-test + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [scheduler_simultaneous_callbacks_component] + +host: + +logger: + level: INFO + +scheduler_simultaneous_callbacks_component: + id: simultaneous_callbacks + +api: + services: + - service: run_simultaneous_callbacks_test + then: + - lambda: |- + id(simultaneous_callbacks)->run_simultaneous_callbacks_test(); diff --git a/tests/integration/fixtures/scheduler_string_lifetime.yaml b/tests/integration/fixtures/scheduler_string_lifetime.yaml new file mode 100644 index 0000000000..ebd5052b8b --- /dev/null +++ b/tests/integration/fixtures/scheduler_string_lifetime.yaml @@ -0,0 +1,47 @@ +esphome: + name: scheduler-string-lifetime-test + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [scheduler_string_lifetime_component] + +host: + +logger: + level: DEBUG + +scheduler_string_lifetime_component: + id: string_lifetime + +api: + services: + - service: run_string_lifetime_test + then: + - lambda: |- + id(string_lifetime)->run_string_lifetime_test(); + - service: run_test1 + then: + - lambda: |- + id(string_lifetime)->run_test1(); + - service: run_test2 + then: + - lambda: |- + id(string_lifetime)->run_test2(); + - service: run_test3 + then: + - lambda: |- + id(string_lifetime)->run_test3(); + - service: run_test4 + then: + - lambda: |- + id(string_lifetime)->run_test4(); + - service: run_test5 + then: + - lambda: |- + id(string_lifetime)->run_test5(); + - service: run_final_check + then: + - lambda: |- + id(string_lifetime)->run_final_check(); diff --git a/tests/integration/fixtures/scheduler_string_name_stress.yaml b/tests/integration/fixtures/scheduler_string_name_stress.yaml new file mode 100644 index 0000000000..d1ef55c8d5 --- /dev/null +++ b/tests/integration/fixtures/scheduler_string_name_stress.yaml @@ -0,0 +1,38 @@ +esphome: + name: sched-string-name-stress + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [scheduler_string_name_stress_component] + +host: + +logger: + level: VERBOSE + +scheduler_string_name_stress_component: + id: string_stress + +api: + services: + - service: run_string_name_stress_test + then: + - lambda: |- + id(string_stress)->run_string_name_stress_test(); + +event: + - platform: template + name: "Test Complete" + id: test_complete + device_class: button + event_types: + - "test_finished" + - platform: template + name: "Test Result" + id: test_result + device_class: button + event_types: + - "passed" + - "failed" diff --git a/tests/integration/test_scheduler_bulk_cleanup.py b/tests/integration/test_scheduler_bulk_cleanup.py new file mode 100644 index 0000000000..08ff293b84 --- /dev/null +++ b/tests/integration/test_scheduler_bulk_cleanup.py @@ -0,0 +1,122 @@ +"""Test that triggers the bulk cleanup path when to_remove_ > MAX_LOGICALLY_DELETED_ITEMS.""" + +import asyncio +from pathlib import Path +import re + +from aioesphomeapi import UserService +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_bulk_cleanup( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that bulk cleanup path is triggered when many items are cancelled.""" + + # Get the absolute path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + # Create a future to signal test completion + loop = asyncio.get_event_loop() + test_complete_future: asyncio.Future[None] = loop.create_future() + bulk_cleanup_triggered = False + cleanup_stats: dict[str, int] = { + "removed": 0, + "before": 0, + "after": 0, + } + post_cleanup_executed = 0 + + def on_log_line(line: str) -> None: + nonlocal bulk_cleanup_triggered, post_cleanup_executed + + # Look for logs indicating bulk cleanup was triggered + # The actual cleanup happens silently, so we track the cancel operations + if "Successfully cancelled" in line and "timeouts" in line: + match = re.search(r"Successfully cancelled (\d+) timeouts", line) + if match and int(match.group(1)) > 10: + bulk_cleanup_triggered = True + + # Track cleanup statistics + match = re.search(r"Bulk cleanup triggered: removed (\d+) items", line) + if match: + cleanup_stats["removed"] = int(match.group(1)) + + match = re.search(r"Items before cleanup: (\d+), after: (\d+)", line) + if match: + cleanup_stats["before"] = int(match.group(1)) + cleanup_stats["after"] = int(match.group(2)) + + # Track post-cleanup timeout executions + if "Post-cleanup timeout" in line and "executed correctly" in line: + match = re.search(r"Post-cleanup timeout (\d+) executed correctly", line) + if match: + post_cleanup_executed += 1 + + # Check for final test completion + if ( + "All post-cleanup timeouts completed - test finished" in line + and not test_complete_future.done() + ): + test_complete_future.set_result(None) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-bulk-cleanup" + + # List entities and services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test service + trigger_bulk_cleanup_service: UserService | None = None + for service in services: + if service.name == "trigger_bulk_cleanup": + trigger_bulk_cleanup_service = service + break + + assert trigger_bulk_cleanup_service is not None, ( + "trigger_bulk_cleanup service not found" + ) + + # Execute the test + client.execute_service(trigger_bulk_cleanup_service, {}) + + # Wait for test completion + try: + await asyncio.wait_for(test_complete_future, timeout=10.0) + except asyncio.TimeoutError: + pytest.fail("Bulk cleanup test timed out") + + # Verify bulk cleanup was triggered + assert bulk_cleanup_triggered, ( + "Bulk cleanup path was not triggered - MAX_LOGICALLY_DELETED_ITEMS threshold not reached" + ) + + # Verify cleanup statistics + assert cleanup_stats["removed"] > 10, ( + f"Expected more than 10 items removed, got {cleanup_stats['removed']}" + ) + + # Verify scheduler still works after bulk cleanup + assert post_cleanup_executed == 5, ( + f"Expected 5 post-cleanup timeouts to execute, but {post_cleanup_executed} executed" + ) diff --git a/tests/integration/test_scheduler_defer_cancel.py b/tests/integration/test_scheduler_defer_cancel.py new file mode 100644 index 0000000000..923cf946c4 --- /dev/null +++ b/tests/integration/test_scheduler_defer_cancel.py @@ -0,0 +1,94 @@ +"""Test that defer() with the same name cancels previous defers.""" + +import asyncio + +from aioesphomeapi import EntityState, Event, EventInfo, UserService +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_defer_cancel( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that defer() with the same name cancels previous defers.""" + + async with run_compiled(yaml_config), api_client_connected() as client: + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-defer-cancel" + + # List entities and services + entity_info, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test entities + test_complete_entity: EventInfo | None = None + test_result_entity: EventInfo | None = None + + for entity in entity_info: + if isinstance(entity, EventInfo): + if entity.object_id == "test_complete": + test_complete_entity = entity + elif entity.object_id == "test_result": + test_result_entity = entity + + assert test_complete_entity is not None, "test_complete event not found" + assert test_result_entity is not None, "test_result event not found" + + # Find our test service + test_defer_cancel_service: UserService | None = None + for service in services: + if service.name == "test_defer_cancel": + test_defer_cancel_service = service + + assert test_defer_cancel_service is not None, ( + "test_defer_cancel service not found" + ) + + # Get the event loop + loop = asyncio.get_running_loop() + + # Subscribe to states + test_complete_future: asyncio.Future[bool] = loop.create_future() + test_result_future: asyncio.Future[int] = loop.create_future() + + def on_state(state: EntityState) -> None: + if not isinstance(state, Event): + return + + if ( + state.key == test_complete_entity.key + and state.event_type == "test_finished" + and not test_complete_future.done() + ): + test_complete_future.set_result(True) + return + + if state.key == test_result_entity.key and not test_result_future.done(): + # Event type should be "defer_executed_X" where X is the defer number + if state.event_type.startswith("defer_executed_"): + defer_num = int(state.event_type.split("_")[-1]) + test_result_future.set_result(defer_num) + + client.subscribe_states(on_state) + + # Execute the test + client.execute_service(test_defer_cancel_service, {}) + + # Wait for test completion + try: + await asyncio.wait_for(test_complete_future, timeout=10.0) + executed_defer = await asyncio.wait_for(test_result_future, timeout=1.0) + except asyncio.TimeoutError: + pytest.fail("Test did not complete within timeout") + + # Verify that only defer 10 was executed + assert executed_defer == 10, ( + f"Expected defer 10 to execute, got {executed_defer}" + ) diff --git a/tests/integration/test_scheduler_defer_cancel_regular.py b/tests/integration/test_scheduler_defer_cancel_regular.py new file mode 100644 index 0000000000..57b7134feb --- /dev/null +++ b/tests/integration/test_scheduler_defer_cancel_regular.py @@ -0,0 +1,90 @@ +"""Test that a deferred timeout cancels a regular timeout with the same name.""" + +import asyncio + +from aioesphomeapi import UserService +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_defer_cancels_regular( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that set_timeout(name, 0) cancels a previously scheduled set_timeout(name, delay).""" + + # Create a future to signal test completion + loop = asyncio.get_running_loop() + test_complete_future: asyncio.Future[None] = loop.create_future() + + # Track log messages + log_messages: list[str] = [] + error_detected = False + + def on_log_line(line: str) -> None: + nonlocal error_detected + if "TEST" in line: + log_messages.append(line) + + if "ERROR: Regular timeout executed" in line: + error_detected = True + + if "Test complete" in line and not test_complete_future.done(): + test_complete_future.set_result(None) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-defer-cancel-regular" + + # List services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test service + test_service: UserService | None = None + for service in services: + if service.name == "test_defer_cancels_regular": + test_service = service + break + + assert test_service is not None, "test_defer_cancels_regular service not found" + + # Execute the test + client.execute_service(test_service, {}) + + # Wait for test completion + try: + await asyncio.wait_for(test_complete_future, timeout=5.0) + except asyncio.TimeoutError: + pytest.fail(f"Test timed out. Log messages: {log_messages}") + + # Verify results + assert not error_detected, ( + f"Regular timeout should have been cancelled but it executed! Logs: {log_messages}" + ) + + # Verify the deferred timeout executed + assert any( + "SUCCESS: Deferred timeout executed" in msg for msg in log_messages + ), f"Deferred timeout should have executed. Logs: {log_messages}" + + # Verify the expected sequence of events + assert any( + "Starting defer cancels regular timeout test" in msg for msg in log_messages + ) + assert any( + "Scheduled regular timeout with 100ms delay" in msg for msg in log_messages + ) + assert any( + "Scheduled deferred timeout - should cancel regular timeout" in msg + for msg in log_messages + ) diff --git a/tests/integration/test_defer_fifo_simple.py b/tests/integration/test_scheduler_defer_fifo_simple.py similarity index 97% rename from tests/integration/test_defer_fifo_simple.py rename to tests/integration/test_scheduler_defer_fifo_simple.py index 5a62a45786..eb4058fedd 100644 --- a/tests/integration/test_defer_fifo_simple.py +++ b/tests/integration/test_scheduler_defer_fifo_simple.py @@ -9,7 +9,7 @@ from .types import APIClientConnectedFactory, RunCompiledFunction @pytest.mark.asyncio -async def test_defer_fifo_simple( +async def test_scheduler_defer_fifo_simple( yaml_config: str, run_compiled: RunCompiledFunction, api_client_connected: APIClientConnectedFactory, @@ -20,7 +20,7 @@ async def test_defer_fifo_simple( # Verify we can connect device_info = await client.device_info() assert device_info is not None - assert device_info.name == "defer-fifo-simple" + assert device_info.name == "scheduler-defer-fifo-simple" # List entities and services entity_info, services = await asyncio.wait_for( diff --git a/tests/integration/test_defer_stress.py b/tests/integration/test_scheduler_defer_stress.py similarity index 97% rename from tests/integration/test_defer_stress.py rename to tests/integration/test_scheduler_defer_stress.py index f63ec8d25f..d546b7132f 100644 --- a/tests/integration/test_defer_stress.py +++ b/tests/integration/test_scheduler_defer_stress.py @@ -11,7 +11,7 @@ from .types import APIClientConnectedFactory, RunCompiledFunction @pytest.mark.asyncio -async def test_defer_stress( +async def test_scheduler_defer_stress( yaml_config: str, run_compiled: RunCompiledFunction, api_client_connected: APIClientConnectedFactory, @@ -75,7 +75,7 @@ async def test_defer_stress( # Verify we can connect device_info = await client.device_info() assert device_info is not None - assert device_info.name == "defer-stress-test" + assert device_info.name == "scheduler-defer-stress-test" # List entities and services entity_info, services = await asyncio.wait_for( diff --git a/tests/integration/test_scheduler_heap_stress.py b/tests/integration/test_scheduler_heap_stress.py new file mode 100644 index 0000000000..3c757bfc9d --- /dev/null +++ b/tests/integration/test_scheduler_heap_stress.py @@ -0,0 +1,140 @@ +"""Stress test for heap scheduler thread safety with multiple threads.""" + +import asyncio +from pathlib import Path +import re + +from aioesphomeapi import UserService +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_heap_stress( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that set_timeout/set_interval doesn't crash when called rapidly from multiple threads.""" + + # Get the absolute path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + # Create a future to signal test completion + loop = asyncio.get_running_loop() + test_complete_future: asyncio.Future[None] = loop.create_future() + + # Track executed timeouts/intervals and their order + executed_callbacks: set[int] = set() + thread_executions: dict[ + int, list[int] + ] = {} # thread_id -> list of indices in execution order + callback_types: dict[int, str] = {} # callback_id -> "timeout" or "interval" + + def on_log_line(line: str) -> None: + # Track all executed callbacks with thread and index info + match = re.search( + r"Executed (timeout|interval) (\d+) \(thread (\d+), index (\d+)\)", line + ) + if not match: + # Also check for the completion message + if "All threads finished" in line and "Created 1000 callbacks" in line: + # Give scheduler some time to execute callbacks + pass + return + + callback_type = match.group(1) + callback_id = int(match.group(2)) + thread_id = int(match.group(3)) + index = int(match.group(4)) + + # Only count each callback ID once (intervals might fire multiple times) + if callback_id not in executed_callbacks: + executed_callbacks.add(callback_id) + callback_types[callback_id] = callback_type + + # Track execution order per thread + if thread_id not in thread_executions: + thread_executions[thread_id] = [] + + # Only append if this is a new execution for this thread + if index not in thread_executions[thread_id]: + thread_executions[thread_id].append(index) + + # Check if we've executed all 1000 callbacks (0-999) + if len(executed_callbacks) >= 1000 and not test_complete_future.done(): + test_complete_future.set_result(None) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-heap-stress-test" + + # List entities and services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test service + run_stress_test_service: UserService | None = None + for service in services: + if service.name == "run_heap_stress_test": + run_stress_test_service = service + break + + assert run_stress_test_service is not None, ( + "run_heap_stress_test service not found" + ) + + # Call the run_heap_stress_test service to start the test + client.execute_service(run_stress_test_service, {}) + + # Wait for all callbacks to execute (should be quick, but give more time for scheduling) + try: + await asyncio.wait_for(test_complete_future, timeout=60.0) + except asyncio.TimeoutError: + # Report how many we got + pytest.fail( + f"Stress test timed out. Only {len(executed_callbacks)} of " + f"1000 callbacks executed. Missing IDs: " + f"{sorted(set(range(1000)) - executed_callbacks)[:10]}..." + ) + + # Verify all callbacks executed + assert len(executed_callbacks) == 1000, ( + f"Expected 1000 callbacks, got {len(executed_callbacks)}" + ) + + # Verify we have all IDs from 0-999 + expected_ids = set(range(1000)) + missing_ids = expected_ids - executed_callbacks + assert not missing_ids, f"Missing callback IDs: {sorted(missing_ids)}" + + # Verify we have a mix of timeouts and intervals + timeout_count = sum(1 for t in callback_types.values() if t == "timeout") + interval_count = sum(1 for t in callback_types.values() if t == "interval") + assert timeout_count > 0, "No timeouts were executed" + assert interval_count > 0, "No intervals were executed" + + # Verify each thread executed callbacks + for thread_id, indices in thread_executions.items(): + assert len(indices) == 100, ( + f"Thread {thread_id} executed {len(indices)} callbacks, expected 100" + ) + # Total should be 1000 callbacks + total_callbacks = timeout_count + interval_count + assert total_callbacks == 1000, ( + f"Expected 1000 total callbacks but got {total_callbacks}" + ) diff --git a/tests/integration/test_scheduler_rapid_cancellation.py b/tests/integration/test_scheduler_rapid_cancellation.py new file mode 100644 index 0000000000..90577f36f1 --- /dev/null +++ b/tests/integration/test_scheduler_rapid_cancellation.py @@ -0,0 +1,142 @@ +"""Rapid cancellation test - schedule and immediately cancel timeouts with string names.""" + +import asyncio +from pathlib import Path +import re + +from aioesphomeapi import UserService +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_rapid_cancellation( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test rapid schedule/cancel cycles that might expose race conditions.""" + + # Get the absolute path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + # Create a future to signal test completion + loop = asyncio.get_running_loop() + test_complete_future: asyncio.Future[None] = loop.create_future() + + # Track test progress + test_stats = { + "log_count": 0, + "errors": [], + "summary_scheduled": None, + "final_scheduled": 0, + "final_executed": 0, + "final_implicit_cancellations": 0, + } + + def on_log_line(line: str) -> None: + # Count log lines + test_stats["log_count"] += 1 + + # Check for errors (only ERROR level, not WARN) + if "ERROR" in line: + test_stats["errors"].append(line) + + # Parse summary statistics + if "All threads completed. Scheduled:" in line: + # Extract the scheduled count from the summary + if match := re.search(r"Scheduled: (\d+)", line): + test_stats["summary_scheduled"] = int(match.group(1)) + elif "Total scheduled:" in line: + if match := re.search(r"Total scheduled: (\d+)", line): + test_stats["final_scheduled"] = int(match.group(1)) + elif "Total executed:" in line: + if match := re.search(r"Total executed: (\d+)", line): + test_stats["final_executed"] = int(match.group(1)) + elif "Implicit cancellations (replaced):" in line: + if match := re.search(r"Implicit cancellations \(replaced\): (\d+)", line): + test_stats["final_implicit_cancellations"] = int(match.group(1)) + + # Check for crash indicators + if any( + indicator in line.lower() + for indicator in ["segfault", "abort", "assertion", "heap corruption"] + ): + if not test_complete_future.done(): + test_complete_future.set_exception(Exception(f"Crash detected: {line}")) + return + + # Check for completion - wait for final message after all stats are logged + if ( + "Test finished - all statistics reported" in line + and not test_complete_future.done() + ): + test_complete_future.set_result(None) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "sched-rapid-cancel-test" + + # List entities and services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test service + run_test_service: UserService | None = None + for service in services: + if service.name == "run_rapid_cancellation_test": + run_test_service = service + break + + assert run_test_service is not None, ( + "run_rapid_cancellation_test service not found" + ) + + # Call the service to start the test + client.execute_service(run_test_service, {}) + + # Wait for test to complete with timeout + try: + await asyncio.wait_for(test_complete_future, timeout=10.0) + except asyncio.TimeoutError: + pytest.fail(f"Test timed out. Stats: {test_stats}") + + # Check for any errors + assert len(test_stats["errors"]) == 0, ( + f"Errors detected: {test_stats['errors']}" + ) + + # Check that we received log messages + assert test_stats["log_count"] > 0, "No log messages received" + + # Check the summary line to verify all threads scheduled their operations + assert test_stats["summary_scheduled"] == 400, ( + f"Expected summary to show 400 scheduled operations but got {test_stats['summary_scheduled']}" + ) + + # Check final statistics + assert test_stats["final_scheduled"] == 400, ( + f"Expected final stats to show 400 scheduled but got {test_stats['final_scheduled']}" + ) + + assert test_stats["final_executed"] == 10, ( + f"Expected final stats to show 10 executed but got {test_stats['final_executed']}" + ) + + assert test_stats["final_implicit_cancellations"] == 390, ( + f"Expected final stats to show 390 implicit cancellations but got {test_stats['final_implicit_cancellations']}" + ) diff --git a/tests/integration/test_scheduler_recursive_timeout.py b/tests/integration/test_scheduler_recursive_timeout.py new file mode 100644 index 0000000000..c015978e15 --- /dev/null +++ b/tests/integration/test_scheduler_recursive_timeout.py @@ -0,0 +1,101 @@ +"""Test for recursive timeout scheduling - scheduling timeouts from within timeout callbacks.""" + +import asyncio +from pathlib import Path + +from aioesphomeapi import UserService +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_recursive_timeout( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that scheduling timeouts from within timeout callbacks works correctly.""" + + # Get the absolute path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + # Create a future to signal test completion + loop = asyncio.get_running_loop() + test_complete_future: asyncio.Future[None] = loop.create_future() + + # Track execution sequence + execution_sequence: list[str] = [] + expected_sequence = [ + "initial_timeout", + "nested_timeout_1", + "nested_timeout_2", + "test_complete", + ] + + def on_log_line(line: str) -> None: + # Track execution sequence + if "Executing initial timeout" in line: + execution_sequence.append("initial_timeout") + elif "Executing nested timeout 1" in line: + execution_sequence.append("nested_timeout_1") + elif "Executing nested timeout 2" in line: + execution_sequence.append("nested_timeout_2") + elif "Recursive timeout test complete" in line: + execution_sequence.append("test_complete") + if not test_complete_future.done(): + test_complete_future.set_result(None) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "sched-recursive-timeout" + + # List entities and services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test service + run_test_service: UserService | None = None + for service in services: + if service.name == "run_recursive_timeout_test": + run_test_service = service + break + + assert run_test_service is not None, ( + "run_recursive_timeout_test service not found" + ) + + # Call the service to start the test + client.execute_service(run_test_service, {}) + + # Wait for test to complete + try: + await asyncio.wait_for(test_complete_future, timeout=10.0) + except asyncio.TimeoutError: + pytest.fail( + f"Recursive timeout test timed out. Got sequence: {execution_sequence}" + ) + + # Verify execution sequence + assert execution_sequence == expected_sequence, ( + f"Execution sequence mismatch. Expected {expected_sequence}, " + f"got {execution_sequence}" + ) + + # Verify we got exactly 4 events (Initial + Level 1 + Level 2 + Complete) + assert len(execution_sequence) == 4, ( + f"Expected 4 events but got {len(execution_sequence)}" + ) diff --git a/tests/integration/test_scheduler_simultaneous_callbacks.py b/tests/integration/test_scheduler_simultaneous_callbacks.py new file mode 100644 index 0000000000..f5120ce4ce --- /dev/null +++ b/tests/integration/test_scheduler_simultaneous_callbacks.py @@ -0,0 +1,123 @@ +"""Simultaneous callbacks test - schedule many callbacks for the same time from multiple threads.""" + +import asyncio +from pathlib import Path +import re + +from aioesphomeapi import UserService +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_simultaneous_callbacks( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test scheduling many callbacks for the exact same time from multiple threads.""" + + # Get the absolute path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + # Create a future to signal test completion + loop = asyncio.get_running_loop() + test_complete_future: asyncio.Future[None] = loop.create_future() + + # Track test progress + test_stats = { + "scheduled": 0, + "executed": 0, + "expected": 1000, # 10 threads * 100 callbacks + "errors": [], + } + + def on_log_line(line: str) -> None: + # Track operations + if "Scheduled callback" in line: + test_stats["scheduled"] += 1 + elif "Callback executed" in line: + test_stats["executed"] += 1 + elif "ERROR" in line: + test_stats["errors"].append(line) + + # Check for crash indicators + if any( + indicator in line.lower() + for indicator in ["segfault", "abort", "assertion", "heap corruption"] + ): + if not test_complete_future.done(): + test_complete_future.set_exception(Exception(f"Crash detected: {line}")) + return + + # Check for completion with final count + if "Final executed count:" in line: + # Extract number from log line like: "[07:59:47][I][simultaneous_callbacks:093]: Simultaneous callbacks test complete. Final executed count: 1000" + match = re.search(r"Final executed count:\s*(\d+)", line) + if match: + test_stats["final_count"] = int(match.group(1)) + + # Check for completion + if ( + "Simultaneous callbacks test complete" in line + and not test_complete_future.done() + ): + test_complete_future.set_result(None) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "sched-simul-callbacks-test" + + # List entities and services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test service + run_test_service: UserService | None = None + for service in services: + if service.name == "run_simultaneous_callbacks_test": + run_test_service = service + break + + assert run_test_service is not None, ( + "run_simultaneous_callbacks_test service not found" + ) + + # Call the service to start the test + client.execute_service(run_test_service, {}) + + # Wait for test to complete + try: + await asyncio.wait_for(test_complete_future, timeout=30.0) + except asyncio.TimeoutError: + pytest.fail(f"Simultaneous callbacks test timed out. Stats: {test_stats}") + + # Check for any errors + assert len(test_stats["errors"]) == 0, ( + f"Errors detected: {test_stats['errors']}" + ) + + # Verify all callbacks executed using the final count from C++ + final_count = test_stats.get("final_count", 0) + assert final_count == test_stats["expected"], ( + f"Expected {test_stats['expected']} callbacks, but only {final_count} executed" + ) + + # The final_count is the authoritative count from the C++ component + assert final_count == 1000, ( + f"Expected 1000 executed callbacks but got {final_count}" + ) diff --git a/tests/integration/test_scheduler_string_lifetime.py b/tests/integration/test_scheduler_string_lifetime.py new file mode 100644 index 0000000000..4d77abd954 --- /dev/null +++ b/tests/integration/test_scheduler_string_lifetime.py @@ -0,0 +1,169 @@ +"""String lifetime test - verify scheduler handles string destruction correctly.""" + +import asyncio +from pathlib import Path +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_string_lifetime( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that scheduler correctly handles string lifetimes when strings go out of scope.""" + + # Get the absolute path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + # Create events for synchronization + test1_complete = asyncio.Event() + test2_complete = asyncio.Event() + test3_complete = asyncio.Event() + test4_complete = asyncio.Event() + test5_complete = asyncio.Event() + all_tests_complete = asyncio.Event() + + # Track test progress + test_stats = { + "tests_passed": 0, + "tests_failed": 0, + "errors": [], + "current_test": None, + "test_callbacks_executed": {}, + } + + def on_log_line(line: str) -> None: + # Track test-specific events + if "Test 1 complete" in line: + test1_complete.set() + elif "Test 2 complete" in line: + test2_complete.set() + elif "Test 3 complete" in line: + test3_complete.set() + elif "Test 4 complete" in line: + test4_complete.set() + elif "Test 5 complete" in line: + test5_complete.set() + + # Track individual callback executions + callback_match = re.search(r"Callback '(.+?)' executed", line) + if callback_match: + callback_name = callback_match.group(1) + test_stats["test_callbacks_executed"][callback_name] = True + + # Track test results from the C++ test output + if "Tests passed:" in line and "string_lifetime" in line: + # Extract the number from "Tests passed: 32" + match = re.search(r"Tests passed:\s*(\d+)", line) + if match: + test_stats["tests_passed"] = int(match.group(1)) + elif "Tests failed:" in line and "string_lifetime" in line: + match = re.search(r"Tests failed:\s*(\d+)", line) + if match: + test_stats["tests_failed"] = int(match.group(1)) + elif "ERROR" in line and "string_lifetime" in line: + test_stats["errors"].append(line) + + # Check for memory corruption indicators + if any( + indicator in line.lower() + for indicator in [ + "use after free", + "heap corruption", + "segfault", + "abort", + "assertion", + "sanitizer", + "bad memory", + "invalid pointer", + ] + ): + pytest.fail(f"Memory corruption detected: {line}") + + # Check for completion + if "String lifetime tests complete" in line: + all_tests_complete.set() + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-string-lifetime-test" + + # List entities and services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test services + test_services = {} + for service in services: + if service.name == "run_test1": + test_services["test1"] = service + elif service.name == "run_test2": + test_services["test2"] = service + elif service.name == "run_test3": + test_services["test3"] = service + elif service.name == "run_test4": + test_services["test4"] = service + elif service.name == "run_test5": + test_services["test5"] = service + elif service.name == "run_final_check": + test_services["final"] = service + + # Ensure all services are found + required_services = ["test1", "test2", "test3", "test4", "test5", "final"] + for service_name in required_services: + assert service_name in test_services, f"{service_name} service not found" + + # Run tests sequentially, waiting for each to complete + try: + # Test 1 + client.execute_service(test_services["test1"], {}) + await asyncio.wait_for(test1_complete.wait(), timeout=5.0) + + # Test 2 + client.execute_service(test_services["test2"], {}) + await asyncio.wait_for(test2_complete.wait(), timeout=5.0) + + # Test 3 + client.execute_service(test_services["test3"], {}) + await asyncio.wait_for(test3_complete.wait(), timeout=5.0) + + # Test 4 + client.execute_service(test_services["test4"], {}) + await asyncio.wait_for(test4_complete.wait(), timeout=5.0) + + # Test 5 + client.execute_service(test_services["test5"], {}) + await asyncio.wait_for(test5_complete.wait(), timeout=5.0) + + # Final check + client.execute_service(test_services["final"], {}) + await asyncio.wait_for(all_tests_complete.wait(), timeout=5.0) + + except asyncio.TimeoutError: + pytest.fail(f"String lifetime test timed out. Stats: {test_stats}") + + # Check for any errors + assert test_stats["tests_failed"] == 0, f"Tests failed: {test_stats['errors']}" + + # Verify we had the expected number of passing tests + assert test_stats["tests_passed"] == 30, ( + f"Expected exactly 30 tests to pass, but got {test_stats['tests_passed']}" + ) diff --git a/tests/integration/test_scheduler_string_name_stress.py b/tests/integration/test_scheduler_string_name_stress.py new file mode 100644 index 0000000000..3045842223 --- /dev/null +++ b/tests/integration/test_scheduler_string_name_stress.py @@ -0,0 +1,116 @@ +"""Stress test for heap scheduler with std::string names from multiple threads.""" + +import asyncio +from pathlib import Path +import re + +from aioesphomeapi import UserService +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_string_name_stress( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that set_timeout/set_interval with std::string names doesn't crash when called from multiple threads.""" + + # Get the absolute path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + # Create a future to signal test completion + loop = asyncio.get_running_loop() + test_complete_future: asyncio.Future[None] = loop.create_future() + + # Track executed callbacks and any crashes + executed_callbacks: set[int] = set() + error_messages: list[str] = [] + + def on_log_line(line: str) -> None: + # Check for crash indicators + if any( + indicator in line.lower() + for indicator in [ + "segfault", + "abort", + "assertion", + "heap corruption", + "use after free", + ] + ): + error_messages.append(line) + if not test_complete_future.done(): + test_complete_future.set_exception(Exception(f"Crash detected: {line}")) + return + + # Track executed callbacks + match = re.search(r"Executed string-named callback (\d+)", line) + if match: + callback_id = int(match.group(1)) + executed_callbacks.add(callback_id) + + # Check for completion + if ( + "String name stress test complete" in line + and not test_complete_future.done() + ): + test_complete_future.set_result(None) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "sched-string-name-stress" + + # List entities and services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test service + run_stress_test_service: UserService | None = None + for service in services: + if service.name == "run_string_name_stress_test": + run_stress_test_service = service + break + + assert run_stress_test_service is not None, ( + "run_string_name_stress_test service not found" + ) + + # Call the service to start the test + client.execute_service(run_stress_test_service, {}) + + # Wait for test to complete or crash + try: + await asyncio.wait_for(test_complete_future, timeout=30.0) + except asyncio.TimeoutError: + pytest.fail( + f"String name stress test timed out. Executed {len(executed_callbacks)} callbacks. " + f"This might indicate a deadlock." + ) + + # Verify no errors occurred (crashes already handled by exception) + assert not error_messages, f"Errors detected during test: {error_messages}" + + # Verify we executed all 1000 callbacks (10 threads × 100 callbacks each) + assert len(executed_callbacks) == 1000, ( + f"Expected 1000 callbacks but got {len(executed_callbacks)}" + ) + + # Verify each callback ID was executed exactly once + for i in range(1000): + assert i in executed_callbacks, f"Callback {i} was not executed" From 31f36df4ba1c42e59eac9e148c76ded1783739bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jul 2025 15:20:40 -0500 Subject: [PATCH 03/15] Reduce LightCall memory usage by 50 bytes per call (#9333) --- esphome/components/light/addressable_light.h | 6 +- .../components/light/esp_color_correction.h | 2 +- esphome/components/light/light_call.cpp | 383 ++++++++---------- esphome/components/light/light_call.h | 92 ++++- esphome/components/light/light_color_values.h | 8 +- esphome/components/light/light_state.h | 21 +- esphome/components/light/transformers.h | 4 +- tests/integration/fixtures/light_calls.yaml | 80 ++++ tests/integration/test_light_calls.py | 189 +++++++++ 9 files changed, 522 insertions(+), 263 deletions(-) create mode 100644 tests/integration/fixtures/light_calls.yaml create mode 100644 tests/integration/test_light_calls.py diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index 8302239d6a..baa4507d2f 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -97,12 +97,12 @@ class AddressableLight : public LightOutput, public Component { } virtual ESPColorView get_view_internal(int32_t index) const = 0; - bool effect_active_{false}; ESPColorCorrection correction_{}; + LightState *state_parent_{nullptr}; #ifdef USE_POWER_SUPPLY power_supply::PowerSupplyRequester power_; #endif - LightState *state_parent_{nullptr}; + bool effect_active_{false}; }; class AddressableLightTransformer : public LightTransitionTransformer { @@ -114,9 +114,9 @@ class AddressableLightTransformer : public LightTransitionTransformer { protected: AddressableLight &light_; - Color target_color_{}; float last_transition_progress_{0.0f}; float accumulated_alpha_{0.0f}; + Color target_color_{}; }; } // namespace light diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index 39ce5700c6..979a1acb07 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -69,8 +69,8 @@ class ESPColorCorrection { protected: uint8_t gamma_table_[256]; uint8_t gamma_reverse_table_[256]; - uint8_t local_brightness_{255}; Color max_brightness_; + uint8_t local_brightness_{255}; }; } // namespace light diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 78b0ac9feb..a3ffe22591 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -2,12 +2,28 @@ #include "light_call.h" #include "light_state.h" #include "esphome/core/log.h" +#include "esphome/core/optional.h" namespace esphome { namespace light { static const char *const TAG = "light"; +// Macro to reduce repetitive setter code +#define IMPLEMENT_LIGHT_CALL_SETTER(name, type, flag) \ + LightCall &LightCall::set_##name(optional(name)) { \ + if ((name).has_value()) { \ + this->name##_ = (name).value(); \ + } \ + this->set_flag_(flag, (name).has_value()); \ + return *this; \ + } \ + LightCall &LightCall::set_##name(type name) { \ + this->name##_ = name; \ + this->set_flag_(flag, true); \ + return *this; \ + } + static const LogString *color_mode_to_human(ColorMode color_mode) { if (color_mode == ColorMode::UNKNOWN) return LOG_STR("Unknown"); @@ -32,41 +48,43 @@ void LightCall::perform() { const char *name = this->parent_->get_name().c_str(); LightColorValues v = this->validate_(); - if (this->publish_) { + if (this->get_publish_()) { ESP_LOGD(TAG, "'%s' Setting:", name); // Only print color mode when it's being changed ColorMode current_color_mode = this->parent_->remote_values.get_color_mode(); - if (this->color_mode_.value_or(current_color_mode) != current_color_mode) { + ColorMode target_color_mode = this->has_color_mode() ? this->color_mode_ : current_color_mode; + if (target_color_mode != current_color_mode) { ESP_LOGD(TAG, " Color mode: %s", LOG_STR_ARG(color_mode_to_human(v.get_color_mode()))); } // Only print state when it's being changed bool current_state = this->parent_->remote_values.is_on(); - if (this->state_.value_or(current_state) != current_state) { + bool target_state = this->has_state() ? this->state_ : current_state; + if (target_state != current_state) { ESP_LOGD(TAG, " State: %s", ONOFF(v.is_on())); } - if (this->brightness_.has_value()) { + if (this->has_brightness()) { ESP_LOGD(TAG, " Brightness: %.0f%%", v.get_brightness() * 100.0f); } - if (this->color_brightness_.has_value()) { + if (this->has_color_brightness()) { ESP_LOGD(TAG, " Color brightness: %.0f%%", v.get_color_brightness() * 100.0f); } - if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { + if (this->has_red() || this->has_green() || this->has_blue()) { ESP_LOGD(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, v.get_blue() * 100.0f); } - if (this->white_.has_value()) { + if (this->has_white()) { ESP_LOGD(TAG, " White: %.0f%%", v.get_white() * 100.0f); } - if (this->color_temperature_.has_value()) { + if (this->has_color_temperature()) { ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature()); } - if (this->cold_white_.has_value() || this->warm_white_.has_value()) { + if (this->has_cold_white() || this->has_warm_white()) { ESP_LOGD(TAG, " Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f, v.get_warm_white() * 100.0f); } @@ -74,58 +92,57 @@ void LightCall::perform() { if (this->has_flash_()) { // FLASH - if (this->publish_) { - ESP_LOGD(TAG, " Flash length: %.1fs", *this->flash_length_ / 1e3f); + if (this->get_publish_()) { + ESP_LOGD(TAG, " Flash length: %.1fs", this->flash_length_ / 1e3f); } - this->parent_->start_flash_(v, *this->flash_length_, this->publish_); + this->parent_->start_flash_(v, this->flash_length_, this->get_publish_()); } else if (this->has_transition_()) { // TRANSITION - if (this->publish_) { - ESP_LOGD(TAG, " Transition length: %.1fs", *this->transition_length_ / 1e3f); + if (this->get_publish_()) { + ESP_LOGD(TAG, " Transition length: %.1fs", this->transition_length_ / 1e3f); } // Special case: Transition and effect can be set when turning off if (this->has_effect_()) { - if (this->publish_) { + if (this->get_publish_()) { ESP_LOGD(TAG, " Effect: 'None'"); } this->parent_->stop_effect_(); } - this->parent_->start_transition_(v, *this->transition_length_, this->publish_); + this->parent_->start_transition_(v, this->transition_length_, this->get_publish_()); } else if (this->has_effect_()) { // EFFECT - auto effect = this->effect_; const char *effect_s; - if (effect == 0u) { + if (this->effect_ == 0u) { effect_s = "None"; } else { - effect_s = this->parent_->effects_[*this->effect_ - 1]->get_name().c_str(); + effect_s = this->parent_->effects_[this->effect_ - 1]->get_name().c_str(); } - if (this->publish_) { + if (this->get_publish_()) { ESP_LOGD(TAG, " Effect: '%s'", effect_s); } - this->parent_->start_effect_(*this->effect_); + this->parent_->start_effect_(this->effect_); // Also set light color values when starting an effect // For example to turn off the light this->parent_->set_immediately_(v, true); } else { // INSTANT CHANGE - this->parent_->set_immediately_(v, this->publish_); + this->parent_->set_immediately_(v, this->get_publish_()); } if (!this->has_transition_()) { this->parent_->target_state_reached_callback_.call(); } - if (this->publish_) { + if (this->get_publish_()) { this->parent_->publish_state(); } - if (this->save_) { + if (this->get_save_()) { this->parent_->save_remote_values_(); } } @@ -135,82 +152,80 @@ LightColorValues LightCall::validate_() { auto traits = this->parent_->get_traits(); // Color mode check - if (this->color_mode_.has_value() && !traits.supports_color_mode(this->color_mode_.value())) { - ESP_LOGW(TAG, "'%s' does not support color mode %s", name, - LOG_STR_ARG(color_mode_to_human(this->color_mode_.value()))); - this->color_mode_.reset(); + if (this->has_color_mode() && !traits.supports_color_mode(this->color_mode_)) { + ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_))); + this->set_flag_(FLAG_HAS_COLOR_MODE, false); } // Ensure there is always a color mode set - if (!this->color_mode_.has_value()) { + if (!this->has_color_mode()) { this->color_mode_ = this->compute_color_mode_(); + this->set_flag_(FLAG_HAS_COLOR_MODE, true); } - auto color_mode = *this->color_mode_; + auto color_mode = this->color_mode_; // Transform calls that use non-native parameters for the current mode. this->transform_parameters_(); // Brightness exists check - if (this->brightness_.has_value() && *this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) { + if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) { ESP_LOGW(TAG, "'%s': setting brightness not supported", name); - this->brightness_.reset(); + this->set_flag_(FLAG_HAS_BRIGHTNESS, false); } // Transition length possible check - if (this->transition_length_.has_value() && *this->transition_length_ != 0 && - !(color_mode & ColorCapability::BRIGHTNESS)) { + if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) { ESP_LOGW(TAG, "'%s': transitions not supported", name); - this->transition_length_.reset(); + this->set_flag_(FLAG_HAS_TRANSITION, false); } // Color brightness exists check - if (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) { + if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) { ESP_LOGW(TAG, "'%s': color mode does not support setting RGB brightness", name); - this->color_brightness_.reset(); + this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, false); } // RGB exists check - if ((this->red_.has_value() && *this->red_ > 0.0f) || (this->green_.has_value() && *this->green_ > 0.0f) || - (this->blue_.has_value() && *this->blue_ > 0.0f)) { + if ((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) || + (this->has_blue() && this->blue_ > 0.0f)) { if (!(color_mode & ColorCapability::RGB)) { ESP_LOGW(TAG, "'%s': color mode does not support setting RGB color", name); - this->red_.reset(); - this->green_.reset(); - this->blue_.reset(); + this->set_flag_(FLAG_HAS_RED, false); + this->set_flag_(FLAG_HAS_GREEN, false); + this->set_flag_(FLAG_HAS_BLUE, false); } } // White value exists check - if (this->white_.has_value() && *this->white_ > 0.0f && + if (this->has_white() && this->white_ > 0.0f && !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) { ESP_LOGW(TAG, "'%s': color mode does not support setting white value", name); - this->white_.reset(); + this->set_flag_(FLAG_HAS_WHITE, false); } // Color temperature exists check - if (this->color_temperature_.has_value() && + if (this->has_color_temperature() && !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) { ESP_LOGW(TAG, "'%s': color mode does not support setting color temperature", name); - this->color_temperature_.reset(); + this->set_flag_(FLAG_HAS_COLOR_TEMPERATURE, false); } // Cold/warm white value exists check - if ((this->cold_white_.has_value() && *this->cold_white_ > 0.0f) || - (this->warm_white_.has_value() && *this->warm_white_ > 0.0f)) { + if ((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) { if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) { ESP_LOGW(TAG, "'%s': color mode does not support setting cold/warm white value", name); - this->cold_white_.reset(); - this->warm_white_.reset(); + this->set_flag_(FLAG_HAS_COLD_WHITE, false); + this->set_flag_(FLAG_HAS_WARM_WHITE, false); } } #define VALIDATE_RANGE_(name_, upper_name, min, max) \ - if (name_##_.has_value()) { \ - auto val = *name_##_; \ + if (this->has_##name_()) { \ + auto val = this->name_##_; \ if (val < (min) || val > (max)) { \ ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_LITERAL(upper_name), val, \ (min), (max)); \ - name_##_ = clamp(val, (min), (max)); \ + this->name_##_ = clamp(val, (min), (max)); \ } \ } #define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name, 0.0f, 1.0f) @@ -227,110 +242,116 @@ LightColorValues LightCall::validate_() { VALIDATE_RANGE_(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds()) // Flag whether an explicit turn off was requested, in which case we'll also stop the effect. - bool explicit_turn_off_request = this->state_.has_value() && !*this->state_; + bool explicit_turn_off_request = this->has_state() && !this->state_; // Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on). - if (this->brightness_.has_value() && *this->brightness_ == 0.0f) { - this->state_ = optional(false); - this->brightness_ = optional(1.0f); + if (this->has_brightness() && this->brightness_ == 0.0f) { + this->state_ = false; + this->set_flag_(FLAG_HAS_STATE, true); + this->brightness_ = 1.0f; } // Set color brightness to 100% if currently zero and a color is set. - if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - if (!this->color_brightness_.has_value() && this->parent_->remote_values.get_color_brightness() == 0.0f) - this->color_brightness_ = optional(1.0f); + if (this->has_red() || this->has_green() || this->has_blue()) { + if (!this->has_color_brightness() && this->parent_->remote_values.get_color_brightness() == 0.0f) { + this->color_brightness_ = 1.0f; + this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, true); + } } // Create color values for the light with this call applied. auto v = this->parent_->remote_values; - if (this->color_mode_.has_value()) - v.set_color_mode(*this->color_mode_); - if (this->state_.has_value()) - v.set_state(*this->state_); - if (this->brightness_.has_value()) - v.set_brightness(*this->brightness_); - if (this->color_brightness_.has_value()) - v.set_color_brightness(*this->color_brightness_); - if (this->red_.has_value()) - v.set_red(*this->red_); - if (this->green_.has_value()) - v.set_green(*this->green_); - if (this->blue_.has_value()) - v.set_blue(*this->blue_); - if (this->white_.has_value()) - v.set_white(*this->white_); - if (this->color_temperature_.has_value()) - v.set_color_temperature(*this->color_temperature_); - if (this->cold_white_.has_value()) - v.set_cold_white(*this->cold_white_); - if (this->warm_white_.has_value()) - v.set_warm_white(*this->warm_white_); + if (this->has_color_mode()) + v.set_color_mode(this->color_mode_); + if (this->has_state()) + v.set_state(this->state_); + if (this->has_brightness()) + v.set_brightness(this->brightness_); + if (this->has_color_brightness()) + v.set_color_brightness(this->color_brightness_); + if (this->has_red()) + v.set_red(this->red_); + if (this->has_green()) + v.set_green(this->green_); + if (this->has_blue()) + v.set_blue(this->blue_); + if (this->has_white()) + v.set_white(this->white_); + if (this->has_color_temperature()) + v.set_color_temperature(this->color_temperature_); + if (this->has_cold_white()) + v.set_cold_white(this->cold_white_); + if (this->has_warm_white()) + v.set_warm_white(this->warm_white_); v.normalize_color(); // Flash length check - if (this->has_flash_() && *this->flash_length_ == 0) { + if (this->has_flash_() && this->flash_length_ == 0) { ESP_LOGW(TAG, "'%s': flash length must be greater than zero", name); - this->flash_length_.reset(); + this->set_flag_(FLAG_HAS_FLASH, false); } // validate transition length/flash length/effect not used at the same time bool supports_transition = color_mode & ColorCapability::BRIGHTNESS; // If effect is already active, remove effect start - if (this->has_effect_() && *this->effect_ == this->parent_->active_effect_index_) { - this->effect_.reset(); + if (this->has_effect_() && this->effect_ == this->parent_->active_effect_index_) { + this->set_flag_(FLAG_HAS_EFFECT, false); } // validate effect index - if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) { - ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, *this->effect_); - this->effect_.reset(); + if (this->has_effect_() && this->effect_ > this->parent_->effects_.size()) { + ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, this->effect_); + this->set_flag_(FLAG_HAS_EFFECT, false); } if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { ESP_LOGW(TAG, "'%s': effect cannot be used with transition/flash", name); - this->transition_length_.reset(); - this->flash_length_.reset(); + this->set_flag_(FLAG_HAS_TRANSITION, false); + this->set_flag_(FLAG_HAS_FLASH, false); } if (this->has_flash_() && this->has_transition_()) { ESP_LOGW(TAG, "'%s': flash cannot be used with transition", name); - this->transition_length_.reset(); + this->set_flag_(FLAG_HAS_TRANSITION, false); } - if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || *this->effect_ == 0) && + if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || this->effect_ == 0) && supports_transition) { // nothing specified and light supports transitions, set default transition length this->transition_length_ = this->parent_->default_transition_length_; + this->set_flag_(FLAG_HAS_TRANSITION, true); } - if (this->transition_length_.value_or(0) == 0) { + if (this->has_transition_() && this->transition_length_ == 0) { // 0 transition is interpreted as no transition (instant change) - this->transition_length_.reset(); + this->set_flag_(FLAG_HAS_TRANSITION, false); } if (this->has_transition_() && !supports_transition) { ESP_LOGW(TAG, "'%s': transitions not supported", name); - this->transition_length_.reset(); + this->set_flag_(FLAG_HAS_TRANSITION, false); } // If not a flash and turning the light off, then disable the light // Do not use light color values directly, so that effects can set 0% brightness // Reason: When user turns off the light in frontend, the effect should also stop - if (!this->has_flash_() && !this->state_.value_or(v.is_on())) { + bool target_state = this->has_state() ? this->state_ : v.is_on(); + if (!this->has_flash_() && !target_state) { if (this->has_effect_()) { ESP_LOGW(TAG, "'%s': cannot start effect when turning off", name); - this->effect_.reset(); + this->set_flag_(FLAG_HAS_EFFECT, false); } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) { // Auto turn off effect this->effect_ = 0; + this->set_flag_(FLAG_HAS_EFFECT, true); } } // Disable saving for flashes if (this->has_flash_()) - this->save_ = false; + this->set_flag_(FLAG_SAVE, false); return v; } @@ -343,24 +364,27 @@ void LightCall::transform_parameters_() { // - RGBWW lights with color_interlock=true, which also sets "brightness" and // "color_temperature" (without color_interlock, CW/WW are set directly) // - Legacy Home Assistant (pre-colormode), which sets "white" and "color_temperature" - if (((this->white_.has_value() && *this->white_ > 0.0f) || this->color_temperature_.has_value()) && // - (*this->color_mode_ & ColorCapability::COLD_WARM_WHITE) && // - !(*this->color_mode_ & ColorCapability::WHITE) && // - !(*this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && // + if (((this->has_white() && this->white_ > 0.0f) || this->has_color_temperature()) && // + (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) && // + !(this->color_mode_ & ColorCapability::WHITE) && // + !(this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && // traits.get_min_mireds() > 0.0f && traits.get_max_mireds() > 0.0f) { ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values", this->parent_->get_name().c_str()); - if (this->color_temperature_.has_value()) { - const float color_temp = clamp(*this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds()); + if (this->has_color_temperature()) { + const float color_temp = clamp(this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds()); const float ww_fraction = (color_temp - traits.get_min_mireds()) / (traits.get_max_mireds() - traits.get_min_mireds()); const float cw_fraction = 1.0f - ww_fraction; const float max_cw_ww = std::max(ww_fraction, cw_fraction); this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, this->parent_->get_gamma_correct()); this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, this->parent_->get_gamma_correct()); + this->set_flag_(FLAG_HAS_COLD_WHITE, true); + this->set_flag_(FLAG_HAS_WARM_WHITE, true); } - if (this->white_.has_value()) { - this->brightness_ = *this->white_; + if (this->has_white()) { + this->brightness_ = this->white_; + this->set_flag_(FLAG_HAS_BRIGHTNESS, true); } } } @@ -378,7 +402,7 @@ ColorMode LightCall::compute_color_mode_() { // Don't change if the light is being turned off. ColorMode current_mode = this->parent_->remote_values.get_color_mode(); - if (this->state_.has_value() && !*this->state_) + if (this->has_state() && !this->state_) return current_mode; // If no color mode is specified, we try to guess the color mode. This is needed for backward compatibility to @@ -411,12 +435,12 @@ ColorMode LightCall::compute_color_mode_() { return color_mode; } std::set LightCall::get_suitable_color_modes_() { - bool has_white = this->white_.has_value() && *this->white_ > 0.0f; - bool has_ct = this->color_temperature_.has_value(); - bool has_cwww = (this->cold_white_.has_value() && *this->cold_white_ > 0.0f) || - (this->warm_white_.has_value() && *this->warm_white_ > 0.0f); - bool has_rgb = (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f) || - (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()); + bool has_white = this->has_white() && this->white_ > 0.0f; + bool has_ct = this->has_color_temperature(); + bool has_cwww = + (this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f); + bool has_rgb = (this->has_color_brightness() && this->color_brightness_ > 0.0f) || + (this->has_red() || this->has_green() || this->has_blue()); #define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3) #define ENTRY(white, ct, cwww, rgb, ...) \ @@ -491,7 +515,7 @@ LightCall &LightCall::from_light_color_values(const LightColorValues &values) { return *this; } ColorMode LightCall::get_active_color_mode_() { - return this->color_mode_.value_or(this->parent_->remote_values.get_color_mode()); + return this->has_color_mode() ? this->color_mode_ : this->parent_->remote_values.get_color_mode(); } LightCall &LightCall::set_transition_length_if_supported(uint32_t transition_length) { if (this->get_active_color_mode_() & ColorCapability::BRIGHTNESS) @@ -505,7 +529,7 @@ LightCall &LightCall::set_brightness_if_supported(float brightness) { } LightCall &LightCall::set_color_mode_if_supported(ColorMode color_mode) { if (this->parent_->get_traits().supports_color_mode(color_mode)) - this->color_mode_ = color_mode; + this->set_color_mode(color_mode); return *this; } LightCall &LightCall::set_color_brightness_if_supported(float brightness) { @@ -549,110 +573,19 @@ LightCall &LightCall::set_warm_white_if_supported(float warm_white) { this->set_warm_white(warm_white); return *this; } -LightCall &LightCall::set_state(optional state) { - this->state_ = state; - return *this; -} -LightCall &LightCall::set_state(bool state) { - this->state_ = state; - return *this; -} -LightCall &LightCall::set_transition_length(optional transition_length) { - this->transition_length_ = transition_length; - return *this; -} -LightCall &LightCall::set_transition_length(uint32_t transition_length) { - this->transition_length_ = transition_length; - return *this; -} -LightCall &LightCall::set_flash_length(optional flash_length) { - this->flash_length_ = flash_length; - return *this; -} -LightCall &LightCall::set_flash_length(uint32_t flash_length) { - this->flash_length_ = flash_length; - return *this; -} -LightCall &LightCall::set_brightness(optional brightness) { - this->brightness_ = brightness; - return *this; -} -LightCall &LightCall::set_brightness(float brightness) { - this->brightness_ = brightness; - return *this; -} -LightCall &LightCall::set_color_mode(optional color_mode) { - this->color_mode_ = color_mode; - return *this; -} -LightCall &LightCall::set_color_mode(ColorMode color_mode) { - this->color_mode_ = color_mode; - return *this; -} -LightCall &LightCall::set_color_brightness(optional brightness) { - this->color_brightness_ = brightness; - return *this; -} -LightCall &LightCall::set_color_brightness(float brightness) { - this->color_brightness_ = brightness; - return *this; -} -LightCall &LightCall::set_red(optional red) { - this->red_ = red; - return *this; -} -LightCall &LightCall::set_red(float red) { - this->red_ = red; - return *this; -} -LightCall &LightCall::set_green(optional green) { - this->green_ = green; - return *this; -} -LightCall &LightCall::set_green(float green) { - this->green_ = green; - return *this; -} -LightCall &LightCall::set_blue(optional blue) { - this->blue_ = blue; - return *this; -} -LightCall &LightCall::set_blue(float blue) { - this->blue_ = blue; - return *this; -} -LightCall &LightCall::set_white(optional white) { - this->white_ = white; - return *this; -} -LightCall &LightCall::set_white(float white) { - this->white_ = white; - return *this; -} -LightCall &LightCall::set_color_temperature(optional color_temperature) { - this->color_temperature_ = color_temperature; - return *this; -} -LightCall &LightCall::set_color_temperature(float color_temperature) { - this->color_temperature_ = color_temperature; - return *this; -} -LightCall &LightCall::set_cold_white(optional cold_white) { - this->cold_white_ = cold_white; - return *this; -} -LightCall &LightCall::set_cold_white(float cold_white) { - this->cold_white_ = cold_white; - return *this; -} -LightCall &LightCall::set_warm_white(optional warm_white) { - this->warm_white_ = warm_white; - return *this; -} -LightCall &LightCall::set_warm_white(float warm_white) { - this->warm_white_ = warm_white; - return *this; -} +IMPLEMENT_LIGHT_CALL_SETTER(state, bool, FLAG_HAS_STATE) +IMPLEMENT_LIGHT_CALL_SETTER(transition_length, uint32_t, FLAG_HAS_TRANSITION) +IMPLEMENT_LIGHT_CALL_SETTER(flash_length, uint32_t, FLAG_HAS_FLASH) +IMPLEMENT_LIGHT_CALL_SETTER(brightness, float, FLAG_HAS_BRIGHTNESS) +IMPLEMENT_LIGHT_CALL_SETTER(color_mode, ColorMode, FLAG_HAS_COLOR_MODE) +IMPLEMENT_LIGHT_CALL_SETTER(color_brightness, float, FLAG_HAS_COLOR_BRIGHTNESS) +IMPLEMENT_LIGHT_CALL_SETTER(red, float, FLAG_HAS_RED) +IMPLEMENT_LIGHT_CALL_SETTER(green, float, FLAG_HAS_GREEN) +IMPLEMENT_LIGHT_CALL_SETTER(blue, float, FLAG_HAS_BLUE) +IMPLEMENT_LIGHT_CALL_SETTER(white, float, FLAG_HAS_WHITE) +IMPLEMENT_LIGHT_CALL_SETTER(color_temperature, float, FLAG_HAS_COLOR_TEMPERATURE) +IMPLEMENT_LIGHT_CALL_SETTER(cold_white, float, FLAG_HAS_COLD_WHITE) +IMPLEMENT_LIGHT_CALL_SETTER(warm_white, float, FLAG_HAS_WARM_WHITE) LightCall &LightCall::set_effect(optional effect) { if (effect.has_value()) this->set_effect(*effect); @@ -660,18 +593,22 @@ LightCall &LightCall::set_effect(optional effect) { } LightCall &LightCall::set_effect(uint32_t effect_number) { this->effect_ = effect_number; + this->set_flag_(FLAG_HAS_EFFECT, true); return *this; } LightCall &LightCall::set_effect(optional effect_number) { - this->effect_ = effect_number; + if (effect_number.has_value()) { + this->effect_ = effect_number.value(); + } + this->set_flag_(FLAG_HAS_EFFECT, effect_number.has_value()); return *this; } LightCall &LightCall::set_publish(bool publish) { - this->publish_ = publish; + this->set_flag_(FLAG_PUBLISH, publish); return *this; } LightCall &LightCall::set_save(bool save) { - this->save_ = save; + this->set_flag_(FLAG_SAVE, save); return *this; } LightCall &LightCall::set_rgb(float red, float green, float blue) { diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index bca2ac7b07..7e04e1a767 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -1,6 +1,5 @@ #pragma once -#include "esphome/core/optional.h" #include "light_color_values.h" #include @@ -10,6 +9,11 @@ namespace light { class LightState; /** This class represents a requested change in a light state. + * + * Light state changes are tracked using a bitfield flags_ to minimize memory usage. + * Each possible light property has a flag indicating whether it has been set. + * This design keeps LightCall at ~56 bytes to minimize heap fragmentation on + * ESP8266 and other memory-constrained devices. */ class LightCall { public: @@ -131,6 +135,19 @@ class LightCall { /// Set whether this light call should trigger a save state to recover them at startup.. LightCall &set_save(bool save); + // Getter methods to check if values are set + bool has_state() const { return (flags_ & FLAG_HAS_STATE) != 0; } + bool has_brightness() const { return (flags_ & FLAG_HAS_BRIGHTNESS) != 0; } + bool has_color_brightness() const { return (flags_ & FLAG_HAS_COLOR_BRIGHTNESS) != 0; } + bool has_red() const { return (flags_ & FLAG_HAS_RED) != 0; } + bool has_green() const { return (flags_ & FLAG_HAS_GREEN) != 0; } + bool has_blue() const { return (flags_ & FLAG_HAS_BLUE) != 0; } + bool has_white() const { return (flags_ & FLAG_HAS_WHITE) != 0; } + bool has_color_temperature() const { return (flags_ & FLAG_HAS_COLOR_TEMPERATURE) != 0; } + bool has_cold_white() const { return (flags_ & FLAG_HAS_COLD_WHITE) != 0; } + bool has_warm_white() const { return (flags_ & FLAG_HAS_WARM_WHITE) != 0; } + bool has_color_mode() const { return (flags_ & FLAG_HAS_COLOR_MODE) != 0; } + /** Set the RGB color of the light by RGB values. * * Please note that this only changes the color of the light, not the brightness. @@ -170,27 +187,62 @@ class LightCall { /// Some color modes also can be set using non-native parameters, transform those calls. void transform_parameters_(); - bool has_transition_() { return this->transition_length_.has_value(); } - bool has_flash_() { return this->flash_length_.has_value(); } - bool has_effect_() { return this->effect_.has_value(); } + // Bitfield flags - each flag indicates whether a corresponding value has been set. + enum FieldFlags : uint16_t { + FLAG_HAS_STATE = 1 << 0, + FLAG_HAS_TRANSITION = 1 << 1, + FLAG_HAS_FLASH = 1 << 2, + FLAG_HAS_EFFECT = 1 << 3, + FLAG_HAS_BRIGHTNESS = 1 << 4, + FLAG_HAS_COLOR_BRIGHTNESS = 1 << 5, + FLAG_HAS_RED = 1 << 6, + FLAG_HAS_GREEN = 1 << 7, + FLAG_HAS_BLUE = 1 << 8, + FLAG_HAS_WHITE = 1 << 9, + FLAG_HAS_COLOR_TEMPERATURE = 1 << 10, + FLAG_HAS_COLD_WHITE = 1 << 11, + FLAG_HAS_WARM_WHITE = 1 << 12, + FLAG_HAS_COLOR_MODE = 1 << 13, + FLAG_PUBLISH = 1 << 14, + FLAG_SAVE = 1 << 15, + }; + + bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; } + bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; } + bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; } + bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; } + bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; } + + // Helper to set flag + void set_flag_(FieldFlags flag, bool value) { + if (value) { + this->flags_ |= flag; + } else { + this->flags_ &= ~flag; + } + } LightState *parent_; - optional state_; - optional transition_length_; - optional flash_length_; - optional color_mode_; - optional brightness_; - optional color_brightness_; - optional red_; - optional green_; - optional blue_; - optional white_; - optional color_temperature_; - optional cold_white_; - optional warm_white_; - optional effect_; - bool publish_{true}; - bool save_{true}; + + // Light state values - use flags_ to check if a value has been set. + // Group 4-byte aligned members first + uint32_t transition_length_; + uint32_t flash_length_; + uint32_t effect_; + float brightness_; + float color_brightness_; + float red_; + float green_; + float blue_; + float white_; + float color_temperature_; + float cold_white_; + float warm_white_; + + // Smaller members at the end for better packing + uint16_t flags_{FLAG_PUBLISH | FLAG_SAVE}; // Tracks which values are set + ColorMode color_mode_; + bool state_; }; } // namespace light diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index d8eaa6ae24..5653a8d2a5 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -46,8 +46,7 @@ class LightColorValues { public: /// Construct the LightColorValues with all attributes enabled, but state set to off. LightColorValues() - : color_mode_(ColorMode::UNKNOWN), - state_(0.0f), + : state_(0.0f), brightness_(1.0f), color_brightness_(1.0f), red_(1.0f), @@ -56,7 +55,8 @@ class LightColorValues { white_(1.0f), color_temperature_{0.0f}, cold_white_{1.0f}, - warm_white_{1.0f} {} + warm_white_{1.0f}, + color_mode_(ColorMode::UNKNOWN) {} LightColorValues(ColorMode color_mode, float state, float brightness, float color_brightness, float red, float green, float blue, float white, float color_temperature, float cold_white, float warm_white) { @@ -292,7 +292,6 @@ class LightColorValues { void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); } protected: - ColorMode color_mode_; float state_; ///< ON / OFF, float for transition float brightness_; float color_brightness_; @@ -303,6 +302,7 @@ class LightColorValues { float color_temperature_; ///< Color Temperature in Mired float cold_white_; float warm_white_; + ColorMode color_mode_; }; } // namespace light diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index f21fb8a06e..72cb99223e 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -31,9 +31,7 @@ enum LightRestoreMode : uint8_t { struct LightStateRTCState { LightStateRTCState(ColorMode color_mode, bool state, float brightness, float color_brightness, float red, float green, float blue, float white, float color_temp, float cold_white, float warm_white) - : color_mode(color_mode), - state(state), - brightness(brightness), + : brightness(brightness), color_brightness(color_brightness), red(red), green(green), @@ -41,10 +39,12 @@ struct LightStateRTCState { white(white), color_temp(color_temp), cold_white(cold_white), - warm_white(warm_white) {} + warm_white(warm_white), + effect(0), + color_mode(color_mode), + state(state) {} LightStateRTCState() = default; - ColorMode color_mode{ColorMode::UNKNOWN}; - bool state{false}; + // Group 4-byte aligned members first float brightness{1.0f}; float color_brightness{1.0f}; float red{1.0f}; @@ -55,6 +55,9 @@ struct LightStateRTCState { float cold_white{1.0f}; float warm_white{1.0f}; uint32_t effect{0}; + // Group smaller members at the end + ColorMode color_mode{ColorMode::UNKNOWN}; + bool state{false}; }; /** This class represents the communication layer between the front-end MQTT layer and the @@ -216,6 +219,8 @@ class LightState : public EntityBase, public Component { std::unique_ptr transformer_{nullptr}; /// List of effects for this light. std::vector effects_; + /// Object used to store the persisted values of the light. + ESPPreferenceObject rtc_; /// Value for storing the index of the currently active effect. 0 if no effect is active uint32_t active_effect_index_{}; /// Default transition length for all transitions in ms. @@ -224,15 +229,11 @@ class LightState : public EntityBase, public Component { uint32_t flash_transition_length_{}; /// Gamma correction factor for the light. float gamma_correct_{}; - /// Whether the light value should be written in the next cycle. bool next_write_{true}; // for effects, true if a transformer (transition) is active. bool is_transformer_active_ = false; - /// Object used to store the persisted values of the light. - ESPPreferenceObject rtc_; - /** Callback to call when new values for the frontend are available. * * "Remote values" are light color values that are reported to the frontend and have a lower diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index a557bd39b1..8d49acff97 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -59,9 +59,9 @@ class LightTransitionTransformer : public LightTransformer { // transition from 0 to 1 on x = [0, 1] static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } - bool changing_color_mode_{false}; LightColorValues end_values_{}; LightColorValues intermediate_values_{}; + bool changing_color_mode_{false}; }; class LightFlashTransformer : public LightTransformer { @@ -117,8 +117,8 @@ class LightFlashTransformer : public LightTransformer { protected: LightState &state_; - uint32_t transition_length_; std::unique_ptr transformer_{nullptr}; + uint32_t transition_length_; bool begun_lightstate_restore_; }; diff --git a/tests/integration/fixtures/light_calls.yaml b/tests/integration/fixtures/light_calls.yaml new file mode 100644 index 0000000000..d692a11765 --- /dev/null +++ b/tests/integration/fixtures/light_calls.yaml @@ -0,0 +1,80 @@ +esphome: + name: light-calls-test +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +# Test outputs for RGBCW light +output: + - platform: template + id: test_red + type: float + write_action: + - logger.log: + format: "Red output: %.2f" + args: [state] + - platform: template + id: test_green + type: float + write_action: + - logger.log: + format: "Green output: %.2f" + args: [state] + - platform: template + id: test_blue + type: float + write_action: + - logger.log: + format: "Blue output: %.2f" + args: [state] + - platform: template + id: test_cold_white + type: float + write_action: + - logger.log: + format: "Cold white output: %.2f" + args: [state] + - platform: template + id: test_warm_white + type: float + write_action: + - logger.log: + format: "Warm white output: %.2f" + args: [state] + +light: + - platform: rgbww + name: "Test RGBCW Light" + id: test_light + red: test_red + green: test_green + blue: test_blue + cold_white: test_cold_white + warm_white: test_warm_white + cold_white_color_temperature: 6536 K + warm_white_color_temperature: 2000 K + constant_brightness: true + effects: + - random: + name: "Random Effect" + transition_length: 100ms + update_interval: 200ms + - strobe: + name: "Strobe Effect" + - pulse: + name: "Pulse Effect" + transition_length: 100ms + + # Additional lights to test memory with multiple instances + - platform: rgb + name: "Test RGB Light" + id: test_rgb_light + red: test_red + green: test_green + blue: test_blue + + - platform: binary + name: "Test Binary Light" + id: test_binary_light + output: test_red diff --git a/tests/integration/test_light_calls.py b/tests/integration/test_light_calls.py new file mode 100644 index 0000000000..8ecb77fb99 --- /dev/null +++ b/tests/integration/test_light_calls.py @@ -0,0 +1,189 @@ +"""Integration test for all light call combinations. + +Tests that LightCall handles all possible light operations correctly +including RGB, color temperature, effects, transitions, and flash. +""" + +import asyncio +from typing import Any + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_light_calls( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test all possible LightCall operations and combinations.""" + async with run_compiled(yaml_config), api_client_connected() as client: + # Track state changes with futures + state_futures: dict[int, asyncio.Future[Any]] = {} + states: dict[int, Any] = {} + + def on_state(state: Any) -> None: + states[state.key] = state + if state.key in state_futures and not state_futures[state.key].done(): + state_futures[state.key].set_result(state) + + client.subscribe_states(on_state) + + # Get the light entities + entities = await client.list_entities_services() + lights = [e for e in entities[0] if e.object_id.startswith("test_")] + assert len(lights) >= 2 # Should have RGBCW and RGB lights + + rgbcw_light = next(light for light in lights if "RGBCW" in light.name) + rgb_light = next(light for light in lights if "RGB Light" in light.name) + + async def wait_for_state_change(key: int, timeout: float = 1.0) -> Any: + """Wait for a state change for the given entity key.""" + loop = asyncio.get_event_loop() + state_futures[key] = loop.create_future() + try: + return await asyncio.wait_for(state_futures[key], timeout) + finally: + state_futures.pop(key, None) + + # Test all individual parameters first + + # Test 1: state only + client.light_command(key=rgbcw_light.key, state=True) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + + # Test 2: brightness only + client.light_command(key=rgbcw_light.key, brightness=0.5) + state = await wait_for_state_change(rgbcw_light.key) + assert state.brightness == pytest.approx(0.5) + + # Test 3: color_brightness only + client.light_command(key=rgbcw_light.key, color_brightness=0.8) + state = await wait_for_state_change(rgbcw_light.key) + assert state.color_brightness == pytest.approx(0.8) + + # Test 4-7: RGB values must be set together via rgb parameter + client.light_command(key=rgbcw_light.key, rgb=(0.7, 0.3, 0.9)) + state = await wait_for_state_change(rgbcw_light.key) + assert state.red == pytest.approx(0.7, abs=0.1) + assert state.green == pytest.approx(0.3, abs=0.1) + assert state.blue == pytest.approx(0.9, abs=0.1) + + # Test 7: white value + client.light_command(key=rgbcw_light.key, white=0.6) + state = await wait_for_state_change(rgbcw_light.key) + # White might need more tolerance or might not be directly settable + if hasattr(state, "white"): + assert state.white == pytest.approx(0.6, abs=0.1) + + # Test 8: color_temperature only + client.light_command(key=rgbcw_light.key, color_temperature=300) + state = await wait_for_state_change(rgbcw_light.key) + assert state.color_temperature == pytest.approx(300) + + # Test 9: cold_white only + client.light_command(key=rgbcw_light.key, cold_white=0.8) + state = await wait_for_state_change(rgbcw_light.key) + assert state.cold_white == pytest.approx(0.8) + + # Test 10: warm_white only + client.light_command(key=rgbcw_light.key, warm_white=0.2) + state = await wait_for_state_change(rgbcw_light.key) + assert state.warm_white == pytest.approx(0.2) + + # Test 11: transition_length with state change + client.light_command(key=rgbcw_light.key, state=False, transition_length=0.1) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is False + + # Test 12: flash_length + client.light_command(key=rgbcw_light.key, state=True, flash_length=0.2) + state = await wait_for_state_change(rgbcw_light.key) + # Flash starts + assert state.state is True + # Wait for flash to end + state = await wait_for_state_change(rgbcw_light.key) + + # Test 13: effect only + # First ensure light is on + client.light_command(key=rgbcw_light.key, state=True) + state = await wait_for_state_change(rgbcw_light.key) + # Now set effect + client.light_command(key=rgbcw_light.key, effect="Random Effect") + state = await wait_for_state_change(rgbcw_light.key) + assert state.effect == "Random Effect" + + # Test 14: stop effect + client.light_command(key=rgbcw_light.key, effect="None") + state = await wait_for_state_change(rgbcw_light.key) + assert state.effect == "None" + + # Test 15: color_mode parameter + client.light_command( + key=rgbcw_light.key, state=True, color_mode=5 + ) # COLD_WARM_WHITE + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + + # Now test common combinations + + # Test 16: RGB combination (set_rgb) - RGB values get normalized + client.light_command(key=rgbcw_light.key, rgb=(1.0, 0.0, 0.5)) + state = await wait_for_state_change(rgbcw_light.key) + # RGB values get normalized - in this case red is already 1.0 + assert state.red == pytest.approx(1.0, abs=0.1) + assert state.green == pytest.approx(0.0, abs=0.1) + assert state.blue == pytest.approx(0.5, abs=0.1) + + # Test 17: Multiple RGB changes to test transitions + client.light_command(key=rgbcw_light.key, rgb=(0.2, 0.8, 0.4)) + state = await wait_for_state_change(rgbcw_light.key) + # RGB values get normalized so green (highest) becomes 1.0 + # Expected: (0.2/0.8, 0.8/0.8, 0.4/0.8) = (0.25, 1.0, 0.5) + assert state.red == pytest.approx(0.25, abs=0.01) + assert state.green == pytest.approx(1.0, abs=0.01) + assert state.blue == pytest.approx(0.5, abs=0.01) + + # Test 18: State + brightness + transition + client.light_command( + key=rgbcw_light.key, state=True, brightness=0.7, transition_length=0.1 + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + assert state.brightness == pytest.approx(0.7) + + # Test 19: RGB + brightness + color_brightness + client.light_command( + key=rgb_light.key, + state=True, + brightness=0.8, + color_brightness=0.9, + rgb=(0.2, 0.4, 0.6), + ) + state = await wait_for_state_change(rgb_light.key) + assert state.state is True + assert state.brightness == pytest.approx(0.8) + + # Test 20: Color temp + cold/warm white + client.light_command( + key=rgbcw_light.key, color_temperature=250, cold_white=0.7, warm_white=0.3 + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.color_temperature == pytest.approx(250) + + # Test 21: Turn RGB light off + client.light_command(key=rgb_light.key, state=False) + state = await wait_for_state_change(rgb_light.key) + assert state.state is False + + # Final cleanup - turn all lights off + for light in lights: + client.light_command( + key=light.key, + state=False, + ) + state = await wait_for_state_change(light.key) + assert state.state is False From 053feb5e3b4a288bae55920f998b06af5094ac60 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jul 2025 15:22:40 -0500 Subject: [PATCH 04/15] Optimize entity icon memory usage with USE_ENTITY_ICON flag (#9337) --- esphome/core/defines.h | 1 + esphome/core/entity_base.cpp | 12 ++- esphome/core/entity_base.h | 2 + esphome/core/entity_helpers.py | 3 + tests/integration/fixtures/entity_icon.yaml | 78 +++++++++++++++++ tests/integration/test_entity_icon.py | 97 +++++++++++++++++++++ 6 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 tests/integration/fixtures/entity_icon.yaml create mode 100644 tests/integration/test_entity_icon.py diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 4115b97391..d73009436b 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -33,6 +33,7 @@ #define USE_DEEP_SLEEP #define USE_DEVICES #define USE_DISPLAY +#define USE_ENTITY_ICON #define USE_ESP32_IMPROV_STATE_CALLBACK #define USE_EVENT #define USE_FAN diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 6afd02ff65..2ea9c77a3e 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -27,12 +27,22 @@ void EntityBase::set_name(const char *name) { // Entity Icon std::string EntityBase::get_icon() const { +#ifdef USE_ENTITY_ICON if (this->icon_c_str_ == nullptr) { return ""; } return this->icon_c_str_; +#else + return ""; +#endif +} +void EntityBase::set_icon(const char *icon) { +#ifdef USE_ENTITY_ICON + this->icon_c_str_ = icon; +#else + // No-op when USE_ENTITY_ICON is not defined +#endif } -void EntityBase::set_icon(const char *icon) { this->icon_c_str_ = icon; } // Entity Object ID std::string EntityBase::get_object_id() const { diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 4819b66108..00b1264ed0 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -80,7 +80,9 @@ class EntityBase { StringRef name_; const char *object_id_c_str_{nullptr}; +#ifdef USE_ENTITY_ICON const char *icon_c_str_{nullptr}; +#endif uint32_t object_id_hash_{}; #ifdef USE_DEVICES Device *device_{}; diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 2442fbca4b..a3244856a2 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -1,6 +1,7 @@ from collections.abc import Callable import logging +import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( CONF_DEVICE_ID, @@ -108,6 +109,8 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: if CONF_INTERNAL in config: add(var.set_internal(config[CONF_INTERNAL])) if CONF_ICON in config: + # Add USE_ENTITY_ICON define when icons are used + cg.add_define("USE_ENTITY_ICON") add(var.set_icon(config[CONF_ICON])) if CONF_ENTITY_CATEGORY in config: add(var.set_entity_category(config[CONF_ENTITY_CATEGORY])) diff --git a/tests/integration/fixtures/entity_icon.yaml b/tests/integration/fixtures/entity_icon.yaml new file mode 100644 index 0000000000..2ce633fe2c --- /dev/null +++ b/tests/integration/fixtures/entity_icon.yaml @@ -0,0 +1,78 @@ +esphome: + name: icon-test + +host: + +api: + +logger: + +# Test entities with custom icons +sensor: + - platform: template + name: "Sensor With Icon" + icon: "mdi:temperature-celsius" + unit_of_measurement: "°C" + update_interval: 1s + lambda: |- + return 25.5; + + - platform: template + name: "Sensor Without Icon" + unit_of_measurement: "%" + update_interval: 1s + lambda: |- + return 50.0; + +binary_sensor: + - platform: template + name: "Binary Sensor With Icon" + icon: "mdi:motion-sensor" + lambda: |- + return true; + + - platform: template + name: "Binary Sensor Without Icon" + lambda: |- + return false; + +text_sensor: + - platform: template + name: "Text Sensor With Icon" + icon: "mdi:text-box" + lambda: |- + return {"Hello Icons"}; + +switch: + - platform: template + name: "Switch With Icon" + icon: "mdi:toggle-switch" + optimistic: true + +button: + - platform: template + name: "Button With Icon" + icon: "mdi:gesture-tap-button" + on_press: + - logger.log: "Button with icon pressed" + +number: + - platform: template + name: "Number With Icon" + icon: "mdi:numeric" + initial_value: 42 + min_value: 0 + max_value: 100 + step: 1 + optimistic: true + +select: + - platform: template + name: "Select With Icon" + icon: "mdi:format-list-bulleted" + options: + - "Option A" + - "Option B" + - "Option C" + initial_option: "Option A" + optimistic: true diff --git a/tests/integration/test_entity_icon.py b/tests/integration/test_entity_icon.py new file mode 100644 index 0000000000..56e266b486 --- /dev/null +++ b/tests/integration/test_entity_icon.py @@ -0,0 +1,97 @@ +"""Integration test for entity icons with USE_ENTITY_ICON feature.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityState +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_entity_icon( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that entities with custom icons work correctly with USE_ENTITY_ICON.""" + # Write, compile and run the ESPHome device, then connect to API + async with run_compiled(yaml_config), api_client_connected() as client: + # Get all entities + entities = await client.list_entities_services() + + # Create a map of entity names to entity info + entity_map = {entity.name: entity for entity in entities[0]} + + # Test entities with icons + icon_test_cases = [ + # (entity_name, expected_icon) + ("Sensor With Icon", "mdi:temperature-celsius"), + ("Binary Sensor With Icon", "mdi:motion-sensor"), + ("Text Sensor With Icon", "mdi:text-box"), + ("Switch With Icon", "mdi:toggle-switch"), + ("Button With Icon", "mdi:gesture-tap-button"), + ("Number With Icon", "mdi:numeric"), + ("Select With Icon", "mdi:format-list-bulleted"), + ] + + # Test entities without icons (should have empty string) + no_icon_test_cases = [ + "Sensor Without Icon", + "Binary Sensor Without Icon", + ] + + # Verify entities with icons + for entity_name, expected_icon in icon_test_cases: + assert entity_name in entity_map, ( + f"Entity '{entity_name}' not found in API response" + ) + entity = entity_map[entity_name] + + # Check icon field + assert hasattr(entity, "icon"), ( + f"{entity_name}: Entity should have icon attribute" + ) + assert entity.icon == expected_icon, ( + f"{entity_name}: icon mismatch - " + f"expected '{expected_icon}', got '{entity.icon}'" + ) + + # Verify entities without icons + for entity_name in no_icon_test_cases: + assert entity_name in entity_map, ( + f"Entity '{entity_name}' not found in API response" + ) + entity = entity_map[entity_name] + + # Check icon field is empty + assert hasattr(entity, "icon"), ( + f"{entity_name}: Entity should have icon attribute" + ) + assert entity.icon == "", ( + f"{entity_name}: icon should be empty string for entities without icons, " + f"got '{entity.icon}'" + ) + + # Subscribe to states to ensure everything works normally + states: dict[int, EntityState] = {} + state_received = asyncio.Event() + + def on_state(state: EntityState) -> None: + states[state.key] = state + state_received.set() + + client.subscribe_states(on_state) + + # Wait for states + try: + await asyncio.wait_for(state_received.wait(), timeout=5.0) + except asyncio.TimeoutError: + pytest.fail("No states received within 5 seconds") + + # Verify we received states + assert len(states) > 0, ( + "No states received - entities may not be working correctly" + ) From e2de6ee29d7a7f29df04fd0f869f212cbf33a01d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jul 2025 15:28:14 -0500 Subject: [PATCH 05/15] Reduce core RAM usage by 40 bytes with static initialization optimizations (#9340) --- esphome/core/component.cpp | 46 +++++++++++++++++++------------------- esphome/core/helpers.cpp | 30 +++++++++++++++++++------ 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 9ef30081aa..9d863e56cd 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -26,17 +26,17 @@ static const char *const TAG = "component"; // 1. Components are never destroyed in ESPHome // 2. Failed components remain failed (no recovery mechanism) // 3. Memory usage is minimal (only failures with custom messages are stored) -static std::unique_ptr>> &get_component_error_messages() { - static std::unique_ptr>> instance; - return instance; -} +// Using namespace-scope static to avoid guard variables (saves 16 bytes total) +// This is safe because ESPHome is single-threaded during initialization +namespace { +// Error messages for failed components +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +std::unique_ptr>> component_error_messages; // Setup priority overrides - freed after setup completes -// Typically < 5 entries, lazy allocated -static std::unique_ptr>> &get_setup_priority_overrides() { - static std::unique_ptr>> instance; - return instance; -} +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +std::unique_ptr>> setup_priority_overrides; +} // namespace namespace setup_priority { @@ -130,8 +130,8 @@ void Component::call_dump_config() { if (this->is_failed()) { // Look up error message from global vector const char *error_msg = "unspecified"; - if (get_component_error_messages()) { - for (const auto &pair : *get_component_error_messages()) { + if (component_error_messages) { + for (const auto &pair : *component_error_messages) { if (pair.first == this) { error_msg = pair.second; break; @@ -285,18 +285,18 @@ void Component::status_set_error(const char *message) { ESP_LOGE(TAG, "Component %s set Error flag: %s", this->get_component_source(), message); if (strcmp(message, "unspecified") != 0) { // Lazy allocate the error messages vector if needed - if (!get_component_error_messages()) { - get_component_error_messages() = std::make_unique>>(); + if (!component_error_messages) { + component_error_messages = std::make_unique>>(); } // Check if this component already has an error message - for (auto &pair : *get_component_error_messages()) { + for (auto &pair : *component_error_messages) { if (pair.first == this) { pair.second = message; return; } } // Add new error message - get_component_error_messages()->emplace_back(this, message); + component_error_messages->emplace_back(this, message); } } void Component::status_clear_warning() { @@ -322,9 +322,9 @@ void Component::status_momentary_error(const std::string &name, uint32_t length) void Component::dump_config() {} float Component::get_actual_setup_priority() const { // Check if there's an override in the global vector - if (get_setup_priority_overrides()) { + if (setup_priority_overrides) { // Linear search is fine for small n (typically < 5 overrides) - for (const auto &pair : *get_setup_priority_overrides()) { + for (const auto &pair : *setup_priority_overrides) { if (pair.first == this) { return pair.second; } @@ -334,14 +334,14 @@ float Component::get_actual_setup_priority() const { } void Component::set_setup_priority(float priority) { // Lazy allocate the vector if needed - if (!get_setup_priority_overrides()) { - get_setup_priority_overrides() = std::make_unique>>(); + if (!setup_priority_overrides) { + setup_priority_overrides = std::make_unique>>(); // Reserve some space to avoid reallocations (most configs have < 10 overrides) - get_setup_priority_overrides()->reserve(10); + setup_priority_overrides->reserve(10); } // Check if this component already has an override - for (auto &pair : *get_setup_priority_overrides()) { + for (auto &pair : *setup_priority_overrides) { if (pair.first == this) { pair.second = priority; return; @@ -349,7 +349,7 @@ void Component::set_setup_priority(float priority) { } // Add new override - get_setup_priority_overrides()->emplace_back(this, priority); + setup_priority_overrides->emplace_back(this, priority); } bool Component::has_overridden_loop() const { @@ -414,7 +414,7 @@ WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {} void clear_setup_priority_overrides() { // Free the setup priority map completely - get_setup_priority_overrides().reset(); + setup_priority_overrides.reset(); } } // namespace esphome diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 72722169d4..22b74e11fa 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -360,9 +360,22 @@ int8_t step_to_accuracy_decimals(float step) { return str.length() - dot_pos - 1; } -static const std::string BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789+/"; +// Use C-style string constant to store in ROM instead of RAM (saves 24 bytes) +static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + +// Helper function to find the index of a base64 character in the lookup table. +// Returns the character's position (0-63) if found, or 0 if not found. +// NOTE: This returns 0 for both 'A' (valid base64 char at index 0) and invalid characters. +// This is safe because is_base64() is ALWAYS checked before calling this function, +// preventing invalid characters from ever reaching here. The base64_decode function +// stops processing at the first invalid character due to the is_base64() check in its +// while loop condition, making this edge case harmless in practice. +static inline uint8_t base64_find_char(char c) { + const char *pos = strchr(BASE64_CHARS, c); + return pos ? (pos - BASE64_CHARS) : 0; +} static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/')); } @@ -384,7 +397,7 @@ std::string base64_encode(const uint8_t *buf, size_t buf_len) { char_array_4[3] = char_array_3[2] & 0x3f; for (i = 0; (i < 4); i++) - ret += BASE64_CHARS[char_array_4[i]]; + ret += BASE64_CHARS[static_cast(char_array_4[i])]; i = 0; } } @@ -399,7 +412,7 @@ std::string base64_encode(const uint8_t *buf, size_t buf_len) { char_array_4[3] = char_array_3[2] & 0x3f; for (j = 0; (j < i + 1); j++) - ret += BASE64_CHARS[char_array_4[j]]; + ret += BASE64_CHARS[static_cast(char_array_4[j])]; while ((i++ < 3)) ret += '='; @@ -426,12 +439,15 @@ std::vector base64_decode(const std::string &encoded_string) { uint8_t char_array_4[4], char_array_3[3]; std::vector ret; + // SAFETY: The loop condition checks is_base64() before processing each character. + // This ensures base64_find_char() is only called on valid base64 characters, + // preventing the edge case where invalid chars would return 0 (same as 'A'). while (in_len-- && (encoded_string[in] != '=') && is_base64(encoded_string[in])) { char_array_4[i++] = encoded_string[in]; in++; if (i == 4) { for (i = 0; i < 4; i++) - char_array_4[i] = BASE64_CHARS.find(char_array_4[i]); + char_array_4[i] = base64_find_char(char_array_4[i]); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); @@ -448,7 +464,7 @@ std::vector base64_decode(const std::string &encoded_string) { char_array_4[j] = 0; for (j = 0; j < 4; j++) - char_array_4[j] = BASE64_CHARS.find(char_array_4[j]); + char_array_4[j] = base64_find_char(char_array_4[j]); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); From 29747fc7307f2ef7105255b6aeb9086de5d8f920 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jul 2025 15:35:11 -0500 Subject: [PATCH 06/15] Fix flaky test_api_conditional_memory by disabling API batch delay (#9360) --- tests/integration/fixtures/api_conditional_memory.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/fixtures/api_conditional_memory.yaml b/tests/integration/fixtures/api_conditional_memory.yaml index 4bbba5084b..49412c3bfe 100644 --- a/tests/integration/fixtures/api_conditional_memory.yaml +++ b/tests/integration/fixtures/api_conditional_memory.yaml @@ -2,6 +2,7 @@ esphome: name: api-conditional-memory-test host: api: + batch_delay: 0ms actions: - action: test_simple_service then: From 832a7872719037e420b34046bab728d0b9515695 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jul 2025 15:35:27 -0500 Subject: [PATCH 07/15] Fix format specifier warnings in QuantileFilter logging (#9364) --- esphome/components/sensor/filter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index ce23c1f800..dd8635f0c0 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -118,7 +118,7 @@ optional QuantileFilter::new_value(float value) { size_t queue_size = quantile_queue.size(); if (queue_size) { size_t position = ceilf(queue_size * this->quantile_) - 1; - ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %d/%d", this, position + 1, queue_size); + ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %zu/%zu", this, position + 1, queue_size); result = quantile_queue[position]; } } From 90fb3680d47333c7f8278439bb3867a330499c39 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jul 2025 15:36:36 -0500 Subject: [PATCH 08/15] Optimize logger performance by eliminating redundant strlen calls (#9369) --- esphome/components/logger/logger.h | 2 +- esphome/components/logger/logger_esp32.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 38faf73d84..e376d9fbf5 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -355,7 +355,7 @@ class Logger : public Component { } inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { - static const uint16_t RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR); + static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1; this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); } diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 41445fa3b4..2fde0f7d49 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -184,7 +184,9 @@ void HOT Logger::write_msg_(const char *msg) { ) { puts(msg); } else { - uart_write_bytes(this->uart_num_, msg, strlen(msg)); + // Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen + size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg); + uart_write_bytes(this->uart_num_, msg, len); uart_write_bytes(this->uart_num_, "\n", 1); } } From e58c793da2cdc5f5ca083606f1d1863b80cba11b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jul 2025 15:38:41 -0500 Subject: [PATCH 09/15] Replace deprecated sprintf with snprintf in API protobuf code generation (#9365) --- esphome/components/api/api_pb2_dump.cpp | 536 ++++++++++++------------ script/api_protobuf/api_protobuf.py | 24 +- 2 files changed, 280 insertions(+), 280 deletions(-) diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 84e765e40f..48ddd42d61 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -600,12 +600,12 @@ void HelloRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" api_version_major: "); - sprintf(buffer, "%" PRIu32, this->api_version_major); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_major); out.append(buffer); out.append("\n"); out.append(" api_version_minor: "); - sprintf(buffer, "%" PRIu32, this->api_version_minor); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_minor); out.append(buffer); out.append("\n"); out.append("}"); @@ -614,12 +614,12 @@ void HelloResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("HelloResponse {\n"); out.append(" api_version_major: "); - sprintf(buffer, "%" PRIu32, this->api_version_major); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_major); out.append(buffer); out.append("\n"); out.append(" api_version_minor: "); - sprintf(buffer, "%" PRIu32, this->api_version_minor); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_minor); out.append(buffer); out.append("\n"); @@ -657,7 +657,7 @@ void AreaInfo::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("AreaInfo {\n"); out.append(" area_id: "); - sprintf(buffer, "%" PRIu32, this->area_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->area_id); out.append(buffer); out.append("\n"); @@ -670,7 +670,7 @@ void DeviceInfo::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("DeviceInfo {\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); @@ -679,7 +679,7 @@ void DeviceInfo::dump_to(std::string &out) const { out.append("\n"); out.append(" area_id: "); - sprintf(buffer, "%" PRIu32, this->area_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->area_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -724,17 +724,17 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" webserver_port: "); - sprintf(buffer, "%" PRIu32, this->webserver_port); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->webserver_port); out.append(buffer); out.append("\n"); out.append(" legacy_bluetooth_proxy_version: "); - sprintf(buffer, "%" PRIu32, this->legacy_bluetooth_proxy_version); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->legacy_bluetooth_proxy_version); out.append(buffer); out.append("\n"); out.append(" bluetooth_proxy_feature_flags: "); - sprintf(buffer, "%" PRIu32, this->bluetooth_proxy_feature_flags); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->bluetooth_proxy_feature_flags); out.append(buffer); out.append("\n"); @@ -747,12 +747,12 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" legacy_voice_assistant_version: "); - sprintf(buffer, "%" PRIu32, this->legacy_voice_assistant_version); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->legacy_voice_assistant_version); out.append(buffer); out.append("\n"); out.append(" voice_assistant_feature_flags: "); - sprintf(buffer, "%" PRIu32, this->voice_assistant_feature_flags); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->voice_assistant_feature_flags); out.append(buffer); out.append("\n"); @@ -797,7 +797,7 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -830,7 +830,7 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -839,7 +839,7 @@ void BinarySensorStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BinarySensorStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -852,7 +852,7 @@ void BinarySensorStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -867,7 +867,7 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -912,7 +912,7 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -921,7 +921,7 @@ void CoverStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("CoverStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -930,12 +930,12 @@ void CoverStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" position: "); - sprintf(buffer, "%g", this->position); + snprintf(buffer, sizeof(buffer), "%g", this->position); out.append(buffer); out.append("\n"); out.append(" tilt: "); - sprintf(buffer, "%g", this->tilt); + snprintf(buffer, sizeof(buffer), "%g", this->tilt); out.append(buffer); out.append("\n"); @@ -944,7 +944,7 @@ void CoverStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -953,7 +953,7 @@ void CoverCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("CoverCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -970,7 +970,7 @@ void CoverCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" position: "); - sprintf(buffer, "%g", this->position); + snprintf(buffer, sizeof(buffer), "%g", this->position); out.append(buffer); out.append("\n"); @@ -979,7 +979,7 @@ void CoverCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" tilt: "); - sprintf(buffer, "%g", this->tilt); + snprintf(buffer, sizeof(buffer), "%g", this->tilt); out.append(buffer); out.append("\n"); @@ -998,7 +998,7 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1023,7 +1023,7 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" supported_speed_count: "); - sprintf(buffer, "%" PRId32, this->supported_speed_count); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->supported_speed_count); out.append(buffer); out.append("\n"); @@ -1046,7 +1046,7 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { } out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1055,7 +1055,7 @@ void FanStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("FanStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1076,7 +1076,7 @@ void FanStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" speed_level: "); - sprintf(buffer, "%" PRId32, this->speed_level); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->speed_level); out.append(buffer); out.append("\n"); @@ -1085,7 +1085,7 @@ void FanStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1094,7 +1094,7 @@ void FanCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("FanCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1135,7 +1135,7 @@ void FanCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" speed_level: "); - sprintf(buffer, "%" PRId32, this->speed_level); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->speed_level); out.append(buffer); out.append("\n"); @@ -1158,7 +1158,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1193,12 +1193,12 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" min_mireds: "); - sprintf(buffer, "%g", this->min_mireds); + snprintf(buffer, sizeof(buffer), "%g", this->min_mireds); out.append(buffer); out.append("\n"); out.append(" max_mireds: "); - sprintf(buffer, "%g", this->max_mireds); + snprintf(buffer, sizeof(buffer), "%g", this->max_mireds); out.append(buffer); out.append("\n"); @@ -1221,7 +1221,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1230,7 +1230,7 @@ void LightStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("LightStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1239,7 +1239,7 @@ void LightStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" brightness: "); - sprintf(buffer, "%g", this->brightness); + snprintf(buffer, sizeof(buffer), "%g", this->brightness); out.append(buffer); out.append("\n"); @@ -1248,42 +1248,42 @@ void LightStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" color_brightness: "); - sprintf(buffer, "%g", this->color_brightness); + snprintf(buffer, sizeof(buffer), "%g", this->color_brightness); out.append(buffer); out.append("\n"); out.append(" red: "); - sprintf(buffer, "%g", this->red); + snprintf(buffer, sizeof(buffer), "%g", this->red); out.append(buffer); out.append("\n"); out.append(" green: "); - sprintf(buffer, "%g", this->green); + snprintf(buffer, sizeof(buffer), "%g", this->green); out.append(buffer); out.append("\n"); out.append(" blue: "); - sprintf(buffer, "%g", this->blue); + snprintf(buffer, sizeof(buffer), "%g", this->blue); out.append(buffer); out.append("\n"); out.append(" white: "); - sprintf(buffer, "%g", this->white); + snprintf(buffer, sizeof(buffer), "%g", this->white); out.append(buffer); out.append("\n"); out.append(" color_temperature: "); - sprintf(buffer, "%g", this->color_temperature); + snprintf(buffer, sizeof(buffer), "%g", this->color_temperature); out.append(buffer); out.append("\n"); out.append(" cold_white: "); - sprintf(buffer, "%g", this->cold_white); + snprintf(buffer, sizeof(buffer), "%g", this->cold_white); out.append(buffer); out.append("\n"); out.append(" warm_white: "); - sprintf(buffer, "%g", this->warm_white); + snprintf(buffer, sizeof(buffer), "%g", this->warm_white); out.append(buffer); out.append("\n"); @@ -1292,7 +1292,7 @@ void LightStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1301,7 +1301,7 @@ void LightCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("LightCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1318,7 +1318,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" brightness: "); - sprintf(buffer, "%g", this->brightness); + snprintf(buffer, sizeof(buffer), "%g", this->brightness); out.append(buffer); out.append("\n"); @@ -1335,7 +1335,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" color_brightness: "); - sprintf(buffer, "%g", this->color_brightness); + snprintf(buffer, sizeof(buffer), "%g", this->color_brightness); out.append(buffer); out.append("\n"); @@ -1344,17 +1344,17 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" red: "); - sprintf(buffer, "%g", this->red); + snprintf(buffer, sizeof(buffer), "%g", this->red); out.append(buffer); out.append("\n"); out.append(" green: "); - sprintf(buffer, "%g", this->green); + snprintf(buffer, sizeof(buffer), "%g", this->green); out.append(buffer); out.append("\n"); out.append(" blue: "); - sprintf(buffer, "%g", this->blue); + snprintf(buffer, sizeof(buffer), "%g", this->blue); out.append(buffer); out.append("\n"); @@ -1363,7 +1363,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" white: "); - sprintf(buffer, "%g", this->white); + snprintf(buffer, sizeof(buffer), "%g", this->white); out.append(buffer); out.append("\n"); @@ -1372,7 +1372,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" color_temperature: "); - sprintf(buffer, "%g", this->color_temperature); + snprintf(buffer, sizeof(buffer), "%g", this->color_temperature); out.append(buffer); out.append("\n"); @@ -1381,7 +1381,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" cold_white: "); - sprintf(buffer, "%g", this->cold_white); + snprintf(buffer, sizeof(buffer), "%g", this->cold_white); out.append(buffer); out.append("\n"); @@ -1390,7 +1390,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" warm_white: "); - sprintf(buffer, "%g", this->warm_white); + snprintf(buffer, sizeof(buffer), "%g", this->warm_white); out.append(buffer); out.append("\n"); @@ -1399,7 +1399,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" transition_length: "); - sprintf(buffer, "%" PRIu32, this->transition_length); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->transition_length); out.append(buffer); out.append("\n"); @@ -1408,7 +1408,7 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" flash_length: "); - sprintf(buffer, "%" PRIu32, this->flash_length); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flash_length); out.append(buffer); out.append("\n"); @@ -1431,7 +1431,7 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1452,7 +1452,7 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" accuracy_decimals: "); - sprintf(buffer, "%" PRId32, this->accuracy_decimals); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->accuracy_decimals); out.append(buffer); out.append("\n"); @@ -1481,7 +1481,7 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1490,12 +1490,12 @@ void SensorStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SensorStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); out.append(" state: "); - sprintf(buffer, "%g", this->state); + snprintf(buffer, sizeof(buffer), "%g", this->state); out.append(buffer); out.append("\n"); @@ -1504,7 +1504,7 @@ void SensorStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1519,7 +1519,7 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1552,7 +1552,7 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1561,7 +1561,7 @@ void SwitchStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SwitchStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1570,7 +1570,7 @@ void SwitchStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1579,7 +1579,7 @@ void SwitchCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SwitchCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1598,7 +1598,7 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1627,7 +1627,7 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1636,7 +1636,7 @@ void TextSensorStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("TextSensorStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1649,7 +1649,7 @@ void TextSensorStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1786,7 +1786,7 @@ void GetTimeResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("GetTimeResponse {\n"); out.append(" epoch_seconds: "); - sprintf(buffer, "%" PRIu32, this->epoch_seconds); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds); out.append(buffer); out.append("\n"); out.append("}"); @@ -1811,7 +1811,7 @@ void ListEntitiesServicesResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1830,12 +1830,12 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { out.append("\n"); out.append(" legacy_int: "); - sprintf(buffer, "%" PRId32, this->legacy_int); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->legacy_int); out.append(buffer); out.append("\n"); out.append(" float_: "); - sprintf(buffer, "%g", this->float_); + snprintf(buffer, sizeof(buffer), "%g", this->float_); out.append(buffer); out.append("\n"); @@ -1844,7 +1844,7 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { out.append("\n"); out.append(" int_: "); - sprintf(buffer, "%" PRId32, this->int_); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->int_); out.append(buffer); out.append("\n"); @@ -1856,14 +1856,14 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { for (const auto &it : this->int_array) { out.append(" int_array: "); - sprintf(buffer, "%" PRId32, it); + snprintf(buffer, sizeof(buffer), "%" PRId32, it); out.append(buffer); out.append("\n"); } for (const auto &it : this->float_array) { out.append(" float_array: "); - sprintf(buffer, "%g", it); + snprintf(buffer, sizeof(buffer), "%g", it); out.append(buffer); out.append("\n"); } @@ -1879,7 +1879,7 @@ void ExecuteServiceRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ExecuteServiceRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1899,7 +1899,7 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1924,7 +1924,7 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -1933,7 +1933,7 @@ void CameraImageResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("CameraImageResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1968,7 +1968,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -1995,17 +1995,17 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { } out.append(" visual_min_temperature: "); - sprintf(buffer, "%g", this->visual_min_temperature); + snprintf(buffer, sizeof(buffer), "%g", this->visual_min_temperature); out.append(buffer); out.append("\n"); out.append(" visual_max_temperature: "); - sprintf(buffer, "%g", this->visual_max_temperature); + snprintf(buffer, sizeof(buffer), "%g", this->visual_max_temperature); out.append(buffer); out.append("\n"); out.append(" visual_target_temperature_step: "); - sprintf(buffer, "%g", this->visual_target_temperature_step); + snprintf(buffer, sizeof(buffer), "%g", this->visual_target_temperature_step); out.append(buffer); out.append("\n"); @@ -2060,7 +2060,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" visual_current_temperature_step: "); - sprintf(buffer, "%g", this->visual_current_temperature_step); + snprintf(buffer, sizeof(buffer), "%g", this->visual_current_temperature_step); out.append(buffer); out.append("\n"); @@ -2073,17 +2073,17 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" visual_min_humidity: "); - sprintf(buffer, "%g", this->visual_min_humidity); + snprintf(buffer, sizeof(buffer), "%g", this->visual_min_humidity); out.append(buffer); out.append("\n"); out.append(" visual_max_humidity: "); - sprintf(buffer, "%g", this->visual_max_humidity); + snprintf(buffer, sizeof(buffer), "%g", this->visual_max_humidity); out.append(buffer); out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2092,7 +2092,7 @@ void ClimateStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ClimateStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2101,22 +2101,22 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" current_temperature: "); - sprintf(buffer, "%g", this->current_temperature); + snprintf(buffer, sizeof(buffer), "%g", this->current_temperature); out.append(buffer); out.append("\n"); out.append(" target_temperature: "); - sprintf(buffer, "%g", this->target_temperature); + snprintf(buffer, sizeof(buffer), "%g", this->target_temperature); out.append(buffer); out.append("\n"); out.append(" target_temperature_low: "); - sprintf(buffer, "%g", this->target_temperature_low); + snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_low); out.append(buffer); out.append("\n"); out.append(" target_temperature_high: "); - sprintf(buffer, "%g", this->target_temperature_high); + snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_high); out.append(buffer); out.append("\n"); @@ -2149,17 +2149,17 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" current_humidity: "); - sprintf(buffer, "%g", this->current_humidity); + snprintf(buffer, sizeof(buffer), "%g", this->current_humidity); out.append(buffer); out.append("\n"); out.append(" target_humidity: "); - sprintf(buffer, "%g", this->target_humidity); + snprintf(buffer, sizeof(buffer), "%g", this->target_humidity); out.append(buffer); out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2168,7 +2168,7 @@ void ClimateCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ClimateCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2185,7 +2185,7 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" target_temperature: "); - sprintf(buffer, "%g", this->target_temperature); + snprintf(buffer, sizeof(buffer), "%g", this->target_temperature); out.append(buffer); out.append("\n"); @@ -2194,7 +2194,7 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" target_temperature_low: "); - sprintf(buffer, "%g", this->target_temperature_low); + snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_low); out.append(buffer); out.append("\n"); @@ -2203,7 +2203,7 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" target_temperature_high: "); - sprintf(buffer, "%g", this->target_temperature_high); + snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_high); out.append(buffer); out.append("\n"); @@ -2260,7 +2260,7 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" target_humidity: "); - sprintf(buffer, "%g", this->target_humidity); + snprintf(buffer, sizeof(buffer), "%g", this->target_humidity); out.append(buffer); out.append("\n"); out.append("}"); @@ -2275,7 +2275,7 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2292,17 +2292,17 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" min_value: "); - sprintf(buffer, "%g", this->min_value); + snprintf(buffer, sizeof(buffer), "%g", this->min_value); out.append(buffer); out.append("\n"); out.append(" max_value: "); - sprintf(buffer, "%g", this->max_value); + snprintf(buffer, sizeof(buffer), "%g", this->max_value); out.append(buffer); out.append("\n"); out.append(" step: "); - sprintf(buffer, "%g", this->step); + snprintf(buffer, sizeof(buffer), "%g", this->step); out.append(buffer); out.append("\n"); @@ -2327,7 +2327,7 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2336,12 +2336,12 @@ void NumberStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("NumberStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); out.append(" state: "); - sprintf(buffer, "%g", this->state); + snprintf(buffer, sizeof(buffer), "%g", this->state); out.append(buffer); out.append("\n"); @@ -2350,7 +2350,7 @@ void NumberStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2359,12 +2359,12 @@ void NumberCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("NumberCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); out.append(" state: "); - sprintf(buffer, "%g", this->state); + snprintf(buffer, sizeof(buffer), "%g", this->state); out.append(buffer); out.append("\n"); out.append("}"); @@ -2379,7 +2379,7 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2410,7 +2410,7 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2419,7 +2419,7 @@ void SelectStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SelectStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2432,7 +2432,7 @@ void SelectStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2441,7 +2441,7 @@ void SelectCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SelectCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2460,7 +2460,7 @@ void ListEntitiesSirenResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2499,7 +2499,7 @@ void ListEntitiesSirenResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2508,7 +2508,7 @@ void SirenStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SirenStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2517,7 +2517,7 @@ void SirenStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2526,7 +2526,7 @@ void SirenCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("SirenCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2551,7 +2551,7 @@ void SirenCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" duration: "); - sprintf(buffer, "%" PRIu32, this->duration); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->duration); out.append(buffer); out.append("\n"); @@ -2560,7 +2560,7 @@ void SirenCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" volume: "); - sprintf(buffer, "%g", this->volume); + snprintf(buffer, sizeof(buffer), "%g", this->volume); out.append(buffer); out.append("\n"); out.append("}"); @@ -2575,7 +2575,7 @@ void ListEntitiesLockResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2616,7 +2616,7 @@ void ListEntitiesLockResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2625,7 +2625,7 @@ void LockStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("LockStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2634,7 +2634,7 @@ void LockStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2643,7 +2643,7 @@ void LockCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("LockCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2670,7 +2670,7 @@ void ListEntitiesButtonResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2699,7 +2699,7 @@ void ListEntitiesButtonResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2708,7 +2708,7 @@ void ButtonCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ButtonCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); out.append("}"); @@ -2723,12 +2723,12 @@ void MediaPlayerSupportedFormat::dump_to(std::string &out) const { out.append("\n"); out.append(" sample_rate: "); - sprintf(buffer, "%" PRIu32, this->sample_rate); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->sample_rate); out.append(buffer); out.append("\n"); out.append(" num_channels: "); - sprintf(buffer, "%" PRIu32, this->num_channels); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->num_channels); out.append(buffer); out.append("\n"); @@ -2737,7 +2737,7 @@ void MediaPlayerSupportedFormat::dump_to(std::string &out) const { out.append("\n"); out.append(" sample_bytes: "); - sprintf(buffer, "%" PRIu32, this->sample_bytes); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->sample_bytes); out.append(buffer); out.append("\n"); out.append("}"); @@ -2750,7 +2750,7 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2785,7 +2785,7 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { } out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2794,7 +2794,7 @@ void MediaPlayerStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("MediaPlayerStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2803,7 +2803,7 @@ void MediaPlayerStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" volume: "); - sprintf(buffer, "%g", this->volume); + snprintf(buffer, sizeof(buffer), "%g", this->volume); out.append(buffer); out.append("\n"); @@ -2812,7 +2812,7 @@ void MediaPlayerStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -2821,7 +2821,7 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("MediaPlayerCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -2838,7 +2838,7 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" volume: "); - sprintf(buffer, "%g", this->volume); + snprintf(buffer, sizeof(buffer), "%g", this->volume); out.append(buffer); out.append("\n"); @@ -2865,7 +2865,7 @@ void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const __attribute__((unused)) char buffer[64]; out.append("SubscribeBluetoothLEAdvertisementsRequest {\n"); out.append(" flags: "); - sprintf(buffer, "%" PRIu32, this->flags); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flags); out.append(buffer); out.append("\n"); out.append("}"); @@ -2879,7 +2879,7 @@ void BluetoothServiceData::dump_to(std::string &out) const { for (const auto &it : this->legacy_data) { out.append(" legacy_data: "); - sprintf(buffer, "%" PRIu32, it); + snprintf(buffer, sizeof(buffer), "%" PRIu32, it); out.append(buffer); out.append("\n"); } @@ -2893,7 +2893,7 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothLEAdvertisementResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); @@ -2902,7 +2902,7 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" rssi: "); - sprintf(buffer, "%" PRId32, this->rssi); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->rssi); out.append(buffer); out.append("\n"); @@ -2925,7 +2925,7 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { } out.append(" address_type: "); - sprintf(buffer, "%" PRIu32, this->address_type); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type); out.append(buffer); out.append("\n"); out.append("}"); @@ -2934,17 +2934,17 @@ void BluetoothLERawAdvertisement::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothLERawAdvertisement {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" rssi: "); - sprintf(buffer, "%" PRId32, this->rssi); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->rssi); out.append(buffer); out.append("\n"); out.append(" address_type: "); - sprintf(buffer, "%" PRIu32, this->address_type); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type); out.append(buffer); out.append("\n"); @@ -2967,7 +2967,7 @@ void BluetoothDeviceRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothDeviceRequest {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); @@ -2980,7 +2980,7 @@ void BluetoothDeviceRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" address_type: "); - sprintf(buffer, "%" PRIu32, this->address_type); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type); out.append(buffer); out.append("\n"); out.append("}"); @@ -2989,7 +2989,7 @@ void BluetoothDeviceConnectionResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothDeviceConnectionResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); @@ -2998,12 +2998,12 @@ void BluetoothDeviceConnectionResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" mtu: "); - sprintf(buffer, "%" PRIu32, this->mtu); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->mtu); out.append(buffer); out.append("\n"); out.append(" error: "); - sprintf(buffer, "%" PRId32, this->error); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); out.append(buffer); out.append("\n"); out.append("}"); @@ -3012,7 +3012,7 @@ void BluetoothGATTGetServicesRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTGetServicesRequest {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append("}"); @@ -3022,13 +3022,13 @@ void BluetoothGATTDescriptor::dump_to(std::string &out) const { out.append("BluetoothGATTDescriptor {\n"); for (const auto &it : this->uuid) { out.append(" uuid: "); - sprintf(buffer, "%llu", it); + snprintf(buffer, sizeof(buffer), "%llu", it); out.append(buffer); out.append("\n"); } out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append("}"); @@ -3038,18 +3038,18 @@ void BluetoothGATTCharacteristic::dump_to(std::string &out) const { out.append("BluetoothGATTCharacteristic {\n"); for (const auto &it : this->uuid) { out.append(" uuid: "); - sprintf(buffer, "%llu", it); + snprintf(buffer, sizeof(buffer), "%llu", it); out.append(buffer); out.append("\n"); } out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append(" properties: "); - sprintf(buffer, "%" PRIu32, this->properties); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->properties); out.append(buffer); out.append("\n"); @@ -3065,13 +3065,13 @@ void BluetoothGATTService::dump_to(std::string &out) const { out.append("BluetoothGATTService {\n"); for (const auto &it : this->uuid) { out.append(" uuid: "); - sprintf(buffer, "%llu", it); + snprintf(buffer, sizeof(buffer), "%llu", it); out.append(buffer); out.append("\n"); } out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); @@ -3086,7 +3086,7 @@ void BluetoothGATTGetServicesResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTGetServicesResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); @@ -3101,7 +3101,7 @@ void BluetoothGATTGetServicesDoneResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTGetServicesDoneResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append("}"); @@ -3110,12 +3110,12 @@ void BluetoothGATTReadRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTReadRequest {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append("}"); @@ -3124,12 +3124,12 @@ void BluetoothGATTReadResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTReadResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); @@ -3142,12 +3142,12 @@ void BluetoothGATTWriteRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTWriteRequest {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); @@ -3164,12 +3164,12 @@ void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTReadDescriptorRequest {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append("}"); @@ -3178,12 +3178,12 @@ void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTWriteDescriptorRequest {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); @@ -3196,12 +3196,12 @@ void BluetoothGATTNotifyRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTNotifyRequest {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); @@ -3214,12 +3214,12 @@ void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTNotifyDataResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); @@ -3235,18 +3235,18 @@ void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothConnectionsFreeResponse {\n"); out.append(" free: "); - sprintf(buffer, "%" PRIu32, this->free); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->free); out.append(buffer); out.append("\n"); out.append(" limit: "); - sprintf(buffer, "%" PRIu32, this->limit); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->limit); out.append(buffer); out.append("\n"); for (const auto &it : this->allocated) { out.append(" allocated: "); - sprintf(buffer, "%llu", it); + snprintf(buffer, sizeof(buffer), "%llu", it); out.append(buffer); out.append("\n"); } @@ -3256,17 +3256,17 @@ void BluetoothGATTErrorResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTErrorResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append(" error: "); - sprintf(buffer, "%" PRId32, this->error); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); out.append(buffer); out.append("\n"); out.append("}"); @@ -3275,12 +3275,12 @@ void BluetoothGATTWriteResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTWriteResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append("}"); @@ -3289,12 +3289,12 @@ void BluetoothGATTNotifyResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothGATTNotifyResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); out.append(buffer); out.append("\n"); out.append("}"); @@ -3303,7 +3303,7 @@ void BluetoothDevicePairingResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothDevicePairingResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); @@ -3312,7 +3312,7 @@ void BluetoothDevicePairingResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" error: "); - sprintf(buffer, "%" PRId32, this->error); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); out.append(buffer); out.append("\n"); out.append("}"); @@ -3321,7 +3321,7 @@ void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothDeviceUnpairingResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); @@ -3330,7 +3330,7 @@ void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" error: "); - sprintf(buffer, "%" PRId32, this->error); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); out.append(buffer); out.append("\n"); out.append("}"); @@ -3342,7 +3342,7 @@ void BluetoothDeviceClearCacheResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothDeviceClearCacheResponse {\n"); out.append(" address: "); - sprintf(buffer, "%llu", this->address); + snprintf(buffer, sizeof(buffer), "%llu", this->address); out.append(buffer); out.append("\n"); @@ -3351,7 +3351,7 @@ void BluetoothDeviceClearCacheResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" error: "); - sprintf(buffer, "%" PRId32, this->error); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); out.append(buffer); out.append("\n"); out.append("}"); @@ -3386,7 +3386,7 @@ void SubscribeVoiceAssistantRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" flags: "); - sprintf(buffer, "%" PRIu32, this->flags); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flags); out.append(buffer); out.append("\n"); out.append("}"); @@ -3395,17 +3395,17 @@ void VoiceAssistantAudioSettings::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("VoiceAssistantAudioSettings {\n"); out.append(" noise_suppression_level: "); - sprintf(buffer, "%" PRIu32, this->noise_suppression_level); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->noise_suppression_level); out.append(buffer); out.append("\n"); out.append(" auto_gain: "); - sprintf(buffer, "%" PRIu32, this->auto_gain); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->auto_gain); out.append(buffer); out.append("\n"); out.append(" volume_multiplier: "); - sprintf(buffer, "%g", this->volume_multiplier); + snprintf(buffer, sizeof(buffer), "%g", this->volume_multiplier); out.append(buffer); out.append("\n"); out.append("}"); @@ -3422,7 +3422,7 @@ void VoiceAssistantRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" flags: "); - sprintf(buffer, "%" PRIu32, this->flags); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flags); out.append(buffer); out.append("\n"); @@ -3439,7 +3439,7 @@ void VoiceAssistantResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("VoiceAssistantResponse {\n"); out.append(" port: "); - sprintf(buffer, "%" PRIu32, this->port); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->port); out.append(buffer); out.append("\n"); @@ -3502,12 +3502,12 @@ void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" total_seconds: "); - sprintf(buffer, "%" PRIu32, this->total_seconds); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->total_seconds); out.append(buffer); out.append("\n"); out.append(" seconds_left: "); - sprintf(buffer, "%" PRIu32, this->seconds_left); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->seconds_left); out.append(buffer); out.append("\n"); @@ -3581,7 +3581,7 @@ void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const { } out.append(" max_active_wake_words: "); - sprintf(buffer, "%" PRIu32, this->max_active_wake_words); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->max_active_wake_words); out.append(buffer); out.append("\n"); out.append("}"); @@ -3606,7 +3606,7 @@ void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3631,7 +3631,7 @@ void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" supported_features: "); - sprintf(buffer, "%" PRIu32, this->supported_features); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->supported_features); out.append(buffer); out.append("\n"); @@ -3644,7 +3644,7 @@ void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -3653,7 +3653,7 @@ void AlarmControlPanelStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("AlarmControlPanelStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3662,7 +3662,7 @@ void AlarmControlPanelStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -3671,7 +3671,7 @@ void AlarmControlPanelCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("AlarmControlPanelCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3694,7 +3694,7 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3719,12 +3719,12 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" min_length: "); - sprintf(buffer, "%" PRIu32, this->min_length); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->min_length); out.append(buffer); out.append("\n"); out.append(" max_length: "); - sprintf(buffer, "%" PRIu32, this->max_length); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->max_length); out.append(buffer); out.append("\n"); @@ -3737,7 +3737,7 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -3746,7 +3746,7 @@ void TextStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("TextStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3759,7 +3759,7 @@ void TextStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -3768,7 +3768,7 @@ void TextCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("TextCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3787,7 +3787,7 @@ void ListEntitiesDateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3812,7 +3812,7 @@ void ListEntitiesDateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -3821,7 +3821,7 @@ void DateStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("DateStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3830,22 +3830,22 @@ void DateStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" year: "); - sprintf(buffer, "%" PRIu32, this->year); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->year); out.append(buffer); out.append("\n"); out.append(" month: "); - sprintf(buffer, "%" PRIu32, this->month); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->month); out.append(buffer); out.append("\n"); out.append(" day: "); - sprintf(buffer, "%" PRIu32, this->day); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->day); out.append(buffer); out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -3854,22 +3854,22 @@ void DateCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("DateCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); out.append(" year: "); - sprintf(buffer, "%" PRIu32, this->year); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->year); out.append(buffer); out.append("\n"); out.append(" month: "); - sprintf(buffer, "%" PRIu32, this->month); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->month); out.append(buffer); out.append("\n"); out.append(" day: "); - sprintf(buffer, "%" PRIu32, this->day); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->day); out.append(buffer); out.append("\n"); out.append("}"); @@ -3884,7 +3884,7 @@ void ListEntitiesTimeResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3909,7 +3909,7 @@ void ListEntitiesTimeResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -3918,7 +3918,7 @@ void TimeStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("TimeStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -3927,22 +3927,22 @@ void TimeStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" hour: "); - sprintf(buffer, "%" PRIu32, this->hour); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->hour); out.append(buffer); out.append("\n"); out.append(" minute: "); - sprintf(buffer, "%" PRIu32, this->minute); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->minute); out.append(buffer); out.append("\n"); out.append(" second: "); - sprintf(buffer, "%" PRIu32, this->second); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->second); out.append(buffer); out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -3951,22 +3951,22 @@ void TimeCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("TimeCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); out.append(" hour: "); - sprintf(buffer, "%" PRIu32, this->hour); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->hour); out.append(buffer); out.append("\n"); out.append(" minute: "); - sprintf(buffer, "%" PRIu32, this->minute); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->minute); out.append(buffer); out.append("\n"); out.append(" second: "); - sprintf(buffer, "%" PRIu32, this->second); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->second); out.append(buffer); out.append("\n"); out.append("}"); @@ -3981,7 +3981,7 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4016,7 +4016,7 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const { } out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -4025,7 +4025,7 @@ void EventResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("EventResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4034,7 +4034,7 @@ void EventResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -4049,7 +4049,7 @@ void ListEntitiesValveResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4090,7 +4090,7 @@ void ListEntitiesValveResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -4099,12 +4099,12 @@ void ValveStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ValveStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); out.append(" position: "); - sprintf(buffer, "%g", this->position); + snprintf(buffer, sizeof(buffer), "%g", this->position); out.append(buffer); out.append("\n"); @@ -4113,7 +4113,7 @@ void ValveStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -4122,7 +4122,7 @@ void ValveCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ValveCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4131,7 +4131,7 @@ void ValveCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append(" position: "); - sprintf(buffer, "%g", this->position); + snprintf(buffer, sizeof(buffer), "%g", this->position); out.append(buffer); out.append("\n"); @@ -4150,7 +4150,7 @@ void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4175,7 +4175,7 @@ void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -4184,7 +4184,7 @@ void DateTimeStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("DateTimeStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4193,12 +4193,12 @@ void DateTimeStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" epoch_seconds: "); - sprintf(buffer, "%" PRIu32, this->epoch_seconds); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds); out.append(buffer); out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -4207,12 +4207,12 @@ void DateTimeCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("DateTimeCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); out.append(" epoch_seconds: "); - sprintf(buffer, "%" PRIu32, this->epoch_seconds); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds); out.append(buffer); out.append("\n"); out.append("}"); @@ -4227,7 +4227,7 @@ void ListEntitiesUpdateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4256,7 +4256,7 @@ void ListEntitiesUpdateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -4265,7 +4265,7 @@ void UpdateStateResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("UpdateStateResponse {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); @@ -4282,7 +4282,7 @@ void UpdateStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" progress: "); - sprintf(buffer, "%g", this->progress); + snprintf(buffer, sizeof(buffer), "%g", this->progress); out.append(buffer); out.append("\n"); @@ -4307,7 +4307,7 @@ void UpdateStateResponse::dump_to(std::string &out) const { out.append("\n"); out.append(" device_id: "); - sprintf(buffer, "%" PRIu32, this->device_id); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); out.append("}"); @@ -4316,7 +4316,7 @@ void UpdateCommandRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("UpdateCommandRequest {\n"); out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); out.append(buffer); out.append("\n"); diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 2266dda81c..df1f3f8caa 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -290,7 +290,7 @@ class DoubleType(TypeInfo): wire_type = WireType.FIXED64 # Uses wire type 1 according to protobuf spec def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%g", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%g", {name});\n' o += "out.append(buffer);" return o @@ -312,7 +312,7 @@ class FloatType(TypeInfo): wire_type = WireType.FIXED32 # Uses wire type 5 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%g", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%g", {name});\n' o += "out.append(buffer);" return o @@ -334,7 +334,7 @@ class Int64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' o += "out.append(buffer);" return o @@ -356,7 +356,7 @@ class UInt64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%llu", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n' o += "out.append(buffer);" return o @@ -378,7 +378,7 @@ class Int32Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%" PRId32, {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId32, {name});\n' o += "out.append(buffer);" return o @@ -400,7 +400,7 @@ class Fixed64Type(TypeInfo): wire_type = WireType.FIXED64 # Uses wire type 1 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%llu", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n' o += "out.append(buffer);" return o @@ -422,7 +422,7 @@ class Fixed32Type(TypeInfo): wire_type = WireType.FIXED32 # Uses wire type 5 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%" PRIu32, {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRIu32, {name});\n' o += "out.append(buffer);" return o @@ -555,7 +555,7 @@ class UInt32Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%" PRIu32, {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRIu32, {name});\n' o += "out.append(buffer);" return o @@ -607,7 +607,7 @@ class SFixed32Type(TypeInfo): wire_type = WireType.FIXED32 # Uses wire type 5 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%" PRId32, {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId32, {name});\n' o += "out.append(buffer);" return o @@ -629,7 +629,7 @@ class SFixed64Type(TypeInfo): wire_type = WireType.FIXED64 # Uses wire type 1 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' o += "out.append(buffer);" return o @@ -651,7 +651,7 @@ class SInt32Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%" PRId32, {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId32, {name});\n' o += "out.append(buffer);" return o @@ -673,7 +673,7 @@ class SInt64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'sprintf(buffer, "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' o += "out.append(buffer);" return o From 3976fd02eaea2faaa56954b15772c5ed33a007b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jul 2025 15:39:13 -0500 Subject: [PATCH 10/15] Refactor duplicate socket read error handling in API frame helper (#9370) --- esphome/components/api/api_frame_helper.cpp | 72 ++++++++------------- esphome/components/api/api_frame_helper.h | 3 + 2 files changed, 31 insertions(+), 44 deletions(-) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 6ed9c95354..2f5acc3bfa 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -225,6 +225,22 @@ APIError APIFrameHelper::init_common_() { } #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->info_.c_str(), ##__VA_ARGS__) + +APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) { + if (received == -1) { + if (errno == EWOULDBLOCK || errno == EAGAIN) { + return APIError::WOULD_BLOCK; + } + state_ = State::FAILED; + HELPER_LOG("Socket read failed with errno %d", errno); + return APIError::SOCKET_READ_FAILED; + } else if (received == 0) { + state_ = State::FAILED; + HELPER_LOG("Connection closed"); + return APIError::CONNECTION_CLOSED; + } + return APIError::OK; +} // uncomment to log raw packets //#define HELPER_LOG_PACKETS @@ -327,17 +343,9 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { // no header information yet uint8_t to_read = 3 - rx_header_buf_len_; ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); - if (received == -1) { - if (errno == EWOULDBLOCK || errno == EAGAIN) { - return APIError::WOULD_BLOCK; - } - state_ = State::FAILED; - HELPER_LOG("Socket read failed with errno %d", errno); - return APIError::SOCKET_READ_FAILED; - } else if (received == 0) { - state_ = State::FAILED; - HELPER_LOG("Connection closed"); - return APIError::CONNECTION_CLOSED; + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; } rx_header_buf_len_ += static_cast(received); if (static_cast(received) != to_read) { @@ -372,17 +380,9 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { // more data to read uint16_t to_read = msg_size - rx_buf_len_; ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read); - if (received == -1) { - if (errno == EWOULDBLOCK || errno == EAGAIN) { - return APIError::WOULD_BLOCK; - } - state_ = State::FAILED; - HELPER_LOG("Socket read failed with errno %d", errno); - return APIError::SOCKET_READ_FAILED; - } else if (received == 0) { - state_ = State::FAILED; - HELPER_LOG("Connection closed"); - return APIError::CONNECTION_CLOSED; + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; } rx_buf_len_ += static_cast(received); if (static_cast(received) != to_read) { @@ -855,17 +855,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { // Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1); - if (received == -1) { - if (errno == EWOULDBLOCK || errno == EAGAIN) { - return APIError::WOULD_BLOCK; - } - state_ = State::FAILED; - HELPER_LOG("Socket read failed with errno %d", errno); - return APIError::SOCKET_READ_FAILED; - } else if (received == 0) { - state_ = State::FAILED; - HELPER_LOG("Connection closed"); - return APIError::CONNECTION_CLOSED; + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; } // If this was the first read, validate the indicator byte @@ -949,17 +941,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { // more data to read uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_; ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read); - if (received == -1) { - if (errno == EWOULDBLOCK || errno == EAGAIN) { - return APIError::WOULD_BLOCK; - } - state_ = State::FAILED; - HELPER_LOG("Socket read failed with errno %d", errno); - return APIError::SOCKET_READ_FAILED; - } else if (received == 0) { - state_ = State::FAILED; - HELPER_LOG("Connection closed"); - return APIError::CONNECTION_CLOSED; + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; } rx_buf_len_ += static_cast(received); if (static_cast(received) != to_read) { diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 1bb6bc7ed3..eae83a3484 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -176,6 +176,9 @@ class APIFrameHelper { // Common initialization for both plaintext and noise protocols APIError init_common_(); + + // Helper method to handle socket read results + APIError handle_socket_read_result_(ssize_t received); }; #ifdef USE_API_NOISE From fe258e10073a88bf41fa70cf29d1adbba4ad62cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jul 2025 15:49:23 -0500 Subject: [PATCH 11/15] Refactor entity lookup methods with macros in preparation for device_id support (#9371) --- esphome/components/api/api_connection.cpp | 108 ++++--------- esphome/core/application.h | 179 ++++------------------ 2 files changed, 62 insertions(+), 225 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 51a5769f99..13c5b345b6 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -42,6 +42,19 @@ static const char *const TAG = "api.connection"; static const int CAMERA_STOP_STREAM = 5000; #endif +// Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call object +#define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \ + entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \ + if ((entity_var) == nullptr) \ + return; \ + auto call = (entity_var)->make_call(); + +// Helper macro for entity command handlers that don't use make_call() - gets entity by key and returns if not found +#define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \ + entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \ + if ((entity_var) == nullptr) \ + return; + APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) @@ -361,11 +374,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c return encode_message_to_buffer(msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::cover_command(const CoverCommandRequest &msg) { - cover::Cover *cover = App.get_cover_by_key(msg.key); - if (cover == nullptr) - return; - - auto call = cover->make_call(); + ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) if (msg.has_legacy_command) { switch (msg.legacy_command) { case enums::LEGACY_COVER_COMMAND_OPEN: @@ -427,11 +436,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con return encode_message_to_buffer(msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::fan_command(const FanCommandRequest &msg) { - fan::Fan *fan = App.get_fan_by_key(msg.key); - if (fan == nullptr) - return; - - auto call = fan->make_call(); + ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan) if (msg.has_state) call.set_state(msg.state); if (msg.has_oscillating) @@ -504,11 +509,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c return encode_message_to_buffer(msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::light_command(const LightCommandRequest &msg) { - light::LightState *light = App.get_light_by_key(msg.key); - if (light == nullptr) - return; - - auto call = light->make_call(); + ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light) if (msg.has_state) call.set_state(msg.state); if (msg.has_brightness) @@ -597,9 +598,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection * return encode_message_to_buffer(msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::switch_command(const SwitchCommandRequest &msg) { - switch_::Switch *a_switch = App.get_switch_by_key(msg.key); - if (a_switch == nullptr) - return; + ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) if (msg.state) { a_switch->turn_on(); @@ -708,11 +707,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection return encode_message_to_buffer(msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::climate_command(const ClimateCommandRequest &msg) { - climate::Climate *climate = App.get_climate_by_key(msg.key); - if (climate == nullptr) - return; - - auto call = climate->make_call(); + ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate) if (msg.has_mode) call.set_mode(static_cast(msg.mode)); if (msg.has_target_temperature) @@ -767,11 +762,7 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection * return encode_message_to_buffer(msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::number_command(const NumberCommandRequest &msg) { - number::Number *number = App.get_number_by_key(msg.key); - if (number == nullptr) - return; - - auto call = number->make_call(); + ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) call.set_value(msg.state); call.perform(); } @@ -801,11 +792,7 @@ uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *co return encode_message_to_buffer(msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::date_command(const DateCommandRequest &msg) { - datetime::DateEntity *date = App.get_date_by_key(msg.key); - if (date == nullptr) - return; - - auto call = date->make_call(); + ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date) call.set_date(msg.year, msg.month, msg.day); call.perform(); } @@ -835,11 +822,7 @@ uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *co return encode_message_to_buffer(msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::time_command(const TimeCommandRequest &msg) { - datetime::TimeEntity *time = App.get_time_by_key(msg.key); - if (time == nullptr) - return; - - auto call = time->make_call(); + ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time) call.set_time(msg.hour, msg.minute, msg.second); call.perform(); } @@ -871,11 +854,7 @@ uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection return encode_message_to_buffer(msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { - datetime::DateTimeEntity *datetime = App.get_datetime_by_key(msg.key); - if (datetime == nullptr) - return; - - auto call = datetime->make_call(); + ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime) call.set_datetime(msg.epoch_seconds); call.perform(); } @@ -909,11 +888,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co return encode_message_to_buffer(msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::text_command(const TextCommandRequest &msg) { - text::Text *text = App.get_text_by_key(msg.key); - if (text == nullptr) - return; - - auto call = text->make_call(); + ENTITY_COMMAND_MAKE_CALL(text::Text, text, text) call.set_value(msg.state); call.perform(); } @@ -945,11 +920,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection * return encode_message_to_buffer(msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::select_command(const SelectCommandRequest &msg) { - select::Select *select = App.get_select_by_key(msg.key); - if (select == nullptr) - return; - - auto call = select->make_call(); + ENTITY_COMMAND_MAKE_CALL(select::Select, select, select) call.set_option(msg.state); call.perform(); } @@ -966,10 +937,7 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection * return encode_message_to_buffer(msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) { - button::Button *button = App.get_button_by_key(msg.key); - if (button == nullptr) - return; - + ENTITY_COMMAND_GET(button::Button, button, button) button->press(); } #endif @@ -1000,9 +968,7 @@ uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *co return encode_message_to_buffer(msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::lock_command(const LockCommandRequest &msg) { - lock::Lock *a_lock = App.get_lock_by_key(msg.key); - if (a_lock == nullptr) - return; + ENTITY_COMMAND_GET(lock::Lock, a_lock, lock) switch (msg.command) { case enums::LOCK_UNLOCK: @@ -1045,11 +1011,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c return encode_message_to_buffer(msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::valve_command(const ValveCommandRequest &msg) { - valve::Valve *valve = App.get_valve_by_key(msg.key); - if (valve == nullptr) - return; - - auto call = valve->make_call(); + ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) if (msg.has_position) call.set_position(msg.position); if (msg.stop) @@ -1096,11 +1058,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec return encode_message_to_buffer(msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { - media_player::MediaPlayer *media_player = App.get_media_player_by_key(msg.key); - if (media_player == nullptr) - return; - - auto call = media_player->make_call(); + ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player) if (msg.has_command) { call.set_command(static_cast(msg.command)); } @@ -1346,11 +1304,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, AP is_single); } void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) { - alarm_control_panel::AlarmControlPanel *a_alarm_control_panel = App.get_alarm_control_panel_by_key(msg.key); - if (a_alarm_control_panel == nullptr) - return; - - auto call = a_alarm_control_panel->make_call(); + ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel) switch (msg.command) { case enums::ALARM_CONTROL_PANEL_DISARM: call.disarm(); @@ -1438,9 +1392,7 @@ uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection * return encode_message_to_buffer(msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::update_command(const UpdateCommandRequest &msg) { - update::UpdateEntity *update = App.get_update_by_key(msg.key); - if (update == nullptr) - return; + ENTITY_COMMAND_GET(update::UpdateEntity, update, update) switch (msg.command) { case enums::UPDATE_COMMAND_UPDATE: diff --git a/esphome/core/application.h b/esphome/core/application.h index 6ee05309ca..f2b5cb5c89 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -368,6 +368,17 @@ class Application { uint8_t get_app_state() const { return this->app_state_; } +// Helper macro for entity getter method declarations - reduces code duplication +// When USE_DEVICE_ID is enabled in the future, this can be conditionally compiled to add device_id parameter +#define GET_ENTITY_METHOD(entity_type, entity_name, entities_member) \ + entity_type *get_##entity_name##_by_key(uint32_t key, bool include_internal = false) { \ + for (auto *obj : this->entities_member##_) { \ + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) \ + return obj; \ + } \ + return nullptr; \ + } + #ifdef USE_DEVICES const std::vector &get_devices() { return this->devices_; } #endif @@ -376,218 +387,92 @@ class Application { #endif #ifdef USE_BINARY_SENSOR const std::vector &get_binary_sensors() { return this->binary_sensors_; } - binary_sensor::BinarySensor *get_binary_sensor_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->binary_sensors_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(binary_sensor::BinarySensor, binary_sensor, binary_sensors) #endif #ifdef USE_SWITCH const std::vector &get_switches() { return this->switches_; } - switch_::Switch *get_switch_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->switches_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(switch_::Switch, switch, switches) #endif #ifdef USE_BUTTON const std::vector &get_buttons() { return this->buttons_; } - button::Button *get_button_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->buttons_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(button::Button, button, buttons) #endif #ifdef USE_SENSOR const std::vector &get_sensors() { return this->sensors_; } - sensor::Sensor *get_sensor_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->sensors_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(sensor::Sensor, sensor, sensors) #endif #ifdef USE_TEXT_SENSOR const std::vector &get_text_sensors() { return this->text_sensors_; } - text_sensor::TextSensor *get_text_sensor_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->text_sensors_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(text_sensor::TextSensor, text_sensor, text_sensors) #endif #ifdef USE_FAN const std::vector &get_fans() { return this->fans_; } - fan::Fan *get_fan_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->fans_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(fan::Fan, fan, fans) #endif #ifdef USE_COVER const std::vector &get_covers() { return this->covers_; } - cover::Cover *get_cover_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->covers_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(cover::Cover, cover, covers) #endif #ifdef USE_LIGHT const std::vector &get_lights() { return this->lights_; } - light::LightState *get_light_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->lights_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(light::LightState, light, lights) #endif #ifdef USE_CLIMATE const std::vector &get_climates() { return this->climates_; } - climate::Climate *get_climate_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->climates_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(climate::Climate, climate, climates) #endif #ifdef USE_NUMBER const std::vector &get_numbers() { return this->numbers_; } - number::Number *get_number_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->numbers_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(number::Number, number, numbers) #endif #ifdef USE_DATETIME_DATE const std::vector &get_dates() { return this->dates_; } - datetime::DateEntity *get_date_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->dates_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(datetime::DateEntity, date, dates) #endif #ifdef USE_DATETIME_TIME const std::vector &get_times() { return this->times_; } - datetime::TimeEntity *get_time_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->times_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(datetime::TimeEntity, time, times) #endif #ifdef USE_DATETIME_DATETIME const std::vector &get_datetimes() { return this->datetimes_; } - datetime::DateTimeEntity *get_datetime_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->datetimes_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(datetime::DateTimeEntity, datetime, datetimes) #endif #ifdef USE_TEXT const std::vector &get_texts() { return this->texts_; } - text::Text *get_text_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->texts_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(text::Text, text, texts) #endif #ifdef USE_SELECT const std::vector &get_selects() { return this->selects_; } - select::Select *get_select_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->selects_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(select::Select, select, selects) #endif #ifdef USE_LOCK const std::vector &get_locks() { return this->locks_; } - lock::Lock *get_lock_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->locks_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(lock::Lock, lock, locks) #endif #ifdef USE_VALVE const std::vector &get_valves() { return this->valves_; } - valve::Valve *get_valve_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->valves_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(valve::Valve, valve, valves) #endif #ifdef USE_MEDIA_PLAYER const std::vector &get_media_players() { return this->media_players_; } - media_player::MediaPlayer *get_media_player_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->media_players_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(media_player::MediaPlayer, media_player, media_players) #endif #ifdef USE_ALARM_CONTROL_PANEL const std::vector &get_alarm_control_panels() { return this->alarm_control_panels_; } - alarm_control_panel::AlarmControlPanel *get_alarm_control_panel_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->alarm_control_panels_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(alarm_control_panel::AlarmControlPanel, alarm_control_panel, alarm_control_panels) #endif #ifdef USE_EVENT const std::vector &get_events() { return this->events_; } - event::Event *get_event_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->events_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(event::Event, event, events) #endif #ifdef USE_UPDATE const std::vector &get_updates() { return this->updates_; } - update::UpdateEntity *get_update_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->updates_) { - if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) - return obj; - } - return nullptr; - } + GET_ENTITY_METHOD(update::UpdateEntity, update, updates) #endif Scheduler scheduler; From b122112d58ddac936844f62637abf9eec181d135 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jul 2025 15:51:17 -0500 Subject: [PATCH 12/15] Refactor API entity update dispatch to reduce code duplication (#9372) --- esphome/components/api/api_server.cpp | 150 ++++++++------------------ 1 file changed, 42 insertions(+), 108 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 575229cf04..70f2ff714d 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -260,180 +260,114 @@ bool APIServer::check_password(const std::string &password) const { void APIServer::handle_disconnect(APIConnection *conn) {} +// Macro for entities without extra parameters +#define API_DISPATCH_UPDATE(entity_type, entity_name) \ + void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ + if (obj->is_internal()) \ + return; \ + for (auto &c : this->clients_) \ + c->send_##entity_name##_state(obj); \ + } + +// Macro for entities with extra parameters (but parameters not used in send) +#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \ + void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { /* NOLINT(bugprone-macro-parentheses) */ \ + if (obj->is_internal()) \ + return; \ + for (auto &c : this->clients_) \ + c->send_##entity_name##_state(obj); \ + } + #ifdef USE_BINARY_SENSOR -void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_binary_sensor_state(obj); -} +API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor) #endif #ifdef USE_COVER -void APIServer::on_cover_update(cover::Cover *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_cover_state(obj); -} +API_DISPATCH_UPDATE(cover::Cover, cover) #endif #ifdef USE_FAN -void APIServer::on_fan_update(fan::Fan *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_fan_state(obj); -} +API_DISPATCH_UPDATE(fan::Fan, fan) #endif #ifdef USE_LIGHT -void APIServer::on_light_update(light::LightState *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_light_state(obj); -} +API_DISPATCH_UPDATE(light::LightState, light) #endif #ifdef USE_SENSOR -void APIServer::on_sensor_update(sensor::Sensor *obj, float state) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_sensor_state(obj); -} +API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state) #endif #ifdef USE_SWITCH -void APIServer::on_switch_update(switch_::Switch *obj, bool state) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_switch_state(obj); -} +API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state) #endif #ifdef USE_TEXT_SENSOR -void APIServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_text_sensor_state(obj); -} +API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state) #endif #ifdef USE_CLIMATE -void APIServer::on_climate_update(climate::Climate *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_climate_state(obj); -} +API_DISPATCH_UPDATE(climate::Climate, climate) #endif #ifdef USE_NUMBER -void APIServer::on_number_update(number::Number *obj, float state) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_number_state(obj); -} +API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state) #endif #ifdef USE_DATETIME_DATE -void APIServer::on_date_update(datetime::DateEntity *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_date_state(obj); -} +API_DISPATCH_UPDATE(datetime::DateEntity, date) #endif #ifdef USE_DATETIME_TIME -void APIServer::on_time_update(datetime::TimeEntity *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_time_state(obj); -} +API_DISPATCH_UPDATE(datetime::TimeEntity, time) #endif #ifdef USE_DATETIME_DATETIME -void APIServer::on_datetime_update(datetime::DateTimeEntity *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_datetime_state(obj); -} +API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime) #endif #ifdef USE_TEXT -void APIServer::on_text_update(text::Text *obj, const std::string &state) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_text_state(obj); -} +API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state) #endif #ifdef USE_SELECT -void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_select_state(obj); -} +API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index) #endif #ifdef USE_LOCK -void APIServer::on_lock_update(lock::Lock *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_lock_state(obj); -} +API_DISPATCH_UPDATE(lock::Lock, lock) #endif #ifdef USE_VALVE -void APIServer::on_valve_update(valve::Valve *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_valve_state(obj); -} +API_DISPATCH_UPDATE(valve::Valve, valve) #endif #ifdef USE_MEDIA_PLAYER -void APIServer::on_media_player_update(media_player::MediaPlayer *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_media_player_state(obj); -} +API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player) #endif #ifdef USE_EVENT +// Event is a special case - it's the only entity that passes extra parameters to the send method void APIServer::on_event(event::Event *obj, const std::string &event_type) { + if (obj->is_internal()) + return; for (auto &c : this->clients_) c->send_event(obj, event_type); } #endif #ifdef USE_UPDATE +// Update is a special case - the method is called on_update, not on_update_update void APIServer::on_update(update::UpdateEntity *obj) { + if (obj->is_internal()) + return; for (auto &c : this->clients_) c->send_update_state(obj); } #endif #ifdef USE_ALARM_CONTROL_PANEL -void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_alarm_control_panel_state(obj); -} +API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel) #endif float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } From 440de12e3f23c3a2e5be371fc40fc799f232634d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jul 2025 16:04:41 -0500 Subject: [PATCH 13/15] Don't compile unnecessary platform files (e.g. ESP8266 files on ESP32) (#9354) --- esphome/components/adc/__init__.py | 26 +++- esphome/components/api/__init__.py | 15 ++ esphome/components/debug/__init__.py | 20 +++ esphome/components/deep_sleep/__init__.py | 13 ++ esphome/components/http_request/__init__.py | 18 +++ esphome/components/i2c/__init__.py | 17 +++ esphome/components/logger/__init__.py | 24 ++++ esphome/components/mdns/__init__.py | 20 +++ esphome/components/mqtt/__init__.py | 12 ++ esphome/components/nextion/__init__.py | 16 +++ esphome/components/ota/__init__.py | 17 +++ .../components/remote_receiver/__init__.py | 18 +++ .../components/remote_transmitter/__init__.py | 18 +++ esphome/components/socket/__init__.py | 16 +++ esphome/components/spi/__init__.py | 17 +++ esphome/components/uart/__init__.py | 18 +++ esphome/components/wifi/__init__.py | 17 +++ esphome/config_helpers.py | 75 +++++++++- esphome/const.py | 61 ++++++-- esphome/core/config.py | 15 ++ esphome/dashboard/entries.py | 2 +- esphome/{dashboard => }/enum.py | 0 esphome/loader.py | 16 ++- tests/unit_tests/test_config_helpers.py | 135 ++++++++++++++++++ tests/unit_tests/test_loader.py | 63 ++++++++ 25 files changed, 657 insertions(+), 12 deletions(-) rename esphome/{dashboard => }/enum.py (100%) create mode 100644 tests/unit_tests/test_config_helpers.py create mode 100644 tests/unit_tests/test_loader.py diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index 5f94c61a08..10b7df8638 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -10,8 +10,15 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv -from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266 +from esphome.const import ( + CONF_ANALOG, + CONF_INPUT, + CONF_NUMBER, + PLATFORM_ESP8266, + PlatformFramework, +) from esphome.core import CORE CODEOWNERS = ["@esphome/core"] @@ -229,3 +236,20 @@ def validate_adc_pin(value): )(value) raise NotImplementedError + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "adc_sensor_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "adc_sensor_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + } +) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 2f1be28293..eb8883b025 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -3,6 +3,7 @@ import base64 from esphome import automation from esphome.automation import Condition import esphome.codegen as cg +from esphome.config_helpers import get_logger_level import esphome.config_validation as cv from esphome.const import ( CONF_ACTION, @@ -313,3 +314,17 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg @automation.register_condition("api.connected", APIConnectedCondition, {}) async def api_connected_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg) + + +def FILTER_SOURCE_FILES() -> list[str]: + """Filter out api_pb2_dump.cpp when proto message dumping is not enabled.""" + # api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined + # This is a particularly large file that still needs to be opened and read + # all the way to the end even when ifdef'd out + # + # HAS_PROTO_MESSAGE_DUMP is defined when ESPHOME_LOG_HAS_VERY_VERBOSE is set, + # which happens when the logger level is VERY_VERBOSE + if get_logger_level() != "VERY_VERBOSE": + return ["api_pb2_dump.cpp"] + + return [] diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py index 1955b5d22c..500dfac1fe 100644 --- a/esphome/components/debug/__init__.py +++ b/esphome/components/debug/__init__.py @@ -1,4 +1,5 @@ import esphome.codegen as cg +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_BLOCK, @@ -7,6 +8,7 @@ from esphome.const import ( CONF_FREE, CONF_ID, CONF_LOOP_TIME, + PlatformFramework, ) CODEOWNERS = ["@OttoWinter"] @@ -44,3 +46,21 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "debug_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "debug_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "debug_host.cpp": {PlatformFramework.HOST_NATIVE}, + "debug_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "debug_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + } +) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 63b359bd5b..55826f52bb 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -11,6 +11,7 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_DEFAULT, @@ -27,6 +28,7 @@ from esphome.const import ( CONF_WAKEUP_PIN, PLATFORM_ESP32, PLATFORM_ESP8266, + PlatformFramework, ) WAKEUP_PINS = { @@ -313,3 +315,14 @@ async def deep_sleep_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "deep_sleep_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "deep_sleep_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + } +) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 18373edb77..0d32bc97c2 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -2,6 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import esp32 from esphome.components.const import CONF_REQUEST_HEADERS +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ESP8266_DISABLE_SSL_SUPPORT, @@ -13,6 +14,7 @@ from esphome.const import ( CONF_URL, CONF_WATCHDOG_TIMEOUT, PLATFORM_HOST, + PlatformFramework, __version__, ) from esphome.core import CORE, Lambda @@ -319,3 +321,19 @@ async def http_request_action_to_code(config, action_id, template_arg, args): await automation.build_automation(trigger, [], conf) return var + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "http_request_host.cpp": {PlatformFramework.HOST_NATIVE}, + "http_request_arduino.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP8266_ARDUINO, + PlatformFramework.RP2040_ARDUINO, + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + "http_request_idf.cpp": {PlatformFramework.ESP32_IDF}, + } +) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 6adb9b71aa..4172b23845 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -3,6 +3,7 @@ import logging from esphome import pins import esphome.codegen as cg from esphome.components import esp32 +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, @@ -18,6 +19,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, + PlatformFramework, ) from esphome.core import CORE, coroutine_with_priority import esphome.final_validate as fv @@ -205,3 +207,18 @@ def final_validate_device_schema( {cv.Required(CONF_I2C_ID): fv.id_declaration_match_schema(hub_schema)}, extra=cv.ALLOW_EXTRA, ) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "i2c_bus_arduino.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP8266_ARDUINO, + PlatformFramework.RP2040_ARDUINO, + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + "i2c_bus_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + } +) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 3d4907aa6e..9ac2999696 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -21,6 +21,7 @@ from esphome.components.libretiny.const import ( COMPONENT_LN882X, COMPONENT_RTL87XX, ) +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ARGS, @@ -42,6 +43,7 @@ from esphome.const import ( PLATFORM_LN882X, PLATFORM_RP2040, PLATFORM_RTL87XX, + PlatformFramework, ) from esphome.core import CORE, Lambda, coroutine_with_priority @@ -444,3 +446,25 @@ async def logger_set_level_to_code(config, action_id, template_arg, args): lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) return cg.new_Pvariable(action_id, template_arg, lambda_) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "logger_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "logger_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "logger_host.cpp": {PlatformFramework.HOST_NATIVE}, + "logger_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "logger_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + "task_log_buffer.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + } +) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index ed230d43aa..e32d39cede 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components.esp32 import add_idf_component +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_DISABLED, @@ -8,6 +9,7 @@ from esphome.const import ( CONF_PROTOCOL, CONF_SERVICE, CONF_SERVICES, + PlatformFramework, ) from esphome.core import CORE, coroutine_with_priority @@ -108,3 +110,21 @@ async def to_code(config): ) cg.add(var.add_extra_service(exp)) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "mdns_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "mdns_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "mdns_host.cpp": {PlatformFramework.HOST_NATIVE}, + "mdns_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "mdns_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + } +) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index f0d5a95d43..1a6fcabf42 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -5,6 +5,7 @@ from esphome.automation import Condition import esphome.codegen as cg from esphome.components import logger from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_AVAILABILITY, @@ -54,6 +55,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PlatformFramework, ) from esphome.core import CORE, coroutine_with_priority @@ -596,3 +598,13 @@ async def mqtt_enable_to_code(config, action_id, template_arg, args): async def mqtt_disable_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "mqtt_backend_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + } +) diff --git a/esphome/components/nextion/__init__.py b/esphome/components/nextion/__init__.py index fb75daf4ba..8adc49d68c 100644 --- a/esphome/components/nextion/__init__.py +++ b/esphome/components/nextion/__init__.py @@ -1,5 +1,7 @@ import esphome.codegen as cg from esphome.components import uart +from esphome.config_helpers import filter_source_files_from_platform +from esphome.const import PlatformFramework nextion_ns = cg.esphome_ns.namespace("nextion") Nextion = nextion_ns.class_("Nextion", cg.PollingComponent, uart.UARTDevice) @@ -8,3 +10,17 @@ nextion_ref = Nextion.operator("ref") CONF_NEXTION_ID = "nextion_id" CONF_PUBLISH_STATE = "publish_state" CONF_SEND_TO_NEXTION = "send_to_nextion" + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "nextion_upload_arduino.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP8266_ARDUINO, + PlatformFramework.RP2040_ARDUINO, + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + "nextion_upload_idf.cpp": {PlatformFramework.ESP32_IDF}, + } +) diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 627c55e910..4d5b8a61e2 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -1,5 +1,6 @@ from esphome import automation import esphome.codegen as cg +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ESPHOME, @@ -7,6 +8,7 @@ from esphome.const import ( CONF_OTA, CONF_PLATFORM, CONF_TRIGGER_ID, + PlatformFramework, ) from esphome.core import CORE, coroutine_with_priority @@ -120,3 +122,18 @@ async def ota_to_code(var, config): use_state_callback = True if use_state_callback: cg.add_define("USE_OTA_STATE_CALLBACK") + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "ota_backend_arduino_esp32.cpp": {PlatformFramework.ESP32_ARDUINO}, + "ota_backend_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + "ota_backend_arduino_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "ota_backend_arduino_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "ota_backend_arduino_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + } +) diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 5de7d8c9c4..dffc088085 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -1,6 +1,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import esp32, esp32_rmt, remote_base +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, @@ -15,6 +16,7 @@ from esphome.const import ( CONF_TYPE, CONF_USE_DMA, CONF_VALUE, + PlatformFramework, ) from esphome.core import CORE, TimePeriod @@ -170,3 +172,19 @@ async def to_code(config): cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) cg.add(var.set_filter_us(config[CONF_FILTER])) cg.add(var.set_idle_us(config[CONF_IDLE])) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "remote_receiver_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "remote_receiver_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "remote_receiver_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + } +) diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 713cee0186..47a46ff56b 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -1,6 +1,7 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import esp32, esp32_rmt, remote_base +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_CARRIER_DUTY_PERCENT, @@ -12,6 +13,7 @@ from esphome.const import ( CONF_PIN, CONF_RMT_SYMBOLS, CONF_USE_DMA, + PlatformFramework, ) from esphome.core import CORE @@ -95,3 +97,19 @@ async def to_code(config): await automation.build_automation( var.get_complete_trigger(), [], on_complete_config ) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "remote_transmitter_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "remote_transmitter_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "remote_transmitter_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + } +) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 26031a8da5..e085a09eac 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome.core import CORE CODEOWNERS = ["@esphome/core"] @@ -40,3 +41,18 @@ async def to_code(config): elif impl == IMPLEMENTATION_BSD_SOCKETS: cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") cg.add_define("USE_SOCKET_SELECT_SUPPORT") + + +def FILTER_SOURCE_FILES() -> list[str]: + """Return list of socket implementation files that aren't selected by the user.""" + impl = CORE.config["socket"][CONF_IMPLEMENTATION] + + # Build list of files to exclude based on selected implementation + excluded = [] + if impl != IMPLEMENTATION_LWIP_TCP: + excluded.append("lwip_raw_tcp_impl.cpp") + if impl != IMPLEMENTATION_BSD_SOCKETS: + excluded.append("bsd_sockets_impl.cpp") + if impl != IMPLEMENTATION_LWIP_SOCKETS: + excluded.append("lwip_sockets_impl.cpp") + return excluded diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 55a4b9c8f6..58bfc3f411 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -13,6 +13,7 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_CLK_PIN, @@ -31,6 +32,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, + PlatformFramework, ) from esphome.core import CORE, coroutine_with_priority import esphome.final_validate as fv @@ -423,3 +425,18 @@ def final_validate_device_schema(name: str, *, require_mosi: bool, require_miso: {cv.Required(CONF_SPI_ID): fv.id_declaration_match_schema(hub_schema)}, extra=cv.ALLOW_EXTRA, ) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "spi_arduino.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP8266_ARDUINO, + PlatformFramework.RP2040_ARDUINO, + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + "spi_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + } +) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index a0908a299c..7d4c6360fe 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -2,6 +2,7 @@ import re from esphome import automation, pins import esphome.codegen as cg +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_AFTER, @@ -27,6 +28,7 @@ from esphome.const import ( CONF_TX_PIN, CONF_UART_ID, PLATFORM_HOST, + PlatformFramework, ) from esphome.core import CORE import esphome.final_validate as fv @@ -438,3 +440,19 @@ async def uart_write_to_code(config, action_id, template_arg, args): else: cg.add(var.set_data_static(data)) return var + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "uart_component_esp32_arduino.cpp": {PlatformFramework.ESP32_ARDUINO}, + "uart_component_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + "uart_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "uart_component_host.cpp": {PlatformFramework.HOST_NATIVE}, + "uart_component_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "uart_component_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + } +) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index e8ae9b1b4e..61f37556ba 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -3,6 +3,7 @@ from esphome.automation import Condition import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant from esphome.components.network import IPAddress +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_AP, @@ -39,6 +40,7 @@ from esphome.const import ( CONF_TTLS_PHASE_2, CONF_USE_ADDRESS, CONF_USERNAME, + PlatformFramework, ) from esphome.core import CORE, HexInt, coroutine_with_priority import esphome.final_validate as fv @@ -526,3 +528,18 @@ async def wifi_set_sta_to_code(config, action_id, template_arg, args): await automation.build_automation(var.get_error_trigger(), [], on_error_config) await cg.register_component(var, config) return var + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "wifi_component_esp32_arduino.cpp": {PlatformFramework.ESP32_ARDUINO}, + "wifi_component_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + "wifi_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "wifi_component_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + "wifi_component_pico_w.cpp": {PlatformFramework.RP2040_ARDUINO}, + } +) diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py index 54242bc259..50ce4e8e34 100644 --- a/esphome/config_helpers.py +++ b/esphome/config_helpers.py @@ -1,4 +1,20 @@ -from esphome.const import CONF_ID +from collections.abc import Callable + +from esphome.const import ( + CONF_ID, + CONF_LEVEL, + CONF_LOGGER, + KEY_CORE, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + PlatformFramework, +) +from esphome.core import CORE + +# Pre-build lookup map from (platform, framework) tuples to PlatformFramework enum +_PLATFORM_FRAMEWORK_LOOKUP = { + (pf.value[0].value, pf.value[1].value): pf for pf in PlatformFramework +} class Extend: @@ -103,3 +119,60 @@ def merge_config(full_old, full_new): return new return merge(full_old, full_new) + + +def filter_source_files_from_platform( + files_map: dict[str, set[PlatformFramework]], +) -> Callable[[], list[str]]: + """Helper to build a FILTER_SOURCE_FILES function from platform mapping. + + Args: + files_map: Dict mapping filename to set of PlatformFramework enums + that should compile this file + + Returns: + Function that returns list of files to exclude for current platform + """ + + def filter_source_files() -> list[str]: + # Get current platform/framework + core_data = CORE.data.get(KEY_CORE, {}) + target_platform = core_data.get(KEY_TARGET_PLATFORM) + target_framework = core_data.get(KEY_TARGET_FRAMEWORK) + + if not target_platform or not target_framework: + return [] + + # Direct lookup of current PlatformFramework + current_platform_framework = _PLATFORM_FRAMEWORK_LOOKUP.get( + (target_platform, target_framework) + ) + + if not current_platform_framework: + return [] + + # Return files that should be excluded for current platform + return [ + filename + for filename, platforms in files_map.items() + if current_platform_framework not in platforms + ] + + return filter_source_files + + +def get_logger_level() -> str: + """Get the configured logger level. + + This is used by components to determine what logging features to include + based on the configured log level. + + Returns: + The configured logger level string, defaults to "DEBUG" if not configured + """ + # Check if logger config exists + if CONF_LOGGER not in CORE.config: + return "DEBUG" + + logger_config = CORE.config[CONF_LOGGER] + return logger_config.get(CONF_LEVEL, "DEBUG") diff --git a/esphome/const.py b/esphome/const.py index 4aeb5179e6..085b9b39b8 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,5 +1,9 @@ """Constants used by esphome.""" +from enum import Enum + +from esphome.enum import StrEnum + __version__ = "2025.7.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" @@ -7,14 +11,55 @@ VALID_SUBSTITUTIONS_CHARACTERS = ( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" ) -PLATFORM_BK72XX = "bk72xx" -PLATFORM_ESP32 = "esp32" -PLATFORM_ESP8266 = "esp8266" -PLATFORM_HOST = "host" -PLATFORM_LIBRETINY_OLDSTYLE = "libretiny" -PLATFORM_LN882X = "ln882x" -PLATFORM_RP2040 = "rp2040" -PLATFORM_RTL87XX = "rtl87xx" + +class Platform(StrEnum): + """Platform identifiers for ESPHome.""" + + BK72XX = "bk72xx" + ESP32 = "esp32" + ESP8266 = "esp8266" + HOST = "host" + LIBRETINY_OLDSTYLE = "libretiny" + LN882X = "ln882x" + RP2040 = "rp2040" + RTL87XX = "rtl87xx" + + +class Framework(StrEnum): + """Framework identifiers for ESPHome.""" + + ARDUINO = "arduino" + ESP_IDF = "esp-idf" + NATIVE = "host" + + +class PlatformFramework(Enum): + """Combined platform-framework identifiers with tuple values.""" + + # ESP32 variants + ESP32_ARDUINO = (Platform.ESP32, Framework.ARDUINO) + ESP32_IDF = (Platform.ESP32, Framework.ESP_IDF) + + # Arduino framework platforms + ESP8266_ARDUINO = (Platform.ESP8266, Framework.ARDUINO) + RP2040_ARDUINO = (Platform.RP2040, Framework.ARDUINO) + BK72XX_ARDUINO = (Platform.BK72XX, Framework.ARDUINO) + RTL87XX_ARDUINO = (Platform.RTL87XX, Framework.ARDUINO) + LN882X_ARDUINO = (Platform.LN882X, Framework.ARDUINO) + + # Host platform (native) + HOST_NATIVE = (Platform.HOST, Framework.NATIVE) + + +# Maintain backward compatibility by reassigning after enum definition +PLATFORM_BK72XX = Platform.BK72XX +PLATFORM_ESP32 = Platform.ESP32 +PLATFORM_ESP8266 = Platform.ESP8266 +PLATFORM_HOST = Platform.HOST +PLATFORM_LIBRETINY_OLDSTYLE = Platform.LIBRETINY_OLDSTYLE +PLATFORM_LN882X = Platform.LN882X +PLATFORM_RP2040 = Platform.RP2040 +PLATFORM_RTL87XX = Platform.RTL87XX SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"} diff --git a/esphome/core/config.py b/esphome/core/config.py index 641c73a292..f73369f28f 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -6,6 +6,7 @@ from pathlib import Path from esphome import automation, core import esphome.codegen as cg +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_AREA, @@ -35,6 +36,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VERSION, KEY_CORE, + PlatformFramework, __version__ as ESPHOME_VERSION, ) from esphome.core import CORE, coroutine_with_priority @@ -551,3 +553,16 @@ async def to_code(config: ConfigType) -> None: cg.add(dev.set_area_id(area_id_hash)) cg.add(cg.App.register_device(dev)) + + +# Platform-specific source files for core +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "ring_buffer.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + # Note: lock_free_queue.h and event_pool.h are header files and don't need to be filtered + # as they are only included when needed by the preprocessor + } +) diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py index e4825298f7..b138cfd272 100644 --- a/esphome/dashboard/entries.py +++ b/esphome/dashboard/entries.py @@ -9,6 +9,7 @@ import os from typing import TYPE_CHECKING, Any from esphome import const, util +from esphome.enum import StrEnum from esphome.storage_json import StorageJSON, ext_storage_path from .const import ( @@ -18,7 +19,6 @@ from .const import ( EVENT_ENTRY_STATE_CHANGED, EVENT_ENTRY_UPDATED, ) -from .enum import StrEnum from .util.subprocess import async_run_system_command if TYPE_CHECKING: diff --git a/esphome/dashboard/enum.py b/esphome/enum.py similarity index 100% rename from esphome/dashboard/enum.py rename to esphome/enum.py diff --git a/esphome/loader.py b/esphome/loader.py index 79a1d7f576..7b2472521a 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -112,8 +112,17 @@ class ComponentManifest: This will return all cpp source files that are located in the same folder as the loaded .py file (does not look through subdirectories) """ - ret = [] + ret: list[FileResource] = [] + # Get filter function for source files + filter_source_files_func = getattr(self.module, "FILTER_SOURCE_FILES", None) + + # Get list of files to exclude + excluded_files = ( + set(filter_source_files_func()) if filter_source_files_func else set() + ) + + # Process all resources for resource in ( r.name for r in importlib.resources.files(self.package).iterdir() @@ -124,6 +133,11 @@ class ComponentManifest: if not importlib.resources.files(self.package).joinpath(resource).is_file(): # Not a resource = this is a directory (yeah this is confusing) continue + + # Skip excluded files + if resource in excluded_files: + continue + ret.append(FileResource(self.package, resource)) return ret diff --git a/tests/unit_tests/test_config_helpers.py b/tests/unit_tests/test_config_helpers.py new file mode 100644 index 0000000000..1c850e3759 --- /dev/null +++ b/tests/unit_tests/test_config_helpers.py @@ -0,0 +1,135 @@ +"""Unit tests for esphome.config_helpers module.""" + +from collections.abc import Callable +from unittest.mock import patch + +from esphome.config_helpers import filter_source_files_from_platform, get_logger_level +from esphome.const import ( + CONF_LEVEL, + CONF_LOGGER, + KEY_CORE, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + PlatformFramework, +) + + +def test_filter_source_files_from_platform_esp32() -> None: + """Test that filter_source_files_from_platform correctly filters files for ESP32 platform.""" + # Define test file mappings + files_map: dict[str, set[PlatformFramework]] = { + "logger_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "logger_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "logger_host.cpp": {PlatformFramework.HOST_NATIVE}, + "logger_common.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + PlatformFramework.ESP8266_ARDUINO, + PlatformFramework.HOST_NATIVE, + }, + } + + # Create the filter function + filter_func: Callable[[], list[str]] = filter_source_files_from_platform(files_map) + + # Test ESP32 with Arduino framework + mock_core_data: dict[str, dict[str, str]] = { + KEY_CORE: { + KEY_TARGET_PLATFORM: "esp32", + KEY_TARGET_FRAMEWORK: "arduino", + } + } + + with patch("esphome.config_helpers.CORE.data", mock_core_data): + excluded: list[str] = filter_func() + # ESP32 Arduino should exclude ESP8266 and HOST files + assert "logger_esp8266.cpp" in excluded + assert "logger_host.cpp" in excluded + # But not ESP32 or common files + assert "logger_esp32.cpp" not in excluded + assert "logger_common.cpp" not in excluded + + +def test_filter_source_files_from_platform_host() -> None: + """Test that filter_source_files_from_platform correctly filters files for HOST platform.""" + # Define test file mappings + files_map: dict[str, set[PlatformFramework]] = { + "logger_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "logger_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "logger_host.cpp": {PlatformFramework.HOST_NATIVE}, + "logger_common.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + PlatformFramework.ESP8266_ARDUINO, + PlatformFramework.HOST_NATIVE, + }, + } + + # Create the filter function + filter_func: Callable[[], list[str]] = filter_source_files_from_platform(files_map) + + # Test Host platform + mock_core_data: dict[str, dict[str, str]] = { + KEY_CORE: { + KEY_TARGET_PLATFORM: "host", + KEY_TARGET_FRAMEWORK: "host", # Framework.NATIVE is "host" + } + } + + with patch("esphome.config_helpers.CORE.data", mock_core_data): + excluded: list[str] = filter_func() + # Host should exclude ESP32 and ESP8266 files + assert "logger_esp32.cpp" in excluded + assert "logger_esp8266.cpp" in excluded + # But not host or common files + assert "logger_host.cpp" not in excluded + assert "logger_common.cpp" not in excluded + + +def test_filter_source_files_from_platform_handles_missing_data() -> None: + """Test that filter_source_files_from_platform returns empty list when platform/framework data is missing.""" + # Define test file mappings + files_map: dict[str, set[PlatformFramework]] = { + "logger_esp32.cpp": {PlatformFramework.ESP32_ARDUINO}, + "logger_host.cpp": {PlatformFramework.HOST_NATIVE}, + } + + # Create the filter function + filter_func: Callable[[], list[str]] = filter_source_files_from_platform(files_map) + + # Test case: Missing platform/framework data + mock_core_data: dict[str, dict[str, str]] = {KEY_CORE: {}} + + with patch("esphome.config_helpers.CORE.data", mock_core_data): + excluded: list[str] = filter_func() + # Should return empty list when platform/framework not set + assert excluded == [] + + +def test_get_logger_level() -> None: + """Test get_logger_level helper function.""" + # Test no logger config - should return default DEBUG + mock_config = {} + with patch("esphome.config_helpers.CORE.config", mock_config): + assert get_logger_level() == "DEBUG" + + # Test with logger set to INFO + mock_config = {CONF_LOGGER: {CONF_LEVEL: "INFO"}} + with patch("esphome.config_helpers.CORE.config", mock_config): + assert get_logger_level() == "INFO" + + # Test with VERY_VERBOSE + mock_config = {CONF_LOGGER: {CONF_LEVEL: "VERY_VERBOSE"}} + with patch("esphome.config_helpers.CORE.config", mock_config): + assert get_logger_level() == "VERY_VERBOSE" + + # Test with logger missing level (uses default DEBUG) + mock_config = {CONF_LOGGER: {}} + with patch("esphome.config_helpers.CORE.config", mock_config): + assert get_logger_level() == "DEBUG" diff --git a/tests/unit_tests/test_loader.py b/tests/unit_tests/test_loader.py new file mode 100644 index 0000000000..c6d4c4aef0 --- /dev/null +++ b/tests/unit_tests/test_loader.py @@ -0,0 +1,63 @@ +"""Unit tests for esphome.loader module.""" + +from unittest.mock import MagicMock, patch + +from esphome.loader import ComponentManifest + + +def test_component_manifest_resources_with_filter_source_files() -> None: + """Test that ComponentManifest.resources correctly filters out excluded files.""" + # Create a mock module with FILTER_SOURCE_FILES function + mock_module = MagicMock() + mock_module.FILTER_SOURCE_FILES = lambda: [ + "platform_esp32.cpp", + "platform_esp8266.cpp", + ] + mock_module.__package__ = "esphome.components.test_component" + + # Create ComponentManifest instance + manifest = ComponentManifest(mock_module) + + # Mock the files in the package + def create_mock_file(filename: str) -> MagicMock: + mock_file = MagicMock() + mock_file.name = filename + mock_file.is_file.return_value = True + return mock_file + + mock_files = [ + create_mock_file("test.cpp"), + create_mock_file("test.h"), + create_mock_file("platform_esp32.cpp"), + create_mock_file("platform_esp8266.cpp"), + create_mock_file("common.cpp"), + create_mock_file("README.md"), # Should be excluded by extension + ] + + # Mock importlib.resources + with patch("importlib.resources.files") as mock_files_func: + mock_package_files = MagicMock() + mock_package_files.iterdir.return_value = mock_files + mock_package_files.joinpath = lambda name: MagicMock(is_file=lambda: True) + mock_files_func.return_value = mock_package_files + + # Get resources + resources = manifest.resources + + # Convert to list of filenames for easier testing + resource_names = [r.resource for r in resources] + + # Check that platform files are excluded + assert "platform_esp32.cpp" not in resource_names + assert "platform_esp8266.cpp" not in resource_names + + # Check that other source files are included + assert "test.cpp" in resource_names + assert "test.h" in resource_names + assert "common.cpp" in resource_names + + # Check that non-source files are excluded + assert "README.md" not in resource_names + + # Verify the correct number of resources + assert len(resources) == 3 # test.cpp, test.h, common.cpp From 206659ddb82d1a95a29d38b78f76200b3a8c9204 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jul 2025 16:15:49 -0500 Subject: [PATCH 14/15] Refactor voice assistant API methods to reduce code duplication (#9374) --- esphome/components/api/api_connection.cpp | 103 +++++++++------------- esphome/components/api/api_connection.h | 5 ++ 2 files changed, 47 insertions(+), 61 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 13c5b345b6..01f4552842 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1176,66 +1176,53 @@ void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequ #endif #ifdef USE_VOICE_ASSISTANT +bool APIConnection::check_voice_assistant_api_connection_() const { + return voice_assistant::global_voice_assistant != nullptr && + voice_assistant::global_voice_assistant->get_api_connection() == this; +} + void APIConnection::subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) { if (voice_assistant::global_voice_assistant != nullptr) { voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe); } } void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) { - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return; - } + if (!this->check_voice_assistant_api_connection_()) { + return; + } - if (msg.error) { - voice_assistant::global_voice_assistant->failed_to_start(); - return; - } - if (msg.port == 0) { - // Use API Audio - voice_assistant::global_voice_assistant->start_streaming(); - } else { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - this->helper_->getpeername((struct sockaddr *) &storage, &len); - voice_assistant::global_voice_assistant->start_streaming(&storage, msg.port); - } + if (msg.error) { + voice_assistant::global_voice_assistant->failed_to_start(); + return; + } + if (msg.port == 0) { + // Use API Audio + voice_assistant::global_voice_assistant->start_streaming(); + } else { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + this->helper_->getpeername((struct sockaddr *) &storage, &len); + voice_assistant::global_voice_assistant->start_streaming(&storage, msg.port); } }; void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) { - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return; - } - + if (this->check_voice_assistant_api_connection_()) { voice_assistant::global_voice_assistant->on_event(msg); } } void APIConnection::on_voice_assistant_audio(const VoiceAssistantAudio &msg) { - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return; - } - + if (this->check_voice_assistant_api_connection_()) { voice_assistant::global_voice_assistant->on_audio(msg); } }; void APIConnection::on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) { - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return; - } - + if (this->check_voice_assistant_api_connection_()) { voice_assistant::global_voice_assistant->on_timer_event(msg); } }; void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) { - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return; - } - + if (this->check_voice_assistant_api_connection_()) { voice_assistant::global_voice_assistant->on_announce(msg); } } @@ -1243,35 +1230,29 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno VoiceAssistantConfigurationResponse APIConnection::voice_assistant_get_configuration( const VoiceAssistantConfigurationRequest &msg) { VoiceAssistantConfigurationResponse resp; - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return resp; - } - - auto &config = voice_assistant::global_voice_assistant->get_configuration(); - for (auto &wake_word : config.available_wake_words) { - VoiceAssistantWakeWord resp_wake_word; - resp_wake_word.id = wake_word.id; - resp_wake_word.wake_word = wake_word.wake_word; - for (const auto &lang : wake_word.trained_languages) { - resp_wake_word.trained_languages.push_back(lang); - } - resp.available_wake_words.push_back(std::move(resp_wake_word)); - } - for (auto &wake_word_id : config.active_wake_words) { - resp.active_wake_words.push_back(wake_word_id); - } - resp.max_active_wake_words = config.max_active_wake_words; + if (!this->check_voice_assistant_api_connection_()) { + return resp; } + + auto &config = voice_assistant::global_voice_assistant->get_configuration(); + for (auto &wake_word : config.available_wake_words) { + VoiceAssistantWakeWord resp_wake_word; + resp_wake_word.id = wake_word.id; + resp_wake_word.wake_word = wake_word.wake_word; + for (const auto &lang : wake_word.trained_languages) { + resp_wake_word.trained_languages.push_back(lang); + } + resp.available_wake_words.push_back(std::move(resp_wake_word)); + } + for (auto &wake_word_id : config.active_wake_words) { + resp.active_wake_words.push_back(wake_word_id); + } + resp.max_active_wake_words = config.max_active_wake_words; return resp; } void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return; - } - + if (this->check_voice_assistant_api_connection_()) { voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words); } } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 166dbc3656..aa323d339d 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -301,6 +301,11 @@ class APIConnection : public APIServerConnection { static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, uint32_t remaining_size, bool is_single); +#ifdef USE_VOICE_ASSISTANT + // Helper to check voice assistant validity and connection ownership + inline bool check_voice_assistant_api_connection_() const; +#endif + // Helper method to process multiple entities from an iterator in a batch template void process_iterator_batch_(Iterator &iterator) { size_t initial_size = this->deferred_batch_.size(); From 42a1f6922fe78b049c00b9494542afe1bf1cb0c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jul 2025 16:16:48 -0500 Subject: [PATCH 15/15] Eliminate bluetooth_proxy guard variable to save 8 bytes RAM (#9343) --- .../components/bluetooth_proxy/bluetooth_proxy.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 98f11fac7a..a5e8ec0860 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -58,10 +58,15 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) // 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload // This achieves ~97% WiFi MTU utilization while staying under the limit static constexpr size_t FLUSH_BATCH_SIZE = 16; -static std::vector &get_batch_buffer() { - static std::vector batch_buffer; - return batch_buffer; -} + +namespace { +// Batch buffer in anonymous namespace to avoid guard variable (saves 8 bytes) +// This is initialized at program startup before any threads +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +std::vector batch_buffer; +} // namespace + +static std::vector &get_batch_buffer() { return batch_buffer; } bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) { if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_)