mirror of
https://github.com/esphome/esphome.git
synced 2026-10-04 02:05:59 +00:00
[ld6002b] Add area and zone configuration (5/5) (#17823)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
co-authored by
Jonathan Swoboda
parent
25cb440005
commit
b0a9bfd381
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_WAKEUP_PIN
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .const import CONF_AUTO_WAKE, CONF_WAKEUP_PULSE
|
||||
|
||||
@@ -14,7 +15,7 @@ ld6002b_ns = cg.esphome_ns.namespace("ld6002b")
|
||||
LD6002BComponent = ld6002b_ns.class_("LD6002BComponent", cg.Component, uart.UARTDevice)
|
||||
|
||||
|
||||
def _validate_wakeup_options(config):
|
||||
def _validate_wakeup_options(config: ConfigType) -> ConfigType:
|
||||
"""Reject wake options that would silently do nothing.
|
||||
|
||||
Runs before the schema so the defaults for the keys below have not been
|
||||
|
||||
@@ -4,24 +4,35 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_TARGET, DEVICE_CLASS_OCCUPANCY
|
||||
|
||||
from . import LD6002BComponent
|
||||
from .const import CONF_LD6002B_ID, MAX_TARGETS
|
||||
from .const import AREA_COUNT, CONF_LD6002B_ID, MAX_TARGETS
|
||||
|
||||
DEPENDENCIES = ["ld6002b"]
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
|
||||
cv.Optional(CONF_TARGET): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_OCCUPANCY,
|
||||
),
|
||||
}
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(f"target_{i + 1}"): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_OCCUPANCY,
|
||||
)
|
||||
for i in range(MAX_TARGETS)
|
||||
}
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
|
||||
cv.Optional(CONF_TARGET): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_OCCUPANCY,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(f"target_{i + 1}"): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_OCCUPANCY,
|
||||
)
|
||||
for i in range(MAX_TARGETS)
|
||||
}
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(f"detection_area_{i}"): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_OCCUPANCY,
|
||||
)
|
||||
for i in range(AREA_COUNT)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -36,3 +47,8 @@ async def to_code(config):
|
||||
if target_config := config.get(f"target_{i + 1}"):
|
||||
sens = await binary_sensor.new_binary_sensor(target_config)
|
||||
cg.add(hub.set_target_presence_binary_sensor(i, sens))
|
||||
|
||||
for i in range(AREA_COUNT):
|
||||
if area_config := config.get(f"detection_area_{i}"):
|
||||
sens = await binary_sensor.new_binary_sensor(area_config)
|
||||
cg.add(hub.set_area_presence_binary_sensor(i, sens))
|
||||
|
||||
@@ -2,15 +2,21 @@ import esphome.codegen as cg
|
||||
from esphome.components import button
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AREA_ID,
|
||||
CONF_ID,
|
||||
CONF_WAKEUP_PIN,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import LD6002BComponent, ld6002b_ns
|
||||
from ..const import (
|
||||
CONF_APPLY_AREA,
|
||||
CONF_AUTO_INTERFERENCE,
|
||||
CONF_CLEAR_INTERFERENCE,
|
||||
CONF_GET_AREAS,
|
||||
CONF_GET_DELAY,
|
||||
CONF_GET_INSTALLATION,
|
||||
CONF_GET_LOW_POWER_MODE,
|
||||
@@ -19,6 +25,7 @@ from ..const import (
|
||||
CONF_GET_TRIGGER_SPEED,
|
||||
CONF_GET_Z_RANGE,
|
||||
CONF_LD6002B_ID,
|
||||
CONF_RESET_DETECTION_AREA,
|
||||
CONF_RESET_UNATTENDED,
|
||||
CONF_WAKE,
|
||||
)
|
||||
@@ -31,6 +38,21 @@ ButtonType = ld6002b_ns.enum("ButtonType", is_class=True)
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
|
||||
cv.Optional(CONF_APPLY_AREA): button.button_schema(
|
||||
LD6002BButton, entity_category=ENTITY_CATEGORY_CONFIG
|
||||
),
|
||||
cv.Optional(CONF_AUTO_INTERFERENCE): button.button_schema(
|
||||
LD6002BButton, entity_category=ENTITY_CATEGORY_CONFIG
|
||||
),
|
||||
cv.Optional(CONF_GET_AREAS): button.button_schema(
|
||||
LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
||||
),
|
||||
cv.Optional(CONF_CLEAR_INTERFERENCE): button.button_schema(
|
||||
LD6002BButton, entity_category=ENTITY_CATEGORY_CONFIG
|
||||
),
|
||||
cv.Optional(CONF_RESET_DETECTION_AREA): button.button_schema(
|
||||
LD6002BButton, entity_category=ENTITY_CATEGORY_CONFIG
|
||||
),
|
||||
cv.Optional(CONF_GET_DELAY): button.button_schema(
|
||||
LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
||||
),
|
||||
@@ -62,10 +84,21 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
def final_validate(config):
|
||||
def final_validate(config: ConfigType) -> ConfigType:
|
||||
full_config = fv.full_config.get()
|
||||
hub_id = config[CONF_LD6002B_ID]
|
||||
|
||||
if config.get(CONF_APPLY_AREA):
|
||||
has_area_id_select = any(
|
||||
entry.get(CONF_LD6002B_ID) == hub_id and entry.get(CONF_AREA_ID) is not None
|
||||
for entry in full_config.get("select", [])
|
||||
)
|
||||
if not has_area_id_select:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_APPLY_AREA} requires select.area_id for the same ld6002b instance",
|
||||
path=[CONF_APPLY_AREA],
|
||||
)
|
||||
|
||||
if config.get(CONF_WAKE):
|
||||
hub_path = full_config.get_path_for_id(hub_id)
|
||||
hub_config = full_config.get_config_for_path(hub_path[:-1])
|
||||
@@ -81,6 +114,11 @@ def final_validate(config):
|
||||
FINAL_VALIDATE_SCHEMA = final_validate
|
||||
|
||||
BUTTON_MAP = {
|
||||
CONF_APPLY_AREA: ButtonType.APPLY_AREA,
|
||||
CONF_AUTO_INTERFERENCE: ButtonType.AUTO_INTERFERENCE,
|
||||
CONF_GET_AREAS: ButtonType.GET_AREAS,
|
||||
CONF_CLEAR_INTERFERENCE: ButtonType.CLEAR_INTERFERENCE,
|
||||
CONF_RESET_DETECTION_AREA: ButtonType.RESET_DETECTION_AREA,
|
||||
CONF_GET_DELAY: ButtonType.GET_DELAY,
|
||||
CONF_GET_SENSITIVITY: ButtonType.GET_SENSITIVITY,
|
||||
CONF_GET_TRIGGER_SPEED: ButtonType.GET_TRIGGER_SPEED,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
CONF_APPLY_AREA = "apply_area"
|
||||
CONF_AREA_CONFIG = "area_config"
|
||||
CONF_AUTO_INTERFERENCE = "auto_interference"
|
||||
CONF_AUTO_WAKE = "auto_wake"
|
||||
CONF_CLEAR_INTERFERENCE = "clear_interference"
|
||||
CONF_CLUSTER_ID = "cluster_id"
|
||||
CONF_DOPPLER_INDEX = "doppler_index"
|
||||
CONF_GET_AREAS = "get_areas"
|
||||
CONF_GET_DELAY = "get_delay"
|
||||
CONF_GET_INSTALLATION = "get_installation"
|
||||
CONF_GET_LOW_POWER_MODE = "get_low_power_mode"
|
||||
@@ -16,6 +21,7 @@ CONF_LOW_POWER_SLEEP_TIME = "low_power_sleep_time"
|
||||
CONF_OTA_VERSION = "ota_version"
|
||||
CONF_POINT_CLOUD = "point_cloud"
|
||||
CONF_POINT_COUNT = "point_count"
|
||||
CONF_RESET_DETECTION_AREA = "reset_detection_area"
|
||||
CONF_RESET_UNATTENDED = "reset_unattended"
|
||||
CONF_TARGET_DISPLAY = "target_display"
|
||||
CONF_TRIGGER_SPEED = "trigger_speed"
|
||||
@@ -26,4 +32,10 @@ CONF_Z = "z"
|
||||
CONF_Z_MAX = "z_max"
|
||||
CONF_Z_MIN = "z_min"
|
||||
|
||||
KEY_X_MIN = "x_min"
|
||||
KEY_X_MAX = "x_max"
|
||||
KEY_Y_MIN = "y_min"
|
||||
KEY_Y_MAX = "y_max"
|
||||
|
||||
AREA_COUNT = 4
|
||||
MAX_TARGETS = 3
|
||||
|
||||
@@ -15,12 +15,16 @@ static constexpr uint32_t SETUP_DELAY_MS = 100;
|
||||
|
||||
// Command/message types
|
||||
static constexpr uint16_t TYPE_CONTROL = 0x0201;
|
||||
static constexpr uint16_t TYPE_SET_AREA = 0x0202;
|
||||
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_AREA_PRESENCE = 0x0A0A;
|
||||
static constexpr uint16_t TYPE_REPORT_INTERFERENCE_AREAS = 0x0A0B;
|
||||
static constexpr uint16_t TYPE_REPORT_DETECTION_AREAS = 0x0A0C;
|
||||
static constexpr uint16_t TYPE_REPORT_DELAY = 0x0A0D;
|
||||
static constexpr uint16_t TYPE_REPORT_SENSITIVITY = 0x0A0E;
|
||||
static constexpr uint16_t TYPE_REPORT_TRIGGER = 0x0A0F;
|
||||
@@ -32,6 +36,10 @@ 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_AUTO_INTERFERENCE = 0x01;
|
||||
static constexpr uint32_t CMD_GET_AREAS = 0x02;
|
||||
static constexpr uint32_t CMD_CLEAR_INTERFERENCE = 0x03;
|
||||
static constexpr uint32_t CMD_RESET_DETECTION_AREA = 0x04;
|
||||
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;
|
||||
@@ -55,13 +63,26 @@ static constexpr uint32_t CMD_GET_LOW_POWER = 0x18;
|
||||
static constexpr uint32_t CMD_GET_LOW_POWER_SLEEP = 0x19;
|
||||
static constexpr uint32_t CMD_RESET_UNATTENDED = 0x1A;
|
||||
|
||||
static constexpr uint16_t TARGET_DATA_LEN = 20; // x,y,z,dop_idx,cluster_id
|
||||
static constexpr uint16_t TARGET_DATA_LEN = 20; // x,y,z,dop_idx,cluster_id
|
||||
static constexpr uint16_t AREA_DATA_LEN = 24; // 6 floats
|
||||
static constexpr uint16_t AREA_CONFIG_LEN = 28; // int32 + 6 floats
|
||||
static constexpr uint16_t AREA_PRESENCE_ENTRY_LEN = 4; // uint32 per detection area
|
||||
|
||||
static constexpr uint8_t AREA_ID_DEFAULT = 4; // detection_area_0 for initial display
|
||||
|
||||
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_AUTO_INTERFERENCE:
|
||||
return "auto_interference";
|
||||
case CMD_GET_AREAS:
|
||||
return "get_areas";
|
||||
case CMD_CLEAR_INTERFERENCE:
|
||||
return "clear_interference";
|
||||
case CMD_RESET_DETECTION_AREA:
|
||||
return "reset_detection_area";
|
||||
case CMD_GET_DELAY:
|
||||
return "get_delay";
|
||||
case CMD_POINT_CLOUD_ON:
|
||||
@@ -115,6 +136,8 @@ static const char *frame_type_name(uint16_t type) {
|
||||
switch (type) {
|
||||
case TYPE_CONTROL:
|
||||
return "control";
|
||||
case TYPE_SET_AREA:
|
||||
return "set_area";
|
||||
case TYPE_SET_HOLD_DELAY:
|
||||
return "set_hold_delay";
|
||||
case TYPE_SET_Z_RANGE:
|
||||
@@ -125,6 +148,12 @@ static const char *frame_type_name(uint16_t type) {
|
||||
return "report_target";
|
||||
case TYPE_REPORT_POINT_CLOUD:
|
||||
return "report_point_cloud";
|
||||
case TYPE_REPORT_AREA_PRESENCE:
|
||||
return "report_area_presence";
|
||||
case TYPE_REPORT_INTERFERENCE_AREAS:
|
||||
return "report_interference_areas";
|
||||
case TYPE_REPORT_DETECTION_AREAS:
|
||||
return "report_detection_areas";
|
||||
case TYPE_REPORT_DELAY:
|
||||
return "report_delay";
|
||||
case TYPE_REPORT_SENSITIVITY:
|
||||
@@ -150,6 +179,8 @@ static const char *frame_type_name(uint16_t type) {
|
||||
|
||||
static bool is_expected_control_report(uint32_t command, uint16_t type) {
|
||||
switch (command) {
|
||||
case CMD_GET_AREAS:
|
||||
return type == TYPE_REPORT_INTERFERENCE_AREAS || type == TYPE_REPORT_DETECTION_AREAS;
|
||||
case CMD_GET_DELAY:
|
||||
return type == TYPE_REPORT_DELAY;
|
||||
case CMD_GET_SENSITIVITY:
|
||||
@@ -200,6 +231,10 @@ void LD6002BComponent::write_u32_le(uint8_t *data, uint32_t value) {
|
||||
data[3] = (value >> 24) & 0xFF;
|
||||
}
|
||||
|
||||
void LD6002BComponent::write_int32_le(uint8_t *data, int32_t value) {
|
||||
write_u32_le(data, static_cast<uint32_t>(value));
|
||||
}
|
||||
|
||||
void LD6002BComponent::write_f32_le(uint8_t *data, float value) {
|
||||
uint32_t raw;
|
||||
std::memcpy(&raw, &value, sizeof(raw));
|
||||
@@ -356,6 +391,37 @@ void LD6002BComponent::setup() {
|
||||
this->send_control_command_(CMD_GET_LOW_POWER);
|
||||
}
|
||||
|
||||
bool want_area_report = false;
|
||||
#ifdef USE_SENSOR
|
||||
for (const auto &area : this->interference_areas_) {
|
||||
if (area.x_min != nullptr || area.x_max != nullptr || area.y_min != nullptr || area.y_max != nullptr ||
|
||||
area.z_min != nullptr || area.z_max != nullptr) {
|
||||
want_area_report = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!want_area_report) {
|
||||
for (const auto &area : this->detection_areas_) {
|
||||
if (area.x_min != nullptr || area.x_max != nullptr || area.y_min != nullptr || area.y_max != nullptr ||
|
||||
area.z_min != nullptr || area.z_max != nullptr) {
|
||||
want_area_report = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
if (this->area_x_min_number_ != nullptr || this->area_x_max_number_ != nullptr ||
|
||||
this->area_y_min_number_ != nullptr || this->area_y_max_number_ != nullptr ||
|
||||
this->area_z_min_number_ != nullptr || this->area_z_max_number_ != nullptr) {
|
||||
want_area_report = true;
|
||||
}
|
||||
#endif
|
||||
if (want_area_report) {
|
||||
this->send_control_command_(CMD_GET_AREAS);
|
||||
}
|
||||
|
||||
this->init_area_id_pref_();
|
||||
this->init_version_pref_();
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
@@ -374,7 +440,7 @@ void LD6002BComponent::dump_config() {
|
||||
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_);
|
||||
ESP_LOGCONFIG(TAG, " Wake Pulse: %" PRIu32 "ms", this->wakeup_pulse_ms_);
|
||||
}
|
||||
#ifdef USE_SENSOR
|
||||
LOG_SENSOR(" ", "Target Count", this->target_count_sensor_);
|
||||
@@ -386,12 +452,31 @@ void LD6002BComponent::dump_config() {
|
||||
LOG_SENSOR(" ", "Target Doppler Index", target.dop_idx);
|
||||
LOG_SENSOR(" ", "Target Cluster ID", target.cluster_id);
|
||||
}
|
||||
for (auto &area : this->interference_areas_) {
|
||||
LOG_SENSOR(" ", "Interference Area X Min", area.x_min);
|
||||
LOG_SENSOR(" ", "Interference Area X Max", area.x_max);
|
||||
LOG_SENSOR(" ", "Interference Area Y Min", area.y_min);
|
||||
LOG_SENSOR(" ", "Interference Area Y Max", area.y_max);
|
||||
LOG_SENSOR(" ", "Interference Area Z Min", area.z_min);
|
||||
LOG_SENSOR(" ", "Interference Area Z Max", area.z_max);
|
||||
}
|
||||
for (auto &area : this->detection_areas_) {
|
||||
LOG_SENSOR(" ", "Detection Area X Min", area.x_min);
|
||||
LOG_SENSOR(" ", "Detection Area X Max", area.x_max);
|
||||
LOG_SENSOR(" ", "Detection Area Y Min", area.y_min);
|
||||
LOG_SENSOR(" ", "Detection Area Y Max", area.y_max);
|
||||
LOG_SENSOR(" ", "Detection Area Z Min", area.z_min);
|
||||
LOG_SENSOR(" ", "Detection Area Z Max", area.z_max);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "Presence", this->presence_binary_sensor_);
|
||||
for (uint8_t i = 0; i < MAX_TARGETS; i++) {
|
||||
LOG_BINARY_SENSOR(" ", "Target Presence", this->target_presence_[i]);
|
||||
}
|
||||
for (uint8_t i = 0; i < AREA_COUNT; i++) {
|
||||
LOG_BINARY_SENSOR(" ", "Detection Area Presence", this->area_presence_[i]);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
LOG_TEXT_SENSOR(" ", "Work Mode", this->work_mode_text_sensor_);
|
||||
@@ -402,6 +487,12 @@ void LD6002BComponent::dump_config() {
|
||||
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_);
|
||||
LOG_NUMBER(" ", "Area X Min", this->area_x_min_number_);
|
||||
LOG_NUMBER(" ", "Area X Max", this->area_x_max_number_);
|
||||
LOG_NUMBER(" ", "Area Y Min", this->area_y_min_number_);
|
||||
LOG_NUMBER(" ", "Area Y Max", this->area_y_max_number_);
|
||||
LOG_NUMBER(" ", "Area Z Min", this->area_z_min_number_);
|
||||
LOG_NUMBER(" ", "Area Z Max", this->area_z_max_number_);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
LOG_SWITCH(" ", "Low Power", this->low_power_switch_);
|
||||
@@ -412,6 +503,7 @@ void LD6002BComponent::dump_config() {
|
||||
LOG_SELECT(" ", "Sensitivity", this->sensitivity_select_);
|
||||
LOG_SELECT(" ", "Trigger Speed", this->trigger_speed_select_);
|
||||
LOG_SELECT(" ", "Installation Mode", this->installation_select_);
|
||||
LOG_SELECT(" ", "Area ID", this->area_id_select_);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -527,6 +619,7 @@ void LD6002BComponent::handle_frame_(uint16_t type, const uint8_t *data, uint16_
|
||||
}
|
||||
if (len == 0 && this->command_active_ && this->command_sent_ && type == this->active_command_.type) {
|
||||
ESP_LOGV(TAG, "ACK for command 0x%04X (module frame 0x%04X)", type, this->frame_id_);
|
||||
const bool refresh_areas = (type == TYPE_SET_AREA) && this->area_write_in_flight_;
|
||||
// This settles one expected reply; the rest stay owed and become the debt for the next command.
|
||||
this->send_generation_++;
|
||||
this->stale_ack_type_ = type;
|
||||
@@ -536,6 +629,10 @@ void LD6002BComponent::handle_frame_(uint16_t type, const uint8_t *data, uint16_
|
||||
this->command_sent_ = false;
|
||||
this->last_send_ms_ = 0;
|
||||
this->process_command_queue_();
|
||||
if (refresh_areas) {
|
||||
this->area_write_in_flight_ = false;
|
||||
this->set_timeout(AREA_REFRESH_TIMEOUT, 50, [this]() { this->send_control_command_(CMD_GET_AREAS); });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -557,6 +654,15 @@ void LD6002BComponent::handle_frame_(uint16_t type, const uint8_t *data, uint16_
|
||||
case TYPE_REPORT_POINT_CLOUD:
|
||||
this->handle_point_cloud_(data, len);
|
||||
break;
|
||||
case TYPE_REPORT_AREA_PRESENCE:
|
||||
this->handle_area_presence_(data, len);
|
||||
break;
|
||||
case TYPE_REPORT_INTERFERENCE_AREAS:
|
||||
this->handle_area_report_(true, data, len);
|
||||
break;
|
||||
case TYPE_REPORT_DETECTION_AREAS:
|
||||
this->handle_area_report_(false, data, len);
|
||||
break;
|
||||
case TYPE_REPORT_DELAY:
|
||||
this->handle_delay_report_(data, len);
|
||||
break;
|
||||
@@ -654,8 +760,9 @@ void LD6002BComponent::handle_target_report_(const uint8_t *data, uint16_t len)
|
||||
|
||||
this->target_presence_any_ = (reported > 0);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
bool presence = this->target_presence_any_ || this->area_presence_any_;
|
||||
if (this->presence_binary_sensor_ != nullptr) {
|
||||
this->presence_binary_sensor_->publish_state(this->target_presence_any_);
|
||||
this->presence_binary_sensor_->publish_state(presence);
|
||||
}
|
||||
#endif
|
||||
this->update_work_mode_fallback_();
|
||||
@@ -728,6 +835,84 @@ void LD6002BComponent::handle_point_cloud_(const uint8_t *data, uint16_t len) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// 0x0A0A carries one uint32 per detection area -- the protocol names the four
|
||||
// fields detection_state_area0..3 -- so this covers area ids 4..7 only. The
|
||||
// interference areas have no presence report: a target inside one is what they
|
||||
// exist to suppress.
|
||||
void LD6002BComponent::handle_area_presence_(const uint8_t *data, uint16_t len) {
|
||||
const uint16_t needed = AREA_COUNT * AREA_PRESENCE_ENTRY_LEN;
|
||||
if (len < needed)
|
||||
return;
|
||||
|
||||
this->area_presence_any_ = false;
|
||||
for (uint8_t i = 0; i < AREA_COUNT; i++) {
|
||||
uint32_t state = read_u32_le(data + (i * AREA_PRESENCE_ENTRY_LEN));
|
||||
bool present = state != 0;
|
||||
this->area_presence_any_ = this->area_presence_any_ || present;
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->area_presence_[i] != nullptr) {
|
||||
this->area_presence_[i]->publish_state(present);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
bool presence = this->target_presence_any_ || this->area_presence_any_;
|
||||
if (this->presence_binary_sensor_ != nullptr) {
|
||||
this->presence_binary_sensor_->publish_state(presence);
|
||||
}
|
||||
#endif
|
||||
this->update_work_mode_fallback_();
|
||||
}
|
||||
|
||||
void LD6002BComponent::handle_area_report_(bool interference, const uint8_t *data, uint16_t len) {
|
||||
uint16_t needed = AREA_COUNT * AREA_DATA_LEN;
|
||||
if (len < needed)
|
||||
return;
|
||||
|
||||
for (uint8_t i = 0; i < AREA_COUNT; i++) {
|
||||
uint16_t offset = i * AREA_DATA_LEN;
|
||||
float x_min = read_f32_le(data + offset + 0);
|
||||
float x_max = read_f32_le(data + offset + 4);
|
||||
float y_min = read_f32_le(data + offset + 8);
|
||||
float y_max = read_f32_le(data + offset + 12);
|
||||
float z_min = read_f32_le(data + offset + 16);
|
||||
float z_max = read_f32_le(data + offset + 20);
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
AreaSensors &area = interference ? this->interference_areas_[i] : this->detection_areas_[i];
|
||||
if (area.x_min != nullptr)
|
||||
area.x_min->publish_state(x_min);
|
||||
if (area.x_max != nullptr)
|
||||
area.x_max->publish_state(x_max);
|
||||
if (area.y_min != nullptr)
|
||||
area.y_min->publish_state(y_min);
|
||||
if (area.y_max != nullptr)
|
||||
area.y_max->publish_state(y_max);
|
||||
if (area.z_min != nullptr)
|
||||
area.z_min->publish_state(z_min);
|
||||
if (area.z_max != nullptr)
|
||||
area.z_max->publish_state(z_max);
|
||||
#endif
|
||||
|
||||
AreaConfig &store = interference ? this->interference_area_values_[i] : this->detection_area_values_[i];
|
||||
store.x_min = x_min;
|
||||
store.x_max = x_max;
|
||||
store.y_min = y_min;
|
||||
store.y_max = y_max;
|
||||
store.z_min = z_min;
|
||||
store.z_max = z_max;
|
||||
|
||||
uint8_t selected_id = this->area_id_set_ ? this->area_id_ : AREA_ID_DEFAULT;
|
||||
bool selected_interference = selected_id < AREA_COUNT;
|
||||
uint8_t selected_index = selected_interference ? selected_id : static_cast<uint8_t>(selected_id - AREA_COUNT);
|
||||
if (selected_interference == interference && selected_index == i) {
|
||||
this->update_area_numbers_(store);
|
||||
}
|
||||
}
|
||||
this->try_apply_pending_area_(interference);
|
||||
}
|
||||
|
||||
void LD6002BComponent::handle_delay_report_(const uint8_t *data, uint16_t len) {
|
||||
if (len < 4)
|
||||
return;
|
||||
@@ -815,13 +1000,24 @@ void LD6002BComponent::handle_low_power_sleep_report_(const uint8_t *data, uint1
|
||||
void LD6002BComponent::handle_work_mode_report_(const uint8_t *data, uint16_t len) {
|
||||
if (len < 1)
|
||||
return;
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
// Zero is the unattended half of this transition. Read outside the text sensor's
|
||||
// ifdef because the area sensors do not need one configured to have gone stale.
|
||||
const bool low_power = (data[0] == 0);
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->work_mode_text_sensor_ != nullptr) {
|
||||
this->work_mode_reported_ = true;
|
||||
this->publish_work_mode_(low_power);
|
||||
}
|
||||
#endif
|
||||
// Protocol V1.2 section 2.1.17: this message is sent only on the transition
|
||||
// between the unattended low-power mode and normal operation, so a zero is the
|
||||
// module stating that nobody is in any area. Not while a target is still being
|
||||
// tracked, though: the reset_unattended command is undocumented on whether it
|
||||
// forces this report, and where two statements from the module disagree the live
|
||||
// one wins.
|
||||
if (low_power && !this->target_presence_any_) {
|
||||
this->clear_area_presence_();
|
||||
}
|
||||
}
|
||||
|
||||
void LD6002BComponent::update_work_mode_fallback_() {
|
||||
@@ -832,9 +1028,10 @@ void LD6002BComponent::update_work_mode_fallback_() {
|
||||
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_;
|
||||
// Target presence is only meaningful while the stream that maintains it runs.
|
||||
// Area presence keeps its own report, so it still counts with the target stream
|
||||
// off and low power alone decides only when neither half has anything to say.
|
||||
const bool presence = (this->target_display_enabled_ && this->target_presence_any_) || this->area_presence_any_;
|
||||
this->publish_work_mode_(this->low_power_enabled_ && !presence);
|
||||
#endif
|
||||
}
|
||||
@@ -857,6 +1054,16 @@ void LD6002BComponent::publish_work_mode_(bool low_power) {
|
||||
void LD6002BComponent::publish_number_clamped_(number::Number *number, float value) {
|
||||
if (number == nullptr)
|
||||
return;
|
||||
if (std::isnan(value)) {
|
||||
// NAN is this component's "the module has not told us yet". Publishing it on an
|
||||
// entity that has never had a state would report a nan where unknown is the
|
||||
// truth; on one that already shows a value it is the only way to say that value
|
||||
// no longer describes the selected area.
|
||||
if (number->has_state()) {
|
||||
number->publish_state(value);
|
||||
}
|
||||
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
|
||||
@@ -891,14 +1098,14 @@ void LD6002BComponent::handle_version_report_(const uint8_t *data, uint16_t len)
|
||||
#endif
|
||||
}
|
||||
|
||||
void LD6002BComponent::queue_command_(uint16_t type, const uint8_t *data, uint8_t len) {
|
||||
bool 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);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (this->cmd_count_ >= CMD_QUEUE_SIZE) {
|
||||
ESP_LOGW(TAG, "Command queue full, dropping command 0x%04X", type);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
PendingCommand &cmd = this->cmd_queue_[this->cmd_tail_];
|
||||
@@ -911,6 +1118,7 @@ void LD6002BComponent::queue_command_(uint16_t type, const uint8_t *data, uint8_
|
||||
this->cmd_tail_ = (this->cmd_tail_ + 1) % CMD_QUEUE_SIZE;
|
||||
this->cmd_count_++;
|
||||
this->process_command_queue_();
|
||||
return true;
|
||||
}
|
||||
|
||||
void LD6002BComponent::process_command_queue_() {
|
||||
@@ -945,6 +1153,18 @@ void LD6002BComponent::process_command_queue_() {
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Command 0x%04X timed out", this->active_command_.type);
|
||||
}
|
||||
if (this->active_command_.type == TYPE_SET_AREA) {
|
||||
this->area_write_in_flight_ = false;
|
||||
}
|
||||
// The deferred apply is waiting on the report this command would have
|
||||
// brought back, and nothing else re-arms it. Dropping it here is the
|
||||
// difference between one apply lost to a timeout and one that rides in on
|
||||
// an unrelated area report later, writing bounds the user has moved on from.
|
||||
if (active_control_command == CMD_GET_AREAS && this->deferred_apply_pending_) {
|
||||
this->deferred_apply_pending_ = false;
|
||||
this->restore_deferred_edits_();
|
||||
ESP_LOGW(TAG, "Area read timed out, dropping deferred area apply");
|
||||
}
|
||||
// A reply may still be in flight for the attempt we just gave up on, so carry one over as
|
||||
// debt rather than clearing the ledger, or that late ACK would retire the successor. Only
|
||||
// one: reaching this point means nothing was answered at all, so the older attempts are
|
||||
@@ -1067,10 +1287,10 @@ void LD6002BComponent::write_frame_(uint16_t type, const uint8_t *data, uint8_t
|
||||
this->last_traffic_ms_ = now;
|
||||
}
|
||||
|
||||
void LD6002BComponent::send_control_command_(uint32_t command) {
|
||||
bool LD6002BComponent::send_control_command_(uint32_t command) {
|
||||
uint8_t data[4];
|
||||
write_u32_le(data, command);
|
||||
this->queue_command_(TYPE_CONTROL, data, sizeof(data));
|
||||
return this->queue_command_(TYPE_CONTROL, data, sizeof(data));
|
||||
}
|
||||
|
||||
void LD6002BComponent::send_z_range_() {
|
||||
@@ -1090,6 +1310,64 @@ void LD6002BComponent::send_z_range_() {
|
||||
this->queue_command_(TYPE_SET_Z_RANGE, data, sizeof(data));
|
||||
}
|
||||
|
||||
void LD6002BComponent::apply_area_config_() {
|
||||
if (!this->area_id_set_) {
|
||||
ESP_LOGW(TAG, "Area ID not selected; ignoring apply");
|
||||
return;
|
||||
}
|
||||
if (this->area_id_ >= AREA_ID_COUNT) {
|
||||
ESP_LOGW(TAG, "Invalid area id: %u", this->area_id_);
|
||||
return;
|
||||
}
|
||||
|
||||
const bool interference = this->area_id_ < AREA_COUNT;
|
||||
const uint8_t index = interference ? this->area_id_ : static_cast<uint8_t>(this->area_id_ - AREA_COUNT);
|
||||
AreaConfig desired = interference ? this->interference_area_values_[index] : this->detection_area_values_[index];
|
||||
if (!std::isnan(this->area_x_min_))
|
||||
desired.x_min = this->area_x_min_;
|
||||
if (!std::isnan(this->area_x_max_))
|
||||
desired.x_max = this->area_x_max_;
|
||||
if (!std::isnan(this->area_y_min_))
|
||||
desired.y_min = this->area_y_min_;
|
||||
if (!std::isnan(this->area_y_max_))
|
||||
desired.y_max = this->area_y_max_;
|
||||
if (!std::isnan(this->area_z_min_))
|
||||
desired.z_min = this->area_z_min_;
|
||||
if (!std::isnan(this->area_z_max_))
|
||||
desired.z_max = this->area_z_max_;
|
||||
|
||||
if (std::isnan(desired.x_min) || std::isnan(desired.x_max) || std::isnan(desired.y_min) ||
|
||||
std::isnan(desired.y_max) || std::isnan(desired.z_min) || std::isnan(desired.z_max)) {
|
||||
// Ask first: a read that never reached the queue would leave a deferral waiting
|
||||
// on a report nobody requested, with the user's values already retired for it.
|
||||
if (!this->send_control_command_(CMD_GET_AREAS)) {
|
||||
ESP_LOGW(TAG, "Area read not queued; area config left unapplied");
|
||||
return;
|
||||
}
|
||||
this->deferred_apply_pending_ = true;
|
||||
this->pending_area_id_ = this->area_id_;
|
||||
// The ledger, not the mirror: the mirror also carries whatever the module last
|
||||
// reported for the axes the user never touched, and staging those would hand them
|
||||
// back later wearing the user's badge -- a module value the next report is then
|
||||
// kept away from. Staging only what was actually typed is also what makes the
|
||||
// replay's overlay right: the untouched axes come from the fresh report. An
|
||||
// empty ledger is a meaning rather than a gap, then: an apply with nothing
|
||||
// staged rewrites the area exactly as the report just described it, which is
|
||||
// what a direct apply with nothing staged already does.
|
||||
this->pending_area_updates_ = this->area_edits_;
|
||||
// Staged above, so they are the deferred apply's values now rather than an
|
||||
// unsent edit. Anything typed from here belongs to whatever the user does
|
||||
// next, which may well be a different area.
|
||||
this->area_edits_ = AreaConfig{};
|
||||
ESP_LOGI(TAG, "Area config incomplete; requesting current areas before applying");
|
||||
return;
|
||||
}
|
||||
// Only a write the module will actually see retires them.
|
||||
if (this->queue_area_config_(this->area_id_, desired)) {
|
||||
this->area_edits_ = AreaConfig{};
|
||||
}
|
||||
}
|
||||
|
||||
void LD6002BComponent::wake_() {
|
||||
// A command's own pulse raises the pin and writes after it, so ride along instead of
|
||||
// claiming the flag: claiming it would send that command down the immediate-write path
|
||||
@@ -1124,6 +1402,30 @@ void LD6002BComponent::set_number_value(NumberType type, float value) {
|
||||
this->queue_command_(TYPE_SET_LOW_POWER_SLEEP, data, sizeof(data));
|
||||
break;
|
||||
}
|
||||
case NumberType::AREA_X_MIN:
|
||||
this->area_x_min_ = value;
|
||||
this->area_edits_.x_min = value;
|
||||
break;
|
||||
case NumberType::AREA_X_MAX:
|
||||
this->area_x_max_ = value;
|
||||
this->area_edits_.x_max = value;
|
||||
break;
|
||||
case NumberType::AREA_Y_MIN:
|
||||
this->area_y_min_ = value;
|
||||
this->area_edits_.y_min = value;
|
||||
break;
|
||||
case NumberType::AREA_Y_MAX:
|
||||
this->area_y_max_ = value;
|
||||
this->area_edits_.y_max = value;
|
||||
break;
|
||||
case NumberType::AREA_Z_MIN:
|
||||
this->area_z_min_ = value;
|
||||
this->area_edits_.z_min = value;
|
||||
break;
|
||||
case NumberType::AREA_Z_MAX:
|
||||
this->area_z_max_ = value;
|
||||
this->area_edits_.z_max = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1154,9 +1456,179 @@ void LD6002BComponent::set_select_value(SelectType type, size_t index) {
|
||||
this->send_control_command_(CMD_INSTALL_SIDE);
|
||||
}
|
||||
break;
|
||||
case SelectType::AREA_ID:
|
||||
this->area_id_ = static_cast<uint8_t>(index);
|
||||
this->area_id_set_ = true;
|
||||
this->update_area_numbers_for_id_(this->area_id_);
|
||||
this->save_area_id_pref_(this->area_id_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void LD6002BComponent::update_area_numbers_(const AreaConfig &area) {
|
||||
// A report refreshes every axis the user is not in the middle of changing. An
|
||||
// unapplied edit is the one value here the module cannot know about, so taking
|
||||
// the report over it would discard what the user typed with nothing to show for it.
|
||||
const AreaConfig &edits = this->area_edits_;
|
||||
if (std::isnan(edits.x_min))
|
||||
this->area_x_min_ = area.x_min;
|
||||
if (std::isnan(edits.x_max))
|
||||
this->area_x_max_ = area.x_max;
|
||||
if (std::isnan(edits.y_min))
|
||||
this->area_y_min_ = area.y_min;
|
||||
if (std::isnan(edits.y_max))
|
||||
this->area_y_max_ = area.y_max;
|
||||
if (std::isnan(edits.z_min))
|
||||
this->area_z_min_ = area.z_min;
|
||||
if (std::isnan(edits.z_max))
|
||||
this->area_z_max_ = area.z_max;
|
||||
this->publish_area_numbers_();
|
||||
}
|
||||
|
||||
// The mirror, not the report: an axis a report was kept away from has to keep its
|
||||
// displayed value too, or the entity and the value the next apply sends disagree.
|
||||
void LD6002BComponent::publish_area_numbers_() {
|
||||
#ifdef USE_NUMBER
|
||||
this->publish_number_clamped_(this->area_x_min_number_, this->area_x_min_);
|
||||
this->publish_number_clamped_(this->area_x_max_number_, this->area_x_max_);
|
||||
this->publish_number_clamped_(this->area_y_min_number_, this->area_y_min_);
|
||||
this->publish_number_clamped_(this->area_y_max_number_, this->area_y_max_);
|
||||
this->publish_number_clamped_(this->area_z_min_number_, this->area_z_min_);
|
||||
this->publish_number_clamped_(this->area_z_max_number_, this->area_z_max_);
|
||||
#endif
|
||||
}
|
||||
|
||||
void LD6002BComponent::update_area_numbers_for_id_(uint8_t area_id) {
|
||||
if (area_id >= AREA_ID_COUNT)
|
||||
return;
|
||||
const bool interference = area_id < AREA_COUNT;
|
||||
const uint8_t index = interference ? area_id : static_cast<uint8_t>(area_id - AREA_COUNT);
|
||||
const AreaConfig &area = interference ? this->interference_area_values_[index] : this->detection_area_values_[index];
|
||||
// The edits belonged to the area being navigated away from.
|
||||
this->area_edits_ = AreaConfig{};
|
||||
this->update_area_numbers_(area);
|
||||
}
|
||||
|
||||
bool LD6002BComponent::queue_area_config_(uint8_t area_id, const AreaConfig &desired) {
|
||||
// One frame carries all three pairs and cannot express a crossed one; the module
|
||||
// would keep a box nothing can ever be inside. Both callers arrive with the six
|
||||
// bounds resolved, so this is the last place that can say no -- and the return
|
||||
// value is how saying no reaches the caller, which must not then retire the edits
|
||||
// the user still has to fix.
|
||||
if (desired.x_min > desired.x_max || desired.y_min > desired.y_max || desired.z_min > desired.z_max) {
|
||||
ESP_LOGW(TAG, "Area %u not written, min above max", area_id);
|
||||
return false;
|
||||
}
|
||||
uint8_t data[AREA_CONFIG_LEN];
|
||||
write_int32_le(data, static_cast<int32_t>(area_id));
|
||||
write_f32_le(data + 4, desired.x_min);
|
||||
write_f32_le(data + 8, desired.x_max);
|
||||
write_f32_le(data + 12, desired.y_min);
|
||||
write_f32_le(data + 16, desired.y_max);
|
||||
write_f32_le(data + 20, desired.z_min);
|
||||
write_f32_le(data + 24, desired.z_max);
|
||||
|
||||
if (!this->queue_command_(TYPE_SET_AREA, data, sizeof(data))) {
|
||||
// Nothing is on its way, so the cache must not claim these bounds, the ack
|
||||
// refresh must not be armed for an ack that cannot come, and the values stay
|
||||
// the user's unsent edit.
|
||||
return false;
|
||||
}
|
||||
this->area_write_in_flight_ = true;
|
||||
|
||||
const bool interference = area_id < AREA_COUNT;
|
||||
const uint8_t index = interference ? area_id : static_cast<uint8_t>(area_id - AREA_COUNT);
|
||||
AreaConfig &store = interference ? this->interference_area_values_[index] : this->detection_area_values_[index];
|
||||
store = desired;
|
||||
// The six numbers show one area at a time, and a deferred apply can land here for
|
||||
// an area the user has navigated away from. Same question handle_area_report_
|
||||
// asks before it touches them.
|
||||
const uint8_t selected_id = this->area_id_set_ ? this->area_id_ : AREA_ID_DEFAULT;
|
||||
if (area_id == selected_id) {
|
||||
this->update_area_numbers_(store);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void LD6002BComponent::try_apply_pending_area_(bool reported_interference) {
|
||||
if (!this->deferred_apply_pending_) {
|
||||
return;
|
||||
}
|
||||
if (this->pending_area_id_ >= AREA_ID_COUNT) {
|
||||
this->deferred_apply_pending_ = false;
|
||||
return;
|
||||
}
|
||||
const bool interference = this->pending_area_id_ < AREA_COUNT;
|
||||
const uint8_t index =
|
||||
interference ? this->pending_area_id_ : static_cast<uint8_t>(this->pending_area_id_ - AREA_COUNT);
|
||||
AreaConfig desired = interference ? this->interference_area_values_[index] : this->detection_area_values_[index];
|
||||
|
||||
if (!std::isnan(this->pending_area_updates_.x_min))
|
||||
desired.x_min = this->pending_area_updates_.x_min;
|
||||
if (!std::isnan(this->pending_area_updates_.x_max))
|
||||
desired.x_max = this->pending_area_updates_.x_max;
|
||||
if (!std::isnan(this->pending_area_updates_.y_min))
|
||||
desired.y_min = this->pending_area_updates_.y_min;
|
||||
if (!std::isnan(this->pending_area_updates_.y_max))
|
||||
desired.y_max = this->pending_area_updates_.y_max;
|
||||
if (!std::isnan(this->pending_area_updates_.z_min))
|
||||
desired.z_min = this->pending_area_updates_.z_min;
|
||||
if (!std::isnan(this->pending_area_updates_.z_max))
|
||||
desired.z_max = this->pending_area_updates_.z_max;
|
||||
|
||||
if (std::isnan(desired.x_min) || std::isnan(desired.x_max) || std::isnan(desired.y_min) ||
|
||||
std::isnan(desired.y_max) || std::isnan(desired.z_min) || std::isnan(desired.z_max)) {
|
||||
// Only the report covering this area's half can still fill it in, and there is
|
||||
// exactly one of those per read. Once it has landed with a bound still unknown,
|
||||
// nothing further is coming and waiting means waiting forever.
|
||||
if (reported_interference == interference) {
|
||||
this->deferred_apply_pending_ = false;
|
||||
this->restore_deferred_edits_();
|
||||
ESP_LOGW(TAG, "Dropping deferred area apply, area report incomplete");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t area_id = this->pending_area_id_;
|
||||
this->deferred_apply_pending_ = false;
|
||||
if (!this->queue_area_config_(area_id, desired)) {
|
||||
// Nothing was queued, so this is a drop like the other two: hand the staged
|
||||
// values back rather than leaving them with no ledger to protect them.
|
||||
this->restore_deferred_edits_();
|
||||
}
|
||||
}
|
||||
|
||||
void LD6002BComponent::init_area_id_pref_() {
|
||||
#ifdef USE_SELECT
|
||||
if (this->area_id_select_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
this->area_id_pref_ = this->area_id_select_->make_entity_preference<uint8_t>();
|
||||
this->area_id_pref_initialized_ = true;
|
||||
|
||||
uint8_t value = 0;
|
||||
if (!this->area_id_pref_.load(&value) || value >= AREA_ID_COUNT) {
|
||||
// No stored selection. The numbers are about to display this area either way,
|
||||
// so select it for real: a displayed area that apply_area then refuses to write
|
||||
// is the one combination the user cannot make sense of.
|
||||
value = AREA_ID_DEFAULT;
|
||||
}
|
||||
this->area_id_select_->publish_state(value);
|
||||
this->area_id_ = value;
|
||||
this->area_id_set_ = true;
|
||||
this->update_area_numbers_for_id_(value);
|
||||
#endif
|
||||
}
|
||||
|
||||
void LD6002BComponent::save_area_id_pref_(uint8_t value) {
|
||||
#ifdef USE_SELECT
|
||||
if (!this->area_id_pref_initialized_) {
|
||||
return;
|
||||
}
|
||||
this->area_id_pref_.save(&value);
|
||||
#endif
|
||||
}
|
||||
|
||||
void LD6002BComponent::init_version_pref_() {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->ota_version_text_sensor_ == nullptr) {
|
||||
@@ -1211,6 +1683,74 @@ void LD6002BComponent::clear_target_slot_(uint8_t index) {
|
||||
}
|
||||
#endif
|
||||
|
||||
void LD6002BComponent::restore_deferred_edits_() {
|
||||
// The staged values become an unsent edit again, but only for the user who is
|
||||
// still looking at the area they were staged for; anyone else's ledger belongs to
|
||||
// the area they are on now.
|
||||
const uint8_t selected_id = this->area_id_set_ ? this->area_id_ : AREA_ID_DEFAULT;
|
||||
if (this->pending_area_id_ != selected_id) {
|
||||
return;
|
||||
}
|
||||
// Axis by axis rather than a whole-struct assignment: the user can have edited
|
||||
// another bound while the deferral was in flight, and that edit is newer than
|
||||
// anything the deferral staged. Assigning over the ledger would drop it back to
|
||||
// NaN and let the next report take the value away. A live edit wins; only an axis
|
||||
// with nothing in the ledger takes its staged value back.
|
||||
//
|
||||
// The mirror moves with the ledger, because on the report path handle_area_report_
|
||||
// ran update_area_numbers_ before the replay, with the ledger still empty -- so the
|
||||
// mirror already holds the module's bounds and both the entities and the next apply
|
||||
// would build on them. On the timeout path no report arrived, the mirror still
|
||||
// holds the staged values, and this is an identity.
|
||||
const AreaConfig &staged = this->pending_area_updates_;
|
||||
if (std::isnan(this->area_edits_.x_min) && !std::isnan(staged.x_min)) {
|
||||
this->area_edits_.x_min = staged.x_min;
|
||||
this->area_x_min_ = staged.x_min;
|
||||
}
|
||||
if (std::isnan(this->area_edits_.x_max) && !std::isnan(staged.x_max)) {
|
||||
this->area_edits_.x_max = staged.x_max;
|
||||
this->area_x_max_ = staged.x_max;
|
||||
}
|
||||
if (std::isnan(this->area_edits_.y_min) && !std::isnan(staged.y_min)) {
|
||||
this->area_edits_.y_min = staged.y_min;
|
||||
this->area_y_min_ = staged.y_min;
|
||||
}
|
||||
if (std::isnan(this->area_edits_.y_max) && !std::isnan(staged.y_max)) {
|
||||
this->area_edits_.y_max = staged.y_max;
|
||||
this->area_y_max_ = staged.y_max;
|
||||
}
|
||||
if (std::isnan(this->area_edits_.z_min) && !std::isnan(staged.z_min)) {
|
||||
this->area_edits_.z_min = staged.z_min;
|
||||
this->area_z_min_ = staged.z_min;
|
||||
}
|
||||
if (std::isnan(this->area_edits_.z_max) && !std::isnan(staged.z_max)) {
|
||||
this->area_edits_.z_max = staged.z_max;
|
||||
this->area_z_max_ = staged.z_max;
|
||||
}
|
||||
this->publish_area_numbers_();
|
||||
}
|
||||
|
||||
void LD6002BComponent::clear_area_presence_() {
|
||||
if (!this->area_presence_any_) {
|
||||
return;
|
||||
}
|
||||
// Nothing else corrects this: 0x0A0A carries no period the protocol states and no
|
||||
// command stops it, so the module going unattended is the only moment the
|
||||
// component can know a stored "occupied" has stopped being true.
|
||||
this->area_presence_any_ = false;
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
for (uint8_t i = 0; i < AREA_COUNT; i++) {
|
||||
if (this->area_presence_[i] != nullptr) {
|
||||
this->area_presence_[i]->publish_state(false);
|
||||
}
|
||||
}
|
||||
const bool presence = this->target_presence_any_ || this->area_presence_any_;
|
||||
if (this->presence_binary_sensor_ != nullptr) {
|
||||
this->presence_binary_sensor_->publish_state(presence);
|
||||
}
|
||||
#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
|
||||
@@ -1242,8 +1782,9 @@ void LD6002BComponent::clear_target_state_() {
|
||||
if (this->target_presence_any_) {
|
||||
this->target_presence_any_ = false;
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
bool presence = this->target_presence_any_ || this->area_presence_any_;
|
||||
if (this->presence_binary_sensor_ != nullptr) {
|
||||
this->presence_binary_sensor_->publish_state(this->target_presence_any_);
|
||||
this->presence_binary_sensor_->publish_state(presence);
|
||||
}
|
||||
#endif
|
||||
this->update_work_mode_fallback_();
|
||||
@@ -1284,6 +1825,27 @@ void LD6002BComponent::set_switch_state(SwitchType type, bool state) {
|
||||
|
||||
void LD6002BComponent::press_button(ButtonType type) {
|
||||
switch (type) {
|
||||
case ButtonType::APPLY_AREA:
|
||||
this->apply_area_config_();
|
||||
break;
|
||||
case ButtonType::AUTO_INTERFERENCE:
|
||||
this->send_control_command_(CMD_AUTO_INTERFERENCE);
|
||||
// The module recomputes the interference areas without reporting them.
|
||||
this->send_control_command_(CMD_GET_AREAS);
|
||||
break;
|
||||
case ButtonType::GET_AREAS:
|
||||
this->send_control_command_(CMD_GET_AREAS);
|
||||
break;
|
||||
case ButtonType::CLEAR_INTERFERENCE:
|
||||
this->send_control_command_(CMD_CLEAR_INTERFERENCE);
|
||||
// The module rewrites the areas but does not report them, so ask for the new geometry the
|
||||
// way the apply_area ack path does; the queue keeps it behind the command above.
|
||||
this->send_control_command_(CMD_GET_AREAS);
|
||||
break;
|
||||
case ButtonType::RESET_DETECTION_AREA:
|
||||
this->send_control_command_(CMD_RESET_DETECTION_AREA);
|
||||
this->send_control_command_(CMD_GET_AREAS);
|
||||
break;
|
||||
case ButtonType::GET_DELAY:
|
||||
this->send_control_command_(CMD_GET_DELAY);
|
||||
break;
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
namespace esphome::ld6002b {
|
||||
|
||||
static constexpr uint8_t MAX_TARGETS = 3;
|
||||
static constexpr uint8_t AREA_COUNT = 4;
|
||||
// Interference areas own ids 0..AREA_COUNT-1 and detection areas the next four, so
|
||||
// this is the whole id space TYPE_SET_AREA accepts.
|
||||
static constexpr uint8_t AREA_ID_COUNT = AREA_COUNT * 2;
|
||||
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.
|
||||
@@ -41,12 +45,19 @@ enum class NumberType : uint8_t {
|
||||
Z_MIN,
|
||||
Z_MAX,
|
||||
LOW_POWER_SLEEP,
|
||||
AREA_X_MIN,
|
||||
AREA_X_MAX,
|
||||
AREA_Y_MIN,
|
||||
AREA_Y_MAX,
|
||||
AREA_Z_MIN,
|
||||
AREA_Z_MAX,
|
||||
};
|
||||
|
||||
enum class SelectType : uint8_t {
|
||||
SENSITIVITY,
|
||||
TRIGGER_SPEED,
|
||||
INSTALLATION_MODE,
|
||||
AREA_ID,
|
||||
};
|
||||
|
||||
enum class SwitchType : uint8_t {
|
||||
@@ -56,6 +67,11 @@ enum class SwitchType : uint8_t {
|
||||
};
|
||||
|
||||
enum class ButtonType : uint8_t {
|
||||
APPLY_AREA,
|
||||
AUTO_INTERFERENCE,
|
||||
GET_AREAS,
|
||||
CLEAR_INTERFERENCE,
|
||||
RESET_DETECTION_AREA,
|
||||
GET_DELAY,
|
||||
GET_SENSITIVITY,
|
||||
GET_TRIGGER_SPEED,
|
||||
@@ -76,8 +92,25 @@ struct TargetSensors {
|
||||
sensor::Sensor *cluster_id{nullptr};
|
||||
};
|
||||
|
||||
struct AreaSensors {
|
||||
sensor::Sensor *x_min{nullptr};
|
||||
sensor::Sensor *x_max{nullptr};
|
||||
sensor::Sensor *y_min{nullptr};
|
||||
sensor::Sensor *y_max{nullptr};
|
||||
sensor::Sensor *z_min{nullptr};
|
||||
sensor::Sensor *z_max{nullptr};
|
||||
};
|
||||
#endif
|
||||
|
||||
struct AreaConfig {
|
||||
float x_min{NAN};
|
||||
float x_max{NAN};
|
||||
float y_min{NAN};
|
||||
float y_max{NAN};
|
||||
float z_min{NAN};
|
||||
float z_max{NAN};
|
||||
};
|
||||
|
||||
struct VersionPref {
|
||||
char value[20];
|
||||
};
|
||||
@@ -122,6 +155,67 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
|
||||
return;
|
||||
this->targets_[target].cluster_id = sensor;
|
||||
}
|
||||
void set_interference_area_x_min_sensor(uint8_t area, sensor::Sensor *sensor) {
|
||||
if (area >= AREA_COUNT)
|
||||
return;
|
||||
this->interference_areas_[area].x_min = sensor;
|
||||
}
|
||||
void set_interference_area_x_max_sensor(uint8_t area, sensor::Sensor *sensor) {
|
||||
if (area >= AREA_COUNT)
|
||||
return;
|
||||
this->interference_areas_[area].x_max = sensor;
|
||||
}
|
||||
void set_interference_area_y_min_sensor(uint8_t area, sensor::Sensor *sensor) {
|
||||
if (area >= AREA_COUNT)
|
||||
return;
|
||||
this->interference_areas_[area].y_min = sensor;
|
||||
}
|
||||
void set_interference_area_y_max_sensor(uint8_t area, sensor::Sensor *sensor) {
|
||||
if (area >= AREA_COUNT)
|
||||
return;
|
||||
this->interference_areas_[area].y_max = sensor;
|
||||
}
|
||||
void set_interference_area_z_min_sensor(uint8_t area, sensor::Sensor *sensor) {
|
||||
if (area >= AREA_COUNT)
|
||||
return;
|
||||
this->interference_areas_[area].z_min = sensor;
|
||||
}
|
||||
void set_interference_area_z_max_sensor(uint8_t area, sensor::Sensor *sensor) {
|
||||
if (area >= AREA_COUNT)
|
||||
return;
|
||||
this->interference_areas_[area].z_max = sensor;
|
||||
}
|
||||
|
||||
void set_detection_area_x_min_sensor(uint8_t area, sensor::Sensor *sensor) {
|
||||
if (area >= AREA_COUNT)
|
||||
return;
|
||||
this->detection_areas_[area].x_min = sensor;
|
||||
}
|
||||
void set_detection_area_x_max_sensor(uint8_t area, sensor::Sensor *sensor) {
|
||||
if (area >= AREA_COUNT)
|
||||
return;
|
||||
this->detection_areas_[area].x_max = sensor;
|
||||
}
|
||||
void set_detection_area_y_min_sensor(uint8_t area, sensor::Sensor *sensor) {
|
||||
if (area >= AREA_COUNT)
|
||||
return;
|
||||
this->detection_areas_[area].y_min = sensor;
|
||||
}
|
||||
void set_detection_area_y_max_sensor(uint8_t area, sensor::Sensor *sensor) {
|
||||
if (area >= AREA_COUNT)
|
||||
return;
|
||||
this->detection_areas_[area].y_max = sensor;
|
||||
}
|
||||
void set_detection_area_z_min_sensor(uint8_t area, sensor::Sensor *sensor) {
|
||||
if (area >= AREA_COUNT)
|
||||
return;
|
||||
this->detection_areas_[area].z_min = sensor;
|
||||
}
|
||||
void set_detection_area_z_max_sensor(uint8_t area, sensor::Sensor *sensor) {
|
||||
if (area >= AREA_COUNT)
|
||||
return;
|
||||
this->detection_areas_[area].z_max = sensor;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
@@ -131,6 +225,11 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
|
||||
return;
|
||||
this->target_presence_[target] = sensor;
|
||||
}
|
||||
void set_area_presence_binary_sensor(uint8_t area, binary_sensor::BinarySensor *sensor) {
|
||||
if (area >= AREA_COUNT)
|
||||
return;
|
||||
this->area_presence_[area] = sensor;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
@@ -143,12 +242,20 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
|
||||
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; }
|
||||
|
||||
void set_area_x_min_number(number::Number *number) { this->area_x_min_number_ = number; }
|
||||
void set_area_x_max_number(number::Number *number) { this->area_x_max_number_ = number; }
|
||||
void set_area_y_min_number(number::Number *number) { this->area_y_min_number_ = number; }
|
||||
void set_area_y_max_number(number::Number *number) { this->area_y_max_number_ = number; }
|
||||
void set_area_z_min_number(number::Number *number) { this->area_z_min_number_ = number; }
|
||||
void set_area_z_max_number(number::Number *number) { this->area_z_max_number_ = number; }
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
void set_sensitivity_select(select::Select *select) { this->sensitivity_select_ = select; }
|
||||
void set_trigger_speed_select(select::Select *select) { this->trigger_speed_select_ = select; }
|
||||
void set_installation_select(select::Select *select) { this->installation_select_ = select; }
|
||||
void set_area_id_select(select::Select *select) { this->area_id_select_ = select; }
|
||||
#endif
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
@@ -176,6 +283,8 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
|
||||
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_area_presence_(const uint8_t *data, uint16_t len);
|
||||
void handle_area_report_(bool interference, const uint8_t *data, uint16_t len);
|
||||
void handle_delay_report_(const uint8_t *data, uint16_t len);
|
||||
void handle_sensitivity_report_(const uint8_t *data, uint16_t len);
|
||||
void handle_trigger_speed_report_(const uint8_t *data, uint16_t len);
|
||||
@@ -189,22 +298,35 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
|
||||
void publish_work_mode_(bool low_power);
|
||||
// Drops every target-derived reading and the slot table they are indexed by.
|
||||
void clear_target_state_();
|
||||
void clear_area_presence_();
|
||||
void restore_deferred_edits_();
|
||||
void publish_area_numbers_();
|
||||
#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 update_area_numbers_(const AreaConfig &area);
|
||||
void update_area_numbers_for_id_(uint8_t area_id);
|
||||
bool queue_area_config_(uint8_t area_id, const AreaConfig &desired);
|
||||
void try_apply_pending_area_(bool reported_interference);
|
||||
void init_area_id_pref_();
|
||||
void save_area_id_pref_(uint8_t value);
|
||||
void init_version_pref_();
|
||||
void save_version_pref_(const char *value);
|
||||
|
||||
void queue_command_(uint16_t type, const uint8_t *data, uint8_t len);
|
||||
// Returns whether the command was queued: it is dropped, with a log line, when
|
||||
// the payload is too long or the ring is full.
|
||||
bool queue_command_(uint16_t type, const uint8_t *data, uint8_t len);
|
||||
void process_command_queue_();
|
||||
void send_command_(uint16_t type, const uint8_t *data, uint8_t len);
|
||||
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);
|
||||
// Returns whether the command reached the queue; see queue_command_.
|
||||
bool send_control_command_(uint32_t command);
|
||||
void send_z_range_();
|
||||
void apply_area_config_();
|
||||
void wake_();
|
||||
|
||||
static uint16_t read_u16_be(const uint8_t *data);
|
||||
@@ -212,16 +334,20 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
|
||||
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_int32_le(uint8_t *data, int32_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};
|
||||
std::array<AreaSensors, AREA_COUNT> interference_areas_{};
|
||||
std::array<AreaSensors, AREA_COUNT> detection_areas_{};
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
binary_sensor::BinarySensor *presence_binary_sensor_{nullptr};
|
||||
std::array<binary_sensor::BinarySensor *, MAX_TARGETS> target_presence_{};
|
||||
std::array<binary_sensor::BinarySensor *, AREA_COUNT> area_presence_{};
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
text_sensor::TextSensor *work_mode_text_sensor_{nullptr};
|
||||
@@ -234,11 +360,21 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
|
||||
number::Number *z_min_number_{nullptr};
|
||||
number::Number *z_max_number_{nullptr};
|
||||
number::Number *low_power_sleep_number_{nullptr};
|
||||
|
||||
number::Number *area_x_min_number_{nullptr};
|
||||
number::Number *area_x_max_number_{nullptr};
|
||||
number::Number *area_y_min_number_{nullptr};
|
||||
number::Number *area_y_max_number_{nullptr};
|
||||
number::Number *area_z_min_number_{nullptr};
|
||||
number::Number *area_z_max_number_{nullptr};
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
select::Select *sensitivity_select_{nullptr};
|
||||
select::Select *trigger_speed_select_{nullptr};
|
||||
select::Select *installation_select_{nullptr};
|
||||
select::Select *area_id_select_{nullptr};
|
||||
ESPPreferenceObject area_id_pref_{};
|
||||
bool area_id_pref_initialized_{false};
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
switch_::Switch *low_power_switch_{nullptr};
|
||||
@@ -264,9 +400,13 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
|
||||
uint8_t *data_buf_{nullptr};
|
||||
uint16_t next_frame_id_{0};
|
||||
|
||||
// Sized for the boot burst: with every platform configured, setup() enqueues
|
||||
// roughly ten GET/config commands back to back before the first ack lands.
|
||||
static constexpr uint8_t CMD_QUEUE_SIZE = 16;
|
||||
// Sized for the two bursts that reach it, both counted as what is still queued
|
||||
// once the first command is dequeued: boot leaves 11 with every platform
|
||||
// configured, and pressing all fourteen buttons before an ack lands leaves 15.
|
||||
// Neither overflowed 16, but one free slot is not headroom, and overflowing is a
|
||||
// dropped command with only a log line to show for it. Costs 256 bytes more per
|
||||
// configured instance, and this component is MULTI_CONF.
|
||||
static constexpr uint8_t CMD_QUEUE_SIZE = 24;
|
||||
static constexpr uint32_t CMD_ACK_TIMEOUT_MS = 300;
|
||||
// A sleeping module consumes the first frame to wake and answers only the one after it.
|
||||
static constexpr uint32_t CMD_FIRST_ACK_TIMEOUT_MS = 600;
|
||||
@@ -276,6 +416,9 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
|
||||
// Named so a repeated press replaces its own pending timeout instead of stacking
|
||||
// another, and so the command path can cancel it when it takes the pin over.
|
||||
static constexpr const char *WAKE_BUTTON_TIMEOUT = "wake_button";
|
||||
// Named so a burst of writes collapses to one read once they settle, rather than
|
||||
// one read per write.
|
||||
static constexpr const char *AREA_REFRESH_TIMEOUT = "area_refresh";
|
||||
// A reply cannot trail the frame that earned it for longer than this; the field worst case is ~726ms.
|
||||
static constexpr uint32_t STALE_ACK_MAX_AGE_MS = 1000;
|
||||
|
||||
@@ -307,6 +450,24 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
|
||||
|
||||
float z_min_{NAN};
|
||||
float z_max_{NAN};
|
||||
float area_x_min_{NAN};
|
||||
float area_x_max_{NAN};
|
||||
float area_y_min_{NAN};
|
||||
float area_y_max_{NAN};
|
||||
float area_z_min_{NAN};
|
||||
float area_z_max_{NAN};
|
||||
// What the user has typed and not yet applied; NaN per axis means "nothing of
|
||||
// mine here, take the module's value". Same sentinel shape as
|
||||
// pending_area_updates_. Exactly two things empty it: the area_id select moving
|
||||
// to another area, and an apply that was accepted. A write the bounds guard
|
||||
// refused leaves it alone, and a deferred apply that had to be dropped hands its
|
||||
// staged values back here -- but only while the user is still on the area they
|
||||
// were staged for. Either way the values stay the user's to fix.
|
||||
AreaConfig area_edits_{};
|
||||
std::array<AreaConfig, AREA_COUNT> interference_area_values_{};
|
||||
std::array<AreaConfig, AREA_COUNT> detection_area_values_{};
|
||||
uint8_t area_id_{0xFF};
|
||||
bool area_id_set_{false};
|
||||
|
||||
// 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_{};
|
||||
@@ -318,9 +479,14 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
|
||||
// The report handlers read these and drop anything a stopped stream still emits.
|
||||
bool target_display_enabled_{false};
|
||||
bool point_cloud_enabled_{false};
|
||||
bool area_presence_any_{false};
|
||||
bool area_write_in_flight_{false};
|
||||
bool work_mode_reported_{false};
|
||||
bool low_power_enabled_{false};
|
||||
bool low_power_reported_{false};
|
||||
bool deferred_apply_pending_{false};
|
||||
uint8_t pending_area_id_{0xFF};
|
||||
AreaConfig pending_area_updates_{};
|
||||
bool last_work_mode_valid_{false};
|
||||
bool last_work_mode_low_power_{false};
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import esphome.codegen as cg
|
||||
from esphome.components import number
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AREA_ID,
|
||||
CONF_BUTTON,
|
||||
DEVICE_CLASS_DISTANCE,
|
||||
DEVICE_CLASS_DURATION,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
@@ -9,14 +11,22 @@ from esphome.const import (
|
||||
UNIT_MILLISECOND,
|
||||
UNIT_SECOND,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import LD6002BComponent, ld6002b_ns
|
||||
from ..const import (
|
||||
CONF_APPLY_AREA,
|
||||
CONF_AREA_CONFIG,
|
||||
CONF_HOLD_DELAY,
|
||||
CONF_LD6002B_ID,
|
||||
CONF_LOW_POWER_SLEEP_TIME,
|
||||
CONF_Z_MAX,
|
||||
CONF_Z_MIN,
|
||||
KEY_X_MAX,
|
||||
KEY_X_MIN,
|
||||
KEY_Y_MAX,
|
||||
KEY_Y_MIN,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["ld6002b"]
|
||||
@@ -51,10 +61,83 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
),
|
||||
cv.Optional(CONF_AREA_CONFIG): cv.Schema(
|
||||
{
|
||||
cv.Optional(KEY_X_MIN): number.number_schema(
|
||||
LD6002BNumber,
|
||||
unit_of_measurement=UNIT_METER,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
),
|
||||
cv.Optional(KEY_X_MAX): number.number_schema(
|
||||
LD6002BNumber,
|
||||
unit_of_measurement=UNIT_METER,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
),
|
||||
cv.Optional(KEY_Y_MIN): number.number_schema(
|
||||
LD6002BNumber,
|
||||
unit_of_measurement=UNIT_METER,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
),
|
||||
cv.Optional(KEY_Y_MAX): number.number_schema(
|
||||
LD6002BNumber,
|
||||
unit_of_measurement=UNIT_METER,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
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,
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def final_validate(config: ConfigType) -> ConfigType:
|
||||
if config.get(CONF_AREA_CONFIG) is None:
|
||||
return config
|
||||
|
||||
full_config = fv.full_config.get()
|
||||
hub_id = config[CONF_LD6002B_ID]
|
||||
|
||||
has_apply_area = any(
|
||||
entry.get(CONF_LD6002B_ID) == hub_id and entry.get(CONF_APPLY_AREA) is not None
|
||||
for entry in full_config.get(CONF_BUTTON, [])
|
||||
)
|
||||
if not has_apply_area:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_AREA_CONFIG} requires button.apply_area for the same ld6002b instance",
|
||||
path=[CONF_AREA_CONFIG],
|
||||
)
|
||||
|
||||
has_area_id_select = any(
|
||||
entry.get(CONF_LD6002B_ID) == hub_id and entry.get(CONF_AREA_ID) is not None
|
||||
for entry in full_config.get("select", [])
|
||||
)
|
||||
if not has_area_id_select:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_AREA_CONFIG} requires select.area_id for the same ld6002b instance",
|
||||
path=[CONF_AREA_CONFIG],
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_LD6002B_ID])
|
||||
|
||||
@@ -80,3 +163,19 @@ async def to_code(config):
|
||||
)
|
||||
await cg.register_parented(n, config[CONF_LD6002B_ID])
|
||||
cg.add(getattr(hub, setter)(n))
|
||||
|
||||
if area_config := config.get(CONF_AREA_CONFIG):
|
||||
for key, number_type, setter in (
|
||||
(KEY_X_MIN, NumberType.AREA_X_MIN, "set_area_x_min_number"),
|
||||
(KEY_X_MAX, NumberType.AREA_X_MAX, "set_area_x_max_number"),
|
||||
(KEY_Y_MIN, NumberType.AREA_Y_MIN, "set_area_y_min_number"),
|
||||
(KEY_Y_MAX, NumberType.AREA_Y_MAX, "set_area_y_max_number"),
|
||||
(CONF_Z_MIN, NumberType.AREA_Z_MIN, "set_area_z_min_number"),
|
||||
(CONF_Z_MAX, NumberType.AREA_Z_MAX, "set_area_z_max_number"),
|
||||
):
|
||||
if conf := area_config.get(key):
|
||||
n = await number.new_number(
|
||||
conf, number_type, min_value=-10, max_value=10, step=0.1
|
||||
)
|
||||
await cg.register_parented(n, config[CONF_LD6002B_ID])
|
||||
cg.add(getattr(hub, setter)(n))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import select
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_SENSITIVITY, ENTITY_CATEGORY_CONFIG
|
||||
from esphome.const import CONF_AREA_ID, CONF_SENSITIVITY, ENTITY_CATEGORY_CONFIG
|
||||
|
||||
from .. import LD6002BComponent, ld6002b_ns
|
||||
from ..const import CONF_INSTALLATION_MODE, CONF_LD6002B_ID, CONF_TRIGGER_SPEED
|
||||
@@ -11,6 +11,16 @@ DEPENDENCIES = ["ld6002b"]
|
||||
LD6002BSelect = ld6002b_ns.class_("LD6002BSelect", select.Select)
|
||||
SelectType = ld6002b_ns.enum("SelectType", is_class=True)
|
||||
|
||||
AREA_ID_OPTIONS = [
|
||||
"interference_area_0",
|
||||
"interference_area_1",
|
||||
"interference_area_2",
|
||||
"interference_area_3",
|
||||
"detection_area_0",
|
||||
"detection_area_1",
|
||||
"detection_area_2",
|
||||
"detection_area_3",
|
||||
]
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
@@ -24,6 +34,9 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_INSTALLATION_MODE): select.select_schema(
|
||||
LD6002BSelect, entity_category=ENTITY_CATEGORY_CONFIG
|
||||
),
|
||||
cv.Optional(CONF_AREA_ID): select.select_schema(
|
||||
LD6002BSelect, entity_category=ENTITY_CATEGORY_CONFIG
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -47,6 +60,7 @@ SELECT_MAP = (
|
||||
"set_installation_select",
|
||||
["top", "side"],
|
||||
),
|
||||
(CONF_AREA_ID, SelectType.AREA_ID, "set_area_id_select", AREA_ID_OPTIONS),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -12,11 +12,18 @@ from esphome.const import (
|
||||
|
||||
from . import LD6002BComponent
|
||||
from .const import (
|
||||
AREA_COUNT,
|
||||
CONF_CLUSTER_ID,
|
||||
CONF_DOPPLER_INDEX,
|
||||
CONF_LD6002B_ID,
|
||||
CONF_POINT_COUNT,
|
||||
CONF_Z,
|
||||
CONF_Z_MAX,
|
||||
CONF_Z_MIN,
|
||||
KEY_X_MAX,
|
||||
KEY_X_MIN,
|
||||
KEY_Y_MAX,
|
||||
KEY_Y_MIN,
|
||||
MAX_TARGETS,
|
||||
)
|
||||
|
||||
@@ -68,20 +75,79 @@ TARGET_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
AREA_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
|
||||
cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
cv.Optional(KEY_X_MIN): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_METER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_POINT_COUNT): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
cv.Optional(KEY_X_MAX): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_METER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(KEY_Y_MIN): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_METER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(KEY_Y_MAX): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_METER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_Z_MIN): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_METER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_Z_MAX): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_METER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
).extend({cv.Optional(f"target_{i + 1}"): TARGET_SCHEMA for i in range(MAX_TARGETS)})
|
||||
)
|
||||
|
||||
# (config key, C++ setter axis) for the six bounds every area sensor block carries.
|
||||
_AREA_AXES = (
|
||||
(KEY_X_MIN, "x_min"),
|
||||
(KEY_X_MAX, "x_max"),
|
||||
(KEY_Y_MIN, "y_min"),
|
||||
(KEY_Y_MAX, "y_max"),
|
||||
(CONF_Z_MIN, "z_min"),
|
||||
(CONF_Z_MAX, "z_max"),
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
|
||||
cv.Optional(CONF_TARGET_COUNT): sensor.sensor_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)})
|
||||
.extend(
|
||||
{cv.Optional(f"interference_area_{i}"): AREA_SCHEMA for i in range(AREA_COUNT)}
|
||||
)
|
||||
.extend(
|
||||
{cv.Optional(f"detection_area_{i}"): AREA_SCHEMA for i in range(AREA_COUNT)}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
@@ -112,3 +178,11 @@ async def to_code(config):
|
||||
if cluster_id_config := target_config.get(CONF_CLUSTER_ID):
|
||||
sens = await sensor.new_sensor(cluster_id_config)
|
||||
cg.add(hub.set_target_cluster_id_sensor(i, sens))
|
||||
|
||||
for kind in ("interference", "detection"):
|
||||
for i in range(AREA_COUNT):
|
||||
if area_config := config.get(f"{kind}_area_{i}"):
|
||||
for key, axis in _AREA_AXES:
|
||||
if axis_config := area_config.get(key):
|
||||
sens = await sensor.new_sensor(axis_config)
|
||||
cg.add(getattr(hub, f"set_{kind}_area_{axis}_sensor")(i, sens))
|
||||
|
||||
@@ -1,30 +1,62 @@
|
||||
"""Tests for the wake button's wakeup_pin requirement in ld6002b."""
|
||||
"""Tests for the ld6002b validators that reach across platforms.
|
||||
|
||||
wake needs a pin on its own hub, apply_area needs a select on its own hub, and
|
||||
area_config needs both a button and a select on its own hub. Every one of them
|
||||
is a same-instance check, which is the half that breaks quietly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.ld6002b.button import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
|
||||
from esphome.components.ld6002b.button import (
|
||||
CONFIG_SCHEMA as BUTTON_CONFIG_SCHEMA,
|
||||
FINAL_VALIDATE_SCHEMA as BUTTON_FINAL_VALIDATE_SCHEMA,
|
||||
)
|
||||
from esphome.components.ld6002b.const import CONF_AREA_CONFIG, CONF_Z_MIN
|
||||
from esphome.components.ld6002b.number import (
|
||||
CONFIG_SCHEMA as NUMBER_CONFIG_SCHEMA,
|
||||
FINAL_VALIDATE_SCHEMA as NUMBER_FINAL_VALIDATE_SCHEMA,
|
||||
)
|
||||
from esphome.config import Config
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_WAKEUP_PIN, PlatformFramework
|
||||
from esphome.const import (
|
||||
CONF_AREA_ID,
|
||||
CONF_BUTTON,
|
||||
CONF_ID,
|
||||
CONF_WAKEUP_PIN,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.types import ConfigType
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
HUB_ID = "ld6002b_hub"
|
||||
OTHER_HUB_ID = "ld6002b_other"
|
||||
|
||||
|
||||
def _full_config(hub: ConfigType) -> Config:
|
||||
def _full_config(
|
||||
hub: ConfigType,
|
||||
*,
|
||||
selects: list[ConfigType] | None = None,
|
||||
buttons: list[ConfigType] | None = None,
|
||||
) -> Config:
|
||||
"""A full config carrying one ld6002b hub, as the ID pass leaves it.
|
||||
|
||||
final_validate resolves the hub through get_path_for_id, so the declaring
|
||||
path has to be registered the way validate_config registers it: the path of
|
||||
the id value itself, whose parent is the hub's own config.
|
||||
|
||||
The platform lists are what the cross-platform validators scan, so a test can
|
||||
say which of them exist and which hub each one names.
|
||||
"""
|
||||
full = Config()
|
||||
full["ld6002b"] = [hub]
|
||||
full.declare_ids.append((hub[CONF_ID], ["ld6002b", 0, CONF_ID]))
|
||||
if selects is not None:
|
||||
full["select"] = selects
|
||||
if buttons is not None:
|
||||
full[CONF_BUTTON] = buttons
|
||||
return full
|
||||
|
||||
|
||||
@@ -44,10 +76,33 @@ def _buttons(**buttons: str) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def _select(*, hub_id: str = HUB_ID) -> ConfigType:
|
||||
"""A select platform config naming area_id on the given hub."""
|
||||
return {
|
||||
"ld6002b_id": ID(hub_id, is_declaration=False, type="ld6002b"),
|
||||
CONF_AREA_ID: {"name": "Area ID"},
|
||||
}
|
||||
|
||||
|
||||
def _area_numbers(*, hub_id: str = HUB_ID) -> ConfigType:
|
||||
"""A number platform config carrying one area_config bound."""
|
||||
return {
|
||||
"ld6002b_id": ID(hub_id, is_declaration=False, type="ld6002b"),
|
||||
CONF_AREA_CONFIG: {CONF_Z_MIN: {"name": "Area Z Min"}},
|
||||
}
|
||||
|
||||
|
||||
def _validated(config: ConfigType) -> ConfigType:
|
||||
"""Run the button schema, then the final validation the hub is checked in."""
|
||||
config = CONFIG_SCHEMA(config)
|
||||
FINAL_VALIDATE_SCHEMA(config)
|
||||
config = BUTTON_CONFIG_SCHEMA(config)
|
||||
BUTTON_FINAL_VALIDATE_SCHEMA(config)
|
||||
return config
|
||||
|
||||
|
||||
def _validated_numbers(config: ConfigType) -> ConfigType:
|
||||
"""The same two passes for the number platform."""
|
||||
config = NUMBER_CONFIG_SCHEMA(config)
|
||||
NUMBER_FINAL_VALIDATE_SCHEMA(config)
|
||||
return config
|
||||
|
||||
|
||||
@@ -80,3 +135,82 @@ def test_other_buttons_do_not_need_the_pin(
|
||||
)
|
||||
|
||||
_validated(_buttons(get_delay="Get Delay"))
|
||||
|
||||
|
||||
def test_apply_area_without_select_is_rejected(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""apply_area sends the staged bounds to whichever area the select names."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF, full_config=_full_config(_hub(wakeup_pin=False))
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match=(
|
||||
r"^apply_area requires select\.area_id for the same ld6002b instance"
|
||||
r" @ data\['apply_area'\]$"
|
||||
),
|
||||
):
|
||||
_validated(_buttons(apply_area="Apply Area"))
|
||||
|
||||
|
||||
def test_apply_area_select_on_another_hub_is_rejected(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A select exists, but on a second ld6002b -- which cannot serve this one."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
full_config=_full_config(
|
||||
_hub(wakeup_pin=False), selects=[_select(hub_id=OTHER_HUB_ID)]
|
||||
),
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match=(
|
||||
r"^apply_area requires select\.area_id for the same ld6002b instance"
|
||||
r" @ data\['apply_area'\]$"
|
||||
),
|
||||
):
|
||||
_validated(_buttons(apply_area="Apply Area"))
|
||||
|
||||
|
||||
def test_area_config_without_apply_area_is_rejected(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""The six numbers only stage a write; apply_area is what sends it."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
full_config=_full_config(_hub(wakeup_pin=False), selects=[_select()]),
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match=(
|
||||
r"^area_config requires button\.apply_area for the same ld6002b instance"
|
||||
r" @ data\['area_config'\]$"
|
||||
),
|
||||
):
|
||||
_validated_numbers(_area_numbers())
|
||||
|
||||
|
||||
def test_area_config_without_select_is_rejected(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""The validator's other half: the staged bounds also need an area to land in."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
full_config=_full_config(
|
||||
_hub(wakeup_pin=False), buttons=[_buttons(apply_area="Apply Area")]
|
||||
),
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match=(
|
||||
r"^area_config requires select\.area_id for the same ld6002b instance"
|
||||
r" @ data\['area_config'\]$"
|
||||
),
|
||||
):
|
||||
_validated_numbers(_area_numbers())
|
||||
|
||||
@@ -42,6 +42,32 @@ sensor:
|
||||
name: Target-3 Dop
|
||||
cluster_id:
|
||||
name: Target-3 Cluster
|
||||
interference_area_0:
|
||||
x_min:
|
||||
name: Interference-0 X Min
|
||||
x_max:
|
||||
name: Interference-0 X Max
|
||||
y_min:
|
||||
name: Interference-0 Y Min
|
||||
y_max:
|
||||
name: Interference-0 Y Max
|
||||
z_min:
|
||||
name: Interference-0 Z Min
|
||||
z_max:
|
||||
name: Interference-0 Z Max
|
||||
detection_area_0:
|
||||
x_min:
|
||||
name: Detection-0 X Min
|
||||
x_max:
|
||||
name: Detection-0 X Max
|
||||
y_min:
|
||||
name: Detection-0 Y Min
|
||||
y_max:
|
||||
name: Detection-0 Y Max
|
||||
z_min:
|
||||
name: Detection-0 Z Min
|
||||
z_max:
|
||||
name: Detection-0 Z Max
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld6002b
|
||||
@@ -50,6 +76,8 @@ binary_sensor:
|
||||
name: Presence
|
||||
target_1:
|
||||
name: Target-1 Presence
|
||||
detection_area_0:
|
||||
name: Detection Area-0 Presence
|
||||
|
||||
text_sensor:
|
||||
- platform: ld6002b
|
||||
@@ -70,6 +98,19 @@ number:
|
||||
name: Z Max
|
||||
low_power_sleep_time:
|
||||
name: Low Power Sleep
|
||||
area_config:
|
||||
x_min:
|
||||
name: Area X Min
|
||||
x_max:
|
||||
name: Area X Max
|
||||
y_min:
|
||||
name: Area Y Min
|
||||
y_max:
|
||||
name: Area Y Max
|
||||
z_min:
|
||||
name: Area Z Min
|
||||
z_max:
|
||||
name: Area Z Max
|
||||
|
||||
select:
|
||||
- platform: ld6002b
|
||||
@@ -80,6 +121,8 @@ select:
|
||||
name: Trigger Speed
|
||||
installation_mode:
|
||||
name: Installation
|
||||
area_id:
|
||||
name: Area ID
|
||||
|
||||
switch:
|
||||
- platform: ld6002b
|
||||
@@ -94,6 +137,16 @@ switch:
|
||||
button:
|
||||
- platform: ld6002b
|
||||
ld6002b_id: ld6002b_radar
|
||||
apply_area:
|
||||
name: Apply Area
|
||||
auto_interference:
|
||||
name: Auto Interference
|
||||
get_areas:
|
||||
name: Get Areas
|
||||
clear_interference:
|
||||
name: Clear Interference
|
||||
reset_detection_area:
|
||||
name: Reset Detection
|
||||
get_delay:
|
||||
name: Get Delay
|
||||
get_sensitivity:
|
||||
|
||||
Reference in New Issue
Block a user