From b9361b0868500ce4eec9b7ae0b71308185d02819 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Sep 2025 15:08:55 -0600 Subject: [PATCH 1/8] [esp32_improv] Disable loop by default until provisioning needed (#10764) --- esphome/components/esp32_improv/esp32_improv_component.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index d41094fda1..d47cc50a00 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -31,6 +31,9 @@ void ESP32ImprovComponent::setup() { #endif global_ble_server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT, [this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); }); + + // Start with loop disabled - will be enabled by start() when needed + this->disable_loop(); } void ESP32ImprovComponent::setup_characteristics() { From 6d9fc672d5734908b3ce4ac37bf528096ef64bf2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:21:19 -0400 Subject: [PATCH 2/8] [libretiny] Fix lib_ignore handling and ignore incompatible libraries (#10846) --- esphome/components/heatpumpir/climate.py | 2 +- esphome/components/web_server_base/__init__.py | 2 ++ esphome/core/config.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 4f83bf2435..ec6eac670f 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -128,4 +128,4 @@ async def to_code(config): cg.add_library("tonia/HeatpumpIR", "1.0.37") if CORE.is_libretiny or CORE.is_esp32: - CORE.add_platformio_option("lib_ignore", "IRremoteESP8266") + CORE.add_platformio_option("lib_ignore", ["IRremoteESP8266"]) diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 2aff405036..a82ec462d9 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -40,5 +40,7 @@ async def to_code(config): cg.add_library("Update", None) if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) + if CORE.is_libretiny: + CORE.add_platformio_option("lib_ignore", ["ESPAsyncTCP", "RPAsyncTCP"]) # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.10") diff --git a/esphome/core/config.py b/esphome/core/config.py index 96b9e23861..87e529143d 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -396,7 +396,7 @@ async def add_includes(includes): async def _add_platformio_options(pio_options): # Add includes at the very end, so that they override everything for key, val in pio_options.items(): - if key == "build_flags" and not isinstance(val, list): + if key in ["build_flags", "lib_ignore"] and not isinstance(val, list): val = [val] cg.add_platformio_option(key, val) From 1ecd26adb5b87515d0eae1b74b9286f1636f17c0 Mon Sep 17 00:00:00 2001 From: Stuart Parmenter Date: Wed, 24 Sep 2025 06:59:16 -0700 Subject: [PATCH 3/8] Set color_order to RGB for the Waveshare ESP32-S3-TOUCH-LCD-4.3 and ESP32-S3-TOUCH-LCD-7-800X480 (#10835) --- esphome/components/mipi_rgb/models/waveshare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/mipi_rgb/models/waveshare.py b/esphome/components/mipi_rgb/models/waveshare.py index 49a75da232..a38493e816 100644 --- a/esphome/components/mipi_rgb/models/waveshare.py +++ b/esphome/components/mipi_rgb/models/waveshare.py @@ -7,6 +7,7 @@ wave_4_3 = DriverChip( "ESP32-S3-TOUCH-LCD-4.3", swap_xy=UNDEFINED, initsequence=(), + color_order="RGB", width=800, height=480, pclk_frequency="16MHz", From f2a9e9265ee7cf32215507aa6fdcf0c5b94121a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Sep 2025 09:24:17 -0500 Subject: [PATCH 4/8] [esp32_improv] Fix crashes from uninitialized pointers and missing null checks (#10902) --- .../esp32_improv/esp32_improv_component.cpp | 74 ++++++++++++++----- .../esp32_improv/esp32_improv_component.h | 15 ++-- 2 files changed, 66 insertions(+), 23 deletions(-) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index d47cc50a00..c5a0b89f99 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -15,6 +15,8 @@ using namespace bytebuffer; static const char *const TAG = "esp32_improv.component"; static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; +static constexpr uint16_t STOP_ADVERTISING_DELAY = + 10000; // Delay (ms) before stopping service to allow BLE clients to read the final state ESP32ImprovComponent::ESP32ImprovComponent() { global_improv_component = this; } @@ -193,6 +195,25 @@ void ESP32ImprovComponent::set_status_indicator_state_(bool state) { #endif } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG +const char *ESP32ImprovComponent::state_to_string_(improv::State state) { + switch (state) { + case improv::STATE_STOPPED: + return "STOPPED"; + case improv::STATE_AWAITING_AUTHORIZATION: + return "AWAITING_AUTHORIZATION"; + case improv::STATE_AUTHORIZED: + return "AUTHORIZED"; + case improv::STATE_PROVISIONING: + return "PROVISIONING"; + case improv::STATE_PROVISIONED: + return "PROVISIONED"; + default: + return "UNKNOWN"; + } +} +#endif + bool ESP32ImprovComponent::check_identify_() { uint32_t now = millis(); @@ -206,31 +227,42 @@ bool ESP32ImprovComponent::check_identify_() { } void ESP32ImprovComponent::set_state_(improv::State state) { - ESP_LOGV(TAG, "Setting state: %d", state); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG + if (this->state_ != state) { + ESP_LOGD(TAG, "State transition: %s (0x%02X) -> %s (0x%02X)", this->state_to_string_(this->state_), this->state_, + this->state_to_string_(state), state); + } +#endif this->state_ = state; - if (this->status_->get_value().empty() || this->status_->get_value()[0] != state) { + if (this->status_ != nullptr && (this->status_->get_value().empty() || this->status_->get_value()[0] != state)) { this->status_->set_value(ByteBuffer::wrap(static_cast(state))); if (state != improv::STATE_STOPPED) this->status_->notify(); } - std::vector service_data(8, 0); - service_data[0] = 0x77; // PR - service_data[1] = 0x46; // IM - service_data[2] = static_cast(state); + // Only advertise valid Improv states (0x01-0x04). + // STATE_STOPPED (0x00) is internal only and not part of the Improv spec. + // Advertising 0x00 causes undefined behavior in some clients and makes them + // repeatedly connect trying to determine the actual state. + if (state != improv::STATE_STOPPED) { + std::vector service_data(8, 0); + service_data[0] = 0x77; // PR + service_data[1] = 0x46; // IM + service_data[2] = static_cast(state); - uint8_t capabilities = 0x00; + uint8_t capabilities = 0x00; #ifdef USE_OUTPUT - if (this->status_indicator_ != nullptr) - capabilities |= improv::CAPABILITY_IDENTIFY; + if (this->status_indicator_ != nullptr) + capabilities |= improv::CAPABILITY_IDENTIFY; #endif - service_data[3] = capabilities; - service_data[4] = 0x00; // Reserved - service_data[5] = 0x00; // Reserved - service_data[6] = 0x00; // Reserved - service_data[7] = 0x00; // Reserved + service_data[3] = capabilities; + service_data[4] = 0x00; // Reserved + service_data[5] = 0x00; // Reserved + service_data[6] = 0x00; // Reserved + service_data[7] = 0x00; // Reserved - esp32_ble::global_ble->advertising_set_service_data(service_data); + esp32_ble::global_ble->advertising_set_service_data(service_data); + } #ifdef USE_ESP32_IMPROV_STATE_CALLBACK this->state_callback_.call(this->state_, this->error_state_); #endif @@ -240,7 +272,12 @@ void ESP32ImprovComponent::set_error_(improv::Error error) { if (error != improv::ERROR_NONE) { ESP_LOGE(TAG, "Error: %d", error); } - if (this->error_->get_value().empty() || this->error_->get_value()[0] != error) { + // The error_ characteristic is initialized in setup_characteristics() which is called + // from the loop, while the BLE disconnect callback is registered in setup(). + // error_ can be nullptr if: + // 1. A client connects/disconnects before setup_characteristics() is called + // 2. The device is already provisioned so the service never starts (should_start_ is false) + if (this->error_ != nullptr && (this->error_->get_value().empty() || this->error_->get_value()[0] != error)) { this->error_->set_value(ByteBuffer::wrap(static_cast(error))); if (this->state_ != improv::STATE_STOPPED) this->error_->notify(); @@ -264,7 +301,10 @@ void ESP32ImprovComponent::start() { void ESP32ImprovComponent::stop() { this->should_start_ = false; - this->set_timeout("end-service", 1000, [this] { + // Wait before stopping the service to ensure all BLE clients see the state change. + // This prevents clients from repeatedly reconnecting and wasting resources by allowing + // them to observe that the device is provisioned before the service disappears. + this->set_timeout("end-service", STOP_ADVERTISING_DELAY, [this] { if (this->state_ == improv::STATE_STOPPED || this->service_ == nullptr) return; this->service_->stop(); diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 87cec23876..686da08111 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -79,12 +79,12 @@ class ESP32ImprovComponent : public Component { std::vector incoming_data_; wifi::WiFiAP connecting_sta_; - BLEService *service_ = nullptr; - BLECharacteristic *status_; - BLECharacteristic *error_; - BLECharacteristic *rpc_; - BLECharacteristic *rpc_response_; - BLECharacteristic *capabilities_; + BLEService *service_{nullptr}; + BLECharacteristic *status_{nullptr}; + BLECharacteristic *error_{nullptr}; + BLECharacteristic *rpc_{nullptr}; + BLECharacteristic *rpc_response_{nullptr}; + BLECharacteristic *capabilities_{nullptr}; #ifdef USE_BINARY_SENSOR binary_sensor::BinarySensor *authorizer_{nullptr}; @@ -108,6 +108,9 @@ class ESP32ImprovComponent : public Component { void process_incoming_data_(); void on_wifi_connect_timeout_(); bool check_identify_(); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG + const char *state_to_string_(improv::State state); +#endif }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) From 57f7a709cfdc8ac11909f92b247145a5e40fb34a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sat, 27 Sep 2025 15:55:14 -0400 Subject: [PATCH 5/8] [sx126x] Fix issues with variable length FSK packets (#10911) --- esphome/components/sx126x/sx126x.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp index cae047d168..f5393c478a 100644 --- a/esphome/components/sx126x/sx126x.cpp +++ b/esphome/components/sx126x/sx126x.cpp @@ -217,7 +217,7 @@ void SX126x::configure() { this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 4); // set packet params and sync word - this->set_packet_params_(this->payload_length_); + this->set_packet_params_(this->get_max_packet_size()); if (this->sync_value_.size() == 2) { this->write_register_(REG_LORA_SYNCWORD, this->sync_value_.data(), this->sync_value_.size()); } @@ -236,7 +236,7 @@ void SX126x::configure() { this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 8); // set packet params and sync word - this->set_packet_params_(this->payload_length_); + this->set_packet_params_(this->get_max_packet_size()); if (!this->sync_value_.empty()) { this->write_register_(REG_GFSK_SYNCWORD, this->sync_value_.data(), this->sync_value_.size()); } @@ -274,7 +274,7 @@ void SX126x::set_packet_params_(uint8_t payload_length) { buf[2] = (this->preamble_detect_ > 0) ? ((this->preamble_detect_ - 1) | 0x04) : 0x00; buf[3] = this->sync_value_.size() * 8; buf[4] = 0x00; - buf[5] = 0x00; + buf[5] = (this->payload_length_ > 0) ? 0x00 : 0x01; buf[6] = payload_length; buf[7] = this->crc_enable_ ? 0x06 : 0x01; buf[8] = 0x00; @@ -314,6 +314,9 @@ SX126xError SX126x::transmit_packet(const std::vector &packet) { buf[0] = 0xFF; buf[1] = 0xFF; this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2); + if (this->payload_length_ == 0) { + this->set_packet_params_(this->get_max_packet_size()); + } if (this->rx_start_) { this->set_mode_rx(); } else { From 127058e70024e4675dff9b3cf174f2eebd558c47 Mon Sep 17 00:00:00 2001 From: Oliver Kleinecke Date: Sun, 28 Sep 2025 11:35:40 +0200 Subject: [PATCH 6/8] [usb_uart] Disable flow control on ch34x --- esphome/components/usb_uart/ch34x.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/usb_uart/ch34x.cpp b/esphome/components/usb_uart/ch34x.cpp index 74e7933824..37cd33f841 100644 --- a/esphome/components/usb_uart/ch34x.cpp +++ b/esphome/components/usb_uart/ch34x.cpp @@ -72,6 +72,7 @@ void USBUartTypeCH34X::enable_channels() { if (channel->index_ >= 2) cmd += 0xE; this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd, value, (factor << 8) | divisor, callback); + this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd + 3, 0x80, 0, callback); } USBUartTypeCdcAcm::enable_channels(); } From 345fc0b6caaa4c84d3cd79f2343608a858d3d783 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 29 Sep 2025 09:12:06 +1000 Subject: [PATCH 7/8] [mipi_spi] Fix t-display-amoled (#10922) --- esphome/components/mipi/__init__.py | 10 +++++++++- esphome/components/mipi_spi/models/amoled.py | 4 +++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index 8b1ca899df..f670a5913d 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -401,6 +401,12 @@ class DriverChip: sequence.append((MADCTL, madctl)) return madctl + def skip_command(self, command: str): + """ + Allow suppressing a standard command in the init sequence. + """ + return self.get_default(f"no_{command.lower()}", False) + def get_sequence(self, config) -> tuple[tuple[int, ...], int]: """ Create the init sequence for the display. @@ -432,7 +438,9 @@ class DriverChip: sequence.append((INVOFF,)) if brightness := config.get(CONF_BRIGHTNESS, self.get_default(CONF_BRIGHTNESS)): sequence.append((BRIGHTNESS, brightness)) - sequence.append((SLPOUT,)) + # Add a SLPOUT command if required. + if not self.skip_command("SLPOUT"): + sequence.append((SLPOUT,)) sequence.append((DISPON,)) # Flatten the sequence into a list of bytes, with the length of each command diff --git a/esphome/components/mipi_spi/models/amoled.py b/esphome/components/mipi_spi/models/amoled.py index bc95fc7f71..4d6c8da4b0 100644 --- a/esphome/components/mipi_spi/models/amoled.py +++ b/esphome/components/mipi_spi/models/amoled.py @@ -27,7 +27,8 @@ DriverChip( bus_mode=TYPE_QUAD, brightness=0xD0, color_order=MODE_RGB, - initsequence=(SLPOUT,), # Requires early SLPOUT + no_slpout=True, # SLPOUT is in the init sequence, early + initsequence=(SLPOUT,), ) DriverChip( @@ -95,6 +96,7 @@ CO5300 = DriverChip( brightness=0xD0, color_order=MODE_RGB, bus_mode=TYPE_QUAD, + no_slpout=True, initsequence=( (SLPOUT,), # Requires early SLPOUT (PAGESEL, 0x00), From 58166b3e71cb56eecaa67755a5aa14f5b4d16451 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 29 Sep 2025 21:39:17 +1300 Subject: [PATCH 8/8] Bump version to 2025.9.2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 6ef1d86c74..b0b92dfd63 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.9.1 +PROJECT_NUMBER = 2025.9.2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 0b36e26ec0..dafd49c066 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.9.1" +__version__ = "2025.9.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = (