From b052c9f562cb21fa5996235d814ed6b103b2135e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Jan 2026 17:22:10 -1000 Subject: [PATCH 01/10] [esp32_camera][uart] Add missing wake_loop_threadsafe() preprocessor guards (#13043) --- esphome/components/esp32_camera/esp32_camera.cpp | 2 ++ esphome/components/uart/uart_component_esp_idf.cpp | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 06ba7ff16f0..5466d2e7efa 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -429,9 +429,11 @@ void ESP32Camera::framebuffer_task(void *pv) { camera_fb_t *framebuffer = esp_camera_fb_get(); xQueueSend(that->framebuffer_get_queue_, &framebuffer, portMAX_DELAY); // Only wake the main loop if there's a pending request to consume the frame +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) if (that->has_requested_image_()) { App.wake_loop_threadsafe(); } +#endif // return is no-op for config with 1 fb xQueueReceive(that->framebuffer_return_queue_, &framebuffer, portMAX_DELAY); esp_camera_fb_return(framebuffer); diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index b4f6eedf91c..90997787aa0 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -398,14 +398,18 @@ void IDFUARTComponent::rx_event_task_func(void *param) { case UART_DATA: // Data available in UART RX buffer - wake the main loop ESP_LOGVV(TAG, "Data event: %d bytes", event.size); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); +#endif break; case UART_FIFO_OVF: case UART_BUFFER_FULL: ESP_LOGW(TAG, "FIFO overflow or ring buffer full - clearing"); uart_flush_input(self->uart_num_); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); +#endif break; default: From 68b4bc9d9ed321449b93a425f4525964fc0650c0 Mon Sep 17 00:00:00 2001 From: Kyrill <1942093+poolski@users.noreply.github.com> Date: Wed, 7 Jan 2026 03:28:41 +0000 Subject: [PATCH 02/10] Map `HEAT_COOL` to `MODE_AUTO` in HeatpumpIR component (#12058) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/heatpumpir/heatpumpir.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp index f4d2ca6c1dd..67447a31238 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -181,6 +181,11 @@ void HeatpumpIRClimate::transmit_state() { power_mode_cmd = POWER_ON; operating_mode_cmd = MODE_HEAT; break; + // Map HEAT_COOL to hardware AUTO mode (automatic heat/cool changeover based on temperature). + // In hardware AUTO mode, the device automatically switches between heating and cooling + // based on the current temperature versus the target temperature. + // See https://github.com/esphome/esphome/issues/11161 for further discussion. + case climate::CLIMATE_MODE_HEAT_COOL: case climate::CLIMATE_MODE_AUTO: power_mode_cmd = POWER_ON; operating_mode_cmd = MODE_AUTO; From 4391457a9650718cfbfa697ce764bd8132a3f103 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Jan 2026 17:51:26 -1000 Subject: [PATCH 03/10] [sml] Eliminate heap allocations in text sensor (#13039) --- .../sml/text_sensor/sml_text_sensor.cpp | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/esphome/components/sml/text_sensor/sml_text_sensor.cpp b/esphome/components/sml/text_sensor/sml_text_sensor.cpp index 64f10698f04..6ceff26fe59 100644 --- a/esphome/components/sml/text_sensor/sml_text_sensor.cpp +++ b/esphome/components/sml/text_sensor/sml_text_sensor.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #include "sml_text_sensor.h" #include "../sml_parser.h" +#include namespace esphome { namespace sml { @@ -21,22 +22,33 @@ void SmlTextSensor::publish_val(const ObisInfo &obis_info) { switch (value_type) { case SML_HEX: { - publish_state("0x" + bytes_repr(obis_info.value)); + // Buffer for "0x" + up to 32 bytes as hex + null + char buf[67]; + buf[0] = '0'; + buf[1] = 'x'; + // Max 32 bytes of data fit in remaining buffer ((65-1)/2) + size_t hex_bytes = std::min(obis_info.value.size(), size_t(32)); + format_hex_to(buf + 2, sizeof(buf) - 2, obis_info.value.begin(), hex_bytes); + publish_state(buf, 2 + hex_bytes * 2); break; } case SML_INT: { - publish_state(to_string(bytes_to_int(obis_info.value))); + char buf[21]; // Enough for int64_t (-9223372036854775808) + int len = snprintf(buf, sizeof(buf), "%" PRId64, bytes_to_int(obis_info.value)); + publish_state(buf, static_cast(len)); break; } case SML_BOOL: publish_state(bytes_to_uint(obis_info.value) ? "True" : "False"); break; case SML_UINT: { - publish_state(to_string(bytes_to_uint(obis_info.value))); + char buf[21]; // Enough for uint64_t (18446744073709551615) + int len = snprintf(buf, sizeof(buf), "%" PRIu64, bytes_to_uint(obis_info.value)); + publish_state(buf, static_cast(len)); break; } case SML_OCTET: { - publish_state(std::string(obis_info.value.begin(), obis_info.value.end())); + publish_state(reinterpret_cast(obis_info.value.begin()), obis_info.value.size()); break; } } From 3a84e4a0b4f44ea15b325ff3ca79bcf1e74436be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Jan 2026 17:53:00 -1000 Subject: [PATCH 04/10] [openthread_info] Eliminate heap allocations in text sensors (#13036) --- .../openthread_info_text_sensor.h | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/esphome/components/openthread_info/openthread_info_text_sensor.h b/esphome/components/openthread_info/openthread_info_text_sensor.h index 35e46212cbf..ac5623e0c1a 100644 --- a/esphome/components/openthread_info/openthread_info_text_sensor.h +++ b/esphome/components/openthread_info/openthread_info_text_sensor.h @@ -33,13 +33,12 @@ class IPAddressOpenThreadInfo : public PollingComponent, public text_sensor::Tex return; } - char address_as_string[40]; - otIp6AddressToString(&*address, address_as_string, 40); - std::string ip = address_as_string; + char buf[OT_IP6_ADDRESS_STRING_SIZE]; + otIp6AddressToString(&*address, buf, sizeof(buf)); - if (this->last_ip_ != ip) { - this->last_ip_ = ip; - this->publish_state(this->last_ip_); + if (this->last_ip_ != buf) { + this->last_ip_ = buf; + this->publish_state(buf); } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } @@ -89,7 +88,9 @@ class ExtAddrOpenThreadInfo : public OpenThreadInstancePollingComponent, public const auto *extaddr = otLinkGetExtendedAddress(instance); if (!std::equal(this->last_extaddr_.begin(), this->last_extaddr_.end(), extaddr->m8)) { std::copy(extaddr->m8, extaddr->m8 + 8, this->last_extaddr_.begin()); - this->publish_state(format_hex(extaddr->m8, 8)); + char buf[format_hex_size(8)]; + format_hex_to(buf, extaddr->m8, 8); + this->publish_state(buf); } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } @@ -107,7 +108,9 @@ class Eui64OpenThreadInfo : public OpenThreadInstancePollingComponent, public te if (!std::equal(this->last_eui64_.begin(), this->last_eui64_.end(), addr.m8)) { std::copy(addr.m8, addr.m8 + 8, this->last_eui64_.begin()); - this->publish_state(format_hex(this->last_eui64_.begin(), 8)); + char buf[format_hex_size(8)]; + format_hex_to(buf, this->last_eui64_.data(), 8); + this->publish_state(buf); } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } @@ -123,7 +126,9 @@ class ChannelOpenThreadInfo : public OpenThreadInstancePollingComponent, public uint8_t channel = otLinkGetChannel(instance); if (this->last_channel_ != channel) { this->last_channel_ = channel; - this->publish_state(std::to_string(this->last_channel_)); + char buf[4]; // max "255" + null + snprintf(buf, sizeof(buf), "%u", channel); + this->publish_state(buf); } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } @@ -168,7 +173,9 @@ class NetworkKeyOpenThreadInfo : public DatasetOpenThreadInfo, public text_senso void update_dataset(otOperationalDataset *dataset) override { if (!std::equal(this->last_key_.begin(), this->last_key_.end(), dataset->mNetworkKey.m8)) { std::copy(dataset->mNetworkKey.m8, dataset->mNetworkKey.m8 + 16, this->last_key_.begin()); - this->publish_state(format_hex(dataset->mNetworkKey.m8, 16)); + char buf[format_hex_size(16)]; + format_hex_to(buf, dataset->mNetworkKey.m8, 16); + this->publish_state(buf); } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } @@ -201,7 +208,9 @@ class ExtPanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor: void update_dataset(otOperationalDataset *dataset) override { if (!std::equal(this->last_extpanid_.begin(), this->last_extpanid_.end(), dataset->mExtendedPanId.m8)) { std::copy(dataset->mExtendedPanId.m8, dataset->mExtendedPanId.m8 + 8, this->last_extpanid_.begin()); - this->publish_state(format_hex(this->last_extpanid_.begin(), 8)); + char buf[format_hex_size(8)]; + format_hex_to(buf, this->last_extpanid_.data(), 8); + this->publish_state(buf); } } From 498477c5a2a26cc76d227f66f71bdf7781502286 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Jan 2026 17:53:14 -1000 Subject: [PATCH 05/10] [homeassistant] Eliminate heap allocation in text sensor state updates (#13035) --- .../homeassistant/text_sensor/homeassistant_text_sensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp index 6f773495352..109574e0c8e 100644 --- a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp +++ b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp @@ -15,7 +15,7 @@ void HomeassistantTextSensor::setup() { } else { ESP_LOGD(TAG, "'%s': Got state '%s'", this->entity_id_, state.c_str()); } - this->publish_state(state.str()); + this->publish_state(state.c_str(), state.size()); }); } void HomeassistantTextSensor::dump_config() { From 35118da606e3b0dce213675ae04e8d3f5c98e0d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Jan 2026 17:53:29 -1000 Subject: [PATCH 06/10] [ethernet_info] Eliminate heap allocations in text sensors (#13034) --- .../ethernet_info/ethernet_info_text_sensor.h | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.h b/esphome/components/ethernet_info/ethernet_info_text_sensor.h index b49ddc263df..5b858b772f8 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.h +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.h @@ -14,12 +14,15 @@ class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextS auto ips = ethernet::global_eth_component->get_ip_addresses(); if (ips != this->last_ips_) { this->last_ips_ = ips; - this->publish_state(ips[0].str()); + char buf[network::IP_ADDRESS_BUFFER_SIZE]; + ips[0].str_to(buf); + this->publish_state(buf); uint8_t sensor = 0; for (auto &ip : ips) { if (ip.is_set()) { if (this->ip_sensors_[sensor] != nullptr) { - this->ip_sensors_[sensor]->publish_state(ip.str()); + ip.str_to(buf); + this->ip_sensors_[sensor]->publish_state(buf); } sensor++; } @@ -64,7 +67,10 @@ class DNSAddressEthernetInfo : public PollingComponent, public text_sensor::Text class MACAddressEthernetInfo : public Component, public text_sensor::TextSensor { public: - void setup() override { this->publish_state(ethernet::global_eth_component->get_eth_mac_address_pretty()); } + void setup() override { + char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + this->publish_state(ethernet::global_eth_component->get_eth_mac_address_pretty_into_buffer(buf)); + } float get_setup_priority() const override { return setup_priority::ETHERNET; } void dump_config() override; }; From f9ed2aa17f1cf0786b69464fc30625f2dd98bf59 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Jan 2026 17:53:42 -1000 Subject: [PATCH 07/10] [pylontech] Eliminate heap allocations in text sensors (#13033) --- .../pylontech/text_sensor/pylontech_text_sensor.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp index 55e02f3e33b..8175477cb2d 100644 --- a/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp +++ b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp @@ -25,16 +25,16 @@ void PylontechTextSensor::on_line_read(PylontechListener::LineContents *line) { return; } if (this->base_state_text_sensor_ != nullptr) { - this->base_state_text_sensor_->publish_state(std::string(line->base_st)); + this->base_state_text_sensor_->publish_state(line->base_st); } if (this->voltage_state_text_sensor_ != nullptr) { - this->voltage_state_text_sensor_->publish_state(std::string(line->volt_st)); + this->voltage_state_text_sensor_->publish_state(line->volt_st); } if (this->current_state_text_sensor_ != nullptr) { - this->current_state_text_sensor_->publish_state(std::string(line->curr_st)); + this->current_state_text_sensor_->publish_state(line->curr_st); } if (this->temperature_state_text_sensor_ != nullptr) { - this->temperature_state_text_sensor_->publish_state(std::string(line->temp_st)); + this->temperature_state_text_sensor_->publish_state(line->temp_st); } } From 6d1f6a1084be5787b148c908eecf083bcd50b8ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Jan 2026 17:53:54 -1000 Subject: [PATCH 08/10] [wifi_info] Eliminate heap churn in text sensors (#13031) --- esphome/components/wifi_info/wifi_info_text_sensor.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index 0cca3e16efc..2c0e66eeafb 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -24,12 +24,15 @@ void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "IP Address", this); void IPAddressWiFiInfo::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, const network::IPAddress &dns2) { - this->publish_state(ips[0].str()); + char buf[network::IP_ADDRESS_BUFFER_SIZE]; + ips[0].str_to(buf); + this->publish_state(buf); uint8_t sensor = 0; for (const auto &ip : ips) { if (ip.is_set()) { if (this->ip_sensors_[sensor] != nullptr) { - this->ip_sensors_[sensor]->publish_state(ip.str()); + ip.str_to(buf); + this->ip_sensors_[sensor]->publish_state(buf); } sensor++; } @@ -104,7 +107,7 @@ void SSIDWiFiInfo::setup() { wifi::global_wifi_component->add_connect_state_list void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); } void SSIDWiFiInfo::on_wifi_connect_state(StringRef ssid, std::span bssid) { - this->publish_state(ssid.str()); + this->publish_state(ssid.c_str(), ssid.size()); } /**************** From 5b9be7c1695018162462e6b953b6b71db5099174 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Jan 2026 17:54:04 -1000 Subject: [PATCH 09/10] [ci] Add lint check to prevent usage of deprecated CORE.using_esp_idf (#13029) --- script/ci-custom.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/script/ci-custom.py b/script/ci-custom.py index cf59c3883bf..77d2ab287d0 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -619,6 +619,19 @@ def lint_esphome_h(fname, line, col, content): ) +@lint_content_find_check( + "CORE.using_esp_idf", + include=py_include, + exclude=["esphome/core/__init__.py", "script/ci-custom.py"], +) +def lint_using_esp_idf_deprecated(fname, line, col, content): + return ( + f"{highlight('CORE.using_esp_idf')} is deprecated and will change behavior in 2026.6. " + "ESP32 Arduino builds on top of ESP-IDF, so ESP-IDF features are available in both frameworks. " + f"Please use {highlight('CORE.is_esp32')} and/or {highlight('CORE.using_arduino')} instead." + ) + + @lint_content_check(include=["*.h"]) def lint_pragma_once(fname, content): if "#pragma once" not in content: From fb47bfe92acf70e25bceb4b6bdece129f51d75e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Jan 2026 17:54:20 -1000 Subject: [PATCH 10/10] [dsmr] Eliminate heap allocation when publishing telegram (#13032) --- esphome/components/dsmr/dsmr.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index 41fc2f0d857..5c62aa93ab1 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -268,7 +268,7 @@ bool Dsmr::parse_telegram() { // publish the telegram, after publishing the sensors so it can also trigger action based on latest values if (this->s_telegram_ != nullptr) { - this->s_telegram_->publish_state(std::string(this->telegram_, this->bytes_read_)); + this->s_telegram_->publish_state(this->telegram_, this->bytes_read_); } return true; }