ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
combination.cpp
Go to the documentation of this file.
1#include "combination.h"
2
3#include "esphome/core/log.h"
4#include "esphome/core/hal.h"
5
6#include <cmath>
7
9
10ESPHOME_LOG_TAG(TAG, "combination");
11
12static constexpr uint32_t NEW_VALUE_DEFER_ID = 0;
13
14void CombinationComponent::log_config_(const LogString *combo_type) {
15 LOG_SENSOR("", "Combination Sensor:", this);
16 ESP_LOGCONFIG(TAG, " Combination Type: %s", LOG_STR_ARG(combo_type));
17 this->log_source_sensors();
18}
19
20void CombinationNoParameterComponent::add_source(Sensor *sensor) { this->sensors_.emplace_back(sensor); }
21
22void CombinationOneParameterComponent::add_source(Sensor *sensor, std::function<float(float)> const &compute) {
23 this->sensor_sources_.push_back({sensor, compute, this});
24}
25
26void CombinationOneParameterComponent::add_source(Sensor *sensor, float value) {
27 this->add_source(sensor, std::function<float(float)>{[value](float x) -> float { return value; }});
28}
29
31 ESP_LOGCONFIG(TAG, " Source Sensors:");
32 for (const auto &sensor : this->sensors_) {
33 ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(sensor->get_log_name()));
34 }
35}
36
38 ESP_LOGCONFIG(TAG, " Source Sensors:");
39 for (const auto &source : this->sensor_sources_) {
40 ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(source.sensor->get_log_name()));
41 }
42}
43
45 for (const auto &sensor : this->sensors_) {
46 // All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result
47 // repeatedly in the same loop if multiple source senors update.
48 sensor->add_on_state_callback([this](float value) -> void {
49 this->defer(NEW_VALUE_DEFER_ID, [this, value]() { this->handle_new_value(value); });
50 });
51 }
52}
53
55 this->log_config_(LOG_STR("kalman"));
56 ESP_LOGCONFIG(TAG, " Update variance: %f per ms", this->update_variance_value_);
57
58 if (this->std_dev_sensor_ != nullptr) {
59 LOG_SENSOR(" ", "Standard Deviation Sensor:", this->std_dev_sensor_);
60 }
61}
62
64 for (auto &source : this->sensor_sources_) {
65 // [&source] is safe: source refers to a FixedVector element that never reallocates,
66 // so the reference remains valid for the component's lifetime.
67 source.sensor->add_on_state_callback([&source](float x) -> void {
68 static_cast<KalmanCombinationComponent *>(source.parent)->correct_(x, source.compute(x));
69 });
70 }
71}
72
74 uint32_t now = millis();
75
76 // Variance increases by update_variance_ each millisecond
77 auto dt = now - this->last_update_;
78 auto dv = this->update_variance_value_ * dt;
79 this->variance_ += dv;
80 this->last_update_ = now;
81}
82
83void KalmanCombinationComponent::correct_(float value, float stddev) {
84 if (std::isnan(value) || std::isinf(stddev)) {
85 return;
86 }
87
88 if (std::isnan(this->state_) || std::isinf(this->variance_)) {
89 this->state_ = value;
90 this->variance_ = stddev * stddev;
91 if (this->std_dev_sensor_ != nullptr) {
92 this->std_dev_sensor_->publish_state(stddev);
93 }
94 return;
95 }
96
97 this->update_variance_();
98
99 // Combine two gaussian distributions mu1+-var1, mu2+-var2 to a new one around mu
100 // Use the value with the smaller variance as mu1 to prevent precision errors
101 const bool this_first = this->variance_ < (stddev * stddev);
102 const float mu1 = this_first ? this->state_ : value;
103 const float mu2 = this_first ? value : this->state_;
104
105 const float var1 = this_first ? this->variance_ : stddev * stddev;
106 const float var2 = this_first ? stddev * stddev : this->variance_;
107
108 const float mu = mu1 + var1 * (mu2 - mu1) / (var1 + var2);
109 const float var = var1 - (var1 * var1) / (var1 + var2);
110
111 // Update and publish state
112 this->state_ = mu;
113 this->variance_ = var;
114
115 this->publish_state(mu);
116 if (this->std_dev_sensor_ != nullptr) {
117 this->std_dev_sensor_->publish_state(std::sqrt(var));
118 }
119}
120
122 for (auto &source : this->sensor_sources_) {
123 // All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result
124 // repeatedly in the same loop if multiple source senors update.
125 source.sensor->add_on_state_callback([this](float value) -> void {
126 this->defer(NEW_VALUE_DEFER_ID, [this, value]() { this->handle_new_value(value); });
127 });
128 }
129}
130
132 // Multiplies each sensor state by a configured coefficient and then sums
133
134 if (!std::isfinite(value))
135 return;
136
137 float sum = 0.0;
138
139 for (const auto &source : this->sensor_sources_) {
140 const float sensor_state = source.sensor->state;
141 if (std::isfinite(sensor_state)) {
142 sum += sensor_state * source.compute(sensor_state);
143 }
144 }
145
146 this->publish_state(sum);
147};
148
150 if (!std::isfinite(value))
151 return;
152
153 float max_value = (-1) * std::numeric_limits<float>::infinity(); // note x = max(x, -infinity)
154
155 for (const auto &sensor : this->sensors_) {
156 if (std::isfinite(sensor->state)) {
157 max_value = std::max(max_value, sensor->state);
158 }
159 }
160
161 this->publish_state(max_value);
162}
163
165 if (!std::isfinite(value))
166 return;
167
168 float sum = 0.0;
169 size_t count = 0;
170
171 for (const auto &sensor : this->sensors_) {
172 if (std::isfinite(sensor->state)) {
173 ++count;
174 sum += sensor->state;
175 }
176 }
177
178 if (count == 0) {
179 this->publish_state(NAN);
180 return;
181 }
182 float mean = sum / count;
183
184 this->publish_state(mean);
185}
186
188 // Sorts sensor states in ascending order and determines the middle value
189
190 if (!std::isfinite(value))
191 return;
192
193 std::vector<float> sensor_states;
194 for (const auto &sensor : this->sensors_) {
195 if (std::isfinite(sensor->state)) {
196 sensor_states.push_back(sensor->state);
197 }
198 }
199
200 sort(sensor_states.begin(), sensor_states.end());
201 size_t sensor_states_size = sensor_states.size();
202
203 float median = NAN;
204
205 if (sensor_states_size) {
206 if (sensor_states_size % 2) {
207 // Odd number of measurements, use middle measurement
208 median = sensor_states[sensor_states_size / 2];
209 } else {
210 // Even number of measurements, use the average of the two middle measurements
211 median = (sensor_states[sensor_states_size / 2] + sensor_states[sensor_states_size / 2 - 1]) / 2.0f;
212 }
213 }
214
215 this->publish_state(median);
216}
217
219 if (!std::isfinite(value))
220 return;
221
222 float min_value = std::numeric_limits<float>::infinity(); // note x = min(x, infinity)
223
224 for (const auto &sensor : this->sensors_) {
225 if (std::isfinite(sensor->state)) {
226 min_value = std::min(min_value, sensor->state);
227 }
228 }
229
230 this->publish_state(min_value);
231}
232
234
236 // Sorts sensor states then takes difference between largest and smallest states
237
238 if (!std::isfinite(value))
239 return;
240
241 std::vector<float> sensor_states;
242 for (const auto &sensor : this->sensors_) {
243 if (std::isfinite(sensor->state)) {
244 sensor_states.push_back(sensor->state);
245 }
246 }
247
248 if (sensor_states.empty()) {
249 this->publish_state(NAN);
250 return;
251 }
252
253 sort(sensor_states.begin(), sensor_states.end());
254
255 float range = sensor_states.back() - sensor_states.front();
256 this->publish_state(range);
257}
258
260 if (!std::isfinite(value))
261 return;
262
263 float sum = 0.0;
264 for (const auto &sensor : this->sensors_) {
265 if (std::isfinite(sensor->state)) {
266 sum += sensor->state;
267 }
268 }
269
270 this->publish_state(sum);
271}
272
273} // namespace esphome::combination
void defer(const char *name, std::function< void()> &&f)
Defer a callback to the next loop() call with a const char* name.
void log_config_(const LogString *combo_type)
Logs the sensor for use in dump_config.
virtual void log_source_sensors()=0
Logs all source sensor's names.
void setup() override
Adds a callback to each source sensor.
virtual void handle_new_value(float value)=0
Computes the combination.
void log_source_sensors() override
Logs all source sensor's names in sensors_.
void log_source_sensors() override
Logs all source sensors' names in sensor_sources_.
void add_source(Sensor *sensor, std::function< float(float)> const &compute)
void correct_(float value, float stddev)
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:65
unsigned count
Definition gif.cpp:51
Range range
Definition msa3xx.h:0
ESPHOME_LOG_TAG(TAG, "combination")
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
static void uint32_t
uint16_t x
Definition tt21100.cpp:5