#pragma once #include #include #include #include "api_pb2.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON #include "esphome/components/json/json_util.h" #endif #ifdef USE_API_USER_DEFINED_ACTIONS namespace esphome::api { // Forward declaration - full definition in api_server.h class APIServer; class UserServiceDescriptor { public: virtual ListEntitiesServicesResponse encode_list_service_response() = 0; virtual bool execute_service(const ExecuteServiceRequest &req) = 0; #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES // Overload that accepts server-generated action_call_id (avoids client call_id collisions) virtual bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) = 0; #endif bool is_internal() { return false; } }; template T get_execute_arg_value(const ExecuteServiceArgument &arg); template enums::ServiceArgType to_service_arg_type(); // Base class for YAML-defined services (most common case) // Stores only pointers to string literals in flash - no heap allocation template class UserServiceBase : public UserServiceDescriptor { public: UserServiceBase(const char *name, const std::array &arg_names, enums::SupportsResponseType supports_response = enums::SUPPORTS_RESPONSE_NONE) : name_(name), arg_names_(arg_names), supports_response_(supports_response) { this->key_ = fnv1_hash(name); } ListEntitiesServicesResponse encode_list_service_response() override { ListEntitiesServicesResponse msg; msg.name = StringRef(this->name_); msg.key = this->key_; msg.supports_response = this->supports_response_; std::array arg_types = {to_service_arg_type()...}; msg.args.init(sizeof...(Ts)); for (size_t i = 0; i < sizeof...(Ts); i++) { auto &arg = msg.args.emplace_back(); arg.type = arg_types[i]; arg.name = StringRef(this->arg_names_[i]); } return msg; } bool execute_service(const ExecuteServiceRequest &req) override { if (req.key != this->key_) return false; if (req.args.size() != sizeof...(Ts)) return false; #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES this->execute_(req.args, req.call_id, req.return_response, std::make_index_sequence{}); #else this->execute_(req.args, 0, false, std::make_index_sequence{}); #endif return true; } #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) override { if (req.key != this->key_) return false; if (req.args.size() != sizeof...(Ts)) return false; this->execute_(req.args, action_call_id, req.return_response, std::make_index_sequence{}); return true; } #endif protected: virtual void execute(uint32_t call_id, bool return_response, Ts... x) = 0; template void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence /*type*/) { this->execute(call_id, return_response, (get_execute_arg_value(args[S]))...); } // Pointers to string literals in flash - no heap allocation const char *name_; std::array arg_names_; uint32_t key_{0}; enums::SupportsResponseType supports_response_{enums::SUPPORTS_RESPONSE_NONE}; }; // Separate class for custom_api_device services (rare case) // Stores copies of runtime-generated names template class UserServiceDynamic : public UserServiceDescriptor { public: UserServiceDynamic(std::string name, const std::array &arg_names) : name_(std::move(name)), arg_names_(arg_names) { this->key_ = fnv1_hash(this->name_.c_str()); } ListEntitiesServicesResponse encode_list_service_response() override { ListEntitiesServicesResponse msg; msg.name = StringRef(this->name_); msg.key = this->key_; msg.supports_response = enums::SUPPORTS_RESPONSE_NONE; // Dynamic services don't support responses yet std::array arg_types = {to_service_arg_type()...}; msg.args.init(sizeof...(Ts)); for (size_t i = 0; i < sizeof...(Ts); i++) { auto &arg = msg.args.emplace_back(); arg.type = arg_types[i]; arg.name = StringRef(this->arg_names_[i]); } return msg; } bool execute_service(const ExecuteServiceRequest &req) override { if (req.key != this->key_) return false; if (req.args.size() != sizeof...(Ts)) return false; #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES this->execute_(req.args, req.call_id, req.return_response, std::make_index_sequence{}); #else this->execute_(req.args, 0, false, std::make_index_sequence{}); #endif return true; } #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES // Dynamic services don't support responses yet, but need to implement the interface bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) override { if (req.key != this->key_) return false; if (req.args.size() != sizeof...(Ts)) return false; this->execute_(req.args, action_call_id, req.return_response, std::make_index_sequence{}); return true; } #endif protected: virtual void execute(uint32_t call_id, bool return_response, Ts... x) = 0; template void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence /*type*/) { this->execute(call_id, return_response, (get_execute_arg_value(args[S]))...); } // Heap-allocated strings for runtime-generated names std::string name_; std::array arg_names_; uint32_t key_{0}; }; // Primary template declaration template class UserServiceTrigger; // Specialization for NONE - no extra trigger arguments template class UserServiceTrigger : public UserServiceBase, public Trigger { public: UserServiceTrigger(const char *name, const std::array &arg_names) : UserServiceBase(name, arg_names, enums::SUPPORTS_RESPONSE_NONE) {} protected: void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override { this->trigger(x...); } }; // Specialization for OPTIONAL - call_id and return_response trigger arguments template class UserServiceTrigger : public UserServiceBase, public Trigger { public: UserServiceTrigger(const char *name, const std::array &arg_names) : UserServiceBase(name, arg_names, enums::SUPPORTS_RESPONSE_OPTIONAL) {} protected: void execute(uint32_t call_id, bool return_response, Ts... x) override { this->trigger(call_id, return_response, x...); } }; // Specialization for ONLY - just call_id trigger argument template class UserServiceTrigger : public UserServiceBase, public Trigger { public: UserServiceTrigger(const char *name, const std::array &arg_names) : UserServiceBase(name, arg_names, enums::SUPPORTS_RESPONSE_ONLY) {} protected: void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); } }; // Specialization for STATUS - just call_id trigger argument (reports success/error without data) template class UserServiceTrigger : public UserServiceBase, public Trigger { public: UserServiceTrigger(const char *name, const std::array &arg_names) : UserServiceBase(name, arg_names, enums::SUPPORTS_RESPONSE_STATUS) {} protected: void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); } }; } // namespace esphome::api #endif // USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES // Include full definition of APIServer for template implementation // Must be outside namespace to avoid including STL headers inside namespace #include "api_server.h" namespace esphome::api { template class APIRespondAction : public Action { public: explicit APIRespondAction(APIServer *parent) : parent_(parent) {} template void set_success(V success) { this->success_ = success; } template void set_error_message(V error) { this->error_message_ = error; } void set_is_optional_mode(bool is_optional) { this->is_optional_mode_ = is_optional; } #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON void set_data(std::function &&func) { this->json_builder_ = std::move(func); this->has_data_ = true; } #endif void play(const Ts &...x) override { // Extract call_id from first argument - it's always first for optional/only/status modes auto args = std::make_tuple(x...); uint32_t call_id = std::get<0>(args); bool success = this->success_.value(x...); std::string error_message = this->error_message_.value(x...); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON if (this->has_data_) { // For optional mode, check return_response (second arg) to decide if client wants data // Use nested if constexpr to avoid compile error when tuple doesn't have enough elements // (std::tuple_element_t is evaluated before the && short-circuit, so we must nest) if constexpr (sizeof...(Ts) >= 2) { if constexpr (std::is_same_v>, bool>) { if (this->is_optional_mode_) { bool return_response = std::get<1>(args); if (!return_response) { // Client doesn't want response data, just send success/error this->parent_->send_action_response(call_id, success, StringRef(error_message)); return; } } } } // Build and send JSON response json::JsonBuilder builder; this->json_builder_(x..., builder.root()); auto json_buf = builder.serialize(); this->parent_->send_action_response(call_id, success, StringRef(error_message), reinterpret_cast(json_buf.data()), json_buf.size()); return; } #endif this->parent_->send_action_response(call_id, success, StringRef(error_message)); } protected: APIServer *parent_; TemplatableFn success_{[](Ts...) -> bool { return true; }}; TemplatableValue error_message_{""}; #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON std::function json_builder_; bool has_data_{false}; #endif bool is_optional_mode_{false}; }; // Action to unregister a service call after execution completes // Automatically appended to the end of action lists for non-none response modes template class APIUnregisterServiceCallAction : public Action { public: explicit APIUnregisterServiceCallAction(APIServer *parent) : parent_(parent) {} void play(const Ts &...x) override { // Extract call_id from first argument - same convention as APIRespondAction auto args = std::make_tuple(x...); uint32_t call_id = std::get<0>(args); if (call_id != 0) { this->parent_->unregister_active_action_call(call_id); } } protected: APIServer *parent_; }; } // namespace esphome::api #endif // USE_API_USER_DEFINED_ACTION_RESPONSES