From 0256e0005e703ead256361106c8b5cd770f9f3db Mon Sep 17 00:00:00 2001 From: Rihan9 <33684020+Rihan9@users.noreply.github.com> Date: Tue, 12 Aug 2025 07:34:37 +0200 Subject: [PATCH] [ld2412] New component (#9075) Co-authored-by: Keith Burzinski Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + esphome/components/ld2412/__init__.py | 46 + esphome/components/ld2412/binary_sensor.py | 70 ++ esphome/components/ld2412/button/__init__.py | 74 ++ .../ld2412/button/factory_reset_button.cpp | 9 + .../ld2412/button/factory_reset_button.h | 18 + .../components/ld2412/button/query_button.cpp | 9 + .../components/ld2412/button/query_button.h | 18 + .../ld2412/button/restart_button.cpp | 9 + .../components/ld2412/button/restart_button.h | 18 + ...t_dynamic_background_correction_button.cpp | 11 + ...art_dynamic_background_correction_button.h | 18 + esphome/components/ld2412/ld2412.cpp | 861 ++++++++++++++++++ esphome/components/ld2412/ld2412.h | 141 +++ esphome/components/ld2412/number/__init__.py | 126 +++ .../ld2412/number/gate_threshold_number.cpp | 14 + .../ld2412/number/gate_threshold_number.h | 19 + .../ld2412/number/light_threshold_number.cpp | 12 + .../ld2412/number/light_threshold_number.h | 18 + .../number/max_distance_timeout_number.cpp | 12 + .../number/max_distance_timeout_number.h | 18 + esphome/components/ld2412/select/__init__.py | 82 ++ .../ld2412/select/baud_rate_select.cpp | 12 + .../ld2412/select/baud_rate_select.h | 18 + .../select/distance_resolution_select.cpp | 12 + .../select/distance_resolution_select.h | 18 + .../select/light_out_control_select.cpp | 12 + .../ld2412/select/light_out_control_select.h | 18 + esphome/components/ld2412/sensor.py | 124 +++ esphome/components/ld2412/switch/__init__.py | 45 + .../ld2412/switch/bluetooth_switch.cpp | 12 + .../ld2412/switch/bluetooth_switch.h | 18 + .../ld2412/switch/engineering_mode_switch.cpp | 12 + .../ld2412/switch/engineering_mode_switch.h | 18 + esphome/components/ld2412/text_sensor.py | 34 + tests/components/ld2412/common.yaml | 233 +++++ tests/components/ld2412/test.esp32-ard.yaml | 5 + .../components/ld2412/test.esp32-c3-ard.yaml | 5 + .../components/ld2412/test.esp32-c3-idf.yaml | 5 + tests/components/ld2412/test.esp32-idf.yaml | 5 + tests/components/ld2412/test.esp8266-ard.yaml | 5 + tests/components/ld2412/test.rp2040-ard.yaml | 5 + 42 files changed, 2220 insertions(+) create mode 100644 esphome/components/ld2412/__init__.py create mode 100644 esphome/components/ld2412/binary_sensor.py create mode 100644 esphome/components/ld2412/button/__init__.py create mode 100644 esphome/components/ld2412/button/factory_reset_button.cpp create mode 100644 esphome/components/ld2412/button/factory_reset_button.h create mode 100644 esphome/components/ld2412/button/query_button.cpp create mode 100644 esphome/components/ld2412/button/query_button.h create mode 100644 esphome/components/ld2412/button/restart_button.cpp create mode 100644 esphome/components/ld2412/button/restart_button.h create mode 100644 esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp create mode 100644 esphome/components/ld2412/button/start_dynamic_background_correction_button.h create mode 100644 esphome/components/ld2412/ld2412.cpp create mode 100644 esphome/components/ld2412/ld2412.h create mode 100644 esphome/components/ld2412/number/__init__.py create mode 100644 esphome/components/ld2412/number/gate_threshold_number.cpp create mode 100644 esphome/components/ld2412/number/gate_threshold_number.h create mode 100644 esphome/components/ld2412/number/light_threshold_number.cpp create mode 100644 esphome/components/ld2412/number/light_threshold_number.h create mode 100644 esphome/components/ld2412/number/max_distance_timeout_number.cpp create mode 100644 esphome/components/ld2412/number/max_distance_timeout_number.h create mode 100644 esphome/components/ld2412/select/__init__.py create mode 100644 esphome/components/ld2412/select/baud_rate_select.cpp create mode 100644 esphome/components/ld2412/select/baud_rate_select.h create mode 100644 esphome/components/ld2412/select/distance_resolution_select.cpp create mode 100644 esphome/components/ld2412/select/distance_resolution_select.h create mode 100644 esphome/components/ld2412/select/light_out_control_select.cpp create mode 100644 esphome/components/ld2412/select/light_out_control_select.h create mode 100644 esphome/components/ld2412/sensor.py create mode 100644 esphome/components/ld2412/switch/__init__.py create mode 100644 esphome/components/ld2412/switch/bluetooth_switch.cpp create mode 100644 esphome/components/ld2412/switch/bluetooth_switch.h create mode 100644 esphome/components/ld2412/switch/engineering_mode_switch.cpp create mode 100644 esphome/components/ld2412/switch/engineering_mode_switch.h create mode 100644 esphome/components/ld2412/text_sensor.py create mode 100644 tests/components/ld2412/common.yaml create mode 100644 tests/components/ld2412/test.esp32-ard.yaml create mode 100644 tests/components/ld2412/test.esp32-c3-ard.yaml create mode 100644 tests/components/ld2412/test.esp32-c3-idf.yaml create mode 100644 tests/components/ld2412/test.esp32-idf.yaml create mode 100644 tests/components/ld2412/test.esp8266-ard.yaml create mode 100644 tests/components/ld2412/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 509b0c0b9e..a1a74e9c99 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -246,6 +246,7 @@ esphome/components/kuntze/* @ssieb esphome/components/lc709203f/* @ilikecake esphome/components/lcd_menu/* @numo68 esphome/components/ld2410/* @regevbr @sebcaps +esphome/components/ld2412/* @Rihan9 esphome/components/ld2420/* @descipher esphome/components/ld2450/* @hareeshmu esphome/components/ld24xx/* @kbx81 diff --git a/esphome/components/ld2412/__init__.py b/esphome/components/ld2412/__init__.py new file mode 100644 index 0000000000..e701d0bda9 --- /dev/null +++ b/esphome/components/ld2412/__init__.py @@ -0,0 +1,46 @@ +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_THROTTLE + +AUTO_LOAD = ["ld24xx"] +CODEOWNERS = ["@Rihan9"] +DEPENDENCIES = ["uart"] +MULTI_CONF = True + +LD2412_ns = cg.esphome_ns.namespace("ld2412") +LD2412Component = LD2412_ns.class_("LD2412Component", cg.Component, uart.UARTDevice) + +CONF_LD2412_ID = "ld2412_id" + +CONF_MAX_MOVE_DISTANCE = "max_move_distance" +CONF_MAX_STILL_DISTANCE = "max_still_distance" +CONF_MOVE_THRESHOLDS = [f"g{x}_move_threshold" for x in range(9)] +CONF_STILL_THRESHOLDS = [f"g{x}_still_threshold" for x in range(9)] + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(LD2412Component), + cv.Optional(CONF_THROTTLE): cv.invalid( + f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead" + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "ld2412", + 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/ld2412/binary_sensor.py b/esphome/components/ld2412/binary_sensor.py new file mode 100644 index 0000000000..aa1b0d2cd8 --- /dev/null +++ b/esphome/components/ld2412/binary_sensor.py @@ -0,0 +1,70 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_HAS_MOVING_TARGET, + CONF_HAS_STILL_TARGET, + CONF_HAS_TARGET, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_RUNNING, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_ACCOUNT, + ICON_MOTION_SENSOR, +) + +from . import CONF_LD2412_ID, LD2412Component + +DEPENDENCIES = ["ld2412"] + +CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS = "dynamic_background_correction_status" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional( + CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS + ): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_RUNNING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_ACCOUNT, + ), + cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_OCCUPANCY, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_ACCOUNT, + ), + cv.Optional(CONF_HAS_MOVING_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_MOTION, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_HAS_STILL_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_OCCUPANCY, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_MOTION_SENSOR, + ), +} + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if dynamic_background_correction_status_config := config.get( + CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS + ): + sens = await binary_sensor.new_binary_sensor( + dynamic_background_correction_status_config + ) + cg.add( + LD2412_component.set_dynamic_background_correction_status_binary_sensor( + sens + ) + ) + if has_target_config := config.get(CONF_HAS_TARGET): + sens = await binary_sensor.new_binary_sensor(has_target_config) + cg.add(LD2412_component.set_target_binary_sensor(sens)) + if has_moving_target_config := config.get(CONF_HAS_MOVING_TARGET): + sens = await binary_sensor.new_binary_sensor(has_moving_target_config) + cg.add(LD2412_component.set_moving_target_binary_sensor(sens)) + if has_still_target_config := config.get(CONF_HAS_STILL_TARGET): + sens = await binary_sensor.new_binary_sensor(has_still_target_config) + cg.add(LD2412_component.set_still_target_binary_sensor(sens)) diff --git a/esphome/components/ld2412/button/__init__.py b/esphome/components/ld2412/button/__init__.py new file mode 100644 index 0000000000..e78cad4b88 --- /dev/null +++ b/esphome/components/ld2412/button/__init__.py @@ -0,0 +1,74 @@ +import esphome.codegen as cg +from esphome.components import button +import esphome.config_validation as cv +from esphome.const import ( + CONF_FACTORY_RESET, + CONF_RESTART, + DEVICE_CLASS_RESTART, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_DATABASE, + ICON_PULSE, + ICON_RESTART, + ICON_RESTART_ALERT, +) + +from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component + +FactoryResetButton = LD2412_ns.class_("FactoryResetButton", button.Button) +QueryButton = LD2412_ns.class_("QueryButton", button.Button) +RestartButton = LD2412_ns.class_("RestartButton", button.Button) +StartDynamicBackgroundCorrectionButton = LD2412_ns.class_( + "StartDynamicBackgroundCorrectionButton", button.Button +) + +CONF_QUERY_PARAMS = "query_params" +CONF_START_DYNAMIC_BACKGROUND_CORRECTION = "start_dynamic_background_correction" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_FACTORY_RESET): button.button_schema( + FactoryResetButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART_ALERT, + ), + cv.Optional(CONF_QUERY_PARAMS): button.button_schema( + QueryButton, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_DATABASE, + ), + cv.Optional(CONF_RESTART): button.button_schema( + RestartButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_RESTART, + ), + cv.Optional(CONF_START_DYNAMIC_BACKGROUND_CORRECTION): button.button_schema( + StartDynamicBackgroundCorrectionButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_PULSE, + ), +} + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if factory_reset_config := config.get(CONF_FACTORY_RESET): + b = await button.new_button(factory_reset_config) + await cg.register_parented(b, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_factory_reset_button(b)) + if query_params_config := config.get(CONF_QUERY_PARAMS): + b = await button.new_button(query_params_config) + await cg.register_parented(b, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_query_button(b)) + if restart_config := config.get(CONF_RESTART): + b = await button.new_button(restart_config) + await cg.register_parented(b, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_restart_button(b)) + if start_dynamic_background_correction_config := config.get( + CONF_START_DYNAMIC_BACKGROUND_CORRECTION + ): + b = await button.new_button(start_dynamic_background_correction_config) + await cg.register_parented(b, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_start_dynamic_background_correction_button(b)) diff --git a/esphome/components/ld2412/button/factory_reset_button.cpp b/esphome/components/ld2412/button/factory_reset_button.cpp new file mode 100644 index 0000000000..7ee85bc8f9 --- /dev/null +++ b/esphome/components/ld2412/button/factory_reset_button.cpp @@ -0,0 +1,9 @@ +#include "factory_reset_button.h" + +namespace esphome { +namespace ld2412 { + +void FactoryResetButton::press_action() { this->parent_->factory_reset(); } + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/factory_reset_button.h b/esphome/components/ld2412/button/factory_reset_button.h new file mode 100644 index 0000000000..36a3fffcd5 --- /dev/null +++ b/esphome/components/ld2412/button/factory_reset_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class FactoryResetButton : public button::Button, public Parented { + public: + FactoryResetButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/query_button.cpp b/esphome/components/ld2412/button/query_button.cpp new file mode 100644 index 0000000000..536f74427f --- /dev/null +++ b/esphome/components/ld2412/button/query_button.cpp @@ -0,0 +1,9 @@ +#include "query_button.h" + +namespace esphome { +namespace ld2412 { + +void QueryButton::press_action() { this->parent_->read_all_info(); } + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/query_button.h b/esphome/components/ld2412/button/query_button.h new file mode 100644 index 0000000000..595ef6d1e9 --- /dev/null +++ b/esphome/components/ld2412/button/query_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class QueryButton : public button::Button, public Parented { + public: + QueryButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/restart_button.cpp b/esphome/components/ld2412/button/restart_button.cpp new file mode 100644 index 0000000000..aca0d17841 --- /dev/null +++ b/esphome/components/ld2412/button/restart_button.cpp @@ -0,0 +1,9 @@ +#include "restart_button.h" + +namespace esphome { +namespace ld2412 { + +void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); } + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/restart_button.h b/esphome/components/ld2412/button/restart_button.h new file mode 100644 index 0000000000..5cd582e2a3 --- /dev/null +++ b/esphome/components/ld2412/button/restart_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class RestartButton : public button::Button, public Parented { + public: + RestartButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp b/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp new file mode 100644 index 0000000000..9b37243b82 --- /dev/null +++ b/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp @@ -0,0 +1,11 @@ +#include "start_dynamic_background_correction_button.h" + +#include "restart_button.h" + +namespace esphome { +namespace ld2412 { + +void StartDynamicBackgroundCorrectionButton::press_action() { this->parent_->start_dynamic_background_correction(); } + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/start_dynamic_background_correction_button.h b/esphome/components/ld2412/button/start_dynamic_background_correction_button.h new file mode 100644 index 0000000000..3af0a8a149 --- /dev/null +++ b/esphome/components/ld2412/button/start_dynamic_background_correction_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class StartDynamicBackgroundCorrectionButton : public button::Button, public Parented { + public: + StartDynamicBackgroundCorrectionButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp new file mode 100644 index 0000000000..63af69ce0d --- /dev/null +++ b/esphome/components/ld2412/ld2412.cpp @@ -0,0 +1,861 @@ +#include "ld2412.h" + +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ld2412 { + +static const char *const TAG = "ld2412"; +static const char *const UNKNOWN_MAC = "unknown"; +static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; + +enum BaudRate : uint8_t { + BAUD_RATE_9600 = 1, + BAUD_RATE_19200 = 2, + BAUD_RATE_38400 = 3, + BAUD_RATE_57600 = 4, + BAUD_RATE_115200 = 5, + BAUD_RATE_230400 = 6, + BAUD_RATE_256000 = 7, + BAUD_RATE_460800 = 8, +}; + +enum DistanceResolution : uint8_t { + DISTANCE_RESOLUTION_0_2 = 0x03, + DISTANCE_RESOLUTION_0_5 = 0x01, + DISTANCE_RESOLUTION_0_75 = 0x00, +}; + +enum LightFunction : uint8_t { + LIGHT_FUNCTION_OFF = 0x00, + LIGHT_FUNCTION_BELOW = 0x01, + LIGHT_FUNCTION_ABOVE = 0x02, +}; + +enum OutPinLevel : uint8_t { + OUT_PIN_LEVEL_LOW = 0x01, + OUT_PIN_LEVEL_HIGH = 0x00, +}; + +/* +Data Type: 6th byte +Target states: 9th byte + Moving target distance: 10~11th bytes + Moving target energy: 12th byte + Still target distance: 13~14th bytes + Still target energy: 15th byte + Detect distance: 16~17th bytes +*/ +enum PeriodicData : uint8_t { + DATA_TYPES = 6, + TARGET_STATES = 8, + MOVING_TARGET_LOW = 9, + MOVING_TARGET_HIGH = 10, + MOVING_ENERGY = 11, + STILL_TARGET_LOW = 12, + STILL_TARGET_HIGH = 13, + STILL_ENERGY = 14, + MOVING_SENSOR_START = 17, + STILL_SENSOR_START = 31, + LIGHT_SENSOR = 45, + OUT_PIN_SENSOR = 38, +}; + +enum PeriodicDataValue : uint8_t { + HEADER = 0XAA, + FOOTER = 0x55, + CHECK = 0x00, +}; + +enum AckData : uint8_t { + COMMAND = 6, + COMMAND_STATUS = 7, +}; + +// Memory-efficient lookup tables +struct StringToUint8 { + const char *str; + const uint8_t value; +}; + +struct Uint8ToString { + const uint8_t value; + const char *str; +}; + +constexpr StringToUint8 BAUD_RATES_BY_STR[] = { + {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, + {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, + {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}, +}; + +constexpr StringToUint8 DISTANCE_RESOLUTIONS_BY_STR[] = { + {"0.2m", DISTANCE_RESOLUTION_0_2}, + {"0.5m", DISTANCE_RESOLUTION_0_5}, + {"0.75m", DISTANCE_RESOLUTION_0_75}, +}; + +constexpr Uint8ToString DISTANCE_RESOLUTIONS_BY_UINT[] = { + {DISTANCE_RESOLUTION_0_2, "0.2m"}, + {DISTANCE_RESOLUTION_0_5, "0.5m"}, + {DISTANCE_RESOLUTION_0_75, "0.75m"}, +}; + +constexpr StringToUint8 LIGHT_FUNCTIONS_BY_STR[] = { + {"off", LIGHT_FUNCTION_OFF}, + {"below", LIGHT_FUNCTION_BELOW}, + {"above", LIGHT_FUNCTION_ABOVE}, +}; + +constexpr Uint8ToString LIGHT_FUNCTIONS_BY_UINT[] = { + {LIGHT_FUNCTION_OFF, "off"}, + {LIGHT_FUNCTION_BELOW, "below"}, + {LIGHT_FUNCTION_ABOVE, "above"}, +}; + +constexpr StringToUint8 OUT_PIN_LEVELS_BY_STR[] = { + {"low", OUT_PIN_LEVEL_LOW}, + {"high", OUT_PIN_LEVEL_HIGH}, +}; + +constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = { + {OUT_PIN_LEVEL_LOW, "low"}, + {OUT_PIN_LEVEL_HIGH, "high"}, +}; + +// Helper functions for lookups +template uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { + for (const auto &entry : arr) { + if (str == entry.str) { + return entry.value; + } + } + return 0xFF; // Not found +} + +template const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) { + for (const auto &entry : arr) { + if (value == entry.value) { + return entry.str; + } + } + return ""; // Not found +} + +static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Default used when number component is not defined +// Commands +static constexpr uint8_t CMD_ENABLE_CONF = 0xFF; +static constexpr uint8_t CMD_DISABLE_CONF = 0xFE; +static constexpr uint8_t CMD_ENABLE_ENG = 0x62; +static constexpr uint8_t CMD_DISABLE_ENG = 0x63; +static constexpr uint8_t CMD_QUERY_BASIC_CONF = 0x12; +static constexpr uint8_t CMD_BASIC_CONF = 0x02; +static constexpr uint8_t CMD_QUERY_VERSION = 0xA0; +static constexpr uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0x11; +static constexpr uint8_t CMD_SET_DISTANCE_RESOLUTION = 0x01; +static constexpr uint8_t CMD_QUERY_LIGHT_CONTROL = 0x1C; +static constexpr uint8_t CMD_SET_LIGHT_CONTROL = 0x0C; +static constexpr uint8_t CMD_SET_BAUD_RATE = 0xA1; +static constexpr uint8_t CMD_QUERY_MAC_ADDRESS = 0xA5; +static constexpr uint8_t CMD_FACTORY_RESET = 0xA2; +static constexpr uint8_t CMD_RESTART = 0xA3; +static constexpr uint8_t CMD_BLUETOOTH = 0xA4; +static constexpr uint8_t CMD_DYNAMIC_BACKGROUND_CORRECTION = 0x0B; +static constexpr uint8_t CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION = 0x1B; +static constexpr uint8_t CMD_MOTION_GATE_SENS = 0x03; +static constexpr uint8_t CMD_QUERY_MOTION_GATE_SENS = 0x13; +static constexpr uint8_t CMD_STATIC_GATE_SENS = 0x04; +static constexpr uint8_t CMD_QUERY_STATIC_GATE_SENS = 0x14; +static constexpr uint8_t CMD_NONE = 0x00; +// Commands values +static constexpr uint8_t CMD_MAX_MOVE_VALUE = 0x00; +static constexpr uint8_t CMD_MAX_STILL_VALUE = 0x01; +static constexpr uint8_t CMD_DURATION_VALUE = 0x02; +// Bitmasks for target states +static constexpr uint8_t MOVE_BITMASK = 0x01; +static constexpr uint8_t STILL_BITMASK = 0x02; +// Header & Footer size +static constexpr uint8_t HEADER_FOOTER_SIZE = 4; +// Command Header & Footer +static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA}; +static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01}; +// Data Header & Footer +static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xF4, 0xF3, 0xF2, 0xF1}; +static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0xF6, 0xF5}; +// MAC address the module uses when Bluetooth is disabled +static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01}; + +static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } + +static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { + return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0; +} + +void LD2412Component::dump_config() { + std::string mac_str = + mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; + std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], + this->version_[4], this->version_[3], this->version_[2]); + ESP_LOGCONFIG(TAG, + "LD2412:\n" + " Firmware version: %s\n" + " MAC address: %s", + version.c_str(), mac_str.c_str()); +#ifdef USE_BINARY_SENSOR + ESP_LOGCONFIG(TAG, "Binary Sensors:"); + LOG_BINARY_SENSOR(" ", "DynamicBackgroundCorrectionStatus", + this->dynamic_background_correction_status_binary_sensor_); + LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "StillTarget", this->still_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_); +#endif +#ifdef USE_SENSOR + ESP_LOGCONFIG(TAG, "Sensors:"); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "Light", this->light_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "DetectionDistance", this->detection_distance_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetDistance", this->moving_target_distance_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetEnergy", this->moving_target_energy_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetDistance", this->still_target_distance_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetEnergy", this->still_target_energy_sensor_); + for (auto &s : this->gate_still_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "GateStill", s); + } + for (auto &s : this->gate_move_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "GateMove", s); + } +#endif +#ifdef USE_TEXT_SENSOR + ESP_LOGCONFIG(TAG, "Text Sensors:"); + LOG_TEXT_SENSOR(" ", "MAC address", this->mac_text_sensor_); + LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_); +#endif +#ifdef USE_NUMBER + ESP_LOGCONFIG(TAG, "Numbers:"); + LOG_NUMBER(" ", "LightThreshold", this->light_threshold_number_); + LOG_NUMBER(" ", "MaxDistanceGate", this->max_distance_gate_number_); + LOG_NUMBER(" ", "MinDistanceGate", this->min_distance_gate_number_); + LOG_NUMBER(" ", "Timeout", this->timeout_number_); + for (number::Number *n : this->gate_move_threshold_numbers_) { + LOG_NUMBER(" ", "Move Thresholds", n); + } + for (number::Number *n : this->gate_still_threshold_numbers_) { + LOG_NUMBER(" ", "Still Thresholds", n); + } +#endif +#ifdef USE_SELECT + ESP_LOGCONFIG(TAG, "Selects:"); + LOG_SELECT(" ", "BaudRate", this->baud_rate_select_); + LOG_SELECT(" ", "DistanceResolution", this->distance_resolution_select_); + LOG_SELECT(" ", "LightFunction", this->light_function_select_); + LOG_SELECT(" ", "OutPinLevel", this->out_pin_level_select_); +#endif +#ifdef USE_SWITCH + ESP_LOGCONFIG(TAG, "Switches:"); + LOG_SWITCH(" ", "Bluetooth", this->bluetooth_switch_); + LOG_SWITCH(" ", "EngineeringMode", this->engineering_mode_switch_); +#endif +#ifdef USE_BUTTON + ESP_LOGCONFIG(TAG, "Buttons:"); + LOG_BUTTON(" ", "FactoryReset", this->factory_reset_button_); + LOG_BUTTON(" ", "Query", this->query_button_); + LOG_BUTTON(" ", "Restart", this->restart_button_); + LOG_BUTTON(" ", "StartDynamicBackgroundCorrection", this->start_dynamic_background_correction_button_); +#endif +} + +void LD2412Component::setup() { + ESP_LOGCONFIG(TAG, "Running setup"); + this->read_all_info(); +} + +void LD2412Component::read_all_info() { + this->set_config_mode_(true); + this->get_version_(); + delay(10); // NOLINT + this->get_mac_(); + delay(10); // NOLINT + this->get_distance_resolution_(); + delay(10); // NOLINT + this->query_parameters_(); + delay(10); // NOLINT + this->query_dynamic_background_correction_(); + delay(10); // NOLINT + this->query_light_control_(); + delay(10); // NOLINT +#ifdef USE_NUMBER + this->get_gate_threshold(); + delay(10); // NOLINT +#endif + this->set_config_mode_(false); +#ifdef USE_SELECT + const auto baud_rate = std::to_string(this->parent_->get_baud_rate()); + if (this->baud_rate_select_ != nullptr) { + this->baud_rate_select_->publish_state(baud_rate); + } +#endif +} + +void LD2412Component::restart_and_read_all_info() { + this->set_config_mode_(true); + this->restart_(); + this->set_timeout(1000, [this]() { this->read_all_info(); }); +} + +void LD2412Component::loop() { + while (this->available()) { + this->readline_(this->read()); + } +} + +void LD2412Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) { + ESP_LOGV(TAG, "Sending COMMAND %02X", command); + // frame header bytes + this->write_array(CMD_FRAME_HEADER, HEADER_FOOTER_SIZE); + // length bytes + uint8_t len = 2; + if (command_value != nullptr) { + len += command_value_len; + } + // 2 length bytes (low, high) + 2 command bytes (low, high) + uint8_t len_cmd[] = {len, 0x00, command, 0x00}; + this->write_array(len_cmd, sizeof(len_cmd)); + + // command value bytes + if (command_value != nullptr) { + this->write_array(command_value, command_value_len); + } + // frame footer bytes + this->write_array(CMD_FRAME_FOOTER, HEADER_FOOTER_SIZE); + + if (command != CMD_ENABLE_CONF && command != CMD_DISABLE_CONF) { + delay(30); // NOLINT + } + delay(20); // NOLINT +} + +void LD2412Component::handle_periodic_data_() { + // 4 frame header bytes + 2 length bytes + 1 data end byte + 1 crc byte + 4 frame footer bytes + // data header=0xAA, data footer=0x55, crc=0x00 + if (this->buffer_pos_ < 12 || !ld2412::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) || + this->buffer_data_[7] != HEADER || this->buffer_data_[this->buffer_pos_ - 6] != FOOTER) { + return; + } + /* + Data Type: 7th + 0x01: Engineering mode + 0x02: Normal mode + */ + bool engineering_mode = this->buffer_data_[DATA_TYPES] == 0x01; +#ifdef USE_SWITCH + if (this->engineering_mode_switch_ != nullptr) { + this->engineering_mode_switch_->publish_state(engineering_mode); + } +#endif + +#ifdef USE_BINARY_SENSOR + /* + Target states: 9th + 0x00 = No target + 0x01 = Moving targets + 0x02 = Still targets + 0x03 = Moving+Still targets + */ + char target_state = this->buffer_data_[TARGET_STATES]; + if (this->target_binary_sensor_ != nullptr) { + this->target_binary_sensor_->publish_state(target_state != 0x00); + } + if (this->moving_target_binary_sensor_ != nullptr) { + this->moving_target_binary_sensor_->publish_state(target_state & MOVE_BITMASK); + } + if (this->still_target_binary_sensor_ != nullptr) { + this->still_target_binary_sensor_->publish_state(target_state & STILL_BITMASK); + } +#endif + /* + Moving target distance: 10~11th bytes + Moving target energy: 12th byte + Still target distance: 13~14th bytes + Still target energy: 15th byte + Detect distance: 16~17th bytes + */ +#ifdef USE_SENSOR + SAFE_PUBLISH_SENSOR( + this->moving_target_distance_sensor_, + ld2412::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH])) + SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]) + SAFE_PUBLISH_SENSOR( + this->still_target_distance_sensor_, + ld2412::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH])) + SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]) + if (this->detection_distance_sensor_ != nullptr) { + int new_detect_distance = 0; + if (target_state != 0x00 && (target_state & MOVE_BITMASK)) { + new_detect_distance = + ld2412::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]); + } else if (target_state != 0x00) { + new_detect_distance = + ld2412::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]); + } + this->detection_distance_sensor_->publish_state_if_not_dup(new_detect_distance); + } + if (engineering_mode) { + /* + Moving distance range: 18th byte + Still distance range: 19th byte + Moving energy: 20~28th bytes + */ + for (uint8_t i = 0; i < TOTAL_GATES; i++) { + SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]) + } + /* + Still energy: 29~37th bytes + */ + for (uint8_t i = 0; i < TOTAL_GATES; i++) { + SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]) + } + /* + Light sensor: 38th bytes + */ + SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]) + } else { + for (auto &gate_move_sensor : this->gate_move_sensors_) { + SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor) + } + for (auto &gate_still_sensor : this->gate_still_sensors_) { + SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor) + } + SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_) + } +#endif + // the radar module won't tell us when it's done, so we just have to keep polling... + if (this->dynamic_background_correction_active_) { + this->set_config_mode_(true); + this->query_dynamic_background_correction_(); + this->set_config_mode_(false); + } +} + +#ifdef USE_NUMBER +std::function set_number_value(number::Number *n, float value) { + if (n != nullptr && (!n->has_state() || n->state != value)) { + n->state = value; + return [n, value]() { n->publish_state(value); }; + } + return []() {}; +} +#endif + +bool LD2412Component::handle_ack_data_() { + ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]); + if (this->buffer_pos_ < 10) { + ESP_LOGW(TAG, "Invalid length"); + return true; + } + if (!ld2412::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) { + ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str()); + return true; + } + if (this->buffer_data_[COMMAND_STATUS] != 0x01) { + ESP_LOGW(TAG, "Invalid status"); + return true; + } + if (this->buffer_data_[8] || this->buffer_data_[9]) { + ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]); + return true; + } + + switch (this->buffer_data_[COMMAND]) { + case CMD_ENABLE_CONF: + ESP_LOGV(TAG, "Enable conf"); + break; + + case CMD_DISABLE_CONF: + ESP_LOGV(TAG, "Disabled conf"); + break; + + case CMD_SET_BAUD_RATE: + ESP_LOGV(TAG, "Baud rate change"); +#ifdef USE_SELECT + if (this->baud_rate_select_ != nullptr) { + ESP_LOGW(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); + } +#endif + break; + + case CMD_QUERY_VERSION: { + std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); + std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], + this->version_[4], this->version_[3], this->version_[2]); + ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); +#ifdef USE_TEXT_SENSOR + if (this->version_text_sensor_ != nullptr) { + this->version_text_sensor_->publish_state(version); + } +#endif + break; + } + case CMD_QUERY_DISTANCE_RESOLUTION: { + const auto *distance_resolution = find_str(DISTANCE_RESOLUTIONS_BY_UINT, this->buffer_data_[10]); + ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution); +#ifdef USE_SELECT + if (this->distance_resolution_select_ != nullptr) { + this->distance_resolution_select_->publish_state(distance_resolution); + } +#endif + break; + } + + case CMD_QUERY_LIGHT_CONTROL: { + this->light_function_ = this->buffer_data_[10]; + this->light_threshold_ = this->buffer_data_[11]; + const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_); + ESP_LOGV(TAG, + "Light function: %s\n" + "Light threshold: %u", + light_function_str, this->light_threshold_); +#ifdef USE_SELECT + if (this->light_function_select_ != nullptr) { + this->light_function_select_->publish_state(light_function_str); + } +#endif +#ifdef USE_NUMBER + if (this->light_threshold_number_ != nullptr) { + this->light_threshold_number_->publish_state(static_cast(this->light_threshold_)); + } +#endif + break; + } + + case CMD_QUERY_MAC_ADDRESS: { + if (this->buffer_pos_ < 20) { + return false; + } + + this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0; + if (this->bluetooth_on_) { + std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); + } + + std::string mac_str = + mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; + ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); +#ifdef USE_TEXT_SENSOR + if (this->mac_text_sensor_ != nullptr) { + this->mac_text_sensor_->publish_state(mac_str); + } +#endif +#ifdef USE_SWITCH + if (this->bluetooth_switch_ != nullptr) { + this->bluetooth_switch_->publish_state(this->bluetooth_on_); + } +#endif + break; + } + + case CMD_SET_DISTANCE_RESOLUTION: + ESP_LOGV(TAG, "Handled set distance resolution command"); + break; + + case CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION: { + ESP_LOGV(TAG, "Handled query dynamic background correction"); + bool dynamic_background_correction_active = (this->buffer_data_[10] != 0x00); +#ifdef USE_BINARY_SENSOR + if (this->dynamic_background_correction_status_binary_sensor_ != nullptr) { + this->dynamic_background_correction_status_binary_sensor_->publish_state(dynamic_background_correction_active); + } +#endif + this->dynamic_background_correction_active_ = dynamic_background_correction_active; + break; + } + + case CMD_BLUETOOTH: + ESP_LOGV(TAG, "Handled bluetooth command"); + break; + + case CMD_SET_LIGHT_CONTROL: + ESP_LOGV(TAG, "Handled set light control command"); + break; + + case CMD_QUERY_MOTION_GATE_SENS: { +#ifdef USE_NUMBER + std::vector> updates; + updates.reserve(this->gate_still_threshold_numbers_.size()); + for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) { + updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[10 + i])); + } + for (auto &update : updates) { + update(); + } +#endif + break; + } + + case CMD_QUERY_STATIC_GATE_SENS: { +#ifdef USE_NUMBER + std::vector> updates; + updates.reserve(this->gate_still_threshold_numbers_.size()); + for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) { + updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[10 + i])); + } + for (auto &update : updates) { + update(); + } +#endif + break; + } + + case CMD_QUERY_BASIC_CONF: // Query parameters response + { +#ifdef USE_NUMBER + /* + Moving distance range: 9th byte + Still distance range: 10th byte + */ + std::vector> updates; + updates.push_back(set_number_value(this->min_distance_gate_number_, this->buffer_data_[10])); + updates.push_back(set_number_value(this->max_distance_gate_number_, this->buffer_data_[11] - 1)); + ESP_LOGV(TAG, "min_distance_gate_number_: %u, max_distance_gate_number_ %u", this->buffer_data_[10], + this->buffer_data_[11]); + /* + None Duration: 11~12th bytes + */ + updates.push_back(set_number_value(this->timeout_number_, + ld2412::two_byte_to_int(this->buffer_data_[12], this->buffer_data_[13]))); + ESP_LOGV(TAG, "timeout_number_: %u", ld2412::two_byte_to_int(this->buffer_data_[12], this->buffer_data_[13])); + /* + Output pin configuration: 13th bytes + */ + this->out_pin_level_ = this->buffer_data_[14]; +#ifdef USE_SELECT + const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_); + if (this->out_pin_level_select_ != nullptr) { + this->out_pin_level_select_->publish_state(out_pin_level_str); + } +#endif + for (auto &update : updates) { + update(); + } +#endif + } break; + default: + break; + } + + return true; +} + +void LD2412Component::readline_(int readch) { + if (readch < 0) { + return; // No data available + } + if (this->buffer_pos_ < HEADER_FOOTER_SIZE && readch != DATA_FRAME_HEADER[this->buffer_pos_] && + readch != CMD_FRAME_HEADER[this->buffer_pos_]) { + this->buffer_pos_ = 0; + return; + } + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { + this->buffer_data_[this->buffer_pos_++] = readch; + this->buffer_data_[this->buffer_pos_] = 0; + } else { + // We should never get here, but just in case... + ESP_LOGW(TAG, "Max command length exceeded; ignoring"); + this->buffer_pos_ = 0; + } + if (this->buffer_pos_ < 4) { + return; // Not enough data to process yet + } + if (ld2412::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { + ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); + this->handle_periodic_data_(); + this->buffer_pos_ = 0; // Reset position index for next message + } else if (ld2412::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { + ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); + if (this->handle_ack_data_()) { + this->buffer_pos_ = 0; // Reset position index for next message + } else { + ESP_LOGV(TAG, "Ack Data incomplete"); + } + } +} + +void LD2412Component::set_config_mode_(bool enable) { + const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; + const uint8_t cmd_value[2] = {0x01, 0x00}; + this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value)); +} + +void LD2412Component::set_bluetooth(bool enable) { + this->set_config_mode_(true); + const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00}; + this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value)); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +void LD2412Component::set_distance_resolution(const std::string &state) { + this->set_config_mode_(true); + const uint8_t cmd_value[6] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00, 0x00, 0x00, 0x00, 0x00}; + this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value)); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +void LD2412Component::set_baud_rate(const std::string &state) { + this->set_config_mode_(true); + const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; + this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); + this->set_timeout(200, [this]() { this->restart_(); }); +} + +void LD2412Component::query_dynamic_background_correction_() { + this->send_command_(CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION, nullptr, 0); +} + +void LD2412Component::start_dynamic_background_correction() { + if (this->dynamic_background_correction_active_) { + return; // Already in progress + } +#ifdef USE_BINARY_SENSOR + if (this->dynamic_background_correction_status_binary_sensor_ != nullptr) { + this->dynamic_background_correction_status_binary_sensor_->publish_state(true); + } +#endif + this->dynamic_background_correction_active_ = true; + this->set_config_mode_(true); + this->send_command_(CMD_DYNAMIC_BACKGROUND_CORRECTION, nullptr, 0); + this->set_config_mode_(false); +} + +void LD2412Component::set_engineering_mode(bool enable) { + const uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG; + this->set_config_mode_(true); + this->send_command_(cmd, nullptr, 0); + this->set_config_mode_(false); +} + +void LD2412Component::factory_reset() { + this->set_config_mode_(true); + this->send_command_(CMD_FACTORY_RESET, nullptr, 0); + this->set_timeout(2000, [this]() { this->restart_and_read_all_info(); }); +} + +void LD2412Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); } + +void LD2412Component::query_parameters_() { this->send_command_(CMD_QUERY_BASIC_CONF, nullptr, 0); } + +void LD2412Component::get_version_() { this->send_command_(CMD_QUERY_VERSION, nullptr, 0); } + +void LD2412Component::get_mac_() { + const uint8_t cmd_value[2] = {0x01, 0x00}; + this->send_command_(CMD_QUERY_MAC_ADDRESS, cmd_value, sizeof(cmd_value)); +} + +void LD2412Component::get_distance_resolution_() { this->send_command_(CMD_QUERY_DISTANCE_RESOLUTION, nullptr, 0); } + +void LD2412Component::query_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); } + +void LD2412Component::set_basic_config() { +#ifdef USE_NUMBER + if (!this->min_distance_gate_number_->has_state() || !this->max_distance_gate_number_->has_state() || + !this->timeout_number_->has_state()) { + return; + } +#endif +#ifdef USE_SELECT + if (!this->out_pin_level_select_->has_state()) { + return; + } +#endif + + uint8_t value[5] = { +#ifdef USE_NUMBER + lowbyte(static_cast(this->min_distance_gate_number_->state)), + lowbyte(static_cast(this->max_distance_gate_number_->state) + 1), + lowbyte(static_cast(this->timeout_number_->state)), + highbyte(static_cast(this->timeout_number_->state)), +#else + 1, TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0, +#endif +#ifdef USE_SELECT + find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->state), +#else + 0x01, // Default value if not using select +#endif + }; + this->set_config_mode_(true); + this->send_command_(CMD_BASIC_CONF, value, sizeof(value)); + this->set_config_mode_(false); +} + +#ifdef USE_NUMBER +void LD2412Component::set_gate_threshold() { + if (this->gate_move_threshold_numbers_.empty() && this->gate_still_threshold_numbers_.empty()) { + return; // No gate threshold numbers set; nothing to do here + } + uint8_t value[TOTAL_GATES] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + this->set_config_mode_(true); + if (!this->gate_move_threshold_numbers_.empty()) { + for (size_t i = 0; i < this->gate_move_threshold_numbers_.size(); i++) { + value[i] = lowbyte(static_cast(this->gate_move_threshold_numbers_[i]->state)); + } + this->send_command_(CMD_MOTION_GATE_SENS, value, sizeof(value)); + } + if (!this->gate_still_threshold_numbers_.empty()) { + for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) { + value[i] = lowbyte(static_cast(this->gate_still_threshold_numbers_[i]->state)); + } + this->send_command_(CMD_STATIC_GATE_SENS, value, sizeof(value)); + } + this->set_config_mode_(false); +} + +void LD2412Component::get_gate_threshold() { + this->send_command_(CMD_QUERY_MOTION_GATE_SENS, nullptr, 0); + this->send_command_(CMD_QUERY_STATIC_GATE_SENS, nullptr, 0); +} + +void LD2412Component::set_gate_still_threshold_number(uint8_t gate, number::Number *n) { + this->gate_still_threshold_numbers_[gate] = n; +} + +void LD2412Component::set_gate_move_threshold_number(uint8_t gate, number::Number *n) { + this->gate_move_threshold_numbers_[gate] = n; +} +#endif + +void LD2412Component::set_light_out_control() { +#ifdef USE_NUMBER + if (this->light_threshold_number_ != nullptr && this->light_threshold_number_->has_state()) { + this->light_threshold_ = static_cast(this->light_threshold_number_->state); + } +#endif +#ifdef USE_SELECT + if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { + this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->state); + } +#endif + uint8_t value[2] = {this->light_function_, this->light_threshold_}; + this->set_config_mode_(true); + this->send_command_(CMD_SET_LIGHT_CONTROL, value, sizeof(value)); + this->query_light_control_(); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +#ifdef USE_SENSOR +// These could leak memory, but they are only set once prior to 'setup()' and should never be used again. +void LD2412Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) { + this->gate_move_sensors_[gate] = new SensorWithDedup(s); +} +void LD2412Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { + this->gate_still_sensors_[gate] = new SensorWithDedup(s); +} +#endif + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/ld2412.h b/esphome/components/ld2412/ld2412.h new file mode 100644 index 0000000000..41f96ab301 --- /dev/null +++ b/esphome/components/ld2412/ld2412.h @@ -0,0 +1,141 @@ +#pragma once +#include "esphome/core/defines.h" +#include "esphome/core/component.h" +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif +#ifdef USE_BUTTON +#include "esphome/components/button/button.h" +#endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#include "esphome/components/ld24xx/ld24xx.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace ld2412 { + +using namespace ld24xx; + +static constexpr uint8_t MAX_LINE_LENGTH = 54; // Max characters for serial buffer +static constexpr uint8_t TOTAL_GATES = 14; // Total number of gates supported by the LD2412 + +class LD2412Component : public Component, public uart::UARTDevice { +#ifdef USE_BINARY_SENSOR + SUB_BINARY_SENSOR(dynamic_background_correction_status) + SUB_BINARY_SENSOR(moving_target) + SUB_BINARY_SENSOR(still_target) + SUB_BINARY_SENSOR(target) +#endif +#ifdef USE_SENSOR + SUB_SENSOR_WITH_DEDUP(light, uint8_t) + SUB_SENSOR_WITH_DEDUP(detection_distance, int) + SUB_SENSOR_WITH_DEDUP(moving_target_distance, int) + SUB_SENSOR_WITH_DEDUP(moving_target_energy, uint8_t) + SUB_SENSOR_WITH_DEDUP(still_target_distance, int) + SUB_SENSOR_WITH_DEDUP(still_target_energy, uint8_t) +#endif +#ifdef USE_TEXT_SENSOR + SUB_TEXT_SENSOR(mac) + SUB_TEXT_SENSOR(version) +#endif +#ifdef USE_NUMBER + SUB_NUMBER(light_threshold) + SUB_NUMBER(max_distance_gate) + SUB_NUMBER(min_distance_gate) + SUB_NUMBER(timeout) +#endif +#ifdef USE_SELECT + SUB_SELECT(baud_rate) + SUB_SELECT(distance_resolution) + SUB_SELECT(light_function) + SUB_SELECT(out_pin_level) +#endif +#ifdef USE_SWITCH + SUB_SWITCH(bluetooth) + SUB_SWITCH(engineering_mode) +#endif +#ifdef USE_BUTTON + SUB_BUTTON(factory_reset) + SUB_BUTTON(query) + SUB_BUTTON(restart) + SUB_BUTTON(start_dynamic_background_correction) +#endif + + public: + void setup() override; + void dump_config() override; + void loop() override; + void set_light_out_control(); + void set_basic_config(); +#ifdef USE_NUMBER + void set_gate_move_threshold_number(uint8_t gate, number::Number *n); + void set_gate_still_threshold_number(uint8_t gate, number::Number *n); + void set_gate_threshold(); + void get_gate_threshold(); +#endif +#ifdef USE_SENSOR + void set_gate_move_sensor(uint8_t gate, sensor::Sensor *s); + void set_gate_still_sensor(uint8_t gate, sensor::Sensor *s); +#endif + void set_engineering_mode(bool enable); + void read_all_info(); + void restart_and_read_all_info(); + void set_bluetooth(bool enable); + void set_distance_resolution(const std::string &state); + void set_baud_rate(const std::string &state); + void factory_reset(); + void start_dynamic_background_correction(); + + protected: + void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len); + void set_config_mode_(bool enable); + void handle_periodic_data_(); + bool handle_ack_data_(); + void readline_(int readch); + void query_parameters_(); + void get_version_(); + void get_mac_(); + void get_distance_resolution_(); + void query_light_control_(); + void restart_(); + void query_dynamic_background_correction_(); + + uint8_t light_function_ = 0; + uint8_t light_threshold_ = 0; + uint8_t out_pin_level_ = 0; + uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer + uint8_t buffer_data_[MAX_LINE_LENGTH]; + uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0}; + uint8_t version_[6] = {0, 0, 0, 0, 0, 0}; + bool bluetooth_on_{false}; + bool dynamic_background_correction_active_{false}; +#ifdef USE_NUMBER + std::array gate_move_threshold_numbers_{}; + std::array gate_still_threshold_numbers_{}; +#endif +#ifdef USE_SENSOR + std::array *, TOTAL_GATES> gate_move_sensors_{}; + std::array *, TOTAL_GATES> gate_still_sensors_{}; +#endif +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/number/__init__.py b/esphome/components/ld2412/number/__init__.py new file mode 100644 index 0000000000..5b0d6d8749 --- /dev/null +++ b/esphome/components/ld2412/number/__init__.py @@ -0,0 +1,126 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_MOVE_THRESHOLD, + CONF_STILL_THRESHOLD, + CONF_TIMEOUT, + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_SIGNAL_STRENGTH, + ENTITY_CATEGORY_CONFIG, + ICON_LIGHTBULB, + ICON_MOTION_SENSOR, + ICON_TIMELAPSE, + UNIT_PERCENT, + UNIT_SECOND, +) + +from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component + +GateThresholdNumber = LD2412_ns.class_("GateThresholdNumber", number.Number) +LightThresholdNumber = LD2412_ns.class_("LightThresholdNumber", number.Number) +MaxDistanceTimeoutNumber = LD2412_ns.class_("MaxDistanceTimeoutNumber", number.Number) + +CONF_LIGHT_THRESHOLD = "light_threshold" +CONF_MAX_DISTANCE_GATE = "max_distance_gate" +CONF_MIN_DISTANCE_GATE = "min_distance_gate" + +TIMEOUT_GROUP = "timeout" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_LIGHT_THRESHOLD): number.number_schema( + LightThresholdNumber, + device_class=DEVICE_CLASS_ILLUMINANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_LIGHTBULB, + ), + cv.Optional(CONF_MAX_DISTANCE_GATE): number.number_schema( + MaxDistanceTimeoutNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_MIN_DISTANCE_GATE): number.number_schema( + MaxDistanceTimeoutNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_TIMEOUT): number.number_schema( + MaxDistanceTimeoutNumber, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_TIMELAPSE, + unit_of_measurement=UNIT_SECOND, + ), + } +) + +CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + { + cv.Optional(f"gate_{x}"): ( + { + cv.Required(CONF_MOVE_THRESHOLD): number.number_schema( + GateThresholdNumber, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, + ), + cv.Required(CONF_STILL_THRESHOLD): number.number_schema( + GateThresholdNumber, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, + ), + } + ) + for x in range(14) + } +) + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if light_threshold_config := config.get(CONF_LIGHT_THRESHOLD): + n = await number.new_number( + light_threshold_config, min_value=0, max_value=255, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_light_threshold_number(n)) + if max_distance_gate_config := config.get(CONF_MAX_DISTANCE_GATE): + n = await number.new_number( + max_distance_gate_config, min_value=2, max_value=13, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_max_distance_gate_number(n)) + if min_distance_gate_config := config.get(CONF_MIN_DISTANCE_GATE): + n = await number.new_number( + min_distance_gate_config, min_value=1, max_value=12, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_min_distance_gate_number(n)) + for x in range(14): + 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=100, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_gate_move_threshold_number(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=100, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_gate_still_threshold_number(x, n)) + if timeout_config := config.get(CONF_TIMEOUT): + n = await number.new_number(timeout_config, min_value=0, max_value=900, step=1) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_timeout_number(n)) diff --git a/esphome/components/ld2412/number/gate_threshold_number.cpp b/esphome/components/ld2412/number/gate_threshold_number.cpp new file mode 100644 index 0000000000..47f8cd9107 --- /dev/null +++ b/esphome/components/ld2412/number/gate_threshold_number.cpp @@ -0,0 +1,14 @@ +#include "gate_threshold_number.h" + +namespace esphome { +namespace ld2412 { + +GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {} + +void GateThresholdNumber::control(float value) { + this->publish_state(value); + this->parent_->set_gate_threshold(); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/number/gate_threshold_number.h b/esphome/components/ld2412/number/gate_threshold_number.h new file mode 100644 index 0000000000..61d9945a0a --- /dev/null +++ b/esphome/components/ld2412/number/gate_threshold_number.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class GateThresholdNumber : public number::Number, public Parented { + public: + GateThresholdNumber(uint8_t gate); + + protected: + uint8_t gate_; + void control(float value) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/number/light_threshold_number.cpp b/esphome/components/ld2412/number/light_threshold_number.cpp new file mode 100644 index 0000000000..5dab1716bf --- /dev/null +++ b/esphome/components/ld2412/number/light_threshold_number.cpp @@ -0,0 +1,12 @@ +#include "light_threshold_number.h" + +namespace esphome { +namespace ld2412 { + +void LightThresholdNumber::control(float value) { + this->publish_state(value); + this->parent_->set_light_out_control(); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/number/light_threshold_number.h b/esphome/components/ld2412/number/light_threshold_number.h new file mode 100644 index 0000000000..d8727d3c98 --- /dev/null +++ b/esphome/components/ld2412/number/light_threshold_number.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class LightThresholdNumber : public number::Number, public Parented { + public: + LightThresholdNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/number/max_distance_timeout_number.cpp b/esphome/components/ld2412/number/max_distance_timeout_number.cpp new file mode 100644 index 0000000000..1c6471bc72 --- /dev/null +++ b/esphome/components/ld2412/number/max_distance_timeout_number.cpp @@ -0,0 +1,12 @@ +#include "max_distance_timeout_number.h" + +namespace esphome { +namespace ld2412 { + +void MaxDistanceTimeoutNumber::control(float value) { + this->publish_state(value); + this->parent_->set_basic_config(); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/number/max_distance_timeout_number.h b/esphome/components/ld2412/number/max_distance_timeout_number.h new file mode 100644 index 0000000000..af0dcf68c5 --- /dev/null +++ b/esphome/components/ld2412/number/max_distance_timeout_number.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class MaxDistanceTimeoutNumber : public number::Number, public Parented { + public: + MaxDistanceTimeoutNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/select/__init__.py b/esphome/components/ld2412/select/__init__.py new file mode 100644 index 0000000000..d71ce460d9 --- /dev/null +++ b/esphome/components/ld2412/select/__init__.py @@ -0,0 +1,82 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import ( + CONF_BAUD_RATE, + ENTITY_CATEGORY_CONFIG, + ICON_LIGHTBULB, + ICON_RULER, + ICON_SCALE, + ICON_THERMOMETER, +) + +from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component + +BaudRateSelect = LD2412_ns.class_("BaudRateSelect", select.Select) +DistanceResolutionSelect = LD2412_ns.class_("DistanceResolutionSelect", select.Select) +LightOutControlSelect = LD2412_ns.class_("LightOutControlSelect", select.Select) + +CONF_DISTANCE_RESOLUTION = "distance_resolution" +CONF_LIGHT_FUNCTION = "light_function" +CONF_OUT_PIN_LEVEL = "out_pin_level" + + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_BAUD_RATE): select.select_schema( + BaudRateSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_THERMOMETER, + ), + cv.Optional(CONF_DISTANCE_RESOLUTION): select.select_schema( + DistanceResolutionSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RULER, + ), + cv.Optional(CONF_LIGHT_FUNCTION): select.select_schema( + LightOutControlSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_LIGHTBULB, + ), + cv.Optional(CONF_OUT_PIN_LEVEL): select.select_schema( + LightOutControlSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_SCALE, + ), +} + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if baud_rate_config := config.get(CONF_BAUD_RATE): + s = await select.new_select( + baud_rate_config, + options=[ + "9600", + "19200", + "38400", + "57600", + "115200", + "230400", + "256000", + "460800", + ], + ) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_baud_rate_select(s)) + if distance_resolution_config := config.get(CONF_DISTANCE_RESOLUTION): + s = await select.new_select( + distance_resolution_config, options=["0.2m", "0.5m", "0.75m"] + ) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_distance_resolution_select(s)) + if light_function_config := config.get(CONF_LIGHT_FUNCTION): + s = await select.new_select( + light_function_config, options=["off", "below", "above"] + ) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_light_function_select(s)) + if out_pin_level_config := config.get(CONF_OUT_PIN_LEVEL): + s = await select.new_select(out_pin_level_config, options=["low", "high"]) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_out_pin_level_select(s)) diff --git a/esphome/components/ld2412/select/baud_rate_select.cpp b/esphome/components/ld2412/select/baud_rate_select.cpp new file mode 100644 index 0000000000..2291a81896 --- /dev/null +++ b/esphome/components/ld2412/select/baud_rate_select.cpp @@ -0,0 +1,12 @@ +#include "baud_rate_select.h" + +namespace esphome { +namespace ld2412 { + +void BaudRateSelect::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_baud_rate(state); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/select/baud_rate_select.h b/esphome/components/ld2412/select/baud_rate_select.h new file mode 100644 index 0000000000..2ae33551fb --- /dev/null +++ b/esphome/components/ld2412/select/baud_rate_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class BaudRateSelect : public select::Select, public Parented { + public: + BaudRateSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/select/distance_resolution_select.cpp b/esphome/components/ld2412/select/distance_resolution_select.cpp new file mode 100644 index 0000000000..a282215fbd --- /dev/null +++ b/esphome/components/ld2412/select/distance_resolution_select.cpp @@ -0,0 +1,12 @@ +#include "distance_resolution_select.h" + +namespace esphome { +namespace ld2412 { + +void DistanceResolutionSelect::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_distance_resolution(state); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/select/distance_resolution_select.h b/esphome/components/ld2412/select/distance_resolution_select.h new file mode 100644 index 0000000000..0658f5d1a7 --- /dev/null +++ b/esphome/components/ld2412/select/distance_resolution_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class DistanceResolutionSelect : public select::Select, public Parented { + public: + DistanceResolutionSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/select/light_out_control_select.cpp b/esphome/components/ld2412/select/light_out_control_select.cpp new file mode 100644 index 0000000000..c331729d40 --- /dev/null +++ b/esphome/components/ld2412/select/light_out_control_select.cpp @@ -0,0 +1,12 @@ +#include "light_out_control_select.h" + +namespace esphome { +namespace ld2412 { + +void LightOutControlSelect::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_light_out_control(); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/select/light_out_control_select.h b/esphome/components/ld2412/select/light_out_control_select.h new file mode 100644 index 0000000000..a71bab1e14 --- /dev/null +++ b/esphome/components/ld2412/select/light_out_control_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class LightOutControlSelect : public select::Select, public Parented { + public: + LightOutControlSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/sensor.py b/esphome/components/ld2412/sensor.py new file mode 100644 index 0000000000..abb823faad --- /dev/null +++ b/esphome/components/ld2412/sensor.py @@ -0,0 +1,124 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_LIGHT, + CONF_MOVING_DISTANCE, + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_ILLUMINANCE, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_FLASH, + ICON_LIGHTBULB, + ICON_MOTION_SENSOR, + ICON_SIGNAL, + UNIT_CENTIMETER, + UNIT_EMPTY, + UNIT_PERCENT, +) + +from . import CONF_LD2412_ID, LD2412Component + +DEPENDENCIES = ["ld2412"] + +CONF_DETECTION_DISTANCE = "detection_distance" +CONF_MOVE_ENERGY = "move_energy" +CONF_MOVING_ENERGY = "moving_energy" +CONF_STILL_DISTANCE = "still_distance" +CONF_STILL_ENERGY = "still_energy" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_SIGNAL, + unit_of_measurement=UNIT_CENTIMETER, + ), + cv.Optional(CONF_LIGHT): sensor.sensor_schema( + device_class=DEVICE_CLASS_ILLUMINANCE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_LIGHTBULB, + unit_of_measurement=UNIT_EMPTY, # No standard unit for this light sensor + ), + cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_SIGNAL, + unit_of_measurement=UNIT_CENTIMETER, + ), + cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, + ), + cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_SIGNAL, + unit_of_measurement=UNIT_CENTIMETER, + ), + cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_FLASH, + unit_of_measurement=UNIT_PERCENT, + ), + } +) + +CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + { + cv.Optional(f"gate_{x}"): ( + { + cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[ + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + ], + icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, + ), + cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[ + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + ], + icon=ICON_FLASH, + unit_of_measurement=UNIT_PERCENT, + ), + } + ) + for x in range(14) + } +) + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if detection_distance_config := config.get(CONF_DETECTION_DISTANCE): + sens = await sensor.new_sensor(detection_distance_config) + cg.add(LD2412_component.set_detection_distance_sensor(sens)) + if light_config := config.get(CONF_LIGHT): + sens = await sensor.new_sensor(light_config) + cg.add(LD2412_component.set_light_sensor(sens)) + if moving_distance_config := config.get(CONF_MOVING_DISTANCE): + sens = await sensor.new_sensor(moving_distance_config) + cg.add(LD2412_component.set_moving_target_distance_sensor(sens)) + if moving_energy_config := config.get(CONF_MOVING_ENERGY): + sens = await sensor.new_sensor(moving_energy_config) + cg.add(LD2412_component.set_moving_target_energy_sensor(sens)) + if still_distance_config := config.get(CONF_STILL_DISTANCE): + sens = await sensor.new_sensor(still_distance_config) + cg.add(LD2412_component.set_still_target_distance_sensor(sens)) + if still_energy_config := config.get(CONF_STILL_ENERGY): + sens = await sensor.new_sensor(still_energy_config) + cg.add(LD2412_component.set_still_target_energy_sensor(sens)) + for x in range(14): + if gate_conf := config.get(f"gate_{x}"): + if move_config := gate_conf.get(CONF_MOVE_ENERGY): + sens = await sensor.new_sensor(move_config) + cg.add(LD2412_component.set_gate_move_sensor(x, sens)) + if still_config := gate_conf.get(CONF_STILL_ENERGY): + sens = await sensor.new_sensor(still_config) + cg.add(LD2412_component.set_gate_still_sensor(x, sens)) diff --git a/esphome/components/ld2412/switch/__init__.py b/esphome/components/ld2412/switch/__init__.py new file mode 100644 index 0000000000..df994687ec --- /dev/null +++ b/esphome/components/ld2412/switch/__init__.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +from esphome.components import switch +import esphome.config_validation as cv +from esphome.const import ( + CONF_BLUETOOTH, + DEVICE_CLASS_SWITCH, + ENTITY_CATEGORY_CONFIG, + ICON_BLUETOOTH, + ICON_PULSE, +) + +from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component + +BluetoothSwitch = LD2412_ns.class_("BluetoothSwitch", switch.Switch) +EngineeringModeSwitch = LD2412_ns.class_("EngineeringModeSwitch", switch.Switch) + +CONF_ENGINEERING_MODE = "engineering_mode" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_BLUETOOTH): switch.switch_schema( + BluetoothSwitch, + device_class=DEVICE_CLASS_SWITCH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_BLUETOOTH, + ), + cv.Optional(CONF_ENGINEERING_MODE): switch.switch_schema( + EngineeringModeSwitch, + device_class=DEVICE_CLASS_SWITCH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_PULSE, + ), +} + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if bluetooth_config := config.get(CONF_BLUETOOTH): + s = await switch.new_switch(bluetooth_config) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_bluetooth_switch(s)) + if engineering_mode_config := config.get(CONF_ENGINEERING_MODE): + s = await switch.new_switch(engineering_mode_config) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_engineering_mode_switch(s)) diff --git a/esphome/components/ld2412/switch/bluetooth_switch.cpp b/esphome/components/ld2412/switch/bluetooth_switch.cpp new file mode 100644 index 0000000000..14387aa276 --- /dev/null +++ b/esphome/components/ld2412/switch/bluetooth_switch.cpp @@ -0,0 +1,12 @@ +#include "bluetooth_switch.h" + +namespace esphome { +namespace ld2412 { + +void BluetoothSwitch::write_state(bool state) { + this->publish_state(state); + this->parent_->set_bluetooth(state); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/switch/bluetooth_switch.h b/esphome/components/ld2412/switch/bluetooth_switch.h new file mode 100644 index 0000000000..730d338d87 --- /dev/null +++ b/esphome/components/ld2412/switch/bluetooth_switch.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class BluetoothSwitch : public switch_::Switch, public Parented { + public: + BluetoothSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/switch/engineering_mode_switch.cpp b/esphome/components/ld2412/switch/engineering_mode_switch.cpp new file mode 100644 index 0000000000..29ca0c22a8 --- /dev/null +++ b/esphome/components/ld2412/switch/engineering_mode_switch.cpp @@ -0,0 +1,12 @@ +#include "engineering_mode_switch.h" + +namespace esphome { +namespace ld2412 { + +void EngineeringModeSwitch::write_state(bool state) { + this->publish_state(state); + this->parent_->set_engineering_mode(state); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/switch/engineering_mode_switch.h b/esphome/components/ld2412/switch/engineering_mode_switch.h new file mode 100644 index 0000000000..aaa404c673 --- /dev/null +++ b/esphome/components/ld2412/switch/engineering_mode_switch.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class EngineeringModeSwitch : public switch_::Switch, public Parented { + public: + EngineeringModeSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/text_sensor.py b/esphome/components/ld2412/text_sensor.py new file mode 100644 index 0000000000..1074494933 --- /dev/null +++ b/esphome/components/ld2412/text_sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_MAC_ADDRESS, + CONF_VERSION, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_BLUETOOTH, + ICON_CHIP, +) + +from . import CONF_LD2412_ID, LD2412Component + +DEPENDENCIES = ["ld2412"] + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP + ), + cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_BLUETOOTH + ), +} + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if version_config := config.get(CONF_VERSION): + sens = await text_sensor.new_text_sensor(version_config) + cg.add(LD2412_component.set_version_text_sensor(sens)) + if mac_address_config := config.get(CONF_MAC_ADDRESS): + sens = await text_sensor.new_text_sensor(mac_address_config) + cg.add(LD2412_component.set_mac_text_sensor(sens)) diff --git a/tests/components/ld2412/common.yaml b/tests/components/ld2412/common.yaml new file mode 100644 index 0000000000..9176c61fd5 --- /dev/null +++ b/tests/components/ld2412/common.yaml @@ -0,0 +1,233 @@ +uart: + - id: uart_ld2412 + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 + +ld2412: + id: my_ld2412 + +binary_sensor: + - platform: ld2412 + dynamic_background_correction_status: + name: Dynamic Background Correction Status + has_target: + name: Presence + has_moving_target: + name: Moving Target + has_still_target: + name: Still Target + +button: + - platform: ld2412 + factory_reset: + name: Factory reset + restart: + name: Restart + query_params: + name: Query params + start_dynamic_background_correction: + name: Start Dynamic Background Correction + +number: + - platform: ld2412 + light_threshold: + name: Light Threshold + timeout: + name: Presence timeout + min_distance_gate: + name: Minimum distance gate + max_distance_gate: + name: Maximum distance gate + gate_0: + move_threshold: + name: Gate 0 Move Threshold + still_threshold: + name: Gate 0 Still Threshold + gate_1: + move_threshold: + name: Gate 1 Move Threshold + still_threshold: + name: Gate 1 Still Threshold + gate_2: + move_threshold: + name: Gate 2 Move Threshold + still_threshold: + name: Gate 2 Still Threshold + gate_3: + move_threshold: + name: Gate 3 Move Threshold + still_threshold: + name: Gate 3 Still Threshold + gate_4: + move_threshold: + name: Gate 4 Move Threshold + still_threshold: + name: Gate 4 Still Threshold + gate_5: + move_threshold: + name: Gate 5 Move Threshold + still_threshold: + name: Gate 5 Still Threshold + gate_6: + move_threshold: + name: Gate 6 Move Threshold + still_threshold: + name: Gate 6 Still Threshold + gate_7: + move_threshold: + name: Gate 7 Move Threshold + still_threshold: + name: Gate 7 Still Threshold + gate_8: + move_threshold: + name: Gate 8 Move Threshold + still_threshold: + name: Gate 8 Still Threshold + gate_9: + move_threshold: + name: Gate 9 Move Threshold + still_threshold: + name: Gate 9 Still Threshold + gate_10: + move_threshold: + name: Gate 10 Move Threshold + still_threshold: + name: Gate 10 Still Threshold + gate_11: + move_threshold: + name: Gate 11 Move Threshold + still_threshold: + name: Gate 11 Still Threshold + gate_12: + move_threshold: + name: Gate 12 Move Threshold + still_threshold: + name: Gate 12 Still Threshold + gate_13: + move_threshold: + name: Gate 13 Move Threshold + still_threshold: + name: Gate 13 Still Threshold + +select: + - platform: ld2412 + light_function: + name: Light Function + out_pin_level: + name: Hardware output pin level + distance_resolution: + name: Distance resolution + baud_rate: + name: Baud rate + on_value: + - delay: 3s + - lambda: |- + id(uart_ld2412).flush(); + uint32_t new_baud_rate = stoi(x); + ESP_LOGD("change_baud_rate", "Changing baud rate from %i to %i",id(uart_ld2412).get_baud_rate(), new_baud_rate); + if (id(uart_ld2412).get_baud_rate() != new_baud_rate) { + id(uart_ld2412).set_baud_rate(new_baud_rate); + #if defined(USE_ESP8266) || defined(USE_ESP32) + id(uart_ld2412).load_settings(); + #endif + } + +sensor: + - platform: ld2412 + light: + name: Light + moving_distance: + name: Moving Distance + still_distance: + name: Still Distance + moving_energy: + name: Move Energy + still_energy: + name: Still Energy + detection_distance: + name: Detection Distance + gate_0: + move_energy: + name: Gate 0 Move Energy + still_energy: + name: Gate 0 Still Energy + gate_1: + move_energy: + name: Gate 1 Move Energy + still_energy: + name: Gate 1 Still Energy + gate_2: + move_energy: + name: Gate 2 Move Energy + still_energy: + name: Gate 2 Still Energy + gate_3: + move_energy: + name: Gate 3 Move Energy + still_energy: + name: Gate 3 Still Energy + gate_4: + move_energy: + name: Gate 4 Move Energy + still_energy: + name: Gate 4 Still Energy + gate_5: + move_energy: + name: Gate 5 Move Energy + still_energy: + name: Gate 5 Still Energy + gate_6: + move_energy: + name: Gate 6 Move Energy + still_energy: + name: Gate 6 Still Energy + gate_7: + move_energy: + name: Gate 7 Move Energy + still_energy: + name: Gate 7 Still Energy + gate_8: + move_energy: + name: Gate 8 Move Energy + still_energy: + name: Gate 8 Still Energy + gate_9: + move_energy: + name: Gate 9 Move Energy + still_energy: + name: Gate 9 Still Energy + gate_10: + move_energy: + name: Gate 10 Move Energy + still_energy: + name: Gate 10 Still Energy + gate_11: + move_energy: + name: Gate 11 Move Energy + still_energy: + name: Gate 11 Still Energy + gate_12: + move_energy: + name: Gate 12 Move Energy + still_energy: + name: Gate 12 Still Energy + gate_13: + move_energy: + name: Gate 13 Move Energy + still_energy: + name: Gate 13 Still Energy + +switch: + - platform: ld2412 + bluetooth: + name: Bluetooth + engineering_mode: + name: Engineering Mode + +text_sensor: + - platform: ld2412 + version: + name: Firmware version + mac_address: + name: MAC address diff --git a/tests/components/ld2412/test.esp32-ard.yaml b/tests/components/ld2412/test.esp32-ard.yaml new file mode 100644 index 0000000000..f486544afa --- /dev/null +++ b/tests/components/ld2412/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/ld2412/test.esp32-c3-ard.yaml b/tests/components/ld2412/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/ld2412/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ld2412/test.esp32-c3-idf.yaml b/tests/components/ld2412/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/ld2412/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ld2412/test.esp32-idf.yaml b/tests/components/ld2412/test.esp32-idf.yaml new file mode 100644 index 0000000000..f486544afa --- /dev/null +++ b/tests/components/ld2412/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/ld2412/test.esp8266-ard.yaml b/tests/components/ld2412/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/ld2412/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ld2412/test.rp2040-ard.yaml b/tests/components/ld2412/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/ld2412/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml