Compare commits
24 Commits
ota_base_e
...
memory
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1553807f7 | ||
|
|
797d4929ab | ||
|
|
ba5bb9dfa7 | ||
|
|
dd49d832c4 | ||
|
|
5004f44f65 | ||
|
|
bc9c4a8b8e | ||
|
|
6f05ee7427 | ||
|
|
f3523a96c9 | ||
|
|
06957d9895 | ||
|
|
1f361b07d1 | ||
|
|
40d9c0a3db | ||
|
|
548cd39496 | ||
|
|
85049611c3 | ||
|
|
b8a75bc925 | ||
|
|
fae96e279c | ||
|
|
2fb23becec | ||
|
|
095acce3e2 | ||
|
|
5fa9d22c5d | ||
|
|
785b14ac84 | ||
|
|
84ab758b22 | ||
|
|
03566c34ed | ||
|
|
6a096c1d5a | ||
|
|
04a46de237 | ||
|
|
0083abe3b5 |
@@ -498,6 +498,7 @@ esphome/components/voice_assistant/* @jesserockz @kahrendt
|
||||
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
|
||||
esphome/components/watchdog/* @oarcher
|
||||
esphome/components/waveshare_epaper/* @clydebarrow
|
||||
esphome/components/web_server/ota/* @esphome/core
|
||||
esphome/components/web_server_base/* @OttoWinter
|
||||
esphome/components/web_server_idf/* @dentra
|
||||
esphome/components/weikai/* @DrCoolZic
|
||||
|
||||
@@ -458,6 +458,13 @@ def command_vscode(args):
|
||||
|
||||
|
||||
def command_compile(args, config):
|
||||
# Set memory analysis options in config
|
||||
if args.analyze_memory:
|
||||
config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True
|
||||
|
||||
if args.memory_report:
|
||||
config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report
|
||||
|
||||
exit_code = write_cpp(config)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
@@ -837,6 +844,17 @@ def parse_args(argv):
|
||||
help="Only generate source code, do not compile.",
|
||||
action="store_true",
|
||||
)
|
||||
parser_compile.add_argument(
|
||||
"--analyze-memory",
|
||||
help="Analyze and display memory usage by component after compilation.",
|
||||
action="store_true",
|
||||
)
|
||||
parser_compile.add_argument(
|
||||
"--memory-report",
|
||||
help="Save memory analysis report to a file (supports .json or .txt).",
|
||||
type=str,
|
||||
metavar="FILE",
|
||||
)
|
||||
|
||||
parser_upload = subparsers.add_parser(
|
||||
"upload",
|
||||
|
||||
1550
esphome/analyze_memory.py
Normal file
1550
esphome/analyze_memory.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1687,7 +1687,9 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c
|
||||
// O(n) but optimized for RAM and not performance.
|
||||
for (auto &item : items) {
|
||||
if (item.entity == entity && item.message_type == message_type) {
|
||||
// Update the existing item with the new creator
|
||||
// Clean up old creator before replacing
|
||||
item.creator.cleanup(message_type);
|
||||
// Move assign the new creator
|
||||
item.creator = std::move(creator);
|
||||
return;
|
||||
}
|
||||
@@ -1730,11 +1732,11 @@ void APIConnection::process_batch_() {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t num_items = this->deferred_batch_.items.size();
|
||||
size_t num_items = this->deferred_batch_.size();
|
||||
|
||||
// Fast path for single message - allocate exact size needed
|
||||
if (num_items == 1) {
|
||||
const auto &item = this->deferred_batch_.items[0];
|
||||
const auto &item = this->deferred_batch_[0];
|
||||
|
||||
// Let the creator calculate size and encode if it fits
|
||||
uint16_t payload_size =
|
||||
@@ -1764,7 +1766,8 @@ void APIConnection::process_batch_() {
|
||||
|
||||
// Pre-calculate exact buffer size needed based on message types
|
||||
uint32_t total_estimated_size = 0;
|
||||
for (const auto &item : this->deferred_batch_.items) {
|
||||
for (size_t i = 0; i < this->deferred_batch_.size(); i++) {
|
||||
const auto &item = this->deferred_batch_[i];
|
||||
total_estimated_size += get_estimated_message_size(item.message_type);
|
||||
}
|
||||
|
||||
@@ -1785,7 +1788,8 @@ void APIConnection::process_batch_() {
|
||||
uint32_t current_offset = 0;
|
||||
|
||||
// Process items and encode directly to buffer
|
||||
for (const auto &item : this->deferred_batch_.items) {
|
||||
for (size_t i = 0; i < this->deferred_batch_.size(); i++) {
|
||||
const auto &item = this->deferred_batch_[i];
|
||||
// Try to encode message
|
||||
// The creator will calculate overhead to determine if the message fits
|
||||
uint16_t payload_size = item.creator(item.entity, this, remaining_size, false, item.message_type);
|
||||
@@ -1840,17 +1844,15 @@ void APIConnection::process_batch_() {
|
||||
// Log messages after send attempt for VV debugging
|
||||
// It's safe to use the buffer for logging at this point regardless of send result
|
||||
for (size_t i = 0; i < items_processed; i++) {
|
||||
const auto &item = this->deferred_batch_.items[i];
|
||||
const auto &item = this->deferred_batch_[i];
|
||||
this->log_batch_item_(item);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Handle remaining items more efficiently
|
||||
if (items_processed < this->deferred_batch_.items.size()) {
|
||||
// Remove processed items from the beginning
|
||||
this->deferred_batch_.items.erase(this->deferred_batch_.items.begin(),
|
||||
this->deferred_batch_.items.begin() + items_processed);
|
||||
|
||||
if (items_processed < this->deferred_batch_.size()) {
|
||||
// Remove processed items from the beginning with proper cleanup
|
||||
this->deferred_batch_.remove_front(items_processed);
|
||||
// Reschedule for remaining items
|
||||
this->schedule_batch_();
|
||||
} else {
|
||||
@@ -1861,23 +1863,16 @@ void APIConnection::process_batch_() {
|
||||
|
||||
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single, uint16_t message_type) const {
|
||||
if (has_tagged_string_ptr_()) {
|
||||
// Handle string-based messages
|
||||
switch (message_type) {
|
||||
#ifdef USE_EVENT
|
||||
case EventResponse::MESSAGE_TYPE: {
|
||||
auto *e = static_cast<event::Event *>(entity);
|
||||
return APIConnection::try_send_event_response(e, *get_string_ptr_(), conn, remaining_size, is_single);
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
// Should not happen, return 0 to indicate no message
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
// Function pointer case
|
||||
return data_.ptr(entity, conn, remaining_size, is_single);
|
||||
// Special case: EventResponse uses string pointer
|
||||
if (message_type == EventResponse::MESSAGE_TYPE) {
|
||||
auto *e = static_cast<event::Event *>(entity);
|
||||
return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single);
|
||||
}
|
||||
#endif
|
||||
|
||||
// All other message types use function pointers
|
||||
return data_.function_ptr(entity, conn, remaining_size, is_single);
|
||||
}
|
||||
|
||||
uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
|
||||
@@ -451,96 +451,53 @@ class APIConnection : public APIServerConnection {
|
||||
// Function pointer type for message encoding
|
||||
using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single);
|
||||
|
||||
// Optimized MessageCreator class using tagged pointer
|
||||
class MessageCreator {
|
||||
// Ensure pointer alignment allows LSB tagging
|
||||
static_assert(alignof(std::string *) > 1, "String pointer alignment must be > 1 for LSB tagging");
|
||||
|
||||
public:
|
||||
// Constructor for function pointer
|
||||
MessageCreator(MessageCreatorPtr ptr) {
|
||||
// Function pointers are always aligned, so LSB is 0
|
||||
data_.ptr = ptr;
|
||||
}
|
||||
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
|
||||
|
||||
// Constructor for string state capture
|
||||
explicit MessageCreator(const std::string &str_value) {
|
||||
// Allocate string and tag the pointer
|
||||
auto *str = new std::string(str_value);
|
||||
// Set LSB to 1 to indicate string pointer
|
||||
data_.tagged = reinterpret_cast<uintptr_t>(str) | 1;
|
||||
}
|
||||
explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); }
|
||||
|
||||
// Destructor
|
||||
~MessageCreator() {
|
||||
if (has_tagged_string_ptr_()) {
|
||||
delete get_string_ptr_();
|
||||
}
|
||||
}
|
||||
// No destructor - cleanup must be called explicitly with message_type
|
||||
|
||||
// Copy constructor
|
||||
MessageCreator(const MessageCreator &other) {
|
||||
if (other.has_tagged_string_ptr_()) {
|
||||
auto *str = new std::string(*other.get_string_ptr_());
|
||||
data_.tagged = reinterpret_cast<uintptr_t>(str) | 1;
|
||||
} else {
|
||||
data_ = other.data_;
|
||||
}
|
||||
}
|
||||
// Delete copy operations - MessageCreator should only be moved
|
||||
MessageCreator(const MessageCreator &other) = delete;
|
||||
MessageCreator &operator=(const MessageCreator &other) = delete;
|
||||
|
||||
// Move constructor
|
||||
MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.ptr = nullptr; }
|
||||
|
||||
// Assignment operators (needed for batch deduplication)
|
||||
MessageCreator &operator=(const MessageCreator &other) {
|
||||
if (this != &other) {
|
||||
// Clean up current string data if needed
|
||||
if (has_tagged_string_ptr_()) {
|
||||
delete get_string_ptr_();
|
||||
}
|
||||
// Copy new data
|
||||
if (other.has_tagged_string_ptr_()) {
|
||||
auto *str = new std::string(*other.get_string_ptr_());
|
||||
data_.tagged = reinterpret_cast<uintptr_t>(str) | 1;
|
||||
} else {
|
||||
data_ = other.data_;
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.function_ptr = nullptr; }
|
||||
|
||||
// Move assignment
|
||||
MessageCreator &operator=(MessageCreator &&other) noexcept {
|
||||
if (this != &other) {
|
||||
// Clean up current string data if needed
|
||||
if (has_tagged_string_ptr_()) {
|
||||
delete get_string_ptr_();
|
||||
}
|
||||
// Move data
|
||||
// IMPORTANT: Caller must ensure cleanup() was called if this contains a string!
|
||||
// In our usage, this happens in add_item() deduplication and vector::erase()
|
||||
data_ = other.data_;
|
||||
// Reset other to safe state
|
||||
other.data_.ptr = nullptr;
|
||||
other.data_.function_ptr = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Call operator - now accepts message_type as parameter
|
||||
// Call operator - uses message_type to determine union type
|
||||
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
|
||||
uint16_t message_type) const;
|
||||
|
||||
private:
|
||||
// Check if this contains a string pointer
|
||||
bool has_tagged_string_ptr_() const { return (data_.tagged & 1) != 0; }
|
||||
|
||||
// Get the actual string pointer (clears the tag bit)
|
||||
std::string *get_string_ptr_() const {
|
||||
// NOLINTNEXTLINE(performance-no-int-to-ptr)
|
||||
return reinterpret_cast<std::string *>(data_.tagged & ~uintptr_t(1));
|
||||
// Manual cleanup method - must be called before destruction for string types
|
||||
void cleanup(uint16_t message_type) {
|
||||
#ifdef USE_EVENT
|
||||
if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) {
|
||||
delete data_.string_ptr;
|
||||
data_.string_ptr = nullptr;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
union {
|
||||
MessageCreatorPtr ptr;
|
||||
uintptr_t tagged;
|
||||
} data_; // 4 bytes on 32-bit
|
||||
private:
|
||||
union Data {
|
||||
MessageCreatorPtr function_ptr;
|
||||
std::string *string_ptr;
|
||||
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before
|
||||
};
|
||||
|
||||
// Generic batching mechanism for both state updates and entity info
|
||||
@@ -558,20 +515,46 @@ class APIConnection : public APIServerConnection {
|
||||
std::vector<BatchItem> items;
|
||||
uint32_t batch_start_time{0};
|
||||
|
||||
private:
|
||||
// Helper to cleanup items from the beginning
|
||||
void cleanup_items_(size_t count) {
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
items[i].creator.cleanup(items[i].message_type);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
DeferredBatch() {
|
||||
// Pre-allocate capacity for typical batch sizes to avoid reallocation
|
||||
items.reserve(8);
|
||||
}
|
||||
|
||||
~DeferredBatch() {
|
||||
// Ensure cleanup of any remaining items
|
||||
clear();
|
||||
}
|
||||
|
||||
// Add item to the batch
|
||||
void add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type);
|
||||
// Add item to the front of the batch (for high priority messages like ping)
|
||||
void add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type);
|
||||
|
||||
// Clear all items with proper cleanup
|
||||
void clear() {
|
||||
cleanup_items_(items.size());
|
||||
items.clear();
|
||||
batch_start_time = 0;
|
||||
}
|
||||
|
||||
// Remove processed items from the front with proper cleanup
|
||||
void remove_front(size_t count) {
|
||||
cleanup_items_(count);
|
||||
items.erase(items.begin(), items.begin() + count);
|
||||
}
|
||||
|
||||
bool empty() const { return items.empty(); }
|
||||
size_t size() const { return items.size(); }
|
||||
const BatchItem &operator[](size_t index) const { return items[index]; }
|
||||
};
|
||||
|
||||
// DeferredBatch here (16 bytes, 4-byte aligned)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "api_pb2.h"
|
||||
#include "api_pb2_size.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -3510,7 +3511,7 @@ void SubscribeLogsResponse::dump_to(std::string &out) const {
|
||||
out.append("\n");
|
||||
|
||||
out.append(" message: ");
|
||||
out.append("'").append(this->message).append("'");
|
||||
out.append(format_hex_pretty(this->message));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" send_failed: ");
|
||||
@@ -3538,7 +3539,7 @@ void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const {
|
||||
__attribute__((unused)) char buffer[64];
|
||||
out.append("NoiseEncryptionSetKeyRequest {\n");
|
||||
out.append(" key: ");
|
||||
out.append("'").append(this->key).append("'");
|
||||
out.append(format_hex_pretty(this->key));
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
@@ -4284,7 +4285,7 @@ void CameraImageResponse::dump_to(std::string &out) const {
|
||||
out.append("\n");
|
||||
|
||||
out.append(" data: ");
|
||||
out.append("'").append(this->data).append("'");
|
||||
out.append(format_hex_pretty(this->data));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" done: ");
|
||||
@@ -6811,7 +6812,7 @@ void BluetoothServiceData::dump_to(std::string &out) const {
|
||||
}
|
||||
|
||||
out.append(" data: ");
|
||||
out.append("'").append(this->data).append("'");
|
||||
out.append(format_hex_pretty(this->data));
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
@@ -6894,7 +6895,7 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const {
|
||||
out.append("\n");
|
||||
|
||||
out.append(" name: ");
|
||||
out.append("'").append(this->name).append("'");
|
||||
out.append(format_hex_pretty(this->name));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" rssi: ");
|
||||
@@ -6987,7 +6988,7 @@ void BluetoothLERawAdvertisement::dump_to(std::string &out) const {
|
||||
out.append("\n");
|
||||
|
||||
out.append(" data: ");
|
||||
out.append("'").append(this->data).append("'");
|
||||
out.append(format_hex_pretty(this->data));
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
@@ -7514,7 +7515,7 @@ void BluetoothGATTReadResponse::dump_to(std::string &out) const {
|
||||
out.append("\n");
|
||||
|
||||
out.append(" data: ");
|
||||
out.append("'").append(this->data).append("'");
|
||||
out.append(format_hex_pretty(this->data));
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
@@ -7578,7 +7579,7 @@ void BluetoothGATTWriteRequest::dump_to(std::string &out) const {
|
||||
out.append("\n");
|
||||
|
||||
out.append(" data: ");
|
||||
out.append("'").append(this->data).append("'");
|
||||
out.append(format_hex_pretty(this->data));
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
@@ -7670,7 +7671,7 @@ void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const {
|
||||
out.append("\n");
|
||||
|
||||
out.append(" data: ");
|
||||
out.append("'").append(this->data).append("'");
|
||||
out.append(format_hex_pretty(this->data));
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
@@ -7772,7 +7773,7 @@ void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const {
|
||||
out.append("\n");
|
||||
|
||||
out.append(" data: ");
|
||||
out.append("'").append(this->data).append("'");
|
||||
out.append(format_hex_pretty(this->data));
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
@@ -8492,7 +8493,7 @@ void VoiceAssistantAudio::dump_to(std::string &out) const {
|
||||
__attribute__((unused)) char buffer[64];
|
||||
out.append("VoiceAssistantAudio {\n");
|
||||
out.append(" data: ");
|
||||
out.append("'").append(this->data).append("'");
|
||||
out.append(format_hex_pretty(this->data));
|
||||
out.append("\n");
|
||||
|
||||
out.append(" end: ");
|
||||
|
||||
@@ -12,7 +12,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
|
||||
AUTO_LOAD = ["web_server_base"]
|
||||
AUTO_LOAD = ["web_server_base", "ota.web_server"]
|
||||
DEPENDENCIES = ["wifi"]
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
|
||||
|
||||
@@ -47,9 +47,6 @@ void CaptivePortal::start() {
|
||||
this->base_->init();
|
||||
if (!this->initialized_) {
|
||||
this->base_->add_handler(this);
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
this->base_->add_ota_handler();
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
|
||||
@@ -21,6 +21,43 @@ static const uint32_t RMT_CLK_FREQ = 80000000;
|
||||
static const uint8_t RMT_CLK_DIV = 2;
|
||||
#endif
|
||||
|
||||
static const size_t RMT_SYMBOLS_PER_BYTE = 8;
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t symbols_written, size_t symbols_free,
|
||||
rmt_symbol_word_t *symbols, bool *done, void *arg) {
|
||||
auto *params = static_cast<LedParams *>(arg);
|
||||
const auto *bytes = static_cast<const uint8_t *>(data);
|
||||
size_t index = symbols_written / RMT_SYMBOLS_PER_BYTE;
|
||||
|
||||
// convert byte to symbols
|
||||
if (index < size) {
|
||||
if (symbols_free < RMT_SYMBOLS_PER_BYTE) {
|
||||
return 0;
|
||||
}
|
||||
for (int32_t i = 0; i < RMT_SYMBOLS_PER_BYTE; i++) {
|
||||
if (bytes[index] & (1 << (7 - i))) {
|
||||
symbols[i] = params->bit1;
|
||||
} else {
|
||||
symbols[i] = params->bit0;
|
||||
}
|
||||
}
|
||||
if ((index + 1) >= size && params->reset.duration0 == 0 && params->reset.duration1 == 0) {
|
||||
*done = true;
|
||||
}
|
||||
return RMT_SYMBOLS_PER_BYTE;
|
||||
}
|
||||
|
||||
// send reset
|
||||
if (symbols_free < 1) {
|
||||
return 0;
|
||||
}
|
||||
symbols[0] = params->reset;
|
||||
*done = true;
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
void ESP32RMTLEDStripLightOutput::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
@@ -42,10 +79,15 @@ void ESP32RMTLEDStripLightOutput::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
// copy of the led buffer
|
||||
this->rmt_buf_ = allocator.allocate(buffer_size);
|
||||
#else
|
||||
RAMAllocator<rmt_symbol_word_t> rmt_allocator(this->use_psram_ ? 0 : RAMAllocator<rmt_symbol_word_t>::ALLOC_INTERNAL);
|
||||
|
||||
// 8 bits per byte, 1 rmt_symbol_word_t per bit + 1 rmt_symbol_word_t for reset
|
||||
this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + 1);
|
||||
#endif
|
||||
|
||||
rmt_tx_channel_config_t channel;
|
||||
memset(&channel, 0, sizeof(channel));
|
||||
@@ -65,6 +107,18 @@ void ESP32RMTLEDStripLightOutput::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
rmt_simple_encoder_config_t encoder;
|
||||
memset(&encoder, 0, sizeof(encoder));
|
||||
encoder.callback = encoder_callback;
|
||||
encoder.arg = &this->params_;
|
||||
encoder.min_chunk_size = 8;
|
||||
if (rmt_new_simple_encoder(&encoder, &this->encoder_) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Encoder creation failed");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
#else
|
||||
rmt_copy_encoder_config_t encoder;
|
||||
memset(&encoder, 0, sizeof(encoder));
|
||||
if (rmt_new_copy_encoder(&encoder, &this->encoder_) != ESP_OK) {
|
||||
@@ -72,6 +126,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (rmt_enable(this->channel_) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Enabling channel failed");
|
||||
@@ -85,20 +140,20 @@ void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bi
|
||||
float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f;
|
||||
|
||||
// 0-bit
|
||||
this->bit0_.duration0 = (uint32_t) (ratio * bit0_high);
|
||||
this->bit0_.level0 = 1;
|
||||
this->bit0_.duration1 = (uint32_t) (ratio * bit0_low);
|
||||
this->bit0_.level1 = 0;
|
||||
this->params_.bit0.duration0 = (uint32_t) (ratio * bit0_high);
|
||||
this->params_.bit0.level0 = 1;
|
||||
this->params_.bit0.duration1 = (uint32_t) (ratio * bit0_low);
|
||||
this->params_.bit0.level1 = 0;
|
||||
// 1-bit
|
||||
this->bit1_.duration0 = (uint32_t) (ratio * bit1_high);
|
||||
this->bit1_.level0 = 1;
|
||||
this->bit1_.duration1 = (uint32_t) (ratio * bit1_low);
|
||||
this->bit1_.level1 = 0;
|
||||
this->params_.bit1.duration0 = (uint32_t) (ratio * bit1_high);
|
||||
this->params_.bit1.level0 = 1;
|
||||
this->params_.bit1.duration1 = (uint32_t) (ratio * bit1_low);
|
||||
this->params_.bit1.level1 = 0;
|
||||
// reset
|
||||
this->reset_.duration0 = (uint32_t) (ratio * reset_time_high);
|
||||
this->reset_.level0 = 1;
|
||||
this->reset_.duration1 = (uint32_t) (ratio * reset_time_low);
|
||||
this->reset_.level1 = 0;
|
||||
this->params_.reset.duration0 = (uint32_t) (ratio * reset_time_high);
|
||||
this->params_.reset.level0 = 1;
|
||||
this->params_.reset.duration1 = (uint32_t) (ratio * reset_time_low);
|
||||
this->params_.reset.level1 = 0;
|
||||
}
|
||||
|
||||
void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
|
||||
@@ -122,6 +177,9 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
|
||||
}
|
||||
delayMicroseconds(50);
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
memcpy(this->rmt_buf_, this->buf_, this->get_buffer_size_());
|
||||
#else
|
||||
size_t buffer_size = this->get_buffer_size_();
|
||||
|
||||
size_t size = 0;
|
||||
@@ -131,7 +189,7 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
|
||||
while (size < buffer_size) {
|
||||
uint8_t b = *psrc;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
pdest->val = b & (1 << (7 - i)) ? this->bit1_.val : this->bit0_.val;
|
||||
pdest->val = b & (1 << (7 - i)) ? this->params_.bit1.val : this->params_.bit0.val;
|
||||
pdest++;
|
||||
len++;
|
||||
}
|
||||
@@ -139,17 +197,20 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
|
||||
psrc++;
|
||||
}
|
||||
|
||||
if (this->reset_.duration0 > 0 || this->reset_.duration1 > 0) {
|
||||
pdest->val = this->reset_.val;
|
||||
if (this->params_.reset.duration0 > 0 || this->params_.reset.duration1 > 0) {
|
||||
pdest->val = this->params_.reset.val;
|
||||
pdest++;
|
||||
len++;
|
||||
}
|
||||
#endif
|
||||
|
||||
rmt_transmit_config_t config;
|
||||
memset(&config, 0, sizeof(config));
|
||||
config.loop_count = 0;
|
||||
config.flags.eot_level = 0;
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, this->get_buffer_size_(), &config);
|
||||
#else
|
||||
error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, len * sizeof(rmt_symbol_word_t), &config);
|
||||
#endif
|
||||
if (error != ESP_OK) {
|
||||
ESP_LOGE(TAG, "RMT TX error");
|
||||
this->status_set_warning();
|
||||
|
||||
@@ -25,6 +25,12 @@ enum RGBOrder : uint8_t {
|
||||
ORDER_BRG,
|
||||
};
|
||||
|
||||
struct LedParams {
|
||||
rmt_symbol_word_t bit0;
|
||||
rmt_symbol_word_t bit1;
|
||||
rmt_symbol_word_t reset;
|
||||
};
|
||||
|
||||
class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
|
||||
public:
|
||||
void setup() override;
|
||||
@@ -72,12 +78,15 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
|
||||
|
||||
uint8_t *buf_{nullptr};
|
||||
uint8_t *effect_data_{nullptr};
|
||||
LedParams params_;
|
||||
rmt_channel_handle_t channel_{nullptr};
|
||||
rmt_encoder_handle_t encoder_{nullptr};
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
uint8_t *rmt_buf_{nullptr};
|
||||
#else
|
||||
rmt_symbol_word_t *rmt_buf_{nullptr};
|
||||
rmt_symbol_word_t bit0_, bit1_, reset_;
|
||||
#endif
|
||||
uint32_t rmt_symbols_{48};
|
||||
|
||||
uint8_t pin_;
|
||||
uint16_t num_leds_;
|
||||
bool is_rgbw_{false};
|
||||
|
||||
@@ -133,7 +133,6 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(std::string url, std:
|
||||
std::string header_value = container->client_.header(i).c_str();
|
||||
ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
|
||||
container->response_headers_[header_name].push_back(header_value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
|
||||
const std::string header_value = evt->header_value;
|
||||
ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
|
||||
user_data->response_headers[header_name].push_back(header_value);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ void MMC5603Component::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (id != MMC56X3_CHIP_ID) {
|
||||
if (id != 0 && id != MMC56X3_CHIP_ID) { // ID is not reported correctly by all chips, 0 on some chips
|
||||
ESP_LOGCONFIG(TAG, "Chip Wrong");
|
||||
this->error_code_ = ID_REGISTERS;
|
||||
this->mark_failed();
|
||||
|
||||
@@ -164,7 +164,7 @@ void Nextion::dump_config() {
|
||||
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
|
||||
|
||||
if (this->touch_sleep_timeout_ != 0) {
|
||||
ESP_LOGCONFIG(TAG, " Touch Timeout: %" PRIu32, this->touch_sleep_timeout_);
|
||||
ESP_LOGCONFIG(TAG, " Touch Timeout: %" PRIu16, this->touch_sleep_timeout_);
|
||||
}
|
||||
|
||||
if (this->wake_up_page_ != -1) {
|
||||
@@ -302,11 +302,11 @@ void Nextion::loop() {
|
||||
}
|
||||
|
||||
// Check if a startup page has been set and send the command
|
||||
if (this->start_up_page_ != -1) {
|
||||
if (this->start_up_page_ >= 0) {
|
||||
this->goto_page(this->start_up_page_);
|
||||
}
|
||||
|
||||
if (this->wake_up_page_ != -1) {
|
||||
if (this->wake_up_page_ >= 0) {
|
||||
this->set_wake_up_page(this->wake_up_page_);
|
||||
}
|
||||
|
||||
@@ -418,12 +418,12 @@ void Nextion::process_nextion_commands_() {
|
||||
ESP_LOGN(TAG, "Add 0xFF");
|
||||
}
|
||||
|
||||
this->nextion_event_ = this->command_data_[0];
|
||||
const uint8_t nextion_event = this->command_data_[0];
|
||||
|
||||
to_process_length -= 1;
|
||||
to_process = this->command_data_.substr(1, to_process_length);
|
||||
|
||||
switch (this->nextion_event_) {
|
||||
switch (nextion_event) {
|
||||
case 0x00: // instruction sent by user has failed
|
||||
ESP_LOGW(TAG, "Invalid instruction");
|
||||
this->remove_from_q_();
|
||||
@@ -562,9 +562,9 @@ void Nextion::process_nextion_commands_() {
|
||||
break;
|
||||
}
|
||||
|
||||
uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1];
|
||||
uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3];
|
||||
uint8_t touch_event = to_process[4]; // 0 -> release, 1 -> press
|
||||
const uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1];
|
||||
const uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3];
|
||||
const uint8_t touch_event = to_process[4]; // 0 -> release, 1 -> press
|
||||
ESP_LOGD(TAG, "Touch %s at %u,%u", touch_event ? "PRESS" : "RELEASE", x, y);
|
||||
break;
|
||||
}
|
||||
@@ -820,15 +820,14 @@ void Nextion::process_nextion_commands_() {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown event: 0x%02X", this->nextion_event_);
|
||||
ESP_LOGW(TAG, "Unknown event: 0x%02X", nextion_event);
|
||||
break;
|
||||
}
|
||||
|
||||
// ESP_LOGN(TAG, "nextion_event_ deleting from 0 to %d", to_process_length + COMMAND_DELIMITER.length() + 1);
|
||||
this->command_data_.erase(0, to_process_length + COMMAND_DELIMITER.length() + 1);
|
||||
}
|
||||
|
||||
uint32_t ms = App.get_loop_component_start_time();
|
||||
const uint32_t ms = App.get_loop_component_start_time();
|
||||
|
||||
if (!this->nextion_queue_.empty() && this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) {
|
||||
for (size_t i = 0; i < this->nextion_queue_.size(); i++) {
|
||||
@@ -960,7 +959,6 @@ void Nextion::update_components_by_prefix(const std::string &prefix) {
|
||||
}
|
||||
|
||||
uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag) {
|
||||
uint16_t ret = 0;
|
||||
uint8_t c = 0;
|
||||
uint8_t nr_of_ff_bytes = 0;
|
||||
bool exit_flag = false;
|
||||
@@ -1003,8 +1001,7 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool
|
||||
if (ff_flag)
|
||||
response = response.substr(0, response.length() - 3); // Remove last 3 0xFF
|
||||
|
||||
ret = response.length();
|
||||
return ret;
|
||||
return response.length();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1190,11 +1190,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
||||
* After 30 seconds the display will go to sleep. Note: the display will only wakeup by a restart or by setting up
|
||||
* `thup`.
|
||||
*/
|
||||
void set_touch_sleep_timeout(uint32_t touch_sleep_timeout);
|
||||
void set_touch_sleep_timeout(uint16_t touch_sleep_timeout);
|
||||
|
||||
/**
|
||||
* Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode.
|
||||
* @param wake_up_page The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to
|
||||
* @param wake_up_page The page id, from 0 to the last page in Nextion. Set -1 (not set to any existing page) to
|
||||
* wakes up to current page.
|
||||
*
|
||||
* Example:
|
||||
@@ -1204,11 +1204,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
||||
*
|
||||
* The display will wake up to page 2.
|
||||
*/
|
||||
void set_wake_up_page(uint8_t wake_up_page = 255);
|
||||
void set_wake_up_page(int16_t wake_up_page = -1);
|
||||
|
||||
/**
|
||||
* Sets which page Nextion loads when connecting to ESPHome.
|
||||
* @param start_up_page The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to
|
||||
* @param start_up_page The page id, from 0 to the last page in Nextion. Set -1 (not set to any existing page) to
|
||||
* wakes up to current page.
|
||||
*
|
||||
* Example:
|
||||
@@ -1218,7 +1218,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
||||
*
|
||||
* The display will go to page 2 when it establishes a connection to ESPHome.
|
||||
*/
|
||||
void set_start_up_page(uint8_t start_up_page = 255) { this->start_up_page_ = start_up_page; }
|
||||
void set_start_up_page(int16_t start_up_page = -1) { this->start_up_page_ = start_up_page; }
|
||||
|
||||
/**
|
||||
* Sets if Nextion should auto-wake from sleep when touch press occurs.
|
||||
@@ -1330,7 +1330,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
||||
std::deque<NextionQueue *> waveform_queue_;
|
||||
uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag);
|
||||
void all_components_send_state_(bool force_update = false);
|
||||
uint64_t comok_sent_ = 0;
|
||||
uint32_t comok_sent_ = 0;
|
||||
bool remove_from_q_(bool report_empty = true);
|
||||
|
||||
/**
|
||||
@@ -1340,12 +1340,10 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
||||
bool ignore_is_setup_ = false;
|
||||
|
||||
bool nextion_reports_is_setup_ = false;
|
||||
uint8_t nextion_event_;
|
||||
|
||||
void process_nextion_commands_();
|
||||
void process_serial_();
|
||||
bool is_updating_ = false;
|
||||
uint32_t touch_sleep_timeout_ = 0;
|
||||
uint16_t touch_sleep_timeout_ = 0;
|
||||
int16_t wake_up_page_ = -1;
|
||||
int16_t start_up_page_ = -1;
|
||||
bool auto_wake_on_touch_ = true;
|
||||
|
||||
@@ -10,12 +10,12 @@ static const char *const TAG = "nextion";
|
||||
// Sleep safe commands
|
||||
void Nextion::soft_reset() { this->send_command_("rest"); }
|
||||
|
||||
void Nextion::set_wake_up_page(uint8_t wake_up_page) {
|
||||
void Nextion::set_wake_up_page(int16_t wake_up_page) {
|
||||
this->wake_up_page_ = wake_up_page;
|
||||
this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true);
|
||||
}
|
||||
|
||||
void Nextion::set_touch_sleep_timeout(uint32_t touch_sleep_timeout) {
|
||||
void Nextion::set_touch_sleep_timeout(uint16_t touch_sleep_timeout) {
|
||||
if (touch_sleep_timeout < 3) {
|
||||
ESP_LOGD(TAG, "Sleep timeout out of bounds (3-65535)");
|
||||
return;
|
||||
|
||||
@@ -67,7 +67,28 @@ class OTAComponent : public Component {
|
||||
}
|
||||
|
||||
protected:
|
||||
CallbackManager<void(ota::OTAState, float, uint8_t)> state_callback_{};
|
||||
/** Extended callback manager with deferred call support.
|
||||
*
|
||||
* This adds a call_deferred() method for thread-safe execution from other tasks.
|
||||
*/
|
||||
class StateCallbackManager : public CallbackManager<void(OTAState, float, uint8_t)> {
|
||||
public:
|
||||
StateCallbackManager(OTAComponent *component) : component_(component) {}
|
||||
|
||||
/** Call callbacks with deferral to main loop (for thread safety).
|
||||
*
|
||||
* This should be used by OTA implementations that run in separate tasks
|
||||
* (like web_server OTA) to ensure callbacks execute in the main loop.
|
||||
*/
|
||||
void call_deferred(ota::OTAState state, float progress, uint8_t error) {
|
||||
component_->defer([this, state, progress, error]() { this->call(state, progress, error); });
|
||||
}
|
||||
|
||||
private:
|
||||
OTAComponent *component_;
|
||||
};
|
||||
|
||||
StateCallbackManager state_callback_{this};
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -89,6 +110,11 @@ class OTAGlobalCallback {
|
||||
|
||||
OTAGlobalCallback *get_global_ota_callback();
|
||||
void register_ota_platform(OTAComponent *ota_caller);
|
||||
|
||||
// OTA implementations should use:
|
||||
// - state_callback_.call() when already in main loop (e.g., esphome OTA)
|
||||
// - state_callback_.call_deferred() when in separate task (e.g., web_server OTA)
|
||||
// This ensures proper callback execution in all contexts.
|
||||
#endif
|
||||
std::unique_ptr<ota::OTABackend> make_ota_backend();
|
||||
|
||||
|
||||
@@ -15,6 +15,11 @@ static const char *const TAG = "ota.arduino_esp32";
|
||||
std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoESP32OTABackend>(); }
|
||||
|
||||
OTAResponseTypes ArduinoESP32OTABackend::begin(size_t image_size) {
|
||||
// Handle UPDATE_SIZE_UNKNOWN (0) which is used by web server OTA
|
||||
// where the exact firmware size is unknown due to multipart encoding
|
||||
if (image_size == 0) {
|
||||
image_size = UPDATE_SIZE_UNKNOWN;
|
||||
}
|
||||
bool ret = Update.begin(image_size, U_FLASH);
|
||||
if (ret) {
|
||||
return OTA_RESPONSE_OK;
|
||||
@@ -29,7 +34,10 @@ OTAResponseTypes ArduinoESP32OTABackend::begin(size_t image_size) {
|
||||
return OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
void ArduinoESP32OTABackend::set_update_md5(const char *md5) { Update.setMD5(md5); }
|
||||
void ArduinoESP32OTABackend::set_update_md5(const char *md5) {
|
||||
Update.setMD5(md5);
|
||||
this->md5_set_ = true;
|
||||
}
|
||||
|
||||
OTAResponseTypes ArduinoESP32OTABackend::write(uint8_t *data, size_t len) {
|
||||
size_t written = Update.write(data, len);
|
||||
@@ -44,7 +52,9 @@ OTAResponseTypes ArduinoESP32OTABackend::write(uint8_t *data, size_t len) {
|
||||
}
|
||||
|
||||
OTAResponseTypes ArduinoESP32OTABackend::end() {
|
||||
if (Update.end()) {
|
||||
// Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5
|
||||
// This matches the behavior of the old web_server OTA implementation
|
||||
if (Update.end(!this->md5_set_)) {
|
||||
return OTA_RESPONSE_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ class ArduinoESP32OTABackend : public OTABackend {
|
||||
OTAResponseTypes end() override;
|
||||
void abort() override;
|
||||
bool supports_compression() override { return false; }
|
||||
|
||||
private:
|
||||
bool md5_set_{false};
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
|
||||
@@ -17,6 +17,11 @@ static const char *const TAG = "ota.arduino_esp8266";
|
||||
std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoESP8266OTABackend>(); }
|
||||
|
||||
OTAResponseTypes ArduinoESP8266OTABackend::begin(size_t image_size) {
|
||||
// Handle UPDATE_SIZE_UNKNOWN (0) by calculating available space
|
||||
if (image_size == 0) {
|
||||
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
|
||||
image_size = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
|
||||
}
|
||||
bool ret = Update.begin(image_size, U_FLASH);
|
||||
if (ret) {
|
||||
esp8266::preferences_prevent_write(true);
|
||||
@@ -38,7 +43,10 @@ OTAResponseTypes ArduinoESP8266OTABackend::begin(size_t image_size) {
|
||||
return OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
void ArduinoESP8266OTABackend::set_update_md5(const char *md5) { Update.setMD5(md5); }
|
||||
void ArduinoESP8266OTABackend::set_update_md5(const char *md5) {
|
||||
Update.setMD5(md5);
|
||||
this->md5_set_ = true;
|
||||
}
|
||||
|
||||
OTAResponseTypes ArduinoESP8266OTABackend::write(uint8_t *data, size_t len) {
|
||||
size_t written = Update.write(data, len);
|
||||
@@ -53,13 +61,19 @@ OTAResponseTypes ArduinoESP8266OTABackend::write(uint8_t *data, size_t len) {
|
||||
}
|
||||
|
||||
OTAResponseTypes ArduinoESP8266OTABackend::end() {
|
||||
if (Update.end()) {
|
||||
// Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5
|
||||
// This matches the behavior of the old web_server OTA implementation
|
||||
bool success = Update.end(!this->md5_set_);
|
||||
|
||||
// On ESP8266, Update.end() might return false even with error code 0
|
||||
// Check the actual error code to determine success
|
||||
uint8_t error = Update.getError();
|
||||
|
||||
if (success || error == UPDATE_ERROR_OK) {
|
||||
return OTA_RESPONSE_OK;
|
||||
}
|
||||
|
||||
uint8_t error = Update.getError();
|
||||
ESP_LOGE(TAG, "End error: %d", error);
|
||||
|
||||
return OTA_RESPONSE_ERROR_UPDATE_END;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ class ArduinoESP8266OTABackend : public OTABackend {
|
||||
#else
|
||||
bool supports_compression() override { return false; }
|
||||
#endif
|
||||
|
||||
private:
|
||||
bool md5_set_{false};
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
|
||||
@@ -15,6 +15,11 @@ static const char *const TAG = "ota.arduino_libretiny";
|
||||
std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoLibreTinyOTABackend>(); }
|
||||
|
||||
OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) {
|
||||
// Handle UPDATE_SIZE_UNKNOWN (0) which is used by web server OTA
|
||||
// where the exact firmware size is unknown due to multipart encoding
|
||||
if (image_size == 0) {
|
||||
image_size = UPDATE_SIZE_UNKNOWN;
|
||||
}
|
||||
bool ret = Update.begin(image_size, U_FLASH);
|
||||
if (ret) {
|
||||
return OTA_RESPONSE_OK;
|
||||
@@ -29,7 +34,10 @@ OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) {
|
||||
return OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
void ArduinoLibreTinyOTABackend::set_update_md5(const char *md5) { Update.setMD5(md5); }
|
||||
void ArduinoLibreTinyOTABackend::set_update_md5(const char *md5) {
|
||||
Update.setMD5(md5);
|
||||
this->md5_set_ = true;
|
||||
}
|
||||
|
||||
OTAResponseTypes ArduinoLibreTinyOTABackend::write(uint8_t *data, size_t len) {
|
||||
size_t written = Update.write(data, len);
|
||||
@@ -44,7 +52,9 @@ OTAResponseTypes ArduinoLibreTinyOTABackend::write(uint8_t *data, size_t len) {
|
||||
}
|
||||
|
||||
OTAResponseTypes ArduinoLibreTinyOTABackend::end() {
|
||||
if (Update.end()) {
|
||||
// Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5
|
||||
// This matches the behavior of the old web_server OTA implementation
|
||||
if (Update.end(!this->md5_set_)) {
|
||||
return OTA_RESPONSE_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@ class ArduinoLibreTinyOTABackend : public OTABackend {
|
||||
OTAResponseTypes end() override;
|
||||
void abort() override;
|
||||
bool supports_compression() override { return false; }
|
||||
|
||||
private:
|
||||
bool md5_set_{false};
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
|
||||
@@ -17,6 +17,8 @@ static const char *const TAG = "ota.arduino_rp2040";
|
||||
std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoRP2040OTABackend>(); }
|
||||
|
||||
OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) {
|
||||
// OTA size of 0 is not currently handled, but
|
||||
// web_server is not supported for RP2040, so this is not an issue.
|
||||
bool ret = Update.begin(image_size, U_FLASH);
|
||||
if (ret) {
|
||||
rp2040::preferences_prevent_write(true);
|
||||
@@ -38,7 +40,10 @@ OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) {
|
||||
return OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
void ArduinoRP2040OTABackend::set_update_md5(const char *md5) { Update.setMD5(md5); }
|
||||
void ArduinoRP2040OTABackend::set_update_md5(const char *md5) {
|
||||
Update.setMD5(md5);
|
||||
this->md5_set_ = true;
|
||||
}
|
||||
|
||||
OTAResponseTypes ArduinoRP2040OTABackend::write(uint8_t *data, size_t len) {
|
||||
size_t written = Update.write(data, len);
|
||||
@@ -53,7 +58,9 @@ OTAResponseTypes ArduinoRP2040OTABackend::write(uint8_t *data, size_t len) {
|
||||
}
|
||||
|
||||
OTAResponseTypes ArduinoRP2040OTABackend::end() {
|
||||
if (Update.end()) {
|
||||
// Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5
|
||||
// This matches the behavior of the old web_server OTA implementation
|
||||
if (Update.end(!this->md5_set_)) {
|
||||
return OTA_RESPONSE_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ class ArduinoRP2040OTABackend : public OTABackend {
|
||||
OTAResponseTypes end() override;
|
||||
void abort() override;
|
||||
bool supports_compression() override { return false; }
|
||||
|
||||
private:
|
||||
bool md5_set_{false};
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
|
||||
@@ -56,7 +56,10 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size) {
|
||||
return OTA_RESPONSE_OK;
|
||||
}
|
||||
|
||||
void IDFOTABackend::set_update_md5(const char *expected_md5) { memcpy(this->expected_bin_md5_, expected_md5, 32); }
|
||||
void IDFOTABackend::set_update_md5(const char *expected_md5) {
|
||||
memcpy(this->expected_bin_md5_, expected_md5, 32);
|
||||
this->md5_set_ = true;
|
||||
}
|
||||
|
||||
OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) {
|
||||
esp_err_t err = esp_ota_write(this->update_handle_, data, len);
|
||||
@@ -73,10 +76,12 @@ OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) {
|
||||
}
|
||||
|
||||
OTAResponseTypes IDFOTABackend::end() {
|
||||
this->md5_.calculate();
|
||||
if (!this->md5_.equals_hex(this->expected_bin_md5_)) {
|
||||
this->abort();
|
||||
return OTA_RESPONSE_ERROR_MD5_MISMATCH;
|
||||
if (this->md5_set_) {
|
||||
this->md5_.calculate();
|
||||
if (!this->md5_.equals_hex(this->expected_bin_md5_)) {
|
||||
this->abort();
|
||||
return OTA_RESPONSE_ERROR_MD5_MISMATCH;
|
||||
}
|
||||
}
|
||||
esp_err_t err = esp_ota_end(this->update_handle_);
|
||||
this->update_handle_ = 0;
|
||||
|
||||
@@ -24,6 +24,7 @@ class IDFOTABackend : public OTABackend {
|
||||
const esp_partition_t *partition_;
|
||||
md5::MD5Digest md5_{};
|
||||
char expected_bin_md5_[32];
|
||||
bool md5_set_{false};
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
|
||||
@@ -31,6 +31,10 @@ void PulseMeterSensor::setup() {
|
||||
this->pulse_state_.latched_ = this->last_pin_val_;
|
||||
this->pin_->attach_interrupt(PulseMeterSensor::pulse_intr, this, gpio::INTERRUPT_ANY_EDGE);
|
||||
}
|
||||
|
||||
if (this->total_sensor_ != nullptr) {
|
||||
this->total_sensor_->publish_state(this->total_pulses_);
|
||||
}
|
||||
}
|
||||
|
||||
void PulseMeterSensor::loop() {
|
||||
|
||||
@@ -21,21 +21,24 @@ ECC = {
|
||||
"HIGH": qrcodegen_Ecc.qrcodegen_Ecc_HIGH,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(QRCode),
|
||||
cv.Required(CONF_VALUE): cv.string,
|
||||
cv.Optional(CONF_ECC, default="LOW"): cv.enum(ECC, upper=True),
|
||||
}
|
||||
CONFIG_SCHEMA = cv.ensure_list(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(QRCode),
|
||||
cv.Required(CONF_VALUE): cv.string,
|
||||
cv.Optional(CONF_ECC, default="LOW"): cv.enum(ECC, upper=True),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
cg.add_library("wjtje/qr-code-generator-library", "^1.7.0")
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(var.set_value(config[CONF_VALUE]))
|
||||
cg.add(var.set_ecc(ECC[config[CONF_ECC]]))
|
||||
await cg.register_component(var, config)
|
||||
for entry in config:
|
||||
var = cg.new_Pvariable(entry[CONF_ID])
|
||||
cg.add(var.set_value(entry[CONF_VALUE]))
|
||||
cg.add(var.set_ecc(ECC[entry[CONF_ECC]]))
|
||||
await cg.register_component(var, entry)
|
||||
|
||||
cg.add_define("USE_QR_CODE")
|
||||
|
||||
@@ -33,6 +33,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["json", "web_server_base"]
|
||||
|
||||
@@ -47,7 +48,7 @@ WebServer = web_server_ns.class_("WebServer", cg.Component, cg.Controller)
|
||||
sorting_groups = {}
|
||||
|
||||
|
||||
def default_url(config):
|
||||
def default_url(config: ConfigType) -> ConfigType:
|
||||
config = config.copy()
|
||||
if config[CONF_VERSION] == 1:
|
||||
if CONF_CSS_URL not in config:
|
||||
@@ -67,13 +68,27 @@ def default_url(config):
|
||||
return config
|
||||
|
||||
|
||||
def validate_local(config):
|
||||
def validate_local(config: ConfigType) -> ConfigType:
|
||||
if CONF_LOCAL in config and config[CONF_VERSION] == 1:
|
||||
raise cv.Invalid("'local' is not supported in version 1")
|
||||
return config
|
||||
|
||||
|
||||
def validate_sorting_groups(config):
|
||||
def validate_ota_removed(config: ConfigType) -> ConfigType:
|
||||
# Only raise error if OTA is explicitly enabled (True)
|
||||
# If it's False or not specified, we can safely ignore it
|
||||
if config.get(CONF_OTA):
|
||||
raise cv.Invalid(
|
||||
f"The '{CONF_OTA}' option has been removed from 'web_server'. "
|
||||
f"Please use the new OTA platform structure instead:\n\n"
|
||||
f"ota:\n"
|
||||
f" - platform: web_server\n\n"
|
||||
f"See https://esphome.io/components/ota for more information."
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def validate_sorting_groups(config: ConfigType) -> ConfigType:
|
||||
if CONF_SORTING_GROUPS in config and config[CONF_VERSION] != 3:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_SORTING_GROUPS}' is only supported in 'web_server' version 3"
|
||||
@@ -84,7 +99,7 @@ def validate_sorting_groups(config):
|
||||
def _validate_no_sorting_component(
|
||||
sorting_component: str,
|
||||
webserver_version: int,
|
||||
config: dict,
|
||||
config: ConfigType,
|
||||
path: list[str] | None = None,
|
||||
) -> None:
|
||||
if path is None:
|
||||
@@ -107,7 +122,7 @@ def _validate_no_sorting_component(
|
||||
)
|
||||
|
||||
|
||||
def _final_validate_sorting(config):
|
||||
def _final_validate_sorting(config: ConfigType) -> ConfigType:
|
||||
if (webserver_version := config.get(CONF_VERSION)) != 3:
|
||||
_validate_no_sorting_component(
|
||||
CONF_SORTING_WEIGHT, webserver_version, fv.full_config.get()
|
||||
@@ -170,7 +185,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
web_server_base.WebServerBase
|
||||
),
|
||||
cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean,
|
||||
cv.Optional(CONF_OTA, default=True): cv.boolean,
|
||||
cv.Optional(CONF_OTA, default=False): cv.boolean,
|
||||
cv.Optional(CONF_LOG, default=True): cv.boolean,
|
||||
cv.Optional(CONF_LOCAL): cv.boolean,
|
||||
cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group),
|
||||
@@ -188,6 +203,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
default_url,
|
||||
validate_local,
|
||||
validate_sorting_groups,
|
||||
validate_ota_removed,
|
||||
)
|
||||
|
||||
|
||||
@@ -271,11 +287,8 @@ async def to_code(config):
|
||||
else:
|
||||
cg.add(var.set_css_url(config[CONF_CSS_URL]))
|
||||
cg.add(var.set_js_url(config[CONF_JS_URL]))
|
||||
cg.add(var.set_allow_ota(config[CONF_OTA]))
|
||||
if config[CONF_OTA]:
|
||||
# Define USE_WEBSERVER_OTA based only on web_server OTA config
|
||||
# This allows web server OTA to work without loading the OTA component
|
||||
cg.add_define("USE_WEBSERVER_OTA")
|
||||
# OTA is now handled by the web_server OTA platform
|
||||
# The CONF_OTA option is kept only for backwards compatibility validation
|
||||
cg.add(var.set_expose_log(config[CONF_LOG]))
|
||||
if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]:
|
||||
cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS")
|
||||
|
||||
32
esphome/components/web_server/ota/__init__.py
Normal file
32
esphome/components/web_server/ota/__init__.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["network", "web_server_base"]
|
||||
|
||||
web_server_ns = cg.esphome_ns.namespace("web_server")
|
||||
WebServerOTAComponent = web_server_ns.class_("WebServerOTAComponent", OTAComponent)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(WebServerOTAComponent),
|
||||
}
|
||||
)
|
||||
.extend(BASE_OTA_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(52.0)
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ota_to_code(var, config)
|
||||
await cg.register_component(var, config)
|
||||
cg.add_define("USE_WEBSERVER_OTA")
|
||||
if CORE.using_esp_idf:
|
||||
add_idf_component(name="zorxx/multipart-parser", ref="1.0.1")
|
||||
210
esphome/components/web_server/ota/ota_web_server.cpp
Normal file
210
esphome/components/web_server/ota/ota_web_server.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
#include "ota_web_server.h"
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#ifdef USE_ESP8266
|
||||
#include <Updater.h>
|
||||
#elif defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
#include <Update.h>
|
||||
#endif
|
||||
#endif // USE_ARDUINO
|
||||
|
||||
namespace esphome {
|
||||
namespace web_server {
|
||||
|
||||
static const char *const TAG = "web_server.ota";
|
||||
|
||||
class OTARequestHandler : public AsyncWebHandler {
|
||||
public:
|
||||
OTARequestHandler(WebServerOTAComponent *parent) : parent_(parent) {}
|
||||
void handleRequest(AsyncWebServerRequest *request) override;
|
||||
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
|
||||
bool final) override;
|
||||
bool canHandle(AsyncWebServerRequest *request) const override {
|
||||
return request->url() == "/update" && request->method() == HTTP_POST;
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
bool isRequestHandlerTrivial() const override { return false; }
|
||||
|
||||
protected:
|
||||
void report_ota_progress_(AsyncWebServerRequest *request);
|
||||
void schedule_ota_reboot_();
|
||||
void ota_init_(const char *filename);
|
||||
|
||||
uint32_t last_ota_progress_{0};
|
||||
uint32_t ota_read_length_{0};
|
||||
WebServerOTAComponent *parent_;
|
||||
bool ota_success_{false};
|
||||
|
||||
private:
|
||||
std::unique_ptr<ota::OTABackend> ota_backend_{nullptr};
|
||||
};
|
||||
|
||||
void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
|
||||
const uint32_t now = millis();
|
||||
if (now - this->last_ota_progress_ > 1000) {
|
||||
float percentage = 0.0f;
|
||||
if (request->contentLength() != 0) {
|
||||
// Note: Using contentLength() for progress calculation is technically wrong as it includes
|
||||
// multipart headers/boundaries, but it's only off by a small amount and we don't have
|
||||
// access to the actual firmware size until the upload is complete. This is intentional
|
||||
// as it still gives the user a reasonable progress indication.
|
||||
percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
|
||||
ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_);
|
||||
}
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
// Report progress - use call_deferred since we're in web server task
|
||||
this->parent_->state_callback_.call_deferred(ota::OTA_IN_PROGRESS, percentage, 0);
|
||||
#endif
|
||||
this->last_ota_progress_ = now;
|
||||
}
|
||||
}
|
||||
|
||||
void OTARequestHandler::schedule_ota_reboot_() {
|
||||
ESP_LOGI(TAG, "OTA update successful!");
|
||||
this->parent_->set_timeout(100, []() {
|
||||
ESP_LOGI(TAG, "Performing OTA reboot now");
|
||||
App.safe_reboot();
|
||||
});
|
||||
}
|
||||
|
||||
void OTARequestHandler::ota_init_(const char *filename) {
|
||||
ESP_LOGI(TAG, "OTA Update Start: %s", filename);
|
||||
this->ota_read_length_ = 0;
|
||||
this->ota_success_ = false;
|
||||
}
|
||||
|
||||
void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index,
|
||||
uint8_t *data, size_t len, bool final) {
|
||||
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_OK;
|
||||
|
||||
if (index == 0 && !this->ota_backend_) {
|
||||
// Initialize OTA on first call
|
||||
this->ota_init_(filename.c_str());
|
||||
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
// Notify OTA started - use call_deferred since we're in web server task
|
||||
this->parent_->state_callback_.call_deferred(ota::OTA_STARTED, 0.0f, 0);
|
||||
#endif
|
||||
|
||||
// Platform-specific pre-initialization
|
||||
#ifdef USE_ARDUINO
|
||||
#ifdef USE_ESP8266
|
||||
Update.runAsync(true);
|
||||
#endif
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
if (Update.isRunning()) {
|
||||
Update.abort();
|
||||
}
|
||||
#endif
|
||||
#endif // USE_ARDUINO
|
||||
|
||||
this->ota_backend_ = ota::make_ota_backend();
|
||||
if (!this->ota_backend_) {
|
||||
ESP_LOGE(TAG, "Failed to create OTA backend");
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f,
|
||||
static_cast<uint8_t>(ota::OTA_RESPONSE_ERROR_UNKNOWN));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// Web server OTA uses multipart uploads where the actual firmware size
|
||||
// is unknown (contentLength includes multipart overhead)
|
||||
// Pass 0 to indicate unknown size
|
||||
error_code = this->ota_backend_->begin(0);
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
ESP_LOGE(TAG, "OTA begin failed: %d", error_code);
|
||||
this->ota_backend_.reset();
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->ota_backend_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Process data
|
||||
if (len > 0) {
|
||||
error_code = this->ota_backend_->write(data, len);
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
ESP_LOGE(TAG, "OTA write failed: %d", error_code);
|
||||
this->ota_backend_->abort();
|
||||
this->ota_backend_.reset();
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
this->ota_read_length_ += len;
|
||||
this->report_ota_progress_(request);
|
||||
}
|
||||
|
||||
// Finalize
|
||||
if (final) {
|
||||
ESP_LOGD(TAG, "OTA final chunk: index=%u, len=%u, total_read=%u, contentLength=%u", index, len,
|
||||
this->ota_read_length_, request->contentLength());
|
||||
|
||||
// For Arduino framework, the Update library tracks expected size from firmware header
|
||||
// If we haven't received enough data, calling end() will fail
|
||||
// This can happen if the upload is interrupted or the client disconnects
|
||||
error_code = this->ota_backend_->end();
|
||||
if (error_code == ota::OTA_RESPONSE_OK) {
|
||||
this->ota_success_ = true;
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
// Report completion before reboot - use call_deferred since we're in web server task
|
||||
this->parent_->state_callback_.call_deferred(ota::OTA_COMPLETED, 100.0f, 0);
|
||||
#endif
|
||||
this->schedule_ota_reboot_();
|
||||
} else {
|
||||
ESP_LOGE(TAG, "OTA end failed: %d", error_code);
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
|
||||
#endif
|
||||
}
|
||||
this->ota_backend_.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) {
|
||||
AsyncWebServerResponse *response;
|
||||
// Use the ota_success_ flag to determine the actual result
|
||||
const char *msg = this->ota_success_ ? "Update Successful!" : "Update Failed!";
|
||||
response = request->beginResponse(200, "text/plain", msg);
|
||||
response->addHeader("Connection", "close");
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
void WebServerOTAComponent::setup() {
|
||||
// Get the global web server base instance and register our handler
|
||||
auto *base = web_server_base::global_web_server_base;
|
||||
if (base == nullptr) {
|
||||
ESP_LOGE(TAG, "WebServerBase not found");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// AsyncWebServer takes ownership of the handler and will delete it when the server is destroyed
|
||||
base->add_handler(new OTARequestHandler(this)); // NOLINT
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
// Register with global OTA callback system
|
||||
ota::register_ota_platform(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
void WebServerOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, "Web Server OTA"); }
|
||||
|
||||
} // namespace web_server
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_WEBSERVER_OTA
|
||||
26
esphome/components/web_server/ota/ota_web_server.h
Normal file
26
esphome/components/web_server/ota/ota_web_server.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
#include "esphome/components/web_server_base/web_server_base.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace web_server {
|
||||
|
||||
class WebServerOTAComponent : public ota::OTAComponent {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
protected:
|
||||
friend class OTARequestHandler;
|
||||
};
|
||||
|
||||
} // namespace web_server
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_WEBSERVER_OTA
|
||||
@@ -273,7 +273,11 @@ std::string WebServer::get_config_json() {
|
||||
return json::build_json([this](JsonObject root) {
|
||||
root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name();
|
||||
root["comment"] = App.get_comment();
|
||||
root["ota"] = this->allow_ota_;
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
root["ota"] = true; // web_server OTA platform is configured
|
||||
#else
|
||||
root["ota"] = false;
|
||||
#endif
|
||||
root["log"] = this->expose_log_;
|
||||
root["lang"] = "en";
|
||||
});
|
||||
@@ -299,10 +303,7 @@ void WebServer::setup() {
|
||||
#endif
|
||||
this->base_->add_handler(this);
|
||||
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
if (this->allow_ota_)
|
||||
this->base_->add_ota_handler();
|
||||
#endif
|
||||
// OTA is now handled by the web_server OTA platform
|
||||
|
||||
// doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly
|
||||
// getting a lot of events
|
||||
|
||||
@@ -212,11 +212,6 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
|
||||
* @param include_internal Whether internal components should be displayed.
|
||||
*/
|
||||
void set_include_internal(bool include_internal) { include_internal_ = include_internal; }
|
||||
/** Set whether or not the webserver should expose the OTA form and handler.
|
||||
*
|
||||
* @param allow_ota.
|
||||
*/
|
||||
void set_allow_ota(bool allow_ota) { this->allow_ota_ = allow_ota; }
|
||||
/** Set whether or not the webserver should expose the Log.
|
||||
*
|
||||
* @param expose_log.
|
||||
@@ -525,7 +520,6 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
|
||||
#ifdef USE_WEBSERVER_JS_INCLUDE
|
||||
const char *js_include_{nullptr};
|
||||
#endif
|
||||
bool allow_ota_{true};
|
||||
bool expose_log_{true};
|
||||
#ifdef USE_ESP32
|
||||
std::deque<std::function<void()>> to_schedule_;
|
||||
|
||||
@@ -192,11 +192,10 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
|
||||
|
||||
stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for "
|
||||
"REST API documentation.</p>"));
|
||||
if (this->allow_ota_) {
|
||||
stream->print(
|
||||
F("<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
|
||||
"type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
|
||||
}
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
stream->print(F("<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
|
||||
"type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
|
||||
#endif
|
||||
stream->print(F("<h2>Debug Log</h2><pre id=\"log\"></pre>"));
|
||||
#ifdef USE_WEBSERVER_JS_INCLUDE
|
||||
if (this->js_include_ != nullptr) {
|
||||
|
||||
@@ -30,6 +30,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
cg.add(cg.RawExpression(f"{web_server_base_ns}::global_web_server_base = {var}"))
|
||||
|
||||
if CORE.using_arduino:
|
||||
if CORE.is_esp32:
|
||||
|
||||
@@ -4,123 +4,12 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#include <StreamString.h>
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
#include <Update.h>
|
||||
#endif
|
||||
#ifdef USE_ESP8266
|
||||
#include <Updater.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA)
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_task_wdt.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace web_server_base {
|
||||
|
||||
static const char *const TAG = "web_server_base";
|
||||
|
||||
#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA)
|
||||
// Minimal OTA backend implementation for web server
|
||||
// This allows OTA updates via web server without requiring the OTA component
|
||||
// TODO: In the future, this should be refactored into a common ota_base component
|
||||
// that both web_server and ota components can depend on, avoiding code duplication
|
||||
// while keeping the components independent. This would allow both ESP-IDF and Arduino
|
||||
// implementations to share the base OTA functionality without requiring the full OTA component.
|
||||
// The IDFWebServerOTABackend class is intentionally designed with the same interface
|
||||
// as OTABackend to make it easy to swap to using OTABackend when the ota component
|
||||
// is split into ota and ota_base in the future.
|
||||
class IDFWebServerOTABackend {
|
||||
public:
|
||||
bool begin() {
|
||||
this->partition_ = esp_ota_get_next_update_partition(nullptr);
|
||||
if (this->partition_ == nullptr) {
|
||||
ESP_LOGE(TAG, "No OTA partition available");
|
||||
return false;
|
||||
}
|
||||
|
||||
#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15
|
||||
// The following function takes longer than the default timeout of WDT due to flash erase
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
esp_task_wdt_config_t wdtc;
|
||||
wdtc.idle_core_mask = 0;
|
||||
#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0
|
||||
wdtc.idle_core_mask |= (1 << 0);
|
||||
#endif
|
||||
#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1
|
||||
wdtc.idle_core_mask |= (1 << 1);
|
||||
#endif
|
||||
wdtc.timeout_ms = 15000;
|
||||
wdtc.trigger_panic = false;
|
||||
esp_task_wdt_reconfigure(&wdtc);
|
||||
#else
|
||||
esp_task_wdt_init(15, false);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
esp_err_t err = esp_ota_begin(this->partition_, 0, &this->update_handle_);
|
||||
|
||||
#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15
|
||||
// Set the WDT back to the configured timeout
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
wdtc.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000;
|
||||
esp_task_wdt_reconfigure(&wdtc);
|
||||
#else
|
||||
esp_task_wdt_init(CONFIG_ESP_TASK_WDT_TIMEOUT_S, false);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
if (err != ESP_OK) {
|
||||
esp_ota_abort(this->update_handle_);
|
||||
this->update_handle_ = 0;
|
||||
ESP_LOGE(TAG, "esp_ota_begin failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool write(uint8_t *data, size_t len) {
|
||||
esp_err_t err = esp_ota_write(this->update_handle_, data, len);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ota_write failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool end() {
|
||||
esp_err_t err = esp_ota_end(this->update_handle_);
|
||||
this->update_handle_ = 0;
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ota_end failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
err = esp_ota_set_boot_partition(this->partition_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ota_set_boot_partition failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void abort() {
|
||||
if (this->update_handle_ != 0) {
|
||||
esp_ota_abort(this->update_handle_);
|
||||
this->update_handle_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
esp_ota_handle_t update_handle_{0};
|
||||
const esp_partition_t *partition_{nullptr};
|
||||
};
|
||||
#endif
|
||||
WebServerBase *global_web_server_base = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void WebServerBase::add_handler(AsyncWebHandler *handler) {
|
||||
// remove all handlers
|
||||
@@ -134,157 +23,6 @@ void WebServerBase::add_handler(AsyncWebHandler *handler) {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
|
||||
const uint32_t now = millis();
|
||||
if (now - this->last_ota_progress_ > 1000) {
|
||||
if (request->contentLength() != 0) {
|
||||
float percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
|
||||
ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_);
|
||||
}
|
||||
this->last_ota_progress_ = now;
|
||||
}
|
||||
}
|
||||
|
||||
void OTARequestHandler::schedule_ota_reboot_() {
|
||||
ESP_LOGI(TAG, "OTA update successful!");
|
||||
this->parent_->set_timeout(100, []() {
|
||||
ESP_LOGI(TAG, "Performing OTA reboot now");
|
||||
App.safe_reboot();
|
||||
});
|
||||
}
|
||||
|
||||
void OTARequestHandler::ota_init_(const char *filename) {
|
||||
ESP_LOGI(TAG, "OTA Update Start: %s", filename);
|
||||
this->ota_read_length_ = 0;
|
||||
}
|
||||
|
||||
void report_ota_error() {
|
||||
#ifdef USE_ARDUINO
|
||||
StreamString ss;
|
||||
Update.printError(ss);
|
||||
ESP_LOGW(TAG, "OTA Update failed! Error: %s", ss.c_str());
|
||||
#endif
|
||||
}
|
||||
|
||||
void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index,
|
||||
uint8_t *data, size_t len, bool final) {
|
||||
#ifdef USE_ARDUINO
|
||||
bool success;
|
||||
if (index == 0) {
|
||||
this->ota_init_(filename.c_str());
|
||||
#ifdef USE_ESP8266
|
||||
Update.runAsync(true);
|
||||
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
|
||||
success = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
|
||||
#endif
|
||||
#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_LIBRETINY)
|
||||
if (Update.isRunning()) {
|
||||
Update.abort();
|
||||
}
|
||||
success = Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH);
|
||||
#endif
|
||||
if (!success) {
|
||||
report_ota_error();
|
||||
return;
|
||||
}
|
||||
} else if (Update.hasError()) {
|
||||
// don't spam logs with errors if something failed at start
|
||||
return;
|
||||
}
|
||||
|
||||
success = Update.write(data, len) == len;
|
||||
if (!success) {
|
||||
report_ota_error();
|
||||
return;
|
||||
}
|
||||
this->ota_read_length_ += len;
|
||||
this->report_ota_progress_(request);
|
||||
|
||||
if (final) {
|
||||
if (Update.end(true)) {
|
||||
this->schedule_ota_reboot_();
|
||||
} else {
|
||||
report_ota_error();
|
||||
}
|
||||
}
|
||||
#endif // USE_ARDUINO
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
// ESP-IDF implementation
|
||||
if (index == 0 && !this->ota_backend_) {
|
||||
// Initialize OTA on first call
|
||||
this->ota_init_(filename.c_str());
|
||||
this->ota_success_ = false;
|
||||
|
||||
auto *backend = new IDFWebServerOTABackend();
|
||||
if (!backend->begin()) {
|
||||
ESP_LOGE(TAG, "OTA begin failed");
|
||||
delete backend;
|
||||
return;
|
||||
}
|
||||
this->ota_backend_ = backend;
|
||||
}
|
||||
|
||||
auto *backend = static_cast<IDFWebServerOTABackend *>(this->ota_backend_);
|
||||
if (!backend) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Process data
|
||||
if (len > 0) {
|
||||
if (!backend->write(data, len)) {
|
||||
ESP_LOGE(TAG, "OTA write failed");
|
||||
backend->abort();
|
||||
delete backend;
|
||||
this->ota_backend_ = nullptr;
|
||||
return;
|
||||
}
|
||||
this->ota_read_length_ += len;
|
||||
this->report_ota_progress_(request);
|
||||
}
|
||||
|
||||
// Finalize
|
||||
if (final) {
|
||||
this->ota_success_ = backend->end();
|
||||
if (this->ota_success_) {
|
||||
this->schedule_ota_reboot_();
|
||||
} else {
|
||||
ESP_LOGE(TAG, "OTA end failed");
|
||||
}
|
||||
delete backend;
|
||||
this->ota_backend_ = nullptr;
|
||||
}
|
||||
#endif // USE_ESP_IDF
|
||||
}
|
||||
|
||||
void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) {
|
||||
AsyncWebServerResponse *response;
|
||||
#ifdef USE_ARDUINO
|
||||
if (!Update.hasError()) {
|
||||
response = request->beginResponse(200, "text/plain", "Update Successful!");
|
||||
} else {
|
||||
StreamString ss;
|
||||
ss.print("Update Failed: ");
|
||||
Update.printError(ss);
|
||||
response = request->beginResponse(200, "text/plain", ss);
|
||||
}
|
||||
#endif // USE_ARDUINO
|
||||
#ifdef USE_ESP_IDF
|
||||
// Send response based on the OTA result
|
||||
response = request->beginResponse(200, "text/plain", this->ota_success_ ? "Update Successful!" : "Update Failed!");
|
||||
#endif // USE_ESP_IDF
|
||||
response->addHeader("Connection", "close");
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
void WebServerBase::add_ota_handler() {
|
||||
this->add_handler(new OTARequestHandler(this)); // NOLINT
|
||||
}
|
||||
#endif
|
||||
|
||||
float WebServerBase::get_setup_priority() const {
|
||||
// Before WiFi (captive portal)
|
||||
return setup_priority::WIFI + 2.0f;
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
namespace esphome {
|
||||
namespace web_server_base {
|
||||
|
||||
class WebServerBase;
|
||||
extern WebServerBase *global_web_server_base; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
namespace internal {
|
||||
|
||||
class MiddlewareHandler : public AsyncWebHandler {
|
||||
@@ -110,18 +113,10 @@ class WebServerBase : public Component {
|
||||
|
||||
void add_handler(AsyncWebHandler *handler);
|
||||
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
void add_ota_handler();
|
||||
#endif
|
||||
|
||||
void set_port(uint16_t port) { port_ = port; }
|
||||
uint16_t get_port() const { return port_; }
|
||||
|
||||
protected:
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
friend class OTARequestHandler;
|
||||
#endif
|
||||
|
||||
int initialized_{0};
|
||||
uint16_t port_{80};
|
||||
std::shared_ptr<AsyncWebServer> server_{nullptr};
|
||||
@@ -129,37 +124,6 @@ class WebServerBase : public Component {
|
||||
internal::Credentials credentials_;
|
||||
};
|
||||
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
class OTARequestHandler : public AsyncWebHandler {
|
||||
public:
|
||||
OTARequestHandler(WebServerBase *parent) : parent_(parent) {}
|
||||
void handleRequest(AsyncWebServerRequest *request) override;
|
||||
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
|
||||
bool final) override;
|
||||
bool canHandle(AsyncWebServerRequest *request) const override {
|
||||
return request->url() == "/update" && request->method() == HTTP_POST;
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
bool isRequestHandlerTrivial() const override { return false; }
|
||||
|
||||
protected:
|
||||
void report_ota_progress_(AsyncWebServerRequest *request);
|
||||
void schedule_ota_reboot_();
|
||||
void ota_init_(const char *filename);
|
||||
|
||||
uint32_t last_ota_progress_{0};
|
||||
uint32_t ota_read_length_{0};
|
||||
WebServerBase *parent_;
|
||||
|
||||
private:
|
||||
#ifdef USE_ESP_IDF
|
||||
void *ota_backend_{nullptr};
|
||||
bool ota_success_{false};
|
||||
#endif
|
||||
};
|
||||
#endif // USE_WEBSERVER_OTA
|
||||
|
||||
} // namespace web_server_base
|
||||
} // namespace esphome
|
||||
#endif
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_OTA, CONF_WEB_SERVER
|
||||
from esphome.core import CORE
|
||||
|
||||
CODEOWNERS = ["@dentra"]
|
||||
|
||||
@@ -14,7 +12,3 @@ CONFIG_SCHEMA = cv.All(
|
||||
async def to_code(config):
|
||||
# Increase the maximum supported size of headers section in HTTP request packet to be processed by the server
|
||||
add_idf_sdkconfig_option("CONFIG_HTTPD_MAX_REQ_HDR_LEN", 1024)
|
||||
# Check if web_server component has OTA enabled
|
||||
if CORE.config.get(CONF_WEB_SERVER, {}).get(CONF_OTA, True):
|
||||
# Add multipart parser component for ESP-IDF OTA support
|
||||
add_idf_component(name="zorxx/multipart-parser", ref="1.0.1")
|
||||
|
||||
@@ -67,6 +67,42 @@ ConfigPath = list[str | int]
|
||||
path_context = contextvars.ContextVar("Config path")
|
||||
|
||||
|
||||
def _process_platform_config(
|
||||
result: Config,
|
||||
component_name: str,
|
||||
platform_name: str,
|
||||
platform_config: ConfigType,
|
||||
path: ConfigPath,
|
||||
) -> None:
|
||||
"""Process a platform configuration and add necessary validation steps.
|
||||
|
||||
This is shared between LoadValidationStep and AutoLoadValidationStep to avoid duplication.
|
||||
"""
|
||||
# Get the platform manifest
|
||||
platform = get_platform(component_name, platform_name)
|
||||
if platform is None:
|
||||
result.add_str_error(
|
||||
f"Platform not found: '{component_name}.{platform_name}'", path
|
||||
)
|
||||
return
|
||||
|
||||
# Add platform to loaded integrations
|
||||
CORE.loaded_integrations.add(platform_name)
|
||||
CORE.loaded_platforms.add(f"{component_name}/{platform_name}")
|
||||
|
||||
# Process platform's AUTO_LOAD
|
||||
for load in platform.auto_load:
|
||||
if load not in result:
|
||||
result.add_validation_step(AutoLoadValidationStep(load))
|
||||
|
||||
# Add validation steps for the platform
|
||||
p_domain = f"{component_name}.{platform_name}"
|
||||
result.add_output_path(path, p_domain)
|
||||
result.add_validation_step(
|
||||
MetadataValidationStep(path, p_domain, platform_config, platform)
|
||||
)
|
||||
|
||||
|
||||
def _path_begins_with(path: ConfigPath, other: ConfigPath) -> bool:
|
||||
if len(path) < len(other):
|
||||
return False
|
||||
@@ -379,26 +415,11 @@ class LoadValidationStep(ConfigValidationStep):
|
||||
path,
|
||||
)
|
||||
continue
|
||||
# Remove temp output path and construct new one
|
||||
# Remove temp output path
|
||||
result.remove_output_path(path, p_domain)
|
||||
p_domain = f"{self.domain}.{p_name}"
|
||||
result.add_output_path(path, p_domain)
|
||||
# Try Load platform
|
||||
platform = get_platform(self.domain, p_name)
|
||||
if platform is None:
|
||||
result.add_str_error(f"Platform not found: '{p_domain}'", path)
|
||||
continue
|
||||
CORE.loaded_integrations.add(p_name)
|
||||
CORE.loaded_platforms.add(f"{self.domain}/{p_name}")
|
||||
|
||||
# Process AUTO_LOAD
|
||||
for load in platform.auto_load:
|
||||
if load not in result:
|
||||
result.add_validation_step(AutoLoadValidationStep(load))
|
||||
|
||||
result.add_validation_step(
|
||||
MetadataValidationStep(path, p_domain, p_config, platform)
|
||||
)
|
||||
# Process the platform configuration
|
||||
_process_platform_config(result, self.domain, p_name, p_config, path)
|
||||
|
||||
|
||||
class AutoLoadValidationStep(ConfigValidationStep):
|
||||
@@ -413,10 +434,56 @@ class AutoLoadValidationStep(ConfigValidationStep):
|
||||
self.domain = domain
|
||||
|
||||
def run(self, result: Config) -> None:
|
||||
if self.domain in result:
|
||||
# already loaded
|
||||
# Regular component auto-load (no platform)
|
||||
if "." not in self.domain:
|
||||
if self.domain in result:
|
||||
# already loaded
|
||||
return
|
||||
result.add_validation_step(LoadValidationStep(self.domain, core.AutoLoad()))
|
||||
return
|
||||
result.add_validation_step(LoadValidationStep(self.domain, core.AutoLoad()))
|
||||
|
||||
# Platform-specific auto-load (e.g., "ota.web_server")
|
||||
component_name, _, platform_name = self.domain.partition(".")
|
||||
|
||||
# Check if component exists
|
||||
if component_name not in result:
|
||||
# Component doesn't exist, load it first
|
||||
result.add_validation_step(LoadValidationStep(component_name, []))
|
||||
# Re-run this step after the component is loaded
|
||||
result.add_validation_step(AutoLoadValidationStep(self.domain))
|
||||
return
|
||||
|
||||
# Component exists, check if it's a platform component
|
||||
component = get_component(component_name)
|
||||
if component is None or not component.is_platform_component:
|
||||
result.add_str_error(
|
||||
f"Component {component_name} is not a platform component, "
|
||||
f"cannot auto-load platform {platform_name}",
|
||||
[component_name],
|
||||
)
|
||||
return
|
||||
|
||||
# Ensure the component config is a list
|
||||
component_conf = result.get(component_name)
|
||||
if not isinstance(component_conf, list):
|
||||
component_conf = result[component_name] = []
|
||||
|
||||
# Check if platform already exists
|
||||
if any(
|
||||
isinstance(conf, dict) and conf.get(CONF_PLATFORM) == platform_name
|
||||
for conf in component_conf
|
||||
):
|
||||
return
|
||||
|
||||
# Add and process the platform configuration
|
||||
platform_conf = core.AutoLoad()
|
||||
platform_conf[CONF_PLATFORM] = platform_name
|
||||
component_conf.append(platform_conf)
|
||||
|
||||
path = [component_name, len(component_conf) - 1]
|
||||
_process_platform_config(
|
||||
result, component_name, platform_name, platform_conf, path
|
||||
)
|
||||
|
||||
|
||||
class MetadataValidationStep(ConfigValidationStep):
|
||||
|
||||
@@ -84,6 +84,10 @@ void Application::setup() {
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "setup() finished successfully!");
|
||||
|
||||
// Clear setup priority overrides to free memory
|
||||
clear_setup_priority_overrides();
|
||||
|
||||
this->schedule_dump_config();
|
||||
this->calculate_looping_components_();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
#include <cinttypes>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -12,6 +14,30 @@ namespace esphome {
|
||||
|
||||
static const char *const TAG = "component";
|
||||
|
||||
// Global vectors for component data that doesn't belong in every instance.
|
||||
// Using vector instead of unordered_map for both because:
|
||||
// - Much lower memory overhead (8 bytes per entry vs 20+ for unordered_map)
|
||||
// - Linear search is fine for small n (typically < 5 entries)
|
||||
// - These are rarely accessed (setup only or error cases only)
|
||||
|
||||
// Component error messages - only stores messages for failed components
|
||||
// Lazy allocated since most configs have zero failures
|
||||
// Note: We don't clear this vector because:
|
||||
// 1. Components are never destroyed in ESPHome
|
||||
// 2. Failed components remain failed (no recovery mechanism)
|
||||
// 3. Memory usage is minimal (only failures with custom messages are stored)
|
||||
static std::unique_ptr<std::vector<std::pair<const Component *, const char *>>> &get_component_error_messages() {
|
||||
static std::unique_ptr<std::vector<std::pair<const Component *, const char *>>> instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Setup priority overrides - freed after setup completes
|
||||
// Typically < 5 entries, lazy allocated
|
||||
static std::unique_ptr<std::vector<std::pair<const Component *, float>>> &get_setup_priority_overrides() {
|
||||
static std::unique_ptr<std::vector<std::pair<const Component *, float>>> instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
namespace setup_priority {
|
||||
|
||||
const float BUS = 1000.0f;
|
||||
@@ -102,8 +128,17 @@ void Component::call_setup() { this->setup(); }
|
||||
void Component::call_dump_config() {
|
||||
this->dump_config();
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, " Component %s is marked FAILED: %s", this->get_component_source(),
|
||||
this->error_message_ ? this->error_message_ : "unspecified");
|
||||
// Look up error message from global vector
|
||||
const char *error_msg = "unspecified";
|
||||
if (get_component_error_messages()) {
|
||||
for (const auto &pair : *get_component_error_messages()) {
|
||||
if (pair.first == this) {
|
||||
error_msg = pair.second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ESP_LOGE(TAG, " Component %s is marked FAILED: %s", this->get_component_source(), error_msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,8 +280,21 @@ void Component::status_set_error(const char *message) {
|
||||
this->component_state_ |= STATUS_LED_ERROR;
|
||||
App.app_state_ |= STATUS_LED_ERROR;
|
||||
ESP_LOGE(TAG, "Component %s set Error flag: %s", this->get_component_source(), message);
|
||||
if (strcmp(message, "unspecified") != 0)
|
||||
this->error_message_ = message;
|
||||
if (strcmp(message, "unspecified") != 0) {
|
||||
// Lazy allocate the error messages vector if needed
|
||||
if (!get_component_error_messages()) {
|
||||
get_component_error_messages() = std::make_unique<std::vector<std::pair<const Component *, const char *>>>();
|
||||
}
|
||||
// Check if this component already has an error message
|
||||
for (auto &pair : *get_component_error_messages()) {
|
||||
if (pair.first == this) {
|
||||
pair.second = message;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Add new error message
|
||||
get_component_error_messages()->emplace_back(this, message);
|
||||
}
|
||||
}
|
||||
void Component::status_clear_warning() {
|
||||
if ((this->component_state_ & STATUS_LED_WARNING) == 0)
|
||||
@@ -270,11 +318,36 @@ void Component::status_momentary_error(const std::string &name, uint32_t length)
|
||||
}
|
||||
void Component::dump_config() {}
|
||||
float Component::get_actual_setup_priority() const {
|
||||
if (std::isnan(this->setup_priority_override_))
|
||||
return this->get_setup_priority();
|
||||
return this->setup_priority_override_;
|
||||
// Check if there's an override in the global vector
|
||||
if (get_setup_priority_overrides()) {
|
||||
// Linear search is fine for small n (typically < 5 overrides)
|
||||
for (const auto &pair : *get_setup_priority_overrides()) {
|
||||
if (pair.first == this) {
|
||||
return pair.second;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this->get_setup_priority();
|
||||
}
|
||||
void Component::set_setup_priority(float priority) {
|
||||
// Lazy allocate the vector if needed
|
||||
if (!get_setup_priority_overrides()) {
|
||||
get_setup_priority_overrides() = std::make_unique<std::vector<std::pair<const Component *, float>>>();
|
||||
// Reserve some space to avoid reallocations (most configs have < 10 overrides)
|
||||
get_setup_priority_overrides()->reserve(10);
|
||||
}
|
||||
|
||||
// Check if this component already has an override
|
||||
for (auto &pair : *get_setup_priority_overrides()) {
|
||||
if (pair.first == this) {
|
||||
pair.second = priority;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Add new override
|
||||
get_setup_priority_overrides()->emplace_back(this, priority);
|
||||
}
|
||||
void Component::set_setup_priority(float priority) { this->setup_priority_override_ = priority; }
|
||||
|
||||
bool Component::has_overridden_loop() const {
|
||||
#if defined(USE_HOST) || defined(CLANG_TIDY)
|
||||
@@ -336,4 +409,9 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
|
||||
|
||||
WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {}
|
||||
|
||||
void clear_setup_priority_overrides() {
|
||||
// Free the setup priority map completely
|
||||
get_setup_priority_overrides().reset();
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -387,9 +387,7 @@ class Component {
|
||||
bool cancel_defer(const std::string &name); // NOLINT
|
||||
|
||||
// Ordered for optimal packing on 32-bit systems
|
||||
float setup_priority_override_{NAN};
|
||||
const char *component_source_{nullptr};
|
||||
const char *error_message_{nullptr};
|
||||
uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s)
|
||||
/// State of this component - each bit has a purpose:
|
||||
/// Bits 0-1: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED)
|
||||
@@ -459,4 +457,7 @@ class WarnIfComponentBlockingGuard {
|
||||
Component *component_;
|
||||
};
|
||||
|
||||
// Function to clear setup priority overrides after all components are set up
|
||||
void clear_setup_priority_overrides();
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <strings.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <strings.h>
|
||||
|
||||
#ifdef USE_HOST
|
||||
#ifndef _WIN32
|
||||
@@ -43,10 +43,10 @@
|
||||
#include <random>
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
#include "rom/crc.h"
|
||||
#include "esp_mac.h"
|
||||
#include "esp_efuse.h"
|
||||
#include "esp_efuse_table.h"
|
||||
#include "esp_mac.h"
|
||||
#include "rom/crc.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_LIBRETINY
|
||||
@@ -393,6 +393,21 @@ std::string format_hex_pretty(const uint16_t *data, size_t length) {
|
||||
return ret;
|
||||
}
|
||||
std::string format_hex_pretty(const std::vector<uint16_t> &data) { return format_hex_pretty(data.data(), data.size()); }
|
||||
std::string format_hex_pretty(const std::string &data) {
|
||||
if (data.empty())
|
||||
return "";
|
||||
std::string ret;
|
||||
ret.resize(3 * data.length() - 1);
|
||||
for (size_t i = 0; i < data.length(); i++) {
|
||||
ret[3 * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
|
||||
ret[3 * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
|
||||
if (i != data.length() - 1)
|
||||
ret[3 * i + 2] = '.';
|
||||
}
|
||||
if (data.length() > 4)
|
||||
return ret + " (" + std::to_string(data.length()) + ")";
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string format_bin(const uint8_t *data, size_t length) {
|
||||
std::string result;
|
||||
|
||||
@@ -348,6 +348,8 @@ std::string format_hex_pretty(const uint16_t *data, size_t length);
|
||||
std::string format_hex_pretty(const std::vector<uint8_t> &data);
|
||||
/// Format the vector \p data in pretty-printed, human-readable hex.
|
||||
std::string format_hex_pretty(const std::vector<uint16_t> &data);
|
||||
/// Format the string \p data in pretty-printed, human-readable hex.
|
||||
std::string format_hex_pretty(const std::string &data);
|
||||
/// Format an unsigned integer in pretty-printed, human-readable hex, starting with the most significant byte.
|
||||
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_hex_pretty(T val) {
|
||||
val = convert_big_endian(val);
|
||||
|
||||
@@ -5,6 +5,7 @@ import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE
|
||||
from esphome.core import CORE, EsphomeError
|
||||
@@ -104,7 +105,16 @@ def run_compile(config, verbose):
|
||||
args = []
|
||||
if CONF_COMPILE_PROCESS_LIMIT in config[CONF_ESPHOME]:
|
||||
args += [f"-j{config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]}"]
|
||||
return run_platformio_cli_run(config, verbose, *args)
|
||||
result = run_platformio_cli_run(config, verbose, *args)
|
||||
|
||||
# Run memory analysis if enabled
|
||||
if config.get(CONF_ESPHOME, {}).get("analyze_memory", False):
|
||||
try:
|
||||
analyze_memory_usage(config)
|
||||
except Exception as e:
|
||||
_LOGGER.warning("Failed to analyze memory usage: %s", e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _run_idedata(config):
|
||||
@@ -331,3 +341,93 @@ class IDEData:
|
||||
return f"{self.cc_path[:-7]}addr2line.exe"
|
||||
|
||||
return f"{self.cc_path[:-3]}addr2line"
|
||||
|
||||
@property
|
||||
def objdump_path(self) -> str:
|
||||
# replace gcc at end with objdump
|
||||
|
||||
# Windows
|
||||
if self.cc_path.endswith(".exe"):
|
||||
return f"{self.cc_path[:-7]}objdump.exe"
|
||||
|
||||
return f"{self.cc_path[:-3]}objdump"
|
||||
|
||||
@property
|
||||
def readelf_path(self) -> str:
|
||||
# replace gcc at end with readelf
|
||||
|
||||
# Windows
|
||||
if self.cc_path.endswith(".exe"):
|
||||
return f"{self.cc_path[:-7]}readelf.exe"
|
||||
|
||||
return f"{self.cc_path[:-3]}readelf"
|
||||
|
||||
|
||||
def analyze_memory_usage(config: dict[str, Any]) -> None:
|
||||
"""Analyze memory usage by component after compilation."""
|
||||
# Lazy import to avoid overhead when not needed
|
||||
from esphome.analyze_memory import MemoryAnalyzer
|
||||
|
||||
idedata = get_idedata(config)
|
||||
|
||||
# Get paths to tools
|
||||
elf_path = idedata.firmware_elf_path
|
||||
objdump_path = idedata.objdump_path
|
||||
readelf_path = idedata.readelf_path
|
||||
|
||||
# Debug logging
|
||||
_LOGGER.debug("ELF path from idedata: %s", elf_path)
|
||||
|
||||
# Check if file exists
|
||||
if not Path(elf_path).exists():
|
||||
# Try alternate path
|
||||
alt_path = Path(CORE.relative_build_path(".pioenvs", CORE.name, "firmware.elf"))
|
||||
if alt_path.exists():
|
||||
elf_path = str(alt_path)
|
||||
_LOGGER.debug("Using alternate ELF path: %s", elf_path)
|
||||
else:
|
||||
_LOGGER.warning("ELF file not found at %s or %s", elf_path, alt_path)
|
||||
return
|
||||
|
||||
# Extract external components from config
|
||||
external_components = set()
|
||||
|
||||
# Get the list of built-in ESPHome components
|
||||
from esphome.analyze_memory import get_esphome_components
|
||||
|
||||
builtin_components = get_esphome_components()
|
||||
|
||||
# Special non-component keys that appear in configs
|
||||
NON_COMPONENT_KEYS = {
|
||||
CONF_ESPHOME,
|
||||
"substitutions",
|
||||
"packages",
|
||||
"globals",
|
||||
"<<",
|
||||
}
|
||||
|
||||
# Check all top-level keys in config
|
||||
for key in config:
|
||||
if key not in builtin_components and key not in NON_COMPONENT_KEYS:
|
||||
# This is an external component
|
||||
external_components.add(key)
|
||||
|
||||
_LOGGER.debug("Detected external components: %s", external_components)
|
||||
|
||||
# Create analyzer and run analysis
|
||||
analyzer = MemoryAnalyzer(elf_path, objdump_path, readelf_path, external_components)
|
||||
analyzer.analyze()
|
||||
|
||||
# Generate and print report
|
||||
report = analyzer.generate_report()
|
||||
_LOGGER.info("\n%s", report)
|
||||
|
||||
# Optionally save to file
|
||||
if config.get(CONF_ESPHOME, {}).get("memory_report_file"):
|
||||
report_file = Path(config[CONF_ESPHOME]["memory_report_file"])
|
||||
if report_file.suffix == ".json":
|
||||
report_file.write_text(analyzer.to_json())
|
||||
_LOGGER.info("Memory report saved to %s", report_file)
|
||||
else:
|
||||
report_file.write_text(report)
|
||||
_LOGGER.info("Memory report saved to %s", report_file)
|
||||
|
||||
@@ -530,7 +530,7 @@ class BytesType(TypeInfo):
|
||||
wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2
|
||||
|
||||
def dump(self, name: str) -> str:
|
||||
o = f'out.append("\'").append({name}).append("\'");'
|
||||
o = f"out.append(format_hex_pretty({name}));"
|
||||
return o
|
||||
|
||||
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
||||
@@ -1255,6 +1255,7 @@ def main() -> None:
|
||||
#include "api_pb2.h"
|
||||
#include "api_pb2_size.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
|
||||
102
tests/component_tests/ota/test_web_server_ota.py
Normal file
102
tests/component_tests/ota/test_web_server_ota.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""Tests for the web_server OTA platform."""
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None:
|
||||
"""Test that web_server OTA platform generates correct code."""
|
||||
main_cpp = generate_main("tests/component_tests/ota/test_web_server_ota.yaml")
|
||||
|
||||
# Check that the web server OTA component is included
|
||||
assert "WebServerOTAComponent" in main_cpp
|
||||
assert "web_server::WebServerOTAComponent" in main_cpp
|
||||
|
||||
# Check that global web server base is referenced
|
||||
assert "global_web_server_base" in main_cpp
|
||||
|
||||
# Check component is registered
|
||||
assert "App.register_component(web_server_webserverotacomponent_id)" in main_cpp
|
||||
|
||||
|
||||
def test_web_server_ota_with_callbacks(generate_main: Callable[[str], str]) -> None:
|
||||
"""Test web_server OTA with state callbacks."""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/ota/test_web_server_ota_callbacks.yaml"
|
||||
)
|
||||
|
||||
# Check that web server OTA component is present
|
||||
assert "WebServerOTAComponent" in main_cpp
|
||||
|
||||
# Check that callbacks are configured
|
||||
# The actual callback code is in the component implementation, not main.cpp
|
||||
# But we can check that logger.log statements are present from the callbacks
|
||||
assert "logger.log" in main_cpp
|
||||
assert "OTA started" in main_cpp
|
||||
assert "OTA completed" in main_cpp
|
||||
assert "OTA error" in main_cpp
|
||||
|
||||
|
||||
def test_web_server_ota_idf_multipart(generate_main: Callable[[str], str]) -> None:
|
||||
"""Test that ESP-IDF builds include multipart parser dependency."""
|
||||
main_cpp = generate_main("tests/component_tests/ota/test_web_server_ota_idf.yaml")
|
||||
|
||||
# Check that web server OTA component is present
|
||||
assert "WebServerOTAComponent" in main_cpp
|
||||
|
||||
# For ESP-IDF builds, the framework type is esp-idf
|
||||
# The multipart parser dependency is added by web_server_idf
|
||||
assert "web_server::WebServerOTAComponent" in main_cpp
|
||||
|
||||
|
||||
def test_web_server_ota_without_web_server_fails(
|
||||
generate_main: Callable[[str], str],
|
||||
) -> None:
|
||||
"""Test that web_server OTA requires web_server component."""
|
||||
# This should fail during validation since web_server_base is required
|
||||
# but we can't test validation failures with generate_main
|
||||
# Instead, verify that both components are needed in valid config
|
||||
main_cpp = generate_main("tests/component_tests/ota/test_web_server_ota.yaml")
|
||||
|
||||
# Both web server and OTA components should be present
|
||||
assert "WebServer" in main_cpp
|
||||
assert "WebServerOTAComponent" in main_cpp
|
||||
|
||||
|
||||
def test_multiple_ota_platforms(generate_main: Callable[[str], str]) -> None:
|
||||
"""Test multiple OTA platforms can coexist."""
|
||||
main_cpp = generate_main("tests/component_tests/ota/test_web_server_ota_multi.yaml")
|
||||
|
||||
# Check all OTA platforms are included
|
||||
assert "WebServerOTAComponent" in main_cpp
|
||||
assert "ESPHomeOTAComponent" in main_cpp
|
||||
assert "OtaHttpRequestComponent" in main_cpp
|
||||
|
||||
# Check components are from correct namespaces
|
||||
assert "web_server::WebServerOTAComponent" in main_cpp
|
||||
assert "esphome::ESPHomeOTAComponent" in main_cpp
|
||||
assert "http_request::OtaHttpRequestComponent" in main_cpp
|
||||
|
||||
|
||||
def test_web_server_ota_arduino_with_auth(generate_main: Callable[[str], str]) -> None:
|
||||
"""Test web_server OTA with Arduino framework and authentication."""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/ota/test_web_server_ota_arduino.yaml"
|
||||
)
|
||||
|
||||
# Check web server OTA component is present
|
||||
assert "WebServerOTAComponent" in main_cpp
|
||||
|
||||
# Check authentication is set up for web server
|
||||
assert "set_auth_username" in main_cpp
|
||||
assert "set_auth_password" in main_cpp
|
||||
|
||||
|
||||
def test_web_server_ota_esp8266(generate_main: Callable[[str], str]) -> None:
|
||||
"""Test web_server OTA on ESP8266 platform."""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/ota/test_web_server_ota_esp8266.yaml"
|
||||
)
|
||||
|
||||
# Check web server OTA component is present
|
||||
assert "WebServerOTAComponent" in main_cpp
|
||||
assert "web_server::WebServerOTAComponent" in main_cpp
|
||||
15
tests/component_tests/ota/test_web_server_ota.yaml
Normal file
15
tests/component_tests/ota/test_web_server_ota.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: test_web_server_ota
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
web_server:
|
||||
port: 80
|
||||
|
||||
ota:
|
||||
- platform: web_server
|
||||
18
tests/component_tests/ota/test_web_server_ota_arduino.yaml
Normal file
18
tests/component_tests/ota/test_web_server_ota_arduino.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
esphome:
|
||||
name: test_web_server_ota_arduino
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
web_server:
|
||||
port: 80
|
||||
auth:
|
||||
username: admin
|
||||
password: admin
|
||||
|
||||
ota:
|
||||
- platform: web_server
|
||||
31
tests/component_tests/ota/test_web_server_ota_callbacks.yaml
Normal file
31
tests/component_tests/ota/test_web_server_ota_callbacks.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
esphome:
|
||||
name: test_web_server_ota_callbacks
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
logger:
|
||||
|
||||
web_server:
|
||||
port: 80
|
||||
|
||||
ota:
|
||||
- platform: web_server
|
||||
on_begin:
|
||||
- logger.log: "OTA started"
|
||||
on_progress:
|
||||
- logger.log:
|
||||
format: "OTA progress: %.1f%%"
|
||||
args: ["x"]
|
||||
on_end:
|
||||
- logger.log: "OTA completed"
|
||||
on_error:
|
||||
- logger.log:
|
||||
format: "OTA error: %d"
|
||||
args: ["x"]
|
||||
on_state_change:
|
||||
- logger.log: "OTA state changed"
|
||||
15
tests/component_tests/ota/test_web_server_ota_esp8266.yaml
Normal file
15
tests/component_tests/ota/test_web_server_ota_esp8266.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: test_web_server_ota_esp8266
|
||||
|
||||
esp8266:
|
||||
board: nodemcuv2
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
web_server:
|
||||
port: 80
|
||||
|
||||
ota:
|
||||
- platform: web_server
|
||||
17
tests/component_tests/ota/test_web_server_ota_idf.yaml
Normal file
17
tests/component_tests/ota/test_web_server_ota_idf.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
esphome:
|
||||
name: test_web_server_ota_idf
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
web_server:
|
||||
port: 80
|
||||
|
||||
ota:
|
||||
- platform: web_server
|
||||
21
tests/component_tests/ota/test_web_server_ota_multi.yaml
Normal file
21
tests/component_tests/ota/test_web_server_ota_multi.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
esphome:
|
||||
name: test_web_server_ota_multi
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
web_server:
|
||||
port: 80
|
||||
|
||||
http_request:
|
||||
verify_ssl: false
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "test_password"
|
||||
- platform: web_server
|
||||
- platform: http_request
|
||||
38
tests/component_tests/web_server/test_ota_migration.py
Normal file
38
tests/component_tests/web_server/test_ota_migration.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Tests for web_server OTA migration validation."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
|
||||
def test_web_server_ota_true_fails_validation() -> None:
|
||||
"""Test that web_server with ota: true fails validation with helpful message."""
|
||||
from esphome.components.web_server import validate_ota_removed
|
||||
|
||||
# Config with ota: true should fail
|
||||
config: ConfigType = {"ota": True}
|
||||
|
||||
with pytest.raises(cv.Invalid) as exc_info:
|
||||
validate_ota_removed(config)
|
||||
|
||||
# Check error message contains migration instructions
|
||||
error_msg = str(exc_info.value)
|
||||
assert "has been removed from 'web_server'" in error_msg
|
||||
assert "platform: web_server" in error_msg
|
||||
assert "ota:" in error_msg
|
||||
|
||||
|
||||
def test_web_server_ota_false_passes_validation() -> None:
|
||||
"""Test that web_server with ota: false passes validation."""
|
||||
from esphome.components.web_server import validate_ota_removed
|
||||
|
||||
# Config with ota: false should pass
|
||||
config: ConfigType = {"ota": False}
|
||||
result = validate_ota_removed(config)
|
||||
assert result == config
|
||||
|
||||
# Config without ota should also pass
|
||||
config: ConfigType = {}
|
||||
result = validate_ota_removed(config)
|
||||
assert result == config
|
||||
@@ -1,3 +1,11 @@
|
||||
esphome:
|
||||
name: test-web-server-no-ota-idf
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
packages:
|
||||
device_base: !include common.yaml
|
||||
|
||||
@@ -6,4 +14,3 @@ packages:
|
||||
web_server:
|
||||
port: 8080
|
||||
version: 2
|
||||
ota: false
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# Test configuration for ESP-IDF web server with OTA enabled
|
||||
esphome:
|
||||
name: test-web-server-ota-idf
|
||||
|
||||
# Force ESP-IDF framework
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
@@ -15,17 +13,17 @@ packages:
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "test_ota_password"
|
||||
- platform: web_server
|
||||
|
||||
# Web server with OTA enabled
|
||||
# Web server configuration
|
||||
web_server:
|
||||
port: 8080
|
||||
version: 2
|
||||
ota: true
|
||||
include_internal: true
|
||||
|
||||
# Enable debug logging for OTA
|
||||
logger:
|
||||
level: DEBUG
|
||||
level: VERBOSE
|
||||
logs:
|
||||
web_server: VERBOSE
|
||||
web_server_idf: VERBOSE
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
esphome:
|
||||
name: test-ws-ota-disabled-idf
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
packages:
|
||||
device_base: !include common.yaml
|
||||
|
||||
# OTA is configured but web_server OTA is disabled
|
||||
# OTA is configured but web_server OTA is NOT included
|
||||
ota:
|
||||
- platform: esphome
|
||||
|
||||
web_server:
|
||||
port: 8080
|
||||
version: 2
|
||||
ota: false
|
||||
|
||||
Reference in New Issue
Block a user