Compare commits

...
Author SHA1 Message Date
J. Nick Koston 0e1542d197 [dfrobot_sen0609] Test the number pointer dispatch 2026-10-20 18:00:02 -10:00
J. Nick Koston 3212a61a3c [dfrobot_sen0609] Share one Apply function across the numbers 2026-10-20 17:49:11 -10:00
J. Nick Koston ce428e0c88 [dfrobot_sen0609] Use Apply numbers for the hub settings 2026-10-20 17:10:06 -10:00
J. Nick Koston 5ea7e5fd70 Drop the unused ld2450_ns import 2026-10-20 03:19:36 -10:00
J. Nick Koston 1d280bebac Merge remote-tracking branch 'origin/dev' into apply-entities-number
# Conflicts:
#	esphome/components/ld2450/number/__init__.py
2026-10-20 03:19:27 -10:00
J. Nick Koston 97c732319d Mark the ld2410 test hub pointer as an intentional global 2026-10-19 17:36:39 -10:00
J. Nick Koston d707402ef3 [ld2410] Test the gate threshold number lookup 2026-10-19 17:31:51 -10:00
J. Nick Koston 611e804e52 Merge remote-tracking branch 'origin/dev' into apply-entities-number 2026-10-19 07:52:32 -10:00
J. Nick Koston f308fd9655 Merge branch 'apply-entities' into apply-entities-number 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 33e51b76dc Merge branch 'apply-entities' into apply-entities-number 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 aad0c59855 [seeed_mr24hpc1] Use Apply numbers 2026-10-16 13:46:08 -10:00
J. Nick Koston 482a46489b [ld2450] Use Apply numbers 2026-10-16 13:46:07 -10:00
J. Nick Koston 116ae120dc [ld2412] Use Apply numbers 2026-10-16 13:46:07 -10:00
J. Nick Koston d65620c4d1 [ld2410] Use Apply numbers 2026-10-16 13:46:07 -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 2937d81216 Mark the test recorders as intentional mutable globals 2026-10-16 12:20:50 -10:00
J. Nick Koston 05a4b256db Reject an unknown publish= value 2026-10-16 11:45:47 -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 a990231342 [core] Add Apply button, switch, number and select entities for hub forwarders 2026-10-16 10:48:29 -10:00
41 changed files with 284 additions and 443 deletions
@@ -459,6 +459,15 @@ void DFRobotSEN0609Component::control_number(NumberSetting setting, float value)
this->request_write_(static_cast<Param>(progmem_read_byte(&NUMBER_PARAMS[setting])));
}
void DFRobotSEN0609Component::control_number(number::Number *number, float value) {
for (uint8_t i = 0; i < NUMBER_COUNT; i++) {
if (this->numbers_[i] == number) {
this->control_number(static_cast<NumberSetting>(i), value);
return;
}
}
}
void DFRobotSEN0609Component::forget_number_changes_(Param param, bool unknown) {
for (uint8_t i = 0; i < NUMBER_COUNT; i++) {
if (progmem_read_byte(&NUMBER_PARAMS[i]) != param)
@@ -130,6 +130,8 @@ class DFRobotSEN0609Component final : public Component, public uart::UARTDevice
#ifdef USE_NUMBER
void set_number(NumberSetting setting, number::Number *number) { this->numbers_[setting] = number; }
void control_number(NumberSetting setting, float value);
// Finds the setting by pointer so every number shares one Apply function
void control_number(number::Number *number, float value);
#endif
#ifdef USE_SWITCH
void set_switch(SwitchSetting setting, switch_::Switch *sw) { this->switches_[setting] = sw; }
@@ -31,7 +31,6 @@ from .. import (
get_hub_config,
)
DFRobotSEN0609Number = dfrobot_sen0609_ns.class_("DFRobotSEN0609Number", number.Number)
NumberSetting = dfrobot_sen0609_ns.enum("NumberSetting")
CONF_HOLD_SENSITIVITY = "hold_sensitivity"
@@ -55,7 +54,7 @@ class Setting(NamedTuple):
def _distance(icon: str = ICON_RULER) -> cv.Schema:
return number.number_schema(
DFRobotSEN0609Number,
number.ApplyNumber,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=icon,
@@ -65,7 +64,7 @@ def _distance(icon: str = ICON_RULER) -> cv.Schema:
def _duration() -> cv.Schema:
return number.number_schema(
DFRobotSEN0609Number,
number.ApplyNumber,
device_class=DEVICE_CLASS_DURATION,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_TIMELAPSE,
@@ -75,7 +74,7 @@ def _duration() -> cv.Schema:
def _plain() -> cv.Schema:
return number.number_schema(
DFRobotSEN0609Number,
number.ApplyNumber,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
)
@@ -187,8 +186,8 @@ async def to_code(config: ConfigType) -> None:
await numbers(
s.key,
lambda n, setting=s.setting: hub.set_number(setting, n),
s.setting,
min_value=s.min_value,
max_value=max_range if s.max_value is None else s.max_value,
step=s.step,
apply="control_number(entity, value)",
)
@@ -1,18 +0,0 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../dfrobot_sen0609.h"
namespace esphome::dfrobot_sen0609 {
class DFRobotSEN0609Number final : public number::Number, public Parented<DFRobotSEN0609Component> {
public:
explicit DFRobotSEN0609Number(NumberSetting setting) : setting_(setting) {}
protected:
void control(float value) override { this->parent_->control_number(this->setting_, value); }
NumberSetting setting_;
};
} // namespace esphome::dfrobot_sen0609
+10 -1
View File
@@ -725,7 +725,16 @@ void LD2410Component::set_max_distances_timeout() {
this->set_config_mode_(false);
}
void LD2410Component::set_gate_threshold(uint8_t gate) {
void LD2410Component::set_gate_threshold(number::Number *n) {
// Found by pointer: up to 18 compares per write beat storing the gate in each number
uint8_t gate = 0;
while (gate < TOTAL_GATES && this->gate_move_threshold_numbers_[gate] != n &&
this->gate_still_threshold_numbers_[gate] != n)
gate++;
if (gate == TOTAL_GATES) {
ESP_LOGW(TAG, "Threshold number is not a known gate");
return;
}
number::Number *motionsens = this->gate_move_threshold_numbers_[gate];
number::Number *stillsens = this->gate_still_threshold_numbers_[gate];
+1 -1
View File
@@ -87,7 +87,7 @@ class LD2410Component final : public Component, public uart::UARTDevice {
void set_gate_still_threshold_number(uint8_t gate, number::Number *n);
void set_gate_move_threshold_number(uint8_t gate, number::Number *n);
void set_max_distances_timeout();
void set_gate_threshold(uint8_t gate);
void set_gate_threshold(number::Number *n);
#endif
#ifdef USE_SENSOR
void set_gate_move_sensor(uint8_t gate, sensor::Sensor *s);
+35 -25
View File
@@ -1,3 +1,5 @@
from functools import partial
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
@@ -18,11 +20,7 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns
GateThresholdNumber = ld2410_ns.class_("GateThresholdNumber", number.Number)
LightThresholdNumber = ld2410_ns.class_("LightThresholdNumber", number.Number)
MaxDistanceTimeoutNumber = ld2410_ns.class_("MaxDistanceTimeoutNumber", number.Number)
from .. import CONF_LD2410_ID, LD2410Component
CONF_MAX_MOVE_DISTANCE_GATE = "max_move_distance_gate"
CONF_MAX_STILL_DISTANCE_GATE = "max_still_distance_gate"
@@ -35,25 +33,25 @@ CONFIG_SCHEMA = cv.Schema(
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
cv.Inclusive(CONF_TIMEOUT, TIMEOUT_GROUP): number.number_schema(
MaxDistanceTimeoutNumber,
number.ApplyNumber,
unit_of_measurement=UNIT_SECOND,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_TIMELAPSE,
),
cv.Inclusive(CONF_MAX_MOVE_DISTANCE_GATE, TIMEOUT_GROUP): number.number_schema(
MaxDistanceTimeoutNumber,
number.ApplyNumber,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Inclusive(CONF_MAX_STILL_DISTANCE_GATE, TIMEOUT_GROUP): number.number_schema(
MaxDistanceTimeoutNumber,
number.ApplyNumber,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Optional(CONF_LIGHT_THRESHOLD): number.number_schema(
LightThresholdNumber,
number.ApplyNumber,
device_class=DEVICE_CLASS_ILLUMINANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_LIGHTBULB,
@@ -66,14 +64,14 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
cv.Optional(f"g{x}"): cv.Schema(
{
cv.Required(CONF_MOVE_THRESHOLD): number.number_schema(
GateThresholdNumber,
number.ApplyNumber,
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
unit_of_measurement=UNIT_PERCENT,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Required(CONF_STILL_THRESHOLD): number.number_schema(
GateThresholdNumber,
number.ApplyNumber,
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
unit_of_measurement=UNIT_PERCENT,
entity_category=ENTITY_CATEGORY_CONFIG,
@@ -89,8 +87,14 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2410_ID])
numbers = number.sub_numbers(config, parent=hub)
timeouts = {"apply": "set_max_distances_timeout()", "publish": "before"}
await numbers(
CONF_TIMEOUT, hub.set_timeout_number, min_value=0, max_value=65535, step=1
CONF_TIMEOUT,
hub.set_timeout_number,
min_value=0,
max_value=65535,
step=1,
**timeouts,
)
await numbers(
CONF_MAX_MOVE_DISTANCE_GATE,
@@ -98,6 +102,7 @@ async def to_code(config: ConfigType) -> None:
min_value=2,
max_value=8,
step=1,
**timeouts,
)
await numbers(
CONF_MAX_STILL_DISTANCE_GATE,
@@ -105,6 +110,7 @@ async def to_code(config: ConfigType) -> None:
min_value=2,
max_value=8,
step=1,
**timeouts,
)
await numbers(
CONF_LIGHT_THRESHOLD,
@@ -112,21 +118,25 @@ async def to_code(config: ConfigType) -> None:
min_value=0,
max_value=255,
step=1,
apply="set_light_out_control()",
publish="before",
)
threshold = {
"min_value": 0,
"max_value": 100,
"step": 1,
"apply": "set_gate_threshold(entity)",
"publish": "before",
}
for x in range(9):
if gate_conf := config.get(f"g{x}"):
move_config = gate_conf[CONF_MOVE_THRESHOLD]
n = cg.new_Pvariable(move_config[CONF_ID], x)
await number.register_number(
n, move_config, min_value=0, max_value=100, step=1
await numbers.create(
gate_conf[CONF_MOVE_THRESHOLD],
partial(hub.set_gate_move_threshold_number, x),
**threshold,
)
await cg.register_parented(n, hub)
cg.add(hub.set_gate_move_threshold_number(x, n))
still_config = gate_conf[CONF_STILL_THRESHOLD]
n = cg.new_Pvariable(still_config[CONF_ID], x)
await number.register_number(
n, still_config, min_value=0, max_value=100, step=1
await numbers.create(
gate_conf[CONF_STILL_THRESHOLD],
partial(hub.set_gate_still_threshold_number, x),
**threshold,
)
await cg.register_parented(n, hub)
cg.add(hub.set_gate_still_threshold_number(x, n))
@@ -1,12 +0,0 @@
#include "gate_threshold_number.h"
namespace esphome::ld2410 {
GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {}
void GateThresholdNumber::control(float value) {
this->publish_state(value);
this->parent_->set_gate_threshold(this->gate_);
}
} // namespace esphome::ld2410
@@ -1,17 +0,0 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class GateThresholdNumber final : public number::Number, public Parented<LD2410Component> {
public:
GateThresholdNumber(uint8_t gate);
protected:
uint8_t gate_;
void control(float value) override;
};
} // namespace esphome::ld2410
@@ -1,10 +0,0 @@
#include "light_threshold_number.h"
namespace esphome::ld2410 {
void LightThresholdNumber::control(float value) {
this->publish_state(value);
this->parent_->set_light_out_control();
}
} // namespace esphome::ld2410
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class LightThresholdNumber final : public number::Number, public Parented<LD2410Component> {
public:
LightThresholdNumber() = default;
protected:
void control(float value) override;
};
} // namespace esphome::ld2410
@@ -1,10 +0,0 @@
#include "max_distance_timeout_number.h"
namespace esphome::ld2410 {
void MaxDistanceTimeoutNumber::control(float value) {
this->publish_state(value);
this->parent_->set_max_distances_timeout();
}
} // namespace esphome::ld2410
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2410.h"
namespace esphome::ld2410 {
class MaxDistanceTimeoutNumber final : public number::Number, public Parented<LD2410Component> {
public:
MaxDistanceTimeoutNumber() = default;
protected:
void control(float value) override;
};
} // namespace esphome::ld2410
+31 -20
View File
@@ -1,3 +1,5 @@
from functools import partial
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
@@ -18,11 +20,7 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component
GateThresholdNumber = LD2412_ns.class_("GateThresholdNumber", number.Number)
LightThresholdNumber = LD2412_ns.class_("LightThresholdNumber", number.Number)
MaxDistanceTimeoutNumber = LD2412_ns.class_("MaxDistanceTimeoutNumber", number.Number)
from .. import CONF_LD2412_ID, LD2412Component
CONF_LIGHT_THRESHOLD = "light_threshold"
CONF_MAX_DISTANCE_GATE = "max_distance_gate"
@@ -35,25 +33,25 @@ CONFIG_SCHEMA = cv.Schema(
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
cv.Optional(CONF_LIGHT_THRESHOLD): number.number_schema(
LightThresholdNumber,
number.ApplyNumber,
device_class=DEVICE_CLASS_ILLUMINANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_LIGHTBULB,
),
cv.Optional(CONF_MAX_DISTANCE_GATE): number.number_schema(
MaxDistanceTimeoutNumber,
number.ApplyNumber,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Optional(CONF_MIN_DISTANCE_GATE): number.number_schema(
MaxDistanceTimeoutNumber,
number.ApplyNumber,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
),
cv.Optional(CONF_TIMEOUT): number.number_schema(
MaxDistanceTimeoutNumber,
number.ApplyNumber,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_TIMELAPSE,
unit_of_measurement=UNIT_SECOND,
@@ -66,14 +64,14 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
cv.Optional(f"gate_{x}"): (
{
cv.Required(CONF_MOVE_THRESHOLD): number.number_schema(
GateThresholdNumber,
number.ApplyNumber,
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
unit_of_measurement=UNIT_PERCENT,
),
cv.Required(CONF_STILL_THRESHOLD): number.number_schema(
GateThresholdNumber,
number.ApplyNumber,
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_MOTION_SENSOR,
@@ -95,13 +93,17 @@ async def to_code(config: ConfigType) -> None:
min_value=0,
max_value=255,
step=1,
apply="set_light_out_control()",
publish="before",
)
basic = {"apply": "set_basic_config()", "publish": "before"}
await numbers(
CONF_MAX_DISTANCE_GATE,
hub.set_max_distance_gate_number,
min_value=2,
max_value=13,
step=1,
**basic,
)
await numbers(
CONF_MIN_DISTANCE_GATE,
@@ -109,23 +111,32 @@ async def to_code(config: ConfigType) -> None:
min_value=1,
max_value=12,
step=1,
**basic,
)
threshold = {
"min_value": 0,
"max_value": 100,
"step": 1,
"apply": "set_gate_threshold(entity)",
"publish": "before",
}
for x in range(14):
if gate_conf := config.get(f"gate_{x}"):
move_config = gate_conf[CONF_MOVE_THRESHOLD]
n = await number.new_number(move_config, min_value=0, max_value=100, step=1)
await cg.register_parented(n, hub)
cg.add(hub.set_gate_move_threshold_number(x, n))
still_config = gate_conf[CONF_STILL_THRESHOLD]
n = await number.new_number(
still_config, min_value=0, max_value=100, step=1
await numbers.create(
gate_conf[CONF_MOVE_THRESHOLD],
partial(hub.set_gate_move_threshold_number, x),
**threshold,
)
await numbers.create(
gate_conf[CONF_STILL_THRESHOLD],
partial(hub.set_gate_still_threshold_number, x),
**threshold,
)
await cg.register_parented(n, hub)
cg.add(hub.set_gate_still_threshold_number(x, n))
await numbers(
CONF_TIMEOUT,
hub.set_timeout_number,
min_value=0,
max_value=900,
step=1,
**basic,
)
@@ -1,10 +0,0 @@
#include "gate_threshold_number.h"
namespace esphome::ld2412 {
void GateThresholdNumber::control(float value) {
this->publish_state(value);
this->parent_->set_gate_threshold(this);
}
} // namespace esphome::ld2412
@@ -1,17 +0,0 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2412.h"
namespace esphome::ld2412 {
class GateThresholdNumber final : public number::Number, public Parented<LD2412Component> {
public:
// Not "= default": that makes new(p) T() zero-fill the object at every codegen site before the ctor runs.
GateThresholdNumber() {}
protected:
void control(float value) override;
};
} // namespace esphome::ld2412
@@ -1,10 +0,0 @@
#include "light_threshold_number.h"
namespace esphome::ld2412 {
void LightThresholdNumber::control(float value) {
this->publish_state(value);
this->parent_->set_light_out_control();
}
} // namespace esphome::ld2412
@@ -1,17 +0,0 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2412.h"
namespace esphome::ld2412 {
class LightThresholdNumber final : public number::Number, public Parented<LD2412Component> {
public:
// User provided, not "= default": `new(p) LightThresholdNumber()` would zero-fill .bss that is already zero.
LightThresholdNumber() {}
protected:
void control(float value) override;
};
} // namespace esphome::ld2412
@@ -1,10 +0,0 @@
#include "max_distance_timeout_number.h"
namespace esphome::ld2412 {
void MaxDistanceTimeoutNumber::control(float value) {
this->publish_state(value);
this->parent_->set_basic_config();
}
} // namespace esphome::ld2412
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2412.h"
namespace esphome::ld2412 {
class MaxDistanceTimeoutNumber final : public number::Number, public Parented<LD2412Component> {
public:
MaxDistanceTimeoutNumber() = default;
protected:
void control(float value) override;
};
} // namespace esphome::ld2412
+4 -3
View File
@@ -11,7 +11,7 @@ from esphome.const import (
)
from esphome.types import ConfigType
from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns
from .. import CONF_LD2450_ID, LD2450Component
CONF_PRESENCE_TIMEOUT = "presence_timeout"
CONF_X1 = "x1"
@@ -24,14 +24,13 @@ ICON_ARROW_TOP_LEFT = "mdi:arrow-top-left"
ICON_ARROW_TOP_LEFT_BOLD_BOX_OUTLINE = "mdi:arrow-top-left-bold-box-outline"
MAX_ZONES = 3
PresenceTimeoutNumber = ld2450_ns.class_("PresenceTimeoutNumber", number.Number)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Required(CONF_PRESENCE_TIMEOUT): number.number_schema(
PresenceTimeoutNumber,
number.ApplyNumber,
unit_of_measurement=UNIT_SECOND,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_TIMELAPSE,
@@ -87,6 +86,8 @@ async def to_code(config: ConfigType) -> None:
min_value=0,
max_value=3600,
step=1,
apply="set_presence_timeout()",
publish="before",
)
for zone_num in range(MAX_ZONES):
if zone_conf := config.get(f"zone_{zone_num + 1}"):
@@ -1,10 +0,0 @@
#include "presence_timeout_number.h"
namespace esphome::ld2450 {
void PresenceTimeoutNumber::control(float value) {
this->publish_state(value);
this->parent_->set_presence_timeout();
}
} // namespace esphome::ld2450
@@ -1,17 +0,0 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2450.h"
namespace esphome::ld2450 {
class PresenceTimeoutNumber : public number::Number, public Parented<LD2450Component> {
public:
// User provided, not "= default": `new(p) PresenceTimeoutNumber()` would zero-fill .bss that is already zero.
PresenceTimeoutNumber() {}
protected:
void control(float value) override;
};
} // namespace esphome::ld2450
@@ -4,15 +4,7 @@ import esphome.config_validation as cv
from esphome.const import CONF_SENSITIVITY, ENTITY_CATEGORY_CONFIG
from esphome.types import ConfigType
from .. import CONF_MR24HPC1_ID, MR24HPC1Component, mr24hpc1_ns
SensitivityNumber = mr24hpc1_ns.class_("SensitivityNumber", number.Number)
CustomModeNumber = mr24hpc1_ns.class_("CustomModeNumber", number.Number)
ExistenceThresholdNumber = mr24hpc1_ns.class_("ExistenceThresholdNumber", number.Number)
MotionThresholdNumber = mr24hpc1_ns.class_("MotionThresholdNumber", number.Number)
MotionTriggerTimeNumber = mr24hpc1_ns.class_("MotionTriggerTimeNumber", number.Number)
MotionToRestTimeNumber = mr24hpc1_ns.class_("MotionToRestTimeNumber", number.Number)
CustomUnmanTimeNumber = mr24hpc1_ns.class_("CustomUnmanTimeNumber", number.Number)
from .. import CONF_MR24HPC1_ID, MR24HPC1Component
CONF_CUSTOM_MODE = "custom_mode"
CONF_EXISTENCE_THRESHOLD = "existence_threshold"
@@ -25,37 +17,37 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_MR24HPC1_ID): cv.use_id(MR24HPC1Component),
cv.Optional(CONF_SENSITIVITY): number.number_schema(
SensitivityNumber,
number.ApplyNumber,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:archive-check-outline",
),
cv.Optional(CONF_CUSTOM_MODE): number.number_schema(
CustomModeNumber,
number.ApplyNumber,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:cog",
),
cv.Optional(CONF_EXISTENCE_THRESHOLD): number.number_schema(
ExistenceThresholdNumber,
number.ApplyNumber,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_MOTION_THRESHOLD): number.number_schema(
MotionThresholdNumber,
number.ApplyNumber,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_MOTION_TRIGGER): number.number_schema(
MotionTriggerTimeNumber,
number.ApplyNumber,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:camera-timer",
unit_of_measurement="ms",
),
cv.Optional(CONF_MOTION_TO_REST): number.number_schema(
MotionToRestTimeNumber,
number.ApplyNumber,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:camera-timer",
unit_of_measurement="ms",
),
cv.Optional(CONF_CUSTOM_UNMAN_TIME): number.number_schema(
CustomUnmanTimeNumber,
number.ApplyNumber,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:camera-timer",
unit_of_measurement="s",
@@ -68,10 +60,21 @@ async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_MR24HPC1_ID])
numbers = number.sub_numbers(config, parent=hub)
await numbers(
CONF_SENSITIVITY, hub.set_sensitivity_number, min_value=0, max_value=3, step=1
CONF_SENSITIVITY,
hub.set_sensitivity_number,
min_value=0,
max_value=3,
step=1,
apply="set_sensitivity(value)",
)
await numbers(
CONF_CUSTOM_MODE, hub.set_custom_mode_number, min_value=0, max_value=4, step=1
CONF_CUSTOM_MODE,
hub.set_custom_mode_number,
min_value=0,
max_value=4,
step=1,
apply="set_custom_mode(value)",
publish="before",
)
await numbers(
CONF_EXISTENCE_THRESHOLD,
@@ -79,6 +82,7 @@ async def to_code(config: ConfigType) -> None:
min_value=0,
max_value=250,
step=1,
apply="set_existence_threshold(value)",
)
await numbers(
CONF_MOTION_THRESHOLD,
@@ -86,6 +90,7 @@ async def to_code(config: ConfigType) -> None:
min_value=0,
max_value=250,
step=1,
apply="set_motion_threshold(value)",
)
await numbers(
CONF_MOTION_TRIGGER,
@@ -93,6 +98,7 @@ async def to_code(config: ConfigType) -> None:
min_value=0,
max_value=150,
step=1,
apply="set_motion_trigger_time(value)",
)
await numbers(
CONF_MOTION_TO_REST,
@@ -100,6 +106,7 @@ async def to_code(config: ConfigType) -> None:
min_value=0,
max_value=3000,
step=1,
apply="set_motion_to_rest_time(value)",
)
await numbers(
CONF_CUSTOM_UNMAN_TIME,
@@ -107,4 +114,5 @@ async def to_code(config: ConfigType) -> None:
min_value=0,
max_value=3600,
step=1,
apply="set_custom_unman_time(value)",
)
@@ -1,10 +0,0 @@
#include "custom_mode_number.h"
namespace esphome::seeed_mr24hpc1 {
void CustomModeNumber::control(float value) {
this->publish_state(value);
this->parent_->set_custom_mode(value);
}
} // namespace esphome::seeed_mr24hpc1
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../seeed_mr24hpc1.h"
namespace esphome::seeed_mr24hpc1 {
class CustomModeNumber final : public number::Number, public Parented<MR24HPC1Component> {
public:
CustomModeNumber() = default;
protected:
void control(float value) override;
};
} // namespace esphome::seeed_mr24hpc1
@@ -1,7 +0,0 @@
#include "custom_unman_time_number.h"
namespace esphome::seeed_mr24hpc1 {
void CustomUnmanTimeNumber::control(float value) { this->parent_->set_custom_unman_time(value); }
} // namespace esphome::seeed_mr24hpc1
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../seeed_mr24hpc1.h"
namespace esphome::seeed_mr24hpc1 {
class CustomUnmanTimeNumber final : public number::Number, public Parented<MR24HPC1Component> {
public:
CustomUnmanTimeNumber() = default;
protected:
void control(float value) override;
};
} // namespace esphome::seeed_mr24hpc1
@@ -1,7 +0,0 @@
#include "existence_threshold_number.h"
namespace esphome::seeed_mr24hpc1 {
void ExistenceThresholdNumber::control(float value) { this->parent_->set_existence_threshold(value); }
} // namespace esphome::seeed_mr24hpc1
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../seeed_mr24hpc1.h"
namespace esphome::seeed_mr24hpc1 {
class ExistenceThresholdNumber final : public number::Number, public Parented<MR24HPC1Component> {
public:
ExistenceThresholdNumber() = default;
protected:
void control(float value) override;
};
} // namespace esphome::seeed_mr24hpc1
@@ -1,7 +0,0 @@
#include "motion_threshold_number.h"
namespace esphome::seeed_mr24hpc1 {
void MotionThresholdNumber::control(float value) { this->parent_->set_motion_threshold(value); }
} // namespace esphome::seeed_mr24hpc1
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../seeed_mr24hpc1.h"
namespace esphome::seeed_mr24hpc1 {
class MotionThresholdNumber final : public number::Number, public Parented<MR24HPC1Component> {
public:
MotionThresholdNumber() = default;
protected:
void control(float value) override;
};
} // namespace esphome::seeed_mr24hpc1
@@ -1,7 +0,0 @@
#include "motion_trigger_time_number.h"
namespace esphome::seeed_mr24hpc1 {
void MotionTriggerTimeNumber::control(float value) { this->parent_->set_motion_trigger_time(value); }
} // namespace esphome::seeed_mr24hpc1
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../seeed_mr24hpc1.h"
namespace esphome::seeed_mr24hpc1 {
class MotionTriggerTimeNumber final : public number::Number, public Parented<MR24HPC1Component> {
public:
MotionTriggerTimeNumber() = default;
protected:
void control(float value) override;
};
} // namespace esphome::seeed_mr24hpc1
@@ -1,7 +0,0 @@
#include "motiontorest_time_number.h"
namespace esphome::seeed_mr24hpc1 {
void MotionToRestTimeNumber::control(float value) { this->parent_->set_motion_to_rest_time(value); }
} // namespace esphome::seeed_mr24hpc1
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../seeed_mr24hpc1.h"
namespace esphome::seeed_mr24hpc1 {
class MotionToRestTimeNumber final : public number::Number, public Parented<MR24HPC1Component> {
public:
MotionToRestTimeNumber() = default;
protected:
void control(float value) override;
};
} // namespace esphome::seeed_mr24hpc1
@@ -1,7 +0,0 @@
#include "sensitivity_number.h"
namespace esphome::seeed_mr24hpc1 {
void SensitivityNumber::control(float value) { this->parent_->set_sensitivity(value); }
} // namespace esphome::seeed_mr24hpc1
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../seeed_mr24hpc1.h"
namespace esphome::seeed_mr24hpc1 {
class SensitivityNumber final : public number::Number, public Parented<MR24HPC1Component> {
public:
SensitivityNumber() = default;
protected:
void control(float value) override;
};
} // namespace esphome::seeed_mr24hpc1
@@ -54,6 +54,35 @@ TEST_F(SEN0609SequencerTest, ChangesMadeTogetherShareOneSave) {
EXPECT_EQ(this->next_command_(), "");
}
// The Apply numbers pass themselves; the hub finds the setting by pointer
TEST_F(SEN0609SequencerTest, NumberPointerFindsLaterSetting) {
this->boot_();
// Hold sensitivity sits after unregistered slots in the number array
this->hub_.control_number(&this->hold_sensitivity_, 3.0f);
EXPECT_FLOAT_EQ(this->hold_sensitivity_.state, 3.0f);
this->expect_and_answer_("sensorStop");
this->expect_and_answer_("setSensitivity 3 5");
this->expect_and_answer_("saveConfig");
this->expect_and_answer_("sensorStart");
EXPECT_EQ(this->next_command_(), "");
}
TEST_F(SEN0609SequencerTest, NumberPointerInvalidRangeKeepsState) {
this->boot_();
this->hub_.control_number(&this->min_range_, 20.0f);
EXPECT_FLOAT_EQ(this->min_range_.state, 0.6f);
EXPECT_EQ(this->next_command_(), "");
}
TEST_F(SEN0609SequencerTest, UnregisteredNumberPointerWritesNothing) {
this->boot_();
TestNumber other;
this->hub_.control_number(&other, 5.0f);
EXPECT_FLOAT_EQ(this->min_range_.state, 0.6f);
EXPECT_FLOAT_EQ(this->hold_sensitivity_.state, 7.0f);
EXPECT_EQ(this->next_command_(), "");
}
TEST_F(SEN0609SequencerTest, WriteWaitsBeforeStarting) {
this->boot_();
this->hub_.control_number(NUMBER_MIN_RANGE, 1.0f);
@@ -0,0 +1,121 @@
#include <algorithm>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "esphome/components/ld2410/ld2410.h"
#include "esphome/components/uart/uart_component.h"
namespace esphome::ld2410::testing {
namespace {
class CapturingUARTComponent : public uart::UARTComponent {
public:
void write_array(const uint8_t *data, size_t len) override {
this->written.insert(this->written.end(), data, data + len);
}
MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override));
MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override));
MOCK_METHOD(size_t, available, (), (override));
MOCK_METHOD(uart::UARTFlushResult, flush, (), (override));
MOCK_METHOD(void, check_logger_conflict, (), (override));
#if defined(USE_ESP8266) || defined(USE_ESP32)
void load_settings(bool dump_config) override {}
#endif // USE_ESP8266 || USE_ESP32
std::vector<uint8_t> written;
};
LD2410Component *hub = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// Same body codegen emits for publish="before": publish the new value, then hand the number to the hub.
void apply_threshold(number::Number *entity, float value) {
entity->publish_state(value);
hub->set_gate_threshold(entity);
}
using ThresholdNumber = number::ApplyNumber<apply_threshold>;
// Only these gates get numbers; gate 8 is the last one.
constexpr uint8_t GATES[] = {4, 8};
// Header, length 20, command 0x64 (set gate sensitivity): the 18 byte payload follows.
constexpr uint8_t GATE_SENS_PREFIX[] = {0xFD, 0xFC, 0xFB, 0xFA, 0x14, 0x00, 0x64, 0x00};
std::vector<uint8_t> gate_sens_payload(uint8_t gate, uint8_t move, uint8_t still) {
return {0x00, 0x00, gate, 0x00, 0x00, 0x00, 0x01, 0x00, move, 0x00, 0x00, 0x00, 0x02, 0x00, still, 0x00, 0x00, 0x00};
}
} // namespace
class LD2410GateThresholdTest : public ::testing::Test {
protected:
void SetUp() override {
hub = &this->ld2410_;
this->ld2410_.set_uart_parent(&this->uart_);
// Gates 0 to 3 and 5 to 7 have no numbers, so the lookup must walk past the empty slots.
for (size_t i = 0; i < 2; i++) {
const uint8_t gate = GATES[i];
this->ld2410_.set_gate_move_threshold_number(gate, &this->move_[i]);
this->ld2410_.set_gate_still_threshold_number(gate, &this->still_[i]);
for (ThresholdNumber *n : {&this->move_[i], &this->still_[i]}) {
n->traits.set_min_value(0);
n->traits.set_max_value(100);
n->traits.set_step(1);
}
this->move_[i].publish_state(10 + gate);
this->still_[i].publish_state(20 + gate);
}
}
void TearDown() override { hub = nullptr; }
// Every gate sensitivity command written since the last clear, as its payload.
std::vector<std::vector<uint8_t>> sent_gate_commands_() const {
std::vector<std::vector<uint8_t>> out;
const auto &w = this->uart_.written;
auto it = w.begin();
while ((it = std::search(it, w.end(), std::begin(GATE_SENS_PREFIX), std::end(GATE_SENS_PREFIX))) != w.end()) {
it += sizeof(GATE_SENS_PREFIX);
if (static_cast<size_t>(w.end() - it) < 18)
break;
out.emplace_back(it, it + 18);
it += 18;
}
return out;
}
CapturingUARTComponent uart_;
LD2410Component ld2410_;
ThresholdNumber move_[2];
ThresholdNumber still_[2];
};
TEST_F(LD2410GateThresholdTest, DirectCallSendsTheNumbersGateWithBothValues) {
for (size_t i = 0; i < 2; i++) {
for (ThresholdNumber *n : {&this->move_[i], &this->still_[i]}) {
this->uart_.written.clear();
this->ld2410_.set_gate_threshold(n);
auto sent = this->sent_gate_commands_();
ASSERT_EQ(sent.size(), 1u) << "gate " << +GATES[i];
EXPECT_EQ(sent[0], gate_sens_payload(GATES[i], 10 + GATES[i], 20 + GATES[i])) << "gate " << +GATES[i];
}
}
}
TEST_F(LD2410GateThresholdTest, PublishBeforeApplySendsTheNewValue) {
this->move_[1].make_call().set_value(55).perform();
auto sent = this->sent_gate_commands_();
ASSERT_EQ(sent.size(), 1u);
EXPECT_EQ(sent[0], gate_sens_payload(8, 55, 28));
this->uart_.written.clear();
this->still_[0].make_call().set_value(66).perform();
sent = this->sent_gate_commands_();
ASSERT_EQ(sent.size(), 1u);
EXPECT_EQ(sent[0], gate_sens_payload(4, 14, 66));
}
TEST_F(LD2410GateThresholdTest, UnregisteredNumberSendsNothing) {
ThresholdNumber other;
other.publish_state(42);
this->ld2410_.set_gate_threshold(&other);
EXPECT_TRUE(this->uart_.written.empty());
}
} // namespace esphome::ld2410::testing
@@ -0,0 +1,12 @@
from esphome.core import CORE
from esphome.types import ConfigType
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# Turns on USE_NUMBER so the hub's number forwarding code is in the host test build
async def to_code_testing(config: ConfigType) -> None:
CORE.testing_ensure_platform_registered("number")
manifest.to_code = to_code_testing
manifest.dependencies = manifest.dependencies + ["number"]