From 22cdb8dfc3f30902a0e9bff2bbb846c6d59d9332 Mon Sep 17 00:00:00 2001 From: Mike La Spina Date: Thu, 2 Nov 2023 18:02:23 -0500 Subject: [PATCH] Add HLK-LD2420 mmWave Radar module component (#4847) Co-authored-by: descipher <120155735+GelidusResearch@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/ld2420/__init__.py | 39 + .../ld2420/binary_sensor/__init__.py | 33 + .../binary_sensor/ld2420_binary_sensor.cpp | 16 + .../binary_sensor/ld2420_binary_sensor.h | 25 + esphome/components/ld2420/button/__init__.py | 69 ++ .../ld2420/button/reconfig_buttons.cpp | 16 + .../ld2420/button/reconfig_buttons.h | 42 + esphome/components/ld2420/ld2420.cpp | 775 ++++++++++++++++++ esphome/components/ld2420/ld2420.h | 272 ++++++ esphome/components/ld2420/number/__init__.py | 183 +++++ .../ld2420/number/gate_config_number.cpp | 73 ++ .../ld2420/number/gate_config_number.h | 78 ++ esphome/components/ld2420/select/__init__.py | 33 + .../ld2420/select/operating_mode_select.cpp | 16 + .../ld2420/select/operating_mode_select.h | 18 + esphome/components/ld2420/sensor/__init__.py | 35 + .../ld2420/sensor/ld2420_sensor.cpp | 16 + .../components/ld2420/sensor/ld2420_sensor.h | 34 + .../components/ld2420/text_sensor/__init__.py | 38 + .../ld2420/text_sensor/text_sensor.cpp | 16 + .../ld2420/text_sensor/text_sensor.h | 24 + tests/test1.yaml | 13 + 23 files changed, 1865 insertions(+) create mode 100644 esphome/components/ld2420/__init__.py create mode 100644 esphome/components/ld2420/binary_sensor/__init__.py create mode 100644 esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp create mode 100644 esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h create mode 100644 esphome/components/ld2420/button/__init__.py create mode 100644 esphome/components/ld2420/button/reconfig_buttons.cpp create mode 100644 esphome/components/ld2420/button/reconfig_buttons.h create mode 100644 esphome/components/ld2420/ld2420.cpp create mode 100644 esphome/components/ld2420/ld2420.h create mode 100644 esphome/components/ld2420/number/__init__.py create mode 100644 esphome/components/ld2420/number/gate_config_number.cpp create mode 100644 esphome/components/ld2420/number/gate_config_number.h create mode 100644 esphome/components/ld2420/select/__init__.py create mode 100644 esphome/components/ld2420/select/operating_mode_select.cpp create mode 100644 esphome/components/ld2420/select/operating_mode_select.h create mode 100644 esphome/components/ld2420/sensor/__init__.py create mode 100644 esphome/components/ld2420/sensor/ld2420_sensor.cpp create mode 100644 esphome/components/ld2420/sensor/ld2420_sensor.h create mode 100644 esphome/components/ld2420/text_sensor/__init__.py create mode 100644 esphome/components/ld2420/text_sensor/text_sensor.cpp create mode 100644 esphome/components/ld2420/text_sensor/text_sensor.h diff --git a/CODEOWNERS b/CODEOWNERS index 38bb21e0ac..bf5f6a233e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -152,6 +152,7 @@ esphome/components/key_provider/* @ssieb esphome/components/kuntze/* @ssieb esphome/components/lcd_menu/* @numo68 esphome/components/ld2410/* @regevbr @sebcaps +esphome/components/ld2420/* @descipher esphome/components/ledc/* @OttoWinter esphome/components/libretiny/* @kuba2k2 esphome/components/libretiny_pwm/* @kuba2k2 diff --git a/esphome/components/ld2420/__init__.py b/esphome/components/ld2420/__init__.py new file mode 100644 index 0000000000..c701423081 --- /dev/null +++ b/esphome/components/ld2420/__init__.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID + +CODEOWNERS = ["@descipher"] + +DEPENDENCIES = ["uart"] + +MULTI_CONF = True + +ld2420_ns = cg.esphome_ns.namespace("ld2420") +LD2420Component = ld2420_ns.class_("LD2420Component", cg.Component, uart.UARTDevice) + +CONF_LD2420_ID = "ld2420_id" + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(LD2420Component), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "ld2420_uart", + require_tx=True, + require_rx=True, + parity="NONE", + stop_bits=1, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/ld2420/binary_sensor/__init__.py b/esphome/components/ld2420/binary_sensor/__init__.py new file mode 100644 index 0000000000..f94e4d969f --- /dev/null +++ b/esphome/components/ld2420/binary_sensor/__init__.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_ID, DEVICE_CLASS_OCCUPANCY +from .. import ld2420_ns, LD2420Component, CONF_LD2420_ID + +LD2420BinarySensor = ld2420_ns.class_( + "LD2420BinarySensor", binary_sensor.BinarySensor, cg.Component +) + +CONF_HAS_TARGET = "has_target" + +CONFIG_SCHEMA = cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(LD2420BinarySensor), + cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component), + cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_OCCUPANCY + ), + } + ), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + if CONF_HAS_TARGET in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_HAS_TARGET]) + cg.add(var.set_presence_sensor(sens)) + ld2420 = await cg.get_variable(config[CONF_LD2420_ID]) + cg.add(ld2420.register_listener(var)) diff --git a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp new file mode 100644 index 0000000000..c6ea0a348b --- /dev/null +++ b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp @@ -0,0 +1,16 @@ +#include "ld2420_binary_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ld2420 { + +static const char *const TAG = "LD2420.binary_sensor"; + +void LD2420BinarySensor::dump_config() { + ESP_LOGCONFIG(TAG, "LD2420 BinarySensor:"); + LOG_BINARY_SENSOR(" ", "Presence", this->presence_bsensor_); +} + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h new file mode 100644 index 0000000000..ee06439090 --- /dev/null +++ b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h @@ -0,0 +1,25 @@ +#pragma once + +#include "../ld2420.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace ld2420 { + +class LD2420BinarySensor : public LD2420Listener, public Component, binary_sensor::BinarySensor { + public: + void dump_config() override; + void set_presence_sensor(binary_sensor::BinarySensor *bsensor) { this->presence_bsensor_ = bsensor; }; + void on_presence(bool presence) override { + if (this->presence_bsensor_ != nullptr) { + if (this->presence_bsensor_->state != presence) + this->presence_bsensor_->publish_state(presence); + } + } + + protected: + binary_sensor::BinarySensor *presence_bsensor_{nullptr}; +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/button/__init__.py b/esphome/components/ld2420/button/__init__.py new file mode 100644 index 0000000000..675e041dd4 --- /dev/null +++ b/esphome/components/ld2420/button/__init__.py @@ -0,0 +1,69 @@ +import esphome.codegen as cg +from esphome.components import button +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_RESTART, + ENTITY_CATEGORY_DIAGNOSTIC, + ENTITY_CATEGORY_CONFIG, + ICON_RESTART, + ICON_RESTART_ALERT, + ICON_DATABASE, +) +from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns + +LD2420ApplyConfigButton = ld2420_ns.class_("LD2420ApplyConfigButton", button.Button) +LD2420RevertConfigButton = ld2420_ns.class_("LD2420RevertConfigButton", button.Button) +LD2420RestartModuleButton = ld2420_ns.class_("LD2420RestartModuleButton", button.Button) +LD2420FactoryResetButton = ld2420_ns.class_("LD2420FactoryResetButton", button.Button) + +CONF_APPLY_CONFIG = "apply_config" +CONF_REVERT_CONFIG = "revert_config" +CONF_RESTART_MODULE = "restart_module" +CONF_FACTORY_RESET = "factory_reset" + + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component), + cv.Required(CONF_APPLY_CONFIG): button.button_schema( + LD2420ApplyConfigButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART_ALERT, + ), + cv.Optional(CONF_REVERT_CONFIG): button.button_schema( + LD2420RevertConfigButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART, + ), + cv.Optional(CONF_RESTART_MODULE): button.button_schema( + LD2420RestartModuleButton, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_DATABASE, + ), + cv.Optional(CONF_FACTORY_RESET): button.button_schema( + LD2420FactoryResetButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_DATABASE, + ), +} + + +async def to_code(config): + ld2420_component = await cg.get_variable(config[CONF_LD2420_ID]) + if apply_config := config.get(CONF_APPLY_CONFIG): + b = await button.new_button(apply_config) + await cg.register_parented(b, config[CONF_LD2420_ID]) + cg.add(ld2420_component.set_apply_config_button(b)) + if revert_config := config.get(CONF_REVERT_CONFIG): + b = await button.new_button(revert_config) + await cg.register_parented(b, config[CONF_LD2420_ID]) + cg.add(ld2420_component.set_revert_config_button(b)) + if restart_config := config.get(CONF_RESTART_MODULE): + b = await button.new_button(restart_config) + await cg.register_parented(b, config[CONF_LD2420_ID]) + cg.add(ld2420_component.set_restart_module_button(b)) + if factory_reset := config.get(CONF_FACTORY_RESET): + b = await button.new_button(factory_reset) + await cg.register_parented(b, config[CONF_LD2420_ID]) + cg.add(ld2420_component.set_factory_reset_button(b)) diff --git a/esphome/components/ld2420/button/reconfig_buttons.cpp b/esphome/components/ld2420/button/reconfig_buttons.cpp new file mode 100644 index 0000000000..3537c1d64a --- /dev/null +++ b/esphome/components/ld2420/button/reconfig_buttons.cpp @@ -0,0 +1,16 @@ +#include "reconfig_buttons.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +static const char *const TAG = "LD2420.button"; + +namespace esphome { +namespace ld2420 { + +void LD2420ApplyConfigButton::press_action() { this->parent_->apply_config_action(); } +void LD2420RevertConfigButton::press_action() { this->parent_->revert_config_action(); } +void LD2420RestartModuleButton::press_action() { this->parent_->restart_module_action(); } +void LD2420FactoryResetButton::press_action() { this->parent_->factory_reset_action(); } + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/button/reconfig_buttons.h b/esphome/components/ld2420/button/reconfig_buttons.h new file mode 100644 index 0000000000..4e9e7a3692 --- /dev/null +++ b/esphome/components/ld2420/button/reconfig_buttons.h @@ -0,0 +1,42 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2420.h" + +namespace esphome { +namespace ld2420 { + +class LD2420ApplyConfigButton : public button::Button, public Parented { + public: + LD2420ApplyConfigButton() = default; + + protected: + void press_action() override; +}; + +class LD2420RevertConfigButton : public button::Button, public Parented { + public: + LD2420RevertConfigButton() = default; + + protected: + void press_action() override; +}; + +class LD2420RestartModuleButton : public button::Button, public Parented { + public: + LD2420RestartModuleButton() = default; + + protected: + void press_action() override; +}; + +class LD2420FactoryResetButton : public button::Button, public Parented { + public: + LD2420FactoryResetButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp new file mode 100644 index 0000000000..6130617457 --- /dev/null +++ b/esphome/components/ld2420/ld2420.cpp @@ -0,0 +1,775 @@ +#include "ld2420.h" +#include "esphome/core/helpers.h" + +/* +Configure commands - little endian + +No command can exceed 64 bytes, otherwise they would need be to be split up into multiple sends. + +All send command frames will have: + Header = FD FC FB FA, Bytes 0 - 3, uint32_t 0xFAFBFCFD + Length, bytes 4 - 5, uint16_t 0x0002, must be at least 2 for the command byte if no addon data. + Command bytes 6 - 7, uint16_t + Footer = 04 03 02 01 - uint32_t 0x01020304, Always last 4 Bytes. +Receive + Error bytes 8-9 uint16_t, 0 = success, all other positive values = error + +Enable config mode: +Send: + UART Tx: FD FC FB FA 04 00 FF 00 02 00 04 03 02 01 + Command = FF 00 - uint16_t 0x00FF + Protocol version = 02 00, can be 1 or 2 - uint16_t 0x0002 +Reply: + UART Rx: FD FC FB FA 06 00 FF 01 00 00 02 00 04 03 02 01 + +Disable config mode: +Send: + UART Tx: FD FC FB FA 02 00 FE 00 04 03 02 01 + Command = FE 00 - uint16_t 0x00FE +Receive: + UART Rx: FD FC FB FA 04 00 FE 01 00 00 04 03 02 01 + +Configure system parameters: + +UART Tx: FD FC FB FA 08 00 12 00 00 00 64 00 00 00 04 03 02 01 Set system parms +Command = 12 00 - uint16_t 0x0012, Param +There are three documented parameters for modes: + 00 64 = Basic status mode + This mode outputs text as presence "ON" or "OFF" and "Range XXXX" + where XXXX is a decimal value for distance in cm + 00 04 = Energy output mode + This mode outputs detailed signal energy values for each gate and the target distance. + The data format consist of the following. + Header HH, Length LL, Persence PP, Distance DD, Range Gate GG, 16 Gate Energies EE, Footer FF + HH HH HH HH LL LL PP DD DD GG GG EE EE .. 16x .. FF FF FF FF + F4 F3 F2 F1 00 23 00 00 00 00 01 00 00 .. .. .. .. F8 F7 F6 F5 + 00 00 = debug output mode + This mode outputs detailed values consisting of 20 Dopplers, 16 Ranges for a total 20 * 16 * 4 bytes + The data format consist of the following. + Header HH, Doppler DD, Range RR, Footer FF + HH HH HH HH DD DD DD DD .. 20x .. RR RR RR RR .. 16x .. FF FF FF FF + AA BF 10 14 00 00 00 00 .. .. .. .. 00 00 00 00 .. .. .. .. FD FC FB FA + +Configure gate sensitivity parameters: +UART Tx: FD FC FB FA 0E 00 07 00 10 00 60 EA 00 00 20 00 60 EA 00 00 04 03 02 01 +Command = 12 00 - uint16_t 0x0007 +Gate 0 high thresh = 10 00 uint16_t 0x0010, Threshold value = 60 EA 00 00 uint32_t 0x0000EA60 +Gate 0 low thresh = 20 00 uint16_t 0x0020, Threshold value = 60 EA 00 00 uint32_t 0x0000EA60 +*/ + +namespace esphome { +namespace ld2420 { + +static const char *const TAG = "ld2420"; + +float LD2420Component::get_setup_priority() const { return setup_priority::BUS; } + +void LD2420Component::dump_config() { + ESP_LOGCONFIG(TAG, "LD2420:"); + ESP_LOGCONFIG(TAG, " Firmware Version : %7s", this->ld2420_firmware_ver_); + ESP_LOGCONFIG(TAG, "LD2420 Number:"); + LOG_NUMBER(TAG, " Gate Timeout:", this->gate_timeout_number_); + LOG_NUMBER(TAG, " Gate Max Distance:", this->max_gate_distance_number_); + LOG_NUMBER(TAG, " Gate Min Distance:", this->min_gate_distance_number_); + LOG_NUMBER(TAG, " Gate Select:", this->gate_select_number_); + for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + LOG_NUMBER(TAG, " Gate Move Threshold:", this->gate_move_threshold_numbers_[gate]); + LOG_NUMBER(TAG, " Gate Still Threshold::", this->gate_still_threshold_numbers_[gate]); + } + LOG_BUTTON(TAG, " Apply Config:", this->apply_config_button_); + LOG_BUTTON(TAG, " Revert Edits:", this->revert_config_button_); + LOG_BUTTON(TAG, " Factory Reset:", this->factory_reset_button_); + LOG_BUTTON(TAG, " Restart Module:", this->restart_module_button_); + ESP_LOGCONFIG(TAG, "LD2420 Select:"); + LOG_SELECT(TAG, " Operating Mode", this->operating_selector_); + if (this->get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { + ESP_LOGW(TAG, "LD2420 Firmware Version %s and older are only supported in Simple Mode", ld2420_firmware_ver_); + } +} + +uint8_t LD2420Component::calc_checksum(void *data, size_t size) { + uint8_t checksum = 0; + uint8_t *data_bytes = (uint8_t *) data; + for (size_t i = 0; i < size; i++) { + checksum ^= data_bytes[i]; // XOR operation + } + return checksum; +} + +int LD2420Component::get_firmware_int_(const char *version_string) { + std::string version_str = version_string; + if (version_str[0] == 'v') { + version_str = version_str.substr(1); + } + version_str.erase(remove(version_str.begin(), version_str.end(), '.'), version_str.end()); + int version_integer = stoi(version_str); + return version_integer; +} + +void LD2420Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up LD2420..."); + if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { + ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections."); + this->mark_failed(); + return; + } + this->get_min_max_distances_timeout_(); +#ifdef USE_NUMBER + this->init_gate_config_numbers(); +#endif + this->get_firmware_version_(); + const char *pfw = this->ld2420_firmware_ver_; + std::string fw_str(pfw); + + for (auto &listener : listeners_) { + listener->on_fw_version(fw_str); + } + + for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + delay_microseconds_safe(125); + this->get_gate_threshold_(gate); + } + + memcpy(&this->new_config, &this->current_config, sizeof(this->current_config)); + if (get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { + this->set_operating_mode(OP_SIMPLE_MODE_STRING); + this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING); + this->set_mode_(CMD_SYSTEM_MODE_SIMPLE); + ESP_LOGW(TAG, "LD2420 Frimware Version %s and older are only supported in Simple Mode", ld2420_firmware_ver_); + } else { + this->set_mode_(CMD_SYSTEM_MODE_ENERGY); + this->operating_selector_->publish_state(OP_NORMAL_MODE_STRING); + } +#ifdef USE_NUMBER + this->init_gate_config_numbers(); +#endif + this->set_system_mode(this->system_mode_); + this->set_config_mode(false); + ESP_LOGCONFIG(TAG, "LD2420 setup complete."); +} + +void LD2420Component::apply_config_action() { + const uint8_t checksum = calc_checksum(&this->new_config, sizeof(this->new_config)); + if (checksum == calc_checksum(&this->current_config, sizeof(this->current_config))) { + ESP_LOGCONFIG(TAG, "No configuration change detected"); + return; + } + ESP_LOGCONFIG(TAG, "Reconfiguring LD2420..."); + if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { + ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections."); + this->mark_failed(); + return; + } + this->set_min_max_distances_timeout(this->new_config.max_gate, this->new_config.min_gate, this->new_config.timeout); + for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + delay_microseconds_safe(125); + this->set_gate_threshold(gate); + } + memcpy(¤t_config, &new_config, sizeof(new_config)); +#ifdef USE_NUMBER + this->init_gate_config_numbers(); +#endif + this->set_system_mode(this->system_mode_); + this->set_config_mode(false); // Disable config mode to save new values in LD2420 nvm + this->set_operating_mode(OP_NORMAL_MODE_STRING); + ESP_LOGCONFIG(TAG, "LD2420 reconfig complete."); +} + +void LD2420Component::factory_reset_action() { + ESP_LOGCONFIG(TAG, "Setiing factory defaults..."); + if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { + ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections."); + this->mark_failed(); + return; + } + this->set_min_max_distances_timeout(FACTORY_MAX_GATE, FACTORY_MIN_GATE, FACTORY_TIMEOUT); + this->gate_timeout_number_->state = FACTORY_TIMEOUT; + this->min_gate_distance_number_->state = FACTORY_MIN_GATE; + this->max_gate_distance_number_->state = FACTORY_MAX_GATE; + for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + this->new_config.move_thresh[gate] = FACTORY_MOVE_THRESH[gate]; + this->new_config.still_thresh[gate] = FACTORY_STILL_THRESH[gate]; + delay_microseconds_safe(125); + this->set_gate_threshold(gate); + } + memcpy(&this->current_config, &this->new_config, sizeof(this->new_config)); + this->set_system_mode(this->system_mode_); + this->set_config_mode(false); +#ifdef USE_NUMBER + this->init_gate_config_numbers(); + this->refresh_gate_config_numbers(); +#endif + ESP_LOGCONFIG(TAG, "LD2420 factory reset complete."); +} + +void LD2420Component::restart_module_action() { + ESP_LOGCONFIG(TAG, "Restarting LD2420 module..."); + this->send_module_restart(); + delay_microseconds_safe(45000); + this->set_config_mode(true); + this->set_system_mode(system_mode_); + this->set_config_mode(false); + ESP_LOGCONFIG(TAG, "LD2420 Restarted."); +} + +void LD2420Component::revert_config_action() { + memcpy(&this->new_config, &this->current_config, sizeof(this->current_config)); +#ifdef USE_NUMBER + this->init_gate_config_numbers(); +#endif + ESP_LOGCONFIG(TAG, "Reverted config number edits."); +} + +void LD2420Component::loop() { + // If there is a active send command do not process it here, the send command call will handle it. + if (!get_cmd_active_()) { + if (!available()) + return; + static uint8_t buffer[2048]; + static uint8_t rx_data; + while (available()) { + rx_data = read(); + this->readline_(rx_data, buffer, sizeof(buffer)); + } + } +} + +void LD2420Component::update_radar_data(uint16_t const *gate_energy, uint8_t sample_number) { + for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; ++gate) { + this->radar_data[gate][sample_number] = gate_energy[gate]; + } + this->total_sample_number_counter++; +} + +void LD2420Component::auto_calibrate_sensitivity() { + // Calculate average and peak values for each gate + const float move_factor = gate_move_sensitivity_factor + 1; + const float still_factor = (gate_still_sensitivity_factor / 2) + 1; + for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; ++gate) { + uint32_t sum = 0; + uint16_t peak = 0; + + for (uint8_t sample_number = 0; sample_number < CALIBRATE_SAMPLES; ++sample_number) { + // Calculate average + sum += this->radar_data[gate][sample_number]; + + // Calculate max value + if (this->radar_data[gate][sample_number] > peak) { + peak = this->radar_data[gate][sample_number]; + } + } + + // Store average and peak values + this->gate_avg[gate] = sum / CALIBRATE_SAMPLES; + if (this->gate_peak[gate] < peak) + this->gate_peak[gate] = peak; + + uint32_t calculated_value = + (static_cast(this->gate_peak[gate]) + (move_factor * static_cast(this->gate_peak[gate]))); + this->new_config.move_thresh[gate] = static_cast(calculated_value <= 65535 ? calculated_value : 65535); + calculated_value = + (static_cast(this->gate_peak[gate]) + (still_factor * static_cast(this->gate_peak[gate]))); + this->new_config.still_thresh[gate] = static_cast(calculated_value <= 65535 ? calculated_value : 65535); + } +} + +void LD2420Component::report_gate_data() { + for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; ++gate) { + // Output results + ESP_LOGI(TAG, "Gate: %2d Avg: %5d Peak: %5d", gate, this->gate_avg[gate], this->gate_peak[gate]); + } + ESP_LOGI(TAG, "Total samples: %d", this->total_sample_number_counter); +} + +void LD2420Component::set_operating_mode(const std::string &state) { + // If unsupported firmware ignore mode select + if (get_firmware_int_(ld2420_firmware_ver_) >= CALIBRATE_VERSION_MIN) { + this->current_operating_mode = OP_MODE_TO_UINT.at(state); + // Entering Auto Calibrate we need to clear the privoiuos data collection + this->operating_selector_->publish_state(state); + if (current_operating_mode == OP_CALIBRATE_MODE) { + this->set_calibration_(true); + for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + this->gate_avg[gate] = 0; + this->gate_peak[gate] = 0; + for (uint8_t i = 0; i < CALIBRATE_SAMPLES; i++) { + this->radar_data[gate][i] = 0; + } + this->total_sample_number_counter = 0; + } + } else { + // Set the current data back so we don't have new data that can be applied in error. + if (this->get_calibration_()) + memcpy(&this->new_config, &this->current_config, sizeof(this->current_config)); + this->set_calibration_(false); + } + } else { + this->current_operating_mode = OP_SIMPLE_MODE; + this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING); + } +} + +void LD2420Component::readline_(int rx_data, uint8_t *buffer, int len) { + static int pos = 0; + + if (rx_data >= 0) { + if (pos < len - 1) { + buffer[pos++] = rx_data; + buffer[pos] = 0; + } else { + pos = 0; + } + if (pos >= 4) { + if (memcmp(&buffer[pos - 4], &CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)) == 0) { + this->set_cmd_active_(false); // Set command state to inactive after responce. + this->handle_ack_data_(buffer, pos); + pos = 0; + } else if ((buffer[pos - 2] == 0x0D && buffer[pos - 1] == 0x0A) && (get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) { + this->handle_simple_mode_(buffer, pos); + pos = 0; + } else if ((memcmp(&buffer[pos - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) && + (get_mode_() == CMD_SYSTEM_MODE_ENERGY)) { + this->handle_energy_mode_(buffer, pos); + pos = 0; + } + } + } +} + +void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) { + uint8_t index = 6; // Start at presence byte position + uint16_t range; + const uint8_t elements = sizeof(this->gate_energy_) / sizeof(this->gate_energy_[0]); + this->set_presence_(buffer[index]); + index++; + memcpy(&range, &buffer[index], sizeof(range)); + index += sizeof(range); + this->set_distance_(range); + for (uint8_t i = 0; i < elements; i++) { // NOLINT + memcpy(&this->gate_energy_[i], &buffer[index], sizeof(this->gate_energy_[0])); + index += sizeof(this->gate_energy_[0]); + } + + if (this->current_operating_mode == OP_CALIBRATE_MODE) { + this->update_radar_data(gate_energy_, sample_number_counter); + this->sample_number_counter > CALIBRATE_SAMPLES ? this->sample_number_counter = 0 : this->sample_number_counter++; + } + + // Resonable refresh rate for home assistant database size health + const int32_t current_millis = millis(); + if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS) + return; + this->last_periodic_millis = current_millis; + for (auto &listener : this->listeners_) { + listener->on_distance(get_distance_()); + listener->on_presence(get_presence_()); + listener->on_energy(this->gate_energy_, sizeof(this->gate_energy_) / sizeof(this->gate_energy_[0])); + } + + if (this->current_operating_mode == OP_CALIBRATE_MODE) { + this->auto_calibrate_sensitivity(); + if (current_millis - this->report_periodic_millis > REFRESH_RATE_MS * CALIBRATE_REPORT_INTERVAL) { + this->report_periodic_millis = current_millis; + this->report_gate_data(); + } + } +} + +void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) { + const uint8_t bufsize = 16; + uint8_t index{0}; + uint8_t pos{0}; + char *endptr{nullptr}; + char outbuf[bufsize]{0}; + while (true) { + if (inbuf[pos - 2] == 'O' && inbuf[pos - 1] == 'F' && inbuf[pos] == 'F') { + set_presence_(false); + } else if (inbuf[pos - 1] == 'O' && inbuf[pos] == 'N') { + set_presence_(true); + } + if (inbuf[pos] >= '0' && inbuf[pos] <= '9') { + if (index < bufsize - 1) { + outbuf[index++] = inbuf[pos]; + pos++; + } + } else { + if (pos < len - 1) { + pos++; + } else { + break; + } + } + } + outbuf[index] = '\0'; + if (index > 1) + set_distance_(strtol(outbuf, &endptr, 10)); + + if (get_mode_() == CMD_SYSTEM_MODE_SIMPLE) { + // Resonable refresh rate for home assistant database size health + const int32_t current_millis = millis(); + if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS) + return; + this->last_normal_periodic_millis = current_millis; + for (auto &listener : this->listeners_) + listener->on_distance(get_distance_()); + for (auto &listener : this->listeners_) + listener->on_presence(get_presence_()); + } +} + +void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { + this->cmd_reply_.command = buffer[CMD_FRAME_COMMAND]; + this->cmd_reply_.length = buffer[CMD_FRAME_DATA_LENGTH]; + uint8_t reg_element = 0; + uint8_t data_element = 0; + uint16_t data_pos = 0; + if (this->cmd_reply_.length > CMD_MAX_BYTES) { + ESP_LOGW(TAG, "LD2420 reply - received command reply frame is corrupt, length exceeds %d bytes.", CMD_MAX_BYTES); + return; + } else if (this->cmd_reply_.length < 2) { + ESP_LOGW(TAG, "LD2420 reply - received command frame is corrupt, length is less than 2 bytes."); + return; + } + memcpy(&this->cmd_reply_.error, &buffer[CMD_ERROR_WORD], sizeof(this->cmd_reply_.error)); + const char *result = this->cmd_reply_.error ? "failure" : "success"; + if (this->cmd_reply_.error > 0) { + return; + }; + this->cmd_reply_.ack = true; + switch ((uint16_t) this->cmd_reply_.command) { + case (CMD_ENABLE_CONF): + ESP_LOGD(TAG, "LD2420 reply - set config enable: CMD = %2X %s", CMD_ENABLE_CONF, result); + break; + case (CMD_DISABLE_CONF): + ESP_LOGD(TAG, "LD2420 reply - set config disable: CMD = %2X %s", CMD_DISABLE_CONF, result); + break; + case (CMD_READ_REGISTER): + ESP_LOGD(TAG, "LD2420 reply - read register: CMD = %2X %s", CMD_READ_REGISTER, result); + // TODO Read/Write register is not implemented yet, this will get flushed out to a proper header file + data_pos = 0x0A; + for (uint16_t index = 0; index < (CMD_REG_DATA_REPLY_SIZE * // NOLINT + ((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_REG_DATA_REPLY_SIZE)); + index += CMD_REG_DATA_REPLY_SIZE) { + memcpy(&this->cmd_reply_.data[reg_element], &buffer[data_pos + index], sizeof(CMD_REG_DATA_REPLY_SIZE)); + byteswap(this->cmd_reply_.data[reg_element]); + reg_element++; + } + break; + case (CMD_WRITE_REGISTER): + ESP_LOGD(TAG, "LD2420 reply - write register: CMD = %2X %s", CMD_WRITE_REGISTER, result); + break; + case (CMD_WRITE_ABD_PARAM): + ESP_LOGD(TAG, "LD2420 reply - write gate parameter(s): %2X %s", CMD_WRITE_ABD_PARAM, result); + break; + case (CMD_READ_ABD_PARAM): + ESP_LOGD(TAG, "LD2420 reply - read gate parameter(s): %2X %s", CMD_READ_ABD_PARAM, result); + data_pos = CMD_ABD_DATA_REPLY_START; + for (uint16_t index = 0; index < (CMD_ABD_DATA_REPLY_SIZE * // NOLINT + ((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_ABD_DATA_REPLY_SIZE)); + index += CMD_ABD_DATA_REPLY_SIZE) { + memcpy(&this->cmd_reply_.data[data_element], &buffer[data_pos + index], + sizeof(this->cmd_reply_.data[data_element])); + byteswap(this->cmd_reply_.data[data_element]); + data_element++; + } + break; + case (CMD_WRITE_SYS_PARAM): + ESP_LOGD(TAG, "LD2420 reply - set system parameter(s): %2X %s", CMD_WRITE_SYS_PARAM, result); + break; + case (CMD_READ_VERSION): + memcpy(this->ld2420_firmware_ver_, &buffer[12], buffer[10]); + ESP_LOGD(TAG, "LD2420 reply - module firmware version: %7s %s", this->ld2420_firmware_ver_, result); + break; + default: + break; + } +} + +int LD2420Component::send_cmd_from_array(CmdFrameT frame) { + uint8_t error = 0; + uint8_t ack_buffer[64]; + uint8_t cmd_buffer[64]; + uint16_t loop_count; + this->cmd_reply_.ack = false; + if (frame.command != CMD_RESTART) + this->set_cmd_active_(true); // Restart does not reply, thus no ack state required. + uint8_t retry = 3; + while (retry) { + // TODO setup a dynamic method e.g. millis time count etc. to tune for non ESP32 240Mhz devices + // this is ok for now since the module firmware is changing like the weather atm + frame.length = 0; + loop_count = 1250; + uint16_t frame_data_bytes = frame.data_length + 2; // Always add two bytes for the cmd size + + memcpy(&cmd_buffer[frame.length], &frame.header, sizeof(frame.header)); + frame.length += sizeof(frame.header); + + memcpy(&cmd_buffer[frame.length], &frame_data_bytes, sizeof(frame.data_length)); + frame.length += sizeof(frame.data_length); + + memcpy(&cmd_buffer[frame.length], &frame.command, sizeof(frame.command)); + frame.length += sizeof(frame.command); + + for (uint16_t index = 0; index < frame.data_length; index++) { + memcpy(&cmd_buffer[frame.length], &frame.data[index], sizeof(frame.data[index])); + frame.length += sizeof(frame.data[index]); + } + + memcpy(cmd_buffer + frame.length, &frame.footer, sizeof(frame.footer)); + frame.length += sizeof(frame.footer); + for (uint16_t index = 0; index < frame.length; index++) { + this->write_byte(cmd_buffer[index]); + } + + delay_microseconds_safe(500); // give the module a moment to process it + error = 0; + if (frame.command == CMD_RESTART) { + delay_microseconds_safe(25000); // Wait for the restart + return 0; // restart does not reply exit now + } + + while (!this->cmd_reply_.ack) { + while (available()) { + this->readline_(read(), ack_buffer, sizeof(ack_buffer)); + } + delay_microseconds_safe(250); + if (loop_count <= 0) { + error = LD2420_ERROR_TIMEOUT; + retry--; + break; + } + loop_count--; + } + if (this->cmd_reply_.ack) + retry = 0; + if (this->cmd_reply_.error > 0) + handle_cmd_error(error); + } + return error; +} + +uint8_t LD2420Component::set_config_mode(bool enable) { + CmdFrameT cmd_frame; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; + if (enable) { + memcpy(&cmd_frame.data[0], &CMD_PROTOCOL_VER, sizeof(CMD_PROTOCOL_VER)); + cmd_frame.data_length += sizeof(CMD_PROTOCOL_VER); + } + cmd_frame.footer = CMD_FRAME_FOOTER; + ESP_LOGD(TAG, "Sending set config %s command: %2X", enable ? "enable" : "disable", cmd_frame.command); + return this->send_cmd_from_array(cmd_frame); +} + +// Sends a restart and set system running mode to normal +void LD2420Component::send_module_restart() { this->ld2420_restart(); } + +void LD2420Component::ld2420_restart() { + CmdFrameT cmd_frame; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = CMD_RESTART; + cmd_frame.footer = CMD_FRAME_FOOTER; + ESP_LOGD(TAG, "Sending restart command: %2X", cmd_frame.command); + this->send_cmd_from_array(cmd_frame); +} + +void LD2420Component::get_reg_value_(uint16_t reg) { + CmdFrameT cmd_frame; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = CMD_READ_REGISTER; + cmd_frame.data[1] = reg; + cmd_frame.data_length += 2; + cmd_frame.footer = CMD_FRAME_FOOTER; + ESP_LOGD(TAG, "Sending read register %4X command: %2X", reg, cmd_frame.command); + this->send_cmd_from_array(cmd_frame); +} + +void LD2420Component::set_reg_value(uint16_t reg, uint16_t value) { + CmdFrameT cmd_frame; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = CMD_WRITE_REGISTER; + memcpy(&cmd_frame.data[cmd_frame.data_length], ®, sizeof(CMD_REG_DATA_REPLY_SIZE)); + cmd_frame.data_length += 2; + memcpy(&cmd_frame.data[cmd_frame.data_length], &value, sizeof(CMD_REG_DATA_REPLY_SIZE)); + cmd_frame.data_length += 2; + cmd_frame.footer = CMD_FRAME_FOOTER; + ESP_LOGD(TAG, "Sending write register %4X command: %2X data = %4X", reg, cmd_frame.command, value); + this->send_cmd_from_array(cmd_frame); +} + +void LD2420Component::handle_cmd_error(uint8_t error) { ESP_LOGI(TAG, "Command failed: %s", ERR_MESSAGE[error]); } + +int LD2420Component::get_gate_threshold_(uint8_t gate) { + uint8_t error; + CmdFrameT cmd_frame; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = CMD_READ_ABD_PARAM; + memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_GATE_MOVE_THRESH[gate], sizeof(CMD_GATE_MOVE_THRESH[gate])); + cmd_frame.data_length += 2; + memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_GATE_STILL_THRESH[gate], sizeof(CMD_GATE_STILL_THRESH[gate])); + cmd_frame.data_length += 2; + cmd_frame.footer = CMD_FRAME_FOOTER; + ESP_LOGD(TAG, "Sending read gate %d high/low theshold command: %2X", gate, cmd_frame.command); + error = this->send_cmd_from_array(cmd_frame); + if (error == 0) { + this->current_config.move_thresh[gate] = cmd_reply_.data[0]; + this->current_config.still_thresh[gate] = cmd_reply_.data[1]; + } + return error; +} + +int LD2420Component::get_min_max_distances_timeout_() { + uint8_t error; + CmdFrameT cmd_frame; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = CMD_READ_ABD_PARAM; + memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MIN_GATE_REG, + sizeof(CMD_MIN_GATE_REG)); // Register: global min detect gate number + cmd_frame.data_length += sizeof(CMD_MIN_GATE_REG); + memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MAX_GATE_REG, + sizeof(CMD_MAX_GATE_REG)); // Register: global max detect gate number + cmd_frame.data_length += sizeof(CMD_MAX_GATE_REG); + memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_TIMEOUT_REG, + sizeof(CMD_TIMEOUT_REG)); // Register: global delay time + cmd_frame.data_length += sizeof(CMD_TIMEOUT_REG); + cmd_frame.footer = CMD_FRAME_FOOTER; + ESP_LOGD(TAG, "Sending read gate min max and timeout command: %2X", cmd_frame.command); + error = this->send_cmd_from_array(cmd_frame); + if (error == 0) { + this->current_config.min_gate = (uint16_t) cmd_reply_.data[0]; + this->current_config.max_gate = (uint16_t) cmd_reply_.data[1]; + this->current_config.timeout = (uint16_t) cmd_reply_.data[2]; + } + return error; +} + +void LD2420Component::set_system_mode(uint16_t mode) { + CmdFrameT cmd_frame; + uint16_t unknown_parm = 0x0000; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = CMD_WRITE_SYS_PARAM; + memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_SYSTEM_MODE, sizeof(CMD_SYSTEM_MODE)); + cmd_frame.data_length += sizeof(CMD_SYSTEM_MODE); + memcpy(&cmd_frame.data[cmd_frame.data_length], &mode, sizeof(mode)); + cmd_frame.data_length += sizeof(mode); + memcpy(&cmd_frame.data[cmd_frame.data_length], &unknown_parm, sizeof(unknown_parm)); + cmd_frame.data_length += sizeof(unknown_parm); + cmd_frame.footer = CMD_FRAME_FOOTER; + ESP_LOGD(TAG, "Sending write system mode command: %2X", cmd_frame.command); + if (this->send_cmd_from_array(cmd_frame) == 0) + set_mode_(mode); +} + +void LD2420Component::get_firmware_version_() { + CmdFrameT cmd_frame; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = CMD_READ_VERSION; + cmd_frame.footer = CMD_FRAME_FOOTER; + + ESP_LOGD(TAG, "Sending read firmware version command: %2X", cmd_frame.command); + this->send_cmd_from_array(cmd_frame); +} + +void LD2420Component::set_min_max_distances_timeout(uint32_t max_gate_distance, uint32_t min_gate_distance, // NOLINT + uint32_t timeout) { + // Header H, Length L, Register R, Value V, Footer F + // |Min Gate |Max Gate |Timeout | + // HH HH HH HH LL LL CC CC RR RR VV VV VV VV RR RR VV VV VV VV RR RR VV VV VV VV FF FF FF FF + // FD FC FB FA 14 00 07 00 00 00 01 00 00 00 01 00 09 00 00 00 04 00 0A 00 00 00 04 03 02 01 e.g. + + CmdFrameT cmd_frame; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = CMD_WRITE_ABD_PARAM; + memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MIN_GATE_REG, + sizeof(CMD_MIN_GATE_REG)); // Register: global min detect gate number + cmd_frame.data_length += sizeof(CMD_MIN_GATE_REG); + memcpy(&cmd_frame.data[cmd_frame.data_length], &min_gate_distance, sizeof(min_gate_distance)); + cmd_frame.data_length += sizeof(min_gate_distance); + memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MAX_GATE_REG, + sizeof(CMD_MAX_GATE_REG)); // Register: global max detect gate number + cmd_frame.data_length += sizeof(CMD_MAX_GATE_REG); + memcpy(&cmd_frame.data[cmd_frame.data_length], &max_gate_distance, sizeof(max_gate_distance)); + cmd_frame.data_length += sizeof(max_gate_distance); + memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_TIMEOUT_REG, + sizeof(CMD_TIMEOUT_REG)); // Register: global delay time + cmd_frame.data_length += sizeof(CMD_TIMEOUT_REG); + memcpy(&cmd_frame.data[cmd_frame.data_length], &timeout, sizeof(timeout)); + ; + cmd_frame.data_length += sizeof(timeout); + cmd_frame.footer = CMD_FRAME_FOOTER; + + ESP_LOGD(TAG, "Sending write gate min max and timeout command: %2X", cmd_frame.command); + this->send_cmd_from_array(cmd_frame); +} + +void LD2420Component::set_gate_threshold(uint8_t gate) { + // Header H, Length L, Command C, Register R, Value V, Footer F + // HH HH HH HH LL LL CC CC RR RR VV VV VV VV RR RR VV VV VV VV FF FF FF FF + // FD FC FB FA 14 00 07 00 10 00 00 FF 00 00 00 01 00 0F 00 00 04 03 02 01 + + uint16_t move_threshold_gate = CMD_GATE_MOVE_THRESH[gate]; + uint16_t still_threshold_gate = CMD_GATE_STILL_THRESH[gate]; + CmdFrameT cmd_frame; + cmd_frame.data_length = 0; + cmd_frame.header = CMD_FRAME_HEADER; + cmd_frame.command = CMD_WRITE_ABD_PARAM; + memcpy(&cmd_frame.data[cmd_frame.data_length], &move_threshold_gate, sizeof(move_threshold_gate)); + cmd_frame.data_length += sizeof(move_threshold_gate); + memcpy(&cmd_frame.data[cmd_frame.data_length], &this->new_config.move_thresh[gate], + sizeof(this->new_config.move_thresh[gate])); + cmd_frame.data_length += sizeof(this->new_config.move_thresh[gate]); + memcpy(&cmd_frame.data[cmd_frame.data_length], &still_threshold_gate, sizeof(still_threshold_gate)); + cmd_frame.data_length += sizeof(still_threshold_gate); + memcpy(&cmd_frame.data[cmd_frame.data_length], &this->new_config.still_thresh[gate], + sizeof(this->new_config.still_thresh[gate])); + cmd_frame.data_length += sizeof(this->new_config.still_thresh[gate]); + cmd_frame.footer = CMD_FRAME_FOOTER; + ESP_LOGD(TAG, "Sending set gate %4X sensitivity command: %2X", gate, cmd_frame.command); + this->send_cmd_from_array(cmd_frame); +} + +#ifdef USE_NUMBER +void LD2420Component::init_gate_config_numbers() { + if (this->gate_timeout_number_ != nullptr) + this->gate_timeout_number_->publish_state(static_cast(this->current_config.timeout)); + if (this->gate_select_number_ != nullptr) + this->gate_select_number_->publish_state(0); + if (this->min_gate_distance_number_ != nullptr) + this->min_gate_distance_number_->publish_state(static_cast(this->current_config.min_gate)); + if (this->max_gate_distance_number_ != nullptr) + this->max_gate_distance_number_->publish_state(static_cast(this->current_config.max_gate)); + if (this->gate_move_sensitivity_factor_number_ != nullptr) + this->gate_move_sensitivity_factor_number_->publish_state(this->gate_move_sensitivity_factor); + if (this->gate_still_sensitivity_factor_number_ != nullptr) + this->gate_still_sensitivity_factor_number_->publish_state(this->gate_still_sensitivity_factor); + for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + if (this->gate_still_threshold_numbers_[gate] != nullptr) { + this->gate_still_threshold_numbers_[gate]->publish_state( + static_cast(this->current_config.still_thresh[gate])); + } + if (this->gate_move_threshold_numbers_[gate] != nullptr) { + this->gate_move_threshold_numbers_[gate]->publish_state( + static_cast(this->current_config.move_thresh[gate])); + } + } +} + +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); + this->max_gate_distance_number_->publish_state(this->new_config.max_gate); +} + +#endif + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h new file mode 100644 index 0000000000..2780503776 --- /dev/null +++ b/esphome/components/ld2420/ld2420.h @@ -0,0 +1,272 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_BUTTON +#include "esphome/components/button/button.h" +#endif +#include +#include + +namespace esphome { +namespace ld2420 { + +// Local const's +static const uint16_t REFRESH_RATE_MS = 1000; + +// Command sets +static const uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; +static const uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; +static const uint16_t CMD_DISABLE_CONF = 0x00FE; +static const uint16_t CMD_ENABLE_CONF = 0x00FF; +static const uint8_t CMD_MAX_BYTES = 0x64; +static const uint16_t CMD_PARM_HIGH_TRESH = 0x0012; +static const uint16_t CMD_PARM_LOW_TRESH = 0x0021; +static const uint16_t CMD_PROTOCOL_VER = 0x0002; +static const uint16_t CMD_READ_ABD_PARAM = 0x0008; +static const uint16_t CMD_READ_REG_ADDR = 0x0020; +static const uint16_t CMD_READ_REGISTER = 0x0002; +static const uint16_t CMD_READ_SERIAL_NUM = 0x0011; +static const uint16_t CMD_READ_SYS_PARAM = 0x0013; +static const uint16_t CMD_READ_VERSION = 0x0000; +static const uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; +static const uint16_t CMD_RESTART = 0x0068; +static const uint16_t CMD_SYSTEM_MODE = 0x0000; +static const uint16_t CMD_SYSTEM_MODE_GR = 0x0003; +static const uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; +static const uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; +static const uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; +static const uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; +static const uint16_t CMD_SYSTEM_MODE_VS = 0x0002; +static const uint16_t CMD_WRITE_ABD_PARAM = 0x0007; +static const uint16_t CMD_WRITE_REGISTER = 0x0001; +static const uint16_t CMD_WRITE_SYS_PARAM = 0x0012; + +static const uint8_t LD2420_ERROR_NONE = 0x00; +static const uint8_t LD2420_ERROR_TIMEOUT = 0x02; +static const uint8_t LD2420_ERROR_UNKNOWN = 0x01; +static const uint8_t LD2420_TOTAL_GATES = 16; +static const uint8_t CALIBRATE_SAMPLES = 64; + +// Register address values +static const uint16_t CMD_MIN_GATE_REG = 0x0000; +static const uint16_t CMD_MAX_GATE_REG = 0x0001; +static const uint16_t CMD_TIMEOUT_REG = 0x0004; +static const uint16_t CMD_GATE_MOVE_THRESH[LD2420_TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, + 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, + 0x001C, 0x001D, 0x001E, 0x001F}; +static const uint16_t CMD_GATE_STILL_THRESH[LD2420_TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, + 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, + 0x002C, 0x002D, 0x002E, 0x002F}; +static const uint32_t FACTORY_MOVE_THRESH[LD2420_TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, + 250, 250, 250, 250, 250, 250, 250, 250}; +static const uint32_t FACTORY_STILL_THRESH[LD2420_TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, + 150, 100, 100, 100, 100, 100, 100, 100}; +static const uint16_t FACTORY_TIMEOUT = 120; +static const uint16_t FACTORY_MIN_GATE = 1; +static const uint16_t FACTORY_MAX_GATE = 12; + +// COMMAND_BYTE Header & Footer +static const uint8_t CMD_FRAME_COMMAND = 6; +static const uint8_t CMD_FRAME_DATA_LENGTH = 4; +static const uint32_t CMD_FRAME_FOOTER = 0x01020304; +static const uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; +static const uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; +static const uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; +static const uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; +static const uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; +static const uint8_t CMD_FRAME_STATUS = 7; +static const uint8_t CMD_ERROR_WORD = 8; +static const uint8_t ENERGY_SENSOR_START = 9; +static const uint8_t CALIBRATE_REPORT_INTERVAL = 4; +static const int CALIBRATE_VERSION_MIN = 154; +static const std::string OP_NORMAL_MODE_STRING = "Normal"; +static const std::string OP_SIMPLE_MODE_STRING = "Simple"; + +enum OpModeStruct : uint8_t { OP_NORMAL_MODE = 1, OP_CALIBRATE_MODE = 2, OP_SIMPLE_MODE = 3 }; +static const std::map OP_MODE_TO_UINT{ + {"Normal", OP_NORMAL_MODE}, {"Calibrate", OP_CALIBRATE_MODE}, {"Simple", OP_SIMPLE_MODE}}; +static constexpr const char *ERR_MESSAGE[] = {"None", "Unknown", "Timeout"}; + +class LD2420Listener { + public: + virtual void on_presence(bool presence){}; + virtual void on_distance(uint16_t distance){}; + virtual void on_energy(uint16_t *sensor_energy, size_t size){}; + virtual void on_fw_version(std::string &fw){}; +}; + +class LD2420Component : public Component, public uart::UARTDevice { + public: + void setup() override; + void dump_config() override; + void loop() override; +#ifdef USE_SELECT + void set_operating_mode_select(select::Select *selector) { this->operating_selector_ = selector; }; +#endif +#ifdef USE_NUMBER + void set_gate_timeout_number(number::Number *number) { this->gate_timeout_number_ = number; }; + void set_gate_select_number(number::Number *number) { this->gate_select_number_ = number; }; + void set_min_gate_distance_number(number::Number *number) { this->min_gate_distance_number_ = number; }; + void set_max_gate_distance_number(number::Number *number) { this->max_gate_distance_number_ = number; }; + void set_gate_move_sensitivity_factor_number(number::Number *number) { + this->gate_move_sensitivity_factor_number_ = number; + }; + void set_gate_still_sensitivity_factor_number(number::Number *number) { + this->gate_still_sensitivity_factor_number_ = number; + }; + void set_gate_still_threshold_numbers(int gate, number::Number *n) { this->gate_still_threshold_numbers_[gate] = n; }; + void set_gate_move_threshold_numbers(int gate, number::Number *n) { this->gate_move_threshold_numbers_[gate] = n; }; + bool is_gate_select() { return gate_select_number_ != nullptr; }; + uint8_t get_gate_select_value() { return static_cast(this->gate_select_number_->state); }; + float get_min_gate_distance_value() { return min_gate_distance_number_->state; }; + float get_max_gate_distance_value() { return max_gate_distance_number_->state; }; + void publish_gate_move_threshold(uint8_t gate) { + // With gate_select we only use 1 number pointer, thus we hard code [0] + this->gate_move_threshold_numbers_[0]->publish_state(this->new_config.move_thresh[gate]); + }; + void publish_gate_still_threshold(uint8_t gate) { + this->gate_still_threshold_numbers_[0]->publish_state(this->new_config.still_thresh[gate]); + }; + void init_gate_config_numbers(); + void refresh_gate_config_numbers(); +#endif +#ifdef USE_BUTTON + void set_apply_config_button(button::Button *button) { this->apply_config_button_ = button; }; + void set_revert_config_button(button::Button *button) { this->revert_config_button_ = button; }; + void set_restart_module_button(button::Button *button) { this->restart_module_button_ = button; }; + void set_factory_reset_button(button::Button *button) { this->factory_reset_button_ = button; }; +#endif + void register_listener(LD2420Listener *listener) { this->listeners_.push_back(listener); } + + struct CmdFrameT { + uint32_t header{0}; + uint16_t length{0}; + uint16_t command{0}; + uint8_t data[18]; + uint16_t data_length{0}; + uint32_t footer{0}; + }; + + struct RegConfigT { + uint16_t min_gate{0}; + uint16_t max_gate{0}; + uint16_t timeout{0}; + uint32_t move_thresh[LD2420_TOTAL_GATES]; + uint32_t still_thresh[LD2420_TOTAL_GATES]; + }; + + void send_module_restart(); + void restart_module_action(); + void apply_config_action(); + void factory_reset_action(); + void revert_config_action(); + float get_setup_priority() const override; + int send_cmd_from_array(CmdFrameT cmd_frame); + void report_gate_data(); + void handle_cmd_error(uint8_t error); + void set_operating_mode(const std::string &state); + void auto_calibrate_sensitivity(); + void update_radar_data(uint16_t const *gate_energy, uint8_t sample_number); + uint8_t calc_checksum(void *data, size_t size); + + RegConfigT current_config; + RegConfigT new_config; + int32_t last_periodic_millis = millis(); + int32_t report_periodic_millis = millis(); + int32_t monitor_periodic_millis = millis(); + int32_t last_normal_periodic_millis = millis(); + bool output_energy_state{false}; + uint8_t current_operating_mode{OP_NORMAL_MODE}; + uint16_t radar_data[LD2420_TOTAL_GATES][CALIBRATE_SAMPLES]; + uint16_t gate_avg[LD2420_TOTAL_GATES]; + uint16_t gate_peak[LD2420_TOTAL_GATES]; + uint8_t sample_number_counter{0}; + uint16_t total_sample_number_counter{0}; + float gate_move_sensitivity_factor{0.5}; + float gate_still_sensitivity_factor{0.5}; +#ifdef USE_SELECT + select::Select *operating_selector_{nullptr}; +#endif +#ifdef USE_BUTTON + button::Button *apply_config_button_{nullptr}; + button::Button *revert_config_button_{nullptr}; + button::Button *restart_module_button_{nullptr}; + button::Button *factory_reset_button_{nullptr}; +#endif + void set_min_max_distances_timeout(uint32_t max_gate_distance, uint32_t min_gate_distance, uint32_t timeout); + void set_gate_threshold(uint8_t gate); + void set_reg_value(uint16_t reg, uint16_t value); + uint8_t set_config_mode(bool enable); + void set_system_mode(uint16_t mode); + void ld2420_restart(); + + protected: + struct CmdReplyT { + uint8_t command; + uint8_t status; + uint32_t data[4]; + uint8_t length; + uint16_t error; + volatile bool ack; + }; + + int get_firmware_int_(const char *version_string); + void get_firmware_version_(); + int get_gate_threshold_(uint8_t gate); + void get_reg_value_(uint16_t reg); + int get_min_max_distances_timeout_(); + uint16_t get_mode_() { return this->system_mode_; }; + void set_mode_(uint16_t mode) { this->system_mode_ = mode; }; + bool get_presence_() { return this->presence_; }; + void set_presence_(bool presence) { this->presence_ = presence; }; + uint16_t get_distance_() { return this->distance_; }; + void set_distance_(uint16_t distance) { this->distance_ = distance; }; + bool get_cmd_active_() { return this->cmd_active_; }; + void set_cmd_active_(bool active) { this->cmd_active_ = active; }; + void handle_simple_mode_(const uint8_t *inbuf, int len); + void handle_energy_mode_(uint8_t *buffer, int len); + void handle_ack_data_(uint8_t *buffer, int len); + void readline_(int rx_data, uint8_t *buffer, int len); + void set_calibration_(bool state) { this->calibration_ = state; }; + bool get_calibration_() { return this->calibration_; }; + +#ifdef USE_NUMBER + number::Number *gate_timeout_number_{nullptr}; + number::Number *gate_select_number_{nullptr}; + number::Number *min_gate_distance_number_{nullptr}; + number::Number *max_gate_distance_number_{nullptr}; + number::Number *gate_move_sensitivity_factor_number_{nullptr}; + number::Number *gate_still_sensitivity_factor_number_{nullptr}; + std::vector gate_still_threshold_numbers_ = std::vector(16); + std::vector gate_move_threshold_numbers_ = std::vector(16); +#endif + + uint16_t gate_energy_[LD2420_TOTAL_GATES]; + CmdReplyT cmd_reply_; + uint32_t timeout_; + uint32_t max_distance_gate_; + uint32_t min_distance_gate_; + uint16_t system_mode_{CMD_SYSTEM_MODE_ENERGY}; + bool cmd_active_{false}; + char ld2420_firmware_ver_[8]; + bool presence_{false}; + bool calibration_{false}; + uint16_t distance_{0}; + uint8_t config_checksum_{0}; + std::vector listeners_{}; +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/number/__init__.py b/esphome/components/ld2420/number/__init__.py new file mode 100644 index 0000000000..4ae08356fc --- /dev/null +++ b/esphome/components/ld2420/number/__init__.py @@ -0,0 +1,183 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_DISTANCE, + UNIT_SECOND, + ENTITY_CATEGORY_CONFIG, + ICON_MOTION_SENSOR, + ICON_TIMELAPSE, + ICON_SCALE, +) +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_STILL_THRESHOLD = "still_threshold" +CONF_MOVE_THRESHOLD = "move_threshold" +CONF_GATE_MOVE_SENSITIVITY = "gate_move_sensitivity" +CONF_GATE_STILL_SENSITIVITY = "gate_still_sensitivity" +CONF_GATE_SELECT = "gate_select" +CONF_PRESENCE_TIMEOUT = "presence_timeout" +GATE_GROUP = "gate_group" +TIMEOUT_GROUP = "timeout_group" + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component), + cv.Inclusive(CONF_PRESENCE_TIMEOUT, TIMEOUT_GROUP): number.number_schema( + LD2420TimeoutNumber, + unit_of_measurement=UNIT_SECOND, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_TIMELAPSE, + ), + cv.Inclusive(CONF_MIN_GATE_DISTANCE, TIMEOUT_GROUP): number.number_schema( + LD2420MinDistanceNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Inclusive(CONF_MAX_GATE_DISTANCE, TIMEOUT_GROUP): number.number_schema( + LD2420MaxDistanceNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Inclusive(CONF_GATE_SELECT, GATE_GROUP): number.number_schema( + LD2420GateSelectNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Inclusive(CONF_STILL_THRESHOLD, GATE_GROUP): number.number_schema( + LD2420StillThresholdNumbers, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Inclusive(CONF_MOVE_THRESHOLD, GATE_GROUP): number.number_schema( + LD2420MoveThresholdNumbers, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_GATE_MOVE_SENSITIVITY): number.number_schema( + LD2420MoveSensFactorNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_SCALE, + ), + cv.Optional(CONF_GATE_STILL_SENSITIVITY): number.number_schema( + LD2420StillSensFactorNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_SCALE, + ), + } +) +CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + { + cv.Optional(f"gate_{x}"): ( + { + cv.Required(CONF_MOVE_THRESHOLD): number.number_schema( + LD2420MoveThresholdNumbers, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Required(CONF_STILL_THRESHOLD): number.number_schema( + LD2420StillThresholdNumbers, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + } + ) + for x in range(16) + } +) + + +async def to_code(config): + LD2420_component = await cg.get_variable(config[CONF_LD2420_ID]) + if gate_timeout_config := config.get(CONF_PRESENCE_TIMEOUT): + n = await number.new_number( + gate_timeout_config, min_value=0, max_value=255, step=5 + ) + await cg.register_parented(n, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_gate_timeout_number(n)) + if min_distance_gate_config := config.get(CONF_MIN_GATE_DISTANCE): + n = await number.new_number( + min_distance_gate_config, min_value=0, max_value=15, step=1 + ) + await cg.register_parented(n, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_min_gate_distance_number(n)) + if max_distance_gate_config := config.get(CONF_MAX_GATE_DISTANCE): + n = await number.new_number( + max_distance_gate_config, min_value=1, max_value=15, step=1 + ) + await cg.register_parented(n, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_max_gate_distance_number(n)) + if gate_move_sensitivity_config := config.get(CONF_GATE_MOVE_SENSITIVITY): + n = await number.new_number( + gate_move_sensitivity_config, min_value=0.05, max_value=1, step=0.025 + ) + await cg.register_parented(n, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_gate_move_sensitivity_factor_number(n)) + if gate_still_sensitivity_config := config.get(CONF_GATE_STILL_SENSITIVITY): + n = await number.new_number( + gate_still_sensitivity_config, min_value=0.05, max_value=1, step=0.025 + ) + await cg.register_parented(n, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_gate_still_sensitivity_factor_number(n)) + if config.get(CONF_GATE_SELECT): + if gate_number := config.get(CONF_GATE_SELECT): + n = await number.new_number(gate_number, min_value=0, max_value=15, step=1) + await cg.register_parented(n, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_gate_select_number(n)) + 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, config[CONF_LD2420_ID]) + cg.add(LD2420_component.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, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_gate_move_threshold_numbers(0, n)) + 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 cg.register_parented(n, config[CONF_LD2420_ID]) + cg.add(LD2420_component.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 cg.register_parented(n, config[CONF_LD2420_ID]) + cg.add(LD2420_component.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 new file mode 100644 index 0000000000..e5eaafb46d --- /dev/null +++ b/esphome/components/ld2420/number/gate_config_number.cpp @@ -0,0 +1,73 @@ +#include "gate_config_number.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +static const char *const TAG = "LD2420.number"; + +namespace esphome { +namespace 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(); + } else { + this->parent_->new_config.min_gate = (uint16_t) min_gate; + } + this->publish_state(min_gate); +} + +void LD2420MaxDistanceNumber::control(float max_gate) { + if ((uint16_t) max_gate < this->parent_->new_config.min_gate) { + max_gate = this->parent_->get_max_gate_distance_value(); + } else { + this->parent_->new_config.max_gate = (uint16_t) max_gate; + } + this->publish_state(max_gate); +} + +void LD2420GateSelectNumber::control(float gate_select) { + const uint8_t gate = (uint8_t) gate_select; + this->publish_state(gate_select); + this->parent_->publish_gate_move_threshold(gate); + 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 ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/number/gate_config_number.h b/esphome/components/ld2420/number/gate_config_number.h new file mode 100644 index 0000000000..459a8026e3 --- /dev/null +++ b/esphome/components/ld2420/number/gate_config_number.h @@ -0,0 +1,78 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2420.h" + +namespace esphome { +namespace ld2420 { + +class LD2420TimeoutNumber : public number::Number, public Parented { + public: + LD2420TimeoutNumber() = default; + + protected: + void control(float timeout) override; +}; + +class LD2420MinDistanceNumber : public number::Number, public Parented { + public: + LD2420MinDistanceNumber() = default; + + protected: + void control(float min_gate) override; +}; + +class LD2420MaxDistanceNumber : public number::Number, public Parented { + public: + LD2420MaxDistanceNumber() = default; + + protected: + void control(float max_gate) override; +}; + +class LD2420GateSelectNumber : public number::Number, public Parented { + public: + LD2420GateSelectNumber() = default; + + protected: + void control(float gate_select) override; +}; + +class LD2420MoveSensFactorNumber : public number::Number, public Parented { + public: + LD2420MoveSensFactorNumber() = default; + + protected: + void control(float move_factor) override; +}; + +class LD2420StillSensFactorNumber : public number::Number, public Parented { + public: + LD2420StillSensFactorNumber() = default; + + protected: + void control(float still_factor) override; +}; + +class LD2420StillThresholdNumbers : public number::Number, public Parented { + public: + LD2420StillThresholdNumbers() = default; + LD2420StillThresholdNumbers(uint8_t gate); + + protected: + uint8_t gate_; + void control(float still_threshold) override; +}; + +class LD2420MoveThresholdNumbers : public number::Number, public Parented { + public: + LD2420MoveThresholdNumbers() = default; + LD2420MoveThresholdNumbers(uint8_t gate); + + protected: + uint8_t gate_; + void control(float move_threshold) override; +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/select/__init__.py b/esphome/components/ld2420/select/__init__.py new file mode 100644 index 0000000000..554bd4147d --- /dev/null +++ b/esphome/components/ld2420/select/__init__.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import ENTITY_CATEGORY_CONFIG +from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns + +CONF_OPERATING_MODE = "operating_mode" +CONF_SELECTS = [ + "Normal", + "Calibrate", + "Simple", +] + +LD2420Select = ld2420_ns.class_("LD2420Select", cg.Component) + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component), + cv.Required(CONF_OPERATING_MODE): select.select_schema( + LD2420Select, + entity_category=ENTITY_CATEGORY_CONFIG, + ), +} + + +async def to_code(config): + LD2420_component = await cg.get_variable(config[CONF_LD2420_ID]) + if operating_mode_config := config.get(CONF_OPERATING_MODE): + sel = await select.new_select( + operating_mode_config, + options=[CONF_SELECTS], + ) + await cg.register_parented(sel, config[CONF_LD2420_ID]) + cg.add(LD2420_component.set_operating_mode_select(sel)) diff --git a/esphome/components/ld2420/select/operating_mode_select.cpp b/esphome/components/ld2420/select/operating_mode_select.cpp new file mode 100644 index 0000000000..1c59f443a5 --- /dev/null +++ b/esphome/components/ld2420/select/operating_mode_select.cpp @@ -0,0 +1,16 @@ +#include "operating_mode_select.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ld2420 { + +static const char *const TAG = "LD2420.select"; + +void LD2420Select::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_operating_mode(value); +} + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/select/operating_mode_select.h b/esphome/components/ld2420/select/operating_mode_select.h new file mode 100644 index 0000000000..317b2af8c0 --- /dev/null +++ b/esphome/components/ld2420/select/operating_mode_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "../ld2420.h" +#include "esphome/components/select/select.h" + +namespace esphome { +namespace ld2420 { + +class LD2420Select : public Component, public select::Select, public Parented { + public: + LD2420Select() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/sensor/__init__.py b/esphome/components/ld2420/sensor/__init__.py new file mode 100644 index 0000000000..6a67d1fc41 --- /dev/null +++ b/esphome/components/ld2420/sensor/__init__.py @@ -0,0 +1,35 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import CONF_ID, DEVICE_CLASS_DISTANCE, UNIT_CENTIMETER +from .. import ld2420_ns, LD2420Component, CONF_LD2420_ID + +LD2420Sensor = ld2420_ns.class_("LD2420Sensor", sensor.Sensor, cg.Component) + +CONF_MOVING_DISTANCE = "moving_distance" +CONF_GATE_ENERGY = "gate_energy" + +CONFIG_SCHEMA = cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(LD2420Sensor), + cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component), + cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_CENTIMETER + ), + } + ), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + if CONF_MOVING_DISTANCE in config: + sens = await sensor.new_sensor(config[CONF_MOVING_DISTANCE]) + cg.add(var.set_distance_sensor(sens)) + if CONF_GATE_ENERGY in config: + sens = await sensor.new_sensor(config[CONF_GATE_ENERGY]) + cg.add(var.set_energy_sensor(sens)) + ld2420 = await cg.get_variable(config[CONF_LD2420_ID]) + cg.add(ld2420.register_listener(var)) diff --git a/esphome/components/ld2420/sensor/ld2420_sensor.cpp b/esphome/components/ld2420/sensor/ld2420_sensor.cpp new file mode 100644 index 0000000000..97f0c594b7 --- /dev/null +++ b/esphome/components/ld2420/sensor/ld2420_sensor.cpp @@ -0,0 +1,16 @@ +#include "ld2420_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ld2420 { + +static const char *const TAG = "LD2420.sensor"; + +void LD2420Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "LD2420 Sensor:"); + LOG_SENSOR(" ", "Distance", this->distance_sensor_); +} + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/sensor/ld2420_sensor.h b/esphome/components/ld2420/sensor/ld2420_sensor.h new file mode 100644 index 0000000000..4eebefe0e3 --- /dev/null +++ b/esphome/components/ld2420/sensor/ld2420_sensor.h @@ -0,0 +1,34 @@ +#pragma once + +#include "../ld2420.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace ld2420 { + +class LD2420Sensor : public LD2420Listener, public Component, sensor::Sensor { + public: + void dump_config() override; + void set_distance_sensor(sensor::Sensor *sensor) { this->distance_sensor_ = sensor; } + void on_distance(uint16_t distance) override { + if (this->distance_sensor_ != nullptr) { + if (this->distance_sensor_->get_state() != distance) { + this->distance_sensor_->publish_state(distance); + } + } + } + void on_energy(uint16_t *gate_energy, size_t size) override { + for (size_t active = 0; active < size; active++) { + if (this->energy_sensors_[active] != nullptr) { + this->energy_sensors_[active]->publish_state(gate_energy[active]); + } + } + } + + protected: + sensor::Sensor *distance_sensor_{nullptr}; + std::vector energy_sensors_ = std::vector(LD2420_TOTAL_GATES); +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/text_sensor/__init__.py b/esphome/components/ld2420/text_sensor/__init__.py new file mode 100644 index 0000000000..b6d8c7c0e4 --- /dev/null +++ b/esphome/components/ld2420/text_sensor/__init__.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_CHIP, +) + +from .. import ld2420_ns, LD2420Component, CONF_LD2420_ID + +LD2420TextSensor = ld2420_ns.class_( + "LD2420TextSensor", text_sensor.TextSensor, cg.Component +) + +CONF_FW_VERSION = "fw_version" + +CONFIG_SCHEMA = cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(LD2420TextSensor), + cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component), + cv.Optional(CONF_FW_VERSION): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP + ), + } + ), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + if CONF_FW_VERSION in config: + sens = await text_sensor.new_text_sensor(config[CONF_FW_VERSION]) + cg.add(var.set_fw_version_text_sensor(sens)) + ld2420 = await cg.get_variable(config[CONF_LD2420_ID]) + cg.add(ld2420.register_listener(var)) diff --git a/esphome/components/ld2420/text_sensor/text_sensor.cpp b/esphome/components/ld2420/text_sensor/text_sensor.cpp new file mode 100644 index 0000000000..1dcdcf7d60 --- /dev/null +++ b/esphome/components/ld2420/text_sensor/text_sensor.cpp @@ -0,0 +1,16 @@ +#include "text_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ld2420 { + +static const char *const TAG = "LD2420.text_sensor"; + +void LD2420TextSensor::dump_config() { + ESP_LOGCONFIG(TAG, "LD2420 TextSensor:"); + LOG_TEXT_SENSOR(" ", "Firmware", this->fw_version_text_sensor_); +} + +} // namespace ld2420 +} // namespace esphome diff --git a/esphome/components/ld2420/text_sensor/text_sensor.h b/esphome/components/ld2420/text_sensor/text_sensor.h new file mode 100644 index 0000000000..073ddd5d0f --- /dev/null +++ b/esphome/components/ld2420/text_sensor/text_sensor.h @@ -0,0 +1,24 @@ +#pragma once + +#include "../ld2420.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome { +namespace ld2420 { + +class LD2420TextSensor : public LD2420Listener, public Component, text_sensor::TextSensor { + public: + void dump_config() override; + void set_fw_version_text_sensor(text_sensor::TextSensor *tsensor) { this->fw_version_text_sensor_ = tsensor; }; + void on_fw_version(std::string &fw) override { + if (this->fw_version_text_sensor_ != nullptr) { + this->fw_version_text_sensor_->publish_state(fw); + } + } + + protected: + text_sensor::TextSensor *fw_version_text_sensor_{nullptr}; +}; + +} // namespace ld2420 +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 1498dac69d..da6227e235 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -224,6 +224,12 @@ uart: tx_pin: 14 rx_pin: 27 baud_rate: 115200 + - id: ld2420_uart + tx_pin: 17 + rx_pin: 16 + baud_rate: 115200 + parity: NONE + stop_bits: 1 - id: gcja5_uart rx_pin: GPIO10 parity: EVEN @@ -1441,6 +1447,9 @@ sensor: still_energy: name: g8 still energy + - platform: ld2420 + moving_distance: + name: "Moving distance (cm)" - platform: sen21231 name: "Person Sensor" i2c_id: i2c_bus @@ -3615,6 +3624,10 @@ ld2410: id: my_ld2410 uart_id: ld2410_uart +ld2420: + id: my_ld2420 + uart_id: ld2420_uart + lcd_menu: display_id: my_lcd_gpio mark_back: 0x5e