From 6c362d42c3a72ddc7309ed38636687d6c511ce66 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:28:41 +1300 Subject: [PATCH 01/21] [api] Add support for getting action responses from home-assistant --- esphome/components/api/__init__.py | 53 +++++++++++- esphome/components/api/api.proto | 15 ++++ esphome/components/api/api_connection.cpp | 6 ++ esphome/components/api/api_connection.h | 1 + esphome/components/api/api_pb2.cpp | 30 +++++++ esphome/components/api/api_pb2.h | 23 +++++- esphome/components/api/api_pb2_dump.cpp | 9 +++ esphome/components/api/api_pb2_service.cpp | 11 +++ esphome/components/api/api_pb2_service.h | 3 + esphome/components/api/api_server.cpp | 23 ++++++ esphome/components/api/api_server.h | 9 +++ .../components/api/homeassistant_service.h | 81 +++++++++++++++++++ esphome/const.py | 2 + 13 files changed, 262 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 6a0e092008..0a7a0d347b 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -16,23 +16,26 @@ from esphome.const import ( CONF_KEY, CONF_ON_CLIENT_CONNECTED, CONF_ON_CLIENT_DISCONNECTED, + CONF_ON_RESPONSE, CONF_PASSWORD, CONF_PORT, CONF_REBOOT_TIMEOUT, + CONF_RESPONSE_TEMPLATE, CONF_SERVICE, CONF_SERVICES, CONF_TAG, CONF_TRIGGER_ID, CONF_VARIABLES, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import TemplateArgsType from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) DOMAIN = "api" DEPENDENCIES = ["network"] -AUTO_LOAD = ["socket"] +AUTO_LOAD = ["socket", "json"] CODEOWNERS = ["@esphome/core"] api_ns = cg.esphome_ns.namespace("api") @@ -40,6 +43,10 @@ APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller) HomeAssistantServiceCallAction = api_ns.class_( "HomeAssistantServiceCallAction", automation.Action ) +ActionResponse = api_ns.class_("ActionResponse") +HomeAssistantActionResponseTrigger = api_ns.class_( + "HomeAssistantActionResponseTrigger", automation.Trigger +) APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition) UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger) @@ -244,6 +251,14 @@ async def to_code(config): KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)}) +def _validate_response_config(config): + if CONF_RESPONSE_TEMPLATE in config and not config.get(CONF_ON_RESPONSE): + raise cv.Invalid( + "`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_ON_RESPONSE}` to be set." + ) + return config + + HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( cv.Schema( { @@ -259,10 +274,20 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( cv.Optional(CONF_VARIABLES, default={}): cv.Schema( {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.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), cv.rename_key(CONF_SERVICE, CONF_ACTION), + _validate_response_config, ) @@ -276,7 +301,12 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( HomeAssistantServiceCallAction, HOMEASSISTANT_ACTION_ACTION_SCHEMA, ) -async def homeassistant_service_to_code(config, action_id, template_arg, args): +async def homeassistant_service_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +): cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, False) @@ -291,6 +321,23 @@ async def homeassistant_service_to_code(config, action_id, template_arg, args): for key, value in config[CONF_VARIABLES].items(): templ = await cg.templatable(value, args, None) cg.add(var.add_variable(key, templ)) + + if response_template := config.get(CONF_RESPONSE_TEMPLATE): + templ = await cg.templatable(response_template, args, cg.std_string) + cg.add(var.set_response_template(templ)) + + if on_response := config.get(CONF_ON_RESPONSE): + trigger = cg.new_Pvariable( + on_response[CONF_TRIGGER_ID], + template_arg, + var, + ) + await automation.build_automation( + trigger, + [(cg.std_shared_ptr.template(ActionResponse), "response"), *args], + on_response, + ) + return var diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 0e385c4a17..b37344f566 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -780,6 +780,21 @@ 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 +} + +// Message sent by Home Assistant to ESPHome with service call response data +message HomeassistantActionResponse { + option (id) = 130; + option (source) = SOURCE_CLIENT; + option (no_delay) = true; + option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES"; + + 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 } // ==================== IMPORT HOME ASSISTANT STATES ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 30b98803d1..9d76adf98e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1549,6 +1549,12 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) { } } #endif + +#ifdef USE_API_HOMEASSISTANT_SERVICES +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); +}; +#endif #ifdef USE_API_NOISE bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) { NoiseEncryptionSetKeyResponse resp; diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index cc7e4d6895..401fef28dc 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -137,6 +137,7 @@ class APIConnection final : public APIServerConnection { return; this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE); } + void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override; #endif #ifdef USE_BLUETOOTH_PROXY void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 0140c60e5b..210b6505ce 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -884,6 +884,8 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_message(4, it, true); } buffer.encode_bool(5, this->is_event); + buffer.encode_uint32(6, this->call_id); + buffer.encode_string(7, this->response_template); } void HomeassistantActionRequest::calculate_size(ProtoSize &size) const { size.add_length(1, this->service_ref_.size()); @@ -891,6 +893,34 @@ 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); + size.add_uint32(1, this->call_id); + size.add_length(1, this->response_template.size()); +} +bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->call_id = value.as_uint32(); + break; + case 2: + this->success = value.as_bool(); + break; + default: + return false; + } + return true; +} +bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 3: + this->error_message = value.as_string(); + break; + case 4: + this->response_data = value.as_string(); + break; + default: + return false; + } + return true; } #endif #ifdef USE_API_HOMEASSISTANT_STATES diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index d71ee9777d..aa5ef155ea 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1104,7 +1104,7 @@ class HomeassistantServiceMap final : public ProtoMessage { class HomeassistantActionRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 35; - static constexpr uint8_t ESTIMATED_SIZE = 113; + static constexpr uint8_t ESTIMATED_SIZE = 126; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "homeassistant_action_request"; } #endif @@ -1114,6 +1114,8 @@ class HomeassistantActionRequest final : public ProtoMessage { std::vector data_template{}; std::vector variables{}; bool is_event{false}; + uint32_t call_id{0}; + std::string response_template{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1122,6 +1124,25 @@ class HomeassistantActionRequest final : public ProtoMessage { protected: }; +class HomeassistantActionResponse final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 130; + static constexpr uint8_t ESTIMATED_SIZE = 24; +#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{}; +#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; +}; #endif #ifdef USE_API_HOMEASSISTANT_STATES class SubscribeHomeAssistantStatesRequest final : public ProtoMessage { diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index c5f1d99dd4..9b655cc1a2 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1122,6 +1122,15 @@ void HomeassistantActionRequest::dump_to(std::string &out) const { out.append("\n"); } dump_field(out, "is_event", this->is_event); + dump_field(out, "call_id", this->call_id); + dump_field(out, "response_template", this->response_template); +} +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); } #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 ccbd781431..6f596a2edc 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -610,6 +610,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_z_wave_proxy_request(msg); break; } +#endif +#ifdef USE_API_HOMEASSISTANT_SERVICES + case HomeassistantActionResponse::MESSAGE_TYPE: { + HomeassistantActionResponse msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_homeassistant_action_response: %s", msg.dump().c_str()); +#endif + this->on_homeassistant_action_response(msg); + break; + } #endif default: break; diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 1afcba6664..f3f39d48ec 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -66,6 +66,9 @@ class APIServerConnectionBase : public ProtoService { virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){}; #endif +#ifdef USE_API_HOMEASSISTANT_SERVICES + virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){}; +#endif #ifdef USE_API_HOMEASSISTANT_STATES virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){}; #endif diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index dd6eb950a6..254bdcd509 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -9,6 +9,9 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" #include "esphome/core/version.h" +#ifdef USE_API_HOMEASSISTANT_SERVICES +#include "homeassistant_service.h" +#endif #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -387,6 +390,26 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call client->send_homeassistant_action(call); } } + +void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback callback) { + this->action_response_callbacks_[call_id] = callback; +} + +void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message, + const std::string &response_data) { + 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 + this->action_response_callbacks_.erase(it); + } +} #endif #ifdef USE_API_HOMEASSISTANT_STATES diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 627870af1d..d6fca08cff 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -16,6 +16,7 @@ #include "user_services.h" #endif +#include #include namespace esphome::api { @@ -109,6 +110,11 @@ class APIServer : public Component, public Controller { #ifdef USE_API_HOMEASSISTANT_SERVICES void send_homeassistant_action(const HomeassistantActionRequest &call); + // 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 #ifdef USE_API_SERVICES void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } @@ -185,6 +191,9 @@ class APIServer : public Component, public Controller { #ifdef USE_API_SERVICES std::vector user_services_; #endif +#ifdef USE_API_HOMEASSISTANT_SERVICES + std::map action_response_callbacks_; +#endif // Group smaller types together uint16_t port_{6053}; diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 4026741ee4..ac568e6518 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -3,8 +3,10 @@ #include "api_server.h" #ifdef USE_API #ifdef USE_API_HOMEASSISTANT_SERVICES +#include #include #include "api_pb2.h" +#include "esphome/components/json/json_util.h" #include "esphome/core/automation.h" #include "esphome/core/helpers.h" @@ -44,6 +46,43 @@ template class TemplatableKeyValuePair { TemplatableStringValue value; }; +// Represents the response data from a Home Assistant action +class ActionResponse { + public: + ActionResponse(bool success, const std::string &error_message = "") + : success_(success), error_message_(error_message) {} + + 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; } + + 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...)>; + template class HomeAssistantServiceCallAction : public Action { public: explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {} @@ -61,6 +100,16 @@ template class HomeAssistantServiceCallAction : public Actionvariables_.emplace_back(std::move(key), value); } + 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 play(Ts... x) override { HomeassistantActionRequest resp; std::string service_value = this->service_.value(x...); @@ -84,6 +133,25 @@ template class HomeAssistantServiceCallAction : public Actionwants_response_) { + // Generate a unique call ID for this service call + static uint32_t call_id_counter = 1; + uint32_t call_id = call_id_counter++; + resp.call_id = call_id; + // Set response template if provided + if (this->has_response_template_) { + std::string response_template_value = this->response_template_.value(x...); + resp.response_template = response_template_value; + } + + auto captured_args = std::make_tuple(x...); + this->parent_->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_->send_homeassistant_action(resp); } @@ -94,6 +162,19 @@ template class HomeAssistantServiceCallAction : public Action> data_; std::vector> data_template_; std::vector> variables_; + TemplatableStringValue response_template_{""}; + ActionResponseCallback response_callback_; + 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...); }); + } }; } // namespace esphome::api diff --git a/esphome/const.py b/esphome/const.py index 3e93200f14..ee424d095e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -671,6 +671,7 @@ CONF_ON_PRESET_SET = "on_preset_set" CONF_ON_PRESS = "on_press" CONF_ON_RAW_VALUE = "on_raw_value" CONF_ON_RELEASE = "on_release" +CONF_ON_RESPONSE = "on_response" CONF_ON_SHUTDOWN = "on_shutdown" CONF_ON_SPEED_SET = "on_speed_set" CONF_ON_STATE = "on_state" @@ -816,6 +817,7 @@ CONF_RESET_DURATION = "reset_duration" CONF_RESET_PIN = "reset_pin" CONF_RESIZE = "resize" CONF_RESOLUTION = "resolution" +CONF_RESPONSE_TEMPLATE = "response_template" CONF_RESTART = "restart" CONF_RESTORE = "restore" CONF_RESTORE_MODE = "restore_mode" From 2ef4f3c65f95983aa9f86b63d2b1069b9a3201a5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 1 Oct 2025 08:45:58 +1300 Subject: [PATCH 02/21] Update esphome/components/api/__init__.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/api/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 0a7a0d347b..4649b596f5 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -254,7 +254,7 @@ KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)}) def _validate_response_config(config): if CONF_RESPONSE_TEMPLATE in config and not config.get(CONF_ON_RESPONSE): raise cv.Invalid( - "`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_ON_RESPONSE}` to be set." + f"`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_ON_RESPONSE}` to be set." ) return config From 226399222d617e000e3d3e20f1cd4f6eb32d514a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:16:07 +1300 Subject: [PATCH 03/21] move error message --- esphome/components/api/homeassistant_service.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index ac568e6518..1a1f9c4810 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -4,6 +4,7 @@ #ifdef USE_API #ifdef USE_API_HOMEASSISTANT_SERVICES #include +#include #include #include "api_pb2.h" #include "esphome/components/json/json_util.h" @@ -49,8 +50,8 @@ template class TemplatableKeyValuePair { // Represents the response data from a Home Assistant action class ActionResponse { public: - ActionResponse(bool success, const std::string &error_message = "") - : success_(success), error_message_(error_message) {} + ActionResponse(bool success, std::string error_message = "") + : success_(success), error_message_(std::move(error_message)) {} bool is_success() const { return this->success_; } const std::string &get_error_message() const { return this->error_message_; } From f4b7009c969f51e3ffb5fce5aa20b962ff1ee23a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 1 Oct 2025 13:50:07 +1300 Subject: [PATCH 04/21] move callback --- esphome/components/api/api_server.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index cec225f385..d21658c940 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -18,6 +18,7 @@ #endif #include +#include namespace esphome::api { @@ -404,7 +405,7 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call } void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback callback) { - this->action_response_callbacks_[call_id] = 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, From 211a8c872b4073261ffeac60f995386dfe520f10 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 1 Oct 2025 13:58:19 +1300 Subject: [PATCH 05/21] Add action response to tests --- tests/components/api/common.yaml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/components/api/common.yaml b/tests/components/api/common.yaml index 4f1693dac8..061282d184 100644 --- a/tests/components/api/common.yaml +++ b/tests/components/api/common.yaml @@ -10,6 +10,36 @@ esphome: data: message: Button was pressed - homeassistant.tag_scanned: pulse + - homeassistant.action: + action: weather.get_forecasts + data: + entity_id: weather.forecast_home + 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()); + } + - homeassistant.action: + action: weather.get_forecasts + data: + entity_id: weather.forecast_home + type: hourly + 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()); + } api: port: 8000 From a405592385110a5c08f2e7f97dbfc59777201e7a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:32:09 +1300 Subject: [PATCH 06/21] Update esphome/components/api/__init__.py Co-authored-by: J. Nick Koston --- esphome/components/api/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 47ed036658..a684439bf9 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -356,6 +356,7 @@ async def homeassistant_service_to_code( cg.add(var.set_response_template(templ)) 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, 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 07/21] 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 From c4f0f146967fad674e9d02a1991a9cce5c9ab58b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 14:33:10 -0500 Subject: [PATCH 08/21] [esp32] Fix clang-tidy error for Arduino watchdog function declarations --- esphome/components/esp32/core.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index f3bdfea2a0..e54c07e304 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -14,6 +14,7 @@ #ifdef USE_ARDUINO #include +#include #else #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) #include From 3dbdcab7e5dc6b40550e510512a774e3dd20ac67 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 15:06:26 -0500 Subject: [PATCH 09/21] try a forward dec --- esphome/components/esp32/core.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index e54c07e304..f67706ca5d 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -15,6 +15,10 @@ #ifdef USE_ARDUINO #include #include +// Forward declarations for Arduino watchdog functions (implemented in esp32-hal-misc.c) +extern "C" void enableLoopWDT(); +extern "C" void disableCore0WDT(); +extern "C" void disableCore1WDT(); #else #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) #include From b0e15cdabd0b97d064bc8c58b368dc84d4137715 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 15:24:57 -0500 Subject: [PATCH 10/21] oops they are bool --- esphome/components/esp32/core.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index f67706ca5d..77beda0e82 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -16,9 +16,10 @@ #include #include // Forward declarations for Arduino watchdog functions (implemented in esp32-hal-misc.c) -extern "C" void enableLoopWDT(); -extern "C" void disableCore0WDT(); -extern "C" void disableCore1WDT(); +// These are behind preprocessor guards in esp32-hal.h that static analysis tools may not see +void enableLoopWDT(); +bool disableCore0WDT(); +bool disableCore1WDT(); #else #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) #include From 514830b372bf6334fc0957780dbeb137f23c45d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Oct 2025 15:41:48 -0500 Subject: [PATCH 11/21] sdkconfig instead --- esphome/components/esp32/core.cpp | 6 ------ sdkconfig.defaults | 1 + 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 77beda0e82..f3bdfea2a0 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -14,12 +14,6 @@ #ifdef USE_ARDUINO #include -#include -// Forward declarations for Arduino watchdog functions (implemented in esp32-hal-misc.c) -// These are behind preprocessor guards in esp32-hal.h that static analysis tools may not see -void enableLoopWDT(); -bool disableCore0WDT(); -bool disableCore1WDT(); #else #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) #include diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 72ca3f6e9c..322efb701a 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -13,6 +13,7 @@ CONFIG_ESP_TASK_WDT=y CONFIG_ESP_TASK_WDT_PANIC=y CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n +CONFIG_AUTOSTART_ARDUINO=y # esp32_ble CONFIG_BT_ENABLED=y From a11bef05588cab8b7423c48ed840d5ddb6f3cf05 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:25:35 +1300 Subject: [PATCH 12/21] Handle action status response without json --- esphome/components/api/__init__.py | 79 ++++++++++++++----- esphome/components/api/api.proto | 7 +- esphome/components/api/api_connection.cpp | 11 ++- esphome/components/api/api_pb2.cpp | 14 +++- esphome/components/api/api_pb2.h | 9 ++- esphome/components/api/api_pb2_dump.cpp | 7 +- esphome/components/api/api_server.cpp | 11 +++ esphome/components/api/api_server.h | 3 + .../components/api/homeassistant_service.h | 77 ++++++++++++------ esphome/const.py | 2 + esphome/core/defines.h | 2 +- 11 files changed, 164 insertions(+), 58 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 77c97f948e..1c74b2d355 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -9,6 +9,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ACTION, CONF_ACTIONS, + CONF_CAPTURE_RESPONSE, CONF_DATA, CONF_DATA_TEMPLATE, CONF_EVENT, @@ -18,7 +19,7 @@ from esphome.const import ( CONF_ON_CLIENT_CONNECTED, CONF_ON_CLIENT_DISCONNECTED, CONF_ON_ERROR, - CONF_ON_RESPONSE, + CONF_ON_SUCCESS, CONF_PASSWORD, CONF_PORT, CONF_REBOOT_TIMEOUT, @@ -37,9 +38,21 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "api" DEPENDENCIES = ["network"] -AUTO_LOAD = ["socket", "json"] CODEOWNERS = ["@esphome/core"] + +def AUTO_LOAD(config: ConfigType) -> list[str]: + """Conditionally auto-load json only when capture_response is used.""" + base = ["socket"] + + # Check if any homeassistant.action/homeassistant.service has capture_response: true + # This flag is set during config validation in _validate_response_config + if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False): + return base + ["json"] + + return base + + api_ns = cg.esphome_ns.namespace("api") APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller) HomeAssistantServiceCallAction = api_ns.class_( @@ -296,11 +309,26 @@ async def to_code(config): KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)}) -def _validate_response_config(config): - if CONF_RESPONSE_TEMPLATE in config and not config.get(CONF_ON_RESPONSE): +def _validate_response_config(config: ConfigType) -> ConfigType: + # Validate dependencies: + # - response_template requires capture_response: true + # - capture_response: true requires on_success + if CONF_RESPONSE_TEMPLATE in config and not config[CONF_CAPTURE_RESPONSE]: raise cv.Invalid( - f"`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_ON_RESPONSE}` to be set." + f"`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_CAPTURE_RESPONSE}: true` to be set.", + path=[CONF_RESPONSE_TEMPLATE], ) + + if config[CONF_CAPTURE_RESPONSE] and CONF_ON_SUCCESS not in config: + raise cv.Invalid( + f"`{CONF_CAPTURE_RESPONSE}: true` requires `{CONF_ON_SUCCESS}` to be set.", + path=[CONF_CAPTURE_RESPONSE], + ) + + # Track if any action uses capture_response for AUTO_LOAD + if config[CONF_CAPTURE_RESPONSE]: + CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True + return config @@ -320,7 +348,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(single=True), + cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean, + cv.Optional(CONF_ON_SUCCESS): automation.validate_automation(single=True), cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True), } ), @@ -361,29 +390,39 @@ async def homeassistant_service_to_code( templ = await cg.templatable(value, args, None) cg.add(var.add_variable(key, templ)) - if response_template := config.get(CONF_RESPONSE_TEMPLATE): - templ = await cg.templatable(response_template, args, cg.std_string) - cg.add(var.set_response_template(templ)) - - if on_response := config.get(CONF_ON_RESPONSE): - cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES") - cg.add(var.set_wants_response()) - await automation.build_automation( - 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()) + cg.add(var.set_wants_status()) await automation.build_automation( var.get_error_trigger(), [(cg.std_string, "error"), *args], on_error, ) + if on_success := config.get(CONF_ON_SUCCESS): + cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES") + cg.add(var.set_wants_status()) + if config[CONF_CAPTURE_RESPONSE]: + cg.add(var.set_wants_response()) + cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON") + await automation.build_automation( + var.get_success_trigger_with_response(), + [(cg.JsonObject, "response"), *args], + on_success, + ) + + if response_template := config.get(CONF_RESPONSE_TEMPLATE): + templ = await cg.templatable(response_template, args, cg.std_string) + cg.add(var.set_response_template(templ)) + + else: + await automation.build_automation( + var.get_success_trigger(), + args, + on_success, + ) + return var diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 6fbd26985d..87f477799d 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -780,8 +780,9 @@ message HomeassistantActionRequest { repeated HomeassistantServiceMap data_template = 3; repeated HomeassistantServiceMap variables = 4; bool is_event = 5; - 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 + uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"]; + bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; + string response_template = 8 [(no_zero_copy) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; } // Message sent by Home Assistant to ESPHome with service call response data @@ -794,7 +795,7 @@ message HomeassistantActionResponse { 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 - bytes response_data = 4 [(pointer_to_buffer) = true]; // Service response data + bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; } // ==================== IMPORT HOME ASSISTANT STATES ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index a06616af9c..ae03dfbb33 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1552,8 +1552,15 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) { #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, - msg.response_data_len); +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + if (msg.response_data_len > 0) { + this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message, msg.response_data, + msg.response_data_len); + } else +#endif + { + this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message); + } }; #endif #ifdef USE_API_NOISE diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 5a005a78de..70bcf082a6 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -887,8 +887,11 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const { #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); +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + buffer.encode_bool(7, this->wants_response); +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + buffer.encode_string(8, this->response_template); #endif } void HomeassistantActionRequest::calculate_size(ProtoSize &size) const { @@ -900,7 +903,10 @@ void HomeassistantActionRequest::calculate_size(ProtoSize &size) const { #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES size.add_uint32(1, this->call_id); #endif -#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + size.add_bool(1, this->wants_response); +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON size.add_length(1, this->response_template.size()); #endif } @@ -924,12 +930,14 @@ bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDe case 3: this->error_message = value.as_string(); break; +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON case 4: { // Use raw data directly to avoid allocation this->response_data = value.data(); this->response_data_len = value.size(); break; } +#endif default: return false; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 78f6a3cae5..d9e68ece9b 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1104,7 +1104,7 @@ class HomeassistantServiceMap final : public ProtoMessage { class HomeassistantActionRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 35; - static constexpr uint8_t ESTIMATED_SIZE = 126; + static constexpr uint8_t ESTIMATED_SIZE = 128; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "homeassistant_action_request"; } #endif @@ -1117,7 +1117,10 @@ class HomeassistantActionRequest final : public ProtoMessage { #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES uint32_t call_id{0}; #endif -#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + bool wants_response{false}; +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON std::string response_template{}; #endif void encode(ProtoWriteBuffer buffer) const override; @@ -1140,8 +1143,10 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage { uint32_t call_id{0}; bool success{false}; std::string error_message{}; +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON const uint8_t *response_data{nullptr}; uint16_t response_data_len{0}; +#endif #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 68965a92bc..cf732e451b 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1125,7 +1125,10 @@ void HomeassistantActionRequest::dump_to(std::string &out) const { #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES dump_field(out, "call_id", this->call_id); #endif -#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + dump_field(out, "wants_response", this->wants_response); +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON dump_field(out, "response_template", this->response_template); #endif } @@ -1136,9 +1139,11 @@ void HomeassistantActionResponse::dump_to(std::string &out) const { dump_field(out, "call_id", this->call_id); dump_field(out, "success", this->success); dump_field(out, "error_message", this->error_message); +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON out.append(" response_data: "); out.append(format_hex_pretty(this->response_data, this->response_data_len)); out.append("\n"); +#endif } #endif #ifdef USE_API_HOMEASSISTANT_STATES diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 170e1092b6..95617c75f1 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -409,6 +409,16 @@ void APIServer::register_action_response_callback(uint32_t call_id, ActionRespon 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) { + auto it = this->action_response_callbacks_.find(call_id); + if (it != this->action_response_callbacks_.end()) { + auto callback = std::move(it->second); + this->action_response_callbacks_.erase(it); + auto response = std::make_shared(success, error_message); + callback(response); + } +} +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message, const uint8_t *response_data, size_t response_data_len) { auto it = this->action_response_callbacks_.find(call_id); @@ -419,6 +429,7 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, const std callback(response); } } +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_SERVICES diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 2c99f17060..cd6c51cad2 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -116,8 +116,11 @@ class APIServer : public Component, public Controller { // 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); +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON void handle_action_response(uint32_t call_id, bool success, const std::string &error_message, const uint8_t *response_data, size_t response_data_len); +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_SERVICES diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index beb91acef6..bc7afadb49 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -51,24 +51,34 @@ template class TemplatableKeyValuePair { // Represents the response data from a Home Assistant action class ActionResponse { public: - ActionResponse(bool success, std::string error_message = "", const uint8_t *data = nullptr, size_t data_len = 0) + ActionResponse(bool success, std::string error_message = "") + : success_(success), error_message_(std::move(error_message)) {} + +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + ActionResponse(bool success, std::string error_message, const uint8_t *data, size_t data_len) : 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(); } +#endif bool is_success() const { return this->success_; } const std::string &get_error_message() const { return this->error_message_; } + +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON // Get data as parsed JSON object JsonObject get_json() { return this->json_; } +#endif protected: bool success_; std::string error_message_; +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON JsonDocument json_document_; JsonObject json_; +#endif }; // Callback type for action responses @@ -77,7 +87,9 @@ template using ActionResponseCallback = std::function class HomeAssistantServiceCallAction : public Action { public: - explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {} + explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent) { + this->flags_.is_event = is_event; + } template void set_service(T service) { this->service_ = service; } @@ -95,22 +107,24 @@ template class HomeAssistantServiceCallAction : public Action void set_response_template(T response_template) { this->response_template_ = response_template; - this->has_response_template_ = true; + this->flags_.has_response_template = true; } - void set_wants_response() { this->wants_response_ = true; } + void set_wants_status() { this->flags_.wants_status = true; } + void set_wants_response() { this->flags_.wants_response = true; } - Trigger *get_response_trigger() const { return this->response_trigger_; } -#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + Trigger *get_success_trigger_with_response() const { return this->success_trigger_with_response_; } +#endif + Trigger *get_success_trigger() const { return this->success_trigger_; } 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; std::string service_value = this->service_.value(x...); resp.set_service(StringRef(service_value)); - resp.is_event = this->is_event_; + resp.is_event = this->flags_.is_event; for (auto &it : this->data_) { resp.data.emplace_back(); auto &kv = resp.data.back(); @@ -131,15 +145,18 @@ template class HomeAssistantServiceCallAction : public Actionwants_response_) { + if (this->flags_.wants_status) { // Generate a unique call ID for this service call static uint32_t call_id_counter = 1; uint32_t call_id = call_id_counter++; resp.call_id = call_id; - // Set response template if provided - if (this->has_response_template_) { - std::string response_template_value = this->response_template_.value(x...); - resp.response_template = response_template_value; + if (this->flags_.wants_response) { + resp.wants_response = true; + // Set response template if provided + if (this->flags_.has_response_template) { + std::string response_template_value = this->response_template_.value(x...); + resp.response_template = response_template_value; + } } auto captured_args = std::make_tuple(x...); @@ -148,15 +165,17 @@ template class HomeAssistantServiceCallAction : public Actionis_success()) { - if (this->response_trigger_ != nullptr) { - this->response_trigger_->trigger(response->get_json(), args...); +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + if (this->flags_.wants_response) { + this->success_trigger_with_response_->trigger(response->get_json(), args...); + } else +#endif + { + this->success_trigger_->trigger(args...); } - } -#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS - else if (this->error_trigger_ != nullptr) { + } else { this->error_trigger_->trigger(response->get_error_message(), args...); } -#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS }, captured_args); }); @@ -168,20 +187,26 @@ template class HomeAssistantServiceCallAction : public Action service_{}; std::vector> data_; std::vector> data_template_; std::vector> variables_; #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON TemplatableStringValue response_template_{""}; - Trigger *response_trigger_ = new Trigger(); -#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS + Trigger *success_trigger_with_response_ = new Trigger(); +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + Trigger *success_trigger_ = new Trigger(); Trigger *error_trigger_ = new Trigger(); -#endif - bool wants_response_{false}; - bool has_response_template_{false}; -#endif +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES + + struct Flags { + uint8_t is_event : 1; + uint8_t wants_status : 1; + uint8_t wants_response : 1; + uint8_t has_response_template : 1; + uint8_t reserved : 5; + } flags_{0}; }; } // namespace esphome::api diff --git a/esphome/const.py b/esphome/const.py index 0fdf87c01e..d8240e4bdf 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -174,6 +174,7 @@ CONF_CALIBRATE_LINEAR = "calibrate_linear" CONF_CALIBRATION = "calibration" CONF_CAPACITANCE = "capacitance" CONF_CAPACITY = "capacity" +CONF_CAPTURE_RESPONSE = "capture_response" CONF_CARBON_MONOXIDE = "carbon_monoxide" CONF_CARRIER_DUTY_PERCENT = "carrier_duty_percent" CONF_CARRIER_FREQUENCY = "carrier_frequency" @@ -675,6 +676,7 @@ CONF_ON_RELEASE = "on_release" CONF_ON_RESPONSE = "on_response" CONF_ON_SHUTDOWN = "on_shutdown" CONF_ON_SPEED_SET = "on_speed_set" +CONF_ON_SUCCESS = "on_success" CONF_ON_STATE = "on_state" CONF_ON_TAG = "on_tag" CONF_ON_TAG_REMOVED = "on_tag_removed" diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 142b5ad284..2317c0ed32 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -113,7 +113,7 @@ #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_ACTION_RESPONSES_JSON #define USE_API_HOMEASSISTANT_SERVICES #define USE_API_HOMEASSISTANT_STATES #define USE_API_NOISE From f95b4bfce56bfeb0e7865e48c5dfe4b5c4917bd7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:26:44 +1300 Subject: [PATCH 13/21] Update test --- tests/components/api/common.yaml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/components/api/common.yaml b/tests/components/api/common.yaml index 4927e0b2d6..d3c549fff0 100644 --- a/tests/components/api/common.yaml +++ b/tests/components/api/common.yaml @@ -15,7 +15,8 @@ esphome: data: entity_id: weather.forecast_home type: hourly - on_response: + capture_response: true + on_success: - lambda: |- JsonObject next_hour = response["response"]["weather.forecast_home"]["forecast"][0]; float next_temperature = next_hour["temperature"].as(); @@ -28,11 +29,20 @@ esphome: data: entity_id: weather.forecast_home type: hourly + capture_response: true response_template: "{{ response['weather.forecast_home']['forecast'][0]['temperature'] }}" - on_response: + on_success: - lambda: |- float temperature = response["response"].as(); ESP_LOGD("main", "Next hour temperature: %f", temperature); + - homeassistant.action: + action: light.toggle + data: + entity_id: light.demo_light + on_success: + - logger.log: "Toggled demo light" + on_error: + - logger.log: "Failed to toggle demo light" api: port: 8000 From 635ef722b5da21cd725b956264a13809fede010d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:31:47 +1300 Subject: [PATCH 14/21] [const] Move `CONF_CAPTURE_RESPONSE` to const.py --- esphome/components/http_request/__init__.py | 2 +- esphome/const.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 98dbc29a86..e428838c83 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -5,6 +5,7 @@ from esphome.components.const import CONF_REQUEST_HEADERS from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( + CONF_CAPTURE_RESPONSE, CONF_ESP8266_DISABLE_SSL_SUPPORT, CONF_ID, CONF_METHOD, @@ -57,7 +58,6 @@ CONF_HEADERS = "headers" CONF_COLLECT_HEADERS = "collect_headers" CONF_BODY = "body" CONF_JSON = "json" -CONF_CAPTURE_RESPONSE = "capture_response" def validate_url(value): diff --git a/esphome/const.py b/esphome/const.py index ee6eec32b1..6d044a55ab 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -174,6 +174,7 @@ CONF_CALIBRATE_LINEAR = "calibrate_linear" CONF_CALIBRATION = "calibration" CONF_CAPACITANCE = "capacitance" CONF_CAPACITY = "capacity" +CONF_CAPTURE_RESPONSE = "capture_response" CONF_CARBON_MONOXIDE = "carbon_monoxide" CONF_CARRIER_DUTY_PERCENT = "carrier_duty_percent" CONF_CARRIER_FREQUENCY = "carrier_frequency" From 317ce7719724b5835bb54e2ec1c0a77c76fae42d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:38:11 +1300 Subject: [PATCH 15/21] [core] Update helpers for new auto load functionality --- script/helpers.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/script/helpers.py b/script/helpers.py index 38e6fcbd1e..61306b9489 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -529,7 +529,16 @@ def get_all_dependencies(component_names: set[str]) -> set[str]: new_components.update(dep.split(".")[0] for dep in comp.dependencies) # Add auto_load components - new_components.update(comp.auto_load) + auto_load = comp.auto_load + if callable(auto_load): + import inspect + + if inspect.signature(auto_load).parameters: + auto_load = auto_load(None) + else: + auto_load = auto_load() + + new_components.update(auto_load) # Check if we found any new components new_components -= all_components From 9608d8793c1706e08ba2a8206894ab8098fd1786 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:41:16 +1300 Subject: [PATCH 16/21] Fix order --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index d8240e4bdf..44dc5a6052 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -676,8 +676,8 @@ CONF_ON_RELEASE = "on_release" CONF_ON_RESPONSE = "on_response" CONF_ON_SHUTDOWN = "on_shutdown" CONF_ON_SPEED_SET = "on_speed_set" -CONF_ON_SUCCESS = "on_success" CONF_ON_STATE = "on_state" +CONF_ON_SUCCESS = "on_success" CONF_ON_TAG = "on_tag" CONF_ON_TAG_REMOVED = "on_tag_removed" CONF_ON_TIME = "on_time" From 49b271747de53e955e5f9010c4bf38b170534b5e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 7 Oct 2025 20:11:43 +1300 Subject: [PATCH 17/21] Add missing ifdef --- esphome/components/api/homeassistant_service.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index bc7afadb49..d1eb7ffc29 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -7,7 +7,9 @@ #include #include #include "api_pb2.h" +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #include "esphome/components/json/json_util.h" +#endif #include "esphome/core/automation.h" #include "esphome/core/helpers.h" From 1f557b46b35cd585e6815d40a57b4f6fd7616c05 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Oct 2025 09:53:41 -0500 Subject: [PATCH 18/21] fix ifdefs --- esphome/components/api/homeassistant_service.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index d1eb7ffc29..28d5b7f456 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -152,6 +152,7 @@ template class HomeAssistantServiceCallAction : public Actionflags_.wants_response) { resp.wants_response = true; // Set response template if provided @@ -160,6 +161,7 @@ template class HomeAssistantServiceCallAction : public Actionparent_->register_action_response_callback( From cd4c4eab35ba4869d956834061557ec25c23633a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Oct 2025 09:47:52 -0500 Subject: [PATCH 19/21] remove std::map, only 1 or 2 callbacks in flight ever --- esphome/components/api/api_server.cpp | 30 +++++++++++++++------------ esphome/components/api/api_server.h | 6 +++++- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 95617c75f1..778d9389ef 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -406,27 +406,31 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &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); + this->action_response_callbacks_.push_back({call_id, std::move(callback)}); } void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message) { - auto it = this->action_response_callbacks_.find(call_id); - if (it != this->action_response_callbacks_.end()) { - auto callback = std::move(it->second); - this->action_response_callbacks_.erase(it); - auto response = std::make_shared(success, error_message); - callback(response); + for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) { + if (it->call_id == call_id) { + auto callback = std::move(it->callback); + this->action_response_callbacks_.erase(it); + ActionResponse response(success, error_message); + callback(response); + return; + } } } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message, 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()) { - 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); + for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) { + if (it->call_id == call_id) { + auto callback = std::move(it->callback); + this->action_response_callbacks_.erase(it); + ActionResponse response(success, error_message, response_data, response_data_len); + callback(response); + return; + } } } #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index cd6c51cad2..4f7a3f93d8 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -199,7 +199,11 @@ class APIServer : public Component, public Controller { std::vector user_services_; #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - std::map action_response_callbacks_; + struct PendingActionResponse { + uint32_t call_id; + ActionResponseCallback callback; + }; + std::vector action_response_callbacks_; #endif // Group smaller types together From cbd30ce37a65c4ee7ce4b98c1cb28f6e2fee1be9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Oct 2025 08:58:14 -0500 Subject: [PATCH 20/21] as const object --- esphome/components/api/__init__.py | 2 +- esphome/components/api/api_server.h | 2 +- .../components/api/homeassistant_service.h | 47 +++++++++---------- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 1c74b2d355..58828c131d 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -408,7 +408,7 @@ async def homeassistant_service_to_code( cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON") await automation.build_automation( var.get_success_trigger_with_response(), - [(cg.JsonObject, "response"), *args], + [(cg.JsonObjectConst, "response"), *args], on_success, ) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 4f7a3f93d8..5d038e5ddd 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -114,7 +114,7 @@ class APIServer : public Component, public Controller { #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES // Action response handling - using ActionResponseCallback = std::function)>; + 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); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 28d5b7f456..730024f7b7 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -62,7 +62,6 @@ class ActionResponse { if (data == nullptr || data_len == 0) return; this->json_document_ = json::parse_json(data, data_len); - this->json_ = this->json_document_.as(); } #endif @@ -70,8 +69,8 @@ class ActionResponse { const std::string &get_error_message() const { return this->error_message_; } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - // Get data as parsed JSON object - JsonObject get_json() { return this->json_; } + // Get data as parsed JSON object (const version returns read-only view) + JsonObjectConst get_json() const { return this->json_document_.as(); } #endif protected: @@ -79,12 +78,11 @@ class ActionResponse { std::string error_message_; #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON JsonDocument json_document_; - JsonObject json_; #endif }; // Callback type for action responses -template using ActionResponseCallback = std::function, Ts...)>; +template using ActionResponseCallback = std::function; #endif template class HomeAssistantServiceCallAction : public Action { @@ -116,7 +114,9 @@ template class HomeAssistantServiceCallAction : public Actionflags_.wants_response = true; } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - Trigger *get_success_trigger_with_response() const { return this->success_trigger_with_response_; } + Trigger *get_success_trigger_with_response() const { + return this->success_trigger_with_response_; + } #endif Trigger *get_success_trigger() const { return this->success_trigger_; } Trigger *get_error_trigger() const { return this->error_trigger_; } @@ -164,25 +164,24 @@ template class HomeAssistantServiceCallAction : public Actionparent_->register_action_response_callback( - call_id, [this, captured_args](std::shared_ptr response) { - std::apply( - [this, &response](auto &&...args) { - if (response->is_success()) { + this->parent_->register_action_response_callback(call_id, [this, captured_args](const ActionResponse &response) { + std::apply( + [this, &response](auto &&...args) { + if (response.is_success()) { #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - if (this->flags_.wants_response) { - this->success_trigger_with_response_->trigger(response->get_json(), args...); - } else + if (this->flags_.wants_response) { + this->success_trigger_with_response_->trigger(response.get_json(), args...); + } else #endif - { - this->success_trigger_->trigger(args...); - } - } else { - this->error_trigger_->trigger(response->get_error_message(), args...); - } - }, - captured_args); - }); + { + this->success_trigger_->trigger(args...); + } + } else { + this->error_trigger_->trigger(response.get_error_message(), args...); + } + }, + captured_args); + }); } #endif @@ -198,7 +197,7 @@ template class HomeAssistantServiceCallAction : public Action response_template_{""}; - Trigger *success_trigger_with_response_ = new Trigger(); + Trigger *success_trigger_with_response_ = new Trigger(); #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON Trigger *success_trigger_ = new Trigger(); Trigger *error_trigger_ = new Trigger(); From 03884d05b4c596057b3f252b8c244d4580e36382 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Oct 2025 14:34:03 -0500 Subject: [PATCH 21/21] fix test --- tests/components/api/common.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/api/common.yaml b/tests/components/api/common.yaml index d3c549fff0..d87ae56ec2 100644 --- a/tests/components/api/common.yaml +++ b/tests/components/api/common.yaml @@ -18,7 +18,7 @@ esphome: capture_response: true on_success: - lambda: |- - JsonObject next_hour = response["response"]["weather.forecast_home"]["forecast"][0]; + JsonObjectConst 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: