mirror of
https://github.com/esphome/esphome.git
synced 2026-02-10 03:27:36 -07:00
Compare commits
1 Commits
scheduler-
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b97a728cf1 |
@@ -1,7 +1,8 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_THROTTLE
|
||||
from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE, CONF_TRIGGER_ID
|
||||
|
||||
AUTO_LOAD = ["ld24xx"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
@@ -11,6 +12,8 @@ MULTI_CONF = True
|
||||
ld2450_ns = cg.esphome_ns.namespace("ld2450")
|
||||
LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice)
|
||||
|
||||
LD2450DataTrigger = ld2450_ns.class_("LD2450DataTrigger", automation.Trigger.template())
|
||||
|
||||
CONF_LD2450_ID = "ld2450_id"
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
@@ -20,6 +23,11 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_THROTTLE): cv.invalid(
|
||||
f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead"
|
||||
),
|
||||
cv.Optional(CONF_ON_DATA): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LD2450DataTrigger),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
@@ -45,3 +53,6 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
for conf in config.get(CONF_ON_DATA, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
@@ -413,6 +413,10 @@ void LD2450Component::restart_and_read_all_info() {
|
||||
this->set_timeout(1500, [this]() { this->read_all_info(); });
|
||||
}
|
||||
|
||||
void LD2450Component::add_on_data_callback(std::function<void()> &&callback) {
|
||||
this->data_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
// Send command with values to LD2450
|
||||
void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
|
||||
ESP_LOGV(TAG, "Sending COMMAND %02X", command);
|
||||
@@ -613,6 +617,8 @@ void LD2450Component::handle_periodic_data_() {
|
||||
this->still_presence_millis_ = App.get_loop_component_start_time();
|
||||
}
|
||||
#endif
|
||||
|
||||
this->data_callback_.call();
|
||||
}
|
||||
|
||||
bool LD2450Component::handle_ack_data_() {
|
||||
|
||||
@@ -141,6 +141,9 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2, int32_t zone2_y2, int32_t zone3_x1,
|
||||
int32_t zone3_y1, int32_t zone3_x2, int32_t zone3_y2);
|
||||
|
||||
/// Add a callback that will be called after each successfully processed periodic data frame.
|
||||
void add_on_data_callback(std::function<void()> &&callback);
|
||||
|
||||
protected:
|
||||
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
|
||||
void set_config_mode_(bool enable);
|
||||
@@ -190,6 +193,15 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
std::array<text_sensor::TextSensor *, 3> direction_text_sensors_{};
|
||||
#endif
|
||||
|
||||
LazyCallbackManager<void()> data_callback_;
|
||||
};
|
||||
|
||||
class LD2450DataTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit LD2450DataTrigger(LD2450Component *parent) {
|
||||
parent->add_on_data_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2450
|
||||
|
||||
@@ -107,24 +107,6 @@ static void validate_static_string(const char *name) {
|
||||
// iterating over them from the loop task is fine; but iterating from any other context requires the lock to be held to
|
||||
// avoid the main thread modifying the list while it is being accessed.
|
||||
|
||||
// Calculate random offset for interval timers
|
||||
// Extracted from set_timer_common_ to reduce code size - float math + random_float()
|
||||
// only needed for intervals, not timeouts
|
||||
uint32_t Scheduler::calculate_interval_offset_(uint32_t delay) {
|
||||
return static_cast<uint32_t>(std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float());
|
||||
}
|
||||
|
||||
// Check if a retry was already cancelled in items_ or to_add_
|
||||
// Extracted from set_timer_common_ to reduce code size - retry path is cold and deprecated
|
||||
// Remove before 2026.8.0 along with all retry code
|
||||
bool Scheduler::is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name,
|
||||
uint32_t hash_or_id) {
|
||||
return has_cancelled_timeout_in_container_locked_(this->items_, component, name_type, static_name, hash_or_id,
|
||||
/* match_retry= */ true) ||
|
||||
has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_type, static_name, hash_or_id,
|
||||
/* match_retry= */ true);
|
||||
}
|
||||
|
||||
// Common implementation for both timeout and interval
|
||||
// name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id
|
||||
void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type,
|
||||
@@ -148,66 +130,84 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
// Create and populate the scheduler item
|
||||
auto item = this->get_item_from_pool_locked_();
|
||||
item->component = component;
|
||||
item->set_name(name_type, static_name, hash_or_id);
|
||||
switch (name_type) {
|
||||
case NameType::STATIC_STRING:
|
||||
item->set_static_name(static_name);
|
||||
break;
|
||||
case NameType::HASHED_STRING:
|
||||
item->set_hashed_name(hash_or_id);
|
||||
break;
|
||||
case NameType::NUMERIC_ID:
|
||||
item->set_numeric_id(hash_or_id);
|
||||
break;
|
||||
case NameType::NUMERIC_ID_INTERNAL:
|
||||
item->set_internal_id(hash_or_id);
|
||||
break;
|
||||
}
|
||||
item->type = type;
|
||||
item->callback = std::move(func);
|
||||
// Reset remove flag - recycled items may have been cancelled (remove=true) in previous use
|
||||
this->set_item_removed_(item.get(), false);
|
||||
item->is_retry = is_retry;
|
||||
|
||||
// Determine target container: defer_queue_ for deferred items, to_add_ for everything else.
|
||||
// Using a pointer lets both paths share the cancel + push_back epilogue.
|
||||
auto *target = &this->to_add_;
|
||||
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
// Special handling for defer() (delay = 0, type = TIMEOUT)
|
||||
// Single-core platforms don't need thread-safe defer handling
|
||||
if (delay == 0 && type == SchedulerItem::TIMEOUT) {
|
||||
// Put in defer queue for guaranteed FIFO execution
|
||||
target = &this->defer_queue_;
|
||||
} else
|
||||
if (!skip_cancel) {
|
||||
this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type);
|
||||
}
|
||||
this->defer_queue_.push_back(std::move(item));
|
||||
return;
|
||||
}
|
||||
#endif /* not ESPHOME_THREAD_SINGLE */
|
||||
{
|
||||
// Type-specific setup
|
||||
if (type == SchedulerItem::INTERVAL) {
|
||||
item->interval = delay;
|
||||
// first execution happens immediately after a random smallish offset
|
||||
uint32_t offset = this->calculate_interval_offset_(delay);
|
||||
item->set_next_execution(now + offset);
|
||||
|
||||
// Type-specific setup
|
||||
if (type == SchedulerItem::INTERVAL) {
|
||||
item->interval = delay;
|
||||
// first execution happens immediately after a random smallish offset
|
||||
// Calculate random offset (0 to min(interval/2, 5s))
|
||||
uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float());
|
||||
item->set_next_execution(now + offset);
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
SchedulerNameLog name_log;
|
||||
ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms",
|
||||
name_log.format(name_type, static_name, hash_or_id), delay, offset);
|
||||
SchedulerNameLog name_log;
|
||||
ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms",
|
||||
name_log.format(name_type, static_name, hash_or_id), delay, offset);
|
||||
#endif
|
||||
} else {
|
||||
item->interval = 0;
|
||||
item->set_next_execution(now + delay);
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now);
|
||||
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||
|
||||
// For retries, check if there's a cancelled timeout first
|
||||
// Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name
|
||||
if (is_retry && (name_type != NameType::STATIC_STRING || static_name != nullptr) &&
|
||||
type == SchedulerItem::TIMEOUT &&
|
||||
this->is_retry_cancelled_locked_(component, name_type, static_name, hash_or_id)) {
|
||||
// Skip scheduling - the retry was cancelled
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
SchedulerNameLog skip_name_log;
|
||||
ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item",
|
||||
skip_name_log.format(name_type, static_name, hash_or_id));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
item->interval = 0;
|
||||
item->set_next_execution(now + delay);
|
||||
}
|
||||
|
||||
// Common epilogue: atomic cancel-and-add (unless skip_cancel is true)
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now);
|
||||
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||
|
||||
// For retries, check if there's a cancelled timeout first
|
||||
// Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name
|
||||
if (is_retry && (name_type != NameType::STATIC_STRING || static_name != nullptr) && type == SchedulerItem::TIMEOUT &&
|
||||
(has_cancelled_timeout_in_container_locked_(this->items_, component, name_type, static_name, hash_or_id,
|
||||
/* match_retry= */ true) ||
|
||||
has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_type, static_name, hash_or_id,
|
||||
/* match_retry= */ true))) {
|
||||
// Skip scheduling - the retry was cancelled
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
SchedulerNameLog skip_name_log;
|
||||
ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item",
|
||||
skip_name_log.format(name_type, static_name, hash_or_id));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// If name is provided, do atomic cancel-and-add (unless skip_cancel is true)
|
||||
// Cancel existing items
|
||||
if (!skip_cancel) {
|
||||
this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type);
|
||||
}
|
||||
target->push_back(std::move(item));
|
||||
// Add new item directly to to_add_
|
||||
// since we have the lock held
|
||||
this->to_add_.push_back(std::move(item));
|
||||
}
|
||||
|
||||
void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function<void()> func) {
|
||||
|
||||
@@ -219,15 +219,28 @@ class Scheduler {
|
||||
// Helper to get the name type
|
||||
NameType get_name_type() const { return name_type_; }
|
||||
|
||||
// Set name storage: for STATIC_STRING stores the pointer, for all other types stores hash_or_id.
|
||||
// Both union members occupy the same offset, so only one store is needed.
|
||||
void set_name(NameType type, const char *static_name, uint32_t hash_or_id) {
|
||||
if (type == NameType::STATIC_STRING) {
|
||||
name_.static_name = static_name;
|
||||
} else {
|
||||
name_.hash_or_id = hash_or_id;
|
||||
}
|
||||
name_type_ = type;
|
||||
// Helper to set a static string name (no allocation)
|
||||
void set_static_name(const char *name) {
|
||||
name_.static_name = name;
|
||||
name_type_ = NameType::STATIC_STRING;
|
||||
}
|
||||
|
||||
// Helper to set a hashed string name (hash computed from std::string)
|
||||
void set_hashed_name(uint32_t hash) {
|
||||
name_.hash_or_id = hash;
|
||||
name_type_ = NameType::HASHED_STRING;
|
||||
}
|
||||
|
||||
// Helper to set a numeric ID name
|
||||
void set_numeric_id(uint32_t id) {
|
||||
name_.hash_or_id = id;
|
||||
name_type_ = NameType::NUMERIC_ID;
|
||||
}
|
||||
|
||||
// Helper to set an internal numeric ID (separate namespace from NUMERIC_ID)
|
||||
void set_internal_id(uint32_t id) {
|
||||
name_.hash_or_id = id;
|
||||
name_type_ = NameType::NUMERIC_ID_INTERNAL;
|
||||
}
|
||||
|
||||
static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b);
|
||||
@@ -342,17 +355,6 @@ class Scheduler {
|
||||
// Helper to perform full cleanup when too many items are cancelled
|
||||
void full_cleanup_removed_items_();
|
||||
|
||||
// Helper to calculate random offset for interval timers - extracted to reduce code size of set_timer_common_
|
||||
// IMPORTANT: Must not be inlined - called only for intervals, keeping it out of the hot path saves flash.
|
||||
uint32_t __attribute__((noinline)) calculate_interval_offset_(uint32_t delay);
|
||||
|
||||
// Helper to check if a retry was already cancelled - extracted to reduce code size of set_timer_common_
|
||||
// Remove before 2026.8.0 along with all retry code.
|
||||
// IMPORTANT: Must not be inlined - retry path is cold and deprecated.
|
||||
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
|
||||
bool __attribute__((noinline))
|
||||
is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id);
|
||||
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
// Helper for debug logging in set_timer_common_ - extracted to reduce code size
|
||||
void debug_log_timer_(const SchedulerItem *item, NameType name_type, const char *static_name, uint32_t hash_or_id,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
ld2450:
|
||||
- id: ld2450_radar
|
||||
on_data:
|
||||
then:
|
||||
- logger.log: "LD2450 Radar Data Received"
|
||||
|
||||
button:
|
||||
- platform: ld2450
|
||||
|
||||
Reference in New Issue
Block a user