From 0e0b67f1263fa51c1576513b56e816b4b02692ad Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 6 Oct 2025 17:04:47 +1300 Subject: [PATCH] Split response and error triggers Simplify variables in response lambdas to JsonObject Use `const char *` for message and parse to json right away --- esphome/components/api/__init__.py | 31 +++---- esphome/components/api/api.proto | 8 +- esphome/components/api/api_connection.cpp | 7 +- esphome/components/api/api_connection.h | 4 +- esphome/components/api/api_pb2.cpp | 17 +++- esphome/components/api/api_pb2.h | 11 ++- esphome/components/api/api_pb2_dump.cpp | 10 ++- esphome/components/api/api_pb2_service.cpp | 2 +- esphome/components/api/api_pb2_service.h | 2 +- esphome/components/api/api_server.cpp | 18 ++--- esphome/components/api/api_server.h | 8 +- .../components/api/homeassistant_service.h | 81 ++++++++++--------- esphome/core/defines.h | 2 + tests/components/api/common.yaml | 23 ++---- 14 files changed, 128 insertions(+), 96 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index a684439bf9..37dd9bf101 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -16,6 +16,7 @@ from esphome.const import ( CONF_KEY, CONF_ON_CLIENT_CONNECTED, CONF_ON_CLIENT_DISCONNECTED, + CONF_ON_ERROR, CONF_ON_RESPONSE, CONF_PASSWORD, CONF_PORT, @@ -304,14 +305,8 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( {cv.string: cv.returning_lambda} ), cv.Optional(CONF_RESPONSE_TEMPLATE): cv.templatable(cv.string), - cv.Optional(CONF_ON_RESPONSE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - HomeAssistantActionResponseTrigger - ), - }, - single=True, - ), + cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(single=True), + cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True), } ), cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), @@ -357,17 +352,23 @@ async def homeassistant_service_to_code( if on_response := config.get(CONF_ON_RESPONSE): cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES") - trigger = cg.new_Pvariable( - on_response[CONF_TRIGGER_ID], - template_arg, - var, - ) + cg.add(var.set_wants_response()) await automation.build_automation( - trigger, - [(cg.std_shared_ptr.template(ActionResponse), "response"), *args], + var.get_response_trigger(), + [(cg.JsonObject, "response"), *args], on_response, ) + if on_error := config.get(CONF_ON_ERROR): + cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES") + cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS") + cg.add(var.set_wants_response()) + await automation.build_automation( + var.get_error_trigger(), + [(cg.std_string, "error"), *args], + on_error, + ) + return var diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index b37344f566..6fbd26985d 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -780,8 +780,8 @@ message HomeassistantActionRequest { repeated HomeassistantServiceMap data_template = 3; repeated HomeassistantServiceMap variables = 4; bool is_event = 5; - uint32 call_id = 6; // Call ID for response tracking - string response_template = 7 [(no_zero_copy) = true]; // Optional Jinja template for response processing + uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"]; // Call ID for response tracking + string response_template = 7 [(no_zero_copy) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"]; // Optional Jinja template for response processing } // Message sent by Home Assistant to ESPHome with service call response data @@ -789,12 +789,12 @@ message HomeassistantActionResponse { option (id) = 130; option (source) = SOURCE_CLIENT; option (no_delay) = true; - option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES"; + option (ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"; uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest bool success = 2; // Whether the service call succeeded string error_message = 3; // Error message if success = false - string response_data = 4; // Service response data + bytes response_data = 4 [(pointer_to_buffer) = true]; // Service response data } // ==================== IMPORT HOME ASSISTANT STATES ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 9d76adf98e..c7edef220a 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -8,9 +8,9 @@ #endif #include #include -#include #include #include +#include #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/entity_base.h" @@ -1550,9 +1550,10 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) { } #endif -#ifdef USE_API_HOMEASSISTANT_SERVICES +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) { - this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message, msg.response_data); + this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message, msg.response_data, + msg.response_data_len); }; #endif #ifdef USE_API_NOISE diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 401fef28dc..b235fa98ac 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -137,8 +137,10 @@ class APIConnection final : public APIServerConnection { return; this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE); } +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override; -#endif +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES +#endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_BLUETOOTH_PROXY void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 210b6505ce..5a005a78de 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -884,8 +884,12 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_message(4, it, true); } buffer.encode_bool(5, this->is_event); +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES buffer.encode_uint32(6, this->call_id); +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES buffer.encode_string(7, this->response_template); +#endif } void HomeassistantActionRequest::calculate_size(ProtoSize &size) const { size.add_length(1, this->service_ref_.size()); @@ -893,9 +897,15 @@ void HomeassistantActionRequest::calculate_size(ProtoSize &size) const { size.add_repeated_message(1, this->data_template); size.add_repeated_message(1, this->variables); size.add_bool(1, this->is_event); +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES size.add_uint32(1, this->call_id); +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES size.add_length(1, this->response_template.size()); +#endif } +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: @@ -914,9 +924,12 @@ bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDe case 3: this->error_message = value.as_string(); break; - case 4: - this->response_data = value.as_string(); + case 4: { + // Use raw data directly to avoid allocation + this->response_data = value.data(); + this->response_data_len = value.size(); break; + } default: return false; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index aa5ef155ea..78f6a3cae5 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1114,8 +1114,12 @@ class HomeassistantActionRequest final : public ProtoMessage { std::vector data_template{}; std::vector variables{}; bool is_event{false}; +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES uint32_t call_id{0}; +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES std::string response_template{}; +#endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1124,17 +1128,20 @@ class HomeassistantActionRequest final : public ProtoMessage { protected: }; +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES class HomeassistantActionResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 130; - static constexpr uint8_t ESTIMATED_SIZE = 24; + static constexpr uint8_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "homeassistant_action_response"; } #endif uint32_t call_id{0}; bool success{false}; std::string error_message{}; - std::string response_data{}; + const uint8_t *response_data{nullptr}; + uint16_t response_data_len{0}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 9b655cc1a2..68965a92bc 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1122,15 +1122,23 @@ void HomeassistantActionRequest::dump_to(std::string &out) const { out.append("\n"); } dump_field(out, "is_event", this->is_event); +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES dump_field(out, "call_id", this->call_id); +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES dump_field(out, "response_template", this->response_template); +#endif } +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES void HomeassistantActionResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "HomeassistantActionResponse"); dump_field(out, "call_id", this->call_id); dump_field(out, "success", this->success); dump_field(out, "error_message", this->error_message); - dump_field(out, "response_data", this->response_data); + out.append(" response_data: "); + out.append(format_hex_pretty(this->response_data, this->response_data_len)); + out.append("\n"); } #endif #ifdef USE_API_HOMEASSISTANT_STATES diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 6f596a2edc..9d227af0a3 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -611,7 +611,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, break; } #endif -#ifdef USE_API_HOMEASSISTANT_SERVICES +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES case HomeassistantActionResponse::MESSAGE_TYPE: { HomeassistantActionResponse msg; msg.decode(msg_data, msg_size); diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index f3f39d48ec..549b00ee6a 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -66,7 +66,7 @@ class APIServerConnectionBase : public ProtoService { virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){}; #endif -#ifdef USE_API_HOMEASSISTANT_SERVICES +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){}; #endif #ifdef USE_API_HOMEASSISTANT_STATES diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index d21658c940..08f0d7cf72 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -403,27 +403,23 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call client->send_homeassistant_action(call); } } - +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback callback) { this->action_response_callbacks_[call_id] = std::move(callback); } void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message, - const std::string &response_data) { + const uint8_t *response_data, size_t response_data_len) { auto it = this->action_response_callbacks_.find(call_id); if (it != this->action_response_callbacks_.end()) { - // Create the response object - auto response = std::make_shared(success, error_message); - response->set_data(response_data); - - // Call the callback - it->second(response); - - // Remove the callback as it's one-time use + auto callback = std::move(it->second); this->action_response_callbacks_.erase(it); + auto response = std::make_shared(success, error_message, response_data, response_data_len); + callback(response); } } -#endif +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES +#endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_STATES void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index b346d83ac8..2c99f17060 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -112,12 +112,14 @@ class APIServer : public Component, public Controller { #ifdef USE_API_HOMEASSISTANT_SERVICES void send_homeassistant_action(const HomeassistantActionRequest &call); +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES // Action response handling using ActionResponseCallback = std::function)>; void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback); void handle_action_response(uint32_t call_id, bool success, const std::string &error_message, - const std::string &response_data); -#endif + const uint8_t *response_data, size_t response_data_len); +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES +#endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_SERVICES void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } #endif @@ -193,7 +195,7 @@ class APIServer : public Component, public Controller { #ifdef USE_API_SERVICES std::vector user_services_; #endif -#ifdef USE_API_HOMEASSISTANT_SERVICES +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES std::map action_response_callbacks_; #endif diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 1a1f9c4810..beb91acef6 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -47,42 +47,33 @@ template class TemplatableKeyValuePair { TemplatableStringValue value; }; +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES // Represents the response data from a Home Assistant action class ActionResponse { public: - ActionResponse(bool success, std::string error_message = "") - : success_(success), error_message_(std::move(error_message)) {} + ActionResponse(bool success, std::string error_message = "", const uint8_t *data = nullptr, size_t data_len = 0) + : success_(success), error_message_(std::move(error_message)) { + if (data == nullptr || data_len == 0) + return; + this->json_document_ = json::parse_json(data, data_len); + this->json_ = this->json_document_.as(); + } bool is_success() const { return this->success_; } const std::string &get_error_message() const { return this->error_message_; } - const std::string &get_data() const { return this->data_; } // Get data as parsed JSON object - // Returns unbound JsonObject if data is empty or invalid JSON - JsonObject get_json() { - if (this->data_.empty()) - return JsonObject(); // Return unbound JsonObject if no data - - if (!this->parsed_json_) { - this->json_document_ = json::parse_json(this->data_); - this->json_ = this->json_document_.as(); - this->parsed_json_ = true; - } - return this->json_; - } - - void set_data(const std::string &data) { this->data_ = data; } + JsonObject get_json() { return this->json_; } protected: bool success_; std::string error_message_; - std::string data_; JsonDocument json_document_; JsonObject json_; - bool parsed_json_{false}; }; // Callback type for action responses template using ActionResponseCallback = std::function, Ts...)>; +#endif template class HomeAssistantServiceCallAction : public Action { public: @@ -101,15 +92,19 @@ template class HomeAssistantServiceCallAction : public Actionvariables_.emplace_back(std::move(key), value); } +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES template void set_response_template(T response_template) { this->response_template_ = response_template; this->has_response_template_ = true; } - void set_response_callback(ActionResponseCallback callback) { - this->wants_response_ = true; - this->response_callback_ = callback; - } + void set_wants_response() { this->wants_response_ = true; } + + Trigger *get_response_trigger() const { return this->response_trigger_; } +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS + Trigger *get_error_trigger() const { return this->error_trigger_; } +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES void play(Ts... x) override { HomeassistantActionRequest resp; @@ -135,6 +130,7 @@ template class HomeAssistantServiceCallAction : public Actionwants_response_) { // Generate a unique call ID for this service call static uint32_t call_id_counter = 1; @@ -147,11 +143,25 @@ template class HomeAssistantServiceCallAction : public Actionparent_->register_action_response_callback(call_id, [this, captured_args]( - std::shared_ptr response) { - std::apply([this, &response](auto &&...args) { this->response_callback_(response, args...); }, captured_args); - }); + this->parent_->register_action_response_callback( + call_id, [this, captured_args](std::shared_ptr response) { + std::apply( + [this, &response](auto &&...args) { + if (response->is_success()) { + if (this->response_trigger_ != nullptr) { + this->response_trigger_->trigger(response->get_json(), args...); + } + } +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS + else if (this->error_trigger_ != nullptr) { + this->error_trigger_->trigger(response->get_error_message(), args...); + } +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS + }, + captured_args); + }); } +#endif this->parent_->send_homeassistant_action(resp); } @@ -163,21 +173,18 @@ template class HomeAssistantServiceCallAction : public Action> data_; std::vector> data_template_; std::vector> variables_; +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES TemplatableStringValue response_template_{""}; - ActionResponseCallback response_callback_; + Trigger *response_trigger_ = new Trigger(); +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS + Trigger *error_trigger_ = new Trigger(); +#endif bool wants_response_{false}; bool has_response_template_{false}; -}; - -template -class HomeAssistantActionResponseTrigger : public Trigger, Ts...> { - public: - HomeAssistantActionResponseTrigger(HomeAssistantServiceCallAction *action) { - action->set_response_callback( - [this](std::shared_ptr response, Ts... x) { this->trigger(response, x...); }); - } +#endif }; } // namespace esphome::api + #endif #endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 7fc42ea334..fb3a559b64 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -110,6 +110,8 @@ #define USE_API #define USE_API_CLIENT_CONNECTED_TRIGGER #define USE_API_CLIENT_DISCONNECTED_TRIGGER +#define USE_API_HOMEASSISTANT_ACTION_RESPONSES +#define USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS #define USE_API_HOMEASSISTANT_SERVICES #define USE_API_HOMEASSISTANT_STATES #define USE_API_NOISE diff --git a/tests/components/api/common.yaml b/tests/components/api/common.yaml index 061282d184..4927e0b2d6 100644 --- a/tests/components/api/common.yaml +++ b/tests/components/api/common.yaml @@ -17,14 +17,12 @@ esphome: type: hourly on_response: - lambda: |- - if (response->is_success()) { - JsonObject json = response->get_json(); - JsonObject next_hour = json["response"]["weather.forecast_home"]["forecast"][0]; - float next_temperature = next_hour["temperature"].as(); - ESP_LOGD("main", "Next hour temperature: %f", next_temperature); - } else { - ESP_LOGE("main", "Action failed: %s", response->get_error_message().c_str()); - } + JsonObject next_hour = response["response"]["weather.forecast_home"]["forecast"][0]; + float next_temperature = next_hour["temperature"].as(); + ESP_LOGD("main", "Next hour temperature: %f", next_temperature); + on_error: + - lambda: |- + ESP_LOGE("main", "Action failed with error: %s", error.c_str()); - homeassistant.action: action: weather.get_forecasts data: @@ -33,13 +31,8 @@ esphome: response_template: "{{ response['weather.forecast_home']['forecast'][0]['temperature'] }}" on_response: - lambda: |- - if (response->is_success()) { - JsonObject json = response->get_json(); - float temperature = json["response"].as(); - ESP_LOGD("main", "Next hour temperature: %f", temperature); - } else { - ESP_LOGE("main", "Action failed: %s", response->get_error_message().c_str()); - } + float temperature = response["response"].as(); + ESP_LOGD("main", "Next hour temperature: %f", temperature); api: port: 8000