diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index b5f3292fbc2..849190ff3a2 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -917,6 +917,17 @@ void LD2420Component::init_gate_config_numbers() { } } +void LD2420Component::set_gate_threshold_value(number::Number *n, float value) { + for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { + const bool move = this->gate_move_threshold_numbers_[gate] == n; + if (!move && this->gate_still_threshold_numbers_[gate] != n) + continue; + const uint8_t target = this->is_gate_select() ? this->get_gate_select_value() : gate; + (move ? this->new_config.move_thresh : this->new_config.still_thresh)[target] = value; + return; + } +} + void LD2420Component::refresh_gate_config_numbers() { this->gate_timeout_number_->publish_state(this->new_config.timeout); this->min_gate_distance_number_->publish_state(this->new_config.min_gate); diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index e13d0271e10..14c83497997 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -89,6 +89,8 @@ class LD2420Component final : public Component, public uart::UARTDevice { void publish_gate_still_threshold(uint8_t gate) { this->gate_still_threshold_numbers_[0]->publish_state(this->new_config.still_thresh[gate]); }; + // Stores a threshold number's value for its gate, or for the selected gate with gate_select + void set_gate_threshold_value(number::Number *n, float value); void init_gate_config_numbers(); void refresh_gate_config_numbers(); #endif diff --git a/esphome/components/ld2420/number/__init__.py b/esphome/components/ld2420/number/__init__.py index 200f4e202fb..07abfdbf440 100644 --- a/esphome/components/ld2420/number/__init__.py +++ b/esphome/components/ld2420/number/__init__.py @@ -1,8 +1,9 @@ +from functools import partial + import esphome.codegen as cg from esphome.components import number import esphome.config_validation as cv from esphome.const import ( - CONF_ID, CONF_MOVE_THRESHOLD, CONF_STILL_THRESHOLD, DEVICE_CLASS_DISTANCE, @@ -16,22 +17,9 @@ from esphome.types import ConfigType from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns -LD2420TimeoutNumber = ld2420_ns.class_("LD2420TimeoutNumber", number.Number) -LD2420MoveSensFactorNumber = ld2420_ns.class_( - "LD2420MoveSensFactorNumber", number.Number -) -LD2420StillSensFactorNumber = ld2420_ns.class_( - "LD2420StillSensFactorNumber", number.Number -) LD2420MinDistanceNumber = ld2420_ns.class_("LD2420MinDistanceNumber", number.Number) LD2420MaxDistanceNumber = ld2420_ns.class_("LD2420MaxDistanceNumber", number.Number) LD2420GateSelectNumber = ld2420_ns.class_("LD2420GateSelectNumber", number.Number) -LD2420MoveThresholdNumbers = ld2420_ns.class_( - "LD2420MoveThresholdNumbers", number.Number -) -LD2420StillThresholdNumbers = ld2420_ns.class_( - "LD2420StillThresholdNumbers", number.Number -) CONF_MIN_GATE_DISTANCE = "min_gate_distance" CONF_MAX_GATE_DISTANCE = "max_gate_distance" CONF_GATE_MOVE_SENSITIVITY = "gate_move_sensitivity" @@ -46,7 +34,7 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component), cv.Inclusive(CONF_PRESENCE_TIMEOUT, TIMEOUT_GROUP): number.number_schema( - LD2420TimeoutNumber, + number.ApplyNumber, unit_of_measurement=UNIT_SECOND, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_TIMELAPSE, @@ -70,23 +58,23 @@ CONFIG_SCHEMA = cv.Schema( icon=ICON_MOTION_SENSOR, ), cv.Inclusive(CONF_STILL_THRESHOLD, GATE_GROUP): number.number_schema( - LD2420StillThresholdNumbers, + number.ApplyNumber, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_MOTION_SENSOR, ), cv.Inclusive(CONF_MOVE_THRESHOLD, GATE_GROUP): number.number_schema( - LD2420MoveThresholdNumbers, + number.ApplyNumber, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_MOTION_SENSOR, ), cv.Optional(CONF_GATE_MOVE_SENSITIVITY): number.number_schema( - LD2420MoveSensFactorNumber, + number.ApplyNumber, device_class=DEVICE_CLASS_DISTANCE, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_SCALE, ), cv.Optional(CONF_GATE_STILL_SENSITIVITY): number.number_schema( - LD2420StillSensFactorNumber, + number.ApplyNumber, device_class=DEVICE_CLASS_DISTANCE, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_SCALE, @@ -98,12 +86,12 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(f"gate_{x}"): ( { cv.Required(CONF_MOVE_THRESHOLD): number.number_schema( - LD2420MoveThresholdNumbers, + number.ApplyNumber, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_MOTION_SENSOR, ), cv.Required(CONF_STILL_THRESHOLD): number.number_schema( - LD2420StillThresholdNumbers, + number.ApplyNumber, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_MOTION_SENSOR, ), @@ -123,6 +111,8 @@ async def to_code(config: ConfigType) -> None: min_value=0, max_value=255, step=5, + apply="new_config.timeout = value", + publish="before", ) await numbers( CONF_MIN_GATE_DISTANCE, @@ -144,6 +134,8 @@ async def to_code(config: ConfigType) -> None: min_value=0.05, max_value=1, step=0.025, + apply="gate_move_sensitivity_factor = value", + publish="before", ) await numbers( CONF_GATE_STILL_SENSITIVITY, @@ -151,7 +143,16 @@ async def to_code(config: ConfigType) -> None: min_value=0.05, max_value=1, step=0.025, + apply="gate_still_sensitivity_factor = value", + publish="before", ) + threshold = { + "min_value": 0, + "max_value": 65535, + "step": 25, + "apply": "set_gate_threshold_value(entity, value)", + "publish": "before", + } if config.get(CONF_GATE_SELECT): await numbers( CONF_GATE_SELECT, @@ -160,35 +161,27 @@ async def to_code(config: ConfigType) -> None: max_value=15, step=1, ) - if gate_still_threshold := config.get(CONF_STILL_THRESHOLD): - n = cg.new_Pvariable(gate_still_threshold[CONF_ID]) - await number.register_number( - n, gate_still_threshold, min_value=0, max_value=65535, step=25 - ) - await cg.register_parented(n, hub) - cg.add(hub.set_gate_still_threshold_numbers(0, n)) - if gate_move_threshold := config.get(CONF_MOVE_THRESHOLD): - n = cg.new_Pvariable(gate_move_threshold[CONF_ID]) - await number.register_number( - n, gate_move_threshold, min_value=0, max_value=65535, step=25 - ) - await cg.register_parented(n, hub) - cg.add(hub.set_gate_move_threshold_numbers(0, n)) + # With gate_select one pair of numbers sits at index 0 and edits the selected gate + await numbers( + CONF_STILL_THRESHOLD, + partial(hub.set_gate_still_threshold_numbers, 0), + **threshold, + ) + await numbers( + CONF_MOVE_THRESHOLD, + partial(hub.set_gate_move_threshold_numbers, 0), + **threshold, + ) else: for x in range(16): if gate_conf := config.get(f"gate_{x}"): - move_config = gate_conf[CONF_MOVE_THRESHOLD] - n = cg.new_Pvariable(move_config[CONF_ID], x) - await number.register_number( - n, move_config, min_value=0, max_value=65535, step=25 + await numbers.create( + gate_conf[CONF_MOVE_THRESHOLD], + partial(hub.set_gate_move_threshold_numbers, x), + **threshold, ) - await cg.register_parented(n, hub) - cg.add(hub.set_gate_move_threshold_numbers(x, n)) - - still_config = gate_conf[CONF_STILL_THRESHOLD] - n = cg.new_Pvariable(still_config[CONF_ID], x) - await number.register_number( - n, still_config, min_value=0, max_value=65535, step=25 + await numbers.create( + gate_conf[CONF_STILL_THRESHOLD], + partial(hub.set_gate_still_threshold_numbers, x), + **threshold, ) - await cg.register_parented(n, hub) - cg.add(hub.set_gate_still_threshold_numbers(x, n)) diff --git a/esphome/components/ld2420/number/gate_config_number.cpp b/esphome/components/ld2420/number/gate_config_number.cpp index 491cd857349..ee0088c6499 100644 --- a/esphome/components/ld2420/number/gate_config_number.cpp +++ b/esphome/components/ld2420/number/gate_config_number.cpp @@ -6,11 +6,6 @@ ESPHOME_LOG_TAG(TAG, "ld2420.number"); namespace esphome::ld2420 { -void LD2420TimeoutNumber::control(float timeout) { - this->publish_state(timeout); - this->parent_->new_config.timeout = timeout; -} - void LD2420MinDistanceNumber::control(float min_gate) { if ((uint16_t) min_gate > this->parent_->new_config.max_gate) { min_gate = this->parent_->get_min_gate_distance_value(); @@ -36,36 +31,4 @@ void LD2420GateSelectNumber::control(float gate_select) { this->parent_->publish_gate_still_threshold(gate); } -void LD2420MoveSensFactorNumber::control(float move_factor) { - this->publish_state(move_factor); - this->parent_->gate_move_sensitivity_factor = move_factor; -} - -void LD2420StillSensFactorNumber::control(float still_factor) { - this->publish_state(still_factor); - this->parent_->gate_still_sensitivity_factor = still_factor; -} - -LD2420MoveThresholdNumbers::LD2420MoveThresholdNumbers(uint8_t gate) : gate_(gate) {} - -void LD2420MoveThresholdNumbers::control(float move_threshold) { - this->publish_state(move_threshold); - if (!this->parent_->is_gate_select()) { - this->parent_->new_config.move_thresh[this->gate_] = move_threshold; - } else { - this->parent_->new_config.move_thresh[this->parent_->get_gate_select_value()] = move_threshold; - } -} - -LD2420StillThresholdNumbers::LD2420StillThresholdNumbers(uint8_t gate) : gate_(gate) {} - -void LD2420StillThresholdNumbers::control(float still_threshold) { - this->publish_state(still_threshold); - if (!this->parent_->is_gate_select()) { - this->parent_->new_config.still_thresh[this->gate_] = still_threshold; - } else { - this->parent_->new_config.still_thresh[this->parent_->get_gate_select_value()] = still_threshold; - } -} - } // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/number/gate_config_number.h b/esphome/components/ld2420/number/gate_config_number.h index e1c12e023a9..eb19db95d9a 100644 --- a/esphome/components/ld2420/number/gate_config_number.h +++ b/esphome/components/ld2420/number/gate_config_number.h @@ -5,14 +5,6 @@ namespace esphome::ld2420 { -class LD2420TimeoutNumber final : public number::Number, public Parented { - public: - LD2420TimeoutNumber() = default; - - protected: - void control(float timeout) override; -}; - class LD2420MinDistanceNumber final : public number::Number, public Parented { public: LD2420MinDistanceNumber() = default; @@ -37,40 +29,4 @@ class LD2420GateSelectNumber final : public number::Number, public Parented { - public: - LD2420MoveSensFactorNumber() = default; - - protected: - void control(float move_factor) override; -}; - -class LD2420StillSensFactorNumber final : public number::Number, public Parented { - public: - LD2420StillSensFactorNumber() = default; - - protected: - void control(float still_factor) override; -}; - -class LD2420StillThresholdNumbers final : public number::Number, public Parented { - public: - LD2420StillThresholdNumbers() = default; - LD2420StillThresholdNumbers(uint8_t gate); - - protected: - uint8_t gate_; - void control(float still_threshold) override; -}; - -class LD2420MoveThresholdNumbers final : public number::Number, public Parented { - public: - LD2420MoveThresholdNumbers() = default; - LD2420MoveThresholdNumbers(uint8_t gate); - - protected: - uint8_t gate_; - void control(float move_threshold) override; -}; - } // namespace esphome::ld2420 diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index e14be5ac968..a1bcb96b607 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -957,7 +957,13 @@ void LD2450Component::set_direction_text_sensor(uint8_t target, text_sensor::Tex // Send Zone coordinates data to LD2450 #ifdef USE_NUMBER -void LD2450Component::set_zone_coordinate(uint8_t zone) { +void LD2450Component::set_zone_coordinate(number::Number *n) { + uint8_t zone = 0; + while (zone < MAX_ZONES && this->zone_numbers_[zone].x1 != n && this->zone_numbers_[zone].y1 != n && + this->zone_numbers_[zone].x2 != n && this->zone_numbers_[zone].y2 != n) + zone++; + if (zone == MAX_ZONES) + return; number::Number *x1sens = this->zone_numbers_[zone].x1; number::Number *y1sens = this->zone_numbers_[zone].y1; number::Number *x2sens = this->zone_numbers_[zone].x2; diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index f85a26ba675..feaaa500e33 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -128,7 +128,7 @@ class LD2450Component : public Component, public uart::UARTDevice { void set_direction_text_sensor(uint8_t target, text_sensor::TextSensor *s); #endif #ifdef USE_NUMBER - void set_zone_coordinate(uint8_t zone); + void set_zone_coordinate(number::Number *n); void set_zone_numbers(uint8_t zone, number::Number *x1, number::Number *y1, number::Number *x2, number::Number *y2); #endif #ifdef USE_SENSOR diff --git a/esphome/components/ld2450/number/__init__.py b/esphome/components/ld2450/number/__init__.py index 237c7c4c231..576b8c966ce 100644 --- a/esphome/components/ld2450/number/__init__.py +++ b/esphome/components/ld2450/number/__init__.py @@ -25,7 +25,6 @@ ICON_ARROW_TOP_LEFT_BOLD_BOX_OUTLINE = "mdi:arrow-top-left-bold-box-outline" MAX_ZONES = 3 PresenceTimeoutNumber = ld2450_ns.class_("PresenceTimeoutNumber", number.Number) -ZoneCoordinateNumber = ld2450_ns.class_("ZoneCoordinateNumber", number.Number) CONFIG_SCHEMA = cv.Schema( { @@ -45,28 +44,28 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(f"zone_{n + 1}"): cv.Schema( { cv.Required(CONF_X1): number.number_schema( - ZoneCoordinateNumber, + number.ApplyNumber, device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_MILLIMETER, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_ARROW_TOP_LEFT_BOLD_BOX_OUTLINE, ), cv.Required(CONF_Y1): number.number_schema( - ZoneCoordinateNumber, + number.ApplyNumber, device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_MILLIMETER, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_ARROW_TOP_LEFT, ), cv.Required(CONF_X2): number.number_schema( - ZoneCoordinateNumber, + number.ApplyNumber, device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_MILLIMETER, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_ARROW_BOTTOM_RIGHT_BOLD_BOX_OUTLINE, ), cv.Required(CONF_Y2): number.number_schema( - ZoneCoordinateNumber, + number.ApplyNumber, device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_MILLIMETER, entity_category=ENTITY_CATEGORY_CONFIG, @@ -91,32 +90,21 @@ async def to_code(config: ConfigType) -> None: ) for zone_num in range(MAX_ZONES): if zone_conf := config.get(f"zone_{zone_num + 1}"): - zone_x1_config = zone_conf.get(CONF_X1) - x1 = cg.new_Pvariable(zone_x1_config[CONF_ID], zone_num) - await number.register_number( - x1, zone_x1_config, min_value=-4860, max_value=4860, step=1 - ) - await cg.register_parented(x1, hub) - - zone_y1_config = zone_conf.get(CONF_Y1) - y1 = cg.new_Pvariable(zone_y1_config[CONF_ID], zone_num) - await number.register_number( - y1, zone_y1_config, min_value=0, max_value=7560, step=1 - ) - await cg.register_parented(y1, hub) - - zone_x2_config = zone_conf.get(CONF_X2) - x2 = cg.new_Pvariable(zone_x2_config[CONF_ID], zone_num) - await number.register_number( - x2, zone_x2_config, min_value=-4860, max_value=4860, step=1 - ) - await cg.register_parented(x2, hub) - - zone_y2_config = zone_conf.get(CONF_Y2) - y2 = cg.new_Pvariable(zone_y2_config[CONF_ID], zone_num) - await number.register_number( - y2, zone_y2_config, min_value=0, max_value=7560, step=1 - ) - await cg.register_parented(y2, hub) - + x1, y1, x2, y2 = [ + await numbers.create( + zone_conf[key], + None, + min_value=lo, + max_value=hi, + step=1, + apply="set_zone_coordinate(entity)", + publish="before", + ) + for key, lo, hi in ( + (CONF_X1, -4860, 4860), + (CONF_Y1, 0, 7560), + (CONF_X2, -4860, 4860), + (CONF_Y2, 0, 7560), + ) + ] cg.add(hub.set_zone_numbers(zone_num, x1, y1, x2, y2)) diff --git a/esphome/components/ld2450/number/zone_coordinate_number.cpp b/esphome/components/ld2450/number/zone_coordinate_number.cpp deleted file mode 100644 index 83be3cbec4b..00000000000 --- a/esphome/components/ld2450/number/zone_coordinate_number.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include "zone_coordinate_number.h" - -namespace esphome::ld2450 { - -ZoneCoordinateNumber::ZoneCoordinateNumber(uint8_t zone) : zone_(zone) {} - -void ZoneCoordinateNumber::control(float value) { - this->publish_state(value); - this->parent_->set_zone_coordinate(this->zone_); -} - -} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/number/zone_coordinate_number.h b/esphome/components/ld2450/number/zone_coordinate_number.h deleted file mode 100644 index f5a389d7129..00000000000 --- a/esphome/components/ld2450/number/zone_coordinate_number.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "esphome/components/number/number.h" -#include "../ld2450.h" - -namespace esphome::ld2450 { - -class ZoneCoordinateNumber : public number::Number, public Parented { - public: - ZoneCoordinateNumber(uint8_t zone); - - protected: - uint8_t zone_; - void control(float value) override; -}; - -} // namespace esphome::ld2450 diff --git a/tests/components/ld2420/ld2420_gate_threshold.cpp b/tests/components/ld2420/ld2420_gate_threshold.cpp new file mode 100644 index 00000000000..7937001fcb7 --- /dev/null +++ b/tests/components/ld2420/ld2420_gate_threshold.cpp @@ -0,0 +1,71 @@ +#include +#include "esphome/components/ld2420/ld2420.h" + +namespace esphome::ld2420::testing { + +class TestNumber : public number::Number { + protected: + void control(float value) override {} +}; + +class LD2420GateThresholdTest : public ::testing::Test { + protected: + void SetUp() override { + for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { + this->ld2420_.new_config.move_thresh[gate] = 1000 + gate; + this->ld2420_.new_config.still_thresh[gate] = 2000 + gate; + } + } + void expect_untouched_except_(const uint32_t *thresh, uint32_t base, int skip) { + for (int gate = 0; gate < TOTAL_GATES; gate++) { + if (gate != skip) + EXPECT_EQ(thresh[gate], base + gate) << "gate " << gate; + } + } + + LD2420Component ld2420_; + TestNumber move_; + TestNumber still_; +}; + +TEST_F(LD2420GateThresholdTest, PerGateNumbersWriteTheirOwnGate) { + this->ld2420_.set_gate_move_threshold_numbers(3, &this->move_); + this->ld2420_.set_gate_still_threshold_numbers(3, &this->still_); + + this->ld2420_.set_gate_threshold_value(&this->move_, 500); + EXPECT_EQ(this->ld2420_.new_config.move_thresh[3], 500u); + this->expect_untouched_except_(this->ld2420_.new_config.move_thresh, 1000, 3); + this->expect_untouched_except_(this->ld2420_.new_config.still_thresh, 2000, -1); + + this->ld2420_.set_gate_threshold_value(&this->still_, 750); + EXPECT_EQ(this->ld2420_.new_config.still_thresh[3], 750u); + EXPECT_EQ(this->ld2420_.new_config.move_thresh[3], 500u); + this->expect_untouched_except_(this->ld2420_.new_config.still_thresh, 2000, 3); +} + +TEST_F(LD2420GateThresholdTest, GateSelectWritesTheSelectedGate) { + TestNumber select; + this->ld2420_.set_gate_select_number(&select); + this->ld2420_.set_gate_move_threshold_numbers(0, &this->move_); + this->ld2420_.set_gate_still_threshold_numbers(0, &this->still_); + select.publish_state(5); + + this->ld2420_.set_gate_threshold_value(&this->move_, 300); + this->ld2420_.set_gate_threshold_value(&this->still_, 400); + EXPECT_EQ(this->ld2420_.new_config.move_thresh[5], 300u); + EXPECT_EQ(this->ld2420_.new_config.still_thresh[5], 400u); + this->expect_untouched_except_(this->ld2420_.new_config.move_thresh, 1000, 5); + this->expect_untouched_except_(this->ld2420_.new_config.still_thresh, 2000, 5); +} + +TEST_F(LD2420GateThresholdTest, UnregisteredNumberChangesNothing) { + this->ld2420_.set_gate_move_threshold_numbers(3, &this->move_); + this->ld2420_.set_gate_still_threshold_numbers(3, &this->still_); + TestNumber other; + + this->ld2420_.set_gate_threshold_value(&other, 123); + this->expect_untouched_except_(this->ld2420_.new_config.move_thresh, 1000, -1); + this->expect_untouched_except_(this->ld2420_.new_config.still_thresh, 2000, -1); +} + +} // namespace esphome::ld2420::testing diff --git a/tests/components/ld2420/number/__init__.py b/tests/components/ld2420/number/__init__.py new file mode 100644 index 00000000000..539d06de1a3 --- /dev/null +++ b/tests/components/ld2420/number/__init__.py @@ -0,0 +1,12 @@ +from esphome.core import CORE +from esphome.types import ConfigType +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # Turns on USE_NUMBER so the hub's number forwarding code is in the host test build + async def to_code_testing(config: ConfigType) -> None: + CORE.testing_ensure_platform_registered("number") + + manifest.to_code = to_code_testing + manifest.dependencies = manifest.dependencies + ["number"] diff --git a/tests/components/ld2450/ld2450_zone_number.cpp b/tests/components/ld2450/ld2450_zone_number.cpp new file mode 100644 index 00000000000..0c5cf993aeb --- /dev/null +++ b/tests/components/ld2450/ld2450_zone_number.cpp @@ -0,0 +1,83 @@ +#include +#include "common.h" + +namespace esphome::ld2450::testing { + +namespace { +class CapturingUARTComponent : public MockUARTComponent { + public: + void write_array(const uint8_t *data, size_t len) override { + this->written.insert(this->written.end(), data, data + len); + } + std::vector written; +}; + +class TestNumber : public number::Number { + protected: + void control(float value) override {} +}; + +// Header, length 28, command 0xC2 (set zone): the coordinate payload follows two zone type bytes. +constexpr uint8_t SET_ZONE_PREFIX[] = {0xFD, 0xFC, 0xFB, 0xFA, 0x1C, 0x00, 0xC2, 0x00}; +} // namespace + +class LD2450ZoneNumberTest : public ::testing::Test { + protected: + void SetUp() override { + this->ld2450_.set_uart_parent(&this->uart_); + for (auto &zone : this->ld2450_.zone_config_) { + zone = {1, 2, 3, 4}; + } + // Only the third zone has numbers, so the lookup must walk past the empty slots. + this->ld2450_.set_zone_numbers(2, &this->x1_, &this->y1_, &this->x2_, &this->y2_); + this->x1_.publish_state(-100); + this->y1_.publish_state(200); + this->x2_.publish_state(1500); + this->y2_.publish_state(3000); + } + + CapturingUARTComponent uart_; + TestableLD2450 ld2450_; + TestNumber x1_, y1_, x2_, y2_; +}; + +TEST_F(LD2450ZoneNumberTest, EachCoordinateNumberUpdatesItsZoneAndSendsCommand) { + for (TestNumber *n : {&this->x1_, &this->y1_, &this->x2_, &this->y2_}) { + this->uart_.written.clear(); + this->ld2450_.zone_config_[2] = {0, 0, 0, 0}; + this->ld2450_.set_zone_coordinate(n); + + EXPECT_EQ(this->ld2450_.zone_config_[2].x1, -100); + EXPECT_EQ(this->ld2450_.zone_config_[2].y1, 200); + EXPECT_EQ(this->ld2450_.zone_config_[2].x2, 1500); + EXPECT_EQ(this->ld2450_.zone_config_[2].y2, 3000); + for (size_t i = 0; i < 2; i++) { + EXPECT_EQ(this->ld2450_.zone_config_[i].x1, 1) << "zone " << i; + EXPECT_EQ(this->ld2450_.zone_config_[i].y2, 4) << "zone " << i; + } + + const auto &w = this->uart_.written; + auto it = std::search(w.begin(), w.end(), std::begin(SET_ZONE_PREFIX), std::end(SET_ZONE_PREFIX)); + ASSERT_NE(it, w.end()); + ASSERT_GE(static_cast(w.end() - it), sizeof(SET_ZONE_PREFIX) + 26 + 4); + const uint8_t *zone2 = &*it + sizeof(SET_ZONE_PREFIX) + 2 + 2 * 8; + const std::vector expected = {0x9C, 0xFF, 0xC8, 0x00, 0xDC, 0x05, 0xB8, 0x0B}; + EXPECT_EQ(std::vector(zone2, zone2 + 8), expected); + } +} + +TEST_F(LD2450ZoneNumberTest, UnregisteredNumberChangesNothing) { + TestNumber other; + other.publish_state(42); + this->ld2450_.set_zone_coordinate(&other); + + for (size_t i = 0; i < MAX_ZONES; i++) { + EXPECT_EQ(this->ld2450_.zone_config_[i].x1, 1) << "zone " << i; + EXPECT_EQ(this->ld2450_.zone_config_[i].y1, 2) << "zone " << i; + EXPECT_EQ(this->ld2450_.zone_config_[i].x2, 3) << "zone " << i; + EXPECT_EQ(this->ld2450_.zone_config_[i].y2, 4) << "zone " << i; + } + EXPECT_TRUE(this->uart_.written.empty()); +} + +} // namespace esphome::ld2450::testing diff --git a/tests/components/ld2450/number/__init__.py b/tests/components/ld2450/number/__init__.py new file mode 100644 index 00000000000..539d06de1a3 --- /dev/null +++ b/tests/components/ld2450/number/__init__.py @@ -0,0 +1,12 @@ +from esphome.core import CORE +from esphome.types import ConfigType +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # Turns on USE_NUMBER so the hub's number forwarding code is in the host test build + async def to_code_testing(config: ConfigType) -> None: + CORE.testing_ensure_platform_registered("number") + + manifest.to_code = to_code_testing + manifest.dependencies = manifest.dependencies + ["number"]