Merge branch 'dev' into copilot/update-cover-component-triggers

This commit is contained in:
Clyde Stubbs
2026-01-28 17:07:10 +11:00
committed by GitHub
40 changed files with 252 additions and 171 deletions

View File

@@ -1 +1 @@
a172e2f65981e98354cc6b5ecf69bdb055dd13602226042ab2c7acd037a2bf41
cf3d341206b4184ec8b7fe85141aef4fe4696aa720c3f8a06d4e57930574bdab

View File

@@ -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

View File

@@ -8,8 +8,8 @@
// Use ESPAsyncTCP library for ESP8266 (always Arduino)
#include <ESPAsyncTCP.h>
#elif defined(USE_RP2040)
// Use AsyncTCP_RP2040W library for RP2040
#include <AsyncTCP_RP2040W.h>
// Use RPAsyncTCP library for RP2040
#include <RPAsyncTCP.h>
#else
// Use socket-based implementation for other platforms
#include "async_tcp_socket.h"

View File

@@ -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) {

View File

@@ -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() {

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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
)

View File

@@ -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); \
} \

View File

@@ -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

View File

@@ -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",
],
)

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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) \

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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.

View File

@@ -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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> AppendFilter::new_value(std::string value) {
bool AppendFilter::new_value(std::string &value) {
value.append(this->suffix_);
return value;
return true;
}
// Prepend
optional<std::string> 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<Substitution> &substitutions)
: substitutions_(substitutions) {}
optional<std::string> 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<std::string> SubstituteFilter::new_value(std::string value) {
pos += to_len;
}
}
return value;
return true;
}
// Map
MapFilter::MapFilter(const std::initializer_list<Substitution> &mappings) : mappings_(mappings) {}
optional<std::string> 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

View File

@@ -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<std::string> 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<optional<std::string>(std::string)>;
/** This class allows for creation of simple template filters.
*
* The constructor accepts a lambda of the form std::string -> optional<std::string>.
* 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<std::string> 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<std::string> (*lambda_filter)(std::string)) : lambda_filter_(lambda_filter) {}
optional<std::string> 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<std::string> (*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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<Substitution> &substitutions);
optional<std::string> new_value(std::string value) override;
bool new_value(std::string &value) override;
protected:
FixedVector<Substitution> substitutions_;
@@ -151,7 +156,7 @@ class SubstituteFilter : public Filter {
class MapFilter : public Filter {
public:
explicit MapFilter(const std::initializer_list<Substitution> &mappings);
optional<std::string> new_value(std::string value) override;
bool new_value(std::string &value) override;
protected:
FixedVector<Substitution> mappings_;

View File

@@ -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()); }

View File

@@ -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;

View File

@@ -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,
]
),

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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")

View File

@@ -246,21 +246,16 @@ optional<std::string> 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<char, URL_BUF_SIZE> buffer) const {
const char *uri = this->req_->uri;
const char *query_start = strchr(uri, '?');
size_t uri_len = query_start ? static_cast<size_t>(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(); }

View File

@@ -3,12 +3,14 @@
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/string_ref.h"
#include <esp_http_server.h>
#include <atomic>
#include <functional>
#include <list>
#include <map>
#include <span>
#include <string>
#include <utility>
#include <vector>
@@ -110,7 +112,15 @@ class AsyncWebServerRequest {
~AsyncWebServerRequest();
http_method method() const { return static_cast<http_method>(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<char, URL_BUF_SIZE> 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;

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -20,4 +20,4 @@ nrf52:
voltage: 2.1V
uicr_erase: true
framework:
version: "2.6.1-7"
version: "2.6.1-a"

View File

@@ -0,0 +1 @@
<<: !include common_v2.yaml