Compare commits

...

31 Commits

Author SHA1 Message Date
J. Nick Koston
0cccbd7797 Merge branch 'ota_base_extract' into ota_base_extract_bk 2025-07-01 16:33:42 -05:00
J. Nick Koston
e920ffbef3 preen 2025-07-01 16:33:27 -05:00
J. Nick Koston
31b5a9a127 Merge branch 'ota_base_extract' into ota_base_extract_bk 2025-07-01 16:30:16 -05:00
J. Nick Koston
6395567f2a type 2025-07-01 16:19:18 -05:00
J. Nick Koston
c8e7a945ae Apply suggestions from code review 2025-07-01 16:14:59 -05:00
J. Nick Koston
f2b5a8e3b1 tweak 2025-07-01 16:09:57 -05:00
J. Nick Koston
0a836c7001 tweak 2025-07-01 16:06:31 -05:00
J. Nick Koston
69819cdcc5 fix import 2025-07-01 15:58:54 -05:00
J. Nick Koston
1756017181 fix import 2025-07-01 15:50:34 -05:00
J. Nick Koston
11b5b00cd6 fix import 2025-07-01 15:47:25 -05:00
J. Nick Koston
2c349cad8b fix import 2025-07-01 15:46:39 -05:00
J. Nick Koston
08f563167f typing 2025-07-01 15:44:32 -05:00
J. Nick Koston
8713945c6a typing 2025-07-01 15:44:17 -05:00
J. Nick Koston
e7e6b7f89b typing 2025-07-01 15:44:08 -05:00
J. Nick Koston
4ec8cd9611 revert 2025-07-01 15:43:55 -05:00
J. Nick Koston
b89c5751b1 Merge branch 'ota_base_extract' of https://github.com/esphome/esphome into ota_base_extract 2025-07-01 15:43:42 -05:00
J. Nick Koston
9d1e68e45d Apply suggestions from code review 2025-07-01 15:42:28 -05:00
J. Nick Koston
bc0a248149 Merge branch 'ota_base_extract' of https://github.com/esphome/esphome into ota_base_extract 2025-07-01 15:40:49 -05:00
J. Nick Koston
ec99692963 Apply suggestions from code review 2025-07-01 15:40:40 -05:00
J. Nick Koston
77f7816114 Update esphome/components/ota/__init__.py 2025-07-01 15:40:18 -05:00
J. Nick Koston
f3e7a6887b revert 2025-07-01 15:40:08 -05:00
J. Nick Koston
401523c854 revert 2025-07-01 15:39:43 -05:00
J. Nick Koston
5b0c249272 revert 2025-07-01 15:38:47 -05:00
J. Nick Koston
3f650b2c15 more relos 2025-07-01 15:35:40 -05:00
J. Nick Koston
a1e3b67683 Update CODEOWNERS 2025-07-01 15:33:28 -05:00
J. Nick Koston
6e45b9d2dd fixes 2025-07-01 15:31:29 -05:00
J. Nick Koston
c008e6aa1c revert ota_base changes, move to platform 2025-07-01 15:28:11 -05:00
J. Nick Koston
5f764fc019 fix 2025-07-01 11:39:34 -05:00
J. Nick Koston
fc5ab71772 Merge branch 'ota_base_extract' into ota_base_extract_bk 2025-07-01 11:29:51 -05:00
J. Nick Koston
9632f0248e Merge branch 'bk7200_tagged_pointer_fix' into ota_base_extract_bk 2025-07-01 11:21:05 -05:00
J. Nick Koston
b000b1b70c Fix regression: BK7231N devices not returning entities via API 2025-07-01 09:43:50 -05:00
52 changed files with 782 additions and 514 deletions

View File

@@ -327,7 +327,6 @@ esphome/components/opentherm/* @olegtarasov
esphome/components/openthread/* @mrene
esphome/components/opt3001/* @ccutrer
esphome/components/ota/* @esphome/core
esphome/components/ota_base/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/packet_transport/* @clydebarrow
esphome/components/pca6416a/* @Mat931
@@ -499,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

View File

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

View File

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

View File

@@ -18,7 +18,7 @@
#include <cinttypes>
#ifdef USE_OTA
#include "esphome/components/ota_base/ota_backend.h"
#include "esphome/components/ota/ota_backend.h"
#endif
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
@@ -61,9 +61,9 @@ void ESP32BLETracker::setup() {
global_esp32_ble_tracker = this;
#ifdef USE_OTA
ota_base::get_global_ota_callback()->add_on_state_callback(
[this](ota_base::OTAState state, float progress, uint8_t error, ota_base::OTAComponent *comp) {
if (state == ota_base::OTA_STARTED) {
ota::get_global_ota_callback()->add_on_state_callback(
[this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) {
this->stop_scan();
for (auto *client : this->clients_) {
client->disconnect();

View File

@@ -1,8 +1,7 @@
import logging
import esphome.codegen as cg
from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code
from esphome.components.ota_base import OTAComponent
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
from esphome.config_helpers import merge_config
import esphome.config_validation as cv
from esphome.const import (

View File

@@ -2,12 +2,12 @@
#ifdef USE_OTA
#include "esphome/components/md5/md5.h"
#include "esphome/components/network/util.h"
#include "esphome/components/ota_base/ota_backend.h" // For OTAComponent and callbacks
#include "esphome/components/ota_base/ota_backend_arduino_esp32.h"
#include "esphome/components/ota_base/ota_backend_arduino_esp8266.h"
#include "esphome/components/ota_base/ota_backend_arduino_libretiny.h"
#include "esphome/components/ota_base/ota_backend_arduino_rp2040.h"
#include "esphome/components/ota_base/ota_backend_esp_idf.h"
#include "esphome/components/ota/ota_backend.h"
#include "esphome/components/ota/ota_backend_arduino_esp32.h"
#include "esphome/components/ota/ota_backend_arduino_esp8266.h"
#include "esphome/components/ota/ota_backend_arduino_libretiny.h"
#include "esphome/components/ota/ota_backend_arduino_rp2040.h"
#include "esphome/components/ota/ota_backend_esp_idf.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
@@ -23,7 +23,7 @@ static constexpr u_int16_t OTA_BLOCK_SIZE = 8192;
void ESPHomeOTAComponent::setup() {
#ifdef USE_OTA_STATE_CALLBACK
ota_base::register_ota_platform(this);
ota::register_ota_platform(this);
#endif
this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
@@ -94,7 +94,7 @@ void ESPHomeOTAComponent::loop() {
static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01;
void ESPHomeOTAComponent::handle_() {
ota_base::OTAResponseTypes error_code = ota_base::OTA_RESPONSE_ERROR_UNKNOWN;
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
bool update_started = false;
size_t total = 0;
uint32_t last_progress = 0;
@@ -102,7 +102,7 @@ void ESPHomeOTAComponent::handle_() {
char *sbuf = reinterpret_cast<char *>(buf);
size_t ota_size;
uint8_t ota_features;
std::unique_ptr<ota_base::OTABackend> backend;
std::unique_ptr<ota::OTABackend> backend;
(void) ota_features;
#if USE_OTA_VERSION == 2
size_t size_acknowledged = 0;
@@ -129,7 +129,7 @@ void ESPHomeOTAComponent::handle_() {
ESP_LOGD(TAG, "Starting update from %s", this->client_->getpeername().c_str());
this->status_set_warning();
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota_base::OTA_STARTED, 0.0f, 0);
this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0);
#endif
if (!this->readall_(buf, 5)) {
@@ -140,16 +140,16 @@ void ESPHomeOTAComponent::handle_() {
if (buf[0] != 0x6C || buf[1] != 0x26 || buf[2] != 0xF7 || buf[3] != 0x5C || buf[4] != 0x45) {
ESP_LOGW(TAG, "Magic bytes do not match! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", buf[0], buf[1], buf[2], buf[3],
buf[4]);
error_code = ota_base::OTA_RESPONSE_ERROR_MAGIC;
error_code = ota::OTA_RESPONSE_ERROR_MAGIC;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
// Send OK and version - 2 bytes
buf[0] = ota_base::OTA_RESPONSE_OK;
buf[0] = ota::OTA_RESPONSE_OK;
buf[1] = USE_OTA_VERSION;
this->writeall_(buf, 2);
backend = ota_base::make_ota_backend();
backend = ota::make_ota_backend();
// Read features - 1 byte
if (!this->readall_(buf, 1)) {
@@ -160,16 +160,16 @@ void ESPHomeOTAComponent::handle_() {
ESP_LOGV(TAG, "Features: 0x%02X", ota_features);
// Acknowledge header - 1 byte
buf[0] = ota_base::OTA_RESPONSE_HEADER_OK;
buf[0] = ota::OTA_RESPONSE_HEADER_OK;
if ((ota_features & FEATURE_SUPPORTS_COMPRESSION) != 0 && backend->supports_compression()) {
buf[0] = ota_base::OTA_RESPONSE_SUPPORTS_COMPRESSION;
buf[0] = ota::OTA_RESPONSE_SUPPORTS_COMPRESSION;
}
this->writeall_(buf, 1);
#ifdef USE_OTA_PASSWORD
if (!this->password_.empty()) {
buf[0] = ota_base::OTA_RESPONSE_REQUEST_AUTH;
buf[0] = ota::OTA_RESPONSE_REQUEST_AUTH;
this->writeall_(buf, 1);
md5::MD5Digest md5{};
md5.init();
@@ -220,14 +220,14 @@ void ESPHomeOTAComponent::handle_() {
if (!matches) {
ESP_LOGW(TAG, "Auth failed! Passwords do not match");
error_code = ota_base::OTA_RESPONSE_ERROR_AUTH_INVALID;
error_code = ota::OTA_RESPONSE_ERROR_AUTH_INVALID;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
}
#endif // USE_OTA_PASSWORD
// Acknowledge auth OK - 1 byte
buf[0] = ota_base::OTA_RESPONSE_AUTH_OK;
buf[0] = ota::OTA_RESPONSE_AUTH_OK;
this->writeall_(buf, 1);
// Read size, 4 bytes MSB first
@@ -243,12 +243,12 @@ void ESPHomeOTAComponent::handle_() {
ESP_LOGV(TAG, "Size is %u bytes", ota_size);
error_code = backend->begin(ota_size);
if (error_code != ota_base::OTA_RESPONSE_OK)
if (error_code != ota::OTA_RESPONSE_OK)
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
update_started = true;
// Acknowledge prepare OK - 1 byte
buf[0] = ota_base::OTA_RESPONSE_UPDATE_PREPARE_OK;
buf[0] = ota::OTA_RESPONSE_UPDATE_PREPARE_OK;
this->writeall_(buf, 1);
// Read binary MD5, 32 bytes
@@ -261,7 +261,7 @@ void ESPHomeOTAComponent::handle_() {
backend->set_update_md5(sbuf);
// Acknowledge MD5 OK - 1 byte
buf[0] = ota_base::OTA_RESPONSE_BIN_MD5_OK;
buf[0] = ota::OTA_RESPONSE_BIN_MD5_OK;
this->writeall_(buf, 1);
while (total < ota_size) {
@@ -285,14 +285,14 @@ void ESPHomeOTAComponent::handle_() {
}
error_code = backend->write(buf, read);
if (error_code != ota_base::OTA_RESPONSE_OK) {
if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "Error writing binary data to flash!, error_code: %d", error_code);
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
total += read;
#if USE_OTA_VERSION == 2
while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
buf[0] = ota_base::OTA_RESPONSE_CHUNK_OK;
buf[0] = ota::OTA_RESPONSE_CHUNK_OK;
this->writeall_(buf, 1);
size_acknowledged += OTA_BLOCK_SIZE;
}
@@ -304,7 +304,7 @@ void ESPHomeOTAComponent::handle_() {
float percentage = (total * 100.0f) / ota_size;
ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota_base::OTA_IN_PROGRESS, percentage, 0);
this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0);
#endif
// feed watchdog and give other tasks a chance to run
App.feed_wdt();
@@ -313,21 +313,21 @@ void ESPHomeOTAComponent::handle_() {
}
// Acknowledge receive OK - 1 byte
buf[0] = ota_base::OTA_RESPONSE_RECEIVE_OK;
buf[0] = ota::OTA_RESPONSE_RECEIVE_OK;
this->writeall_(buf, 1);
error_code = backend->end();
if (error_code != ota_base::OTA_RESPONSE_OK) {
if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code);
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
// Acknowledge Update end OK - 1 byte
buf[0] = ota_base::OTA_RESPONSE_UPDATE_END_OK;
buf[0] = ota::OTA_RESPONSE_UPDATE_END_OK;
this->writeall_(buf, 1);
// Read ACK
if (!this->readall_(buf, 1) || buf[0] != ota_base::OTA_RESPONSE_OK) {
if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "Reading back acknowledgement failed");
// do not go to error, this is not fatal
}
@@ -338,7 +338,7 @@ void ESPHomeOTAComponent::handle_() {
ESP_LOGI(TAG, "Update complete");
this->status_clear_warning();
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota_base::OTA_COMPLETED, 100.0f, 0);
this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, 0);
#endif
delay(100); // NOLINT
App.safe_reboot();
@@ -355,7 +355,7 @@ error:
this->status_momentary_error("onerror", 5000);
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota_base::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
this->state_callback_.call(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#endif
}

View File

@@ -4,13 +4,13 @@
#ifdef USE_OTA
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/components/ota_base/ota_backend.h"
#include "esphome/components/ota/ota_backend.h"
#include "esphome/components/socket/socket.h"
namespace esphome {
/// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA.
class ESPHomeOTAComponent : public ota_base::OTAComponent {
class ESPHomeOTAComponent : public ota::OTAComponent {
public:
#ifdef USE_OTA_PASSWORD
void set_auth_password(const std::string &password) { password_ = password; }

View File

@@ -1,6 +1,6 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PASSWORD, CONF_URL, CONF_USERNAME
from esphome.core import coroutine_with_priority
@@ -15,9 +15,6 @@ DEPENDENCIES = ["network", "http_request"]
CONF_MD5 = "md5"
CONF_MD5_URL = "md5_url"
ota_base_ns = cg.esphome_ns.namespace("ota_base")
OTAComponent = ota_base_ns.class_("OTAComponent", cg.Component)
OtaHttpRequestComponent = http_request_ns.class_(
"OtaHttpRequestComponent", OTAComponent
)

View File

@@ -6,11 +6,11 @@
#include "esphome/components/md5/md5.h"
#include "esphome/components/watchdog/watchdog.h"
#include "esphome/components/ota_base/ota_backend.h"
#include "esphome/components/ota_base/ota_backend_arduino_esp32.h"
#include "esphome/components/ota_base/ota_backend_arduino_esp8266.h"
#include "esphome/components/ota_base/ota_backend_arduino_rp2040.h"
#include "esphome/components/ota_base/ota_backend_esp_idf.h"
#include "esphome/components/ota/ota_backend.h"
#include "esphome/components/ota/ota_backend_arduino_esp32.h"
#include "esphome/components/ota/ota_backend_arduino_esp8266.h"
#include "esphome/components/ota/ota_backend_arduino_rp2040.h"
#include "esphome/components/ota/ota_backend_esp_idf.h"
namespace esphome {
namespace http_request {
@@ -19,7 +19,7 @@ static const char *const TAG = "http_request.ota";
void OtaHttpRequestComponent::setup() {
#ifdef USE_OTA_STATE_CALLBACK
ota_base::register_ota_platform(this);
ota::register_ota_platform(this);
#endif
}
@@ -50,15 +50,15 @@ void OtaHttpRequestComponent::flash() {
ESP_LOGI(TAG, "Starting update");
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota_base::OTA_STARTED, 0.0f, 0);
this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0);
#endif
auto ota_status = this->do_ota_();
switch (ota_status) {
case ota_base::OTA_RESPONSE_OK:
case ota::OTA_RESPONSE_OK:
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota_base::OTA_COMPLETED, 100.0f, ota_status);
this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, ota_status);
#endif
delay(10);
App.safe_reboot();
@@ -66,7 +66,7 @@ void OtaHttpRequestComponent::flash() {
default:
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota_base::OTA_ERROR, 0.0f, ota_status);
this->state_callback_.call(ota::OTA_ERROR, 0.0f, ota_status);
#endif
this->md5_computed_.clear(); // will be reset at next attempt
this->md5_expected_.clear(); // will be reset at next attempt
@@ -74,7 +74,7 @@ void OtaHttpRequestComponent::flash() {
}
}
void OtaHttpRequestComponent::cleanup_(std::unique_ptr<ota_base::OTABackend> backend,
void OtaHttpRequestComponent::cleanup_(std::unique_ptr<ota::OTABackend> backend,
const std::shared_ptr<HttpContainer> &container) {
if (this->update_started_) {
ESP_LOGV(TAG, "Aborting OTA backend");
@@ -115,9 +115,9 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
ESP_LOGV(TAG, "MD5Digest initialized");
ESP_LOGV(TAG, "OTA backend begin");
auto backend = ota_base::make_ota_backend();
auto backend = ota::make_ota_backend();
auto error_code = backend->begin(container->content_length);
if (error_code != ota_base::OTA_RESPONSE_OK) {
if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "backend->begin error: %d", error_code);
this->cleanup_(std::move(backend), container);
return error_code;
@@ -144,7 +144,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
// write bytes to OTA backend
this->update_started_ = true;
error_code = backend->write(buf, bufsize);
if (error_code != ota_base::OTA_RESPONSE_OK) {
if (error_code != ota::OTA_RESPONSE_OK) {
// error code explanation available at
// https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h
ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code,
@@ -160,7 +160,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
float percentage = container->get_bytes_read() * 100.0f / container->content_length;
ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota_base::OTA_IN_PROGRESS, percentage, 0);
this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0);
#endif
}
} // while
@@ -174,7 +174,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) {
ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str());
this->cleanup_(std::move(backend), container);
return ota_base::OTA_RESPONSE_ERROR_MD5_MISMATCH;
return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH;
} else {
backend->set_update_md5(md5_receive_str.get());
}
@@ -187,14 +187,14 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
delay(100); // NOLINT
error_code = backend->end();
if (error_code != ota_base::OTA_RESPONSE_OK) {
if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code);
this->cleanup_(std::move(backend), container);
return error_code;
}
ESP_LOGI(TAG, "Update complete");
return ota_base::OTA_RESPONSE_OK;
return ota::OTA_RESPONSE_OK;
}
std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url) {

View File

@@ -1,6 +1,6 @@
#pragma once
#include "esphome/components/ota_base/ota_backend.h"
#include "esphome/components/ota/ota_backend.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
@@ -22,7 +22,7 @@ enum OtaHttpRequestError : uint8_t {
OTA_CONNECTION_ERROR = 0x12,
};
class OtaHttpRequestComponent : public ota_base::OTAComponent, public Parented<HttpRequestComponent> {
class OtaHttpRequestComponent : public ota::OTAComponent, public Parented<HttpRequestComponent> {
public:
void setup() override;
void dump_config() override;
@@ -40,7 +40,7 @@ class OtaHttpRequestComponent : public ota_base::OTAComponent, public Parented<H
void flash();
protected:
void cleanup_(std::unique_ptr<ota_base::OTABackend> backend, const std::shared_ptr<HttpContainer> &container);
void cleanup_(std::unique_ptr<ota::OTABackend> backend, const std::shared_ptr<HttpContainer> &container);
uint8_t do_ota_();
std::string get_url_with_auth_(const std::string &url);
bool http_get_md5_();

View File

@@ -5,7 +5,6 @@
#include "esphome/components/json/json_util.h"
#include "esphome/components/network/util.h"
#include "esphome/components/ota_base/ota_backend.h"
namespace esphome {
namespace http_request {
@@ -22,13 +21,13 @@ static const char *const TAG = "http_request.update";
static const size_t MAX_READ_SIZE = 256;
void HttpRequestUpdate::setup() {
this->ota_parent_->add_on_state_callback([this](ota_base::OTAState state, float progress, uint8_t err) {
if (state == ota_base::OTAState::OTA_IN_PROGRESS) {
this->ota_parent_->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t err) {
if (state == ota::OTAState::OTA_IN_PROGRESS) {
this->state_ = update::UPDATE_STATE_INSTALLING;
this->update_info_.has_progress = true;
this->update_info_.progress = progress;
this->publish_state();
} else if (state == ota_base::OTAState::OTA_ABORT || state == ota_base::OTAState::OTA_ERROR) {
} else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) {
this->state_ = update::UPDATE_STATE_AVAILABLE;
this->status_set_error("Failed to install firmware");
this->publish_state();

View File

@@ -9,7 +9,7 @@
#include "esphome/components/audio/audio_transfer_buffer.h"
#ifdef USE_OTA
#include "esphome/components/ota_base/ota_backend.h"
#include "esphome/components/ota/ota_backend.h"
#endif
namespace esphome {
@@ -121,11 +121,11 @@ void MicroWakeWord::setup() {
});
#ifdef USE_OTA
ota_base::get_global_ota_callback()->add_on_state_callback(
[this](ota_base::OTAState state, float progress, uint8_t error, ota_base::OTAComponent *comp) {
if (state == ota_base::OTA_STARTED) {
ota::get_global_ota_callback()->add_on_state_callback(
[this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) {
this->suspend_task_();
} else if (state == ota_base::OTA_ERROR) {
} else if (state == ota::OTA_ERROR) {
this->resume_task_();
}
});

View File

@@ -8,12 +8,10 @@ from esphome.const import (
CONF_PLATFORM,
CONF_TRIGGER_ID,
)
from esphome.core import coroutine_with_priority
from ..ota_base import OTAState
from esphome.core import CORE, coroutine_with_priority
CODEOWNERS = ["@esphome/core"]
AUTO_LOAD = ["safe_mode", "ota_base"]
AUTO_LOAD = ["md5", "safe_mode"]
IS_PLATFORM_COMPONENT = True
@@ -25,6 +23,8 @@ CONF_ON_STATE_CHANGE = "on_state_change"
ota_ns = cg.esphome_ns.namespace("ota")
OTAComponent = ota_ns.class_("OTAComponent", cg.Component)
OTAState = ota_ns.enum("OTAState")
OTAAbortTrigger = ota_ns.class_("OTAAbortTrigger", automation.Trigger.template())
OTAEndTrigger = ota_ns.class_("OTAEndTrigger", automation.Trigger.template())
OTAErrorTrigger = ota_ns.class_("OTAErrorTrigger", automation.Trigger.template())
@@ -84,6 +84,12 @@ BASE_OTA_SCHEMA = cv.Schema(
async def to_code(config):
cg.add_define("USE_OTA")
if CORE.is_esp32 and CORE.using_arduino:
cg.add_library("Update", None)
if CORE.is_rp2040 and CORE.using_arduino:
cg.add_library("Updater", None)
async def ota_to_code(var, config):
await cg.past_safe_mode()

View File

@@ -1,16 +1,12 @@
#pragma once
#ifdef USE_OTA_STATE_CALLBACK
#include "esphome/components/ota_base/ota_backend.h"
#include "ota_backend.h"
#include "esphome/core/automation.h"
namespace esphome {
namespace ota {
// Import types from ota_base for the automation triggers
using ota_base::OTAComponent;
using ota_base::OTAState;
class OTAStateChangeTrigger : public Trigger<OTAState> {
public:
explicit OTAStateChangeTrigger(OTAComponent *parent) {
@@ -26,7 +22,7 @@ class OTAStartTrigger : public Trigger<> {
public:
explicit OTAStartTrigger(OTAComponent *parent) {
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
if (state == ota_base::OTA_STARTED && !parent->is_failed()) {
if (state == OTA_STARTED && !parent->is_failed()) {
trigger();
}
});
@@ -37,7 +33,7 @@ class OTAProgressTrigger : public Trigger<float> {
public:
explicit OTAProgressTrigger(OTAComponent *parent) {
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
if (state == ota_base::OTA_IN_PROGRESS && !parent->is_failed()) {
if (state == OTA_IN_PROGRESS && !parent->is_failed()) {
trigger(progress);
}
});
@@ -48,7 +44,7 @@ class OTAEndTrigger : public Trigger<> {
public:
explicit OTAEndTrigger(OTAComponent *parent) {
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
if (state == ota_base::OTA_COMPLETED && !parent->is_failed()) {
if (state == OTA_COMPLETED && !parent->is_failed()) {
trigger();
}
});
@@ -59,7 +55,7 @@ class OTAAbortTrigger : public Trigger<> {
public:
explicit OTAAbortTrigger(OTAComponent *parent) {
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
if (state == ota_base::OTA_ABORT && !parent->is_failed()) {
if (state == OTA_ABORT && !parent->is_failed()) {
trigger();
}
});
@@ -70,7 +66,7 @@ class OTAErrorTrigger : public Trigger<uint8_t> {
public:
explicit OTAErrorTrigger(OTAComponent *parent) {
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
if (state == ota_base::OTA_ERROR && !parent->is_failed()) {
if (state == OTA_ERROR && !parent->is_failed()) {
trigger(error);
}
});

View File

@@ -1,9 +1,7 @@
#include "ota_backend.h"
namespace esphome {
namespace ota_base {
// The make_ota_backend() implementation is provided by each platform-specific backend
namespace ota {
#ifdef USE_OTA_STATE_CALLBACK
OTAGlobalCallback *global_ota_callback{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -18,5 +16,5 @@ OTAGlobalCallback *get_global_ota_callback() {
void register_ota_platform(OTAComponent *ota_caller) { get_global_ota_callback()->register_ota(ota_caller); }
#endif
} // namespace ota_base
} // namespace ota
} // namespace esphome

View File

@@ -9,7 +9,7 @@
#endif
namespace esphome {
namespace ota_base {
namespace ota {
enum OTAResponseTypes {
OTA_RESPONSE_OK = 0x00,
@@ -59,12 +59,10 @@ class OTABackend {
virtual bool supports_compression() = 0;
};
std::unique_ptr<OTABackend> make_ota_backend();
class OTAComponent : public Component {
#ifdef USE_OTA_STATE_CALLBACK
public:
void add_on_state_callback(std::function<void(OTAState, float, uint8_t)> &&callback) {
void add_on_state_callback(std::function<void(ota::OTAState, float, uint8_t)> &&callback) {
this->state_callback_.add(std::move(callback));
}
@@ -82,7 +80,7 @@ class OTAComponent : public Component {
* 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(OTAState state, float progress, uint8_t error) {
void call_deferred(ota::OTAState state, float progress, uint8_t error) {
component_->defer([this, state, progress, error]() { this->call(state, progress, error); });
}
@@ -118,6 +116,7 @@ void register_ota_platform(OTAComponent *ota_caller);
// - 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();
} // namespace ota_base
} // namespace ota
} // namespace esphome

View File

@@ -8,11 +8,11 @@
#include <Update.h>
namespace esphome {
namespace ota_base {
namespace ota {
static const char *const TAG = "ota.arduino_esp32";
std::unique_ptr<OTABackend> make_ota_backend() { return make_unique<ArduinoESP32OTABackend>(); }
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
@@ -66,7 +66,7 @@ OTAResponseTypes ArduinoESP32OTABackend::end() {
void ArduinoESP32OTABackend::abort() { Update.abort(); }
} // namespace ota_base
} // namespace ota
} // namespace esphome
#endif // USE_ESP32_FRAMEWORK_ARDUINO

View File

@@ -6,7 +6,7 @@
#include "esphome/core/helpers.h"
namespace esphome {
namespace ota_base {
namespace ota {
class ArduinoESP32OTABackend : public OTABackend {
public:
@@ -21,7 +21,7 @@ class ArduinoESP32OTABackend : public OTABackend {
bool md5_set_{false};
};
} // namespace ota_base
} // namespace ota
} // namespace esphome
#endif // USE_ESP32_FRAMEWORK_ARDUINO

View File

@@ -10,11 +10,11 @@
#include <Updater.h>
namespace esphome {
namespace ota_base {
namespace ota {
static const char *const TAG = "ota.arduino_esp8266";
std::unique_ptr<OTABackend> make_ota_backend() { return make_unique<ArduinoESP8266OTABackend>(); }
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
@@ -82,7 +82,7 @@ void ArduinoESP8266OTABackend::abort() {
esp8266::preferences_prevent_write(false);
}
} // namespace ota_base
} // namespace ota
} // namespace esphome
#endif

View File

@@ -7,7 +7,7 @@
#include "esphome/core/macros.h"
namespace esphome {
namespace ota_base {
namespace ota {
class ArduinoESP8266OTABackend : public OTABackend {
public:
@@ -26,7 +26,7 @@ class ArduinoESP8266OTABackend : public OTABackend {
bool md5_set_{false};
};
} // namespace ota_base
} // namespace ota
} // namespace esphome
#endif

View File

@@ -8,11 +8,11 @@
#include <Update.h>
namespace esphome {
namespace ota_base {
namespace ota {
static const char *const TAG = "ota.arduino_libretiny";
std::unique_ptr<OTABackend> make_ota_backend() { return make_unique<ArduinoLibreTinyOTABackend>(); }
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
@@ -66,7 +66,7 @@ OTAResponseTypes ArduinoLibreTinyOTABackend::end() {
void ArduinoLibreTinyOTABackend::abort() { Update.abort(); }
} // namespace ota_base
} // namespace ota
} // namespace esphome
#endif // USE_LIBRETINY

View File

@@ -5,7 +5,7 @@
#include "esphome/core/defines.h"
namespace esphome {
namespace ota_base {
namespace ota {
class ArduinoLibreTinyOTABackend : public OTABackend {
public:
@@ -20,7 +20,7 @@ class ArduinoLibreTinyOTABackend : public OTABackend {
bool md5_set_{false};
};
} // namespace ota_base
} // namespace ota
} // namespace esphome
#endif // USE_LIBRETINY

View File

@@ -10,24 +10,15 @@
#include <Updater.h>
namespace esphome {
namespace ota_base {
namespace ota {
static const char *const TAG = "ota.arduino_rp2040";
std::unique_ptr<OTABackend> make_ota_backend() { return make_unique<ArduinoRP2040OTABackend>(); }
std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoRP2040OTABackend>(); }
OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) {
// Handle UPDATE_SIZE_UNKNOWN (0) by calculating available space
if (image_size == 0) {
// Similar to ESP8266, calculate available space from flash layout
extern uint8_t _FS_start;
extern uint8_t _FS_end;
// Calculate the size of the filesystem area which will be used for OTA
size_t fs_size = &_FS_end - &_FS_start;
// Reserve some space for filesystem overhead
image_size = (fs_size - 0x1000) & 0xFFFFF000;
ESP_LOGD(TAG, "OTA size unknown, using filesystem size: %u bytes", 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);
@@ -84,7 +75,7 @@ void ArduinoRP2040OTABackend::abort() {
rp2040::preferences_prevent_write(false);
}
} // namespace ota_base
} // namespace ota
} // namespace esphome
#endif // USE_RP2040

View File

@@ -7,7 +7,7 @@
#include "esphome/core/macros.h"
namespace esphome {
namespace ota_base {
namespace ota {
class ArduinoRP2040OTABackend : public OTABackend {
public:
@@ -22,7 +22,7 @@ class ArduinoRP2040OTABackend : public OTABackend {
bool md5_set_{false};
};
} // namespace ota_base
} // namespace ota
} // namespace esphome
#endif // USE_RP2040

View File

@@ -9,9 +9,9 @@
#include <spi_flash_mmap.h>
namespace esphome {
namespace ota_base {
namespace ota {
std::unique_ptr<OTABackend> make_ota_backend() { return make_unique<IDFOTABackend>(); }
std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::IDFOTABackend>(); }
OTAResponseTypes IDFOTABackend::begin(size_t image_size) {
this->partition_ = esp_ota_get_next_update_partition(nullptr);
@@ -105,6 +105,6 @@ void IDFOTABackend::abort() {
this->update_handle_ = 0;
}
} // namespace ota_base
} // namespace ota
} // namespace esphome
#endif

View File

@@ -8,7 +8,7 @@
#include <esp_ota_ops.h>
namespace esphome {
namespace ota_base {
namespace ota {
class IDFOTABackend : public OTABackend {
public:
@@ -27,6 +27,6 @@ class IDFOTABackend : public OTABackend {
bool md5_set_{false};
};
} // namespace ota_base
} // namespace ota
} // namespace esphome
#endif

View File

@@ -1,23 +0,0 @@
import esphome.codegen as cg
from esphome.core import CORE, coroutine_with_priority
CODEOWNERS = ["@esphome/core"]
AUTO_LOAD = ["md5"]
ota_base_ns = cg.esphome_ns.namespace("ota_base")
OTAComponent = ota_base_ns.class_("OTAComponent", cg.Component)
OTAState = ota_base_ns.enum("OTAState")
@coroutine_with_priority(52.0)
async def to_code(config):
# Note: USE_OTA_STATE_CALLBACK is not defined here
# Components that need OTA callbacks (like esp32_ble_tracker, speaker, etc.)
# define USE_OTA_STATE_CALLBACK themselves in their own __init__.py files
# This ensures the callback functionality is only compiled when actually needed
if CORE.is_esp32 and CORE.using_arduino:
cg.add_library("Update", None)
if CORE.is_rp2040 and CORE.using_arduino:
cg.add_library("Updater", None)

View File

@@ -6,7 +6,7 @@
#include "esphome/components/audio/audio.h"
#ifdef USE_OTA
#include "esphome/components/ota_base/ota_backend.h"
#include "esphome/components/ota/ota_backend.h"
#endif
namespace esphome {
@@ -67,16 +67,16 @@ void SpeakerMediaPlayer::setup() {
}
#ifdef USE_OTA
ota_base::get_global_ota_callback()->add_on_state_callback(
[this](ota_base::OTAState state, float progress, uint8_t error, ota_base::OTAComponent *comp) {
if (state == ota_base::OTA_STARTED) {
ota::get_global_ota_callback()->add_on_state_callback(
[this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) {
if (this->media_pipeline_ != nullptr) {
this->media_pipeline_->suspend_tasks();
}
if (this->announcement_pipeline_ != nullptr) {
this->announcement_pipeline_->suspend_tasks();
}
} else if (state == ota_base::OTA_ERROR) {
} else if (state == ota::OTA_ERROR) {
if (this->media_pipeline_ != nullptr) {
this->media_pipeline_->resume_tasks();
}

View File

@@ -33,8 +33,9 @@ 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", "ota_base"]
AUTO_LOAD = ["json", "web_server_base"]
CONF_SORTING_GROUP_ID = "sorting_group_id"
CONF_SORTING_GROUPS = "sorting_groups"
@@ -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
# Web server OTA now uses ota_base backend for consistency
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")

View File

@@ -0,0 +1,29 @@
import esphome.codegen as cg
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 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")

View File

@@ -0,0 +1,201 @@
#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"
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_FRAMEWORK_ARDUINO) || 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. WebServer OTA requires web_server_base component");
this->mark_failed();
return;
}
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

View 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

View File

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

View File

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

View File

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

View File

@@ -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:
@@ -39,4 +40,7 @@ async def to_code(config):
if CORE.is_esp8266:
cg.add_library("ESP8266WiFi", None)
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8")
# Use fork with libretiny compatibility fix
cg.add_library(
"https://github.com/bdraco/ESPAsyncWebServer.git#libretiny_Fix", None
)

View File

@@ -4,23 +4,13 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifdef USE_WEBSERVER_OTA
#include "esphome/components/ota_base/ota_backend.h"
#endif
#ifdef USE_ARDUINO
#ifdef USE_ESP8266
#include <Updater.h>
#elif defined(USE_ESP32) || defined(USE_LIBRETINY)
#include <Update.h>
#endif
#endif
namespace esphome {
namespace web_server_base {
static const char *const TAG = "web_server_base";
WebServerBase *global_web_server_base = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void WebServerBase::add_handler(AsyncWebHandler *handler) {
// remove all handlers
@@ -33,156 +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) {
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_base::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_base::OTAResponseTypes error_code = ota_base::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_base::OTA_STARTED, 0.0f, 0);
#endif
// Platform-specific pre-initialization
#ifdef USE_ARDUINO
#ifdef USE_ESP8266
Update.runAsync(true);
#endif
#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_LIBRETINY)
if (Update.isRunning()) {
Update.abort();
}
#endif
#endif // USE_ARDUINO
this->ota_backend_ = ota_base::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_base::OTA_ERROR, 0.0f,
static_cast<uint8_t>(ota_base::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_base::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_base::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_base::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_base::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_base::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_base::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_base::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 WebServerBase::add_ota_handler() {
this->add_handler(new OTARequestHandler(this)); // NOLINT
#ifdef USE_OTA_STATE_CALLBACK
// Register with global OTA callback system
ota_base::register_ota_platform(this);
#endif
}
#endif
float WebServerBase::get_setup_priority() const {
// Before WiFi (captive portal)
return setup_priority::WIFI + 2.0f;

View File

@@ -14,13 +14,12 @@
#include "esphome/components/web_server_idf/web_server_idf.h"
#endif
#ifdef USE_WEBSERVER_OTA
#include "esphome/components/ota_base/ota_backend.h"
#endif
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 {
@@ -83,11 +82,7 @@ class AuthMiddlewareHandler : public MiddlewareHandler {
} // namespace internal
#ifdef USE_WEBSERVER_OTA
class WebServerBase : public ota_base::OTAComponent {
#else
class WebServerBase : public Component {
#endif
public:
void init() {
if (this->initialized_) {
@@ -118,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};
@@ -137,35 +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_;
bool ota_success_{false};
private:
std::unique_ptr<ota_base::OTABackend> ota_backend_{nullptr};
};
#endif // USE_WEBSERVER_OTA
} // namespace web_server_base
} // namespace esphome
#endif

View File

@@ -1,6 +1,6 @@
from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
import esphome.config_validation as cv
from esphome.const import CONF_OTA, CONF_WEB_SERVER
from esphome.const import CONF_OTA
from esphome.core import CORE
CODEOWNERS = ["@dentra"]
@@ -14,7 +14,12 @@ 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
# Check if web_server OTA platform is configured
ota_config = CORE.config.get(CONF_OTA, [])
has_web_server_ota = any(
platform.get("platform") == "web_server" for platform in ota_config
)
if has_web_server_ota:
# Add multipart parser dependency for web server OTA
add_idf_component(name="zorxx/multipart-parser", ref="1.0.1")

View 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

View 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

View 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

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

View 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

View 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

View 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

View 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

View File

@@ -1,10 +0,0 @@
# Test that ota_base compiles correctly as a dependency
# This component is typically auto-loaded by other components
wifi:
ssid: MySSID
password: password1
ota:
- platform: esphome
password: "test1234"

View File

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

View File

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

View File

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

View File

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