[ld6002b] Add switch, number and text sensor platforms (3/5) (#17821)

Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
Mustafa KURU
2026-08-07 11:11:29 -04:00
committed by GitHub
co-authored by Jonathan Swoboda
parent 1bbe8b415f
commit b802740997
12 changed files with 949 additions and 27 deletions
+10
View File
@@ -1,8 +1,18 @@
CONF_AUTO_WAKE = "auto_wake"
CONF_CLUSTER_ID = "cluster_id"
CONF_DOPPLER_INDEX = "doppler_index"
CONF_HOLD_DELAY = "hold_delay"
CONF_LD6002B_ID = "ld6002b_id"
CONF_LOW_POWER = "low_power"
CONF_LOW_POWER_SLEEP_TIME = "low_power_sleep_time"
CONF_OTA_VERSION = "ota_version"
CONF_POINT_CLOUD = "point_cloud"
CONF_POINT_COUNT = "point_count"
CONF_TARGET_DISPLAY = "target_display"
CONF_WAKEUP_PULSE = "wakeup_pulse"
CONF_WORK_MODE = "work_mode"
CONF_Z = "z"
CONF_Z_MAX = "z_max"
CONF_Z_MIN = "z_min"
MAX_TARGETS = 3
File diff suppressed because it is too large Load Diff
+105
View File
@@ -11,16 +11,41 @@
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/core/preferences.h"
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#endif
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#include <array>
#include <cmath>
namespace esphome::ld6002b {
static constexpr uint8_t MAX_TARGETS = 3;
static constexpr size_t DEFAULT_MAX_DATA_LEN = 1024;
static constexpr size_t DEFAULT_MAX_DATA_LEN_POINT_CLOUD = 4096;
// Largest protocol payload is TYPE_SET_AREA: int32 area id + 6 floats = 28 bytes.
static constexpr size_t CMD_MAX_DATA_LEN = 28;
enum class NumberType : uint8_t {
HOLD_DELAY,
Z_MIN,
Z_MAX,
LOW_POWER_SLEEP,
};
enum class SwitchType : uint8_t {
LOW_POWER,
POINT_CLOUD,
TARGET_DISPLAY,
};
#ifdef USE_SENSOR
struct TargetSensors {
sensor::Sensor *x{nullptr};
@@ -32,6 +57,10 @@ struct TargetSensors {
#endif
struct VersionPref {
char value[20];
};
class LD6002BComponent : public Component, public uart::UARTDevice {
public:
void setup() override;
@@ -45,6 +74,7 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
#ifdef USE_SENSOR
void set_target_count_sensor(sensor::Sensor *sensor) { this->target_count_sensor_ = sensor; }
void set_point_count_sensor(sensor::Sensor *sensor) { this->point_count_sensor_ = sensor; }
void set_target_x_sensor(uint8_t target, sensor::Sensor *sensor) {
if (target >= MAX_TARGETS)
@@ -82,6 +112,27 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
}
#endif
#ifdef USE_TEXT_SENSOR
void set_work_mode_text_sensor(text_sensor::TextSensor *sensor) { this->work_mode_text_sensor_ = sensor; }
void set_ota_version_text_sensor(text_sensor::TextSensor *sensor) { this->ota_version_text_sensor_ = sensor; }
#endif
#ifdef USE_NUMBER
void set_hold_delay_number(number::Number *number) { this->hold_delay_number_ = number; }
void set_z_min_number(number::Number *number) { this->z_min_number_ = number; }
void set_z_max_number(number::Number *number) { this->z_max_number_ = number; }
void set_low_power_sleep_number(number::Number *number) { this->low_power_sleep_number_ = number; }
#endif
#ifdef USE_SWITCH
void set_low_power_switch(switch_::Switch *sw) { this->low_power_switch_ = sw; }
void set_point_cloud_switch(switch_::Switch *sw) { this->point_cloud_switch_ = sw; }
void set_target_display_switch(switch_::Switch *sw) { this->target_display_switch_ = sw; }
#endif
void set_number_value(NumberType type, float value);
void set_switch_state(SwitchType type, bool state);
protected:
enum class ParseState : uint8_t { SOF, HEADER, HCK, DATA, DCK, DISCARD };
@@ -95,6 +146,25 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
void reset_parser_();
void handle_frame_(uint16_t type, const uint8_t *data, uint16_t len);
void handle_target_report_(const uint8_t *data, uint16_t len);
void handle_point_cloud_(const uint8_t *data, uint16_t len);
void handle_delay_report_(const uint8_t *data, uint16_t len);
void handle_z_range_report_(const uint8_t *data, uint16_t len);
void handle_low_power_report_(const uint8_t *data, uint16_t len);
void handle_low_power_sleep_report_(const uint8_t *data, uint16_t len);
void handle_work_mode_report_(const uint8_t *data, uint16_t len);
void handle_version_report_(const uint8_t *data, uint16_t len);
void update_work_mode_fallback_();
void publish_work_mode_(bool low_power);
// Drops every target-derived reading and the slot table they are indexed by.
void clear_target_state_();
#ifdef USE_SENSOR
void clear_target_slot_(uint8_t index);
#endif
#ifdef USE_NUMBER
void publish_number_clamped_(number::Number *number, float value);
#endif
void init_version_pref_();
void save_version_pref_(const char *value);
void queue_command_(uint16_t type, const uint8_t *data, uint8_t len);
void process_command_queue_();
@@ -102,21 +172,41 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
void send_command_internal_(uint16_t type, const uint8_t *data, uint8_t len, bool track);
void write_frame_(uint16_t type, const uint8_t *data, uint8_t len, bool track);
void send_control_command_(uint32_t command);
void send_z_range_();
static uint16_t read_u16_be(const uint8_t *data);
static uint32_t read_u32_le(const uint8_t *data);
static int32_t read_int32_le(const uint8_t *data);
static float read_f32_le(const uint8_t *data);
static void write_u32_le(uint8_t *data, uint32_t value);
static void write_f32_le(uint8_t *data, float value);
#ifdef USE_SENSOR
std::array<TargetSensors, MAX_TARGETS> targets_{};
sensor::Sensor *target_count_sensor_{nullptr};
sensor::Sensor *point_count_sensor_{nullptr};
#endif
#ifdef USE_BINARY_SENSOR
binary_sensor::BinarySensor *presence_binary_sensor_{nullptr};
std::array<binary_sensor::BinarySensor *, MAX_TARGETS> target_presence_{};
#endif
#ifdef USE_TEXT_SENSOR
text_sensor::TextSensor *work_mode_text_sensor_{nullptr};
text_sensor::TextSensor *ota_version_text_sensor_{nullptr};
ESPPreferenceObject version_pref_{};
bool version_pref_initialized_{false};
#endif
#ifdef USE_NUMBER
number::Number *hold_delay_number_{nullptr};
number::Number *z_min_number_{nullptr};
number::Number *z_max_number_{nullptr};
number::Number *low_power_sleep_number_{nullptr};
#endif
#ifdef USE_SWITCH
switch_::Switch *low_power_switch_{nullptr};
switch_::Switch *point_cloud_switch_{nullptr};
switch_::Switch *target_display_switch_{nullptr};
#endif
GPIOPin *wakeup_pin_{nullptr};
uint32_t wakeup_pulse_ms_{50};
@@ -132,6 +222,7 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
uint8_t data_xor_{0};
uint32_t discard_remaining_{0};
bool frame_oversize_{false};
size_t max_data_len_{0};
uint8_t *data_buf_{nullptr};
uint16_t next_frame_id_{0};
@@ -173,11 +264,24 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
// Bumped whenever the active command changes, so a deferred send can tell it was retired.
uint8_t send_generation_{0};
float z_min_{NAN};
float z_max_{NAN};
// Which person owns each target_N slot, so a slot survives the module re-sorting its array.
std::array<int32_t, MAX_TARGETS> slot_cluster_{};
std::array<bool, MAX_TARGETS> slot_occupied_{};
bool target_presence_any_{false};
// What the switches and setup asked the module for, which is not the same as
// what it is doing yet: a stream keeps sending until it acts on the command.
// The report handlers read these and drop anything a stopped stream still emits.
bool target_display_enabled_{false};
bool point_cloud_enabled_{false};
bool work_mode_reported_{false};
bool low_power_enabled_{false};
bool low_power_reported_{false};
bool last_work_mode_valid_{false};
bool last_work_mode_low_power_{false};
#ifdef USE_SENSOR
std::array<bool, MAX_TARGETS> last_target_presence_{}; // one-shot NAN clear for target sensors
@@ -185,6 +289,7 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
std::array<int32_t, MAX_TARGETS> last_cluster_id_{};
std::array<bool, MAX_TARGETS> last_cluster_id_valid_{};
uint32_t last_target_count_{0xFFFFFFFF};
uint32_t last_point_count_{0xFFFFFFFF};
#endif
};
@@ -0,0 +1,82 @@
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import (
DEVICE_CLASS_DISTANCE,
DEVICE_CLASS_DURATION,
ENTITY_CATEGORY_CONFIG,
UNIT_METER,
UNIT_MILLISECOND,
UNIT_SECOND,
)
from .. import LD6002BComponent, ld6002b_ns
from ..const import (
CONF_HOLD_DELAY,
CONF_LD6002B_ID,
CONF_LOW_POWER_SLEEP_TIME,
CONF_Z_MAX,
CONF_Z_MIN,
)
DEPENDENCIES = ["ld6002b"]
LD6002BNumber = ld6002b_ns.class_("LD6002BNumber", number.Number)
NumberType = ld6002b_ns.enum("NumberType", is_class=True)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
cv.Optional(CONF_HOLD_DELAY): number.number_schema(
LD6002BNumber,
unit_of_measurement=UNIT_SECOND,
device_class=DEVICE_CLASS_DURATION,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_Z_MIN): number.number_schema(
LD6002BNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_Z_MAX): number.number_schema(
LD6002BNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_LOW_POWER_SLEEP_TIME): number.number_schema(
LD6002BNumber,
unit_of_measurement=UNIT_MILLISECOND,
device_class=DEVICE_CLASS_DURATION,
entity_category=ENTITY_CATEGORY_CONFIG,
),
}
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_LD6002B_ID])
for key, number_type, setter, min_value, max_value, step in (
(CONF_HOLD_DELAY, NumberType.HOLD_DELAY, "set_hold_delay_number", 0, 65535, 1),
(CONF_Z_MIN, NumberType.Z_MIN, "set_z_min_number", -10, 10, 0.1),
(CONF_Z_MAX, NumberType.Z_MAX, "set_z_max_number", -10, 10, 0.1),
# 0x0205 carries a uint32 of milliseconds; the vendor documents 500 ms as
# the default and no upper bound, so the range ends at a minute rather
# than at a default the module is free to be sleeping past.
(
CONF_LOW_POWER_SLEEP_TIME,
NumberType.LOW_POWER_SLEEP,
"set_low_power_sleep_number",
0,
60000,
100,
),
):
if conf := config.get(key):
n = await number.new_number(
conf, number_type, min_value=min_value, max_value=max_value, step=step
)
await cg.register_parented(n, config[CONF_LD6002B_ID])
cg.add(getattr(hub, setter)(n))
@@ -0,0 +1,10 @@
#include "ld6002b_number.h"
namespace esphome::ld6002b {
void LD6002BNumber::control(float value) {
this->publish_state(value);
this->parent_->set_number_value(this->type_, value);
}
} // namespace esphome::ld6002b
@@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld6002b.h"
namespace esphome::ld6002b {
class LD6002BNumber : public number::Number, public Parented<LD6002BComponent> {
public:
explicit LD6002BNumber(NumberType type) : type_(type) {}
protected:
void control(float value) override;
NumberType type_;
};
} // namespace esphome::ld6002b
+9
View File
@@ -15,6 +15,7 @@ from .const import (
CONF_CLUSTER_ID,
CONF_DOPPLER_INDEX,
CONF_LD6002B_ID,
CONF_POINT_COUNT,
CONF_Z,
MAX_TARGETS,
)
@@ -75,6 +76,10 @@ CONFIG_SCHEMA = cv.Schema(
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POINT_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
}
).extend({cv.Optional(f"target_{i + 1}"): TARGET_SCHEMA for i in range(MAX_TARGETS)})
@@ -86,6 +91,10 @@ async def to_code(config):
sens = await sensor.new_sensor(target_count_config)
cg.add(hub.set_target_count_sensor(sens))
if point_count_config := config.get(CONF_POINT_COUNT):
sens = await sensor.new_sensor(point_count_config)
cg.add(hub.set_point_count_sensor(sens))
for i in range(MAX_TARGETS):
if target_config := config.get(f"target_{i + 1}"):
if x_config := target_config.get(CONF_X):
@@ -0,0 +1,60 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import DEVICE_CLASS_SWITCH, ENTITY_CATEGORY_CONFIG
from .. import LD6002BComponent, ld6002b_ns
from ..const import (
CONF_LD6002B_ID,
CONF_LOW_POWER,
CONF_POINT_CLOUD,
CONF_TARGET_DISPLAY,
)
DEPENDENCIES = ["ld6002b"]
LD6002BSwitch = ld6002b_ns.class_("LD6002BSwitch", switch.Switch)
SwitchType = ld6002b_ns.enum("SwitchType", is_class=True)
# None of these three carry an inversion. They name what the module is doing, not
# how something is wired to it, so an inverted one would only report the opposite
# of the truth -- and the boot restore, which applies a state nothing reports back,
# is where that would be hardest to spot.
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
cv.Optional(CONF_LOW_POWER): switch.switch_schema(
LD6002BSwitch,
block_inverted=True,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_POINT_CLOUD): switch.switch_schema(
LD6002BSwitch,
block_inverted=True,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_TARGET_DISPLAY): switch.switch_schema(
LD6002BSwitch,
block_inverted=True,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
default_restore_mode="RESTORE_DEFAULT_ON",
),
}
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_LD6002B_ID])
for key, switch_type, setter in (
(CONF_LOW_POWER, SwitchType.LOW_POWER, "set_low_power_switch"),
(CONF_POINT_CLOUD, SwitchType.POINT_CLOUD, "set_point_cloud_switch"),
(CONF_TARGET_DISPLAY, SwitchType.TARGET_DISPLAY, "set_target_display_switch"),
):
if conf := config.get(key):
s = await switch.new_switch(conf, switch_type)
await cg.register_parented(s, config[CONF_LD6002B_ID])
cg.add(getattr(hub, setter)(s))
@@ -0,0 +1,10 @@
#include "ld6002b_switch.h"
namespace esphome::ld6002b {
void LD6002BSwitch::write_state(bool state) {
this->parent_->set_switch_state(this->type_, state);
this->publish_state(state);
}
} // namespace esphome::ld6002b
@@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../ld6002b.h"
namespace esphome::ld6002b {
class LD6002BSwitch : public switch_::Switch, public Parented<LD6002BComponent> {
public:
explicit LD6002BSwitch(SwitchType type) : type_(type) {}
protected:
void write_state(bool state) override;
SwitchType type_;
};
} // namespace esphome::ld6002b
+31
View File
@@ -0,0 +1,31 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_DIAGNOSTIC
from . import LD6002BComponent
from .const import CONF_LD6002B_ID, CONF_OTA_VERSION, CONF_WORK_MODE
DEPENDENCIES = ["ld6002b"]
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
cv.Optional(CONF_WORK_MODE): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_OTA_VERSION): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
}
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_LD6002B_ID])
if work_mode_config := config.get(CONF_WORK_MODE):
sens = await text_sensor.new_text_sensor(work_mode_config)
cg.add(hub.set_work_mode_text_sensor(sens))
if ota_config := config.get(CONF_OTA_VERSION):
sens = await text_sensor.new_text_sensor(ota_config)
cg.add(hub.set_ota_version_text_sensor(sens))
+32
View File
@@ -7,6 +7,8 @@ sensor:
ld6002b_id: ld6002b_radar
target_count:
name: Target Count
point_count:
name: Point Count
target_1:
x:
name: Target-1 X
@@ -48,3 +50,33 @@ binary_sensor:
name: Presence
target_1:
name: Target-1 Presence
text_sensor:
- platform: ld6002b
ld6002b_id: ld6002b_radar
work_mode:
name: Work Mode
ota_version:
name: OTA Version
number:
- platform: ld6002b
ld6002b_id: ld6002b_radar
hold_delay:
name: Hold Delay
z_min:
name: Z Min
z_max:
name: Z Max
low_power_sleep_time:
name: Low Power Sleep
switch:
- platform: ld6002b
ld6002b_id: ld6002b_radar
low_power:
name: Low Power
point_cloud:
name: Point Cloud
target_display:
name: Target Display