Compare commits

...
Author SHA1 Message Date
J. Nick Koston 64c18b8006 Merge remote-tracking branch 'origin/dev' into apply-entities-ld6002b
# Conflicts:
#	esphome/components/ld6002b/__init__.py
2026-10-19 07:52:47 -10:00
J. Nick Koston 731134e540 [ld6002b] Test the generated button dispatch 2026-10-16 16:00:39 -10:00
J. Nick Koston 7dbec6abf7 [ld6002b] Check each area_config setting on its own for duplicates, and test the check 2026-10-16 15:51:05 -10:00
J. Nick Koston 204a7cd1dd Merge branch 'apply-entities' into apply-entities-ld6002b 2026-10-16 14:07:10 -10:00
J. Nick Koston 754a960ed1 [core] Declare apply locals right before their first use
A local declared at the top ran option_at before a publish placed before
the statement, which changed the call order and grew ld2412 by 16 bytes.
2026-10-16 14:06:17 -10:00
J. Nick Koston 40c34b8628 Merge branch 'apply-entities' into apply-entities-ld6002b 2026-10-16 14:04:31 -10:00
J. Nick Koston f7a60f5859 [core] Give select apply statements an option local
Select apply statements can name the selected option text as option; the
local is declared only when a statement uses it.
2026-10-16 14:04:26 -10:00
J. Nick Koston 1dc9bb81f2 Drop the other components' migrations, which move to their own PRs
ld6002b needs only the Apply entity helpers, so this branch now differs from
apply-entities in ld6002b alone.
2026-10-16 13:45:31 -10:00
J. Nick Koston f4ea1d3b21 [ld6002b] Branch on the entity directly and drop the setting enums
The setters run each setting's code in its pointer branch, so the number, select
and switch type enums go. The buttons read the public APPLY_ENTITY and the unique
setting check is a partial.
2026-10-16 13:44:52 -10:00
J. Nick Koston ded8f7f31e Merge branch 'apply-entities' into apply-entities-ld6002b 2026-10-16 13:43:28 -10:00
J. Nick Koston b8a0de91db [core] Simplify the Apply entity helpers
Fold the function cache into SubEntities, make each domain's APPLY_ENTITY public,
shorten the class and docstring comments and reuse the shared test hub.
2026-10-16 13:42:23 -10:00
J. Nick Koston 2c659096de [ld6002b] Reject a setting configured twice on one hub 2026-10-16 13:19:16 -10:00
J. Nick Koston a8572e6885 Shorten the sleep time range comment 2026-10-16 13:14:15 -10:00
J. Nick Koston 3a2f9a24c2 Merge branch 'apply-entities-migrate' into apply-entities-ld6002b 2026-10-16 13:06:43 -10:00
J. Nick Koston fb54defeda [ld2410] Warn when a threshold number matches no gate 2026-10-16 13:06:31 -10:00
J. Nick Koston fbfe2a5590 Merge branch 'apply-entities-migrate' into apply-entities-ld6002b 2026-10-16 12:20:56 -10:00
J. Nick Koston e2d37f0d7e Merge branch 'apply-entities' into apply-entities-migrate 2026-10-16 12:20:54 -10:00
J. Nick Koston 2937d81216 Mark the test recorders as intentional mutable globals 2026-10-16 12:20:50 -10:00
J. Nick Koston afa42646fe [ld6002b] Brace the entity lookup chains 2026-10-16 12:19:39 -10:00
J. Nick Koston 3db32a284c [ld6002b] Name the button function after its first button and note the last-type fallback 2026-10-16 11:49:35 -10:00
J. Nick Koston 9dc43f5668 Merge branch 'apply-entities-migrate' into apply-entities-ld6002b 2026-10-16 11:47:49 -10:00
J. Nick Koston 9d59415766 [hoermann_hcp] Move the door command tests next to the hub tests 2026-10-16 11:47:43 -10:00
J. Nick Koston 298ca0c2b6 Merge branch 'apply-entities-migrate' into apply-entities-ld6002b 2026-10-16 11:45:54 -10:00
J. Nick Koston 29ef01a207 Merge branch 'apply-entities' into apply-entities-migrate 2026-10-16 11:45:51 -10:00
J. Nick Koston 05a4b256db Reject an unknown publish= value 2026-10-16 11:45:47 -10:00
J. Nick Koston 93c1ef5c50 [ld6002b] Find the number, select and switch setting by entity pointer in the hub 2026-10-16 11:15:12 -10:00
J. Nick Koston bed58f5895 [ld6002b] Share one generated function per platform, picking the type by entity pointer 2026-10-16 11:07:32 -10:00
J. Nick Koston 7070d516ca Merge branch 'apply-entities-migrate' into apply-entities-ld6002b 2026-10-16 11:05:56 -10:00
J. Nick Koston e8e98ee3db [ld2410] Find the gate from the number so all gate numbers share one class 2026-10-16 11:05:25 -10:00
J. Nick Koston 6aa647638c Merge branch 'apply-entities' into apply-entities-migrate 2026-10-16 11:04:41 -10:00
J. Nick Koston d0427c36eb [core] Share the apply function naming with automations and check the Apply class at codegen 2026-10-16 11:04:36 -10:00
J. Nick Koston d48ed8227f [ld6002b] Use Apply entities for the buttons, switches, numbers and selects 2026-10-16 10:53:37 -10:00
J. Nick Koston e2c2fdfb3d [bq25186] Use Apply entities for the SHIP_RST buttons 2026-10-16 10:51:36 -10:00
J. Nick Koston 7425b8d3b1 [es8388] Use Apply entities for the line selects 2026-10-16 10:51:36 -10:00
J. Nick Koston 5f697bb0d6 [espectre] Use Apply entities for the recalibrate button and traffic select 2026-10-16 10:51:36 -10:00
J. Nick Koston 8f79467d5f [at581x] Use an Apply entity for the RF switch 2026-10-16 10:51:36 -10:00
J. Nick Koston 5ca927da8e [hoermann_hcp] Use Apply entities for the buttons 2026-10-16 10:48:32 -10:00
J. Nick Koston 776a7f68d8 [tfluna] Use Apply entities for the buttons 2026-10-16 10:48:31 -10:00
J. Nick Koston 87de7d679e [haier] Use Apply entities for the cleaning buttons and airflow selects 2026-10-16 10:48:31 -10:00
J. Nick Koston e75a10ec7d [seeed_mr24hpc1] Use Apply entities for the forwarding buttons, switch, numbers and selects 2026-10-16 10:48:31 -10:00
J. Nick Koston f0ea8edfad [seeed_mr60fda2] Use Apply entities for the forwarding buttons and selects 2026-10-16 10:48:31 -10:00
J. Nick Koston 5df5d44516 [ld2420] Use Apply entities for the forwarding buttons and select 2026-10-16 10:48:31 -10:00
J. Nick Koston dd59a827ff [ld2450] Use Apply entities for the forwarding buttons, switches, numbers and selects 2026-10-16 10:48:30 -10:00
J. Nick Koston ed9b566091 [ld2412] Use Apply entities for the forwarding buttons, switches, numbers and selects 2026-10-16 10:48:30 -10:00
J. Nick Koston 5fd7c33916 [ld2410] Use Apply entities for the forwarding buttons, switches, numbers and selects 2026-10-16 10:48:30 -10:00
J. Nick Koston a990231342 [core] Add Apply button, switch, number and select entities for hub forwarders 2026-10-16 10:48:29 -10:00
18 changed files with 454 additions and 372 deletions
+33 -2
View File
@@ -2,10 +2,11 @@ from esphome import pins
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_WAKEUP_PIN
from esphome.const import CONF_PLATFORM, CONF_WAKEUP_PIN
import esphome.final_validate as fv
from esphome.types import ConfigType
from .const import CONF_AUTO_WAKE, CONF_WAKEUP_PULSE
from .const import CONF_AREA_CONFIG, CONF_AUTO_WAKE, CONF_LD6002B_ID, CONF_WAKEUP_PULSE
CODEOWNERS = ["@hepter"]
DEPENDENCIES = ["uart"]
@@ -69,3 +70,33 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_wakeup_pulse_ms(config[CONF_WAKEUP_PULSE].total_milliseconds))
cg.add(var.set_auto_wake(config[CONF_AUTO_WAKE]))
def validate_unique_settings(domain: str, config: ConfigType) -> None:
"""Reject a setting set in two platform blocks of one hub, which keeps one entity per setting."""
hub_id = config[CONF_LD6002B_ID]
entries = [
entry
for entry in fv.full_config.get().get(domain, [])
if entry.get(CONF_PLATFORM) == DOMAIN and entry.get(CONF_LD6002B_ID) == hub_id
]
for key, value in config.items():
if key in (CONF_PLATFORM, CONF_LD6002B_ID):
continue
# area_config groups six settings, so each one is checked on its own
for path in (
[[key, sub] for sub in value] if key == CONF_AREA_CONFIG else [[key]]
):
if sum(_has_path(entry, path) for entry in entries) > 1:
raise cv.Invalid(
f"{'.'.join(path)} is already set for this ld6002b in another {domain} block",
path=path,
)
def _has_path(entry: ConfigType, path: list[str]) -> bool:
for key in path:
if key not in entry:
return False
entry = entry[key]
return True
+47 -20
View File
@@ -1,3 +1,4 @@
from esphome.automation import parent_ref
import esphome.codegen as cg
from esphome.components import button
from esphome.components.select import DOMAIN as SELECT_DOMAIN
@@ -9,6 +10,7 @@ from esphome.const import (
ENTITY_CATEGORY_CONFIG,
ENTITY_CATEGORY_DIAGNOSTIC,
)
from esphome.cpp_generator import ParameterListExpression
import esphome.final_validate as fv
from esphome.types import ConfigType
@@ -33,53 +35,52 @@ from ..const import (
DEPENDENCIES = ["ld6002b"]
LD6002BButton = ld6002b_ns.class_("LD6002BButton", button.Button)
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
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_AUTO_INTERFERENCE): button.button_schema(
LD6002BButton, entity_category=ENTITY_CATEGORY_CONFIG
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_GET_AREAS): button.button_schema(
LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_CLEAR_INTERFERENCE): button.button_schema(
LD6002BButton, entity_category=ENTITY_CATEGORY_CONFIG
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_RESET_DETECTION_AREA): button.button_schema(
LD6002BButton, entity_category=ENTITY_CATEGORY_CONFIG
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_GET_DELAY): button.button_schema(
LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_GET_SENSITIVITY): button.button_schema(
LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_GET_TRIGGER_SPEED): button.button_schema(
LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_GET_Z_RANGE): button.button_schema(
LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_GET_INSTALLATION): button.button_schema(
LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_GET_LOW_POWER_MODE): button.button_schema(
LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_GET_LOW_POWER_SLEEP_TIME): button.button_schema(
LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_RESET_UNATTENDED): button.button_schema(
LD6002BButton, entity_category=ENTITY_CATEGORY_CONFIG
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_WAKE): button.button_schema(
LD6002BButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
}
)
@@ -131,8 +132,34 @@ BUTTON_MAP = {
async def to_code(config: ConfigType) -> None:
for key, button_type in BUTTON_MAP.items():
if button_config := config.get(key):
b = cg.new_Pvariable(button_config[CONF_ID], button_type)
await button.register_button(b, button_config)
await cg.register_parented(b, config[CONF_LD6002B_ID])
confs = [
(button_type, conf)
for key, button_type in BUTTON_MAP.items()
if (conf := config.get(key)) is not None
]
if not confs:
return
hub = await cg.get_variable(config[CONF_LD6002B_ID])
params = button.APPLY_ENTITY.params
# One function for all buttons, picking the type by pointer: one class instead of one per button.
# Named after the first button so two button blocks on one hub do not clash.
fn = f"esphome__{confs[0][1][CONF_ID].id}__button_fn"
cg.add_global(
cg.RawStatement(f"static void {fn}({ParameterListExpression(*params)});")
)
template = cg.TemplateArguments(cg.RawExpression(fn))
created = [
(button_type, await button.new_button(conf, template))
for button_type, conf in confs
]
*head, (last_type, _) = created
chain = "".join(f"entity == {parent_ref(var)} ? {t} : " for t, var in head)
cg.static_function(
fn,
cg.void,
list(params),
[
f"const auto type = {chain}{last_type};",
f"{parent_ref(hub)}->press_button(type);",
],
)
@@ -1,7 +0,0 @@
#include "ld6002b_button.h"
namespace esphome::ld6002b {
void LD6002BButton::press_action() { this->parent_->press_button(this->type_); }
} // namespace esphome::ld6002b
@@ -1,18 +0,0 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld6002b.h"
namespace esphome::ld6002b {
class LD6002BButton : public button::Button, public Parented<LD6002BComponent> {
public:
explicit LD6002BButton(ButtonType type) : type_(type) {}
protected:
void press_action() override;
ButtonType type_;
};
} // namespace esphome::ld6002b
+88 -108
View File
@@ -1347,92 +1347,74 @@ void LD6002BComponent::wake_() {
[this]() { this->wakeup_pin_->digital_write(true); });
}
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;
}
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;
#ifdef USE_NUMBER
// Each setter picks its setting by entity pointer, so the entities share one generated class.
void LD6002BComponent::set_number_value(number::Number *entity, float value) {
if (entity == this->hold_delay_number_ || entity == this->low_power_sleep_number_) {
uint8_t data[4];
write_u32_le(data, static_cast<uint32_t>(value));
this->queue_command_(entity == this->hold_delay_number_ ? TYPE_SET_HOLD_DELAY : TYPE_SET_LOW_POWER_SLEEP, data,
sizeof(data));
} else if (entity == this->z_min_number_) {
this->z_min_ = value;
this->send_z_range_();
} else if (entity == this->z_max_number_) {
this->z_max_ = value;
this->send_z_range_();
} else if (entity == this->area_x_min_number_) {
this->area_x_min_ = value;
this->area_edits_.x_min = value;
} else if (entity == this->area_x_max_number_) {
this->area_x_max_ = value;
this->area_edits_.x_max = value;
} else if (entity == this->area_y_min_number_) {
this->area_y_min_ = value;
this->area_edits_.y_min = value;
} else if (entity == this->area_y_max_number_) {
this->area_y_max_ = value;
this->area_edits_.y_max = value;
} else if (entity == this->area_z_min_number_) {
this->area_z_min_ = value;
this->area_edits_.z_min = value;
} else { // Only registered entities call this, so the last one needs no compare
this->area_z_max_ = value;
this->area_edits_.z_max = value;
}
}
#endif
void LD6002BComponent::set_select_value(SelectType type, size_t index) {
switch (type) {
case SelectType::SENSITIVITY:
if (index == 0) {
this->send_control_command_(CMD_SENSITIVITY_LOW);
} else if (index == 1) {
this->send_control_command_(CMD_SENSITIVITY_MEDIUM);
} else if (index == 2) {
this->send_control_command_(CMD_SENSITIVITY_HIGH);
}
break;
case SelectType::TRIGGER_SPEED:
if (index == 0) {
this->send_control_command_(CMD_TRIGGER_SLOW);
} else if (index == 1) {
this->send_control_command_(CMD_TRIGGER_MEDIUM);
} else if (index == 2) {
this->send_control_command_(CMD_TRIGGER_FAST);
}
break;
case SelectType::INSTALLATION_MODE:
if (index == 0) {
this->send_control_command_(CMD_INSTALL_TOP);
} else if (index == 1) {
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;
#ifdef USE_SELECT
void LD6002BComponent::set_select_value(select::Select *entity, size_t index) {
if (entity == this->sensitivity_select_) {
if (index == 0) {
this->send_control_command_(CMD_SENSITIVITY_LOW);
} else if (index == 1) {
this->send_control_command_(CMD_SENSITIVITY_MEDIUM);
} else if (index == 2) {
this->send_control_command_(CMD_SENSITIVITY_HIGH);
}
} else if (entity == this->trigger_speed_select_) {
if (index == 0) {
this->send_control_command_(CMD_TRIGGER_SLOW);
} else if (index == 1) {
this->send_control_command_(CMD_TRIGGER_MEDIUM);
} else if (index == 2) {
this->send_control_command_(CMD_TRIGGER_FAST);
}
} else if (entity == this->installation_select_) {
if (index == 0) {
this->send_control_command_(CMD_INSTALL_TOP);
} else if (index == 1) {
this->send_control_command_(CMD_INSTALL_SIDE);
}
} else { // Only registered entities call this, so the last one needs no compare
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_);
}
}
#endif
void LD6002BComponent::update_area_numbers_(const AreaConfig &area) {
// A report refreshes every axis the user is not in the middle of changing. An
@@ -1760,37 +1742,35 @@ void LD6002BComponent::clear_target_state_() {
}
}
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_SWITCH
void LD6002BComponent::set_switch_state(switch_::Switch *entity, bool state) {
if (entity == this->low_power_switch_) {
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_();
} else if (entity == this->point_cloud_switch_) {
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;
}
// 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;
} else { // Only registered entities call this, so the last one needs no compare
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_();
}
}
}
#endif
void LD6002BComponent::press_button(ButtonType type) {
switch (type) {
+9 -29
View File
@@ -40,32 +40,6 @@ 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,
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 {
LOW_POWER,
POINT_CLOUD,
TARGET_DISPLAY,
};
enum class ButtonType : uint8_t {
APPLY_AREA,
AUTO_INTERFERENCE,
@@ -264,9 +238,15 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
void set_target_display_switch(switch_::Switch *sw) { this->target_display_switch_ = sw; }
#endif
void set_number_value(NumberType type, float value);
void set_select_value(SelectType type, size_t index);
void set_switch_state(SwitchType type, bool state);
#ifdef USE_NUMBER
void set_number_value(number::Number *entity, float value);
#endif
#ifdef USE_SELECT
void set_select_value(select::Select *entity, size_t index);
#endif
#ifdef USE_SWITCH
void set_switch_state(switch_::Switch *entity, bool state);
#endif
void press_button(ButtonType type);
protected:
+41 -51
View File
@@ -15,7 +15,7 @@ from esphome.const import (
import esphome.final_validate as fv
from esphome.types import ConfigType
from .. import LD6002BComponent, ld6002b_ns
from .. import LD6002BComponent, validate_unique_settings
from ..const import (
CONF_APPLY_AREA,
CONF_AREA_CONFIG,
@@ -32,32 +32,29 @@ from ..const import (
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,
number.ApplyNumber,
unit_of_measurement=UNIT_SECOND,
device_class=DEVICE_CLASS_DURATION,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_Z_MIN): number.number_schema(
LD6002BNumber,
number.ApplyNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_Z_MAX): number.number_schema(
LD6002BNumber,
number.ApplyNumber,
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,
number.ApplyNumber,
unit_of_measurement=UNIT_MILLISECOND,
device_class=DEVICE_CLASS_DURATION,
entity_category=ENTITY_CATEGORY_CONFIG,
@@ -65,37 +62,37 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(CONF_AREA_CONFIG): cv.Schema(
{
cv.Optional(KEY_X_MIN): number.number_schema(
LD6002BNumber,
number.ApplyNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(KEY_X_MAX): number.number_schema(
LD6002BNumber,
number.ApplyNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(KEY_Y_MIN): number.number_schema(
LD6002BNumber,
number.ApplyNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(KEY_Y_MAX): number.number_schema(
LD6002BNumber,
number.ApplyNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_Z_MIN): number.number_schema(
LD6002BNumber,
number.ApplyNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_Z_MAX): number.number_schema(
LD6002BNumber,
number.ApplyNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
@@ -107,6 +104,7 @@ CONFIG_SCHEMA = cv.Schema(
def final_validate(config: ConfigType) -> None:
validate_unique_settings(number.DOMAIN, config)
if config.get(CONF_AREA_CONFIG) is None:
return
@@ -139,42 +137,34 @@ FINAL_VALIDATE_SCHEMA = final_validate
async def to_code(config: ConfigType) -> None:
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))
control = {"apply": "set_number_value(entity, value)", "publish": "before"}
numbers = number.sub_numbers(config, parent=hub)
await numbers(
CONF_HOLD_DELAY,
hub.set_hold_delay_number,
min_value=0,
max_value=65535,
step=1,
**control,
)
meters = {"min_value": -10, "max_value": 10, "step": 0.1, **control}
await numbers(CONF_Z_MIN, hub.set_z_min_number, **meters)
await numbers(CONF_Z_MAX, hub.set_z_max_number, **meters)
# 0x0205 is a uint32 of ms with no documented maximum; cap it at a minute
await numbers(
CONF_LOW_POWER_SLEEP_TIME,
hub.set_low_power_sleep_number,
min_value=0,
max_value=60000,
step=100,
**control,
)
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))
area = number.sub_numbers(area_config, parent=hub)
await area(KEY_X_MIN, hub.set_area_x_min_number, **meters)
await area(KEY_X_MAX, hub.set_area_x_max_number, **meters)
await area(KEY_Y_MIN, hub.set_area_y_min_number, **meters)
await area(KEY_Y_MAX, hub.set_area_y_max_number, **meters)
await area(CONF_Z_MIN, hub.set_area_z_min_number, **meters)
await area(CONF_Z_MAX, hub.set_area_z_max_number, **meters)
@@ -1,10 +0,0 @@
#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
@@ -1,18 +0,0 @@
#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
+31 -35
View File
@@ -1,17 +1,16 @@
from functools import partial
import esphome.codegen as cg
from esphome.components import select
import esphome.config_validation as cv
from esphome.const import CONF_AREA_ID, CONF_SENSITIVITY, ENTITY_CATEGORY_CONFIG
from esphome.types import ConfigType
from .. import LD6002BComponent, ld6002b_ns
from .. import LD6002BComponent, validate_unique_settings
from ..const import CONF_INSTALLATION_MODE, CONF_LD6002B_ID, CONF_TRIGGER_SPEED
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",
@@ -27,49 +26,46 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
cv.Optional(CONF_SENSITIVITY): select.select_schema(
LD6002BSelect, entity_category=ENTITY_CATEGORY_CONFIG
select.ApplySelect, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_TRIGGER_SPEED): select.select_schema(
LD6002BSelect, entity_category=ENTITY_CATEGORY_CONFIG
select.ApplySelect, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_INSTALLATION_MODE): select.select_schema(
LD6002BSelect, entity_category=ENTITY_CATEGORY_CONFIG
select.ApplySelect, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_AREA_ID): select.select_schema(
LD6002BSelect, entity_category=ENTITY_CATEGORY_CONFIG
select.ApplySelect, entity_category=ENTITY_CATEGORY_CONFIG
),
}
)
SELECT_MAP = (
(
CONF_SENSITIVITY,
SelectType.SENSITIVITY,
"set_sensitivity_select",
["low", "medium", "high"],
),
(
CONF_TRIGGER_SPEED,
SelectType.TRIGGER_SPEED,
"set_trigger_speed_select",
["slow", "medium", "fast"],
),
(
CONF_INSTALLATION_MODE,
SelectType.INSTALLATION_MODE,
"set_installation_select",
["top", "side"],
),
(CONF_AREA_ID, SelectType.AREA_ID, "set_area_id_select", AREA_ID_OPTIONS),
)
FINAL_VALIDATE_SCHEMA = partial(validate_unique_settings, select.DOMAIN)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD6002B_ID])
for key, select_type, setter, options in SELECT_MAP:
if conf := config.get(key):
s = await select.new_select(conf, select_type, options=options)
await cg.register_parented(s, config[CONF_LD6002B_ID])
cg.add(getattr(hub, setter)(s))
control = {"apply": "set_select_value(entity, index)", "publish": "before"}
selects = select.sub_selects(config, parent=hub)
await selects(
CONF_SENSITIVITY,
hub.set_sensitivity_select,
options=["low", "medium", "high"],
**control,
)
await selects(
CONF_TRIGGER_SPEED,
hub.set_trigger_speed_select,
options=["slow", "medium", "fast"],
**control,
)
await selects(
CONF_INSTALLATION_MODE,
hub.set_installation_select,
options=["top", "side"],
**control,
)
await selects(
CONF_AREA_ID, hub.set_area_id_select, options=AREA_ID_OPTIONS, **control
)
@@ -1,10 +0,0 @@
#include "ld6002b_select.h"
namespace esphome::ld6002b {
void LD6002BSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_select_value(this->type_, index);
}
} // namespace esphome::ld6002b
@@ -1,18 +0,0 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../ld6002b.h"
namespace esphome::ld6002b {
class LD6002BSelect : public select::Select, public Parented<LD6002BComponent> {
public:
explicit LD6002BSelect(SelectType type) : type_(type) {}
protected:
void control(size_t index) override;
SelectType type_;
};
} // namespace esphome::ld6002b
+14 -17
View File
@@ -1,10 +1,12 @@
from functools import partial
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 esphome.types import ConfigType
from .. import LD6002BComponent, ld6002b_ns
from .. import LD6002BComponent, validate_unique_settings
from ..const import (
CONF_LD6002B_ID,
CONF_LOW_POWER,
@@ -14,9 +16,6 @@ from ..const import (
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,
@@ -25,19 +24,19 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
cv.Optional(CONF_LOW_POWER): switch.switch_schema(
LD6002BSwitch,
switch.ApplySwitch,
block_inverted=True,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_POINT_CLOUD): switch.switch_schema(
LD6002BSwitch,
switch.ApplySwitch,
block_inverted=True,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_TARGET_DISPLAY): switch.switch_schema(
LD6002BSwitch,
switch.ApplySwitch,
block_inverted=True,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
@@ -47,15 +46,13 @@ CONFIG_SCHEMA = cv.Schema(
)
FINAL_VALIDATE_SCHEMA = partial(validate_unique_settings, switch.DOMAIN)
async def to_code(config: ConfigType) -> None:
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))
control = {"apply": "set_switch_state(entity, state)", "publish": "after"}
switches = switch.sub_switches(config, parent=hub)
await switches(CONF_LOW_POWER, hub.set_low_power_switch, **control)
await switches(CONF_POINT_CLOUD, hub.set_point_cloud_switch, **control)
await switches(CONF_TARGET_DISPLAY, hub.set_target_display_switch, **control)
@@ -1,10 +0,0 @@
#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
@@ -1,18 +0,0 @@
#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,49 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
uart:
- id: uart_a
tx_pin: GPIO17
rx_pin: GPIO16
baud_rate: 115200
- id: uart_b
tx_pin: GPIO18
rx_pin: GPIO19
baud_rate: 115200
ld6002b:
- id: hub_a
uart_id: uart_a
wakeup_pin: GPIO14
- id: hub_b
uart_id: uart_b
button:
# Two blocks on hub_a: the dispatcher names must not clash.
- platform: ld6002b
ld6002b_id: hub_a
get_delay:
id: a_get_delay
name: A Get Delay
get_sensitivity:
id: a_get_sensitivity
name: A Get Sensitivity
wake:
id: a_wake
name: A Wake
- platform: ld6002b
ld6002b_id: hub_a
get_z_range:
id: a_get_z_range
name: A Get Z Range
# A single button on hub_b: no pointer chain, only the fallback type.
- platform: ld6002b
ld6002b_id: hub_b
get_installation:
id: b_get_installation
name: B Get Installation
@@ -0,0 +1,82 @@
"""Code generation tests for the ld6002b button dispatcher.
Each button block gets one function that maps the pressed entity to its button
type and calls its own hub; the last button of a block is the fallback type.
"""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
import re
import pytest
YAML = Path(__file__).parent / "button_dispatch.yaml"
def _fn_body(main_cpp: str, fn: str) -> str:
match = re.search(
rf"static void {fn}\(button::Button \* entity\) \{{\n(.*?)\n\}}",
main_cpp,
re.DOTALL,
)
assert match is not None, f"{fn} not defined"
return match.group(1)
@pytest.mark.parametrize(
("fn", "type_expr", "hub"),
[
# Two blocks on hub_a: each named after its first button, chain ends in the fallback.
(
"esphome__a_get_delay__button_fn",
(
"entity == ::a_get_delay ? ld6002b::ButtonType::GET_DELAY : "
"entity == ::a_get_sensitivity ? ld6002b::ButtonType::GET_SENSITIVITY : "
"ld6002b::ButtonType::WAKE"
),
"hub_a",
),
(
"esphome__a_get_z_range__button_fn",
"ld6002b::ButtonType::GET_Z_RANGE",
"hub_a",
),
# A single button on a second hub: no chain, its own hub.
(
"esphome__b_get_installation__button_fn",
"ld6002b::ButtonType::GET_INSTALLATION",
"hub_b",
),
],
)
def test_dispatch_function(
generate_main: Callable[[str | Path], str], fn: str, type_expr: str, hub: str
) -> None:
main_cpp = generate_main(YAML)
assert main_cpp.count(f"static void {fn}(button::Button * entity) {{") == 1
assert _fn_body(main_cpp, fn) == (
f" const auto type = {type_expr};\n ::{hub}->press_button(type);"
)
def test_each_button_uses_its_block_function(
generate_main: Callable[[str | Path], str],
) -> None:
main_cpp = generate_main(YAML)
expected = {
"a_get_delay": "esphome__a_get_delay__button_fn",
"a_get_sensitivity": "esphome__a_get_delay__button_fn",
"a_wake": "esphome__a_get_delay__button_fn",
"a_get_z_range": "esphome__a_get_z_range__button_fn",
"b_get_installation": "esphome__b_get_installation__button_fn",
}
for button_id, fn in expected.items():
assert f"new({button_id}) button::ApplyButton<{fn}>();" in main_cpp
# One definition per block, no more.
assert (
len(re.findall(r"static void esphome__\w+__button_fn\(.*\) \{", main_cpp)) == 3
)
@@ -9,11 +9,17 @@ from __future__ import annotations
import pytest
from esphome.components.ld6002b import validate_unique_settings
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.const import (
CONF_AREA_CONFIG,
CONF_HOLD_DELAY,
CONF_Z_MAX,
CONF_Z_MIN,
)
from esphome.components.ld6002b.number import (
CONFIG_SCHEMA as NUMBER_CONFIG_SCHEMA,
FINAL_VALIDATE_SCHEMA as NUMBER_FINAL_VALIDATE_SCHEMA,
@@ -40,6 +46,7 @@ def _full_config(
*,
selects: list[ConfigType] | None = None,
buttons: list[ConfigType] | None = None,
numbers: list[ConfigType] | None = None,
) -> Config:
"""A full config carrying one ld6002b hub, as the ID pass leaves it.
@@ -57,6 +64,8 @@ def _full_config(
full["select"] = selects
if buttons is not None:
full[CONF_BUTTON] = buttons
if numbers is not None:
full["number"] = numbers
return full
@@ -214,3 +223,53 @@ def test_area_config_without_select_is_rejected(
),
):
_validated_numbers(_area_numbers())
def _number_block(**settings: ConfigType) -> ConfigType:
"""A number platform block on the shared hub, as final validation sees it."""
return {
"platform": "ld6002b",
"ld6002b_id": ID(HUB_ID, is_declaration=False, type="ld6002b"),
**settings,
}
@pytest.mark.parametrize(
("first", "second", "error"),
[
(
{CONF_HOLD_DELAY: {"name": "A"}},
{CONF_HOLD_DELAY: {"name": "B"}},
r"^hold_delay is already set .* @ data\['hold_delay'\]$",
),
(
{CONF_AREA_CONFIG: {CONF_Z_MIN: {"name": "A"}}},
{CONF_AREA_CONFIG: {CONF_Z_MIN: {"name": "B"}}},
r"^area_config\.z_min is already set .* @ data\['area_config'\]\['z_min'\]$",
),
(
{CONF_AREA_CONFIG: {CONF_Z_MIN: {"name": "A"}}},
{CONF_AREA_CONFIG: {CONF_Z_MAX: {"name": "B"}}},
None,
),
({CONF_HOLD_DELAY: {"name": "A"}}, {CONF_Z_MIN: {"name": "B"}}, None),
],
)
def test_setting_in_two_blocks_of_one_hub(
set_core_config: SetCoreConfigCallable,
first: ConfigType,
second: ConfigType,
error: str | None,
) -> None:
"""Each setting maps to one entity pointer, so two blocks cannot share one."""
blocks = [_number_block(**first), _number_block(**second)]
set_core_config(
PlatformFramework.ESP32_IDF,
full_config=_full_config(_hub(wakeup_pin=False), numbers=blocks),
)
if error is None:
validate_unique_settings("number", blocks[1])
return
with pytest.raises(cv.Invalid, match=error):
validate_unique_settings("number", blocks[1])