diff --git a/esphome/components/sensor/sensor_with_dedup.h b/esphome/components/sensor/sensor_with_dedup.h new file mode 100644 index 00000000000..8bd94bb93af --- /dev/null +++ b/esphome/components/sensor/sensor_with_dedup.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/helpers.h" + +#include + +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 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 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(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 dedup_; +}; + +} // namespace esphome::sensor diff --git a/tests/components/sensor/sensor_with_dedup_test.cpp b/tests/components/sensor/sensor_with_dedup_test.cpp new file mode 100644 index 00000000000..1bff52193b0 --- /dev/null +++ b/tests/components/sensor/sensor_with_dedup_test.cpp @@ -0,0 +1,63 @@ +#include +#include + +#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 published_; +}; + +TEST_F(SensorWithDedupTest, NoSensorDoesNothing) { + SensorWithDedup 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 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 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 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