Compare commits

..

23 Commits

Author SHA1 Message Date
Clyde Stubbs
0a1fa05c8f Merge branch 'dev' into template_select_trigger 2026-02-03 14:57:02 +11:00
clydebarrow
cb9fbf8970 Fix parameter name; set update_interval to never if no lambda to poll 2026-02-03 14:56:21 +11:00
Roger Fachini
a430b3a426 [speaker.media_player]: Add verbose error message for puremagic parsing (#13725)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-03 03:46:46 +00:00
J. Nick Koston
fbeb0e8e54 [opentherm] Fix ESP-IDF build by re-enabling legacy driver component (#13732) 2026-02-03 03:40:44 +00:00
J. Nick Koston
9d63642bdb [media_player] Store command strings in flash and avoid heap allocation in set_command (#13731) 2026-02-03 04:29:43 +01:00
J. Nick Koston
8cb701e412 [water_heater] Store mode strings in flash and avoid heap allocation in set_mode (#13728) 2026-02-03 04:29:31 +01:00
J. Nick Koston
d41c84d624 [wifi] Conditionally compile on_connect/on_disconnect triggers (#13684) 2026-02-03 04:29:18 +01:00
J. Nick Koston
9f1a427ce2 [preferences] Use static storage for singletons and flash buffer (#13727) 2026-02-03 04:03:52 +01:00
J. Nick Koston
ae71f07abb [http_request] Fix requests taking full timeout when response is already complete (#13649) 2026-02-03 03:19:38 +01:00
clydebarrow
5a2774876a Use templates to customise classes 2026-02-03 13:14:09 +11:00
J. Nick Koston
ccf5c1f7e9 [esp32] Exclude additional unused IDF components (driver, dac, mcpwm, twai, openthread, ulp) (#13664) 2026-02-03 03:12:12 +01:00
dependabot[bot]
efecea9450 Bump github/codeql-action from 4.32.0 to 4.32.1 (#13726)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 02:27:34 +01:00
J. Nick Koston
26e4cda610 [logger] Use vsnprintf_P directly for ESP8266 flash format strings (#13716) 2026-02-03 02:25:54 +01:00
clydebarrow
ede2f205d3 Merge branch 'template_select_trigger' of https://github.com/esphome/esphome into template_select_trigger 2026-02-03 08:33:25 +11:00
Clyde Stubbs
8cf29c40a9 Merge branch 'dev' into template_select_trigger 2026-02-03 08:33:10 +11:00
clydebarrow
0332cbfdd4 Merge branch 'dev' of https://github.com/esphome/esphome into template_select_trigger 2026-02-03 08:32:40 +11:00
J. Nick Koston
89bd9b610e modify in validation instead to avoid copy 2026-02-02 02:48:00 +01:00
J. Nick Koston
9dbcf1447b integration test 2026-02-02 02:45:43 +01:00
J. Nick Koston
6c853cae57 use pattern from sensor filters 2026-02-02 02:40:45 +01:00
J. Nick Koston
48e6efb6aa use pattern from sensor filters 2026-02-02 02:40:30 +01:00
J. Nick Koston
cfc3b3336f fix 2026-02-02 02:35:51 +01:00
J. Nick Koston
9ca394d1e5 not as bad as I was thinking it would be 2026-02-02 02:31:29 +01:00
J. Nick Koston
e62a87afe1 [template] Conditionally compile select set_trigger
Only allocate the set_trigger when set_action is configured.
This saves ~20-24 bytes of heap per template select that doesn't
use set_action.
2026-02-01 20:39:52 +01:00
47 changed files with 484 additions and 429 deletions

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 uses: github/codeql-action/init@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 uses: github/codeql-action/analyze@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@@ -124,10 +124,14 @@ COMPILER_OPTIMIZATIONS = {
# - "sdmmc": driver -> esp_driver_sdmmc -> sdmmc dependency chain # - "sdmmc": driver -> esp_driver_sdmmc -> sdmmc dependency chain
DEFAULT_EXCLUDED_IDF_COMPONENTS = ( DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing "cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing
"driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers
"esp_adc", # ADC driver - only needed by adc component "esp_adc", # ADC driver - only needed by adc component
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
"esp_driver_i2s", # I2S driver - only needed by i2s_audio component "esp_driver_i2s", # I2S driver - only needed by i2s_audio component
"esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM
"esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus "esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch "esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component
"esp_eth", # Ethernet driver - only needed by ethernet component "esp_eth", # Ethernet driver - only needed by ethernet component
"esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality "esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality
"esp_http_client", # HTTP client - only needed by http_request component "esp_http_client", # HTTP client - only needed by http_request component
@@ -138,9 +142,11 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"espcoredump", # Core dump support - ESPHome has its own debug component "espcoredump", # Core dump support - ESPHome has its own debug component
"fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage "fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage
"mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation "mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation
"openthread", # Thread protocol - only needed by openthread component
"perfmon", # Xtensa performance monitor - ESPHome has its own debug component "perfmon", # Xtensa performance monitor - ESPHome has its own debug component
"protocomm", # Protocol communication for provisioning - unused by ESPHome "protocomm", # Protocol communication for provisioning - unused by ESPHome
"spiffs", # SPIFFS filesystem - ESPHome doesn't use filesystem storage (IDF only) "spiffs", # SPIFFS filesystem - ESPHome doesn't use filesystem storage (IDF only)
"ulp", # ULP coprocessor - not currently used by any ESPHome component
"unity", # Unit testing framework - ESPHome doesn't use IDF's testing "unity", # Unit testing framework - ESPHome doesn't use IDF's testing
"wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused "wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused
"wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation "wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation

View File

@@ -203,10 +203,11 @@ class ESP32Preferences : public ESPPreferences {
} }
}; };
static ESP32Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void setup_preferences() { void setup_preferences() {
auto *prefs = new ESP32Preferences(); // NOLINT(cppcoreguidelines-owning-memory) s_preferences.open();
prefs->open(); global_preferences = &s_preferences;
global_preferences = prefs;
} }
} // namespace esp32 } // namespace esp32

View File

@@ -15,6 +15,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
get_esp32_variant, get_esp32_variant,
include_builtin_idf_component,
) )
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -121,6 +122,10 @@ def get_default_tx_enqueue_timeout(bit_rate):
async def to_code(config): async def to_code(config):
# Legacy driver component provides driver/twai.h header
include_builtin_idf_component("driver")
# Also enable esp_driver_twai for future migration to new API
include_builtin_idf_component("esp_driver_twai")
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await canbus.register_canbus(var, config) await canbus.register_canbus(var, config)

View File

@@ -1,7 +1,12 @@
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import output from esphome.components import output
from esphome.components.esp32 import VARIANT_ESP32, VARIANT_ESP32S2, get_esp32_variant from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32S2,
get_esp32_variant,
include_builtin_idf_component,
)
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN
@@ -38,6 +43,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
async def to_code(config): async def to_code(config):
include_builtin_idf_component("esp_driver_dac")
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await output.register_output(var, config) await output.register_output(var, config)

View File

@@ -211,11 +211,14 @@ bool Esp32HostedUpdate::fetch_manifest_() {
int read_or_error = container->read(buf, sizeof(buf)); int read_or_error = container->read(buf, sizeof(buf));
App.feed_wdt(); App.feed_wdt();
yield(); yield();
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout); auto result =
http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
if (result == http_request::HttpReadLoopResult::RETRY) if (result == http_request::HttpReadLoopResult::RETRY)
continue; continue;
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
// but this is defensive code in case chunked transfer encoding support is added in the future.
if (result != http_request::HttpReadLoopResult::DATA) if (result != http_request::HttpReadLoopResult::DATA)
break; // ERROR or TIMEOUT break; // COMPLETE, ERROR, or TIMEOUT
json_str.append(reinterpret_cast<char *>(buf), read_or_error); json_str.append(reinterpret_cast<char *>(buf), read_or_error);
} }
container->end(); container->end();
@@ -336,9 +339,14 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
App.feed_wdt(); App.feed_wdt();
yield(); yield();
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout); auto result =
http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
if (result == http_request::HttpReadLoopResult::RETRY) if (result == http_request::HttpReadLoopResult::RETRY)
continue; continue;
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
// but this is defensive code in case chunked transfer encoding support is added in the future.
if (result == http_request::HttpReadLoopResult::COMPLETE)
break;
if (result != http_request::HttpReadLoopResult::DATA) { if (result != http_request::HttpReadLoopResult::DATA) {
if (result == http_request::HttpReadLoopResult::TIMEOUT) { if (result == http_request::HttpReadLoopResult::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading firmware data"); ESP_LOGE(TAG, "Timeout reading firmware data");

View File

@@ -269,6 +269,8 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
# Re-enable ESP-IDF's touch sensor driver (excluded by default to save compile time) # Re-enable ESP-IDF's touch sensor driver (excluded by default to save compile time)
include_builtin_idf_component("esp_driver_touch_sens") include_builtin_idf_component("esp_driver_touch_sens")
# Legacy driver component provides driver/touch_sensor.h header
include_builtin_idf_component("driver")
touch = cg.new_Pvariable(config[CONF_ID]) touch = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(touch, config) await cg.register_component(touch, config)

View File

@@ -17,10 +17,6 @@ namespace esphome::esp8266 {
static const char *const TAG = "esp8266.preferences"; static const char *const TAG = "esp8266.preferences";
static uint32_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200; static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200;
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128;
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4;
@@ -43,6 +39,11 @@ static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64; static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64;
#endif #endif
static uint32_t
s_flash_storage[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) { static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) {
if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) { if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
return false; return false;
@@ -180,7 +181,6 @@ class ESP8266Preferences : public ESPPreferences {
uint32_t current_flash_offset = 0; // in words uint32_t current_flash_offset = 0; // in words
void setup() { void setup() {
s_flash_storage = new uint32_t[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT
ESP_LOGVV(TAG, "Loading preferences from flash"); ESP_LOGVV(TAG, "Loading preferences from flash");
{ {
@@ -283,10 +283,11 @@ class ESP8266Preferences : public ESPPreferences {
} }
}; };
static ESP8266Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void setup_preferences() { void setup_preferences() {
auto *pref = new ESP8266Preferences(); // NOLINT(cppcoreguidelines-owning-memory) s_preferences.setup();
pref->setup(); global_preferences = &s_preferences;
global_preferences = pref;
} }
void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; } void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; }

View File

@@ -66,10 +66,11 @@ ESPPreferenceObject HostPreferences::make_preference(size_t length, uint32_t typ
return ESPPreferenceObject(backend); return ESPPreferenceObject(backend);
}; };
static HostPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void setup_preferences() { void setup_preferences() {
auto *pref = new HostPreferences(); // NOLINT(cppcoreguidelines-owning-memory) host_preferences = &s_preferences;
host_preferences = pref; global_preferences = &s_preferences;
global_preferences = pref;
} }
bool HostPreferenceBackend::save(const uint8_t *data, size_t len) { bool HostPreferenceBackend::save(const uint8_t *data, size_t len) {

View File

@@ -26,6 +26,7 @@ struct Header {
enum HttpStatus { enum HttpStatus {
HTTP_STATUS_OK = 200, HTTP_STATUS_OK = 200,
HTTP_STATUS_NO_CONTENT = 204, HTTP_STATUS_NO_CONTENT = 204,
HTTP_STATUS_RESET_CONTENT = 205,
HTTP_STATUS_PARTIAL_CONTENT = 206, HTTP_STATUS_PARTIAL_CONTENT = 206,
/* 3xx - Redirection */ /* 3xx - Redirection */
@@ -126,19 +127,21 @@ struct HttpReadResult {
/// Result of processing a non-blocking read with timeout (for manual loops) /// Result of processing a non-blocking read with timeout (for manual loops)
enum class HttpReadLoopResult : uint8_t { enum class HttpReadLoopResult : uint8_t {
DATA, ///< Data was read, process it DATA, ///< Data was read, process it
RETRY, ///< No data yet, already delayed, caller should continue loop COMPLETE, ///< All content has been read, caller should exit loop
ERROR, ///< Read error, caller should exit loop RETRY, ///< No data yet, already delayed, caller should continue loop
TIMEOUT, ///< Timeout waiting for data, caller should exit loop ERROR, ///< Read error, caller should exit loop
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
}; };
/// Process a read result with timeout tracking and delay handling /// Process a read result with timeout tracking and delay handling
/// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error /// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error
/// @param last_data_time Time of last successful read, updated when data received /// @param last_data_time Time of last successful read, updated when data received
/// @param timeout_ms Maximum time to wait for data /// @param timeout_ms Maximum time to wait for data
/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit /// @param is_read_complete Whether all expected content has been read (from HttpContainer::is_read_complete())
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, /// @return How the caller should proceed - see HttpReadLoopResult enum
uint32_t timeout_ms) { inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, uint32_t timeout_ms,
bool is_read_complete) {
if (bytes_read_or_error > 0) { if (bytes_read_or_error > 0) {
last_data_time = millis(); last_data_time = millis();
return HttpReadLoopResult::DATA; return HttpReadLoopResult::DATA;
@@ -146,7 +149,10 @@ inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_
if (bytes_read_or_error < 0) { if (bytes_read_or_error < 0) {
return HttpReadLoopResult::ERROR; return HttpReadLoopResult::ERROR;
} }
// bytes_read_or_error == 0: no data available yet // bytes_read_or_error == 0: either "no data yet" or "all content read"
if (is_read_complete) {
return HttpReadLoopResult::COMPLETE;
}
if (millis() - last_data_time >= timeout_ms) { if (millis() - last_data_time >= timeout_ms) {
return HttpReadLoopResult::TIMEOUT; return HttpReadLoopResult::TIMEOUT;
} }
@@ -159,9 +165,9 @@ class HttpRequestComponent;
class HttpContainer : public Parented<HttpRequestComponent> { class HttpContainer : public Parented<HttpRequestComponent> {
public: public:
virtual ~HttpContainer() = default; virtual ~HttpContainer() = default;
size_t content_length; size_t content_length{0};
int status_code; int status_code{-1}; ///< -1 indicates no response received yet
uint32_t duration_ms; uint32_t duration_ms{0};
/** /**
* @brief Read data from the HTTP response body. * @brief Read data from the HTTP response body.
@@ -194,9 +200,24 @@ class HttpContainer : public Parented<HttpRequestComponent> {
virtual void end() = 0; virtual void end() = 0;
void set_secure(bool secure) { this->secure_ = secure; } void set_secure(bool secure) { this->secure_ = secure; }
void set_chunked(bool chunked) { this->is_chunked_ = chunked; }
size_t get_bytes_read() const { return this->bytes_read_; } size_t get_bytes_read() const { return this->bytes_read_; }
/// Check if all expected content has been read
/// For chunked responses, returns false (completion detected via read() returning error/EOF)
bool is_read_complete() const {
// Per RFC 9112, these responses have no body:
// - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified
if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT ||
this->status_code == HTTP_STATUS_RESET_CONTENT || this->status_code == HTTP_STATUS_NOT_MODIFIED) {
return true;
}
// For non-chunked responses, complete when bytes_read >= content_length
// This handles both Content-Length: 0 and Content-Length: N cases
return !this->is_chunked_ && this->bytes_read_ >= this->content_length;
}
/** /**
* @brief Get response headers. * @brief Get response headers.
* *
@@ -209,6 +230,7 @@ class HttpContainer : public Parented<HttpRequestComponent> {
protected: protected:
size_t bytes_read_{0}; size_t bytes_read_{0};
bool secure_{false}; bool secure_{false};
bool is_chunked_{false}; ///< True if response uses chunked transfer encoding
std::map<std::string, std::list<std::string>> response_headers_{}; std::map<std::string, std::list<std::string>> response_headers_{};
}; };
@@ -219,7 +241,7 @@ class HttpContainer : public Parented<HttpRequestComponent> {
/// @param total_size Total bytes to read /// @param total_size Total bytes to read
/// @param chunk_size Maximum bytes per read call /// @param chunk_size Maximum bytes per read call
/// @param timeout_ms Read timeout in milliseconds /// @param timeout_ms Read timeout in milliseconds
/// @return HttpReadResult with status and error_code on failure /// @return HttpReadResult with status and error_code on failure; use container->get_bytes_read() for total bytes read
inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size, inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size,
uint32_t timeout_ms) { uint32_t timeout_ms) {
size_t read_index = 0; size_t read_index = 0;
@@ -231,9 +253,11 @@ inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer,
App.feed_wdt(); App.feed_wdt();
yield(); yield();
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms); auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms, container->is_read_complete());
if (result == HttpReadLoopResult::RETRY) if (result == HttpReadLoopResult::RETRY)
continue; continue;
if (result == HttpReadLoopResult::COMPLETE)
break; // Server sent less data than requested, but transfer is complete
if (result == HttpReadLoopResult::ERROR) if (result == HttpReadLoopResult::ERROR)
return {HttpReadStatus::ERROR, read_bytes_or_error}; return {HttpReadStatus::ERROR, read_bytes_or_error};
if (result == HttpReadLoopResult::TIMEOUT) if (result == HttpReadLoopResult::TIMEOUT)
@@ -393,11 +417,12 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512)); int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
App.feed_wdt(); App.feed_wdt();
yield(); yield();
auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout); auto result =
http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
if (result == HttpReadLoopResult::RETRY) if (result == HttpReadLoopResult::RETRY)
continue; continue;
if (result != HttpReadLoopResult::DATA) if (result != HttpReadLoopResult::DATA)
break; // ERROR or TIMEOUT break; // COMPLETE, ERROR, or TIMEOUT
read_index += read_or_error; read_index += read_or_error;
} }
response_body.reserve(read_index); response_body.reserve(read_index);

View File

@@ -135,9 +135,23 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
// When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit). // When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit).
// The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the // The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the
// early return check (bytes_read_ >= content_length) will never trigger. // early return check (bytes_read_ >= content_length) will never trigger.
//
// TODO: Chunked transfer encoding is NOT properly supported on Arduino.
// The implementation in #7884 was incomplete - it only works correctly on ESP-IDF where
// esp_http_client_read() decodes chunks internally. On Arduino, using getStreamPtr()
// returns raw TCP data with chunk framing (e.g., "12a\r\n{json}\r\n0\r\n\r\n") instead
// of decoded content. This wasn't noticed because requests would complete and payloads
// were only examined on IDF. The long transfer times were also masked by the misleading
// "HTTP on Arduino version >= 3.1 is **very** slow" warning above. This causes two issues:
// 1. Response body is corrupted - contains chunk size headers mixed with data
// 2. Cannot detect end of transfer - connection stays open (keep-alive), causing timeout
// The proper fix would be to use getString() for chunked responses, which decodes chunks
// internally, but this buffers the entire response in memory.
int content_length = container->client_.getSize(); int content_length = container->client_.getSize();
ESP_LOGD(TAG, "Content-Length: %d", content_length); ESP_LOGD(TAG, "Content-Length: %d", content_length);
container->content_length = (size_t) content_length; container->content_length = (size_t) content_length;
// -1 (SIZE_MAX when cast to size_t) means chunked transfer encoding
container->set_chunked(content_length == -1);
container->duration_ms = millis() - start; container->duration_ms = millis() - start;
return container; return container;
@@ -178,9 +192,9 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
if (bufsize == 0) { if (bufsize == 0) {
this->duration_ms += (millis() - start); this->duration_ms += (millis() - start);
// Check if we've read all expected content (only valid when content_length is known and not SIZE_MAX) // Check if we've read all expected content (non-chunked only)
// For chunked encoding (content_length == SIZE_MAX), we can't use this check // For chunked encoding (content_length == SIZE_MAX), is_read_complete() returns false
if (this->content_length > 0 && this->bytes_read_ >= this->content_length) { if (this->is_read_complete()) {
return 0; // All content read successfully return 0; // All content read successfully
} }
// No data available - check if connection is still open // No data available - check if connection is still open

View File

@@ -160,6 +160,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
// esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header). // esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header).
// The read() method handles content_length == 0 specially to support chunked responses. // The read() method handles content_length == 0 specially to support chunked responses.
container->content_length = esp_http_client_fetch_headers(client); container->content_length = esp_http_client_fetch_headers(client);
container->set_chunked(esp_http_client_is_chunked_response(client));
container->feed_wdt(); container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client); container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt(); container->feed_wdt();
@@ -195,6 +196,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
container->feed_wdt(); container->feed_wdt();
container->content_length = esp_http_client_fetch_headers(client); container->content_length = esp_http_client_fetch_headers(client);
container->set_chunked(esp_http_client_is_chunked_response(client));
container->feed_wdt(); container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client); container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt(); container->feed_wdt();
@@ -239,10 +241,9 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis(); const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
// Check if we've already read all expected content // Check if we've already read all expected content (non-chunked only)
// Skip this check when content_length is 0 (chunked transfer encoding or unknown length) // For chunked responses (content_length == 0), esp_http_client_read() handles EOF
// For chunked responses, esp_http_client_read() will return 0 when all data is received if (this->is_read_complete()) {
if (this->content_length > 0 && this->bytes_read_ >= this->content_length) {
return 0; // All content read successfully return 0; // All content read successfully
} }

View File

@@ -130,9 +130,13 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
App.feed_wdt(); App.feed_wdt();
yield(); yield();
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout); auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout, container->is_read_complete());
if (result == HttpReadLoopResult::RETRY) if (result == HttpReadLoopResult::RETRY)
continue; continue;
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
// but this is defensive code in case chunked transfer encoding support is added for OTA in the future.
if (result == HttpReadLoopResult::COMPLETE)
break;
if (result != HttpReadLoopResult::DATA) { if (result != HttpReadLoopResult::DATA) {
if (result == HttpReadLoopResult::TIMEOUT) { if (result == HttpReadLoopResult::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading data"); ESP_LOGE(TAG, "Timeout reading data");

View File

@@ -267,26 +267,16 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
for (auto &scan : results) { for (auto &scan : results) {
if (scan.get_is_hidden()) if (scan.get_is_hidden())
continue; continue;
const char *ssid_cstr = scan.get_ssid().c_str(); const std::string &ssid = scan.get_ssid();
// Check if we've already sent this SSID if (std::find(networks.begin(), networks.end(), ssid) != networks.end())
bool duplicate = false;
for (const auto &seen : networks) {
if (strcmp(seen.c_str(), ssid_cstr) == 0) {
duplicate = true;
break;
}
}
if (duplicate)
continue; continue;
// Only allocate std::string after confirming it's not a duplicate
std::string ssid(ssid_cstr);
// Send each ssid separately to avoid overflowing the buffer // Send each ssid separately to avoid overflowing the buffer
char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null
*int8_to_str(rssi_buf, scan.get_rssi()) = '\0'; *int8_to_str(rssi_buf, scan.get_rssi()) = '\0';
std::vector<uint8_t> data = std::vector<uint8_t> data =
improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false); improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false);
this->send_response_(data); this->send_response_(data);
networks.push_back(std::move(ssid)); networks.push_back(ssid);
} }
// Send empty response to signify the end of the list. // Send empty response to signify the end of the list.
std::vector<uint8_t> data = std::vector<uint8_t> data =

View File

@@ -189,10 +189,11 @@ class LibreTinyPreferences : public ESPPreferences {
} }
}; };
static LibreTinyPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void setup_preferences() { void setup_preferences() {
auto *prefs = new LibreTinyPreferences(); // NOLINT(cppcoreguidelines-owning-memory) s_preferences.open();
prefs->open(); global_preferences = &s_preferences;
global_preferences = prefs;
} }
} // namespace libretiny } // namespace libretiny

View File

@@ -128,22 +128,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
// Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266. // Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266.
// //
// This function handles format strings stored in flash memory (PROGMEM) to save RAM. // This function handles format strings stored in flash memory (PROGMEM) to save RAM.
// The buffer is used in a special way to avoid allocating extra memory: // Uses vsnprintf_P to read the format string directly from flash without copying to RAM.
//
// Memory layout during execution:
// Step 1: Copy format string from flash to buffer
// tx_buffer_: [format_string][null][.....................]
// tx_buffer_at_: ------------------^
// msg_start: saved here -----------^
//
// Step 2: format_log_to_buffer_with_terminator_ reads format string from beginning
// and writes formatted output starting at msg_start position
// tx_buffer_: [format_string][null][formatted_message][null]
// tx_buffer_at_: -------------------------------------^
//
// Step 3: Output the formatted message (starting at msg_start)
// write_msg_ and callbacks receive: this->tx_buffer_ + msg_start
// which points to: [formatted_message][null]
// //
void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format,
va_list args) { // NOLINT va_list args) { // NOLINT
@@ -153,35 +138,25 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
RecursionGuard guard(global_recursion_guard_); RecursionGuard guard(global_recursion_guard_);
this->tx_buffer_at_ = 0; this->tx_buffer_at_ = 0;
// Copy format string from progmem // Write header, format body directly from flash, and write footer
auto *format_pgm_p = reinterpret_cast<const uint8_t *>(format); this->write_header_to_buffer_(level, tag, line, nullptr, this->tx_buffer_, &this->tx_buffer_at_,
char ch = '.'; this->tx_buffer_size_);
while (this->tx_buffer_at_ < this->tx_buffer_size_ && ch != '\0') { this->format_body_to_buffer_P_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_,
this->tx_buffer_[this->tx_buffer_at_++] = ch = (char) progmem_read_byte(format_pgm_p++); reinterpret_cast<PGM_P>(format), args);
} this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_);
// Buffer full from copying format - RAII guard handles cleanup on return // Ensure null termination
if (this->tx_buffer_at_ >= this->tx_buffer_size_) { uint16_t null_pos = this->tx_buffer_at_ >= this->tx_buffer_size_ ? this->tx_buffer_size_ - 1 : this->tx_buffer_at_;
return; this->tx_buffer_[null_pos] = '\0';
}
// Save the offset before calling format_log_to_buffer_with_terminator_
// since it will increment tx_buffer_at_ to the end of the formatted string
uint16_t msg_start = this->tx_buffer_at_;
this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_,
&this->tx_buffer_at_, this->tx_buffer_size_);
uint16_t msg_length =
this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position
// Listeners get message first (before console write) // Listeners get message first (before console write)
#ifdef USE_LOG_LISTENERS #ifdef USE_LOG_LISTENERS
for (auto *listener : this->log_listeners_) for (auto *listener : this->log_listeners_)
listener->on_log(level, tag, this->tx_buffer_ + msg_start, msg_length); listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_);
#endif #endif
// Write to console starting at the msg_start // Write to console
this->write_tx_buffer_to_console_(msg_start, &msg_length); this->write_tx_buffer_to_console_();
} }
#endif // USE_STORE_LOG_STR_IN_FLASH #endif // USE_STORE_LOG_STR_IN_FLASH

View File

@@ -597,31 +597,40 @@ class Logger : public Component {
*buffer_at = pos; *buffer_at = pos;
} }
// Helper to process vsnprintf return value and strip trailing newlines.
// Updates buffer_at with the formatted length, handling truncation:
// - When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator
// - When it doesn't truncate (ret < remaining), it writes ret chars + null terminator
__attribute__((always_inline)) static inline void process_vsnprintf_result(const char *buffer, uint16_t *buffer_at,
uint16_t remaining, int ret) {
if (ret < 0)
return; // Encoding error, do not increment buffer_at
*buffer_at += (ret >= remaining) ? (remaining - 1) : static_cast<uint16_t>(ret);
// Remove all trailing newlines right after formatting
while (*buffer_at > 0 && buffer[*buffer_at - 1] == '\n')
(*buffer_at)--;
}
inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format,
va_list args) { va_list args) {
// Get remaining capacity in the buffer // Check remaining capacity in the buffer
if (*buffer_at >= buffer_size) if (*buffer_at >= buffer_size)
return; return;
const uint16_t remaining = buffer_size - *buffer_at; const uint16_t remaining = buffer_size - *buffer_at;
process_vsnprintf_result(buffer, buffer_at, remaining, vsnprintf(buffer + *buffer_at, remaining, format, args));
const int ret = vsnprintf(buffer + *buffer_at, remaining, format, args);
if (ret < 0) {
return; // Encoding error, do not increment buffer_at
}
// Update buffer_at with the formatted length (handle truncation)
// When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator
// When it doesn't truncate (ret < remaining), it writes ret chars + null terminator
uint16_t formatted_len = (ret >= remaining) ? (remaining - 1) : ret;
*buffer_at += formatted_len;
// Remove all trailing newlines right after formatting
while (*buffer_at > 0 && buffer[*buffer_at - 1] == '\n') {
(*buffer_at)--;
}
} }
#ifdef USE_STORE_LOG_STR_IN_FLASH
// ESP8266 variant that reads format string directly from flash using vsnprintf_P
inline void HOT format_body_to_buffer_P_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, PGM_P format,
va_list args) {
if (*buffer_at >= buffer_size)
return;
const uint16_t remaining = buffer_size - *buffer_at;
process_vsnprintf_result(buffer, buffer_at, remaining, vsnprintf_P(buffer + *buffer_at, remaining, format, args));
}
#endif
inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) {
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1; static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size);

View File

@@ -2,6 +2,7 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h" #include "esphome/core/controller_registry.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome { namespace esphome {
namespace media_player { namespace media_player {
@@ -107,25 +108,25 @@ MediaPlayerCall &MediaPlayerCall::set_command(optional<MediaPlayerCommand> comma
this->command_ = command; this->command_ = command;
return *this; return *this;
} }
MediaPlayerCall &MediaPlayerCall::set_command(const std::string &command) { MediaPlayerCall &MediaPlayerCall::set_command(const char *command) {
if (str_equals_case_insensitive(command, "PLAY")) { if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("PLAY")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_PLAY); this->set_command(MEDIA_PLAYER_COMMAND_PLAY);
} else if (str_equals_case_insensitive(command, "PAUSE")) { } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("PAUSE")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_PAUSE); this->set_command(MEDIA_PLAYER_COMMAND_PAUSE);
} else if (str_equals_case_insensitive(command, "STOP")) { } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("STOP")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_STOP); this->set_command(MEDIA_PLAYER_COMMAND_STOP);
} else if (str_equals_case_insensitive(command, "MUTE")) { } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("MUTE")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_MUTE); this->set_command(MEDIA_PLAYER_COMMAND_MUTE);
} else if (str_equals_case_insensitive(command, "UNMUTE")) { } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("UNMUTE")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_UNMUTE); this->set_command(MEDIA_PLAYER_COMMAND_UNMUTE);
} else if (str_equals_case_insensitive(command, "TOGGLE")) { } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TOGGLE")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_TOGGLE); this->set_command(MEDIA_PLAYER_COMMAND_TOGGLE);
} else if (str_equals_case_insensitive(command, "TURN_ON")) { } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TURN_ON")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_TURN_ON); this->set_command(MEDIA_PLAYER_COMMAND_TURN_ON);
} else if (str_equals_case_insensitive(command, "TURN_OFF")) { } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TURN_OFF")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_TURN_OFF); this->set_command(MEDIA_PLAYER_COMMAND_TURN_OFF);
} else { } else {
ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command.c_str()); ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command);
} }
return *this; return *this;
} }

View File

@@ -114,7 +114,8 @@ class MediaPlayerCall {
MediaPlayerCall &set_command(MediaPlayerCommand command); MediaPlayerCall &set_command(MediaPlayerCommand command);
MediaPlayerCall &set_command(optional<MediaPlayerCommand> command); MediaPlayerCall &set_command(optional<MediaPlayerCommand> command);
MediaPlayerCall &set_command(const std::string &command); MediaPlayerCall &set_command(const char *command);
MediaPlayerCall &set_command(const std::string &command) { return this->set_command(command.c_str()); }
MediaPlayerCall &set_media_url(const std::string &url); MediaPlayerCall &set_media_url(const std::string &url);

View File

@@ -4,8 +4,10 @@ from typing import Any
from esphome import automation, pins from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import sensor from esphome.components import sensor
from esphome.components.esp32 import include_builtin_idf_component
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TRIGGER_ID, PLATFORM_ESP32, PLATFORM_ESP8266 from esphome.const import CONF_ID, CONF_TRIGGER_ID, PLATFORM_ESP32, PLATFORM_ESP8266
from esphome.core import CORE
from . import const, generate, schema, validate from . import const, generate, schema, validate
@@ -83,6 +85,12 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: dict[str, Any]) -> None: async def to_code(config: dict[str, Any]) -> None:
if CORE.is_esp32:
# Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time)
# Provides driver/timer.h header for hardware timer API
# TODO: Remove this once opentherm migrates to GPTimer API (driver/gptimer.h)
include_builtin_idf_component("driver")
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)

View File

@@ -8,6 +8,8 @@
#include "opentherm.h" #include "opentherm.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include <cinttypes> #include <cinttypes>
// TODO: Migrate from legacy timer API (driver/timer.h) to GPTimer API (driver/gptimer.h)
// The legacy timer API is deprecated in ESP-IDF 5.x. See opentherm.h for details.
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "driver/timer.h" #include "driver/timer.h"
#include "esp_err.h" #include "esp_err.h"

View File

@@ -12,6 +12,10 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
// TODO: Migrate from legacy timer API (driver/timer.h) to GPTimer API (driver/gptimer.h)
// The legacy timer API is deprecated in ESP-IDF 5.x. Migration would allow removing the
// "driver" IDF component dependency. See:
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id4
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "driver/timer.h" #include "driver/timer.h"
#endif #endif

View File

@@ -4,6 +4,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32H2, VARIANT_ESP32H2,
add_idf_sdkconfig_option, add_idf_sdkconfig_option,
include_builtin_idf_component,
only_on_variant, only_on_variant,
require_vfs_select, require_vfs_select,
) )
@@ -172,6 +173,9 @@ FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config): async def to_code(config):
# Re-enable openthread IDF component (excluded by default)
include_builtin_idf_component("openthread")
cg.add_define("USE_OPENTHREAD") cg.add_define("USE_OPENTHREAD")
# OpenThread SRP needs access to mDNS services after setup # OpenThread SRP needs access to mDNS services after setup

View File

@@ -18,11 +18,12 @@ namespace rp2040 {
static const char *const TAG = "rp2040.preferences"; static const char *const TAG = "rp2040.preferences";
static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static constexpr uint32_t RP2040_FLASH_STORAGE_SIZE = 512;
static uint8_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static const uint32_t RP2040_FLASH_STORAGE_SIZE = 512; static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static uint8_t
s_flash_storage[RP2040_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// Stack buffer size for preferences - covers virtually all real-world preferences without heap allocation // Stack buffer size for preferences - covers virtually all real-world preferences without heap allocation
static constexpr size_t PREF_BUFFER_SIZE = 64; static constexpr size_t PREF_BUFFER_SIZE = 64;
@@ -91,7 +92,6 @@ class RP2040Preferences : public ESPPreferences {
RP2040Preferences() : eeprom_sector_(&_EEPROM_start) {} RP2040Preferences() : eeprom_sector_(&_EEPROM_start) {}
void setup() { void setup() {
s_flash_storage = new uint8_t[RP2040_FLASH_STORAGE_SIZE]; // NOLINT
ESP_LOGVV(TAG, "Loading preferences from flash"); ESP_LOGVV(TAG, "Loading preferences from flash");
memcpy(s_flash_storage, this->eeprom_sector_, RP2040_FLASH_STORAGE_SIZE); memcpy(s_flash_storage, this->eeprom_sector_, RP2040_FLASH_STORAGE_SIZE);
} }
@@ -149,10 +149,11 @@ class RP2040Preferences : public ESPPreferences {
uint8_t *eeprom_sector_; uint8_t *eeprom_sector_;
}; };
static RP2040Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void setup_preferences() { void setup_preferences() {
auto *prefs = new RP2040Preferences(); // NOLINT(cppcoreguidelines-owning-memory) s_preferences.setup();
prefs->setup(); global_preferences = &s_preferences;
global_preferences = prefs;
} }
void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; } void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; }

View File

@@ -157,8 +157,14 @@ def _read_audio_file_and_type(file_config):
import puremagic import puremagic
file_type: str = puremagic.from_string(data) try:
file_type = file_type.removeprefix(".") file_type: str = puremagic.from_string(data)
file_type = file_type.removeprefix(".")
except puremagic.PureError as e:
raise cv.Invalid(
f"Unable to determine audio file type of '{path}'. "
f"Try re-encoding the file into a supported format. Details: {e}"
)
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"] media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
if file_type in ("wav"): if file_type in ("wav"):

View File

@@ -10,35 +10,65 @@ from esphome.const import (
CONF_OPTIONS, CONF_OPTIONS,
CONF_RESTORE_VALUE, CONF_RESTORE_VALUE,
CONF_SET_ACTION, CONF_SET_ACTION,
CONF_UPDATE_INTERVAL,
SCHEDULER_DONT_RUN,
) )
from esphome.core import TimePeriodMilliseconds
from esphome.cpp_generator import TemplateArguments
from .. import template_ns from .. import template_ns
TemplateSelect = template_ns.class_( TemplateSelect = template_ns.class_(
"TemplateSelect", select.Select, cg.PollingComponent "TemplateSelect", select.Select, cg.PollingComponent
) )
TemplateSelectWithSetAction = template_ns.class_(
"TemplateSelectWithSetAction", TemplateSelect
)
def validate(config): def validate(config):
errors = []
if CONF_LAMBDA in config: if CONF_LAMBDA in config:
if config[CONF_OPTIMISTIC]: if config[CONF_OPTIMISTIC]:
raise cv.Invalid("optimistic cannot be used with lambda") errors.append(
cv.Invalid(
"optimistic cannot be used with lambda", path=[CONF_OPTIMISTIC]
)
)
if CONF_INITIAL_OPTION in config: if CONF_INITIAL_OPTION in config:
raise cv.Invalid("initial_value cannot be used with lambda") errors.append(
cv.Invalid(
"initial_value cannot be used with lambda",
path=[CONF_INITIAL_OPTION],
)
)
if CONF_RESTORE_VALUE in config: if CONF_RESTORE_VALUE in config:
raise cv.Invalid("restore_value cannot be used with lambda") errors.append(
cv.Invalid(
"restore_value cannot be used with lambda",
path=[CONF_RESTORE_VALUE],
)
)
elif CONF_INITIAL_OPTION in config: elif CONF_INITIAL_OPTION in config:
if config[CONF_INITIAL_OPTION] not in config[CONF_OPTIONS]: if config[CONF_INITIAL_OPTION] not in config[CONF_OPTIONS]:
raise cv.Invalid( errors.append(
f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]" cv.Invalid(
f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]",
path=[CONF_INITIAL_OPTION],
)
) )
else: else:
config[CONF_INITIAL_OPTION] = config[CONF_OPTIONS][0] config[CONF_INITIAL_OPTION] = config[CONF_OPTIONS][0]
if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config: if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config:
raise cv.Invalid( errors.append(
"Either optimistic mode must be enabled, or set_action must be set, to handle the option being set." cv.Invalid(
"Either optimistic mode must be enabled, or set_action must be set, to handle the option being set."
)
) )
if errors:
raise cv.MultipleInvalid(errors)
return config return config
@@ -62,29 +92,34 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var_id = config[CONF_ID]
await cg.register_component(var, config) if CONF_SET_ACTION in config:
await select.register_select(var, config, options=config[CONF_OPTIONS]) var_id.type = TemplateSelectWithSetAction
has_lambda = CONF_LAMBDA in config
optimistic = config.get(CONF_OPTIMISTIC, False)
restore_value = config.get(CONF_RESTORE_VALUE, False)
options = config[CONF_OPTIONS]
initial_option = config.get(CONF_INITIAL_OPTION, 0)
initial_option_index = options.index(initial_option) if not has_lambda else 0
var = cg.new_Pvariable(
var_id,
TemplateArguments(has_lambda, optimistic, restore_value, initial_option_index),
)
component_config = config.copy()
if not has_lambda:
# No point in polling if not using a lambda
component_config[CONF_UPDATE_INTERVAL] = TimePeriodMilliseconds(
milliseconds=SCHEDULER_DONT_RUN
)
await cg.register_component(var, component_config)
await select.register_select(var, config, options=options)
if CONF_LAMBDA in config: if CONF_LAMBDA in config:
template_ = await cg.process_lambda( lambda_ = await cg.process_lambda(
config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.std_string) config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.std_string)
) )
cg.add(var.set_template(template_)) cg.add(var.set_lambda(lambda_))
else:
# Only set if non-default to avoid bloating setup() function
if config[CONF_OPTIMISTIC]:
cg.add(var.set_optimistic(True))
initial_option_index = config[CONF_OPTIONS].index(config[CONF_INITIAL_OPTION])
# Only set if non-zero to avoid bloating setup() function
# (initial_option_index_ is zero-initialized in the header)
if initial_option_index != 0:
cg.add(var.set_initial_option_index(initial_option_index))
# Only set if True (default is False)
if config.get(CONF_RESTORE_VALUE):
cg.add(var.set_restore_value(True))
if CONF_SET_ACTION in config: if CONF_SET_ACTION in config:
await automation.build_automation( await automation.build_automation(

View File

@@ -3,63 +3,17 @@
namespace esphome::template_ { namespace esphome::template_ {
static const char *const TAG = "template.select"; void dump_config_helper(BaseTemplateSelect *sel_comp, bool optimistic, bool has_lambda,
const size_t initial_option_index, bool restore_value) {
void TemplateSelect::setup() { LOG_SELECT("", "Template Select", sel_comp);
if (this->f_.has_value()) if (has_lambda) {
return; LOG_UPDATE_INTERVAL(sel_comp);
size_t index = this->initial_option_index_;
if (this->restore_value_) {
this->pref_ = this->make_entity_preference<size_t>();
size_t restored_index;
if (this->pref_.load(&restored_index) && this->has_index(restored_index)) {
index = restored_index;
ESP_LOGD(TAG, "State from restore: %s", this->option_at(index));
} else {
ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->option_at(index));
}
} else { } else {
ESP_LOGD(TAG, "State from initial: %s", this->option_at(index)); ESP_LOGCONFIG(TAG,
} " Optimistic: %s\n"
" Initial Option: %s\n"
this->publish_state(index); " Restore Value: %s",
} YESNO(optimistic), sel_comp->option_at(initial_option_index), YESNO(restore_value));
void TemplateSelect::update() {
if (!this->f_.has_value())
return;
auto val = this->f_();
if (val.has_value()) {
if (!this->has_option(*val)) {
ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str());
return;
}
this->publish_state(*val);
} }
} }
void TemplateSelect::control(size_t index) {
this->set_trigger_->trigger(StringRef(this->option_at(index)));
if (this->optimistic_)
this->publish_state(index);
if (this->restore_value_)
this->pref_.save(&index);
}
void TemplateSelect::dump_config() {
LOG_SELECT("", "Template Select", this);
LOG_UPDATE_INTERVAL(this);
if (this->f_.has_value())
return;
ESP_LOGCONFIG(TAG,
" Optimistic: %s\n"
" Initial Option: %s\n"
" Restore Value: %s",
YESNO(this->optimistic_), this->option_at(this->initial_option_index_), YESNO(this->restore_value_));
}
} // namespace esphome::template_ } // namespace esphome::template_

View File

@@ -8,30 +8,83 @@
#include "esphome/core/template_lambda.h" #include "esphome/core/template_lambda.h"
namespace esphome::template_ { namespace esphome::template_ {
static const char *const TAG = "template.select";
struct Empty {};
class BaseTemplateSelect : public select::Select, public PollingComponent {};
class TemplateSelect final : public select::Select, public PollingComponent { void dump_config_helper(BaseTemplateSelect *sel_comp, bool optimistic, bool has_lambda, size_t initial_option_index,
bool restore_value);
/// Base template select class - used when no set_action is configured
template<bool HAS_LAMBDA, bool OPTIMISTIC, bool RESTORE_VALUE, size_t INITIAL_OPTION_INDEX>
class TemplateSelect : public BaseTemplateSelect {
public: public:
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); } template<typename F> void set_lambda(F &&f) {
if constexpr (HAS_LAMBDA) {
this->f_.set(std::forward<F>(f));
}
}
void setup() override; void setup() override {
void update() override; if constexpr (!HAS_LAMBDA) {
void dump_config() override; size_t index = INITIAL_OPTION_INDEX;
if constexpr (RESTORE_VALUE) {
this->pref_ = this->template make_entity_preference<size_t>();
if (this->pref_.load(&index) && this->has_index(index)) {
esph_log_d(TAG, "State from restore: %s", this->option_at(index));
} else {
index = INITIAL_OPTION_INDEX;
esph_log_d(TAG, "State from initial (no valid stored index): %s", this->option_at(INITIAL_OPTION_INDEX));
}
} else {
esph_log_d(TAG, "State from initial: %s", this->option_at(INITIAL_OPTION_INDEX));
}
this->publish_state(index);
}
}
void update() override {
if constexpr (HAS_LAMBDA) {
auto val = this->f_();
if (val.has_value()) {
if (!this->has_option(*val)) {
esph_log_e(TAG, "Lambda returned an invalid option: %s", (*val).c_str());
return;
}
this->publish_state(*val);
}
}
}
void dump_config() override {
dump_config_helper(this, OPTIMISTIC, HAS_LAMBDA, INITIAL_OPTION_INDEX, RESTORE_VALUE);
};
float get_setup_priority() const override { return setup_priority::HARDWARE; } float get_setup_priority() const override { return setup_priority::HARDWARE; }
Trigger<StringRef> *get_set_trigger() const { return this->set_trigger_; } protected:
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void control(size_t index) override {
void set_initial_option_index(size_t initial_option_index) { this->initial_option_index_ = initial_option_index; } if constexpr (OPTIMISTIC)
void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } this->publish_state(index);
if constexpr (RESTORE_VALUE)
this->pref_.save(&index);
}
[[no_unique_address]] std::conditional_t<HAS_LAMBDA, TemplateLambda<std::string>, Empty> f_{};
[[no_unique_address]] std::conditional_t<RESTORE_VALUE, ESPPreferenceObject, Empty> pref_{};
};
/// Template select with set_action trigger - only instantiated when set_action is configured
template<bool HAS_LAMBDA, bool OPTIMISTIC, bool RESTORE_VALUE, size_t INITIAL_OPTION_INDEX>
class TemplateSelectWithSetAction final
: public TemplateSelect<HAS_LAMBDA, OPTIMISTIC, RESTORE_VALUE, INITIAL_OPTION_INDEX> {
public:
Trigger<StringRef> *get_set_trigger() { return &this->set_trigger_; }
protected: protected:
void control(size_t index) override; void control(size_t index) override {
bool optimistic_ = false; this->set_trigger_.trigger(StringRef(this->option_at(index)));
size_t initial_option_index_{0}; TemplateSelect<HAS_LAMBDA, OPTIMISTIC, RESTORE_VALUE, INITIAL_OPTION_INDEX>::control(index);
bool restore_value_ = false; }
Trigger<StringRef> *set_trigger_ = new Trigger<StringRef>(); Trigger<StringRef> set_trigger_;
TemplateLambda<std::string> f_;
ESPPreferenceObject pref_;
}; };
} // namespace esphome::template_ } // namespace esphome::template_

View File

@@ -2,6 +2,7 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/controller_registry.h" #include "esphome/core/controller_registry.h"
#include "esphome/core/progmem.h"
#include <cmath> #include <cmath>
@@ -22,23 +23,23 @@ WaterHeaterCall &WaterHeaterCall::set_mode(WaterHeaterMode mode) {
return *this; return *this;
} }
WaterHeaterCall &WaterHeaterCall::set_mode(const std::string &mode) { WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode) {
if (str_equals_case_insensitive(mode, "OFF")) { if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("OFF")) == 0) {
this->set_mode(WATER_HEATER_MODE_OFF); this->set_mode(WATER_HEATER_MODE_OFF);
} else if (str_equals_case_insensitive(mode, "ECO")) { } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("ECO")) == 0) {
this->set_mode(WATER_HEATER_MODE_ECO); this->set_mode(WATER_HEATER_MODE_ECO);
} else if (str_equals_case_insensitive(mode, "ELECTRIC")) { } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("ELECTRIC")) == 0) {
this->set_mode(WATER_HEATER_MODE_ELECTRIC); this->set_mode(WATER_HEATER_MODE_ELECTRIC);
} else if (str_equals_case_insensitive(mode, "PERFORMANCE")) { } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("PERFORMANCE")) == 0) {
this->set_mode(WATER_HEATER_MODE_PERFORMANCE); this->set_mode(WATER_HEATER_MODE_PERFORMANCE);
} else if (str_equals_case_insensitive(mode, "HIGH_DEMAND")) { } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("HIGH_DEMAND")) == 0) {
this->set_mode(WATER_HEATER_MODE_HIGH_DEMAND); this->set_mode(WATER_HEATER_MODE_HIGH_DEMAND);
} else if (str_equals_case_insensitive(mode, "HEAT_PUMP")) { } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("HEAT_PUMP")) == 0) {
this->set_mode(WATER_HEATER_MODE_HEAT_PUMP); this->set_mode(WATER_HEATER_MODE_HEAT_PUMP);
} else if (str_equals_case_insensitive(mode, "GAS")) { } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("GAS")) == 0) {
this->set_mode(WATER_HEATER_MODE_GAS); this->set_mode(WATER_HEATER_MODE_GAS);
} else { } else {
ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str()); ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode);
} }
return *this; return *this;
} }

View File

@@ -75,7 +75,8 @@ class WaterHeaterCall {
WaterHeaterCall(WaterHeater *parent); WaterHeaterCall(WaterHeater *parent);
WaterHeaterCall &set_mode(WaterHeaterMode mode); WaterHeaterCall &set_mode(WaterHeaterMode mode);
WaterHeaterCall &set_mode(const std::string &mode); WaterHeaterCall &set_mode(const char *mode);
WaterHeaterCall &set_mode(const std::string &mode) { return this->set_mode(mode.c_str()); }
WaterHeaterCall &set_target_temperature(float temperature); WaterHeaterCall &set_target_temperature(float temperature);
WaterHeaterCall &set_target_temperature_low(float temperature); WaterHeaterCall &set_target_temperature_low(float temperature);
WaterHeaterCall &set_target_temperature_high(float temperature); WaterHeaterCall &set_target_temperature_high(float temperature);

View File

@@ -585,11 +585,13 @@ async def to_code(config):
await cg.past_safe_mode() await cg.past_safe_mode()
if on_connect_config := config.get(CONF_ON_CONNECT): if on_connect_config := config.get(CONF_ON_CONNECT):
cg.add_define("USE_WIFI_CONNECT_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_connect_trigger(), [], on_connect_config var.get_connect_trigger(), [], on_connect_config
) )
if on_disconnect_config := config.get(CONF_ON_DISCONNECT): if on_disconnect_config := config.get(CONF_ON_DISCONNECT):
cg.add_define("USE_WIFI_DISCONNECT_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_disconnect_trigger(), [], on_disconnect_config var.get_disconnect_trigger(), [], on_disconnect_config
) )

View File

@@ -48,7 +48,7 @@ template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, publi
char ssid_buf[SSID_BUFFER_SIZE]; char ssid_buf[SSID_BUFFER_SIZE];
if (strcmp(global_wifi_component->wifi_ssid_to(ssid_buf), ssid.c_str()) == 0) { if (strcmp(global_wifi_component->wifi_ssid_to(ssid_buf), ssid.c_str()) == 0) {
// Callback to notify the user that the connection was successful // Callback to notify the user that the connection was successful
this->connect_trigger_->trigger(); this->connect_trigger_.trigger();
return; return;
} }
// Create a new WiFiAP object with the new SSID and password // Create a new WiFiAP object with the new SSID and password
@@ -79,13 +79,13 @@ template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, publi
// Start a timeout for the fallback if the connection to the old AP fails // Start a timeout for the fallback if the connection to the old AP fails
this->set_timeout("wifi-fallback-timeout", this->connection_timeout_.value(x...), [this]() { this->set_timeout("wifi-fallback-timeout", this->connection_timeout_.value(x...), [this]() {
this->connecting_ = false; this->connecting_ = false;
this->error_trigger_->trigger(); this->error_trigger_.trigger();
}); });
}); });
} }
Trigger<> *get_connect_trigger() const { return this->connect_trigger_; } Trigger<> *get_connect_trigger() { return &this->connect_trigger_; }
Trigger<> *get_error_trigger() const { return this->error_trigger_; } Trigger<> *get_error_trigger() { return &this->error_trigger_; }
void loop() override { void loop() override {
if (!this->connecting_) if (!this->connecting_)
@@ -98,10 +98,10 @@ template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, publi
char ssid_buf[SSID_BUFFER_SIZE]; char ssid_buf[SSID_BUFFER_SIZE];
if (strcmp(global_wifi_component->wifi_ssid_to(ssid_buf), this->new_sta_.get_ssid().c_str()) == 0) { if (strcmp(global_wifi_component->wifi_ssid_to(ssid_buf), this->new_sta_.get_ssid().c_str()) == 0) {
// Callback to notify the user that the connection was successful // Callback to notify the user that the connection was successful
this->connect_trigger_->trigger(); this->connect_trigger_.trigger();
} else { } else {
// Callback to notify the user that the connection failed // Callback to notify the user that the connection failed
this->error_trigger_->trigger(); this->error_trigger_.trigger();
} }
} }
} }
@@ -110,8 +110,8 @@ template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, publi
bool connecting_{false}; bool connecting_{false};
WiFiAP new_sta_; WiFiAP new_sta_;
WiFiAP old_sta_; WiFiAP old_sta_;
Trigger<> *connect_trigger_{new Trigger<>()}; Trigger<> connect_trigger_;
Trigger<> *error_trigger_{new Trigger<>()}; Trigger<> error_trigger_;
}; };
} // namespace esphome::wifi } // namespace esphome::wifi

View File

@@ -351,7 +351,7 @@ bool WiFiComponent::needs_scan_results_() const {
return this->scan_result_.empty() || !this->scan_result_[0].get_matches(); return this->scan_result_.empty() || !this->scan_result_[0].get_matches();
} }
bool WiFiComponent::ssid_was_seen_in_scan_(const CompactString &ssid) const { bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const {
// Check if this SSID is configured as hidden // Check if this SSID is configured as hidden
// If explicitly marked hidden, we should always try hidden mode regardless of scan results // If explicitly marked hidden, we should always try hidden mode regardless of scan results
for (const auto &conf : this->sta_) { for (const auto &conf : this->sta_) {
@@ -651,14 +651,21 @@ void WiFiComponent::loop() {
const uint32_t now = App.get_loop_component_start_time(); const uint32_t now = App.get_loop_component_start_time();
if (this->has_sta()) { if (this->has_sta()) {
#if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER)
if (this->is_connected() != this->handled_connected_state_) { if (this->is_connected() != this->handled_connected_state_) {
#ifdef USE_WIFI_DISCONNECT_TRIGGER
if (this->handled_connected_state_) { if (this->handled_connected_state_) {
this->disconnect_trigger_->trigger(); this->disconnect_trigger_.trigger();
} else {
this->connect_trigger_->trigger();
} }
#endif
#ifdef USE_WIFI_CONNECT_TRIGGER
if (!this->handled_connected_state_) {
this->connect_trigger_.trigger();
}
#endif
this->handled_connected_state_ = this->is_connected(); this->handled_connected_state_ = this->is_connected();
} }
#endif // USE_WIFI_CONNECT_TRIGGER || USE_WIFI_DISCONNECT_TRIGGER
switch (this->state_) { switch (this->state_) {
case WIFI_COMPONENT_STATE_COOLDOWN: { case WIFI_COMPONENT_STATE_COOLDOWN: {
@@ -955,12 +962,9 @@ WiFiAP WiFiComponent::get_sta() const {
return config ? *config : WiFiAP{}; return config ? *config : WiFiAP{};
} }
void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) { void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) {
this->save_wifi_sta(ssid.c_str(), password.c_str());
}
void WiFiComponent::save_wifi_sta(const char *ssid, const char *password) {
SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination
strncpy(save.ssid, ssid, sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0 strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
strncpy(save.password, password, sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0 strncpy(save.password, password.c_str(), sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
this->pref_.save(&save); this->pref_.save(&save);
// ensure it's written immediately // ensure it's written immediately
global_preferences->sync(); global_preferences->sync();
@@ -1805,11 +1809,11 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
} }
// Get SSID for logging (use pointer to avoid copy) // Get SSID for logging (use pointer to avoid copy)
const char *ssid = nullptr; const std::string *ssid = nullptr;
if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) { if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
ssid = this->scan_result_[0].get_ssid().c_str(); ssid = &this->scan_result_[0].get_ssid();
} else if (const WiFiAP *config = this->get_selected_sta_()) { } else if (const WiFiAP *config = this->get_selected_sta_()) {
ssid = config->get_ssid().c_str(); ssid = &config->get_ssid();
} }
// Only decrease priority on the last attempt for this phase // Only decrease priority on the last attempt for this phase
@@ -1829,8 +1833,8 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
} }
char bssid_s[18]; char bssid_s[18];
format_mac_addr_upper(failed_bssid.value().data(), bssid_s); format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid != nullptr ? ssid : "", ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d",
bssid_s, old_priority, new_priority); ssid != nullptr ? ssid->c_str() : "", bssid_s, old_priority, new_priority);
// After adjusting priority, check if all priorities are now at minimum // After adjusting priority, check if all priorities are now at minimum
// If so, clear the vector to save memory and reset for fresh start // If so, clear the vector to save memory and reset for fresh start
@@ -2078,14 +2082,10 @@ void WiFiComponent::save_fast_connect_settings_() {
} }
#endif #endif
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); } void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
void WiFiAP::set_ssid(const char *ssid) { this->ssid_ = CompactString(ssid, strlen(ssid)); }
void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; } void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; }
void WiFiAP::clear_bssid() { this->bssid_ = {}; } void WiFiAP::clear_bssid() { this->bssid_ = {}; }
void WiFiAP::set_password(const std::string &password) { void WiFiAP::set_password(const std::string &password) { this->password_ = password; }
this->password_ = CompactString(password.c_str(), password.size());
}
void WiFiAP::set_password(const char *password) { this->password_ = CompactString(password, strlen(password)); }
#ifdef USE_WIFI_WPA2_EAP #ifdef USE_WIFI_WPA2_EAP
void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); } void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
#endif #endif
@@ -2095,8 +2095,10 @@ void WiFiAP::clear_channel() { this->channel_ = 0; }
void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; } void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
#endif #endif
void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
const std::string &WiFiAP::get_ssid() const { return this->ssid_; }
const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; } const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; }
bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; } bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; }
const std::string &WiFiAP::get_password() const { return this->password_; }
#ifdef USE_WIFI_WPA2_EAP #ifdef USE_WIFI_WPA2_EAP
const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; } const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
#endif #endif
@@ -2107,12 +2109,12 @@ const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip
#endif #endif
bool WiFiAP::get_hidden() const { return this->hidden_; } bool WiFiAP::get_hidden() const { return this->hidden_; }
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth,
bool with_auth, bool is_hidden) bool is_hidden)
: bssid_(bssid), : bssid_(bssid),
channel_(channel), channel_(channel),
rssi_(rssi), rssi_(rssi),
ssid_(ssid, ssid_len), ssid_(std::move(ssid)),
with_auth_(with_auth), with_auth_(with_auth),
is_hidden_(is_hidden) {} is_hidden_(is_hidden) {}
bool WiFiScanResult::matches(const WiFiAP &config) const { bool WiFiScanResult::matches(const WiFiAP &config) const {
@@ -2155,6 +2157,7 @@ bool WiFiScanResult::matches(const WiFiAP &config) const {
bool WiFiScanResult::get_matches() const { return this->matches_; } bool WiFiScanResult::get_matches() const { return this->matches_; }
void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; } void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; }
const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; } const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; }
const std::string &WiFiScanResult::get_ssid() const { return this->ssid_; }
uint8_t WiFiScanResult::get_channel() const { return this->channel_; } uint8_t WiFiScanResult::get_channel() const { return this->channel_; }
int8_t WiFiScanResult::get_rssi() const { return this->rssi_; } int8_t WiFiScanResult::get_rssi() const { return this->rssi_; }
bool WiFiScanResult::get_with_auth() const { return this->with_auth_; } bool WiFiScanResult::get_with_auth() const { return this->with_auth_; }
@@ -2227,7 +2230,7 @@ void WiFiComponent::process_roaming_scan_() {
for (const auto &result : this->scan_result_) { for (const auto &result : this->scan_result_) {
// Must be same SSID, different BSSID // Must be same SSID, different BSSID
if (result.get_ssid() != current_ssid.c_str() || result.get_bssid() == current_bssid) if (current_ssid != result.get_ssid() || result.get_bssid() == current_bssid)
continue; continue;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE

View File

@@ -175,13 +175,9 @@ template<typename T> using wifi_scan_vector_t = FixedVector<T>;
class WiFiAP { class WiFiAP {
public: public:
void set_ssid(const std::string &ssid); void set_ssid(const std::string &ssid);
void set_ssid(const char *ssid);
void set_ssid(const CompactString &ssid) { this->ssid_ = ssid; }
void set_bssid(const bssid_t &bssid); void set_bssid(const bssid_t &bssid);
void clear_bssid(); void clear_bssid();
void set_password(const std::string &password); void set_password(const std::string &password);
void set_password(const char *password);
void set_password(const CompactString &password) { this->password_ = password; }
#ifdef USE_WIFI_WPA2_EAP #ifdef USE_WIFI_WPA2_EAP
void set_eap(optional<EAPAuth> eap_auth); void set_eap(optional<EAPAuth> eap_auth);
#endif // USE_WIFI_WPA2_EAP #endif // USE_WIFI_WPA2_EAP
@@ -192,10 +188,10 @@ class WiFiAP {
void set_manual_ip(optional<ManualIP> manual_ip); void set_manual_ip(optional<ManualIP> manual_ip);
#endif #endif
void set_hidden(bool hidden); void set_hidden(bool hidden);
const CompactString &get_ssid() const { return this->ssid_; } const std::string &get_ssid() const;
const CompactString &get_password() const { return this->password_; }
const bssid_t &get_bssid() const; const bssid_t &get_bssid() const;
bool has_bssid() const; bool has_bssid() const;
const std::string &get_password() const;
#ifdef USE_WIFI_WPA2_EAP #ifdef USE_WIFI_WPA2_EAP
const optional<EAPAuth> &get_eap() const; const optional<EAPAuth> &get_eap() const;
#endif // USE_WIFI_WPA2_EAP #endif // USE_WIFI_WPA2_EAP
@@ -208,8 +204,8 @@ class WiFiAP {
bool get_hidden() const; bool get_hidden() const;
protected: protected:
CompactString ssid_; std::string ssid_;
CompactString password_; std::string password_;
#ifdef USE_WIFI_WPA2_EAP #ifdef USE_WIFI_WPA2_EAP
optional<EAPAuth> eap_; optional<EAPAuth> eap_;
#endif // USE_WIFI_WPA2_EAP #endif // USE_WIFI_WPA2_EAP
@@ -225,15 +221,14 @@ class WiFiAP {
class WiFiScanResult { class WiFiScanResult {
public: public:
WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, bool with_auth, WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden);
bool is_hidden);
bool matches(const WiFiAP &config) const; bool matches(const WiFiAP &config) const;
bool get_matches() const; bool get_matches() const;
void set_matches(bool matches); void set_matches(bool matches);
const bssid_t &get_bssid() const; const bssid_t &get_bssid() const;
const CompactString &get_ssid() const { return this->ssid_; } const std::string &get_ssid() const;
uint8_t get_channel() const; uint8_t get_channel() const;
int8_t get_rssi() const; int8_t get_rssi() const;
bool get_with_auth() const; bool get_with_auth() const;
@@ -247,7 +242,7 @@ class WiFiScanResult {
bssid_t bssid_; bssid_t bssid_;
uint8_t channel_; uint8_t channel_;
int8_t rssi_; int8_t rssi_;
CompactString ssid_; std::string ssid_;
int8_t priority_{0}; int8_t priority_{0};
bool matches_{false}; bool matches_{false};
bool with_auth_; bool with_auth_;
@@ -386,10 +381,6 @@ class WiFiComponent : public Component {
void set_passive_scan(bool passive); void set_passive_scan(bool passive);
void save_wifi_sta(const std::string &ssid, const std::string &password); void save_wifi_sta(const std::string &ssid, const std::string &password);
void save_wifi_sta(const char *ssid, const char *password);
void save_wifi_sta(const CompactString &ssid, const CompactString &password) {
this->save_wifi_sta(ssid.c_str(), password.c_str());
}
// ========== INTERNAL METHODS ========== // ========== INTERNAL METHODS ==========
// (In most use cases you won't need these) // (In most use cases you won't need these)
@@ -463,8 +454,12 @@ class WiFiComponent : public Component {
void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; } void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; }
void set_post_connect_roaming(bool enabled) { this->post_connect_roaming_ = enabled; } void set_post_connect_roaming(bool enabled) { this->post_connect_roaming_ = enabled; }
Trigger<> *get_connect_trigger() const { return this->connect_trigger_; }; #ifdef USE_WIFI_CONNECT_TRIGGER
Trigger<> *get_disconnect_trigger() const { return this->disconnect_trigger_; }; Trigger<> *get_connect_trigger() { return &this->connect_trigger_; }
#endif
#ifdef USE_WIFI_DISCONNECT_TRIGGER
Trigger<> *get_disconnect_trigger() { return &this->disconnect_trigger_; }
#endif
int32_t get_wifi_channel(); int32_t get_wifi_channel();
@@ -550,7 +545,7 @@ class WiFiComponent : public Component {
int8_t find_first_non_hidden_index_() const; int8_t find_first_non_hidden_index_() const;
/// Check if an SSID was seen in the most recent scan results /// Check if an SSID was seen in the most recent scan results
/// Used to skip hidden mode for SSIDs we know are visible /// Used to skip hidden mode for SSIDs we know are visible
bool ssid_was_seen_in_scan_(const CompactString &ssid) const; bool ssid_was_seen_in_scan_(const std::string &ssid) const;
/// Check if full scan results are needed (captive portal active, improv, listeners) /// Check if full scan results are needed (captive portal active, improv, listeners)
bool needs_full_scan_results_() const; bool needs_full_scan_results_() const;
/// Check if network matches any configured network (for scan result filtering) /// Check if network matches any configured network (for scan result filtering)
@@ -715,7 +710,9 @@ class WiFiComponent : public Component {
// Group all boolean values together // Group all boolean values together
bool has_ap_{false}; bool has_ap_{false};
#if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER)
bool handled_connected_state_{false}; bool handled_connected_state_{false};
#endif
bool error_from_callback_{false}; bool error_from_callback_{false};
bool scan_done_{false}; bool scan_done_{false};
bool ap_setup_{false}; bool ap_setup_{false};
@@ -742,9 +739,12 @@ class WiFiComponent : public Component {
SemaphoreHandle_t high_performance_semaphore_{nullptr}; SemaphoreHandle_t high_performance_semaphore_{nullptr};
#endif #endif
// Pointers at the end (naturally aligned) #ifdef USE_WIFI_CONNECT_TRIGGER
Trigger<> *connect_trigger_{new Trigger<>()}; Trigger<> connect_trigger_;
Trigger<> *disconnect_trigger_{new Trigger<>()}; #endif
#ifdef USE_WIFI_DISCONNECT_TRIGGER
Trigger<> disconnect_trigger_;
#endif
private: private:
// Stores a pointer to a string literal (static storage duration). // Stores a pointer to a string literal (static storage duration).

View File

@@ -784,8 +784,8 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid); const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid);
if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) { if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) {
this->scan_result_.emplace_back( this->scan_result_.emplace_back(
bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, ssid_cstr, bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
it->ssid_len, it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0); std::string(ssid_cstr, it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0);
} else { } else {
this->log_discarded_scan_result_(ssid_cstr, it->bssid, it->rssi, it->channel); this->log_discarded_scan_result_(ssid_cstr, it->bssid, it->rssi, it->channel);
} }

View File

@@ -870,7 +870,8 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) { if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) {
bssid_t bssid; bssid_t bssid;
std::copy(record.bssid, record.bssid + 6, bssid.begin()); std::copy(record.bssid, record.bssid + 6, bssid.begin());
this->scan_result_.emplace_back(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi, std::string ssid(ssid_cstr);
this->scan_result_.emplace_back(bssid, std::move(ssid), record.primary, record.rssi,
record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0'); record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0');
} else { } else {
this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary); this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary);

View File

@@ -694,7 +694,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
auto &ap = scan->ap[i]; auto &ap = scan->ap[i];
this->scan_result_.emplace_back(bssid_t{ap.bssid.addr[0], ap.bssid.addr[1], ap.bssid.addr[2], ap.bssid.addr[3], this->scan_result_.emplace_back(bssid_t{ap.bssid.addr[0], ap.bssid.addr[1], ap.bssid.addr[2], ap.bssid.addr[3],
ap.bssid.addr[4], ap.bssid.addr[5]}, ap.bssid.addr[4], ap.bssid.addr[5]},
ssid_cstr, strlen(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN, std::string(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN,
ssid_cstr[0] == '\0'); ssid_cstr[0] == '\0');
} else { } else {
auto &ap = scan->ap[i]; auto &ap = scan->ap[i];

View File

@@ -149,8 +149,9 @@ void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *re
bssid_t bssid; bssid_t bssid;
std::copy(result->bssid, result->bssid + 6, bssid.begin()); std::copy(result->bssid, result->bssid + 6, bssid.begin());
WiFiScanResult res(bssid, ssid_cstr, strlen(ssid_cstr), result->channel, result->rssi, std::string ssid(ssid_cstr);
result->auth_mode != CYW43_AUTH_OPEN, ssid_cstr[0] == '\0'); WiFiScanResult res(bssid, std::move(ssid), result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN,
ssid_cstr[0] == '\0');
if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) { if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) {
this->scan_result_.push_back(res); this->scan_result_.push_back(res);
} }

View File

@@ -89,7 +89,7 @@ void ScanResultsWiFiInfo::on_wifi_scan_results(const wifi::wifi_scan_vector_t<wi
for (const auto &scan : results) { for (const auto &scan : results) {
if (scan.get_is_hidden()) if (scan.get_is_hidden())
continue; continue;
const auto &ssid = scan.get_ssid(); const std::string &ssid = scan.get_ssid();
// Max space: ssid + ": " (2) + "-128" (4) + "dB\n" (3) = ssid + 9 // Max space: ssid + ": " (2) + "-128" (4) + "dB\n" (3) = ssid + 9
if (ptr + ssid.size() + 9 > end) if (ptr + ssid.size() + 9 > end)
break; break;

View File

@@ -152,10 +152,11 @@ class ZephyrPreferences : public ESPPreferences {
} }
}; };
static ZephyrPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void setup_preferences() { void setup_preferences() {
auto *prefs = new ZephyrPreferences(); // NOLINT(cppcoreguidelines-owning-memory) global_preferences = &s_preferences;
global_preferences = prefs; s_preferences.open();
prefs->open();
} }
} // namespace zephyr } // namespace zephyr

View File

@@ -227,6 +227,8 @@
#define USE_WIFI_SCAN_RESULTS_LISTENERS #define USE_WIFI_SCAN_RESULTS_LISTENERS
#define USE_WIFI_CONNECT_STATE_LISTENERS #define USE_WIFI_CONNECT_STATE_LISTENERS
#define USE_WIFI_POWER_SAVE_LISTENERS #define USE_WIFI_POWER_SAVE_LISTENERS
#define USE_WIFI_CONNECT_TRIGGER
#define USE_WIFI_DISCONNECT_TRIGGER
#define ESPHOME_WIFI_IP_STATE_LISTENERS 2 #define ESPHOME_WIFI_IP_STATE_LISTENERS 2
#define ESPHOME_WIFI_SCAN_RESULTS_LISTENERS 2 #define ESPHOME_WIFI_SCAN_RESULTS_LISTENERS 2
#define ESPHOME_WIFI_CONNECT_STATE_LISTENERS 2 #define ESPHOME_WIFI_CONNECT_STATE_LISTENERS 2

View File

@@ -13,7 +13,6 @@
#include <cstdarg> #include <cstdarg>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <new>
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "rom/crc.h" #include "rom/crc.h"
@@ -859,60 +858,4 @@ void IRAM_ATTR HOT delay_microseconds_safe(uint32_t us) {
; ;
} }
// CompactString implementation
CompactString::CompactString(const char *str, size_t len) {
if (len > MAX_LENGTH) {
len = MAX_LENGTH; // Clamp to max valid length
}
this->length_ = len;
if (len <= INLINE_CAPACITY) {
// Store inline with null terminator
this->is_heap_ = 0;
if (len > 0) {
std::memcpy(this->storage_, str, len);
}
this->storage_[len] = '\0';
} else {
// Heap allocate with null terminator
this->is_heap_ = 1;
char *heap_data = new char[len + 1]; // NOLINT(cppcoreguidelines-owning-memory)
std::memcpy(heap_data, str, len);
heap_data[len] = '\0';
this->set_heap_ptr_(heap_data);
}
}
CompactString::CompactString(const CompactString &other) : CompactString(other.data(), other.size()) {}
CompactString &CompactString::operator=(const CompactString &other) {
if (this != &other) {
this->~CompactString();
new (this) CompactString(other);
}
return *this;
}
CompactString::CompactString(CompactString &&other) noexcept : length_(other.length_), is_heap_(other.is_heap_) {
// Copy full storage (includes null terminator for inline, or pointer for heap)
std::memcpy(this->storage_, other.storage_, INLINE_CAPACITY + 1);
other.length_ = 0;
other.is_heap_ = 0;
other.storage_[0] = '\0';
}
CompactString &CompactString::operator=(CompactString &&other) noexcept {
if (this != &other) {
this->~CompactString();
new (this) CompactString(std::move(other));
}
return *this;
}
CompactString::~CompactString() {
if (this->is_heap_) {
delete[] this->get_heap_ptr_(); // NOLINT(cppcoreguidelines-owning-memory)
}
}
} // namespace esphome } // namespace esphome

View File

@@ -1784,58 +1784,4 @@ template<typename T, enable_if_t<std::is_pointer<T *>::value, int> = 0> T &id(T
///@} ///@}
/// 20-byte string: 18 chars inline + null, heap for longer. Always null-terminated.
class CompactString {
public:
static constexpr uint8_t MAX_LENGTH = 127;
static constexpr uint8_t INLINE_CAPACITY = 18; // 18 chars + null terminator fits in 19 bytes
static constexpr uint8_t BUFFER_SIZE = MAX_LENGTH + 1; // For external buffer (128 bytes)
CompactString() : length_(0), is_heap_(0) { this->storage_[0] = '\0'; }
CompactString(const char *str, size_t len);
CompactString(const CompactString &other);
CompactString(CompactString &&other) noexcept;
CompactString &operator=(const CompactString &other);
CompactString &operator=(CompactString &&other) noexcept;
~CompactString();
const char *data() const { return this->is_heap_ ? this->get_heap_ptr_() : this->storage_; }
const char *c_str() const { return this->data(); } // Always null-terminated
size_t size() const { return this->length_; }
bool empty() const { return this->length_ == 0; }
// Implicit conversion to std::string for backwards compatibility
operator std::string() const { return std::string(this->data(), this->size()); }
bool operator==(const CompactString &other) const {
return this->size() == other.size() && std::memcmp(this->data(), other.data(), this->size()) == 0;
}
bool operator==(const std::string &other) const {
return this->size() == other.size() && std::memcmp(this->data(), other.data(), this->size()) == 0;
}
bool operator==(const char *other) const {
return this->size() == std::strlen(other) && std::memcmp(this->data(), other, this->size()) == 0;
}
bool operator!=(const CompactString &other) const { return !(*this == other); }
bool operator!=(const std::string &other) const { return !(*this == other); }
bool operator!=(const char *other) const { return !(*this == other); }
protected:
char *get_heap_ptr_() const {
char *ptr;
std::memcpy(&ptr, this->storage_, sizeof(ptr));
return ptr;
}
void set_heap_ptr_(char *ptr) { std::memcpy(this->storage_, &ptr, sizeof(ptr)); }
// Storage for string data. When is_heap_=0, contains the string directly (null-terminated).
// When is_heap_=1, first sizeof(char*) bytes contain pointer to heap allocation.
char storage_[INLINE_CAPACITY + 1]; // 19 bytes: 18 chars + null terminator
uint8_t length_ : 7; // String length (0-127)
uint8_t is_heap_ : 1; // 1 if using heap pointer, 0 if using inline storage
// Total size: 20 bytes (19 bytes storage + 1 byte bitfields)
};
static_assert(sizeof(CompactString) == 20, "CompactString must be exactly 20 bytes");
} // namespace esphome } // namespace esphome

View File

@@ -296,6 +296,16 @@ select:
// Migration guide: Store in std::string // Migration guide: Store in std::string
std::string stored_option(id(template_select).current_option()); std::string stored_option(id(template_select).current_option());
ESP_LOGI("test", "Stored: %s", stored_option.c_str()); ESP_LOGI("test", "Stored: %s", stored_option.c_str());
- platform: template
id: template_select_with_action
name: "Template select with action"
options:
- option_a
- option_b
set_action:
- logger.log:
format: "Selected: %s"
args: ["x.c_str()"]
lock: lock:
- platform: template - platform: template

View File

@@ -26,3 +26,7 @@ wifi:
- ssid: MySSID3 - ssid: MySSID3
password: password3 password: password3
priority: 0 priority: 0
on_connect:
- logger.log: "WiFi connected!"
on_disconnect:
- logger.log: "WiFi disconnected!"

View File

@@ -56,7 +56,21 @@ select:
std::string prefix = x.substr(0, 6); std::string prefix = x.substr(0, 6);
ESP_LOGI("test", "Substr prefix: %s", prefix.c_str()); ESP_LOGI("test", "Substr prefix: %s", prefix.c_str());
# Second select with numeric options to test ADL functions # Second select with set_action trigger (uses TemplateSelectWithSetAction subclass)
- platform: template
name: "Action Select"
id: action_select
options:
- "Action A"
- "Action B"
set_action:
then:
# Test: set_action trigger receives StringRef
- logger.log:
format: "set_action triggered: %s"
args: ['x.c_str()']
# Third select with numeric options to test ADL functions
- platform: template - platform: template
name: "Baud Rate" name: "Baud Rate"
id: baud_select id: baud_select

View File

@@ -28,6 +28,8 @@ async def test_select_stringref_trigger(
find_substr_future = loop.create_future() find_substr_future = loop.create_future()
find_char_future = loop.create_future() find_char_future = loop.create_future()
substr_future = loop.create_future() substr_future = loop.create_future()
# set_action trigger (TemplateSelectWithSetAction subclass)
set_action_future = loop.create_future()
# ADL functions # ADL functions
stoi_future = loop.create_future() stoi_future = loop.create_future()
stol_future = loop.create_future() stol_future = loop.create_future()
@@ -43,6 +45,8 @@ async def test_select_stringref_trigger(
find_substr_pattern = re.compile(r"Found 'Option' in value") find_substr_pattern = re.compile(r"Found 'Option' in value")
find_char_pattern = re.compile(r"Space at position: 6") # space at index 6 find_char_pattern = re.compile(r"Space at position: 6") # space at index 6
substr_pattern = re.compile(r"Substr prefix: Option") substr_pattern = re.compile(r"Substr prefix: Option")
# set_action trigger pattern (TemplateSelectWithSetAction subclass)
set_action_pattern = re.compile(r"set_action triggered: Action B")
# ADL function patterns (115200 from baud rate select) # ADL function patterns (115200 from baud rate select)
stoi_pattern = re.compile(r"stoi result: 115200") stoi_pattern = re.compile(r"stoi result: 115200")
stol_pattern = re.compile(r"stol result: 115200") stol_pattern = re.compile(r"stol result: 115200")
@@ -67,6 +71,9 @@ async def test_select_stringref_trigger(
find_char_future.set_result(True) find_char_future.set_result(True)
if not substr_future.done() and substr_pattern.search(line): if not substr_future.done() and substr_pattern.search(line):
substr_future.set_result(True) substr_future.set_result(True)
# set_action trigger
if not set_action_future.done() and set_action_pattern.search(line):
set_action_future.set_result(True)
# ADL functions # ADL functions
if not stoi_future.done() and stoi_pattern.search(line): if not stoi_future.done() and stoi_pattern.search(line):
stoi_future.set_result(True) stoi_future.set_result(True)
@@ -89,22 +96,21 @@ async def test_select_stringref_trigger(
# List entities to find our select # List entities to find our select
entities, _ = await client.list_entities_services() entities, _ = await client.list_entities_services()
select_entity = next( select_entity = next((e for e in entities if e.name == "Test Select"), None)
(e for e in entities if hasattr(e, "options") and e.name == "Test Select"),
None,
)
assert select_entity is not None, "Test Select entity not found" assert select_entity is not None, "Test Select entity not found"
baud_entity = next( baud_entity = next((e for e in entities if e.name == "Baud Rate"), None)
(e for e in entities if hasattr(e, "options") and e.name == "Baud Rate"),
None,
)
assert baud_entity is not None, "Baud Rate entity not found" assert baud_entity is not None, "Baud Rate entity not found"
action_entity = next((e for e in entities if e.name == "Action Select"), None)
assert action_entity is not None, "Action Select entity not found"
# Change select to Option B - this should trigger on_value with StringRef # Change select to Option B - this should trigger on_value with StringRef
client.select_command(select_entity.key, "Option B") client.select_command(select_entity.key, "Option B")
# Change baud to 115200 - this tests ADL functions (stoi, stol, stof, stod) # Change baud to 115200 - this tests ADL functions (stoi, stol, stof, stod)
client.select_command(baud_entity.key, "115200") client.select_command(baud_entity.key, "115200")
# Change action select - tests set_action trigger (TemplateSelectWithSetAction)
client.select_command(action_entity.key, "Action B")
# Wait for all log messages confirming StringRef operations work # Wait for all log messages confirming StringRef operations work
try: try:
@@ -118,6 +124,7 @@ async def test_select_stringref_trigger(
find_substr_future, find_substr_future,
find_char_future, find_char_future,
substr_future, substr_future,
set_action_future,
stoi_future, stoi_future,
stol_future, stol_future,
stof_future, stof_future,
@@ -135,6 +142,7 @@ async def test_select_stringref_trigger(
"find_substr": find_substr_future.done(), "find_substr": find_substr_future.done(),
"find_char": find_char_future.done(), "find_char": find_char_future.done(),
"substr": substr_future.done(), "substr": substr_future.done(),
"set_action": set_action_future.done(),
"stoi": stoi_future.done(), "stoi": stoi_future.done(),
"stol": stol_future.done(), "stol": stol_future.done(),
"stof": stof_future.done(), "stof": stof_future.done(),