1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 16:51:52 +00:00

Compare commits

...

4 Commits

Author SHA1 Message Date
J. Nick Koston
3c85ff4744 try to avoid bloat 2026-02-07 19:56:20 +01:00
J. Nick Koston
0fa7050b1c remove temp test 2026-02-07 10:01:57 +01:00
J. Nick Koston
fa1554cac0 [scheduler] Eliminate heap allocation in full_cleanup_removed_items_
Replace the temporary std::vector copy with in-place compaction using a
read/write pointer pattern. This avoids a heap allocation+deallocation
cycle during scheduler cleanup, reducing heap fragmentation on
long-running ESP devices.

The new approach compacts valid items forward in the existing vector,
recycles removed items as they are encountered, then resizes the vector
(no reallocation since size only shrinks). Same O(n) complexity, same
behavior, zero allocations.
2026-02-07 09:54:43 +01:00
tronikos
eb7aa3420f Add target_temperature to the template water heater (#13661)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-02-06 21:23:42 +01:00
7 changed files with 41 additions and 12 deletions

View File

@@ -46,6 +46,7 @@ CONFIG_SCHEMA = (
RESTORE_MODES, upper=True
),
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda,
cv.Optional(CONF_TARGET_TEMPERATURE): cv.returning_lambda,
cv.Optional(CONF_MODE): cv.returning_lambda,
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
water_heater.validate_water_heater_mode
@@ -78,6 +79,14 @@ async def to_code(config: ConfigType) -> None:
)
cg.add(var.set_current_temperature_lambda(template_))
if CONF_TARGET_TEMPERATURE in config:
template_ = await cg.process_lambda(
config[CONF_TARGET_TEMPERATURE],
[],
return_type=cg.optional.template(cg.float_),
)
cg.add(var.set_target_temperature_lambda(template_))
if CONF_MODE in config:
template_ = await cg.process_lambda(
config[CONF_MODE],

View File

@@ -16,7 +16,8 @@ void TemplateWaterHeater::setup() {
restore->perform();
}
}
if (!this->current_temperature_f_.has_value() && !this->mode_f_.has_value())
if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() &&
!this->mode_f_.has_value())
this->disable_loop();
}
@@ -28,6 +29,9 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() {
}
traits.set_supports_current_temperature(true);
if (this->target_temperature_f_.has_value()) {
traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE);
}
return traits;
}
@@ -42,6 +46,14 @@ void TemplateWaterHeater::loop() {
}
}
auto target_temp = this->target_temperature_f_.call();
if (target_temp.has_value()) {
if (*target_temp != this->target_temperature_) {
this->target_temperature_ = *target_temp;
changed = true;
}
}
auto new_mode = this->mode_f_.call();
if (new_mode.has_value()) {
if (*new_mode != this->mode_) {

View File

@@ -20,6 +20,9 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
template<typename F> void set_current_temperature_lambda(F &&f) {
this->current_temperature_f_.set(std::forward<F>(f));
}
template<typename F> void set_target_temperature_lambda(F &&f) {
this->target_temperature_f_.set(std::forward<F>(f));
}
template<typename F> void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward<F>(f)); }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
@@ -44,6 +47,7 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
// Ordered to minimize padding on 32-bit: 4-byte members first, then smaller
Trigger<> set_trigger_;
TemplateLambda<float> current_temperature_f_;
TemplateLambda<float> target_temperature_f_;
TemplateLambda<water_heater::WaterHeaterMode> mode_f_;
TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE};
water_heater::WaterHeaterModeMask supported_modes_;

View File

@@ -390,20 +390,19 @@ void Scheduler::full_cleanup_removed_items_() {
// 4. No operations inside can block or take other locks, so no deadlock risk
LockGuard guard{this->lock_};
std::vector<std::unique_ptr<SchedulerItem>> valid_items;
// Move all non-removed items to valid_items, recycle removed ones
for (auto &item : this->items_) {
if (!is_item_removed_(item.get())) {
valid_items.push_back(std::move(item));
// Compact in-place: move valid items forward, recycle removed ones
size_t write = 0;
for (size_t read = 0; read < this->items_.size(); ++read) {
if (!is_item_removed_(this->items_[read].get())) {
if (write != read) {
this->items_[write] = std::move(this->items_[read]);
}
++write;
} else {
// Recycle removed items
this->recycle_item_main_loop_(std::move(item));
this->recycle_item_main_loop_(std::move(this->items_[read]));
}
}
// Replace items_ with the filtered list
this->items_ = std::move(valid_items);
this->items_.erase(this->items_.begin() + write, this->items_.end());
// Rebuild the heap structure since items are no longer in heap order
std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
this->to_remove_ = 0;

View File

@@ -412,6 +412,7 @@ water_heater:
name: "Template Water Heater"
optimistic: true
current_temperature: !lambda "return 42.0f;"
target_temperature: !lambda "return 60.0f;"
mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;"
supported_modes:
- "OFF"

View File

@@ -10,6 +10,7 @@ water_heater:
name: Test Boiler
optimistic: true
current_temperature: !lambda "return 45.0f;"
target_temperature: !lambda "return 60.0f;"
# Note: No mode lambda - we want optimistic mode changes to stick
# A mode lambda would override mode changes in loop()
supported_modes:

View File

@@ -85,6 +85,9 @@ async def test_water_heater_template(
assert initial_state.current_temperature == 45.0, (
f"Expected current temp 45.0, got {initial_state.current_temperature}"
)
assert initial_state.target_temperature == 60.0, (
f"Expected target temp 60.0, got {initial_state.target_temperature}"
)
# Test changing to GAS mode
client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS)