mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 15:27:33 +00:00
Dial immediately when no client has ever connected and polish review nits
This commit is contained in:
@@ -25,6 +25,7 @@ from esphome.const import (
|
||||
CONF_DATA,
|
||||
CONF_DATA_TEMPLATE,
|
||||
CONF_DELAY,
|
||||
CONF_ENABLE_IPV6,
|
||||
CONF_ENCRYPTION,
|
||||
CONF_EVENT,
|
||||
CONF_ID,
|
||||
@@ -48,6 +49,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import fnv1_hash
|
||||
from esphome.types import ConfigFragmentType, ConfigType
|
||||
|
||||
@@ -462,7 +464,27 @@ def _validate_esp8266_action_strings(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _validate_esp8266_action_strings
|
||||
def _validate_outgoing_host_ipv6(config: ConfigType) -> ConfigType:
|
||||
"""An IPv6 host silently dials 255.255.255.255 on a build without IPv6."""
|
||||
if (
|
||||
(outgoing := config.get(CONF_OUTGOING_CONNECTION)) is None
|
||||
or (host := outgoing.get(CONF_HOST)) is None
|
||||
or host.version != 6
|
||||
):
|
||||
return config
|
||||
network_conf = fv.full_config.get().get("network") or {}
|
||||
if not network_conf.get(CONF_ENABLE_IPV6):
|
||||
raise cv.Invalid(
|
||||
"outgoing_connection host is an IPv6 address but IPv6 is not "
|
||||
"enabled; set 'network: enable_ipv6: true'",
|
||||
path=[CONF_OUTGOING_CONNECTION, CONF_HOST],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
_validate_esp8266_action_strings, _validate_outgoing_host_ipv6
|
||||
)
|
||||
|
||||
|
||||
def _add_action_strings(
|
||||
|
||||
@@ -45,8 +45,9 @@ void OutgoingConnectionManager::loop(APIServer *server) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
switch (this->state_) {
|
||||
case DialState::DIAL_STATE_IDLE:
|
||||
// The target client just went away; give it the configured delay to
|
||||
// reconnect on its own before dialing.
|
||||
// The connected target went away; give it the configured delay to
|
||||
// reconnect on its own before dialing. (Boot skips this state so a
|
||||
// device whose client never reached it dials right away.)
|
||||
this->state_ = DialState::DIAL_STATE_WAITING;
|
||||
this->state_ts_ = now;
|
||||
this->wait_ = API_OUTGOING_CONNECTION_DELAY;
|
||||
@@ -117,6 +118,7 @@ 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);
|
||||
this->schedule_retry_(now);
|
||||
return;
|
||||
}
|
||||
@@ -205,6 +207,8 @@ void OutgoingConnectionManager::dump_config() const {
|
||||
#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");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||
#error "api outgoing_connection needs a socket implementation that can make outgoing connections"
|
||||
#endif
|
||||
#ifndef USE_API_NOISE
|
||||
#error "api outgoing_connection needs noise encryption so the peer is verified by key"
|
||||
#endif
|
||||
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
@@ -41,7 +44,10 @@ class OutgoingConnectionManager {
|
||||
/// re-dials. A dialed connection dying without ever sending the flagged
|
||||
/// hello is the unproven-peer case, so the backoff escalates here.
|
||||
void on_client_removed(APIConnection *conn);
|
||||
void on_shutdown() { this->dial_socket_.reset(); }
|
||||
void on_shutdown() {
|
||||
this->dial_socket_.reset();
|
||||
this->state_ = DialState::DIAL_STATE_IDLE;
|
||||
}
|
||||
void dump_config() const;
|
||||
|
||||
protected:
|
||||
@@ -88,7 +94,10 @@ class OutgoingConnectionManager {
|
||||
uint32_t wait_{0};
|
||||
uint32_t state_ts_{0};
|
||||
uint32_t last_poll_{0};
|
||||
DialState state_{DialState::DIAL_STATE_IDLE};
|
||||
// Boot starts in WAITING with wait_ 0: when no dial-back client has ever
|
||||
// connected (a device its client could never reach), dial immediately.
|
||||
// The configured delay only applies after a connected target goes away.
|
||||
DialState state_{DialState::DIAL_STATE_WAITING};
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -288,9 +288,11 @@ void APIServer::add_client_(APIConnection *conn) {
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
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);
|
||||
// flight; re-check at the handoff so add_client_ cannot write past clients_.
|
||||
// The PSK can also have been cleared mid-dial, and mark_outgoing() requires
|
||||
// the noise helper the constructor only picks while a PSK is set.
|
||||
if (this->at_client_limit_() || !this->noise_ctx_.has_psk()) {
|
||||
ESP_LOGW(TAG, "Dropping outgoing connection (%s)", this->at_client_limit_() ? "max connections" : "no key active");
|
||||
return nullptr;
|
||||
}
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
|
||||
@@ -5,9 +5,8 @@ wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
# Outgoing connection: the device dials Home Assistant when no client with a
|
||||
# state subscription is connected. Requires encryption so the peer is
|
||||
# verified by key.
|
||||
# Outgoing connection: the device dials out when no dial-back client is
|
||||
# connected. Requires encryption so the peer is verified by key.
|
||||
api:
|
||||
encryption:
|
||||
key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU=
|
||||
|
||||
@@ -153,8 +153,9 @@ async def test_api_outgoing_connection_remembered(
|
||||
"""No host configured: the device remembers the client whose hello carried
|
||||
the dial-back flag and dials that address after a restart."""
|
||||
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
# Bound but not yet listening: dial attempts in the first phase are
|
||||
# refused, exercising the retry path without queueing stale connections.
|
||||
# Bound but not yet listening so first-phase dials cannot queue stale
|
||||
# connections; whether the device attempts any dial before the restart
|
||||
# is timing dependent and not asserted here.
|
||||
listener.bind(("127.0.0.1", 0))
|
||||
port = listener.getsockname()[1]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user