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