From d88bee1a2854624c556e704e28ebe4c0b9805585 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 1 Apr 2024 21:47:56 -0500
Subject: [PATCH 01/26] Separate OTABackend from OTA component

---
 esphome/__main__.py                           |  28 +--
 .../esp32_ble_tracker/esp32_ble_tracker.cpp   |  13 +-
 esphome/components/esphome/ota/__init__.py    | 152 +++++++++++++++
 esphome/components/esphome/ota/automation.h   |  71 +++++++
 .../ota/ota_esphome.cpp}                      | 182 +++++++++---------
 .../ota/ota_esphome.h}                        |  57 ++----
 esphome/components/ota/__init__.py            | 136 +------------
 esphome/components/ota/automation.h           |  71 -------
 esphome/components/ota/ota_backend.h          |  31 ++-
 .../ota/ota_backend_arduino_esp32.cpp         |   3 +-
 .../ota/ota_backend_arduino_esp32.h           |   6 +-
 .../ota/ota_backend_arduino_esp8266.cpp       |   7 +-
 .../ota/ota_backend_arduino_esp8266.h         |   5 +-
 .../ota/ota_backend_arduino_libretiny.cpp     |   7 +-
 .../ota/ota_backend_arduino_libretiny.h       |   5 +-
 .../ota/ota_backend_arduino_rp2040.cpp        |   7 +-
 .../ota/ota_backend_arduino_rp2040.h          |   7 +-
 .../components/ota/ota_backend_esp_idf.cpp    |  11 +-
 esphome/components/ota/ota_backend_esp_idf.h  |   8 +-
 .../components/safe_mode/button/__init__.py   |  14 +-
 .../safe_mode/button/safe_mode_button.cpp     |   2 +-
 .../safe_mode/button/safe_mode_button.h       |   8 +-
 .../components/safe_mode/switch/__init__.py   |  12 +-
 .../safe_mode/switch/safe_mode_switch.cpp     |   4 +-
 .../safe_mode/switch/safe_mode_switch.h       |   8 +-
 esphome/cpp_helpers.py                        |  22 ++-
 tests/dummy_main.cpp                          |   4 +-
 tests/test1.yaml                              |  47 ++---
 tests/test11.5.yaml                           |   1 +
 tests/test2.yaml                              |   7 +-
 tests/test3.1.yaml                            |   3 +-
 tests/test3.yaml                              |   7 +-
 tests/test4.yaml                              |   5 +-
 tests/test5.yaml                              |   1 +
 tests/test6.yaml                              |   1 +
 tests/test9.1.yaml                            |   1 +
 tests/test9.yaml                              |   1 +
 37 files changed, 502 insertions(+), 453 deletions(-)
 create mode 100644 esphome/components/esphome/ota/__init__.py
 create mode 100644 esphome/components/esphome/ota/automation.h
 rename esphome/components/{ota/ota_component.cpp => esphome/ota/ota_esphome.cpp} (69%)
 rename esphome/components/{ota/ota_component.h => esphome/ota/ota_esphome.h} (53%)
 delete mode 100644 esphome/components/ota/automation.h

diff --git a/esphome/__main__.py b/esphome/__main__.py
index dcd2dddb4b..43550ec8df 100644
--- a/esphome/__main__.py
+++ b/esphome/__main__.py
@@ -18,22 +18,23 @@ from esphome.const import (
     CONF_BAUD_RATE,
     CONF_BROKER,
     CONF_DEASSERT_RTS_DTR,
+    CONF_DISABLED,
+    CONF_ESPHOME,
     CONF_LOGGER,
+    CONF_MDNS,
+    CONF_MQTT,
     CONF_NAME,
     CONF_OTA,
-    CONF_MQTT,
-    CONF_MDNS,
-    CONF_DISABLED,
     CONF_PASSWORD,
-    CONF_PORT,
-    CONF_ESPHOME,
+    CONF_PLATFORM,
     CONF_PLATFORMIO_OPTIONS,
+    CONF_PORT,
     CONF_SUBSTITUTIONS,
     PLATFORM_BK72XX,
-    PLATFORM_RTL87XX,
     PLATFORM_ESP32,
     PLATFORM_ESP8266,
     PLATFORM_RP2040,
+    PLATFORM_RTL87XX,
     SECRETS_FILES,
 )
 from esphome.core import CORE, EsphomeError, coroutine
@@ -65,7 +66,7 @@ def choose_prompt(options, purpose: str = None):
         f'Found multiple options{f" for {purpose}" if purpose else ""}, please choose one:'
     )
     for i, (desc, _) in enumerate(options):
-        safe_print(f"  [{i+1}] {desc}")
+        safe_print(f"  [{i + 1}] {desc}")
 
     while True:
         opt = input("(number): ")
@@ -330,15 +331,20 @@ def upload_program(config, args, host):
 
         return 1  # Unknown target platform
 
-    if CONF_OTA not in config:
+    ota_conf = {}
+    if CONF_OTA in config:
+        for ota_item in config.get(CONF_OTA):
+            if ota_item[CONF_PLATFORM] == CONF_ESPHOME:
+                ota_conf = ota_item
+                break
+
+    if not ota_conf:
         raise EsphomeError(
-            "Cannot upload Over the Air as the config does not include the ota: "
-            "component"
+            f"Cannot upload Over the Air as the {CONF_OTA} configuration is not present or does not include {CONF_PLATFORM}: {CONF_ESPHOME}"
         )
 
     from esphome import espota2
 
-    ota_conf = config[CONF_OTA]
     remote_port = ota_conf[CONF_PORT]
     password = ota_conf.get(CONF_PASSWORD, "")
 
diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
index a5bbd85b47..e102fe41c4 100644
--- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
+++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
@@ -18,7 +18,7 @@
 #include <cinttypes>
 
 #ifdef USE_OTA
-#include "esphome/components/ota/ota_component.h"
+#include "esphome/components/esphome/ota/ota_esphome.h"
 #endif
 
 #ifdef USE_ARDUINO
@@ -58,11 +58,12 @@ void ESP32BLETracker::setup() {
   this->scanner_idle_ = true;
 
 #ifdef USE_OTA
-  ota::global_ota_component->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t error) {
-    if (state == ota::OTA_STARTED) {
-      this->stop_scan();
-    }
-  });
+  ota_esphome::global_ota_component->add_on_state_callback(
+      [this](ota_esphome::OTAESPHomeState state, float progress, uint8_t error) {
+        if (state == ota_esphome::OTA_STARTED) {
+          this->stop_scan();
+        }
+      });
 #endif
 }
 
diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py
new file mode 100644
index 0000000000..3143d5408d
--- /dev/null
+++ b/esphome/components/esphome/ota/__init__.py
@@ -0,0 +1,152 @@
+from esphome.cpp_generator import RawExpression
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import automation
+from esphome.const import (
+    CONF_ID,
+    CONF_NUM_ATTEMPTS,
+    CONF_OTA,
+    CONF_PASSWORD,
+    CONF_PORT,
+    CONF_REBOOT_TIMEOUT,
+    CONF_SAFE_MODE,
+    CONF_TRIGGER_ID,
+    CONF_VERSION,
+    KEY_PAST_SAFE_MODE,
+)
+from esphome.core import CORE, coroutine_with_priority
+
+
+CODEOWNERS = ["@esphome/core"]
+AUTO_LOAD = ["md5", "socket"]
+DEPENDENCIES = ["network", "ota"]
+
+CONF_ON_BEGIN = "on_begin"
+CONF_ON_END = "on_end"
+CONF_ON_ERROR = "on_error"
+CONF_ON_PROGRESS = "on_progress"
+CONF_ON_STATE_CHANGE = "on_state_change"
+
+ota_esphome = cg.esphome_ns.namespace("ota_esphome")
+
+OTAESPHomeComponent = ota_esphome.class_("OTAESPHomeComponent", cg.Component)
+OTAESPHomeEndTrigger = ota_esphome.class_(
+    "OTAESPHomeEndTrigger", automation.Trigger.template()
+)
+OTAESPHomeErrorTrigger = ota_esphome.class_(
+    "OTAESPHomeErrorTrigger", automation.Trigger.template()
+)
+OTAESPHomeProgressTrigger = ota_esphome.class_(
+    "OTAESPHomeProgressTrigger", automation.Trigger.template()
+)
+OTAESPHomeStartTrigger = ota_esphome.class_(
+    "OTAESPHomeStartTrigger", automation.Trigger.template()
+)
+OTAESPHomeStateChangeTrigger = ota_esphome.class_(
+    "OTAESPHomeStateChangeTrigger", automation.Trigger.template()
+)
+
+OTAESPHomeState = ota_esphome.enum("OTAESPHomeState")
+
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(OTAESPHomeComponent),
+        cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean,
+        cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, int=True),
+        cv.SplitDefault(
+            CONF_PORT,
+            esp8266=8266,
+            esp32=3232,
+            rp2040=2040,
+            bk72xx=8892,
+            rtl87xx=8892,
+        ): cv.port,
+        cv.Optional(CONF_PASSWORD): cv.string,
+        cv.Optional(
+            CONF_REBOOT_TIMEOUT, default="5min"
+        ): cv.positive_time_period_milliseconds,
+        cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int,
+        cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
+                    OTAESPHomeStateChangeTrigger
+                ),
+            }
+        ),
+        cv.Optional(CONF_ON_BEGIN): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAESPHomeStartTrigger),
+            }
+        ),
+        cv.Optional(CONF_ON_END): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAESPHomeEndTrigger),
+            }
+        ),
+        cv.Optional(CONF_ON_ERROR): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAESPHomeErrorTrigger),
+            }
+        ),
+        cv.Optional(CONF_ON_PROGRESS): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
+                    OTAESPHomeProgressTrigger
+                ),
+            }
+        ),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+@coroutine_with_priority(50.0)
+async def to_code(config):
+    CORE.data[CONF_OTA] = {}
+
+    var = cg.new_Pvariable(config[CONF_ID])
+    cg.add(var.set_port(config[CONF_PORT]))
+    cg.add_define("USE_OTA")
+    if CONF_PASSWORD in config:
+        cg.add(var.set_auth_password(config[CONF_PASSWORD]))
+        cg.add_define("USE_OTA_PASSWORD")
+    cg.add_define("USE_OTA_VERSION", config[CONF_VERSION])
+
+    await cg.register_component(var, config)
+
+    if config[CONF_SAFE_MODE]:
+        condition = var.should_enter_safe_mode(
+            config[CONF_NUM_ATTEMPTS], config[CONF_REBOOT_TIMEOUT]
+        )
+        cg.add(RawExpression(f"if ({condition}) return"))
+        CORE.data[CONF_OTA][KEY_PAST_SAFE_MODE] = True
+
+    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)
+
+    use_state_callback = False
+    for conf in config.get(CONF_ON_STATE_CHANGE, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        await automation.build_automation(trigger, [(OTAESPHomeState, "state")], conf)
+        use_state_callback = True
+    for conf in config.get(CONF_ON_BEGIN, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        await automation.build_automation(trigger, [], conf)
+        use_state_callback = True
+    for conf in config.get(CONF_ON_PROGRESS, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        await automation.build_automation(trigger, [(float, "x")], conf)
+        use_state_callback = True
+    for conf in config.get(CONF_ON_END, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        await automation.build_automation(trigger, [], conf)
+        use_state_callback = True
+    for conf in config.get(CONF_ON_ERROR, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        await automation.build_automation(trigger, [(cg.uint8, "x")], conf)
+        use_state_callback = True
+    if use_state_callback:
+        cg.add_define("USE_OTA_STATE_CALLBACK")
diff --git a/esphome/components/esphome/ota/automation.h b/esphome/components/esphome/ota/automation.h
new file mode 100644
index 0000000000..a23b1c5590
--- /dev/null
+++ b/esphome/components/esphome/ota/automation.h
@@ -0,0 +1,71 @@
+#pragma once
+
+#ifdef USE_OTA_STATE_CALLBACK
+#include "ota_esphome.h"
+
+#include "esphome/core/component.h"
+#include "esphome/core/defines.h"
+#include "esphome/core/automation.h"
+
+namespace esphome {
+namespace ota_esphome {
+
+class OTAESPHomeStateChangeTrigger : public Trigger<OTAESPHomeState> {
+ public:
+  explicit OTAESPHomeStateChangeTrigger(OTAESPHomeComponent *parent) {
+    parent->add_on_state_callback([this, parent](OTAESPHomeState state, float progress, uint8_t error) {
+      if (!parent->is_failed()) {
+        return trigger(state);
+      }
+    });
+  }
+};
+
+class OTAESPHomeStartTrigger : public Trigger<> {
+ public:
+  explicit OTAESPHomeStartTrigger(OTAESPHomeComponent *parent) {
+    parent->add_on_state_callback([this, parent](OTAESPHomeState state, float progress, uint8_t error) {
+      if (state == OTA_STARTED && !parent->is_failed()) {
+        trigger();
+      }
+    });
+  }
+};
+
+class OTAESPHomeProgressTrigger : public Trigger<float> {
+ public:
+  explicit OTAESPHomeProgressTrigger(OTAESPHomeComponent *parent) {
+    parent->add_on_state_callback([this, parent](OTAESPHomeState state, float progress, uint8_t error) {
+      if (state == OTA_IN_PROGRESS && !parent->is_failed()) {
+        trigger(progress);
+      }
+    });
+  }
+};
+
+class OTAESPHomeEndTrigger : public Trigger<> {
+ public:
+  explicit OTAESPHomeEndTrigger(OTAESPHomeComponent *parent) {
+    parent->add_on_state_callback([this, parent](OTAESPHomeState state, float progress, uint8_t error) {
+      if (state == OTA_COMPLETED && !parent->is_failed()) {
+        trigger();
+      }
+    });
+  }
+};
+
+class OTAESPHomeErrorTrigger : public Trigger<uint8_t> {
+ public:
+  explicit OTAESPHomeErrorTrigger(OTAESPHomeComponent *parent) {
+    parent->add_on_state_callback([this, parent](OTAESPHomeState state, float progress, uint8_t error) {
+      if (state == OTA_ERROR && !parent->is_failed()) {
+        trigger(error);
+      }
+    });
+  }
+};
+
+}  // namespace ota_esphome
+}  // namespace esphome
+
+#endif  // USE_OTA_STATE_CALLBACK
diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/esphome/ota/ota_esphome.cpp
similarity index 69%
rename from esphome/components/ota/ota_component.cpp
rename to esphome/components/esphome/ota/ota_esphome.cpp
index 15af14ff1a..447f0587f8 100644
--- a/esphome/components/ota/ota_component.cpp
+++ b/esphome/components/esphome/ota/ota_esphome.cpp
@@ -1,55 +1,55 @@
-#include "ota_component.h"
-#include "ota_backend.h"
-#include "ota_backend_arduino_esp32.h"
-#include "ota_backend_arduino_esp8266.h"
-#include "ota_backend_arduino_rp2040.h"
-#include "ota_backend_arduino_libretiny.h"
-#include "ota_backend_esp_idf.h"
+#include "ota_esphome.h"
 
-#include "esphome/core/log.h"
-#include "esphome/core/application.h"
-#include "esphome/core/hal.h"
-#include "esphome/core/util.h"
 #include "esphome/components/md5/md5.h"
 #include "esphome/components/network/util.h"
+#include "esphome/components/ota/ota_backend.h"
+#include "esphome/components/ota/ota_backend_arduino_esp32.h"
+#include "esphome/components/ota/ota_backend_arduino_esp8266.h"
+#include "esphome/components/ota/ota_backend_arduino_libretiny.h"
+#include "esphome/components/ota/ota_backend_arduino_rp2040.h"
+#include "esphome/components/ota/ota_backend_esp_idf.h"
+#include "esphome/core/application.h"
+#include "esphome/core/hal.h"
+#include "esphome/core/log.h"
+#include "esphome/core/util.h"
 
 #include <cerrno>
 #include <cstdio>
 
 namespace esphome {
-namespace ota {
+namespace ota_esphome {
 
-static const char *const TAG = "ota";
+static const char *const TAG = "esphome.ota";
 static constexpr u_int16_t OTA_BLOCK_SIZE = 8192;
 
-OTAComponent *global_ota_component = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+OTAESPHomeComponent *global_ota_component = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
-std::unique_ptr<OTABackend> make_ota_backend() {
+std::unique_ptr<ota::OTABackend> make_ota_backend() {
 #ifdef USE_ARDUINO
 #ifdef USE_ESP8266
-  return make_unique<ArduinoESP8266OTABackend>();
+  return make_unique<ota::ArduinoESP8266OTABackend>();
 #endif  // USE_ESP8266
 #ifdef USE_ESP32
-  return make_unique<ArduinoESP32OTABackend>();
+  return make_unique<ota::ArduinoESP32OTABackend>();
 #endif  // USE_ESP32
 #endif  // USE_ARDUINO
 #ifdef USE_ESP_IDF
-  return make_unique<IDFOTABackend>();
+  return make_unique<ota::IDFOTABackend>();
 #endif  // USE_ESP_IDF
 #ifdef USE_RP2040
-  return make_unique<ArduinoRP2040OTABackend>();
+  return make_unique<ota::ArduinoRP2040OTABackend>();
 #endif  // USE_RP2040
 #ifdef USE_LIBRETINY
-  return make_unique<ArduinoLibreTinyOTABackend>();
+  return make_unique<ota::ArduinoLibreTinyOTABackend>();
 #endif
 }
 
-OTAComponent::OTAComponent() { global_ota_component = this; }
+OTAESPHomeComponent::OTAESPHomeComponent() { global_ota_component = this; }
 
-void OTAComponent::setup() {
+void OTAESPHomeComponent::setup() {
   server_ = socket::socket_ip(SOCK_STREAM, 0);
   if (server_ == nullptr) {
-    ESP_LOGW(TAG, "Could not create socket.");
+    ESP_LOGW(TAG, "Could not create socket");
     this->mark_failed();
     return;
   }
@@ -88,41 +88,39 @@ void OTAComponent::setup() {
     this->mark_failed();
     return;
   }
-
-  this->dump_config();
 }
 
-void OTAComponent::dump_config() {
-  ESP_LOGCONFIG(TAG, "Over-The-Air Updates:");
+void OTAESPHomeComponent::dump_config() {
+  ESP_LOGCONFIG(TAG, "Over-The-Air updates:");
   ESP_LOGCONFIG(TAG, "  Address: %s:%u", network::get_use_address().c_str(), this->port_);
+  ESP_LOGCONFIG(TAG, "  OTA version: %d", USE_OTA_VERSION);
 #ifdef USE_OTA_PASSWORD
   if (!this->password_.empty()) {
-    ESP_LOGCONFIG(TAG, "  Using Password.");
+    ESP_LOGCONFIG(TAG, "  Password configured");
   }
 #endif
-  ESP_LOGCONFIG(TAG, "  OTA version: %d.", USE_OTA_VERSION);
   if (this->has_safe_mode_ && this->safe_mode_rtc_value_ > 1 &&
-      this->safe_mode_rtc_value_ != esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC) {
-    ESP_LOGW(TAG, "Last Boot was an unhandled reset, will proceed to safe mode in %" PRIu32 " restarts",
+      this->safe_mode_rtc_value_ != OTAESPHomeComponent::ENTER_SAFE_MODE_MAGIC) {
+    ESP_LOGW(TAG, "Last reset occurred too quickly; safe mode will be invoked in %" PRIu32 " restarts",
              this->safe_mode_num_attempts_ - this->safe_mode_rtc_value_);
   }
 }
 
-void OTAComponent::loop() {
+void OTAESPHomeComponent::loop() {
   this->handle_();
 
   if (this->has_safe_mode_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_enable_time_) {
     this->has_safe_mode_ = false;
     // successful boot, reset counter
-    ESP_LOGI(TAG, "Boot seems successful, resetting boot loop counter.");
+    ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter");
     this->clean_rtc();
   }
 }
 
 static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01;
 
-void OTAComponent::handle_() {
-  OTAResponseTypes error_code = OTA_RESPONSE_ERROR_UNKNOWN;
+void OTAESPHomeComponent::handle_() {
+  ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
   bool update_started = false;
   size_t total = 0;
   uint32_t last_progress = 0;
@@ -130,7 +128,7 @@ void OTAComponent::handle_() {
   char *sbuf = reinterpret_cast<char *>(buf);
   size_t ota_size;
   uint8_t ota_features;
-  std::unique_ptr<OTABackend> backend;
+  std::unique_ptr<ota::OTABackend> backend;
   (void) ota_features;
 #if USE_OTA_VERSION == 2
   size_t size_acknowledged = 0;
@@ -147,30 +145,30 @@ void OTAComponent::handle_() {
   int enable = 1;
   int err = client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
   if (err != 0) {
-    ESP_LOGW(TAG, "Socket could not enable tcp nodelay, errno: %d", errno);
+    ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno);
     return;
   }
 
-  ESP_LOGD(TAG, "Starting OTA Update from %s...", this->client_->getpeername().c_str());
+  ESP_LOGD(TAG, "Starting OTA update from %s...", this->client_->getpeername().c_str());
   this->status_set_warning();
 #ifdef USE_OTA_STATE_CALLBACK
   this->state_callback_.call(OTA_STARTED, 0.0f, 0);
 #endif
 
   if (!this->readall_(buf, 5)) {
-    ESP_LOGW(TAG, "Reading magic bytes failed!");
+    ESP_LOGW(TAG, "Reading magic bytes failed");
     goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
   }
   // 0x6C, 0x26, 0xF7, 0x5C, 0x45
   if (buf[0] != 0x6C || buf[1] != 0x26 || buf[2] != 0xF7 || buf[3] != 0x5C || buf[4] != 0x45) {
     ESP_LOGW(TAG, "Magic bytes do not match! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", buf[0], buf[1], buf[2], buf[3],
              buf[4]);
-    error_code = OTA_RESPONSE_ERROR_MAGIC;
+    error_code = ota::OTA_RESPONSE_ERROR_MAGIC;
     goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
   }
 
   // Send OK and version - 2 bytes
-  buf[0] = OTA_RESPONSE_OK;
+  buf[0] = ota::OTA_RESPONSE_OK;
   buf[1] = USE_OTA_VERSION;
   this->writeall_(buf, 2);
 
@@ -178,23 +176,23 @@ void OTAComponent::handle_() {
 
   // Read features - 1 byte
   if (!this->readall_(buf, 1)) {
-    ESP_LOGW(TAG, "Reading features failed!");
+    ESP_LOGW(TAG, "Reading features failed");
     goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
   }
   ota_features = buf[0];  // NOLINT
   ESP_LOGV(TAG, "OTA features is 0x%02X", ota_features);
 
   // Acknowledge header - 1 byte
-  buf[0] = OTA_RESPONSE_HEADER_OK;
+  buf[0] = ota::OTA_RESPONSE_HEADER_OK;
   if ((ota_features & FEATURE_SUPPORTS_COMPRESSION) != 0 && backend->supports_compression()) {
-    buf[0] = OTA_RESPONSE_SUPPORTS_COMPRESSION;
+    buf[0] = ota::OTA_RESPONSE_SUPPORTS_COMPRESSION;
   }
 
   this->writeall_(buf, 1);
 
 #ifdef USE_OTA_PASSWORD
   if (!this->password_.empty()) {
-    buf[0] = OTA_RESPONSE_REQUEST_AUTH;
+    buf[0] = ota::OTA_RESPONSE_REQUEST_AUTH;
     this->writeall_(buf, 1);
     md5::MD5Digest md5{};
     md5.init();
@@ -206,7 +204,7 @@ void OTAComponent::handle_() {
 
     // Send nonce, 32 bytes hex MD5
     if (!this->writeall_(reinterpret_cast<uint8_t *>(sbuf), 32)) {
-      ESP_LOGW(TAG, "Auth: Writing nonce failed!");
+      ESP_LOGW(TAG, "Auth: Writing nonce failed");
       goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
     }
 
@@ -218,7 +216,7 @@ void OTAComponent::handle_() {
 
     // Receive cnonce, 32 bytes hex MD5
     if (!this->readall_(buf, 32)) {
-      ESP_LOGW(TAG, "Auth: Reading cnonce failed!");
+      ESP_LOGW(TAG, "Auth: Reading cnonce failed");
       goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
     }
     sbuf[32] = '\0';
@@ -233,7 +231,7 @@ void OTAComponent::handle_() {
 
     // Receive result, 32 bytes hex MD5
     if (!this->readall_(buf + 64, 32)) {
-      ESP_LOGW(TAG, "Auth: Reading response failed!");
+      ESP_LOGW(TAG, "Auth: Reading response failed");
       goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
     }
     sbuf[64 + 32] = '\0';
@@ -244,20 +242,20 @@ void OTAComponent::handle_() {
       matches = matches && buf[i] == buf[64 + i];
 
     if (!matches) {
-      ESP_LOGW(TAG, "Auth failed! Passwords do not match!");
-      error_code = OTA_RESPONSE_ERROR_AUTH_INVALID;
+      ESP_LOGW(TAG, "Auth failed! Passwords do not match");
+      error_code = ota::OTA_RESPONSE_ERROR_AUTH_INVALID;
       goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
     }
   }
 #endif  // USE_OTA_PASSWORD
 
   // Acknowledge auth OK - 1 byte
-  buf[0] = OTA_RESPONSE_AUTH_OK;
+  buf[0] = ota::OTA_RESPONSE_AUTH_OK;
   this->writeall_(buf, 1);
 
   // Read size, 4 bytes MSB first
   if (!this->readall_(buf, 4)) {
-    ESP_LOGW(TAG, "Reading size failed!");
+    ESP_LOGW(TAG, "Reading size failed");
     goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
   }
   ota_size = 0;
@@ -268,17 +266,17 @@ void OTAComponent::handle_() {
   ESP_LOGV(TAG, "OTA size is %u bytes", ota_size);
 
   error_code = backend->begin(ota_size);
-  if (error_code != OTA_RESPONSE_OK)
+  if (error_code != ota::OTA_RESPONSE_OK)
     goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
   update_started = true;
 
   // Acknowledge prepare OK - 1 byte
-  buf[0] = OTA_RESPONSE_UPDATE_PREPARE_OK;
+  buf[0] = ota::OTA_RESPONSE_UPDATE_PREPARE_OK;
   this->writeall_(buf, 1);
 
   // Read binary MD5, 32 bytes
   if (!this->readall_(buf, 32)) {
-    ESP_LOGW(TAG, "Reading binary MD5 checksum failed!");
+    ESP_LOGW(TAG, "Reading binary MD5 checksum failed");
     goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
   }
   sbuf[32] = '\0';
@@ -286,7 +284,7 @@ void OTAComponent::handle_() {
   backend->set_update_md5(sbuf);
 
   // Acknowledge MD5 OK - 1 byte
-  buf[0] = OTA_RESPONSE_BIN_MD5_OK;
+  buf[0] = ota::OTA_RESPONSE_BIN_MD5_OK;
   this->writeall_(buf, 1);
 
   while (total < ota_size) {
@@ -299,7 +297,7 @@ void OTAComponent::handle_() {
         delay(1);
         continue;
       }
-      ESP_LOGW(TAG, "Error receiving data for update, errno: %d", errno);
+      ESP_LOGW(TAG, "Error receiving data for update, errno %d", errno);
       goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
     } else if (read == 0) {
       // $ man recv
@@ -310,14 +308,14 @@ void OTAComponent::handle_() {
     }
 
     error_code = backend->write(buf, read);
-    if (error_code != OTA_RESPONSE_OK) {
+    if (error_code != ota::OTA_RESPONSE_OK) {
       ESP_LOGW(TAG, "Error writing binary data to flash!, error_code: %d", error_code);
       goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
     }
     total += read;
 #if USE_OTA_VERSION == 2
     while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
-      buf[0] = OTA_RESPONSE_CHUNK_OK;
+      buf[0] = ota::OTA_RESPONSE_CHUNK_OK;
       this->writeall_(buf, 1);
       size_acknowledged += OTA_BLOCK_SIZE;
     }
@@ -338,29 +336,29 @@ void OTAComponent::handle_() {
   }
 
   // Acknowledge receive OK - 1 byte
-  buf[0] = OTA_RESPONSE_RECEIVE_OK;
+  buf[0] = ota::OTA_RESPONSE_RECEIVE_OK;
   this->writeall_(buf, 1);
 
   error_code = backend->end();
-  if (error_code != OTA_RESPONSE_OK) {
+  if (error_code != ota::OTA_RESPONSE_OK) {
     ESP_LOGW(TAG, "Error ending OTA!, error_code: %d", error_code);
     goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
   }
 
   // Acknowledge Update end OK - 1 byte
-  buf[0] = OTA_RESPONSE_UPDATE_END_OK;
+  buf[0] = ota::OTA_RESPONSE_UPDATE_END_OK;
   this->writeall_(buf, 1);
 
   // Read ACK
-  if (!this->readall_(buf, 1) || buf[0] != OTA_RESPONSE_OK) {
-    ESP_LOGW(TAG, "Reading back acknowledgement failed!");
+  if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
+    ESP_LOGW(TAG, "Reading back acknowledgement failed");
     // do not go to error, this is not fatal
   }
 
   this->client_->close();
   this->client_ = nullptr;
   delay(10);
-  ESP_LOGI(TAG, "OTA update finished!");
+  ESP_LOGI(TAG, "OTA update finished");
   this->status_clear_warning();
 #ifdef USE_OTA_STATE_CALLBACK
   this->state_callback_.call(OTA_COMPLETED, 100.0f, 0);
@@ -384,7 +382,7 @@ error:
 #endif
 }
 
-bool OTAComponent::readall_(uint8_t *buf, size_t len) {
+bool OTAESPHomeComponent::readall_(uint8_t *buf, size_t len) {
   uint32_t start = millis();
   uint32_t at = 0;
   while (len - at > 0) {
@@ -401,7 +399,7 @@ bool OTAComponent::readall_(uint8_t *buf, size_t len) {
         delay(1);
         continue;
       }
-      ESP_LOGW(TAG, "Failed to read %d bytes of data, errno: %d", len, errno);
+      ESP_LOGW(TAG, "Failed to read %d bytes of data, errno %d", len, errno);
       return false;
     } else if (read == 0) {
       ESP_LOGW(TAG, "Remote closed connection");
@@ -415,7 +413,7 @@ bool OTAComponent::readall_(uint8_t *buf, size_t len) {
 
   return true;
 }
-bool OTAComponent::writeall_(const uint8_t *buf, size_t len) {
+bool OTAESPHomeComponent::writeall_(const uint8_t *buf, size_t len) {
   uint32_t start = millis();
   uint32_t at = 0;
   while (len - at > 0) {
@@ -432,7 +430,7 @@ bool OTAComponent::writeall_(const uint8_t *buf, size_t len) {
         delay(1);
         continue;
       }
-      ESP_LOGW(TAG, "Failed to write %d bytes of data, errno: %d", len, errno);
+      ESP_LOGW(TAG, "Failed to write %d bytes of data, errno %d", len, errno);
       return false;
     } else {
       at += written;
@@ -443,31 +441,31 @@ bool OTAComponent::writeall_(const uint8_t *buf, size_t len) {
   return true;
 }
 
-float OTAComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
-uint16_t OTAComponent::get_port() const { return this->port_; }
-void OTAComponent::set_port(uint16_t port) { this->port_ = port; }
+float OTAESPHomeComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
+uint16_t OTAESPHomeComponent::get_port() const { return this->port_; }
+void OTAESPHomeComponent::set_port(uint16_t port) { this->port_ = port; }
 
-void OTAComponent::set_safe_mode_pending(const bool &pending) {
+void OTAESPHomeComponent::set_safe_mode_pending(const bool &pending) {
   if (!this->has_safe_mode_)
     return;
 
   uint32_t current_rtc = this->read_rtc_();
 
-  if (pending && current_rtc != esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC) {
-    ESP_LOGI(TAG, "Device will enter safe mode on next boot.");
-    this->write_rtc_(esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC);
+  if (pending && current_rtc != OTAESPHomeComponent::ENTER_SAFE_MODE_MAGIC) {
+    ESP_LOGI(TAG, "Device will enter safe mode on next boot");
+    this->write_rtc_(OTAESPHomeComponent::ENTER_SAFE_MODE_MAGIC);
   }
 
-  if (!pending && current_rtc == esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC) {
+  if (!pending && current_rtc == OTAESPHomeComponent::ENTER_SAFE_MODE_MAGIC) {
     ESP_LOGI(TAG, "Safe mode pending has been cleared");
     this->clean_rtc();
   }
 }
-bool OTAComponent::get_safe_mode_pending() {
-  return this->has_safe_mode_ && this->read_rtc_() == esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC;
+bool OTAESPHomeComponent::get_safe_mode_pending() {
+  return this->has_safe_mode_ && this->read_rtc_() == OTAESPHomeComponent::ENTER_SAFE_MODE_MAGIC;
 }
 
-bool OTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time) {
+bool OTAESPHomeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time) {
   this->has_safe_mode_ = true;
   this->safe_mode_start_time_ = millis();
   this->safe_mode_enable_time_ = enable_time;
@@ -475,24 +473,24 @@ bool OTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_
   this->rtc_ = global_preferences->make_preference<uint32_t>(233825507UL, false);
   this->safe_mode_rtc_value_ = this->read_rtc_();
 
-  bool is_manual_safe_mode = this->safe_mode_rtc_value_ == esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC;
+  bool is_manual_safe_mode = this->safe_mode_rtc_value_ == OTAESPHomeComponent::ENTER_SAFE_MODE_MAGIC;
 
   if (is_manual_safe_mode) {
     ESP_LOGI(TAG, "Safe mode has been entered manually");
   } else {
-    ESP_LOGCONFIG(TAG, "There have been %" PRIu32 " suspected unsuccessful boot attempts.", this->safe_mode_rtc_value_);
+    ESP_LOGCONFIG(TAG, "There have been %" PRIu32 " suspected unsuccessful boot attempts", this->safe_mode_rtc_value_);
   }
 
   if (this->safe_mode_rtc_value_ >= num_attempts || is_manual_safe_mode) {
     this->clean_rtc();
 
     if (!is_manual_safe_mode) {
-      ESP_LOGE(TAG, "Boot loop detected. Proceeding to safe mode.");
+      ESP_LOGE(TAG, "Boot loop detected. Proceeding to safe mode");
     }
 
     this->status_set_error();
     this->set_timeout(enable_time, []() {
-      ESP_LOGE(TAG, "No OTA attempt made, restarting.");
+      ESP_LOGE(TAG, "No OTA attempt made, restarting");
       App.reboot();
     });
 
@@ -500,7 +498,7 @@ bool OTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_
     delay(300);  // NOLINT
     App.setup();
 
-    ESP_LOGI(TAG, "Waiting for OTA attempt.");
+    ESP_LOGI(TAG, "Waiting for OTA attempt");
 
     return true;
   } else {
@@ -509,27 +507,27 @@ bool OTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_
     return false;
   }
 }
-void OTAComponent::write_rtc_(uint32_t val) {
+void OTAESPHomeComponent::write_rtc_(uint32_t val) {
   this->rtc_.save(&val);
   global_preferences->sync();
 }
-uint32_t OTAComponent::read_rtc_() {
+uint32_t OTAESPHomeComponent::read_rtc_() {
   uint32_t val;
   if (!this->rtc_.load(&val))
     return 0;
   return val;
 }
-void OTAComponent::clean_rtc() { this->write_rtc_(0); }
-void OTAComponent::on_safe_shutdown() {
-  if (this->has_safe_mode_ && this->read_rtc_() != esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC)
+void OTAESPHomeComponent::clean_rtc() { this->write_rtc_(0); }
+void OTAESPHomeComponent::on_safe_shutdown() {
+  if (this->has_safe_mode_ && this->read_rtc_() != OTAESPHomeComponent::ENTER_SAFE_MODE_MAGIC)
     this->clean_rtc();
 }
 
 #ifdef USE_OTA_STATE_CALLBACK
-void OTAComponent::add_on_state_callback(std::function<void(OTAState, float, uint8_t)> &&callback) {
+void OTAESPHomeComponent::add_on_state_callback(std::function<void(OTAESPHomeState, float, uint8_t)> &&callback) {
   this->state_callback_.add(std::move(callback));
 }
 #endif
 
-}  // namespace ota
+}  // namespace ota_esphome
 }  // namespace esphome
diff --git a/esphome/components/ota/ota_component.h b/esphome/components/esphome/ota/ota_esphome.h
similarity index 53%
rename from esphome/components/ota/ota_component.h
rename to esphome/components/esphome/ota/ota_esphome.h
index c20f4f0709..19008d458e 100644
--- a/esphome/components/ota/ota_component.h
+++ b/esphome/components/esphome/ota/ota_esphome.h
@@ -2,48 +2,19 @@
 
 #include "esphome/components/socket/socket.h"
 #include "esphome/core/component.h"
-#include "esphome/core/preferences.h"
-#include "esphome/core/helpers.h"
 #include "esphome/core/defines.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/preferences.h"
 
 namespace esphome {
-namespace ota {
+namespace ota_esphome {
 
-enum OTAResponseTypes {
-  OTA_RESPONSE_OK = 0x00,
-  OTA_RESPONSE_REQUEST_AUTH = 0x01,
+enum OTAESPHomeState { OTA_COMPLETED = 0, OTA_STARTED, OTA_IN_PROGRESS, OTA_ERROR };
 
-  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_ERROR };
-
-/// OTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA.
-class OTAComponent : public Component {
+/// OTAESPHomeComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA.
+class OTAESPHomeComponent : public Component {
  public:
-  OTAComponent();
+  OTAESPHomeComponent();
 #ifdef USE_OTA_PASSWORD
   void set_auth_password(const std::string &password) { password_ = password; }
 #endif  // USE_OTA_PASSWORD
@@ -58,7 +29,7 @@ class OTAComponent : public Component {
   bool get_safe_mode_pending();
 
 #ifdef USE_OTA_STATE_CALLBACK
-  void add_on_state_callback(std::function<void(OTAState, float, uint8_t)> &&callback);
+  void add_on_state_callback(std::function<void(OTAESPHomeState, float, uint8_t)> &&callback);
 #endif
 
   // ========== INTERNAL METHODS ==========
@@ -91,9 +62,9 @@ class OTAComponent : public Component {
   std::unique_ptr<socket::Socket> server_;
   std::unique_ptr<socket::Socket> client_;
 
-  bool has_safe_mode_{false};              ///< stores whether safe mode can be enabled.
-  uint32_t safe_mode_start_time_;          ///< stores when safe mode was enabled.
-  uint32_t safe_mode_enable_time_{60000};  ///< The time safe mode should be on for.
+  bool has_safe_mode_{false};              ///< stores whether safe mode can be enabled
+  uint32_t safe_mode_start_time_;          ///< stores when safe mode was enabled
+  uint32_t safe_mode_enable_time_{60000};  ///< The time safe mode should be on for
   uint32_t safe_mode_rtc_value_;
   uint8_t safe_mode_num_attempts_;
   ESPPreferenceObject rtc_;
@@ -102,11 +73,11 @@ class OTAComponent : public Component {
       0x5afe5afe;  ///< a magic number to indicate that safe mode should be entered on next boot
 
 #ifdef USE_OTA_STATE_CALLBACK
-  CallbackManager<void(OTAState, float, uint8_t)> state_callback_{};
+  CallbackManager<void(OTAESPHomeState, float, uint8_t)> state_callback_{};
 #endif
 };
 
-extern OTAComponent *global_ota_component;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+extern OTAESPHomeComponent *global_ota_component;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
-}  // namespace ota
+}  // namespace ota_esphome
 }  // namespace esphome
diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py
index 3c845490dc..aed0b87f85 100644
--- a/esphome/components/ota/__init__.py
+++ b/esphome/components/ota/__init__.py
@@ -1,137 +1,19 @@
-from esphome.cpp_generator import RawExpression
-import esphome.codegen as cg
 import esphome.config_validation as cv
-from esphome import automation
-from esphome.const import (
-    CONF_ID,
-    CONF_NUM_ATTEMPTS,
-    CONF_PASSWORD,
-    CONF_PORT,
-    CONF_REBOOT_TIMEOUT,
-    CONF_SAFE_MODE,
-    CONF_TRIGGER_ID,
-    CONF_OTA,
-    KEY_PAST_SAFE_MODE,
-    CONF_VERSION,
-)
-from esphome.core import CORE, coroutine_with_priority
+
+from esphome.const import CONF_ESPHOME, CONF_OTA, CONF_PLATFORM
 
 CODEOWNERS = ["@esphome/core"]
+AUTO_LOAD = ["md5"]
 DEPENDENCIES = ["network"]
-AUTO_LOAD = ["socket", "md5"]
 
-CONF_ON_STATE_CHANGE = "on_state_change"
-CONF_ON_BEGIN = "on_begin"
-CONF_ON_PROGRESS = "on_progress"
-CONF_ON_END = "on_end"
-CONF_ON_ERROR = "on_error"
-
-ota_ns = cg.esphome_ns.namespace("ota")
-OTAState = ota_ns.enum("OTAState")
-OTAComponent = ota_ns.class_("OTAComponent", cg.Component)
-OTAStateChangeTrigger = ota_ns.class_(
-    "OTAStateChangeTrigger", automation.Trigger.template()
-)
-OTAStartTrigger = ota_ns.class_("OTAStartTrigger", automation.Trigger.template())
-OTAProgressTrigger = ota_ns.class_("OTAProgressTrigger", automation.Trigger.template())
-OTAEndTrigger = ota_ns.class_("OTAEndTrigger", automation.Trigger.template())
-OTAErrorTrigger = ota_ns.class_("OTAErrorTrigger", automation.Trigger.template())
+IS_PLATFORM_COMPONENT = True
 
 
-CONFIG_SCHEMA = cv.Schema(
-    {
-        cv.GenerateID(): cv.declare_id(OTAComponent),
-        cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean,
-        cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, int=True),
-        cv.SplitDefault(
-            CONF_PORT,
-            esp8266=8266,
-            esp32=3232,
-            rp2040=2040,
-            bk72xx=8892,
-            rtl87xx=8892,
-        ): cv.port,
-        cv.Optional(CONF_PASSWORD): cv.string,
-        cv.Optional(
-            CONF_REBOOT_TIMEOUT, default="5min"
-        ): cv.positive_time_period_milliseconds,
-        cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int,
-        cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation(
-            {
-                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAStateChangeTrigger),
-            }
-        ),
-        cv.Optional(CONF_ON_BEGIN): automation.validate_automation(
-            {
-                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAStartTrigger),
-            }
-        ),
-        cv.Optional(CONF_ON_ERROR): automation.validate_automation(
-            {
-                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAErrorTrigger),
-            }
-        ),
-        cv.Optional(CONF_ON_PROGRESS): automation.validate_automation(
-            {
-                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAProgressTrigger),
-            }
-        ),
-        cv.Optional(CONF_ON_END): automation.validate_automation(
-            {
-                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAEndTrigger),
-            }
-        ),
-    }
-).extend(cv.COMPONENT_SCHEMA)
-
-
-@coroutine_with_priority(50.0)
-async def to_code(config):
-    CORE.data[CONF_OTA] = {}
-
-    var = cg.new_Pvariable(config[CONF_ID])
-    cg.add(var.set_port(config[CONF_PORT]))
-    cg.add_define("USE_OTA")
-    if CONF_PASSWORD in config:
-        cg.add(var.set_auth_password(config[CONF_PASSWORD]))
-        cg.add_define("USE_OTA_PASSWORD")
-    cg.add_define("USE_OTA_VERSION", config[CONF_VERSION])
-
-    await cg.register_component(var, config)
-
-    if config[CONF_SAFE_MODE]:
-        condition = var.should_enter_safe_mode(
-            config[CONF_NUM_ATTEMPTS], config[CONF_REBOOT_TIMEOUT]
+def _ota_final_validate(config):
+    if len(config) < 1:
+        raise cv.Invalid(
+            f"At least one platform must be specified for '{CONF_OTA}'; add '{CONF_PLATFORM}: {CONF_ESPHOME}' for original OTA functionality"
         )
-        cg.add(RawExpression(f"if ({condition}) return"))
-        CORE.data[CONF_OTA][KEY_PAST_SAFE_MODE] = True
 
-    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)
-
-    use_state_callback = False
-    for conf in config.get(CONF_ON_STATE_CHANGE, []):
-        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        await automation.build_automation(trigger, [(OTAState, "state")], conf)
-        use_state_callback = True
-    for conf in config.get(CONF_ON_BEGIN, []):
-        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        await automation.build_automation(trigger, [], conf)
-        use_state_callback = True
-    for conf in config.get(CONF_ON_PROGRESS, []):
-        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        await automation.build_automation(trigger, [(float, "x")], conf)
-        use_state_callback = True
-    for conf in config.get(CONF_ON_END, []):
-        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        await automation.build_automation(trigger, [], conf)
-        use_state_callback = True
-    for conf in config.get(CONF_ON_ERROR, []):
-        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        await automation.build_automation(trigger, [(cg.uint8, "x")], conf)
-        use_state_callback = True
-    if use_state_callback:
-        cg.add_define("USE_OTA_STATE_CALLBACK")
+FINAL_VALIDATE_SCHEMA = _ota_final_validate
diff --git a/esphome/components/ota/automation.h b/esphome/components/ota/automation.h
deleted file mode 100644
index 0c77a18ce1..0000000000
--- a/esphome/components/ota/automation.h
+++ /dev/null
@@ -1,71 +0,0 @@
-#pragma once
-
-#include "esphome/core/defines.h"
-#ifdef USE_OTA_STATE_CALLBACK
-
-#include "esphome/core/component.h"
-#include "esphome/core/automation.h"
-#include "esphome/components/ota/ota_component.h"
-
-namespace esphome {
-namespace ota {
-
-class OTAStateChangeTrigger : public Trigger<OTAState> {
- public:
-  explicit OTAStateChangeTrigger(OTAComponent *parent) {
-    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
-      if (!parent->is_failed()) {
-        return trigger(state);
-      }
-    });
-  }
-};
-
-class OTAStartTrigger : public Trigger<> {
- public:
-  explicit OTAStartTrigger(OTAComponent *parent) {
-    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
-      if (state == OTA_STARTED && !parent->is_failed()) {
-        trigger();
-      }
-    });
-  }
-};
-
-class OTAProgressTrigger : public Trigger<float> {
- public:
-  explicit OTAProgressTrigger(OTAComponent *parent) {
-    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
-      if (state == OTA_IN_PROGRESS && !parent->is_failed()) {
-        trigger(progress);
-      }
-    });
-  }
-};
-
-class OTAEndTrigger : public Trigger<> {
- public:
-  explicit OTAEndTrigger(OTAComponent *parent) {
-    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
-      if (state == OTA_COMPLETED && !parent->is_failed()) {
-        trigger();
-      }
-    });
-  }
-};
-
-class OTAErrorTrigger : public Trigger<uint8_t> {
- public:
-  explicit OTAErrorTrigger(OTAComponent *parent) {
-    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
-      if (state == OTA_ERROR && !parent->is_failed()) {
-        trigger(error);
-      }
-    });
-  }
-};
-
-}  // namespace ota
-}  // namespace esphome
-
-#endif  // USE_OTA_STATE_CALLBACK
diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h
index 5c5b61a278..f7646dbdea 100644
--- a/esphome/components/ota/ota_backend.h
+++ b/esphome/components/ota/ota_backend.h
@@ -1,9 +1,38 @@
 #pragma once
-#include "ota_component.h"
+#include "esphome/core/helpers.h"
 
 namespace esphome {
 namespace ota {
 
+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,
+};
+
 class OTABackend {
  public:
   virtual ~OTABackend() = default;
diff --git a/esphome/components/ota/ota_backend_arduino_esp32.cpp b/esphome/components/ota/ota_backend_arduino_esp32.cpp
index 4759737dbd..35d8ac9665 100644
--- a/esphome/components/ota/ota_backend_arduino_esp32.cpp
+++ b/esphome/components/ota/ota_backend_arduino_esp32.cpp
@@ -1,8 +1,7 @@
-#include "esphome/core/defines.h"
 #ifdef USE_ESP32_FRAMEWORK_ARDUINO
+#include "esphome/core/defines.h"
 
 #include "ota_backend_arduino_esp32.h"
-#include "ota_component.h"
 #include "ota_backend.h"
 
 #include <Update.h>
diff --git a/esphome/components/ota/ota_backend_arduino_esp32.h b/esphome/components/ota/ota_backend_arduino_esp32.h
index f86a70d678..ac7fe9f14f 100644
--- a/esphome/components/ota/ota_backend_arduino_esp32.h
+++ b/esphome/components/ota/ota_backend_arduino_esp32.h
@@ -1,10 +1,10 @@
 #pragma once
-#include "esphome/core/defines.h"
 #ifdef USE_ESP32_FRAMEWORK_ARDUINO
-
-#include "ota_component.h"
 #include "ota_backend.h"
 
+#include "esphome/core/defines.h"
+#include "esphome/core/helpers.h"
+
 namespace esphome {
 namespace ota {
 
diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.cpp b/esphome/components/ota/ota_backend_arduino_esp8266.cpp
index 23dc0d4e21..068c0ffeaa 100644
--- a/esphome/components/ota/ota_backend_arduino_esp8266.cpp
+++ b/esphome/components/ota/ota_backend_arduino_esp8266.cpp
@@ -1,10 +1,9 @@
-#include "esphome/core/defines.h"
 #ifdef USE_ARDUINO
 #ifdef USE_ESP8266
-
-#include "ota_backend_arduino_esp8266.h"
-#include "ota_component.h"
 #include "ota_backend.h"
+#include "ota_backend_arduino_esp8266.h"
+
+#include "esphome/core/defines.h"
 #include "esphome/components/esp8266/preferences.h"
 
 #include <Updater.h>
diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.h b/esphome/components/ota/ota_backend_arduino_esp8266.h
index 7937c665b0..7f44d7c965 100644
--- a/esphome/components/ota/ota_backend_arduino_esp8266.h
+++ b/esphome/components/ota/ota_backend_arduino_esp8266.h
@@ -1,10 +1,9 @@
 #pragma once
-#include "esphome/core/defines.h"
 #ifdef USE_ARDUINO
 #ifdef USE_ESP8266
-
-#include "ota_component.h"
 #include "ota_backend.h"
+
+#include "esphome/core/defines.h"
 #include "esphome/core/macros.h"
 
 namespace esphome {
diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.cpp b/esphome/components/ota/ota_backend_arduino_libretiny.cpp
index dbf6c97988..f845ec7466 100644
--- a/esphome/components/ota/ota_backend_arduino_libretiny.cpp
+++ b/esphome/components/ota/ota_backend_arduino_libretiny.cpp
@@ -1,9 +1,8 @@
-#include "esphome/core/defines.h"
 #ifdef USE_LIBRETINY
-
-#include "ota_backend_arduino_libretiny.h"
-#include "ota_component.h"
 #include "ota_backend.h"
+#include "ota_backend_arduino_libretiny.h"
+
+#include "esphome/core/defines.h"
 
 #include <Update.h>
 
diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.h b/esphome/components/ota/ota_backend_arduino_libretiny.h
index 79656bb353..11deb6e2f2 100644
--- a/esphome/components/ota/ota_backend_arduino_libretiny.h
+++ b/esphome/components/ota/ota_backend_arduino_libretiny.h
@@ -1,10 +1,9 @@
 #pragma once
-#include "esphome/core/defines.h"
 #ifdef USE_LIBRETINY
-
-#include "ota_component.h"
 #include "ota_backend.h"
 
+#include "esphome/core/defines.h"
+
 namespace esphome {
 namespace ota {
 
diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.cpp b/esphome/components/ota/ota_backend_arduino_rp2040.cpp
index 260387cec1..78935c5bfd 100644
--- a/esphome/components/ota/ota_backend_arduino_rp2040.cpp
+++ b/esphome/components/ota/ota_backend_arduino_rp2040.cpp
@@ -1,11 +1,10 @@
-#include "esphome/core/defines.h"
 #ifdef USE_ARDUINO
 #ifdef USE_RP2040
-
-#include "esphome/components/rp2040/preferences.h"
 #include "ota_backend.h"
 #include "ota_backend_arduino_rp2040.h"
-#include "ota_component.h"
+
+#include "esphome/components/rp2040/preferences.h"
+#include "esphome/core/defines.h"
 
 #include <Updater.h>
 
diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.h b/esphome/components/ota/ota_backend_arduino_rp2040.h
index 5aa2ec9435..b189964ab3 100644
--- a/esphome/components/ota/ota_backend_arduino_rp2040.h
+++ b/esphome/components/ota/ota_backend_arduino_rp2040.h
@@ -1,11 +1,10 @@
 #pragma once
-#include "esphome/core/defines.h"
 #ifdef USE_ARDUINO
 #ifdef USE_RP2040
-
-#include "esphome/core/macros.h"
 #include "ota_backend.h"
-#include "ota_component.h"
+
+#include "esphome/core/defines.h"
+#include "esphome/core/macros.h"
 
 namespace esphome {
 namespace ota {
diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp
index 319a1482f1..a551bb6b32 100644
--- a/esphome/components/ota/ota_backend_esp_idf.cpp
+++ b/esphome/components/ota/ota_backend_esp_idf.cpp
@@ -1,12 +1,11 @@
-#include "esphome/core/defines.h"
 #ifdef USE_ESP_IDF
-
-#include <esp_task_wdt.h>
-
 #include "ota_backend_esp_idf.h"
-#include "ota_component.h"
-#include <esp_ota_ops.h>
+
 #include "esphome/components/md5/md5.h"
+#include "esphome/core/defines.h"
+
+#include <esp_ota_ops.h>
+#include <esp_task_wdt.h>
 
 #if ESP_IDF_VERSION_MAJOR >= 5
 #include <spi_flash_mmap.h>
diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h
index af09d0d693..ed66d9b970 100644
--- a/esphome/components/ota/ota_backend_esp_idf.h
+++ b/esphome/components/ota/ota_backend_esp_idf.h
@@ -1,11 +1,11 @@
 #pragma once
-#include "esphome/core/defines.h"
 #ifdef USE_ESP_IDF
-
-#include "ota_component.h"
 #include "ota_backend.h"
-#include <esp_ota_ops.h>
+
 #include "esphome/components/md5/md5.h"
+#include "esphome/core/defines.h"
+
+#include <esp_ota_ops.h>
 
 namespace esphome {
 namespace ota {
diff --git a/esphome/components/safe_mode/button/__init__.py b/esphome/components/safe_mode/button/__init__.py
index 307e4e372e..ffd2b4e6ee 100644
--- a/esphome/components/safe_mode/button/__init__.py
+++ b/esphome/components/safe_mode/button/__init__.py
@@ -1,18 +1,17 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import button
-from esphome.components.ota import OTAComponent
+from esphome.components.esphome.ota import OTAESPHomeComponent
 from esphome.const import (
-    CONF_ID,
-    CONF_OTA,
+    CONF_ESPHOME,
     DEVICE_CLASS_RESTART,
     ENTITY_CATEGORY_CONFIG,
     ICON_RESTART_ALERT,
 )
+from .. import safe_mode_ns
 
 DEPENDENCIES = ["ota"]
 
-safe_mode_ns = cg.esphome_ns.namespace("safe_mode")
 SafeModeButton = safe_mode_ns.class_("SafeModeButton", button.Button, cg.Component)
 
 CONFIG_SCHEMA = (
@@ -22,15 +21,14 @@ CONFIG_SCHEMA = (
         entity_category=ENTITY_CATEGORY_CONFIG,
         icon=ICON_RESTART_ALERT,
     )
-    .extend({cv.GenerateID(CONF_OTA): cv.use_id(OTAComponent)})
+    .extend({cv.GenerateID(CONF_ESPHOME): cv.use_id(OTAESPHomeComponent)})
     .extend(cv.COMPONENT_SCHEMA)
 )
 
 
 async def to_code(config):
-    var = cg.new_Pvariable(config[CONF_ID])
+    var = await button.new_button(config)
     await cg.register_component(var, config)
-    await button.register_button(var, config)
 
-    ota = await cg.get_variable(config[CONF_OTA])
+    ota = await cg.get_variable(config[CONF_ESPHOME])
     cg.add(var.set_ota(ota))
diff --git a/esphome/components/safe_mode/button/safe_mode_button.cpp b/esphome/components/safe_mode/button/safe_mode_button.cpp
index 2b8654de46..673af02c20 100644
--- a/esphome/components/safe_mode/button/safe_mode_button.cpp
+++ b/esphome/components/safe_mode/button/safe_mode_button.cpp
@@ -8,7 +8,7 @@ namespace safe_mode {
 
 static const char *const TAG = "safe_mode.button";
 
-void SafeModeButton::set_ota(ota::OTAComponent *ota) { this->ota_ = ota; }
+void SafeModeButton::set_ota(ota_esphome::OTAESPHomeComponent *ota) { this->ota_ = ota; }
 
 void SafeModeButton::press_action() {
   ESP_LOGI(TAG, "Restarting device in safe mode...");
diff --git a/esphome/components/safe_mode/button/safe_mode_button.h b/esphome/components/safe_mode/button/safe_mode_button.h
index 63e0d1755e..4ac0d2d177 100644
--- a/esphome/components/safe_mode/button/safe_mode_button.h
+++ b/esphome/components/safe_mode/button/safe_mode_button.h
@@ -1,8 +1,8 @@
 #pragma once
 
-#include "esphome/core/component.h"
-#include "esphome/components/ota/ota_component.h"
 #include "esphome/components/button/button.h"
+#include "esphome/components/esphome/ota/ota_esphome.h"
+#include "esphome/core/component.h"
 
 namespace esphome {
 namespace safe_mode {
@@ -10,10 +10,10 @@ namespace safe_mode {
 class SafeModeButton : public button::Button, public Component {
  public:
   void dump_config() override;
-  void set_ota(ota::OTAComponent *ota);
+  void set_ota(ota_esphome::OTAESPHomeComponent *ota);
 
  protected:
-  ota::OTAComponent *ota_;
+  ota_esphome::OTAESPHomeComponent *ota_;
   void press_action() override;
 };
 
diff --git a/esphome/components/safe_mode/switch/__init__.py b/esphome/components/safe_mode/switch/__init__.py
index a6fcdfbece..c576cc7403 100644
--- a/esphome/components/safe_mode/switch/__init__.py
+++ b/esphome/components/safe_mode/switch/__init__.py
@@ -1,9 +1,9 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import switch
-from esphome.components.ota import OTAComponent
+from esphome.components.esphome.ota import OTAESPHomeComponent
 from esphome.const import (
-    CONF_OTA,
+    CONF_ESPHOME,
     ENTITY_CATEGORY_CONFIG,
     ICON_RESTART_ALERT,
 )
@@ -16,11 +16,11 @@ SafeModeSwitch = safe_mode_ns.class_("SafeModeSwitch", switch.Switch, cg.Compone
 CONFIG_SCHEMA = (
     switch.switch_schema(
         SafeModeSwitch,
-        icon=ICON_RESTART_ALERT,
-        entity_category=ENTITY_CATEGORY_CONFIG,
         block_inverted=True,
+        entity_category=ENTITY_CATEGORY_CONFIG,
+        icon=ICON_RESTART_ALERT,
     )
-    .extend({cv.GenerateID(CONF_OTA): cv.use_id(OTAComponent)})
+    .extend({cv.GenerateID(CONF_ESPHOME): cv.use_id(OTAESPHomeComponent)})
     .extend(cv.COMPONENT_SCHEMA)
 )
 
@@ -29,5 +29,5 @@ async def to_code(config):
     var = await switch.new_switch(config)
     await cg.register_component(var, config)
 
-    ota = await cg.get_variable(config[CONF_OTA])
+    ota = await cg.get_variable(config[CONF_ESPHOME])
     cg.add(var.set_ota(ota))
diff --git a/esphome/components/safe_mode/switch/safe_mode_switch.cpp b/esphome/components/safe_mode/switch/safe_mode_switch.cpp
index a3979eec06..a145a71386 100644
--- a/esphome/components/safe_mode/switch/safe_mode_switch.cpp
+++ b/esphome/components/safe_mode/switch/safe_mode_switch.cpp
@@ -1,14 +1,14 @@
 #include "safe_mode_switch.h"
+#include "esphome/core/application.h"
 #include "esphome/core/hal.h"
 #include "esphome/core/log.h"
-#include "esphome/core/application.h"
 
 namespace esphome {
 namespace safe_mode {
 
 static const char *const TAG = "safe_mode_switch";
 
-void SafeModeSwitch::set_ota(ota::OTAComponent *ota) { this->ota_ = ota; }
+void SafeModeSwitch::set_ota(ota_esphome::OTAESPHomeComponent *ota) { this->ota_ = ota; }
 
 void SafeModeSwitch::write_state(bool state) {
   // Acknowledge
diff --git a/esphome/components/safe_mode/switch/safe_mode_switch.h b/esphome/components/safe_mode/switch/safe_mode_switch.h
index 2772db3d84..7fc8927c80 100644
--- a/esphome/components/safe_mode/switch/safe_mode_switch.h
+++ b/esphome/components/safe_mode/switch/safe_mode_switch.h
@@ -1,8 +1,8 @@
 #pragma once
 
-#include "esphome/core/component.h"
-#include "esphome/components/ota/ota_component.h"
+#include "esphome/components/esphome/ota/ota_esphome.h"
 #include "esphome/components/switch/switch.h"
+#include "esphome/core/component.h"
 
 namespace esphome {
 namespace safe_mode {
@@ -10,10 +10,10 @@ namespace safe_mode {
 class SafeModeSwitch : public switch_::Switch, public Component {
  public:
   void dump_config() override;
-  void set_ota(ota::OTAComponent *ota);
+  void set_ota(ota_esphome::OTAESPHomeComponent *ota);
 
  protected:
-  ota::OTAComponent *ota_;
+  ota_esphome::OTAESPHomeComponent *ota_;
   void write_state(bool state) override;
 };
 
diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py
index 4b3716e223..402f60284e 100644
--- a/esphome/cpp_helpers.py
+++ b/esphome/cpp_helpers.py
@@ -3,14 +3,16 @@ import logging
 from esphome.const import (
     CONF_DISABLED_BY_DEFAULT,
     CONF_ENTITY_CATEGORY,
+    CONF_ESPHOME,
     CONF_ICON,
     CONF_INTERNAL,
     CONF_NAME,
-    CONF_SETUP_PRIORITY,
-    CONF_UPDATE_INTERVAL,
-    CONF_TYPE_ID,
     CONF_OTA,
+    CONF_PLATFORM,
     CONF_SAFE_MODE,
+    CONF_SETUP_PRIORITY,
+    CONF_TYPE_ID,
+    CONF_UPDATE_INTERVAL,
     KEY_PAST_SAFE_MODE,
 )
 
@@ -139,9 +141,17 @@ async def build_registry_list(registry, config):
 
 
 async def past_safe_mode():
-    safe_mode_enabled = (
-        CONF_OTA in CORE.config and CORE.config[CONF_OTA][CONF_SAFE_MODE]
-    )
+    ota_conf = {}
+    if CONF_OTA in CORE.config:
+        for ota_item in CORE.config.get(CONF_OTA):
+            if ota_item[CONF_PLATFORM] == CONF_ESPHOME:
+                ota_conf = ota_item
+                break
+
+    if not ota_conf:
+        return
+
+    safe_mode_enabled = ota_conf[CONF_SAFE_MODE]
     if not safe_mode_enabled:
         return
 
diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp
index da5c6d10d0..42af4eb47f 100644
--- a/tests/dummy_main.cpp
+++ b/tests/dummy_main.cpp
@@ -5,7 +5,7 @@
 
 #include <esphome/components/gpio/switch/gpio_switch.h>
 #include <esphome/components/logger/logger.h>
-#include <esphome/components/ota/ota_component.h>
+#include <esphome/components/esphome/ota/ota_esphome.h>
 #include <esphome/components/wifi/wifi_component.h>
 #include <esphome/core/application.h>
 
@@ -25,7 +25,7 @@ void setup() {
   ap.set_password("password1");
   wifi->add_sta(ap);
 
-  auto *ota = new ota::OTAComponent();  // NOLINT
+  auto *ota = new ota_esphome::OTAESPHomeComponent();  // NOLINT
   ota->set_port(8266);
 
   App.setup();
diff --git a/tests/test1.yaml b/tests/test1.yaml
index c8ae9691c2..6d93900b47 100644
--- a/tests/test1.yaml
+++ b/tests/test1.yaml
@@ -265,29 +265,30 @@ uart:
     baud_rate: 9600
 
 ota:
-  safe_mode: true
-  password: "superlongpasswordthatnoonewillknow"
-  port: 3286
-  reboot_timeout: 2min
-  num_attempts: 5
-  on_state_change:
-    then:
-      lambda: >-
-        ESP_LOGD("ota", "State %d", state);
-  on_begin:
-    then:
-      logger.log: OTA begin
-  on_progress:
-    then:
-      lambda: >-
-        ESP_LOGD("ota", "Got progress %f", x);
-  on_end:
-    then:
-      logger.log: OTA end
-  on_error:
-    then:
-      lambda: >-
-        ESP_LOGD("ota", "Got error code %d", x);
+  - platform: esphome
+    safe_mode: true
+    password: "superlongpasswordthatnoonewillknow"
+    port: 3286
+    reboot_timeout: 2min
+    num_attempts: 5
+    on_state_change:
+      then:
+        lambda: >-
+          ESP_LOGD("ota", "State %d", state);
+    on_begin:
+      then:
+        logger.log: OTA begin
+    on_progress:
+      then:
+        lambda: >-
+          ESP_LOGD("ota", "Got progress %f", x);
+    on_end:
+      then:
+        logger.log: OTA end
+    on_error:
+      then:
+        lambda: >-
+          ESP_LOGD("ota", "Got error code %d", x);
 
 logger:
   baud_rate: 0
diff --git a/tests/test11.5.yaml b/tests/test11.5.yaml
index ef260d79c0..e7eadbae8d 100644
--- a/tests/test11.5.yaml
+++ b/tests/test11.5.yaml
@@ -31,6 +31,7 @@ network:
 api:
 
 ota:
+  - platform: esphome
 
 logger:
 
diff --git a/tests/test2.yaml b/tests/test2.yaml
index 2fdef72c08..1cad026ed2 100644
--- a/tests/test2.yaml
+++ b/tests/test2.yaml
@@ -78,9 +78,10 @@ uart:
       - lambda: UARTDebug::log_hex(direction, bytes, ':');
 
 ota:
-  safe_mode: true
-  port: 3286
-  num_attempts: 15
+  - platform: esphome
+    safe_mode: true
+    port: 3286
+    num_attempts: 15
 
 logger:
   level: DEBUG
diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml
index 2bddd6f4d7..18d92289cd 100644
--- a/tests/test3.1.yaml
+++ b/tests/test3.1.yaml
@@ -49,7 +49,8 @@ spi:
     number: GPIO14
 
 ota:
-  version: 2
+  - platform: esphome
+    version: 2
 
 logger:
 
diff --git a/tests/test3.yaml b/tests/test3.yaml
index 61d814385b..7554d4bcb2 100644
--- a/tests/test3.yaml
+++ b/tests/test3.yaml
@@ -328,9 +328,10 @@ vbus:
   uart_id: uart_4
 
 ota:
-  safe_mode: true
-  port: 3286
-  reboot_timeout: 15min
+  - platform: esphome
+    safe_mode: true
+    port: 3286
+    reboot_timeout: 15min
 
 logger:
   hardware_uart: UART1
diff --git a/tests/test4.yaml b/tests/test4.yaml
index 993ce126a8..86beee81c6 100644
--- a/tests/test4.yaml
+++ b/tests/test4.yaml
@@ -103,8 +103,9 @@ uart:
     parity: EVEN
 
 ota:
-  safe_mode: true
-  port: 3286
+  - platform: esphome
+    safe_mode: true
+    port: 3286
 
 logger:
   level: DEBUG
diff --git a/tests/test5.yaml b/tests/test5.yaml
index 81615b24b0..914fa65c20 100644
--- a/tests/test5.yaml
+++ b/tests/test5.yaml
@@ -28,6 +28,7 @@ network:
 api:
 
 ota:
+  - platform: esphome
 
 logger:
 
diff --git a/tests/test6.yaml b/tests/test6.yaml
index 2c5aa30aad..b1103eb126 100644
--- a/tests/test6.yaml
+++ b/tests/test6.yaml
@@ -22,6 +22,7 @@ network:
 api:
 
 ota:
+  - platform: esphome
 
 logger:
 
diff --git a/tests/test9.1.yaml b/tests/test9.1.yaml
index f7455b7668..2d205ef4e6 100644
--- a/tests/test9.1.yaml
+++ b/tests/test9.1.yaml
@@ -12,6 +12,7 @@ esphome:
 logger:
 
 ota:
+  - platform: esphome
 
 captive_portal:
 
diff --git a/tests/test9.yaml b/tests/test9.yaml
index d660b4f24a..5017ccc5ed 100644
--- a/tests/test9.yaml
+++ b/tests/test9.yaml
@@ -12,6 +12,7 @@ esphome:
 logger:
 
 ota:
+  - platform: esphome
 
 captive_portal:
 

From 91198556f639bfba3d6a1fd5648218604f334829 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Thu, 11 Apr 2024 00:18:16 -0500
Subject: [PATCH 02/26] Move make_ota_backend into the OTA backend

---
 .../components/esphome/ota/ota_esphome.cpp    | 22 +------------------
 esphome/components/ota/ota_backend.h          |  2 ++
 .../ota/ota_backend_arduino_esp32.cpp         |  2 ++
 .../ota/ota_backend_arduino_esp8266.cpp       |  2 ++
 .../ota/ota_backend_arduino_libretiny.cpp     |  2 ++
 .../ota/ota_backend_arduino_rp2040.cpp        |  2 ++
 .../components/ota/ota_backend_esp_idf.cpp    |  2 ++
 7 files changed, 13 insertions(+), 21 deletions(-)

diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp
index 447f0587f8..5be1a5944c 100644
--- a/esphome/components/esphome/ota/ota_esphome.cpp
+++ b/esphome/components/esphome/ota/ota_esphome.cpp
@@ -24,26 +24,6 @@ static constexpr u_int16_t OTA_BLOCK_SIZE = 8192;
 
 OTAESPHomeComponent *global_ota_component = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
-std::unique_ptr<ota::OTABackend> make_ota_backend() {
-#ifdef USE_ARDUINO
-#ifdef USE_ESP8266
-  return make_unique<ota::ArduinoESP8266OTABackend>();
-#endif  // USE_ESP8266
-#ifdef USE_ESP32
-  return make_unique<ota::ArduinoESP32OTABackend>();
-#endif  // USE_ESP32
-#endif  // USE_ARDUINO
-#ifdef USE_ESP_IDF
-  return make_unique<ota::IDFOTABackend>();
-#endif  // USE_ESP_IDF
-#ifdef USE_RP2040
-  return make_unique<ota::ArduinoRP2040OTABackend>();
-#endif  // USE_RP2040
-#ifdef USE_LIBRETINY
-  return make_unique<ota::ArduinoLibreTinyOTABackend>();
-#endif
-}
-
 OTAESPHomeComponent::OTAESPHomeComponent() { global_ota_component = this; }
 
 void OTAESPHomeComponent::setup() {
@@ -172,7 +152,7 @@ void OTAESPHomeComponent::handle_() {
   buf[1] = USE_OTA_VERSION;
   this->writeall_(buf, 2);
 
-  backend = make_ota_backend();
+  backend = ota::make_ota_backend();
 
   // Read features - 1 byte
   if (!this->readall_(buf, 1)) {
diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h
index f7646dbdea..471eebb390 100644
--- a/esphome/components/ota/ota_backend.h
+++ b/esphome/components/ota/ota_backend.h
@@ -44,5 +44,7 @@ class OTABackend {
   virtual bool supports_compression() = 0;
 };
 
+std::unique_ptr<ota::OTABackend> make_ota_backend();
+
 }  // namespace ota
 }  // namespace esphome
diff --git a/esphome/components/ota/ota_backend_arduino_esp32.cpp b/esphome/components/ota/ota_backend_arduino_esp32.cpp
index 35d8ac9665..62c6a72388 100644
--- a/esphome/components/ota/ota_backend_arduino_esp32.cpp
+++ b/esphome/components/ota/ota_backend_arduino_esp32.cpp
@@ -9,6 +9,8 @@
 namespace esphome {
 namespace ota {
 
+std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoESP32OTABackend>(); }
+
 OTAResponseTypes ArduinoESP32OTABackend::begin(size_t image_size) {
   bool ret = Update.begin(image_size, U_FLASH);
   if (ret) {
diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.cpp b/esphome/components/ota/ota_backend_arduino_esp8266.cpp
index 068c0ffeaa..b317075bd0 100644
--- a/esphome/components/ota/ota_backend_arduino_esp8266.cpp
+++ b/esphome/components/ota/ota_backend_arduino_esp8266.cpp
@@ -11,6 +11,8 @@
 namespace esphome {
 namespace ota {
 
+std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoESP8266OTABackend>(); }
+
 OTAResponseTypes ArduinoESP8266OTABackend::begin(size_t image_size) {
   bool ret = Update.begin(image_size, U_FLASH);
   if (ret) {
diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.cpp b/esphome/components/ota/ota_backend_arduino_libretiny.cpp
index f845ec7466..df4e774ebc 100644
--- a/esphome/components/ota/ota_backend_arduino_libretiny.cpp
+++ b/esphome/components/ota/ota_backend_arduino_libretiny.cpp
@@ -9,6 +9,8 @@
 namespace esphome {
 namespace ota {
 
+std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoLibreTinyOTABackend>(); }
+
 OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) {
   bool ret = Update.begin(image_size, U_FLASH);
   if (ret) {
diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.cpp b/esphome/components/ota/ota_backend_arduino_rp2040.cpp
index 78935c5bfd..4448b0c95e 100644
--- a/esphome/components/ota/ota_backend_arduino_rp2040.cpp
+++ b/esphome/components/ota/ota_backend_arduino_rp2040.cpp
@@ -11,6 +11,8 @@
 namespace esphome {
 namespace ota {
 
+std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoRP2040OTABackend>(); }
+
 OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) {
   bool ret = Update.begin(image_size, U_FLASH);
   if (ret) {
diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp
index a551bb6b32..6f45fb75e4 100644
--- a/esphome/components/ota/ota_backend_esp_idf.cpp
+++ b/esphome/components/ota/ota_backend_esp_idf.cpp
@@ -14,6 +14,8 @@
 namespace esphome {
 namespace ota {
 
+std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::IDFOTABackend>(); }
+
 OTAResponseTypes IDFOTABackend::begin(size_t image_size) {
   this->partition_ = esp_ota_get_next_update_partition(nullptr);
   if (this->partition_ == nullptr) {

From a067693774ebd1c4db2c1cddee6d65f2a7117a05 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 15 Apr 2024 21:09:57 -0500
Subject: [PATCH 03/26] Update wizard.py

---
 esphome/wizard.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/esphome/wizard.py b/esphome/wizard.py
index 4ec366bbb9..9680ade044 100644
--- a/esphome/wizard.py
+++ b/esphome/wizard.py
@@ -153,10 +153,11 @@ def wizard_file(**kwargs):
 
     # Configure OTA
     config += "\nota:\n"
+    config += "  - platform: esphome\n"
     if "ota_password" in kwargs:
-        config += f"  password: \"{kwargs['ota_password']}\""
+        config += f"    password: \"{kwargs['ota_password']}\""
     elif "password" in kwargs:
-        config += f"  password: \"{kwargs['password']}\""
+        config += f"    password: \"{kwargs['password']}\""
 
     # Configuring wifi
     config += "\n\nwifi:\n"

From 05a940572cc3cd49456be0dcb3b2cd4b0c766425 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 17 Apr 2024 20:55:51 -0500
Subject: [PATCH 04/26] Remove `ota` from esphome.ota dependencies

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/esphome/ota/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py
index 3143d5408d..496261c5fc 100644
--- a/esphome/components/esphome/ota/__init__.py
+++ b/esphome/components/esphome/ota/__init__.py
@@ -19,7 +19,7 @@ from esphome.core import CORE, coroutine_with_priority
 
 CODEOWNERS = ["@esphome/core"]
 AUTO_LOAD = ["md5", "socket"]
-DEPENDENCIES = ["network", "ota"]
+DEPENDENCIES = ["network"]
 
 CONF_ON_BEGIN = "on_begin"
 CONF_ON_END = "on_end"

From cc8dd928548bd981385bcee6e6bd6ebf2f193d0a Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 17 Apr 2024 20:58:13 -0500
Subject: [PATCH 05/26] Rename class to `ESPHomeOTAComponent`

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/esphome/ota/ota_esphome.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h
index 19008d458e..593a5e9a6d 100644
--- a/esphome/components/esphome/ota/ota_esphome.h
+++ b/esphome/components/esphome/ota/ota_esphome.h
@@ -12,7 +12,7 @@ namespace ota_esphome {
 enum OTAESPHomeState { OTA_COMPLETED = 0, OTA_STARTED, OTA_IN_PROGRESS, OTA_ERROR };
 
 /// OTAESPHomeComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA.
-class OTAESPHomeComponent : public Component {
+class ESPHomeOTAComponent : public Component {
  public:
   OTAESPHomeComponent();
 #ifdef USE_OTA_PASSWORD

From af6dffd150212e0865ba90fbc8d0e294efe6802b Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 17 Apr 2024 23:12:30 -0500
Subject: [PATCH 06/26] Rename class, adjust namespace

---
 esphome/components/esphome/ota/__init__.py    | 40 +++++++-------
 esphome/components/esphome/ota/automation.h   |  2 -
 .../components/esphome/ota/ota_esphome.cpp    | 54 +++++++++----------
 esphome/components/esphome/ota/ota_esphome.h  |  8 ++-
 .../components/safe_mode/button/__init__.py   |  4 +-
 .../safe_mode/button/safe_mode_button.cpp     |  2 +-
 .../safe_mode/button/safe_mode_button.h       |  4 +-
 .../components/safe_mode/switch/__init__.py   |  4 +-
 .../safe_mode/switch/safe_mode_switch.cpp     |  2 +-
 .../safe_mode/switch/safe_mode_switch.h       |  4 +-
 10 files changed, 59 insertions(+), 65 deletions(-)

diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py
index 496261c5fc..d94e2ab921 100644
--- a/esphome/components/esphome/ota/__init__.py
+++ b/esphome/components/esphome/ota/__init__.py
@@ -27,31 +27,31 @@ CONF_ON_ERROR = "on_error"
 CONF_ON_PROGRESS = "on_progress"
 CONF_ON_STATE_CHANGE = "on_state_change"
 
-ota_esphome = cg.esphome_ns.namespace("ota_esphome")
+esphome = cg.esphome_ns.namespace("esphome")
 
-OTAESPHomeComponent = ota_esphome.class_("OTAESPHomeComponent", cg.Component)
-OTAESPHomeEndTrigger = ota_esphome.class_(
-    "OTAESPHomeEndTrigger", automation.Trigger.template()
+ESPHomeOTAComponent = esphome.class_("ESPHomeOTAComponent", cg.Component)
+ESPHomeOTAEndTrigger = esphome.class_(
+    "ESPHomeOTAEndTrigger", automation.Trigger.template()
 )
-OTAESPHomeErrorTrigger = ota_esphome.class_(
-    "OTAESPHomeErrorTrigger", automation.Trigger.template()
+ESPHomeOTAErrorTrigger = esphome.class_(
+    "ESPHomeOTAErrorTrigger", automation.Trigger.template()
 )
-OTAESPHomeProgressTrigger = ota_esphome.class_(
-    "OTAESPHomeProgressTrigger", automation.Trigger.template()
+ESPHomeOTAProgressTrigger = esphome.class_(
+    "ESPHomeOTAProgressTrigger", automation.Trigger.template()
 )
-OTAESPHomeStartTrigger = ota_esphome.class_(
-    "OTAESPHomeStartTrigger", automation.Trigger.template()
+ESPHomeOTAStartTrigger = esphome.class_(
+    "ESPHomeOTAStartTrigger", automation.Trigger.template()
 )
-OTAESPHomeStateChangeTrigger = ota_esphome.class_(
-    "OTAESPHomeStateChangeTrigger", automation.Trigger.template()
+ESPHomeOTAStateChangeTrigger = esphome.class_(
+    "ESPHomeOTAStateChangeTrigger", automation.Trigger.template()
 )
 
-OTAESPHomeState = ota_esphome.enum("OTAESPHomeState")
+ESPHomeOTAState = esphome.enum("ESPHomeOTAState")
 
 
 CONFIG_SCHEMA = cv.Schema(
     {
-        cv.GenerateID(): cv.declare_id(OTAESPHomeComponent),
+        cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent),
         cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean,
         cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, int=True),
         cv.SplitDefault(
@@ -70,29 +70,29 @@ CONFIG_SCHEMA = cv.Schema(
         cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation(
             {
                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
-                    OTAESPHomeStateChangeTrigger
+                    ESPHomeOTAStateChangeTrigger
                 ),
             }
         ),
         cv.Optional(CONF_ON_BEGIN): automation.validate_automation(
             {
-                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAESPHomeStartTrigger),
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPHomeOTAStartTrigger),
             }
         ),
         cv.Optional(CONF_ON_END): automation.validate_automation(
             {
-                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAESPHomeEndTrigger),
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPHomeOTAEndTrigger),
             }
         ),
         cv.Optional(CONF_ON_ERROR): automation.validate_automation(
             {
-                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAESPHomeErrorTrigger),
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPHomeOTAErrorTrigger),
             }
         ),
         cv.Optional(CONF_ON_PROGRESS): automation.validate_automation(
             {
                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
-                    OTAESPHomeProgressTrigger
+                    ESPHomeOTAProgressTrigger
                 ),
             }
         ),
@@ -130,7 +130,7 @@ async def to_code(config):
     use_state_callback = False
     for conf in config.get(CONF_ON_STATE_CHANGE, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        await automation.build_automation(trigger, [(OTAESPHomeState, "state")], conf)
+        await automation.build_automation(trigger, [(ESPHomeOTAState, "state")], conf)
         use_state_callback = True
     for conf in config.get(CONF_ON_BEGIN, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
diff --git a/esphome/components/esphome/ota/automation.h b/esphome/components/esphome/ota/automation.h
index a23b1c5590..b922b6d77b 100644
--- a/esphome/components/esphome/ota/automation.h
+++ b/esphome/components/esphome/ota/automation.h
@@ -8,7 +8,6 @@
 #include "esphome/core/automation.h"
 
 namespace esphome {
-namespace ota_esphome {
 
 class OTAESPHomeStateChangeTrigger : public Trigger<OTAESPHomeState> {
  public:
@@ -65,7 +64,6 @@ class OTAESPHomeErrorTrigger : public Trigger<uint8_t> {
   }
 };
 
-}  // namespace ota_esphome
 }  // namespace esphome
 
 #endif  // USE_OTA_STATE_CALLBACK
diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp
index 5be1a5944c..1fa3052e78 100644
--- a/esphome/components/esphome/ota/ota_esphome.cpp
+++ b/esphome/components/esphome/ota/ota_esphome.cpp
@@ -17,16 +17,15 @@
 #include <cstdio>
 
 namespace esphome {
-namespace ota_esphome {
 
 static const char *const TAG = "esphome.ota";
 static constexpr u_int16_t OTA_BLOCK_SIZE = 8192;
 
-OTAESPHomeComponent *global_ota_component = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+ESPHomeOTAComponent *global_ota_component = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
-OTAESPHomeComponent::OTAESPHomeComponent() { global_ota_component = this; }
+ESPHomeOTAComponent::ESPHomeOTAComponent() { global_ota_component = this; }
 
-void OTAESPHomeComponent::setup() {
+void ESPHomeOTAComponent::setup() {
   server_ = socket::socket_ip(SOCK_STREAM, 0);
   if (server_ == nullptr) {
     ESP_LOGW(TAG, "Could not create socket");
@@ -70,7 +69,7 @@ void OTAESPHomeComponent::setup() {
   }
 }
 
-void OTAESPHomeComponent::dump_config() {
+void ESPHomeOTAComponent::dump_config() {
   ESP_LOGCONFIG(TAG, "Over-The-Air updates:");
   ESP_LOGCONFIG(TAG, "  Address: %s:%u", network::get_use_address().c_str(), this->port_);
   ESP_LOGCONFIG(TAG, "  OTA version: %d", USE_OTA_VERSION);
@@ -80,13 +79,13 @@ void OTAESPHomeComponent::dump_config() {
   }
 #endif
   if (this->has_safe_mode_ && this->safe_mode_rtc_value_ > 1 &&
-      this->safe_mode_rtc_value_ != OTAESPHomeComponent::ENTER_SAFE_MODE_MAGIC) {
+      this->safe_mode_rtc_value_ != ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC) {
     ESP_LOGW(TAG, "Last reset occurred too quickly; safe mode will be invoked in %" PRIu32 " restarts",
              this->safe_mode_num_attempts_ - this->safe_mode_rtc_value_);
   }
 }
 
-void OTAESPHomeComponent::loop() {
+void ESPHomeOTAComponent::loop() {
   this->handle_();
 
   if (this->has_safe_mode_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_enable_time_) {
@@ -99,7 +98,7 @@ void OTAESPHomeComponent::loop() {
 
 static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01;
 
-void OTAESPHomeComponent::handle_() {
+void ESPHomeOTAComponent::handle_() {
   ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
   bool update_started = false;
   size_t total = 0;
@@ -362,7 +361,7 @@ error:
 #endif
 }
 
-bool OTAESPHomeComponent::readall_(uint8_t *buf, size_t len) {
+bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
   uint32_t start = millis();
   uint32_t at = 0;
   while (len - at > 0) {
@@ -393,7 +392,7 @@ bool OTAESPHomeComponent::readall_(uint8_t *buf, size_t len) {
 
   return true;
 }
-bool OTAESPHomeComponent::writeall_(const uint8_t *buf, size_t len) {
+bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
   uint32_t start = millis();
   uint32_t at = 0;
   while (len - at > 0) {
@@ -421,31 +420,31 @@ bool OTAESPHomeComponent::writeall_(const uint8_t *buf, size_t len) {
   return true;
 }
 
-float OTAESPHomeComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
-uint16_t OTAESPHomeComponent::get_port() const { return this->port_; }
-void OTAESPHomeComponent::set_port(uint16_t port) { this->port_ = port; }
+float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
+uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; }
+void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; }
 
-void OTAESPHomeComponent::set_safe_mode_pending(const bool &pending) {
+void ESPHomeOTAComponent::set_safe_mode_pending(const bool &pending) {
   if (!this->has_safe_mode_)
     return;
 
   uint32_t current_rtc = this->read_rtc_();
 
-  if (pending && current_rtc != OTAESPHomeComponent::ENTER_SAFE_MODE_MAGIC) {
+  if (pending && current_rtc != ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC) {
     ESP_LOGI(TAG, "Device will enter safe mode on next boot");
-    this->write_rtc_(OTAESPHomeComponent::ENTER_SAFE_MODE_MAGIC);
+    this->write_rtc_(ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC);
   }
 
-  if (!pending && current_rtc == OTAESPHomeComponent::ENTER_SAFE_MODE_MAGIC) {
+  if (!pending && current_rtc == ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC) {
     ESP_LOGI(TAG, "Safe mode pending has been cleared");
     this->clean_rtc();
   }
 }
-bool OTAESPHomeComponent::get_safe_mode_pending() {
-  return this->has_safe_mode_ && this->read_rtc_() == OTAESPHomeComponent::ENTER_SAFE_MODE_MAGIC;
+bool ESPHomeOTAComponent::get_safe_mode_pending() {
+  return this->has_safe_mode_ && this->read_rtc_() == ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC;
 }
 
-bool OTAESPHomeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time) {
+bool ESPHomeOTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time) {
   this->has_safe_mode_ = true;
   this->safe_mode_start_time_ = millis();
   this->safe_mode_enable_time_ = enable_time;
@@ -453,7 +452,7 @@ bool OTAESPHomeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t
   this->rtc_ = global_preferences->make_preference<uint32_t>(233825507UL, false);
   this->safe_mode_rtc_value_ = this->read_rtc_();
 
-  bool is_manual_safe_mode = this->safe_mode_rtc_value_ == OTAESPHomeComponent::ENTER_SAFE_MODE_MAGIC;
+  bool is_manual_safe_mode = this->safe_mode_rtc_value_ == ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC;
 
   if (is_manual_safe_mode) {
     ESP_LOGI(TAG, "Safe mode has been entered manually");
@@ -487,27 +486,26 @@ bool OTAESPHomeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t
     return false;
   }
 }
-void OTAESPHomeComponent::write_rtc_(uint32_t val) {
+void ESPHomeOTAComponent::write_rtc_(uint32_t val) {
   this->rtc_.save(&val);
   global_preferences->sync();
 }
-uint32_t OTAESPHomeComponent::read_rtc_() {
+uint32_t ESPHomeOTAComponent::read_rtc_() {
   uint32_t val;
   if (!this->rtc_.load(&val))
     return 0;
   return val;
 }
-void OTAESPHomeComponent::clean_rtc() { this->write_rtc_(0); }
-void OTAESPHomeComponent::on_safe_shutdown() {
-  if (this->has_safe_mode_ && this->read_rtc_() != OTAESPHomeComponent::ENTER_SAFE_MODE_MAGIC)
+void ESPHomeOTAComponent::clean_rtc() { this->write_rtc_(0); }
+void ESPHomeOTAComponent::on_safe_shutdown() {
+  if (this->has_safe_mode_ && this->read_rtc_() != ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC)
     this->clean_rtc();
 }
 
 #ifdef USE_OTA_STATE_CALLBACK
-void OTAESPHomeComponent::add_on_state_callback(std::function<void(OTAESPHomeState, float, uint8_t)> &&callback) {
+void ESPHomeOTAComponent::add_on_state_callback(std::function<void(OTAESPHomeState, float, uint8_t)> &&callback) {
   this->state_callback_.add(std::move(callback));
 }
 #endif
 
-}  // namespace ota_esphome
 }  // namespace esphome
diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h
index 593a5e9a6d..bbe3173ade 100644
--- a/esphome/components/esphome/ota/ota_esphome.h
+++ b/esphome/components/esphome/ota/ota_esphome.h
@@ -7,14 +7,13 @@
 #include "esphome/core/preferences.h"
 
 namespace esphome {
-namespace ota_esphome {
 
 enum OTAESPHomeState { OTA_COMPLETED = 0, OTA_STARTED, OTA_IN_PROGRESS, OTA_ERROR };
 
-/// OTAESPHomeComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA.
+/// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA.
 class ESPHomeOTAComponent : public Component {
  public:
-  OTAESPHomeComponent();
+  ESPHomeOTAComponent();
 #ifdef USE_OTA_PASSWORD
   void set_auth_password(const std::string &password) { password_ = password; }
 #endif  // USE_OTA_PASSWORD
@@ -77,7 +76,6 @@ class ESPHomeOTAComponent : public Component {
 #endif
 };
 
-extern OTAESPHomeComponent *global_ota_component;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+extern ESPHomeOTAComponent *global_ota_component;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
-}  // namespace ota_esphome
 }  // namespace esphome
diff --git a/esphome/components/safe_mode/button/__init__.py b/esphome/components/safe_mode/button/__init__.py
index ffd2b4e6ee..68aecdbc53 100644
--- a/esphome/components/safe_mode/button/__init__.py
+++ b/esphome/components/safe_mode/button/__init__.py
@@ -1,7 +1,7 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import button
-from esphome.components.esphome.ota import OTAESPHomeComponent
+from esphome.components.esphome.ota import ESPHomeOTAComponent
 from esphome.const import (
     CONF_ESPHOME,
     DEVICE_CLASS_RESTART,
@@ -21,7 +21,7 @@ CONFIG_SCHEMA = (
         entity_category=ENTITY_CATEGORY_CONFIG,
         icon=ICON_RESTART_ALERT,
     )
-    .extend({cv.GenerateID(CONF_ESPHOME): cv.use_id(OTAESPHomeComponent)})
+    .extend({cv.GenerateID(CONF_ESPHOME): cv.use_id(ESPHomeOTAComponent)})
     .extend(cv.COMPONENT_SCHEMA)
 )
 
diff --git a/esphome/components/safe_mode/button/safe_mode_button.cpp b/esphome/components/safe_mode/button/safe_mode_button.cpp
index 673af02c20..d513b79c12 100644
--- a/esphome/components/safe_mode/button/safe_mode_button.cpp
+++ b/esphome/components/safe_mode/button/safe_mode_button.cpp
@@ -8,7 +8,7 @@ namespace safe_mode {
 
 static const char *const TAG = "safe_mode.button";
 
-void SafeModeButton::set_ota(ota_esphome::OTAESPHomeComponent *ota) { this->ota_ = ota; }
+void SafeModeButton::set_ota(esphome::ESPHomeOTAComponent *ota) { this->ota_ = ota; }
 
 void SafeModeButton::press_action() {
   ESP_LOGI(TAG, "Restarting device in safe mode...");
diff --git a/esphome/components/safe_mode/button/safe_mode_button.h b/esphome/components/safe_mode/button/safe_mode_button.h
index 4ac0d2d177..a306735b7f 100644
--- a/esphome/components/safe_mode/button/safe_mode_button.h
+++ b/esphome/components/safe_mode/button/safe_mode_button.h
@@ -10,10 +10,10 @@ namespace safe_mode {
 class SafeModeButton : public button::Button, public Component {
  public:
   void dump_config() override;
-  void set_ota(ota_esphome::OTAESPHomeComponent *ota);
+  void set_ota(esphome::ESPHomeOTAComponent *ota);
 
  protected:
-  ota_esphome::OTAESPHomeComponent *ota_;
+  esphome::ESPHomeOTAComponent *ota_;
   void press_action() override;
 };
 
diff --git a/esphome/components/safe_mode/switch/__init__.py b/esphome/components/safe_mode/switch/__init__.py
index c576cc7403..970e97ae52 100644
--- a/esphome/components/safe_mode/switch/__init__.py
+++ b/esphome/components/safe_mode/switch/__init__.py
@@ -1,7 +1,7 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import switch
-from esphome.components.esphome.ota import OTAESPHomeComponent
+from esphome.components.esphome.ota import ESPHomeOTAComponent
 from esphome.const import (
     CONF_ESPHOME,
     ENTITY_CATEGORY_CONFIG,
@@ -20,7 +20,7 @@ CONFIG_SCHEMA = (
         entity_category=ENTITY_CATEGORY_CONFIG,
         icon=ICON_RESTART_ALERT,
     )
-    .extend({cv.GenerateID(CONF_ESPHOME): cv.use_id(OTAESPHomeComponent)})
+    .extend({cv.GenerateID(CONF_ESPHOME): cv.use_id(ESPHomeOTAComponent)})
     .extend(cv.COMPONENT_SCHEMA)
 )
 
diff --git a/esphome/components/safe_mode/switch/safe_mode_switch.cpp b/esphome/components/safe_mode/switch/safe_mode_switch.cpp
index a145a71386..71408df140 100644
--- a/esphome/components/safe_mode/switch/safe_mode_switch.cpp
+++ b/esphome/components/safe_mode/switch/safe_mode_switch.cpp
@@ -8,7 +8,7 @@ namespace safe_mode {
 
 static const char *const TAG = "safe_mode_switch";
 
-void SafeModeSwitch::set_ota(ota_esphome::OTAESPHomeComponent *ota) { this->ota_ = ota; }
+void SafeModeSwitch::set_ota(esphome::ESPHomeOTAComponent *ota) { this->ota_ = ota; }
 
 void SafeModeSwitch::write_state(bool state) {
   // Acknowledge
diff --git a/esphome/components/safe_mode/switch/safe_mode_switch.h b/esphome/components/safe_mode/switch/safe_mode_switch.h
index 7fc8927c80..5bd15a44de 100644
--- a/esphome/components/safe_mode/switch/safe_mode_switch.h
+++ b/esphome/components/safe_mode/switch/safe_mode_switch.h
@@ -10,10 +10,10 @@ namespace safe_mode {
 class SafeModeSwitch : public switch_::Switch, public Component {
  public:
   void dump_config() override;
-  void set_ota(ota_esphome::OTAESPHomeComponent *ota);
+  void set_ota(esphome::ESPHomeOTAComponent *ota);
 
  protected:
-  ota_esphome::OTAESPHomeComponent *ota_;
+  esphome::ESPHomeOTAComponent *ota_;
   void write_state(bool state) override;
 };
 

From 8426084ebfe523066855c20dcd636b0dfb04bc5d Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Sun, 21 Apr 2024 04:14:07 -0500
Subject: [PATCH 07/26] Refactor more bits into the base `ota` component

---
 esphome/components/esphome/ota/__init__.py    | 112 +++++-------------
 esphome/components/esphome/ota/automation.h   |  69 -----------
 .../components/esphome/ota/ota_esphome.cpp    |  19 ++-
 esphome/components/esphome/ota/ota_esphome.h  |  16 +--
 esphome/components/ota/__init__.py            |  52 +++++++-
 esphome/components/ota/ota_backend.h          |  81 +++++++++++++
 6 files changed, 175 insertions(+), 174 deletions(-)
 delete mode 100644 esphome/components/esphome/ota/automation.h

diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py
index d94e2ab921..e6f99ba18a 100644
--- a/esphome/components/esphome/ota/__init__.py
+++ b/esphome/components/esphome/ota/__init__.py
@@ -2,6 +2,7 @@ from esphome.cpp_generator import RawExpression
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import automation
+from esphome.components import ota
 from esphome.const import (
     CONF_ID,
     CONF_NUM_ATTEMPTS,
@@ -21,83 +22,34 @@ CODEOWNERS = ["@esphome/core"]
 AUTO_LOAD = ["md5", "socket"]
 DEPENDENCIES = ["network"]
 
-CONF_ON_BEGIN = "on_begin"
-CONF_ON_END = "on_end"
-CONF_ON_ERROR = "on_error"
-CONF_ON_PROGRESS = "on_progress"
-CONF_ON_STATE_CHANGE = "on_state_change"
-
 esphome = cg.esphome_ns.namespace("esphome")
-
-ESPHomeOTAComponent = esphome.class_("ESPHomeOTAComponent", cg.Component)
-ESPHomeOTAEndTrigger = esphome.class_(
-    "ESPHomeOTAEndTrigger", automation.Trigger.template()
-)
-ESPHomeOTAErrorTrigger = esphome.class_(
-    "ESPHomeOTAErrorTrigger", automation.Trigger.template()
-)
-ESPHomeOTAProgressTrigger = esphome.class_(
-    "ESPHomeOTAProgressTrigger", automation.Trigger.template()
-)
-ESPHomeOTAStartTrigger = esphome.class_(
-    "ESPHomeOTAStartTrigger", automation.Trigger.template()
-)
-ESPHomeOTAStateChangeTrigger = esphome.class_(
-    "ESPHomeOTAStateChangeTrigger", automation.Trigger.template()
-)
-
-ESPHomeOTAState = esphome.enum("ESPHomeOTAState")
+ESPHomeOTAComponent = esphome.class_("ESPHomeOTAComponent", ota.OTAComponent)
 
 
-CONFIG_SCHEMA = cv.Schema(
-    {
-        cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent),
-        cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean,
-        cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, int=True),
-        cv.SplitDefault(
-            CONF_PORT,
-            esp8266=8266,
-            esp32=3232,
-            rp2040=2040,
-            bk72xx=8892,
-            rtl87xx=8892,
-        ): cv.port,
-        cv.Optional(CONF_PASSWORD): cv.string,
-        cv.Optional(
-            CONF_REBOOT_TIMEOUT, default="5min"
-        ): cv.positive_time_period_milliseconds,
-        cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int,
-        cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation(
-            {
-                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
-                    ESPHomeOTAStateChangeTrigger
-                ),
-            }
-        ),
-        cv.Optional(CONF_ON_BEGIN): automation.validate_automation(
-            {
-                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPHomeOTAStartTrigger),
-            }
-        ),
-        cv.Optional(CONF_ON_END): automation.validate_automation(
-            {
-                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPHomeOTAEndTrigger),
-            }
-        ),
-        cv.Optional(CONF_ON_ERROR): automation.validate_automation(
-            {
-                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPHomeOTAErrorTrigger),
-            }
-        ),
-        cv.Optional(CONF_ON_PROGRESS): automation.validate_automation(
-            {
-                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
-                    ESPHomeOTAProgressTrigger
-                ),
-            }
-        ),
-    }
-).extend(cv.COMPONENT_SCHEMA)
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent),
+            cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean,
+            cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, int=True),
+            cv.SplitDefault(
+                CONF_PORT,
+                esp8266=8266,
+                esp32=3232,
+                rp2040=2040,
+                bk72xx=8892,
+                rtl87xx=8892,
+            ): cv.port,
+            cv.Optional(CONF_PASSWORD): cv.string,
+            cv.Optional(
+                CONF_REBOOT_TIMEOUT, default="5min"
+            ): cv.positive_time_period_milliseconds,
+            cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int,
+        }
+    )
+    .extend(ota.BASE_OTA_SCHEMA)
+    .extend(cv.COMPONENT_SCHEMA)
+)
 
 
 @coroutine_with_priority(50.0)
@@ -128,23 +80,23 @@ async def to_code(config):
         cg.add_library("Updater", None)
 
     use_state_callback = False
-    for conf in config.get(CONF_ON_STATE_CHANGE, []):
+    for conf in config.get(ota.CONF_ON_STATE_CHANGE, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        await automation.build_automation(trigger, [(ESPHomeOTAState, "state")], conf)
+        await automation.build_automation(trigger, [(ota.OTAState, "state")], conf)
         use_state_callback = True
-    for conf in config.get(CONF_ON_BEGIN, []):
+    for conf in config.get(ota.CONF_ON_BEGIN, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
         await automation.build_automation(trigger, [], conf)
         use_state_callback = True
-    for conf in config.get(CONF_ON_PROGRESS, []):
+    for conf in config.get(ota.CONF_ON_PROGRESS, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
         await automation.build_automation(trigger, [(float, "x")], conf)
         use_state_callback = True
-    for conf in config.get(CONF_ON_END, []):
+    for conf in config.get(ota.CONF_ON_END, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
         await automation.build_automation(trigger, [], conf)
         use_state_callback = True
-    for conf in config.get(CONF_ON_ERROR, []):
+    for conf in config.get(ota.CONF_ON_ERROR, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
         await automation.build_automation(trigger, [(cg.uint8, "x")], conf)
         use_state_callback = True
diff --git a/esphome/components/esphome/ota/automation.h b/esphome/components/esphome/ota/automation.h
deleted file mode 100644
index b922b6d77b..0000000000
--- a/esphome/components/esphome/ota/automation.h
+++ /dev/null
@@ -1,69 +0,0 @@
-#pragma once
-
-#ifdef USE_OTA_STATE_CALLBACK
-#include "ota_esphome.h"
-
-#include "esphome/core/component.h"
-#include "esphome/core/defines.h"
-#include "esphome/core/automation.h"
-
-namespace esphome {
-
-class OTAESPHomeStateChangeTrigger : public Trigger<OTAESPHomeState> {
- public:
-  explicit OTAESPHomeStateChangeTrigger(OTAESPHomeComponent *parent) {
-    parent->add_on_state_callback([this, parent](OTAESPHomeState state, float progress, uint8_t error) {
-      if (!parent->is_failed()) {
-        return trigger(state);
-      }
-    });
-  }
-};
-
-class OTAESPHomeStartTrigger : public Trigger<> {
- public:
-  explicit OTAESPHomeStartTrigger(OTAESPHomeComponent *parent) {
-    parent->add_on_state_callback([this, parent](OTAESPHomeState state, float progress, uint8_t error) {
-      if (state == OTA_STARTED && !parent->is_failed()) {
-        trigger();
-      }
-    });
-  }
-};
-
-class OTAESPHomeProgressTrigger : public Trigger<float> {
- public:
-  explicit OTAESPHomeProgressTrigger(OTAESPHomeComponent *parent) {
-    parent->add_on_state_callback([this, parent](OTAESPHomeState state, float progress, uint8_t error) {
-      if (state == OTA_IN_PROGRESS && !parent->is_failed()) {
-        trigger(progress);
-      }
-    });
-  }
-};
-
-class OTAESPHomeEndTrigger : public Trigger<> {
- public:
-  explicit OTAESPHomeEndTrigger(OTAESPHomeComponent *parent) {
-    parent->add_on_state_callback([this, parent](OTAESPHomeState state, float progress, uint8_t error) {
-      if (state == OTA_COMPLETED && !parent->is_failed()) {
-        trigger();
-      }
-    });
-  }
-};
-
-class OTAESPHomeErrorTrigger : public Trigger<uint8_t> {
- public:
-  explicit OTAESPHomeErrorTrigger(OTAESPHomeComponent *parent) {
-    parent->add_on_state_callback([this, parent](OTAESPHomeState state, float progress, uint8_t error) {
-      if (state == OTA_ERROR && !parent->is_failed()) {
-        trigger(error);
-      }
-    });
-  }
-};
-
-}  // namespace esphome
-
-#endif  // USE_OTA_STATE_CALLBACK
diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp
index 1fa3052e78..7b76380fb4 100644
--- a/esphome/components/esphome/ota/ota_esphome.cpp
+++ b/esphome/components/esphome/ota/ota_esphome.cpp
@@ -131,7 +131,7 @@ void ESPHomeOTAComponent::handle_() {
   ESP_LOGD(TAG, "Starting OTA update from %s...", this->client_->getpeername().c_str());
   this->status_set_warning();
 #ifdef USE_OTA_STATE_CALLBACK
-  this->state_callback_.call(OTA_STARTED, 0.0f, 0);
+  this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0);
 #endif
 
   if (!this->readall_(buf, 5)) {
@@ -306,7 +306,7 @@ void ESPHomeOTAComponent::handle_() {
       float percentage = (total * 100.0f) / ota_size;
       ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
 #ifdef USE_OTA_STATE_CALLBACK
-      this->state_callback_.call(OTA_IN_PROGRESS, percentage, 0);
+      this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0);
 #endif
       // feed watchdog and give other tasks a chance to run
       App.feed_wdt();
@@ -340,7 +340,7 @@ void ESPHomeOTAComponent::handle_() {
   ESP_LOGI(TAG, "OTA update finished");
   this->status_clear_warning();
 #ifdef USE_OTA_STATE_CALLBACK
-  this->state_callback_.call(OTA_COMPLETED, 100.0f, 0);
+  this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, 0);
 #endif
   delay(100);  // NOLINT
   App.safe_reboot();
@@ -357,7 +357,7 @@ error:
 
   this->status_momentary_error("onerror", 5000);
 #ifdef USE_OTA_STATE_CALLBACK
-  this->state_callback_.call(OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
+  this->state_callback_.call(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
 #endif
 }
 
@@ -486,26 +486,23 @@ bool ESPHomeOTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t
     return false;
   }
 }
+
 void ESPHomeOTAComponent::write_rtc_(uint32_t val) {
   this->rtc_.save(&val);
   global_preferences->sync();
 }
+
 uint32_t ESPHomeOTAComponent::read_rtc_() {
   uint32_t val;
   if (!this->rtc_.load(&val))
     return 0;
   return val;
 }
+
 void ESPHomeOTAComponent::clean_rtc() { this->write_rtc_(0); }
+
 void ESPHomeOTAComponent::on_safe_shutdown() {
   if (this->has_safe_mode_ && this->read_rtc_() != ESPHomeOTAComponent::ENTER_SAFE_MODE_MAGIC)
     this->clean_rtc();
 }
-
-#ifdef USE_OTA_STATE_CALLBACK
-void ESPHomeOTAComponent::add_on_state_callback(std::function<void(OTAESPHomeState, float, uint8_t)> &&callback) {
-  this->state_callback_.add(std::move(callback));
-}
-#endif
-
 }  // namespace esphome
diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h
index bbe3173ade..f230f2b465 100644
--- a/esphome/components/esphome/ota/ota_esphome.h
+++ b/esphome/components/esphome/ota/ota_esphome.h
@@ -1,17 +1,15 @@
 #pragma once
 
-#include "esphome/components/socket/socket.h"
-#include "esphome/core/component.h"
 #include "esphome/core/defines.h"
 #include "esphome/core/helpers.h"
 #include "esphome/core/preferences.h"
+#include "esphome/components/ota/ota_backend.h"
+#include "esphome/components/socket/socket.h"
 
 namespace esphome {
 
-enum OTAESPHomeState { OTA_COMPLETED = 0, OTA_STARTED, OTA_IN_PROGRESS, OTA_ERROR };
-
 /// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA.
-class ESPHomeOTAComponent : public Component {
+class ESPHomeOTAComponent : public ota::OTAComponent {
  public:
   ESPHomeOTAComponent();
 #ifdef USE_OTA_PASSWORD
@@ -27,10 +25,6 @@ class ESPHomeOTAComponent : public Component {
   void set_safe_mode_pending(const bool &pending);
   bool get_safe_mode_pending();
 
-#ifdef USE_OTA_STATE_CALLBACK
-  void add_on_state_callback(std::function<void(OTAESPHomeState, float, uint8_t)> &&callback);
-#endif
-
   // ========== INTERNAL METHODS ==========
   // (In most use cases you won't need these)
   void setup() override;
@@ -70,10 +64,6 @@ class ESPHomeOTAComponent : public Component {
 
   static const uint32_t ENTER_SAFE_MODE_MAGIC =
       0x5afe5afe;  ///< a magic number to indicate that safe mode should be entered on next boot
-
-#ifdef USE_OTA_STATE_CALLBACK
-  CallbackManager<void(OTAESPHomeState, float, uint8_t)> state_callback_{};
-#endif
 };
 
 extern ESPHomeOTAComponent *global_ota_component;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py
index aed0b87f85..719f25082f 100644
--- a/esphome/components/ota/__init__.py
+++ b/esphome/components/ota/__init__.py
@@ -1,6 +1,8 @@
+import esphome.codegen as cg
 import esphome.config_validation as cv
+from esphome import automation
 
-from esphome.const import CONF_ESPHOME, CONF_OTA, CONF_PLATFORM
+from esphome.const import CONF_ESPHOME, CONF_OTA, CONF_PLATFORM, CONF_TRIGGER_ID
 
 CODEOWNERS = ["@esphome/core"]
 AUTO_LOAD = ["md5"]
@@ -8,6 +10,24 @@ DEPENDENCIES = ["network"]
 
 IS_PLATFORM_COMPONENT = True
 
+CONF_ON_BEGIN = "on_begin"
+CONF_ON_END = "on_end"
+CONF_ON_ERROR = "on_error"
+CONF_ON_PROGRESS = "on_progress"
+CONF_ON_STATE_CHANGE = "on_state_change"
+
+
+ota = cg.esphome_ns.namespace("ota")
+OTAComponent = ota.class_("OTAComponent", cg.Component)
+OTAState = ota.enum("OTAState")
+OTAEndTrigger = ota.class_("OTAEndTrigger", automation.Trigger.template())
+OTAErrorTrigger = ota.class_("OTAErrorTrigger", automation.Trigger.template())
+OTAProgressTrigger = ota.class_("OTAProgressTrigger", automation.Trigger.template())
+OTAStartTrigger = ota.class_("OTAStartTrigger", automation.Trigger.template())
+OTAStateChangeTrigger = ota.class_(
+    "OTAStateChangeTrigger", automation.Trigger.template()
+)
+
 
 def _ota_final_validate(config):
     if len(config) < 1:
@@ -17,3 +37,33 @@ def _ota_final_validate(config):
 
 
 FINAL_VALIDATE_SCHEMA = _ota_final_validate
+
+BASE_OTA_SCHEMA = cv.Schema(
+    {
+        cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAStateChangeTrigger),
+            }
+        ),
+        cv.Optional(CONF_ON_BEGIN): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAStartTrigger),
+            }
+        ),
+        cv.Optional(CONF_ON_END): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAEndTrigger),
+            }
+        ),
+        cv.Optional(CONF_ON_ERROR): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAErrorTrigger),
+            }
+        ),
+        cv.Optional(CONF_ON_PROGRESS): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAProgressTrigger),
+            }
+        ),
+    }
+)
diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h
index 471eebb390..9f2a851f5a 100644
--- a/esphome/components/ota/ota_backend.h
+++ b/esphome/components/ota/ota_backend.h
@@ -1,4 +1,10 @@
 #pragma once
+#ifdef USE_OTA_STATE_CALLBACK
+#include "esphome/core/automation.h"
+#include "esphome/core/defines.h"
+#endif
+
+#include "esphome/core/component.h"
 #include "esphome/core/helpers.h"
 
 namespace esphome {
@@ -33,6 +39,8 @@ enum OTAResponseTypes {
   OTA_RESPONSE_ERROR_UNKNOWN = 0xFF,
 };
 
+enum OTAState { OTA_COMPLETED = 0, OTA_STARTED, OTA_IN_PROGRESS, OTA_ERROR };
+
 class OTABackend {
  public:
   virtual ~OTABackend() = default;
@@ -44,7 +52,80 @@ class OTABackend {
   virtual bool supports_compression() = 0;
 };
 
+class OTAComponent : public Component {
+#ifdef USE_OTA_STATE_CALLBACK
+ public:
+  void add_on_state_callback(std::function<void(ota::OTAState, float, uint8_t)> &&callback) {
+    this->state_callback_.add(std::move(callback));
+  }
+
+ protected:
+  CallbackManager<void(ota::OTAState, float, uint8_t)> state_callback_{};
+#endif
+};
+
 std::unique_ptr<ota::OTABackend> make_ota_backend();
 
+///
+/// Automations
+///
+
+#ifdef USE_OTA_STATE_CALLBACK
+class OTAStateChangeTrigger : public Trigger<OTAState> {
+ public:
+  explicit OTAStateChangeTrigger(OTAComponent *parent) {
+    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
+      if (!parent->is_failed()) {
+        return trigger(state);
+      }
+    });
+  }
+};
+
+class OTAStartTrigger : public Trigger<> {
+ public:
+  explicit OTAStartTrigger(OTAComponent *parent) {
+    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
+      if (state == OTA_STARTED && !parent->is_failed()) {
+        trigger();
+      }
+    });
+  }
+};
+
+class OTAProgressTrigger : public Trigger<float> {
+ public:
+  explicit OTAProgressTrigger(OTAComponent *parent) {
+    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
+      if (state == OTA_IN_PROGRESS && !parent->is_failed()) {
+        trigger(progress);
+      }
+    });
+  }
+};
+
+class OTAEndTrigger : public Trigger<> {
+ public:
+  explicit OTAEndTrigger(OTAComponent *parent) {
+    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
+      if (state == OTA_COMPLETED && !parent->is_failed()) {
+        trigger();
+      }
+    });
+  }
+};
+
+class OTAErrorTrigger : public Trigger<uint8_t> {
+ public:
+  explicit OTAErrorTrigger(OTAComponent *parent) {
+    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
+      if (state == OTA_ERROR && !parent->is_failed()) {
+        trigger(error);
+      }
+    });
+  }
+};
+#endif
+
 }  // namespace ota
 }  // namespace esphome

From 17f9774418d7ea8ad148df3ae2cb726183a9c852 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Sun, 21 Apr 2024 04:25:17 -0500
Subject: [PATCH 08/26] Fix esp32_ble_tracker

---
 .../esp32_ble_tracker/esp32_ble_tracker.cpp           | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
index e102fe41c4..f6148a64f9 100644
--- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
+++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
@@ -58,12 +58,11 @@ void ESP32BLETracker::setup() {
   this->scanner_idle_ = true;
 
 #ifdef USE_OTA
-  ota_esphome::global_ota_component->add_on_state_callback(
-      [this](ota_esphome::OTAESPHomeState state, float progress, uint8_t error) {
-        if (state == ota_esphome::OTA_STARTED) {
-          this->stop_scan();
-        }
-      });
+  esphome::global_ota_component->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t error) {
+    if (state == ota::OTA_STARTED) {
+      this->stop_scan();
+    }
+  });
 #endif
 }
 

From 5d890be1138b4cd1da3920debbe540b43cc09cf2 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Sun, 21 Apr 2024 20:55:53 -0500
Subject: [PATCH 09/26] Fix `dummy_main.cpp`, too

---
 tests/dummy_main.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp
index 42af4eb47f..3ba4c8bd07 100644
--- a/tests/dummy_main.cpp
+++ b/tests/dummy_main.cpp
@@ -25,7 +25,7 @@ void setup() {
   ap.set_password("password1");
   wifi->add_sta(ap);
 
-  auto *ota = new ota_esphome::OTAESPHomeComponent();  // NOLINT
+  auto *ota = new esphome::ESPHomeOTAComponent();  // NOLINT
   ota->set_port(8266);
 
   App.setup();

From d5885f51a697856665d0b56f2e6de1d63397866d Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 24 Apr 2024 00:03:32 -0500
Subject: [PATCH 10/26] Update tests

---
 tests/components/ota/common.yaml            | 31 +++++++++++++++++++++
 tests/components/ota/test.esp32-c3-idf.yaml | 31 +--------------------
 tests/components/ota/test.esp32-c3.yaml     | 31 +--------------------
 tests/components/ota/test.esp32-idf.yaml    | 31 +--------------------
 tests/components/ota/test.esp32.yaml        | 31 +--------------------
 tests/components/ota/test.esp8266.yaml      | 31 +--------------------
 tests/components/ota/test.rp2040.yaml       | 31 +--------------------
 7 files changed, 37 insertions(+), 180 deletions(-)
 create mode 100644 tests/components/ota/common.yaml

diff --git a/tests/components/ota/common.yaml b/tests/components/ota/common.yaml
new file mode 100644
index 0000000000..4910e2d891
--- /dev/null
+++ b/tests/components/ota/common.yaml
@@ -0,0 +1,31 @@
+wifi:
+  ssid: MySSID
+  password: password1
+
+ota:
+  - platform: esphome
+    safe_mode: true
+    password: "superlongpasswordthatnoonewillknow"
+    port: 3286
+    reboot_timeout: 2min
+    num_attempts: 5
+    on_begin:
+      then:
+        - logger.log: "OTA start"
+    on_progress:
+      then:
+        - logger.log:
+            format: "OTA progress %0.1f%%"
+            args: ["x"]
+    on_end:
+      then:
+        - logger.log: "OTA end"
+    on_error:
+      then:
+        - logger.log:
+            format: "OTA update error %d"
+            args: ["x"]
+    on_state_change:
+      then:
+        lambda: >-
+          ESP_LOGD("ota", "State %d", state);
diff --git a/tests/components/ota/test.esp32-c3-idf.yaml b/tests/components/ota/test.esp32-c3-idf.yaml
index 367454995f..dade44d145 100644
--- a/tests/components/ota/test.esp32-c3-idf.yaml
+++ b/tests/components/ota/test.esp32-c3-idf.yaml
@@ -1,30 +1 @@
-wifi:
-  ssid: MySSID
-  password: password1
-
-ota:
-  safe_mode: true
-  password: "superlongpasswordthatnoonewillknow"
-  port: 3286
-  reboot_timeout: 2min
-  num_attempts: 5
-  on_begin:
-    then:
-      - logger.log: "OTA start"
-  on_progress:
-    then:
-      - logger.log:
-          format: "OTA progress %0.1f%%"
-          args: ["x"]
-  on_end:
-    then:
-      - logger.log: "OTA end"
-  on_error:
-    then:
-      - logger.log:
-          format: "OTA update error %d"
-          args: ["x"]
-  on_state_change:
-    then:
-      lambda: >-
-        ESP_LOGD("ota", "State %d", state);
+<<: !include common.yaml
diff --git a/tests/components/ota/test.esp32-c3.yaml b/tests/components/ota/test.esp32-c3.yaml
index 367454995f..dade44d145 100644
--- a/tests/components/ota/test.esp32-c3.yaml
+++ b/tests/components/ota/test.esp32-c3.yaml
@@ -1,30 +1 @@
-wifi:
-  ssid: MySSID
-  password: password1
-
-ota:
-  safe_mode: true
-  password: "superlongpasswordthatnoonewillknow"
-  port: 3286
-  reboot_timeout: 2min
-  num_attempts: 5
-  on_begin:
-    then:
-      - logger.log: "OTA start"
-  on_progress:
-    then:
-      - logger.log:
-          format: "OTA progress %0.1f%%"
-          args: ["x"]
-  on_end:
-    then:
-      - logger.log: "OTA end"
-  on_error:
-    then:
-      - logger.log:
-          format: "OTA update error %d"
-          args: ["x"]
-  on_state_change:
-    then:
-      lambda: >-
-        ESP_LOGD("ota", "State %d", state);
+<<: !include common.yaml
diff --git a/tests/components/ota/test.esp32-idf.yaml b/tests/components/ota/test.esp32-idf.yaml
index 367454995f..dade44d145 100644
--- a/tests/components/ota/test.esp32-idf.yaml
+++ b/tests/components/ota/test.esp32-idf.yaml
@@ -1,30 +1 @@
-wifi:
-  ssid: MySSID
-  password: password1
-
-ota:
-  safe_mode: true
-  password: "superlongpasswordthatnoonewillknow"
-  port: 3286
-  reboot_timeout: 2min
-  num_attempts: 5
-  on_begin:
-    then:
-      - logger.log: "OTA start"
-  on_progress:
-    then:
-      - logger.log:
-          format: "OTA progress %0.1f%%"
-          args: ["x"]
-  on_end:
-    then:
-      - logger.log: "OTA end"
-  on_error:
-    then:
-      - logger.log:
-          format: "OTA update error %d"
-          args: ["x"]
-  on_state_change:
-    then:
-      lambda: >-
-        ESP_LOGD("ota", "State %d", state);
+<<: !include common.yaml
diff --git a/tests/components/ota/test.esp32.yaml b/tests/components/ota/test.esp32.yaml
index 367454995f..dade44d145 100644
--- a/tests/components/ota/test.esp32.yaml
+++ b/tests/components/ota/test.esp32.yaml
@@ -1,30 +1 @@
-wifi:
-  ssid: MySSID
-  password: password1
-
-ota:
-  safe_mode: true
-  password: "superlongpasswordthatnoonewillknow"
-  port: 3286
-  reboot_timeout: 2min
-  num_attempts: 5
-  on_begin:
-    then:
-      - logger.log: "OTA start"
-  on_progress:
-    then:
-      - logger.log:
-          format: "OTA progress %0.1f%%"
-          args: ["x"]
-  on_end:
-    then:
-      - logger.log: "OTA end"
-  on_error:
-    then:
-      - logger.log:
-          format: "OTA update error %d"
-          args: ["x"]
-  on_state_change:
-    then:
-      lambda: >-
-        ESP_LOGD("ota", "State %d", state);
+<<: !include common.yaml
diff --git a/tests/components/ota/test.esp8266.yaml b/tests/components/ota/test.esp8266.yaml
index 367454995f..dade44d145 100644
--- a/tests/components/ota/test.esp8266.yaml
+++ b/tests/components/ota/test.esp8266.yaml
@@ -1,30 +1 @@
-wifi:
-  ssid: MySSID
-  password: password1
-
-ota:
-  safe_mode: true
-  password: "superlongpasswordthatnoonewillknow"
-  port: 3286
-  reboot_timeout: 2min
-  num_attempts: 5
-  on_begin:
-    then:
-      - logger.log: "OTA start"
-  on_progress:
-    then:
-      - logger.log:
-          format: "OTA progress %0.1f%%"
-          args: ["x"]
-  on_end:
-    then:
-      - logger.log: "OTA end"
-  on_error:
-    then:
-      - logger.log:
-          format: "OTA update error %d"
-          args: ["x"]
-  on_state_change:
-    then:
-      lambda: >-
-        ESP_LOGD("ota", "State %d", state);
+<<: !include common.yaml
diff --git a/tests/components/ota/test.rp2040.yaml b/tests/components/ota/test.rp2040.yaml
index 367454995f..dade44d145 100644
--- a/tests/components/ota/test.rp2040.yaml
+++ b/tests/components/ota/test.rp2040.yaml
@@ -1,30 +1 @@
-wifi:
-  ssid: MySSID
-  password: password1
-
-ota:
-  safe_mode: true
-  password: "superlongpasswordthatnoonewillknow"
-  port: 3286
-  reboot_timeout: 2min
-  num_attempts: 5
-  on_begin:
-    then:
-      - logger.log: "OTA start"
-  on_progress:
-    then:
-      - logger.log:
-          format: "OTA progress %0.1f%%"
-          args: ["x"]
-  on_end:
-    then:
-      - logger.log: "OTA end"
-  on_error:
-    then:
-      - logger.log:
-          format: "OTA update error %d"
-          args: ["x"]
-  on_state_change:
-    then:
-      lambda: >-
-        ESP_LOGD("ota", "State %d", state);
+<<: !include common.yaml

From 0d8e731e6963056b09accdda5472eea0a8d99c20 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 24 Apr 2024 01:19:29 -0500
Subject: [PATCH 11/26] Update safe_mode tests, too

---
 tests/components/safe_mode/common.yaml            | 15 +++++++++++++++
 tests/components/safe_mode/test.esp32-c3-idf.yaml | 14 +-------------
 tests/components/safe_mode/test.esp32-c3.yaml     | 14 +-------------
 tests/components/safe_mode/test.esp32-idf.yaml    | 14 +-------------
 tests/components/safe_mode/test.esp32.yaml        | 14 +-------------
 tests/components/safe_mode/test.esp8266.yaml      | 14 +-------------
 tests/components/safe_mode/test.rp2040.yaml       | 14 +-------------
 7 files changed, 21 insertions(+), 78 deletions(-)
 create mode 100644 tests/components/safe_mode/common.yaml

diff --git a/tests/components/safe_mode/common.yaml b/tests/components/safe_mode/common.yaml
new file mode 100644
index 0000000000..1dfc516254
--- /dev/null
+++ b/tests/components/safe_mode/common.yaml
@@ -0,0 +1,15 @@
+wifi:
+  ssid: MySSID
+  password: password1
+
+ota:
+  - platform: esphome
+    safe_mode: true
+
+button:
+  - platform: safe_mode
+    name: Safe Mode Button
+
+switch:
+  - platform: safe_mode
+    name: Safe Mode Switch
diff --git a/tests/components/safe_mode/test.esp32-c3-idf.yaml b/tests/components/safe_mode/test.esp32-c3-idf.yaml
index df0abd9aec..dade44d145 100644
--- a/tests/components/safe_mode/test.esp32-c3-idf.yaml
+++ b/tests/components/safe_mode/test.esp32-c3-idf.yaml
@@ -1,13 +1 @@
-wifi:
-  ssid: MySSID
-  password: password1
-
-ota:
-
-button:
-  - platform: safe_mode
-    name: Safe Mode Button
-
-switch:
-  - platform: safe_mode
-    name: Safe Mode Switch
+<<: !include common.yaml
diff --git a/tests/components/safe_mode/test.esp32-c3.yaml b/tests/components/safe_mode/test.esp32-c3.yaml
index df0abd9aec..dade44d145 100644
--- a/tests/components/safe_mode/test.esp32-c3.yaml
+++ b/tests/components/safe_mode/test.esp32-c3.yaml
@@ -1,13 +1 @@
-wifi:
-  ssid: MySSID
-  password: password1
-
-ota:
-
-button:
-  - platform: safe_mode
-    name: Safe Mode Button
-
-switch:
-  - platform: safe_mode
-    name: Safe Mode Switch
+<<: !include common.yaml
diff --git a/tests/components/safe_mode/test.esp32-idf.yaml b/tests/components/safe_mode/test.esp32-idf.yaml
index df0abd9aec..dade44d145 100644
--- a/tests/components/safe_mode/test.esp32-idf.yaml
+++ b/tests/components/safe_mode/test.esp32-idf.yaml
@@ -1,13 +1 @@
-wifi:
-  ssid: MySSID
-  password: password1
-
-ota:
-
-button:
-  - platform: safe_mode
-    name: Safe Mode Button
-
-switch:
-  - platform: safe_mode
-    name: Safe Mode Switch
+<<: !include common.yaml
diff --git a/tests/components/safe_mode/test.esp32.yaml b/tests/components/safe_mode/test.esp32.yaml
index df0abd9aec..dade44d145 100644
--- a/tests/components/safe_mode/test.esp32.yaml
+++ b/tests/components/safe_mode/test.esp32.yaml
@@ -1,13 +1 @@
-wifi:
-  ssid: MySSID
-  password: password1
-
-ota:
-
-button:
-  - platform: safe_mode
-    name: Safe Mode Button
-
-switch:
-  - platform: safe_mode
-    name: Safe Mode Switch
+<<: !include common.yaml
diff --git a/tests/components/safe_mode/test.esp8266.yaml b/tests/components/safe_mode/test.esp8266.yaml
index df0abd9aec..dade44d145 100644
--- a/tests/components/safe_mode/test.esp8266.yaml
+++ b/tests/components/safe_mode/test.esp8266.yaml
@@ -1,13 +1 @@
-wifi:
-  ssid: MySSID
-  password: password1
-
-ota:
-
-button:
-  - platform: safe_mode
-    name: Safe Mode Button
-
-switch:
-  - platform: safe_mode
-    name: Safe Mode Switch
+<<: !include common.yaml
diff --git a/tests/components/safe_mode/test.rp2040.yaml b/tests/components/safe_mode/test.rp2040.yaml
index df0abd9aec..dade44d145 100644
--- a/tests/components/safe_mode/test.rp2040.yaml
+++ b/tests/components/safe_mode/test.rp2040.yaml
@@ -1,13 +1 @@
-wifi:
-  ssid: MySSID
-  password: password1
-
-ota:
-
-button:
-  - platform: safe_mode
-    name: Safe Mode Button
-
-switch:
-  - platform: safe_mode
-    name: Safe Mode Switch
+<<: !include common.yaml

From f2b6a0909a4e72e4ab597dd1b0410c33751c9886 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Mon, 29 Apr 2024 09:24:10 +1200
Subject: [PATCH 12/26] Update dependencies

---
 esphome/components/safe_mode/button/__init__.py | 2 +-
 esphome/components/safe_mode/switch/__init__.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/esphome/components/safe_mode/button/__init__.py b/esphome/components/safe_mode/button/__init__.py
index 68aecdbc53..bd51d2e038 100644
--- a/esphome/components/safe_mode/button/__init__.py
+++ b/esphome/components/safe_mode/button/__init__.py
@@ -10,7 +10,7 @@ from esphome.const import (
 )
 from .. import safe_mode_ns
 
-DEPENDENCIES = ["ota"]
+DEPENDENCIES = ["ota.esphome"]
 
 SafeModeButton = safe_mode_ns.class_("SafeModeButton", button.Button, cg.Component)
 
diff --git a/esphome/components/safe_mode/switch/__init__.py b/esphome/components/safe_mode/switch/__init__.py
index 970e97ae52..0f8e500482 100644
--- a/esphome/components/safe_mode/switch/__init__.py
+++ b/esphome/components/safe_mode/switch/__init__.py
@@ -9,7 +9,7 @@ from esphome.const import (
 )
 from .. import safe_mode_ns
 
-DEPENDENCIES = ["ota"]
+DEPENDENCIES = ["ota.esphome"]
 
 SafeModeSwitch = safe_mode_ns.class_("SafeModeSwitch", switch.Switch, cg.Component)
 

From e234557cf3116a93e5586b883052b71572b1092c Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Mon, 29 Apr 2024 10:20:03 +1200
Subject: [PATCH 13/26] A wild walrus appeared

---
 esphome/__main__.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/esphome/__main__.py b/esphome/__main__.py
index df57cf5ab7..a83cbbfeb1 100644
--- a/esphome/__main__.py
+++ b/esphome/__main__.py
@@ -332,8 +332,8 @@ def upload_program(config, args, host):
         return 1  # Unknown target platform
 
     ota_conf = {}
-    if CONF_OTA in config:
-        for ota_item in config.get(CONF_OTA):
+    if ota := config.get(CONF_OTA):
+        for ota_item in ota:
             if ota_item[CONF_PLATFORM] == CONF_ESPHOME:
                 ota_conf = ota_item
                 break

From 164936c5fb9e65f4dca09bbefddc428874e774f2 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Mon, 29 Apr 2024 11:50:23 +1200
Subject: [PATCH 14/26] The walrus ran away

---
 esphome/__main__.py    | 9 ++++-----
 esphome/cpp_helpers.py | 9 ++++-----
 2 files changed, 8 insertions(+), 10 deletions(-)

diff --git a/esphome/__main__.py b/esphome/__main__.py
index a83cbbfeb1..9930119c86 100644
--- a/esphome/__main__.py
+++ b/esphome/__main__.py
@@ -332,11 +332,10 @@ def upload_program(config, args, host):
         return 1  # Unknown target platform
 
     ota_conf = {}
-    if ota := config.get(CONF_OTA):
-        for ota_item in ota:
-            if ota_item[CONF_PLATFORM] == CONF_ESPHOME:
-                ota_conf = ota_item
-                break
+    for ota_item in config.get(CONF_OTA, []):
+        if ota_item[CONF_PLATFORM] == CONF_ESPHOME:
+            ota_conf = ota_item
+            break
 
     if not ota_conf:
         raise EsphomeError(
diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py
index 402f60284e..ce494e5d9d 100644
--- a/esphome/cpp_helpers.py
+++ b/esphome/cpp_helpers.py
@@ -142,11 +142,10 @@ async def build_registry_list(registry, config):
 
 async def past_safe_mode():
     ota_conf = {}
-    if CONF_OTA in CORE.config:
-        for ota_item in CORE.config.get(CONF_OTA):
-            if ota_item[CONF_PLATFORM] == CONF_ESPHOME:
-                ota_conf = ota_item
-                break
+    for ota_item in CORE.config.get(CONF_OTA, []):
+        if ota_item[CONF_PLATFORM] == CONF_ESPHOME:
+            ota_conf = ota_item
+            break
 
     if not ota_conf:
         return

From c719b7203cd5e269479a66b15bf18e46aa20b076 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Sun, 28 Apr 2024 21:11:16 -0500
Subject: [PATCH 15/26] Move automations to `automation.h`, remove `network`
 dependency

---
 esphome/components/ota/__init__.py   |  1 -
 esphome/components/ota/automation.h  | 67 ++++++++++++++++++++++++++++
 esphome/components/ota/ota_backend.h | 62 -------------------------
 3 files changed, 67 insertions(+), 63 deletions(-)
 create mode 100644 esphome/components/ota/automation.h

diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py
index 719f25082f..94ba199788 100644
--- a/esphome/components/ota/__init__.py
+++ b/esphome/components/ota/__init__.py
@@ -6,7 +6,6 @@ from esphome.const import CONF_ESPHOME, CONF_OTA, CONF_PLATFORM, CONF_TRIGGER_ID
 
 CODEOWNERS = ["@esphome/core"]
 AUTO_LOAD = ["md5"]
-DEPENDENCIES = ["network"]
 
 IS_PLATFORM_COMPONENT = True
 
diff --git a/esphome/components/ota/automation.h b/esphome/components/ota/automation.h
new file mode 100644
index 0000000000..91ec123010
--- /dev/null
+++ b/esphome/components/ota/automation.h
@@ -0,0 +1,67 @@
+#pragma once
+#ifdef USE_OTA_STATE_CALLBACK
+#include "ota_backend.h"
+
+#include "esphome/core/automation.h"
+
+namespace esphome {
+namespace ota {
+
+class OTAStateChangeTrigger : public Trigger<OTAState> {
+ public:
+  explicit OTAStateChangeTrigger(OTAComponent *parent) {
+    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
+      if (!parent->is_failed()) {
+        return trigger(state);
+      }
+    });
+  }
+};
+
+class OTAStartTrigger : public Trigger<> {
+ public:
+  explicit OTAStartTrigger(OTAComponent *parent) {
+    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
+      if (state == OTA_STARTED && !parent->is_failed()) {
+        trigger();
+      }
+    });
+  }
+};
+
+class OTAProgressTrigger : public Trigger<float> {
+ public:
+  explicit OTAProgressTrigger(OTAComponent *parent) {
+    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
+      if (state == OTA_IN_PROGRESS && !parent->is_failed()) {
+        trigger(progress);
+      }
+    });
+  }
+};
+
+class OTAEndTrigger : public Trigger<> {
+ public:
+  explicit OTAEndTrigger(OTAComponent *parent) {
+    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
+      if (state == OTA_COMPLETED && !parent->is_failed()) {
+        trigger();
+      }
+    });
+  }
+};
+
+class OTAErrorTrigger : public Trigger<uint8_t> {
+ public:
+  explicit OTAErrorTrigger(OTAComponent *parent) {
+    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
+      if (state == OTA_ERROR && !parent->is_failed()) {
+        trigger(error);
+      }
+    });
+  }
+};
+
+}  // namespace ota
+}  // namespace esphome
+#endif
diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h
index 9f2a851f5a..9899d62fab 100644
--- a/esphome/components/ota/ota_backend.h
+++ b/esphome/components/ota/ota_backend.h
@@ -65,67 +65,5 @@ class OTAComponent : public Component {
 };
 
 std::unique_ptr<ota::OTABackend> make_ota_backend();
-
-///
-/// Automations
-///
-
-#ifdef USE_OTA_STATE_CALLBACK
-class OTAStateChangeTrigger : public Trigger<OTAState> {
- public:
-  explicit OTAStateChangeTrigger(OTAComponent *parent) {
-    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
-      if (!parent->is_failed()) {
-        return trigger(state);
-      }
-    });
-  }
-};
-
-class OTAStartTrigger : public Trigger<> {
- public:
-  explicit OTAStartTrigger(OTAComponent *parent) {
-    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
-      if (state == OTA_STARTED && !parent->is_failed()) {
-        trigger();
-      }
-    });
-  }
-};
-
-class OTAProgressTrigger : public Trigger<float> {
- public:
-  explicit OTAProgressTrigger(OTAComponent *parent) {
-    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
-      if (state == OTA_IN_PROGRESS && !parent->is_failed()) {
-        trigger(progress);
-      }
-    });
-  }
-};
-
-class OTAEndTrigger : public Trigger<> {
- public:
-  explicit OTAEndTrigger(OTAComponent *parent) {
-    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
-      if (state == OTA_COMPLETED && !parent->is_failed()) {
-        trigger();
-      }
-    });
-  }
-};
-
-class OTAErrorTrigger : public Trigger<uint8_t> {
- public:
-  explicit OTAErrorTrigger(OTAComponent *parent) {
-    parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
-      if (state == OTA_ERROR && !parent->is_failed()) {
-        trigger(error);
-      }
-    });
-  }
-};
-#endif
-
 }  // namespace ota
 }  // namespace esphome

From 4f8dd25478df50150e3c01e8a77195387a8dd5c4 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Sun, 28 Apr 2024 21:50:53 -0500
Subject: [PATCH 16/26] Enable `esp32_ble_tracker` to `stop_scan()` for all
 configured OTA platforms

---
 esphome/components/esp32_ble_tracker/__init__.py |  6 ++++++
 .../esp32_ble_tracker/esp32_ble_tracker.cpp      | 16 +++++++---------
 .../esp32_ble_tracker/esp32_ble_tracker.h        | 13 +++++++++++++
 3 files changed, 26 insertions(+), 9 deletions(-)

diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py
index 1edeaadbfd..62efb4a278 100644
--- a/esphome/components/esp32_ble_tracker/__init__.py
+++ b/esphome/components/esp32_ble_tracker/__init__.py
@@ -15,6 +15,7 @@ from esphome.const import (
     CONF_ON_BLE_ADVERTISE,
     CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
     CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
+    CONF_OTA,
     CONF_SERVICE_UUID,
     CONF_TRIGGER_ID,
     KEY_CORE,
@@ -272,6 +273,11 @@ async def to_code(config):
             add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192)
         add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9)
 
+    if CONF_OTA in CORE.config:
+        for ota_config in CORE.config.get(CONF_OTA):
+            ota_platform = await cg.get_variable(ota_config[CONF_ID])
+            cg.add(var.add_ota_component(ota_platform))
+
     cg.add_define("USE_OTA_STATE_CALLBACK")  # To be notified when an OTA update starts
     cg.add_define("USE_ESP32_BLE_CLIENT")
 
diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
index 64e9a0b16e..a26ff0c7b5 100644
--- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
+++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
@@ -17,10 +17,6 @@
 #include <nvs_flash.h>
 #include <cinttypes>
 
-#ifdef USE_OTA
-#include "esphome/components/esphome/ota/ota_esphome.h"
-#endif
-
 #ifdef USE_ARDUINO
 #include <esp32-hal-bt.h>
 #endif
@@ -58,11 +54,13 @@ void ESP32BLETracker::setup() {
   this->scanner_idle_ = true;
 
 #ifdef USE_OTA
-  esphome::global_ota_component->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t error) {
-    if (state == ota::OTA_STARTED) {
-      this->stop_scan();
-    }
-  });
+  for (auto &ota_comp : this->ota_components_) {
+    ota_comp->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t error) {
+      if (state == ota::OTA_STARTED) {
+        this->stop_scan();
+      }
+    });
+  }
 #endif
 }
 
diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
index 76dee875c5..c8dae96a97 100644
--- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
+++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
@@ -11,6 +11,10 @@
 
 #ifdef USE_ESP32
 
+#ifdef USE_OTA
+#include "esphome/components/ota/ota_backend.h"
+#endif
+
 #include <esp_gap_ble_api.h>
 #include <esp_gattc_api.h>
 #include <esp_bt_defs.h>
@@ -197,6 +201,10 @@ class ESP32BLETracker : public Component,
 
   void loop() override;
 
+#ifdef USE_OTA
+  void add_ota_component(ota::OTAComponent *ota_component) { this->ota_components_.push_back(ota_component); }
+#endif
+
   void register_listener(ESPBTDeviceListener *listener);
   void register_client(ESPBTClient *client);
   void recalculate_advertisement_parser_types();
@@ -228,6 +236,11 @@ class ESP32BLETracker : public Component,
 
   int app_id_;
 
+#ifdef USE_OTA
+  /// Vector of OTA components we'll hook into so we can stop scanning when an OTA update begins
+  std::vector<ota::OTAComponent *> ota_components_{};
+#endif
+
   /// Vector of addresses that have already been printed in print_bt_device_info
   std::vector<uint64_t> already_discovered_;
   std::vector<ESPBTDeviceListener *> listeners_;

From 2da0bcadc70093b9fc234baad5ee88ef104596b3 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 30 Apr 2024 01:31:59 -0500
Subject: [PATCH 17/26] Refactor `global_ota_component` usage

---
 .../components/esp32_ble_tracker/__init__.py  |  6 ----
 .../esp32_ble_tracker/esp32_ble_tracker.cpp   | 17 ++++++-----
 .../esp32_ble_tracker/esp32_ble_tracker.h     | 13 ---------
 .../components/esphome/ota/ota_esphome.cpp    |  8 +++---
 esphome/components/esphome/ota/ota_esphome.h  |  3 --
 esphome/components/ota/ota_backend.cpp        | 12 ++++++++
 esphome/components/ota/ota_backend.h          | 28 ++++++++++++++++---
 7 files changed, 50 insertions(+), 37 deletions(-)
 create mode 100644 esphome/components/ota/ota_backend.cpp

diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py
index 62efb4a278..1edeaadbfd 100644
--- a/esphome/components/esp32_ble_tracker/__init__.py
+++ b/esphome/components/esp32_ble_tracker/__init__.py
@@ -15,7 +15,6 @@ from esphome.const import (
     CONF_ON_BLE_ADVERTISE,
     CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
     CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
-    CONF_OTA,
     CONF_SERVICE_UUID,
     CONF_TRIGGER_ID,
     KEY_CORE,
@@ -273,11 +272,6 @@ async def to_code(config):
             add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192)
         add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9)
 
-    if CONF_OTA in CORE.config:
-        for ota_config in CORE.config.get(CONF_OTA):
-            ota_platform = await cg.get_variable(ota_config[CONF_ID])
-            cg.add(var.add_ota_component(ota_platform))
-
     cg.add_define("USE_OTA_STATE_CALLBACK")  # To be notified when an OTA update starts
     cg.add_define("USE_ESP32_BLE_CLIENT")
 
diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
index a26ff0c7b5..6b2a1caa13 100644
--- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
+++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
@@ -17,6 +17,10 @@
 #include <nvs_flash.h>
 #include <cinttypes>
 
+#ifdef USE_OTA
+#include "esphome/components/ota/ota_backend.h"
+#endif
+
 #ifdef USE_ARDUINO
 #include <esp32-hal-bt.h>
 #endif
@@ -54,13 +58,12 @@ void ESP32BLETracker::setup() {
   this->scanner_idle_ = true;
 
 #ifdef USE_OTA
-  for (auto &ota_comp : this->ota_components_) {
-    ota_comp->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t error) {
-      if (state == ota::OTA_STARTED) {
-        this->stop_scan();
-      }
-    });
-  }
+  ota::global_ota_component->add_on_state_callback(
+      [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
+        if (state == ota::OTA_STARTED) {
+          this->stop_scan();
+        }
+      });
 #endif
 }
 
diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
index c8dae96a97..76dee875c5 100644
--- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
+++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
@@ -11,10 +11,6 @@
 
 #ifdef USE_ESP32
 
-#ifdef USE_OTA
-#include "esphome/components/ota/ota_backend.h"
-#endif
-
 #include <esp_gap_ble_api.h>
 #include <esp_gattc_api.h>
 #include <esp_bt_defs.h>
@@ -201,10 +197,6 @@ class ESP32BLETracker : public Component,
 
   void loop() override;
 
-#ifdef USE_OTA
-  void add_ota_component(ota::OTAComponent *ota_component) { this->ota_components_.push_back(ota_component); }
-#endif
-
   void register_listener(ESPBTDeviceListener *listener);
   void register_client(ESPBTClient *client);
   void recalculate_advertisement_parser_types();
@@ -236,11 +228,6 @@ class ESP32BLETracker : public Component,
 
   int app_id_;
 
-#ifdef USE_OTA
-  /// Vector of OTA components we'll hook into so we can stop scanning when an OTA update begins
-  std::vector<ota::OTAComponent *> ota_components_{};
-#endif
-
   /// Vector of addresses that have already been printed in print_bt_device_info
   std::vector<uint64_t> already_discovered_;
   std::vector<ESPBTDeviceListener *> listeners_;
diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp
index 7b76380fb4..26bc6b82f9 100644
--- a/esphome/components/esphome/ota/ota_esphome.cpp
+++ b/esphome/components/esphome/ota/ota_esphome.cpp
@@ -21,11 +21,11 @@ namespace esphome {
 static const char *const TAG = "esphome.ota";
 static constexpr u_int16_t OTA_BLOCK_SIZE = 8192;
 
-ESPHomeOTAComponent *global_ota_component = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
-
-ESPHomeOTAComponent::ESPHomeOTAComponent() { global_ota_component = this; }
-
 void ESPHomeOTAComponent::setup() {
+#ifdef USE_OTA_STATE_CALLBACK
+  ota::global_ota_component->register_ota(this);
+#endif
+
   server_ = socket::socket_ip(SOCK_STREAM, 0);
   if (server_ == nullptr) {
     ESP_LOGW(TAG, "Could not create socket");
diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h
index f230f2b465..e8f36f05ca 100644
--- a/esphome/components/esphome/ota/ota_esphome.h
+++ b/esphome/components/esphome/ota/ota_esphome.h
@@ -11,7 +11,6 @@ namespace esphome {
 /// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA.
 class ESPHomeOTAComponent : public ota::OTAComponent {
  public:
-  ESPHomeOTAComponent();
 #ifdef USE_OTA_PASSWORD
   void set_auth_password(const std::string &password) { password_ = password; }
 #endif  // USE_OTA_PASSWORD
@@ -66,6 +65,4 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
       0x5afe5afe;  ///< a magic number to indicate that safe mode should be entered on next boot
 };
 
-extern ESPHomeOTAComponent *global_ota_component;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
-
 }  // namespace esphome
diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp
new file mode 100644
index 0000000000..bc7755fa83
--- /dev/null
+++ b/esphome/components/ota/ota_backend.cpp
@@ -0,0 +1,12 @@
+#include "ota_backend.h"
+
+namespace esphome {
+namespace ota {
+
+#ifdef USE_OTA_STATE_CALLBACK
+OTAGlobalCallback *global_ota_component =
+    new OTAGlobalCallback;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+#endif
+
+}  // namespace ota
+}  // namespace esphome
\ No newline at end of file
diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h
index 9899d62fab..f72a63d0e4 100644
--- a/esphome/components/ota/ota_backend.h
+++ b/esphome/components/ota/ota_backend.h
@@ -1,12 +1,13 @@
 #pragma once
-#ifdef USE_OTA_STATE_CALLBACK
-#include "esphome/core/automation.h"
-#include "esphome/core/defines.h"
-#endif
 
 #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 {
 
@@ -64,6 +65,25 @@ class OTAComponent : public Component {
 #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_{};
+};
+
+extern OTAGlobalCallback *global_ota_component;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+#endif
 std::unique_ptr<ota::OTABackend> make_ota_backend();
+
 }  // namespace ota
 }  // namespace esphome

From d1ccee8f4350876754d32ffdc740eb390e35c74a Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 30 Apr 2024 01:34:56 -0500
Subject: [PATCH 18/26] Add newline

---
 esphome/components/ota/ota_backend.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp
index bc7755fa83..16a5d56554 100644
--- a/esphome/components/ota/ota_backend.cpp
+++ b/esphome/components/ota/ota_backend.cpp
@@ -9,4 +9,4 @@ OTAGlobalCallback *global_ota_component =
 #endif
 
 }  // namespace ota
-}  // namespace esphome
\ No newline at end of file
+}  // namespace esphome

From 68cb348a8b6eb61e36efe69e6cb9c5fcf95bd3c8 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 30 Apr 2024 02:07:47 -0500
Subject: [PATCH 19/26] No walruses were harmed in the committing of this
 commit

---
 esphome/components/ota/ota_backend.cpp | 5 +++--
 esphome/components/ota/ota_backend.h   | 1 +
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp
index 16a5d56554..bad3148e16 100644
--- a/esphome/components/ota/ota_backend.cpp
+++ b/esphome/components/ota/ota_backend.cpp
@@ -4,8 +4,9 @@ namespace esphome {
 namespace ota {
 
 #ifdef USE_OTA_STATE_CALLBACK
-OTAGlobalCallback *global_ota_component =
-    new OTAGlobalCallback;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+OTAGlobalCallback *global_ota_component = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+
+OTAGlobalCallback::OTAGlobalCallback() { global_ota_component = this; }
 #endif
 
 }  // namespace ota
diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h
index f72a63d0e4..271d709bde 100644
--- a/esphome/components/ota/ota_backend.h
+++ b/esphome/components/ota/ota_backend.h
@@ -68,6 +68,7 @@ class OTAComponent : public Component {
 #ifdef USE_OTA_STATE_CALLBACK
 class OTAGlobalCallback {
  public:
+  OTAGlobalCallback();
   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);

From f82ca372a0a4bc4cef6e7fa705c3c639362ef315 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 30 Apr 2024 02:11:42 -0500
Subject: [PATCH 20/26] Revert "No walruses were harmed in the committing of
 this commit"

This reverts commit 68cb348a8b6eb61e36efe69e6cb9c5fcf95bd3c8.
---
 esphome/components/ota/ota_backend.cpp | 5 ++---
 esphome/components/ota/ota_backend.h   | 1 -
 2 files changed, 2 insertions(+), 4 deletions(-)

diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp
index bad3148e16..16a5d56554 100644
--- a/esphome/components/ota/ota_backend.cpp
+++ b/esphome/components/ota/ota_backend.cpp
@@ -4,9 +4,8 @@ namespace esphome {
 namespace ota {
 
 #ifdef USE_OTA_STATE_CALLBACK
-OTAGlobalCallback *global_ota_component = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
-
-OTAGlobalCallback::OTAGlobalCallback() { global_ota_component = this; }
+OTAGlobalCallback *global_ota_component =
+    new OTAGlobalCallback;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 #endif
 
 }  // namespace ota
diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h
index 271d709bde..f72a63d0e4 100644
--- a/esphome/components/ota/ota_backend.h
+++ b/esphome/components/ota/ota_backend.h
@@ -68,7 +68,6 @@ class OTAComponent : public Component {
 #ifdef USE_OTA_STATE_CALLBACK
 class OTAGlobalCallback {
  public:
-  OTAGlobalCallback();
   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);

From 437b2540e9a784b80933638e75eee99df0bf0ca9 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 30 Apr 2024 02:13:10 -0500
Subject: [PATCH 21/26] Update nolint

---
 esphome/components/ota/ota_backend.cpp | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp
index 16a5d56554..cb026b033b 100644
--- a/esphome/components/ota/ota_backend.cpp
+++ b/esphome/components/ota/ota_backend.cpp
@@ -4,8 +4,7 @@ namespace esphome {
 namespace ota {
 
 #ifdef USE_OTA_STATE_CALLBACK
-OTAGlobalCallback *global_ota_component =
-    new OTAGlobalCallback;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+OTAGlobalCallback *global_ota_component = new OTAGlobalCallback;  // NOLINT(cppcoreguidelines-owning-memory)
 #endif
 
 }  // namespace ota

From a7ff9abdae0c231d0c94c8f77496a6d02dd353bd Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 30 Apr 2024 02:39:00 -0500
Subject: [PATCH 22/26] Update nolint take 2

---
 esphome/components/ota/ota_backend.cpp | 2 +-
 esphome/components/ota/ota_backend.h   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp
index cb026b033b..2f2fec2020 100644
--- a/esphome/components/ota/ota_backend.cpp
+++ b/esphome/components/ota/ota_backend.cpp
@@ -4,7 +4,7 @@ namespace esphome {
 namespace ota {
 
 #ifdef USE_OTA_STATE_CALLBACK
-OTAGlobalCallback *global_ota_component = new OTAGlobalCallback;  // NOLINT(cppcoreguidelines-owning-memory)
+OTAGlobalCallback *global_ota_component = new OTAGlobalCallback;  // NOLINT
 #endif
 
 }  // namespace ota
diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h
index f72a63d0e4..b76883df74 100644
--- a/esphome/components/ota/ota_backend.h
+++ b/esphome/components/ota/ota_backend.h
@@ -81,7 +81,7 @@ class OTAGlobalCallback {
   CallbackManager<void(OTAState, float, uint8_t, OTAComponent *)> state_callback_{};
 };
 
-extern OTAGlobalCallback *global_ota_component;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+extern OTAGlobalCallback *global_ota_component;  // NOLINT
 #endif
 std::unique_ptr<ota::OTABackend> make_ota_backend();
 

From 3c70762369ab83ade5d963c80eccd5796b98edd8 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 1 May 2024 16:00:36 -0500
Subject: [PATCH 23/26] A little more code consolidation

---
 esphome/components/esphome/ota/__init__.py | 39 +++-------------------
 esphome/components/ota/__init__.py         | 33 ++++++++++++++++++
 esphome/components/ota/ota_backend.cpp     |  2 +-
 3 files changed, 38 insertions(+), 36 deletions(-)

diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py
index e6f99ba18a..9dbf39662e 100644
--- a/esphome/components/esphome/ota/__init__.py
+++ b/esphome/components/esphome/ota/__init__.py
@@ -1,8 +1,7 @@
 from esphome.cpp_generator import RawExpression
 import esphome.codegen as cg
 import esphome.config_validation as cv
-from esphome import automation
-from esphome.components import ota
+from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent
 from esphome.const import (
     CONF_ID,
     CONF_NUM_ATTEMPTS,
@@ -11,7 +10,6 @@ from esphome.const import (
     CONF_PORT,
     CONF_REBOOT_TIMEOUT,
     CONF_SAFE_MODE,
-    CONF_TRIGGER_ID,
     CONF_VERSION,
     KEY_PAST_SAFE_MODE,
 )
@@ -23,7 +21,7 @@ AUTO_LOAD = ["md5", "socket"]
 DEPENDENCIES = ["network"]
 
 esphome = cg.esphome_ns.namespace("esphome")
-ESPHomeOTAComponent = esphome.class_("ESPHomeOTAComponent", ota.OTAComponent)
+ESPHomeOTAComponent = esphome.class_("ESPHomeOTAComponent", OTAComponent)
 
 
 CONFIG_SCHEMA = (
@@ -47,7 +45,7 @@ CONFIG_SCHEMA = (
             cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int,
         }
     )
-    .extend(ota.BASE_OTA_SCHEMA)
+    .extend(BASE_OTA_SCHEMA)
     .extend(cv.COMPONENT_SCHEMA)
 )
 
@@ -57,6 +55,7 @@ async def to_code(config):
     CORE.data[CONF_OTA] = {}
 
     var = cg.new_Pvariable(config[CONF_ID])
+    await ota_to_code(var, config)
     cg.add(var.set_port(config[CONF_PORT]))
     cg.add_define("USE_OTA")
     if CONF_PASSWORD in config:
@@ -72,33 +71,3 @@ async def to_code(config):
         )
         cg.add(RawExpression(f"if ({condition}) return"))
         CORE.data[CONF_OTA][KEY_PAST_SAFE_MODE] = True
-
-    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)
-
-    use_state_callback = False
-    for conf in config.get(ota.CONF_ON_STATE_CHANGE, []):
-        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        await automation.build_automation(trigger, [(ota.OTAState, "state")], conf)
-        use_state_callback = True
-    for conf in config.get(ota.CONF_ON_BEGIN, []):
-        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        await automation.build_automation(trigger, [], conf)
-        use_state_callback = True
-    for conf in config.get(ota.CONF_ON_PROGRESS, []):
-        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        await automation.build_automation(trigger, [(float, "x")], conf)
-        use_state_callback = True
-    for conf in config.get(ota.CONF_ON_END, []):
-        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        await automation.build_automation(trigger, [], conf)
-        use_state_callback = True
-    for conf in config.get(ota.CONF_ON_ERROR, []):
-        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        await automation.build_automation(trigger, [(cg.uint8, "x")], conf)
-        use_state_callback = True
-    if use_state_callback:
-        cg.add_define("USE_OTA_STATE_CALLBACK")
diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py
index 94ba199788..5a5c69bd7a 100644
--- a/esphome/components/ota/__init__.py
+++ b/esphome/components/ota/__init__.py
@@ -1,6 +1,7 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import automation
+from esphome.core import CORE
 
 from esphome.const import CONF_ESPHOME, CONF_OTA, CONF_PLATFORM, CONF_TRIGGER_ID
 
@@ -66,3 +67,35 @@ BASE_OTA_SCHEMA = cv.Schema(
         ),
     }
 )
+
+
+async def ota_to_code(var, config):
+    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)
+
+    use_state_callback = False
+    for conf in config.get(CONF_ON_STATE_CHANGE, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        await automation.build_automation(trigger, [(OTAState, "state")], conf)
+        use_state_callback = True
+    for conf in config.get(CONF_ON_BEGIN, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        await automation.build_automation(trigger, [], conf)
+        use_state_callback = True
+    for conf in config.get(CONF_ON_PROGRESS, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        await automation.build_automation(trigger, [(float, "x")], conf)
+        use_state_callback = True
+    for conf in config.get(CONF_ON_END, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        await automation.build_automation(trigger, [], conf)
+        use_state_callback = True
+    for conf in config.get(CONF_ON_ERROR, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        await automation.build_automation(trigger, [(cg.uint8, "x")], conf)
+        use_state_callback = True
+    if use_state_callback:
+        cg.add_define("USE_OTA_STATE_CALLBACK")
diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp
index 2f2fec2020..32b9c19164 100644
--- a/esphome/components/ota/ota_backend.cpp
+++ b/esphome/components/ota/ota_backend.cpp
@@ -4,7 +4,7 @@ namespace esphome {
 namespace ota {
 
 #ifdef USE_OTA_STATE_CALLBACK
-OTAGlobalCallback *global_ota_component = new OTAGlobalCallback;  // NOLINT
+OTAGlobalCallback *global_ota_component = new OTAGlobalCallback();  // NOLINT
 #endif
 
 }  // namespace ota

From 75d5ba09e786a9f919b20a5aee87d8dc958828ec Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Thu, 2 May 2024 04:09:27 -0500
Subject: [PATCH 24/26] Fix variable name

---
 esphome/components/ota/__init__.py | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py
index 5a5c69bd7a..e9e63fbff0 100644
--- a/esphome/components/ota/__init__.py
+++ b/esphome/components/ota/__init__.py
@@ -17,14 +17,14 @@ CONF_ON_PROGRESS = "on_progress"
 CONF_ON_STATE_CHANGE = "on_state_change"
 
 
-ota = cg.esphome_ns.namespace("ota")
-OTAComponent = ota.class_("OTAComponent", cg.Component)
-OTAState = ota.enum("OTAState")
-OTAEndTrigger = ota.class_("OTAEndTrigger", automation.Trigger.template())
-OTAErrorTrigger = ota.class_("OTAErrorTrigger", automation.Trigger.template())
-OTAProgressTrigger = ota.class_("OTAProgressTrigger", automation.Trigger.template())
-OTAStartTrigger = ota.class_("OTAStartTrigger", automation.Trigger.template())
-OTAStateChangeTrigger = ota.class_(
+ota_ns = cg.esphome_ns.namespace("ota")
+OTAComponent = ota_ns.class_("OTAComponent", cg.Component)
+OTAState = ota_ns.enum("OTAState")
+OTAEndTrigger = ota_ns.class_("OTAEndTrigger", automation.Trigger.template())
+OTAErrorTrigger = ota_ns.class_("OTAErrorTrigger", automation.Trigger.template())
+OTAProgressTrigger = ota_ns.class_("OTAProgressTrigger", automation.Trigger.template())
+OTAStartTrigger = ota_ns.class_("OTAStartTrigger", automation.Trigger.template())
+OTAStateChangeTrigger = ota_ns.class_(
     "OTAStateChangeTrigger", automation.Trigger.template()
 )
 

From 8dd03a3dac480e8eda0c8d8e31d1bf62ae23efae Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 6 May 2024 19:04:52 -0500
Subject: [PATCH 25/26] Manage global pointer better

---
 .../esp32_ble_tracker/esp32_ble_tracker.cpp           |  2 +-
 esphome/components/esphome/ota/ota_esphome.cpp        |  2 +-
 esphome/components/ota/ota_backend.cpp                | 11 ++++++++++-
 esphome/components/ota/ota_backend.h                  |  3 ++-
 4 files changed, 14 insertions(+), 4 deletions(-)

diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
index 6b2a1caa13..82855b2038 100644
--- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
+++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
@@ -58,7 +58,7 @@ void ESP32BLETracker::setup() {
   this->scanner_idle_ = true;
 
 #ifdef USE_OTA
-  ota::global_ota_component->add_on_state_callback(
+  ota::get_global_ota_callback()->add_on_state_callback(
       [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
         if (state == ota::OTA_STARTED) {
           this->stop_scan();
diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp
index 26bc6b82f9..9ce4c8fe27 100644
--- a/esphome/components/esphome/ota/ota_esphome.cpp
+++ b/esphome/components/esphome/ota/ota_esphome.cpp
@@ -23,7 +23,7 @@ static constexpr u_int16_t OTA_BLOCK_SIZE = 8192;
 
 void ESPHomeOTAComponent::setup() {
 #ifdef USE_OTA_STATE_CALLBACK
-  ota::global_ota_component->register_ota(this);
+  ota::register_ota_platform(this);
 #endif
 
   server_ = socket::socket_ip(SOCK_STREAM, 0);
diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp
index 32b9c19164..5e459aa034 100644
--- a/esphome/components/ota/ota_backend.cpp
+++ b/esphome/components/ota/ota_backend.cpp
@@ -4,7 +4,16 @@ namespace esphome {
 namespace ota {
 
 #ifdef USE_OTA_STATE_CALLBACK
-OTAGlobalCallback *global_ota_component = new OTAGlobalCallback();  // NOLINT
+OTAGlobalCallback *global_ota_callback{nullptr};
+
+OTAGlobalCallback *get_global_ota_callback() {
+  if (global_ota_callback == nullptr) {
+    global_ota_callback = new OTAGlobalCallback();
+  }
+  return global_ota_callback;
+}
+
+void register_ota_platform(OTAComponent *ota_caller) { get_global_ota_callback()->register_ota(ota_caller); }
 #endif
 
 }  // namespace ota
diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h
index b76883df74..2537cdb67a 100644
--- a/esphome/components/ota/ota_backend.h
+++ b/esphome/components/ota/ota_backend.h
@@ -81,7 +81,8 @@ class OTAGlobalCallback {
   CallbackManager<void(OTAState, float, uint8_t, OTAComponent *)> state_callback_{};
 };
 
-extern OTAGlobalCallback *global_ota_component;  // NOLINT
+OTAGlobalCallback *get_global_ota_callback();
+void register_ota_platform(OTAComponent *ota_caller);
 #endif
 std::unique_ptr<ota::OTABackend> make_ota_backend();
 

From 6d335ceeddd69da6163b2f31c7967f0cacd1d4b4 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 6 May 2024 19:35:42 -0500
Subject: [PATCH 26/26] Put back a couple nolints

---
 esphome/components/ota/ota_backend.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp
index 5e459aa034..30de4ec4b3 100644
--- a/esphome/components/ota/ota_backend.cpp
+++ b/esphome/components/ota/ota_backend.cpp
@@ -4,11 +4,11 @@ namespace esphome {
 namespace ota {
 
 #ifdef USE_OTA_STATE_CALLBACK
-OTAGlobalCallback *global_ota_callback{nullptr};
+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();
+    global_ota_callback = new OTAGlobalCallback();  // NOLINT(cppcoreguidelines-owning-memory)
   }
   return global_ota_callback;
 }