diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 55239f961c..1cb5f98c28 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -a172e2f65981e98354cc6b5ecf69bdb055dd13602226042ab2c7acd037a2bf41 +cf3d341206b4184ec8b7fe85141aef4fe4696aa720c3f8a06d4e57930574bdab 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/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/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index bf65ae67c0..8d88a10b27 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -96,10 +96,16 @@ void CaptivePortal::start() { } void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { - if (req->url() == ESPHOME_F("/config.json")) { +#ifdef USE_ESP32 + char url_buf[AsyncWebServerRequest::URL_BUF_SIZE]; + StringRef url = req->url_to(url_buf); +#else + const auto &url = req->url(); +#endif + if (url == ESPHOME_F("/config.json")) { this->handle_config(req); return; - } else if (req->url() == ESPHOME_F("/wifisave")) { + } else if (url == ESPHOME_F("/wifisave")) { this->handle_wifisave(req); return; } diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 5258a0d9e4..3da6109e37 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -20,9 +20,7 @@ static constexpr const float COVER_CLOSED = 0.0f; 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/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/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/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/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/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 4bba33f961..e57a258edb 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -150,27 +150,24 @@ void Nextion::dump_config() { #ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE ESP_LOGCONFIG(TAG, " Skip handshake: YES"); #else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE - ESP_LOGCONFIG(TAG, #ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO + ESP_LOGCONFIG(TAG, " Device Model: %s\n" " FW Version: %s\n" " Serial Number: %s\n" " Flash Size: %s\n" " Max queue age: %u ms\n" " Startup override: %u ms\n", + this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(), + this->flash_size_.c_str(), this->max_q_age_ms_, this->startup_override_ms_); #endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO #ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START - " Exit reparse: YES\n" + ESP_LOGCONFIG(TAG, " Exit reparse: YES\n"); #endif // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START + ESP_LOGCONFIG(TAG, " Wake On Touch: %s\n" " Touch Timeout: %" PRIu16, -#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO - this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(), - this->flash_size_.c_str(), this->max_q_age_ms_, - this->startup_override_ms_ -#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO - YESNO(this->connection_state_.auto_wake_on_touch_), - this->touch_sleep_timeout_); + YESNO(this->connection_state_.auto_wake_on_touch_), this->touch_sleep_timeout_); #endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP 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/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/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index fc48ad67e3..7aecab99d1 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -41,12 +41,14 @@ class PrometheusHandler : public AsyncWebHandler, public Component { void add_label_name(EntityBase *obj, const std::string &value) { relabel_map_name_.insert({obj, value}); } bool canHandle(AsyncWebServerRequest *request) const override { - if (request->method() == HTTP_GET) { - if (request->url() == "/metrics") - return true; - } - - return false; + if (request->method() != HTTP_GET) + return false; +#ifdef USE_ESP32 + char url_buf[AsyncWebServerRequest::URL_BUF_SIZE]; + return request->url_to(url_buf) == "/metrics"; +#else + return request->url() == ESPHOME_F("/metrics"); +#endif } void handleRequest(AsyncWebServerRequest *req) override; 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/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_; 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/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/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 3793f01eb5..4be162ccd3 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -32,8 +32,15 @@ class OTARequestHandler : public AsyncWebHandler { void handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data, size_t len, bool final) override; bool canHandle(AsyncWebServerRequest *request) const override { - // Check if this is an OTA update request - bool is_ota_request = request->url() == "/update" && request->method() == HTTP_POST; + if (request->method() != HTTP_POST) + return false; + // Check if this is an OTA update request +#ifdef USE_ESP32 + char url_buf[AsyncWebServerRequest::URL_BUF_SIZE]; + bool is_ota_request = request->url_to(url_buf) == "/update"; +#else + bool is_ota_request = request->url() == ESPHOME_F("/update"); +#endif #if defined(USE_WEBSERVER_OTA_DISABLED) && defined(USE_CAPTIVE_PORTAL) // IMPORTANT: USE_WEBSERVER_OTA_DISABLED only disables OTA for the web_server component diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index a19c1c9b17..d30cb524f4 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -2187,7 +2187,12 @@ std::string WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_ #endif bool WebServer::canHandle(AsyncWebServerRequest *request) const { +#ifdef USE_ESP32 + char url_buf[AsyncWebServerRequest::URL_BUF_SIZE]; + StringRef url = request->url_to(url_buf); +#else const auto &url = request->url(); +#endif const auto method = request->method(); // Static URL checks - use ESPHOME_F to keep strings in flash on ESP8266 @@ -2323,30 +2328,35 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const { return false; } void WebServer::handleRequest(AsyncWebServerRequest *request) { +#ifdef USE_ESP32 + char url_buf[AsyncWebServerRequest::URL_BUF_SIZE]; + StringRef url = request->url_to(url_buf); +#else const auto &url = request->url(); +#endif // Handle static routes first - if (url == "/") { + if (url == ESPHOME_F("/")) { this->handle_index_request(request); return; } #if !defined(USE_ESP32) && defined(USE_ARDUINO) - if (url == "/events") { + if (url == ESPHOME_F("/events")) { this->events_.add_new_client(this, request); return; } #endif #ifdef USE_WEBSERVER_CSS_INCLUDE - if (url == "/0.css") { + if (url == ESPHOME_F("/0.css")) { this->handle_css_request(request); return; } #endif #ifdef USE_WEBSERVER_JS_INCLUDE - if (url == "/0.js") { + if (url == ESPHOME_F("/0.js")) { this->handle_js_request(request); return; } 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/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index abeda5fc46..2e5a74cbef 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -246,21 +246,16 @@ optional AsyncWebServerRequest::get_header(const char *name) const return request_get_header(*this, name); } -std::string AsyncWebServerRequest::url() const { - auto *query_start = strchr(this->req_->uri, '?'); - std::string result; - if (query_start == nullptr) { - result = this->req_->uri; - } else { - result = std::string(this->req_->uri, query_start - this->req_->uri); - } +StringRef AsyncWebServerRequest::url_to(std::span buffer) const { + const char *uri = this->req_->uri; + const char *query_start = strchr(uri, '?'); + size_t uri_len = query_start ? static_cast(query_start - uri) : strlen(uri); + size_t copy_len = std::min(uri_len, URL_BUF_SIZE - 1); + memcpy(buffer.data(), uri, copy_len); + buffer[copy_len] = '\0'; // Decode URL-encoded characters in-place (e.g., %20 -> space) - // This matches AsyncWebServer behavior on Arduino - if (!result.empty()) { - size_t new_len = url_decode(&result[0]); - result.resize(new_len); - } - return result; + size_t decoded_len = url_decode(buffer.data()); + return StringRef(buffer.data(), decoded_len); } std::string AsyncWebServerRequest::host() const { return this->get_header("Host").value(); } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index a6c984792a..e38913ef4a 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -3,12 +3,14 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" #include #include #include #include #include +#include #include #include #include @@ -110,7 +112,15 @@ class AsyncWebServerRequest { ~AsyncWebServerRequest(); http_method method() const { return static_cast(this->req_->method); } - std::string url() const; + static constexpr size_t URL_BUF_SIZE = CONFIG_HTTPD_MAX_URI_LEN + 1; ///< Buffer size for url_to() + /// Write URL (without query string) to buffer, returns StringRef pointing to buffer. + /// URL is decoded (e.g., %20 -> space). + StringRef url_to(std::span buffer) const; + /// Get URL as std::string. Prefer url_to() to avoid heap allocation. + std::string url() const { + char buffer[URL_BUF_SIZE]; + return std::string(this->url_to(buffer)); + } std::string host() const; // NOLINTNEXTLINE(readability-identifier-naming) size_t contentLength() const { return this->req_->content_len; } @@ -306,7 +316,10 @@ class AsyncEventSource : public AsyncWebHandler { // NOLINTNEXTLINE(readability-identifier-naming) bool canHandle(AsyncWebServerRequest *request) const override { - return request->method() == HTTP_GET && request->url() == this->url_; + if (request->method() != HTTP_GET) + return false; + char url_buf[AsyncWebServerRequest::URL_BUF_SIZE]; + return request->url_to(url_buf) == this->url_; } // NOLINTNEXTLINE(readability-identifier-naming) void handleRequest(AsyncWebServerRequest *request) override; 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 diff --git a/platformio.ini b/platformio.ini index accc40ecf2..0f5bf2f8fb 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 = @@ -229,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/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 diff --git a/tests/components/nextion/common.yaml b/tests/components/nextion/common.yaml index 48a7292f43..4373fe5462 100644 --- a/tests/components/nextion/common.yaml +++ b/tests/components/nextion/common.yaml @@ -273,12 +273,9 @@ text_sensor: display: - platform: nextion id: main_lcd - update_interval: 5s - command_spacing: 5ms max_commands_per_loop: 20 max_queue_size: 50 - startup_override_ms: 10000ms # Wait 10s for display ready - max_queue_age: 5000ms # Remove queue items after 5s + update_interval: 5s on_sleep: then: lambda: 'ESP_LOGD("display","Display went to sleep");' @@ -294,3 +291,8 @@ display: on_buffer_overflow: then: logger.log: "Nextion reported a buffer overflow!" + + command_spacing: 5ms + dump_device_info: true + max_queue_age: 5000ms # Remove queue items after 5s + startup_override_ms: 10000ms # Wait 10s for display ready 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" 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