From b6fdd299537529f128b570594b3a3584cf7cc8df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Feb 2026 14:42:40 -0600 Subject: [PATCH] [voice_assistant] Replace timer unordered_map with vector to eliminate per-tick heap allocation (#13857) --- .../components/voice_assistant/__init__.py | 7 ++- .../voice_assistant/voice_assistant.cpp | 46 ++++++++++--------- .../voice_assistant/voice_assistant.h | 9 ++-- .../voice_assistant/common-idf.yaml | 21 +++++++++ tests/components/voice_assistant/common.yaml | 21 +++++++++ 5 files changed, 77 insertions(+), 27 deletions(-) diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index d28c786dd8..8b7dcb4f21 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -371,7 +371,12 @@ async def to_code(config): if on_timer_tick := config.get(CONF_ON_TIMER_TICK): await automation.build_automation( var.get_timer_tick_trigger(), - [(cg.std_vector.template(Timer), "timers")], + [ + ( + cg.std_vector.template(Timer).operator("const").operator("ref"), + "timers", + ) + ], on_timer_tick, ) has_timers = True diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 7f5fbe62e1..6267d97480 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -859,35 +859,43 @@ void VoiceAssistant::on_audio(const api::VoiceAssistantAudio &msg) { } void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse &msg) { - Timer timer = { - .id = msg.timer_id, - .name = msg.name, - .total_seconds = msg.total_seconds, - .seconds_left = msg.seconds_left, - .is_active = msg.is_active, - }; - this->timers_[timer.id] = timer; + // Find existing timer or add a new one + auto it = this->timers_.begin(); + for (; it != this->timers_.end(); ++it) { + if (it->id == msg.timer_id) + break; + } + if (it == this->timers_.end()) { + this->timers_.push_back({}); + it = this->timers_.end() - 1; + } + it->id = msg.timer_id; + it->name = msg.name; + it->total_seconds = msg.total_seconds; + it->seconds_left = msg.seconds_left; + it->is_active = msg.is_active; + char timer_buf[Timer::TO_STR_BUFFER_SIZE]; ESP_LOGD(TAG, "Timer Event\n" " Type: %" PRId32 "\n" " %s", - msg.event_type, timer.to_str(timer_buf)); + msg.event_type, it->to_str(timer_buf)); switch (msg.event_type) { case api::enums::VOICE_ASSISTANT_TIMER_STARTED: - this->timer_started_trigger_.trigger(timer); + this->timer_started_trigger_.trigger(*it); break; case api::enums::VOICE_ASSISTANT_TIMER_UPDATED: - this->timer_updated_trigger_.trigger(timer); + this->timer_updated_trigger_.trigger(*it); break; case api::enums::VOICE_ASSISTANT_TIMER_CANCELLED: - this->timer_cancelled_trigger_.trigger(timer); - this->timers_.erase(timer.id); + this->timer_cancelled_trigger_.trigger(*it); + this->timers_.erase(it); break; case api::enums::VOICE_ASSISTANT_TIMER_FINISHED: - this->timer_finished_trigger_.trigger(timer); - this->timers_.erase(timer.id); + this->timer_finished_trigger_.trigger(*it); + this->timers_.erase(it); break; } @@ -901,16 +909,12 @@ void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse } void VoiceAssistant::timer_tick_() { - std::vector res; - res.reserve(this->timers_.size()); - for (auto &pair : this->timers_) { - auto &timer = pair.second; + for (auto &timer : this->timers_) { if (timer.is_active && timer.seconds_left > 0) { timer.seconds_left--; } - res.push_back(timer); } - this->timer_tick_trigger_.trigger(res); + this->timer_tick_trigger_.trigger(this->timers_); } void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg) { diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 0ef7ecc81a..b1b5f20bff 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -24,7 +24,6 @@ #include "esphome/components/socket/socket.h" #include -#include #include namespace esphome { @@ -226,9 +225,9 @@ class VoiceAssistant : public Component { Trigger *get_timer_updated_trigger() { return &this->timer_updated_trigger_; } Trigger *get_timer_cancelled_trigger() { return &this->timer_cancelled_trigger_; } Trigger *get_timer_finished_trigger() { return &this->timer_finished_trigger_; } - Trigger> *get_timer_tick_trigger() { return &this->timer_tick_trigger_; } + Trigger &> *get_timer_tick_trigger() { return &this->timer_tick_trigger_; } void set_has_timers(bool has_timers) { this->has_timers_ = has_timers; } - const std::unordered_map &get_timers() const { return this->timers_; } + const std::vector &get_timers() const { return this->timers_; } protected: bool allocate_buffers_(); @@ -267,13 +266,13 @@ class VoiceAssistant : public Component { api::APIConnection *api_client_{nullptr}; - std::unordered_map timers_; + std::vector timers_; void timer_tick_(); Trigger timer_started_trigger_; Trigger timer_finished_trigger_; Trigger timer_updated_trigger_; Trigger timer_cancelled_trigger_; - Trigger> timer_tick_trigger_; + Trigger &> timer_tick_trigger_; bool has_timers_{false}; bool timer_tick_running_{false}; diff --git a/tests/components/voice_assistant/common-idf.yaml b/tests/components/voice_assistant/common-idf.yaml index ab8cbf2434..8565683700 100644 --- a/tests/components/voice_assistant/common-idf.yaml +++ b/tests/components/voice_assistant/common-idf.yaml @@ -68,3 +68,24 @@ voice_assistant: - logger.log: format: "Voice assistant error - code %s, message: %s" args: [code.c_str(), message.c_str()] + on_timer_started: + - logger.log: + format: "Timer started: %s" + args: [timer.id.c_str()] + on_timer_updated: + - logger.log: + format: "Timer updated: %s" + args: [timer.id.c_str()] + on_timer_cancelled: + - logger.log: + format: "Timer cancelled: %s" + args: [timer.id.c_str()] + on_timer_finished: + - logger.log: + format: "Timer finished: %s" + args: [timer.id.c_str()] + on_timer_tick: + - lambda: |- + for (auto &timer : timers) { + ESP_LOGD("timer", "Timer %s: %" PRIu32 "s left", timer.name.c_str(), timer.seconds_left); + } diff --git a/tests/components/voice_assistant/common.yaml b/tests/components/voice_assistant/common.yaml index f248154b7e..d09de74396 100644 --- a/tests/components/voice_assistant/common.yaml +++ b/tests/components/voice_assistant/common.yaml @@ -58,3 +58,24 @@ voice_assistant: - logger.log: format: "Voice assistant error - code %s, message: %s" args: [code.c_str(), message.c_str()] + on_timer_started: + - logger.log: + format: "Timer started: %s" + args: [timer.id.c_str()] + on_timer_updated: + - logger.log: + format: "Timer updated: %s" + args: [timer.id.c_str()] + on_timer_cancelled: + - logger.log: + format: "Timer cancelled: %s" + args: [timer.id.c_str()] + on_timer_finished: + - logger.log: + format: "Timer finished: %s" + args: [timer.id.c_str()] + on_timer_tick: + - lambda: |- + for (auto &timer : timers) { + ESP_LOGD("timer", "Timer %s: %" PRIu32 "s left", timer.name.c_str(), timer.seconds_left); + }