From 5b8acaa7c0b5e70246dd9e9fe917719c7ee52779 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Mar 2026 20:05:49 -1000 Subject: [PATCH 1/4] [combination] Store parent pointer to enable inline Callback storage Replace std::pair with a SensorSource struct that includes a back-pointer to the parent component. This allows KalmanCombinationComponent::setup() to capture only [&source] (one pointer) instead of [this, stddev] (this + a full std::function copy), enabling inline Callback storage and avoiding a heap allocation per sensor callback registration. --- .../components/combination/combination.cpp | 32 +++++++++---------- esphome/components/combination/combination.h | 15 ++++++--- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp index 2f0bd26a02..27d6af1346 100644 --- a/esphome/components/combination/combination.cpp +++ b/esphome/components/combination/combination.cpp @@ -4,8 +4,6 @@ #include "esphome/core/hal.h" #include -#include -#include namespace esphome { namespace combination { @@ -20,12 +18,12 @@ void CombinationComponent::log_config_(const LogString *combo_type) { void CombinationNoParameterComponent::add_source(Sensor *sensor) { this->sensors_.emplace_back(sensor); } -void CombinationOneParameterComponent::add_source(Sensor *sensor, std::function const &stddev) { - this->sensor_pairs_.emplace_back(sensor, stddev); +void CombinationOneParameterComponent::add_source(Sensor *sensor, std::function const &compute) { + this->sensor_sources_.push_back({sensor, compute, this}); } -void CombinationOneParameterComponent::add_source(Sensor *sensor, float stddev) { - this->add_source(sensor, std::function{[stddev](float x) -> float { return stddev; }}); +void CombinationOneParameterComponent::add_source(Sensor *sensor, float value) { + this->add_source(sensor, std::function{[value](float x) -> float { return value; }}); } void CombinationNoParameterComponent::log_source_sensors() { @@ -37,9 +35,8 @@ void CombinationNoParameterComponent::log_source_sensors() { void CombinationOneParameterComponent::log_source_sensors() { ESP_LOGCONFIG(TAG, " Source Sensors:"); - for (const auto &sensor : this->sensor_pairs_) { - auto &entity = *sensor.first; - ESP_LOGCONFIG(TAG, " - %s", entity.get_name().c_str()); + for (const auto &source : this->sensor_sources_) { + ESP_LOGCONFIG(TAG, " - %s", source.sensor->get_name().c_str()); } } @@ -62,9 +59,10 @@ void KalmanCombinationComponent::dump_config() { } void KalmanCombinationComponent::setup() { - for (const auto &sensor : this->sensor_pairs_) { - const auto stddev = sensor.second; - sensor.first->add_on_state_callback([this, stddev](float x) -> void { this->correct_(x, stddev(x)); }); + for (auto &source : this->sensor_sources_) { + source.sensor->add_on_state_callback([&source](float x) -> void { + static_cast(source.parent)->correct_(x, source.compute(x)); + }); } } @@ -117,10 +115,10 @@ void KalmanCombinationComponent::correct_(float value, float stddev) { } void LinearCombinationComponent::setup() { - for (const auto &sensor : this->sensor_pairs_) { + for (auto &source : this->sensor_sources_) { // All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result // repeatedly in the same loop if multiple source senors update. - sensor.first->add_on_state_callback( + source.sensor->add_on_state_callback( [this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); }); } } @@ -133,10 +131,10 @@ void LinearCombinationComponent::handle_new_value(float value) { float sum = 0.0; - for (const auto &sensor : this->sensor_pairs_) { - const float sensor_state = sensor.first->state; + for (const auto &source : this->sensor_sources_) { + const float sensor_state = source.sensor->state; if (std::isfinite(sensor_state)) { - sum += sensor_state * sensor.second(sensor_state); + sum += sensor_state * source.compute(sensor_state); } } diff --git a/esphome/components/combination/combination.h b/esphome/components/combination/combination.h index fb5e156da9..f99eeb5872 100644 --- a/esphome/components/combination/combination.h +++ b/esphome/components/combination/combination.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" +#include #include namespace esphome { @@ -41,14 +42,20 @@ class CombinationNoParameterComponent : public CombinationComponent { // Base class for opertions that require one parameter to compute the combination class CombinationOneParameterComponent : public CombinationComponent { public: - void add_source(Sensor *sensor, std::function const &stddev); - void add_source(Sensor *sensor, float stddev); + void add_source(Sensor *sensor, std::function const &compute); + void add_source(Sensor *sensor, float value); - /// @brief Logs all source sensor's names in sensor_pairs_ + /// @brief Logs all source sensor's names in sensor_sources_ void log_source_sensors() override; + struct SensorSource { + sensor::Sensor *sensor; + std::function compute; + CombinationOneParameterComponent *parent; + }; + protected: - std::vector>> sensor_pairs_; + std::vector sensor_sources_; }; class KalmanCombinationComponent : public CombinationOneParameterComponent { From 04552e240eb335579883ff18b12979a8452718c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Mar 2026 21:19:22 -1000 Subject: [PATCH 2/4] [combination] Use FixedVector for sensor sources to eliminate realloc machinery The source count is known at config time, so pre-allocate with FixedVector::init() instead of using std::vector which pulls in _M_realloc_insert template instantiation code. --- esphome/components/combination/combination.h | 5 +++-- esphome/components/combination/sensor.py | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/combination/combination.h b/esphome/components/combination/combination.h index f99eeb5872..6e3f7c5ece 100644 --- a/esphome/components/combination/combination.h +++ b/esphome/components/combination/combination.h @@ -1,10 +1,10 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/components/sensor/sensor.h" #include -#include namespace esphome { namespace combination { @@ -42,6 +42,7 @@ class CombinationNoParameterComponent : public CombinationComponent { // Base class for opertions that require one parameter to compute the combination class CombinationOneParameterComponent : public CombinationComponent { public: + void set_source_count(size_t count) { this->sensor_sources_.init(count); } void add_source(Sensor *sensor, std::function const &compute); void add_source(Sensor *sensor, float value); @@ -55,7 +56,7 @@ class CombinationOneParameterComponent : public CombinationComponent { }; protected: - std::vector sensor_sources_; + FixedVector sensor_sources_; }; class KalmanCombinationComponent : public CombinationOneParameterComponent { diff --git a/esphome/components/combination/sensor.py b/esphome/components/combination/sensor.py index 0204162e8d..327cedee1e 100644 --- a/esphome/components/combination/sensor.py +++ b/esphome/components/combination/sensor.py @@ -180,6 +180,9 @@ async def to_code(config): if proces_std_dev := config.get(CONF_PROCESS_STD_DEV): cg.add(var.set_process_std_dev(proces_std_dev)) + if config[CONF_TYPE] in (CONF_KALMAN, CONF_LINEAR): + cg.add(var.set_source_count(len(config[CONF_SOURCES]))) + for source_conf in config[CONF_SOURCES]: source = await cg.get_variable(source_conf[CONF_SOURCE]) if config[CONF_TYPE] == CONF_KALMAN: From ded490dc9fe9b148d69f5613544fe2034b47fddc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Mar 2026 21:24:09 -1000 Subject: [PATCH 3/4] [combination] Move SensorSource to protected and fix grammar nit --- esphome/components/combination/combination.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/combination/combination.h b/esphome/components/combination/combination.h index 6e3f7c5ece..463eedc564 100644 --- a/esphome/components/combination/combination.h +++ b/esphome/components/combination/combination.h @@ -46,16 +46,16 @@ class CombinationOneParameterComponent : public CombinationComponent { void add_source(Sensor *sensor, std::function const &compute); void add_source(Sensor *sensor, float value); - /// @brief Logs all source sensor's names in sensor_sources_ + /// @brief Logs all source sensors' names in sensor_sources_ void log_source_sensors() override; + protected: struct SensorSource { sensor::Sensor *sensor; std::function compute; CombinationOneParameterComponent *parent; }; - protected: FixedVector sensor_sources_; }; From 084790809f970fce31b61a2a3b4f1a711557d357 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Mar 2026 21:26:20 -1000 Subject: [PATCH 4/4] [combination] Add safety comment for [&source] capture lifetime --- esphome/components/combination/combination.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp index 27d6af1346..b858eee4ee 100644 --- a/esphome/components/combination/combination.cpp +++ b/esphome/components/combination/combination.cpp @@ -60,6 +60,8 @@ void KalmanCombinationComponent::dump_config() { void KalmanCombinationComponent::setup() { for (auto &source : this->sensor_sources_) { + // [&source] is safe: source refers to a FixedVector element that never reallocates, + // so the reference remains valid for the component's lifetime. source.sensor->add_on_state_callback([&source](float x) -> void { static_cast(source.parent)->correct_(x, source.compute(x)); });