diff --git a/CODEOWNERS b/CODEOWNERS index bcf2b7aca5..ddfcff9955 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -499,6 +499,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 diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index b02a875d72..2f1be28293 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -132,7 +132,9 @@ async def to_code(config): await cg.register_component(var, config) cg.add(var.set_port(config[CONF_PORT])) - cg.add(var.set_password(config[CONF_PASSWORD])) + if config[CONF_PASSWORD]: + cg.add_define("USE_API_PASSWORD") + cg.add(var.set_password(config[CONF_PASSWORD])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY])) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index e83d508c50..49ad9706bc 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1503,7 +1503,10 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { return resp; } ConnectResponse APIConnection::connect(const ConnectRequest &msg) { - bool correct = this->parent_->check_password(msg.password); + bool correct = true; +#ifdef USE_API_PASSWORD + correct = this->parent_->check_password(msg.password); +#endif ConnectResponse resp; // bool invalid_password = 1; @@ -1524,7 +1527,11 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { } DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { DeviceInfoResponse resp{}; +#ifdef USE_API_PASSWORD resp.uses_password = this->parent_->uses_password(); +#else + resp.uses_password = false; +#endif resp.name = App.get_name(); resp.friendly_name = App.get_friendly_name(); resp.suggested_area = App.get_area(); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 7b14c803b2..7d16e43ce6 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3,6 +3,7 @@ #include "api_pb2.h" #include "api_pb2_size.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" #include @@ -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: "); @@ -3540,7 +3541,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("}"); } @@ -4286,7 +4287,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: "); @@ -6813,7 +6814,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("}"); } @@ -6896,7 +6897,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: "); @@ -6989,7 +6990,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("}"); } @@ -7516,7 +7517,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("}"); } @@ -7580,7 +7581,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("}"); } @@ -7672,7 +7673,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("}"); } @@ -7774,7 +7775,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("}"); } @@ -8494,7 +8495,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: "); diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index ebe80604dc..0fd9c1a228 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -218,6 +218,7 @@ void APIServer::dump_config() { #endif } +#ifdef USE_API_PASSWORD bool APIServer::uses_password() const { return !this->password_.empty(); } bool APIServer::check_password(const std::string &password) const { @@ -248,6 +249,7 @@ bool APIServer::check_password(const std::string &password) const { return result == 0; } +#endif void APIServer::handle_disconnect(APIConnection *conn) {} @@ -431,7 +433,9 @@ float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; void APIServer::set_port(uint16_t port) { this->port_ = port; } +#ifdef USE_API_PASSWORD void APIServer::set_password(const std::string &password) { this->password_ = password; } +#endif void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 5a9b0677bc..9dc2b4b7d6 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -35,10 +35,12 @@ class APIServer : public Component, public Controller { void dump_config() override; void on_shutdown() override; bool teardown() override; +#ifdef USE_API_PASSWORD bool check_password(const std::string &password) const; bool uses_password() const; - void set_port(uint16_t port); void set_password(const std::string &password); +#endif + void set_port(uint16_t port); void set_reboot_timeout(uint32_t reboot_timeout); void set_batch_delay(uint16_t batch_delay); uint16_t get_batch_delay() const { return batch_delay_; } @@ -179,7 +181,9 @@ class APIServer : public Component, public Controller { // Vectors and strings (12 bytes each on 32-bit) std::vector> clients_; +#ifdef USE_API_PASSWORD std::string password_; +#endif std::vector shared_write_buffer_; // Shared proto write buffer for all connections std::vector state_subs_; #ifdef USE_API_YAML_SERVICES diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index cba3b4921a..7e8afd8fab 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -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"] diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index ba392bb0f2..25179fdacc 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -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 diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 576c1a5649..5a91b1c750 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -93,7 +93,6 @@ class ESP32TouchComponent : public Component { uint32_t last_release_check_{0}; uint32_t release_timeout_ms_{1500}; uint32_t release_check_interval_ms_{50}; - bool initial_state_published_[TOUCH_PAD_MAX] = {false}; // Common configuration parameters uint16_t sleep_cycle_{4095}; @@ -123,13 +122,6 @@ class ESP32TouchComponent : public Component { }; protected: - // Design note: last_touch_time_ does not require synchronization primitives because: - // 1. ESP32 guarantees atomic 32-bit aligned reads/writes - // 2. ISR only writes timestamps, main loop only reads - // 3. Timing tolerance allows for occasional stale reads (50ms check interval) - // 4. Queue operations provide implicit memory barriers - // Using atomic/critical sections would add overhead without meaningful benefit - uint32_t last_touch_time_[TOUCH_PAD_MAX] = {0}; uint32_t iir_filter_{0}; bool iir_filter_enabled_() const { return this->iir_filter_ > 0; } @@ -147,9 +139,6 @@ class ESP32TouchComponent : public Component { uint32_t intr_mask; }; - // Track last touch time for timeout-based release detection - uint32_t last_touch_time_[TOUCH_PAD_MAX] = {0}; - protected: // Filter configuration touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX}; @@ -255,11 +244,22 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { touch_pad_t touch_pad_{TOUCH_PAD_MAX}; uint32_t threshold_{0}; + uint32_t benchmark_{}; #ifdef USE_ESP32_VARIANT_ESP32 uint32_t value_{0}; #endif bool last_state_{false}; const uint32_t wakeup_threshold_{0}; + + // Track last touch time for timeout-based release detection + // Design note: last_touch_time_ does not require synchronization primitives because: + // 1. ESP32 guarantees atomic 32-bit aligned reads/writes + // 2. ISR only writes timestamps, main loop only reads + // 3. Timing tolerance allows for occasional stale reads (50ms check interval) + // 4. Queue operations provide implicit memory barriers + // Using atomic/critical sections would add overhead without meaningful benefit + uint32_t last_touch_time_{}; + bool initial_state_published_{}; }; } // namespace esp32_touch diff --git a/esphome/components/esp32_touch/esp32_touch_common.cpp b/esphome/components/esp32_touch/esp32_touch_common.cpp index fd2cdfcbad..2d93de077e 100644 --- a/esphome/components/esp32_touch/esp32_touch_common.cpp +++ b/esphome/components/esp32_touch/esp32_touch_common.cpp @@ -22,16 +22,20 @@ void ESP32TouchComponent::dump_config_base_() { " Sleep cycle: %.2fms\n" " Low Voltage Reference: %s\n" " High Voltage Reference: %s\n" - " Voltage Attenuation: %s", + " Voltage Attenuation: %s\n" + " Release Timeout: %" PRIu32 "ms\n", this->meas_cycle_ / (8000000.0f / 1000.0f), this->sleep_cycle_ / (150000.0f / 1000.0f), lv_s, hv_s, - atten_s); + atten_s, this->release_timeout_ms_); } void ESP32TouchComponent::dump_config_sensors_() { for (auto *child : this->children_) { LOG_BINARY_SENSOR(" ", "Touch Pad", child); - ESP_LOGCONFIG(TAG, " Pad: T%" PRIu32, (uint32_t) child->get_touch_pad()); - ESP_LOGCONFIG(TAG, " Threshold: %" PRIu32, child->get_threshold()); + ESP_LOGCONFIG(TAG, + " Pad: T%u\n" + " Threshold: %" PRIu32 "\n" + " Benchmark: %" PRIu32, + (unsigned) child->touch_pad_, child->threshold_, child->benchmark_); } } @@ -112,12 +116,11 @@ bool ESP32TouchComponent::should_check_for_releases_(uint32_t now) { } void ESP32TouchComponent::publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now) { - touch_pad_t pad = child->get_touch_pad(); - if (!this->initial_state_published_[pad]) { + if (!child->initial_state_published_) { // Check if enough time has passed since startup if (now > this->release_timeout_ms_) { child->publish_initial_state(false); - this->initial_state_published_[pad] = true; + child->initial_state_published_ = true; ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (initial)", child->get_name().c_str()); } } diff --git a/esphome/components/esp32_touch/esp32_touch_v1.cpp b/esphome/components/esp32_touch/esp32_touch_v1.cpp index a6d499e9fa..6f05610ed6 100644 --- a/esphome/components/esp32_touch/esp32_touch_v1.cpp +++ b/esphome/components/esp32_touch/esp32_touch_v1.cpp @@ -104,7 +104,7 @@ void ESP32TouchComponent::loop() { // Track when we last saw this pad as touched if (new_state) { - this->last_touch_time_[event.pad] = now; + child->last_touch_time_ = now; } // Only publish if state changed - this filters out repeated events @@ -127,15 +127,13 @@ void ESP32TouchComponent::loop() { size_t pads_off = 0; for (auto *child : this->children_) { - touch_pad_t pad = child->get_touch_pad(); - // Handle initial state publication after startup this->publish_initial_state_if_needed_(child, now); if (child->last_state_) { // Pad is currently in touched state - check for release timeout // Using subtraction handles 32-bit rollover correctly - uint32_t time_diff = now - this->last_touch_time_[pad]; + uint32_t time_diff = now - child->last_touch_time_; // Check if we haven't seen this pad recently if (time_diff > this->release_timeout_ms_) { diff --git a/esphome/components/esp32_touch/esp32_touch_v2.cpp b/esphome/components/esp32_touch/esp32_touch_v2.cpp index ad77881724..afd2655fd7 100644 --- a/esphome/components/esp32_touch/esp32_touch_v2.cpp +++ b/esphome/components/esp32_touch/esp32_touch_v2.cpp @@ -14,19 +14,16 @@ static const char *const TAG = "esp32_touch"; void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched) { // Always update timer when touched if (is_touched) { - this->last_touch_time_[child->get_touch_pad()] = App.get_loop_component_start_time(); + child->last_touch_time_ = App.get_loop_component_start_time(); } if (child->last_state_ != is_touched) { - // Read value for logging - uint32_t value = this->read_touch_value(child->get_touch_pad()); - child->last_state_ = is_touched; child->publish_state(is_touched); if (is_touched) { // ESP32-S2/S3 v2: touched when value > threshold ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " > threshold: %" PRIu32 ")", child->get_name().c_str(), - value, child->get_threshold()); + this->read_touch_value(child->touch_pad_), child->threshold_ + child->benchmark_); } else { ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str()); } @@ -36,10 +33,13 @@ void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, boo // Helper to read touch value and update state for a given child (used for timeout events) bool ESP32TouchComponent::check_and_update_touch_state_(ESP32TouchBinarySensor *child) { // Read current touch value - uint32_t value = this->read_touch_value(child->get_touch_pad()); + uint32_t value = this->read_touch_value(child->touch_pad_); - // ESP32-S2/S3 v2: Touch is detected when value > threshold - bool is_touched = value > child->get_threshold(); + // ESP32-S2/S3 v2: Touch is detected when value > threshold + benchmark + ESP_LOGV(TAG, + "Checking touch state for '%s' (T%d): value = %" PRIu32 ", threshold = %" PRIu32 ", benchmark = %" PRIu32, + child->get_name().c_str(), child->touch_pad_, value, child->threshold_, child->benchmark_); + bool is_touched = value > child->benchmark_ + child->threshold_; this->update_touch_state_(child, is_touched); return is_touched; @@ -61,9 +61,9 @@ void ESP32TouchComponent::setup() { // Configure each touch pad first for (auto *child : this->children_) { - esp_err_t config_err = touch_pad_config(child->get_touch_pad()); + esp_err_t config_err = touch_pad_config(child->touch_pad_); if (config_err != ESP_OK) { - ESP_LOGE(TAG, "Failed to configure touch pad %d: %s", child->get_touch_pad(), esp_err_to_name(config_err)); + ESP_LOGE(TAG, "Failed to configure touch pad %d: %s", child->touch_pad_, esp_err_to_name(config_err)); } } @@ -100,8 +100,8 @@ void ESP32TouchComponent::setup() { // Configure measurement parameters touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_); - // ESP32-S2/S3 always use the older API - touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_); + touch_pad_set_charge_discharge_times(this->meas_cycle_); + touch_pad_set_measurement_interval(this->sleep_cycle_); // Configure timeout if needed touch_pad_timeout_set(true, TOUCH_PAD_THRESHOLD_MAX); @@ -118,8 +118,8 @@ void ESP32TouchComponent::setup() { // Set thresholds for each pad BEFORE starting FSM for (auto *child : this->children_) { - if (child->get_threshold() != 0) { - touch_pad_set_thresh(child->get_touch_pad(), child->get_threshold()); + if (child->threshold_ != 0) { + touch_pad_set_thresh(child->touch_pad_, child->threshold_); } } @@ -277,6 +277,7 @@ void ESP32TouchComponent::loop() { // Process any queued touch events from interrupts TouchPadEventV2 event; while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) { + ESP_LOGD(TAG, "Event received, mask = 0x%" PRIx32 ", pad = %d", event.intr_mask, event.pad); // Handle timeout events if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) { // Resume measurement after timeout @@ -289,18 +290,16 @@ void ESP32TouchComponent::loop() { // Find the child for the pad that triggered the interrupt for (auto *child : this->children_) { - if (child->get_touch_pad() != event.pad) { - continue; + if (child->touch_pad_ == event.pad) { + if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) { + // For timeout events, we need to read the value to determine state + this->check_and_update_touch_state_(child); + } else if (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) { + // We only get ACTIVE interrupts now, releases are detected by timeout + this->update_touch_state_(child, true); // Always touched for ACTIVE interrupts + } + break; } - - if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) { - // For timeout events, we need to read the value to determine state - this->check_and_update_touch_state_(child); - } else if (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) { - // We only get ACTIVE interrupts now, releases are detected by timeout - this->update_touch_state_(child, true); // Always touched for ACTIVE interrupts - } - break; } } @@ -311,15 +310,15 @@ void ESP32TouchComponent::loop() { size_t pads_off = 0; for (auto *child : this->children_) { - touch_pad_t pad = child->get_touch_pad(); - + if (child->benchmark_ == 0) + touch_pad_read_benchmark(child->touch_pad_, &child->benchmark_); // Handle initial state publication after startup this->publish_initial_state_if_needed_(child, now); if (child->last_state_) { // Pad is currently in touched state - check for release timeout // Using subtraction handles 32-bit rollover correctly - uint32_t time_diff = now - this->last_touch_time_[pad]; + uint32_t time_diff = now - child->last_touch_time_; // Check if we haven't seen this pad recently if (time_diff > this->release_timeout_ms_) { diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 9e5a2bf45c..0f9f146ae9 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -70,6 +70,7 @@ PROTOCOLS = { "airway": Protocol.PROTOCOL_AIRWAY, "bgh_aud": Protocol.PROTOCOL_BGH_AUD, "panasonic_altdke": Protocol.PROTOCOL_PANASONIC_ALTDKE, + "philco_phs32": Protocol.PROTOCOL_PHILCO_PHS32, "vaillantvai8": Protocol.PROTOCOL_VAILLANTVAI8, "r51m": Protocol.PROTOCOL_R51M, } diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp index d3476c6a71..f4d2ca6c1d 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -65,6 +65,7 @@ const std::map> PROTOCOL_CONSTRUCTOR_MAP {PROTOCOL_AIRWAY, []() { return new AIRWAYHeatpumpIR(); }}, // NOLINT {PROTOCOL_BGH_AUD, []() { return new BGHHeatpumpIR(); }}, // NOLINT {PROTOCOL_PANASONIC_ALTDKE, []() { return new PanasonicAltDKEHeatpumpIR(); }}, // NOLINT + {PROTOCOL_PHILCO_PHS32, []() { return new PhilcoPHS32HeatpumpIR(); }}, // NOLINT {PROTOCOL_VAILLANTVAI8, []() { return new VaillantHeatpumpIR(); }}, // NOLINT {PROTOCOL_R51M, []() { return new R51MHeatpumpIR(); }}, // NOLINT }; diff --git a/esphome/components/heatpumpir/heatpumpir.h b/esphome/components/heatpumpir/heatpumpir.h index b740d27af7..3e14c11861 100644 --- a/esphome/components/heatpumpir/heatpumpir.h +++ b/esphome/components/heatpumpir/heatpumpir.h @@ -65,6 +65,7 @@ enum Protocol { PROTOCOL_AIRWAY, PROTOCOL_BGH_AUD, PROTOCOL_PANASONIC_ALTDKE, + PROTOCOL_PHILCO_PHS32, PROTOCOL_VAILLANTVAI8, PROTOCOL_R51M, }; diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index b4378cdce6..c009b33c2d 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -133,7 +133,6 @@ std::shared_ptr 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; } } diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 6a779ba03a..68c06d28f2 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -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; } diff --git a/esphome/components/mmc5603/mmc5603.cpp b/esphome/components/mmc5603/mmc5603.cpp index 86b1b23c15..7f78f9592a 100644 --- a/esphome/components/mmc5603/mmc5603.cpp +++ b/esphome/components/mmc5603/mmc5603.cpp @@ -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(); diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 042a595ff8..bb75385d8c 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -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(); } /** diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 0cd559d251..0b77d234f5 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -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 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; diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 0226e0a13c..84aacd1868 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -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; diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 65138e28c7..25e3153d1b 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -11,6 +11,7 @@ from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID import esphome.final_validate as fv from .const import ( + CONF_DEVICE_TYPE, CONF_EXT_PAN_ID, CONF_FORCE_DATASET, CONF_MDNS_ID, @@ -32,6 +33,11 @@ AUTO_LOAD = ["network"] CONFLICTS_WITH = ["wifi"] DEPENDENCIES = ["esp32"] +CONF_DEVICE_TYPES = [ + "FTD", + "MTD", +] + def set_sdkconfig_options(config): # and expose options for using SPI/UART RCPs @@ -82,7 +88,7 @@ def set_sdkconfig_options(config): add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT_MAX_SERVICES", 5) # TODO: Add suport for sleepy end devices - add_idf_sdkconfig_option("CONFIG_OPENTHREAD_FTD", True) # Full Thread Device + add_idf_sdkconfig_option(f"CONFIG_OPENTHREAD_{config.get(CONF_DEVICE_TYPE)}", True) openthread_ns = cg.esphome_ns.namespace("openthread") @@ -107,6 +113,9 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.declare_id(OpenThreadComponent), cv.GenerateID(CONF_SRP_ID): cv.declare_id(OpenThreadSrpComponent), cv.GenerateID(CONF_MDNS_ID): cv.use_id(MDNSComponent), + cv.Optional(CONF_DEVICE_TYPE, default="FTD"): cv.one_of( + *CONF_DEVICE_TYPES, upper=True + ), cv.Optional(CONF_FORCE_DATASET): cv.boolean, cv.Optional(CONF_TLV): cv.string_strict, } diff --git a/esphome/components/openthread/const.py b/esphome/components/openthread/const.py index 7837e69eea..7a6ffb2df4 100644 --- a/esphome/components/openthread/const.py +++ b/esphome/components/openthread/const.py @@ -1,3 +1,4 @@ +CONF_DEVICE_TYPE = "device_type" CONF_EXT_PAN_ID = "ext_pan_id" CONF_FORCE_DATASET = "force_dataset" CONF_MDNS_ID = "mdns_id" diff --git a/esphome/components/ota_base/__init__.py b/esphome/components/ota_base/__init__.py deleted file mode 100644 index 2203785953..0000000000 --- a/esphome/components/ota_base/__init__.py +++ /dev/null @@ -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) diff --git a/esphome/components/ota_base/ota_backend.cpp b/esphome/components/ota_base/ota_backend.cpp deleted file mode 100644 index 7cbc795866..0000000000 --- a/esphome/components/ota_base/ota_backend.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "ota_backend.h" - -namespace esphome { -namespace ota_base { - -// The make_ota_backend() implementation is provided by each platform-specific backend - -#ifdef USE_OTA_STATE_CALLBACK -OTAGlobalCallback *global_ota_callback{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -OTAGlobalCallback *get_global_ota_callback() { - if (global_ota_callback == nullptr) { - global_ota_callback = new OTAGlobalCallback(); // NOLINT(cppcoreguidelines-owning-memory) - } - return global_ota_callback; -} - -void register_ota_platform(OTAComponent *ota_caller) { get_global_ota_callback()->register_ota(ota_caller); } -#endif - -} // namespace ota_base -} // namespace esphome diff --git a/esphome/components/ota_base/ota_backend.h b/esphome/components/ota_base/ota_backend.h deleted file mode 100644 index 27637a9af2..0000000000 --- a/esphome/components/ota_base/ota_backend.h +++ /dev/null @@ -1,123 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/core/defines.h" -#include "esphome/core/helpers.h" - -#ifdef USE_OTA_STATE_CALLBACK -#include "esphome/core/automation.h" -#endif - -namespace esphome { -namespace ota_base { - -enum OTAResponseTypes { - OTA_RESPONSE_OK = 0x00, - OTA_RESPONSE_REQUEST_AUTH = 0x01, - - OTA_RESPONSE_HEADER_OK = 0x40, - OTA_RESPONSE_AUTH_OK = 0x41, - OTA_RESPONSE_UPDATE_PREPARE_OK = 0x42, - OTA_RESPONSE_BIN_MD5_OK = 0x43, - OTA_RESPONSE_RECEIVE_OK = 0x44, - OTA_RESPONSE_UPDATE_END_OK = 0x45, - OTA_RESPONSE_SUPPORTS_COMPRESSION = 0x46, - OTA_RESPONSE_CHUNK_OK = 0x47, - - OTA_RESPONSE_ERROR_MAGIC = 0x80, - OTA_RESPONSE_ERROR_UPDATE_PREPARE = 0x81, - OTA_RESPONSE_ERROR_AUTH_INVALID = 0x82, - OTA_RESPONSE_ERROR_WRITING_FLASH = 0x83, - OTA_RESPONSE_ERROR_UPDATE_END = 0x84, - OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING = 0x85, - OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG = 0x86, - OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG = 0x87, - OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 0x88, - OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 0x89, - OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION = 0x8A, - OTA_RESPONSE_ERROR_MD5_MISMATCH = 0x8B, - OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 0x8C, - OTA_RESPONSE_ERROR_UNKNOWN = 0xFF, -}; - -enum OTAState { - OTA_COMPLETED = 0, - OTA_STARTED, - OTA_IN_PROGRESS, - OTA_ABORT, - OTA_ERROR, -}; - -class OTABackend { - public: - virtual ~OTABackend() = default; - virtual OTAResponseTypes begin(size_t image_size) = 0; - virtual void set_update_md5(const char *md5) = 0; - virtual OTAResponseTypes write(uint8_t *data, size_t len) = 0; - virtual OTAResponseTypes end() = 0; - virtual void abort() = 0; - virtual bool supports_compression() = 0; -}; - -std::unique_ptr make_ota_backend(); - -class OTAComponent : public Component { -#ifdef USE_OTA_STATE_CALLBACK - public: - void add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); - } - - protected: - /** Extended callback manager with deferred call support. - * - * This adds a call_deferred() method for thread-safe execution from other tasks. - */ - class StateCallbackManager : public CallbackManager { - 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(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 -}; - -#ifdef USE_OTA_STATE_CALLBACK -class OTAGlobalCallback { - public: - void register_ota(OTAComponent *ota_caller) { - ota_caller->add_on_state_callback([this, ota_caller](OTAState state, float progress, uint8_t error) { - this->state_callback_.call(state, progress, error, ota_caller); - }); - } - void add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); - } - - protected: - CallbackManager state_callback_{}; -}; - -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 - -} // namespace ota_base -} // namespace esphome diff --git a/esphome/components/ota_base/ota_backend_arduino_esp32.cpp b/esphome/components/ota_base/ota_backend_arduino_esp32.cpp deleted file mode 100644 index f239544cfe..0000000000 --- a/esphome/components/ota_base/ota_backend_arduino_esp32.cpp +++ /dev/null @@ -1,72 +0,0 @@ -#ifdef USE_ESP32_FRAMEWORK_ARDUINO -#include "esphome/core/defines.h" -#include "esphome/core/log.h" - -#include "ota_backend.h" -#include "ota_backend_arduino_esp32.h" - -#include - -namespace esphome { -namespace ota_base { - -static const char *const TAG = "ota.arduino_esp32"; - -std::unique_ptr make_ota_backend() { return make_unique(); } - -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; - } - - uint8_t error = Update.getError(); - if (error == UPDATE_ERROR_SIZE) - return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE; - - ESP_LOGE(TAG, "Begin error: %d", error); - - return OTA_RESPONSE_ERROR_UNKNOWN; -} - -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); - if (written == len) { - return OTA_RESPONSE_OK; - } - - uint8_t error = Update.getError(); - ESP_LOGE(TAG, "Write error: %d", error); - - return OTA_RESPONSE_ERROR_WRITING_FLASH; -} - -OTAResponseTypes ArduinoESP32OTABackend::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; - } - - uint8_t error = Update.getError(); - ESP_LOGE(TAG, "End error: %d", error); - - return OTA_RESPONSE_ERROR_UPDATE_END; -} - -void ArduinoESP32OTABackend::abort() { Update.abort(); } - -} // namespace ota_base -} // namespace esphome - -#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/ota_base/ota_backend_arduino_esp32.h b/esphome/components/ota_base/ota_backend_arduino_esp32.h deleted file mode 100644 index e3966eb2f6..0000000000 --- a/esphome/components/ota_base/ota_backend_arduino_esp32.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once -#ifdef USE_ESP32_FRAMEWORK_ARDUINO -#include "ota_backend.h" - -#include "esphome/core/defines.h" -#include "esphome/core/helpers.h" - -namespace esphome { -namespace ota_base { - -class ArduinoESP32OTABackend : public OTABackend { - public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; - bool supports_compression() override { return false; } - - private: - bool md5_set_{false}; -}; - -} // namespace ota_base -} // namespace esphome - -#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/ota_base/ota_backend_arduino_esp8266.cpp b/esphome/components/ota_base/ota_backend_arduino_esp8266.cpp deleted file mode 100644 index a9d48b59df..0000000000 --- a/esphome/components/ota_base/ota_backend_arduino_esp8266.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#ifdef USE_ARDUINO -#ifdef USE_ESP8266 -#include "ota_backend_arduino_esp8266.h" -#include "ota_backend.h" - -#include "esphome/components/esp8266/preferences.h" -#include "esphome/core/defines.h" -#include "esphome/core/log.h" - -#include - -namespace esphome { -namespace ota_base { - -static const char *const TAG = "ota.arduino_esp8266"; - -std::unique_ptr make_ota_backend() { return make_unique(); } - -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); - return OTA_RESPONSE_OK; - } - - uint8_t error = Update.getError(); - if (error == UPDATE_ERROR_BOOTSTRAP) - return OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING; - if (error == UPDATE_ERROR_NEW_FLASH_CONFIG) - return OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG; - if (error == UPDATE_ERROR_FLASH_CONFIG) - return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; - if (error == UPDATE_ERROR_SPACE) - return OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE; - - ESP_LOGE(TAG, "Begin error: %d", error); - - return OTA_RESPONSE_ERROR_UNKNOWN; -} - -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); - if (written == len) { - return OTA_RESPONSE_OK; - } - - uint8_t error = Update.getError(); - ESP_LOGE(TAG, "Write error: %d", error); - - return OTA_RESPONSE_ERROR_WRITING_FLASH; -} - -OTAResponseTypes ArduinoESP8266OTABackend::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; - } - - ESP_LOGE(TAG, "End error: %d", error); - return OTA_RESPONSE_ERROR_UPDATE_END; -} - -void ArduinoESP8266OTABackend::abort() { - Update.end(); - esp8266::preferences_prevent_write(false); -} - -} // namespace ota_base -} // namespace esphome - -#endif -#endif diff --git a/esphome/components/ota_base/ota_backend_arduino_esp8266.h b/esphome/components/ota_base/ota_backend_arduino_esp8266.h deleted file mode 100644 index f399013121..0000000000 --- a/esphome/components/ota_base/ota_backend_arduino_esp8266.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once -#ifdef USE_ARDUINO -#ifdef USE_ESP8266 -#include "ota_backend.h" - -#include "esphome/core/defines.h" -#include "esphome/core/macros.h" - -namespace esphome { -namespace ota_base { - -class ArduinoESP8266OTABackend : public OTABackend { - public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) - bool supports_compression() override { return true; } -#else - bool supports_compression() override { return false; } -#endif - - private: - bool md5_set_{false}; -}; - -} // namespace ota_base -} // namespace esphome - -#endif -#endif diff --git a/esphome/components/ota_base/ota_backend_arduino_libretiny.cpp b/esphome/components/ota_base/ota_backend_arduino_libretiny.cpp deleted file mode 100644 index 2596e3c2a3..0000000000 --- a/esphome/components/ota_base/ota_backend_arduino_libretiny.cpp +++ /dev/null @@ -1,72 +0,0 @@ -#ifdef USE_LIBRETINY -#include "ota_backend_arduino_libretiny.h" -#include "ota_backend.h" - -#include "esphome/core/defines.h" -#include "esphome/core/log.h" - -#include - -namespace esphome { -namespace ota_base { - -static const char *const TAG = "ota.arduino_libretiny"; - -std::unique_ptr make_ota_backend() { return make_unique(); } - -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; - } - - uint8_t error = Update.getError(); - if (error == UPDATE_ERROR_SIZE) - return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE; - - ESP_LOGE(TAG, "Begin error: %d", error); - - return OTA_RESPONSE_ERROR_UNKNOWN; -} - -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); - if (written == len) { - return OTA_RESPONSE_OK; - } - - uint8_t error = Update.getError(); - ESP_LOGE(TAG, "Write error: %d", error); - - return OTA_RESPONSE_ERROR_WRITING_FLASH; -} - -OTAResponseTypes ArduinoLibreTinyOTABackend::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; - } - - uint8_t error = Update.getError(); - ESP_LOGE(TAG, "End error: %d", error); - - return OTA_RESPONSE_ERROR_UPDATE_END; -} - -void ArduinoLibreTinyOTABackend::abort() { Update.abort(); } - -} // namespace ota_base -} // namespace esphome - -#endif // USE_LIBRETINY diff --git a/esphome/components/ota_base/ota_backend_arduino_libretiny.h b/esphome/components/ota_base/ota_backend_arduino_libretiny.h deleted file mode 100644 index 33eebeb95a..0000000000 --- a/esphome/components/ota_base/ota_backend_arduino_libretiny.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once -#ifdef USE_LIBRETINY -#include "ota_backend.h" - -#include "esphome/core/defines.h" - -namespace esphome { -namespace ota_base { - -class ArduinoLibreTinyOTABackend : public OTABackend { - public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; - bool supports_compression() override { return false; } - - private: - bool md5_set_{false}; -}; - -} // namespace ota_base -} // namespace esphome - -#endif // USE_LIBRETINY diff --git a/esphome/components/ota_base/ota_backend_arduino_rp2040.cpp b/esphome/components/ota_base/ota_backend_arduino_rp2040.cpp deleted file mode 100644 index 160c529231..0000000000 --- a/esphome/components/ota_base/ota_backend_arduino_rp2040.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#ifdef USE_ARDUINO -#ifdef USE_RP2040 -#include "ota_backend_arduino_rp2040.h" -#include "ota_backend.h" - -#include "esphome/components/rp2040/preferences.h" -#include "esphome/core/defines.h" -#include "esphome/core/log.h" - -#include - -namespace esphome { -namespace ota_base { - -static const char *const TAG = "ota.arduino_rp2040"; - -std::unique_ptr make_ota_backend() { return make_unique(); } - -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); - } - bool ret = Update.begin(image_size, U_FLASH); - if (ret) { - rp2040::preferences_prevent_write(true); - return OTA_RESPONSE_OK; - } - - uint8_t error = Update.getError(); - if (error == UPDATE_ERROR_BOOTSTRAP) - return OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING; - if (error == UPDATE_ERROR_NEW_FLASH_CONFIG) - return OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG; - if (error == UPDATE_ERROR_FLASH_CONFIG) - return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; - if (error == UPDATE_ERROR_SPACE) - return OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE; - - ESP_LOGE(TAG, "Begin error: %d", error); - - return OTA_RESPONSE_ERROR_UNKNOWN; -} - -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); - if (written == len) { - return OTA_RESPONSE_OK; - } - - uint8_t error = Update.getError(); - ESP_LOGE(TAG, "Write error: %d", error); - - return OTA_RESPONSE_ERROR_WRITING_FLASH; -} - -OTAResponseTypes ArduinoRP2040OTABackend::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; - } - - uint8_t error = Update.getError(); - ESP_LOGE(TAG, "End error: %d", error); - - return OTA_RESPONSE_ERROR_UPDATE_END; -} - -void ArduinoRP2040OTABackend::abort() { - Update.end(); - rp2040::preferences_prevent_write(false); -} - -} // namespace ota_base -} // namespace esphome - -#endif // USE_RP2040 -#endif // USE_ARDUINO diff --git a/esphome/components/ota_base/ota_backend_arduino_rp2040.h b/esphome/components/ota_base/ota_backend_arduino_rp2040.h deleted file mode 100644 index 6d622d4a5a..0000000000 --- a/esphome/components/ota_base/ota_backend_arduino_rp2040.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once -#ifdef USE_ARDUINO -#ifdef USE_RP2040 -#include "ota_backend.h" - -#include "esphome/core/defines.h" -#include "esphome/core/macros.h" - -namespace esphome { -namespace ota_base { - -class ArduinoRP2040OTABackend : public OTABackend { - public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; - bool supports_compression() override { return false; } - - private: - bool md5_set_{false}; -}; - -} // namespace ota_base -} // namespace esphome - -#endif // USE_RP2040 -#endif // USE_ARDUINO diff --git a/esphome/components/ota_base/ota_backend_esp_idf.cpp b/esphome/components/ota_base/ota_backend_esp_idf.cpp deleted file mode 100644 index e5cc1efaf6..0000000000 --- a/esphome/components/ota_base/ota_backend_esp_idf.cpp +++ /dev/null @@ -1,110 +0,0 @@ -#ifdef USE_ESP_IDF -#include "ota_backend_esp_idf.h" - -#include "esphome/components/md5/md5.h" -#include "esphome/core/defines.h" - -#include -#include -#include - -namespace esphome { -namespace ota_base { - -std::unique_ptr make_ota_backend() { return make_unique(); } - -OTAResponseTypes IDFOTABackend::begin(size_t image_size) { - this->partition_ = esp_ota_get_next_update_partition(nullptr); - if (this->partition_ == nullptr) { - return OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION; - } - -#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15 - // The following function takes longer than the 5 seconds timeout of WDT - 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); -#endif - - esp_err_t err = esp_ota_begin(this->partition_, image_size, &this->update_handle_); - -#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15 - // Set the WDT back to the configured timeout - wdtc.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000; - esp_task_wdt_reconfigure(&wdtc); -#endif - - if (err != ESP_OK) { - esp_ota_abort(this->update_handle_); - this->update_handle_ = 0; - if (err == ESP_ERR_INVALID_SIZE) { - return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE; - } else if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) { - return OTA_RESPONSE_ERROR_WRITING_FLASH; - } - return OTA_RESPONSE_ERROR_UNKNOWN; - } - this->md5_.init(); - return OTA_RESPONSE_OK; -} - -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); - this->md5_.add(data, len); - if (err != ESP_OK) { - if (err == ESP_ERR_OTA_VALIDATE_FAILED) { - return OTA_RESPONSE_ERROR_MAGIC; - } else if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) { - return OTA_RESPONSE_ERROR_WRITING_FLASH; - } - return OTA_RESPONSE_ERROR_UNKNOWN; - } - return OTA_RESPONSE_OK; -} - -OTAResponseTypes IDFOTABackend::end() { - 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; - if (err == ESP_OK) { - err = esp_ota_set_boot_partition(this->partition_); - if (err == ESP_OK) { - return OTA_RESPONSE_OK; - } - } - if (err == ESP_ERR_OTA_VALIDATE_FAILED) { - return OTA_RESPONSE_ERROR_UPDATE_END; - } - if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) { - return OTA_RESPONSE_ERROR_WRITING_FLASH; - } - return OTA_RESPONSE_ERROR_UNKNOWN; -} - -void IDFOTABackend::abort() { - esp_ota_abort(this->update_handle_); - this->update_handle_ = 0; -} - -} // namespace ota_base -} // namespace esphome -#endif diff --git a/esphome/components/ota_base/ota_backend_esp_idf.h b/esphome/components/ota_base/ota_backend_esp_idf.h deleted file mode 100644 index 3c760df1c8..0000000000 --- a/esphome/components/ota_base/ota_backend_esp_idf.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once -#ifdef USE_ESP_IDF -#include "ota_backend.h" - -#include "esphome/components/md5/md5.h" -#include "esphome/core/defines.h" - -#include - -namespace esphome { -namespace ota_base { - -class IDFOTABackend : public OTABackend { - public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; - bool supports_compression() override { return false; } - - private: - esp_ota_handle_t update_handle_{0}; - const esp_partition_t *partition_; - md5::MD5Digest md5_{}; - char expected_bin_md5_[32]; - bool md5_set_{false}; -}; - -} // namespace ota_base -} // namespace esphome -#endif diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index 81ecf22c71..9a7630a7be 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -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() { diff --git a/esphome/components/qr_code/__init__.py b/esphome/components/qr_code/__init__.py index 1c5e0471b0..6ff92b8a7f 100644 --- a/esphome/components/qr_code/__init__.py +++ b/esphome/components/qr_code/__init__.py @@ -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") diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 2c4a0f917f..65a3af1bbc 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -371,6 +371,7 @@ void Rtttl::finish_() { ESP_LOGD(TAG, "Playback finished"); } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG static const LogString *state_to_string(State state) { switch (state) { case STATE_STOPPED: @@ -387,6 +388,7 @@ static const LogString *state_to_string(State state) { return LOG_STR("UNKNOWN"); } }; +#endif void Rtttl::set_state_(State state) { State old_state = this->state_; diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 6b3ff6f4d3..ab821d457b 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -268,7 +268,19 @@ def validate_tz(value: str) -> str: TIME_SCHEMA = cv.Schema( { - cv.Optional(CONF_TIMEZONE, default=detect_tz): validate_tz, + cv.SplitDefault( + CONF_TIMEZONE, + esp8266=detect_tz, + esp32=detect_tz, + rp2040=detect_tz, + bk72xx=detect_tz, + rtl87xx=detect_tz, + ln882x=detect_tz, + host=detect_tz, + ): cv.All( + cv.only_with_framework(["arduino", "esp-idf", "host"]), + validate_tz, + ), cv.Optional(CONF_ON_TIME): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CronTrigger), @@ -293,7 +305,9 @@ TIME_SCHEMA = cv.Schema( async def setup_time_core_(time_var, config): - cg.add(time_var.set_timezone(config[CONF_TIMEZONE])) + if timezone := config.get(CONF_TIMEZONE): + cg.add(time_var.set_timezone(timezone)) + cg.add_define("USE_TIME_TIMEZONE") for conf in config.get(CONF_ON_TIME, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var) diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 11e39e8f67..61391d2c6b 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -35,8 +35,10 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { ret = settimeofday(&timev, nullptr); } +#ifdef USE_TIME_TIMEZONE // Move timezone back to local timezone. this->apply_timezone_(); +#endif if (ret != 0) { ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); @@ -49,10 +51,12 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { this->time_sync_callback_.call(); } +#ifdef USE_TIME_TIMEZONE void RealTimeClock::apply_timezone_() { setenv("TZ", this->timezone_.c_str(), 1); tzset(); } +#endif } // namespace time } // namespace esphome diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 401798a568..4b98a88975 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -20,6 +20,7 @@ class RealTimeClock : public PollingComponent { public: explicit RealTimeClock(); +#ifdef USE_TIME_TIMEZONE /// Set the time zone. void set_timezone(const std::string &tz) { this->timezone_ = tz; @@ -28,6 +29,7 @@ class RealTimeClock : public PollingComponent { /// Get the time zone currently in use. std::string get_timezone() { return this->timezone_; } +#endif /// Get the time in the currently defined timezone. ESPTime now() { return ESPTime::from_epoch_local(this->timestamp_now()); } @@ -38,7 +40,7 @@ class RealTimeClock : public PollingComponent { /// Get the current time as the UTC epoch since January 1st 1970. time_t timestamp_now() { return ::time(nullptr); } - void add_on_time_sync_callback(std::function callback) { + void add_on_time_sync_callback(std::function &&callback) { this->time_sync_callback_.add(std::move(callback)); }; @@ -46,8 +48,10 @@ class RealTimeClock : public PollingComponent { /// Report a unix epoch as current time. void synchronize_epoch_(uint32_t epoch); +#ifdef USE_TIME_TIMEZONE std::string timezone_{}; void apply_timezone_(); +#endif CallbackManager time_sync_callback_; }; diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 8c77866540..6890f60014 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -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") diff --git a/esphome/components/web_server/ota/__init__.py b/esphome/components/web_server/ota/__init__.py new file mode 100644 index 0000000000..3af14fd453 --- /dev/null +++ b/esphome/components/web_server/ota/__init__.py @@ -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") diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp new file mode 100644 index 0000000000..4f8f6fda17 --- /dev/null +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -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 +#elif defined(USE_ESP32) || defined(USE_LIBRETINY) +#include +#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_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(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(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(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(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 diff --git a/esphome/components/web_server/ota/ota_web_server.h b/esphome/components/web_server/ota/ota_web_server.h new file mode 100644 index 0000000000..a7170c0e34 --- /dev/null +++ b/esphome/components/web_server/ota/ota_web_server.h @@ -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 diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index e0027d0b27..d5ded2a02c 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -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 diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 991bca6fa7..5f175b6bdd 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -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> to_schedule_; diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index c9b38a2dc4..5db0f1cae9 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -192,11 +192,10 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { stream->print(F("

See ESPHome Web API for " "REST API documentation.

")); - if (this->allow_ota_) { - stream->print( - F("

OTA Update

")); - } +#ifdef USE_WEBSERVER_OTA + stream->print(F("

OTA Update

")); +#endif stream->print(F("

Debug Log

"));
 #ifdef USE_WEBSERVER_JS_INCLUDE
   if (this->js_include_ != nullptr) {
diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py
index c17bab2128..754bf7d433 100644
--- a/esphome/components/web_server_base/__init__.py
+++ b/esphome/components/web_server_base/__init__.py
@@ -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:
diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp
index a683ee85eb..e1c2bc0b25 100644
--- a/esphome/components/web_server_base/web_server_base.cpp
+++ b/esphome/components/web_server_base/web_server_base.cpp
@@ -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 
-#elif defined(USE_ESP32) || defined(USE_LIBRETINY)
-#include 
-#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(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(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(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(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;
diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h
index 99087ddfa8..a475238a37 100644
--- a/esphome/components/web_server_base/web_server_base.h
+++ b/esphome/components/web_server_base/web_server_base.h
@@ -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 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_backend_{nullptr};
-};
-#endif  // USE_WEBSERVER_OTA
-
 }  // namespace web_server_base
 }  // namespace esphome
 #endif
diff --git a/esphome/components/web_server_idf/__init__.py b/esphome/components/web_server_idf/__init__.py
index fe1c6f2640..506e1c5c13 100644
--- a/esphome/components/web_server_idf/__init__.py
+++ b/esphome/components/web_server_idf/__init__.py
@@ -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")
diff --git a/esphome/config.py b/esphome/config.py
index 73cc7657cc..c4aa9aea24 100644
--- a/esphome/config.py
+++ b/esphome/config.py
@@ -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):
diff --git a/esphome/core/defines.h b/esphome/core/defines.h
index cfaed6fdb7..be872689f3 100644
--- a/esphome/core/defines.h
+++ b/esphome/core/defines.h
@@ -116,6 +116,7 @@
 #define USE_OTA_PASSWORD
 #define USE_OTA_STATE_CALLBACK
 #define USE_OTA_VERSION 2
+#define USE_TIME_TIMEZONE
 #define USE_WIFI
 #define USE_WIFI_AP
 #define USE_WIREGUARD
diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp
index fc91d83972..b4923c7af0 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -4,13 +4,13 @@
 #include "esphome/core/hal.h"
 #include "esphome/core/log.h"
 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
-#include 
 
 #ifdef USE_HOST
 #ifndef _WIN32
@@ -43,10 +43,10 @@
 #include 
 #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 &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;
diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h
index 7d5366f323..362f3d1fa4 100644
--- a/esphome/core/helpers.h
+++ b/esphome/core/helpers.h
@@ -348,6 +348,8 @@ std::string format_hex_pretty(const uint16_t *data, size_t length);
 std::string format_hex_pretty(const std::vector &data);
 /// Format the vector \p data in pretty-printed, human-readable hex.
 std::string format_hex_pretty(const std::vector &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::value, int> = 0> std::string format_hex_pretty(T val) {
   val = convert_big_endian(val);
diff --git a/esphome/writer.py b/esphome/writer.py
index 7a5089e384..943dfa78cc 100644
--- a/esphome/writer.py
+++ b/esphome/writer.py
@@ -107,6 +107,11 @@ def storage_should_clean(old: StorageJSON, new: StorageJSON) -> bool:
         return True
     if old.build_path != new.build_path:
         return True
+
+    return False
+
+
+def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool:
     if (
         old.loaded_integrations != new.loaded_integrations
         or old.loaded_platforms != new.loaded_platforms
@@ -126,10 +131,11 @@ def update_storage_json():
         return
 
     if storage_should_clean(old, new):
-        _LOGGER.info(
-            "Core config, version or integrations changed, cleaning build files..."
-        )
+        _LOGGER.info("Core config, version changed, cleaning build files...")
         clean_build()
+    elif storage_should_update_cmake_cache(old, new):
+        _LOGGER.info("Integrations changed, cleaning cmake cache...")
+        clean_cmake_cache()
 
     new.save(path)
 
@@ -353,6 +359,15 @@ def write_cpp(code_s):
     write_file_if_changed(path, full_file)
 
 
+def clean_cmake_cache():
+    pioenvs = CORE.relative_pioenvs_path()
+    if os.path.isdir(pioenvs):
+        pioenvs_cmake_path = CORE.relative_pioenvs_path(CORE.name, "CMakeCache.txt")
+        if os.path.isfile(pioenvs_cmake_path):
+            _LOGGER.info("Deleting %s", pioenvs_cmake_path)
+            os.remove(pioenvs_cmake_path)
+
+
 def clean_build():
     import shutil
 
diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py
index 15313f48ee..56a46a7701 100755
--- a/script/api_protobuf/api_protobuf.py
+++ b/script/api_protobuf/api_protobuf.py
@@ -534,7 +534,7 @@ class BytesType(TypeInfo):
         return f"buffer.encode_bytes({self.number}, reinterpret_cast(this->{self.field_name}.data()), this->{self.field_name}.size());"
 
     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:
@@ -1259,6 +1259,7 @@ def main() -> None:
     #include "api_pb2.h"
     #include "api_pb2_size.h"
     #include "esphome/core/log.h"
+    #include "esphome/core/helpers.h"
 
     #include 
 
diff --git a/tests/component_tests/ota/test_web_server_ota.py b/tests/component_tests/ota/test_web_server_ota.py
new file mode 100644
index 0000000000..0d8ff6f134
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota.py
@@ -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
diff --git a/tests/component_tests/ota/test_web_server_ota.yaml b/tests/component_tests/ota/test_web_server_ota.yaml
new file mode 100644
index 0000000000..e0fda3d0b5
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota.yaml
@@ -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
diff --git a/tests/component_tests/ota/test_web_server_ota_arduino.yaml b/tests/component_tests/ota/test_web_server_ota_arduino.yaml
new file mode 100644
index 0000000000..9462548cc8
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota_arduino.yaml
@@ -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
diff --git a/tests/component_tests/ota/test_web_server_ota_callbacks.yaml b/tests/component_tests/ota/test_web_server_ota_callbacks.yaml
new file mode 100644
index 0000000000..c2fd9e0f19
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota_callbacks.yaml
@@ -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"
diff --git a/tests/component_tests/ota/test_web_server_ota_esp8266.yaml b/tests/component_tests/ota/test_web_server_ota_esp8266.yaml
new file mode 100644
index 0000000000..a1b66a5b53
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota_esp8266.yaml
@@ -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
diff --git a/tests/component_tests/ota/test_web_server_ota_idf.yaml b/tests/component_tests/ota/test_web_server_ota_idf.yaml
new file mode 100644
index 0000000000..18b639347c
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota_idf.yaml
@@ -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
diff --git a/tests/component_tests/ota/test_web_server_ota_multi.yaml b/tests/component_tests/ota/test_web_server_ota_multi.yaml
new file mode 100644
index 0000000000..7926b09c71
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota_multi.yaml
@@ -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
diff --git a/tests/component_tests/web_server/test_ota_migration.py b/tests/component_tests/web_server/test_ota_migration.py
new file mode 100644
index 0000000000..7f34ec75f6
--- /dev/null
+++ b/tests/component_tests/web_server/test_ota_migration.py
@@ -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
diff --git a/tests/components/openthread/test.esp32-c6-idf.yaml b/tests/components/openthread/test.esp32-c6-idf.yaml
index f53b323bec..bbcf48efa5 100644
--- a/tests/components/openthread/test.esp32-c6-idf.yaml
+++ b/tests/components/openthread/test.esp32-c6-idf.yaml
@@ -2,6 +2,7 @@ network:
   enable_ipv6: true
 
 openthread:
+  device_type: FTD
   channel: 13
   network_name: OpenThread-8f28
   network_key: 0xdfd34f0f05cad978ec4e32b0413038ff
diff --git a/tests/components/web_server/test_no_ota.esp32-idf.yaml b/tests/components/web_server/test_no_ota.esp32-idf.yaml
index 1f677fb948..4064f518cf 100644
--- a/tests/components/web_server/test_no_ota.esp32-idf.yaml
+++ b/tests/components/web_server/test_no_ota.esp32-idf.yaml
@@ -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
diff --git a/tests/components/web_server/test_ota.esp32-idf.yaml b/tests/components/web_server/test_ota.esp32-idf.yaml
index 294e7f862e..37838b3d34 100644
--- a/tests/components/web_server/test_ota.esp32-idf.yaml
+++ b/tests/components/web_server/test_ota.esp32-idf.yaml
@@ -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
diff --git a/tests/components/web_server/test_ota_disabled.esp32-idf.yaml b/tests/components/web_server/test_ota_disabled.esp32-idf.yaml
index c7c7574e3b..b88b845db7 100644
--- a/tests/components/web_server/test_ota_disabled.esp32-idf.yaml
+++ b/tests/components/web_server/test_ota_disabled.esp32-idf.yaml
@@ -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