mirror of
https://github.com/esphome/esphome.git
synced 2026-02-04 01:29:40 -07:00
Compare commits
23 Commits
compact_st
...
template_s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a1fa05c8f | ||
|
|
cb9fbf8970 | ||
|
|
a430b3a426 | ||
|
|
fbeb0e8e54 | ||
|
|
9d63642bdb | ||
|
|
8cb701e412 | ||
|
|
d41c84d624 | ||
|
|
9f1a427ce2 | ||
|
|
ae71f07abb | ||
|
|
5a2774876a | ||
|
|
ccf5c1f7e9 | ||
|
|
efecea9450 | ||
|
|
26e4cda610 | ||
|
|
ede2f205d3 | ||
|
|
8cf29c40a9 | ||
|
|
0332cbfdd4 | ||
|
|
89bd9b610e | ||
|
|
9dbcf1447b | ||
|
|
6c853cae57 | ||
|
|
48e6efb6aa | ||
|
|
cfc3b3336f | ||
|
|
9ca394d1e5 | ||
|
|
e62a87afe1 |
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -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}}"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
@@ -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"):
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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_
|
||||||
|
|||||||
@@ -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_
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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!"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
Reference in New Issue
Block a user