From f22143f0908b128a8e2b57dc3b466962c5534864 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 24 Sep 2025 19:08:29 -0500 Subject: [PATCH] Add external wake word message (#10850) --- esphome/components/api/api.proto | 12 ++++++ esphome/components/api/api_connection.cpp | 17 ++++++++ esphome/components/api/api_pb2.cpp | 46 ++++++++++++++++++++++ esphome/components/api/api_pb2.h | 31 ++++++++++++--- esphome/components/api/api_pb2_dump.cpp | 19 ++++++++- esphome/components/api/api_pb2_service.cpp | 2 +- 6 files changed, 119 insertions(+), 8 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 21727df307..9273cca2d3 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1865,10 +1865,22 @@ message VoiceAssistantWakeWord { repeated string trained_languages = 3; } +message VoiceAssistantExternalWakeWord { + string id = 1; + string wake_word = 2; + repeated string trained_languages = 3; + string model_type = 4; + uint32 model_size = 5; + string model_hash = 6; + string url = 7; +} + message VoiceAssistantConfigurationRequest { option (id) = 121; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_VOICE_ASSISTANT"; + + repeated VoiceAssistantExternalWakeWord external_wake_words = 1; } message VoiceAssistantConfigurationResponse { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 357b6e1a32..30b98803d1 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1202,6 +1202,23 @@ bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceA resp_wake_word.trained_languages.push_back(lang); } } + + // Filter external wake words + for (auto &wake_word : msg.external_wake_words) { + if (wake_word.model_type != "micro") { + // microWakeWord only + continue; + } + + resp.available_wake_words.emplace_back(); + auto &resp_wake_word = resp.available_wake_words.back(); + resp_wake_word.set_id(StringRef(wake_word.id)); + resp_wake_word.set_wake_word(StringRef(wake_word.wake_word)); + for (const auto &lang : wake_word.trained_languages) { + resp_wake_word.trained_languages.push_back(lang); + } + } + resp.active_wake_words = &config.active_wake_words; resp.max_active_wake_words = config.max_active_wake_words; return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 21641e7963..410ba2334e 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2397,6 +2397,52 @@ void VoiceAssistantWakeWord::calculate_size(ProtoSize &size) const { } } } +bool VoiceAssistantExternalWakeWord::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 5: + this->model_size = value.as_uint32(); + break; + default: + return false; + } + return true; +} +bool VoiceAssistantExternalWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: + this->id = value.as_string(); + break; + case 2: + this->wake_word = value.as_string(); + break; + case 3: + this->trained_languages.push_back(value.as_string()); + break; + case 4: + this->model_type = value.as_string(); + break; + case 6: + this->model_hash = value.as_string(); + break; + case 7: + this->url = value.as_string(); + break; + default: + return false; + } + return true; +} +bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: + this->external_wake_words.emplace_back(); + value.decode_to_message(this->external_wake_words.back()); + break; + default: + return false; + } + return true; +} void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->available_wake_words) { buffer.encode_message(1, it, true); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 2d06ddbb08..ee8472b21c 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -2456,18 +2456,37 @@ class VoiceAssistantWakeWord final : public ProtoMessage { protected: }; -class VoiceAssistantConfigurationRequest final : public ProtoMessage { +class VoiceAssistantExternalWakeWord final : public ProtoDecodableMessage { public: - static constexpr uint8_t MESSAGE_TYPE = 121; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_configuration_request"; } -#endif + std::string id{}; + std::string wake_word{}; + std::vector trained_languages{}; + std::string model_type{}; + uint32_t model_size{0}; + std::string model_hash{}; + std::string url{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class VoiceAssistantConfigurationRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 121; + static constexpr uint8_t ESTIMATED_SIZE = 34; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "voice_assistant_configuration_request"; } +#endif + std::vector external_wake_words{}; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; class VoiceAssistantConfigurationResponse final : public ProtoMessage { public: diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index ca3b041456..a5494168f9 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1824,8 +1824,25 @@ void VoiceAssistantWakeWord::dump_to(std::string &out) const { dump_field(out, "trained_languages", it, 4); } } +void VoiceAssistantExternalWakeWord::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "VoiceAssistantExternalWakeWord"); + dump_field(out, "id", this->id); + dump_field(out, "wake_word", this->wake_word); + for (const auto &it : this->trained_languages) { + dump_field(out, "trained_languages", it, 4); + } + dump_field(out, "model_type", this->model_type); + dump_field(out, "model_size", this->model_size); + dump_field(out, "model_hash", this->model_hash); + dump_field(out, "url", this->url); +} void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const { - out.append("VoiceAssistantConfigurationRequest {}"); + MessageDumpHelper helper(out, "VoiceAssistantConfigurationRequest"); + for (const auto &it : this->external_wake_words) { + out.append(" external_wake_words: "); + it.dump_to(out); + out.append("\n"); + } } void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "VoiceAssistantConfigurationResponse"); diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 24a7740ec0..ccbd781431 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -548,7 +548,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, #ifdef USE_VOICE_ASSISTANT case VoiceAssistantConfigurationRequest::MESSAGE_TYPE: { VoiceAssistantConfigurationRequest msg; - // Empty message: no decode needed + msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str()); #endif