mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 09:01:49 +00:00 
			
		
		
		
	Merge branch 'password_api' into integration
This commit is contained in:
		@@ -499,6 +499,7 @@ esphome/components/voice_assistant/* @jesserockz @kahrendt
 | 
				
			|||||||
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
 | 
					esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
 | 
				
			||||||
esphome/components/watchdog/* @oarcher
 | 
					esphome/components/watchdog/* @oarcher
 | 
				
			||||||
esphome/components/waveshare_epaper/* @clydebarrow
 | 
					esphome/components/waveshare_epaper/* @clydebarrow
 | 
				
			||||||
 | 
					esphome/components/web_server/ota/* @esphome/core
 | 
				
			||||||
esphome/components/web_server_base/* @OttoWinter
 | 
					esphome/components/web_server_base/* @OttoWinter
 | 
				
			||||||
esphome/components/web_server_idf/* @dentra
 | 
					esphome/components/web_server_idf/* @dentra
 | 
				
			||||||
esphome/components/weikai/* @DrCoolZic
 | 
					esphome/components/weikai/* @DrCoolZic
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -132,6 +132,8 @@ async def to_code(config):
 | 
				
			|||||||
    await cg.register_component(var, config)
 | 
					    await cg.register_component(var, config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    cg.add(var.set_port(config[CONF_PORT]))
 | 
					    cg.add(var.set_port(config[CONF_PORT]))
 | 
				
			||||||
 | 
					    if config[CONF_PASSWORD]:
 | 
				
			||||||
 | 
					        cg.add_define("USE_API_PASSWORD")
 | 
				
			||||||
        cg.add(var.set_password(config[CONF_PASSWORD]))
 | 
					        cg.add(var.set_password(config[CONF_PASSWORD]))
 | 
				
			||||||
    cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
 | 
					    cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
 | 
				
			||||||
    cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
 | 
					    cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1503,7 +1503,10 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) {
 | 
				
			|||||||
  return resp;
 | 
					  return resp;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
 | 
					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;
 | 
					  ConnectResponse resp;
 | 
				
			||||||
  // bool invalid_password = 1;
 | 
					  // bool invalid_password = 1;
 | 
				
			||||||
@@ -1524,7 +1527,11 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
 | 
					DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
 | 
				
			||||||
  DeviceInfoResponse resp{};
 | 
					  DeviceInfoResponse resp{};
 | 
				
			||||||
 | 
					#ifdef USE_API_PASSWORD
 | 
				
			||||||
  resp.uses_password = this->parent_->uses_password();
 | 
					  resp.uses_password = this->parent_->uses_password();
 | 
				
			||||||
 | 
					#else
 | 
				
			||||||
 | 
					  resp.uses_password = false;
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
  resp.name = App.get_name();
 | 
					  resp.name = App.get_name();
 | 
				
			||||||
  resp.friendly_name = App.get_friendly_name();
 | 
					  resp.friendly_name = App.get_friendly_name();
 | 
				
			||||||
  resp.suggested_area = App.get_area();
 | 
					  resp.suggested_area = App.get_area();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@
 | 
				
			|||||||
#include "api_pb2.h"
 | 
					#include "api_pb2.h"
 | 
				
			||||||
#include "api_pb2_size.h"
 | 
					#include "api_pb2_size.h"
 | 
				
			||||||
#include "esphome/core/log.h"
 | 
					#include "esphome/core/log.h"
 | 
				
			||||||
 | 
					#include "esphome/core/helpers.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <cinttypes>
 | 
					#include <cinttypes>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -3510,7 +3511,7 @@ void SubscribeLogsResponse::dump_to(std::string &out) const {
 | 
				
			|||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  out.append("  message: ");
 | 
					  out.append("  message: ");
 | 
				
			||||||
  out.append("'").append(this->message).append("'");
 | 
					  out.append(format_hex_pretty(this->message));
 | 
				
			||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  out.append("  send_failed: ");
 | 
					  out.append("  send_failed: ");
 | 
				
			||||||
@@ -3540,7 +3541,7 @@ void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const {
 | 
				
			|||||||
  __attribute__((unused)) char buffer[64];
 | 
					  __attribute__((unused)) char buffer[64];
 | 
				
			||||||
  out.append("NoiseEncryptionSetKeyRequest {\n");
 | 
					  out.append("NoiseEncryptionSetKeyRequest {\n");
 | 
				
			||||||
  out.append("  key: ");
 | 
					  out.append("  key: ");
 | 
				
			||||||
  out.append("'").append(this->key).append("'");
 | 
					  out.append(format_hex_pretty(this->key));
 | 
				
			||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
  out.append("}");
 | 
					  out.append("}");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -4286,7 +4287,7 @@ void CameraImageResponse::dump_to(std::string &out) const {
 | 
				
			|||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  out.append("  data: ");
 | 
					  out.append("  data: ");
 | 
				
			||||||
  out.append("'").append(this->data).append("'");
 | 
					  out.append(format_hex_pretty(this->data));
 | 
				
			||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  out.append("  done: ");
 | 
					  out.append("  done: ");
 | 
				
			||||||
@@ -6813,7 +6814,7 @@ void BluetoothServiceData::dump_to(std::string &out) const {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  out.append("  data: ");
 | 
					  out.append("  data: ");
 | 
				
			||||||
  out.append("'").append(this->data).append("'");
 | 
					  out.append(format_hex_pretty(this->data));
 | 
				
			||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
  out.append("}");
 | 
					  out.append("}");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -6896,7 +6897,7 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const {
 | 
				
			|||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  out.append("  name: ");
 | 
					  out.append("  name: ");
 | 
				
			||||||
  out.append("'").append(this->name).append("'");
 | 
					  out.append(format_hex_pretty(this->name));
 | 
				
			||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  out.append("  rssi: ");
 | 
					  out.append("  rssi: ");
 | 
				
			||||||
@@ -6989,7 +6990,7 @@ void BluetoothLERawAdvertisement::dump_to(std::string &out) const {
 | 
				
			|||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  out.append("  data: ");
 | 
					  out.append("  data: ");
 | 
				
			||||||
  out.append("'").append(this->data).append("'");
 | 
					  out.append(format_hex_pretty(this->data));
 | 
				
			||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
  out.append("}");
 | 
					  out.append("}");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -7516,7 +7517,7 @@ void BluetoothGATTReadResponse::dump_to(std::string &out) const {
 | 
				
			|||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  out.append("  data: ");
 | 
					  out.append("  data: ");
 | 
				
			||||||
  out.append("'").append(this->data).append("'");
 | 
					  out.append(format_hex_pretty(this->data));
 | 
				
			||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
  out.append("}");
 | 
					  out.append("}");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -7580,7 +7581,7 @@ void BluetoothGATTWriteRequest::dump_to(std::string &out) const {
 | 
				
			|||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  out.append("  data: ");
 | 
					  out.append("  data: ");
 | 
				
			||||||
  out.append("'").append(this->data).append("'");
 | 
					  out.append(format_hex_pretty(this->data));
 | 
				
			||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
  out.append("}");
 | 
					  out.append("}");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -7672,7 +7673,7 @@ void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const {
 | 
				
			|||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  out.append("  data: ");
 | 
					  out.append("  data: ");
 | 
				
			||||||
  out.append("'").append(this->data).append("'");
 | 
					  out.append(format_hex_pretty(this->data));
 | 
				
			||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
  out.append("}");
 | 
					  out.append("}");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -7774,7 +7775,7 @@ void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const {
 | 
				
			|||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  out.append("  data: ");
 | 
					  out.append("  data: ");
 | 
				
			||||||
  out.append("'").append(this->data).append("'");
 | 
					  out.append(format_hex_pretty(this->data));
 | 
				
			||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
  out.append("}");
 | 
					  out.append("}");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -8494,7 +8495,7 @@ void VoiceAssistantAudio::dump_to(std::string &out) const {
 | 
				
			|||||||
  __attribute__((unused)) char buffer[64];
 | 
					  __attribute__((unused)) char buffer[64];
 | 
				
			||||||
  out.append("VoiceAssistantAudio {\n");
 | 
					  out.append("VoiceAssistantAudio {\n");
 | 
				
			||||||
  out.append("  data: ");
 | 
					  out.append("  data: ");
 | 
				
			||||||
  out.append("'").append(this->data).append("'");
 | 
					  out.append(format_hex_pretty(this->data));
 | 
				
			||||||
  out.append("\n");
 | 
					  out.append("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  out.append("  end: ");
 | 
					  out.append("  end: ");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -218,6 +218,7 @@ void APIServer::dump_config() {
 | 
				
			|||||||
#endif
 | 
					#endif
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ifdef USE_API_PASSWORD
 | 
				
			||||||
bool APIServer::uses_password() const { return !this->password_.empty(); }
 | 
					bool APIServer::uses_password() const { return !this->password_.empty(); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bool APIServer::check_password(const std::string &password) const {
 | 
					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;
 | 
					  return result == 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void APIServer::handle_disconnect(APIConnection *conn) {}
 | 
					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; }
 | 
					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; }
 | 
					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; }
 | 
					void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,10 +35,12 @@ class APIServer : public Component, public Controller {
 | 
				
			|||||||
  void dump_config() override;
 | 
					  void dump_config() override;
 | 
				
			||||||
  void on_shutdown() override;
 | 
					  void on_shutdown() override;
 | 
				
			||||||
  bool teardown() override;
 | 
					  bool teardown() override;
 | 
				
			||||||
 | 
					#ifdef USE_API_PASSWORD
 | 
				
			||||||
  bool check_password(const std::string &password) const;
 | 
					  bool check_password(const std::string &password) const;
 | 
				
			||||||
  bool uses_password() const;
 | 
					  bool uses_password() const;
 | 
				
			||||||
  void set_port(uint16_t port);
 | 
					 | 
				
			||||||
  void set_password(const std::string &password);
 | 
					  void set_password(const std::string &password);
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					  void set_port(uint16_t port);
 | 
				
			||||||
  void set_reboot_timeout(uint32_t reboot_timeout);
 | 
					  void set_reboot_timeout(uint32_t reboot_timeout);
 | 
				
			||||||
  void set_batch_delay(uint16_t batch_delay);
 | 
					  void set_batch_delay(uint16_t batch_delay);
 | 
				
			||||||
  uint16_t get_batch_delay() const { return 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)
 | 
					  // Vectors and strings (12 bytes each on 32-bit)
 | 
				
			||||||
  std::vector<std::unique_ptr<APIConnection>> clients_;
 | 
					  std::vector<std::unique_ptr<APIConnection>> clients_;
 | 
				
			||||||
 | 
					#ifdef USE_API_PASSWORD
 | 
				
			||||||
  std::string password_;
 | 
					  std::string password_;
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
  std::vector<uint8_t> shared_write_buffer_;  // Shared proto write buffer for all connections
 | 
					  std::vector<uint8_t> shared_write_buffer_;  // Shared proto write buffer for all connections
 | 
				
			||||||
  std::vector<HomeAssistantStateSubscription> state_subs_;
 | 
					  std::vector<HomeAssistantStateSubscription> state_subs_;
 | 
				
			||||||
#ifdef USE_API_YAML_SERVICES
 | 
					#ifdef USE_API_YAML_SERVICES
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,7 +12,7 @@ from esphome.const import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
from esphome.core import CORE, coroutine_with_priority
 | 
					from esphome.core import CORE, coroutine_with_priority
 | 
				
			||||||
 | 
					
 | 
				
			||||||
AUTO_LOAD = ["web_server_base"]
 | 
					AUTO_LOAD = ["web_server_base", "ota.web_server"]
 | 
				
			||||||
DEPENDENCIES = ["wifi"]
 | 
					DEPENDENCIES = ["wifi"]
 | 
				
			||||||
CODEOWNERS = ["@OttoWinter"]
 | 
					CODEOWNERS = ["@OttoWinter"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,9 +47,6 @@ void CaptivePortal::start() {
 | 
				
			|||||||
  this->base_->init();
 | 
					  this->base_->init();
 | 
				
			||||||
  if (!this->initialized_) {
 | 
					  if (!this->initialized_) {
 | 
				
			||||||
    this->base_->add_handler(this);
 | 
					    this->base_->add_handler(this);
 | 
				
			||||||
#ifdef USE_WEBSERVER_OTA
 | 
					 | 
				
			||||||
    this->base_->add_ota_handler();
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#ifdef USE_ARDUINO
 | 
					#ifdef USE_ARDUINO
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -93,7 +93,6 @@ class ESP32TouchComponent : public Component {
 | 
				
			|||||||
  uint32_t last_release_check_{0};
 | 
					  uint32_t last_release_check_{0};
 | 
				
			||||||
  uint32_t release_timeout_ms_{1500};
 | 
					  uint32_t release_timeout_ms_{1500};
 | 
				
			||||||
  uint32_t release_check_interval_ms_{50};
 | 
					  uint32_t release_check_interval_ms_{50};
 | 
				
			||||||
  bool initial_state_published_[TOUCH_PAD_MAX] = {false};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Common configuration parameters
 | 
					  // Common configuration parameters
 | 
				
			||||||
  uint16_t sleep_cycle_{4095};
 | 
					  uint16_t sleep_cycle_{4095};
 | 
				
			||||||
@@ -123,13 +122,6 @@ class ESP32TouchComponent : public Component {
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 protected:
 | 
					 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};
 | 
					  uint32_t iir_filter_{0};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool iir_filter_enabled_() const { return this->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;
 | 
					    uint32_t intr_mask;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Track last touch time for timeout-based release detection
 | 
					 | 
				
			||||||
  uint32_t last_touch_time_[TOUCH_PAD_MAX] = {0};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 protected:
 | 
					 protected:
 | 
				
			||||||
  // Filter configuration
 | 
					  // Filter configuration
 | 
				
			||||||
  touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX};
 | 
					  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};
 | 
					  touch_pad_t touch_pad_{TOUCH_PAD_MAX};
 | 
				
			||||||
  uint32_t threshold_{0};
 | 
					  uint32_t threshold_{0};
 | 
				
			||||||
 | 
					  uint32_t benchmark_{};
 | 
				
			||||||
#ifdef USE_ESP32_VARIANT_ESP32
 | 
					#ifdef USE_ESP32_VARIANT_ESP32
 | 
				
			||||||
  uint32_t value_{0};
 | 
					  uint32_t value_{0};
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
  bool last_state_{false};
 | 
					  bool last_state_{false};
 | 
				
			||||||
  const uint32_t wakeup_threshold_{0};
 | 
					  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
 | 
					}  // namespace esp32_touch
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,16 +22,20 @@ void ESP32TouchComponent::dump_config_base_() {
 | 
				
			|||||||
                "  Sleep cycle: %.2fms\n"
 | 
					                "  Sleep cycle: %.2fms\n"
 | 
				
			||||||
                "  Low Voltage Reference: %s\n"
 | 
					                "  Low Voltage Reference: %s\n"
 | 
				
			||||||
                "  High 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,
 | 
					                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_() {
 | 
					void ESP32TouchComponent::dump_config_sensors_() {
 | 
				
			||||||
  for (auto *child : this->children_) {
 | 
					  for (auto *child : this->children_) {
 | 
				
			||||||
    LOG_BINARY_SENSOR("  ", "Touch Pad", child);
 | 
					    LOG_BINARY_SENSOR("  ", "Touch Pad", child);
 | 
				
			||||||
    ESP_LOGCONFIG(TAG, "    Pad: T%" PRIu32, (uint32_t) child->get_touch_pad());
 | 
					    ESP_LOGCONFIG(TAG,
 | 
				
			||||||
    ESP_LOGCONFIG(TAG, "    Threshold: %" PRIu32, child->get_threshold());
 | 
					                  "    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) {
 | 
					void ESP32TouchComponent::publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now) {
 | 
				
			||||||
  touch_pad_t pad = child->get_touch_pad();
 | 
					  if (!child->initial_state_published_) {
 | 
				
			||||||
  if (!this->initial_state_published_[pad]) {
 | 
					 | 
				
			||||||
    // Check if enough time has passed since startup
 | 
					    // Check if enough time has passed since startup
 | 
				
			||||||
    if (now > this->release_timeout_ms_) {
 | 
					    if (now > this->release_timeout_ms_) {
 | 
				
			||||||
      child->publish_initial_state(false);
 | 
					      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());
 | 
					      ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (initial)", child->get_name().c_str());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -104,7 +104,7 @@ void ESP32TouchComponent::loop() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      // Track when we last saw this pad as touched
 | 
					      // Track when we last saw this pad as touched
 | 
				
			||||||
      if (new_state) {
 | 
					      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
 | 
					      // Only publish if state changed - this filters out repeated events
 | 
				
			||||||
@@ -127,15 +127,13 @@ void ESP32TouchComponent::loop() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  size_t pads_off = 0;
 | 
					  size_t pads_off = 0;
 | 
				
			||||||
  for (auto *child : this->children_) {
 | 
					  for (auto *child : this->children_) {
 | 
				
			||||||
    touch_pad_t pad = child->get_touch_pad();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Handle initial state publication after startup
 | 
					    // Handle initial state publication after startup
 | 
				
			||||||
    this->publish_initial_state_if_needed_(child, now);
 | 
					    this->publish_initial_state_if_needed_(child, now);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (child->last_state_) {
 | 
					    if (child->last_state_) {
 | 
				
			||||||
      // Pad is currently in touched state - check for release timeout
 | 
					      // Pad is currently in touched state - check for release timeout
 | 
				
			||||||
      // Using subtraction handles 32-bit rollover correctly
 | 
					      // 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
 | 
					      // Check if we haven't seen this pad recently
 | 
				
			||||||
      if (time_diff > this->release_timeout_ms_) {
 | 
					      if (time_diff > this->release_timeout_ms_) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,19 +14,16 @@ static const char *const TAG = "esp32_touch";
 | 
				
			|||||||
void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched) {
 | 
					void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched) {
 | 
				
			||||||
  // Always update timer when touched
 | 
					  // Always update timer when touched
 | 
				
			||||||
  if (is_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) {
 | 
					  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->last_state_ = is_touched;
 | 
				
			||||||
    child->publish_state(is_touched);
 | 
					    child->publish_state(is_touched);
 | 
				
			||||||
    if (is_touched) {
 | 
					    if (is_touched) {
 | 
				
			||||||
      // ESP32-S2/S3 v2: touched when value > threshold
 | 
					      // 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(),
 | 
					      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 {
 | 
					    } else {
 | 
				
			||||||
      ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str());
 | 
					      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)
 | 
					// 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) {
 | 
					bool ESP32TouchComponent::check_and_update_touch_state_(ESP32TouchBinarySensor *child) {
 | 
				
			||||||
  // Read current touch value
 | 
					  // 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
 | 
					  // ESP32-S2/S3 v2: Touch is detected when value > threshold + benchmark
 | 
				
			||||||
  bool is_touched = value > child->get_threshold();
 | 
					  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);
 | 
					  this->update_touch_state_(child, is_touched);
 | 
				
			||||||
  return is_touched;
 | 
					  return is_touched;
 | 
				
			||||||
@@ -61,9 +61,9 @@ void ESP32TouchComponent::setup() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Configure each touch pad first
 | 
					  // Configure each touch pad first
 | 
				
			||||||
  for (auto *child : this->children_) {
 | 
					  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) {
 | 
					    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
 | 
					  // Configure measurement parameters
 | 
				
			||||||
  touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_);
 | 
					  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_charge_discharge_times(this->meas_cycle_);
 | 
				
			||||||
  touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_);
 | 
					  touch_pad_set_measurement_interval(this->sleep_cycle_);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Configure timeout if needed
 | 
					  // Configure timeout if needed
 | 
				
			||||||
  touch_pad_timeout_set(true, TOUCH_PAD_THRESHOLD_MAX);
 | 
					  touch_pad_timeout_set(true, TOUCH_PAD_THRESHOLD_MAX);
 | 
				
			||||||
@@ -118,8 +118,8 @@ void ESP32TouchComponent::setup() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Set thresholds for each pad BEFORE starting FSM
 | 
					  // Set thresholds for each pad BEFORE starting FSM
 | 
				
			||||||
  for (auto *child : this->children_) {
 | 
					  for (auto *child : this->children_) {
 | 
				
			||||||
    if (child->get_threshold() != 0) {
 | 
					    if (child->threshold_ != 0) {
 | 
				
			||||||
      touch_pad_set_thresh(child->get_touch_pad(), child->get_threshold());
 | 
					      touch_pad_set_thresh(child->touch_pad_, child->threshold_);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -277,6 +277,7 @@ void ESP32TouchComponent::loop() {
 | 
				
			|||||||
  // Process any queued touch events from interrupts
 | 
					  // Process any queued touch events from interrupts
 | 
				
			||||||
  TouchPadEventV2 event;
 | 
					  TouchPadEventV2 event;
 | 
				
			||||||
  while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) {
 | 
					  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
 | 
					    // Handle timeout events
 | 
				
			||||||
    if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) {
 | 
					    if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) {
 | 
				
			||||||
      // Resume measurement after timeout
 | 
					      // Resume measurement after timeout
 | 
				
			||||||
@@ -289,10 +290,7 @@ void ESP32TouchComponent::loop() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Find the child for the pad that triggered the interrupt
 | 
					    // Find the child for the pad that triggered the interrupt
 | 
				
			||||||
    for (auto *child : this->children_) {
 | 
					    for (auto *child : this->children_) {
 | 
				
			||||||
      if (child->get_touch_pad() != event.pad) {
 | 
					      if (child->touch_pad_ == event.pad) {
 | 
				
			||||||
        continue;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) {
 | 
					        if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) {
 | 
				
			||||||
          // For timeout events, we need to read the value to determine state
 | 
					          // For timeout events, we need to read the value to determine state
 | 
				
			||||||
          this->check_and_update_touch_state_(child);
 | 
					          this->check_and_update_touch_state_(child);
 | 
				
			||||||
@@ -303,6 +301,7 @@ void ESP32TouchComponent::loop() {
 | 
				
			|||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Check for released pads periodically (like v1)
 | 
					  // Check for released pads periodically (like v1)
 | 
				
			||||||
  if (!this->should_check_for_releases_(now)) {
 | 
					  if (!this->should_check_for_releases_(now)) {
 | 
				
			||||||
@@ -311,15 +310,15 @@ void ESP32TouchComponent::loop() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  size_t pads_off = 0;
 | 
					  size_t pads_off = 0;
 | 
				
			||||||
  for (auto *child : this->children_) {
 | 
					  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
 | 
					    // Handle initial state publication after startup
 | 
				
			||||||
    this->publish_initial_state_if_needed_(child, now);
 | 
					    this->publish_initial_state_if_needed_(child, now);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (child->last_state_) {
 | 
					    if (child->last_state_) {
 | 
				
			||||||
      // Pad is currently in touched state - check for release timeout
 | 
					      // Pad is currently in touched state - check for release timeout
 | 
				
			||||||
      // Using subtraction handles 32-bit rollover correctly
 | 
					      // 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
 | 
					      // Check if we haven't seen this pad recently
 | 
				
			||||||
      if (time_diff > this->release_timeout_ms_) {
 | 
					      if (time_diff > this->release_timeout_ms_) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -70,6 +70,7 @@ PROTOCOLS = {
 | 
				
			|||||||
    "airway": Protocol.PROTOCOL_AIRWAY,
 | 
					    "airway": Protocol.PROTOCOL_AIRWAY,
 | 
				
			||||||
    "bgh_aud": Protocol.PROTOCOL_BGH_AUD,
 | 
					    "bgh_aud": Protocol.PROTOCOL_BGH_AUD,
 | 
				
			||||||
    "panasonic_altdke": Protocol.PROTOCOL_PANASONIC_ALTDKE,
 | 
					    "panasonic_altdke": Protocol.PROTOCOL_PANASONIC_ALTDKE,
 | 
				
			||||||
 | 
					    "philco_phs32": Protocol.PROTOCOL_PHILCO_PHS32,
 | 
				
			||||||
    "vaillantvai8": Protocol.PROTOCOL_VAILLANTVAI8,
 | 
					    "vaillantvai8": Protocol.PROTOCOL_VAILLANTVAI8,
 | 
				
			||||||
    "r51m": Protocol.PROTOCOL_R51M,
 | 
					    "r51m": Protocol.PROTOCOL_R51M,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -65,6 +65,7 @@ const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP
 | 
				
			|||||||
    {PROTOCOL_AIRWAY, []() { return new AIRWAYHeatpumpIR(); }},                              // NOLINT
 | 
					    {PROTOCOL_AIRWAY, []() { return new AIRWAYHeatpumpIR(); }},                              // NOLINT
 | 
				
			||||||
    {PROTOCOL_BGH_AUD, []() { return new BGHHeatpumpIR(); }},                                // NOLINT
 | 
					    {PROTOCOL_BGH_AUD, []() { return new BGHHeatpumpIR(); }},                                // NOLINT
 | 
				
			||||||
    {PROTOCOL_PANASONIC_ALTDKE, []() { return new PanasonicAltDKEHeatpumpIR(); }},           // NOLINT
 | 
					    {PROTOCOL_PANASONIC_ALTDKE, []() { return new PanasonicAltDKEHeatpumpIR(); }},           // NOLINT
 | 
				
			||||||
 | 
					    {PROTOCOL_PHILCO_PHS32, []() { return new PhilcoPHS32HeatpumpIR(); }},                   // NOLINT
 | 
				
			||||||
    {PROTOCOL_VAILLANTVAI8, []() { return new VaillantHeatpumpIR(); }},                      // NOLINT
 | 
					    {PROTOCOL_VAILLANTVAI8, []() { return new VaillantHeatpumpIR(); }},                      // NOLINT
 | 
				
			||||||
    {PROTOCOL_R51M, []() { return new R51MHeatpumpIR(); }},                                  // NOLINT
 | 
					    {PROTOCOL_R51M, []() { return new R51MHeatpumpIR(); }},                                  // NOLINT
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -65,6 +65,7 @@ enum Protocol {
 | 
				
			|||||||
  PROTOCOL_AIRWAY,
 | 
					  PROTOCOL_AIRWAY,
 | 
				
			||||||
  PROTOCOL_BGH_AUD,
 | 
					  PROTOCOL_BGH_AUD,
 | 
				
			||||||
  PROTOCOL_PANASONIC_ALTDKE,
 | 
					  PROTOCOL_PANASONIC_ALTDKE,
 | 
				
			||||||
 | 
					  PROTOCOL_PHILCO_PHS32,
 | 
				
			||||||
  PROTOCOL_VAILLANTVAI8,
 | 
					  PROTOCOL_VAILLANTVAI8,
 | 
				
			||||||
  PROTOCOL_R51M,
 | 
					  PROTOCOL_R51M,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -133,7 +133,6 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(std::string url, std:
 | 
				
			|||||||
      std::string header_value = container->client_.header(i).c_str();
 | 
					      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());
 | 
					      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);
 | 
					      container->response_headers_[header_name].push_back(header_value);
 | 
				
			||||||
      break;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -42,7 +42,6 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
 | 
				
			|||||||
        const std::string header_value = evt->header_value;
 | 
					        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());
 | 
					        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);
 | 
					        user_data->response_headers[header_name].push_back(header_value);
 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      break;
 | 
					      break;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,7 +39,7 @@ void MMC5603Component::setup() {
 | 
				
			|||||||
    return;
 | 
					    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");
 | 
					    ESP_LOGCONFIG(TAG, "Chip Wrong");
 | 
				
			||||||
    this->error_code_ = ID_REGISTERS;
 | 
					    this->error_code_ = ID_REGISTERS;
 | 
				
			||||||
    this->mark_failed();
 | 
					    this->mark_failed();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -164,7 +164,7 @@ void Nextion::dump_config() {
 | 
				
			|||||||
#endif  // USE_NEXTION_MAX_COMMANDS_PER_LOOP
 | 
					#endif  // USE_NEXTION_MAX_COMMANDS_PER_LOOP
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (this->touch_sleep_timeout_ != 0) {
 | 
					  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) {
 | 
					  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
 | 
					    // 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_);
 | 
					      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_);
 | 
					      this->set_wake_up_page(this->wake_up_page_);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -418,12 +418,12 @@ void Nextion::process_nextion_commands_() {
 | 
				
			|||||||
      ESP_LOGN(TAG, "Add 0xFF");
 | 
					      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_length -= 1;
 | 
				
			||||||
    to_process = this->command_data_.substr(1, to_process_length);
 | 
					    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
 | 
					      case 0x00:  // instruction sent by user has failed
 | 
				
			||||||
        ESP_LOGW(TAG, "Invalid instruction");
 | 
					        ESP_LOGW(TAG, "Invalid instruction");
 | 
				
			||||||
        this->remove_from_q_();
 | 
					        this->remove_from_q_();
 | 
				
			||||||
@@ -562,9 +562,9 @@ void Nextion::process_nextion_commands_() {
 | 
				
			|||||||
          break;
 | 
					          break;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1];
 | 
					        const uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1];
 | 
				
			||||||
        uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3];
 | 
					        const uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3];
 | 
				
			||||||
        uint8_t touch_event = to_process[4];  // 0 -> release, 1 -> press
 | 
					        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);
 | 
					        ESP_LOGD(TAG, "Touch %s at %u,%u", touch_event ? "PRESS" : "RELEASE", x, y);
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -820,15 +820,14 @@ void Nextion::process_nextion_commands_() {
 | 
				
			|||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
        ESP_LOGW(TAG, "Unknown event: 0x%02X", this->nextion_event_);
 | 
					        ESP_LOGW(TAG, "Unknown event: 0x%02X", nextion_event);
 | 
				
			||||||
        break;
 | 
					        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);
 | 
					    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) {
 | 
					  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++) {
 | 
					    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 Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag) {
 | 
				
			||||||
  uint16_t ret = 0;
 | 
					 | 
				
			||||||
  uint8_t c = 0;
 | 
					  uint8_t c = 0;
 | 
				
			||||||
  uint8_t nr_of_ff_bytes = 0;
 | 
					  uint8_t nr_of_ff_bytes = 0;
 | 
				
			||||||
  bool exit_flag = false;
 | 
					  bool exit_flag = false;
 | 
				
			||||||
@@ -1003,8 +1001,7 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool
 | 
				
			|||||||
  if (ff_flag)
 | 
					  if (ff_flag)
 | 
				
			||||||
    response = response.substr(0, response.length() - 3);  // Remove last 3 0xFF
 | 
					    response = response.substr(0, response.length() - 3);  // Remove last 3 0xFF
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ret = response.length();
 | 
					  return response.length();
 | 
				
			||||||
  return ret;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
					   * After 30 seconds the display will go to sleep. Note: the display will only wakeup by a restart or by setting up
 | 
				
			||||||
   * `thup`.
 | 
					   * `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.
 | 
					   * 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.
 | 
					   * wakes up to current page.
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * Example:
 | 
					   * Example:
 | 
				
			||||||
@@ -1204,11 +1204,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
 | 
				
			|||||||
   *
 | 
					   *
 | 
				
			||||||
   * The display will wake up to page 2.
 | 
					   * 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.
 | 
					   * 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.
 | 
					   * wakes up to current page.
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * Example:
 | 
					   * 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.
 | 
					   * 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.
 | 
					   * Sets if Nextion should auto-wake from sleep when touch press occurs.
 | 
				
			||||||
@@ -1330,7 +1330,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
 | 
				
			|||||||
  std::deque<NextionQueue *> waveform_queue_;
 | 
					  std::deque<NextionQueue *> waveform_queue_;
 | 
				
			||||||
  uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag);
 | 
					  uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag);
 | 
				
			||||||
  void all_components_send_state_(bool force_update = false);
 | 
					  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);
 | 
					  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 ignore_is_setup_ = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool nextion_reports_is_setup_ = false;
 | 
					  bool nextion_reports_is_setup_ = false;
 | 
				
			||||||
  uint8_t nextion_event_;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void process_nextion_commands_();
 | 
					  void process_nextion_commands_();
 | 
				
			||||||
  void process_serial_();
 | 
					  void process_serial_();
 | 
				
			||||||
  bool is_updating_ = false;
 | 
					  bool is_updating_ = false;
 | 
				
			||||||
  uint32_t touch_sleep_timeout_ = 0;
 | 
					  uint16_t touch_sleep_timeout_ = 0;
 | 
				
			||||||
  int16_t wake_up_page_ = -1;
 | 
					  int16_t wake_up_page_ = -1;
 | 
				
			||||||
  int16_t start_up_page_ = -1;
 | 
					  int16_t start_up_page_ = -1;
 | 
				
			||||||
  bool auto_wake_on_touch_ = true;
 | 
					  bool auto_wake_on_touch_ = true;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,12 +10,12 @@ static const char *const TAG = "nextion";
 | 
				
			|||||||
// Sleep safe commands
 | 
					// Sleep safe commands
 | 
				
			||||||
void Nextion::soft_reset() { this->send_command_("rest"); }
 | 
					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->wake_up_page_ = wake_up_page;
 | 
				
			||||||
  this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true);
 | 
					  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) {
 | 
					  if (touch_sleep_timeout < 3) {
 | 
				
			||||||
    ESP_LOGD(TAG, "Sleep timeout out of bounds (3-65535)");
 | 
					    ESP_LOGD(TAG, "Sleep timeout out of bounds (3-65535)");
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,7 @@ from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID
 | 
				
			|||||||
import esphome.final_validate as fv
 | 
					import esphome.final_validate as fv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .const import (
 | 
					from .const import (
 | 
				
			||||||
 | 
					    CONF_DEVICE_TYPE,
 | 
				
			||||||
    CONF_EXT_PAN_ID,
 | 
					    CONF_EXT_PAN_ID,
 | 
				
			||||||
    CONF_FORCE_DATASET,
 | 
					    CONF_FORCE_DATASET,
 | 
				
			||||||
    CONF_MDNS_ID,
 | 
					    CONF_MDNS_ID,
 | 
				
			||||||
@@ -32,6 +33,11 @@ AUTO_LOAD = ["network"]
 | 
				
			|||||||
CONFLICTS_WITH = ["wifi"]
 | 
					CONFLICTS_WITH = ["wifi"]
 | 
				
			||||||
DEPENDENCIES = ["esp32"]
 | 
					DEPENDENCIES = ["esp32"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CONF_DEVICE_TYPES = [
 | 
				
			||||||
 | 
					    "FTD",
 | 
				
			||||||
 | 
					    "MTD",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def set_sdkconfig_options(config):
 | 
					def set_sdkconfig_options(config):
 | 
				
			||||||
    # and expose options for using SPI/UART RCPs
 | 
					    # 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)
 | 
					    add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT_MAX_SERVICES", 5)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # TODO: Add suport for sleepy end devices
 | 
					    # 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")
 | 
					openthread_ns = cg.esphome_ns.namespace("openthread")
 | 
				
			||||||
@@ -107,6 +113,9 @@ CONFIG_SCHEMA = cv.All(
 | 
				
			|||||||
            cv.GenerateID(): cv.declare_id(OpenThreadComponent),
 | 
					            cv.GenerateID(): cv.declare_id(OpenThreadComponent),
 | 
				
			||||||
            cv.GenerateID(CONF_SRP_ID): cv.declare_id(OpenThreadSrpComponent),
 | 
					            cv.GenerateID(CONF_SRP_ID): cv.declare_id(OpenThreadSrpComponent),
 | 
				
			||||||
            cv.GenerateID(CONF_MDNS_ID): cv.use_id(MDNSComponent),
 | 
					            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_FORCE_DATASET): cv.boolean,
 | 
				
			||||||
            cv.Optional(CONF_TLV): cv.string_strict,
 | 
					            cv.Optional(CONF_TLV): cv.string_strict,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					CONF_DEVICE_TYPE = "device_type"
 | 
				
			||||||
CONF_EXT_PAN_ID = "ext_pan_id"
 | 
					CONF_EXT_PAN_ID = "ext_pan_id"
 | 
				
			||||||
CONF_FORCE_DATASET = "force_dataset"
 | 
					CONF_FORCE_DATASET = "force_dataset"
 | 
				
			||||||
CONF_MDNS_ID = "mdns_id"
 | 
					CONF_MDNS_ID = "mdns_id"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
					 | 
				
			||||||
@@ -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
 | 
					 | 
				
			||||||
@@ -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<OTABackend> make_ota_backend();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class OTAComponent : public Component {
 | 
					 | 
				
			||||||
#ifdef USE_OTA_STATE_CALLBACK
 | 
					 | 
				
			||||||
 public:
 | 
					 | 
				
			||||||
  void add_on_state_callback(std::function<void(OTAState, float, uint8_t)> &&callback) {
 | 
					 | 
				
			||||||
    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<void(OTAState, float, uint8_t)> {
 | 
					 | 
				
			||||||
   public:
 | 
					 | 
				
			||||||
    StateCallbackManager(OTAComponent *component) : component_(component) {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /** Call callbacks with deferral to main loop (for thread safety).
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * This should be used by OTA implementations that run in separate tasks
 | 
					 | 
				
			||||||
     * (like web_server OTA) to ensure callbacks execute in the main loop.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    void call_deferred(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<void(OTAState, float, uint8_t, OTAComponent *)> &&callback) {
 | 
					 | 
				
			||||||
    this->state_callback_.add(std::move(callback));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 protected:
 | 
					 | 
				
			||||||
  CallbackManager<void(OTAState, float, uint8_t, OTAComponent *)> 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
 | 
					 | 
				
			||||||
@@ -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 <Update.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace esphome {
 | 
					 | 
				
			||||||
namespace ota_base {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static const char *const TAG = "ota.arduino_esp32";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
std::unique_ptr<OTABackend> make_ota_backend() { return make_unique<ArduinoESP32OTABackend>(); }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
OTAResponseTypes ArduinoESP32OTABackend::begin(size_t image_size) {
 | 
					 | 
				
			||||||
  // Handle UPDATE_SIZE_UNKNOWN (0) which is used by web server OTA
 | 
					 | 
				
			||||||
  // where the exact firmware size is unknown due to multipart encoding
 | 
					 | 
				
			||||||
  if (image_size == 0) {
 | 
					 | 
				
			||||||
    image_size = UPDATE_SIZE_UNKNOWN;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  bool ret = Update.begin(image_size, U_FLASH);
 | 
					 | 
				
			||||||
  if (ret) {
 | 
					 | 
				
			||||||
    return OTA_RESPONSE_OK;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  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
 | 
					 | 
				
			||||||
@@ -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
 | 
					 | 
				
			||||||
@@ -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 <Updater.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace esphome {
 | 
					 | 
				
			||||||
namespace ota_base {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static const char *const TAG = "ota.arduino_esp8266";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
std::unique_ptr<OTABackend> make_ota_backend() { return make_unique<ArduinoESP8266OTABackend>(); }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
OTAResponseTypes ArduinoESP8266OTABackend::begin(size_t image_size) {
 | 
					 | 
				
			||||||
  // Handle UPDATE_SIZE_UNKNOWN (0) by calculating available space
 | 
					 | 
				
			||||||
  if (image_size == 0) {
 | 
					 | 
				
			||||||
    // NOLINTNEXTLINE(readability-static-accessed-through-instance)
 | 
					 | 
				
			||||||
    image_size = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  bool ret = Update.begin(image_size, U_FLASH);
 | 
					 | 
				
			||||||
  if (ret) {
 | 
					 | 
				
			||||||
    esp8266::preferences_prevent_write(true);
 | 
					 | 
				
			||||||
    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
 | 
					 | 
				
			||||||
@@ -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
 | 
					 | 
				
			||||||
@@ -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 <Update.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace esphome {
 | 
					 | 
				
			||||||
namespace ota_base {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static const char *const TAG = "ota.arduino_libretiny";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
std::unique_ptr<OTABackend> make_ota_backend() { return make_unique<ArduinoLibreTinyOTABackend>(); }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) {
 | 
					 | 
				
			||||||
  // Handle UPDATE_SIZE_UNKNOWN (0) which is used by web server OTA
 | 
					 | 
				
			||||||
  // where the exact firmware size is unknown due to multipart encoding
 | 
					 | 
				
			||||||
  if (image_size == 0) {
 | 
					 | 
				
			||||||
    image_size = UPDATE_SIZE_UNKNOWN;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  bool ret = Update.begin(image_size, U_FLASH);
 | 
					 | 
				
			||||||
  if (ret) {
 | 
					 | 
				
			||||||
    return OTA_RESPONSE_OK;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  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
 | 
					 | 
				
			||||||
@@ -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
 | 
					 | 
				
			||||||
@@ -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 <Updater.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace esphome {
 | 
					 | 
				
			||||||
namespace ota_base {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static const char *const TAG = "ota.arduino_rp2040";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
std::unique_ptr<OTABackend> make_ota_backend() { return make_unique<ArduinoRP2040OTABackend>(); }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) {
 | 
					 | 
				
			||||||
  // Handle UPDATE_SIZE_UNKNOWN (0) by calculating available space
 | 
					 | 
				
			||||||
  if (image_size == 0) {
 | 
					 | 
				
			||||||
    // Similar to ESP8266, calculate available space from flash layout
 | 
					 | 
				
			||||||
    extern uint8_t _FS_start;
 | 
					 | 
				
			||||||
    extern uint8_t _FS_end;
 | 
					 | 
				
			||||||
    // Calculate the size of the filesystem area which will be used for OTA
 | 
					 | 
				
			||||||
    size_t fs_size = &_FS_end - &_FS_start;
 | 
					 | 
				
			||||||
    // Reserve some space for filesystem overhead
 | 
					 | 
				
			||||||
    image_size = (fs_size - 0x1000) & 0xFFFFF000;
 | 
					 | 
				
			||||||
    ESP_LOGD(TAG, "OTA size unknown, using filesystem size: %u bytes", image_size);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  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
 | 
					 | 
				
			||||||
@@ -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
 | 
					 | 
				
			||||||
@@ -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 <esp_ota_ops.h>
 | 
					 | 
				
			||||||
#include <esp_task_wdt.h>
 | 
					 | 
				
			||||||
#include <spi_flash_mmap.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace esphome {
 | 
					 | 
				
			||||||
namespace ota_base {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
std::unique_ptr<OTABackend> make_ota_backend() { return make_unique<IDFOTABackend>(); }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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
 | 
					 | 
				
			||||||
@@ -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 <esp_ota_ops.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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
 | 
					 | 
				
			||||||
@@ -31,6 +31,10 @@ void PulseMeterSensor::setup() {
 | 
				
			|||||||
    this->pulse_state_.latched_ = this->last_pin_val_;
 | 
					    this->pulse_state_.latched_ = this->last_pin_val_;
 | 
				
			||||||
    this->pin_->attach_interrupt(PulseMeterSensor::pulse_intr, this, gpio::INTERRUPT_ANY_EDGE);
 | 
					    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() {
 | 
					void PulseMeterSensor::loop() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,21 +21,24 @@ ECC = {
 | 
				
			|||||||
    "HIGH": qrcodegen_Ecc.qrcodegen_Ecc_HIGH,
 | 
					    "HIGH": qrcodegen_Ecc.qrcodegen_Ecc_HIGH,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CONFIG_SCHEMA = cv.Schema(
 | 
					CONFIG_SCHEMA = cv.ensure_list(
 | 
				
			||||||
 | 
					    cv.Schema(
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            cv.Required(CONF_ID): cv.declare_id(QRCode),
 | 
					            cv.Required(CONF_ID): cv.declare_id(QRCode),
 | 
				
			||||||
            cv.Required(CONF_VALUE): cv.string,
 | 
					            cv.Required(CONF_VALUE): cv.string,
 | 
				
			||||||
            cv.Optional(CONF_ECC, default="LOW"): cv.enum(ECC, upper=True),
 | 
					            cv.Optional(CONF_ECC, default="LOW"): cv.enum(ECC, upper=True),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async def to_code(config):
 | 
					async def to_code(config):
 | 
				
			||||||
    cg.add_library("wjtje/qr-code-generator-library", "^1.7.0")
 | 
					    cg.add_library("wjtje/qr-code-generator-library", "^1.7.0")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
					    for entry in config:
 | 
				
			||||||
    cg.add(var.set_value(config[CONF_VALUE]))
 | 
					        var = cg.new_Pvariable(entry[CONF_ID])
 | 
				
			||||||
    cg.add(var.set_ecc(ECC[config[CONF_ECC]]))
 | 
					        cg.add(var.set_value(entry[CONF_VALUE]))
 | 
				
			||||||
    await cg.register_component(var, config)
 | 
					        cg.add(var.set_ecc(ECC[entry[CONF_ECC]]))
 | 
				
			||||||
 | 
					        await cg.register_component(var, entry)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    cg.add_define("USE_QR_CODE")
 | 
					    cg.add_define("USE_QR_CODE")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -371,6 +371,7 @@ void Rtttl::finish_() {
 | 
				
			|||||||
  ESP_LOGD(TAG, "Playback finished");
 | 
					  ESP_LOGD(TAG, "Playback finished");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
 | 
				
			||||||
static const LogString *state_to_string(State state) {
 | 
					static const LogString *state_to_string(State state) {
 | 
				
			||||||
  switch (state) {
 | 
					  switch (state) {
 | 
				
			||||||
    case STATE_STOPPED:
 | 
					    case STATE_STOPPED:
 | 
				
			||||||
@@ -387,6 +388,7 @@ static const LogString *state_to_string(State state) {
 | 
				
			|||||||
      return LOG_STR("UNKNOWN");
 | 
					      return LOG_STR("UNKNOWN");
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void Rtttl::set_state_(State state) {
 | 
					void Rtttl::set_state_(State state) {
 | 
				
			||||||
  State old_state = this->state_;
 | 
					  State old_state = this->state_;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -268,7 +268,19 @@ def validate_tz(value: str) -> str:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
TIME_SCHEMA = cv.Schema(
 | 
					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.Optional(CONF_ON_TIME): automation.validate_automation(
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CronTrigger),
 | 
					                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):
 | 
					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, []):
 | 
					    for conf in config.get(CONF_ON_TIME, []):
 | 
				
			||||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var)
 | 
					        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,8 +35,10 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
 | 
				
			|||||||
    ret = settimeofday(&timev, nullptr);
 | 
					    ret = settimeofday(&timev, nullptr);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ifdef USE_TIME_TIMEZONE
 | 
				
			||||||
  // Move timezone back to local timezone.
 | 
					  // Move timezone back to local timezone.
 | 
				
			||||||
  this->apply_timezone_();
 | 
					  this->apply_timezone_();
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (ret != 0) {
 | 
					  if (ret != 0) {
 | 
				
			||||||
    ESP_LOGW(TAG, "setimeofday() failed with code %d", ret);
 | 
					    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();
 | 
					  this->time_sync_callback_.call();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ifdef USE_TIME_TIMEZONE
 | 
				
			||||||
void RealTimeClock::apply_timezone_() {
 | 
					void RealTimeClock::apply_timezone_() {
 | 
				
			||||||
  setenv("TZ", this->timezone_.c_str(), 1);
 | 
					  setenv("TZ", this->timezone_.c_str(), 1);
 | 
				
			||||||
  tzset();
 | 
					  tzset();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}  // namespace time
 | 
					}  // namespace time
 | 
				
			||||||
}  // namespace esphome
 | 
					}  // namespace esphome
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,7 @@ class RealTimeClock : public PollingComponent {
 | 
				
			|||||||
 public:
 | 
					 public:
 | 
				
			||||||
  explicit RealTimeClock();
 | 
					  explicit RealTimeClock();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ifdef USE_TIME_TIMEZONE
 | 
				
			||||||
  /// Set the time zone.
 | 
					  /// Set the time zone.
 | 
				
			||||||
  void set_timezone(const std::string &tz) {
 | 
					  void set_timezone(const std::string &tz) {
 | 
				
			||||||
    this->timezone_ = tz;
 | 
					    this->timezone_ = tz;
 | 
				
			||||||
@@ -28,6 +29,7 @@ class RealTimeClock : public PollingComponent {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  /// Get the time zone currently in use.
 | 
					  /// Get the time zone currently in use.
 | 
				
			||||||
  std::string get_timezone() { return this->timezone_; }
 | 
					  std::string get_timezone() { return this->timezone_; }
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Get the time in the currently defined timezone.
 | 
					  /// Get the time in the currently defined timezone.
 | 
				
			||||||
  ESPTime now() { return ESPTime::from_epoch_local(this->timestamp_now()); }
 | 
					  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.
 | 
					  /// Get the current time as the UTC epoch since January 1st 1970.
 | 
				
			||||||
  time_t timestamp_now() { return ::time(nullptr); }
 | 
					  time_t timestamp_now() { return ::time(nullptr); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void add_on_time_sync_callback(std::function<void()> callback) {
 | 
					  void add_on_time_sync_callback(std::function<void()> &&callback) {
 | 
				
			||||||
    this->time_sync_callback_.add(std::move(callback));
 | 
					    this->time_sync_callback_.add(std::move(callback));
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -46,8 +48,10 @@ class RealTimeClock : public PollingComponent {
 | 
				
			|||||||
  /// Report a unix epoch as current time.
 | 
					  /// Report a unix epoch as current time.
 | 
				
			||||||
  void synchronize_epoch_(uint32_t epoch);
 | 
					  void synchronize_epoch_(uint32_t epoch);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ifdef USE_TIME_TIMEZONE
 | 
				
			||||||
  std::string timezone_{};
 | 
					  std::string timezone_{};
 | 
				
			||||||
  void apply_timezone_();
 | 
					  void apply_timezone_();
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  CallbackManager<void()> time_sync_callback_;
 | 
					  CallbackManager<void()> time_sync_callback_;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,8 +33,9 @@ from esphome.const import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
from esphome.core import CORE, coroutine_with_priority
 | 
					from esphome.core import CORE, coroutine_with_priority
 | 
				
			||||||
import esphome.final_validate as fv
 | 
					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_GROUP_ID = "sorting_group_id"
 | 
				
			||||||
CONF_SORTING_GROUPS = "sorting_groups"
 | 
					CONF_SORTING_GROUPS = "sorting_groups"
 | 
				
			||||||
@@ -47,7 +48,7 @@ WebServer = web_server_ns.class_("WebServer", cg.Component, cg.Controller)
 | 
				
			|||||||
sorting_groups = {}
 | 
					sorting_groups = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def default_url(config):
 | 
					def default_url(config: ConfigType) -> ConfigType:
 | 
				
			||||||
    config = config.copy()
 | 
					    config = config.copy()
 | 
				
			||||||
    if config[CONF_VERSION] == 1:
 | 
					    if config[CONF_VERSION] == 1:
 | 
				
			||||||
        if CONF_CSS_URL not in config:
 | 
					        if CONF_CSS_URL not in config:
 | 
				
			||||||
@@ -67,13 +68,27 @@ def default_url(config):
 | 
				
			|||||||
    return config
 | 
					    return config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def validate_local(config):
 | 
					def validate_local(config: ConfigType) -> ConfigType:
 | 
				
			||||||
    if CONF_LOCAL in config and config[CONF_VERSION] == 1:
 | 
					    if CONF_LOCAL in config and config[CONF_VERSION] == 1:
 | 
				
			||||||
        raise cv.Invalid("'local' is not supported in version 1")
 | 
					        raise cv.Invalid("'local' is not supported in version 1")
 | 
				
			||||||
    return config
 | 
					    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:
 | 
					    if CONF_SORTING_GROUPS in config and config[CONF_VERSION] != 3:
 | 
				
			||||||
        raise cv.Invalid(
 | 
					        raise cv.Invalid(
 | 
				
			||||||
            f"'{CONF_SORTING_GROUPS}' is only supported in 'web_server' version 3"
 | 
					            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(
 | 
					def _validate_no_sorting_component(
 | 
				
			||||||
    sorting_component: str,
 | 
					    sorting_component: str,
 | 
				
			||||||
    webserver_version: int,
 | 
					    webserver_version: int,
 | 
				
			||||||
    config: dict,
 | 
					    config: ConfigType,
 | 
				
			||||||
    path: list[str] | None = None,
 | 
					    path: list[str] | None = None,
 | 
				
			||||||
) -> None:
 | 
					) -> None:
 | 
				
			||||||
    if path is 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:
 | 
					    if (webserver_version := config.get(CONF_VERSION)) != 3:
 | 
				
			||||||
        _validate_no_sorting_component(
 | 
					        _validate_no_sorting_component(
 | 
				
			||||||
            CONF_SORTING_WEIGHT, webserver_version, fv.full_config.get()
 | 
					            CONF_SORTING_WEIGHT, webserver_version, fv.full_config.get()
 | 
				
			||||||
@@ -170,7 +185,7 @@ CONFIG_SCHEMA = cv.All(
 | 
				
			|||||||
                web_server_base.WebServerBase
 | 
					                web_server_base.WebServerBase
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean,
 | 
					            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_LOG, default=True): cv.boolean,
 | 
				
			||||||
            cv.Optional(CONF_LOCAL): cv.boolean,
 | 
					            cv.Optional(CONF_LOCAL): cv.boolean,
 | 
				
			||||||
            cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group),
 | 
					            cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group),
 | 
				
			||||||
@@ -188,6 +203,7 @@ CONFIG_SCHEMA = cv.All(
 | 
				
			|||||||
    default_url,
 | 
					    default_url,
 | 
				
			||||||
    validate_local,
 | 
					    validate_local,
 | 
				
			||||||
    validate_sorting_groups,
 | 
					    validate_sorting_groups,
 | 
				
			||||||
 | 
					    validate_ota_removed,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -271,11 +287,8 @@ async def to_code(config):
 | 
				
			|||||||
    else:
 | 
					    else:
 | 
				
			||||||
        cg.add(var.set_css_url(config[CONF_CSS_URL]))
 | 
					        cg.add(var.set_css_url(config[CONF_CSS_URL]))
 | 
				
			||||||
        cg.add(var.set_js_url(config[CONF_JS_URL]))
 | 
					        cg.add(var.set_js_url(config[CONF_JS_URL]))
 | 
				
			||||||
    cg.add(var.set_allow_ota(config[CONF_OTA]))
 | 
					    # OTA is now handled by the web_server OTA platform
 | 
				
			||||||
    if config[CONF_OTA]:
 | 
					    # The CONF_OTA option is kept only for backwards compatibility validation
 | 
				
			||||||
        # 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")
 | 
					 | 
				
			||||||
    cg.add(var.set_expose_log(config[CONF_LOG]))
 | 
					    cg.add(var.set_expose_log(config[CONF_LOG]))
 | 
				
			||||||
    if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]:
 | 
					    if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]:
 | 
				
			||||||
        cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS")
 | 
					        cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS")
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										32
									
								
								esphome/components/web_server/ota/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								esphome/components/web_server/ota/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					import esphome.codegen as cg
 | 
				
			||||||
 | 
					from esphome.components.esp32 import add_idf_component
 | 
				
			||||||
 | 
					from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
 | 
				
			||||||
 | 
					import esphome.config_validation as cv
 | 
				
			||||||
 | 
					from esphome.const import CONF_ID
 | 
				
			||||||
 | 
					from esphome.core import CORE, coroutine_with_priority
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CODEOWNERS = ["@esphome/core"]
 | 
				
			||||||
 | 
					DEPENDENCIES = ["network", "web_server_base"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					web_server_ns = cg.esphome_ns.namespace("web_server")
 | 
				
			||||||
 | 
					WebServerOTAComponent = web_server_ns.class_("WebServerOTAComponent", OTAComponent)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CONFIG_SCHEMA = (
 | 
				
			||||||
 | 
					    cv.Schema(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            cv.GenerateID(): cv.declare_id(WebServerOTAComponent),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .extend(BASE_OTA_SCHEMA)
 | 
				
			||||||
 | 
					    .extend(cv.COMPONENT_SCHEMA)
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@coroutine_with_priority(52.0)
 | 
				
			||||||
 | 
					async def to_code(config):
 | 
				
			||||||
 | 
					    var = cg.new_Pvariable(config[CONF_ID])
 | 
				
			||||||
 | 
					    await ota_to_code(var, config)
 | 
				
			||||||
 | 
					    await cg.register_component(var, config)
 | 
				
			||||||
 | 
					    cg.add_define("USE_WEBSERVER_OTA")
 | 
				
			||||||
 | 
					    if CORE.using_esp_idf:
 | 
				
			||||||
 | 
					        add_idf_component(name="zorxx/multipart-parser", ref="1.0.1")
 | 
				
			||||||
							
								
								
									
										210
									
								
								esphome/components/web_server/ota/ota_web_server.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								esphome/components/web_server/ota/ota_web_server.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,210 @@
 | 
				
			|||||||
 | 
					#include "ota_web_server.h"
 | 
				
			||||||
 | 
					#ifdef USE_WEBSERVER_OTA
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "esphome/components/ota/ota_backend.h"
 | 
				
			||||||
 | 
					#include "esphome/core/application.h"
 | 
				
			||||||
 | 
					#include "esphome/core/log.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ifdef USE_ARDUINO
 | 
				
			||||||
 | 
					#ifdef USE_ESP8266
 | 
				
			||||||
 | 
					#include <Updater.h>
 | 
				
			||||||
 | 
					#elif defined(USE_ESP32) || defined(USE_LIBRETINY)
 | 
				
			||||||
 | 
					#include <Update.h>
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					#endif  // USE_ARDUINO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace web_server {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const char *const TAG = "web_server.ota";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class OTARequestHandler : public AsyncWebHandler {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  OTARequestHandler(WebServerOTAComponent *parent) : parent_(parent) {}
 | 
				
			||||||
 | 
					  void handleRequest(AsyncWebServerRequest *request) override;
 | 
				
			||||||
 | 
					  void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
 | 
				
			||||||
 | 
					                    bool final) override;
 | 
				
			||||||
 | 
					  bool canHandle(AsyncWebServerRequest *request) const override {
 | 
				
			||||||
 | 
					    return request->url() == "/update" && request->method() == HTTP_POST;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // NOLINTNEXTLINE(readability-identifier-naming)
 | 
				
			||||||
 | 
					  bool isRequestHandlerTrivial() const override { return false; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 protected:
 | 
				
			||||||
 | 
					  void report_ota_progress_(AsyncWebServerRequest *request);
 | 
				
			||||||
 | 
					  void schedule_ota_reboot_();
 | 
				
			||||||
 | 
					  void ota_init_(const char *filename);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  uint32_t last_ota_progress_{0};
 | 
				
			||||||
 | 
					  uint32_t ota_read_length_{0};
 | 
				
			||||||
 | 
					  WebServerOTAComponent *parent_;
 | 
				
			||||||
 | 
					  bool ota_success_{false};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 private:
 | 
				
			||||||
 | 
					  std::unique_ptr<ota::OTABackend> ota_backend_{nullptr};
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
 | 
				
			||||||
 | 
					  const uint32_t now = millis();
 | 
				
			||||||
 | 
					  if (now - this->last_ota_progress_ > 1000) {
 | 
				
			||||||
 | 
					    float percentage = 0.0f;
 | 
				
			||||||
 | 
					    if (request->contentLength() != 0) {
 | 
				
			||||||
 | 
					      // Note: Using contentLength() for progress calculation is technically wrong as it includes
 | 
				
			||||||
 | 
					      // multipart headers/boundaries, but it's only off by a small amount and we don't have
 | 
				
			||||||
 | 
					      // access to the actual firmware size until the upload is complete. This is intentional
 | 
				
			||||||
 | 
					      // as it still gives the user a reasonable progress indication.
 | 
				
			||||||
 | 
					      percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
 | 
				
			||||||
 | 
					      ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					#ifdef USE_OTA_STATE_CALLBACK
 | 
				
			||||||
 | 
					    // Report progress - use call_deferred since we're in web server task
 | 
				
			||||||
 | 
					    this->parent_->state_callback_.call_deferred(ota::OTA_IN_PROGRESS, percentage, 0);
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					    this->last_ota_progress_ = now;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void OTARequestHandler::schedule_ota_reboot_() {
 | 
				
			||||||
 | 
					  ESP_LOGI(TAG, "OTA update successful!");
 | 
				
			||||||
 | 
					  this->parent_->set_timeout(100, []() {
 | 
				
			||||||
 | 
					    ESP_LOGI(TAG, "Performing OTA reboot now");
 | 
				
			||||||
 | 
					    App.safe_reboot();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void OTARequestHandler::ota_init_(const char *filename) {
 | 
				
			||||||
 | 
					  ESP_LOGI(TAG, "OTA Update Start: %s", filename);
 | 
				
			||||||
 | 
					  this->ota_read_length_ = 0;
 | 
				
			||||||
 | 
					  this->ota_success_ = false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index,
 | 
				
			||||||
 | 
					                                     uint8_t *data, size_t len, bool final) {
 | 
				
			||||||
 | 
					  ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_OK;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (index == 0 && !this->ota_backend_) {
 | 
				
			||||||
 | 
					    // Initialize OTA on first call
 | 
				
			||||||
 | 
					    this->ota_init_(filename.c_str());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ifdef USE_OTA_STATE_CALLBACK
 | 
				
			||||||
 | 
					    // Notify OTA started - use call_deferred since we're in web server task
 | 
				
			||||||
 | 
					    this->parent_->state_callback_.call_deferred(ota::OTA_STARTED, 0.0f, 0);
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Platform-specific pre-initialization
 | 
				
			||||||
 | 
					#ifdef USE_ARDUINO
 | 
				
			||||||
 | 
					#ifdef USE_ESP8266
 | 
				
			||||||
 | 
					    Update.runAsync(true);
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					#if defined(USE_ESP32) || defined(USE_LIBRETINY)
 | 
				
			||||||
 | 
					    if (Update.isRunning()) {
 | 
				
			||||||
 | 
					      Update.abort();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					#endif  // USE_ARDUINO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this->ota_backend_ = ota::make_ota_backend();
 | 
				
			||||||
 | 
					    if (!this->ota_backend_) {
 | 
				
			||||||
 | 
					      ESP_LOGE(TAG, "Failed to create OTA backend");
 | 
				
			||||||
 | 
					#ifdef USE_OTA_STATE_CALLBACK
 | 
				
			||||||
 | 
					      this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f,
 | 
				
			||||||
 | 
					                                                   static_cast<uint8_t>(ota::OTA_RESPONSE_ERROR_UNKNOWN));
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Web server OTA uses multipart uploads where the actual firmware size
 | 
				
			||||||
 | 
					    // is unknown (contentLength includes multipart overhead)
 | 
				
			||||||
 | 
					    // Pass 0 to indicate unknown size
 | 
				
			||||||
 | 
					    error_code = this->ota_backend_->begin(0);
 | 
				
			||||||
 | 
					    if (error_code != ota::OTA_RESPONSE_OK) {
 | 
				
			||||||
 | 
					      ESP_LOGE(TAG, "OTA begin failed: %d", error_code);
 | 
				
			||||||
 | 
					      this->ota_backend_.reset();
 | 
				
			||||||
 | 
					#ifdef USE_OTA_STATE_CALLBACK
 | 
				
			||||||
 | 
					      this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!this->ota_backend_) {
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Process data
 | 
				
			||||||
 | 
					  if (len > 0) {
 | 
				
			||||||
 | 
					    error_code = this->ota_backend_->write(data, len);
 | 
				
			||||||
 | 
					    if (error_code != ota::OTA_RESPONSE_OK) {
 | 
				
			||||||
 | 
					      ESP_LOGE(TAG, "OTA write failed: %d", error_code);
 | 
				
			||||||
 | 
					      this->ota_backend_->abort();
 | 
				
			||||||
 | 
					      this->ota_backend_.reset();
 | 
				
			||||||
 | 
					#ifdef USE_OTA_STATE_CALLBACK
 | 
				
			||||||
 | 
					      this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this->ota_read_length_ += len;
 | 
				
			||||||
 | 
					    this->report_ota_progress_(request);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Finalize
 | 
				
			||||||
 | 
					  if (final) {
 | 
				
			||||||
 | 
					    ESP_LOGD(TAG, "OTA final chunk: index=%u, len=%u, total_read=%u, contentLength=%u", index, len,
 | 
				
			||||||
 | 
					             this->ota_read_length_, request->contentLength());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // For Arduino framework, the Update library tracks expected size from firmware header
 | 
				
			||||||
 | 
					    // If we haven't received enough data, calling end() will fail
 | 
				
			||||||
 | 
					    // This can happen if the upload is interrupted or the client disconnects
 | 
				
			||||||
 | 
					    error_code = this->ota_backend_->end();
 | 
				
			||||||
 | 
					    if (error_code == ota::OTA_RESPONSE_OK) {
 | 
				
			||||||
 | 
					      this->ota_success_ = true;
 | 
				
			||||||
 | 
					#ifdef USE_OTA_STATE_CALLBACK
 | 
				
			||||||
 | 
					      // Report completion before reboot - use call_deferred since we're in web server task
 | 
				
			||||||
 | 
					      this->parent_->state_callback_.call_deferred(ota::OTA_COMPLETED, 100.0f, 0);
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					      this->schedule_ota_reboot_();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      ESP_LOGE(TAG, "OTA end failed: %d", error_code);
 | 
				
			||||||
 | 
					#ifdef USE_OTA_STATE_CALLBACK
 | 
				
			||||||
 | 
					      this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this->ota_backend_.reset();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) {
 | 
				
			||||||
 | 
					  AsyncWebServerResponse *response;
 | 
				
			||||||
 | 
					  // Use the ota_success_ flag to determine the actual result
 | 
				
			||||||
 | 
					  const char *msg = this->ota_success_ ? "Update Successful!" : "Update Failed!";
 | 
				
			||||||
 | 
					  response = request->beginResponse(200, "text/plain", msg);
 | 
				
			||||||
 | 
					  response->addHeader("Connection", "close");
 | 
				
			||||||
 | 
					  request->send(response);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void WebServerOTAComponent::setup() {
 | 
				
			||||||
 | 
					  // Get the global web server base instance and register our handler
 | 
				
			||||||
 | 
					  auto *base = web_server_base::global_web_server_base;
 | 
				
			||||||
 | 
					  if (base == nullptr) {
 | 
				
			||||||
 | 
					    ESP_LOGE(TAG, "WebServerBase not found");
 | 
				
			||||||
 | 
					    this->mark_failed();
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // AsyncWebServer takes ownership of the handler and will delete it when the server is destroyed
 | 
				
			||||||
 | 
					  base->add_handler(new OTARequestHandler(this));  // NOLINT
 | 
				
			||||||
 | 
					#ifdef USE_OTA_STATE_CALLBACK
 | 
				
			||||||
 | 
					  // Register with global OTA callback system
 | 
				
			||||||
 | 
					  ota::register_ota_platform(this);
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void WebServerOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, "Web Server OTA"); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace web_server
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#endif  // USE_WEBSERVER_OTA
 | 
				
			||||||
							
								
								
									
										26
									
								
								esphome/components/web_server/ota/ota_web_server.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								esphome/components/web_server/ota/ota_web_server.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "esphome/core/defines.h"
 | 
				
			||||||
 | 
					#ifdef USE_WEBSERVER_OTA
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "esphome/components/ota/ota_backend.h"
 | 
				
			||||||
 | 
					#include "esphome/components/web_server_base/web_server_base.h"
 | 
				
			||||||
 | 
					#include "esphome/core/component.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace web_server {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class WebServerOTAComponent : public ota::OTAComponent {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  void setup() override;
 | 
				
			||||||
 | 
					  void dump_config() override;
 | 
				
			||||||
 | 
					  float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 protected:
 | 
				
			||||||
 | 
					  friend class OTARequestHandler;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace web_server
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#endif  // USE_WEBSERVER_OTA
 | 
				
			||||||
@@ -273,7 +273,11 @@ std::string WebServer::get_config_json() {
 | 
				
			|||||||
  return json::build_json([this](JsonObject root) {
 | 
					  return json::build_json([this](JsonObject root) {
 | 
				
			||||||
    root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name();
 | 
					    root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name();
 | 
				
			||||||
    root["comment"] = App.get_comment();
 | 
					    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["log"] = this->expose_log_;
 | 
				
			||||||
    root["lang"] = "en";
 | 
					    root["lang"] = "en";
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
@@ -299,10 +303,7 @@ void WebServer::setup() {
 | 
				
			|||||||
#endif
 | 
					#endif
 | 
				
			||||||
  this->base_->add_handler(this);
 | 
					  this->base_->add_handler(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#ifdef USE_WEBSERVER_OTA
 | 
					  // OTA is now handled by the web_server OTA platform
 | 
				
			||||||
  if (this->allow_ota_)
 | 
					 | 
				
			||||||
    this->base_->add_ota_handler();
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly
 | 
					  // 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
 | 
					  // getting a lot of events
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -212,11 +212,6 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
 | 
				
			|||||||
   * @param include_internal Whether internal components should be displayed.
 | 
					   * @param include_internal Whether internal components should be displayed.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  void set_include_internal(bool include_internal) { include_internal_ = include_internal; }
 | 
					  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.
 | 
					  /** Set whether or not the webserver should expose the Log.
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param expose_log.
 | 
					   * @param expose_log.
 | 
				
			||||||
@@ -525,7 +520,6 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
 | 
				
			|||||||
#ifdef USE_WEBSERVER_JS_INCLUDE
 | 
					#ifdef USE_WEBSERVER_JS_INCLUDE
 | 
				
			||||||
  const char *js_include_{nullptr};
 | 
					  const char *js_include_{nullptr};
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
  bool allow_ota_{true};
 | 
					 | 
				
			||||||
  bool expose_log_{true};
 | 
					  bool expose_log_{true};
 | 
				
			||||||
#ifdef USE_ESP32
 | 
					#ifdef USE_ESP32
 | 
				
			||||||
  std::deque<std::function<void()>> to_schedule_;
 | 
					  std::deque<std::function<void()>> to_schedule_;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -192,11 +192,10 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for "
 | 
					  stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for "
 | 
				
			||||||
                  "REST API documentation.</p>"));
 | 
					                  "REST API documentation.</p>"));
 | 
				
			||||||
  if (this->allow_ota_) {
 | 
					#ifdef USE_WEBSERVER_OTA
 | 
				
			||||||
    stream->print(
 | 
					  stream->print(F("<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
 | 
				
			||||||
        F("<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
 | 
					 | 
				
			||||||
                  "type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
 | 
					                  "type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
 | 
				
			||||||
  }
 | 
					#endif
 | 
				
			||||||
  stream->print(F("<h2>Debug Log</h2><pre id=\"log\"></pre>"));
 | 
					  stream->print(F("<h2>Debug Log</h2><pre id=\"log\"></pre>"));
 | 
				
			||||||
#ifdef USE_WEBSERVER_JS_INCLUDE
 | 
					#ifdef USE_WEBSERVER_JS_INCLUDE
 | 
				
			||||||
  if (this->js_include_ != nullptr) {
 | 
					  if (this->js_include_ != nullptr) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,6 +30,7 @@ CONFIG_SCHEMA = cv.Schema(
 | 
				
			|||||||
async def to_code(config):
 | 
					async def to_code(config):
 | 
				
			||||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
					    var = cg.new_Pvariable(config[CONF_ID])
 | 
				
			||||||
    await cg.register_component(var, config)
 | 
					    await cg.register_component(var, config)
 | 
				
			||||||
 | 
					    cg.add(cg.RawExpression(f"{web_server_base_ns}::global_web_server_base = {var}"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if CORE.using_arduino:
 | 
					    if CORE.using_arduino:
 | 
				
			||||||
        if CORE.is_esp32:
 | 
					        if CORE.is_esp32:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,23 +4,13 @@
 | 
				
			|||||||
#include "esphome/core/helpers.h"
 | 
					#include "esphome/core/helpers.h"
 | 
				
			||||||
#include "esphome/core/log.h"
 | 
					#include "esphome/core/log.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#ifdef USE_WEBSERVER_OTA
 | 
					 | 
				
			||||||
#include "esphome/components/ota_base/ota_backend.h"
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#ifdef USE_ARDUINO
 | 
					 | 
				
			||||||
#ifdef USE_ESP8266
 | 
					 | 
				
			||||||
#include <Updater.h>
 | 
					 | 
				
			||||||
#elif defined(USE_ESP32) || defined(USE_LIBRETINY)
 | 
					 | 
				
			||||||
#include <Update.h>
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace esphome {
 | 
					namespace esphome {
 | 
				
			||||||
namespace web_server_base {
 | 
					namespace web_server_base {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static const char *const TAG = "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) {
 | 
					void WebServerBase::add_handler(AsyncWebHandler *handler) {
 | 
				
			||||||
  // remove all handlers
 | 
					  // remove all handlers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -33,156 +23,6 @@ void WebServerBase::add_handler(AsyncWebHandler *handler) {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#ifdef USE_WEBSERVER_OTA
 | 
					 | 
				
			||||||
void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
 | 
					 | 
				
			||||||
  const uint32_t now = millis();
 | 
					 | 
				
			||||||
  if (now - this->last_ota_progress_ > 1000) {
 | 
					 | 
				
			||||||
    float percentage = 0.0f;
 | 
					 | 
				
			||||||
    if (request->contentLength() != 0) {
 | 
					 | 
				
			||||||
      // Note: Using contentLength() for progress calculation is technically wrong as it includes
 | 
					 | 
				
			||||||
      // multipart headers/boundaries, but it's only off by a small amount and we don't have
 | 
					 | 
				
			||||||
      // access to the actual firmware size until the upload is complete. This is intentional
 | 
					 | 
				
			||||||
      // as it still gives the user a reasonable progress indication.
 | 
					 | 
				
			||||||
      percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
 | 
					 | 
				
			||||||
      ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
#ifdef USE_OTA_STATE_CALLBACK
 | 
					 | 
				
			||||||
    // Report progress - use call_deferred since we're in web server task
 | 
					 | 
				
			||||||
    this->parent_->state_callback_.call_deferred(ota_base::OTA_IN_PROGRESS, percentage, 0);
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
    this->last_ota_progress_ = now;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void OTARequestHandler::schedule_ota_reboot_() {
 | 
					 | 
				
			||||||
  ESP_LOGI(TAG, "OTA update successful!");
 | 
					 | 
				
			||||||
  this->parent_->set_timeout(100, []() {
 | 
					 | 
				
			||||||
    ESP_LOGI(TAG, "Performing OTA reboot now");
 | 
					 | 
				
			||||||
    App.safe_reboot();
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void OTARequestHandler::ota_init_(const char *filename) {
 | 
					 | 
				
			||||||
  ESP_LOGI(TAG, "OTA Update Start: %s", filename);
 | 
					 | 
				
			||||||
  this->ota_read_length_ = 0;
 | 
					 | 
				
			||||||
  this->ota_success_ = false;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index,
 | 
					 | 
				
			||||||
                                     uint8_t *data, size_t len, bool final) {
 | 
					 | 
				
			||||||
  ota_base::OTAResponseTypes error_code = ota_base::OTA_RESPONSE_OK;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (index == 0 && !this->ota_backend_) {
 | 
					 | 
				
			||||||
    // Initialize OTA on first call
 | 
					 | 
				
			||||||
    this->ota_init_(filename.c_str());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#ifdef USE_OTA_STATE_CALLBACK
 | 
					 | 
				
			||||||
    // Notify OTA started - use call_deferred since we're in web server task
 | 
					 | 
				
			||||||
    this->parent_->state_callback_.call_deferred(ota_base::OTA_STARTED, 0.0f, 0);
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Platform-specific pre-initialization
 | 
					 | 
				
			||||||
#ifdef USE_ARDUINO
 | 
					 | 
				
			||||||
#ifdef USE_ESP8266
 | 
					 | 
				
			||||||
    Update.runAsync(true);
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_LIBRETINY)
 | 
					 | 
				
			||||||
    if (Update.isRunning()) {
 | 
					 | 
				
			||||||
      Update.abort();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
#endif  // USE_ARDUINO
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this->ota_backend_ = ota_base::make_ota_backend();
 | 
					 | 
				
			||||||
    if (!this->ota_backend_) {
 | 
					 | 
				
			||||||
      ESP_LOGE(TAG, "Failed to create OTA backend");
 | 
					 | 
				
			||||||
#ifdef USE_OTA_STATE_CALLBACK
 | 
					 | 
				
			||||||
      this->parent_->state_callback_.call_deferred(ota_base::OTA_ERROR, 0.0f,
 | 
					 | 
				
			||||||
                                                   static_cast<uint8_t>(ota_base::OTA_RESPONSE_ERROR_UNKNOWN));
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Web server OTA uses multipart uploads where the actual firmware size
 | 
					 | 
				
			||||||
    // is unknown (contentLength includes multipart overhead)
 | 
					 | 
				
			||||||
    // Pass 0 to indicate unknown size
 | 
					 | 
				
			||||||
    error_code = this->ota_backend_->begin(0);
 | 
					 | 
				
			||||||
    if (error_code != ota_base::OTA_RESPONSE_OK) {
 | 
					 | 
				
			||||||
      ESP_LOGE(TAG, "OTA begin failed: %d", error_code);
 | 
					 | 
				
			||||||
      this->ota_backend_.reset();
 | 
					 | 
				
			||||||
#ifdef USE_OTA_STATE_CALLBACK
 | 
					 | 
				
			||||||
      this->parent_->state_callback_.call_deferred(ota_base::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!this->ota_backend_) {
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Process data
 | 
					 | 
				
			||||||
  if (len > 0) {
 | 
					 | 
				
			||||||
    error_code = this->ota_backend_->write(data, len);
 | 
					 | 
				
			||||||
    if (error_code != ota_base::OTA_RESPONSE_OK) {
 | 
					 | 
				
			||||||
      ESP_LOGE(TAG, "OTA write failed: %d", error_code);
 | 
					 | 
				
			||||||
      this->ota_backend_->abort();
 | 
					 | 
				
			||||||
      this->ota_backend_.reset();
 | 
					 | 
				
			||||||
#ifdef USE_OTA_STATE_CALLBACK
 | 
					 | 
				
			||||||
      this->parent_->state_callback_.call_deferred(ota_base::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    this->ota_read_length_ += len;
 | 
					 | 
				
			||||||
    this->report_ota_progress_(request);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Finalize
 | 
					 | 
				
			||||||
  if (final) {
 | 
					 | 
				
			||||||
    ESP_LOGD(TAG, "OTA final chunk: index=%u, len=%u, total_read=%u, contentLength=%u", index, len,
 | 
					 | 
				
			||||||
             this->ota_read_length_, request->contentLength());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // For Arduino framework, the Update library tracks expected size from firmware header
 | 
					 | 
				
			||||||
    // If we haven't received enough data, calling end() will fail
 | 
					 | 
				
			||||||
    // This can happen if the upload is interrupted or the client disconnects
 | 
					 | 
				
			||||||
    error_code = this->ota_backend_->end();
 | 
					 | 
				
			||||||
    if (error_code == ota_base::OTA_RESPONSE_OK) {
 | 
					 | 
				
			||||||
      this->ota_success_ = true;
 | 
					 | 
				
			||||||
#ifdef USE_OTA_STATE_CALLBACK
 | 
					 | 
				
			||||||
      // Report completion before reboot - use call_deferred since we're in web server task
 | 
					 | 
				
			||||||
      this->parent_->state_callback_.call_deferred(ota_base::OTA_COMPLETED, 100.0f, 0);
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
      this->schedule_ota_reboot_();
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      ESP_LOGE(TAG, "OTA end failed: %d", error_code);
 | 
					 | 
				
			||||||
#ifdef USE_OTA_STATE_CALLBACK
 | 
					 | 
				
			||||||
      this->parent_->state_callback_.call_deferred(ota_base::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    this->ota_backend_.reset();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) {
 | 
					 | 
				
			||||||
  AsyncWebServerResponse *response;
 | 
					 | 
				
			||||||
  // Use the ota_success_ flag to determine the actual result
 | 
					 | 
				
			||||||
  const char *msg = this->ota_success_ ? "Update Successful!" : "Update Failed!";
 | 
					 | 
				
			||||||
  response = request->beginResponse(200, "text/plain", msg);
 | 
					 | 
				
			||||||
  response->addHeader("Connection", "close");
 | 
					 | 
				
			||||||
  request->send(response);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void WebServerBase::add_ota_handler() {
 | 
					 | 
				
			||||||
  this->add_handler(new OTARequestHandler(this));  // NOLINT
 | 
					 | 
				
			||||||
#ifdef USE_OTA_STATE_CALLBACK
 | 
					 | 
				
			||||||
  // Register with global OTA callback system
 | 
					 | 
				
			||||||
  ota_base::register_ota_platform(this);
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
float WebServerBase::get_setup_priority() const {
 | 
					float WebServerBase::get_setup_priority() const {
 | 
				
			||||||
  // Before WiFi (captive portal)
 | 
					  // Before WiFi (captive portal)
 | 
				
			||||||
  return setup_priority::WIFI + 2.0f;
 | 
					  return setup_priority::WIFI + 2.0f;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,13 +14,12 @@
 | 
				
			|||||||
#include "esphome/components/web_server_idf/web_server_idf.h"
 | 
					#include "esphome/components/web_server_idf/web_server_idf.h"
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#ifdef USE_WEBSERVER_OTA
 | 
					 | 
				
			||||||
#include "esphome/components/ota_base/ota_backend.h"
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace esphome {
 | 
					namespace esphome {
 | 
				
			||||||
namespace web_server_base {
 | 
					namespace web_server_base {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class WebServerBase;
 | 
				
			||||||
 | 
					extern WebServerBase *global_web_server_base;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace internal {
 | 
					namespace internal {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MiddlewareHandler : public AsyncWebHandler {
 | 
					class MiddlewareHandler : public AsyncWebHandler {
 | 
				
			||||||
@@ -83,11 +82,7 @@ class AuthMiddlewareHandler : public MiddlewareHandler {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
}  // namespace internal
 | 
					}  // namespace internal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#ifdef USE_WEBSERVER_OTA
 | 
					 | 
				
			||||||
class WebServerBase : public ota_base::OTAComponent {
 | 
					 | 
				
			||||||
#else
 | 
					 | 
				
			||||||
class WebServerBase : public Component {
 | 
					class WebServerBase : public Component {
 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
 public:
 | 
					 public:
 | 
				
			||||||
  void init() {
 | 
					  void init() {
 | 
				
			||||||
    if (this->initialized_) {
 | 
					    if (this->initialized_) {
 | 
				
			||||||
@@ -118,18 +113,10 @@ class WebServerBase : public Component {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  void add_handler(AsyncWebHandler *handler);
 | 
					  void add_handler(AsyncWebHandler *handler);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#ifdef USE_WEBSERVER_OTA
 | 
					 | 
				
			||||||
  void add_ota_handler();
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void set_port(uint16_t port) { port_ = port; }
 | 
					  void set_port(uint16_t port) { port_ = port; }
 | 
				
			||||||
  uint16_t get_port() const { return port_; }
 | 
					  uint16_t get_port() const { return port_; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 protected:
 | 
					 protected:
 | 
				
			||||||
#ifdef USE_WEBSERVER_OTA
 | 
					 | 
				
			||||||
  friend class OTARequestHandler;
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  int initialized_{0};
 | 
					  int initialized_{0};
 | 
				
			||||||
  uint16_t port_{80};
 | 
					  uint16_t port_{80};
 | 
				
			||||||
  std::shared_ptr<AsyncWebServer> server_{nullptr};
 | 
					  std::shared_ptr<AsyncWebServer> server_{nullptr};
 | 
				
			||||||
@@ -137,35 +124,6 @@ class WebServerBase : public Component {
 | 
				
			|||||||
  internal::Credentials credentials_;
 | 
					  internal::Credentials credentials_;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#ifdef USE_WEBSERVER_OTA
 | 
					 | 
				
			||||||
class OTARequestHandler : public AsyncWebHandler {
 | 
					 | 
				
			||||||
 public:
 | 
					 | 
				
			||||||
  OTARequestHandler(WebServerBase *parent) : parent_(parent) {}
 | 
					 | 
				
			||||||
  void handleRequest(AsyncWebServerRequest *request) override;
 | 
					 | 
				
			||||||
  void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
 | 
					 | 
				
			||||||
                    bool final) override;
 | 
					 | 
				
			||||||
  bool canHandle(AsyncWebServerRequest *request) const override {
 | 
					 | 
				
			||||||
    return request->url() == "/update" && request->method() == HTTP_POST;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // NOLINTNEXTLINE(readability-identifier-naming)
 | 
					 | 
				
			||||||
  bool isRequestHandlerTrivial() const override { return false; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 protected:
 | 
					 | 
				
			||||||
  void report_ota_progress_(AsyncWebServerRequest *request);
 | 
					 | 
				
			||||||
  void schedule_ota_reboot_();
 | 
					 | 
				
			||||||
  void ota_init_(const char *filename);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  uint32_t last_ota_progress_{0};
 | 
					 | 
				
			||||||
  uint32_t ota_read_length_{0};
 | 
					 | 
				
			||||||
  WebServerBase *parent_;
 | 
					 | 
				
			||||||
  bool ota_success_{false};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 private:
 | 
					 | 
				
			||||||
  std::unique_ptr<ota_base::OTABackend> ota_backend_{nullptr};
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
#endif  // USE_WEBSERVER_OTA
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}  // namespace web_server_base
 | 
					}  // namespace web_server_base
 | 
				
			||||||
}  // namespace esphome
 | 
					}  // namespace esphome
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,5 @@
 | 
				
			|||||||
from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
 | 
					from esphome.components.esp32 import add_idf_sdkconfig_option
 | 
				
			||||||
import esphome.config_validation as cv
 | 
					import esphome.config_validation as cv
 | 
				
			||||||
from esphome.const import CONF_OTA, CONF_WEB_SERVER
 | 
					 | 
				
			||||||
from esphome.core import CORE
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
CODEOWNERS = ["@dentra"]
 | 
					CODEOWNERS = ["@dentra"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -14,7 +12,3 @@ CONFIG_SCHEMA = cv.All(
 | 
				
			|||||||
async def to_code(config):
 | 
					async def to_code(config):
 | 
				
			||||||
    # Increase the maximum supported size of headers section in HTTP request packet to be processed by the server
 | 
					    # 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)
 | 
					    add_idf_sdkconfig_option("CONFIG_HTTPD_MAX_REQ_HDR_LEN", 1024)
 | 
				
			||||||
    # Check if web_server component has OTA enabled
 | 
					 | 
				
			||||||
    if CORE.config.get(CONF_WEB_SERVER, {}).get(CONF_OTA, True):
 | 
					 | 
				
			||||||
        # Add multipart parser component for ESP-IDF OTA support
 | 
					 | 
				
			||||||
        add_idf_component(name="zorxx/multipart-parser", ref="1.0.1")
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -67,6 +67,42 @@ ConfigPath = list[str | int]
 | 
				
			|||||||
path_context = contextvars.ContextVar("Config path")
 | 
					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:
 | 
					def _path_begins_with(path: ConfigPath, other: ConfigPath) -> bool:
 | 
				
			||||||
    if len(path) < len(other):
 | 
					    if len(path) < len(other):
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
@@ -379,26 +415,11 @@ class LoadValidationStep(ConfigValidationStep):
 | 
				
			|||||||
                    path,
 | 
					                    path,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
            # Remove temp output path and construct new one
 | 
					            # Remove temp output path
 | 
				
			||||||
            result.remove_output_path(path, p_domain)
 | 
					            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
 | 
					            # Process the platform configuration
 | 
				
			||||||
            for load in platform.auto_load:
 | 
					            _process_platform_config(result, self.domain, p_name, p_config, path)
 | 
				
			||||||
                if load not in result:
 | 
					 | 
				
			||||||
                    result.add_validation_step(AutoLoadValidationStep(load))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            result.add_validation_step(
 | 
					 | 
				
			||||||
                MetadataValidationStep(path, p_domain, p_config, platform)
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AutoLoadValidationStep(ConfigValidationStep):
 | 
					class AutoLoadValidationStep(ConfigValidationStep):
 | 
				
			||||||
@@ -413,10 +434,56 @@ class AutoLoadValidationStep(ConfigValidationStep):
 | 
				
			|||||||
        self.domain = domain
 | 
					        self.domain = domain
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def run(self, result: Config) -> None:
 | 
					    def run(self, result: Config) -> None:
 | 
				
			||||||
 | 
					        # Regular component auto-load (no platform)
 | 
				
			||||||
 | 
					        if "." not in self.domain:
 | 
				
			||||||
            if self.domain in result:
 | 
					            if self.domain in result:
 | 
				
			||||||
                # already loaded
 | 
					                # already loaded
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
            result.add_validation_step(LoadValidationStep(self.domain, core.AutoLoad()))
 | 
					            result.add_validation_step(LoadValidationStep(self.domain, core.AutoLoad()))
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # 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):
 | 
					class MetadataValidationStep(ConfigValidationStep):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -116,6 +116,7 @@
 | 
				
			|||||||
#define USE_OTA_PASSWORD
 | 
					#define USE_OTA_PASSWORD
 | 
				
			||||||
#define USE_OTA_STATE_CALLBACK
 | 
					#define USE_OTA_STATE_CALLBACK
 | 
				
			||||||
#define USE_OTA_VERSION 2
 | 
					#define USE_OTA_VERSION 2
 | 
				
			||||||
 | 
					#define USE_TIME_TIMEZONE
 | 
				
			||||||
#define USE_WIFI
 | 
					#define USE_WIFI
 | 
				
			||||||
#define USE_WIFI_AP
 | 
					#define USE_WIFI_AP
 | 
				
			||||||
#define USE_WIREGUARD
 | 
					#define USE_WIREGUARD
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,13 +4,13 @@
 | 
				
			|||||||
#include "esphome/core/hal.h"
 | 
					#include "esphome/core/hal.h"
 | 
				
			||||||
#include "esphome/core/log.h"
 | 
					#include "esphome/core/log.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <strings.h>
 | 
				
			||||||
#include <algorithm>
 | 
					#include <algorithm>
 | 
				
			||||||
#include <cctype>
 | 
					#include <cctype>
 | 
				
			||||||
#include <cmath>
 | 
					#include <cmath>
 | 
				
			||||||
#include <cstdarg>
 | 
					#include <cstdarg>
 | 
				
			||||||
#include <cstdio>
 | 
					#include <cstdio>
 | 
				
			||||||
#include <cstring>
 | 
					#include <cstring>
 | 
				
			||||||
#include <strings.h>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#ifdef USE_HOST
 | 
					#ifdef USE_HOST
 | 
				
			||||||
#ifndef _WIN32
 | 
					#ifndef _WIN32
 | 
				
			||||||
@@ -43,10 +43,10 @@
 | 
				
			|||||||
#include <random>
 | 
					#include <random>
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
#ifdef USE_ESP32
 | 
					#ifdef USE_ESP32
 | 
				
			||||||
#include "rom/crc.h"
 | 
					 | 
				
			||||||
#include "esp_mac.h"
 | 
					 | 
				
			||||||
#include "esp_efuse.h"
 | 
					#include "esp_efuse.h"
 | 
				
			||||||
#include "esp_efuse_table.h"
 | 
					#include "esp_efuse_table.h"
 | 
				
			||||||
 | 
					#include "esp_mac.h"
 | 
				
			||||||
 | 
					#include "rom/crc.h"
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#ifdef USE_LIBRETINY
 | 
					#ifdef USE_LIBRETINY
 | 
				
			||||||
@@ -393,6 +393,21 @@ std::string format_hex_pretty(const uint16_t *data, size_t length) {
 | 
				
			|||||||
  return ret;
 | 
					  return ret;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
std::string format_hex_pretty(const std::vector<uint16_t> &data) { return format_hex_pretty(data.data(), data.size()); }
 | 
					std::string format_hex_pretty(const std::vector<uint16_t> &data) { return format_hex_pretty(data.data(), data.size()); }
 | 
				
			||||||
 | 
					std::string format_hex_pretty(const std::string &data) {
 | 
				
			||||||
 | 
					  if (data.empty())
 | 
				
			||||||
 | 
					    return "";
 | 
				
			||||||
 | 
					  std::string ret;
 | 
				
			||||||
 | 
					  ret.resize(3 * data.length() - 1);
 | 
				
			||||||
 | 
					  for (size_t i = 0; i < data.length(); i++) {
 | 
				
			||||||
 | 
					    ret[3 * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
 | 
				
			||||||
 | 
					    ret[3 * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
 | 
				
			||||||
 | 
					    if (i != data.length() - 1)
 | 
				
			||||||
 | 
					      ret[3 * i + 2] = '.';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (data.length() > 4)
 | 
				
			||||||
 | 
					    return ret + " (" + std::to_string(data.length()) + ")";
 | 
				
			||||||
 | 
					  return ret;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
std::string format_bin(const uint8_t *data, size_t length) {
 | 
					std::string format_bin(const uint8_t *data, size_t length) {
 | 
				
			||||||
  std::string result;
 | 
					  std::string result;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -348,6 +348,8 @@ std::string format_hex_pretty(const uint16_t *data, size_t length);
 | 
				
			|||||||
std::string format_hex_pretty(const std::vector<uint8_t> &data);
 | 
					std::string format_hex_pretty(const std::vector<uint8_t> &data);
 | 
				
			||||||
/// Format the vector \p data in pretty-printed, human-readable hex.
 | 
					/// Format the vector \p data in pretty-printed, human-readable hex.
 | 
				
			||||||
std::string format_hex_pretty(const std::vector<uint16_t> &data);
 | 
					std::string format_hex_pretty(const std::vector<uint16_t> &data);
 | 
				
			||||||
 | 
					/// Format the string \p data in pretty-printed, human-readable hex.
 | 
				
			||||||
 | 
					std::string format_hex_pretty(const std::string &data);
 | 
				
			||||||
/// Format an unsigned integer in pretty-printed, human-readable hex, starting with the most significant byte.
 | 
					/// Format an unsigned integer in pretty-printed, human-readable hex, starting with the most significant byte.
 | 
				
			||||||
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_hex_pretty(T val) {
 | 
					template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_hex_pretty(T val) {
 | 
				
			||||||
  val = convert_big_endian(val);
 | 
					  val = convert_big_endian(val);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -107,6 +107,11 @@ def storage_should_clean(old: StorageJSON, new: StorageJSON) -> bool:
 | 
				
			|||||||
        return True
 | 
					        return True
 | 
				
			||||||
    if old.build_path != new.build_path:
 | 
					    if old.build_path != new.build_path:
 | 
				
			||||||
        return True
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool:
 | 
				
			||||||
    if (
 | 
					    if (
 | 
				
			||||||
        old.loaded_integrations != new.loaded_integrations
 | 
					        old.loaded_integrations != new.loaded_integrations
 | 
				
			||||||
        or old.loaded_platforms != new.loaded_platforms
 | 
					        or old.loaded_platforms != new.loaded_platforms
 | 
				
			||||||
@@ -126,10 +131,11 @@ def update_storage_json():
 | 
				
			|||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if storage_should_clean(old, new):
 | 
					    if storage_should_clean(old, new):
 | 
				
			||||||
        _LOGGER.info(
 | 
					        _LOGGER.info("Core config, version changed, cleaning build files...")
 | 
				
			||||||
            "Core config, version or integrations changed, cleaning build files..."
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        clean_build()
 | 
					        clean_build()
 | 
				
			||||||
 | 
					    elif storage_should_update_cmake_cache(old, new):
 | 
				
			||||||
 | 
					        _LOGGER.info("Integrations changed, cleaning cmake cache...")
 | 
				
			||||||
 | 
					        clean_cmake_cache()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    new.save(path)
 | 
					    new.save(path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -353,6 +359,15 @@ def write_cpp(code_s):
 | 
				
			|||||||
    write_file_if_changed(path, full_file)
 | 
					    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():
 | 
					def clean_build():
 | 
				
			||||||
    import shutil
 | 
					    import shutil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -534,7 +534,7 @@ class BytesType(TypeInfo):
 | 
				
			|||||||
        return f"buffer.encode_bytes({self.number}, reinterpret_cast<const uint8_t*>(this->{self.field_name}.data()), this->{self.field_name}.size());"
 | 
					        return f"buffer.encode_bytes({self.number}, reinterpret_cast<const uint8_t*>(this->{self.field_name}.data()), this->{self.field_name}.size());"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dump(self, name: str) -> str:
 | 
					    def dump(self, name: str) -> str:
 | 
				
			||||||
        o = f'out.append("\'").append({name}).append("\'");'
 | 
					        o = f"out.append(format_hex_pretty({name}));"
 | 
				
			||||||
        return o
 | 
					        return o
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_size_calculation(self, name: str, force: bool = False) -> str:
 | 
					    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.h"
 | 
				
			||||||
    #include "api_pb2_size.h"
 | 
					    #include "api_pb2_size.h"
 | 
				
			||||||
    #include "esphome/core/log.h"
 | 
					    #include "esphome/core/log.h"
 | 
				
			||||||
 | 
					    #include "esphome/core/helpers.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #include <cinttypes>
 | 
					    #include <cinttypes>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										102
									
								
								tests/component_tests/ota/test_web_server_ota.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								tests/component_tests/ota/test_web_server_ota.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,102 @@
 | 
				
			|||||||
 | 
					"""Tests for the web_server OTA platform."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from collections.abc import Callable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None:
 | 
				
			||||||
 | 
					    """Test that web_server OTA platform generates correct code."""
 | 
				
			||||||
 | 
					    main_cpp = generate_main("tests/component_tests/ota/test_web_server_ota.yaml")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check that the web server OTA component is included
 | 
				
			||||||
 | 
					    assert "WebServerOTAComponent" in main_cpp
 | 
				
			||||||
 | 
					    assert "web_server::WebServerOTAComponent" in main_cpp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check that global web server base is referenced
 | 
				
			||||||
 | 
					    assert "global_web_server_base" in main_cpp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check component is registered
 | 
				
			||||||
 | 
					    assert "App.register_component(web_server_webserverotacomponent_id)" in main_cpp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_web_server_ota_with_callbacks(generate_main: Callable[[str], str]) -> None:
 | 
				
			||||||
 | 
					    """Test web_server OTA with state callbacks."""
 | 
				
			||||||
 | 
					    main_cpp = generate_main(
 | 
				
			||||||
 | 
					        "tests/component_tests/ota/test_web_server_ota_callbacks.yaml"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check that web server OTA component is present
 | 
				
			||||||
 | 
					    assert "WebServerOTAComponent" in main_cpp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check that callbacks are configured
 | 
				
			||||||
 | 
					    # The actual callback code is in the component implementation, not main.cpp
 | 
				
			||||||
 | 
					    # But we can check that logger.log statements are present from the callbacks
 | 
				
			||||||
 | 
					    assert "logger.log" in main_cpp
 | 
				
			||||||
 | 
					    assert "OTA started" in main_cpp
 | 
				
			||||||
 | 
					    assert "OTA completed" in main_cpp
 | 
				
			||||||
 | 
					    assert "OTA error" in main_cpp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_web_server_ota_idf_multipart(generate_main: Callable[[str], str]) -> None:
 | 
				
			||||||
 | 
					    """Test that ESP-IDF builds include multipart parser dependency."""
 | 
				
			||||||
 | 
					    main_cpp = generate_main("tests/component_tests/ota/test_web_server_ota_idf.yaml")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check that web server OTA component is present
 | 
				
			||||||
 | 
					    assert "WebServerOTAComponent" in main_cpp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # For ESP-IDF builds, the framework type is esp-idf
 | 
				
			||||||
 | 
					    # The multipart parser dependency is added by web_server_idf
 | 
				
			||||||
 | 
					    assert "web_server::WebServerOTAComponent" in main_cpp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_web_server_ota_without_web_server_fails(
 | 
				
			||||||
 | 
					    generate_main: Callable[[str], str],
 | 
				
			||||||
 | 
					) -> None:
 | 
				
			||||||
 | 
					    """Test that web_server OTA requires web_server component."""
 | 
				
			||||||
 | 
					    # This should fail during validation since web_server_base is required
 | 
				
			||||||
 | 
					    # but we can't test validation failures with generate_main
 | 
				
			||||||
 | 
					    # Instead, verify that both components are needed in valid config
 | 
				
			||||||
 | 
					    main_cpp = generate_main("tests/component_tests/ota/test_web_server_ota.yaml")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Both web server and OTA components should be present
 | 
				
			||||||
 | 
					    assert "WebServer" in main_cpp
 | 
				
			||||||
 | 
					    assert "WebServerOTAComponent" in main_cpp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_multiple_ota_platforms(generate_main: Callable[[str], str]) -> None:
 | 
				
			||||||
 | 
					    """Test multiple OTA platforms can coexist."""
 | 
				
			||||||
 | 
					    main_cpp = generate_main("tests/component_tests/ota/test_web_server_ota_multi.yaml")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check all OTA platforms are included
 | 
				
			||||||
 | 
					    assert "WebServerOTAComponent" in main_cpp
 | 
				
			||||||
 | 
					    assert "ESPHomeOTAComponent" in main_cpp
 | 
				
			||||||
 | 
					    assert "OtaHttpRequestComponent" in main_cpp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check components are from correct namespaces
 | 
				
			||||||
 | 
					    assert "web_server::WebServerOTAComponent" in main_cpp
 | 
				
			||||||
 | 
					    assert "esphome::ESPHomeOTAComponent" in main_cpp
 | 
				
			||||||
 | 
					    assert "http_request::OtaHttpRequestComponent" in main_cpp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_web_server_ota_arduino_with_auth(generate_main: Callable[[str], str]) -> None:
 | 
				
			||||||
 | 
					    """Test web_server OTA with Arduino framework and authentication."""
 | 
				
			||||||
 | 
					    main_cpp = generate_main(
 | 
				
			||||||
 | 
					        "tests/component_tests/ota/test_web_server_ota_arduino.yaml"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check web server OTA component is present
 | 
				
			||||||
 | 
					    assert "WebServerOTAComponent" in main_cpp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check authentication is set up for web server
 | 
				
			||||||
 | 
					    assert "set_auth_username" in main_cpp
 | 
				
			||||||
 | 
					    assert "set_auth_password" in main_cpp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_web_server_ota_esp8266(generate_main: Callable[[str], str]) -> None:
 | 
				
			||||||
 | 
					    """Test web_server OTA on ESP8266 platform."""
 | 
				
			||||||
 | 
					    main_cpp = generate_main(
 | 
				
			||||||
 | 
					        "tests/component_tests/ota/test_web_server_ota_esp8266.yaml"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check web server OTA component is present
 | 
				
			||||||
 | 
					    assert "WebServerOTAComponent" in main_cpp
 | 
				
			||||||
 | 
					    assert "web_server::WebServerOTAComponent" in main_cpp
 | 
				
			||||||
							
								
								
									
										15
									
								
								tests/component_tests/ota/test_web_server_ota.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								tests/component_tests/ota/test_web_server_ota.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					esphome:
 | 
				
			||||||
 | 
					  name: test_web_server_ota
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					esp32:
 | 
				
			||||||
 | 
					  board: esp32dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					wifi:
 | 
				
			||||||
 | 
					  ssid: MySSID
 | 
				
			||||||
 | 
					  password: password1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					web_server:
 | 
				
			||||||
 | 
					  port: 80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ota:
 | 
				
			||||||
 | 
					  - platform: web_server
 | 
				
			||||||
							
								
								
									
										18
									
								
								tests/component_tests/ota/test_web_server_ota_arduino.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								tests/component_tests/ota/test_web_server_ota_arduino.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					esphome:
 | 
				
			||||||
 | 
					  name: test_web_server_ota_arduino
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					esp32:
 | 
				
			||||||
 | 
					  board: esp32dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					wifi:
 | 
				
			||||||
 | 
					  ssid: MySSID
 | 
				
			||||||
 | 
					  password: password1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					web_server:
 | 
				
			||||||
 | 
					  port: 80
 | 
				
			||||||
 | 
					  auth:
 | 
				
			||||||
 | 
					    username: admin
 | 
				
			||||||
 | 
					    password: admin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ota:
 | 
				
			||||||
 | 
					  - platform: web_server
 | 
				
			||||||
							
								
								
									
										31
									
								
								tests/component_tests/ota/test_web_server_ota_callbacks.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								tests/component_tests/ota/test_web_server_ota_callbacks.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					esphome:
 | 
				
			||||||
 | 
					  name: test_web_server_ota_callbacks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					esp32:
 | 
				
			||||||
 | 
					  board: esp32dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					wifi:
 | 
				
			||||||
 | 
					  ssid: MySSID
 | 
				
			||||||
 | 
					  password: password1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					logger:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					web_server:
 | 
				
			||||||
 | 
					  port: 80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ota:
 | 
				
			||||||
 | 
					  - platform: web_server
 | 
				
			||||||
 | 
					    on_begin:
 | 
				
			||||||
 | 
					      - logger.log: "OTA started"
 | 
				
			||||||
 | 
					    on_progress:
 | 
				
			||||||
 | 
					      - logger.log:
 | 
				
			||||||
 | 
					          format: "OTA progress: %.1f%%"
 | 
				
			||||||
 | 
					          args: ["x"]
 | 
				
			||||||
 | 
					    on_end:
 | 
				
			||||||
 | 
					      - logger.log: "OTA completed"
 | 
				
			||||||
 | 
					    on_error:
 | 
				
			||||||
 | 
					      - logger.log:
 | 
				
			||||||
 | 
					          format: "OTA error: %d"
 | 
				
			||||||
 | 
					          args: ["x"]
 | 
				
			||||||
 | 
					    on_state_change:
 | 
				
			||||||
 | 
					      - logger.log: "OTA state changed"
 | 
				
			||||||
							
								
								
									
										15
									
								
								tests/component_tests/ota/test_web_server_ota_esp8266.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								tests/component_tests/ota/test_web_server_ota_esp8266.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					esphome:
 | 
				
			||||||
 | 
					  name: test_web_server_ota_esp8266
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					esp8266:
 | 
				
			||||||
 | 
					  board: nodemcuv2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					wifi:
 | 
				
			||||||
 | 
					  ssid: MySSID
 | 
				
			||||||
 | 
					  password: password1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					web_server:
 | 
				
			||||||
 | 
					  port: 80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ota:
 | 
				
			||||||
 | 
					  - platform: web_server
 | 
				
			||||||
							
								
								
									
										17
									
								
								tests/component_tests/ota/test_web_server_ota_idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								tests/component_tests/ota/test_web_server_ota_idf.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					esphome:
 | 
				
			||||||
 | 
					  name: test_web_server_ota_idf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					esp32:
 | 
				
			||||||
 | 
					  board: esp32dev
 | 
				
			||||||
 | 
					  framework:
 | 
				
			||||||
 | 
					    type: esp-idf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					wifi:
 | 
				
			||||||
 | 
					  ssid: MySSID
 | 
				
			||||||
 | 
					  password: password1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					web_server:
 | 
				
			||||||
 | 
					  port: 80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ota:
 | 
				
			||||||
 | 
					  - platform: web_server
 | 
				
			||||||
							
								
								
									
										21
									
								
								tests/component_tests/ota/test_web_server_ota_multi.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								tests/component_tests/ota/test_web_server_ota_multi.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					esphome:
 | 
				
			||||||
 | 
					  name: test_web_server_ota_multi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					esp32:
 | 
				
			||||||
 | 
					  board: esp32dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					wifi:
 | 
				
			||||||
 | 
					  ssid: MySSID
 | 
				
			||||||
 | 
					  password: password1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					web_server:
 | 
				
			||||||
 | 
					  port: 80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					http_request:
 | 
				
			||||||
 | 
					  verify_ssl: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ota:
 | 
				
			||||||
 | 
					  - platform: esphome
 | 
				
			||||||
 | 
					    password: "test_password"
 | 
				
			||||||
 | 
					  - platform: web_server
 | 
				
			||||||
 | 
					  - platform: http_request
 | 
				
			||||||
							
								
								
									
										38
									
								
								tests/component_tests/web_server/test_ota_migration.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								tests/component_tests/web_server/test_ota_migration.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					"""Tests for web_server OTA migration validation."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from esphome import config_validation as cv
 | 
				
			||||||
 | 
					from esphome.types import ConfigType
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_web_server_ota_true_fails_validation() -> None:
 | 
				
			||||||
 | 
					    """Test that web_server with ota: true fails validation with helpful message."""
 | 
				
			||||||
 | 
					    from esphome.components.web_server import validate_ota_removed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Config with ota: true should fail
 | 
				
			||||||
 | 
					    config: ConfigType = {"ota": True}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with pytest.raises(cv.Invalid) as exc_info:
 | 
				
			||||||
 | 
					        validate_ota_removed(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check error message contains migration instructions
 | 
				
			||||||
 | 
					    error_msg = str(exc_info.value)
 | 
				
			||||||
 | 
					    assert "has been removed from 'web_server'" in error_msg
 | 
				
			||||||
 | 
					    assert "platform: web_server" in error_msg
 | 
				
			||||||
 | 
					    assert "ota:" in error_msg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_web_server_ota_false_passes_validation() -> None:
 | 
				
			||||||
 | 
					    """Test that web_server with ota: false passes validation."""
 | 
				
			||||||
 | 
					    from esphome.components.web_server import validate_ota_removed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Config with ota: false should pass
 | 
				
			||||||
 | 
					    config: ConfigType = {"ota": False}
 | 
				
			||||||
 | 
					    result = validate_ota_removed(config)
 | 
				
			||||||
 | 
					    assert result == config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Config without ota should also pass
 | 
				
			||||||
 | 
					    config: ConfigType = {}
 | 
				
			||||||
 | 
					    result = validate_ota_removed(config)
 | 
				
			||||||
 | 
					    assert result == config
 | 
				
			||||||
@@ -2,6 +2,7 @@ network:
 | 
				
			|||||||
  enable_ipv6: true
 | 
					  enable_ipv6: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
openthread:
 | 
					openthread:
 | 
				
			||||||
 | 
					  device_type: FTD
 | 
				
			||||||
  channel: 13
 | 
					  channel: 13
 | 
				
			||||||
  network_name: OpenThread-8f28
 | 
					  network_name: OpenThread-8f28
 | 
				
			||||||
  network_key: 0xdfd34f0f05cad978ec4e32b0413038ff
 | 
					  network_key: 0xdfd34f0f05cad978ec4e32b0413038ff
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,11 @@
 | 
				
			|||||||
 | 
					esphome:
 | 
				
			||||||
 | 
					  name: test-web-server-no-ota-idf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					esp32:
 | 
				
			||||||
 | 
					  board: esp32dev
 | 
				
			||||||
 | 
					  framework:
 | 
				
			||||||
 | 
					    type: esp-idf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
packages:
 | 
					packages:
 | 
				
			||||||
  device_base: !include common.yaml
 | 
					  device_base: !include common.yaml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -6,4 +14,3 @@ packages:
 | 
				
			|||||||
web_server:
 | 
					web_server:
 | 
				
			||||||
  port: 8080
 | 
					  port: 8080
 | 
				
			||||||
  version: 2
 | 
					  version: 2
 | 
				
			||||||
  ota: false
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,6 @@
 | 
				
			|||||||
# Test configuration for ESP-IDF web server with OTA enabled
 | 
					 | 
				
			||||||
esphome:
 | 
					esphome:
 | 
				
			||||||
  name: test-web-server-ota-idf
 | 
					  name: test-web-server-ota-idf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Force ESP-IDF framework
 | 
					 | 
				
			||||||
esp32:
 | 
					esp32:
 | 
				
			||||||
  board: esp32dev
 | 
					  board: esp32dev
 | 
				
			||||||
  framework:
 | 
					  framework:
 | 
				
			||||||
@@ -15,17 +13,17 @@ packages:
 | 
				
			|||||||
ota:
 | 
					ota:
 | 
				
			||||||
  - platform: esphome
 | 
					  - platform: esphome
 | 
				
			||||||
    password: "test_ota_password"
 | 
					    password: "test_ota_password"
 | 
				
			||||||
 | 
					  - platform: web_server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Web server with OTA enabled
 | 
					# Web server configuration
 | 
				
			||||||
web_server:
 | 
					web_server:
 | 
				
			||||||
  port: 8080
 | 
					  port: 8080
 | 
				
			||||||
  version: 2
 | 
					  version: 2
 | 
				
			||||||
  ota: true
 | 
					 | 
				
			||||||
  include_internal: true
 | 
					  include_internal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Enable debug logging for OTA
 | 
					# Enable debug logging for OTA
 | 
				
			||||||
logger:
 | 
					logger:
 | 
				
			||||||
  level: DEBUG
 | 
					  level: VERBOSE
 | 
				
			||||||
  logs:
 | 
					  logs:
 | 
				
			||||||
    web_server: VERBOSE
 | 
					    web_server: VERBOSE
 | 
				
			||||||
    web_server_idf: VERBOSE
 | 
					    web_server_idf: VERBOSE
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,18 @@
 | 
				
			|||||||
 | 
					esphome:
 | 
				
			||||||
 | 
					  name: test-ws-ota-disabled-idf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					esp32:
 | 
				
			||||||
 | 
					  board: esp32dev
 | 
				
			||||||
 | 
					  framework:
 | 
				
			||||||
 | 
					    type: esp-idf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
packages:
 | 
					packages:
 | 
				
			||||||
  device_base: !include common.yaml
 | 
					  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:
 | 
					ota:
 | 
				
			||||||
  - platform: esphome
 | 
					  - platform: esphome
 | 
				
			||||||
 | 
					
 | 
				
			||||||
web_server:
 | 
					web_server:
 | 
				
			||||||
  port: 8080
 | 
					  port: 8080
 | 
				
			||||||
  version: 2
 | 
					  version: 2
 | 
				
			||||||
  ota: false
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user