[sensor] Add SensorWithDedup for child sensors that skip repeated values

This commit is contained in:
J. Nick Koston
2026-10-16 10:36:23 -10:00
parent 10d3ced897
commit a43574f6ac
2 changed files with 115 additions and 0 deletions
@@ -0,0 +1,52 @@
#pragma once
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/helpers.h"
#include <cmath>
namespace esphome::sensor {
/// Declare an optional child sensor that skips publishing a value equal to the last one.
#define SUB_SENSOR_WITH_DEDUP(name, dedup_type) \
protected: \
sensor::SensorWithDedup<dedup_type> name##_sensor_{}; \
\
public: \
void set_##name##_sensor(sensor::Sensor *sensor) { this->name##_sensor_.set_sensor(sensor); }
#define LOG_SENSOR_WITH_DEDUP_SAFE(tag, name, sensor) \
if ((sensor).has_sensor()) { \
LOG_SENSOR(tag, name, (sensor).get_sensor()); \
}
/// A sensor pointer that may be null plus the last value it published, stored inline.
/// Publishing does nothing when no sensor is set, and skips a value equal to the last one.
template<typename T> class SensorWithDedup {
public:
void set_sensor(Sensor *sens) {
this->sens_ = sens;
this->dedup_ = {};
}
void publish_state_if_not_dup(T state) {
if (this->sens_ != nullptr && this->dedup_.next(state)) {
this->sens_->publish_state(static_cast<float>(state));
}
}
void publish_state_unknown() {
if (this->sens_ != nullptr && this->dedup_.next_unknown()) {
this->sens_->publish_state(NAN);
}
}
bool has_sensor() const { return this->sens_ != nullptr; }
Sensor *get_sensor() const { return this->sens_; }
protected:
Sensor *sens_{nullptr};
Deduplicator<T> dedup_;
};
} // namespace esphome::sensor
@@ -0,0 +1,63 @@
#include <gtest/gtest.h>
#include <cmath>
#include "esphome/components/sensor/sensor_with_dedup.h"
namespace esphome::sensor::testing {
class SensorWithDedupTest : public ::testing::Test {
protected:
void SetUp() override {
this->sensor_.add_on_state_callback([this](float value) { this->published_.push_back(value); });
}
Sensor sensor_;
std::vector<float> published_;
};
TEST_F(SensorWithDedupTest, NoSensorDoesNothing) {
SensorWithDedup<uint8_t> dedup;
EXPECT_FALSE(dedup.has_sensor());
EXPECT_EQ(dedup.get_sensor(), nullptr);
dedup.publish_state_if_not_dup(5);
dedup.publish_state_unknown();
EXPECT_TRUE(this->published_.empty());
}
TEST_F(SensorWithDedupTest, SkipsRepeatedValues) {
SensorWithDedup<int16_t> dedup;
dedup.set_sensor(&this->sensor_);
EXPECT_TRUE(dedup.has_sensor());
EXPECT_EQ(dedup.get_sensor(), &this->sensor_);
dedup.publish_state_if_not_dup(-3);
dedup.publish_state_if_not_dup(-3);
dedup.publish_state_if_not_dup(7);
dedup.publish_state_if_not_dup(7);
ASSERT_EQ(this->published_.size(), 2u);
EXPECT_EQ(this->published_[0], -3.0f);
EXPECT_EQ(this->published_[1], 7.0f);
}
TEST_F(SensorWithDedupTest, UnknownPublishedOnceThenValueAgain) {
SensorWithDedup<float> dedup;
dedup.set_sensor(&this->sensor_);
dedup.publish_state_if_not_dup(1.5f);
dedup.publish_state_unknown();
dedup.publish_state_unknown();
dedup.publish_state_if_not_dup(1.5f);
ASSERT_EQ(this->published_.size(), 3u);
EXPECT_EQ(this->published_[0], 1.5f);
EXPECT_TRUE(std::isnan(this->published_[1]));
EXPECT_EQ(this->published_[2], 1.5f);
}
TEST_F(SensorWithDedupTest, SetSensorResetsHistory) {
SensorWithDedup<uint16_t> dedup;
dedup.set_sensor(&this->sensor_);
dedup.publish_state_if_not_dup(100);
dedup.set_sensor(&this->sensor_);
dedup.publish_state_if_not_dup(100);
EXPECT_EQ(this->published_.size(), 2u);
}
} // namespace esphome::sensor::testing