mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 07:17:33 +00:00
Address review findings and advertise support in device info
This commit is contained in:
@@ -336,6 +336,10 @@ message DeviceInfoResponse {
|
||||
// all-zeros PSK, so the api encryption key can be provisioned without being
|
||||
// sent in plaintext (protects against passive sniffing, not active MITM)
|
||||
bool api_encryption_provisionable = 26 [(field_ifdef) = "USE_API_NOISE"];
|
||||
|
||||
// Device is built with the api outgoing_connection option and can open
|
||||
// the TCP connection to a dial-back target itself
|
||||
bool api_outgoing_connection_supported = 27 [(field_ifdef) = "USE_API_OUTGOING_CONNECTION"];
|
||||
}
|
||||
|
||||
// ==================== DEVICE CAPABILITIES ====================
|
||||
|
||||
@@ -1955,6 +1955,9 @@ bool APIConnection::send_device_info_response_() {
|
||||
// one) so this advertisement survives the plaintext removal in 2027.2.0.
|
||||
resp.api_encryption_provisionable = !this->parent_->get_noise_ctx().has_psk();
|
||||
#endif
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
resp.api_outgoing_connection_supported = true;
|
||||
#endif
|
||||
#endif
|
||||
#ifdef USE_DEVICES
|
||||
size_t device_index = 0;
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace esphome::api {
|
||||
static const char *const TAG = "api.outgoing";
|
||||
|
||||
void OutgoingConnectionManager::setup() {
|
||||
#ifndef API_OUTGOING_CONNECTION_HOST
|
||||
this->target_pref_ = global_preferences->make_preference<SavedOutgoingTarget>(629847102UL, true);
|
||||
if (this->target_pref_.load(&this->saved_)) {
|
||||
// Defend against a corrupt or truncated preference blob
|
||||
@@ -23,6 +24,7 @@ void OutgoingConnectionManager::setup() {
|
||||
} else {
|
||||
this->saved_.host[0] = '\0';
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void OutgoingConnectionManager::loop(APIServer *server) {
|
||||
@@ -31,6 +33,12 @@ void OutgoingConnectionManager::loop(APIServer *server) {
|
||||
// hello arrived; nothing to do while it stays connected.
|
||||
return;
|
||||
}
|
||||
if (this->dialed_conn_ != nullptr) {
|
||||
// A dialed connection is still open but its peer has not sent a flagged
|
||||
// hello (yet); dialing again would only burn connection slots. The
|
||||
// connection's own timeouts remove it eventually if the peer is silent.
|
||||
return;
|
||||
}
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
switch (this->state_) {
|
||||
case DialState::DIAL_STATE_IDLE:
|
||||
@@ -54,7 +62,8 @@ void OutgoingConnectionManager::loop(APIServer *server) {
|
||||
void OutgoingConnectionManager::try_dial_(APIServer *server, uint32_t now) {
|
||||
const char *host = this->target_host_();
|
||||
if (host == nullptr || !network::is_connected() || server->at_client_limit_() || !server->noise_ctx_.has_psk()) {
|
||||
this->schedule_retry_(now);
|
||||
// Not a dial failure; retry soon without escalating the backoff
|
||||
this->schedule_wait_(now, PRECONDITION_RETRY_MS);
|
||||
return;
|
||||
}
|
||||
struct sockaddr_storage addr;
|
||||
@@ -74,7 +83,7 @@ void OutgoingConnectionManager::try_dial_(APIServer *server, uint32_t now) {
|
||||
int err = this->dial_socket_->connect((struct sockaddr *) &addr, addr_len);
|
||||
if (err == 0) {
|
||||
// Immediate success (possible for localhost)
|
||||
server->add_outgoing_client_(std::move(this->dial_socket_));
|
||||
this->dialed_conn_ = server->add_outgoing_client_(std::move(this->dial_socket_));
|
||||
this->schedule_retry_(now);
|
||||
return;
|
||||
}
|
||||
@@ -99,7 +108,7 @@ void OutgoingConnectionManager::poll_connect_(APIServer *server, uint32_t now) {
|
||||
}
|
||||
this->last_poll_ = now;
|
||||
int fd = this->dial_socket_->get_fd();
|
||||
if (fd < 0) {
|
||||
if (fd < 0 || fd >= FD_SETSIZE) {
|
||||
this->schedule_retry_(now);
|
||||
return;
|
||||
}
|
||||
@@ -109,8 +118,14 @@ void OutgoingConnectionManager::poll_connect_(APIServer *server, uint32_t now) {
|
||||
FD_ZERO(&writefds);
|
||||
FD_SET(fd, &writefds);
|
||||
struct timeval tv = {0, 0};
|
||||
int ret = select(fd + 1, nullptr, &writefds, nullptr, &tv);
|
||||
if (ret <= 0 || !FD_ISSET(fd, &writefds)) {
|
||||
// Global-scope select: the entity namespace esphome::select shadows it here
|
||||
int ret = ::select(fd + 1, nullptr, &writefds, nullptr, &tv);
|
||||
if (ret < 0) {
|
||||
ESP_LOGW(TAG, "Connect poll failed: errno %d", errno);
|
||||
this->schedule_retry_(now);
|
||||
return;
|
||||
}
|
||||
if (ret == 0 || !FD_ISSET(fd, &writefds)) {
|
||||
return; // still in progress
|
||||
}
|
||||
int error = 0;
|
||||
@@ -120,7 +135,7 @@ void OutgoingConnectionManager::poll_connect_(APIServer *server, uint32_t now) {
|
||||
this->schedule_retry_(now);
|
||||
return;
|
||||
}
|
||||
server->add_outgoing_client_(std::move(this->dial_socket_));
|
||||
this->dialed_conn_ = server->add_outgoing_client_(std::move(this->dial_socket_));
|
||||
// Stay in a retry wait until the peer proves itself by sending a flagged
|
||||
// hello; on_target_client() then resets to idle and clears the backoff.
|
||||
this->schedule_retry_(now);
|
||||
@@ -139,19 +154,23 @@ void OutgoingConnectionManager::schedule_retry_(uint32_t now) {
|
||||
void OutgoingConnectionManager::on_target_client(APIConnection *conn) {
|
||||
// The target is connected; stop any dial in flight and reset the backoff.
|
||||
this->dial_socket_.reset();
|
||||
this->dialed_conn_ = nullptr;
|
||||
this->state_ = DialState::DIAL_STATE_IDLE;
|
||||
this->backoff_ = BACKOFF_MIN_MS;
|
||||
#ifndef API_OUTGOING_CONNECTION_HOST
|
||||
SavedOutgoingTarget target{};
|
||||
conn->get_peername_to(target.host);
|
||||
if (target.host[0] == '\0' || strcmp(target.host, this->saved_.host) == 0) {
|
||||
return; // unknown peer or unchanged; avoid flash wear
|
||||
}
|
||||
this->saved_ = target;
|
||||
if (!this->target_pref_.save(&this->saved_) || !global_preferences->sync()) {
|
||||
if (!this->target_pref_.save(&target) || !global_preferences->sync()) {
|
||||
// Keep the old value so the save is retried on the next flagged hello
|
||||
ESP_LOGW(TAG, "Failed to save target");
|
||||
return;
|
||||
}
|
||||
this->saved_ = target;
|
||||
ESP_LOGD(TAG, "Saved %s as outgoing connection target", this->saved_.host);
|
||||
#endif
|
||||
}
|
||||
|
||||
void OutgoingConnectionManager::dump_config() const {
|
||||
|
||||
@@ -37,6 +37,12 @@ class OutgoingConnectionManager {
|
||||
/// Called when a key-verified client declares itself a dial-back target;
|
||||
/// the last such client wins as the remembered address.
|
||||
void on_target_client(APIConnection *conn);
|
||||
/// Called for every removed connection so a dialed one stops gating re-dials
|
||||
void on_client_removed(APIConnection *conn) {
|
||||
if (conn == this->dialed_conn_) {
|
||||
this->dialed_conn_ = nullptr;
|
||||
}
|
||||
}
|
||||
void on_shutdown() { this->dial_socket_.reset(); }
|
||||
void dump_config() const;
|
||||
|
||||
@@ -56,6 +62,13 @@ class OutgoingConnectionManager {
|
||||
void poll_connect_(APIServer *server, uint32_t now);
|
||||
// Close any half-open dial and wait a jittered backoff before retrying
|
||||
void schedule_retry_(uint32_t now);
|
||||
// Wait without escalating the backoff (used for unmet preconditions)
|
||||
void schedule_wait_(uint32_t now, uint32_t wait) {
|
||||
this->dial_socket_.reset();
|
||||
this->state_ = DialState::DIAL_STATE_WAITING;
|
||||
this->state_ts_ = now;
|
||||
this->wait_ = wait;
|
||||
}
|
||||
const char *target_host_() const {
|
||||
#ifdef API_OUTGOING_CONNECTION_HOST
|
||||
return API_OUTGOING_CONNECTION_HOST;
|
||||
@@ -63,10 +76,16 @@ class OutgoingConnectionManager {
|
||||
return this->saved_.host[0] != '\0' ? this->saved_.host : nullptr;
|
||||
#endif
|
||||
}
|
||||
static constexpr uint32_t PRECONDITION_RETRY_MS = 5000;
|
||||
|
||||
std::unique_ptr<socket::Socket> dial_socket_;
|
||||
// Connection created by the last successful dial; compared, never
|
||||
// dereferenced. Cleared by on_client_removed()/on_target_client().
|
||||
APIConnection *dialed_conn_{nullptr};
|
||||
#ifndef API_OUTGOING_CONNECTION_HOST
|
||||
ESPPreferenceObject target_pref_;
|
||||
SavedOutgoingTarget saved_{};
|
||||
#endif
|
||||
uint32_t backoff_{BACKOFF_MIN_MS};
|
||||
uint32_t wait_{0};
|
||||
uint32_t state_ts_{0};
|
||||
|
||||
@@ -180,6 +180,9 @@ uint8_t *DeviceInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 26, this->api_encryption_provisionable);
|
||||
#endif
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 27, this->api_outgoing_connection_supported);
|
||||
#endif
|
||||
return pos;
|
||||
}
|
||||
@@ -245,6 +248,9 @@ uint32_t DeviceInfoResponse::calculate_size() const {
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
size += ProtoSize::calc_bool(2, this->api_encryption_provisionable);
|
||||
#endif
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
size += ProtoSize::calc_bool(2, this->api_outgoing_connection_supported);
|
||||
#endif
|
||||
return size;
|
||||
}
|
||||
|
||||
@@ -552,7 +552,7 @@ class SerialProxyInfo final : public ProtoMessage {
|
||||
class DeviceInfoResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 10;
|
||||
static constexpr uint16_t ESTIMATED_SIZE = 312;
|
||||
static constexpr uint16_t ESTIMATED_SIZE = 315;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("device_info_response"); }
|
||||
#endif
|
||||
@@ -610,6 +610,9 @@ class DeviceInfoResponse final : public ProtoMessage {
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
bool api_encryption_provisionable{false};
|
||||
#endif
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
bool api_outgoing_connection_supported{false};
|
||||
#endif
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
|
||||
uint32_t calculate_size() const;
|
||||
|
||||
@@ -1011,6 +1011,9 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
dump_field(out, ESPHOME_PSTR("api_encryption_provisionable"), this->api_encryption_provisionable);
|
||||
#endif
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
dump_field(out, ESPHOME_PSTR("api_outgoing_connection_supported"), this->api_outgoing_connection_supported);
|
||||
#endif
|
||||
return out.c_str();
|
||||
}
|
||||
|
||||
@@ -216,6 +216,7 @@ void APIServer::remove_client_(uint8_t client_index) {
|
||||
if (client->flags_.outgoing_connection_target) {
|
||||
this->outgoing_target_count_--;
|
||||
}
|
||||
this->outgoing_conn_.on_client_removed(client.get());
|
||||
#endif
|
||||
|
||||
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
||||
@@ -285,10 +286,17 @@ void APIServer::add_client_(APIConnection *conn) {
|
||||
}
|
||||
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
void APIServer::add_outgoing_client_(std::unique_ptr<socket::Socket> sock) {
|
||||
APIConnection *APIServer::add_outgoing_client_(std::unique_ptr<socket::Socket> sock) {
|
||||
// Inbound clients may have taken the remaining slots while the dial was in
|
||||
// flight; re-check at the handoff so add_client_ cannot write past clients_
|
||||
if (this->at_client_limit_()) {
|
||||
ESP_LOGW(TAG, "Max connections (%d), dropping outgoing connection", MAX_API_CONNECTIONS);
|
||||
return nullptr;
|
||||
}
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
conn->mark_outgoing();
|
||||
this->add_client_(conn);
|
||||
return conn;
|
||||
}
|
||||
|
||||
void APIServer::on_outgoing_target_client(APIConnection *conn) {
|
||||
|
||||
@@ -267,7 +267,8 @@ class APIServer final : public Component,
|
||||
void add_client_(APIConnection *conn);
|
||||
bool at_client_limit_() const { return this->api_connection_count_ >= MAX_API_CONNECTIONS; }
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
void add_outgoing_client_(std::unique_ptr<socket::Socket> sock);
|
||||
// Returns the new connection, or nullptr (socket dropped) when at the limit
|
||||
APIConnection *add_outgoing_client_(std::unique_ptr<socket::Socket> sock);
|
||||
bool has_outgoing_target_client_() const { return this->outgoing_target_count_ != 0; }
|
||||
friend class OutgoingConnectionManager;
|
||||
#endif
|
||||
|
||||
@@ -214,9 +214,11 @@
|
||||
#define USE_API_HOMEASSISTANT_SERVICES
|
||||
#define USE_API_HOMEASSISTANT_STATES
|
||||
#define USE_API_NOISE
|
||||
#if !defined(USE_ESP8266) && !defined(USE_RP2) // raw-lwip sockets cannot make outgoing connections
|
||||
#define USE_API_OUTGOING_CONNECTION
|
||||
#define API_OUTGOING_CONNECTION_PORT 6054
|
||||
#define API_OUTGOING_CONNECTION_DELAY 60000
|
||||
#endif
|
||||
#define USE_API_VARINT64
|
||||
#define USE_API_PLAINTEXT
|
||||
#define USE_API_USER_DEFINED_ACTIONS
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
packages:
|
||||
common: !include common-base.yaml
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
# Outgoing connection on the lwip_sockets implementation used by LibreTiny
|
||||
api:
|
||||
encryption:
|
||||
key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU=
|
||||
outgoing_connection:
|
||||
host: 192.168.1.2
|
||||
Reference in New Issue
Block a user