From f73c539ea7942d9f609bf5975423940e2895bb23 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Jan 2026 17:18:31 -1000 Subject: [PATCH 1/6] [web_server] Add RP2040 platform support (#13576) --- .clang-tidy.hash | 2 +- esphome/components/async_tcp/__init__.py | 6 ++++-- esphome/components/async_tcp/async_tcp.h | 4 ++-- esphome/components/web_server/__init__.py | 2 ++ esphome/components/web_server_base/__init__.py | 5 +++++ platformio.ini | 1 + tests/components/web_server/test.rp2040-ard.yaml | 1 + 7 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 tests/components/web_server/test.rp2040-ard.yaml diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 55239f961c..a1dcdf5799 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -a172e2f65981e98354cc6b5ecf69bdb055dd13602226042ab2c7acd037a2bf41 +08c21fa4c044fd80c8f3296371f30c4f5ff3418f1bc1efe63c5bad938301f122 diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index 1ff4805f03..2a07903b68 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -38,8 +38,10 @@ async def to_code(config): # https://github.com/ESP32Async/ESPAsyncTCP cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0") elif CORE.is_rp2040: - # https://github.com/khoih-prog/AsyncTCP_RP2040W - cg.add_library("khoih-prog/AsyncTCP_RP2040W", "1.2.0") + # https://github.com/ayushsharma82/RPAsyncTCP + # RPAsyncTCP is a drop-in replacement for AsyncTCP_RP2040W with better + # ESPAsyncWebServer compatibility + cg.add_library("ayushsharma82/RPAsyncTCP", "1.3.2") # Other platforms (host, etc) use socket-based implementation diff --git a/esphome/components/async_tcp/async_tcp.h b/esphome/components/async_tcp/async_tcp.h index 6d9211f023..21fcfe239f 100644 --- a/esphome/components/async_tcp/async_tcp.h +++ b/esphome/components/async_tcp/async_tcp.h @@ -8,8 +8,8 @@ // Use ESPAsyncTCP library for ESP8266 (always Arduino) #include #elif defined(USE_RP2040) -// Use AsyncTCP_RP2040W library for RP2040 -#include +// Use RPAsyncTCP library for RP2040 +#include #else // Use socket-based implementation for other platforms #include "async_tcp_socket.h" diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 3f1e094afc..8b02a6baee 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -31,6 +31,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_LN882X, + PLATFORM_RP2040, PLATFORM_RTL87XX, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -213,6 +214,7 @@ CONFIG_SCHEMA = cv.All( PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_LN882X, + PLATFORM_RP2040, PLATFORM_RTL87XX, ] ), diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index e0eec7dedb..6c756575d4 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -47,5 +47,10 @@ async def to_code(config): cg.add_library("ESP8266WiFi", None) if CORE.is_libretiny: CORE.add_platformio_option("lib_ignore", ["ESPAsyncTCP", "RPAsyncTCP"]) + if CORE.is_rp2040: + # Ignore bundled AsyncTCP libraries - we use RPAsyncTCP from async_tcp component + CORE.add_platformio_option( + "lib_ignore", ["ESPAsyncTCP", "AsyncTCP", "AsyncTCP_RP2040W"] + ) # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.5") diff --git a/platformio.ini b/platformio.ini index accc40ecf2..c5a6768ad2 100644 --- a/platformio.ini +++ b/platformio.ini @@ -200,6 +200,7 @@ platform_packages = framework = arduino lib_deps = ${common:arduino.lib_deps} + ayushsharma82/RPAsyncTCP@1.3.2 ; async_tcp bblanchon/ArduinoJson@7.4.2 ; json ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base build_flags = diff --git a/tests/components/web_server/test.rp2040-ard.yaml b/tests/components/web_server/test.rp2040-ard.yaml new file mode 100644 index 0000000000..7e6658e20e --- /dev/null +++ b/tests/components/web_server/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common_v2.yaml From fe6f27c5261ad26f67438a357670697c3490b7ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Jan 2026 17:33:46 -1000 Subject: [PATCH 2/6] [text_sensor] Use in-place mutation for filters to reduce heap allocations (#13475) --- esphome/components/text_sensor/filter.cpp | 50 ++++++++++++----------- esphome/components/text_sensor/filter.h | 43 ++++++++++--------- 2 files changed, 51 insertions(+), 42 deletions(-) diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index 4cace372ae..4ee12e8602 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -9,19 +9,18 @@ namespace text_sensor { static const char *const TAG = "text_sensor.filter"; // Filter -void Filter::input(const std::string &value) { +void Filter::input(std::string value) { ESP_LOGVV(TAG, "Filter(%p)::input(%s)", this, value.c_str()); - optional out = this->new_value(value); - if (out.has_value()) - this->output(*out); + if (this->new_value(value)) + this->output(value); } -void Filter::output(const std::string &value) { +void Filter::output(std::string &value) { if (this->next_ == nullptr) { ESP_LOGVV(TAG, "Filter(%p)::output(%s) -> SENSOR", this, value.c_str()); this->parent_->internal_send_state_to_frontend(value); } else { ESP_LOGVV(TAG, "Filter(%p)::output(%s) -> %p", this, value.c_str(), this->next_); - this->next_->input(value); + this->next_->input(std::move(value)); } } void Filter::initialize(TextSensor *parent, Filter *next) { @@ -35,43 +34,48 @@ LambdaFilter::LambdaFilter(lambda_filter_t lambda_filter) : lambda_filter_(std:: const lambda_filter_t &LambdaFilter::get_lambda_filter() const { return this->lambda_filter_; } void LambdaFilter::set_lambda_filter(const lambda_filter_t &lambda_filter) { this->lambda_filter_ = lambda_filter; } -optional LambdaFilter::new_value(std::string value) { - auto it = this->lambda_filter_(value); - ESP_LOGVV(TAG, "LambdaFilter(%p)::new_value(%s) -> %s", this, value.c_str(), it.value_or("").c_str()); - return it; +bool LambdaFilter::new_value(std::string &value) { + auto result = this->lambda_filter_(value); + if (result.has_value()) { + ESP_LOGVV(TAG, "LambdaFilter(%p)::new_value(%s) -> %s (continue)", this, value.c_str(), result->c_str()); + value = std::move(*result); + return true; + } + ESP_LOGVV(TAG, "LambdaFilter(%p)::new_value(%s) -> (stop)", this, value.c_str()); + return false; } // ToUpperFilter -optional ToUpperFilter::new_value(std::string value) { +bool ToUpperFilter::new_value(std::string &value) { for (char &c : value) c = ::toupper(c); - return value; + return true; } // ToLowerFilter -optional ToLowerFilter::new_value(std::string value) { +bool ToLowerFilter::new_value(std::string &value) { for (char &c : value) c = ::tolower(c); - return value; + return true; } // Append -optional AppendFilter::new_value(std::string value) { +bool AppendFilter::new_value(std::string &value) { value.append(this->suffix_); - return value; + return true; } // Prepend -optional PrependFilter::new_value(std::string value) { +bool PrependFilter::new_value(std::string &value) { value.insert(0, this->prefix_); - return value; + return true; } // Substitute SubstituteFilter::SubstituteFilter(const std::initializer_list &substitutions) : substitutions_(substitutions) {} -optional SubstituteFilter::new_value(std::string value) { +bool SubstituteFilter::new_value(std::string &value) { for (const auto &sub : this->substitutions_) { // Compute lengths once per substitution (strlen is fast, called infrequently) const size_t from_len = strlen(sub.from); @@ -84,20 +88,20 @@ optional SubstituteFilter::new_value(std::string value) { pos += to_len; } } - return value; + return true; } // Map MapFilter::MapFilter(const std::initializer_list &mappings) : mappings_(mappings) {} -optional MapFilter::new_value(std::string value) { +bool MapFilter::new_value(std::string &value) { for (const auto &mapping : this->mappings_) { if (value == mapping.from) { value.assign(mapping.to); - return value; + return true; } } - return value; // Pass through if no match + return true; // Pass through if no match } } // namespace text_sensor diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index 0f66b753b4..1922b503ca 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -17,21 +17,20 @@ class Filter { public: /** This will be called every time the filter receives a new value. * - * It can return an empty optional to indicate that the filter chain - * should stop, otherwise the value in the filter will be passed down - * the chain. + * Modify the value in place. Return false to stop the filter chain + * (value will not be published), or true to continue. * - * @param value The new value. - * @return An optional string, the new value that should be pushed out. + * @param value The value to filter (modified in place). + * @return True to continue the filter chain, false to stop. */ - virtual optional new_value(std::string value) = 0; + virtual bool new_value(std::string &value) = 0; /// Initialize this filter, please note this can be called more than once. virtual void initialize(TextSensor *parent, Filter *next); - void input(const std::string &value); + void input(std::string value); - void output(const std::string &value); + void output(std::string &value); protected: friend TextSensor; @@ -45,15 +44,14 @@ using lambda_filter_t = std::function(std::string)>; /** This class allows for creation of simple template filters. * * The constructor accepts a lambda of the form std::string -> optional. - * It will be called with each new value in the filter chain and returns the modified - * value that shall be passed down the filter chain. Returning an empty Optional - * means that the value shall be discarded. + * Return a modified string to continue the chain, or return {} to stop + * (value will not be published). */ class LambdaFilter : public Filter { public: explicit LambdaFilter(lambda_filter_t lambda_filter); - optional new_value(std::string value) override; + bool new_value(std::string &value) override; const lambda_filter_t &get_lambda_filter() const; void set_lambda_filter(const lambda_filter_t &lambda_filter); @@ -71,7 +69,14 @@ class StatelessLambdaFilter : public Filter { public: explicit StatelessLambdaFilter(optional (*lambda_filter)(std::string)) : lambda_filter_(lambda_filter) {} - optional new_value(std::string value) override { return this->lambda_filter_(value); } + bool new_value(std::string &value) override { + auto result = this->lambda_filter_(value); + if (result.has_value()) { + value = std::move(*result); + return true; + } + return false; + } protected: optional (*lambda_filter_)(std::string); @@ -80,20 +85,20 @@ class StatelessLambdaFilter : public Filter { /// A simple filter that converts all text to uppercase class ToUpperFilter : public Filter { public: - optional new_value(std::string value) override; + bool new_value(std::string &value) override; }; /// A simple filter that converts all text to lowercase class ToLowerFilter : public Filter { public: - optional new_value(std::string value) override; + bool new_value(std::string &value) override; }; /// A simple filter that adds a string to the end of another string class AppendFilter : public Filter { public: explicit AppendFilter(const char *suffix) : suffix_(suffix) {} - optional new_value(std::string value) override; + bool new_value(std::string &value) override; protected: const char *suffix_; @@ -103,7 +108,7 @@ class AppendFilter : public Filter { class PrependFilter : public Filter { public: explicit PrependFilter(const char *prefix) : prefix_(prefix) {} - optional new_value(std::string value) override; + bool new_value(std::string &value) override; protected: const char *prefix_; @@ -118,7 +123,7 @@ struct Substitution { class SubstituteFilter : public Filter { public: explicit SubstituteFilter(const std::initializer_list &substitutions); - optional new_value(std::string value) override; + bool new_value(std::string &value) override; protected: FixedVector substitutions_; @@ -151,7 +156,7 @@ class SubstituteFilter : public Filter { class MapFilter : public Filter { public: explicit MapFilter(const std::initializer_list &mappings); - optional new_value(std::string value) override; + bool new_value(std::string &value) override; protected: FixedVector mappings_; From 73a249c0753c39dce3aed103cd19d951c45cd63f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Jan 2026 18:02:01 -1000 Subject: [PATCH 3/6] [esp32] Default to CMN certificate bundle, saving ~51KB flash (#13574) --- esphome/components/esp32/__init__.py | 29 +++++++++++++++++++++ esphome/components/esp32/const.py | 1 + esphome/components/http_request/__init__.py | 10 +++++++ tests/components/esp32/test.esp32-idf.yaml | 1 + 4 files changed, 41 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b7faccaed6..fccf0ed09f 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -55,6 +55,7 @@ from .const import ( # noqa KEY_ESP32, KEY_EXTRA_BUILD_FILES, KEY_FLASH_SIZE, + KEY_FULL_CERT_BUNDLE, KEY_PATH, KEY_REF, KEY_REPO, @@ -670,6 +671,7 @@ CONF_FREERTOS_IN_IRAM = "freertos_in_iram" CONF_RINGBUF_IN_IRAM = "ringbuf_in_iram" CONF_HEAP_IN_IRAM = "heap_in_iram" CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" +CONF_USE_FULL_CERTIFICATE_BUNDLE = "use_full_certificate_bundle" # VFS requirement tracking # Components that need VFS features can call require_vfs_select() or require_vfs_dir() @@ -695,6 +697,18 @@ def require_vfs_dir() -> None: CORE.data[KEY_VFS_DIR_REQUIRED] = True +def require_full_certificate_bundle() -> None: + """Request the full certificate bundle instead of the common-CAs-only bundle. + + By default, ESPHome uses CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN which + includes only CAs with >1% market share (~51 KB smaller than full bundle). + This covers ~99% of websites including Let's Encrypt, DigiCert, Google, Amazon. + + Call this from components that need to connect to services using uncommon CAs. + """ + CORE.data[KEY_ESP32][KEY_FULL_CERT_BUNDLE] = True + + def _parse_idf_component(value: str) -> ConfigType: """Parse IDF component shorthand syntax like 'owner/component^version'""" # Match operator followed by version-like string (digit or *) @@ -776,6 +790,9 @@ FRAMEWORK_SCHEMA = cv.Schema( min=8192, max=32768 ), cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean, + cv.Optional( + CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False + ): cv.boolean, } ), cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( @@ -1093,6 +1110,18 @@ async def to_code(config): cg.add_build_flag("-Wno-nonnull-compare") + # Use CMN (common CAs) bundle by default to save ~51KB flash + # CMN covers CAs with >1% market share (~99% of websites) + # Components needing uncommon CAs can call require_full_certificate_bundle() + use_full_bundle = conf[CONF_ADVANCED].get( + CONF_USE_FULL_CERTIFICATE_BUNDLE, False + ) or CORE.data[KEY_ESP32].get(KEY_FULL_CERT_BUNDLE, False) + add_idf_sdkconfig_option( + "CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL", use_full_bundle + ) + if not use_full_bundle: + add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN", True) + add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True) add_idf_sdkconfig_option( f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py index 2a9456db23..9f8165818b 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -12,6 +12,7 @@ KEY_REFRESH = "refresh" KEY_PATH = "path" KEY_SUBMODULES = "submodules" KEY_EXTRA_BUILD_FILES = "extra_build_files" +KEY_FULL_CERT_BUNDLE = "full_cert_bundle" VARIANT_ESP32 = "ESP32" VARIANT_ESP32C2 = "ESP32C2" diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 7347b8ebf7..07bc758037 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -165,6 +165,16 @@ async def to_code(config): ca_cert_content = f.read() cg.add(var.set_ca_certificate(ca_cert_content)) else: + # Uses the certificate bundle configured in esp32 component. + # By default, ESPHome uses the CMN (common CAs) bundle which covers + # ~99% of websites including GitHub, Let's Encrypt, DigiCert, etc. + # If connecting to services with uncommon CAs, components can call: + # esp32.require_full_certificate_bundle() + # Or users can set in their config: + # esp32: + # framework: + # advanced: + # use_full_certificate_bundle: true esp32.add_idf_sdkconfig_option( "CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True ) diff --git a/tests/components/esp32/test.esp32-idf.yaml b/tests/components/esp32/test.esp32-idf.yaml index 0e220623a1..d38cdfe2fd 100644 --- a/tests/components/esp32/test.esp32-idf.yaml +++ b/tests/components/esp32/test.esp32-idf.yaml @@ -7,6 +7,7 @@ esp32: enable_lwip_mdns_queries: true enable_lwip_bridge_interface: true disable_libc_locks_in_iram: false # Test explicit opt-out of RAM optimization + use_full_certificate_bundle: false # Test CMN bundle (default) wifi: ssid: MySSID From ded835ab63a3a7abf7a68bde0d618cd9254732a4 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Wed, 28 Jan 2026 05:51:18 +0100 Subject: [PATCH 4/6] [nrf52] Move toolchain to platform (#13498) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .clang-tidy.hash | 2 +- esphome/components/nrf52/__init__.py | 23 ++++++++++++++----- platformio.ini | 5 ++-- .../components/nrf52/test.nrf52-adafruit.yaml | 2 +- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index a1dcdf5799..1cb5f98c28 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -08c21fa4c044fd80c8f3296371f30c4f5ff3418f1bc1efe63c5bad938301f122 +cf3d341206b4184ec8b7fe85141aef4fe4696aa720c3f8a06d4e57930574bdab diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 5fb8abddfc..7d3d59f0ad 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -69,9 +69,21 @@ def set_core_data(config: ConfigType) -> ConfigType: def set_framework(config: ConfigType) -> ConfigType: - version = cv.Version.parse(cv.version_number(config[CONF_FRAMEWORK][CONF_VERSION])) - CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = version - return config + framework_ver = cv.Version.parse( + cv.version_number(config[CONF_FRAMEWORK][CONF_VERSION]) + ) + CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = framework_ver + if framework_ver < cv.Version(2, 9, 2): + return cv.require_framework_version( + nrf52_zephyr=cv.Version(2, 6, 1, "a"), + )(config) + if framework_ver < cv.Version(3, 2, 0): + return cv.require_framework_version( + nrf52_zephyr=cv.Version(2, 9, 2, "2"), + )(config) + return cv.require_framework_version( + nrf52_zephyr=cv.Version(3, 2, 0, "1"), + )(config) BOOTLOADERS = [ @@ -140,7 +152,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_UICR_ERASE, default=False): cv.boolean, } ), - cv.Optional(CONF_FRAMEWORK, default={CONF_VERSION: "2.6.1-7"}): cv.Schema( + cv.Optional(CONF_FRAMEWORK, default={CONF_VERSION: "2.6.1-a"}): cv.Schema( { cv.Required(CONF_VERSION): cv.string_strict, } @@ -181,13 +193,12 @@ async def to_code(config: ConfigType) -> None: cg.add_platformio_option(CONF_FRAMEWORK, CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK]) cg.add_platformio_option( "platform", - "https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-1.zip", + "https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-5.zip", ) cg.add_platformio_option( "platform_packages", [ f"platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v{CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}.zip", - "platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.17.4-0.zip", ], ) diff --git a/platformio.ini b/platformio.ini index c5a6768ad2..0f5bf2f8fb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -230,11 +230,10 @@ build_src_flags = -include Arduino.h ; This is the common settings for the nRF52 using Zephyr. [common:nrf52-zephyr] extends = common -platform = https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-1.zip +platform = https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-5.zip framework = zephyr platform_packages = - platformio/framework-zephyr @ https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-7.zip - platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.17.4-0.zip + platformio/framework-zephyr @ https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-a.zip build_flags = ${common.build_flags} -DUSE_ZEPHYR diff --git a/tests/components/nrf52/test.nrf52-adafruit.yaml b/tests/components/nrf52/test.nrf52-adafruit.yaml index 0ad31993ae..300cb7b5d7 100644 --- a/tests/components/nrf52/test.nrf52-adafruit.yaml +++ b/tests/components/nrf52/test.nrf52-adafruit.yaml @@ -20,4 +20,4 @@ nrf52: voltage: 2.1V uicr_erase: true framework: - version: "2.6.1-7" + version: "2.6.1-a" From b4f63fd992b1fb77a2bdc09b71034dcfd021ba81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 27 Jan 2026 19:11:30 -1000 Subject: [PATCH 5/6] [core] Add LOG_ENTITY_ICON/DEVICE_CLASS/UNIT_OF_MEASUREMENT macros (#13578) --- .../components/binary_sensor/binary_sensor.cpp | 5 +---- esphome/components/button/button.cpp | 5 +---- esphome/components/cover/cover.h | 4 +--- esphome/components/datetime/date_entity.h | 4 +--- esphome/components/datetime/datetime_entity.h | 4 +--- esphome/components/datetime/time_entity.h | 4 +--- esphome/components/event/event.h | 8 ++------ esphome/components/lock/lock.h | 4 +--- esphome/components/number/number.cpp | 15 +++------------ esphome/components/select/select.h | 4 +--- esphome/components/sensor/sensor.cpp | 9 ++------- esphome/components/switch/switch.cpp | 8 ++------ esphome/components/text/text.h | 4 +--- esphome/components/text_sensor/text_sensor.cpp | 10 ++-------- esphome/components/valve/valve.h | 4 +--- esphome/core/entity_base.cpp | 18 ++++++++++++++++++ esphome/core/entity_base.h | 10 ++++++++++ 17 files changed, 49 insertions(+), 71 deletions(-) diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 4fe2a019e0..7c3b06970d 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -14,10 +14,7 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi } ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - - if (!obj->get_device_class_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); - } + LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj); } void BinarySensor::publish_state(bool new_state) { diff --git a/esphome/components/button/button.cpp b/esphome/components/button/button.cpp index 87a222776e..8c06cfe59b 100644 --- a/esphome/components/button/button.cpp +++ b/esphome/components/button/button.cpp @@ -12,10 +12,7 @@ void log_button(const char *tag, const char *prefix, const char *type, Button *o } ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - - if (!obj->get_icon_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); - } + LOG_ENTITY_ICON(tag, prefix, *obj); } void Button::press() { diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index e710915a0e..e5427ceaa8 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -20,9 +20,7 @@ const extern float COVER_CLOSED; if (traits_.get_is_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ - if (!(obj)->get_device_class_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \ - } \ + LOG_ENTITY_DEVICE_CLASS(TAG, prefix, *(obj)); \ } class Cover; diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h index 955fd92c45..cbf2b85506 100644 --- a/esphome/components/datetime/date_entity.h +++ b/esphome/components/datetime/date_entity.h @@ -15,9 +15,7 @@ namespace esphome::datetime { #define LOG_DATETIME_DATE(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ - } \ + LOG_ENTITY_ICON(TAG, prefix, *(obj)); \ } class DateCall; diff --git a/esphome/components/datetime/datetime_entity.h b/esphome/components/datetime/datetime_entity.h index b5b8cd677e..b1b8a77846 100644 --- a/esphome/components/datetime/datetime_entity.h +++ b/esphome/components/datetime/datetime_entity.h @@ -15,9 +15,7 @@ namespace esphome::datetime { #define LOG_DATETIME_DATETIME(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ - } \ + LOG_ENTITY_ICON(TAG, prefix, *(obj)); \ } class DateTimeCall; diff --git a/esphome/components/datetime/time_entity.h b/esphome/components/datetime/time_entity.h index e4bb113eb5..3f224684bb 100644 --- a/esphome/components/datetime/time_entity.h +++ b/esphome/components/datetime/time_entity.h @@ -15,9 +15,7 @@ namespace esphome::datetime { #define LOG_DATETIME_TIME(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ - } \ + LOG_ENTITY_ICON(TAG, prefix, *(obj)); \ } class TimeCall; diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index b5519a0520..a7451407bb 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -16,12 +16,8 @@ namespace event { #define LOG_EVENT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ - } \ - if (!(obj)->get_device_class_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \ - } \ + LOG_ENTITY_ICON(TAG, prefix, *(obj)); \ + LOG_ENTITY_DEVICE_CLASS(TAG, prefix, *(obj)); \ } class Event : public EntityBase, public EntityBase_DeviceClass { diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index b518c8b846..69fc405713 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -14,9 +14,7 @@ class Lock; #define LOG_LOCK(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ - } \ + LOG_ENTITY_ICON(TAG, prefix, *(obj)); \ if ((obj)->traits.get_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index b0af604189..1c4126496c 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -14,18 +14,9 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o } ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - - if (!obj->get_icon_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); - } - - if (!obj->traits.get_unit_of_measurement_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj->traits.get_unit_of_measurement_ref().c_str()); - } - - if (!obj->traits.get_device_class_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->traits.get_device_class_ref().c_str()); - } + LOG_ENTITY_ICON(tag, prefix, *obj); + LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, obj->traits); + LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj->traits); } void Number::publish_state(float state) { diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 8b05487704..c91acd1e19 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -12,9 +12,7 @@ namespace esphome::select { #define LOG_SELECT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ - } \ + LOG_ENTITY_ICON(TAG, prefix, *(obj)); \ } #define SUB_SELECT(name) \ diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 9fdb7bbafd..3f2be02af2 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -22,13 +22,8 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o LOG_STR_ARG(state_class_to_string(obj->get_state_class())), prefix, obj->get_unit_of_measurement_ref().c_str(), prefix, obj->get_accuracy_decimals()); - if (!obj->get_device_class_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); - } - - if (!obj->get_icon_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); - } + LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj); + LOG_ENTITY_ICON(tag, prefix, *obj); if (obj->get_force_update()) { ESP_LOGV(tag, "%s Force Update: YES", prefix); diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index d880139c5f..61a273d25c 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -96,18 +96,14 @@ void log_switch(const char *tag, const char *prefix, const char *type, Switch *o LOG_STR_ARG(onoff)); // Add optional fields separately - if (!obj->get_icon_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); - } + LOG_ENTITY_ICON(tag, prefix, *obj); if (obj->assumed_state()) { ESP_LOGCONFIG(tag, "%s Assumed State: YES", prefix); } if (obj->is_inverted()) { ESP_LOGCONFIG(tag, "%s Inverted: YES", prefix); } - if (!obj->get_device_class_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); - } + LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj); } } diff --git a/esphome/components/text/text.h b/esphome/components/text/text.h index e4ad64334b..3a1bea56cb 100644 --- a/esphome/components/text/text.h +++ b/esphome/components/text/text.h @@ -12,9 +12,7 @@ namespace text { #define LOG_TEXT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ - } \ + LOG_ENTITY_ICON(TAG, prefix, *(obj)); \ } /** Base-class for all text inputs. diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 86e2387dc7..c48bdf4b82 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -15,14 +15,8 @@ void log_text_sensor(const char *tag, const char *prefix, const char *type, Text } ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - - if (!obj->get_device_class_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); - } - - if (!obj->get_icon_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); - } + LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj); + LOG_ENTITY_ICON(tag, prefix, *obj); } void TextSensor::publish_state(const std::string &state) { this->publish_state(state.data(), state.size()); } diff --git a/esphome/components/valve/valve.h b/esphome/components/valve/valve.h index 2b3419b67a..cd46144372 100644 --- a/esphome/components/valve/valve.h +++ b/esphome/components/valve/valve.h @@ -20,9 +20,7 @@ const extern float VALVE_CLOSED; if (traits_.get_is_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ - if (!(obj)->get_device_class_ref().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \ - } \ + LOG_ENTITY_DEVICE_CLASS(TAG, prefix, *(obj)); \ } class Valve; diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 7d7878f53a..811b856b5e 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -152,4 +152,22 @@ void EntityBase_UnitOfMeasurement::set_unit_of_measurement(const char *unit_of_m this->unit_of_measurement_ = unit_of_measurement; } +void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) { + if (!obj.get_icon_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj.get_icon_ref().c_str()); + } +} + +void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj) { + if (!obj.get_device_class_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj.get_device_class_ref().c_str()); + } +} + +void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase_UnitOfMeasurement &obj) { + if (!obj.get_unit_of_measurement_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj.get_unit_of_measurement_ref().c_str()); + } +} + } // namespace esphome diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 0b75b25817..86cb75495b 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -230,6 +230,16 @@ class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) const char *unit_of_measurement_{nullptr}; ///< Unit of measurement override }; +/// Log entity icon if set (for use in dump_config) +#define LOG_ENTITY_ICON(tag, prefix, obj) log_entity_icon(tag, prefix, obj) +void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj); +/// Log entity device class if set (for use in dump_config) +#define LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj) log_entity_device_class(tag, prefix, obj) +void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj); +/// Log entity unit of measurement if set (for use in dump_config) +#define LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, obj) log_entity_unit_of_measurement(tag, prefix, obj) +void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase_UnitOfMeasurement &obj); + /** * An entity that has a state. * @tparam T The type of the state From 22e0a8ce2e330af01e4fe6c06c03794b7d2a91e4 Mon Sep 17 00:00:00 2001 From: Hypothalamus Date: Wed, 28 Jan 2026 07:10:49 +0100 Subject: [PATCH 6/6] [hub75] Add Huidu HD-WF1 board configuration (#13341) --- esphome/components/hub75/boards/huidu.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/esphome/components/hub75/boards/huidu.py b/esphome/components/hub75/boards/huidu.py index 52744d397e..c4e4c3c135 100644 --- a/esphome/components/hub75/boards/huidu.py +++ b/esphome/components/hub75/boards/huidu.py @@ -2,6 +2,25 @@ from . import BoardConfig +# Huidu HD-WF1 +BoardConfig( + "huidu-hd-wf1", + r1_pin=2, + g1_pin=6, + b1_pin=3, + r2_pin=4, + g2_pin=8, + b2_pin=5, + a_pin=39, + b_pin=38, + c_pin=37, + d_pin=36, + e_pin=12, + lat_pin=33, + oe_pin=35, + clk_pin=34, +) + # Huidu HD-WF2 BoardConfig( "huidu-hd-wf2",