mirror of
https://github.com/esphome/esphome.git
synced 2026-09-28 07:20:24 +00:00
Simplify dial bookkeeping and settle the watchdog policy
This commit is contained in:
@@ -294,17 +294,16 @@ def _consume_api_sockets(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def _validate_outgoing_connection_platform(value: ConfigType) -> ConfigType:
|
||||
def _validate_outgoing_connection(config: ConfigType) -> ConfigType:
|
||||
if CONF_OUTGOING_CONNECTION not in config:
|
||||
return config
|
||||
if CORE.is_esp8266 or CORE.is_rp2:
|
||||
raise cv.Invalid(
|
||||
"outgoing_connection is not supported on this platform because its "
|
||||
"socket layer cannot make outgoing connections"
|
||||
"socket layer cannot make outgoing connections",
|
||||
path=[CONF_OUTGOING_CONNECTION],
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _validate_outgoing_connection(config: ConfigType) -> ConfigType:
|
||||
if CONF_OUTGOING_CONNECTION in config and CONF_ENCRYPTION not in config:
|
||||
if CONF_ENCRYPTION not in config:
|
||||
raise cv.Invalid(
|
||||
"outgoing_connection requires 'encryption' so the peer is verified by key",
|
||||
path=[CONF_OUTGOING_CONNECTION],
|
||||
@@ -312,17 +311,12 @@ def _validate_outgoing_connection(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
OUTGOING_CONNECTION_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_HOST): cv.ipaddress,
|
||||
cv.Optional(CONF_PORT, default=6054): cv.port,
|
||||
cv.Optional(
|
||||
CONF_DELAY, default="60s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
),
|
||||
_validate_outgoing_connection_platform,
|
||||
OUTGOING_CONNECTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_HOST): cv.ipaddress,
|
||||
cv.Optional(CONF_PORT, default=6054): cv.port,
|
||||
cv.Optional(CONF_DELAY, default="60s"): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1784,9 +1784,6 @@ void APIConnection::complete_authentication_() {
|
||||
zwave_proxy::global_zwave_proxy->api_connection_authenticated(this);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
this->parent_->on_client_authenticated();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
|
||||
@@ -376,11 +376,18 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
}
|
||||
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
/// Outgoing connection: the noise helper sends its server hello
|
||||
/// first so the peer can pick the matching key. Outgoing connections are only
|
||||
/// dialed when a PSK is set, so the helper is always the noise helper.
|
||||
/// Must be called before start().
|
||||
void mark_outgoing() { static_cast<APINoiseFrameHelper *>(this->helper_.get())->set_server_hello_first(); }
|
||||
/// Outgoing connection: send our server hello immediately so the peer can
|
||||
/// pick the matching key. Outgoing connections are only dialed when a PSK
|
||||
/// is set, so the helper is always the noise helper. Call after start().
|
||||
void mark_outgoing() {
|
||||
if (this->flags_.remove) {
|
||||
return; // start() failed; the connection is already being torn down
|
||||
}
|
||||
APIError err = static_cast<APINoiseFrameHelper *>(this->helper_.get())->send_server_hello_first();
|
||||
if (err != APIError::OK) {
|
||||
this->fatal_error_with_log_(LOG_STR("Server hello failed"), err);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
|
||||
@@ -61,13 +61,6 @@ static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
|
||||
/// Initialize the frame helper, returns OK if successful.
|
||||
APIError APINoiseFrameHelper::init() {
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
// Outgoing mode is marked in state_; restore what init_common_() expects
|
||||
const bool outgoing = this->state_ == State::CLIENT_HELLO_OUTGOING;
|
||||
if (outgoing) {
|
||||
this->state_ = State::INITIALIZE;
|
||||
}
|
||||
#endif
|
||||
APIError err = init_common_();
|
||||
if (err != APIError::OK) {
|
||||
return err;
|
||||
@@ -86,15 +79,15 @@ APIError APINoiseFrameHelper::init() {
|
||||
#endif
|
||||
|
||||
state_ = State::CLIENT_HELLO;
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
if (outgoing) {
|
||||
// The peer needs our name and MAC to pick the key before its first message
|
||||
state_ = State::CLIENT_HELLO_OUTGOING;
|
||||
return this->send_server_hello_frame_();
|
||||
}
|
||||
#endif
|
||||
return APIError::OK;
|
||||
}
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
APIError APINoiseFrameHelper::send_server_hello_first() {
|
||||
// The peer needs our name and MAC to pick the key before its first message
|
||||
this->state_ = State::CLIENT_HELLO_OUTGOING;
|
||||
return this->send_server_hello_frame_();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_PLAINTEXT
|
||||
APIError APINoiseFrameHelper::init_from_handoff(const uint8_t *header, uint8_t header_len) {
|
||||
APIError err = this->init();
|
||||
@@ -304,12 +297,8 @@ APIError APINoiseFrameHelper::state_action_client_hello_() {
|
||||
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
if (this->state_ == State::CLIENT_HELLO_OUTGOING) {
|
||||
// Server hello already went out in init()
|
||||
aerr = init_handshake_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
state_ = State::HANDSHAKE;
|
||||
return APIError::OK;
|
||||
// Server hello already went out at handoff
|
||||
return this->start_handshake_();
|
||||
}
|
||||
#endif
|
||||
state_ = State::SERVER_HELLO;
|
||||
@@ -345,12 +334,12 @@ APIError APINoiseFrameHelper::state_action_server_hello_() {
|
||||
APIError aerr = this->send_server_hello_frame_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
// start handshake
|
||||
aerr = init_handshake_();
|
||||
return this->start_handshake_();
|
||||
}
|
||||
APIError APINoiseFrameHelper::start_handshake_() {
|
||||
APIError aerr = init_handshake_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
state_ = State::HANDSHAKE;
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
@@ -30,10 +30,10 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
APIError init_from_handoff(const uint8_t *header, uint8_t header_len);
|
||||
#endif
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
// init() then sends the server hello first so the peer can pick the key
|
||||
// before its PSK-mixed message. Call before init(); stored in state_ so
|
||||
// the helper does not grow.
|
||||
void set_server_hello_first() { this->state_ = State::CLIENT_HELLO_OUTGOING; }
|
||||
// Send the server hello immediately so the peer can pick the key before
|
||||
// its PSK-mixed message. Call after init(); the mode is tracked in state_
|
||||
// so the helper does not grow.
|
||||
APIError send_server_hello_first();
|
||||
#endif
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
@@ -46,6 +46,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
APIError state_action_client_hello_();
|
||||
APIError state_action_server_hello_();
|
||||
APIError send_server_hello_frame_();
|
||||
APIError start_handshake_();
|
||||
APIError state_action_handshake_();
|
||||
APIError state_action_handshake_read_();
|
||||
APIError state_action_handshake_write_();
|
||||
|
||||
@@ -21,12 +21,20 @@ 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
|
||||
this->saved_.host[socket::SOCKADDR_STR_LEN - 1] = '\0';
|
||||
} else {
|
||||
this->saved_.host[0] = '\0';
|
||||
if (!this->target_pref_.load(&this->saved_)) {
|
||||
this->saved_ = {};
|
||||
}
|
||||
// Defend against a corrupt or truncated preference blob
|
||||
this->saved_.host[socket::SOCKADDR_STR_LEN - 1] = '\0';
|
||||
#ifndef USE_NETWORK_IPV6
|
||||
if (strchr(this->saved_.host, ':') != nullptr) {
|
||||
// Remembered by an earlier IPv6 build; set_sockaddr() would silently
|
||||
// turn it into 255.255.255.255
|
||||
ESP_LOGW(TAG, "Clearing unusable IPv6 target %s", this->saved_.host);
|
||||
this->saved_ = {};
|
||||
this->target_pref_.save(&this->saved_);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -43,9 +51,7 @@ void OutgoingConnectionManager::loop(APIServer *server) {
|
||||
switch (this->state_) {
|
||||
case DialState::DIAL_STATE_IDLE:
|
||||
// Target went away; give it the configured delay to reconnect first
|
||||
this->state_ = DialState::DIAL_STATE_WAITING;
|
||||
this->state_ts_ = now;
|
||||
this->wait_ = API_OUTGOING_CONNECTION_DELAY;
|
||||
this->schedule_wait_(now, API_OUTGOING_CONNECTION_DELAY);
|
||||
break;
|
||||
case DialState::DIAL_STATE_WAITING:
|
||||
if (now - this->state_ts_ >= this->wait_) {
|
||||
@@ -66,26 +72,19 @@ void OutgoingConnectionManager::try_dial_(APIServer *server, uint32_t now) {
|
||||
return;
|
||||
}
|
||||
const char *host = this->target_host_();
|
||||
if (host == nullptr || server->at_client_limit_() || !server->noise_ctx_.has_psk()) {
|
||||
ESP_LOGD(TAG, "Not dialing: %s",
|
||||
host == nullptr ? "no target"
|
||||
: server->at_client_limit_() ? "max connections"
|
||||
: "no key");
|
||||
if (host == nullptr) {
|
||||
// The steady state until a dial-back client has ever connected
|
||||
ESP_LOGV(TAG, "Not dialing: no target");
|
||||
this->schedule_wait_(now, PRECONDITION_RETRY_MS);
|
||||
return;
|
||||
}
|
||||
const bool at_limit = server->at_client_limit_();
|
||||
if (at_limit || !server->noise_ctx_.has_psk()) {
|
||||
ESP_LOGD(TAG, "Not dialing: %s", at_limit ? "max connections" : "no key");
|
||||
// Not a dial failure; retry without escalating the backoff
|
||||
this->schedule_wait_(now, PRECONDITION_RETRY_MS);
|
||||
return;
|
||||
}
|
||||
#if !defined(USE_NETWORK_IPV6) && !defined(API_OUTGOING_CONNECTION_HOST)
|
||||
if (strchr(host, ':') != nullptr) {
|
||||
// Remembered by an earlier IPv6 build; set_sockaddr() would silently
|
||||
// turn it into 255.255.255.255 here
|
||||
ESP_LOGW(TAG, "Clearing unusable IPv6 target %s", host);
|
||||
this->saved_.host[0] = '\0';
|
||||
this->target_pref_.save(&this->saved_);
|
||||
this->schedule_wait_(now, PRECONDITION_RETRY_MS);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
struct sockaddr_storage addr;
|
||||
socklen_t addr_len =
|
||||
socket::set_sockaddr((struct sockaddr *) &addr, sizeof(addr), host, API_OUTGOING_CONNECTION_PORT);
|
||||
@@ -104,13 +103,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)
|
||||
this->dialed_conn_ = server->add_outgoing_client_(std::move(this->dial_socket_));
|
||||
if (this->dialed_conn_ == nullptr) {
|
||||
this->schedule_retry_(now);
|
||||
} else {
|
||||
this->dial_handoff_ts_ = now;
|
||||
this->schedule_wait_(now, PRECONDITION_RETRY_MS);
|
||||
}
|
||||
this->handoff_(server, now);
|
||||
return;
|
||||
}
|
||||
if (errno != EINPROGRESS) {
|
||||
@@ -134,8 +127,8 @@ void OutgoingConnectionManager::poll_connect_(APIServer *server, uint32_t now) {
|
||||
}
|
||||
this->last_poll_ = now;
|
||||
int fd = this->dial_socket_->get_fd();
|
||||
if (fd < 0 || fd >= FD_SETSIZE) {
|
||||
ESP_LOGW(TAG, "Bad fd for connect poll: %d", fd);
|
||||
if (fd >= FD_SETSIZE) {
|
||||
ESP_LOGW(TAG, "fd %d out of select range", fd);
|
||||
this->schedule_retry_(now);
|
||||
return;
|
||||
}
|
||||
@@ -171,35 +164,42 @@ void OutgoingConnectionManager::poll_connect_(APIServer *server, uint32_t now) {
|
||||
this->schedule_retry_(now);
|
||||
return;
|
||||
}
|
||||
this->handoff_(server, now);
|
||||
}
|
||||
|
||||
void OutgoingConnectionManager::handoff_(APIServer *server, uint32_t now) {
|
||||
this->dialed_conn_ = server->add_outgoing_client_(std::move(this->dial_socket_));
|
||||
if (this->dialed_conn_ == nullptr) {
|
||||
this->schedule_retry_(now);
|
||||
return;
|
||||
}
|
||||
this->dial_handoff_ts_ = now;
|
||||
// Hold unescalated until the peer proves itself or dies unproven
|
||||
this->schedule_wait_(now, PRECONDITION_RETRY_MS);
|
||||
// Connected; dialed_conn_ gates further dialing until the session settles
|
||||
this->state_ = DialState::DIAL_STATE_IDLE;
|
||||
}
|
||||
|
||||
void OutgoingConnectionManager::schedule_retry_(uint32_t now) {
|
||||
void OutgoingConnectionManager::schedule_wait_(uint32_t now, uint32_t wait) {
|
||||
this->dial_socket_.reset(); // no-op when the socket was handed off
|
||||
this->state_ = DialState::DIAL_STATE_WAITING;
|
||||
this->state_ts_ = now;
|
||||
this->wait_ = wait;
|
||||
}
|
||||
|
||||
void OutgoingConnectionManager::schedule_retry_(uint32_t now) {
|
||||
// +/-20% jitter so a fleet of devices does not retry one server in lockstep
|
||||
const uint32_t jitter_span = this->backoff_ / 5;
|
||||
this->wait_ = this->backoff_ - jitter_span + (random_uint32() % (2 * jitter_span + 1));
|
||||
this->schedule_wait_(now, this->backoff_ - jitter_span + (random_uint32() % (2 * jitter_span + 1)));
|
||||
this->backoff_ = std::min(this->backoff_ * 2, BACKOFF_MAX_MS);
|
||||
}
|
||||
|
||||
void OutgoingConnectionManager::on_client_removed(APIConnection *conn) {
|
||||
void OutgoingConnectionManager::on_client_removed(APIConnection *conn, bool was_authenticated) {
|
||||
if (conn != this->dialed_conn_) {
|
||||
return;
|
||||
}
|
||||
this->dialed_conn_ = nullptr;
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (now - this->dial_handoff_ts_ >= DIAL_PROVEN_MS) {
|
||||
// Outlived the handshake timeout, so it authenticated: a working peer
|
||||
// (e.g. a host: target that never sends the flag) disconnected normally
|
||||
if (was_authenticated) {
|
||||
// A working peer (e.g. a host: target that never sends the flag)
|
||||
// disconnected normally
|
||||
this->backoff_ = BACKOFF_MIN_MS;
|
||||
this->schedule_wait_(now, API_OUTGOING_CONNECTION_DELAY);
|
||||
} else {
|
||||
@@ -238,16 +238,15 @@ void OutgoingConnectionManager::on_target_client(APIConnection *conn) {
|
||||
}
|
||||
|
||||
void OutgoingConnectionManager::dump_config() const {
|
||||
ESP_LOGCONFIG(TAG, " Outgoing connection port: %u", API_OUTGOING_CONNECTION_PORT);
|
||||
#ifdef API_OUTGOING_CONNECTION_HOST
|
||||
ESP_LOGCONFIG(TAG, " Outgoing connection host: %s", API_OUTGOING_CONNECTION_HOST);
|
||||
const char *host = API_OUTGOING_CONNECTION_HOST;
|
||||
#else
|
||||
if (this->saved_.host[0] != '\0') {
|
||||
ESP_LOGCONFIG(TAG, " Outgoing connection host: %s (remembered)", this->saved_.host);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Outgoing connection host: none remembered yet");
|
||||
}
|
||||
const char *host = this->saved_.host[0] != '\0' ? this->saved_.host : "none remembered yet";
|
||||
#endif
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Outgoing connection port: %u\n"
|
||||
" Outgoing connection host: %s",
|
||||
API_OUTGOING_CONNECTION_PORT, host);
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -35,12 +35,9 @@ class OutgoingConnectionManager {
|
||||
void loop(APIServer *server);
|
||||
/// A key-verified client declared itself a dial-back target; last one wins
|
||||
void on_target_client(APIConnection *conn);
|
||||
/// Clears the dialed-connection gate; dying unproven escalates the backoff
|
||||
void on_client_removed(APIConnection *conn);
|
||||
void on_shutdown() {
|
||||
this->dial_socket_.reset();
|
||||
this->state_ = DialState::DIAL_STATE_IDLE;
|
||||
}
|
||||
/// Clears the dialed-connection gate; dying unauthenticated escalates the backoff
|
||||
void on_client_removed(APIConnection *conn, bool was_authenticated);
|
||||
void on_shutdown() { this->dial_socket_.reset(); }
|
||||
void dump_config() const;
|
||||
|
||||
protected:
|
||||
@@ -55,21 +52,23 @@ class OutgoingConnectionManager {
|
||||
static constexpr uint32_t CONNECT_TIMEOUT_MS = 10000;
|
||||
static constexpr uint32_t CONNECT_POLL_INTERVAL_MS = 250;
|
||||
static constexpr uint32_t NETWORK_RETRY_MS = 500;
|
||||
// Longer than the 60s handshake timeout: a dialed session still alive past
|
||||
// this authenticated, so its death is a normal disconnect, not a bad dial
|
||||
static constexpr uint32_t DIAL_PROVEN_MS = 65000;
|
||||
static constexpr uint32_t PRECONDITION_RETRY_MS = 5000;
|
||||
// Boot waits for the client to connect in first; a deep sleep wake window
|
||||
// is short, so connecting out immediately is the wake state
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
static constexpr uint32_t BOOT_WAIT_MS = 0;
|
||||
#else
|
||||
static constexpr uint32_t BOOT_WAIT_MS = API_OUTGOING_CONNECTION_DELAY;
|
||||
#endif
|
||||
|
||||
void try_dial_(APIServer *server, uint32_t now);
|
||||
void poll_connect_(APIServer *server, uint32_t now);
|
||||
// Hand the connected socket to the server and gate on the new connection
|
||||
void handoff_(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;
|
||||
}
|
||||
void schedule_wait_(uint32_t now, uint32_t wait);
|
||||
const char *target_host_() const {
|
||||
#ifdef API_OUTGOING_CONNECTION_HOST
|
||||
return API_OUTGOING_CONNECTION_HOST;
|
||||
@@ -77,7 +76,6 @@ class OutgoingConnectionManager {
|
||||
return this->saved_.host[0] != '\0' ? this->saved_.host : nullptr;
|
||||
#endif
|
||||
}
|
||||
static constexpr uint32_t PRECONDITION_RETRY_MS = 5000;
|
||||
|
||||
// Pointers first (4 bytes each on 32-bit)
|
||||
std::unique_ptr<socket::Socket> dial_socket_;
|
||||
@@ -89,16 +87,9 @@ class OutgoingConnectionManager {
|
||||
|
||||
// 4-byte types
|
||||
uint32_t backoff_{BACKOFF_MIN_MS};
|
||||
// Boot waits for the client to connect in first; a deep sleep wake window
|
||||
// is short, so connecting out immediately is the wake state
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
uint32_t wait_{0};
|
||||
#else
|
||||
uint32_t wait_{API_OUTGOING_CONNECTION_DELAY};
|
||||
#endif
|
||||
uint32_t wait_{BOOT_WAIT_MS};
|
||||
uint32_t state_ts_{0};
|
||||
uint32_t last_poll_{0};
|
||||
uint32_t dial_handoff_ts_{0};
|
||||
|
||||
// Byte-aligned types last
|
||||
#ifndef API_OUTGOING_CONNECTION_HOST
|
||||
|
||||
@@ -212,13 +212,13 @@ void APIServer::remove_client_(uint8_t client_index) {
|
||||
std::string client_peername(client->get_peername_to(peername_buf));
|
||||
#endif
|
||||
|
||||
// Read before the swap-and-reset below destroys the connection
|
||||
const bool was_authenticated = client->is_authenticated();
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
if (client->flags_.outgoing_connection_target) {
|
||||
this->outgoing_target_count_--;
|
||||
}
|
||||
this->outgoing_conn_.on_client_removed(client.get());
|
||||
// Read before the swap-and-reset below destroys the connection
|
||||
const bool was_authenticated = client->is_authenticated();
|
||||
this->outgoing_conn_.on_client_removed(client.get(), was_authenticated);
|
||||
#endif
|
||||
|
||||
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
||||
@@ -241,12 +241,12 @@ void APIServer::remove_client_(uint8_t client_index) {
|
||||
// (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"));
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
// An unauthenticated session (e.g. a dial to a host that accepts TCP but
|
||||
// never speaks the API) must not keep resetting the reboot watchdog
|
||||
if (was_authenticated)
|
||||
#endif
|
||||
// A session that never authenticated (e.g. a port scan, or a dial to a
|
||||
// host that accepts TCP but never speaks the API) must not reset the
|
||||
// reboot watchdog
|
||||
if (was_authenticated) {
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
@@ -285,10 +285,11 @@ void APIServer::add_client_(APIConnection *conn) {
|
||||
this->clients_[this->api_connection_count_++].reset(conn);
|
||||
conn->start();
|
||||
|
||||
// First client connected - clear warning and update timestamp
|
||||
// First client connected - clear warning. The reboot watchdog timestamp is
|
||||
// refreshed when an authenticated client is removed (see remove_client_),
|
||||
// never on bare TCP connects.
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,21 +302,12 @@ APIConnection *APIServer::add_outgoing_client_(std::unique_ptr<socket::Socket> s
|
||||
return nullptr;
|
||||
}
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
this->add_client_(conn);
|
||||
// After start(): sends our server hello first so the peer can pick the key
|
||||
conn->mark_outgoing();
|
||||
// Unlike add_client_, no watchdog refresh: a dial that never authenticates
|
||||
// must not keep petting the no-client reboot timeout
|
||||
this->clients_[this->api_connection_count_++].reset(conn);
|
||||
conn->start();
|
||||
return conn;
|
||||
}
|
||||
|
||||
void APIServer::on_client_authenticated() {
|
||||
if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
|
||||
this->status_clear_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::on_outgoing_target_client(APIConnection *conn) {
|
||||
this->outgoing_target_count_++;
|
||||
this->outgoing_conn_.on_target_client(conn);
|
||||
|
||||
@@ -85,9 +85,6 @@ class APIServer final : public Component,
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
// Called by APIConnection when a client declares itself a dial-back target in its hello
|
||||
void on_outgoing_target_client(APIConnection *conn);
|
||||
// Called by APIConnection on authentication so dialed sessions feed the
|
||||
// reboot watchdog only once they are real
|
||||
void on_client_authenticated();
|
||||
#endif
|
||||
|
||||
void handle_disconnect(APIConnection *conn);
|
||||
|
||||
@@ -702,3 +702,11 @@ async def run_compiled(
|
||||
)
|
||||
|
||||
yield _run_compiled
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def isolated_preferences(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
"""Point host preferences at a per-test dir so every run starts clean
|
||||
(host preferences otherwise persist to ~/.esphome/prefs, keyed only by
|
||||
device name)."""
|
||||
monkeypatch.setenv("ESPHOME_PREFDIR", str(tmp_path / "prefs"))
|
||||
|
||||
@@ -30,10 +30,8 @@ HA_CLIENT_INFO = "Home Assistant 2026.8.0"
|
||||
HELLO_TARGET_FLAG = b"\x20\x01"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolated_preferences(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None:
|
||||
"""Keep host preferences per-test so every run starts with no saved peer."""
|
||||
monkeypatch.setenv("ESPHOME_PREFDIR", str(tmp_path / "prefs"))
|
||||
# Every run must start with no saved peer
|
||||
pytestmark = pytest.mark.usefixtures("isolated_preferences")
|
||||
|
||||
|
||||
def _frame(payload: bytes) -> bytes:
|
||||
|
||||
@@ -24,10 +24,8 @@ NEW_KEY = base64.b64encode(b"n" * 32)
|
||||
KEY_ACTIVATION_DELAY = 0.5
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolated_preferences(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None:
|
||||
"""Keep host preferences per-test so every run starts unprovisioned."""
|
||||
monkeypatch.setenv("ESPHOME_PREFDIR", str(tmp_path / "prefs"))
|
||||
# Every run must start unprovisioned
|
||||
pytestmark = pytest.mark.usefixtures("isolated_preferences")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -10,13 +10,8 @@ import pytest
|
||||
from .state_utils import InitialStateHelper, require_entity
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolated_preferences(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None:
|
||||
"""Keep host preferences per-test so RESTORE_AND_ON never loads a stale value left
|
||||
behind by a previous run (host preferences otherwise persist to ~/.esphome/prefs,
|
||||
keyed only by device name)."""
|
||||
monkeypatch.setenv("ESPHOME_PREFDIR", str(tmp_path / "prefs"))
|
||||
# RESTORE_AND_ON must never load a stale value left behind by a previous run
|
||||
pytestmark = pytest.mark.usefixtures("isolated_preferences")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user