From a28f331f73ed8f8b0c830767422718ce9cf67cff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 15:48:10 -1000 Subject: [PATCH 01/14] [text_sensor] Replace truncation guard with ESPHOME_DEBUG_ASSERT --- esphome/components/text_sensor/filter.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index ccc52e2770..41eed5f49b 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -167,10 +167,9 @@ template class MapFilter : public Filter { explicit MapFilter(const std::initializer_list &mappings) { size_t i = 0; for (const auto &m : mappings) { - if (i >= N) - break; this->mappings_[i++] = m; } + ESPHOME_DEBUG_ASSERT(i == N); } bool new_value(std::string &value) override { return map_filter_apply(this->mappings_.data(), N, value); } From c2c34d39e7f1f55cef3debe9a29fe4caf588a62a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 15:55:56 -1000 Subject: [PATCH 02/14] [text_sensor] Assert size before writing, not after --- esphome/components/text_sensor/filter.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index 41eed5f49b..0366c7fb41 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -165,11 +165,11 @@ bool map_filter_apply(const Substitution *mappings, size_t count, std::string &v template class MapFilter : public Filter { public: explicit MapFilter(const std::initializer_list &mappings) { + ESPHOME_DEBUG_ASSERT(mappings.size() == N); size_t i = 0; for (const auto &m : mappings) { this->mappings_[i++] = m; } - ESPHOME_DEBUG_ASSERT(i == N); } bool new_value(std::string &value) override { return map_filter_apply(this->mappings_.data(), N, value); } From 7f5409214b66f0b79e8016913397d54b50466137 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 16:14:59 -1000 Subject: [PATCH 03/14] Use init_array_from helper for optimal codegen Uses memcpy for trivially copyable types, element-wise copy otherwise. ESPHOME_DEBUG_ASSERT catches size mismatches in integration tests. --- esphome/components/text_sensor/filter.h | 6 +----- esphome/core/helpers.h | 15 +++++++++++++++ tests/integration/fixtures/.gitignore | 5 +++++ 3 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 tests/integration/fixtures/.gitignore diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index 0366c7fb41..07832af9e2 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -165,11 +165,7 @@ bool map_filter_apply(const Substitution *mappings, size_t count, std::string &v template class MapFilter : public Filter { public: explicit MapFilter(const std::initializer_list &mappings) { - ESPHOME_DEBUG_ASSERT(mappings.size() == N); - size_t i = 0; - for (const auto &m : mappings) { - this->mappings_[i++] = m; - } + init_array_from(this->mappings_, mappings); } bool new_value(std::string &value) override { return map_filter_apply(this->mappings_.data(), N, value); } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 82c6b3833c..51feaa57c5 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -497,6 +497,21 @@ template::max()> index_type capacity_{0}; }; +/// Initialize a std::array from an initializer_list. Uses memcpy for trivially copyable types (optimal codegen), +/// falls back to element-wise copy for non-trivially copyable types (e.g. TemplatableValue). +/// N is set by code generation; ESPHOME_DEBUG_ASSERT catches mismatches in debug/integration tests. +template inline void init_array_from(std::array &dest, std::initializer_list src) { + ESPHOME_DEBUG_ASSERT(src.size() == N); + if constexpr (std::is_trivially_copyable_v) { + __builtin_memcpy(dest.data(), src.begin(), N * sizeof(T)); + } else { + size_t i = 0; + for (const auto &v : src) { + dest[i++] = v; + } + } +} + /// Fixed-capacity vector - allocates once at runtime, never reallocates /// This avoids std::vector template overhead (_M_realloc_insert, _M_default_append) /// when size is known at initialization but not at compile time diff --git a/tests/integration/fixtures/.gitignore b/tests/integration/fixtures/.gitignore new file mode 100644 index 0000000000..d8b4157aef --- /dev/null +++ b/tests/integration/fixtures/.gitignore @@ -0,0 +1,5 @@ +# Gitignore settings for ESPHome +# This is an example and may include too much for your use-case. +# You can modify this file to suit your needs. +/.esphome/ +/secrets.yaml From 3b01e65a2a61fdc0d82c0a65c46dffa4e4917dec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 16:24:35 -1000 Subject: [PATCH 04/14] Remove accidentally committed .gitignore --- tests/integration/fixtures/.gitignore | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 tests/integration/fixtures/.gitignore diff --git a/tests/integration/fixtures/.gitignore b/tests/integration/fixtures/.gitignore deleted file mode 100644 index d8b4157aef..0000000000 --- a/tests/integration/fixtures/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Gitignore settings for ESPHome -# This is an example and may include too much for your use-case. -# You can modify this file to suit your needs. -/.esphome/ -/secrets.yaml From fa6df8ea2abc8b1bff7259401c0fbb0f29acc595 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 16:27:38 -1000 Subject: [PATCH 05/14] Fix: inline ESPHOME_DEBUG_ASSERT to avoid include order issue --- esphome/core/helpers.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 51feaa57c5..6922cd9cc0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -501,7 +501,9 @@ template::max()> /// falls back to element-wise copy for non-trivially copyable types (e.g. TemplatableValue). /// N is set by code generation; ESPHOME_DEBUG_ASSERT catches mismatches in debug/integration tests. template inline void init_array_from(std::array &dest, std::initializer_list src) { - ESPHOME_DEBUG_ASSERT(src.size() == N); +#ifdef ESPHOME_DEBUG + assert(src.size() == N); +#endif if constexpr (std::is_trivially_copyable_v) { __builtin_memcpy(dest.data(), src.begin(), N * sizeof(T)); } else { From 1d8be72a4443b01de5fb367360bed69a8f954e95 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 16:27:40 -1000 Subject: [PATCH 06/14] Fix: inline ESPHOME_DEBUG_ASSERT to avoid include order issue --- esphome/core/helpers.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 51feaa57c5..6922cd9cc0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -501,7 +501,9 @@ template::max()> /// falls back to element-wise copy for non-trivially copyable types (e.g. TemplatableValue). /// N is set by code generation; ESPHOME_DEBUG_ASSERT catches mismatches in debug/integration tests. template inline void init_array_from(std::array &dest, std::initializer_list src) { - ESPHOME_DEBUG_ASSERT(src.size() == N); +#ifdef ESPHOME_DEBUG + assert(src.size() == N); +#endif if constexpr (std::is_trivially_copyable_v) { __builtin_memcpy(dest.data(), src.begin(), N * sizeof(T)); } else { From 1a9c1d484456a38bef38a545e48a0135cee5e1d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 16:27:42 -1000 Subject: [PATCH 07/14] Fix: inline ESPHOME_DEBUG_ASSERT to avoid include order issue --- esphome/core/helpers.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 51feaa57c5..6922cd9cc0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -501,7 +501,9 @@ template::max()> /// falls back to element-wise copy for non-trivially copyable types (e.g. TemplatableValue). /// N is set by code generation; ESPHOME_DEBUG_ASSERT catches mismatches in debug/integration tests. template inline void init_array_from(std::array &dest, std::initializer_list src) { - ESPHOME_DEBUG_ASSERT(src.size() == N); +#ifdef ESPHOME_DEBUG + assert(src.size() == N); +#endif if constexpr (std::is_trivially_copyable_v) { __builtin_memcpy(dest.data(), src.begin(), N * sizeof(T)); } else { From 4eec3f50768c7abdc9f1fe522277e5c65eeaeb51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 16:27:44 -1000 Subject: [PATCH 08/14] Fix: inline ESPHOME_DEBUG_ASSERT to avoid include order issue --- esphome/core/helpers.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 51feaa57c5..6922cd9cc0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -501,7 +501,9 @@ template::max()> /// falls back to element-wise copy for non-trivially copyable types (e.g. TemplatableValue). /// N is set by code generation; ESPHOME_DEBUG_ASSERT catches mismatches in debug/integration tests. template inline void init_array_from(std::array &dest, std::initializer_list src) { - ESPHOME_DEBUG_ASSERT(src.size() == N); +#ifdef ESPHOME_DEBUG + assert(src.size() == N); +#endif if constexpr (std::is_trivially_copyable_v) { __builtin_memcpy(dest.data(), src.begin(), N * sizeof(T)); } else { From 5d7ae5b3ce447a6110d84a854e9813124f67f166 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 16:27:46 -1000 Subject: [PATCH 09/14] Fix: inline ESPHOME_DEBUG_ASSERT to avoid include order issue --- esphome/core/helpers.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 51feaa57c5..6922cd9cc0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -501,7 +501,9 @@ template::max()> /// falls back to element-wise copy for non-trivially copyable types (e.g. TemplatableValue). /// N is set by code generation; ESPHOME_DEBUG_ASSERT catches mismatches in debug/integration tests. template inline void init_array_from(std::array &dest, std::initializer_list src) { - ESPHOME_DEBUG_ASSERT(src.size() == N); +#ifdef ESPHOME_DEBUG + assert(src.size() == N); +#endif if constexpr (std::is_trivially_copyable_v) { __builtin_memcpy(dest.data(), src.begin(), N * sizeof(T)); } else { From 327d0597af03fa5f10d6de9d9a62011558a0de35 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 16:27:48 -1000 Subject: [PATCH 10/14] Fix: inline ESPHOME_DEBUG_ASSERT to avoid include order issue --- esphome/core/helpers.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 51feaa57c5..6922cd9cc0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -501,7 +501,9 @@ template::max()> /// falls back to element-wise copy for non-trivially copyable types (e.g. TemplatableValue). /// N is set by code generation; ESPHOME_DEBUG_ASSERT catches mismatches in debug/integration tests. template inline void init_array_from(std::array &dest, std::initializer_list src) { - ESPHOME_DEBUG_ASSERT(src.size() == N); +#ifdef ESPHOME_DEBUG + assert(src.size() == N); +#endif if constexpr (std::is_trivially_copyable_v) { __builtin_memcpy(dest.data(), src.begin(), N * sizeof(T)); } else { From 4cba679469e359d5c54c9c71c415a97ad3ee4bec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 16:27:50 -1000 Subject: [PATCH 11/14] Fix: inline ESPHOME_DEBUG_ASSERT to avoid include order issue --- esphome/core/helpers.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 51feaa57c5..6922cd9cc0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -501,7 +501,9 @@ template::max()> /// falls back to element-wise copy for non-trivially copyable types (e.g. TemplatableValue). /// N is set by code generation; ESPHOME_DEBUG_ASSERT catches mismatches in debug/integration tests. template inline void init_array_from(std::array &dest, std::initializer_list src) { - ESPHOME_DEBUG_ASSERT(src.size() == N); +#ifdef ESPHOME_DEBUG + assert(src.size() == N); +#endif if constexpr (std::is_trivially_copyable_v) { __builtin_memcpy(dest.data(), src.begin(), N * sizeof(T)); } else { From 711ba557567e563b3d24f19c0904907ee0a18e07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 16:27:52 -1000 Subject: [PATCH 12/14] Fix: inline ESPHOME_DEBUG_ASSERT to avoid include order issue --- esphome/core/helpers.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 51feaa57c5..6922cd9cc0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -501,7 +501,9 @@ template::max()> /// falls back to element-wise copy for non-trivially copyable types (e.g. TemplatableValue). /// N is set by code generation; ESPHOME_DEBUG_ASSERT catches mismatches in debug/integration tests. template inline void init_array_from(std::array &dest, std::initializer_list src) { - ESPHOME_DEBUG_ASSERT(src.size() == N); +#ifdef ESPHOME_DEBUG + assert(src.size() == N); +#endif if constexpr (std::is_trivially_copyable_v) { __builtin_memcpy(dest.data(), src.begin(), N * sizeof(T)); } else { From 19e497cd6121845655d1df598c82dd61b483bc61 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 16:58:49 -1000 Subject: [PATCH 13/14] Remove get_loop_component_start_time wrapper, use forward-declared App directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wrapper added a function call indirection on every filter invocation that the compiler cannot inline without LTO. Forward-declaring Application and App lets the template body call App.get_loop_component_start_time() directly — the method is inline in application.h which is included by main.cpp where the template is instantiated. --- esphome/components/sensor/filter.cpp | 2 -- esphome/components/sensor/filter.h | 12 +++++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index eef5c6cade..998f34be0f 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -222,8 +222,6 @@ MultiplyFilter::MultiplyFilter(TemplatableValue multiplier) : multiplier_ optional MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); } -uint32_t get_loop_component_start_time() { return App.get_loop_component_start_time(); } - // ValueListFilter helper (non-template, shared by all ValueListFilter instantiations) bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableValue *values, size_t count) { int8_t accuracy = parent->get_accuracy_decimals(); diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 87a309ffb3..db8156f544 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -332,8 +332,14 @@ class MultiplyFilter : public Filter { /// Non-template helper for value matching (implementation in filter.cpp) bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableValue *values, size_t count); -/// Non-template helper to get cached loop start time (avoids circular include of application.h) -uint32_t get_loop_component_start_time(); +} // namespace esphome::sensor +// Forward declaration — avoids circular include of application.h. +// Template bodies are only instantiated in main.cpp where Application is fully defined. +namespace esphome { +class Application; +extern Application App; +} // namespace esphome +namespace esphome::sensor { /** Base class for filters that compare sensor values against a fixed list of configured values. * @@ -389,7 +395,7 @@ template class ThrottleWithPriorityFilter : public ValueListFilter : ValueListFilter(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {} optional new_value(float value) override { - const uint32_t now = get_loop_component_start_time(); + const uint32_t now = App.get_loop_component_start_time(); if (this->last_input_ == 0 || now - this->last_input_ >= this->min_time_between_inputs_ || this->value_matches_any_(value)) { this->last_input_ = now; From a3ccb656b25fae9df16791a90364f38bb24cfe36 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2026 17:09:44 -1000 Subject: [PATCH 14/14] Replace forward-declared App with throttle_check_and_update helper Forward declaration fails when filter.h is included before application.h (incomplete type error). Instead, extract the throttle time check into a non-template helper in filter.cpp that accesses App directly. This keeps the same call overhead as the old ThrottleFilter (one function call) but the helper does the time check AND updates last_input, so the template new_value() body has no App dependency at all. --- esphome/components/sensor/filter.cpp | 9 +++++++++ esphome/components/sensor/filter.h | 15 ++++----------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 0b2c45289a..7afc43b8aa 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -222,6 +222,15 @@ MultiplyFilter::MultiplyFilter(TemplatableValue multiplier) : multiplier_ optional MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); } +bool throttle_check_and_update(uint32_t &last_input, uint32_t min_time_between_inputs) { + const uint32_t now = App.get_loop_component_start_time(); + if (last_input == 0 || now - last_input >= min_time_between_inputs) { + last_input = now; + return true; + } + return false; +} + // ValueListFilter helper (non-template, shared by all ValueListFilter instantiations) bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableValue *values, size_t count) { int8_t accuracy = parent->get_accuracy_decimals(); diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index cc9644f6e1..9e97ad432a 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -332,14 +332,9 @@ class MultiplyFilter : public Filter { /// Non-template helper for value matching (implementation in filter.cpp) bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableValue *values, size_t count); -} // namespace esphome::sensor -// Forward declaration — avoids circular include of application.h. -// Template bodies are only instantiated in main.cpp where Application is fully defined. -namespace esphome { -class Application; -extern Application App; -} // namespace esphome -namespace esphome::sensor { +/// Returns true if throttle should allow the value through (time expired or first input). +/// Updates last_input in-place. Implementation in filter.cpp (accesses App without circular include). +bool throttle_check_and_update(uint32_t &last_input, uint32_t min_time_between_inputs); /** Base class for filters that compare sensor values against a fixed list of configured values. * @@ -395,10 +390,8 @@ template class ThrottleWithPriorityFilter : public ValueListFilter : ValueListFilter(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {} optional new_value(float value) override { - const uint32_t now = App.get_loop_component_start_time(); - if (this->last_input_ == 0 || now - this->last_input_ >= this->min_time_between_inputs_ || + if (throttle_check_and_update(this->last_input_, this->min_time_between_inputs_) || this->value_matches_any_(value)) { - this->last_input_ = now; return value; } return {};