ESPHome 2026.8.2
Loading...
Searching...
No Matches
api_server.cpp
Go to the documentation of this file.
1#include "api_server.h"
2#ifdef USE_API
3#include <cerrno>
4#include <cinttypes>
5#include "api_connection.h"
10#include "esphome/core/hal.h"
11#include "esphome/core/log.h"
12#include "esphome/core/util.h"
14#ifdef USE_API_HOMEASSISTANT_SERVICES
16#endif
17
18#ifdef USE_LOGGER
20#endif
21
22#include <algorithm>
23#include <utility>
24
25namespace esphome::api {
26
27static const char *const TAG = "api";
28
29// APIServer
30APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
31
33
34void APIServer::socket_failed_(const LogString *msg) {
35 ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
36 this->destroy_socket_();
37 this->mark_failed();
38}
39
42
43#ifdef USE_API_NOISE
44 uint32_t hash = 88491486UL;
45
47
48#ifndef USE_API_NOISE_PSK_FROM_YAML
49 // Only load saved PSK if not set from YAML
50 if (this->load_and_apply_noise_psk_()) {
51 ESP_LOGD(TAG, "Loaded saved Noise PSK");
52 }
53#endif
54#endif
55
56 this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
57 if (this->socket_ == nullptr) {
58 this->socket_failed_(LOG_STR("creation"));
59 return;
60 }
61 int enable = 1;
62 int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
63 if (err != 0) {
64 ESP_LOGW(TAG, "Socket reuseaddr: errno %d", errno);
65 // we can still continue
66 }
67 err = this->socket_->setblocking(false);
68 if (err != 0) {
69 this->socket_failed_(LOG_STR("nonblocking"));
70 return;
71 }
72
73 struct sockaddr_storage server;
74
75 socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_);
76 if (sl == 0) {
77 this->socket_failed_(LOG_STR("set sockaddr"));
78 return;
79 }
80
81 err = this->socket_->bind((struct sockaddr *) &server, sl);
82 if (err != 0) {
83 this->socket_failed_(LOG_STR("bind"));
84 return;
85 }
86
87 err = this->socket_->listen(this->listen_backlog_);
88 if (err != 0) {
89 this->socket_failed_(LOG_STR("listen"));
90 return;
91 }
92
93#ifdef USE_LOGGER
94 if (logger::global_logger != nullptr) {
96 this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) {
97 static_cast<APIServer *>(self)->on_log(level, tag, message, message_len);
98 });
99 }
100#endif
101
102#ifdef USE_CAMERA
103 if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) {
105 }
106#endif
107
108 // Initialize last_connected_ for reboot timeout tracking
110#if defined(USE_PROVISIONING) && defined(USE_API_NOISE)
111 // Register with the provisioning manager (provisioning:) as a source and
112 // report our current state (provisioned == an encryption key is set). When the
113 // window closes, disconnect any client still attempting to provision so it learns
114 // the reason. The manager owns the timeout, window state and on_timeout automation.
118 this->noise_ctx_.has_psk());
120 for (auto &c : this->active_clients()) {
123 // Best-effort: if the send buffer is full the reason is dropped, but the
124 // client still learns the window is closed when it reconnects (rejected at
125 // hello) or via the socket close.
126 if (!c->send_message(req)) {
127 API_LOG_MSG_DROPPED(TAG, "Disconnect request");
128 }
129 }
130 });
131 }
132#endif
133 // Set warning status if reboot timeout is enabled (suppressed while provisioning
134 // is pending so the device waits to be onboarded instead of rebooting).
135 if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
136 this->status_set_warning(LOG_STR("waiting for client connection"));
137 }
138}
139
141 // Accept new clients only if the socket exists and has incoming connections
142 if (this->socket_ && this->socket_->ready()) {
143 this->accept_new_connections_();
144 }
145
146 if (this->api_connection_count_ == 0) {
147 // Check reboot timeout - done in loop to avoid scheduler heap churn
148 // (cancelled scheduler items sit in heap memory until their scheduled time).
149 // Suppressed while a provisioning window is pending so the device waits to be
150 // onboarded / reset instead of rebooting itself; resumes once provisioned.
151 if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
153 if (now - this->last_connected_ > this->reboot_timeout_) {
154 ESP_LOGE(TAG, "No clients; rebooting");
155 App.reboot();
156 }
157 }
158 return;
159 }
160
161 // Process clients and remove disconnected ones in a single pass
162 // Check network connectivity once for all clients
163 if (!network::is_connected()) {
164 // Network is down - disconnect all clients
165 for (auto &client : this->active_clients()) {
166 client->on_fatal_error();
167 client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect"));
168 }
169 // Continue to process and clean up the clients below
170 }
171
172 uint8_t client_index = 0;
173 while (client_index < this->api_connection_count_) {
174 auto &client = this->clients_[client_index];
175
176 // Common case: process active client
177 if (!client->flags_.remove) {
178 client->loop();
179 }
180 // Handle disconnection promptly - close socket to free LWIP PCB
181 // resources and prevent retransmit crashes on ESP8266.
182 if (client->flags_.remove) {
183 // Rare case: handle disconnection (don't increment - swapped element needs processing)
184 this->remove_client_(client_index);
185 } else {
186 client_index++;
187 }
188 }
189}
190
191void APIServer::remove_client_(uint8_t client_index) {
192 auto &client = this->clients_[client_index];
193
194#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
196#endif
197 ESP_LOGV(TAG, "Remove connection %s", client->get_name());
198
199#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
200 // Save client info before closing socket and removal for the trigger
201 char peername_buf[socket::SOCKADDR_STR_LEN];
202 std::string client_name(client->get_name());
203 std::string client_peername(client->get_peername_to(peername_buf));
204#endif
205
206 // Close socket now (was deferred from on_fatal_error to allow getpeername)
207 client->helper_->close();
208
209 // Swap-and-reset: move the removed client to the trailing slot and null it out so slots
210 // [api_connection_count_, N) remain nullptr.
211 const uint8_t last_index = this->api_connection_count_ - 1;
212 if (client_index < last_index) {
213 std::swap(this->clients_[client_index], this->clients_[last_index]);
214 }
215 // Drop the count before resetting the slot. reset() runs ~APIConnection(), which can reenter the
216 // server (e.g. voice_assistant unsubscribes in its disconnect trigger, publishing entity state ->
217 // on_*_update iterating active_clients()). Excluding the dying slot from the active range first
218 // keeps that reentrant iteration from dereferencing the now-null slot.
219 this->api_connection_count_--;
220 this->clients_[last_index].reset();
221
222 // Last client disconnected - set warning and start tracking for reboot timeout
223 // (suppressed while provisioning is pending - see loop()).
224 if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
225 this->status_set_warning(LOG_STR("waiting for client connection"));
227 }
228
229#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
230 // Fire trigger after client is removed so api.connected reflects the true state
231 this->client_disconnected_trigger_.trigger(client_name, client_peername);
232#endif
233}
234
235void __attribute__((flatten)) APIServer::accept_new_connections_() {
236 while (true) {
237 struct sockaddr_storage source_addr;
238 socklen_t addr_len = sizeof(source_addr);
239
240 auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
241 if (!sock)
242 break;
243
244 char peername[socket::SOCKADDR_STR_LEN];
245 sock->getpeername_to(peername);
246
247 // Check if we're at the connection limit
248 if (this->api_connection_count_ >= MAX_API_CONNECTIONS) {
249 ESP_LOGW(TAG, "Max connections (%d), rejecting %s", MAX_API_CONNECTIONS, peername);
250 // Immediately close - socket destructor will handle cleanup
251 sock.reset();
252 continue;
253 }
254
255 ESP_LOGD(TAG, "Accept %s", peername);
256
257 auto *conn = new APIConnection(std::move(sock), this);
258 this->clients_[this->api_connection_count_++].reset(conn);
259 conn->start();
260
261 // First client connected - clear warning and update timestamp
262 if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
263 this->status_clear_warning();
265 }
266 }
267}
268
270 char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
271 ESP_LOGCONFIG(TAG,
272 "Server:\n"
273 " Address: %s:%u\n"
274 " Listen backlog: %u\n"
275 " Max connections: %u",
276 network::get_use_address_to(addr_buf), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
277#ifdef USE_API_NOISE
278 ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
279 if (!this->noise_ctx_.has_psk()) {
280 ESP_LOGCONFIG(TAG, " Supports encryption: YES");
281 }
282#else
283 ESP_LOGCONFIG(TAG, " Noise encryption: NO");
284#endif
285}
286
288
289// Macro for controller update dispatch
290#define API_DISPATCH_UPDATE(entity_type, entity_name) \
291 void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
292 if (obj->is_internal()) \
293 return; \
294 for (auto &c : this->active_clients()) { \
295 if (c->flags_.state_subscription) \
296 c->send_##entity_name##_state(obj); \
297 } \
298 }
299
300#ifdef USE_BINARY_SENSOR
302#endif
303
304#ifdef USE_COVER
306#endif
307
308#ifdef USE_FAN
310#endif
311
312#ifdef USE_LIGHT
314#endif
315
316#ifdef USE_SENSOR
318#endif
319
320#ifdef USE_SWITCH
322#endif
323
324#ifdef USE_TEXT_SENSOR
326#endif
327
328#ifdef USE_CLIMATE
330#endif
331
332#ifdef USE_NUMBER
334#endif
335
336#ifdef USE_DATETIME_DATE
338#endif
339
340#ifdef USE_DATETIME_TIME
342#endif
343
344#ifdef USE_DATETIME_DATETIME
346#endif
347
348#ifdef USE_TEXT
350#endif
351
352#ifdef USE_SELECT
354#endif
355
356#ifdef USE_LOCK
358#endif
359
360#ifdef USE_VALVE
362#endif
363
364#ifdef USE_MEDIA_PLAYER
366#endif
367
368#ifdef USE_WATER_HEATER
370#endif
371
372#ifdef USE_EVENT
374 if (obj->is_internal())
375 return;
376 for (auto &c : this->active_clients()) {
377 if (c->flags_.state_subscription)
378 c->send_event(obj);
379 }
380}
381#endif
382
383#ifdef USE_UPDATE
384// Update is a special case - the method is called on_update, not on_update_update
386 if (obj->is_internal())
387 return;
388 for (auto &c : this->active_clients()) {
389 if (c->flags_.state_subscription)
390 c->send_update_state(obj);
391 }
392}
393#endif
394
395#ifdef USE_ZWAVE_PROXY
397 // We could add code to manage a second subscription type, but, since this message type is
398 // very infrequent and small, we simply send it to all clients
399 for (auto &c : this->active_clients()) {
400 if (!c->send_message(msg)) {
401 API_LOG_MSG_DROPPED(TAG, "Home ID notification");
402 }
403 }
404}
405#endif
406
407#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
409 const std::vector<int32_t> *timings) {
411#ifdef USE_DEVICES
412 resp.device_id = device_id;
413#endif
414 resp.key = key;
415 resp.timings = timings;
416
417 for (auto &c : this->active_clients())
418 c->send_infrared_rf_receive_event(resp);
419}
420#endif
421
422#ifdef USE_ALARM_CONTROL_PANEL
424#endif
425
427
428void APIServer::set_port(uint16_t port) { this->port_ = port; }
429
430void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
431
432#ifdef USE_API_HOMEASSISTANT_SERVICES
434 bool has_subscriber = false;
435 for (auto &client : this->active_clients()) {
436 has_subscriber |= client->send_homeassistant_action(call);
437 }
438 if (!has_subscriber) {
439 // Home Assistant subscribes to actions shortly *after* authenticating, so actions
440 // fired right at connection time (on_client_connected, on_time_sync, ...) can
441 // arrive before the subscription and are lost - warn instead of failing silently.
442 ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s", call.is_event ? "event" : "action", call.service.c_str(),
443 this->is_connected() ? "client has not subscribed to actions (yet)" : "no client connected");
444 }
445}
446#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
448 this->action_response_callbacks_.push_back({call_id, std::move(callback)});
449}
450
451void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef error_message) {
452 for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
453 if (it->call_id == call_id) {
454 auto callback = std::move(it->callback);
455 this->action_response_callbacks_.erase(it);
456 ActionResponse response(success, error_message);
457 callback(response);
458 return;
459 }
460 }
461}
462#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
463void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef error_message,
464 const uint8_t *response_data, size_t response_data_len) {
465 for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
466 if (it->call_id == call_id) {
467 auto callback = std::move(it->callback);
468 this->action_response_callbacks_.erase(it);
469 ActionResponse response(success, error_message, response_data, response_data_len);
470 callback(response);
471 return;
472 }
473 }
474}
475#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
476#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
477#endif // USE_API_HOMEASSISTANT_SERVICES
478
479#ifdef USE_API_HOMEASSISTANT_STATES
480// Helper to add subscription (reduces duplication)
481void APIServer::add_state_subscription_(const char *entity_id, const char *attribute,
482 std::function<void(StringRef)> &&f, bool once) {
484 .entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once,
485 // entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation)
486 });
487}
488
489// Helper to add subscription with heap-allocated strings (reduces duplication)
490void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
491 std::function<void(StringRef)> &&f, bool once) {
493 // Allocate heap storage for the strings
494 sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id));
495 sub.entity_id = sub.entity_id_dynamic_storage->c_str();
496
497 if (attribute.has_value()) {
498 sub.attribute_dynamic_storage = std::make_unique<std::string>(std::move(attribute.value()));
499 sub.attribute = sub.attribute_dynamic_storage->c_str();
500 } else {
501 sub.attribute = nullptr;
502 }
503
504 sub.callback = std::move(f);
505 sub.once = once;
506 this->state_subs_.push_back(std::move(sub));
507}
508
509// New const char* overload (for internal components - zero allocation)
510void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute,
511 std::function<void(StringRef)> &&f) {
512 this->add_state_subscription_(entity_id, attribute, std::move(f), false);
513}
514
515void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute,
516 std::function<void(StringRef)> &&f) {
517 this->add_state_subscription_(entity_id, attribute, std::move(f), true);
518}
519
520// std::string overload with StringRef callback (zero-allocation callback)
521void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
522 std::function<void(StringRef)> &&f) {
523 this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
524}
525
526void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
527 std::function<void(StringRef)> &&f) {
528 this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
529}
530
531// Legacy helper: wraps std::string callback and delegates to StringRef version
532void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
533 std::function<void(const std::string &)> &&f, bool once) {
534 // Wrap callback to convert StringRef -> std::string, then delegate
535 this->add_state_subscription_(std::move(entity_id), std::move(attribute),
536 std::function<void(StringRef)>([f = std::move(f)](StringRef state) { f(state.str()); }),
537 once);
538}
539
540// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string)
541void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
542 std::function<void(const std::string &)> &&f) {
543 this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
544}
545
546void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
547 std::function<void(const std::string &)> &&f) {
548 this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
549}
550
551const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
552 return this->state_subs_;
553}
554#endif
555
556uint16_t APIServer::get_port() const { return this->port_; }
557
558void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
559
560#ifdef USE_API_NOISE
561bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
562 const LogString *fail_log_msg, bool make_active) {
563 if (!this->noise_pref_.save(&new_psk)) {
564 ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg));
565 return false;
566 }
567 // ensure it's written immediately
568 if (!global_preferences->sync()) {
569 ESP_LOGW(TAG, "Failed to sync preferences");
570 return false;
571 }
572 ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg));
573 if (make_active) {
574 this->set_timeout(100, [this]() {
575 // Re-read the PSK from preferences rather than capturing the 32-byte array
576 // in the lambda (which would exceed std::function SBO and heap-allocate).
577 if (!this->load_and_apply_noise_psk_()) {
578 ESP_LOGW(TAG, "Failed to load saved PSK for activation");
579 return;
580 }
581 ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
582 for (auto &c : this->active_clients()) {
584 if (!c->send_message(req)) {
585 API_LOG_MSG_DROPPED(TAG, "Disconnect request");
586 }
587 }
588 });
589 }
590 return true;
591}
592
594 SavedNoisePsk saved{};
595 if (!this->noise_pref_.load(&saved))
596 return false;
597 this->set_noise_psk(saved.psk);
598 return true;
599}
600
601bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
602#ifdef USE_API_NOISE_PSK_FROM_YAML
603 // When PSK is set from YAML, this function should never be called
604 // but if it is, reject the change
605 ESP_LOGW(TAG, "Key set in YAML");
606 return false;
607#else
608 auto &old_psk = this->noise_ctx_.get_psk();
609 if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) {
610 ESP_LOGW(TAG, "New PSK matches old");
611 return true;
612 }
613
614 SavedNoisePsk new_saved_psk{psk};
615 bool result = this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"),
616 make_active);
617#ifdef USE_PROVISIONING
618 // The device now has a key; report provisioned so the provisioning window is
619 // satisfied and the reboot timeout resumes normal operation.
620 if (result && provisioning::global_provisioning_manager != nullptr) {
622 }
623#endif
624 return result;
625#endif
626}
627bool APIServer::clear_noise_psk(bool make_active) {
628#ifdef USE_API_NOISE_PSK_FROM_YAML
629 // When PSK is set from YAML, this function should never be called
630 // but if it is, reject the change
631 ESP_LOGW(TAG, "Key set in YAML");
632 return false;
633#else
634 SavedNoisePsk empty_psk{};
635 bool result = this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"),
636 make_active);
637#ifdef USE_PROVISIONING
638 // The key was cleared; report unprovisioned so a subsequent reboot reopens the
639 // provisioning window.
640 if (result && provisioning::global_provisioning_manager != nullptr) {
642 }
643#endif
644 return result;
645#endif
646}
647#endif
648
649#ifdef USE_HOMEASSISTANT_TIME
651 for (auto &client : this->active_clients()) {
652 if (!client->flags_.remove && client->is_authenticated()) {
653 client->send_time_request();
654 return; // Only request from one client to avoid clock conflicts
655 }
656 }
657}
658#endif
659
661 for (uint8_t i = 0; i < this->api_connection_count_; i++) {
662 if (this->clients_[i]->flags_.state_subscription) {
663 return true;
664 }
665 }
666 return false;
667}
668
669#ifdef USE_LOGGER
670void APIServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
671 if (this->shutting_down_) {
672 // Don't try to send logs during shutdown
673 // as it could result in a recursion and
674 // we would be filling a buffer we are trying to clear
675 return;
676 }
677 for (auto &c : this->active_clients()) {
678 if (!c->flags_.remove && c->get_log_subscription_level() >= level)
679 c->try_send_log_message(level, tag, message, message_len);
680 }
681}
682#endif
683
684#ifdef USE_CAMERA
685void APIServer::on_camera_image(const std::shared_ptr<camera::CameraImage> &image) {
686 for (auto &c : this->active_clients()) {
687 if (!c->flags_.remove)
688 c->set_camera_state(image);
689 }
690}
691#endif
692
694 this->shutting_down_ = true;
695
696 // Close the listening socket to prevent new connections
697 this->destroy_socket_();
698
699 // Change batch delay to 5ms for quick flushing during shutdown
700 this->batch_delay_ = 5;
701
702 // Send disconnect requests to all connected clients
703 for (auto &c : this->active_clients()) {
705 if (!c->send_message(req)) {
706 // If we can't send the disconnect request directly (tx_buffer full),
707 // schedule it at the front of the batch so it will be sent with priority
708 c->schedule_message_front_(nullptr, DisconnectRequest::MESSAGE_TYPE, DisconnectRequest::ESTIMATED_SIZE);
709 }
710 }
711}
712
714 // If network is disconnected, no point trying to flush buffers
715 if (!network::is_connected()) {
716 return true;
717 }
718 this->loop();
719
720 // Return true only when all clients have been torn down
721 return this->api_connection_count_ == 0;
722}
723
724#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
725// Timeout for action calls - matches aioesphomeapi client timeout (default 30s)
726// Can be overridden via USE_API_ACTION_CALL_TIMEOUT_MS define for testing
727#ifndef USE_API_ACTION_CALL_TIMEOUT_MS
728#define USE_API_ACTION_CALL_TIMEOUT_MS 30000 // NOLINT
729#endif
730
732 uint32_t action_call_id = this->next_action_call_id_++;
733 // Handle wraparound (skip 0 as it means "no call")
734 if (this->next_action_call_id_ == 0) {
735 this->next_action_call_id_ = 1;
736 }
737 this->active_action_calls_.push_back({action_call_id, client_call_id, conn});
738
739 // Schedule automatic cleanup after timeout (client will have given up by then)
740 // Uses numeric ID overload to avoid heap allocation from str_sprintf
741 this->set_timeout(action_call_id, USE_API_ACTION_CALL_TIMEOUT_MS, [this, action_call_id]() {
742 ESP_LOGD(TAG, "Action call %" PRIu32 " timed out", action_call_id);
743 this->unregister_active_action_call(action_call_id);
744 });
745
746 return action_call_id;
747}
748
750 // Cancel the timeout for this action call (uses numeric ID overload)
751 this->cancel_timeout(action_call_id);
752
753 // Swap-and-pop is more efficient than remove_if for unordered vectors
754 for (size_t i = 0; i < this->active_action_calls_.size(); i++) {
755 if (this->active_action_calls_[i].action_call_id == action_call_id) {
756 std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
757 this->active_action_calls_.pop_back();
758 return;
759 }
760 }
761}
762
764 // Remove all active action calls for disconnected connection using swap-and-pop
765 for (size_t i = 0; i < this->active_action_calls_.size();) {
766 if (this->active_action_calls_[i].connection == conn) {
767 // Cancel the timeout for this action call (uses numeric ID overload)
768 this->cancel_timeout(this->active_action_calls_[i].action_call_id);
769
770 std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
771 this->active_action_calls_.pop_back();
772 // Don't increment i - need to check the swapped element
773 } else {
774 i++;
775 }
776 }
777}
778
779void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message) {
780 for (auto &call : this->active_action_calls_) {
781 if (call.action_call_id == action_call_id) {
782 call.connection->send_execute_service_response(call.client_call_id, success, error_message);
783 return;
784 }
785 }
786 ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %" PRIu32, action_call_id);
787}
788#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
789void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message,
790 const uint8_t *response_data, size_t response_data_len) {
791 for (auto &call : this->active_action_calls_) {
792 if (call.action_call_id == action_call_id) {
793 call.connection->send_execute_service_response(call.client_call_id, success, error_message, response_data,
794 response_data_len);
795 return;
796 }
797 }
798 ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %" PRIu32, action_call_id);
799}
800#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
801#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
802
803} // namespace esphome::api
804#endif
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void mark_failed()
Mark this component as failed.
bool cancel_timeout(const char *name)
Cancel a timeout function.
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
void status_clear_warning()
Definition component.h:289
static void register_controller(Controller *controller)
Register a controller to receive entity state updates.
bool is_internal() const
Definition entity_base.h:89
StringRef is a reference to a string owned by something else.
Definition string_ref.h:26
void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE
Inform the parent automation that the event has triggered.
Definition automation.h:461
const psk_t & get_psk() const
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len)
bool is_connected_with_state_subscription() const
std::array< APIConnectionPtr, MAX_API_CONNECTIONS > clients_
Definition api_server.h:311
void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback)
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector< int32_t > *timings)
void add_state_subscription_(const char *entity_id, const char *attribute, std::function< void(StringRef)> &&f, bool once)
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function< void(StringRef)> &&f)
void on_camera_image(const std::shared_ptr< camera::CameraImage > &image) override
void socket_failed_(const LogString *msg)
void set_port(uint16_t port)
void dump_config() override
void unregister_active_action_calls_for_connection(APIConnection *conn)
void handle_disconnect(APIConnection *conn)
void set_batch_delay(uint16_t batch_delay)
void set_reboot_timeout(uint32_t reboot_timeout)
void send_action_response(uint32_t action_call_id, bool success, StringRef error_message)
bool save_noise_psk(psk_t psk, bool make_active=true)
void setup() override
bool teardown() override
APINoiseContext noise_ctx_
Definition api_server.h:357
void unregister_active_action_call(uint32_t action_call_id)
void send_homeassistant_action(const HomeassistantActionRequest &call)
socket::ListenSocket * socket_
Definition api_server.h:298
void on_event(event::Event *obj) override
void on_update(update::UpdateEntity *obj) override
std::vector< PendingActionResponse > action_response_callbacks_
Definition api_server.h:340
const std::vector< HomeAssistantStateSubscription > & get_state_subs() const
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function< void(StringRef)> &&f)
void handle_action_response(uint32_t call_id, bool success, StringRef error_message)
std::function< void(const class ActionResponse &)> ActionResponseCallback
Definition api_server.h:142
bool provisioning_pending_() const
Definition api_server.h:266
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, bool make_active)
ESPPreferenceObject noise_pref_
Definition api_server.h:358
Trigger< std::string, std::string > client_disconnected_trigger_
Definition api_server.h:303
std::vector< HomeAssistantStateSubscription > state_subs_
Definition api_server.h:319
bool clear_noise_psk(bool make_active=true)
ActiveClientsView active_clients() const
Definition api_server.h:209
uint16_t get_port() const
std::vector< ActiveActionCall > active_action_calls_
Definition api_server.h:331
void set_noise_psk(psk_t psk)
Definition api_server.h:78
float get_setup_priority() const override
uint32_t register_active_action_call(uint32_t client_call_id, APIConnection *conn)
void on_shutdown() override
void on_zwave_proxy_request(const ZWaveProxyRequest &msg)
static constexpr uint8_t MESSAGE_TYPE
Definition api_pb2.h:438
static constexpr uint8_t ESTIMATED_SIZE
Definition api_pb2.h:439
enums::DisconnectReason reason
Definition api_pb2.h:443
Base class for all binary_sensor-type classes.
virtual void add_listener(CameraListener *listener)=0
Add a listener to receive camera events.
static Camera * instance()
The singleton instance of the camera implementation.
Definition camera.cpp:18
ClimateDevice - This is the base class for all climate integrations.
Definition climate.h:187
Base class for all cover devices.
Definition cover.h:110
This class represents the communication layer between the front-end MQTT layer and the hardware outpu...
Definition light_state.h:93
Base class for all locks.
Definition lock.h:112
void add_log_callback(void *instance, void(*fn)(void *, uint8_t, const char *, const char *, size_t))
Register a log callback to receive log messages.
Definition logger.h:187
Base-class for all numbers.
Definition number.h:29
void set_source_provisioned(uint8_t source, bool provisioned)
Base-class for all selects.
Definition select.h:29
Base-class for all sensors.
Definition sensor.h:47
bool ready() const
Check if the socket has buffered data ready to read.
Definition socket.h:85
int bind(const struct sockaddr *addr, socklen_t addrlen)
int setsockopt(int level, int optname, const void *optval, socklen_t optlen)
std::unique_ptr< BSDSocketImpl > accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen)
Base class for all switches.
Definition switch.h:38
Base-class for all text inputs.
Definition text.h:21
Base class for all valve devices.
Definition valve.h:103
struct @66::@67 __attribute__
Wake the main loop task from an ISR. ISR-safe.
Definition main_task.h:32
const LogString * message
Definition component.cpp:35
uint16_t addr_len
bool state
Definition fan.h:2
uint32_t socklen_t
Definition headers.h:99
@ DISCONNECT_REASON_PROVISIONING_CLOSED
Definition api_pb2.h:16
APIServer * global_api_server
API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor) API_DISPATCH_UPDATE(cover
std::array< uint8_t, 32 > psk_t
Logger * global_logger
Definition logger.cpp:279
ESPHOME_ALWAYS_INLINE bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
Definition util.h:28
const char * get_use_address_to(std::span< char, USE_ADDRESS_BUFFER_SIZE > buf)
Get the active network address for logging.
Definition util.cpp:42
ProvisioningManager * global_provisioning_manager
constexpr float AFTER_WIFI
For components that should be initialized after WiFi is connected.
Definition component.h:55
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port)
Set a sockaddr to the any address and specified port for the IP version used by socket_ip().
Definition socket.cpp:194
std::unique_ptr< ListenSocket > socket_ip_loop_monitored(int type, int protocol)
Definition socket.cpp:130
const char * tag
Definition log.h:74
ESPPreferences * global_preferences
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
ESPPreferenceObject make_preference(size_t, uint32_t, bool)
Definition preferences.h:24
bool sync()
Commit pending writes to flash.
Definition preferences.h:33
std::unique_ptr< std::string > entity_id_dynamic_storage
Definition api_server.h:222
std::unique_ptr< std::string > attribute_dynamic_storage
Definition api_server.h:223
SemaphoreHandle_t lock