mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 03:16:37 +00:00
[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:
co-authored by
Jonathan Swoboda
parent
1bbe8b415f
commit
b802740997
@@ -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
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::ld6002b {
|
||||
@@ -14,20 +15,40 @@ static constexpr uint32_t SETUP_DELAY_MS = 100;
|
||||
|
||||
// Command/message types
|
||||
static constexpr uint16_t TYPE_CONTROL = 0x0201;
|
||||
static constexpr uint16_t TYPE_SET_HOLD_DELAY = 0x0203;
|
||||
static constexpr uint16_t TYPE_SET_Z_RANGE = 0x0204;
|
||||
static constexpr uint16_t TYPE_SET_LOW_POWER_SLEEP = 0x0205;
|
||||
|
||||
static constexpr uint16_t TYPE_REPORT_TARGET = 0x0A04;
|
||||
static constexpr uint16_t TYPE_REPORT_POINT_CLOUD = 0x0A08;
|
||||
static constexpr uint16_t TYPE_REPORT_DELAY = 0x0A0D;
|
||||
static constexpr uint16_t TYPE_REPORT_Z_RANGE = 0x0A10;
|
||||
static constexpr uint16_t TYPE_REPORT_LOW_POWER = 0x0A12;
|
||||
static constexpr uint16_t TYPE_REPORT_LOW_POWER_SLEEP = 0x0A13;
|
||||
static constexpr uint16_t TYPE_REPORT_WORK_MODE = 0x0A14;
|
||||
static constexpr uint16_t TYPE_QUERY_VERSION = 0xFFFF;
|
||||
|
||||
// Control command values for TYPE_CONTROL
|
||||
static constexpr uint32_t CMD_GET_DELAY = 0x05;
|
||||
static constexpr uint32_t CMD_POINT_CLOUD_ON = 0x06;
|
||||
static constexpr uint32_t CMD_POINT_CLOUD_OFF = 0x07;
|
||||
static constexpr uint32_t CMD_TARGET_DISPLAY_ON = 0x08;
|
||||
static constexpr uint32_t CMD_TARGET_DISPLAY_OFF = 0x09;
|
||||
static constexpr uint32_t CMD_GET_Z_RANGE = 0x12;
|
||||
static constexpr uint32_t CMD_LOW_POWER_ON = 0x16;
|
||||
static constexpr uint32_t CMD_LOW_POWER_OFF = 0x17;
|
||||
static constexpr uint32_t CMD_GET_LOW_POWER = 0x18;
|
||||
static constexpr uint32_t CMD_GET_LOW_POWER_SLEEP = 0x19;
|
||||
|
||||
static constexpr uint16_t TARGET_DATA_LEN = 20; // x,y,z,dop_idx,cluster_id
|
||||
|
||||
static constexpr uint8_t VERSION_QUERY_DATA[] = {0x01, 0x01, 0x00, 0x00};
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
static const char *control_command_name(uint32_t command) {
|
||||
switch (command) {
|
||||
case CMD_GET_DELAY:
|
||||
return "get_delay";
|
||||
case CMD_POINT_CLOUD_ON:
|
||||
return "point_cloud_on";
|
||||
case CMD_POINT_CLOUD_OFF:
|
||||
@@ -36,10 +57,68 @@ static const char *control_command_name(uint32_t command) {
|
||||
return "target_display_on";
|
||||
case CMD_TARGET_DISPLAY_OFF:
|
||||
return "target_display_off";
|
||||
case CMD_GET_Z_RANGE:
|
||||
return "get_z_range";
|
||||
case CMD_LOW_POWER_ON:
|
||||
return "low_power_on";
|
||||
case CMD_LOW_POWER_OFF:
|
||||
return "low_power_off";
|
||||
case CMD_GET_LOW_POWER:
|
||||
return "get_low_power";
|
||||
case CMD_GET_LOW_POWER_SLEEP:
|
||||
return "get_low_power_sleep";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static const char *frame_type_name(uint16_t type) {
|
||||
switch (type) {
|
||||
case TYPE_CONTROL:
|
||||
return "control";
|
||||
case TYPE_SET_HOLD_DELAY:
|
||||
return "set_hold_delay";
|
||||
case TYPE_SET_Z_RANGE:
|
||||
return "set_z_range";
|
||||
case TYPE_SET_LOW_POWER_SLEEP:
|
||||
return "set_low_power_sleep";
|
||||
case TYPE_REPORT_TARGET:
|
||||
return "report_target";
|
||||
case TYPE_REPORT_POINT_CLOUD:
|
||||
return "report_point_cloud";
|
||||
case TYPE_REPORT_DELAY:
|
||||
return "report_delay";
|
||||
case TYPE_REPORT_Z_RANGE:
|
||||
return "report_z_range";
|
||||
case TYPE_REPORT_LOW_POWER:
|
||||
return "report_low_power";
|
||||
case TYPE_REPORT_LOW_POWER_SLEEP:
|
||||
return "report_low_power_sleep";
|
||||
case TYPE_REPORT_WORK_MODE:
|
||||
return "report_work_mode";
|
||||
case TYPE_QUERY_VERSION:
|
||||
return "query_version";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static bool is_expected_control_report(uint32_t command, uint16_t type) {
|
||||
switch (command) {
|
||||
case CMD_GET_DELAY:
|
||||
return type == TYPE_REPORT_DELAY;
|
||||
case CMD_GET_Z_RANGE:
|
||||
return type == TYPE_REPORT_Z_RANGE;
|
||||
case CMD_GET_LOW_POWER:
|
||||
case CMD_LOW_POWER_ON:
|
||||
case CMD_LOW_POWER_OFF:
|
||||
return type == TYPE_REPORT_LOW_POWER;
|
||||
case CMD_GET_LOW_POWER_SLEEP:
|
||||
return type == TYPE_REPORT_LOW_POWER_SLEEP;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
uint16_t LD6002BComponent::read_u16_be(const uint8_t *data) { return (static_cast<uint16_t>(data[0]) << 8) | data[1]; }
|
||||
@@ -70,10 +149,25 @@ void LD6002BComponent::write_u32_le(uint8_t *data, uint32_t value) {
|
||||
data[3] = (value >> 24) & 0xFF;
|
||||
}
|
||||
|
||||
void LD6002BComponent::write_f32_le(uint8_t *data, float value) {
|
||||
uint32_t raw;
|
||||
std::memcpy(&raw, &value, sizeof(raw));
|
||||
write_u32_le(data, raw);
|
||||
}
|
||||
|
||||
void LD6002BComponent::setup() {
|
||||
// Only the point cloud stream needs the larger frame; nothing resizes the buffer after setup.
|
||||
bool point_cloud_configured = false;
|
||||
#ifdef USE_SENSOR
|
||||
point_cloud_configured = point_cloud_configured || this->point_count_sensor_ != nullptr;
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
point_cloud_configured = point_cloud_configured || this->point_cloud_switch_ != nullptr;
|
||||
#endif
|
||||
this->max_data_len_ = point_cloud_configured ? DEFAULT_MAX_DATA_LEN_POINT_CLOUD : DEFAULT_MAX_DATA_LEN;
|
||||
// One allocation for the component lifetime; the parser reuses it for the header and every payload.
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
this->data_buf_ = allocator.allocate(DEFAULT_MAX_DATA_LEN);
|
||||
this->data_buf_ = allocator.allocate(this->max_data_len_);
|
||||
if (this->data_buf_ == nullptr) {
|
||||
this->mark_failed(LOG_STR("Failed to allocate frame buffer"));
|
||||
return;
|
||||
@@ -108,25 +202,120 @@ void LD6002BComponent::setup() {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (want_target_stream) {
|
||||
this->send_control_command_(CMD_TARGET_DISPLAY_ON);
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
// The work mode fallback reads presence off this stream, so it counts as a
|
||||
// consumer of it here. This only feeds the automatic branch below: with a
|
||||
// target_display switch configured that switch still decides, and the
|
||||
// fallback weighs no presence at all while the stream is off.
|
||||
want_target_stream = want_target_stream || this->work_mode_text_sensor_ != nullptr;
|
||||
#endif
|
||||
bool target_display_controlled = false;
|
||||
#ifdef USE_SWITCH
|
||||
if (this->target_display_switch_ != nullptr) {
|
||||
target_display_controlled = true;
|
||||
// Nothing reports this switch back, so its restored state is the only state
|
||||
// there is. Restoring through the switch keeps its inversion in the path:
|
||||
// the restored value is logical, and turn_on()/turn_off() are what turn it
|
||||
// into the raw command, the published state and the stream flag.
|
||||
const bool state = this->target_display_switch_->get_initial_state_with_restore_mode().value_or(true);
|
||||
if (state) {
|
||||
this->target_display_switch_->turn_on();
|
||||
} else {
|
||||
this->target_display_switch_->turn_off();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!target_display_controlled) {
|
||||
// No switch: the stream follows its consumers. With none, nothing is sent
|
||||
// and the module's own default stands -- but the reports are gated out
|
||||
// regardless, because there is nothing configured for them to feed.
|
||||
this->target_display_enabled_ = want_target_stream;
|
||||
if (want_target_stream) {
|
||||
this->send_control_command_(CMD_TARGET_DISPLAY_ON);
|
||||
}
|
||||
}
|
||||
|
||||
this->send_control_command_(CMD_POINT_CLOUD_OFF);
|
||||
bool point_cloud_controlled = false;
|
||||
#ifdef USE_SWITCH
|
||||
if (this->point_cloud_switch_ != nullptr) {
|
||||
point_cloud_controlled = true;
|
||||
// The switch owns the stream, so it is also what applies the restored state:
|
||||
// driving it rather than the module keeps the entity's inversion in the path.
|
||||
const bool state = this->point_cloud_switch_->get_initial_state_with_restore_mode().value_or(false);
|
||||
if (state) {
|
||||
this->point_cloud_switch_->turn_on();
|
||||
} else {
|
||||
this->point_cloud_switch_->turn_off();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!point_cloud_controlled) {
|
||||
// No switch: the stream follows the sensor that reads it, which is also what
|
||||
// the frame buffer above was sized for.
|
||||
bool want_point_cloud = false;
|
||||
#ifdef USE_SENSOR
|
||||
want_point_cloud = this->point_count_sensor_ != nullptr;
|
||||
#endif
|
||||
this->send_control_command_(want_point_cloud ? CMD_POINT_CLOUD_ON : CMD_POINT_CLOUD_OFF);
|
||||
this->point_cloud_enabled_ = want_point_cloud;
|
||||
}
|
||||
#ifdef USE_NUMBER
|
||||
if (this->z_min_number_ != nullptr || this->z_max_number_ != nullptr) {
|
||||
this->send_control_command_(CMD_GET_Z_RANGE);
|
||||
}
|
||||
if (this->low_power_sleep_number_ != nullptr) {
|
||||
this->send_control_command_(CMD_GET_LOW_POWER_SLEEP);
|
||||
}
|
||||
if (this->hold_delay_number_ != nullptr) {
|
||||
this->send_control_command_(CMD_GET_DELAY);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
bool want_low_power = this->low_power_switch_ != nullptr;
|
||||
if (want_low_power) {
|
||||
// The module reports this one back, so the query below confirms what it took.
|
||||
// Driving the switch applies its inversion; it also marks the restored value
|
||||
// as reported, so the work mode fallback runs on that until the query lands.
|
||||
const bool state = this->low_power_switch_->get_initial_state_with_restore_mode().value_or(false);
|
||||
if (state) {
|
||||
this->low_power_switch_->turn_on();
|
||||
} else {
|
||||
this->low_power_switch_->turn_off();
|
||||
}
|
||||
}
|
||||
#else
|
||||
bool want_low_power = false;
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
want_low_power = want_low_power || this->work_mode_text_sensor_ != nullptr;
|
||||
#endif
|
||||
if (want_low_power) {
|
||||
this->send_control_command_(CMD_GET_LOW_POWER);
|
||||
}
|
||||
|
||||
this->init_version_pref_();
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->ota_version_text_sensor_ != nullptr) {
|
||||
this->queue_command_(TYPE_QUERY_VERSION, VERSION_QUERY_DATA, sizeof(VERSION_QUERY_DATA));
|
||||
}
|
||||
#endif
|
||||
});
|
||||
}
|
||||
|
||||
void LD6002BComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"HLK-LD6002B:\n"
|
||||
" Auto wake: %s",
|
||||
this->auto_wake_ ? "true" : "false");
|
||||
" Auto wake: %s\n"
|
||||
" Max data length: %u",
|
||||
this->auto_wake_ ? "true" : "false", static_cast<unsigned>(this->max_data_len_));
|
||||
if (this->wakeup_pin_ != nullptr) {
|
||||
LOG_PIN(" Wake-up Pin: ", this->wakeup_pin_);
|
||||
ESP_LOGCONFIG(TAG, " Wake Pulse: %ums", this->wakeup_pulse_ms_);
|
||||
}
|
||||
#ifdef USE_SENSOR
|
||||
LOG_SENSOR(" ", "Target Count", this->target_count_sensor_);
|
||||
LOG_SENSOR(" ", "Point Count", this->point_count_sensor_);
|
||||
for (auto &target : this->targets_) {
|
||||
LOG_SENSOR(" ", "Target X", target.x);
|
||||
LOG_SENSOR(" ", "Target Y", target.y);
|
||||
@@ -141,6 +330,21 @@ void LD6002BComponent::dump_config() {
|
||||
LOG_BINARY_SENSOR(" ", "Target Presence", this->target_presence_[i]);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
LOG_TEXT_SENSOR(" ", "Work Mode", this->work_mode_text_sensor_);
|
||||
LOG_TEXT_SENSOR(" ", "OTA Version", this->ota_version_text_sensor_);
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
LOG_NUMBER(" ", "Hold Delay", this->hold_delay_number_);
|
||||
LOG_NUMBER(" ", "Z Min", this->z_min_number_);
|
||||
LOG_NUMBER(" ", "Z Max", this->z_max_number_);
|
||||
LOG_NUMBER(" ", "Low Power Sleep", this->low_power_sleep_number_);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
LOG_SWITCH(" ", "Low Power", this->low_power_switch_);
|
||||
LOG_SWITCH(" ", "Point Cloud", this->point_cloud_switch_);
|
||||
LOG_SWITCH(" ", "Target Display", this->target_display_switch_);
|
||||
#endif
|
||||
}
|
||||
|
||||
void LD6002BComponent::loop() {
|
||||
@@ -192,7 +396,7 @@ void LD6002BComponent::parse_byte_(uint8_t byte) {
|
||||
this->frame_type_ = read_u16_be(this->data_buf_ + 4);
|
||||
// The length is only trustworthy once the header checksum has been verified, so just
|
||||
// remember that the frame is oversized and let the HCK state act on it.
|
||||
this->frame_oversize_ = this->data_len_ > DEFAULT_MAX_DATA_LEN;
|
||||
this->frame_oversize_ = this->data_len_ > this->max_data_len_;
|
||||
this->parse_state_ = ParseState::HCK;
|
||||
}
|
||||
}
|
||||
@@ -267,16 +471,54 @@ void LD6002BComponent::handle_frame_(uint16_t type, const uint8_t *data, uint16_
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
const uint32_t active_control_command =
|
||||
(this->command_active_ && this->active_command_.type == TYPE_CONTROL && this->active_command_.len >= 4)
|
||||
? read_u32_le(this->active_command_.data.data())
|
||||
: 0;
|
||||
if (active_control_command != 0 && is_expected_control_report(active_control_command, type)) {
|
||||
ESP_LOGV(TAG, "Received %s (0x%04X) while waiting for %s (0x%02" PRIX32 ") ACK", frame_type_name(type), type,
|
||||
control_command_name(active_control_command), active_control_command);
|
||||
}
|
||||
#endif
|
||||
|
||||
switch (type) {
|
||||
case TYPE_REPORT_TARGET:
|
||||
this->handle_target_report_(data, len);
|
||||
break;
|
||||
case TYPE_REPORT_POINT_CLOUD:
|
||||
this->handle_point_cloud_(data, len);
|
||||
break;
|
||||
case TYPE_REPORT_DELAY:
|
||||
this->handle_delay_report_(data, len);
|
||||
break;
|
||||
case TYPE_REPORT_Z_RANGE:
|
||||
this->handle_z_range_report_(data, len);
|
||||
break;
|
||||
case TYPE_REPORT_LOW_POWER:
|
||||
this->handle_low_power_report_(data, len);
|
||||
break;
|
||||
case TYPE_REPORT_LOW_POWER_SLEEP:
|
||||
this->handle_low_power_sleep_report_(data, len);
|
||||
break;
|
||||
case TYPE_REPORT_WORK_MODE:
|
||||
this->handle_work_mode_report_(data, len);
|
||||
break;
|
||||
case TYPE_QUERY_VERSION:
|
||||
this->handle_version_report_(data, len);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void LD6002BComponent::handle_target_report_(const uint8_t *data, uint16_t len) {
|
||||
// The module stops streaming when it acts on the command, not when the command
|
||||
// is queued, so trailing frames after an off must not repopulate what
|
||||
// set_switch_state just cleared.
|
||||
if (!this->target_display_enabled_) {
|
||||
return;
|
||||
}
|
||||
if (len < 4)
|
||||
return;
|
||||
|
||||
@@ -339,6 +581,7 @@ void LD6002BComponent::handle_target_report_(const uint8_t *data, uint16_t len)
|
||||
this->presence_binary_sensor_->publish_state(this->target_presence_any_);
|
||||
}
|
||||
#endif
|
||||
this->update_work_mode_fallback_();
|
||||
|
||||
for (uint8_t i = 0; i < MAX_TARGETS; i++) {
|
||||
bool has_target = this->slot_occupied_[i];
|
||||
@@ -373,26 +616,7 @@ void LD6002BComponent::handle_target_report_(const uint8_t *data, uint16_t len)
|
||||
#endif
|
||||
} else {
|
||||
#ifdef USE_SENSOR
|
||||
TargetSensors &target = this->targets_[i];
|
||||
if (this->last_target_presence_[i]) {
|
||||
if (target.x != nullptr) {
|
||||
target.x->publish_state(NAN);
|
||||
}
|
||||
if (target.y != nullptr) {
|
||||
target.y->publish_state(NAN);
|
||||
}
|
||||
if (target.z != nullptr) {
|
||||
target.z->publish_state(NAN);
|
||||
}
|
||||
if (target.dop_idx != nullptr) {
|
||||
target.dop_idx->publish_state(NAN);
|
||||
}
|
||||
if (target.cluster_id != nullptr) {
|
||||
target.cluster_id->publish_state(NAN);
|
||||
}
|
||||
// The slot is free: the next person's id is new even when it repeats this one.
|
||||
this->last_cluster_id_valid_[i] = false;
|
||||
}
|
||||
this->clear_target_slot_(i);
|
||||
#endif
|
||||
}
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
@@ -407,6 +631,150 @@ void LD6002BComponent::handle_target_report_(const uint8_t *data, uint16_t len)
|
||||
}
|
||||
}
|
||||
|
||||
void LD6002BComponent::handle_point_cloud_(const uint8_t *data, uint16_t len) {
|
||||
// Same window as the target stream: a frame already in flight must not put the
|
||||
// count back after the switch cleared it.
|
||||
if (!this->point_cloud_enabled_) {
|
||||
return;
|
||||
}
|
||||
if (len < 4)
|
||||
return;
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
uint32_t point_num = read_u32_le(data);
|
||||
if (this->point_count_sensor_ != nullptr) {
|
||||
if (point_num != this->last_point_count_) {
|
||||
this->point_count_sensor_->publish_state(point_num);
|
||||
this->last_point_count_ = point_num;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void LD6002BComponent::handle_delay_report_(const uint8_t *data, uint16_t len) {
|
||||
if (len < 4)
|
||||
return;
|
||||
#ifdef USE_NUMBER
|
||||
uint32_t delay = read_u32_le(data);
|
||||
this->publish_number_clamped_(this->hold_delay_number_, delay);
|
||||
#endif
|
||||
}
|
||||
|
||||
void LD6002BComponent::handle_z_range_report_(const uint8_t *data, uint16_t len) {
|
||||
if (len < 8)
|
||||
return;
|
||||
float z_min = read_f32_le(data);
|
||||
float z_max = read_f32_le(data + 4);
|
||||
this->z_min_ = z_min;
|
||||
this->z_max_ = z_max;
|
||||
#ifdef USE_NUMBER
|
||||
this->publish_number_clamped_(this->z_min_number_, z_min);
|
||||
this->publish_number_clamped_(this->z_max_number_, z_max);
|
||||
#endif
|
||||
}
|
||||
|
||||
void LD6002BComponent::handle_low_power_report_(const uint8_t *data, uint16_t len) {
|
||||
if (len < 1)
|
||||
return;
|
||||
bool enabled = data[0] != 0;
|
||||
this->low_power_enabled_ = enabled;
|
||||
this->low_power_reported_ = true;
|
||||
#ifdef USE_SWITCH
|
||||
if (this->low_power_switch_ != nullptr) {
|
||||
this->low_power_switch_->publish_state(enabled);
|
||||
}
|
||||
#endif
|
||||
this->update_work_mode_fallback_();
|
||||
}
|
||||
|
||||
void LD6002BComponent::handle_low_power_sleep_report_(const uint8_t *data, uint16_t len) {
|
||||
if (len < 4)
|
||||
return;
|
||||
#ifdef USE_NUMBER
|
||||
uint32_t sleep_ms = read_u32_le(data);
|
||||
this->publish_number_clamped_(this->low_power_sleep_number_, sleep_ms);
|
||||
#endif
|
||||
}
|
||||
|
||||
void LD6002BComponent::handle_work_mode_report_(const uint8_t *data, uint16_t len) {
|
||||
if (len < 1)
|
||||
return;
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
const bool low_power = (data[0] == 0);
|
||||
if (this->work_mode_text_sensor_ != nullptr) {
|
||||
this->work_mode_reported_ = true;
|
||||
this->publish_work_mode_(low_power);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void LD6002BComponent::update_work_mode_fallback_() {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->work_mode_text_sensor_ == nullptr || this->work_mode_reported_) {
|
||||
return;
|
||||
}
|
||||
if (!this->low_power_reported_) {
|
||||
return;
|
||||
}
|
||||
// Presence is only meaningful while the stream that maintains it runs; with it
|
||||
// off there is nothing to weigh and low power alone decides.
|
||||
const bool presence = this->target_display_enabled_ && this->target_presence_any_;
|
||||
this->publish_work_mode_(this->low_power_enabled_ && !presence);
|
||||
#endif
|
||||
}
|
||||
|
||||
void LD6002BComponent::publish_work_mode_(bool low_power) {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->work_mode_text_sensor_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (this->last_work_mode_valid_ && this->last_work_mode_low_power_ == low_power) {
|
||||
return;
|
||||
}
|
||||
this->work_mode_text_sensor_->publish_state(low_power ? "low_power" : "normal");
|
||||
this->last_work_mode_valid_ = true;
|
||||
this->last_work_mode_low_power_ = low_power;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
void LD6002BComponent::publish_number_clamped_(number::Number *number, float value) {
|
||||
if (number == nullptr)
|
||||
return;
|
||||
const float min_value = number->traits.get_min_value();
|
||||
const float max_value = number->traits.get_max_value();
|
||||
// Outside the declared range the user cannot write the value back, so publish
|
||||
// what they can reach and say what the module actually sent.
|
||||
if (value < min_value || value > max_value) {
|
||||
ESP_LOGW(TAG, "'%s': module reported %.1f, clamped to %.1f..%.1f", number->get_name().c_str(), value, min_value,
|
||||
max_value);
|
||||
value = std::clamp(value, min_value, max_value);
|
||||
}
|
||||
number->publish_state(value);
|
||||
}
|
||||
#endif
|
||||
|
||||
void LD6002BComponent::handle_version_report_(const uint8_t *data, uint16_t len) {
|
||||
if (len < 4)
|
||||
return;
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->ota_version_text_sensor_ == nullptr)
|
||||
return;
|
||||
uint8_t project = data[0];
|
||||
uint8_t major = data[1];
|
||||
uint8_t minor = data[2];
|
||||
uint8_t patch = data[3];
|
||||
char buf[32];
|
||||
if (project == 0) {
|
||||
std::snprintf(buf, sizeof(buf), "%u.%u.%u", major, minor, patch);
|
||||
} else {
|
||||
std::snprintf(buf, sizeof(buf), "p%u %u.%u.%u", project, major, minor, patch);
|
||||
}
|
||||
this->ota_version_text_sensor_->publish_state(buf);
|
||||
this->save_version_pref_(buf);
|
||||
#endif
|
||||
}
|
||||
|
||||
void LD6002BComponent::queue_command_(uint16_t type, const uint8_t *data, uint8_t len) {
|
||||
if (len > CMD_MAX_DATA_LEN) {
|
||||
ESP_LOGW(TAG, "Command data too large: %u", len);
|
||||
@@ -587,4 +955,173 @@ void LD6002BComponent::send_control_command_(uint32_t command) {
|
||||
this->queue_command_(TYPE_CONTROL, data, sizeof(data));
|
||||
}
|
||||
|
||||
void LD6002BComponent::send_z_range_() {
|
||||
// One frame carries both bounds, so half a range cannot be written.
|
||||
if (std::isnan(this->z_min_) || std::isnan(this->z_max_)) {
|
||||
ESP_LOGW(TAG, "Z range not written, other bound unknown");
|
||||
return;
|
||||
}
|
||||
// Both bounds are known and crossed; the frame has no way to say that.
|
||||
if (this->z_min_ > this->z_max_) {
|
||||
ESP_LOGW(TAG, "Z range not written, min above max");
|
||||
return;
|
||||
}
|
||||
uint8_t data[8];
|
||||
write_f32_le(data, this->z_min_);
|
||||
write_f32_le(data + 4, this->z_max_);
|
||||
this->queue_command_(TYPE_SET_Z_RANGE, data, sizeof(data));
|
||||
}
|
||||
|
||||
void LD6002BComponent::set_number_value(NumberType type, float value) {
|
||||
switch (type) {
|
||||
case NumberType::HOLD_DELAY: {
|
||||
uint32_t delay = static_cast<uint32_t>(value);
|
||||
uint8_t data[4];
|
||||
write_u32_le(data, delay);
|
||||
this->queue_command_(TYPE_SET_HOLD_DELAY, data, sizeof(data));
|
||||
break;
|
||||
}
|
||||
case NumberType::Z_MIN:
|
||||
this->z_min_ = value;
|
||||
this->send_z_range_();
|
||||
break;
|
||||
case NumberType::Z_MAX:
|
||||
this->z_max_ = value;
|
||||
this->send_z_range_();
|
||||
break;
|
||||
case NumberType::LOW_POWER_SLEEP: {
|
||||
uint32_t sleep_ms = static_cast<uint32_t>(value);
|
||||
uint8_t data[4];
|
||||
write_u32_le(data, sleep_ms);
|
||||
this->queue_command_(TYPE_SET_LOW_POWER_SLEEP, data, sizeof(data));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LD6002BComponent::init_version_pref_() {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->ota_version_text_sensor_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
this->version_pref_ = this->ota_version_text_sensor_->make_entity_preference<VersionPref>();
|
||||
this->version_pref_initialized_ = true;
|
||||
|
||||
VersionPref pref{};
|
||||
if (this->version_pref_.load(&pref) && pref.value[0] != '\0') {
|
||||
pref.value[sizeof(pref.value) - 1] = '\0';
|
||||
this->ota_version_text_sensor_->publish_state(pref.value);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void LD6002BComponent::save_version_pref_(const char *value) {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (!this->version_pref_initialized_) {
|
||||
return;
|
||||
}
|
||||
VersionPref pref{};
|
||||
std::strncpy(pref.value, value, sizeof(pref.value) - 1);
|
||||
pref.value[sizeof(pref.value) - 1] = '\0';
|
||||
this->version_pref_.save(&pref);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
void LD6002BComponent::clear_target_slot_(uint8_t index) {
|
||||
if (!this->last_target_presence_[index]) {
|
||||
return;
|
||||
}
|
||||
TargetSensors &target = this->targets_[index];
|
||||
if (target.x != nullptr) {
|
||||
target.x->publish_state(NAN);
|
||||
}
|
||||
if (target.y != nullptr) {
|
||||
target.y->publish_state(NAN);
|
||||
}
|
||||
if (target.z != nullptr) {
|
||||
target.z->publish_state(NAN);
|
||||
}
|
||||
if (target.dop_idx != nullptr) {
|
||||
target.dop_idx->publish_state(NAN);
|
||||
}
|
||||
if (target.cluster_id != nullptr) {
|
||||
target.cluster_id->publish_state(NAN);
|
||||
}
|
||||
// The slot is free: the next person's id is new even when it repeats this one.
|
||||
this->last_cluster_id_valid_[index] = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
void LD6002BComponent::clear_target_state_() {
|
||||
// Nothing corrects any of this until the stream comes back. The slot table goes
|
||||
// with it: slots key on cluster ids, which only track a person while reports are
|
||||
// arriving, and the room can empty and refill across the gap -- so the next
|
||||
// report starts from an empty table and fills slots in wire order, rather than
|
||||
// handing one back to whoever last held that id.
|
||||
for (uint8_t i = 0; i < MAX_TARGETS; i++) {
|
||||
#ifdef USE_SENSOR
|
||||
this->clear_target_slot_(i);
|
||||
this->last_target_presence_[i] = false;
|
||||
#endif
|
||||
if (this->slot_occupied_[i]) {
|
||||
this->slot_occupied_[i] = false;
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->target_presence_[i] != nullptr) {
|
||||
this->target_presence_[i]->publish_state(false);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#ifdef USE_SENSOR
|
||||
if (this->last_target_count_ != 0xFFFFFFFF) {
|
||||
if (this->target_count_sensor_ != nullptr) {
|
||||
this->target_count_sensor_->publish_state(NAN);
|
||||
}
|
||||
this->last_target_count_ = 0xFFFFFFFF;
|
||||
}
|
||||
#endif
|
||||
if (this->target_presence_any_) {
|
||||
this->target_presence_any_ = false;
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->presence_binary_sensor_ != nullptr) {
|
||||
this->presence_binary_sensor_->publish_state(this->target_presence_any_);
|
||||
}
|
||||
#endif
|
||||
this->update_work_mode_fallback_();
|
||||
}
|
||||
}
|
||||
|
||||
void LD6002BComponent::set_switch_state(SwitchType type, bool state) {
|
||||
switch (type) {
|
||||
case SwitchType::LOW_POWER:
|
||||
this->low_power_enabled_ = state;
|
||||
this->low_power_reported_ = true;
|
||||
this->send_control_command_(state ? CMD_LOW_POWER_ON : CMD_LOW_POWER_OFF);
|
||||
this->update_work_mode_fallback_();
|
||||
break;
|
||||
case SwitchType::POINT_CLOUD:
|
||||
this->point_cloud_enabled_ = state;
|
||||
this->send_control_command_(state ? CMD_POINT_CLOUD_ON : CMD_POINT_CLOUD_OFF);
|
||||
#ifdef USE_SENSOR
|
||||
// The count only moves while the stream runs, so the last one would stand as
|
||||
// a live reading. The dedup sentinel is cleared with it: the same count is
|
||||
// new again when the stream comes back.
|
||||
if (!state && this->point_count_sensor_ != nullptr && this->last_point_count_ != 0xFFFFFFFF) {
|
||||
this->point_count_sensor_->publish_state(NAN);
|
||||
this->last_point_count_ = 0xFFFFFFFF;
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case SwitchType::TARGET_DISPLAY:
|
||||
this->target_display_enabled_ = state;
|
||||
this->send_control_command_(state ? CMD_TARGET_DISPLAY_ON : CMD_TARGET_DISPLAY_OFF);
|
||||
if (!state) {
|
||||
// Every target entity is fed by the reports this just stopped.
|
||||
this->clear_target_state_();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::ld6002b
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user