From 1214bb6bad973805a1b4ec99cb56443f4ba10b5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 20:07:20 +0000 Subject: [PATCH 01/43] Bump aioesphomeapi from 43.2.1 to 43.3.0 (#12507) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7a50e1296f..21b575a23f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20251013.0 -aioesphomeapi==43.2.1 +aioesphomeapi==43.3.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.18.16 # dashboard_import From 24d7e9dd233c4acd6d11cbced55decea377ddaa0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 20:08:16 +0000 Subject: [PATCH 02/43] Bump tornado from 6.5.3 to 6.5.4 (#12508) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 21b575a23f..011a2d4f0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ PyYAML==6.0.3 paho-mqtt==1.6.1 colorama==0.4.6 icmplib==3.0.4 -tornado==6.5.3 +tornado==6.5.4 tzlocal==5.3.1 # from time tzdata>=2021.1 # from time pyserial==3.5 From 839139df36fcca5cba859e455ea4de9802a5d898 Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Mon, 15 Dec 2025 20:23:54 +0000 Subject: [PATCH 03/43] Add FNV-1a hash functions (#12502) --- esphome/core/helpers.cpp | 16 ++++- esphome/core/helpers.h | 15 +++++ tests/integration/fixtures/fnv1a_hash.yaml | 60 +++++++++++++++++++ tests/integration/test_fnv1a_hash.py | 69 ++++++++++++++++++++++ 4 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 tests/integration/fixtures/fnv1a_hash.yaml create mode 100644 tests/integration/test_fnv1a_hash.py diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index fb96869d21..55466fca8a 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -143,17 +143,29 @@ uint16_t crc16be(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t poly, return refout ? (crc ^ 0xffff) : crc; } +// FNV-1 hash - deprecated, use fnv1a_hash() for new code uint32_t fnv1_hash(const char *str) { - uint32_t hash = 2166136261UL; + uint32_t hash = FNV1_OFFSET_BASIS; if (str) { while (*str) { - hash *= 16777619UL; + hash *= FNV1_PRIME; hash ^= *str++; } } return hash; } +// FNV-1a hash - preferred for new code +uint32_t fnv1a_hash_extend(uint32_t hash, const char *str) { + if (str) { + while (*str) { + hash ^= *str++; + hash *= FNV1_PRIME; + } + } + return hash; +} + float random_float() { return static_cast(random_uint32()) / static_cast(UINT32_MAX); } // Strings diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 3e44e08dd4..cd9efef213 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -378,9 +378,24 @@ uint16_t crc16be(const uint8_t *data, uint16_t len, uint16_t crc = 0, uint16_t p bool refout = false); /// Calculate a FNV-1 hash of \p str. +/// Note: FNV-1a (fnv1a_hash) is preferred for new code due to better avalanche characteristics. uint32_t fnv1_hash(const char *str); inline uint32_t fnv1_hash(const std::string &str) { return fnv1_hash(str.c_str()); } +/// FNV-1 32-bit offset basis +constexpr uint32_t FNV1_OFFSET_BASIS = 2166136261UL; +/// FNV-1 32-bit prime +constexpr uint32_t FNV1_PRIME = 16777619UL; + +/// Extend a FNV-1a hash with additional string data. +uint32_t fnv1a_hash_extend(uint32_t hash, const char *str); +inline uint32_t fnv1a_hash_extend(uint32_t hash, const std::string &str) { + return fnv1a_hash_extend(hash, str.c_str()); +} +/// Calculate a FNV-1a hash of \p str. +inline uint32_t fnv1a_hash(const char *str) { return fnv1a_hash_extend(FNV1_OFFSET_BASIS, str); } +inline uint32_t fnv1a_hash(const std::string &str) { return fnv1a_hash(str.c_str()); } + /// Return a random 32-bit unsigned integer. uint32_t random_uint32(); /// Return a random float between 0 and 1. diff --git a/tests/integration/fixtures/fnv1a_hash.yaml b/tests/integration/fixtures/fnv1a_hash.yaml new file mode 100644 index 0000000000..d9c80601b8 --- /dev/null +++ b/tests/integration/fixtures/fnv1a_hash.yaml @@ -0,0 +1,60 @@ +esphome: + name: fnv1a-hash-test + platformio_options: + build_flags: + - "-DDEBUG" + on_boot: + - lambda: |- + using esphome::fnv1a_hash; + using esphome::fnv1a_hash_extend; + + // Test empty string (should return offset basis) + uint32_t hash_empty = fnv1a_hash(""); + if (hash_empty == 0x811c9dc5) { + ESP_LOGI("FNV1A", "empty PASSED"); + } else { + ESP_LOGE("FNV1A", "empty FAILED: 0x%08x != 0x811c9dc5", hash_empty); + } + + // Test known FNV-1a hashes + uint32_t hash_hello = fnv1a_hash("hello"); + if (hash_hello == 0x4f9f2cab) { + ESP_LOGI("FNV1A", "known_hello PASSED"); + } else { + ESP_LOGE("FNV1A", "known_hello FAILED: 0x%08x != 0x4f9f2cab", hash_hello); + } + + uint32_t hash_helloworld = fnv1a_hash("helloworld"); + if (hash_helloworld == 0x3b9f5c61) { + ESP_LOGI("FNV1A", "known_helloworld PASSED"); + } else { + ESP_LOGE("FNV1A", "known_helloworld FAILED: 0x%08x != 0x3b9f5c61", hash_helloworld); + } + + // Test fnv1a_hash_extend consistency + uint32_t hash1 = fnv1a_hash("hello"); + hash1 = fnv1a_hash_extend(hash1, "world"); + uint32_t hash2 = fnv1a_hash("helloworld"); + + if (hash1 == hash2) { + ESP_LOGI("FNV1A", "extend PASSED"); + } else { + ESP_LOGE("FNV1A", "extend FAILED: 0x%08x != 0x%08x", hash1, hash2); + } + + // Test with std::string + std::string str1 = "foo"; + std::string str2 = "bar"; + uint32_t hash3 = fnv1a_hash(str1); + hash3 = fnv1a_hash_extend(hash3, str2); + uint32_t hash4 = fnv1a_hash("foobar"); + + if (hash3 == hash4) { + ESP_LOGI("FNV1A", "string PASSED"); + } else { + ESP_LOGE("FNV1A", "string FAILED: 0x%08x != 0x%08x", hash3, hash4); + } + +host: +api: +logger: diff --git a/tests/integration/test_fnv1a_hash.py b/tests/integration/test_fnv1a_hash.py new file mode 100644 index 0000000000..366ea42cda --- /dev/null +++ b/tests/integration/test_fnv1a_hash.py @@ -0,0 +1,69 @@ +"""Integration test for FNV-1a hash functions.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_fnv1a_hash( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that FNV-1a hash functions work correctly.""" + + test_results = {} + all_tests_complete = asyncio.Event() + expected_tests = {"empty", "known_hello", "known_helloworld", "extend", "string"} + + def on_log_line(line: str) -> None: + """Capture log lines with test results.""" + # Strip ANSI escape codes + clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) + # Look for our test result messages + # Format: "[timestamp][level][FNV1A:line]: test_name PASSED" + match = re.search(r"\[FNV1A:\d+\]:\s+(\w+)\s+(PASSED|FAILED)", clean_line) + if match: + test_name = match.group(1) + result = match.group(2) + test_results[test_name] = result + if set(test_results.keys()) >= expected_tests: + all_tests_complete.set() + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "fnv1a-hash-test" + + # Wait for all tests to complete or timeout + try: + await asyncio.wait_for(all_tests_complete.wait(), timeout=2.0) + except TimeoutError: + pytest.fail(f"Tests timed out. Got results for: {set(test_results.keys())}") + + # Verify all tests passed + assert "empty" in test_results, "empty string test not found" + assert test_results["empty"] == "PASSED", "empty string test failed" + + assert "known_hello" in test_results, "known_hello test not found" + assert test_results["known_hello"] == "PASSED", "known_hello test failed" + + assert "known_helloworld" in test_results, "known_helloworld test not found" + assert test_results["known_helloworld"] == "PASSED", ( + "known_helloworld test failed" + ) + + assert "extend" in test_results, "fnv1a_hash_extend test not found" + assert test_results["extend"] == "PASSED", "fnv1a_hash_extend test failed" + + assert "string" in test_results, "std::string test not found" + assert test_results["string"] == "PASSED", "std::string test failed" From 803bb742c9358245e97aed967b962f8aed34ffdc Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:29:51 -0500 Subject: [PATCH 04/43] [remote_base] Fix crash when ABBWelcome action has no data field (#12493) Co-authored-by: Claude --- esphome/components/remote_base/abbwelcome_protocol.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/remote_base/abbwelcome_protocol.h b/esphome/components/remote_base/abbwelcome_protocol.h index 4b922eb2f1..b8d9293c11 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.h +++ b/esphome/components/remote_base/abbwelcome_protocol.h @@ -232,10 +232,10 @@ template class ABBWelcomeAction : public RemoteTransmitterAction data.set_message_id(this->message_id_.value(x...)); data.auto_message_id = this->auto_message_id_.value(x...); std::vector data_vec; - if (this->len_ >= 0) { + if (this->len_ > 0) { // Static mode: copy from flash to vector data_vec.assign(this->data_.data, this->data_.data + this->len_); - } else { + } else if (this->len_ < 0) { // Template mode: call function data_vec = this->data_.func(x...); } @@ -245,7 +245,7 @@ template class ABBWelcomeAction : public RemoteTransmitterAction } protected: - ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + ssize_t len_{0}; // <0 = template mode, >=0 = static mode with length union Data { std::vector (*func)(Ts...); // Function pointer (stateless lambdas) const uint8_t *data; // Pointer to static data in flash From 8dff7ee746fdb76b5c09176e96e5749310a19e61 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 15 Dec 2025 12:07:02 -0500 Subject: [PATCH 05/43] [esp32] Support all IDF component version operators in shorthand syntax (#12499) Co-authored-by: Claude --- esphome/components/esp32/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 3dc5e4bbaa..0142fd4841 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -4,6 +4,7 @@ import itertools import logging import os from pathlib import Path +import re from esphome import yaml_util import esphome.codegen as cg @@ -616,10 +617,13 @@ def require_vfs_dir() -> None: def _parse_idf_component(value: str) -> ConfigType: """Parse IDF component shorthand syntax like 'owner/component^version'""" - if "^" not in value: - raise cv.Invalid(f"Invalid IDF component shorthand '{value}'") - name, ref = value.split("^", 1) - return {CONF_NAME: name, CONF_REF: ref} + # Match operator followed by version-like string (digit or *) + if match := re.search(r"(~=|>=|<=|==|!=|>|<|\^|~)(\d|\*)", value): + return {CONF_NAME: value[: match.start()], CONF_REF: value[match.start() :]} + raise cv.Invalid( + f"Invalid IDF component shorthand '{value}'. " + f"Expected format: 'owner/componentversion' where is one of: ^, ~, ~=, ==, !=, >=, >, <=, <" + ) def _validate_idf_component(config: ConfigType) -> ConfigType: From 57634b612ac73e291522026ed6188f404f7d8b48 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 15 Dec 2025 18:54:12 +0100 Subject: [PATCH 06/43] [http_request] Fix infinite loop when server doesn't send Content-Length header (#12480) Co-authored-by: Claude Sonnet 4.5 --- .../components/http_request/http_request.h | 3 +++ .../http_request/ota/ota_http_request.cpp | 20 ++++++++++++++----- .../update/http_request_update.cpp | 5 +++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 8a82a44d7d..8adf13b954 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -255,6 +255,9 @@ template class HttpRequestSendAction : public Action { size_t read_index = 0; while (container->get_bytes_read() < max_length) { int read = container->read(buf + read_index, std::min(max_length - read_index, 512)); + if (read <= 0) { + break; + } App.feed_wdt(); yield(); read_index += read; diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 4552fcc9df..b257518e06 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -132,11 +132,18 @@ uint8_t OtaHttpRequestComponent::do_ota_() { App.feed_wdt(); yield(); - if (bufsize < 0) { - ESP_LOGE(TAG, "Stream closed"); - this->cleanup_(std::move(backend), container); - return OTA_CONNECTION_ERROR; - } else if (bufsize > 0 && bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) { + // Exit loop if no data available (stream closed or end of data) + if (bufsize <= 0) { + if (bufsize < 0) { + ESP_LOGE(TAG, "Stream closed with error"); + this->cleanup_(std::move(backend), container); + return OTA_CONNECTION_ERROR; + } + // bufsize == 0: no more data available, exit loop + break; + } + + if (bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) { // add read bytes to MD5 md5_receive.add(buf, bufsize); @@ -247,6 +254,9 @@ bool OtaHttpRequestComponent::http_get_md5_() { int read_len = 0; while (container->get_bytes_read() < MD5_SIZE) { read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE); + if (read_len <= 0) { + break; + } App.feed_wdt(); yield(); } diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 26af754e69..22cad625d1 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -76,6 +76,11 @@ void HttpRequestUpdate::update_task(void *params) { yield(); + if (read_bytes <= 0) { + // Network error or connection closed - break to avoid infinite loop + break; + } + read_index += read_bytes; } From 4c926cca60128b4335b4146e98667b01fd678b25 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 15 Dec 2025 18:09:42 -0500 Subject: [PATCH 07/43] Bump version to 2025.12.0b4 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 532e207788..039bba2136 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.12.0b3 +PROJECT_NUMBER = 2025.12.0b4 # 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 916136a69c..0aae3e2b17 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.12.0b3" +__version__ = "2025.12.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From ead60bc5c47e535b12787526f5a42cb91b972fac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 16 Dec 2025 00:48:30 -0600 Subject: [PATCH 08/43] [socket] Fix getpeername() returning local address instead of remote in LWIP raw TCP (#12475) --- esphome/components/socket/lwip_raw_tcp_impl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 5538206058..328df24bdd 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -188,7 +188,7 @@ class LWIPRawImpl : public Socket { errno = EINVAL; return -1; } - return this->ip2sockaddr_(&pcb_->local_ip, pcb_->local_port, name, addrlen); + return this->ip2sockaddr_(&pcb_->remote_ip, pcb_->remote_port, name, addrlen); } std::string getpeername() override { if (pcb_ == nullptr) { From 1897551b2874f2f4fc92407dc39abfb5bdf97866 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:17:17 -0500 Subject: [PATCH 09/43] [uart] Fix UART on default UART0 pins for ESP-IDF (#12519) Co-authored-by: Claude --- .../uart/uart_component_esp_idf.cpp | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index b438e4f7a6..b4f6eedf91 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -9,6 +9,7 @@ #include "esphome/core/gpio.h" #include "driver/gpio.h" #include "soc/gpio_num.h" +#include "soc/uart_pins.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -139,6 +140,22 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } + int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; + int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; + int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; + + // Workaround for ESP-IDF issue: https://github.com/espressif/esp-idf/issues/17459 + // Commit 9ed617fb17 removed gpio_func_sel() calls from uart_set_pin(), which breaks + // UART on default UART0 pins that may have residual state from boot console. + // Reset these pins before configuring UART to ensure they're in a clean state. + if (tx == U0TXD_GPIO_NUM || tx == U0RXD_GPIO_NUM) { + gpio_reset_pin(static_cast(tx)); + } + if (rx == U0TXD_GPIO_NUM || rx == U0RXD_GPIO_NUM) { + gpio_reset_pin(static_cast(rx)); + } + + // Setup pins after reset to preserve open drain/pullup/pulldown flags auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; @@ -154,10 +171,6 @@ void IDFUARTComponent::load_settings(bool dump_config) { setup_pin_if_needed(this->tx_pin_); } - int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; - int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; - int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; - uint32_t invert = 0; if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { invert |= UART_SIGNAL_TXD_INV; From 7216120bfd12f590b6cb84891cc5a14dce908c11 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 16 Dec 2025 00:48:30 -0600 Subject: [PATCH 10/43] [socket] Fix getpeername() returning local address instead of remote in LWIP raw TCP (#12475) --- esphome/components/socket/lwip_raw_tcp_impl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 5538206058..328df24bdd 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -188,7 +188,7 @@ class LWIPRawImpl : public Socket { errno = EINVAL; return -1; } - return this->ip2sockaddr_(&pcb_->local_ip, pcb_->local_port, name, addrlen); + return this->ip2sockaddr_(&pcb_->remote_ip, pcb_->remote_port, name, addrlen); } std::string getpeername() override { if (pcb_ == nullptr) { From 4d6a93f92de0aa16a0a61ac750d5f42076333a9d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:17:17 -0500 Subject: [PATCH 11/43] [uart] Fix UART on default UART0 pins for ESP-IDF (#12519) Co-authored-by: Claude --- .../uart/uart_component_esp_idf.cpp | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index b438e4f7a6..b4f6eedf91 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -9,6 +9,7 @@ #include "esphome/core/gpio.h" #include "driver/gpio.h" #include "soc/gpio_num.h" +#include "soc/uart_pins.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -139,6 +140,22 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } + int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; + int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; + int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; + + // Workaround for ESP-IDF issue: https://github.com/espressif/esp-idf/issues/17459 + // Commit 9ed617fb17 removed gpio_func_sel() calls from uart_set_pin(), which breaks + // UART on default UART0 pins that may have residual state from boot console. + // Reset these pins before configuring UART to ensure they're in a clean state. + if (tx == U0TXD_GPIO_NUM || tx == U0RXD_GPIO_NUM) { + gpio_reset_pin(static_cast(tx)); + } + if (rx == U0TXD_GPIO_NUM || rx == U0RXD_GPIO_NUM) { + gpio_reset_pin(static_cast(rx)); + } + + // Setup pins after reset to preserve open drain/pullup/pulldown flags auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; @@ -154,10 +171,6 @@ void IDFUARTComponent::load_settings(bool dump_config) { setup_pin_if_needed(this->tx_pin_); } - int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; - int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; - int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; - uint32_t invert = 0; if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { invert |= UART_SIGNAL_TXD_INV; From 9c88e44300748b63f35a7f9af1db707b974311a7 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:35:31 -0500 Subject: [PATCH 12/43] Bump version to 2025.12.0b5 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 039bba2136..ee19d5840d 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.12.0b4 +PROJECT_NUMBER = 2025.12.0b5 # 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 0aae3e2b17..9cdc210425 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.12.0b4" +__version__ = "2025.12.0b5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From fa3d998c3da9f95ec48de00553062ef6c14187b5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:15:50 -0500 Subject: [PATCH 13/43] Bump version to 2025.12.0 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index ee19d5840d..7dfcbd6b6f 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.12.0b5 +PROJECT_NUMBER = 2025.12.0 # 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 9cdc210425..111396cab5 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.12.0b5" +__version__ = "2025.12.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From fab4efb4690468004749296b2e7372fc6090a168 Mon Sep 17 00:00:00 2001 From: Jeff Zigler <123041141+zigboi@users.noreply.github.com> Date: Tue, 16 Dec 2025 16:42:12 -0800 Subject: [PATCH 14/43] [esp32] Fix serial logging on h2, c2 & c61 (#12522) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/logger/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index fb0ce92cc9..8968a5eab8 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -241,9 +241,12 @@ CONFIG_SCHEMA = cv.All( CONF_HARDWARE_UART, esp8266=UART0, esp32=UART0, + esp32_c2=UART0, esp32_c3=USB_SERIAL_JTAG, esp32_c5=USB_SERIAL_JTAG, esp32_c6=USB_SERIAL_JTAG, + esp32_c61=USB_SERIAL_JTAG, + esp32_h2=USB_SERIAL_JTAG, esp32_p4=USB_SERIAL_JTAG, esp32_s2=USB_CDC, esp32_s3=USB_SERIAL_JTAG, From 046ea922e8af37c2b91cff9c1e6c54f69fce0812 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Wed, 17 Dec 2025 01:42:52 +0100 Subject: [PATCH 15/43] [esp32] improve types and variable naming (#12423) --- esphome/components/esp32/__init__.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0142fd4841..b726a40508 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -118,8 +118,8 @@ ARDUINO_ALLOWED_VARIANTS = [ ] -def get_cpu_frequencies(*frequencies): - return [str(x) + "MHZ" for x in frequencies] +def get_cpu_frequencies(*frequencies: int) -> list[str]: + return [f"{frequency}MHZ" for frequency in frequencies] CPU_FREQUENCIES = { @@ -136,7 +136,7 @@ CPU_FREQUENCIES = { } # Make sure not missed here if a new variant added. -assert all(v in CPU_FREQUENCIES for v in VARIANTS) +assert all(variant in CPU_FREQUENCIES for variant in VARIANTS) FULL_CPU_FREQUENCIES = set(itertools.chain.from_iterable(CPU_FREQUENCIES.values())) @@ -250,10 +250,10 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): def add_idf_component( *, name: str, - repo: str = None, - ref: str = None, - path: str = None, - refresh: TimePeriod = None, + repo: str | None = None, + ref: str | None = None, + path: str | None = None, + refresh: TimePeriod | None = None, components: list[str] | None = None, submodules: list[str] | None = None, ): @@ -334,7 +334,7 @@ def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}" -def _is_framework_url(source: str) -> str: +def _is_framework_url(source: str) -> bool: # platformio accepts many URL schemes for framework repositories and archives including http, https, git, file, and symlink import urllib.parse @@ -1193,7 +1193,7 @@ APP_PARTITION_SIZES = { } -def get_arduino_partition_csv(flash_size): +def get_arduino_partition_csv(flash_size: str): app_partition_size = APP_PARTITION_SIZES[flash_size] eeprom_partition_size = 0x1000 # 4 KB spiffs_partition_size = 0xF000 # 60 KB @@ -1213,7 +1213,7 @@ spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size: """ -def get_idf_partition_csv(flash_size): +def get_idf_partition_csv(flash_size: str): app_partition_size = APP_PARTITION_SIZES[flash_size] return f"""\ From 93621d85b08d4a84d29c396cbcca9009b472f1f3 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Wed, 17 Dec 2025 01:43:10 +0100 Subject: [PATCH 16/43] [climate] Improve temperature unit regex (#12032) --- esphome/components/climate/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index b8e49db6c0..2150a30c3e 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -117,9 +117,7 @@ CONF_MIN_HUMIDITY = "min_humidity" CONF_MAX_HUMIDITY = "max_humidity" CONF_TARGET_HUMIDITY = "target_humidity" -visual_temperature = cv.float_with_unit( - "visual_temperature", "(°C|° C|°|C|°K|° K|K|°F|° F|F)?" -) +visual_temperature = cv.float_with_unit("visual_temperature", "(°|(° ?)?[CKF])?") VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema( From 9727c7135cdeeed19dbf09950ce4ccf24adc86b7 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Wed, 17 Dec 2025 01:43:18 +0100 Subject: [PATCH 17/43] [openthread] channel range, fix typo, use C++17 nested namespace syntax (#12422) --- esphome/components/openthread/__init__.py | 4 ++-- esphome/components/openthread/openthread.cpp | 7 ++----- esphome/components/openthread/openthread.h | 6 ++---- esphome/components/openthread/openthread_esp.cpp | 6 ++---- .../openthread_info/openthread_info_text_sensor.cpp | 6 ++---- .../openthread_info/openthread_info_text_sensor.h | 6 ++---- 6 files changed, 12 insertions(+), 23 deletions(-) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 5b1abe4fb5..050e45cdc9 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -91,7 +91,7 @@ def set_sdkconfig_options(config): add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT", True) add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT_MAX_SERVICES", 5) - # TODO: Add suport for synchronized sleepy end devices (SSED) + # TODO: Add support for synchronized sleepy end devices (SSED) add_idf_sdkconfig_option(f"CONFIG_OPENTHREAD_{config.get(CONF_DEVICE_TYPE)}", True) @@ -102,7 +102,7 @@ OpenThreadSrpComponent = openthread_ns.class_("OpenThreadSrpComponent", cg.Compo _CONNECTION_SCHEMA = cv.Schema( { cv.Optional(CONF_PAN_ID): cv.hex_int, - cv.Optional(CONF_CHANNEL): cv.int_, + cv.Optional(CONF_CHANNEL): cv.int_range(min=11, max=26), cv.Optional(CONF_NETWORK_KEY): cv.hex_int, cv.Optional(CONF_EXT_PAN_ID): cv.hex_int, cv.Optional(CONF_NETWORK_NAME): cv.string_strict, diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 721ab89326..90da17e2d3 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -21,8 +21,7 @@ static const char *const TAG = "openthread"; -namespace esphome { -namespace openthread { +namespace esphome::openthread { OpenThreadComponent *global_openthread_component = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -275,7 +274,5 @@ const char *OpenThreadComponent::get_use_address() const { return this->use_addr void OpenThreadComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } -} // namespace openthread -} // namespace esphome - +} // namespace esphome::openthread #endif diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 546128b366..3c60acaadd 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -13,8 +13,7 @@ #include #include -namespace esphome { -namespace openthread { +namespace esphome::openthread { class InstanceLock; @@ -91,6 +90,5 @@ class InstanceLock { InstanceLock() {} }; -} // namespace openthread -} // namespace esphome +} // namespace esphome::openthread #endif diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 72dc521091..b47e4b884a 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -24,8 +24,7 @@ static const char *const TAG = "openthread"; -namespace esphome { -namespace openthread { +namespace esphome::openthread { void OpenThreadComponent::setup() { // Used eventfds: @@ -209,6 +208,5 @@ otInstance *InstanceLock::get_instance() { return esp_openthread_get_instance(); InstanceLock::~InstanceLock() { esp_openthread_lock_release(); } -} // namespace openthread -} // namespace esphome +} // namespace esphome::openthread #endif diff --git a/esphome/components/openthread_info/openthread_info_text_sensor.cpp b/esphome/components/openthread_info/openthread_info_text_sensor.cpp index 10724f3e2f..fc61ad81b2 100644 --- a/esphome/components/openthread_info/openthread_info_text_sensor.cpp +++ b/esphome/components/openthread_info/openthread_info_text_sensor.cpp @@ -3,8 +3,7 @@ #ifdef USE_OPENTHREAD #include "esphome/core/log.h" -namespace esphome { -namespace openthread_info { +namespace esphome::openthread_info { static const char *const TAG = "openthread_info"; @@ -19,6 +18,5 @@ void NetworkKeyOpenThreadInfo::dump_config() { LOG_TEXT_SENSOR("", "Network Key" void PanIdOpenThreadInfo::dump_config() { LOG_TEXT_SENSOR("", "PAN ID", this); } void ExtPanIdOpenThreadInfo::dump_config() { LOG_TEXT_SENSOR("", "Extended PAN ID", this); } -} // namespace openthread_info -} // namespace esphome +} // namespace esphome::openthread_info #endif diff --git a/esphome/components/openthread_info/openthread_info_text_sensor.h b/esphome/components/openthread_info/openthread_info_text_sensor.h index bbcd2d4655..35e46212cb 100644 --- a/esphome/components/openthread_info/openthread_info_text_sensor.h +++ b/esphome/components/openthread_info/openthread_info_text_sensor.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #ifdef USE_OPENTHREAD -namespace esphome { -namespace openthread_info { +namespace esphome::openthread_info { using esphome::openthread::InstanceLock; @@ -213,6 +212,5 @@ class ExtPanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor: std::array last_extpanid_{}; }; -} // namespace openthread_info -} // namespace esphome +} // namespace esphome::openthread_info #endif From 9cd888cef6cff7a9309ac01542d228ccc39ce1b3 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:44:01 -0500 Subject: [PATCH 18/43] [spi] Use ESP-IDF driver for ESP32 Arduino (#12420) Co-authored-by: Claude --- esphome/components/spi/__init__.py | 13 ++++++----- esphome/components/spi/spi.cpp | 6 ++--- esphome/components/spi/spi.h | 31 ++++++++++---------------- esphome/components/spi/spi_arduino.cpp | 10 ++++----- esphome/components/spi/spi_esp_idf.cpp | 10 ++++----- 5 files changed, 30 insertions(+), 40 deletions(-) diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 88bb3406e1..ad279dcf1a 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -272,10 +272,11 @@ def validate_spi_config(config): # Given an SPI index, convert to a string that represents the C++ object for it. def get_spi_interface(index): - if CORE.using_esp_idf: + platform = get_target_platform() + if platform == PLATFORM_ESP32: + # ESP32 uses ESP-IDF SPI driver for both Arduino and IDF frameworks return ["SPI2_HOST", "SPI3_HOST"][index] # Arduino code follows - platform = get_target_platform() if platform == PLATFORM_RP2040: return ["&SPI", "&SPI1"][index] if index == 0: @@ -356,7 +357,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(configs): cg.add_define("USE_SPI") cg.add_global(spi_ns.using) - if CORE.using_arduino: + if CORE.using_arduino and not CORE.is_esp32: cg.add_library("SPI", None) for spi in configs: var = cg.new_Pvariable(spi[CONF_ID]) @@ -447,13 +448,15 @@ def final_validate_device_schema(name: str, *, require_mosi: bool, require_miso: FILTER_SOURCE_FILES = filter_source_files_from_platform( { "spi_arduino.cpp": { - PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.RP2040_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, - "spi_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + "spi_esp_idf.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, } ) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index 00e9845a03..c4876d1a74 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace spi { +namespace esphome::spi { const char *const TAG = "spi"; @@ -119,5 +118,4 @@ uint16_t SPIDelegateBitBash::transfer_(uint16_t data, size_t num_bits) { return out_data; } -} // namespace spi -} // namespace esphome +} // namespace esphome::spi diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 5bc80350da..43b55d72bc 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -1,4 +1,5 @@ #pragma once +#ifndef USE_ZEPHYR #include "esphome/core/application.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" @@ -7,7 +8,13 @@ #include #include -#ifdef USE_ARDUINO +#ifdef USE_ESP32 + +#include "driver/spi_master.h" + +using SPIInterface = spi_host_device_t; + +#elif defined(USE_ARDUINO) #include @@ -17,26 +24,12 @@ using SPIInterface = SPIClassRP2040 *; using SPIInterface = SPIClass *; #endif -#endif - -#ifdef USE_ESP_IDF - -#include "driver/spi_master.h" - -using SPIInterface = spi_host_device_t; - -#endif // USE_ESP_IDF - -#ifdef USE_ZEPHYR -// TODO supprse clang-tidy. Remove after SPI driver for nrf52 is added. -using SPIInterface = void *; -#endif +#endif // USE_ESP32 / USE_ARDUINO /** * Implementation of SPI Controller mode. */ -namespace esphome { -namespace spi { +namespace esphome::spi { /// The bit-order for SPI devices. This defines how the data read from and written to the device is interpreted. enum SPIBitOrder { @@ -509,5 +502,5 @@ class SPIDevice : public SPIClient { template void transfer_array(std::array &data) { this->transfer_array(data.data(), N); } }; -} // namespace spi -} // namespace esphome +} // namespace esphome::spi +#endif // USE_ZEPHYR diff --git a/esphome/components/spi/spi_arduino.cpp b/esphome/components/spi/spi_arduino.cpp index a34e3c3c82..4267fe63ce 100644 --- a/esphome/components/spi/spi_arduino.cpp +++ b/esphome/components/spi/spi_arduino.cpp @@ -1,9 +1,8 @@ #include "spi.h" #include -namespace esphome { -namespace spi { -#ifdef USE_ARDUINO +namespace esphome::spi { +#if defined(USE_ARDUINO) && !defined(USE_ESP32) static const char *const TAG = "spi-esp-arduino"; class SPIDelegateHw : public SPIDelegate { @@ -101,6 +100,5 @@ SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo return new SPIBusHw(clk, sdo, sdi, interface); } -#endif // USE_ARDUINO -} // namespace spi -} // namespace esphome +#endif // USE_ARDUINO && !USE_ESP32 +} // namespace esphome::spi diff --git a/esphome/components/spi/spi_esp_idf.cpp b/esphome/components/spi/spi_esp_idf.cpp index 549f516eb1..a1837fa58d 100644 --- a/esphome/components/spi/spi_esp_idf.cpp +++ b/esphome/components/spi/spi_esp_idf.cpp @@ -1,10 +1,9 @@ #include "spi.h" #include -namespace esphome { -namespace spi { +namespace esphome::spi { -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 static const char *const TAG = "spi-esp-idf"; static const size_t MAX_TRANSFER_SIZE = 4092; // dictated by ESP-IDF API. @@ -266,6 +265,5 @@ SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo return new SPIBusHw(clk, sdo, sdi, interface, data_pins); } -#endif -} // namespace spi -} // namespace esphome +#endif // USE_ESP32 +} // namespace esphome::spi From 18814f12dca7034e52935bbabc800427ffea501b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:44:14 -0500 Subject: [PATCH 19/43] [http_request] Use ESP-IDF for ESP32 Arduino (#12428) Co-authored-by: Claude --- esphome/components/http_request/__init__.py | 52 ++++++++----------- .../components/http_request/http_request.cpp | 6 +-- .../components/http_request/http_request.h | 6 +-- .../http_request/http_request_arduino.cpp | 15 ++---- .../http_request/http_request_arduino.h | 12 ++--- .../http_request/http_request_host.cpp | 6 +-- .../http_request/http_request_host.h | 7 ++- .../http_request/http_request_idf.cpp | 10 ++-- .../http_request/http_request_idf.h | 10 ++-- 9 files changed, 49 insertions(+), 75 deletions(-) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index f4fa448c5b..b133aa69b2 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -69,9 +69,6 @@ def validate_url(value): def validate_ssl_verification(config): error_message = "" - if CORE.is_esp32 and not CORE.using_esp_idf and config[CONF_VERIFY_SSL]: - error_message = "ESPHome supports certificate verification only via ESP-IDF" - if CORE.is_rp2040 and config[CONF_VERIFY_SSL]: error_message = "ESPHome does not support certificate verification on RP2040" @@ -93,9 +90,9 @@ def validate_ssl_verification(config): def _declare_request_class(value): if CORE.is_host: return cv.declare_id(HttpRequestHost)(value) - if CORE.using_esp_idf: + if CORE.is_esp32: return cv.declare_id(HttpRequestIDF)(value) - if CORE.is_esp8266 or CORE.is_esp32 or CORE.is_rp2040: + if CORE.is_esp8266 or CORE.is_rp2040: return cv.declare_id(HttpRequestArduino)(value) return NotImplementedError @@ -121,11 +118,11 @@ CONFIG_SCHEMA = cv.All( cv.positive_not_null_time_period, cv.positive_time_period_milliseconds, ), - cv.SplitDefault(CONF_BUFFER_SIZE_RX, esp32_idf=512): cv.All( - cv.uint16_t, cv.only_with_esp_idf + cv.SplitDefault(CONF_BUFFER_SIZE_RX, esp32=512): cv.All( + cv.uint16_t, cv.only_on_esp32 ), - cv.SplitDefault(CONF_BUFFER_SIZE_TX, esp32_idf=512): cv.All( - cv.uint16_t, cv.only_with_esp_idf + cv.SplitDefault(CONF_BUFFER_SIZE_TX, esp32=512): cv.All( + cv.uint16_t, cv.only_on_esp32 ), cv.Optional(CONF_CA_CERTIFICATE_PATH): cv.All( cv.file_, @@ -158,25 +155,20 @@ async def to_code(config): cg.add(var.set_watchdog_timeout(timeout_ms)) if CORE.is_esp32: - if CORE.using_esp_idf: - cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX])) - cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX])) + cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX])) + cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX])) - esp32.add_idf_sdkconfig_option( - "CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", - config.get(CONF_VERIFY_SSL), - ) - esp32.add_idf_sdkconfig_option( - "CONFIG_ESP_TLS_INSECURE", - not config.get(CONF_VERIFY_SSL), - ) - esp32.add_idf_sdkconfig_option( - "CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", - not config.get(CONF_VERIFY_SSL), - ) - else: - cg.add_library("NetworkClientSecure", None) - cg.add_library("HTTPClient", None) + if config.get(CONF_VERIFY_SSL): + esp32.add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) + + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_TLS_INSECURE", + not config.get(CONF_VERIFY_SSL), + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", + not config.get(CONF_VERIFY_SSL), + ) if CORE.is_esp8266: cg.add_library("ESP8266HTTPClient", None) if CORE.is_rp2040 and CORE.using_arduino: @@ -327,13 +319,15 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( { "http_request_host.cpp": {PlatformFramework.HOST_NATIVE}, "http_request_arduino.cpp": { - PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.RP2040_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, - "http_request_idf.cpp": {PlatformFramework.ESP32_IDF}, + "http_request_idf.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, } ) diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 806354baf1..11dde4715a 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -4,8 +4,7 @@ #include -namespace esphome { -namespace http_request { +namespace esphome::http_request { static const char *const TAG = "http_request"; @@ -42,5 +41,4 @@ std::string HttpContainer::get_response_header(const std::string &header_name) { } } -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 8adf13b954..1b5fd9f00e 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -15,8 +15,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace http_request { +namespace esphome::http_request { struct Header { std::string name; @@ -305,5 +304,4 @@ template class HttpRequestSendAction : public Action { size_t max_response_buffer_size_{SIZE_MAX}; }; -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index c64a7be554..a653942b18 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -1,6 +1,6 @@ #include "http_request_arduino.h" -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_ESP32) #include "esphome/components/network/util.h" #include "esphome/components/watchdog/watchdog.h" @@ -9,8 +9,7 @@ #include "esphome/core/defines.h" #include "esphome/core/log.h" -namespace esphome { -namespace http_request { +namespace esphome::http_request { static const char *const TAG = "http_request.arduino"; @@ -75,8 +74,6 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur container->client_.setInsecure(); } bool status = container->client_.begin(url.c_str()); -#elif defined(USE_ESP32) - bool status = container->client_.begin(url.c_str()); #endif App.feed_wdt(); @@ -90,9 +87,6 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur container->client_.setReuse(true); container->client_.setTimeout(this->timeout_); -#if defined(USE_ESP32) - container->client_.setConnectTimeout(this->timeout_); -#endif if (this->useragent_ != nullptr) { container->client_.setUserAgent(this->useragent_); @@ -177,7 +171,6 @@ void HttpContainerArduino::end() { this->client_.end(); } -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request -#endif // USE_ARDUINO +#endif // USE_ARDUINO && !USE_ESP32 diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index b736bb56d1..d9b5af9d81 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -2,9 +2,9 @@ #include "http_request.h" -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_ESP32) -#if defined(USE_ESP32) || defined(USE_RP2040) +#if defined(USE_RP2040) #include #include #endif @@ -15,8 +15,7 @@ #endif #endif -namespace esphome { -namespace http_request { +namespace esphome::http_request { class HttpRequestArduino; class HttpContainerArduino : public HttpContainer { @@ -36,7 +35,6 @@ class HttpRequestArduino : public HttpRequestComponent { const std::set &collect_headers) override; }; -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request -#endif // USE_ARDUINO +#endif // USE_ARDUINO && !USE_ESP32 diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index 402affc1d1..b94570be12 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -12,8 +12,7 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" -namespace esphome { -namespace http_request { +namespace esphome::http_request { static const char *const TAG = "http_request.host"; @@ -139,7 +138,6 @@ void HttpContainerHost::end() { this->bytes_read_ = 0; } -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request #endif // USE_HOST diff --git a/esphome/components/http_request/http_request_host.h b/esphome/components/http_request/http_request_host.h index 886ba94938..32e149e6a3 100644 --- a/esphome/components/http_request/http_request_host.h +++ b/esphome/components/http_request/http_request_host.h @@ -2,8 +2,8 @@ #ifdef USE_HOST #include "http_request.h" -namespace esphome { -namespace http_request { + +namespace esphome::http_request { class HttpRequestHost; class HttpContainerHost : public HttpContainer { @@ -27,7 +27,6 @@ class HttpRequestHost : public HttpRequestComponent { const char *ca_path_{}; }; -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request #endif // USE_HOST diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 34a3fb87eb..725a9c1c1e 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -1,6 +1,6 @@ #include "http_request_idf.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/components/network/util.h" #include "esphome/components/watchdog/watchdog.h" @@ -14,8 +14,7 @@ #include "esp_task_wdt.h" -namespace esphome { -namespace http_request { +namespace esphome::http_request { static const char *const TAG = "http_request.idf"; @@ -245,7 +244,6 @@ void HttpContainerIDF::feed_wdt() { } } -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index e51b3aaebc..4dc4736423 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -2,15 +2,14 @@ #include "http_request.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #include #include #include -namespace esphome { -namespace http_request { +namespace esphome::http_request { class HttpContainerIDF : public HttpContainer { public: @@ -48,7 +47,6 @@ class HttpRequestIDF : public HttpRequestComponent { static esp_err_t http_event_handler(esp_http_client_event_t *evt); }; -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request -#endif // USE_ESP_IDF +#endif // USE_ESP32 From 08beaf875008019f6d6ecb24e8f55e1b0c9143bb Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 16 Dec 2025 20:06:12 -0500 Subject: [PATCH 20/43] [esp32] Remove Arduino-specific code from core.cpp (#12501) Co-authored-by: Claude --- .clang-tidy.hash | 2 +- esphome/components/esp32/__init__.py | 6 ---- esphome/components/esp32/core.cpp | 50 +++++----------------------- esphome/core/defines.h | 2 +- sdkconfig.defaults | 1 - tests/script/test_clang_tidy_hash.py | 2 +- 6 files changed, 12 insertions(+), 51 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index a3322ba731..13c7ce5f97 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -766420905c06eeb6c5f360f68fd965e5ddd9c4a5db6b823263d3ad3accb64a07 +6857423aecf90accd0a8bf584d36ee094a4938f872447a4efc05a2efc6dc6481 diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b726a40508..1379fd705f 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -986,14 +986,8 @@ async def to_code(config): f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})" ), ) - add_idf_sdkconfig_option( - "CONFIG_ARDUINO_LOOP_STACK_SIZE", - conf[CONF_ADVANCED][CONF_LOOP_TASK_STACK_SIZE], - ) - add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) - add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True) # ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency if get_esp32_variant() == VARIANT_ESP32S2: diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 6215ff862f..d8cc909c83 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -4,25 +4,20 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" -#include -#include +#include +#include #include #include #include #include -#include +#include +#include -#include +void setup(); // NOLINT(readability-redundant-declaration) +void loop(); // NOLINT(readability-redundant-declaration) -#ifdef USE_ARDUINO -#include -#else -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) -#include -#endif -void setup(); -void loop(); -#endif +// Weak stub for initArduino - overridden when the Arduino component is present +extern "C" __attribute__((weak)) void initArduino() {} namespace esphome { @@ -41,19 +36,7 @@ void arch_restart() { void arch_init() { // Enable the task watchdog only on the loop task (from which we're currently running) -#if defined(USE_ESP_IDF) esp_task_wdt_add(nullptr); - // Idle task watchdog is disabled on ESP-IDF -#elif defined(USE_ARDUINO) - enableLoopWDT(); - // Disable idle task watchdog on the core we're using (Arduino pins the task to a core) -#if defined(CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0) && CONFIG_ARDUINO_RUNNING_CORE == 0 - disableCore0WDT(); -#endif -#if defined(CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1) && CONFIG_ARDUINO_RUNNING_CORE == 1 - disableCore1WDT(); -#endif -#endif // If the bootloader was compiled with CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE the current // partition will get rolled back unless it is marked as valid. @@ -71,21 +54,10 @@ uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { uint32_t freq = 0; -#ifdef USE_ESP_IDF -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq); -#else - rtc_cpu_freq_config_t config; - rtc_clk_cpu_freq_get_config(&config); - freq = config.freq_mhz * 1000000U; -#endif -#elif defined(USE_ARDUINO) - freq = ESP.getCpuFreqMHz() * 1000000; -#endif return freq; } -#ifdef USE_ESP_IDF TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void loop_task(void *pv_params) { @@ -96,6 +68,7 @@ void loop_task(void *pv_params) { } extern "C" void app_main() { + initArduino(); esp32::setup_preferences(); #if CONFIG_FREERTOS_UNICORE xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle); @@ -103,11 +76,6 @@ extern "C" void app_main() { xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1); #endif } -#endif // USE_ESP_IDF - -#ifdef USE_ARDUINO -extern "C" void init() { esp32::setup_preferences(); } -#endif // USE_ARDUINO } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 750cab5bba..986ab9eff3 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -165,7 +165,6 @@ // IDF-specific feature flags #ifdef USE_ESP_IDF #define USE_MQTT_IDF_ENQUEUE -#define ESPHOME_LOOP_TASK_STACK_SIZE 8192 #endif // ESP32-specific feature flags @@ -197,6 +196,7 @@ #define ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT 1 #define ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT 1 #define ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT 2 +#define ESPHOME_LOOP_TASK_STACK_SIZE 8192 #define USE_ESP32_CAMERA_JPEG_ENCODER #define USE_HTTP_REQUEST_RESPONSE #define USE_I2C diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 322efb701a..72ca3f6e9c 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -13,7 +13,6 @@ CONFIG_ESP_TASK_WDT=y CONFIG_ESP_TASK_WDT_PANIC=y CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n -CONFIG_AUTOSTART_ARDUINO=y # esp32_ble CONFIG_BT_ENABLED=y diff --git a/tests/script/test_clang_tidy_hash.py b/tests/script/test_clang_tidy_hash.py index b1690a6a2d..e19e7886a2 100644 --- a/tests/script/test_clang_tidy_hash.py +++ b/tests/script/test_clang_tidy_hash.py @@ -49,7 +49,7 @@ def test_calculate_clang_tidy_hash_with_sdkconfig(tmp_path: Path) -> None: clang_tidy_content = b"Checks: '-*,readability-*'\n" requirements_version = "clang-tidy==18.1.5" platformio_content = b"[env:esp32]\nplatform = espressif32\n" - sdkconfig_content = b"CONFIG_AUTOSTART_ARDUINO=y\n" + sdkconfig_content = b"" requirements_content = "clang-tidy==18.1.5\n" # Create temporary files From 431183eebcb2450be6e9327f7a26f5344ea05926 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 16 Dec 2025 20:07:09 -0500 Subject: [PATCH 21/43] [ledc,mqtt,resampler] Remove unnecessary ESP-IDF framework restrictions (#12442) Co-authored-by: Claude --- esphome/components/ledc/output.py | 2 +- esphome/components/mqtt/__init__.py | 10 +++++----- esphome/components/resampler/speaker/__init__.py | 4 +--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py index 2133c4daf9..5e74677a84 100644 --- a/esphome/components/ledc/output.py +++ b/esphome/components/ledc/output.py @@ -48,7 +48,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.frequency, cv.Optional(CONF_CHANNEL): cv.int_range(min=0, max=15), cv.Optional(CONF_PHASE_ANGLE): cv.All( - cv.only_with_esp_idf, cv.angle, cv.float_range(min=0.0, max=360.0) + cv.angle, cv.float_range(min=0.0, max=360.0) ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 237ed2ce38..e73de49fef 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -233,11 +233,11 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PASSWORD, default=""): cv.string, cv.Optional(CONF_CLEAN_SESSION, default=False): cv.boolean, cv.Optional(CONF_CLIENT_ID): cv.string, - cv.SplitDefault(CONF_IDF_SEND_ASYNC, esp32_idf=False): cv.All( - cv.boolean, cv.only_with_esp_idf + cv.SplitDefault(CONF_IDF_SEND_ASYNC, esp32=False): cv.All( + cv.boolean, cv.only_on_esp32 ), cv.Optional(CONF_CERTIFICATE_AUTHORITY): cv.All( - cv.string, cv.only_with_esp_idf + cv.string, cv.only_on_esp32 ), cv.Inclusive(CONF_CLIENT_CERTIFICATE, "cert-key-pair"): cv.All( cv.string, cv.only_on_esp32 @@ -245,8 +245,8 @@ CONFIG_SCHEMA = cv.All( cv.Inclusive(CONF_CLIENT_CERTIFICATE_KEY, "cert-key-pair"): cv.All( cv.string, cv.only_on_esp32 ), - cv.SplitDefault(CONF_SKIP_CERT_CN_CHECK, esp32_idf=False): cv.All( - cv.boolean, cv.only_with_esp_idf + cv.SplitDefault(CONF_SKIP_CERT_CN_CHECK, esp32=False): cv.All( + cv.boolean, cv.only_on_esp32 ), cv.Optional(CONF_DISCOVERY, default=True): cv.Any( cv.boolean, cv.one_of("CLEAN", upper=True) diff --git a/esphome/components/resampler/speaker/__init__.py b/esphome/components/resampler/speaker/__init__.py index def62547b2..7036862d14 100644 --- a/esphome/components/resampler/speaker/__init__.py +++ b/esphome/components/resampler/speaker/__init__.py @@ -63,9 +63,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_BUFFER_DURATION, default="100ms" ): cv.positive_time_period_milliseconds, - cv.SplitDefault(CONF_TASK_STACK_IN_PSRAM, esp32_idf=False): cv.All( - cv.boolean, cv.only_with_esp_idf - ), + cv.Optional(CONF_TASK_STACK_IN_PSRAM, default=False): cv.boolean, cv.Optional(CONF_FILTERS, default=16): cv.int_range(min=2, max=1024), cv.Optional(CONF_TAPS, default=16): _validate_taps, } From 1122ec354f994823b2d4e1f32d0056fea35e2434 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 16 Dec 2025 20:07:57 -0500 Subject: [PATCH 22/43] [esp32] Add OTA rollback support (#12460) Co-authored-by: Claude --- esphome/components/esp32/__init__.py | 16 ++++++++++++++++ esphome/components/esp32/core.cpp | 14 +++++--------- esphome/components/safe_mode/safe_mode.cpp | 16 ++++++++++++++++ esphome/core/defines.h | 1 + tests/components/esp32/test.esp32-idf.yaml | 1 + 5 files changed, 39 insertions(+), 9 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 1379fd705f..4448b6bbe7 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( CONF_ADVANCED, CONF_BOARD, CONF_COMPONENTS, + CONF_DISABLED, CONF_ESPHOME, CONF_FRAMEWORK, CONF_IGNORE_EFUSE_CUSTOM_MAC, @@ -24,6 +25,7 @@ from esphome.const import ( CONF_PLATFORMIO_OPTIONS, CONF_REF, CONF_REFRESH, + CONF_SAFE_MODE, CONF_SOURCE, CONF_TYPE, CONF_VARIANT, @@ -81,6 +83,7 @@ CONF_ASSERTION_LEVEL = "assertion_level" CONF_COMPILER_OPTIMIZATION = "compiler_optimization" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" +CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_RELEASE = "release" @@ -571,6 +574,13 @@ def final_validate(config): path=[CONF_FLASH_SIZE], ) ) + if advanced[CONF_ENABLE_OTA_ROLLBACK]: + safe_mode_config = full_config.get(CONF_SAFE_MODE) + if safe_mode_config is None or safe_mode_config.get(CONF_DISABLED, False): + _LOGGER.warning( + "OTA rollback requires safe_mode, disabling rollback support" + ) + advanced[CONF_ENABLE_OTA_ROLLBACK] = False if errs: raise cv.MultipleInvalid(errs) @@ -691,6 +701,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( min=8192, max=32768 ), + cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean, } ), cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( @@ -1158,6 +1169,11 @@ async def to_code(config): "CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH", True ) + # Enable OTA rollback support + if advanced[CONF_ENABLE_OTA_ROLLBACK]: + add_idf_sdkconfig_option("CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE", True) + cg.add_define("USE_OTA_ROLLBACK") + cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE]) cg.add_define( diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index d8cc909c83..09a45c14a6 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -38,15 +38,11 @@ void arch_init() { // Enable the task watchdog only on the loop task (from which we're currently running) esp_task_wdt_add(nullptr); - // If the bootloader was compiled with CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE the current - // partition will get rolled back unless it is marked as valid. - esp_ota_img_states_t state; - const esp_partition_t *running = esp_ota_get_running_partition(); - if (esp_ota_get_state_partition(running, &state) == ESP_OK) { - if (state == ESP_OTA_IMG_PENDING_VERIFY) { - esp_ota_mark_app_valid_cancel_rollback(); - } - } + // Handle OTA rollback: mark partition valid immediately unless USE_OTA_ROLLBACK is enabled, + // in which case safe_mode will mark it valid after confirming successful boot. +#ifndef USE_OTA_ROLLBACK + esp_ota_mark_app_valid_cancel_rollback(); +#endif } void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); } diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index 62bbca4fb1..f8e5d7d8e5 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -9,6 +9,10 @@ #include #include +#ifdef USE_OTA_ROLLBACK +#include +#endif + namespace esphome { namespace safe_mode { @@ -32,6 +36,14 @@ void SafeModeComponent::dump_config() { ESP_LOGW(TAG, "SAFE MODE IS ACTIVE"); } } + +#ifdef USE_OTA_ROLLBACK + const esp_partition_t *last_invalid = esp_ota_get_last_invalid_partition(); + if (last_invalid != nullptr) { + ESP_LOGW(TAG, "OTA rollback detected! Rolled back from partition '%s'", last_invalid->label); + ESP_LOGW(TAG, "The device reset before the boot was marked successful"); + } +#endif } float SafeModeComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; } @@ -42,6 +54,10 @@ void SafeModeComponent::loop() { ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter"); this->clean_rtc(); this->boot_successful_ = true; +#ifdef USE_OTA_ROLLBACK + // Mark OTA partition as valid to prevent rollback + esp_ota_mark_app_valid_cancel_rollback(); +#endif // Disable loop since we no longer need to check this->disable_loop(); } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 986ab9eff3..4cbe683723 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -170,6 +170,7 @@ // ESP32-specific feature flags #ifdef USE_ESP32 #define USE_ESPHOME_TASK_LOG_BUFFER +#define USE_OTA_ROLLBACK #define USE_BLUETOOTH_PROXY #define BLUETOOTH_PROXY_MAX_CONNECTIONS 3 diff --git a/tests/components/esp32/test.esp32-idf.yaml b/tests/components/esp32/test.esp32-idf.yaml index 6338fe98dd..0e220623a1 100644 --- a/tests/components/esp32/test.esp32-idf.yaml +++ b/tests/components/esp32/test.esp32-idf.yaml @@ -3,6 +3,7 @@ esp32: framework: type: esp-idf advanced: + enable_ota_rollback: true enable_lwip_mdns_queries: true enable_lwip_bridge_interface: true disable_libc_locks_in_iram: false # Test explicit opt-out of RAM optimization From 084f517a20dce7a259a67fa56c90ea4561cb07b1 Mon Sep 17 00:00:00 2001 From: Stuart Parmenter Date: Tue, 16 Dec 2025 19:12:33 -0800 Subject: [PATCH 23/43] [hub75] Add set_brightness action (#12521) --- esphome/components/hub75/display.py | 29 ++++++++++++++++++- esphome/components/hub75/hub75.cpp | 2 +- esphome/components/hub75/hub75_component.h | 12 ++++++-- tests/components/hub75/common.yaml | 12 ++++++++ tests/components/hub75/test.esp32-idf.yaml | 7 ++--- .../hub75/test.esp32-s3-idf-board.yaml | 7 ++--- tests/components/hub75/test.esp32-s3-idf.yaml | 7 ++--- 7 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 tests/components/hub75/common.yaml diff --git a/esphome/components/hub75/display.py b/esphome/components/hub75/display.py index 81dd4ffc1c..f401f35406 100644 --- a/esphome/components/hub75/display.py +++ b/esphome/components/hub75/display.py @@ -1,6 +1,6 @@ from typing import Any -from esphome import pins +from esphome import automation, pins import esphome.codegen as cg from esphome.components import display from esphome.components.esp32 import add_idf_component @@ -17,6 +17,8 @@ from esphome.const import ( CONF_OE_PIN, CONF_UPDATE_INTERVAL, ) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType import esphome.final_validate as fv from esphome.types import ConfigType @@ -135,6 +137,7 @@ CLOCK_SPEEDS = { HUB75Display = hub75_ns.class_("HUB75Display", cg.PollingComponent, display.Display) Hub75Config = cg.global_ns.struct("Hub75Config") Hub75Pins = cg.global_ns.struct("Hub75Pins") +SetBrightnessAction = hub75_ns.class_("SetBrightnessAction", automation.Action) def _merge_board_pins(config: ConfigType) -> ConfigType: @@ -576,3 +579,27 @@ async def to_code(config: ConfigType) -> None: config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) + + +@automation.register_action( + "hub75.set_brightness", + SetBrightnessAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(HUB75Display), + cv.Required(CONF_BRIGHTNESS): cv.templatable(cv.int_range(min=0, max=255)), + }, + key=CONF_BRIGHTNESS, + ), +) +async def hub75_set_brightness_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, cg.uint8) + cg.add(var.set_brightness(template_)) + return var diff --git a/esphome/components/hub75/hub75.cpp b/esphome/components/hub75/hub75.cpp index e023e446c4..7317174831 100644 --- a/esphome/components/hub75/hub75.cpp +++ b/esphome/components/hub75/hub75.cpp @@ -179,7 +179,7 @@ void HOT HUB75Display::draw_pixels_at(int x_start, int y_start, int w, int h, co } } -void HUB75Display::set_brightness(int brightness) { +void HUB75Display::set_brightness(uint8_t brightness) { this->brightness_ = brightness; this->enabled_ = (brightness > 0); if (this->driver_ != nullptr) { diff --git a/esphome/components/hub75/hub75_component.h b/esphome/components/hub75/hub75_component.h index 49d4274483..f0e7ea10d5 100644 --- a/esphome/components/hub75/hub75_component.h +++ b/esphome/components/hub75/hub75_component.h @@ -5,6 +5,7 @@ #include #include "esphome/components/display/display_buffer.h" +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" @@ -34,7 +35,7 @@ class HUB75Display : public display::Display { display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override; // Brightness control (runtime mutable) - void set_brightness(int brightness); + void set_brightness(uint8_t brightness); protected: // Display internal methods @@ -46,10 +47,17 @@ class HUB75Display : public display::Display { Hub75Config config_; // Immutable configuration // Runtime state (mutable) - int brightness_{128}; + uint8_t brightness_{128}; bool enabled_{false}; }; +template class SetBrightnessAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, brightness) + + void play(const Ts &...x) override { this->parent_->set_brightness(this->brightness_.value(x...)); } +}; + } // namespace esphome::hub75 #endif diff --git a/tests/components/hub75/common.yaml b/tests/components/hub75/common.yaml new file mode 100644 index 0000000000..87e9e1c128 --- /dev/null +++ b/tests/components/hub75/common.yaml @@ -0,0 +1,12 @@ +esphome: + on_boot: + # Test simple value + - hub75.set_brightness: 200 + + # Test templatable value + - hub75.set_brightness: !lambda 'return 100;' + + # Test with explicit ID + - hub75.set_brightness: + id: my_hub75 + brightness: 50 diff --git a/tests/components/hub75/test.esp32-idf.yaml b/tests/components/hub75/test.esp32-idf.yaml index 9f6bd57292..dad2a02c24 100644 --- a/tests/components/hub75/test.esp32-idf.yaml +++ b/tests/components/hub75/test.esp32-idf.yaml @@ -1,8 +1,3 @@ -esp32: - board: esp32dev - framework: - type: esp-idf - display: - platform: hub75 id: my_hub75 @@ -37,3 +32,5 @@ display: then: lambda: |- ESP_LOGD("display", "1 -> 2"); + +<<: !include common.yaml diff --git a/tests/components/hub75/test.esp32-s3-idf-board.yaml b/tests/components/hub75/test.esp32-s3-idf-board.yaml index 9568ccf3aa..3723a80006 100644 --- a/tests/components/hub75/test.esp32-s3-idf-board.yaml +++ b/tests/components/hub75/test.esp32-s3-idf-board.yaml @@ -1,8 +1,3 @@ -esp32: - board: esp32-s3-devkitc-1 - framework: - type: esp-idf - display: - platform: hub75 id: hub75_display_board @@ -24,3 +19,5 @@ display: then: lambda: |- ESP_LOGD("display", "1 -> 2"); + +<<: !include common.yaml diff --git a/tests/components/hub75/test.esp32-s3-idf.yaml b/tests/components/hub75/test.esp32-s3-idf.yaml index db678c98a4..f8ee26e73d 100644 --- a/tests/components/hub75/test.esp32-s3-idf.yaml +++ b/tests/components/hub75/test.esp32-s3-idf.yaml @@ -1,8 +1,3 @@ -esp32: - board: esp32-s3-devkitc-1 - framework: - type: esp-idf - display: - platform: hub75 id: my_hub75 @@ -37,3 +32,5 @@ display: then: lambda: |- ESP_LOGD("display", "1 -> 2"); + +<<: !include common.yaml From a065990ab9f4818342579d0ed49fc1d1a1697002 Mon Sep 17 00:00:00 2001 From: Roger Fachini Date: Tue, 16 Dec 2025 19:20:12 -0800 Subject: [PATCH 24/43] [update] Add check action to trigger update checks (#12415) --- esphome/components/update/__init__.py | 18 ++++++++++++++++++ esphome/components/update/automation.h | 5 +++++ tests/components/update/common.yaml | 2 ++ 3 files changed, 25 insertions(+) diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index 7a381c85a8..e146f7e685 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -29,6 +29,9 @@ UpdateInfo = update_ns.struct("UpdateInfo") PerformAction = update_ns.class_( "PerformAction", automation.Action, cg.Parented.template(UpdateEntity) ) +CheckAction = update_ns.class_( + "CheckAction", automation.Action, cg.Parented.template(UpdateEntity) +) IsAvailableCondition = update_ns.class_( "IsAvailableCondition", automation.Condition, cg.Parented.template(UpdateEntity) ) @@ -143,6 +146,21 @@ async def update_perform_action_to_code(config, action_id, template_arg, args): return var +@automation.register_action( + "update.check", + CheckAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(UpdateEntity), + } + ), +) +async def update_check_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + @automation.register_condition( "update.is_available", IsAvailableCondition, diff --git a/esphome/components/update/automation.h b/esphome/components/update/automation.h index 8563b855fe..af24c838b1 100644 --- a/esphome/components/update/automation.h +++ b/esphome/components/update/automation.h @@ -14,6 +14,11 @@ template class PerformAction : public Action, public Pare void play(const Ts &...x) override { this->parent_->perform(this->force_.value(x...)); } }; +template class CheckAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->check(); } +}; + template class IsAvailableCondition : public Condition, public Parented { public: bool check(const Ts &...x) override { return this->parent_->state == UPDATE_STATE_AVAILABLE; } diff --git a/tests/components/update/common.yaml b/tests/components/update/common.yaml index 521a0a6a5c..40042945c8 100644 --- a/tests/components/update/common.yaml +++ b/tests/components/update/common.yaml @@ -9,6 +9,8 @@ esphome: update.is_available: then: - logger.log: "Update available" + else: + - update.check: - update.perform: force_update: true From 56c1691d72818ec41b42bb0c77d60c3d352af490 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Wed, 17 Dec 2025 04:52:28 +0100 Subject: [PATCH 25/43] [pca9685,sx126x,sx127x] Use frequency/float_range check (#12490) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/ade7880/sensor.py | 2 +- esphome/components/cc1101/__init__.py | 2 +- esphome/components/esp32_camera/__init__.py | 2 +- esphome/components/esp8266_pwm/output.py | 2 +- esphome/components/i2c/__init__.py | 2 +- esphome/components/ledc/output.py | 4 +++- esphome/components/libretiny_pwm/output.py | 4 +++- esphome/components/pca9685/__init__.py | 2 +- esphome/components/rp2040_pwm/output.py | 2 +- esphome/components/sx126x/__init__.py | 8 ++++++-- esphome/components/sx127x/__init__.py | 8 ++++++-- 11 files changed, 25 insertions(+), 13 deletions(-) diff --git a/esphome/components/ade7880/sensor.py b/esphome/components/ade7880/sensor.py index 39dbeb225f..beb74d7310 100644 --- a/esphome/components/ade7880/sensor.py +++ b/esphome/components/ade7880/sensor.py @@ -227,7 +227,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(ADE7880), cv.Optional(CONF_FREQUENCY, default="50Hz"): cv.All( - cv.frequency, cv.Range(min=45.0, max=66.0) + cv.frequency, cv.float_range(min=45.0, max=66.0) ), cv.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema, cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema, diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index 1971817fb1..e314da7079 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -165,7 +165,7 @@ CONFIG_MAP = { CONF_OUTPUT_POWER: cv.float_range(min=-30.0, max=11.0), CONF_RX_ATTENUATION: cv.enum(RX_ATTENUATION, upper=False), CONF_DC_BLOCKING_FILTER: cv.boolean, - CONF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=300000000, max=928000000)), + CONF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)), CONF_IF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=25000, max=788000)), CONF_FILTER_BANDWIDTH: cv.All(cv.frequency, cv.float_range(min=58000, max=812000)), CONF_CHANNEL: cv.uint8_t, diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index d9d9bc0a56..4182683bdc 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -186,7 +186,7 @@ CONFIG_SCHEMA = cv.All( { cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number, cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All( - cv.frequency, cv.Range(min=8e6, max=20e6) + cv.frequency, cv.float_range(min=8e6, max=20e6) ), } ), diff --git a/esphome/components/esp8266_pwm/output.py b/esphome/components/esp8266_pwm/output.py index 1404ef8ac3..2ddf4b9014 100644 --- a/esphome/components/esp8266_pwm/output.py +++ b/esphome/components/esp8266_pwm/output.py @@ -16,7 +16,7 @@ def valid_pwm_pin(value): esp8266_pwm_ns = cg.esphome_ns.namespace("esp8266_pwm") ESP8266PWM = esp8266_pwm_ns.class_("ESP8266PWM", output.FloatOutput, cg.Component) SetFrequencyAction = esp8266_pwm_ns.class_("SetFrequencyAction", automation.Action) -validate_frequency = cv.All(cv.frequency, cv.Range(min=1.0e-6)) +validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6)) CONFIG_SCHEMA = cv.All( output.FLOAT_OUTPUT_SCHEMA.extend( diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 9e7c9d702c..7706484e97 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -121,7 +121,7 @@ CONFIG_SCHEMA = cv.All( nrf52="100kHz", ): cv.All( cv.frequency, - cv.Range(min=0, min_included=False), + cv.float_range(min=0, min_included=False), ), cv.Optional(CONF_TIMEOUT): cv.All( cv.only_with_framework(["arduino", "esp-idf"]), diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py index 5e74677a84..7a45b9dc3f 100644 --- a/esphome/components/ledc/output.py +++ b/esphome/components/ledc/output.py @@ -45,7 +45,9 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.Required(CONF_ID): cv.declare_id(LEDCOutput), cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, - cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.frequency, + cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.All( + cv.frequency, cv.float_range(min=0, min_included=False) + ), cv.Optional(CONF_CHANNEL): cv.int_range(min=0, max=15), cv.Optional(CONF_PHASE_ANGLE): cv.All( cv.angle, cv.float_range(min=0.0, max=360.0) diff --git a/esphome/components/libretiny_pwm/output.py b/esphome/components/libretiny_pwm/output.py index 1eb4869da3..28556514d8 100644 --- a/esphome/components/libretiny_pwm/output.py +++ b/esphome/components/libretiny_pwm/output.py @@ -14,7 +14,9 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.Required(CONF_ID): cv.declare_id(LibreTinyPWM), cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, - cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.frequency, + cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.All( + cv.frequency, cv.float_range(min=0, min_included=False) + ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/pca9685/__init__.py b/esphome/components/pca9685/__init__.py index 56101c2d62..0e238ff7da 100644 --- a/esphome/components/pca9685/__init__.py +++ b/esphome/components/pca9685/__init__.py @@ -38,7 +38,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(PCA9685Output), cv.Optional(CONF_FREQUENCY): cv.All( - cv.frequency, cv.Range(min=23.84, max=1525.88) + cv.frequency, cv.float_range(min=23.84, max=1525.88) ), cv.Optional(CONF_EXTERNAL_CLOCK_INPUT, default=False): cv.boolean, cv.Optional(CONF_PHASE_BALANCER, default="linear"): cv.enum( diff --git a/esphome/components/rp2040_pwm/output.py b/esphome/components/rp2040_pwm/output.py index ac1892fa29..441a52de7f 100644 --- a/esphome/components/rp2040_pwm/output.py +++ b/esphome/components/rp2040_pwm/output.py @@ -11,7 +11,7 @@ DEPENDENCIES = ["rp2040"] rp2040_pwm_ns = cg.esphome_ns.namespace("rp2040_pwm") RP2040PWM = rp2040_pwm_ns.class_("RP2040PWM", output.FloatOutput, cg.Component) SetFrequencyAction = rp2040_pwm_ns.class_("SetFrequencyAction", automation.Action) -validate_frequency = cv.All(cv.frequency, cv.Range(min=1.0e-6)) +validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6)) CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py index 4641db6483..ed878ed0d4 100644 --- a/esphome/components/sx126x/__init__.py +++ b/esphome/components/sx126x/__init__.py @@ -199,9 +199,13 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_CRC_INITIAL, default=0x1D0F): cv.All( cv.hex_int, cv.Range(min=0, max=0xFFFF) ), - cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000), + cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All( + cv.frequency, cv.float_range(min=0, max=100000) + ), cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema, - cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000), + cv.Required(CONF_FREQUENCY): cv.All( + cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6) + ), cv.Required(CONF_HW_VERSION): cv.one_of( "sx1261", "sx1262", "sx1268", "llcc68", lower=True ), diff --git a/esphome/components/sx127x/__init__.py b/esphome/components/sx127x/__init__.py index b569a75972..f3a9cca93f 100644 --- a/esphome/components/sx127x/__init__.py +++ b/esphome/components/sx127x/__init__.py @@ -196,9 +196,13 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_BITSYNC): cv.boolean, cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE), cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean, - cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000), + cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All( + cv.frequency, cv.float_range(min=0, max=100000) + ), cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema, - cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000), + cv.Required(CONF_FREQUENCY): cv.All( + cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6) + ), cv.Required(CONF_MODULATION): cv.enum(MOD), cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True), cv.Optional(CONF_PA_PIN, default="BOOST"): cv.enum(PA_PIN), From 9928ab09cf94a63893d3fd10aaf0ffd937b147ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Dec 2025 09:29:43 -0700 Subject: [PATCH 26/43] [time] Convert to C++17 nested namespace syntax (#12463) --- esphome/components/time/automation.cpp | 6 ++---- esphome/components/time/automation.h | 6 ++---- esphome/components/time/real_time_clock.cpp | 6 ++---- esphome/components/time/real_time_clock.h | 6 ++---- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/esphome/components/time/automation.cpp b/esphome/components/time/automation.cpp index f7c1916ffe..8bc87878d1 100644 --- a/esphome/components/time/automation.cpp +++ b/esphome/components/time/automation.cpp @@ -4,8 +4,7 @@ #include -namespace esphome { -namespace time { +namespace esphome::time { static const char *const TAG = "automation"; static const int MAX_TIMESTAMP_DRIFT = 900; // how far can the clock drift before we consider @@ -92,5 +91,4 @@ SyncTrigger::SyncTrigger(RealTimeClock *rtc) : rtc_(rtc) { rtc->add_on_time_sync_callback([this]() { this->trigger(); }); } -} // namespace time -} // namespace esphome +} // namespace esphome::time diff --git a/esphome/components/time/automation.h b/esphome/components/time/automation.h index b5c8291533..4ccfc641d6 100644 --- a/esphome/components/time/automation.h +++ b/esphome/components/time/automation.h @@ -8,8 +8,7 @@ #include -namespace esphome { -namespace time { +namespace esphome::time { class CronTrigger : public Trigger<>, public Component { public: @@ -48,5 +47,4 @@ class SyncTrigger : public Trigger<>, public Component { protected: RealTimeClock *rtc_; }; -} // namespace time -} // namespace esphome +} // namespace esphome::time diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 175cee0c1f..639af4457f 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -17,8 +17,7 @@ #include -namespace esphome { -namespace time { +namespace esphome::time { static const char *const TAG = "time"; @@ -78,5 +77,4 @@ void RealTimeClock::apply_timezone_() { } #endif -} // namespace time -} // namespace esphome +} // namespace esphome::time diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 2f17bd86d6..70469e11b0 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -7,8 +7,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/time.h" -namespace esphome { -namespace time { +namespace esphome::time { /// The RealTimeClock class exposes common timekeeping functions via the device's local real-time clock. /// @@ -75,5 +74,4 @@ template class TimeHasTimeCondition : public Condition { RealTimeClock *parent_; }; -} // namespace time -} // namespace esphome +} // namespace esphome::time From bf6a03d1cf70f5ec8d04a129c09a941b14593b3f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Dec 2025 09:29:51 -0700 Subject: [PATCH 27/43] [factory_reset] Optimize memory by storing interval as uint16_t seconds (#12462) --- esphome/components/factory_reset/__init__.py | 6 ++++-- esphome/components/factory_reset/factory_reset.cpp | 14 ++++++-------- esphome/components/factory_reset/factory_reset.h | 14 ++++++-------- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/esphome/components/factory_reset/__init__.py b/esphome/components/factory_reset/__init__.py index f3cefe6970..5784d09ce6 100644 --- a/esphome/components/factory_reset/__init__.py +++ b/esphome/components/factory_reset/__init__.py @@ -50,7 +50,9 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.declare_id(FactoryResetComponent), cv.Optional(CONF_MAX_DELAY, default="10s"): cv.All( cv.positive_time_period_seconds, - cv.Range(min=cv.TimePeriod(milliseconds=1000)), + cv.Range( + min=cv.TimePeriod(seconds=1), max=cv.TimePeriod(seconds=65535) + ), ), cv.Optional(CONF_RESETS_REQUIRED): cv.positive_not_null_int, cv.Optional(CONF_ON_INCREMENT): validate_automation( @@ -82,7 +84,7 @@ async def to_code(config): var = cg.new_Pvariable( config[CONF_ID], reset_count, - config[CONF_MAX_DELAY].total_milliseconds, + config[CONF_MAX_DELAY].total_seconds, ) await cg.register_component(var, config) for conf in config.get(CONF_ON_INCREMENT, []): diff --git a/esphome/components/factory_reset/factory_reset.cpp b/esphome/components/factory_reset/factory_reset.cpp index c900759d90..bbbe399148 100644 --- a/esphome/components/factory_reset/factory_reset.cpp +++ b/esphome/components/factory_reset/factory_reset.cpp @@ -8,8 +8,7 @@ #if !defined(USE_RP2040) && !defined(USE_HOST) -namespace esphome { -namespace factory_reset { +namespace esphome::factory_reset { static const char *const TAG = "factory_reset"; static const uint32_t POWER_CYCLES_KEY = 0xFA5C0DE; @@ -33,10 +32,10 @@ void FactoryResetComponent::dump_config() { this->flash_.load(&count); ESP_LOGCONFIG(TAG, "Factory Reset by Reset:"); ESP_LOGCONFIG(TAG, - " Max interval between resets %" PRIu32 " seconds\n" + " Max interval between resets: %u seconds\n" " Current count: %u\n" " Factory reset after %u resets", - this->max_interval_ / 1000, count, this->required_count_); + this->max_interval_, count, this->required_count_); } void FactoryResetComponent::save_(uint8_t count) { @@ -61,8 +60,8 @@ void FactoryResetComponent::setup() { } this->save_(count); ESP_LOGD(TAG, "Power on reset detected, incremented count to %u", count); - this->set_timeout(this->max_interval_, [this]() { - ESP_LOGD(TAG, "No reset in the last %" PRIu32 " seconds, resetting count", this->max_interval_ / 1000); + this->set_timeout(static_cast(this->max_interval_) * 1000, [this]() { + ESP_LOGD(TAG, "No reset in the last %u seconds, resetting count", this->max_interval_); this->save_(0); // reset count }); } else { @@ -70,7 +69,6 @@ void FactoryResetComponent::setup() { } } -} // namespace factory_reset -} // namespace esphome +} // namespace esphome::factory_reset #endif // !defined(USE_RP2040) && !defined(USE_HOST) diff --git a/esphome/components/factory_reset/factory_reset.h b/esphome/components/factory_reset/factory_reset.h index 80942b29bd..990bb2edb6 100644 --- a/esphome/components/factory_reset/factory_reset.h +++ b/esphome/components/factory_reset/factory_reset.h @@ -9,12 +9,11 @@ #include #endif -namespace esphome { -namespace factory_reset { +namespace esphome::factory_reset { class FactoryResetComponent : public Component { public: - FactoryResetComponent(uint8_t required_count, uint32_t max_interval) - : required_count_(required_count), max_interval_(max_interval) {} + FactoryResetComponent(uint8_t required_count, uint16_t max_interval) + : max_interval_(max_interval), required_count_(required_count) {} void dump_config() override; void setup() override; @@ -26,9 +25,9 @@ class FactoryResetComponent : public Component { ~FactoryResetComponent() = default; void save_(uint8_t count); ESPPreferenceObject flash_{}; // saves the number of fast power cycles - uint8_t required_count_; // The number of boot attempts before fast boot is enabled - uint32_t max_interval_; // max interval between power cycles CallbackManager increment_callback_{}; + uint16_t max_interval_; // max interval between power cycles in seconds + uint8_t required_count_; // The number of boot attempts before fast boot is enabled }; class FastBootTrigger : public Trigger { @@ -37,7 +36,6 @@ class FastBootTrigger : public Trigger { parent->add_increment_callback([this](uint8_t current, uint8_t target) { this->trigger(current, target); }); } }; -} // namespace factory_reset -} // namespace esphome +} // namespace esphome::factory_reset #endif // !defined(USE_RP2040) && !defined(USE_HOST) From ab73ed76b8baf27cb17d5a4b4e11d01c55d0c386 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Dec 2025 09:29:58 -0700 Subject: [PATCH 28/43] [esphome] Improve OTA field alignment to save 4 bytes on 32-bit (#12461) --- esphome/components/esphome/ota/ota_esphome.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 057461e6a4..4412a65757 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -80,6 +80,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { #ifdef USE_OTA_PASSWORD std::string password_; + std::unique_ptr auth_buf_; #endif // USE_OTA_PASSWORD std::unique_ptr server_; @@ -93,7 +94,6 @@ class ESPHomeOTAComponent : public ota::OTAComponent { uint8_t handshake_buf_pos_{0}; uint8_t ota_features_{0}; #ifdef USE_OTA_PASSWORD - std::unique_ptr auth_buf_; uint8_t auth_buf_pos_{0}; uint8_t auth_type_{0}; // Store auth type to know which hasher to use #endif // USE_OTA_PASSWORD From 63fc8b4e5acca786f2fdf95269df98157d518ef6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Dec 2025 09:30:12 -0700 Subject: [PATCH 29/43] [core] Refactor str_snake_case and str_sanitize to use constexpr helpers (#12454) --- esphome/core/helpers.cpp | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 55466fca8a..84079227e1 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -200,22 +200,27 @@ template std::string str_ctype_transform(const std::string &str) } std::string str_lower_case(const std::string &str) { return str_ctype_transform(str); } std::string str_upper_case(const std::string &str) { return str_ctype_transform(str); } +// Convert char to snake_case: lowercase and spaces to underscores +static constexpr char to_snake_case_char(char c) { + return (c == ' ') ? '_' : (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; +} +// Sanitize char: keep alphanumerics, dashes, underscores; replace others with underscore +static constexpr char to_sanitized_char(char c) { + return (c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) ? c : '_'; +} std::string str_snake_case(const std::string &str) { - std::string result; - result.resize(str.length()); - std::transform(str.begin(), str.end(), result.begin(), ::tolower); - std::replace(result.begin(), result.end(), ' ', '_'); + std::string result = str; + for (char &c : result) { + c = to_snake_case_char(c); + } return result; } std::string str_sanitize(const std::string &str) { - std::string out = str; - std::replace_if( - out.begin(), out.end(), - [](const char &c) { - return c != '-' && c != '_' && (c < '0' || c > '9') && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z'); - }, - '_'); - return out; + std::string result = str; + for (char &c : result) { + c = to_sanitized_char(c); + } + return result; } std::string str_snprintf(const char *fmt, size_t len, ...) { std::string str; From e91c6a79ea31a076396a9b5a614e7bef7367353d Mon Sep 17 00:00:00 2001 From: Piotr Szulc Date: Wed, 17 Dec 2025 17:45:05 +0100 Subject: [PATCH 30/43] [deep_sleep] Deep sleep for BK72xx (#12267) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/deep_sleep/__init__.py | 111 ++++++++++++++++-- .../deep_sleep/deep_sleep_bk72xx.cpp | 64 ++++++++++ .../deep_sleep/deep_sleep_component.h | 31 ++++- .../deep_sleep/test.bk72xx-ard.yaml | 14 +++ 4 files changed, 206 insertions(+), 14 deletions(-) create mode 100644 esphome/components/deep_sleep/deep_sleep_bk72xx.cpp create mode 100644 tests/components/deep_sleep/test.bk72xx-ard.yaml diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 8849fad7d6..3cfe7aa641 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -1,4 +1,4 @@ -from esphome import automation, pins +from esphome import automation, core, pins import esphome.codegen as cg from esphome.components import esp32, time from esphome.components.esp32 import ( @@ -23,16 +23,20 @@ from esphome.const import ( CONF_MINUTE, CONF_MODE, CONF_NUMBER, + CONF_PIN, CONF_PINS, CONF_RUN_DURATION, CONF_SECOND, CONF_SLEEP_DURATION, CONF_TIME_ID, CONF_WAKEUP_PIN, + PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, PlatformFramework, ) +from esphome.core import CORE +from esphome.types import ConfigType WAKEUP_PINS = { VARIANT_ESP32: [ @@ -113,7 +117,7 @@ WAKEUP_PINS = { } -def validate_pin_number(value): +def validate_pin_number_esp32(value: ConfigType) -> ConfigType: valid_pins = WAKEUP_PINS.get(get_esp32_variant(), WAKEUP_PINS[VARIANT_ESP32]) if value[CONF_NUMBER] not in valid_pins: raise cv.Invalid( @@ -122,6 +126,51 @@ def validate_pin_number(value): return value +def validate_pin_number(value: ConfigType) -> ConfigType: + if not CORE.is_esp32: + return value + return validate_pin_number_esp32(value) + + +def validate_wakeup_pin( + value: ConfigType | list[ConfigType], +) -> list[ConfigType]: + if not isinstance(value, list): + processed_pins: list[ConfigType] = [{CONF_PIN: value}] + else: + processed_pins = list(value) + + for i, pin_config in enumerate(processed_pins): + # now validate each item + validated_pin = WAKEUP_PIN_SCHEMA(pin_config) + validate_pin_number(validated_pin[CONF_PIN]) + processed_pins[i] = validated_pin + + return processed_pins + + +def validate_config(config: ConfigType) -> ConfigType: + # right now only BK72XX supports the list format for wakeup pins + if CORE.is_bk72xx: + if CONF_WAKEUP_PIN_MODE in config: + wakeup_pins = config.get(CONF_WAKEUP_PIN, []) + if len(wakeup_pins) > 1: + raise cv.Invalid( + "You need to remove the global wakeup_pin_mode and define it per pin" + ) + if wakeup_pins: + wakeup_pins[0][CONF_WAKEUP_PIN_MODE] = config.pop(CONF_WAKEUP_PIN_MODE) + elif ( + isinstance(config.get(CONF_WAKEUP_PIN), list) + and len(config[CONF_WAKEUP_PIN]) > 1 + ): + raise cv.Invalid( + "Your platform does not support providing multiple entries in wakeup_pin" + ) + + return config + + def _validate_ex1_wakeup_mode(value): if value == "ALL_LOW": esp32.only_on_variant(supported=[VARIANT_ESP32], msg_prefix="ALL_LOW")(value) @@ -141,6 +190,15 @@ def _validate_ex1_wakeup_mode(value): return value +def _validate_sleep_duration(value: core.TimePeriod) -> core.TimePeriod: + if not CORE.is_bk72xx: + return value + max_duration = core.TimePeriod(hours=36) + if value > max_duration: + raise cv.Invalid("sleep duration cannot be more than 36 hours on BK72XX") + return value + + deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep") DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component) EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action) @@ -186,6 +244,13 @@ WAKEUP_CAUSES_SCHEMA = cv.Schema( } ) +WAKEUP_PIN_SCHEMA = cv.Schema( + { + cv.Required(CONF_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_WAKEUP_PIN_MODE): cv.enum(WAKEUP_PIN_MODES, upper=True), + } +) + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -194,14 +259,15 @@ CONFIG_SCHEMA = cv.All( cv.All(cv.only_on_esp32, WAKEUP_CAUSES_SCHEMA), cv.positive_time_period_milliseconds, ), - cv.Optional(CONF_SLEEP_DURATION): cv.positive_time_period_milliseconds, - cv.Optional(CONF_WAKEUP_PIN): cv.All( - cv.only_on_esp32, - pins.internal_gpio_input_pin_schema, - validate_pin_number, + cv.Optional(CONF_SLEEP_DURATION): cv.All( + cv.positive_time_period_milliseconds, + _validate_sleep_duration, ), + cv.Optional(CONF_WAKEUP_PIN): validate_wakeup_pin, cv.Optional(CONF_WAKEUP_PIN_MODE): cv.All( - cv.only_on_esp32, cv.enum(WAKEUP_PIN_MODES), upper=True + cv.only_on([PLATFORM_ESP32, PLATFORM_BK72XX]), + cv.enum(WAKEUP_PIN_MODES), + upper=True, ), cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All( cv.only_on_esp32, @@ -212,7 +278,8 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_PINS): cv.ensure_list( - pins.internal_gpio_input_pin_schema, validate_pin_number + pins.internal_gpio_input_pin_schema, + validate_pin_number_esp32, ), cv.Required(CONF_MODE): cv.All( cv.enum(EXT1_WAKEUP_MODES, upper=True), @@ -238,7 +305,8 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]), + validate_config, ) @@ -249,8 +317,21 @@ async def to_code(config): if CONF_SLEEP_DURATION in config: cg.add(var.set_sleep_duration(config[CONF_SLEEP_DURATION])) if CONF_WAKEUP_PIN in config: - pin = await cg.gpio_pin_expression(config[CONF_WAKEUP_PIN]) - cg.add(var.set_wakeup_pin(pin)) + pins_as_list = config.get(CONF_WAKEUP_PIN, []) + if CORE.is_bk72xx: + cg.add(var.init_wakeup_pins_(len(pins_as_list))) + for item in pins_as_list: + cg.add( + var.add_wakeup_pin( + await cg.gpio_pin_expression(item[CONF_PIN]), + item.get( + CONF_WAKEUP_PIN_MODE, WakeupPinMode.WAKEUP_PIN_MODE_IGNORE + ), + ) + ) + else: + pin = await cg.gpio_pin_expression(pins_as_list[0][CONF_PIN]) + cg.add(var.set_wakeup_pin(pin)) if CONF_WAKEUP_PIN_MODE in config: cg.add(var.set_wakeup_pin_mode(config[CONF_WAKEUP_PIN_MODE])) if CONF_RUN_DURATION in config: @@ -305,7 +386,10 @@ DEEP_SLEEP_ENTER_SCHEMA = cv.All( cv.Schema( { cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable( - cv.positive_time_period_milliseconds + cv.All( + cv.positive_time_period_milliseconds, + _validate_sleep_duration, + ) ), # Only on ESP32 due to how long the RTC on ESP8266 can stay asleep cv.Exclusive(CONF_UNTIL, "time"): cv.All( @@ -363,5 +447,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, }, "deep_sleep_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "deep_sleep_bk72xx.cpp": {PlatformFramework.BK72XX_ARDUINO}, } ) diff --git a/esphome/components/deep_sleep/deep_sleep_bk72xx.cpp b/esphome/components/deep_sleep/deep_sleep_bk72xx.cpp new file mode 100644 index 0000000000..b5fadd7230 --- /dev/null +++ b/esphome/components/deep_sleep/deep_sleep_bk72xx.cpp @@ -0,0 +1,64 @@ +#ifdef USE_BK72XX + +#include "deep_sleep_component.h" +#include "esphome/core/log.h" + +namespace esphome::deep_sleep { + +static const char *const TAG = "deep_sleep.bk72xx"; + +optional DeepSleepComponent::get_run_duration_() const { return this->run_duration_; } + +void DeepSleepComponent::dump_config_platform_() { + for (const WakeUpPinItem &item : this->wakeup_pins_) { + LOG_PIN(" Wakeup Pin: ", item.wakeup_pin); + } +} + +bool DeepSleepComponent::pin_prevents_sleep_(WakeUpPinItem &pinItem) const { + return (pinItem.wakeup_pin_mode == WAKEUP_PIN_MODE_KEEP_AWAKE && pinItem.wakeup_pin != nullptr && + !this->sleep_duration_.has_value() && (pinItem.wakeup_level == get_real_pin_state_(*pinItem.wakeup_pin))); +} + +bool DeepSleepComponent::prepare_to_sleep_() { + if (wakeup_pins_.size() > 0) { + for (WakeUpPinItem &item : this->wakeup_pins_) { + if (pin_prevents_sleep_(item)) { + // Defer deep sleep until inactive + if (!this->next_enter_deep_sleep_) { + this->status_set_warning(); + ESP_LOGV(TAG, "Waiting for pin to switch state to enter deep sleep..."); + } + this->next_enter_deep_sleep_ = true; + return false; + } + } + } + return true; +} + +void DeepSleepComponent::deep_sleep_() { + for (WakeUpPinItem &item : this->wakeup_pins_) { + if (item.wakeup_pin_mode == WAKEUP_PIN_MODE_INVERT_WAKEUP) { + if (item.wakeup_level == get_real_pin_state_(*item.wakeup_pin)) { + item.wakeup_level = !item.wakeup_level; + } + } + ESP_LOGI(TAG, "Wake-up on P%u %s (%d)", item.wakeup_pin->get_pin(), item.wakeup_level ? "HIGH" : "LOW", + static_cast(item.wakeup_pin_mode)); + } + + if (this->sleep_duration_.has_value()) + lt_deep_sleep_config_timer((*this->sleep_duration_ / 1000) & 0xFFFFFFFF); + + for (WakeUpPinItem &item : this->wakeup_pins_) { + lt_deep_sleep_config_gpio(1 << item.wakeup_pin->get_pin(), item.wakeup_level); + lt_deep_sleep_keep_floating_gpio(1 << item.wakeup_pin->get_pin(), true); + } + + lt_deep_sleep_enter(); +} + +} // namespace esphome::deep_sleep + +#endif // USE_BK72XX diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index bca3aa5e4d..3e6eda2257 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -19,7 +19,7 @@ namespace esphome { namespace deep_sleep { -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_BK72XX) /** The values of this enum define what should be done if deep sleep is set up with a wakeup pin on the ESP32 * and the scenario occurs that the wakeup pin is already in the wakeup state. @@ -33,7 +33,17 @@ enum WakeupPinMode { */ WAKEUP_PIN_MODE_INVERT_WAKEUP, }; +#endif +#if defined(USE_BK72XX) +struct WakeUpPinItem { + InternalGPIOPin *wakeup_pin; + WakeupPinMode wakeup_pin_mode; + bool wakeup_level; +}; +#endif // USE_BK72XX + +#ifdef USE_ESP32 #if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) struct Ext1Wakeup { uint64_t mask; @@ -75,6 +85,13 @@ class DeepSleepComponent : public Component { void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode); #endif // USE_ESP32 +#if defined(USE_BK72XX) + void init_wakeup_pins_(size_t capacity) { this->wakeup_pins_.init(capacity); } + void add_wakeup_pin(InternalGPIOPin *wakeup_pin, WakeupPinMode wakeup_pin_mode) { + this->wakeup_pins_.emplace_back(WakeUpPinItem{wakeup_pin, wakeup_pin_mode, !wakeup_pin->is_inverted()}); + } +#endif // USE_BK72XX + #if defined(USE_ESP32) #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) void set_ext1_wakeup(Ext1Wakeup ext1_wakeup); @@ -114,7 +131,17 @@ class DeepSleepComponent : public Component { bool prepare_to_sleep_(); void deep_sleep_(); +#ifdef USE_BK72XX + bool pin_prevents_sleep_(WakeUpPinItem &pinItem) const; + bool get_real_pin_state_(InternalGPIOPin &pin) const { return (pin.digital_read() ^ pin.is_inverted()); } +#endif // USE_BK72XX + optional sleep_duration_; + +#ifdef USE_BK72XX + FixedVector wakeup_pins_; +#endif // USE_BK72XX + #ifdef USE_ESP32 InternalGPIOPin *wakeup_pin_; WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE}; @@ -124,8 +151,10 @@ class DeepSleepComponent : public Component { #endif optional touch_wakeup_; + optional wakeup_cause_to_run_duration_; #endif // USE_ESP32 + optional run_duration_; bool next_enter_deep_sleep_{false}; bool prevent_{false}; diff --git a/tests/components/deep_sleep/test.bk72xx-ard.yaml b/tests/components/deep_sleep/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..2385fbb4db --- /dev/null +++ b/tests/components/deep_sleep/test.bk72xx-ard.yaml @@ -0,0 +1,14 @@ +deep_sleep: + run_duration: 30s + sleep_duration: 12h + wakeup_pin: + - pin: + number: P6 + - pin: P7 + wakeup_pin_mode: KEEP_AWAKE + - pin: + number: P10 + inverted: true + wakeup_pin_mode: INVERT_WAKEUP + +<<: !include common.yaml From 0707f383a69cbfc9c9d6365e1f43893ef16204ac Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 17 Dec 2025 17:45:17 +0100 Subject: [PATCH 31/43] [nextion] Use ESP-IDF for ESP32 Arduino (#9429) Co-authored-by: J. Nick Koston Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/nextion/__init__.py | 6 +- esphome/components/nextion/display.py | 5 +- esphome/components/nextion/nextion.h | 41 ++++------- .../nextion/nextion_upload_arduino.cpp | 73 ++++++++----------- ...pload_idf.cpp => nextion_upload_esp32.cpp} | 55 ++++++++------ 5 files changed, 84 insertions(+), 96 deletions(-) rename esphome/components/nextion/{nextion_upload_idf.cpp => nextion_upload_esp32.cpp} (90%) diff --git a/esphome/components/nextion/__init__.py b/esphome/components/nextion/__init__.py index 8adc49d68c..38f449dc03 100644 --- a/esphome/components/nextion/__init__.py +++ b/esphome/components/nextion/__init__.py @@ -13,14 +13,16 @@ CONF_SEND_TO_NEXTION = "send_to_nextion" FILTER_SOURCE_FILES = filter_source_files_from_platform( { - "nextion_upload_arduino.cpp": { + "nextion_upload_esp32.cpp": { PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "nextion_upload_arduino.cpp": { PlatformFramework.ESP8266_ARDUINO, PlatformFramework.RP2040_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, - "nextion_upload_idf.cpp": {PlatformFramework.ESP32_IDF}, } ) diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index ed6cd93027..b95df55a61 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -154,14 +154,11 @@ async def to_code(config): cg.add_define("USE_NEXTION_TFT_UPLOAD") cg.add(var.set_tft_url(config[CONF_TFT_URL])) if CORE.is_esp32: - if CORE.using_arduino: - cg.add_library("NetworkClientSecure", None) - cg.add_library("HTTPClient", None) esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True) esp32.add_idf_sdkconfig_option( "CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True ) - elif CORE.is_esp8266 and CORE.using_arduino: + elif CORE.is_esp8266: cg.add_library("ESP8266HTTPClient", None) if CONF_TOUCH_SLEEP_TIMEOUT in config: diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 7e8f563a96..f4fc50ee7d 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -13,17 +13,12 @@ #include "esphome/components/display/display_color_utils.h" #ifdef USE_NEXTION_TFT_UPLOAD -#ifdef USE_ARDUINO #ifdef USE_ESP32 -#include -#endif // USE_ESP32 -#ifdef USE_ESP8266 +#include +#elif defined(USE_ESP8266) #include #include -#endif // USE_ESP8266 -#elif defined(USE_ESP_IDF) -#include -#endif // ARDUINO vs USE_ESP_IDF +#endif // USE_ESP32 vs USE_ESP8266 #endif // USE_NEXTION_TFT_UPLOAD namespace esphome { @@ -1078,7 +1073,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe #ifdef USE_NEXTION_TFT_UPLOAD /** - * Set the tft file URL. https seems problematic with Arduino.. + * Set the tft file URL. */ void set_tft_url(const std::string &tft_url) { this->tft_url_ = tft_url; } @@ -1422,16 +1417,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe uint32_t original_baud_rate_ = 0; bool upload_first_chunk_sent_ = false; -#ifdef USE_ARDUINO - /** - * will request chunk_size chunks from the web server - * and send each to the nextion - * @param HTTPClient http_client HTTP client handler. - * @param int range_start Position of next byte to transfer. - * @return position of last byte transferred, -1 for failure. - */ - int upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start); -#elif defined(USE_ESP_IDF) +#ifdef USE_ESP32 /** * will request 4096 bytes chunks from the web server * and send each to Nextion @@ -1440,7 +1426,16 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * @return position of last byte transferred, -1 for failure. */ int upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &range_start); -#endif // USE_ARDUINO vs USE_ESP_IDF +#else + /** + * will request chunk_size chunks from the web server + * and send each to the nextion + * @param HTTPClient http_client HTTP client handler. + * @param int range_start Position of next byte to transfer. + * @return position of last byte transferred, -1 for failure. + */ + int upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start); +#endif // USE_ESP32 vs others /** * Ends the upload process, restart Nextion and, if successful, @@ -1450,12 +1445,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ bool upload_end_(bool successful); - /** - * Returns the ESP Free Heap memory. This is framework independent. - * @return Free Heap in bytes. - */ - uint32_t get_free_heap_(); - #endif // USE_NEXTION_TFT_UPLOAD bool check_connect_(); diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index baea938729..dfbb5a497e 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -1,7 +1,7 @@ #include "nextion.h" #ifdef USE_NEXTION_TFT_UPLOAD -#ifdef USE_ARDUINO +#ifndef USE_ESP32 #include #include "esphome/components/network/util.h" @@ -10,10 +10,6 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -#ifdef USE_ESP32 -#include -#endif - namespace esphome { namespace nextion { static const char *const TAG = "nextion.upload.arduino"; @@ -21,23 +17,17 @@ static const char *const TAG = "nextion.upload.arduino"; // Followed guide // https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 -inline uint32_t Nextion::get_free_heap_() { -#if defined(USE_ESP32) - return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); -#elif defined(USE_ESP8266) - return EspClass::getFreeHeap(); -#endif // USE_ESP32 vs USE_ESP8266 -} - int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { uint32_t range_size = this->tft_size_ - range_start; - ESP_LOGV(TAG, "Heap: %" PRIu32, this->get_free_heap_()); + ESP_LOGV(TAG, "Heap: %" PRIu32, EspClass::getFreeHeap()); uint32_t range_end = ((upload_first_chunk_sent_ or this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1; ESP_LOGD(TAG, "Range start: %" PRIu32, range_start); if (range_size <= 0 or range_end <= range_start) { - ESP_LOGD(TAG, "Range end: %" PRIu32, range_end); - ESP_LOGD(TAG, "Range size: %" PRIu32, range_size); ESP_LOGE(TAG, "Invalid range"); + ESP_LOGD(TAG, + "Range end: %" PRIu32 "\n" + "Range size: %" PRIu32, + range_end, range_size); return -1; } @@ -95,14 +85,8 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { this->recv_ret_string_(recv_string, upload_first_chunk_sent_ ? 500 : 5000, true); this->content_length_ -= read_len; const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_; -#if defined(USE_ESP32) && defined(USE_PSRAM) - ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 "+%" PRIu32 ")", upload_percentage, - this->content_length_, static_cast(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)), - static_cast(heap_caps_get_free_size(MALLOC_CAP_SPIRAM))); -#else ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 ")", upload_percentage, this->content_length_, - this->get_free_heap_()); -#endif + EspClass::getFreeHeap()); upload_first_chunk_sent_ = true; if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request ESP_LOGD(TAG, "Recv: [%s]", @@ -148,9 +132,11 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { } bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { - ESP_LOGD(TAG, "TFT upload requested"); - ESP_LOGD(TAG, "Exit reparse: %s", YESNO(exit_reparse)); - ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str()); + ESP_LOGD(TAG, + "TFT upload requested\n" + "Exit reparse: %s\n" + "URL: %s", + YESNO(exit_reparse), this->tft_url_.c_str()); if (this->connection_state_.is_updating_) { ESP_LOGW(TAG, "Upload in progress"); @@ -180,15 +166,14 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client - ESP_LOGV(TAG, "Init HTTP client"); - ESP_LOGV(TAG, "Heap: %" PRIu32, this->get_free_heap_()); + ESP_LOGV(TAG, + "Init HTTP client\n" + "Heap: %" PRIu32, + EspClass::getFreeHeap()); HTTPClient http_client; http_client.setTimeout(15000); // Yes 15 seconds.... Helps 8266s along bool begin_status = false; -#ifdef USE_ESP32 - begin_status = http_client.begin(this->tft_url_.c_str()); -#endif #ifdef USE_ESP8266 #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) http_client.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); @@ -256,22 +241,24 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { this->send_command_("sleep=0"); this->send_command_("dim=100"); delay(250); // NOLINT - ESP_LOGV(TAG, "Heap: %" PRIu32, this->get_free_heap_()); + ESP_LOGV(TAG, "Heap: %" PRIu32, EspClass::getFreeHeap()); App.feed_wdt(); char command[128]; // Tells the Nextion the content length of the tft file and baud rate it will be sent at // Once the Nextion accepts the command it will wait until the file is successfully uploaded // If it fails for any reason a power cycle of the display will be needed - sprintf(command, "whmi-wris %d,%d,1", this->content_length_, baud_rate); + snprintf(command, sizeof(command), "whmi-wris %" PRIu32 ",%" PRIu32 ",1", this->content_length_, baud_rate); // Clear serial receive buffer ESP_LOGV(TAG, "Clear RX buffer"); this->reset_(false); delay(250); // NOLINT - ESP_LOGV(TAG, "Heap: %" PRIu32, this->get_free_heap_()); - ESP_LOGV(TAG, "Upload cmd: %s", command); + ESP_LOGV(TAG, + "Heap: %" PRIu32 "\n" + "Upload cmd: %s", + EspClass::getFreeHeap(), command); this->send_command_(command); if (baud_rate != this->original_baud_rate_) { @@ -290,7 +277,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGD(TAG, "Upload resp: [%s] %zu B", format_hex_pretty(reinterpret_cast(response.data()), response.size()).c_str(), response.length()); - ESP_LOGV(TAG, "Heap: %" PRIu32, this->get_free_heap_()); + ESP_LOGV(TAG, "Heap: %" PRIu32, EspClass::getFreeHeap()); if (response.find(0x05) != std::string::npos) { ESP_LOGV(TAG, "Upload prep done"); @@ -302,10 +289,12 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return this->upload_end_(false); } - ESP_LOGD(TAG, "Upload TFT:"); - ESP_LOGD(TAG, " URL: %s", this->tft_url_.c_str()); - ESP_LOGD(TAG, " Size: %d bytes", this->content_length_); - ESP_LOGD(TAG, " Heap: %" PRIu32, this->get_free_heap_()); + ESP_LOGD(TAG, + "Upload TFT:\n" + " URL: %s\n" + " Size: %d bytes\n" + " Heap: %" PRIu32, + this->tft_url_.c_str(), this->content_length_, EspClass::getFreeHeap()); // Proceed with the content download as before @@ -322,7 +311,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return this->upload_end_(false); } App.feed_wdt(); - ESP_LOGV(TAG, "Heap: %" PRIu32 " left: %" PRIu32, this->get_free_heap_(), this->content_length_); + ESP_LOGV(TAG, "Heap: %" PRIu32 " left: %" PRIu32, EspClass::getFreeHeap(), this->content_length_); } ESP_LOGD(TAG, "Upload complete"); @@ -356,5 +345,5 @@ WiFiClient *Nextion::get_wifi_client_() { } // namespace nextion } // namespace esphome -#endif // USE_ARDUINO +#endif // NOT USE_ESP32 #endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp similarity index 90% rename from esphome/components/nextion/nextion_upload_idf.cpp rename to esphome/components/nextion/nextion_upload_esp32.cpp index 942e3dd6c3..29a7e3c8d7 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -1,7 +1,7 @@ #include "nextion.h" #ifdef USE_NEXTION_TFT_UPLOAD -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #include @@ -14,7 +14,7 @@ namespace esphome { namespace nextion { -static const char *const TAG = "nextion.upload.idf"; +static const char *const TAG = "nextion.upload.esp32"; // Followed guide // https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 @@ -25,8 +25,10 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r uint32_t range_end = ((upload_first_chunk_sent_ or this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1; ESP_LOGD(TAG, "Range start: %" PRIu32, range_start); if (range_size <= 0 or range_end <= range_start) { - ESP_LOGD(TAG, "Range end: %" PRIu32, range_end); - ESP_LOGD(TAG, "Range size: %" PRIu32, range_size); + ESP_LOGD(TAG, + "Range end: %" PRIu32 "\n" + "Range size: %" PRIu32, + range_end, range_size); ESP_LOGE(TAG, "Invalid range"); return -1; } @@ -151,9 +153,11 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r } bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { - ESP_LOGD(TAG, "TFT upload requested"); - ESP_LOGD(TAG, "Exit reparse: %s", YESNO(exit_reparse)); - ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str()); + ESP_LOGD(TAG, + "TFT upload requested\n" + "Exit reparse: %s\n" + "URL: %s", + YESNO(exit_reparse), this->tft_url_.c_str()); if (this->connection_state_.is_updating_) { ESP_LOGW(TAG, "Upload in progress"); @@ -183,8 +187,10 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client - ESP_LOGV(TAG, "Init HTTP client"); - ESP_LOGV(TAG, "Heap: %" PRIu32, esp_get_free_heap_size()); + ESP_LOGV(TAG, + "Init HTTP client\n" + "Heap: %" PRIu32, + esp_get_free_heap_size()); esp_http_client_config_t config = { .url = this->tft_url_.c_str(), .cert_pem = nullptr, @@ -208,8 +214,10 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { } // Perform the HTTP request - ESP_LOGV(TAG, "Check connection"); - ESP_LOGV(TAG, "Heap: %" PRIu32, esp_get_free_heap_size()); + ESP_LOGV(TAG, + "Check connection\n" + "Heap: %" PRIu32, + esp_get_free_heap_size()); err = esp_http_client_perform(http_client); if (err != ESP_OK) { ESP_LOGE(TAG, "HTTP failed: %s", esp_err_to_name(err)); @@ -218,8 +226,10 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { } // Check the HTTP Status Code - ESP_LOGV(TAG, "Check status"); - ESP_LOGV(TAG, "Heap: %" PRIu32, esp_get_free_heap_size()); + ESP_LOGV(TAG, + "Check status\n" + "Heap: %" PRIu32, + esp_get_free_heap_size()); int status_code = esp_http_client_get_status_code(http_client); if (status_code != 200 && status_code != 206) { return this->upload_end_(false); @@ -255,7 +265,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // Tells the Nextion the content length of the tft file and baud rate it will be sent at // Once the Nextion accepts the command it will wait until the file is successfully uploaded // If it fails for any reason a power cycle of the display will be needed - sprintf(command, "whmi-wris %" PRIu32 ",%" PRIu32 ",1", this->content_length_, baud_rate); + snprintf(command, sizeof(command), "whmi-wris %" PRIu32 ",%" PRIu32 ",1", this->content_length_, baud_rate); // Clear serial receive buffer ESP_LOGV(TAG, "Clear RX buffer"); @@ -300,10 +310,12 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return this->upload_end_(false); } - ESP_LOGD(TAG, "Uploading TFT:"); - ESP_LOGD(TAG, " URL: %s", this->tft_url_.c_str()); - ESP_LOGD(TAG, " Size: %" PRIu32 " bytes", this->content_length_); - ESP_LOGD(TAG, " Heap: %" PRIu32, esp_get_free_heap_size()); + ESP_LOGD(TAG, + "Uploading TFT:\n" + " URL: %s\n" + " Size: %" PRIu32 " bytes\n" + " Heap: %" PRIu32, + this->tft_url_.c_str(), this->content_length_, esp_get_free_heap_size()); // Proceed with the content download as before @@ -324,9 +336,8 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGV(TAG, "Heap: %" PRIu32 " left: %" PRIu32, esp_get_free_heap_size(), this->content_length_); } - ESP_LOGD(TAG, "TFT upload complete"); - - ESP_LOGD(TAG, "Close HTTP"); + ESP_LOGD(TAG, "TFT upload complete\n" + "Close HTTP"); esp_http_client_close(http_client); esp_http_client_cleanup(http_client); ESP_LOGV(TAG, "Connection closed"); @@ -336,5 +347,5 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { } // namespace nextion } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 #endif // USE_NEXTION_TFT_UPLOAD From f32bb618ac29bbef674a7735f8a6ffe20bb23918 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Dec 2025 09:59:35 -0700 Subject: [PATCH 32/43] [esp32] Store preference keys as uint32_t, convert to string only at NVS boundary (#12494) --- esphome/components/esp32/preferences.cpp | 79 +++++++++++++----------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 7bdbb265ca..e19a85e4e3 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -4,26 +4,28 @@ #include "esphome/core/log.h" #include "esphome/core/preferences.h" #include -#include #include -#include -#include +#include #include +#include namespace esphome { namespace esp32 { static const char *const TAG = "esp32.preferences"; +// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding +static constexpr size_t KEY_BUFFER_SIZE = 12; + struct NVSData { - std::string key; + uint32_t key; std::unique_ptr data; size_t len; void set_data(const uint8_t *src, size_t size) { - data = std::make_unique(size); - memcpy(data.get(), src, size); - len = size; + this->data = std::make_unique(size); + memcpy(this->data.get(), src, size); + this->len = size; } }; @@ -31,27 +33,27 @@ static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-n class ESP32PreferenceBackend : public ESPPreferenceBackend { public: - std::string key; + uint32_t key; uint32_t nvs_handle; bool save(const uint8_t *data, size_t len) override { // try find in pending saves and update that for (auto &obj : s_pending_save) { - if (obj.key == key) { + if (obj.key == this->key) { obj.set_data(data, len); return true; } } NVSData save{}; - save.key = key; + save.key = this->key; save.set_data(data, len); s_pending_save.emplace_back(std::move(save)); - ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %zu", key.c_str(), len); + ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); return true; } bool load(uint8_t *data, size_t len) override { // try find in pending saves and load from that for (auto &obj : s_pending_save) { - if (obj.key == key) { + if (obj.key == this->key) { if (obj.len != len) { // size mismatch return false; @@ -61,22 +63,24 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { } } + char key_str[KEY_BUFFER_SIZE]; + snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key); size_t actual_len; - esp_err_t err = nvs_get_blob(nvs_handle, key.c_str(), nullptr, &actual_len); + esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len); if (err != 0) { - ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key.c_str(), esp_err_to_name(err)); + ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err)); return false; } if (actual_len != len) { ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len); return false; } - err = nvs_get_blob(nvs_handle, key.c_str(), data, &len); + err = nvs_get_blob(this->nvs_handle, key_str, data, &len); if (err != 0) { - ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key.c_str(), esp_err_to_name(err)); + ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); return false; } else { - ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key.c_str(), len); + ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key_str, len); } return true; } @@ -103,14 +107,12 @@ class ESP32Preferences : public ESPPreferences { } } ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { - return make_preference(length, type); + return this->make_preference(length, type); } ESPPreferenceObject make_preference(size_t length, uint32_t type) override { auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) - pref->nvs_handle = nvs_handle; - - uint32_t keyval = type; - pref->key = str_sprintf("%" PRIu32, keyval); + pref->nvs_handle = this->nvs_handle; + pref->key = type; return ESPPreferenceObject(pref); } @@ -123,17 +125,19 @@ class ESP32Preferences : public ESPPreferences { // goal try write all pending saves even if one fails int cached = 0, written = 0, failed = 0; esp_err_t last_err = ESP_OK; - std::string last_key{}; + uint32_t last_key = 0; // go through vector from back to front (makes erase easier/more efficient) for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) { const auto &save = s_pending_save[i]; - ESP_LOGVV(TAG, "Checking if NVS data %s has changed", save.key.c_str()); - if (is_changed(nvs_handle, save)) { - esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.get(), save.len); - ESP_LOGV(TAG, "sync: key: %s, len: %zu", save.key.c_str(), save.len); + ESP_LOGVV(TAG, "Checking if NVS data %" PRIu32 " has changed", save.key); + if (this->is_changed(this->nvs_handle, save)) { + char key_str[KEY_BUFFER_SIZE]; + snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); + esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len); + ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len); if (err != 0) { - ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", save.key.c_str(), save.len, esp_err_to_name(err)); + ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err)); failed++; last_err = err; last_key = save.key; @@ -141,7 +145,7 @@ class ESP32Preferences : public ESPPreferences { } written++; } else { - ESP_LOGV(TAG, "NVS data not changed skipping %s len=%zu", save.key.c_str(), save.len); + ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len); cached++; } s_pending_save.erase(s_pending_save.begin() + i); @@ -149,12 +153,12 @@ class ESP32Preferences : public ESPPreferences { ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, failed); if (failed > 0) { - ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%s", failed, esp_err_to_name(last_err), - last_key.c_str()); + ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%" PRIu32, failed, esp_err_to_name(last_err), + last_key); } // note: commit on esp-idf currently is a no-op, nvs_set_blob always writes - esp_err_t err = nvs_commit(nvs_handle); + esp_err_t err = nvs_commit(this->nvs_handle); if (err != 0) { ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err)); return false; @@ -163,10 +167,13 @@ class ESP32Preferences : public ESPPreferences { return failed == 0; } bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) { + char key_str[KEY_BUFFER_SIZE]; + snprintf(key_str, sizeof(key_str), "%" PRIu32, to_save.key); + size_t actual_len; - esp_err_t err = nvs_get_blob(nvs_handle, to_save.key.c_str(), nullptr, &actual_len); + esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len); if (err != 0) { - ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err)); + ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err)); return true; } // Check size first before allocating memory @@ -174,9 +181,9 @@ class ESP32Preferences : public ESPPreferences { return true; } auto stored_data = std::make_unique(actual_len); - err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.get(), &actual_len); + err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len); if (err != 0) { - ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err)); + ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); return true; } return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0; From 94763ebdabf614e13b844003212c9e0762622442 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Dec 2025 09:59:40 -0700 Subject: [PATCH 33/43] [libretiny] Store preference keys as uint32_t, convert to string only at FlashDB boundary (#12500) --- esphome/components/libretiny/preferences.cpp | 74 +++++++++++--------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index 871b186d8e..c21c5813a8 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -4,24 +4,27 @@ #include "esphome/core/log.h" #include "esphome/core/preferences.h" #include +#include #include #include -#include namespace esphome { namespace libretiny { static const char *const TAG = "lt.preferences"; +// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding +static constexpr size_t KEY_BUFFER_SIZE = 12; + struct NVSData { - std::string key; + uint32_t key; std::unique_ptr data; size_t len; void set_data(const uint8_t *src, size_t size) { - data = std::make_unique(size); - memcpy(data.get(), src, size); - len = size; + this->data = std::make_unique(size); + memcpy(this->data.get(), src, size); + this->len = size; } }; @@ -29,30 +32,30 @@ static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-n class LibreTinyPreferenceBackend : public ESPPreferenceBackend { public: - std::string key; + uint32_t key; fdb_kvdb_t db; fdb_blob_t blob; bool save(const uint8_t *data, size_t len) override { // try find in pending saves and update that for (auto &obj : s_pending_save) { - if (obj.key == key) { + if (obj.key == this->key) { obj.set_data(data, len); return true; } } NVSData save{}; - save.key = key; + save.key = this->key; save.set_data(data, len); s_pending_save.emplace_back(std::move(save)); - ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %zu", key.c_str(), len); + ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); return true; } bool load(uint8_t *data, size_t len) override { // try find in pending saves and load from that for (auto &obj : s_pending_save) { - if (obj.key == key) { + if (obj.key == this->key) { if (obj.len != len) { // size mismatch return false; @@ -62,13 +65,15 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend { } } - fdb_blob_make(blob, data, len); - size_t actual_len = fdb_kv_get_blob(db, key.c_str(), blob); + char key_str[KEY_BUFFER_SIZE]; + snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key); + fdb_blob_make(this->blob, data, len); + size_t actual_len = fdb_kv_get_blob(this->db, key_str, this->blob); if (actual_len != len) { ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len); return false; } else { - ESP_LOGVV(TAG, "fdb_kv_get_blob: key: %s, len: %zu", key.c_str(), len); + ESP_LOGVV(TAG, "fdb_kv_get_blob: key: %s, len: %zu", key_str, len); } return true; } @@ -90,16 +95,14 @@ class LibreTinyPreferences : public ESPPreferences { } ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { - return make_preference(length, type); + return this->make_preference(length, type); } ESPPreferenceObject make_preference(size_t length, uint32_t type) override { auto *pref = new LibreTinyPreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) - pref->db = &db; - pref->blob = &blob; - - uint32_t keyval = type; - pref->key = str_sprintf("%u", keyval); + pref->db = &this->db; + pref->blob = &this->blob; + pref->key = type; return ESPPreferenceObject(pref); } @@ -112,18 +115,20 @@ class LibreTinyPreferences : public ESPPreferences { // goal try write all pending saves even if one fails int cached = 0, written = 0, failed = 0; fdb_err_t last_err = FDB_NO_ERR; - std::string last_key{}; + uint32_t last_key = 0; // go through vector from back to front (makes erase easier/more efficient) for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) { const auto &save = s_pending_save[i]; - ESP_LOGVV(TAG, "Checking if FDB data %s has changed", save.key.c_str()); - if (is_changed(&db, save)) { - ESP_LOGV(TAG, "sync: key: %s, len: %zu", save.key.c_str(), save.len); - fdb_blob_make(&blob, save.data.get(), save.len); - fdb_err_t err = fdb_kv_set_blob(&db, save.key.c_str(), &blob); + ESP_LOGVV(TAG, "Checking if FDB data %" PRIu32 " has changed", save.key); + if (this->is_changed(&this->db, save)) { + char key_str[KEY_BUFFER_SIZE]; + snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); + ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len); + fdb_blob_make(&this->blob, save.data.get(), save.len); + fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob); if (err != FDB_NO_ERR) { - ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", save.key.c_str(), save.len, err); + ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.len, err); failed++; last_err = err; last_key = save.key; @@ -131,7 +136,7 @@ class LibreTinyPreferences : public ESPPreferences { } written++; } else { - ESP_LOGD(TAG, "FDB data not changed; skipping %s len=%zu", save.key.c_str(), save.len); + ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len); cached++; } s_pending_save.erase(s_pending_save.begin() + i); @@ -139,17 +144,20 @@ class LibreTinyPreferences : public ESPPreferences { ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, failed); if (failed > 0) { - ESP_LOGE(TAG, "Writing %d items failed. Last error=%d for key=%s", failed, last_err, last_key.c_str()); + ESP_LOGE(TAG, "Writing %d items failed. Last error=%d for key=%" PRIu32, failed, last_err, last_key); } return failed == 0; } bool is_changed(const fdb_kvdb_t db, const NVSData &to_save) { + char key_str[KEY_BUFFER_SIZE]; + snprintf(key_str, sizeof(key_str), "%" PRIu32, to_save.key); + struct fdb_kv kv; - fdb_kv_t kvp = fdb_kv_get_obj(db, to_save.key.c_str(), &kv); + fdb_kv_t kvp = fdb_kv_get_obj(db, key_str, &kv); if (kvp == nullptr) { - ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", to_save.key.c_str()); + ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", key_str); return true; } @@ -160,10 +168,10 @@ class LibreTinyPreferences : public ESPPreferences { // Allocate buffer on heap to avoid stack allocation for large data auto stored_data = std::make_unique(kv.value_len); - fdb_blob_make(&blob, stored_data.get(), kv.value_len); - size_t actual_len = fdb_kv_get_blob(db, to_save.key.c_str(), &blob); + fdb_blob_make(&this->blob, stored_data.get(), kv.value_len); + size_t actual_len = fdb_kv_get_blob(db, key_str, &this->blob); if (actual_len != kv.value_len) { - ESP_LOGV(TAG, "fdb_kv_get_blob('%s') len mismatch: %u != %u", to_save.key.c_str(), actual_len, kv.value_len); + ESP_LOGV(TAG, "fdb_kv_get_blob('%s') len mismatch: %u != %u", key_str, actual_len, kv.value_len); return true; } From 42e061c9aeb39145e82d66dc84a58d711aa6ab64 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Dec 2025 10:00:19 -0700 Subject: [PATCH 34/43] [text] Avoid string copies in callbacks by passing const ref (#12504) --- esphome/components/text/text.cpp | 2 +- esphome/components/text/text.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index 933d82c85c..d06c350832 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -23,7 +23,7 @@ void Text::publish_state(const std::string &state) { #endif } -void Text::add_on_state_callback(std::function &&callback) { +void Text::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } diff --git a/esphome/components/text/text.h b/esphome/components/text/text.h index 74d08eda8a..f24464cb20 100644 --- a/esphome/components/text/text.h +++ b/esphome/components/text/text.h @@ -31,7 +31,7 @@ class Text : public EntityBase { /// Instantiate a TextCall object to modify this text component's state. TextCall make_call() { return TextCall(this); } - void add_on_state_callback(std::function &&callback); + void add_on_state_callback(std::function &&callback); protected: friend class TextCall; @@ -44,7 +44,7 @@ class Text : public EntityBase { */ virtual void control(const std::string &value) = 0; - CallbackManager state_callback_; + CallbackManager state_callback_; }; } // namespace text From 0e71fa97a70d1768682a54e004b30f5c269a3cf2 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 17 Dec 2025 12:18:25 -0500 Subject: [PATCH 35/43] [spi] Add SPIInterface stub for clang-tidy on unsupported platforms (#12532) Co-authored-by: Claude --- esphome/components/spi/spi.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 43b55d72bc..256cbcc65f 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -1,5 +1,4 @@ #pragma once -#ifndef USE_ZEPHYR #include "esphome/core/application.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" @@ -24,6 +23,10 @@ using SPIInterface = SPIClassRP2040 *; using SPIInterface = SPIClass *; #endif +#elif defined(CLANG_TIDY) + +using SPIInterface = void *; // Stub for platforms without SPI (e.g., Zephyr) + #endif // USE_ESP32 / USE_ARDUINO /** @@ -503,4 +506,3 @@ class SPIDevice : public SPIClient { }; } // namespace esphome::spi -#endif // USE_ZEPHYR From d7b04a3d18a94dc77c2cdf4a9097f7557e8e9e44 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 17 Dec 2025 13:59:49 -0500 Subject: [PATCH 36/43] [nextion] Fix clang-tidy error on Zephyr for HTTPClient (#12538) Co-authored-by: Claude --- esphome/components/nextion/nextion.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index f4fc50ee7d..331e901578 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1426,7 +1426,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * @return position of last byte transferred, -1 for failure. */ int upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &range_start); -#else +#elif defined(USE_ARDUINO) /** * will request chunk_size chunks from the web server * and send each to the nextion @@ -1435,7 +1435,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * @return position of last byte transferred, -1 for failure. */ int upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start); -#endif // USE_ESP32 vs others +#endif // USE_ESP32 vs USE_ARDUINO /** * Ends the upload process, restart Nextion and, if successful, From f9720026d0aa7c102de65f14856efb3138bcb43b Mon Sep 17 00:00:00 2001 From: Anna Oake Date: Wed, 17 Dec 2025 20:19:18 +0100 Subject: [PATCH 37/43] [cc1101] Fix default frequencies (#12539) --- esphome/components/cc1101/cc1101.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index 5b6eb545bc..1fe402d6c6 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -99,11 +99,11 @@ CC1101Component::CC1101Component() { this->state_.FS_AUTOCAL = 1; // Default Settings - this->set_frequency(433920); - this->set_if_frequency(153); - this->set_filter_bandwidth(203); + this->set_frequency(433920000); + this->set_if_frequency(153000); + this->set_filter_bandwidth(203000); this->set_channel(0); - this->set_channel_spacing(200); + this->set_channel_spacing(200000); this->set_symbol_rate(5000); this->set_sync_mode(SyncMode::SYNC_MODE_NONE); this->set_carrier_sense_above_threshold(true); From b02696edc09996e7420563a22094a3b80bcf9add Mon Sep 17 00:00:00 2001 From: Jack Wilsdon Date: Wed, 17 Dec 2025 20:40:31 +0000 Subject: [PATCH 38/43] [pm1006] Fix "never" update interval detection (#12529) --- esphome/components/pm1006/sensor.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/esphome/components/pm1006/sensor.py b/esphome/components/pm1006/sensor.py index c693cfea19..8ff21ab069 100644 --- a/esphome/components/pm1006/sensor.py +++ b/esphome/components/pm1006/sensor.py @@ -7,10 +7,10 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, DEVICE_CLASS_PM25, ICON_BLUR, + SCHEDULER_DONT_RUN, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, ) -from esphome.core import TimePeriodMilliseconds CODEOWNERS = ["@habbie"] DEPENDENCIES = ["uart"] @@ -41,16 +41,12 @@ CONFIG_SCHEMA = cv.All( def validate_interval_uart(config): - require_tx = False - interval = config.get(CONF_UPDATE_INTERVAL) - - if isinstance(interval, TimePeriodMilliseconds): - # 'never' is encoded as a very large int, not as a TimePeriodMilliseconds objects - require_tx = True - uart.final_validate_device_schema( - "pm1006", baud_rate=9600, require_rx=True, require_tx=require_tx + "pm1006", + baud_rate=9600, + require_rx=True, + require_tx=interval.total_milliseconds != SCHEDULER_DONT_RUN, )(config) From 3d673ac55e571624620b72fb4a3b9e934ea7670b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 17 Dec 2025 16:13:18 -0500 Subject: [PATCH 39/43] [ci] Check changed headers in clang-tidy when using --changed (#12540) Co-authored-by: Claude --- script/clang-tidy | 26 +++++++++++++++++++------- script/helpers.py | 29 ++++++++++++++++------------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/script/clang-tidy b/script/clang-tidy index 142b616119..17bcafacc7 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -253,19 +253,31 @@ def main(): print(f"Split {args.split_at}/{args.split_num}: checking {len(files)} files") # Print file count before adding header file - print(f"\nTotal files to check: {len(files)}") + print(f"\nTotal cpp files to check: {len(files)}") + + # Add header file for checking (before early exit check) + if args.all_headers and args.split_at in (None, 1): + # When --changed is used, only include changed headers instead of all headers + if args.changed: + all_headers = [ + os.path.relpath(p, cwd) for p in git_ls_files(["esphome/**/*.h"]) + ] + changed_headers = filter_changed(all_headers) + if changed_headers: + build_all_include(changed_headers) + files.insert(0, temp_header_file) + else: + print("No changed headers to check") + else: + build_all_include() + files.insert(0, temp_header_file) + print(f"Added all-include header file, new total: {len(files)}") # Early exit if no files to check if not files: print("No files to check - exiting early") return 0 - # Only build header file if we have actual files to check - if args.all_headers and args.split_at in (None, 1): - build_all_include() - files.insert(0, temp_header_file) - print(f"Added all-include header file, new total: {len(files)}") - # Print final file list before loading idedata print_file_list(files, "Final files to process:") diff --git a/script/helpers.py b/script/helpers.py index 06a50a3092..202ac9b5fc 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -156,22 +156,25 @@ def print_error_for_file(file: str | Path, body: str | None) -> None: print() -def build_all_include() -> None: - # Build a cpp file that includes all header files in this repo. - # Otherwise header-only integrations would not be tested by clang-tidy +def build_all_include(header_files: list[str] | None = None) -> None: + # Build a cpp file that includes header files for clang-tidy to check. + # If header_files is provided, only include those headers. + # Otherwise, include all header files in the esphome directory. - # Use git ls-files to find all .h files in the esphome directory - # This is much faster than walking the filesystem - cmd = ["git", "ls-files", "esphome/**/*.h"] - proc = subprocess.run(cmd, capture_output=True, text=True, check=True) + if header_files is None: + # Use git ls-files to find all .h files in the esphome directory + # This is much faster than walking the filesystem + cmd = ["git", "ls-files", "esphome/**/*.h"] + proc = subprocess.run(cmd, capture_output=True, text=True, check=True) - # Process git output - git already returns paths relative to repo root - headers = [ - f'#include "{include_p}"' - for line in proc.stdout.strip().split("\n") - if (include_p := line.replace(os.path.sep, "/")) - ] + # Process git output - git already returns paths relative to repo root + header_files = [ + line.replace(os.path.sep, "/") + for line in proc.stdout.strip().split("\n") + if line + ] + headers = [f'#include "{h}"' for h in header_files] headers.sort() headers.append("") content = "\n".join(headers) From dc8f7abce2d0dea4458f8e0c1d52da1e20fb831c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 17 Dec 2025 17:07:42 -0500 Subject: [PATCH 40/43] [bme68x_bsec2_i2c] Add MULTI_CONF support for multiple sensors (#12535) Co-authored-by: Claude --- esphome/components/bme68x_bsec2_i2c/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/bme68x_bsec2_i2c/__init__.py b/esphome/components/bme68x_bsec2_i2c/__init__.py index d6fb7fa9be..c8ca0ba022 100644 --- a/esphome/components/bme68x_bsec2_i2c/__init__.py +++ b/esphome/components/bme68x_bsec2_i2c/__init__.py @@ -11,6 +11,7 @@ CODEOWNERS = ["@neffs", "@kbx81"] AUTO_LOAD = ["bme68x_bsec2"] DEPENDENCIES = ["i2c"] +MULTI_CONF = True bme68x_bsec2_i2c_ns = cg.esphome_ns.namespace("bme68x_bsec2_i2c") BME68xBSEC2I2CComponent = bme68x_bsec2_i2c_ns.class_( From 91c504061b90ca64c21d42ce26f833a7ec12d9b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Dec 2025 15:19:26 -0700 Subject: [PATCH 41/43] [select] Eliminate string allocation in state callbacks (#12505) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/copy/select/copy_select.cpp | 2 +- esphome/components/mqtt/mqtt_select.cpp | 3 +-- esphome/components/select/automation.h | 8 ++++++-- esphome/components/select/select.cpp | 5 ++--- esphome/components/select/select.h | 4 ++-- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index e45338e785..e85e08e353 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -7,7 +7,7 @@ namespace copy { static const char *const TAG = "copy.select"; void CopySelect::setup() { - source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(index); }); + source_->add_on_state_callback([this](size_t index) { this->publish_state(index); }); traits.set_options(source_->traits.get_options()); diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp index e1660b07ea..e48af980c8 100644 --- a/esphome/components/mqtt/mqtt_select.cpp +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -21,8 +21,7 @@ void MQTTSelectComponent::setup() { call.set_option(state); call.perform(); }); - this->select_->add_on_state_callback( - [this](const std::string &state, size_t index) { this->publish_state(this->select_->option_at(index)); }); + this->select_->add_on_state_callback([this](size_t index) { this->publish_state(this->select_->option_at(index)); }); } void MQTTSelectComponent::dump_config() { diff --git a/esphome/components/select/automation.h b/esphome/components/select/automation.h index 768f2621f7..dda5403557 100644 --- a/esphome/components/select/automation.h +++ b/esphome/components/select/automation.h @@ -8,9 +8,13 @@ namespace esphome::select { class SelectStateTrigger : public Trigger { public: - explicit SelectStateTrigger(Select *parent) { - parent->add_on_state_callback([this](const std::string &value, size_t index) { this->trigger(value, index); }); + explicit SelectStateTrigger(Select *parent) : parent_(parent) { + parent->add_on_state_callback( + [this](size_t index) { this->trigger(std::string(this->parent_->option_at(index)), index); }); } + + protected: + Select *parent_; }; template class SelectSetAction : public Action { diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 4fc4d79b08..28d7eb07d4 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -32,8 +32,7 @@ void Select::publish_state(size_t index) { this->state = option; // Update deprecated member for backward compatibility #pragma GCC diagnostic pop ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index); - // Callback signature requires std::string, create temporary for compatibility - this->state_callback_.call(std::string(option), index); + this->state_callback_.call(index); #if defined(USE_SELECT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_select_update(this); #endif @@ -41,7 +40,7 @@ void Select::publish_state(size_t index) { const char *Select::current_option() const { return this->has_state() ? this->option_at(this->active_index_) : ""; } -void Select::add_on_state_callback(std::function &&callback) { +void Select::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 63707f6bd6..854fdcf252 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -75,7 +75,7 @@ class Select : public EntityBase { /// Return the option value at the provided index offset (as const char* from flash). const char *option_at(size_t index) const; - void add_on_state_callback(std::function &&callback); + void add_on_state_callback(std::function &&callback); protected: friend class SelectCall; @@ -111,7 +111,7 @@ class Select : public EntityBase { } } - CallbackManager state_callback_; + CallbackManager state_callback_; }; } // namespace esphome::select From 4ddaff4027fc8c8cb4e39957087915204922a1da Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 17 Dec 2025 17:26:56 -0500 Subject: [PATCH 42/43] [esp32] Dynamically embed managed component server certificates (#12509) Co-authored-by: Claude Co-authored-by: J. Nick Koston --- esphome/components/esp32/__init__.py | 9 --------- esphome/components/esp32/post_build.py.script | 12 ++++++++++++ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 4448b6bbe7..dc442cfbd2 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -982,15 +982,6 @@ async def to_code(config): cg.add_platformio_option("framework", "arduino, espidf") cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") - cg.add_platformio_option( - "board_build.embed_txtfiles", - [ - "managed_components/espressif__esp_insights/server_certs/https_server.crt", - "managed_components/espressif__esp_rainmaker/server_certs/rmaker_mqtt_server.crt", - "managed_components/espressif__esp_rainmaker/server_certs/rmaker_claim_service_server.crt", - "managed_components/espressif__esp_rainmaker/server_certs/rmaker_ota_server.crt", - ], - ) cg.add_define( "USE_ARDUINO_VERSION_CODE", cg.RawExpression( diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script index c995214232..5ef5860687 100644 --- a/esphome/components/esp32/post_build.py.script +++ b/esphome/components/esp32/post_build.py.script @@ -5,6 +5,7 @@ import json # noqa: E402 import os # noqa: E402 import pathlib # noqa: E402 import shutil # noqa: E402 +from glob import glob # noqa: E402 def merge_factory_bin(source, target, env): @@ -126,3 +127,14 @@ def esp32_copy_ota_bin(source, target, env): # Run merge first, then ota copy second env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821 env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa: F821 + +# Find server certificates in managed components and generate .S files. +# Workaround for PlatformIO not processing target_add_binary_data() from managed component CMakeLists. +project_dir = env.subst("$PROJECT_DIR") +managed_components = os.path.join(project_dir, "managed_components") +if os.path.isdir(managed_components): + for cert_file in glob(os.path.join(managed_components, "**/server_certs/*.crt"), recursive=True): + try: + env.FileToAsm(cert_file, FILE_TYPE="TEXT") + except Exception as e: + print(f"Error processing {os.path.basename(cert_file)}: {e}") From 2b337aa3062b60fe7418540364052f88a86e006f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 17 Dec 2025 17:37:59 -0500 Subject: [PATCH 43/43] [esp32_camera] Fix I2C driver conflict with other components (#12533) Co-authored-by: Claude Co-authored-by: J. Nick Koston --- esphome/components/esp32_camera/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 4182683bdc..ca37cb392d 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -3,7 +3,7 @@ import logging from esphome import automation, pins import esphome.codegen as cg from esphome.components import i2c -from esphome.components.esp32 import add_idf_component +from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option from esphome.components.psram import DOMAIN as psram_domain import esphome.config_validation as cv from esphome.const import ( @@ -352,6 +352,8 @@ async def to_code(config): cg.add_define("USE_CAMERA") add_idf_component(name="espressif/esp32-camera", ref="2.1.1") + add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True) + add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY", False) for conf in config.get(CONF_ON_STREAM_START, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)