diff --git a/esphome/components/dfrobot_sen0395/automation.h b/esphome/components/dfrobot_sen0395/automation.h index c3632085310..58dbb9afb09 100644 --- a/esphome/components/dfrobot_sen0395/automation.h +++ b/esphome/components/dfrobot_sen0395/automation.h @@ -26,17 +26,22 @@ class DfrobotSen0395SettingsAction final : public Action, public Parented TEMPLATABLE_VALUE(float, det_min4) TEMPLATABLE_VALUE(float, det_max4) - void play(const Ts &...x) { + void play(const Ts &...x) override { this->parent_->enqueue(make_unique(0)); if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) { this->parent_->enqueue(make_unique()); } if (this->det_min1_.has_value() && this->det_max1_.has_value()) { - if (this->det_min1_.value() >= 0 && this->det_max1_.value() >= 0) { - this->parent_->enqueue(make_unique( - this->det_min1_.value_or(-1), this->det_max1_.value_or(-1), this->det_min2_.value_or(-1), - this->det_max2_.value_or(-1), this->det_min3_.value_or(-1), this->det_max3_.value_or(-1), - this->det_min4_.value_or(-1), this->det_max4_.value_or(-1))); + // Each segment is read once, so the guard and the command see the same values. + const float min1 = this->det_min1_.value(x...); + if (min1 >= 0) { + const float max1 = this->det_max1_.value(x...); + if (max1 >= 0) { + this->parent_->enqueue(make_unique( + min1, max1, this->det_min2_.value_or(x..., -1), this->det_max2_.value_or(x..., -1), + this->det_min3_.value_or(x..., -1), this->det_max3_.value_or(x..., -1), + this->det_min4_.value_or(x..., -1), this->det_max4_.value_or(x..., -1))); + } } } if (this->delay_after_detect_.has_value() && this->delay_after_disappear_.has_value()) { diff --git a/tests/components/dfrobot_sen0395/common.yaml b/tests/components/dfrobot_sen0395/common.yaml index 7f980574ed9..48226193bc2 100644 --- a/tests/components/dfrobot_sen0395/common.yaml +++ b/tests/components/dfrobot_sen0395/common.yaml @@ -17,6 +17,21 @@ esphome: dfrobot_sen0395: - id: mmwave +number: + - platform: template + id: mmwave_max_distance + name: mmwave max distance + min_value: 1 + max_value: 9 + step: 0.5 + set_action: + - dfrobot_sen0395.settings: + id: mmwave + detection_segments: + - 0cm + - !lambda "return x;" + sensitivity: 5 + binary_sensor: - platform: dfrobot_sen0395 id: mmwave_detected diff --git a/tests/components/dfrobot_sen0395/settings_action.cpp b/tests/components/dfrobot_sen0395/settings_action.cpp new file mode 100644 index 00000000000..92a75b81677 --- /dev/null +++ b/tests/components/dfrobot_sen0395/settings_action.cpp @@ -0,0 +1,145 @@ +#include +#include +#include "esphome/components/dfrobot_sen0395/automation.h" + +namespace esphome::dfrobot_sen0395::testing { +namespace { +struct CallRecord { + std::array segment_calls{}; + std::array seen_float{}; + std::array seen_int{}; + int sensitivity_calls{0}; + float sensitivity_argument{0}; +}; + +// TemplatableFn stores only a function pointer, so the test lambdas cannot capture. They record into this +// function-local static instead, which SetUp() clears before every test. +CallRecord &call_record() { + static CallRecord record{}; + return record; +} + +float segment_value(size_t index, float x, int y = 0) { + CallRecord &record = call_record(); + record.segment_calls[index]++; + record.seen_float[index] = x; + record.seen_int[index] = y; + return x + y + static_cast(index); +} +} // namespace + +// The component is final and its command queue protected, so these tests check the segment +// reads and their arguments; the enqueued DetRangeCfgCommand itself is not inspected. +class SettingsActionTest : public ::testing::Test { + protected: + void SetUp() override { call_record() = CallRecord{}; } + + DfrobotSen0395Component radar_; +}; + +TEST_F(SettingsActionTest, EmptyTriggerPreservesSegmentCallableCounts) { + DfrobotSen0395SettingsAction<> action; + action.set_parent(&this->radar_); + action.set_det_min1([]() -> float { return segment_value(0, 0); }); + action.set_det_max1([]() -> float { return segment_value(1, 0); }); + action.set_det_min2([]() -> float { return segment_value(2, 0); }); + action.set_det_max2([]() -> float { return segment_value(3, 0); }); + action.set_det_min3([]() -> float { return segment_value(4, 0); }); + action.set_det_max3([]() -> float { return segment_value(5, 0); }); + action.set_det_min4([]() -> float { return segment_value(6, 0); }); + action.set_det_max4([]() -> float { return segment_value(7, 0); }); + action.play(); + for (size_t i = 0; i < call_record().segment_calls.size(); i++) { + EXPECT_EQ(call_record().segment_calls[i], 1) << i; + } +} + +TEST_F(SettingsActionTest, EmptyTriggerWithoutSegmentsIsValid) { + DfrobotSen0395SettingsAction<> action; + action.set_parent(&this->radar_); + action.play(); + for (auto calls : call_record().segment_calls) { + EXPECT_EQ(calls, 0); + } +} + +TEST_F(SettingsActionTest, EmptyTriggerNegativeMinKeepsShortCircuit) { + DfrobotSen0395SettingsAction<> action; + action.set_parent(&this->radar_); + action.set_det_min1([]() -> float { return segment_value(0, -1); }); + action.set_det_max1([]() -> float { return segment_value(1, 0); }); + action.play(); + EXPECT_EQ(call_record().segment_calls[0], 1); + EXPECT_EQ(call_record().segment_calls[1], 0); +} + +TEST_F(SettingsActionTest, FloatTriggerForwardsAllSegmentsAndPreservesCounts) { + DfrobotSen0395SettingsAction action; + action.set_parent(&this->radar_); + action.set_det_min1([](float x) -> float { return segment_value(0, x); }); + action.set_det_max1([](float x) -> float { return segment_value(1, x); }); + action.set_det_min2([](float x) -> float { return segment_value(2, x); }); + action.set_det_max2([](float x) -> float { return segment_value(3, x); }); + action.set_det_min3([](float x) -> float { return segment_value(4, x); }); + action.set_det_max3([](float x) -> float { return segment_value(5, x); }); + action.set_det_min4([](float x) -> float { return segment_value(6, x); }); + action.set_det_max4([](float x) -> float { return segment_value(7, x); }); + action.play(2.5f); + for (size_t i = 0; i < call_record().segment_calls.size(); i++) { + EXPECT_EQ(call_record().segment_calls[i], 1) << i; + EXPECT_FLOAT_EQ(call_record().seen_float[i], 2.5f) << i; + } +} + +TEST_F(SettingsActionTest, MultipleTriggerArgumentsReachConfiguredSegments) { + DfrobotSen0395SettingsAction action; + action.set_parent(&this->radar_); + action.set_det_min1([](float x, int y) -> float { return segment_value(0, x, y); }); + action.set_det_max1([](float x, int y) -> float { return segment_value(1, x, y); }); + action.play(1.5f, 2); + for (size_t i = 0; i < 2; i++) { + EXPECT_EQ(call_record().segment_calls[i], 1) << i; + EXPECT_FLOAT_EQ(call_record().seen_float[i], 1.5f) << i; + EXPECT_EQ(call_record().seen_int[i], 2) << i; + } + for (size_t i = 2; i < call_record().segment_calls.size(); i++) { + EXPECT_EQ(call_record().segment_calls[i], 0) << i; + } +} + +TEST_F(SettingsActionTest, FloatTriggerWithoutSegmentsStillEvaluatesSensitivity) { + DfrobotSen0395SettingsAction action; + action.set_parent(&this->radar_); + action.set_sensitivity([](float x) -> int8_t { + call_record().sensitivity_calls++; + call_record().sensitivity_argument = x; + return 5; + }); + action.play(3.25f); + EXPECT_EQ(call_record().sensitivity_calls, 1); + EXPECT_FLOAT_EQ(call_record().sensitivity_argument, 3.25f); + for (auto calls : call_record().segment_calls) { + EXPECT_EQ(calls, 0); + } +} + +TEST_F(SettingsActionTest, FloatTriggerNegativeMinKeepsShortCircuit) { + DfrobotSen0395SettingsAction action; + action.set_parent(&this->radar_); + action.set_det_min1([](float x) -> float { return segment_value(0, x); }); + action.set_det_max1([](float x) -> float { return segment_value(1, x); }); + action.play(-1.0f); + EXPECT_EQ(call_record().segment_calls[0], 1); + EXPECT_EQ(call_record().segment_calls[1], 0); + EXPECT_FLOAT_EQ(call_record().seen_float[0], -1.0f); +} + +TEST_F(SettingsActionTest, FloatTriggerUnpairedSegmentIsNotEvaluated) { + DfrobotSen0395SettingsAction action; + action.set_parent(&this->radar_); + action.set_det_min1([](float x) -> float { return segment_value(0, x); }); + action.play(4.0f); + EXPECT_EQ(call_record().segment_calls[0], 0); +} + +} // namespace esphome::dfrobot_sen0395::testing