From c124d72ea934e01b7b417993f6fbd957f0a7025e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Dec 2025 04:45:27 +0100 Subject: [PATCH 1/3] [esp8266] Eliminate up to 16ms socket latency (#12397) --- .../components/socket/lwip_raw_tcp_impl.cpp | 49 ++++++++++++++++--- esphome/components/socket/socket.h | 9 ++++ esphome/core/application.cpp | 7 +++ 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index e57af91b77..5538206058 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -14,13 +14,36 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#ifdef USE_ESP8266 +#include // For esp_schedule() +#endif + namespace esphome { namespace socket { +#ifdef USE_ESP8266 +// Flag to signal socket activity - checked by socket_delay() to exit early +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static volatile bool s_socket_woke = false; + +void socket_delay(uint32_t ms) { + // Use esp_delay with a callback that checks if socket data arrived. + // This allows the delay to exit early when socket_wake() is called by + // lwip recv_fn/accept_fn callbacks, reducing socket latency. + s_socket_woke = false; + esp_delay(ms, []() { return !s_socket_woke; }); +} + +void socket_wake() { + s_socket_woke = true; + esp_schedule(); +} +#endif + static const char *const TAG = "socket.lwip"; // set to 1 to enable verbose lwip logging -#if 0 +#if 0 // NOLINT(readability-avoid-unconditional-preprocessor-if) #define LWIP_LOG(msg, ...) ESP_LOGVV(TAG, "socket %p: " msg, this, ##__VA_ARGS__) #else #define LWIP_LOG(msg, ...) @@ -323,9 +346,10 @@ class LWIPRawImpl : public Socket { for (int i = 0; i < iovcnt; i++) { ssize_t err = read(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); if (err == -1) { - if (ret != 0) + if (ret != 0) { // if we already read some don't return an error break; + } return err; } ret += err; @@ -393,9 +417,10 @@ class LWIPRawImpl : public Socket { ssize_t written = internal_write(buf, len); if (written == -1) return -1; - if (written == 0) + if (written == 0) { // no need to output if nothing written return 0; + } if (nodelay_) { int err = internal_output(); if (err == -1) @@ -408,18 +433,20 @@ class LWIPRawImpl : public Socket { for (int i = 0; i < iovcnt; i++) { ssize_t err = internal_write(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); if (err == -1) { - if (written != 0) + if (written != 0) { // if we already read some don't return an error break; + } return err; } written += err; if ((size_t) err != iov[i].iov_len) break; } - if (written == 0) + if (written == 0) { // no need to output if nothing written return 0; + } if (nodelay_) { int err = internal_output(); if (err == -1) @@ -473,6 +500,10 @@ class LWIPRawImpl : public Socket { } else { pbuf_cat(rx_buf_, pb); } +#ifdef USE_ESP8266 + // Wake the main loop immediately so it can process the received data. + socket_wake(); +#endif return ERR_OK; } @@ -612,7 +643,7 @@ class LWIPRawListenImpl : public LWIPRawImpl { } private: - err_t accept_fn(struct tcp_pcb *newpcb, err_t err) { + err_t accept_fn_(struct tcp_pcb *newpcb, err_t err) { LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err); if (err != ERR_OK || newpcb == nullptr) { // "An error code if there has been an error accepting. Only return ERR_ABRT if you have @@ -633,12 +664,16 @@ class LWIPRawListenImpl : public LWIPRawImpl { sock->init(); accepted_sockets_[accepted_socket_count_++] = std::move(sock); LWIP_LOG("Accepted connection, queue size: %d", accepted_socket_count_); +#ifdef USE_ESP8266 + // Wake the main loop immediately so it can accept the new connection. + socket_wake(); +#endif return ERR_OK; } static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) { LWIPRawListenImpl *arg_this = reinterpret_cast(arg); - return arg_this->accept_fn(newpcb, err); + return arg_this->accept_fn_(newpcb, err); } // Accept queue - holds incoming connections briefly until the event loop calls accept() diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 78a89fe008..8936b2cd10 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -82,6 +82,15 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri /// Set a sockaddr to the any address and specified port for the IP version used by socket_ip(). socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port); +#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +/// Delay that can be woken early by socket activity. +/// On ESP8266, lwip callbacks set a flag and call esp_schedule() to wake the delay. +void socket_delay(uint32_t ms); + +/// Called by lwip callbacks to signal socket activity and wake delay. +void socket_wake(); +#endif + } // namespace socket } // namespace esphome #endif diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 75814ae253..a85d671a07 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -12,6 +12,10 @@ #include "esphome/components/status_led/status_led.h" #endif +#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +#include "esphome/components/socket/socket.h" +#endif + #ifdef USE_SOCKET_SELECT_SUPPORT #include @@ -627,6 +631,9 @@ void Application::yield_with_select_(uint32_t delay_ms) { // No sockets registered, use regular delay delay(delay_ms); } +#elif defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) + // No select support but can wake on socket activity via esp_schedule() + socket::socket_delay(delay_ms); #else // No select support, use regular delay delay(delay_ms); From d1d376ebc88186532b09afa20be1440415279ffc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 13:05:01 +0100 Subject: [PATCH 2/3] Bump actions/create-github-app-token from 2.2.0 to 2.2.1 (#12370) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto-label-pr.yml | 2 +- .github/workflows/release.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 39164fc2ea..8e96297cc0 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -26,7 +26,7 @@ jobs: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2 + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 51aa1f885e..9933f63a1c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -221,7 +221,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0 + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} @@ -256,7 +256,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0 + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} @@ -287,7 +287,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0 + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} From a3017ca3be62195b1cf5ac030763c97a399284c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Dec 2025 13:46:50 +0100 Subject: [PATCH 3/3] [climate] Save 48 bytes per entity by conditionally compiling visual overrides --- esphome/components/climate/__init__.py | 5 +++++ esphome/components/climate/climate.cpp | 27 ++++++++++++++------------ esphome/components/climate/climate.h | 16 +++++++++------ esphome/core/defines.h | 1 + 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 5824e68141..b8e49db6c0 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -275,10 +275,13 @@ async def setup_climate_core_(var, config): visual = config[CONF_VISUAL] if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None: + cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES") cg.add(var.set_visual_min_temperature_override(min_temp)) if (max_temp := visual.get(CONF_MAX_TEMPERATURE)) is not None: + cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES") cg.add(var.set_visual_max_temperature_override(max_temp)) if (temp_step := visual.get(CONF_TEMPERATURE_STEP)) is not None: + cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES") cg.add( var.set_visual_temperature_step_override( temp_step[CONF_TARGET_TEMPERATURE], @@ -286,8 +289,10 @@ async def setup_climate_core_(var, config): ) ) if (min_humidity := visual.get(CONF_MIN_HUMIDITY)) is not None: + cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES") cg.add(var.set_visual_min_humidity_override(min_humidity)) if (max_humidity := visual.get(CONF_MAX_HUMIDITY)) is not None: + cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES") cg.add(var.set_visual_max_humidity_override(max_humidity)) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index b0fba6aa62..3bc20a17c6 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -473,26 +473,28 @@ void Climate::publish_state() { ClimateTraits Climate::get_traits() { auto traits = this->traits(); - if (this->visual_min_temperature_override_.has_value()) { - traits.set_visual_min_temperature(*this->visual_min_temperature_override_); +#ifdef USE_CLIMATE_VISUAL_OVERRIDES + if (!std::isnan(this->visual_min_temperature_override_)) { + traits.set_visual_min_temperature(this->visual_min_temperature_override_); } - if (this->visual_max_temperature_override_.has_value()) { - traits.set_visual_max_temperature(*this->visual_max_temperature_override_); + if (!std::isnan(this->visual_max_temperature_override_)) { + traits.set_visual_max_temperature(this->visual_max_temperature_override_); } - if (this->visual_target_temperature_step_override_.has_value()) { - traits.set_visual_target_temperature_step(*this->visual_target_temperature_step_override_); - traits.set_visual_current_temperature_step(*this->visual_current_temperature_step_override_); + if (!std::isnan(this->visual_target_temperature_step_override_)) { + traits.set_visual_target_temperature_step(this->visual_target_temperature_step_override_); + traits.set_visual_current_temperature_step(this->visual_current_temperature_step_override_); } - if (this->visual_min_humidity_override_.has_value()) { - traits.set_visual_min_humidity(*this->visual_min_humidity_override_); + if (!std::isnan(this->visual_min_humidity_override_)) { + traits.set_visual_min_humidity(this->visual_min_humidity_override_); } - if (this->visual_max_humidity_override_.has_value()) { - traits.set_visual_max_humidity(*this->visual_max_humidity_override_); + if (!std::isnan(this->visual_max_humidity_override_)) { + traits.set_visual_max_humidity(this->visual_max_humidity_override_); } - +#endif return traits; } +#ifdef USE_CLIMATE_VISUAL_OVERRIDES void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) { this->visual_min_temperature_override_ = visual_min_temperature_override; } @@ -513,6 +515,7 @@ void Climate::set_visual_min_humidity_override(float visual_min_humidity_overrid void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) { this->visual_max_humidity_override_ = visual_max_humidity_override; } +#endif ClimateCall Climate::make_call() { return ClimateCall(this); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 28a73d8c05..82df4b815f 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -213,11 +213,13 @@ class Climate : public EntityBase { */ ClimateTraits get_traits(); +#ifdef USE_CLIMATE_VISUAL_OVERRIDES void set_visual_min_temperature_override(float visual_min_temperature_override); void set_visual_max_temperature_override(float visual_max_temperature_override); void set_visual_temperature_step_override(float target, float current); void set_visual_min_humidity_override(float visual_min_humidity_override); void set_visual_max_humidity_override(float visual_max_humidity_override); +#endif /// Check if a custom fan mode is currently active. bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; } @@ -321,12 +323,14 @@ class Climate : public EntityBase { CallbackManager state_callback_{}; CallbackManager control_callback_{}; ESPPreferenceObject rtc_; - optional visual_min_temperature_override_{}; - optional visual_max_temperature_override_{}; - optional visual_target_temperature_step_override_{}; - optional visual_current_temperature_step_override_{}; - optional visual_min_humidity_override_{}; - optional visual_max_humidity_override_{}; +#ifdef USE_CLIMATE_VISUAL_OVERRIDES + float visual_min_temperature_override_{NAN}; + float visual_max_temperature_override_{NAN}; + float visual_target_temperature_step_override_{NAN}; + float visual_current_temperature_step_override_{NAN}; + float visual_min_humidity_override_{NAN}; + float visual_max_humidity_override_{NAN}; +#endif private: /** The active custom fan mode (private - enforces use of safe setters). diff --git a/esphome/core/defines.h b/esphome/core/defines.h index a5170d73ff..750cab5bba 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -28,6 +28,7 @@ #define USE_BUTTON #define USE_CAMERA #define USE_CLIMATE +#define USE_CLIMATE_VISUAL_OVERRIDES #define USE_CONTROLLER_REGISTRY #define USE_COVER #define USE_DATETIME