mirror of
https://github.com/esphome/esphome.git
synced 2026-01-23 03:19:09 -07:00
Compare commits
15 Commits
str_sprint
...
2026.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44e624d7a7 | ||
|
|
f938de16af | ||
|
|
ec791063b3 | ||
|
|
fb984cd052 | ||
|
|
85181779d1 | ||
|
|
95b23702e4 | ||
|
|
95eebcd74f | ||
|
|
3c3d5c2fca | ||
|
|
811ac81320 | ||
|
|
f01bd68a4b | ||
|
|
5433c0f707 | ||
|
|
b06cce9eeb | ||
|
|
65bcfee035 | ||
|
|
9261b9ecaa | ||
|
|
6725e6c01e |
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.1.0
|
||||
PROJECT_NUMBER = 2026.1.1
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -1844,23 +1844,8 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Toggle Nagle's algorithm based on message type to prevent log messages from
|
||||
// filling the TCP send buffer and crowding out important state updates.
|
||||
//
|
||||
// This honors the `no_delay` proto option - SubscribeLogsResponse is the only
|
||||
// message with `option (no_delay) = false;` in api.proto, indicating it should
|
||||
// allow Nagle coalescing. This option existed since 2019 but was never implemented.
|
||||
//
|
||||
// - Log messages: Enable Nagle (NODELAY=false) so small log packets coalesce
|
||||
// into fewer, larger packets. They flush naturally via TCP delayed ACK timer
|
||||
// (~200ms), buffer filling, or when a state update triggers a flush.
|
||||
//
|
||||
// - All other messages (state updates, responses): Disable Nagle (NODELAY=true)
|
||||
// for immediate delivery. These are time-sensitive and should not be delayed.
|
||||
//
|
||||
// This must be done proactively BEFORE the buffer fills up - checking buffer
|
||||
// state here would be too late since we'd already be in a degraded state.
|
||||
this->helper_->set_nodelay(!is_log_message);
|
||||
// Set TCP_NODELAY based on message type - see set_nodelay_for_message() for details
|
||||
this->helper_->set_nodelay_for_message(is_log_message);
|
||||
|
||||
APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
|
||||
if (err == APIError::WOULD_BLOCK)
|
||||
|
||||
@@ -120,26 +120,39 @@ class APIFrameHelper {
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
/// Toggle TCP_NODELAY socket option to control Nagle's algorithm.
|
||||
///
|
||||
/// This is used to allow log messages to coalesce (Nagle enabled) while keeping
|
||||
/// state updates low-latency (NODELAY enabled). Without this, many small log
|
||||
/// packets fill the TCP send buffer, crowding out important state updates.
|
||||
///
|
||||
/// State is tracked to minimize setsockopt() overhead - on lwip_raw (ESP8266/RP2040)
|
||||
/// this is just a boolean assignment; on other platforms it's a lightweight syscall.
|
||||
///
|
||||
/// @param enable true to enable NODELAY (disable Nagle), false to enable Nagle
|
||||
/// @return true if successful or already in desired state
|
||||
bool set_nodelay(bool enable) {
|
||||
if (this->nodelay_enabled_ == enable)
|
||||
return true;
|
||||
int val = enable ? 1 : 0;
|
||||
int err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
|
||||
if (err == 0) {
|
||||
this->nodelay_enabled_ = enable;
|
||||
// Manage TCP_NODELAY (Nagle's algorithm) based on message type.
|
||||
//
|
||||
// For non-log messages (sensor data, state updates): Always disable Nagle
|
||||
// (NODELAY on) for immediate delivery - these are time-sensitive.
|
||||
//
|
||||
// For log messages: Use Nagle to coalesce multiple small log packets into
|
||||
// fewer larger packets, reducing WiFi overhead. However, we limit batching
|
||||
// to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained
|
||||
// devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into
|
||||
// shared pbufs, but holding data too long waiting for Nagle's timer causes
|
||||
// buffer exhaustion and dropped messages.
|
||||
//
|
||||
// Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all)
|
||||
//
|
||||
void set_nodelay_for_message(bool is_log_message) {
|
||||
if (!is_log_message) {
|
||||
if (this->nodelay_state_ != NODELAY_ON) {
|
||||
this->set_nodelay_raw_(true);
|
||||
this->nodelay_state_ = NODELAY_ON;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Log messages 1-3: state transitions -1 -> 1 -> 2 -> -1 (flush on 3rd)
|
||||
if (this->nodelay_state_ == NODELAY_ON) {
|
||||
this->set_nodelay_raw_(false);
|
||||
this->nodelay_state_ = 1;
|
||||
} else if (this->nodelay_state_ >= LOG_NAGLE_COUNT) {
|
||||
this->set_nodelay_raw_(true);
|
||||
this->nodelay_state_ = NODELAY_ON;
|
||||
} else {
|
||||
this->nodelay_state_++;
|
||||
}
|
||||
return err == 0;
|
||||
}
|
||||
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
||||
// Write multiple protobuf messages in a single operation
|
||||
@@ -229,10 +242,18 @@ class APIFrameHelper {
|
||||
uint8_t tx_buf_head_{0};
|
||||
uint8_t tx_buf_tail_{0};
|
||||
uint8_t tx_buf_count_{0};
|
||||
// Tracks TCP_NODELAY state to minimize setsockopt() calls. Initialized to true
|
||||
// since init_common_() enables NODELAY. Used by set_nodelay() to allow log
|
||||
// messages to coalesce while keeping state updates low-latency.
|
||||
bool nodelay_enabled_{true};
|
||||
// Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled
|
||||
// (immediate send). Values 1-2 count log messages in the current Nagle batch.
|
||||
// After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset.
|
||||
static constexpr int8_t NODELAY_ON = -1;
|
||||
static constexpr int8_t LOG_NAGLE_COUNT = 2;
|
||||
int8_t nodelay_state_{NODELAY_ON};
|
||||
|
||||
// Internal helper to set TCP_NODELAY socket option
|
||||
void set_nodelay_raw_(bool enable) {
|
||||
int val = enable ? 1 : 0;
|
||||
this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
|
||||
}
|
||||
|
||||
// Common initialization for both plaintext and noise protocols
|
||||
APIError init_common_();
|
||||
|
||||
@@ -13,14 +13,11 @@ from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, aqi_ns
|
||||
CODEOWNERS = ["@jasstrong"]
|
||||
DEPENDENCIES = ["sensor"]
|
||||
|
||||
UNIT_INDEX = "index"
|
||||
|
||||
AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
AQISensor,
|
||||
unit_of_measurement=UNIT_INDEX,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
|
||||
@@ -89,10 +89,8 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
|
||||
delayMicroseconds(500);
|
||||
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
|
||||
delayMicroseconds(2000);
|
||||
} else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
|
||||
delayMicroseconds(1000);
|
||||
} else {
|
||||
delayMicroseconds(800);
|
||||
delayMicroseconds(1000);
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
@@ -190,7 +190,7 @@ async def to_code(config):
|
||||
# Rotation is handled by setting the transform
|
||||
display_config = {k: v for k, v in config.items() if k != CONF_ROTATION}
|
||||
await display.register_display(var, display_config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -180,6 +180,12 @@ def set_core_data(config):
|
||||
path=[CONF_CPU_FREQUENCY],
|
||||
)
|
||||
|
||||
if variant == VARIANT_ESP32P4 and cpu_frequency == "400MHZ":
|
||||
_LOGGER.warning(
|
||||
"400MHz on ESP32-P4 is experimental and may not boot. "
|
||||
"Consider using 360MHz instead. See https://github.com/esphome/esphome/issues/13425"
|
||||
)
|
||||
|
||||
CORE.data[KEY_ESP32] = {}
|
||||
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32
|
||||
conf = config[CONF_FRAMEWORK]
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <esp_ota_ops.h>
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
#include "esphome/components/http_request/http_request.h"
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#endif
|
||||
@@ -181,15 +182,23 @@ bool Esp32HostedUpdate::fetch_manifest_() {
|
||||
}
|
||||
|
||||
// Read manifest JSON into string (manifest is small, ~1KB max)
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
|
||||
// Use http_read_loop_result() helper instead of checking return values directly
|
||||
std::string json_str;
|
||||
json_str.reserve(container->content_length);
|
||||
uint8_t buf[256];
|
||||
uint32_t last_data_time = millis();
|
||||
const uint32_t read_timeout = this->http_request_parent_->get_timeout();
|
||||
while (container->get_bytes_read() < container->content_length) {
|
||||
int read = container->read(buf, sizeof(buf));
|
||||
if (read > 0) {
|
||||
json_str.append(reinterpret_cast<char *>(buf), read);
|
||||
}
|
||||
int read_or_error = container->read(buf, sizeof(buf));
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
||||
if (result == http_request::HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != http_request::HttpReadLoopResult::DATA)
|
||||
break; // ERROR or TIMEOUT
|
||||
json_str.append(reinterpret_cast<char *>(buf), read_or_error);
|
||||
}
|
||||
container->end();
|
||||
|
||||
@@ -294,32 +303,38 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
|
||||
}
|
||||
|
||||
// Stream firmware to coprocessor while computing SHA256
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
|
||||
// Use http_read_loop_result() helper instead of checking return values directly
|
||||
sha256::SHA256 hasher;
|
||||
hasher.init();
|
||||
|
||||
uint8_t buffer[CHUNK_SIZE];
|
||||
uint32_t last_data_time = millis();
|
||||
const uint32_t read_timeout = this->http_request_parent_->get_timeout();
|
||||
while (container->get_bytes_read() < total_size) {
|
||||
int read = container->read(buffer, sizeof(buffer));
|
||||
int read_or_error = container->read(buffer, sizeof(buffer));
|
||||
|
||||
// Feed watchdog and give other tasks a chance to run
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
// Exit loop if no data available (stream closed or end of data)
|
||||
if (read <= 0) {
|
||||
if (read < 0) {
|
||||
ESP_LOGE(TAG, "Stream closed with error");
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
container->end();
|
||||
this->status_set_error(LOG_STR("Download failed"));
|
||||
return false;
|
||||
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
||||
if (result == http_request::HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != http_request::HttpReadLoopResult::DATA) {
|
||||
if (result == http_request::HttpReadLoopResult::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading firmware data");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading firmware data: %d", read_or_error);
|
||||
}
|
||||
// read == 0: no more data available, exit loop
|
||||
break;
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
container->end();
|
||||
this->status_set_error(LOG_STR("Download failed"));
|
||||
return false;
|
||||
}
|
||||
|
||||
hasher.add(buffer, read);
|
||||
err = esp_hosted_slave_ota_write(buffer, read); // NOLINT
|
||||
hasher.add(buffer, read_or_error);
|
||||
err = esp_hosted_slave_ota_write(buffer, read_or_error); // NOLINT
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "fingerprint_grow.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -532,14 +533,21 @@ void FingerprintGrowComponent::sensor_sleep_() {
|
||||
}
|
||||
|
||||
void FingerprintGrowComponent::dump_config() {
|
||||
char sensing_pin_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
char power_pin_buf[GPIO_SUMMARY_MAX_LEN];
|
||||
if (this->has_sensing_pin_) {
|
||||
this->sensing_pin_->dump_summary(sensing_pin_buf, sizeof(sensing_pin_buf));
|
||||
}
|
||||
if (this->has_power_pin_) {
|
||||
this->sensor_power_pin_->dump_summary(power_pin_buf, sizeof(power_pin_buf));
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"GROW_FINGERPRINT_READER:\n"
|
||||
" System Identifier Code: 0x%.4X\n"
|
||||
" Touch Sensing Pin: %s\n"
|
||||
" Sensor Power Pin: %s",
|
||||
this->system_identifier_code_,
|
||||
this->has_sensing_pin_ ? this->sensing_pin_->dump_summary().c_str() : "None",
|
||||
this->has_power_pin_ ? this->sensor_power_pin_->dump_summary().c_str() : "None");
|
||||
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : "None",
|
||||
this->has_power_pin_ ? power_pin_buf : "None");
|
||||
if (this->idle_period_to_sleep_ms_ < UINT32_MAX) {
|
||||
ESP_LOGCONFIG(TAG, " Idle Period to Sleep: %" PRIu32 " ms", this->idle_period_to_sleep_ms_);
|
||||
} else {
|
||||
|
||||
@@ -157,6 +157,7 @@ async def to_code(config):
|
||||
if CORE.is_esp32:
|
||||
cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX]))
|
||||
cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX]))
|
||||
cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL]))
|
||||
|
||||
if config.get(CONF_VERIFY_SSL):
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
|
||||
|
||||
@@ -79,6 +79,81 @@ inline bool is_redirect(int const status) {
|
||||
*/
|
||||
inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; }
|
||||
|
||||
/*
|
||||
* HTTP Container Read Semantics
|
||||
* =============================
|
||||
*
|
||||
* IMPORTANT: These semantics differ from standard BSD sockets!
|
||||
*
|
||||
* BSD socket read() returns:
|
||||
* > 0: bytes read
|
||||
* == 0: connection closed (EOF)
|
||||
* < 0: error (check errno)
|
||||
*
|
||||
* HttpContainer::read() returns:
|
||||
* > 0: bytes read successfully
|
||||
* == 0: no data available yet OR all content read
|
||||
* (caller should check bytes_read vs content_length)
|
||||
* < 0: error or connection closed (caller should EXIT)
|
||||
* HTTP_ERROR_CONNECTION_CLOSED (-1) = connection closed prematurely
|
||||
* other negative values = platform-specific errors
|
||||
*
|
||||
* Platform behaviors:
|
||||
* - ESP-IDF: blocking reads, 0 only returned when all content read
|
||||
* - Arduino: non-blocking, 0 means "no data yet" or "all content read"
|
||||
*
|
||||
* Use the helper functions below instead of checking return values directly:
|
||||
* - http_read_loop_result(): for manual loops with per-chunk processing
|
||||
* - http_read_fully(): for simple "read N bytes into buffer" operations
|
||||
*/
|
||||
|
||||
/// Error code returned by HttpContainer::read() when connection closed prematurely
|
||||
/// NOTE: Unlike BSD sockets where 0 means EOF, here 0 means "no data yet, retry"
|
||||
static constexpr int HTTP_ERROR_CONNECTION_CLOSED = -1;
|
||||
|
||||
/// Status of a read operation
|
||||
enum class HttpReadStatus : uint8_t {
|
||||
OK, ///< Read completed successfully
|
||||
ERROR, ///< Read error occurred
|
||||
TIMEOUT, ///< Timeout waiting for data
|
||||
};
|
||||
|
||||
/// Result of an HTTP read operation
|
||||
struct HttpReadResult {
|
||||
HttpReadStatus status; ///< Status of the read operation
|
||||
int error_code; ///< Error code from read() on failure, 0 on success
|
||||
};
|
||||
|
||||
/// Result of processing a non-blocking read with timeout (for manual loops)
|
||||
enum class HttpReadLoopResult : uint8_t {
|
||||
DATA, ///< Data was read, process it
|
||||
RETRY, ///< No data yet, already delayed, caller should continue 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
|
||||
/// @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 timeout_ms Maximum time to wait for data
|
||||
/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit
|
||||
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time,
|
||||
uint32_t timeout_ms) {
|
||||
if (bytes_read_or_error > 0) {
|
||||
last_data_time = millis();
|
||||
return HttpReadLoopResult::DATA;
|
||||
}
|
||||
if (bytes_read_or_error < 0) {
|
||||
return HttpReadLoopResult::ERROR;
|
||||
}
|
||||
// bytes_read_or_error == 0: no data available yet
|
||||
if (millis() - last_data_time >= timeout_ms) {
|
||||
return HttpReadLoopResult::TIMEOUT;
|
||||
}
|
||||
delay(1); // Small delay to prevent tight spinning
|
||||
return HttpReadLoopResult::RETRY;
|
||||
}
|
||||
|
||||
class HttpRequestComponent;
|
||||
|
||||
class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
@@ -88,6 +163,33 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
int status_code;
|
||||
uint32_t duration_ms;
|
||||
|
||||
/**
|
||||
* @brief Read data from the HTTP response body.
|
||||
*
|
||||
* WARNING: These semantics differ from BSD sockets!
|
||||
* BSD sockets: 0 = EOF (connection closed)
|
||||
* This method: 0 = no data yet OR all content read, negative = error/closed
|
||||
*
|
||||
* @param buf Buffer to read data into
|
||||
* @param max_len Maximum number of bytes to read
|
||||
* @return
|
||||
* - > 0: Number of bytes read successfully
|
||||
* - 0: No data available yet OR all content read
|
||||
* (check get_bytes_read() >= content_length to distinguish)
|
||||
* - HTTP_ERROR_CONNECTION_CLOSED (-1): Connection closed prematurely
|
||||
* - < -1: Other error (platform-specific error code)
|
||||
*
|
||||
* Platform notes:
|
||||
* - ESP-IDF: blocking read, 0 only when all content read
|
||||
* - Arduino: non-blocking, 0 can mean "no data yet" or "all content read"
|
||||
*
|
||||
* Use get_bytes_read() and content_length to track progress.
|
||||
* When get_bytes_read() >= content_length, all data has been received.
|
||||
*
|
||||
* IMPORTANT: Do not use raw return values directly. Use these helpers:
|
||||
* - http_read_loop_result(): for loops with per-chunk processing
|
||||
* - http_read_fully(): for simple "read N bytes" operations
|
||||
*/
|
||||
virtual int read(uint8_t *buf, size_t max_len) = 0;
|
||||
virtual void end() = 0;
|
||||
|
||||
@@ -110,6 +212,38 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
std::map<std::string, std::list<std::string>> response_headers_{};
|
||||
};
|
||||
|
||||
/// Read data from HTTP container into buffer with timeout handling
|
||||
/// Handles feed_wdt, yield, and timeout checking internally
|
||||
/// @param container The HTTP container to read from
|
||||
/// @param buffer Buffer to read into
|
||||
/// @param total_size Total bytes to read
|
||||
/// @param chunk_size Maximum bytes per read call
|
||||
/// @param timeout_ms Read timeout in milliseconds
|
||||
/// @return HttpReadResult with status and error_code on failure
|
||||
inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size,
|
||||
uint32_t timeout_ms) {
|
||||
size_t read_index = 0;
|
||||
uint32_t last_data_time = millis();
|
||||
|
||||
while (read_index < total_size) {
|
||||
int read_bytes_or_error = container->read(buffer + read_index, std::min(chunk_size, total_size - read_index));
|
||||
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms);
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result == HttpReadLoopResult::ERROR)
|
||||
return {HttpReadStatus::ERROR, read_bytes_or_error};
|
||||
if (result == HttpReadLoopResult::TIMEOUT)
|
||||
return {HttpReadStatus::TIMEOUT, 0};
|
||||
|
||||
read_index += read_bytes_or_error;
|
||||
}
|
||||
return {HttpReadStatus::OK, 0};
|
||||
}
|
||||
|
||||
class HttpRequestResponseTrigger : public Trigger<std::shared_ptr<HttpContainer>, std::string &> {
|
||||
public:
|
||||
void process(const std::shared_ptr<HttpContainer> &container, std::string &response_body) {
|
||||
@@ -124,6 +258,7 @@ class HttpRequestComponent : public Component {
|
||||
|
||||
void set_useragent(const char *useragent) { this->useragent_ = useragent; }
|
||||
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
|
||||
uint32_t get_timeout() const { return this->timeout_; }
|
||||
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
|
||||
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
|
||||
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
|
||||
@@ -249,15 +384,21 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
uint8_t *buf = allocator.allocate(max_length);
|
||||
if (buf != nullptr) {
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see top of this file
|
||||
// Use http_read_loop_result() helper instead of checking return values directly
|
||||
size_t read_index = 0;
|
||||
uint32_t last_data_time = millis();
|
||||
const uint32_t read_timeout = this->parent_->get_timeout();
|
||||
while (container->get_bytes_read() < max_length) {
|
||||
int read = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
|
||||
if (read <= 0) {
|
||||
break;
|
||||
}
|
||||
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
read_index += read;
|
||||
auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != HttpReadLoopResult::DATA)
|
||||
break; // ERROR or TIMEOUT
|
||||
read_index += read_or_error;
|
||||
}
|
||||
response_body.reserve(read_index);
|
||||
response_body.assign((char *) buf, read_index);
|
||||
|
||||
@@ -139,6 +139,23 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
|
||||
return container;
|
||||
}
|
||||
|
||||
// Arduino HTTP read implementation
|
||||
//
|
||||
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
|
||||
//
|
||||
// Arduino's WiFiClient is inherently non-blocking - available() returns 0 when
|
||||
// no data is ready. We use connected() to distinguish "no data yet" from
|
||||
// "connection closed".
|
||||
//
|
||||
// WiFiClient behavior:
|
||||
// available() > 0: data ready to read
|
||||
// available() == 0 && connected(): no data yet, still connected
|
||||
// available() == 0 && !connected(): connection closed
|
||||
//
|
||||
// We normalize to HttpContainer::read() contract (NOT BSD socket semantics!):
|
||||
// > 0: bytes read
|
||||
// 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF!
|
||||
// < 0: error/connection closed <-- connection closed returns -1, not 0
|
||||
int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
const uint32_t start = millis();
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
@@ -146,7 +163,7 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
WiFiClient *stream_ptr = this->client_.getStreamPtr();
|
||||
if (stream_ptr == nullptr) {
|
||||
ESP_LOGE(TAG, "Stream pointer vanished!");
|
||||
return -1;
|
||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
||||
}
|
||||
|
||||
int available_data = stream_ptr->available();
|
||||
@@ -154,7 +171,15 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
|
||||
if (bufsize == 0) {
|
||||
this->duration_ms += (millis() - start);
|
||||
return 0;
|
||||
// Check if we've read all expected content
|
||||
if (this->bytes_read_ >= this->content_length) {
|
||||
return 0; // All content read successfully
|
||||
}
|
||||
// No data available - check if connection is still open
|
||||
if (!stream_ptr->connected()) {
|
||||
return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed prematurely
|
||||
}
|
||||
return 0; // No data yet, caller should retry
|
||||
}
|
||||
|
||||
App.feed_wdt();
|
||||
|
||||
@@ -89,7 +89,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
||||
config.max_redirection_count = this->redirect_limit_;
|
||||
config.auth_type = HTTP_AUTH_TYPE_BASIC;
|
||||
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
|
||||
if (secure) {
|
||||
if (secure && this->verify_ssl_) {
|
||||
config.crt_bundle_attach = esp_crt_bundle_attach;
|
||||
}
|
||||
#endif
|
||||
@@ -209,26 +209,57 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
||||
return container;
|
||||
}
|
||||
|
||||
// ESP-IDF HTTP read implementation (blocking mode)
|
||||
//
|
||||
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
|
||||
//
|
||||
// esp_http_client_read() in blocking mode returns:
|
||||
// > 0: bytes read
|
||||
// 0: connection closed (end of stream)
|
||||
// < 0: error
|
||||
//
|
||||
// We normalize to HttpContainer::read() contract:
|
||||
// > 0: bytes read
|
||||
// 0: no data yet / all content read (caller should check bytes_read vs content_length)
|
||||
// < 0: error/connection closed
|
||||
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
||||
const uint32_t start = millis();
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
|
||||
this->feed_wdt();
|
||||
int read_len = esp_http_client_read(this->client_, (char *) buf, max_len);
|
||||
this->feed_wdt();
|
||||
if (read_len > 0) {
|
||||
this->bytes_read_ += read_len;
|
||||
// Check if we've already read all expected content
|
||||
if (this->bytes_read_ >= this->content_length) {
|
||||
return 0; // All content read successfully
|
||||
}
|
||||
|
||||
this->feed_wdt();
|
||||
int read_len_or_error = esp_http_client_read(this->client_, (char *) buf, max_len);
|
||||
this->feed_wdt();
|
||||
|
||||
this->duration_ms += (millis() - start);
|
||||
|
||||
return read_len;
|
||||
if (read_len_or_error > 0) {
|
||||
this->bytes_read_ += read_len_or_error;
|
||||
return read_len_or_error;
|
||||
}
|
||||
|
||||
// Connection closed by server before all content received
|
||||
if (read_len_or_error == 0) {
|
||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
||||
}
|
||||
|
||||
// Negative value - error, return the actual error code for debugging
|
||||
return read_len_or_error;
|
||||
}
|
||||
|
||||
void HttpContainerIDF::end() {
|
||||
if (this->client_ == nullptr) {
|
||||
return; // Already cleaned up
|
||||
}
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
|
||||
esp_http_client_close(this->client_);
|
||||
esp_http_client_cleanup(this->client_);
|
||||
this->client_ = nullptr;
|
||||
}
|
||||
|
||||
void HttpContainerIDF::feed_wdt() {
|
||||
|
||||
@@ -34,6 +34,7 @@ class HttpRequestIDF : public HttpRequestComponent {
|
||||
|
||||
void set_buffer_size_rx(uint16_t buffer_size_rx) { this->buffer_size_rx_ = buffer_size_rx; }
|
||||
void set_buffer_size_tx(uint16_t buffer_size_tx) { this->buffer_size_tx_ = buffer_size_tx; }
|
||||
void set_verify_ssl(bool verify_ssl) { this->verify_ssl_ = verify_ssl; }
|
||||
|
||||
protected:
|
||||
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
|
||||
@@ -42,6 +43,7 @@ class HttpRequestIDF : public HttpRequestComponent {
|
||||
// if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
|
||||
uint16_t buffer_size_rx_{};
|
||||
uint16_t buffer_size_tx_{};
|
||||
bool verify_ssl_{true};
|
||||
|
||||
/// @brief Monitors the http client events to gather response headers
|
||||
static esp_err_t http_event_handler(esp_http_client_event_t *evt);
|
||||
|
||||
@@ -115,39 +115,47 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
return error_code;
|
||||
}
|
||||
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
|
||||
// Use http_read_loop_result() helper instead of checking return values directly
|
||||
uint32_t last_data_time = millis();
|
||||
const uint32_t read_timeout = this->parent_->get_timeout();
|
||||
|
||||
while (container->get_bytes_read() < container->content_length) {
|
||||
// read a maximum of chunk_size bytes into buf. (real read size returned)
|
||||
int bufsize = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
|
||||
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", container->get_bytes_read(),
|
||||
container->content_length, bufsize);
|
||||
// read a maximum of chunk_size bytes into buf. (real read size returned, or negative error code)
|
||||
int bufsize_or_error = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
|
||||
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize_or_error = %i", container->get_bytes_read(),
|
||||
container->content_length, bufsize_or_error);
|
||||
|
||||
// feed watchdog and give other tasks a chance to run
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
// Exit loop if no data available (stream closed or end of data)
|
||||
if (bufsize <= 0) {
|
||||
if (bufsize < 0) {
|
||||
ESP_LOGE(TAG, "Stream closed with error");
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return OTA_CONNECTION_ERROR;
|
||||
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout);
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != HttpReadLoopResult::DATA) {
|
||||
if (result == HttpReadLoopResult::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading data");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading data: %d", bufsize_or_error);
|
||||
}
|
||||
// bufsize == 0: no more data available, exit loop
|
||||
break;
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return OTA_CONNECTION_ERROR;
|
||||
}
|
||||
|
||||
if (bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
|
||||
// At this point bufsize_or_error > 0, so it's a valid size
|
||||
if (bufsize_or_error <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
|
||||
// add read bytes to MD5
|
||||
md5_receive.add(buf, bufsize);
|
||||
md5_receive.add(buf, bufsize_or_error);
|
||||
|
||||
// write bytes to OTA backend
|
||||
this->update_started_ = true;
|
||||
error_code = backend->write(buf, bufsize);
|
||||
error_code = backend->write(buf, bufsize_or_error);
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
// error code explanation available at
|
||||
// https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h
|
||||
ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code,
|
||||
container->get_bytes_read() - bufsize, container->content_length);
|
||||
container->get_bytes_read() - bufsize_or_error, container->content_length);
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return error_code;
|
||||
}
|
||||
@@ -244,19 +252,19 @@ bool OtaHttpRequestComponent::http_get_md5_() {
|
||||
}
|
||||
|
||||
this->md5_expected_.resize(MD5_SIZE);
|
||||
int read_len = 0;
|
||||
while (container->get_bytes_read() < MD5_SIZE) {
|
||||
read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
|
||||
if (read_len <= 0) {
|
||||
break;
|
||||
}
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
}
|
||||
auto result = http_read_fully(container.get(), (uint8_t *) this->md5_expected_.data(), MD5_SIZE, MD5_SIZE,
|
||||
this->parent_->get_timeout());
|
||||
container->end();
|
||||
|
||||
ESP_LOGV(TAG, "Read len: %u, MD5 expected: %u", read_len, MD5_SIZE);
|
||||
return read_len == MD5_SIZE;
|
||||
if (result.status != HttpReadStatus::OK) {
|
||||
if (result.status == HttpReadStatus::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading MD5");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading MD5: %d", result.error_code);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaHttpRequestComponent::validate_url_(const std::string &url) {
|
||||
|
||||
@@ -11,7 +11,12 @@ namespace http_request {
|
||||
|
||||
// The update function runs in a task only on ESP32s.
|
||||
#ifdef USE_ESP32
|
||||
#define UPDATE_RETURN vTaskDelete(nullptr) // Delete the current update task
|
||||
// vTaskDelete doesn't return, but clang-tidy doesn't know that
|
||||
#define UPDATE_RETURN \
|
||||
do { \
|
||||
vTaskDelete(nullptr); \
|
||||
__builtin_unreachable(); \
|
||||
} while (0)
|
||||
#else
|
||||
#define UPDATE_RETURN return
|
||||
#endif
|
||||
@@ -70,19 +75,21 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
|
||||
size_t read_index = 0;
|
||||
while (container->get_bytes_read() < container->content_length) {
|
||||
int read_bytes = container->read(data + read_index, MAX_READ_SIZE);
|
||||
|
||||
yield();
|
||||
|
||||
if (read_bytes <= 0) {
|
||||
// Network error or connection closed - break to avoid infinite loop
|
||||
break;
|
||||
auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
|
||||
this_update->request_parent_->get_timeout());
|
||||
if (read_result.status != HttpReadStatus::OK) {
|
||||
if (read_result.status == HttpReadStatus::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading manifest");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
|
||||
}
|
||||
|
||||
read_index += read_bytes;
|
||||
// Defer to main loop to avoid race condition on component_state_ read-modify-write
|
||||
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to read manifest")); });
|
||||
allocator.deallocate(data, container->content_length);
|
||||
container->end();
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
size_t read_index = container->get_bytes_read();
|
||||
|
||||
bool valid = false;
|
||||
{ // Ensures the response string falls out of scope and deallocates before the task ends
|
||||
|
||||
@@ -223,7 +223,7 @@ async def to_code(config):
|
||||
var = cg.Pvariable(config[CONF_ID], rhs)
|
||||
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
if init_sequences := config.get(CONF_INIT_SEQUENCE):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from esphome import codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_OPTIONS
|
||||
|
||||
@@ -24,6 +25,34 @@ from .label import CONF_LABEL
|
||||
CONF_DROPDOWN = "dropdown"
|
||||
CONF_DROPDOWN_LIST = "dropdown_list"
|
||||
|
||||
# Example valid dropdown symbol (left arrow) for error messages
|
||||
EXAMPLE_DROPDOWN_SYMBOL = "\U00002190" # ←
|
||||
|
||||
|
||||
def dropdown_symbol_validator(value):
|
||||
"""
|
||||
Validate that the dropdown symbol is a single Unicode character
|
||||
with a codepoint of 0x100 (256) or greater.
|
||||
This is required because LVGL uses codepoints below 0x100 for internal symbols.
|
||||
"""
|
||||
value = cv.string(value)
|
||||
# len(value) counts Unicode code points, not grapheme clusters or bytes
|
||||
if len(value) != 1:
|
||||
raise cv.Invalid(
|
||||
f"Dropdown symbol must be a single character, got '{value}' with length {len(value)}"
|
||||
)
|
||||
codepoint = ord(value)
|
||||
if codepoint < 0x100:
|
||||
# Format the example symbol as a Unicode escape for the error message
|
||||
example_escape = f"\\U{ord(EXAMPLE_DROPDOWN_SYMBOL):08X}"
|
||||
raise cv.Invalid(
|
||||
f"Dropdown symbol must have a Unicode codepoint of 0x100 (256) or greater. "
|
||||
f"'{value}' has codepoint {codepoint} (0x{codepoint:X}). "
|
||||
f"Use a character like '{example_escape}' ({EXAMPLE_DROPDOWN_SYMBOL}) or other Unicode symbols with codepoint >= 0x100."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
lv_dropdown_t = LvSelect("LvDropdownType", parents=(LvCompound,))
|
||||
|
||||
lv_dropdown_list_t = LvType("lv_dropdown_list_t")
|
||||
@@ -33,7 +62,7 @@ dropdown_list_spec = WidgetType(
|
||||
|
||||
DROPDOWN_BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_SYMBOL): lv_text,
|
||||
cv.Optional(CONF_SYMBOL): dropdown_symbol_validator,
|
||||
cv.Exclusive(CONF_SELECTED_INDEX, CONF_SELECTED_TEXT): lv_int,
|
||||
cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text,
|
||||
cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec.parts),
|
||||
@@ -70,7 +99,7 @@ class DropdownType(WidgetType):
|
||||
if options := config.get(CONF_OPTIONS):
|
||||
lv_add(w.var.set_options(options))
|
||||
if symbol := config.get(CONF_SYMBOL):
|
||||
lv.dropdown_set_symbol(w.var.obj, await lv_text.process(symbol))
|
||||
lv.dropdown_set_symbol(w.var.obj, cg.safe_exp(symbol))
|
||||
if (selected := config.get(CONF_SELECTED_INDEX)) is not None:
|
||||
value = await lv_int.process(selected)
|
||||
lv_add(w.var.set_selected_index(value, literal("LV_ANIM_OFF")))
|
||||
|
||||
@@ -29,7 +29,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await display.register_display(var, config)
|
||||
|
||||
cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))
|
||||
|
||||
@@ -86,7 +86,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await display.register_display(var, config)
|
||||
|
||||
cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))
|
||||
|
||||
@@ -260,7 +260,7 @@ async def to_code(config):
|
||||
cg.add(var.set_enable_pins(enable))
|
||||
|
||||
if CONF_SPI_ID in config:
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
sequence, madctl = model.get_sequence(config)
|
||||
cg.add(var.set_init_sequence(sequence))
|
||||
cg.add(var.set_madctl(madctl))
|
||||
|
||||
@@ -443,6 +443,4 @@ async def to_code(config):
|
||||
)
|
||||
cg.add(var.set_writer(lambda_))
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
# Displays are write-only, set the SPI device to write-only as well
|
||||
cg.add(var.set_write_only(True))
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
@@ -44,7 +44,7 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -161,7 +161,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
chip = DriverChip.chips[config[CONF_MODEL]]
|
||||
if chip.initsequence:
|
||||
|
||||
@@ -39,6 +39,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core", "@clydebarrow"]
|
||||
spi_ns = cg.esphome_ns.namespace("spi")
|
||||
@@ -448,9 +449,13 @@ def spi_device_schema(
|
||||
)
|
||||
|
||||
|
||||
async def register_spi_device(var, config):
|
||||
async def register_spi_device(
|
||||
var: cg.Pvariable, config: ConfigType, write_only: bool = False
|
||||
) -> None:
|
||||
parent = await cg.get_variable(config[CONF_SPI_ID])
|
||||
cg.add(var.set_spi_parent(parent))
|
||||
if write_only:
|
||||
cg.add(var.set_write_only(True))
|
||||
if cs_pin := config.get(CONF_CS_PIN):
|
||||
pin = await cg.gpio_pin_expression(cs_pin)
|
||||
cg.add(var.set_cs_pin(pin))
|
||||
|
||||
@@ -195,8 +195,11 @@ class SPIDelegateHw : public SPIDelegate {
|
||||
config.post_cb = nullptr;
|
||||
if (this->bit_order_ == BIT_ORDER_LSB_FIRST)
|
||||
config.flags |= SPI_DEVICE_BIT_LSBFIRST;
|
||||
if (this->write_only_)
|
||||
if (this->write_only_) {
|
||||
config.flags |= SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY;
|
||||
ESP_LOGD(TAG, "SPI device with CS pin %d using half-duplex mode (write-only)",
|
||||
Utility::get_pin_no(this->cs_pin_));
|
||||
}
|
||||
esp_err_t const err = spi_bus_add_device(this->channel_, &config, &this->handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Add device failed - err %X", err);
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ssd1306_base.setup_ssd1306(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ssd1322_base.setup_ssd1322(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ssd1325_base.setup_ssd1325(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ssd1327_base.setup_ssd1327(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ssd1331_base.setup_ssd1331(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ssd1351_base.setup_ssd1351(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await st7567_base.setup_st7567(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -173,7 +173,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
sequence = []
|
||||
for seq in config[CONF_INIT_SEQUENCE]:
|
||||
|
||||
@@ -99,7 +99,7 @@ async def to_code(config):
|
||||
config[CONF_INVERT_COLORS],
|
||||
)
|
||||
await setup_st7735(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -177,7 +177,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
cg.add(var.set_model_str(config[CONF_MODEL]))
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
if CONF_LAMBDA in config:
|
||||
lambda_ = await cg.process_lambda(
|
||||
|
||||
@@ -40,6 +40,9 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
||||
// Unsigned subtraction handles wraparound correctly, then cast to signed
|
||||
int32_t diff = static_cast<int32_t>(epoch - static_cast<uint32_t>(current_time));
|
||||
if (diff >= -1 && diff <= 1) {
|
||||
// Time is already synchronized, but still call callbacks so components
|
||||
// waiting for time sync (e.g., uptime timestamp sensor) can initialize
|
||||
this->time_sync_callback_.call();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ async def to_code(config):
|
||||
raise NotImplementedError()
|
||||
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -565,6 +565,11 @@ void WiFiComponent::start() {
|
||||
void WiFiComponent::restart_adapter() {
|
||||
ESP_LOGW(TAG, "Restarting adapter");
|
||||
this->wifi_mode_(false, {});
|
||||
// Clear error flag here because restart_adapter() enters COOLDOWN state,
|
||||
// and check_connecting_finished() is called after cooldown without going
|
||||
// through start_connecting() first. Without this clear, stale errors would
|
||||
// trigger spurious "failed (callback)" logs. The canonical clear location
|
||||
// is in start_connecting(); this is the only exception to that pattern.
|
||||
this->error_from_callback_ = false;
|
||||
}
|
||||
|
||||
@@ -618,8 +623,6 @@ void WiFiComponent::loop() {
|
||||
if (!this->is_connected()) {
|
||||
ESP_LOGW(TAG, "Connection lost; reconnecting");
|
||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
|
||||
// Clear error flag before reconnecting so first attempt is not seen as immediate failure
|
||||
this->error_from_callback_ = false;
|
||||
this->retry_connect();
|
||||
} else {
|
||||
this->status_clear_warning();
|
||||
@@ -963,6 +966,12 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
||||
ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden()));
|
||||
#endif
|
||||
|
||||
// Clear any stale error from previous connection attempt.
|
||||
// This is the canonical location for clearing the flag since all connection
|
||||
// attempts go through start_connecting(). The only other clear is in
|
||||
// restart_adapter() which enters COOLDOWN without calling start_connecting().
|
||||
this->error_from_callback_ = false;
|
||||
|
||||
if (!this->wifi_sta_connect_(ap)) {
|
||||
ESP_LOGE(TAG, "wifi_sta_connect_ failed");
|
||||
// Enter cooldown to allow WiFi hardware to stabilize
|
||||
@@ -1068,7 +1077,6 @@ void WiFiComponent::enable() {
|
||||
return;
|
||||
|
||||
ESP_LOGD(TAG, "Enabling");
|
||||
this->error_from_callback_ = false;
|
||||
this->state_ = WIFI_COMPONENT_STATE_OFF;
|
||||
this->start();
|
||||
}
|
||||
@@ -1329,11 +1337,6 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
|
||||
// Reset to initial phase on successful connection (don't log transition, just reset state)
|
||||
this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT;
|
||||
this->num_retried_ = 0;
|
||||
// Ensure next connection attempt does not inherit error state
|
||||
// so when WiFi disconnects later we start fresh and don't see
|
||||
// the first connection as a failure.
|
||||
this->error_from_callback_ = false;
|
||||
|
||||
if (this->has_ap()) {
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
if (this->is_captive_portal_active_()) {
|
||||
@@ -1844,8 +1847,6 @@ void WiFiComponent::retry_connect() {
|
||||
this->advance_to_next_target_or_increment_retry_();
|
||||
}
|
||||
|
||||
this->error_from_callback_ = false;
|
||||
|
||||
yield();
|
||||
// Check if we have a valid target before building params
|
||||
// After exhausting all networks in a phase, selected_sta_index_ may be -1
|
||||
@@ -2171,7 +2172,6 @@ void WiFiComponent::process_roaming_scan_() {
|
||||
this->roaming_state_ = RoamingState::CONNECTING;
|
||||
|
||||
// Connect directly - wifi_sta_connect_ handles disconnect internally
|
||||
this->error_from_callback_ = false;
|
||||
this->start_connecting(roam_params);
|
||||
}
|
||||
|
||||
|
||||
@@ -827,16 +827,17 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
}
|
||||
|
||||
uint16_t number = it.number;
|
||||
auto records = std::make_unique<wifi_ap_record_t[]>(number);
|
||||
err = esp_wifi_scan_get_ap_records(&number, records.get());
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
scan_result_.init(number);
|
||||
for (int i = 0; i < number; i++) {
|
||||
auto &record = records[i];
|
||||
|
||||
// Process one record at a time to avoid large buffer allocation
|
||||
wifi_ap_record_t record;
|
||||
for (uint16_t i = 0; i < number; i++) {
|
||||
err = esp_wifi_scan_get_ap_record(&record);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_record failed: %s", esp_err_to_name(err));
|
||||
esp_wifi_clear_ap_list(); // Free remaining records not yet retrieved
|
||||
break;
|
||||
}
|
||||
bssid_t bssid;
|
||||
std::copy(record.bssid, record.bssid + 6, bssid.begin());
|
||||
std::string ssid(reinterpret_cast<const char *>(record.ssid));
|
||||
|
||||
@@ -460,13 +460,15 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
|
||||
listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid);
|
||||
}
|
||||
#endif
|
||||
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
|
||||
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
|
||||
// For static IP configurations, GOT_IP event may not fire, so set connected state here
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
|
||||
s_sta_state = LTWiFiSTAState::CONNECTED;
|
||||
#ifdef USE_WIFI_IP_STATE_LISTENERS
|
||||
for (auto *listener : this->ip_state_listeners_) {
|
||||
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2026.1.0"
|
||||
__version__ = "2026.1.1"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
Reference in New Issue
Block a user