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