ESPHome 2026.10.0-dev
Loading...
Searching...
No Matches
user_services.h
Go to the documentation of this file.
1#pragma once
2
3#include <span>
4#include <tuple>
5#include <utility>
6#include <vector>
7
8#include "api_pb2.h"
11#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
13#endif
14
15#ifdef USE_API_USER_DEFINED_ACTIONS
16namespace esphome::api {
17
18// Forward declaration - full definition in api_server.h
19class APIServer;
20
22 public:
26
27 virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
28#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
29 // Overload that accepts server-generated action_call_id (avoids client call_id collisions)
30 virtual bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) = 0;
31#endif
32
33 bool is_internal() { return false; }
34};
35
36template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
37
39
40// Scratch buffer list-entities hands to encode_list_service_response(); only ESP8266 copies into it
41#ifdef USE_ESP8266
42using UserActionScratch = std::array<char, API_USER_ACTION_STRINGS_SCRATCH_SIZE>;
43#else
44using UserActionScratch = std::array<char, 0>;
45#endif
46
47// Non-template base for YAML-defined services so the list-entities encoder is compiled once.
48// All strings live in one PROGMEM pointer table emitted by codegen (see _action_strings in
49// __init__.py), so each service costs a single pointer of RAM. Layout: the action name, then
50// each argument name; with USE_API_USER_DEFINED_ACTION_METADATA the action description follows
51// the name and every argument is (name, description, example). Unset metadata is nullptr.
52#ifdef USE_API_USER_DEFINED_ACTION_METADATA
53static constexpr size_t USER_ACTION_HEADER_STRINGS = 2;
54static constexpr size_t USER_ACTION_ARG_STRINGS = 3;
55#else
56static constexpr size_t USER_ACTION_HEADER_STRINGS = 1;
57static constexpr size_t USER_ACTION_ARG_STRINGS = 1;
58#endif
60 public:
61 UserServiceStatic(const char *const *strings, uint32_t key,
63 : strings_(strings), key_(key), supports_response_(supports_response) {}
64
65 protected:
66 ListEntitiesServicesResponse encode_list_service_response_(std::span<const enums::ServiceArgType> arg_types,
67 std::span<char> scratch) const;
71 StringRef str_(size_t idx, std::span<char> &scratch) const;
72
73 const char *const *strings_; // PROGMEM pointer table, read with progmem_read_ptr()
76};
77
78template<typename... Ts> class UserServiceBase : public UserServiceStatic {
79 public:
81
83 std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
84 return this->encode_list_service_response_(arg_types, scratch);
85 }
86
87 bool execute_service(const ExecuteServiceRequest &req) override {
88 if (req.key != this->key_)
89 return false;
90 if (req.args.size() != sizeof...(Ts))
91 return false;
92#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
93 this->execute_(req.args, req.call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
94#else
95 this->execute_(req.args, 0, false, std::make_index_sequence<sizeof...(Ts)>{});
96#endif
97 return true;
98 }
99
100#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
101 bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) override {
102 if (req.key != this->key_)
103 return false;
104 if (req.args.size() != sizeof...(Ts))
105 return false;
106 this->execute_(req.args, action_call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
107 return true;
108 }
109#endif
110
111 protected:
112 virtual void execute(uint32_t call_id, bool return_response, Ts... x) = 0;
113 template<typename ArgsContainer, size_t... S>
114 void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence<S...> /*type*/) {
115 this->execute(call_id, return_response, (get_execute_arg_value<Ts>(args[S]))...);
116 }
117};
118
119// Separate class for custom_api_device services (rare case)
120// Stores copies of runtime-generated names
121template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor {
122 public:
123 UserServiceDynamic(std::string name, const std::array<std::string, sizeof...(Ts)> &arg_names)
124 : name_(std::move(name)), arg_names_(arg_names) {
125 this->key_ = fnv1_hash(this->name_.c_str());
126 }
127
128 ListEntitiesServicesResponse encode_list_service_response(std::span<char> /*scratch*/) override {
130 msg.name = StringRef(this->name_);
131 msg.key = this->key_;
132 msg.supports_response = enums::SUPPORTS_RESPONSE_NONE; // Dynamic services don't support responses yet
133 std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
134 msg.args.init(sizeof...(Ts));
135 for (size_t i = 0; i < sizeof...(Ts); i++) {
136 auto &arg = msg.args.emplace_back();
137 arg.type = arg_types[i];
138 arg.name = StringRef(this->arg_names_[i]);
139 }
140 return msg;
141 }
142
143 bool execute_service(const ExecuteServiceRequest &req) override {
144 if (req.key != this->key_)
145 return false;
146 if (req.args.size() != sizeof...(Ts))
147 return false;
148#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
149 this->execute_(req.args, req.call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
150#else
151 this->execute_(req.args, 0, false, std::make_index_sequence<sizeof...(Ts)>{});
152#endif
153 return true;
154 }
155
156#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
157 // Dynamic services don't support responses yet, but need to implement the interface
158 bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) override {
159 if (req.key != this->key_)
160 return false;
161 if (req.args.size() != sizeof...(Ts))
162 return false;
163 this->execute_(req.args, action_call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
164 return true;
165 }
166#endif
167
168 protected:
169 virtual void execute(uint32_t call_id, bool return_response, Ts... x) = 0;
170 template<typename ArgsContainer, size_t... S>
171 void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence<S...> /*type*/) {
172 this->execute(call_id, return_response, (get_execute_arg_value<Ts>(args[S]))...);
173 }
174
175 // Heap-allocated strings for runtime-generated names
176 std::string name_;
177 std::array<std::string, sizeof...(Ts)> arg_names_;
179};
180
181// Primary template declaration
182template<enums::SupportsResponseType Mode, typename... Ts> class UserServiceTrigger;
183
184// Specialization for NONE - no extra trigger arguments
185template<typename... Ts>
186class UserServiceTrigger<enums::SUPPORTS_RESPONSE_NONE, Ts...> final : public UserServiceBase<Ts...>,
187 public Trigger<Ts...> {
188 public:
189 UserServiceTrigger(const char *const *strings, uint32_t key)
190 : UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_NONE) {}
191
192 protected:
193 void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override { this->trigger(x...); }
194};
195
196// Specialization for OPTIONAL - call_id and return_response trigger arguments
197template<typename... Ts>
198class UserServiceTrigger<enums::SUPPORTS_RESPONSE_OPTIONAL, Ts...> final : public UserServiceBase<Ts...>,
199 public Trigger<uint32_t, bool, Ts...> {
200 public:
201 UserServiceTrigger(const char *const *strings, uint32_t key)
202 : UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_OPTIONAL) {}
203
204 protected:
205 void execute(uint32_t call_id, bool return_response, Ts... x) override {
206 this->trigger(call_id, return_response, x...);
207 }
208};
209
210// Specialization for ONLY - just call_id trigger argument
211template<typename... Ts>
212class UserServiceTrigger<enums::SUPPORTS_RESPONSE_ONLY, Ts...> final : public UserServiceBase<Ts...>,
213 public Trigger<uint32_t, Ts...> {
214 public:
215 UserServiceTrigger(const char *const *strings, uint32_t key)
216 : UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_ONLY) {}
217
218 protected:
219 void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
220};
221
222// Specialization for STATUS - just call_id trigger argument (reports success/error without data)
223template<typename... Ts>
224class UserServiceTrigger<enums::SUPPORTS_RESPONSE_STATUS, Ts...> final : public UserServiceBase<Ts...>,
225 public Trigger<uint32_t, Ts...> {
226 public:
227 UserServiceTrigger(const char *const *strings, uint32_t key)
228 : UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_STATUS) {}
229
230 protected:
231 void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
232};
233
234} // namespace esphome::api
235#endif // USE_API_USER_DEFINED_ACTIONS
236
237#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
238// Include full definition of APIServer for template implementation
239// Must be outside namespace to avoid including STL headers inside namespace
240#include "api_server.h"
241
242namespace esphome::api {
243
244template<typename... Ts> class APIRespondAction final : public Action<Ts...> {
245 public:
246 explicit APIRespondAction(APIServer *parent) : parent_(parent) {}
247
248 template<typename V> void set_success(V success) { this->success_ = success; }
249 template<typename V> void set_error_message(V error) { this->error_message_ = error; }
250 void set_is_optional_mode(bool is_optional) { this->is_optional_mode_ = is_optional; }
251
252#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
253 void set_data(std::function<void(Ts..., JsonObject)> &&func) {
254 this->json_builder_ = std::move(func);
255 this->has_data_ = true;
256 }
257#endif
258
259 void play(const Ts &...x) override {
260 // Extract call_id from first argument - it's always first for optional/only/status modes
261 auto args = std::make_tuple(x...);
262 uint32_t call_id = std::get<0>(args);
263
264 bool success = this->success_.value(x...);
265 std::string error_message = this->error_message_.value(x...);
266
267#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
268 if (this->has_data_) {
269 // For optional mode, check return_response (second arg) to decide if client wants data
270 // Use nested if constexpr to avoid compile error when tuple doesn't have enough elements
271 // (std::tuple_element_t is evaluated before the && short-circuit, so we must nest)
272 if constexpr (sizeof...(Ts) >= 2) {
273 if constexpr (std::is_same_v<std::tuple_element_t<1, std::tuple<Ts...>>, bool>) {
274 if (this->is_optional_mode_) {
275 bool return_response = std::get<1>(args);
276 if (!return_response) {
277 // Client doesn't want response data, just send success/error
278 this->parent_->send_action_response(call_id, success, StringRef(error_message));
279 return;
280 }
281 }
282 }
283 }
284 // Build and send JSON response
285 json::JsonBuilder builder;
286 this->json_builder_(x..., builder.root());
287 auto json_buf = builder.serialize();
288 this->parent_->send_action_response(call_id, success, StringRef(error_message),
289 reinterpret_cast<const uint8_t *>(json_buf.data()), json_buf.size());
290 return;
291 }
292#endif
293 this->parent_->send_action_response(call_id, success, StringRef(error_message));
294 }
295
296 protected:
298 TemplatableFn<bool, Ts...> success_{[](Ts...) -> bool { return true; }};
299 TemplatableValue<std::string, Ts...> error_message_{""};
300#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
301 std::function<void(Ts..., JsonObject)> json_builder_;
302 bool has_data_{false};
303#endif
304 bool is_optional_mode_{false};
305};
306
307// Action to unregister a service call after execution completes
308// Automatically appended to the end of action lists for non-none response modes
309template<typename... Ts> class APIUnregisterServiceCallAction final : public Action<Ts...> {
310 public:
311 explicit APIUnregisterServiceCallAction(APIServer *parent) : parent_(parent) {}
312
313 void play(const Ts &...x) override {
314 // Extract call_id from first argument - same convention as APIRespondAction
315 auto args = std::make_tuple(x...);
316 uint32_t call_id = std::get<0>(args);
317 if (call_id != 0) {
319 }
320 }
321
322 protected:
324};
325
326} // namespace esphome::api
327#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
StringRef is a reference to a string owned by something else.
Definition string_ref.h:26
constexpr size_type size() const
Definition string_ref.h:74
Function-pointer-only templatable storage (4 bytes on 32-bit).
Definition automation.h:19
T value(X... x) const
Definition automation.h:58
Primary TemplatableValue: stores either a constant value or a function pointer.
Definition automation.h:94
T value(X... x) const
Definition automation.h:175
void play(const Ts &...x) override
TemplatableFn< bool, Ts... > success_
void set_data(std::function< void(Ts..., JsonObject)> &&func)
void set_is_optional_mode(bool is_optional)
APIRespondAction(APIServer *parent)
std::function< void(Ts..., JsonObject)> json_builder_
TemplatableValue< std::string, Ts... > error_message_
void send_action_response(uint32_t action_call_id, bool success, StringRef error_message)
void unregister_active_action_call(uint32_t action_call_id)
FixedVector< ExecuteServiceArgument > args
Definition api_pb2.h:1385
enums::SupportsResponseType supports_response
Definition api_pb2.h:1344
FixedVector< ListEntitiesServicesArgument > args
Definition api_pb2.h:1343
bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) override
void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence< S... >)
virtual void execute(uint32_t call_id, bool return_response, Ts... x)=0
bool execute_service(const ExecuteServiceRequest &req) override
ListEntitiesServicesResponse encode_list_service_response(std::span< char > scratch) override
virtual ListEntitiesServicesResponse encode_list_service_response(std::span< char > scratch)=0
Build the list-entities message.
virtual bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id)=0
virtual bool execute_service(const ExecuteServiceRequest &req)=0
ListEntitiesServicesResponse encode_list_service_response(std::span< char >) override
UserServiceDynamic(std::string name, const std::array< std::string, sizeof...(Ts)> &arg_names)
bool execute_service(const ExecuteServiceRequest &req) override
void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence< S... >)
bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) override
std::array< std::string, sizeof...(Ts)> arg_names_
virtual void execute(uint32_t call_id, bool return_response, Ts... x)=0
StringRef str_(size_t idx, std::span< char > &scratch) const
Reference table entry idx; nullptr gives an empty StringRef.
ListEntitiesServicesResponse encode_list_service_response_(std::span< const enums::ServiceArgType > arg_types, std::span< char > scratch) const
UserServiceStatic(const char *const *strings, uint32_t key, enums::SupportsResponseType supports_response=enums::SUPPORTS_RESPONSE_NONE)
enums::SupportsResponseType supports_response_
void execute(uint32_t call_id, bool return_response, Ts... x) override
Builder class for creating JSON documents without lambdas.
Definition json_util.h:169
SerializationBuffer serialize()
Serialize the JSON document to a SerializationBuffer (stack-first allocation) Uses 512-byte stack buf...
Definition json_util.cpp:69
std::array< char, API_USER_ACTION_STRINGS_SCRATCH_SIZE > UserActionScratch
enums::ServiceArgType to_service_arg_type()
T get_execute_arg_value(const ExecuteServiceArgument &arg)
uint32_t fnv1_hash(const char *str)
Calculate a FNV-1 hash of str.
Definition helpers.cpp:160
STL namespace.
static void uint32_t
watchdog_hw scratch[0]
uint16_t x
Definition tt21100.cpp:5