From 950cfc4da317b511f712b717a15ae49a9a2301d8 Mon Sep 17 00:00:00 2001 From: Mustafa KURU Date: Fri, 7 Aug 2026 21:33:37 +0300 Subject: [PATCH] [ld6002b] Add select and button platforms (4/5) (#17822) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/ld6002b/button/__init__.py | 101 ++++++++++ .../ld6002b/button/ld6002b_button.cpp | 7 + .../ld6002b/button/ld6002b_button.h | 18 ++ esphome/components/ld6002b/const.py | 11 + esphome/components/ld6002b/ld6002b.cpp | 190 ++++++++++++++++++ esphome/components/ld6002b/ld6002b.h | 43 +++- esphome/components/ld6002b/select/__init__.py | 60 ++++++ .../ld6002b/select/ld6002b_select.cpp | 10 + .../ld6002b/select/ld6002b_select.h | 18 ++ tests/component_tests/ld6002b/__init__.py | 0 .../ld6002b/test_final_validate.py | 82 ++++++++ tests/components/ld6002b/common.yaml | 32 +++ 12 files changed, 571 insertions(+), 1 deletion(-) create mode 100644 esphome/components/ld6002b/button/__init__.py create mode 100644 esphome/components/ld6002b/button/ld6002b_button.cpp create mode 100644 esphome/components/ld6002b/button/ld6002b_button.h create mode 100644 esphome/components/ld6002b/select/__init__.py create mode 100644 esphome/components/ld6002b/select/ld6002b_select.cpp create mode 100644 esphome/components/ld6002b/select/ld6002b_select.h create mode 100644 tests/component_tests/ld6002b/__init__.py create mode 100644 tests/component_tests/ld6002b/test_final_validate.py diff --git a/esphome/components/ld6002b/button/__init__.py b/esphome/components/ld6002b/button/__init__.py new file mode 100644 index 0000000000..0046131b62 --- /dev/null +++ b/esphome/components/ld6002b/button/__init__.py @@ -0,0 +1,101 @@ +import esphome.codegen as cg +from esphome.components import button +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_WAKEUP_PIN, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, +) +import esphome.final_validate as fv + +from .. import LD6002BComponent, ld6002b_ns +from ..const import ( + CONF_GET_DELAY, + CONF_GET_INSTALLATION, + CONF_GET_LOW_POWER_MODE, + CONF_GET_LOW_POWER_SLEEP_TIME, + CONF_GET_SENSITIVITY, + CONF_GET_TRIGGER_SPEED, + CONF_GET_Z_RANGE, + CONF_LD6002B_ID, + CONF_RESET_UNATTENDED, + CONF_WAKE, +) + +DEPENDENCIES = ["ld6002b"] + +LD6002BButton = ld6002b_ns.class_("LD6002BButton", button.Button) +ButtonType = ld6002b_ns.enum("ButtonType", is_class=True) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent), + cv.Optional(CONF_GET_DELAY): button.button_schema( + LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ), + cv.Optional(CONF_GET_SENSITIVITY): button.button_schema( + LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ), + cv.Optional(CONF_GET_TRIGGER_SPEED): button.button_schema( + LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ), + cv.Optional(CONF_GET_Z_RANGE): button.button_schema( + LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ), + cv.Optional(CONF_GET_INSTALLATION): button.button_schema( + LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ), + cv.Optional(CONF_GET_LOW_POWER_MODE): button.button_schema( + LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ), + cv.Optional(CONF_GET_LOW_POWER_SLEEP_TIME): button.button_schema( + LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ), + cv.Optional(CONF_RESET_UNATTENDED): button.button_schema( + LD6002BButton, entity_category=ENTITY_CATEGORY_CONFIG + ), + cv.Optional(CONF_WAKE): button.button_schema( + LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ), + } +) + + +def final_validate(config): + full_config = fv.full_config.get() + hub_id = config[CONF_LD6002B_ID] + + if config.get(CONF_WAKE): + hub_path = full_config.get_path_for_id(hub_id) + hub_config = full_config.get_config_for_path(hub_path[:-1]) + if hub_config.get(CONF_WAKEUP_PIN) is None: + raise cv.Invalid( + f"{CONF_WAKE} requires {CONF_WAKEUP_PIN} on the parent ld6002b component", + path=[CONF_WAKE], + ) + + return config + + +FINAL_VALIDATE_SCHEMA = final_validate + +BUTTON_MAP = { + CONF_GET_DELAY: ButtonType.GET_DELAY, + CONF_GET_SENSITIVITY: ButtonType.GET_SENSITIVITY, + CONF_GET_TRIGGER_SPEED: ButtonType.GET_TRIGGER_SPEED, + CONF_GET_Z_RANGE: ButtonType.GET_Z_RANGE, + CONF_GET_INSTALLATION: ButtonType.GET_INSTALLATION, + CONF_GET_LOW_POWER_MODE: ButtonType.GET_LOW_POWER_MODE, + CONF_GET_LOW_POWER_SLEEP_TIME: ButtonType.GET_LOW_POWER_SLEEP_TIME, + CONF_RESET_UNATTENDED: ButtonType.RESET_UNATTENDED, + CONF_WAKE: ButtonType.WAKE, +} + + +async def to_code(config): + for key, button_type in BUTTON_MAP.items(): + if button_config := config.get(key): + b = cg.new_Pvariable(button_config[CONF_ID], button_type) + await button.register_button(b, button_config) + await cg.register_parented(b, config[CONF_LD6002B_ID]) diff --git a/esphome/components/ld6002b/button/ld6002b_button.cpp b/esphome/components/ld6002b/button/ld6002b_button.cpp new file mode 100644 index 0000000000..fb398a9a59 --- /dev/null +++ b/esphome/components/ld6002b/button/ld6002b_button.cpp @@ -0,0 +1,7 @@ +#include "ld6002b_button.h" + +namespace esphome::ld6002b { + +void LD6002BButton::press_action() { this->parent_->press_button(this->type_); } + +} // namespace esphome::ld6002b diff --git a/esphome/components/ld6002b/button/ld6002b_button.h b/esphome/components/ld6002b/button/ld6002b_button.h new file mode 100644 index 0000000000..c222143453 --- /dev/null +++ b/esphome/components/ld6002b/button/ld6002b_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld6002b.h" + +namespace esphome::ld6002b { + +class LD6002BButton : public button::Button, public Parented { + public: + explicit LD6002BButton(ButtonType type) : type_(type) {} + + protected: + void press_action() override; + + ButtonType type_; +}; + +} // namespace esphome::ld6002b diff --git a/esphome/components/ld6002b/const.py b/esphome/components/ld6002b/const.py index 9f9227988e..fac9f08015 100644 --- a/esphome/components/ld6002b/const.py +++ b/esphome/components/ld6002b/const.py @@ -1,14 +1,25 @@ CONF_AUTO_WAKE = "auto_wake" CONF_CLUSTER_ID = "cluster_id" CONF_DOPPLER_INDEX = "doppler_index" +CONF_GET_DELAY = "get_delay" +CONF_GET_INSTALLATION = "get_installation" +CONF_GET_LOW_POWER_MODE = "get_low_power_mode" +CONF_GET_LOW_POWER_SLEEP_TIME = "get_low_power_sleep_time" +CONF_GET_SENSITIVITY = "get_sensitivity" +CONF_GET_TRIGGER_SPEED = "get_trigger_speed" +CONF_GET_Z_RANGE = "get_z_range" CONF_HOLD_DELAY = "hold_delay" +CONF_INSTALLATION_MODE = "installation_mode" CONF_LD6002B_ID = "ld6002b_id" CONF_LOW_POWER = "low_power" CONF_LOW_POWER_SLEEP_TIME = "low_power_sleep_time" CONF_OTA_VERSION = "ota_version" CONF_POINT_CLOUD = "point_cloud" CONF_POINT_COUNT = "point_count" +CONF_RESET_UNATTENDED = "reset_unattended" CONF_TARGET_DISPLAY = "target_display" +CONF_TRIGGER_SPEED = "trigger_speed" +CONF_WAKE = "wake" CONF_WAKEUP_PULSE = "wakeup_pulse" CONF_WORK_MODE = "work_mode" CONF_Z = "z" diff --git a/esphome/components/ld6002b/ld6002b.cpp b/esphome/components/ld6002b/ld6002b.cpp index 54979fe9eb..25b3da174c 100644 --- a/esphome/components/ld6002b/ld6002b.cpp +++ b/esphome/components/ld6002b/ld6002b.cpp @@ -22,7 +22,10 @@ static constexpr uint16_t TYPE_SET_LOW_POWER_SLEEP = 0x0205; static constexpr uint16_t TYPE_REPORT_TARGET = 0x0A04; static constexpr uint16_t TYPE_REPORT_POINT_CLOUD = 0x0A08; static constexpr uint16_t TYPE_REPORT_DELAY = 0x0A0D; +static constexpr uint16_t TYPE_REPORT_SENSITIVITY = 0x0A0E; +static constexpr uint16_t TYPE_REPORT_TRIGGER = 0x0A0F; static constexpr uint16_t TYPE_REPORT_Z_RANGE = 0x0A10; +static constexpr uint16_t TYPE_REPORT_INSTALLATION = 0x0A11; static constexpr uint16_t TYPE_REPORT_LOW_POWER = 0x0A12; static constexpr uint16_t TYPE_REPORT_LOW_POWER_SLEEP = 0x0A13; static constexpr uint16_t TYPE_REPORT_WORK_MODE = 0x0A14; @@ -34,11 +37,23 @@ static constexpr uint32_t CMD_POINT_CLOUD_ON = 0x06; static constexpr uint32_t CMD_POINT_CLOUD_OFF = 0x07; static constexpr uint32_t CMD_TARGET_DISPLAY_ON = 0x08; static constexpr uint32_t CMD_TARGET_DISPLAY_OFF = 0x09; +static constexpr uint32_t CMD_SENSITIVITY_LOW = 0x0A; +static constexpr uint32_t CMD_SENSITIVITY_MEDIUM = 0x0B; +static constexpr uint32_t CMD_SENSITIVITY_HIGH = 0x0C; +static constexpr uint32_t CMD_GET_SENSITIVITY = 0x0D; +static constexpr uint32_t CMD_TRIGGER_SLOW = 0x0E; +static constexpr uint32_t CMD_TRIGGER_MEDIUM = 0x0F; +static constexpr uint32_t CMD_TRIGGER_FAST = 0x10; +static constexpr uint32_t CMD_GET_TRIGGER = 0x11; static constexpr uint32_t CMD_GET_Z_RANGE = 0x12; +static constexpr uint32_t CMD_INSTALL_TOP = 0x13; +static constexpr uint32_t CMD_INSTALL_SIDE = 0x14; +static constexpr uint32_t CMD_GET_INSTALLATION = 0x15; static constexpr uint32_t CMD_LOW_POWER_ON = 0x16; static constexpr uint32_t CMD_LOW_POWER_OFF = 0x17; static constexpr uint32_t CMD_GET_LOW_POWER = 0x18; static constexpr uint32_t CMD_GET_LOW_POWER_SLEEP = 0x19; +static constexpr uint32_t CMD_RESET_UNATTENDED = 0x1A; static constexpr uint16_t TARGET_DATA_LEN = 20; // x,y,z,dop_idx,cluster_id @@ -57,8 +72,30 @@ static const char *control_command_name(uint32_t command) { return "target_display_on"; case CMD_TARGET_DISPLAY_OFF: return "target_display_off"; + case CMD_SENSITIVITY_LOW: + return "sensitivity_low"; + case CMD_SENSITIVITY_MEDIUM: + return "sensitivity_medium"; + case CMD_SENSITIVITY_HIGH: + return "sensitivity_high"; + case CMD_GET_SENSITIVITY: + return "get_sensitivity"; + case CMD_TRIGGER_SLOW: + return "trigger_slow"; + case CMD_TRIGGER_MEDIUM: + return "trigger_medium"; + case CMD_TRIGGER_FAST: + return "trigger_fast"; + case CMD_GET_TRIGGER: + return "get_trigger"; case CMD_GET_Z_RANGE: return "get_z_range"; + case CMD_INSTALL_TOP: + return "install_top"; + case CMD_INSTALL_SIDE: + return "install_side"; + case CMD_GET_INSTALLATION: + return "get_installation"; case CMD_LOW_POWER_ON: return "low_power_on"; case CMD_LOW_POWER_OFF: @@ -67,6 +104,8 @@ static const char *control_command_name(uint32_t command) { return "get_low_power"; case CMD_GET_LOW_POWER_SLEEP: return "get_low_power_sleep"; + case CMD_RESET_UNATTENDED: + return "reset_unattended"; default: return "unknown"; } @@ -88,8 +127,14 @@ static const char *frame_type_name(uint16_t type) { return "report_point_cloud"; case TYPE_REPORT_DELAY: return "report_delay"; + case TYPE_REPORT_SENSITIVITY: + return "report_sensitivity"; + case TYPE_REPORT_TRIGGER: + return "report_trigger"; case TYPE_REPORT_Z_RANGE: return "report_z_range"; + case TYPE_REPORT_INSTALLATION: + return "report_installation"; case TYPE_REPORT_LOW_POWER: return "report_low_power"; case TYPE_REPORT_LOW_POWER_SLEEP: @@ -107,8 +152,14 @@ static bool is_expected_control_report(uint32_t command, uint16_t type) { switch (command) { case CMD_GET_DELAY: return type == TYPE_REPORT_DELAY; + case CMD_GET_SENSITIVITY: + return type == TYPE_REPORT_SENSITIVITY; + case CMD_GET_TRIGGER: + return type == TYPE_REPORT_TRIGGER; case CMD_GET_Z_RANGE: return type == TYPE_REPORT_Z_RANGE; + case CMD_GET_INSTALLATION: + return type == TYPE_REPORT_INSTALLATION; case CMD_GET_LOW_POWER: case CMD_LOW_POWER_ON: case CMD_LOW_POWER_OFF: @@ -259,6 +310,18 @@ void LD6002BComponent::setup() { this->send_control_command_(want_point_cloud ? CMD_POINT_CLOUD_ON : CMD_POINT_CLOUD_OFF); this->point_cloud_enabled_ = want_point_cloud; } + +#ifdef USE_SELECT + if (this->sensitivity_select_ != nullptr) { + this->send_control_command_(CMD_GET_SENSITIVITY); + } + if (this->trigger_speed_select_ != nullptr) { + this->send_control_command_(CMD_GET_TRIGGER); + } + if (this->installation_select_ != nullptr) { + this->send_control_command_(CMD_GET_INSTALLATION); + } +#endif #ifdef USE_NUMBER if (this->z_min_number_ != nullptr || this->z_max_number_ != nullptr) { this->send_control_command_(CMD_GET_Z_RANGE); @@ -345,6 +408,11 @@ void LD6002BComponent::dump_config() { LOG_SWITCH(" ", "Point Cloud", this->point_cloud_switch_); LOG_SWITCH(" ", "Target Display", this->target_display_switch_); #endif +#ifdef USE_SELECT + LOG_SELECT(" ", "Sensitivity", this->sensitivity_select_); + LOG_SELECT(" ", "Trigger Speed", this->trigger_speed_select_); + LOG_SELECT(" ", "Installation Mode", this->installation_select_); +#endif } void LD6002BComponent::loop() { @@ -492,9 +560,18 @@ void LD6002BComponent::handle_frame_(uint16_t type, const uint8_t *data, uint16_ case TYPE_REPORT_DELAY: this->handle_delay_report_(data, len); break; + case TYPE_REPORT_SENSITIVITY: + this->handle_sensitivity_report_(data, len); + break; + case TYPE_REPORT_TRIGGER: + this->handle_trigger_speed_report_(data, len); + break; case TYPE_REPORT_Z_RANGE: this->handle_z_range_report_(data, len); break; + case TYPE_REPORT_INSTALLATION: + this->handle_installation_report_(data, len); + break; case TYPE_REPORT_LOW_POWER: this->handle_low_power_report_(data, len); break; @@ -660,6 +737,32 @@ void LD6002BComponent::handle_delay_report_(const uint8_t *data, uint16_t len) { #endif } +void LD6002BComponent::handle_sensitivity_report_(const uint8_t *data, uint16_t len) { + if (len < 1) + return; +#ifdef USE_SELECT + if (this->sensitivity_select_ == nullptr) + return; + uint8_t value = data[0]; + if (value <= 2) { + this->sensitivity_select_->publish_state(value); + } +#endif +} + +void LD6002BComponent::handle_trigger_speed_report_(const uint8_t *data, uint16_t len) { + if (len < 1) + return; +#ifdef USE_SELECT + if (this->trigger_speed_select_ == nullptr) + return; + uint8_t value = data[0]; + if (value <= 2) { + this->trigger_speed_select_->publish_state(value); + } +#endif +} + void LD6002BComponent::handle_z_range_report_(const uint8_t *data, uint16_t len) { if (len < 8) return; @@ -673,6 +776,19 @@ void LD6002BComponent::handle_z_range_report_(const uint8_t *data, uint16_t len) #endif } +void LD6002BComponent::handle_installation_report_(const uint8_t *data, uint16_t len) { + if (len < 1) + return; +#ifdef USE_SELECT + if (this->installation_select_ == nullptr) + return; + uint8_t value = data[0]; + if (value <= 1) { + this->installation_select_->publish_state(value); + } +#endif +} + void LD6002BComponent::handle_low_power_report_(const uint8_t *data, uint16_t len) { if (len < 1) return; @@ -889,6 +1005,8 @@ void LD6002BComponent::send_command_internal_(uint16_t type, const uint8_t *data if (len > 0 && data != nullptr) { std::memcpy(this->wake_scratch_.data(), data, len); } + // A button pulse must not raise the pin in the middle of this one. + this->cancel_timeout(WAKE_BUTTON_TIMEOUT); this->wake_pulse_pending_ = true; this->wakeup_pin_->digital_write(false); const uint8_t generation = this->send_generation_; @@ -972,6 +1090,16 @@ void LD6002BComponent::send_z_range_() { this->queue_command_(TYPE_SET_Z_RANGE, data, sizeof(data)); } +void LD6002BComponent::wake_() { + // A command's own pulse raises the pin and writes after it, so ride along instead of + // claiming the flag: claiming it would send that command down the immediate-write path + // with the pin still low. + if (this->wakeup_pin_ == nullptr || this->wake_pulse_pending_) + return; + this->wakeup_pin_->digital_write(false); + this->set_timeout(WAKE_BUTTON_TIMEOUT, this->wakeup_pulse_ms_, [this]() { this->wakeup_pin_->digital_write(true); }); +} + void LD6002BComponent::set_number_value(NumberType type, float value) { switch (type) { case NumberType::HOLD_DELAY: { @@ -999,6 +1127,36 @@ void LD6002BComponent::set_number_value(NumberType type, float value) { } } +void LD6002BComponent::set_select_value(SelectType type, size_t index) { + switch (type) { + case SelectType::SENSITIVITY: + if (index == 0) { + this->send_control_command_(CMD_SENSITIVITY_LOW); + } else if (index == 1) { + this->send_control_command_(CMD_SENSITIVITY_MEDIUM); + } else if (index == 2) { + this->send_control_command_(CMD_SENSITIVITY_HIGH); + } + break; + case SelectType::TRIGGER_SPEED: + if (index == 0) { + this->send_control_command_(CMD_TRIGGER_SLOW); + } else if (index == 1) { + this->send_control_command_(CMD_TRIGGER_MEDIUM); + } else if (index == 2) { + this->send_control_command_(CMD_TRIGGER_FAST); + } + break; + case SelectType::INSTALLATION_MODE: + if (index == 0) { + this->send_control_command_(CMD_INSTALL_TOP); + } else if (index == 1) { + this->send_control_command_(CMD_INSTALL_SIDE); + } + break; + } +} + void LD6002BComponent::init_version_pref_() { #ifdef USE_TEXT_SENSOR if (this->ota_version_text_sensor_ == nullptr) { @@ -1124,4 +1282,36 @@ void LD6002BComponent::set_switch_state(SwitchType type, bool state) { } } +void LD6002BComponent::press_button(ButtonType type) { + switch (type) { + case ButtonType::GET_DELAY: + this->send_control_command_(CMD_GET_DELAY); + break; + case ButtonType::GET_SENSITIVITY: + this->send_control_command_(CMD_GET_SENSITIVITY); + break; + case ButtonType::GET_TRIGGER_SPEED: + this->send_control_command_(CMD_GET_TRIGGER); + break; + case ButtonType::GET_Z_RANGE: + this->send_control_command_(CMD_GET_Z_RANGE); + break; + case ButtonType::GET_INSTALLATION: + this->send_control_command_(CMD_GET_INSTALLATION); + break; + case ButtonType::GET_LOW_POWER_MODE: + this->send_control_command_(CMD_GET_LOW_POWER); + break; + case ButtonType::GET_LOW_POWER_SLEEP_TIME: + this->send_control_command_(CMD_GET_LOW_POWER_SLEEP); + break; + case ButtonType::RESET_UNATTENDED: + this->send_control_command_(CMD_RESET_UNATTENDED); + break; + case ButtonType::WAKE: + this->wake_(); + break; + } +} + } // namespace esphome::ld6002b diff --git a/esphome/components/ld6002b/ld6002b.h b/esphome/components/ld6002b/ld6002b.h index 5630d2d1a8..141f4ff027 100644 --- a/esphome/components/ld6002b/ld6002b.h +++ b/esphome/components/ld6002b/ld6002b.h @@ -3,6 +3,7 @@ #include "esphome/core/defines.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" #include "esphome/core/gpio.h" #include "esphome/components/uart/uart.h" #ifdef USE_SENSOR @@ -12,12 +13,14 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #endif #ifdef USE_TEXT_SENSOR -#include "esphome/core/preferences.h" #include "esphome/components/text_sensor/text_sensor.h" #endif #ifdef USE_NUMBER #include "esphome/components/number/number.h" #endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif #ifdef USE_SWITCH #include "esphome/components/switch/switch.h" #endif @@ -40,12 +43,30 @@ enum class NumberType : uint8_t { LOW_POWER_SLEEP, }; +enum class SelectType : uint8_t { + SENSITIVITY, + TRIGGER_SPEED, + INSTALLATION_MODE, +}; + enum class SwitchType : uint8_t { LOW_POWER, POINT_CLOUD, TARGET_DISPLAY, }; +enum class ButtonType : uint8_t { + GET_DELAY, + GET_SENSITIVITY, + GET_TRIGGER_SPEED, + GET_Z_RANGE, + GET_INSTALLATION, + GET_LOW_POWER_MODE, + GET_LOW_POWER_SLEEP_TIME, + RESET_UNATTENDED, + WAKE, +}; + #ifdef USE_SENSOR struct TargetSensors { sensor::Sensor *x{nullptr}; @@ -124,6 +145,12 @@ class LD6002BComponent : public Component, public uart::UARTDevice { void set_low_power_sleep_number(number::Number *number) { this->low_power_sleep_number_ = number; } #endif +#ifdef USE_SELECT + void set_sensitivity_select(select::Select *select) { this->sensitivity_select_ = select; } + void set_trigger_speed_select(select::Select *select) { this->trigger_speed_select_ = select; } + void set_installation_select(select::Select *select) { this->installation_select_ = select; } +#endif + #ifdef USE_SWITCH void set_low_power_switch(switch_::Switch *sw) { this->low_power_switch_ = sw; } void set_point_cloud_switch(switch_::Switch *sw) { this->point_cloud_switch_ = sw; } @@ -131,7 +158,9 @@ class LD6002BComponent : public Component, public uart::UARTDevice { #endif void set_number_value(NumberType type, float value); + void set_select_value(SelectType type, size_t index); void set_switch_state(SwitchType type, bool state); + void press_button(ButtonType type); protected: enum class ParseState : uint8_t { SOF, HEADER, HCK, DATA, DCK, DISCARD }; @@ -148,7 +177,10 @@ class LD6002BComponent : public Component, public uart::UARTDevice { void handle_target_report_(const uint8_t *data, uint16_t len); void handle_point_cloud_(const uint8_t *data, uint16_t len); void handle_delay_report_(const uint8_t *data, uint16_t len); + void handle_sensitivity_report_(const uint8_t *data, uint16_t len); + void handle_trigger_speed_report_(const uint8_t *data, uint16_t len); void handle_z_range_report_(const uint8_t *data, uint16_t len); + void handle_installation_report_(const uint8_t *data, uint16_t len); void handle_low_power_report_(const uint8_t *data, uint16_t len); void handle_low_power_sleep_report_(const uint8_t *data, uint16_t len); void handle_work_mode_report_(const uint8_t *data, uint16_t len); @@ -173,6 +205,7 @@ class LD6002BComponent : public Component, public uart::UARTDevice { void write_frame_(uint16_t type, const uint8_t *data, uint8_t len, bool track); void send_control_command_(uint32_t command); void send_z_range_(); + void wake_(); static uint16_t read_u16_be(const uint8_t *data); static uint32_t read_u32_le(const uint8_t *data); @@ -202,6 +235,11 @@ class LD6002BComponent : public Component, public uart::UARTDevice { number::Number *z_max_number_{nullptr}; number::Number *low_power_sleep_number_{nullptr}; #endif +#ifdef USE_SELECT + select::Select *sensitivity_select_{nullptr}; + select::Select *trigger_speed_select_{nullptr}; + select::Select *installation_select_{nullptr}; +#endif #ifdef USE_SWITCH switch_::Switch *low_power_switch_{nullptr}; switch_::Switch *point_cloud_switch_{nullptr}; @@ -235,6 +273,9 @@ class LD6002BComponent : public Component, public uart::UARTDevice { // How long the module stays awake after any frame, and so still answers the next one. static constexpr uint32_t MODULE_AWAKE_MS = 10000; static constexpr uint8_t CMD_MAX_RETRIES = 3; + // Named so a repeated press replaces its own pending timeout instead of stacking + // another, and so the command path can cancel it when it takes the pin over. + static constexpr const char *WAKE_BUTTON_TIMEOUT = "wake_button"; // A reply cannot trail the frame that earned it for longer than this; the field worst case is ~726ms. static constexpr uint32_t STALE_ACK_MAX_AGE_MS = 1000; diff --git a/esphome/components/ld6002b/select/__init__.py b/esphome/components/ld6002b/select/__init__.py new file mode 100644 index 0000000000..3fcc117e2f --- /dev/null +++ b/esphome/components/ld6002b/select/__init__.py @@ -0,0 +1,60 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import CONF_SENSITIVITY, ENTITY_CATEGORY_CONFIG + +from .. import LD6002BComponent, ld6002b_ns +from ..const import CONF_INSTALLATION_MODE, CONF_LD6002B_ID, CONF_TRIGGER_SPEED + +DEPENDENCIES = ["ld6002b"] + +LD6002BSelect = ld6002b_ns.class_("LD6002BSelect", select.Select) +SelectType = ld6002b_ns.enum("SelectType", is_class=True) + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent), + cv.Optional(CONF_SENSITIVITY): select.select_schema( + LD6002BSelect, entity_category=ENTITY_CATEGORY_CONFIG + ), + cv.Optional(CONF_TRIGGER_SPEED): select.select_schema( + LD6002BSelect, entity_category=ENTITY_CATEGORY_CONFIG + ), + cv.Optional(CONF_INSTALLATION_MODE): select.select_schema( + LD6002BSelect, entity_category=ENTITY_CATEGORY_CONFIG + ), + } +) + + +SELECT_MAP = ( + ( + CONF_SENSITIVITY, + SelectType.SENSITIVITY, + "set_sensitivity_select", + ["low", "medium", "high"], + ), + ( + CONF_TRIGGER_SPEED, + SelectType.TRIGGER_SPEED, + "set_trigger_speed_select", + ["slow", "medium", "fast"], + ), + ( + CONF_INSTALLATION_MODE, + SelectType.INSTALLATION_MODE, + "set_installation_select", + ["top", "side"], + ), +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_LD6002B_ID]) + + for key, select_type, setter, options in SELECT_MAP: + if conf := config.get(key): + s = await select.new_select(conf, select_type, options=options) + await cg.register_parented(s, config[CONF_LD6002B_ID]) + cg.add(getattr(hub, setter)(s)) diff --git a/esphome/components/ld6002b/select/ld6002b_select.cpp b/esphome/components/ld6002b/select/ld6002b_select.cpp new file mode 100644 index 0000000000..a6b524a665 --- /dev/null +++ b/esphome/components/ld6002b/select/ld6002b_select.cpp @@ -0,0 +1,10 @@ +#include "ld6002b_select.h" + +namespace esphome::ld6002b { + +void LD6002BSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_select_value(this->type_, index); +} + +} // namespace esphome::ld6002b diff --git a/esphome/components/ld6002b/select/ld6002b_select.h b/esphome/components/ld6002b/select/ld6002b_select.h new file mode 100644 index 0000000000..f380089a1e --- /dev/null +++ b/esphome/components/ld6002b/select/ld6002b_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld6002b.h" + +namespace esphome::ld6002b { + +class LD6002BSelect : public select::Select, public Parented { + public: + explicit LD6002BSelect(SelectType type) : type_(type) {} + + protected: + void control(size_t index) override; + + SelectType type_; +}; + +} // namespace esphome::ld6002b diff --git a/tests/component_tests/ld6002b/__init__.py b/tests/component_tests/ld6002b/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/ld6002b/test_final_validate.py b/tests/component_tests/ld6002b/test_final_validate.py new file mode 100644 index 0000000000..49fa35eb13 --- /dev/null +++ b/tests/component_tests/ld6002b/test_final_validate.py @@ -0,0 +1,82 @@ +"""Tests for the wake button's wakeup_pin requirement in ld6002b.""" + +from __future__ import annotations + +import pytest + +from esphome.components.ld6002b.button import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA +from esphome.config import Config +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_WAKEUP_PIN, PlatformFramework +from esphome.core import ID +from esphome.types import ConfigType +from tests.component_tests.types import SetCoreConfigCallable + +HUB_ID = "ld6002b_hub" + + +def _full_config(hub: ConfigType) -> Config: + """A full config carrying one ld6002b hub, as the ID pass leaves it. + + final_validate resolves the hub through get_path_for_id, so the declaring + path has to be registered the way validate_config registers it: the path of + the id value itself, whose parent is the hub's own config. + """ + full = Config() + full["ld6002b"] = [hub] + full.declare_ids.append((hub[CONF_ID], ["ld6002b", 0, CONF_ID])) + return full + + +def _hub(*, wakeup_pin: bool) -> ConfigType: + hub: ConfigType = {CONF_ID: ID(HUB_ID, is_declaration=True, type="ld6002b")} + if wakeup_pin: + hub[CONF_WAKEUP_PIN] = {"number": 4} + return hub + + +def _buttons(**buttons: str) -> ConfigType: + """A button platform config naming the given buttons on the shared hub.""" + config: ConfigType = { + "ld6002b_id": ID(HUB_ID, is_declaration=False, type="ld6002b") + } + config.update({key: {"name": name} for key, name in buttons.items()}) + return config + + +def _validated(config: ConfigType) -> ConfigType: + """Run the button schema, then the final validation the hub is checked in.""" + config = CONFIG_SCHEMA(config) + FINAL_VALIDATE_SCHEMA(config) + return config + + +def test_wake_without_wakeup_pin_is_rejected( + set_core_config: SetCoreConfigCallable, +) -> None: + """The wake button drives the pin directly, so a hub without one cannot serve it.""" + set_core_config( + PlatformFramework.ESP32_IDF, full_config=_full_config(_hub(wakeup_pin=False)) + ) + + with pytest.raises(cv.Invalid, match="wake requires wakeup_pin"): + _validated(_buttons(wake="Wake")) + + +def test_wake_with_wakeup_pin_passes(set_core_config: SetCoreConfigCallable) -> None: + set_core_config( + PlatformFramework.ESP32_IDF, full_config=_full_config(_hub(wakeup_pin=True)) + ) + + _validated(_buttons(wake="Wake")) + + +def test_other_buttons_do_not_need_the_pin( + set_core_config: SetCoreConfigCallable, +) -> None: + """Only wake drives the pin; the query buttons stay usable without one.""" + set_core_config( + PlatformFramework.ESP32_IDF, full_config=_full_config(_hub(wakeup_pin=False)) + ) + + _validated(_buttons(get_delay="Get Delay")) diff --git a/tests/components/ld6002b/common.yaml b/tests/components/ld6002b/common.yaml index f8a9e95340..e31af49aec 100644 --- a/tests/components/ld6002b/common.yaml +++ b/tests/components/ld6002b/common.yaml @@ -71,6 +71,16 @@ number: low_power_sleep_time: name: Low Power Sleep +select: + - platform: ld6002b + ld6002b_id: ld6002b_radar + sensitivity: + name: Sensitivity + trigger_speed: + name: Trigger Speed + installation_mode: + name: Installation + switch: - platform: ld6002b ld6002b_id: ld6002b_radar @@ -80,3 +90,25 @@ switch: name: Point Cloud target_display: name: Target Display + +button: + - platform: ld6002b + ld6002b_id: ld6002b_radar + get_delay: + name: Get Delay + get_sensitivity: + name: Get Sensitivity + get_trigger_speed: + name: Get Trigger Speed + get_z_range: + name: Get Z Range + get_installation: + name: Get Installation + get_low_power_mode: + name: Get Low Power Mode + get_low_power_sleep_time: + name: Get Low Power Sleep + reset_unattended: + name: Reset Unattended + wake: + name: Wake