diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 8f0910b9a3..58828c131d 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, @@ -17,30 +18,50 @@ from esphome.const import ( CONF_MAX_CONNECTIONS, CONF_ON_CLIENT_CONNECTED, CONF_ON_CLIENT_DISCONNECTED, + CONF_ON_ERROR, + CONF_ON_SUCCESS, 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"] 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_( "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) @@ -288,6 +309,29 @@ async def to_code(config): KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)}) +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_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 + + HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( cv.Schema( { @@ -303,10 +347,15 @@ 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_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), } ), cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), cv.rename_key(CONF_SERVICE, CONF_ACTION), + _validate_response_config, ) @@ -320,7 +369,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) @@ -335,6 +389,40 @@ 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 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_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.JsonObjectConst, "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 0e385c4a17..87f477799d 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -780,6 +780,22 @@ 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"]; + 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 +message HomeassistantActionResponse { + option (id) = 130; + option (source) = SOURCE_CLIENT; + option (no_delay) = true; + 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 + 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 615b7f5764..ae03dfbb33 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" @@ -1549,6 +1549,20 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) { } } #endif + +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES +void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) { +#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 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 ee9c81026c..284fa11a95 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -129,7 +129,10 @@ class APIConnection final : public APIServerConnection { return; this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE); } -#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES + void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override; +#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 0140c60e5b..70bcf082a6 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -884,6 +884,15 @@ 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_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 { size.add_length(1, this->service_ref_.size()); @@ -891,6 +900,48 @@ 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_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 +} +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES +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; +#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; + } + 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..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 = 113; + static constexpr uint8_t ESTIMATED_SIZE = 128; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "homeassistant_action_request"; } #endif @@ -1114,6 +1114,15 @@ 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_JSON + bool wants_response{false}; +#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + std::string response_template{}; +#endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1123,6 +1132,30 @@ 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 = 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{}; +#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 + + 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 { public: diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index c5f1d99dd4..cf732e451b 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1122,6 +1122,28 @@ 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_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 +} +#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); +#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_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index ccbd781431..9d227af0a3 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_ACTION_RESPONSES + 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..549b00ee6a 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_ACTION_RESPONSES + 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 a8fdb635cf..778d9389ef 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -9,12 +9,16 @@ #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" #endif #include +#include namespace esphome::api { @@ -400,7 +404,38 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call client->send_homeassistant_action(call); } } -#endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES +void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback 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) { + 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) { + 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 +#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 b9049c1700..5d038e5ddd 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 { @@ -111,7 +112,17 @@ class APIServer : public Component, public Controller { #ifdef USE_API_HOMEASSISTANT_SERVICES void send_homeassistant_action(const HomeassistantActionRequest &call); -#endif +#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); +#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 void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } #endif @@ -187,6 +198,13 @@ class APIServer : public Component, public Controller { #ifdef USE_API_SERVICES std::vector user_services_; #endif +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES + struct PendingActionResponse { + uint32_t call_id; + ActionResponseCallback callback; + }; + std::vector 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..730024f7b7 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -3,8 +3,13 @@ #include "api_server.h" #ifdef USE_API #ifdef USE_API_HOMEASSISTANT_SERVICES +#include +#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" @@ -44,9 +49,47 @@ 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)) {} + +#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); + } +#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 (const version returns read-only view) + JsonObjectConst get_json() const { return this->json_document_.as(); } +#endif + + protected: + bool success_; + std::string error_message_; +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + JsonDocument json_document_; +#endif +}; + +// Callback type for action responses +template using ActionResponseCallback = std::function; +#endif + template 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; } @@ -61,11 +104,29 @@ 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->flags_.has_response_template = true; + } + + void set_wants_status() { this->flags_.wants_status = true; } + void set_wants_response() { this->flags_.wants_response = true; } + +#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 + 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(); @@ -84,18 +145,74 @@ template class HomeAssistantServiceCallAction : public Actionflags_.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; +#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + 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; + } + } +#endif + + auto captured_args = std::make_tuple(x...); + 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 +#endif + { + this->success_trigger_->trigger(args...); + } + } else { + this->error_trigger_->trigger(response.get_error_message(), args...); + } + }, + captured_args); + }); + } +#endif + this->parent_->send_homeassistant_action(resp); } protected: APIServer *parent_; - bool is_event_; TemplatableStringValue 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 *success_trigger_with_response_ = new Trigger(); +#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON + Trigger *success_trigger_ = new Trigger(); + Trigger *error_trigger_ = new Trigger(); +#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 + #endif #endif diff --git a/esphome/const.py b/esphome/const.py index 6d044a55ab..44dc5a6052 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -677,6 +677,7 @@ CONF_ON_RESPONSE = "on_response" CONF_ON_SHUTDOWN = "on_shutdown" CONF_ON_SPEED_SET = "on_speed_set" 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" @@ -819,6 +820,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" diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 468e9af5fb..2317c0ed32 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -112,6 +112,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_JSON #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 4f1693dac8..d87ae56ec2 100644 --- a/tests/components/api/common.yaml +++ b/tests/components/api/common.yaml @@ -10,6 +10,39 @@ esphome: data: message: Button was pressed - homeassistant.tag_scanned: pulse + - homeassistant.action: + action: weather.get_forecasts + data: + entity_id: weather.forecast_home + type: hourly + capture_response: true + on_success: + - lambda: |- + 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: + - lambda: |- + ESP_LOGE("main", "Action failed with error: %s", error.c_str()); + - homeassistant.action: + action: weather.get_forecasts + data: + entity_id: weather.forecast_home + type: hourly + capture_response: true + response_template: "{{ response['weather.forecast_home']['forecast'][0]['temperature'] }}" + 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