ESPHome 2026.11.0-dev
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"
9#include "esphome/core/hal.h"
10#include "esphome/core/log.h"
11#include "esphome/core/util.h"
13#ifdef USE_API_HOMEASSISTANT_SERVICES
15#endif
16
17#ifdef USE_LOGGER
19#endif
20
21#include <algorithm>
22#include <utility>
23
24namespace esphome::api {
25
26ESPHOME_LOG_TAG(TAG, "api");
27
28// APIServer
29APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
30
31#ifdef USE_API_NOISE
32static constexpr uint32_t NOISE_PSK_PREF_HASH = 88491486UL;
33#endif
34
35#if defined(USE_API_NOISE) && defined(USE_OTA_ENCRYPTION_PROVISIONED)
37 SavedNoisePsk saved;
38#ifdef USE_PREFERENCE_KEY_LOOKUP
39 const bool loaded =
40 global_preferences->load_from_key(NOISE_PSK_PREF_HASH, reinterpret_cast<uint8_t *>(&saved), sizeof(saved));
41#else
42 // Slot backends need the reservation walk; it only lands on the record when the reservations before
43 // it match a normal boot, otherwise the type checked checksum fails the load
44 const bool loaded = global_preferences->make_preference<SavedNoisePsk>(NOISE_PSK_PREF_HASH, true).load(&saved);
45#endif
46 // The all-zeros record means no key
47 if (!loaded || noise::NoiseContext::is_all_zeros(saved.psk))
48 return false;
49 out = saved.psk;
50 return true;
51}
52#endif
53
55
56void APIServer::socket_failed_(const LogString *msg) {
57 ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
58 this->destroy_socket_();
59#ifdef USE_API_OUTGOING_CONNECTION
60 // Dial-out needs no listener; degrade instead of stopping the component
61 this->status_set_error(LOG_STR("listen socket failed"));
62#else
63 this->mark_failed();
64#endif
65}
66
68#if defined(USE_API_HOMEASSISTANT_STATES) && defined(HOMEASSISTANT_STATE_SUBSCRIPTION_COUNT)
69 this->state_subs_.reserve(HOMEASSISTANT_STATE_SUBSCRIPTION_COUNT);
70#endif
71#ifdef USE_API_NOISE
72 // Always reserve the slot: flash preferences are positional on esp8266, so
73 // a yaml key build must keep the layout of a runtime key build
74 this->noise_pref_ = global_preferences->make_preference<SavedNoisePsk>(NOISE_PSK_PREF_HASH, true);
75#ifndef USE_API_NOISE_PSK_FROM_YAML
76 // A cleared record loads fine but holds no key
77 if (this->load_and_apply_noise_psk_() && this->noise_ctx_.has_psk()) {
78 ESP_LOGD(TAG, "Loaded saved Noise PSK");
79 }
80#endif
81#endif
82
83#ifdef USE_LOGGER
84 if (logger::global_logger != nullptr) {
86 this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) {
87 static_cast<APIServer *>(self)->on_log(level, tag, message, message_len);
88 });
89 }
90#endif
91
92#ifdef USE_CAMERA
93 if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) {
95 }
96#endif
97
98 // Initialize last_connected_ for reboot timeout tracking
100#if defined(USE_PROVISIONING) && defined(USE_API_NOISE)
101 // Register with the provisioning manager (provisioning:) as a source and
102 // report our current state (provisioned == an encryption key is set). When the
103 // window closes, disconnect any client still attempting to provision so it learns
104 // the reason. The manager owns the timeout, window state and on_timeout automation.
108 this->noise_ctx_.has_psk());
110 for (auto &c : this->active_clients()) {
113 // Best-effort: if the send buffer is full the reason is dropped, but the
114 // client still learns the window is closed when it reconnects (rejected at
115 // hello) or via the socket close.
116 if (!c->send_message(req)) {
117 API_LOG_MSG_DROPPED(TAG, "Disconnect request");
118 }
119 }
120 });
121 }
122#endif
123 // Set warning status if reboot timeout is enabled (suppressed while provisioning
124 // is pending so the device waits to be onboarded instead of rebooting).
125 if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
126 this->status_set_warning(LOG_STR("waiting for client connection"));
127 }
128#ifdef USE_API_OUTGOING_CONNECTION
129 this->outgoing_conn_.setup();
130#endif
131
132 // Listener last: on failure socket_failed_() returns early, and an
133 // outgoing_connection build keeps dialing out without one
134 this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
135 if (this->socket_ == nullptr) {
136 this->socket_failed_(LOG_STR("creation"));
137 return;
138 }
139 int enable = 1;
140 int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
141 if (err != 0) {
142 ESP_LOGW(TAG, "Socket reuseaddr: errno %d", errno);
143 // we can still continue
144 }
145 err = this->socket_->setblocking(false);
146 if (err != 0) {
147 this->socket_failed_(LOG_STR("nonblocking"));
148 return;
149 }
150
151 struct sockaddr_storage server;
152
153 socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_);
154 if (sl == 0) {
155 this->socket_failed_(LOG_STR("set sockaddr"));
156 return;
157 }
158
159 err = this->socket_->bind((struct sockaddr *) &server, sl);
160 if (err != 0) {
161 this->socket_failed_(LOG_STR("bind"));
162 return;
163 }
164
165 err = this->socket_->listen(this->listen_backlog_);
166 if (err != 0) {
167 this->socket_failed_(LOG_STR("listen"));
168 }
169}
170
172 // Accept new clients only if the socket exists and has incoming connections
173 if (this->socket_ && this->socket_->ready()) {
174 this->accept_new_connections_();
175 }
176
177 const bool connected = network::is_connected();
178#ifdef USE_NOISE_SPARE_EPHEMERAL
179 if (connected && !noise::has_spare_ephemeral()) {
181 }
182#endif
183
184#ifdef USE_API_OUTGOING_CONNECTION
185 if (!this->shutting_down_) {
186 this->outgoing_conn_.loop(this);
187 }
188#endif
189
190 if (this->api_connection_count_ == 0) {
191 // Check reboot timeout - done in loop to avoid scheduler heap churn
192 // (cancelled scheduler items sit in heap memory until their scheduled time).
193 // Suppressed while a provisioning window is pending so the device waits to be
194 // onboarded / reset instead of rebooting itself; resumes once provisioned.
195 if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
197 if (now - this->last_connected_ > this->reboot_timeout_) {
198 ESP_LOGE(TAG, "No clients; rebooting");
199 App.reboot();
200 }
201 }
202 return;
203 }
204
205 // Process clients and remove disconnected ones in a single pass
206 if (!connected) {
207 // Network is down - disconnect all clients
208 for (auto &client : this->active_clients()) {
209 client->on_fatal_error();
210 client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect"));
211 }
212 // Continue to process and clean up the clients below
213 }
214
215 uint8_t client_index = 0;
216 while (client_index < this->api_connection_count_) {
217 auto &client = this->clients_[client_index];
218
219 // Common case: process active client
220 if (!client->flags_.remove) {
221 client->loop();
222 }
223 // Handle disconnection promptly - close socket to free LWIP PCB
224 // resources and prevent retransmit crashes on ESP8266.
225 if (client->flags_.remove) {
226 // Rare case: handle disconnection (don't increment - swapped element needs processing)
227 this->remove_client_(client_index);
228 } else {
229 client_index++;
230 }
231 }
232}
233
234#ifdef USE_NOISE_SPARE_EPHEMERAL
235// An OTA handshake is not visible here and just pays the refill it triggered
238 for (auto &client : this->active_clients()) {
239 if (client->is_still_connecting(now)) {
240 return;
241 }
242 }
243 // Generating the key pair cannot be split across loop passes
246}
247#endif
248
249void APIServer::remove_client_(uint8_t client_index) {
250 auto &client = this->clients_[client_index];
251
252#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
254#endif
255 ESP_LOGV(TAG, "Remove connection %s", client->get_name());
256
257#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
258 // Save client info before closing socket and removal for the trigger
259 char peername_buf[socket::SOCKADDR_STR_LEN];
260 std::string client_name(client->get_name());
261 std::string client_peername(client->get_peername_to(peername_buf));
262#endif
263
264 // Read before the swap-and-reset below destroys the connection
265 const bool was_authenticated = client->is_authenticated();
266#ifdef USE_API_OUTGOING_CONNECTION
267 if (client->flags_.outgoing_connection_target) {
269 }
270 this->outgoing_conn_.on_client_removed(client.get(), was_authenticated);
271#endif
272
273 // Close socket now (was deferred from on_fatal_error to allow getpeername)
274 client->helper_->close();
275
276 // Swap-and-reset: move the removed client to the trailing slot and null it out so slots
277 // [api_connection_count_, N) remain nullptr.
278 const uint8_t last_index = this->api_connection_count_ - 1;
279 if (client_index < last_index) {
280 std::swap(this->clients_[client_index], this->clients_[last_index]);
281 }
282 // Drop the count before resetting the slot. reset() runs ~APIConnection(), which can reenter the
283 // server (e.g. voice_assistant unsubscribes in its disconnect trigger, publishing entity state ->
284 // on_*_update iterating active_clients()). Excluding the dying slot from the active range first
285 // keeps that reentrant iteration from dereferencing the now-null slot.
286 this->api_connection_count_--;
287 this->clients_[last_index].reset();
288
289 // Last client disconnected - set warning and start tracking for reboot timeout
290 // (suppressed while provisioning is pending - see loop()).
291 // Refresh on every authenticated removal, not just the last one, so an
292 // unauthenticated straggler removed later (e.g. a port scan, or a dial to
293 // a host that accepts TCP but never speaks the API) cannot discard a
294 // healthy session's timestamp and trigger a spurious reboot
295 if (was_authenticated) {
297 }
298 if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
299 this->status_set_warning(LOG_STR("waiting for client connection"));
300 }
301
302#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
303 // Fire trigger after client is removed so api.connected reflects the true state
304 this->client_disconnected_trigger_.trigger(client_name, client_peername);
305#endif
306}
307
308void __attribute__((flatten)) APIServer::accept_new_connections_() {
309 while (true) {
310 struct sockaddr_storage source_addr;
311 socklen_t addr_len = sizeof(source_addr);
312
313 auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
314 if (!sock)
315 break;
316
317 char peername[socket::SOCKADDR_STR_LEN];
318 sock->getpeername_to(peername);
319
320 // Check if we're at the connection limit
321 if (this->at_client_limit_()) {
322 ESP_LOGW(TAG, "Max connections (%d), rejecting %s", MAX_API_CONNECTIONS, peername);
323 // Immediately close - socket destructor will handle cleanup
324 sock.reset();
325 continue;
326 }
327
328 ESP_LOGD(TAG, "Accept %s", peername);
329
330 this->add_client_(std::move(sock));
331 }
332}
333
334APIConnection *APIServer::add_client_(std::unique_ptr<socket::Socket> sock) {
335 auto *conn = new APIConnection(std::move(sock), this); // NOLINT(cppcoreguidelines-owning-memory)
336 this->clients_[this->api_connection_count_++].reset(conn);
337 conn->start();
338
339 // First client connected - clear warning. The reboot watchdog timestamp is
340 // refreshed when an authenticated client is removed (see remove_client_),
341 // never on bare TCP connects.
342 if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
343 this->status_clear_warning();
344 }
345 return conn;
346}
347
348#ifdef USE_API_OUTGOING_CONNECTION
349APIConnection *APIServer::add_outgoing_client_(std::unique_ptr<socket::Socket> sock) {
350 // Re-check at the handoff: inbound clients may have taken the last slot and
351 // the PSK may have been cleared since the dial started (mark_outgoing()
352 // needs the noise helper)
353 const bool at_limit = this->at_client_limit_();
354 if (at_limit || !this->noise_ctx_.has_psk()) {
355 ESP_LOGW(TAG, "Dropping outgoing connection (%s)",
356 at_limit ? LOG_STR_LITERAL("max connections") : LOG_STR_LITERAL("no key"));
357 return nullptr;
358 }
359 auto *conn = this->add_client_(std::move(sock));
360 // After start(): sends our server hello first so the peer can pick the key
361 conn->mark_outgoing();
362 return conn;
363}
364
369#endif
370
372 char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
373 ESP_LOGCONFIG(TAG,
374 "Server:\n"
375 " Address: %s:%u\n"
376 " Listen backlog: %u\n"
377 " Max connections: %u",
378 network::get_use_address_to(addr_buf), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
379#ifdef USE_API_NOISE
380 ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
381 if (!this->noise_ctx_.has_psk()) {
382 ESP_LOGCONFIG(TAG, " Supports encryption: YES");
383 }
384#else
385 ESP_LOGCONFIG(TAG, " Noise encryption: NO");
386#endif
387#ifdef USE_API_OUTGOING_CONNECTION
389#endif
390}
391
393
394// Macro for controller update dispatch
395#define API_DISPATCH_UPDATE(entity_type, entity_name) \
396 void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
397 if (obj->is_internal()) \
398 return; \
399 for (auto &c : this->active_clients()) { \
400 if (c->flags_.state_subscription) \
401 c->send_##entity_name##_state(obj); \
402 } \
403 }
404
405#ifdef USE_BINARY_SENSOR
407#endif
408
409#ifdef USE_COVER
411#endif
412
413#ifdef USE_FAN
415#endif
416
417#ifdef USE_LIGHT
419#endif
420
421#ifdef USE_SENSOR
423#endif
424
425#ifdef USE_SWITCH
427#endif
428
429#ifdef USE_TEXT_SENSOR
431#endif
432
433#ifdef USE_CLIMATE
435#endif
436
437#ifdef USE_NUMBER
439#endif
440
441#ifdef USE_DATETIME_DATE
443#endif
444
445#ifdef USE_DATETIME_TIME
447#endif
448
449#ifdef USE_DATETIME_DATETIME
451#endif
452
453#ifdef USE_TEXT
455#endif
456
457#ifdef USE_SELECT
459#endif
460
461#ifdef USE_LOCK
463#endif
464
465#ifdef USE_VALVE
467#endif
468
469#ifdef USE_MEDIA_PLAYER
471#endif
472
473#ifdef USE_WATER_HEATER
475#endif
476
477#ifdef USE_EVENT
479 if (obj->is_internal())
480 return;
481 for (auto &c : this->active_clients()) {
482 if (c->flags_.state_subscription)
483 c->send_event(obj);
484 }
485}
486#endif
487
488#ifdef USE_UPDATE
489// Update is a special case - the method is called on_update, not on_update_update
491 if (obj->is_internal())
492 return;
493 for (auto &c : this->active_clients()) {
494 if (c->flags_.state_subscription)
495 c->send_update_state(obj);
496 }
497}
498#endif
499
500#ifdef USE_ZWAVE_PROXY
502 // We could add code to manage a second subscription type, but, since this message type is
503 // very infrequent and small, we simply send it to all clients
504 for (auto &c : this->active_clients()) {
505 if (!c->send_message(msg)) {
506 API_LOG_MSG_DROPPED(TAG, "Home ID notification");
507 }
508 }
509}
510#endif
511
512#ifdef USE_SERIAL_PROXY_USB_IDENTITY
514 for (auto &c : this->active_clients()) {
515 if (c->flags_.serial_proxy_identity_subscription)
516 c->send_serial_proxy_identity(msg);
517 }
518}
519#endif
520
521#ifdef USE_IR_RF
523 const std::vector<int32_t> *timings) {
525#ifdef USE_DEVICES
526 resp.device_id = device_id;
527#endif
528 resp.key = key;
529 resp.timings = timings;
530
531 for (auto &c : this->active_clients())
532 c->send_infrared_rf_receive_event(resp);
533}
534
535#endif
536
537#ifdef USE_ALARM_CONTROL_PANEL
539#endif
540
541#ifdef USE_API_HOMEASSISTANT_SERVICES
543 bool has_subscriber = false;
544 for (auto &client : this->active_clients()) {
545 has_subscriber |= client->send_homeassistant_action(call);
546 }
547 if (!has_subscriber) {
548 // Home Assistant subscribes to actions shortly *after* authenticating, so actions
549 // fired right at connection time (on_client_connected, on_time_sync, ...) can
550 // arrive before the subscription and are lost - warn instead of failing silently.
551 ESP_LOGW(TAG, "Home Assistant %s '%.*s' dropped; %s",
552 call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"),
553 static_cast<int>(call.service.size()), call.service.empty() ? "" : call.service.progmem_ptr(),
554 this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)")
555 : LOG_STR_LITERAL("no client connected"));
556 }
557}
558#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
560 this->action_response_callbacks_.push_back({call_id, std::move(callback)});
561}
562
563void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef error_message) {
564 for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
565 if (it->call_id == call_id) {
566 auto callback = std::move(it->callback);
567 this->action_response_callbacks_.erase(it);
568 ActionResponse response(success, error_message);
569 callback(response);
570 return;
571 }
572 }
573}
574#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
575void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef error_message,
576 const uint8_t *response_data, size_t response_data_len) {
577 for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
578 if (it->call_id == call_id) {
579 auto callback = std::move(it->callback);
580 this->action_response_callbacks_.erase(it);
581 ActionResponse response(success, error_message, response_data, response_data_len);
582 callback(response);
583 return;
584 }
585 }
586}
587#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
588#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
589#endif // USE_API_HOMEASSISTANT_SERVICES
590
591#ifdef USE_API_HOMEASSISTANT_STATES
592// Helper to add subscription (reduces duplication)
593void APIServer::add_state_subscription_(const char *entity_id, const char *attribute,
594 std::function<void(StringRef)> &&f, bool once) {
596 .entity_id = entity_id,
597 .attribute = attribute,
598 .callback = std::move(f),
599 .once = once,
600 // entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation)
601 });
602}
603
604// Helper to add subscription with heap-allocated strings (reduces duplication)
605void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
606 std::function<void(StringRef)> &&f, bool once) {
608 // Allocate heap storage for the strings
609 sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id));
610 sub.entity_id = sub.entity_id_dynamic_storage->c_str();
611
612 if (attribute.has_value()) {
613 sub.attribute_dynamic_storage = std::make_unique<std::string>(std::move(attribute.value()));
614 sub.attribute = sub.attribute_dynamic_storage->c_str();
615 } else {
616 sub.attribute = nullptr;
617 }
618
619 sub.callback = std::move(f);
620 sub.once = once;
621 this->state_subs_.push_back(std::move(sub));
622}
623
624// New const char* overload (for internal components - zero allocation)
625void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute,
626 std::function<void(StringRef)> &&f) {
627 this->add_state_subscription_(entity_id, attribute, std::move(f), false);
628}
629
630void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute,
631 std::function<void(StringRef)> &&f) {
632 this->add_state_subscription_(entity_id, attribute, std::move(f), true);
633}
634
635// std::string overload with StringRef callback (zero-allocation callback)
636void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
637 std::function<void(StringRef)> &&f) {
638 this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
639}
640
641void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
642 std::function<void(StringRef)> &&f) {
643 this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
644}
645
646// Legacy helper: wraps std::string callback and delegates to StringRef version
647void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
648 std::function<void(const std::string &)> &&f, bool once) {
649 // Wrap callback to convert StringRef -> std::string, then delegate
650 this->add_state_subscription_(std::move(entity_id), std::move(attribute),
651 std::function<void(StringRef)>([f = std::move(f)](StringRef state) { f(state.str()); }),
652 once);
653}
654
655// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string)
656void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
657 std::function<void(const std::string &)> &&f) {
658 this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
659}
660
661void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
662 std::function<void(const std::string &)> &&f) {
663 this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
664}
665
666const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
667 return this->state_subs_;
668}
669#endif
670
671#ifdef USE_API_NOISE
672#ifndef USE_API_NOISE_PSK_FROM_YAML
673bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
674 const LogString *fail_log_msg, bool make_active) {
675 if (!this->noise_pref_.save(&new_psk)) {
676 ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg));
677 return false;
678 }
679 // ensure it's written immediately
680 if (!global_preferences->sync()) {
681 ESP_LOGW(TAG, "Failed to sync preferences");
682 return false;
683 }
684 ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg));
685 if (make_active) {
686 this->set_timeout(100, [this]() {
687 // Re-read the PSK from preferences rather than capturing the 32-byte array
688 // in the lambda (which would exceed std::function SBO and heap-allocate).
689 if (!this->load_and_apply_noise_psk_()) {
690 ESP_LOGW(TAG, "Failed to load saved PSK for activation");
691 return;
692 }
693 ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
694 for (auto &c : this->active_clients()) {
696 if (!c->send_message(req)) {
697 API_LOG_MSG_DROPPED(TAG, "Disconnect request");
698 }
699 // Force it: a session from before the key was active must not survive
700 c->flags_.next_close = true;
701 }
702 });
703 }
704 return true;
705}
706
708 // Load into a temp so a failed read cannot disturb the key in use
709 SavedNoisePsk loaded{};
710 if (!this->noise_pref_.load(&loaded))
711 return false;
712 this->saved_psk_ = loaded;
713 // An unprovisioned device stores the reserved all-zeros key, which is no key
714 const bool has_key = !noise::NoiseContext::is_all_zeros(this->saved_psk_.psk);
715 this->noise_ctx_.set_psk(has_key ? this->saved_psk_.psk.data() : nullptr);
716 return true;
717}
718
719bool APIServer::save_noise_psk(noise::psk_t psk, bool make_active) {
720 if (this->saved_psk_.psk == psk) {
721 ESP_LOGW(TAG, "New PSK matches old");
722 return true;
723 }
724
725 SavedNoisePsk new_saved_psk{psk};
726 bool result = this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"),
727 make_active);
728#ifdef USE_PROVISIONING
729 // The device now has a key; report provisioned so the provisioning window is
730 // satisfied and the reboot timeout resumes normal operation.
731 if (result && provisioning::global_provisioning_manager != nullptr) {
733 }
734#endif
735 return result;
736}
737bool APIServer::clear_noise_psk(bool make_active) {
738 SavedNoisePsk empty_psk{};
739 bool result = this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"),
740 make_active);
741#ifdef USE_PROVISIONING
742 // The key was cleared; report unprovisioned so a subsequent reboot reopens the
743 // provisioning window.
744 if (result && provisioning::global_provisioning_manager != nullptr) {
746 }
747#endif
748 return result;
749}
750#endif // USE_API_NOISE_PSK_FROM_YAML
751#endif
752
753#ifdef USE_HOMEASSISTANT_TIME
755 for (auto &client : this->active_clients()) {
756 if (!client->flags_.remove && client->is_authenticated()) {
757 client->send_time_request();
758 return; // Only request from one client to avoid clock conflicts
759 }
760 }
761}
762#endif
763
765 for (uint8_t i = 0; i < this->api_connection_count_; i++) {
766 if (this->clients_[i]->flags_.state_subscription) {
767 return true;
768 }
769 }
770 return false;
771}
772
773#ifdef USE_LOGGER
774void APIServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
775 if (this->shutting_down_) {
776 // Don't try to send logs during shutdown
777 // as it could result in a recursion and
778 // we would be filling a buffer we are trying to clear
779 return;
780 }
781 for (auto &c : this->active_clients()) {
782 if (!c->flags_.remove && c->get_log_subscription_level() >= level)
783 c->try_send_log_message(level, tag, message, message_len);
784 }
785}
786#endif
787
788#ifdef USE_CAMERA
789void APIServer::on_camera_image(const std::shared_ptr<camera::CameraImage> &image) {
790 for (auto &c : this->active_clients()) {
791 if (!c->flags_.remove)
792 c->set_camera_state(image);
793 }
794}
795#endif
796
798 this->shutting_down_ = true;
799
800 // Close the listening socket to prevent new connections
801 this->destroy_socket_();
802#ifdef USE_API_OUTGOING_CONNECTION
804#endif
805
806 // Change batch delay to 5ms for quick flushing during shutdown
807 this->batch_delay_ = 5;
808
809 // Send disconnect requests to all connected clients
810 for (auto &c : this->active_clients()) {
812 if (!c->send_message(req)) {
813 // If we can't send the disconnect request directly (tx_buffer full),
814 // schedule it at the front of the batch so it will be sent with priority
815 c->schedule_message_front_(nullptr, DisconnectRequest::MESSAGE_TYPE, DisconnectRequest::ESTIMATED_SIZE);
816 }
817 }
818}
819
821 // If network is disconnected, no point trying to flush buffers
822 if (!network::is_connected()) {
823 return true;
824 }
825 this->loop();
826
827 // Return true only when all clients have been torn down
828 return this->api_connection_count_ == 0;
829}
830
831#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
832// Timeout for action calls - matches aioesphomeapi client timeout (default 30s)
833// Can be overridden via USE_API_ACTION_CALL_TIMEOUT_MS define for testing
834#ifndef USE_API_ACTION_CALL_TIMEOUT_MS
835#define USE_API_ACTION_CALL_TIMEOUT_MS 30000 // NOLINT
836#endif
837
839 uint32_t action_call_id = this->next_action_call_id_++;
840 // Handle wraparound (skip 0 as it means "no call")
841 if (this->next_action_call_id_ == 0) {
842 this->next_action_call_id_ = 1;
843 }
844 this->active_action_calls_.push_back({action_call_id, client_call_id, conn});
845
846 // Schedule automatic cleanup after timeout (client will have given up by then)
847 // Uses numeric ID overload to avoid heap allocation from str_sprintf
848 this->set_timeout(action_call_id, USE_API_ACTION_CALL_TIMEOUT_MS, [this, action_call_id]() {
849 ESP_LOGD(TAG, "Action call %" PRIu32 " timed out", action_call_id);
850 this->unregister_active_action_call(action_call_id);
851 });
852
853 return action_call_id;
854}
855
857 // Cancel the timeout for this action call (uses numeric ID overload)
858 this->cancel_timeout(action_call_id);
859
860 // Swap-and-pop is more efficient than remove_if for unordered vectors
861 for (size_t i = 0; i < this->active_action_calls_.size(); i++) {
862 if (this->active_action_calls_[i].action_call_id == action_call_id) {
863 std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
864 this->active_action_calls_.pop_back();
865 return;
866 }
867 }
868}
869
871 // Remove all active action calls for disconnected connection using swap-and-pop
872 for (size_t i = 0; i < this->active_action_calls_.size();) {
873 if (this->active_action_calls_[i].connection == conn) {
874 // Cancel the timeout for this action call (uses numeric ID overload)
875 this->cancel_timeout(this->active_action_calls_[i].action_call_id);
876
877 std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
878 this->active_action_calls_.pop_back();
879 // Don't increment i - need to check the swapped element
880 } else {
881 i++;
882 }
883 }
884}
885
886void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message) {
887 for (auto &call : this->active_action_calls_) {
888 if (call.action_call_id == action_call_id) {
889 call.connection->send_execute_service_response(call.client_call_id, success, error_message);
890 return;
891 }
892 }
893 ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %" PRIu32, action_call_id);
894}
895#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
896void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message,
897 const uint8_t *response_data, size_t response_data_len) {
898 for (auto &call : this->active_action_calls_) {
899 if (call.action_call_id == action_call_id) {
900 call.connection->send_execute_service_response(call.client_call_id, success, error_message, response_data,
901 response_data_len);
902 return;
903 }
904 }
905 ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %" PRIu32, action_call_id);
906}
907#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
908#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
909
910} // namespace esphome::api
911#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:296
bool is_internal() const
StringRef is a reference to a string owned by something else.
Definition string_ref.h:31
void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE
Inform the parent automation that the event has triggered.
Definition automation.h:556
Leaves a stretch of the current loop pass out of the blocking warning.
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len)
bool is_connected_with_state_subscription() const
OutgoingConnectionManager outgoing_conn_
Definition api_server.h:407
std::array< APIConnectionPtr, MAX_API_CONNECTIONS > clients_
Definition api_server.h:349
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 on_update(update::UpdateEntity *obj)
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)
bool at_client_limit_() const
Definition api_server.h:285
void on_camera_image(const std::shared_ptr< camera::CameraImage > &image) override
void socket_failed_(const LogString *msg)
Log the failure, drop the listen socket, and mark the component failed unless this build can still di...
void dump_config() override
void unregister_active_action_calls_for_connection(APIConnection *conn)
void handle_disconnect(APIConnection *conn)
void send_action_response(uint32_t action_call_id, bool success, StringRef error_message)
void setup() override
bool teardown() override
void unregister_active_action_call(uint32_t action_call_id)
void send_homeassistant_action(const HomeassistantActionRequest &call)
socket::ListenSocket * socket_
Definition api_server.h:336
std::vector< PendingActionResponse > action_response_callbacks_
Definition api_server.h:378
const std::vector< HomeAssistantStateSubscription > & get_state_subs() const
void send_serial_proxy_identity(const SerialProxyIdentity &msg)
Tell every subscribed client that a serial proxy port's identity changed.
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:159
void on_event(event::Event *obj)
bool provisioning_pending_() const
Definition api_server.h:300
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:404
SavedNoisePsk saved_psk_
Definition api_server.h:402
Trigger< std::string, std::string > client_disconnected_trigger_
Definition api_server.h:341
std::vector< HomeAssistantStateSubscription > state_subs_
Definition api_server.h:357
bool clear_noise_psk(bool make_active=true)
ActiveClientsView active_clients() const
Definition api_server.h:233
void on_outgoing_target_client(APIConnection *conn)
bool save_noise_psk(noise::psk_t psk, bool make_active=true)
APIConnection * add_client_(std::unique_ptr< socket::Socket > sock)
Takes the socket into a new connection and starts it; callers must have checked at_client_limit_() fi...
std::vector< ActiveActionCall > active_action_calls_
Definition api_server.h:369
APIConnection * add_outgoing_client_(std::unique_ptr< socket::Socket > sock)
uint32_t register_active_action_call(uint32_t client_call_id, APIConnection *conn)
noise::NoiseContext noise_ctx_
Definition api_server.h:400
void on_shutdown() override
void on_zwave_proxy_request(const ZWaveProxyRequest &msg)
static constexpr uint16_t MESSAGE_TYPE
Definition api_pb2.h:489
static constexpr uint8_t ESTIMATED_SIZE
Definition api_pb2.h:490
enums::DisconnectReason reason
Definition api_pb2.h:494
void on_client_removed(APIConnection *conn, bool was_authenticated)
Clears the dialed-connection gate; dying unauthenticated escalates the backoff.
void on_target_client(APIConnection *conn)
A key-verified client declared itself a dial-back target; last one wins.
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:192
Base class for all cover devices.
Definition cover.h:115
This class represents the communication layer between the front-end MQTT layer and the hardware outpu...
Definition light_state.h:68
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:194
void set_psk(const uint8_t *psk)
psk points at 32 bytes that outlive the context (PROGMEM or caller owned RAM); nullptr means no key.
Definition noise.h:32
static bool is_all_zeros(const psk_t &psk)
Definition noise.h:22
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:28
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:106
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:43
Base-class for all text inputs.
Definition text.h:20
Base class for all valve devices.
Definition valve.h:102
struct @66::@67 __attribute__
Wake the main loop task from an ISR. ISR-safe.
Definition main_task.h:45
const LogString * message
Definition component.cpp:35
uint16_t addr_len
const char * key
bool state
Definition fan.h:2
std::vector< uint8_t > & out
Definition gif.cpp:49
uint32_t socklen_t
Definition headers.h:109
@ DISCONNECT_REASON_PROVISIONING_CLOSED
Definition api_pb2.h:20
APIServer * global_api_server
ESPHOME_LOG_TAG(TAG, "api.connection")
API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor) API_DISPATCH_UPDATE(cover
bool load_saved_noise_psk(noise::psk_t &out)
One-shot read of the provisioned key for a boot without an api server (safe mode); false when there i...
Logger * global_logger
Definition logger.cpp:276
ESPHOME_ALWAYS_INLINE bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
Definition util.h:36
const char * get_use_address_to(std::span< char, USE_ADDRESS_BUFFER_SIZE > buf)
Get the active network address for logging.
Definition util.cpp:72
std::array< uint8_t, 32 > psk_t
Definition noise.h:14
bool has_spare_ephemeral()
Definition noise.h:52
void prepare_spare_ephemeral()
Fill the slot; blocks for the base point multiply.
Definition noise.cpp:31
ProvisioningManager * global_provisioning_manager
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().
std::unique_ptr< ListenSocket > socket_ip_loop_monitored(int type, int protocol)
Definition socket.h:175
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 load_from_key(uint32_t, uint8_t *, size_t)
Definition preferences.h:26
bool sync()
Commit pending writes to flash.
Definition preferences.h:33
std::unique_ptr< std::string > entity_id_dynamic_storage
Definition api_server.h:246
std::unique_ptr< std::string > attribute_dynamic_storage
Definition api_server.h:247
SemaphoreHandle_t lock