mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 15:27:33 +00:00
NoiseResponderHandshake::init() now hands the slot straight to noise-c and wipes it, so the api and ota call sites are unchanged, nothing copies the key pair and no transport has to remember the wipe. The slot compiles under USE_NOISE_SPARE_EPHEMERAL, which the api component enables as the refiller, instead of the noise component keying on an api define. The per tick check sits in loop() with the refill out of line, and the grace predicate lives next to the handshake timeout it mirrors.
816 lines
29 KiB
C++
816 lines
29 KiB
C++
#include "api_server.h"
|
|
#ifdef USE_API
|
|
#include <cerrno>
|
|
#include <cinttypes>
|
|
#include "api_connection.h"
|
|
#include "esphome/components/network/util.h"
|
|
#include "esphome/core/application.h"
|
|
#include "esphome/core/controller_registry.h"
|
|
#include "esphome/core/defines.h"
|
|
#include "esphome/core/hal.h"
|
|
#include "esphome/core/log.h"
|
|
#include "esphome/core/util.h"
|
|
#include "esphome/core/version.h"
|
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
|
#include "homeassistant_service.h"
|
|
#endif
|
|
|
|
#ifdef USE_LOGGER
|
|
#include "esphome/components/logger/logger.h"
|
|
#endif
|
|
|
|
#include <algorithm>
|
|
#include <utility>
|
|
|
|
namespace esphome::api {
|
|
|
|
static const char *const TAG = "api";
|
|
|
|
// APIServer
|
|
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
|
|
|
APIServer::APIServer() { global_api_server = this; }
|
|
|
|
void APIServer::socket_failed_(const LogString *msg) {
|
|
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
|
|
this->destroy_socket_();
|
|
this->mark_failed();
|
|
}
|
|
|
|
void APIServer::setup() {
|
|
ControllerRegistry::register_controller(this);
|
|
|
|
#ifdef USE_API_NOISE
|
|
#ifdef USE_ESP8266
|
|
// The spare ephemeral refill blocks ~60 ms here and shares the pass with
|
|
// the client loops; keep the whole pass under the blocking warning
|
|
this->warn_if_blocking_over_ = 10; // centiseconds
|
|
#endif
|
|
// Always reserve the slot: flash preferences are positional on esp8266, so
|
|
// a yaml key build must keep the layout of a runtime key build
|
|
uint32_t hash = 88491486UL;
|
|
this->noise_pref_ = global_preferences->make_preference<SavedNoisePsk>(hash, true);
|
|
#ifndef USE_API_NOISE_PSK_FROM_YAML
|
|
// A cleared record loads fine but holds no key
|
|
if (this->load_and_apply_noise_psk_() && this->noise_ctx_.has_psk()) {
|
|
ESP_LOGD(TAG, "Loaded saved Noise PSK");
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
|
|
if (this->socket_ == nullptr) {
|
|
this->socket_failed_(LOG_STR("creation"));
|
|
return;
|
|
}
|
|
int enable = 1;
|
|
int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
|
|
if (err != 0) {
|
|
ESP_LOGW(TAG, "Socket reuseaddr: errno %d", errno);
|
|
// we can still continue
|
|
}
|
|
err = this->socket_->setblocking(false);
|
|
if (err != 0) {
|
|
this->socket_failed_(LOG_STR("nonblocking"));
|
|
return;
|
|
}
|
|
|
|
struct sockaddr_storage server;
|
|
|
|
socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_);
|
|
if (sl == 0) {
|
|
this->socket_failed_(LOG_STR("set sockaddr"));
|
|
return;
|
|
}
|
|
|
|
err = this->socket_->bind((struct sockaddr *) &server, sl);
|
|
if (err != 0) {
|
|
this->socket_failed_(LOG_STR("bind"));
|
|
return;
|
|
}
|
|
|
|
err = this->socket_->listen(this->listen_backlog_);
|
|
if (err != 0) {
|
|
this->socket_failed_(LOG_STR("listen"));
|
|
return;
|
|
}
|
|
|
|
#ifdef USE_LOGGER
|
|
if (logger::global_logger != nullptr) {
|
|
logger::global_logger->add_log_callback(
|
|
this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) {
|
|
static_cast<APIServer *>(self)->on_log(level, tag, message, message_len);
|
|
});
|
|
}
|
|
#endif
|
|
|
|
#ifdef USE_CAMERA
|
|
if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) {
|
|
camera::Camera::instance()->add_listener(this);
|
|
}
|
|
#endif
|
|
|
|
// Initialize last_connected_ for reboot timeout tracking
|
|
this->last_connected_ = App.get_loop_component_start_time();
|
|
#if defined(USE_PROVISIONING) && defined(USE_API_NOISE)
|
|
// Register with the provisioning manager (provisioning:) as a source and
|
|
// report our current state (provisioned == an encryption key is set). When the
|
|
// window closes, disconnect any client still attempting to provision so it learns
|
|
// the reason. The manager owns the timeout, window state and on_timeout automation.
|
|
if (provisioning::global_provisioning_manager != nullptr) {
|
|
this->provisioning_source_ = provisioning::global_provisioning_manager->register_source();
|
|
provisioning::global_provisioning_manager->set_source_provisioned(this->provisioning_source_,
|
|
this->noise_ctx_.has_psk());
|
|
provisioning::global_provisioning_manager->add_on_closed_callback([this]() {
|
|
for (auto &c : this->active_clients()) {
|
|
DisconnectRequest req;
|
|
req.reason = enums::DISCONNECT_REASON_PROVISIONING_CLOSED;
|
|
// Best-effort: if the send buffer is full the reason is dropped, but the
|
|
// client still learns the window is closed when it reconnects (rejected at
|
|
// hello) or via the socket close.
|
|
if (!c->send_message(req)) {
|
|
API_LOG_MSG_DROPPED(TAG, "Disconnect request");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
#endif
|
|
// Set warning status if reboot timeout is enabled (suppressed while provisioning
|
|
// is pending so the device waits to be onboarded instead of rebooting).
|
|
if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
|
|
this->status_set_warning(LOG_STR("waiting for client connection"));
|
|
}
|
|
}
|
|
|
|
void APIServer::loop() {
|
|
#ifdef USE_API_NOISE
|
|
// Only the flag test is inline; refilling is the rare path
|
|
if (!noise::has_spare_ephemeral()) {
|
|
this->refill_spare_ephemeral_();
|
|
}
|
|
#endif
|
|
// Accept new clients only if the socket exists and has incoming connections
|
|
if (this->socket_ && this->socket_->ready()) {
|
|
this->accept_new_connections_();
|
|
}
|
|
|
|
if (this->api_connection_count_ == 0) {
|
|
// Check reboot timeout - done in loop to avoid scheduler heap churn
|
|
// (cancelled scheduler items sit in heap memory until their scheduled time).
|
|
// Suppressed while a provisioning window is pending so the device waits to be
|
|
// onboarded / reset instead of rebooting itself; resumes once provisioned.
|
|
if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
|
|
const uint32_t now = App.get_loop_component_start_time();
|
|
if (now - this->last_connected_ > this->reboot_timeout_) {
|
|
ESP_LOGE(TAG, "No clients; rebooting");
|
|
App.reboot();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Process clients and remove disconnected ones in a single pass
|
|
// Check network connectivity once for all clients
|
|
if (!network::is_connected()) {
|
|
// Network is down - disconnect all clients
|
|
for (auto &client : this->active_clients()) {
|
|
client->on_fatal_error();
|
|
client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect"));
|
|
}
|
|
// Continue to process and clean up the clients below
|
|
}
|
|
|
|
uint8_t client_index = 0;
|
|
while (client_index < this->api_connection_count_) {
|
|
auto &client = this->clients_[client_index];
|
|
|
|
// Common case: process active client
|
|
if (!client->flags_.remove) {
|
|
client->loop();
|
|
}
|
|
// Handle disconnection promptly - close socket to free LWIP PCB
|
|
// resources and prevent retransmit crashes on ESP8266.
|
|
if (client->flags_.remove) {
|
|
// Rare case: handle disconnection (don't increment - swapped element needs processing)
|
|
this->remove_client_(client_index);
|
|
} else {
|
|
client_index++;
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef USE_API_NOISE
|
|
// Refill only while no api client is still connecting; an OTA handshake is
|
|
// not visible here and just pays the refill it triggered.
|
|
void APIServer::refill_spare_ephemeral_() {
|
|
if (!network::is_connected()) {
|
|
return;
|
|
}
|
|
const uint32_t now = App.get_loop_component_start_time();
|
|
for (auto &client : this->active_clients()) {
|
|
if (client->is_still_connecting(now)) {
|
|
return;
|
|
}
|
|
}
|
|
noise::prepare_spare_ephemeral();
|
|
}
|
|
#endif
|
|
|
|
void APIServer::remove_client_(uint8_t client_index) {
|
|
auto &client = this->clients_[client_index];
|
|
|
|
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
|
this->unregister_active_action_calls_for_connection(client.get());
|
|
#endif
|
|
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
|
|
|
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
|
// Save client info before closing socket and removal for the trigger
|
|
char peername_buf[socket::SOCKADDR_STR_LEN];
|
|
std::string client_name(client->get_name());
|
|
std::string client_peername(client->get_peername_to(peername_buf));
|
|
#endif
|
|
|
|
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
|
client->helper_->close();
|
|
|
|
// Swap-and-reset: move the removed client to the trailing slot and null it out so slots
|
|
// [api_connection_count_, N) remain nullptr.
|
|
const uint8_t last_index = this->api_connection_count_ - 1;
|
|
if (client_index < last_index) {
|
|
std::swap(this->clients_[client_index], this->clients_[last_index]);
|
|
}
|
|
// Drop the count before resetting the slot. reset() runs ~APIConnection(), which can reenter the
|
|
// server (e.g. voice_assistant unsubscribes in its disconnect trigger, publishing entity state ->
|
|
// on_*_update iterating active_clients()). Excluding the dying slot from the active range first
|
|
// keeps that reentrant iteration from dereferencing the now-null slot.
|
|
this->api_connection_count_--;
|
|
this->clients_[last_index].reset();
|
|
|
|
// Last client disconnected - set warning and start tracking for reboot timeout
|
|
// (suppressed while provisioning is pending - see loop()).
|
|
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
|
|
this->status_set_warning(LOG_STR("waiting for client connection"));
|
|
this->last_connected_ = App.get_loop_component_start_time();
|
|
}
|
|
|
|
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
|
// Fire trigger after client is removed so api.connected reflects the true state
|
|
this->client_disconnected_trigger_.trigger(client_name, client_peername);
|
|
#endif
|
|
}
|
|
|
|
void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
|
while (true) {
|
|
struct sockaddr_storage source_addr;
|
|
socklen_t addr_len = sizeof(source_addr);
|
|
|
|
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
|
|
if (!sock)
|
|
break;
|
|
|
|
char peername[socket::SOCKADDR_STR_LEN];
|
|
sock->getpeername_to(peername);
|
|
|
|
// Check if we're at the connection limit
|
|
if (this->api_connection_count_ >= MAX_API_CONNECTIONS) {
|
|
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", MAX_API_CONNECTIONS, peername);
|
|
// Immediately close - socket destructor will handle cleanup
|
|
sock.reset();
|
|
continue;
|
|
}
|
|
|
|
ESP_LOGD(TAG, "Accept %s", peername);
|
|
|
|
auto *conn = new APIConnection(std::move(sock), this);
|
|
this->clients_[this->api_connection_count_++].reset(conn);
|
|
conn->start();
|
|
|
|
// First client connected - clear warning and update timestamp
|
|
if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
|
|
this->status_clear_warning();
|
|
this->last_connected_ = App.get_loop_component_start_time();
|
|
}
|
|
}
|
|
}
|
|
|
|
void APIServer::dump_config() {
|
|
char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
|
|
ESP_LOGCONFIG(TAG,
|
|
"Server:\n"
|
|
" Address: %s:%u\n"
|
|
" Listen backlog: %u\n"
|
|
" Max connections: %u",
|
|
network::get_use_address_to(addr_buf), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
|
|
#ifdef USE_API_NOISE
|
|
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
|
|
if (!this->noise_ctx_.has_psk()) {
|
|
ESP_LOGCONFIG(TAG, " Supports encryption: YES");
|
|
}
|
|
#else
|
|
ESP_LOGCONFIG(TAG, " Noise encryption: NO");
|
|
#endif
|
|
}
|
|
|
|
void APIServer::handle_disconnect(APIConnection *conn) {}
|
|
|
|
// Macro for controller update dispatch
|
|
#define API_DISPATCH_UPDATE(entity_type, entity_name) \
|
|
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
|
|
if (obj->is_internal()) \
|
|
return; \
|
|
for (auto &c : this->active_clients()) { \
|
|
if (c->flags_.state_subscription) \
|
|
c->send_##entity_name##_state(obj); \
|
|
} \
|
|
}
|
|
|
|
#ifdef USE_BINARY_SENSOR
|
|
API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor)
|
|
#endif
|
|
|
|
#ifdef USE_COVER
|
|
API_DISPATCH_UPDATE(cover::Cover, cover)
|
|
#endif
|
|
|
|
#ifdef USE_FAN
|
|
API_DISPATCH_UPDATE(fan::Fan, fan)
|
|
#endif
|
|
|
|
#ifdef USE_LIGHT
|
|
API_DISPATCH_UPDATE(light::LightState, light)
|
|
#endif
|
|
|
|
#ifdef USE_SENSOR
|
|
API_DISPATCH_UPDATE(sensor::Sensor, sensor)
|
|
#endif
|
|
|
|
#ifdef USE_SWITCH
|
|
API_DISPATCH_UPDATE(switch_::Switch, switch)
|
|
#endif
|
|
|
|
#ifdef USE_TEXT_SENSOR
|
|
API_DISPATCH_UPDATE(text_sensor::TextSensor, text_sensor)
|
|
#endif
|
|
|
|
#ifdef USE_CLIMATE
|
|
API_DISPATCH_UPDATE(climate::Climate, climate)
|
|
#endif
|
|
|
|
#ifdef USE_NUMBER
|
|
API_DISPATCH_UPDATE(number::Number, number)
|
|
#endif
|
|
|
|
#ifdef USE_DATETIME_DATE
|
|
API_DISPATCH_UPDATE(datetime::DateEntity, date)
|
|
#endif
|
|
|
|
#ifdef USE_DATETIME_TIME
|
|
API_DISPATCH_UPDATE(datetime::TimeEntity, time)
|
|
#endif
|
|
|
|
#ifdef USE_DATETIME_DATETIME
|
|
API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime)
|
|
#endif
|
|
|
|
#ifdef USE_TEXT
|
|
API_DISPATCH_UPDATE(text::Text, text)
|
|
#endif
|
|
|
|
#ifdef USE_SELECT
|
|
API_DISPATCH_UPDATE(select::Select, select)
|
|
#endif
|
|
|
|
#ifdef USE_LOCK
|
|
API_DISPATCH_UPDATE(lock::Lock, lock)
|
|
#endif
|
|
|
|
#ifdef USE_VALVE
|
|
API_DISPATCH_UPDATE(valve::Valve, valve)
|
|
#endif
|
|
|
|
#ifdef USE_MEDIA_PLAYER
|
|
API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
|
|
#endif
|
|
|
|
#ifdef USE_WATER_HEATER
|
|
API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
|
|
#endif
|
|
|
|
#ifdef USE_EVENT
|
|
void APIServer::on_event(event::Event *obj) {
|
|
if (obj->is_internal())
|
|
return;
|
|
for (auto &c : this->active_clients()) {
|
|
if (c->flags_.state_subscription)
|
|
c->send_event(obj);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifdef USE_UPDATE
|
|
// Update is a special case - the method is called on_update, not on_update_update
|
|
void APIServer::on_update(update::UpdateEntity *obj) {
|
|
if (obj->is_internal())
|
|
return;
|
|
for (auto &c : this->active_clients()) {
|
|
if (c->flags_.state_subscription)
|
|
c->send_update_state(obj);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifdef USE_ZWAVE_PROXY
|
|
void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
|
|
// We could add code to manage a second subscription type, but, since this message type is
|
|
// very infrequent and small, we simply send it to all clients
|
|
for (auto &c : this->active_clients()) {
|
|
if (!c->send_message(msg)) {
|
|
API_LOG_MSG_DROPPED(TAG, "Home ID notification");
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
|
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
|
|
const std::vector<int32_t> *timings) {
|
|
InfraredRFReceiveEvent resp{};
|
|
#ifdef USE_DEVICES
|
|
resp.device_id = device_id;
|
|
#endif
|
|
resp.key = key;
|
|
resp.timings = timings;
|
|
|
|
for (auto &c : this->active_clients())
|
|
c->send_infrared_rf_receive_event(resp);
|
|
}
|
|
#endif
|
|
|
|
#ifdef USE_ALARM_CONTROL_PANEL
|
|
API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
|
|
#endif
|
|
|
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
|
void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
|
|
bool has_subscriber = false;
|
|
for (auto &client : this->active_clients()) {
|
|
has_subscriber |= client->send_homeassistant_action(call);
|
|
}
|
|
if (!has_subscriber) {
|
|
// Home Assistant subscribes to actions shortly *after* authenticating, so actions
|
|
// fired right at connection time (on_client_connected, on_time_sync, ...) can
|
|
// arrive before the subscription and are lost - warn instead of failing silently.
|
|
ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s",
|
|
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"), call.service.c_str(),
|
|
this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)")
|
|
: LOG_STR_LITERAL("no client connected"));
|
|
}
|
|
}
|
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback callback) {
|
|
this->action_response_callbacks_.push_back({call_id, std::move(callback)});
|
|
}
|
|
|
|
void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef error_message) {
|
|
for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
|
|
if (it->call_id == call_id) {
|
|
auto callback = std::move(it->callback);
|
|
this->action_response_callbacks_.erase(it);
|
|
ActionResponse response(success, error_message);
|
|
callback(response);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef error_message,
|
|
const uint8_t *response_data, size_t response_data_len) {
|
|
for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
|
|
if (it->call_id == call_id) {
|
|
auto callback = std::move(it->callback);
|
|
this->action_response_callbacks_.erase(it);
|
|
ActionResponse response(success, error_message, response_data, response_data_len);
|
|
callback(response);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
#endif // USE_API_HOMEASSISTANT_SERVICES
|
|
|
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
|
// Helper to add subscription (reduces duplication)
|
|
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute,
|
|
std::function<void(StringRef)> &&f, bool once) {
|
|
this->state_subs_.push_back(HomeAssistantStateSubscription{
|
|
.entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once,
|
|
// entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation)
|
|
});
|
|
}
|
|
|
|
// Helper to add subscription with heap-allocated strings (reduces duplication)
|
|
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
|
|
std::function<void(StringRef)> &&f, bool once) {
|
|
HomeAssistantStateSubscription sub;
|
|
// Allocate heap storage for the strings
|
|
sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id));
|
|
sub.entity_id = sub.entity_id_dynamic_storage->c_str();
|
|
|
|
if (attribute.has_value()) {
|
|
sub.attribute_dynamic_storage = std::make_unique<std::string>(std::move(attribute.value()));
|
|
sub.attribute = sub.attribute_dynamic_storage->c_str();
|
|
} else {
|
|
sub.attribute = nullptr;
|
|
}
|
|
|
|
sub.callback = std::move(f);
|
|
sub.once = once;
|
|
this->state_subs_.push_back(std::move(sub));
|
|
}
|
|
|
|
// New const char* overload (for internal components - zero allocation)
|
|
void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute,
|
|
std::function<void(StringRef)> &&f) {
|
|
this->add_state_subscription_(entity_id, attribute, std::move(f), false);
|
|
}
|
|
|
|
void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute,
|
|
std::function<void(StringRef)> &&f) {
|
|
this->add_state_subscription_(entity_id, attribute, std::move(f), true);
|
|
}
|
|
|
|
// std::string overload with StringRef callback (zero-allocation callback)
|
|
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
|
std::function<void(StringRef)> &&f) {
|
|
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
|
|
}
|
|
|
|
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
|
std::function<void(StringRef)> &&f) {
|
|
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
|
|
}
|
|
|
|
// Legacy helper: wraps std::string callback and delegates to StringRef version
|
|
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
|
|
std::function<void(const std::string &)> &&f, bool once) {
|
|
// Wrap callback to convert StringRef -> std::string, then delegate
|
|
this->add_state_subscription_(std::move(entity_id), std::move(attribute),
|
|
std::function<void(StringRef)>([f = std::move(f)](StringRef state) { f(state.str()); }),
|
|
once);
|
|
}
|
|
|
|
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string)
|
|
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
|
std::function<void(const std::string &)> &&f) {
|
|
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
|
|
}
|
|
|
|
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
|
std::function<void(const std::string &)> &&f) {
|
|
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
|
|
}
|
|
|
|
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
|
|
return this->state_subs_;
|
|
}
|
|
#endif
|
|
|
|
#ifdef USE_API_NOISE
|
|
#ifndef USE_API_NOISE_PSK_FROM_YAML
|
|
bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
|
|
const LogString *fail_log_msg, bool make_active) {
|
|
if (!this->noise_pref_.save(&new_psk)) {
|
|
ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg));
|
|
return false;
|
|
}
|
|
// ensure it's written immediately
|
|
if (!global_preferences->sync()) {
|
|
ESP_LOGW(TAG, "Failed to sync preferences");
|
|
return false;
|
|
}
|
|
ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg));
|
|
if (make_active) {
|
|
this->set_timeout(100, [this]() {
|
|
// Re-read the PSK from preferences rather than capturing the 32-byte array
|
|
// in the lambda (which would exceed std::function SBO and heap-allocate).
|
|
if (!this->load_and_apply_noise_psk_()) {
|
|
ESP_LOGW(TAG, "Failed to load saved PSK for activation");
|
|
return;
|
|
}
|
|
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
|
|
for (auto &c : this->active_clients()) {
|
|
DisconnectRequest req;
|
|
if (!c->send_message(req)) {
|
|
API_LOG_MSG_DROPPED(TAG, "Disconnect request");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool APIServer::load_and_apply_noise_psk_() {
|
|
// Load into a temp so a failed read cannot disturb the key in use
|
|
SavedNoisePsk loaded{};
|
|
if (!this->noise_pref_.load(&loaded))
|
|
return false;
|
|
this->saved_psk_ = loaded;
|
|
// An unprovisioned device stores the reserved all-zeros key, which is no key
|
|
const bool has_key = !noise::NoiseContext::is_all_zeros(this->saved_psk_.psk);
|
|
this->noise_ctx_.set_psk(has_key ? this->saved_psk_.psk.data() : nullptr);
|
|
return true;
|
|
}
|
|
|
|
bool APIServer::save_noise_psk(noise::psk_t psk, bool make_active) {
|
|
if (this->saved_psk_.psk == psk) {
|
|
ESP_LOGW(TAG, "New PSK matches old");
|
|
return true;
|
|
}
|
|
|
|
SavedNoisePsk new_saved_psk{psk};
|
|
bool result = this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"),
|
|
make_active);
|
|
#ifdef USE_PROVISIONING
|
|
// The device now has a key; report provisioned so the provisioning window is
|
|
// satisfied and the reboot timeout resumes normal operation.
|
|
if (result && provisioning::global_provisioning_manager != nullptr) {
|
|
provisioning::global_provisioning_manager->set_source_provisioned(this->provisioning_source_, true);
|
|
}
|
|
#endif
|
|
return result;
|
|
}
|
|
bool APIServer::clear_noise_psk(bool make_active) {
|
|
SavedNoisePsk empty_psk{};
|
|
bool result = this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"),
|
|
make_active);
|
|
#ifdef USE_PROVISIONING
|
|
// The key was cleared; report unprovisioned so a subsequent reboot reopens the
|
|
// provisioning window.
|
|
if (result && provisioning::global_provisioning_manager != nullptr) {
|
|
provisioning::global_provisioning_manager->set_source_provisioned(this->provisioning_source_, false);
|
|
}
|
|
#endif
|
|
return result;
|
|
}
|
|
#endif // USE_API_NOISE_PSK_FROM_YAML
|
|
#endif
|
|
|
|
#ifdef USE_HOMEASSISTANT_TIME
|
|
void APIServer::request_time() {
|
|
for (auto &client : this->active_clients()) {
|
|
if (!client->flags_.remove && client->is_authenticated()) {
|
|
client->send_time_request();
|
|
return; // Only request from one client to avoid clock conflicts
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
bool APIServer::is_connected_with_state_subscription() const {
|
|
for (uint8_t i = 0; i < this->api_connection_count_; i++) {
|
|
if (this->clients_[i]->flags_.state_subscription) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#ifdef USE_LOGGER
|
|
void APIServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
|
|
if (this->shutting_down_) {
|
|
// Don't try to send logs during shutdown
|
|
// as it could result in a recursion and
|
|
// we would be filling a buffer we are trying to clear
|
|
return;
|
|
}
|
|
for (auto &c : this->active_clients()) {
|
|
if (!c->flags_.remove && c->get_log_subscription_level() >= level)
|
|
c->try_send_log_message(level, tag, message, message_len);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifdef USE_CAMERA
|
|
void APIServer::on_camera_image(const std::shared_ptr<camera::CameraImage> &image) {
|
|
for (auto &c : this->active_clients()) {
|
|
if (!c->flags_.remove)
|
|
c->set_camera_state(image);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void APIServer::on_shutdown() {
|
|
this->shutting_down_ = true;
|
|
|
|
// Close the listening socket to prevent new connections
|
|
this->destroy_socket_();
|
|
|
|
// Change batch delay to 5ms for quick flushing during shutdown
|
|
this->batch_delay_ = 5;
|
|
|
|
// Send disconnect requests to all connected clients
|
|
for (auto &c : this->active_clients()) {
|
|
DisconnectRequest req;
|
|
if (!c->send_message(req)) {
|
|
// If we can't send the disconnect request directly (tx_buffer full),
|
|
// schedule it at the front of the batch so it will be sent with priority
|
|
c->schedule_message_front_(nullptr, DisconnectRequest::MESSAGE_TYPE, DisconnectRequest::ESTIMATED_SIZE);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool APIServer::teardown() {
|
|
// If network is disconnected, no point trying to flush buffers
|
|
if (!network::is_connected()) {
|
|
return true;
|
|
}
|
|
this->loop();
|
|
|
|
// Return true only when all clients have been torn down
|
|
return this->api_connection_count_ == 0;
|
|
}
|
|
|
|
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
|
// Timeout for action calls - matches aioesphomeapi client timeout (default 30s)
|
|
// Can be overridden via USE_API_ACTION_CALL_TIMEOUT_MS define for testing
|
|
#ifndef USE_API_ACTION_CALL_TIMEOUT_MS
|
|
#define USE_API_ACTION_CALL_TIMEOUT_MS 30000 // NOLINT
|
|
#endif
|
|
|
|
uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConnection *conn) {
|
|
uint32_t action_call_id = this->next_action_call_id_++;
|
|
// Handle wraparound (skip 0 as it means "no call")
|
|
if (this->next_action_call_id_ == 0) {
|
|
this->next_action_call_id_ = 1;
|
|
}
|
|
this->active_action_calls_.push_back({action_call_id, client_call_id, conn});
|
|
|
|
// Schedule automatic cleanup after timeout (client will have given up by then)
|
|
// Uses numeric ID overload to avoid heap allocation from str_sprintf
|
|
this->set_timeout(action_call_id, USE_API_ACTION_CALL_TIMEOUT_MS, [this, action_call_id]() {
|
|
ESP_LOGD(TAG, "Action call %" PRIu32 " timed out", action_call_id);
|
|
this->unregister_active_action_call(action_call_id);
|
|
});
|
|
|
|
return action_call_id;
|
|
}
|
|
|
|
void APIServer::unregister_active_action_call(uint32_t action_call_id) {
|
|
// Cancel the timeout for this action call (uses numeric ID overload)
|
|
this->cancel_timeout(action_call_id);
|
|
|
|
// Swap-and-pop is more efficient than remove_if for unordered vectors
|
|
for (size_t i = 0; i < this->active_action_calls_.size(); i++) {
|
|
if (this->active_action_calls_[i].action_call_id == action_call_id) {
|
|
std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
|
|
this->active_action_calls_.pop_back();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void APIServer::unregister_active_action_calls_for_connection(APIConnection *conn) {
|
|
// Remove all active action calls for disconnected connection using swap-and-pop
|
|
for (size_t i = 0; i < this->active_action_calls_.size();) {
|
|
if (this->active_action_calls_[i].connection == conn) {
|
|
// Cancel the timeout for this action call (uses numeric ID overload)
|
|
this->cancel_timeout(this->active_action_calls_[i].action_call_id);
|
|
|
|
std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
|
|
this->active_action_calls_.pop_back();
|
|
// Don't increment i - need to check the swapped element
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
|
|
void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message) {
|
|
for (auto &call : this->active_action_calls_) {
|
|
if (call.action_call_id == action_call_id) {
|
|
call.connection->send_execute_service_response(call.client_call_id, success, error_message);
|
|
return;
|
|
}
|
|
}
|
|
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %" PRIu32, action_call_id);
|
|
}
|
|
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
|
void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message,
|
|
const uint8_t *response_data, size_t response_data_len) {
|
|
for (auto &call : this->active_action_calls_) {
|
|
if (call.action_call_id == action_call_id) {
|
|
call.connection->send_execute_service_response(call.client_call_id, success, error_message, response_data,
|
|
response_data_len);
|
|
return;
|
|
}
|
|
}
|
|
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %" PRIu32, action_call_id);
|
|
}
|
|
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
|
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
|
|
|
} // namespace esphome::api
|
|
#endif
|