mirror of
https://github.com/esphome/esphome.git
synced 2026-10-13 01:32:49 +00:00
[exponential_moving_average] Add EMA preserved across reboots (#19882)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
co-authored by
J. Nick Koston
parent
6f2d2a2001
commit
d9b3f299e0
+40
@@ -0,0 +1,40 @@
|
||||
esphome:
|
||||
name: test
|
||||
on_boot:
|
||||
then:
|
||||
- sensor.exponential_moving_average.reset: ema_default
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: source_sensor
|
||||
unit_of_measurement: "°C"
|
||||
accuracy_decimals: 1
|
||||
device_class: temperature
|
||||
state_class: measurement
|
||||
lambda: return 1.0;
|
||||
- platform: exponential_moving_average
|
||||
id: ema_default
|
||||
name: EMA Default
|
||||
sensor: source_sensor
|
||||
- platform: exponential_moving_average
|
||||
id: ema_alpha
|
||||
name: EMA Alpha
|
||||
sensor: source_sensor
|
||||
alpha: 0.25
|
||||
unit_of_measurement: "K"
|
||||
accuracy_decimals: 3
|
||||
- platform: exponential_moving_average
|
||||
id: ema_time_constant
|
||||
name: EMA Time Constant
|
||||
sensor: source_sensor
|
||||
time_constant: 5min
|
||||
restore: false
|
||||
- platform: exponential_moving_average
|
||||
id: ema_linear
|
||||
name: EMA Linear
|
||||
sensor: source_sensor
|
||||
time_constant: 30s
|
||||
time_weighting: linear
|
||||
@@ -0,0 +1,166 @@
|
||||
"""Tests for the exponential_moving_average sensor."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.exponential_moving_average.sensor import CONFIG_SCHEMA
|
||||
|
||||
|
||||
def test_default_alpha_and_restore(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Without alpha or time_constant, alpha defaults to 0.1 and restore is on."""
|
||||
main_cpp = generate_main(
|
||||
component_config_path("exponential_moving_average_test.yaml")
|
||||
)
|
||||
|
||||
assert (
|
||||
"new(ema_default) exponential_moving_average::ExponentialMovingAverageSensor(source_sensor);"
|
||||
in main_cpp
|
||||
)
|
||||
assert "ema_default->set_alpha(0.1f);" in main_cpp
|
||||
assert "ema_default->set_restore(true);" in main_cpp
|
||||
|
||||
|
||||
def test_alpha(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(
|
||||
component_config_path("exponential_moving_average_test.yaml")
|
||||
)
|
||||
|
||||
assert "ema_alpha->set_alpha(0.25f);" in main_cpp
|
||||
assert "ema_alpha->set_time_constant" not in main_cpp
|
||||
|
||||
|
||||
def test_time_constant_replaces_alpha(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(
|
||||
component_config_path("exponential_moving_average_test.yaml")
|
||||
)
|
||||
|
||||
assert "ema_time_constant->set_time_constant(300000);" in main_cpp
|
||||
assert "ema_time_constant->set_alpha" not in main_cpp
|
||||
assert "ema_time_constant->set_restore(false);" in main_cpp
|
||||
assert "ema_time_constant->set_time_weighting" not in main_cpp
|
||||
|
||||
|
||||
def test_time_weighting(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(
|
||||
component_config_path("exponential_moving_average_test.yaml")
|
||||
)
|
||||
|
||||
assert "ema_linear->set_time_constant(30000);" in main_cpp
|
||||
assert (
|
||||
"ema_linear->set_time_weighting(exponential_moving_average::TIME_WEIGHTING_LINEAR);"
|
||||
in main_cpp
|
||||
)
|
||||
|
||||
|
||||
def test_properties_inherited_from_source(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Unset properties come from the source sensor, with one extra decimal; set ones are kept."""
|
||||
main_cpp = generate_main(
|
||||
component_config_path("exponential_moving_average_test.yaml")
|
||||
)
|
||||
|
||||
assert "ema_default->set_accuracy_decimals(2);" in main_cpp
|
||||
assert "ema_alpha->set_accuracy_decimals(3);" in main_cpp
|
||||
default_line = next(
|
||||
line for line in main_cpp.splitlines() if '"EMA Default"' in line
|
||||
)
|
||||
alpha_line = next(line for line in main_cpp.splitlines() if '"EMA Alpha"' in line)
|
||||
assert "°C" in default_line
|
||||
assert "temperature" in default_line
|
||||
assert "K" in alpha_line
|
||||
|
||||
|
||||
def test_reset_action(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(
|
||||
component_config_path("exponential_moving_average_test.yaml")
|
||||
)
|
||||
|
||||
assert "::ema_default->reset();" in main_cpp
|
||||
|
||||
|
||||
def test_alpha_and_time_constant_are_exclusive() -> None:
|
||||
with pytest.raises(cv.Invalid, match="Cannot specify more than one of"):
|
||||
CONFIG_SCHEMA(
|
||||
{
|
||||
"id": "ema",
|
||||
"name": "EMA",
|
||||
"sensor": "source",
|
||||
"alpha": 0.5,
|
||||
"time_constant": "1min",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("alpha", [0, -0.1, 1.5])
|
||||
def test_alpha_out_of_range(alpha: float) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA({"id": "ema", "name": "EMA", "sensor": "source", "alpha": alpha})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("alpha", [0.01, 1])
|
||||
def test_alpha_in_range(alpha: float) -> None:
|
||||
config = CONFIG_SCHEMA(
|
||||
{"id": "ema", "name": "EMA", "sensor": "source", "alpha": alpha}
|
||||
)
|
||||
|
||||
assert config["alpha"] == alpha
|
||||
|
||||
|
||||
def test_time_weighting_requires_time_constant() -> None:
|
||||
with pytest.raises(cv.Invalid, match="can only be used with 'time_constant'"):
|
||||
CONFIG_SCHEMA(
|
||||
{
|
||||
"id": "ema",
|
||||
"name": "EMA",
|
||||
"sensor": "source",
|
||||
"time_weighting": "previous",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("weighting", ["new", "previous", "linear", "LINEAR"])
|
||||
def test_time_weighting_values(weighting: str) -> None:
|
||||
config = CONFIG_SCHEMA(
|
||||
{
|
||||
"id": "ema",
|
||||
"name": "EMA",
|
||||
"sensor": "source",
|
||||
"time_constant": "1min",
|
||||
"time_weighting": weighting,
|
||||
}
|
||||
)
|
||||
|
||||
assert config["time_weighting"] == weighting.lower()
|
||||
|
||||
|
||||
def test_time_weighting_rejects_unknown_value() -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA(
|
||||
{
|
||||
"id": "ema",
|
||||
"name": "EMA",
|
||||
"sensor": "source",
|
||||
"time_constant": "1min",
|
||||
"time_weighting": "trapezoid",
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "esphome/components/exponential_moving_average/exponential_moving_average_sensor.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#ifdef USE_HOST
|
||||
#include "esphome/components/host/preferences.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::exponential_moving_average::testing {
|
||||
|
||||
class TestableExponentialMovingAverageSensor : public ExponentialMovingAverageSensor {
|
||||
public:
|
||||
using ExponentialMovingAverageSensor::ExponentialMovingAverageSensor;
|
||||
using ExponentialMovingAverageSensor::process_;
|
||||
};
|
||||
|
||||
// Unnamed sensors share one preference key, so a second instance created after
|
||||
// the first one behaves like the same sensor after a reboot.
|
||||
class ExponentialMovingAverageTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
if (const char *prefdir = getenv("ESPHOME_PREFDIR"); prefdir != nullptr)
|
||||
this->saved_prefdir_ = prefdir;
|
||||
// Keep preferences away from the user's home directory.
|
||||
setenv("ESPHOME_PREFDIR", std::filesystem::temp_directory_path().c_str(), 1);
|
||||
#ifdef USE_HOST
|
||||
host::setup_preferences();
|
||||
#endif
|
||||
global_preferences->reset();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
global_preferences->reset();
|
||||
if (this->saved_prefdir_.has_value()) {
|
||||
setenv("ESPHOME_PREFDIR", this->saved_prefdir_->c_str(), 1);
|
||||
} else {
|
||||
unsetenv("ESPHOME_PREFDIR");
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::string> saved_prefdir_;
|
||||
sensor::Sensor source_;
|
||||
};
|
||||
|
||||
} // namespace esphome::exponential_moving_average::testing
|
||||
@@ -0,0 +1,25 @@
|
||||
sensor:
|
||||
- platform: template
|
||||
id: ema_source
|
||||
name: EMA Source
|
||||
unit_of_measurement: "°C"
|
||||
accuracy_decimals: 1
|
||||
lambda: return 21.5;
|
||||
update_interval: 10s
|
||||
- platform: exponential_moving_average
|
||||
id: ema_alpha
|
||||
name: EMA Alpha
|
||||
sensor: ema_source
|
||||
alpha: 0.2
|
||||
- platform: exponential_moving_average
|
||||
name: EMA Time Constant
|
||||
sensor: ema_source
|
||||
time_constant: 5min
|
||||
time_weighting: previous
|
||||
restore: false
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: EMA Reset
|
||||
on_press:
|
||||
- sensor.exponential_moving_average.reset: ema_alpha
|
||||
+356
@@ -0,0 +1,356 @@
|
||||
#include <cmath>
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
namespace esphome::exponential_moving_average::testing {
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, FirstValueStartsTheAverage) {
|
||||
TestableExponentialMovingAverageSensor ema(&this->source_);
|
||||
ema.setup();
|
||||
EXPECT_FALSE(ema.has_state());
|
||||
|
||||
ema.process_(10.0f, 0);
|
||||
EXPECT_FLOAT_EQ(ema.state, 10.0f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, AlphaWeightsEachValue) {
|
||||
TestableExponentialMovingAverageSensor ema(&this->source_);
|
||||
ema.set_alpha(0.5f);
|
||||
ema.setup();
|
||||
|
||||
ema.process_(10.0f, 0);
|
||||
ema.process_(20.0f, 0);
|
||||
EXPECT_FLOAT_EQ(ema.state, 15.0f);
|
||||
ema.process_(20.0f, 0);
|
||||
EXPECT_FLOAT_EQ(ema.state, 17.5f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, NanValuesAreIgnored) {
|
||||
TestableExponentialMovingAverageSensor ema(&this->source_);
|
||||
ema.set_alpha(0.5f);
|
||||
ema.setup();
|
||||
|
||||
ema.process_(10.0f, 0);
|
||||
ema.process_(NAN, 0);
|
||||
EXPECT_FLOAT_EQ(ema.state, 10.0f);
|
||||
ema.process_(20.0f, 0);
|
||||
EXPECT_FLOAT_EQ(ema.state, 15.0f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, FollowsSourceSensor) {
|
||||
TestableExponentialMovingAverageSensor ema(&this->source_);
|
||||
ema.set_alpha(0.25f);
|
||||
ema.setup();
|
||||
|
||||
this->source_.publish_state(8.0f);
|
||||
this->source_.publish_state(0.0f);
|
||||
EXPECT_FLOAT_EQ(ema.state, 6.0f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, TimeConstantWeightsByElapsedTime) {
|
||||
TestableExponentialMovingAverageSensor ema(&this->source_);
|
||||
ema.set_time_constant(1000);
|
||||
ema.setup();
|
||||
|
||||
ema.process_(0.0f, 0);
|
||||
ema.process_(1.0f, 1000);
|
||||
EXPECT_NEAR(ema.state, 1.0f - std::exp(-1.0f), 1e-5f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, TimeConstantResultDoesNotDependOnSampleRate) {
|
||||
TestableExponentialMovingAverageSensor fast(&this->source_);
|
||||
fast.set_time_constant(1000);
|
||||
fast.set_restore(false);
|
||||
fast.setup();
|
||||
fast.process_(0.0f, 0);
|
||||
for (uint32_t t = 100; t <= 1000; t += 100)
|
||||
fast.process_(1.0f, t);
|
||||
|
||||
TestableExponentialMovingAverageSensor slow(&this->source_);
|
||||
slow.set_time_constant(1000);
|
||||
slow.set_restore(false);
|
||||
slow.setup();
|
||||
slow.process_(0.0f, 0);
|
||||
slow.process_(1.0f, 1000);
|
||||
|
||||
EXPECT_NEAR(fast.state, slow.state, 1e-5f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, TimeConstantIgnoresRepeatAtSameTime) {
|
||||
TestableExponentialMovingAverageSensor ema(&this->source_);
|
||||
ema.set_time_constant(1000);
|
||||
ema.setup();
|
||||
|
||||
ema.process_(5.0f, 0);
|
||||
ema.process_(100.0f, 0);
|
||||
EXPECT_FLOAT_EQ(ema.state, 5.0f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, TimeConstantHandlesTimerWraparound) {
|
||||
TestableExponentialMovingAverageSensor ema(&this->source_);
|
||||
ema.set_time_constant(1000);
|
||||
ema.setup();
|
||||
|
||||
ema.process_(0.0f, UINT32_MAX - 499);
|
||||
ema.process_(1.0f, 500);
|
||||
EXPECT_NEAR(ema.state, 1.0f - std::exp(-1.0f), 1e-5f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, ResetStartsANewAverage) {
|
||||
TestableExponentialMovingAverageSensor ema(&this->source_);
|
||||
ema.set_alpha(0.5f);
|
||||
ema.setup();
|
||||
|
||||
ema.process_(10.0f, 0);
|
||||
ema.reset();
|
||||
EXPECT_TRUE(std::isnan(ema.state));
|
||||
ema.process_(40.0f, 0);
|
||||
EXPECT_FLOAT_EQ(ema.state, 40.0f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, AverageIsRestoredAfterReboot) {
|
||||
{
|
||||
TestableExponentialMovingAverageSensor before(&this->source_);
|
||||
before.set_alpha(0.5f);
|
||||
before.setup();
|
||||
before.process_(10.0f, 0);
|
||||
before.process_(20.0f, 0);
|
||||
}
|
||||
|
||||
TestableExponentialMovingAverageSensor after(&this->source_);
|
||||
after.set_alpha(0.5f);
|
||||
after.setup();
|
||||
ASSERT_TRUE(after.has_state());
|
||||
EXPECT_FLOAT_EQ(after.state, 15.0f);
|
||||
|
||||
// Continues from the restored value rather than starting again.
|
||||
after.process_(25.0f, 0);
|
||||
EXPECT_FLOAT_EQ(after.state, 20.0f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, NothingRestoredWhenRestoreIsOff) {
|
||||
{
|
||||
TestableExponentialMovingAverageSensor before(&this->source_);
|
||||
before.setup();
|
||||
before.process_(10.0f, 0);
|
||||
}
|
||||
|
||||
TestableExponentialMovingAverageSensor after(&this->source_);
|
||||
after.set_restore(false);
|
||||
after.setup();
|
||||
EXPECT_FALSE(after.has_state());
|
||||
after.process_(30.0f, 0);
|
||||
EXPECT_FLOAT_EQ(after.state, 30.0f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, ResetClearsTheSavedAverage) {
|
||||
{
|
||||
TestableExponentialMovingAverageSensor before(&this->source_);
|
||||
before.setup();
|
||||
before.process_(10.0f, 0);
|
||||
before.reset();
|
||||
}
|
||||
|
||||
TestableExponentialMovingAverageSensor after(&this->source_);
|
||||
after.setup();
|
||||
EXPECT_FALSE(after.has_state());
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, PreviousWeightingCountsGapAtPreviousValue) {
|
||||
TestableExponentialMovingAverageSensor ema(&this->source_);
|
||||
ema.set_time_constant(1000);
|
||||
ema.set_time_weighting(TIME_WEIGHTING_PREVIOUS);
|
||||
ema.setup();
|
||||
|
||||
// The value stayed at 20 for an hour before changing to 25.
|
||||
ema.process_(20.0f, 0);
|
||||
ema.process_(25.0f, 3600000);
|
||||
EXPECT_FLOAT_EQ(ema.state, 20.0f);
|
||||
|
||||
// The 25 is counted over the following interval.
|
||||
ema.process_(25.0f, 3601000);
|
||||
EXPECT_NEAR(ema.state, 20.0f + 5.0f * (1.0f - std::exp(-1.0f)), 1e-4f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, NewWeightingCountsGapAtNewValue) {
|
||||
TestableExponentialMovingAverageSensor ema(&this->source_);
|
||||
ema.set_time_constant(1000);
|
||||
ema.setup();
|
||||
|
||||
ema.process_(20.0f, 0);
|
||||
ema.process_(25.0f, 3600000);
|
||||
EXPECT_FLOAT_EQ(ema.state, 25.0f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, LinearWeightingFollowsStraightLine) {
|
||||
TestableExponentialMovingAverageSensor ema(&this->source_);
|
||||
ema.set_time_constant(1000);
|
||||
ema.set_time_weighting(TIME_WEIGHTING_LINEAR);
|
||||
ema.setup();
|
||||
|
||||
// An average of a value rising steadily from 0 to 1 over one time constant ends at exp(-1).
|
||||
ema.process_(0.0f, 0);
|
||||
ema.process_(1.0f, 1000);
|
||||
EXPECT_NEAR(ema.state, std::exp(-1.0f), 1e-5f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, LinearWeightingMatchesManySmallSteps) {
|
||||
TestableExponentialMovingAverageSensor coarse(&this->source_);
|
||||
coarse.set_time_constant(1000);
|
||||
coarse.set_time_weighting(TIME_WEIGHTING_LINEAR);
|
||||
coarse.set_restore(false);
|
||||
coarse.setup();
|
||||
coarse.process_(0.0f, 0);
|
||||
coarse.process_(10.0f, 2000);
|
||||
|
||||
TestableExponentialMovingAverageSensor fine(&this->source_);
|
||||
fine.set_time_constant(1000);
|
||||
fine.set_time_weighting(TIME_WEIGHTING_LINEAR);
|
||||
fine.set_restore(false);
|
||||
fine.setup();
|
||||
fine.process_(0.0f, 0);
|
||||
for (uint32_t t = 10; t <= 2000; t += 10)
|
||||
fine.process_(t / 200.0f, t);
|
||||
|
||||
EXPECT_NEAR(coarse.state, fine.state, 1e-3f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, LinearWeightingIgnoresRepeatAtSameTime) {
|
||||
TestableExponentialMovingAverageSensor ema(&this->source_);
|
||||
ema.set_time_constant(1000);
|
||||
ema.set_time_weighting(TIME_WEIGHTING_LINEAR);
|
||||
ema.setup();
|
||||
|
||||
ema.process_(5.0f, 0);
|
||||
ema.process_(100.0f, 0);
|
||||
EXPECT_FLOAT_EQ(ema.state, 5.0f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, FirstValueAfterRebootUsesNewValue) {
|
||||
{
|
||||
TestableExponentialMovingAverageSensor before(&this->source_);
|
||||
before.setup();
|
||||
before.process_(10.0f, 0);
|
||||
}
|
||||
|
||||
// No reading from before the reboot is known, so the new value is used for the first interval.
|
||||
TestableExponentialMovingAverageSensor after(&this->source_);
|
||||
after.set_time_constant(1000);
|
||||
after.set_time_weighting(TIME_WEIGHTING_PREVIOUS);
|
||||
after.setup();
|
||||
after.process_(20.0f, 1000);
|
||||
EXPECT_NEAR(after.state, 10.0f + 10.0f * (1.0f - std::exp(-1.0f)), 1e-4f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, StartsFromSourceThatAlreadyHasAValue) {
|
||||
sensor::Sensor source;
|
||||
source.publish_state(12.0f);
|
||||
|
||||
TestableExponentialMovingAverageSensor ema(&source);
|
||||
ema.set_alpha(0.5f);
|
||||
ema.set_restore(false);
|
||||
ema.setup();
|
||||
ASSERT_TRUE(ema.has_state());
|
||||
EXPECT_FLOAT_EQ(ema.state, 12.0f);
|
||||
|
||||
// The value read at setup is only counted once.
|
||||
source.publish_state(20.0f);
|
||||
EXPECT_FLOAT_EQ(ema.state, 16.0f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, SourceValueAtSetupBlendsWithRestoredAverage) {
|
||||
{
|
||||
TestableExponentialMovingAverageSensor before(&this->source_);
|
||||
before.setup();
|
||||
before.process_(10.0f, 0);
|
||||
}
|
||||
|
||||
sensor::Sensor source;
|
||||
source.publish_state(20.0f);
|
||||
TestableExponentialMovingAverageSensor after(&source);
|
||||
after.set_alpha(0.5f);
|
||||
after.setup();
|
||||
EXPECT_FLOAT_EQ(after.state, 15.0f);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, SourceNanAtSetupIsIgnored) {
|
||||
sensor::Sensor source;
|
||||
source.publish_state(NAN);
|
||||
|
||||
TestableExponentialMovingAverageSensor ema(&source);
|
||||
ema.set_restore(false);
|
||||
ema.setup();
|
||||
EXPECT_FALSE(ema.has_state());
|
||||
}
|
||||
|
||||
// Reference weights from the Taylor series, accurate for the small ratios used below.
|
||||
static double series_gain(double x) { return x - x * x / 2 + x * x * x / 6; }
|
||||
static double series_weight_new(double x) { return x / 2 - x * x / 6 + x * x * x / 24; }
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, LinearWeightingAccurateWithLongTimeConstant) {
|
||||
constexpr uint32_t time_constant = 43200000; // 12 hours
|
||||
TestableExponentialMovingAverageSensor ema(&this->source_);
|
||||
ema.set_time_constant(time_constant);
|
||||
ema.set_time_weighting(TIME_WEIGHTING_LINEAR);
|
||||
ema.set_restore(false);
|
||||
ema.setup();
|
||||
|
||||
const double x = 1000.0 / time_constant;
|
||||
const double weight_new = series_weight_new(x);
|
||||
const double weight_previous = series_gain(x) - weight_new;
|
||||
|
||||
ema.process_(0.0f, 0);
|
||||
ema.process_(10.0f, 1000);
|
||||
const double expected = weight_new * 10.0;
|
||||
EXPECT_NEAR(ema.state, expected, expected * 1e-4);
|
||||
|
||||
ema.process_(20.0f, 2000);
|
||||
const double expected2 = expected + weight_previous * (10.0 - expected) + weight_new * (20.0 - expected);
|
||||
EXPECT_NEAR(ema.state, expected2, expected2 * 1e-4);
|
||||
}
|
||||
|
||||
TEST_F(ExponentialMovingAverageTest, VeryShortIntervalStillMovesAverage) {
|
||||
constexpr uint32_t time_constant = 4 * 24 * 3600000; // 4 days, with a reading on every 16 ms loop
|
||||
TestableExponentialMovingAverageSensor ema(&this->source_);
|
||||
ema.set_time_constant(time_constant);
|
||||
ema.set_restore(false);
|
||||
ema.setup();
|
||||
|
||||
ema.process_(0.0f, 0);
|
||||
ema.process_(1000.0f, 16);
|
||||
const double expected = series_gain(16.0 / time_constant) * 1000.0;
|
||||
EXPECT_NEAR(ema.state, expected, expected * 1e-4);
|
||||
}
|
||||
|
||||
TEST(TimeWeightingTest, Names) {
|
||||
EXPECT_STREQ(LOG_STR_ARG(time_weighting_to_string(TIME_WEIGHTING_NEW)), "new");
|
||||
EXPECT_STREQ(LOG_STR_ARG(time_weighting_to_string(TIME_WEIGHTING_PREVIOUS)), "previous");
|
||||
EXPECT_STREQ(LOG_STR_ARG(time_weighting_to_string(TIME_WEIGHTING_LINEAR)), "linear");
|
||||
}
|
||||
|
||||
struct ScaleDurationCase {
|
||||
uint32_t ms;
|
||||
float value;
|
||||
const char *unit;
|
||||
uint8_t decimals;
|
||||
};
|
||||
|
||||
class ScaleDurationTest : public ::testing::TestWithParam<ScaleDurationCase> {};
|
||||
|
||||
TEST_P(ScaleDurationTest, PicksLargestUnitOfAtLeastOne) {
|
||||
const ScaleDurationCase &c = GetParam();
|
||||
const ScaledDuration scaled = scale_duration(c.ms);
|
||||
EXPECT_FLOAT_EQ(scaled.value, c.value);
|
||||
EXPECT_STREQ(LOG_STR_ARG(scaled.unit), c.unit);
|
||||
EXPECT_EQ(scaled.decimals, c.decimals);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
Units, ScaleDurationTest,
|
||||
::testing::Values(ScaleDurationCase{1, 1.0f, "ms", 0}, ScaleDurationCase{999, 999.0f, "ms", 0},
|
||||
ScaleDurationCase{1000, 1.0f, "s", 1}, ScaleDurationCase{95000, 1.5833334f, "min", 1},
|
||||
ScaleDurationCase{59999, 59.999f, "s", 1}, ScaleDurationCase{60000, 1.0f, "min", 1},
|
||||
ScaleDurationCase{300000, 5.0f, "min", 1}, ScaleDurationCase{3599999, 59.999983f, "min", 1},
|
||||
ScaleDurationCase{3600000, 1.0f, "h", 1}, ScaleDurationCase{86400000, 24.0f, "h", 1}));
|
||||
|
||||
} // namespace esphome::exponential_moving_average::testing
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
exponential_moving_average: !include common.yaml
|
||||
Reference in New Issue
Block a user