From dac9768f6ac897d46b093ecee81ee64101b29b59 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Wed, 15 Jan 2025 11:56:52 +1100
Subject: [PATCH 1/7] [spi] Restore ``SPIDelegateDummy`` (#8019)

---
 esphome/components/spi/spi.cpp |  6 ++++++
 esphome/components/spi/spi.h   | 21 +++++++++++++++++++--
 2 files changed, 25 insertions(+), 2 deletions(-)

diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp
index f9435b0424..b13826c443 100644
--- a/esphome/components/spi/spi.cpp
+++ b/esphome/components/spi/spi.cpp
@@ -7,6 +7,10 @@ namespace spi {
 
 const char *const TAG = "spi";
 
+SPIDelegate *const SPIDelegate::NULL_DELEGATE =  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+    new SPIDelegateDummy();
+// https://bugs.llvm.org/show_bug.cgi?id=48040
+
 bool SPIDelegate::is_ready() { return true; }
 
 GPIOPin *const NullPin::NULL_PIN = new NullPin();  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -75,6 +79,8 @@ void SPIComponent::dump_config() {
   }
 }
 
+void SPIDelegateDummy::begin_transaction() { ESP_LOGE(TAG, "SPIDevice not initialised - did you call spi_setup()?"); }
+
 uint8_t SPIDelegateBitBash::transfer(uint8_t data) { return this->transfer_(data, 8); }
 
 void SPIDelegateBitBash::write(uint16_t data, size_t num_bits) { this->transfer_(data, num_bits); }
diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h
index 4cd8d3383c..f581dc3f56 100644
--- a/esphome/components/spi/spi.h
+++ b/esphome/components/spi/spi.h
@@ -163,6 +163,8 @@ class Utility {
   }
 };
 
+class SPIDelegateDummy;
+
 // represents a device attached to an SPI bus, with a defined clock rate, mode and bit order. On Arduino this is
 // a thin wrapper over SPIClass.
 class SPIDelegate {
@@ -248,6 +250,21 @@ class SPIDelegate {
   uint32_t data_rate_{1000000};
   SPIMode mode_{MODE0};
   GPIOPin *cs_pin_{NullPin::NULL_PIN};
+  static SPIDelegate *const NULL_DELEGATE;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+};
+
+/**
+ * A dummy SPIDelegate that complains if it's used.
+ */
+
+class SPIDelegateDummy : public SPIDelegate {
+ public:
+  SPIDelegateDummy() = default;
+
+  uint8_t transfer(uint8_t data) override { return 0; }
+  void end_transaction() override{};
+
+  void begin_transaction() override;
 };
 
 /**
@@ -365,7 +382,7 @@ class SPIClient {
 
   virtual void spi_teardown() {
     this->parent_->unregister_device(this);
-    this->delegate_ = nullptr;
+    this->delegate_ = SPIDelegate::NULL_DELEGATE;
   }
 
   bool spi_is_ready() { return this->delegate_->is_ready(); }
@@ -376,7 +393,7 @@ class SPIClient {
   uint32_t data_rate_{1000000};
   SPIComponent *parent_{nullptr};
   GPIOPin *cs_{nullptr};
-  SPIDelegate *delegate_{nullptr};
+  SPIDelegate *delegate_{SPIDelegate::NULL_DELEGATE};
 };
 
 /**

From 17b88f2e3e5d873b1961b40e4c1fb6acccfc1305 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Wed, 15 Jan 2025 12:29:51 +1100
Subject: [PATCH 2/7] [lvgl] fix lvgl.widget.update and friends (#8087)

---
 esphome/components/lvgl/automation.py       | 15 +++++++++++-
 esphome/components/lvgl/schemas.py          | 27 ++++++++++++++-------
 esphome/components/lvgl/widgets/dropdown.py |  2 +-
 esphome/components/lvgl/widgets/keyboard.py |  9 ++++++-
 esphome/components/lvgl/widgets/msgbox.py   |  2 +-
 esphome/components/lvgl/widgets/obj.py      | 13 +---------
 esphome/components/lvgl/widgets/tabview.py  |  2 +-
 tests/components/lvgl/lvgl-package.yaml     | 15 ++++++++++++
 8 files changed, 59 insertions(+), 26 deletions(-)

diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py
index 7db6e1f045..168fc03cb7 100644
--- a/esphome/components/lvgl/automation.py
+++ b/esphome/components/lvgl/automation.py
@@ -15,6 +15,7 @@ from .defines import (
     CONF_FREEZE,
     CONF_LVGL_ID,
     CONF_SHOW_SNOW,
+    PARTS,
     literal,
 )
 from .lv_validation import lv_bool, lv_color, lv_image, opacity
@@ -33,7 +34,7 @@ from .lvcode import (
     lvgl_comp,
     static_cast,
 )
-from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA
+from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA, base_update_schema
 from .types import (
     LV_STATE,
     LvglAction,
@@ -41,6 +42,7 @@ from .types import (
     ObjUpdateAction,
     lv_disp_t,
     lv_group_t,
+    lv_obj_base_t,
     lv_obj_t,
     lv_pseudo_button_t,
 )
@@ -336,3 +338,14 @@ async def widget_focus(config, action_id, template_arg, args):
             lv.group_focus_freeze(group, True)
         var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
         return var
+
+
+@automation.register_action(
+    "lvgl.widget.update", ObjUpdateAction, base_update_schema(lv_obj_base_t, PARTS)
+)
+async def obj_update_to_code(config, action_id, template_arg, args):
+    async def do_update(widget: Widget):
+        await set_obj_properties(widget, config)
+
+    widgets = await get_widgets(config[CONF_ID])
+    return await action_to_code(widgets, do_update, action_id, template_arg, args)
diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py
index 271dbea19f..f0318dd17a 100644
--- a/esphome/components/lvgl/schemas.py
+++ b/esphome/components/lvgl/schemas.py
@@ -199,13 +199,12 @@ FLAG_SCHEMA = cv.Schema({cv.Optional(flag): lvalid.lv_bool for flag in df.OBJ_FL
 FLAG_LIST = cv.ensure_list(df.LvConstant("LV_OBJ_FLAG_", *df.OBJ_FLAGS).one_of)
 
 
-def part_schema(widget_type: WidgetType):
+def part_schema(parts):
     """
     Generate a schema for the various parts (e.g. main:, indicator:) of a widget type
-    :param widget_type:  The type of widget to generate for
-    :return:
+    :param parts:  The parts to include in the schema
+    :return: The schema
     """
-    parts = widget_type.parts
     return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend(
         STATE_SCHEMA
     )
@@ -228,9 +227,15 @@ def automation_schema(typ: LvType):
     }
 
 
-def create_modify_schema(widget_type):
+def base_update_schema(widget_type, parts):
+    """
+    Create a schema for updating a widgets style properties, states and flags
+    :param widget_type: The type of the ID
+    :param parts:  The allowable parts to specify
+    :return:
+    """
     return (
-        part_schema(widget_type)
+        part_schema(parts)
         .extend(
             {
                 cv.Required(CONF_ID): cv.ensure_list(
@@ -245,7 +250,12 @@ def create_modify_schema(widget_type):
             }
         )
         .extend(FLAG_SCHEMA)
-        .extend(widget_type.modify_schema)
+    )
+
+
+def create_modify_schema(widget_type):
+    return base_update_schema(widget_type.w_type, widget_type.parts).extend(
+        widget_type.modify_schema
     )
 
 
@@ -256,7 +266,7 @@ def obj_schema(widget_type: WidgetType):
     :return:
     """
     return (
-        part_schema(widget_type)
+        part_schema(widget_type.parts)
         .extend(FLAG_SCHEMA)
         .extend(LAYOUT_SCHEMA)
         .extend(ALIGN_TO_SCHEMA)
@@ -341,7 +351,6 @@ FLEX_OBJ_SCHEMA = {
     cv.Optional(df.CONF_FLEX_GROW): cv.int_,
 }
 
-
 DISP_BG_SCHEMA = cv.Schema(
     {
         cv.Optional(df.CONF_DISP_BG_IMAGE): cv.Any(
diff --git a/esphome/components/lvgl/widgets/dropdown.py b/esphome/components/lvgl/widgets/dropdown.py
index a6bfc6bb88..b32b5a2b2e 100644
--- a/esphome/components/lvgl/widgets/dropdown.py
+++ b/esphome/components/lvgl/widgets/dropdown.py
@@ -37,7 +37,7 @@ DROPDOWN_BASE_SCHEMA = cv.Schema(
         cv.Exclusive(CONF_SELECTED_INDEX, CONF_SELECTED_TEXT): lv_int,
         cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text,
         cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of,
-        cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec),
+        cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec.parts),
     }
 )
 
diff --git a/esphome/components/lvgl/widgets/keyboard.py b/esphome/components/lvgl/widgets/keyboard.py
index ba7edb302e..d4a71078d0 100644
--- a/esphome/components/lvgl/widgets/keyboard.py
+++ b/esphome/components/lvgl/widgets/keyboard.py
@@ -16,6 +16,11 @@ KEYBOARD_SCHEMA = {
     cv.Optional(CONF_TEXTAREA): cv.use_id(lv_textarea_t),
 }
 
+KEYBOARD_MODIFY_SCHEMA = {
+    cv.Optional(CONF_MODE): KEYBOARD_MODES.one_of,
+    cv.Optional(CONF_TEXTAREA): cv.use_id(lv_textarea_t),
+}
+
 lv_keyboard_t = LvType(
     "LvKeyboardType",
     parents=(KeyProvider, LvCompound),
@@ -32,6 +37,7 @@ class KeyboardType(WidgetType):
             lv_keyboard_t,
             (CONF_MAIN, CONF_ITEMS),
             KEYBOARD_SCHEMA,
+            modify_schema=KEYBOARD_MODIFY_SCHEMA,
         )
 
     def get_uses(self):
@@ -41,7 +47,8 @@ class KeyboardType(WidgetType):
         lvgl_components_required.add("KEY_LISTENER")
         lvgl_components_required.add(CONF_KEYBOARD)
         add_lv_use("btnmatrix")
-        await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(config[CONF_MODE]))
+        if mode := config.get(CONF_MODE):
+            await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(mode))
         if ta := await get_widgets(config, CONF_TEXTAREA):
             await w.set_property(CONF_TEXTAREA, ta[0].obj)
 
diff --git a/esphome/components/lvgl/widgets/msgbox.py b/esphome/components/lvgl/widgets/msgbox.py
index c3393940b6..82b2442378 100644
--- a/esphome/components/lvgl/widgets/msgbox.py
+++ b/esphome/components/lvgl/widgets/msgbox.py
@@ -51,7 +51,7 @@ MSGBOX_SCHEMA = container_schema(
             cv.Required(CONF_TITLE): STYLED_TEXT_SCHEMA,
             cv.Optional(CONF_BODY, default=""): STYLED_TEXT_SCHEMA,
             cv.Optional(CONF_BUTTONS): cv.ensure_list(BUTTONMATRIX_BUTTON_SCHEMA),
-            cv.Optional(CONF_BUTTON_STYLE): part_schema(buttonmatrix_spec),
+            cv.Optional(CONF_BUTTON_STYLE): part_schema(buttonmatrix_spec.parts),
             cv.Optional(CONF_CLOSE_BUTTON, default=True): lv_bool,
             cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr),
         }
diff --git a/esphome/components/lvgl/widgets/obj.py b/esphome/components/lvgl/widgets/obj.py
index afb4c97f33..ab22a5ce86 100644
--- a/esphome/components/lvgl/widgets/obj.py
+++ b/esphome/components/lvgl/widgets/obj.py
@@ -1,9 +1,5 @@
-from esphome import automation
-
-from ..automation import update_to_code
 from ..defines import CONF_MAIN, CONF_OBJ, CONF_SCROLLBAR
-from ..schemas import create_modify_schema
-from ..types import ObjUpdateAction, WidgetType, lv_obj_t
+from ..types import WidgetType, lv_obj_t
 
 
 class ObjType(WidgetType):
@@ -21,10 +17,3 @@ class ObjType(WidgetType):
 
 
 obj_spec = ObjType()
-
-
-@automation.register_action(
-    "lvgl.widget.update", ObjUpdateAction, create_modify_schema(obj_spec)
-)
-async def obj_update_to_code(config, action_id, template_arg, args):
-    return await update_to_code(config, action_id, template_arg, args)
diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py
index 226fc3f286..1d18ddd259 100644
--- a/esphome/components/lvgl/widgets/tabview.py
+++ b/esphome/components/lvgl/widgets/tabview.py
@@ -38,7 +38,7 @@ TABVIEW_SCHEMA = cv.Schema(
                 },
             )
         ),
-        cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec),
+        cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec.parts),
         cv.Optional(CONF_POSITION, default="top"): DIRECTIONS.one_of,
         cv.Optional(CONF_SIZE, default="10%"): size,
     }
diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml
index 7c59cfa171..b3227bb96e 100644
--- a/tests/components/lvgl/lvgl-package.yaml
+++ b/tests/components/lvgl/lvgl-package.yaml
@@ -805,9 +805,24 @@ lvgl:
                 - logger.log:
                     format: "keyboard value %s"
                     args: [text.c_str()]
+                - lvgl.keyboard.update:
+                    id: lv_keyboard
+                    hidden: true
+            on_ready:
+              - lvgl.widget.update:
+                  id: lv_keyboard
+              - lvgl.keyboard.update:
+                  id: lv_keyboard
+                  hidden: true
+
         - keyboard:
             id: lv_keyboard1
             mode: special
+            on_ready:
+              lvgl.keyboard.update:
+                id: lv_keyboard1
+                hidden: true
+                mode: text_lower
 
 font:
   - file: "gfonts://Roboto"

From c43d8460bdbc8d2e3c77986e55dbdc7deee99184 Mon Sep 17 00:00:00 2001
From: Saninn Salas Diaz <5490201+distante@users.noreply.github.com>
Date: Wed, 15 Jan 2025 03:14:58 +0100
Subject: [PATCH 3/7] fix(web_server/fan): send speed update values even when
 fan is off (#8086)

---
 esphome/components/web_server/web_server.cpp | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp
index ed0cb3db2c..8c09d607a7 100644
--- a/esphome/components/web_server/web_server.cpp
+++ b/esphome/components/web_server/web_server.cpp
@@ -455,8 +455,9 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
     } else if (match.method == "toggle") {
       this->schedule_([obj]() { obj->toggle().perform(); });
       request->send(200);
-    } else if (match.method == "turn_on") {
-      auto call = obj->turn_on();
+    } else if (match.method == "turn_on" || match.method == "turn_off") {
+      auto call = match.method == "turn_on" ? obj->turn_on() : obj->turn_off();
+
       if (request->hasParam("speed_level")) {
         auto speed_level = request->getParam("speed_level")->value();
         auto val = parse_number<int>(speed_level.c_str());
@@ -486,9 +487,6 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
       }
       this->schedule_([call]() mutable { call.perform(); });
       request->send(200);
-    } else if (match.method == "turn_off") {
-      this->schedule_([obj]() { obj->turn_off().perform(); });
-      request->send(200);
     } else {
       request->send(404);
     }

From 98817a5bbfacced643d1046135eea26099a0ac36 Mon Sep 17 00:00:00 2001
From: Kevin Ahrendt <kevin.ahrendt@nabucasa.com>
Date: Tue, 14 Jan 2025 21:47:22 -0600
Subject: [PATCH 4/7] [es7210] add support for es7210 ADC (#8007)

---
 CODEOWNERS                                    |   1 +
 esphome/components/es7210/__init__.py         |  67 ++++++
 esphome/components/es7210/es7210.cpp          | 201 ++++++++++++++++++
 esphome/components/es7210/es7210.h            |  69 ++++++
 esphome/components/es7210/es7210_const.h      | 126 +++++++++++
 esphome/components/es8311/audio_dac.py        |   3 +-
 esphome/const.py                              |   1 +
 tests/components/es7210/common.yaml           |   6 +
 tests/components/es7210/test.esp32-ard.yaml   |   5 +
 .../components/es7210/test.esp32-c3-ard.yaml  |   5 +
 .../components/es7210/test.esp32-c3-idf.yaml  |   5 +
 tests/components/es7210/test.esp32-idf.yaml   |   5 +
 12 files changed, 492 insertions(+), 2 deletions(-)
 create mode 100644 esphome/components/es7210/__init__.py
 create mode 100644 esphome/components/es7210/es7210.cpp
 create mode 100644 esphome/components/es7210/es7210.h
 create mode 100644 esphome/components/es7210/es7210_const.h
 create mode 100644 tests/components/es7210/common.yaml
 create mode 100644 tests/components/es7210/test.esp32-ard.yaml
 create mode 100644 tests/components/es7210/test.esp32-c3-ard.yaml
 create mode 100644 tests/components/es7210/test.esp32-c3-idf.yaml
 create mode 100644 tests/components/es7210/test.esp32-idf.yaml

diff --git a/CODEOWNERS b/CODEOWNERS
index 088e350f5d..ba7106e6a3 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -131,6 +131,7 @@ esphome/components/ens160_base/* @latonita @vincentscode
 esphome/components/ens160_i2c/* @latonita
 esphome/components/ens160_spi/* @latonita
 esphome/components/ens210/* @itn3rd77
+esphome/components/es7210/* @kahrendt
 esphome/components/es8311/* @kahrendt @kroimon
 esphome/components/esp32/* @esphome/core
 esphome/components/esp32_ble/* @Rapsssito @jesserockz
diff --git a/esphome/components/es7210/__init__.py b/esphome/components/es7210/__init__.py
new file mode 100644
index 0000000000..8e63d7f04f
--- /dev/null
+++ b/esphome/components/es7210/__init__.py
@@ -0,0 +1,67 @@
+import esphome.codegen as cg
+from esphome.components import i2c
+import esphome.config_validation as cv
+from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_MIC_GAIN, CONF_SAMPLE_RATE
+
+CODEOWNERS = ["@kahrendt"]
+DEPENDENCIES = ["i2c"]
+
+es7210_ns = cg.esphome_ns.namespace("es7210")
+ES7210 = es7210_ns.class_("ES7210", cg.Component, i2c.I2CDevice)
+
+
+es7210_bits_per_sample = es7210_ns.enum("ES7210BitsPerSample")
+ES7210_BITS_PER_SAMPLE_ENUM = {
+    16: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_16,
+    24: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_24,
+    32: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_32,
+}
+
+
+es7210_mic_gain = es7210_ns.enum("ES7210MicGain")
+ES7210_MIC_GAIN_ENUM = {
+    "0DB": es7210_mic_gain.ES7210_MIC_GAIN_0DB,
+    "3DB": es7210_mic_gain.ES7210_MIC_GAIN_3DB,
+    "6DB": es7210_mic_gain.ES7210_MIC_GAIN_6DB,
+    "9DB": es7210_mic_gain.ES7210_MIC_GAIN_9DB,
+    "12DB": es7210_mic_gain.ES7210_MIC_GAIN_12DB,
+    "15DB": es7210_mic_gain.ES7210_MIC_GAIN_15DB,
+    "18DB": es7210_mic_gain.ES7210_MIC_GAIN_18DB,
+    "21DB": es7210_mic_gain.ES7210_MIC_GAIN_21DB,
+    "24DB": es7210_mic_gain.ES7210_MIC_GAIN_24DB,
+    "27DB": es7210_mic_gain.ES7210_MIC_GAIN_27DB,
+    "30DB": es7210_mic_gain.ES7210_MIC_GAIN_30DB,
+    "33DB": es7210_mic_gain.ES7210_MIC_GAIN_33DB,
+    "34.5DB": es7210_mic_gain.ES7210_MIC_GAIN_34_5DB,
+    "36DB": es7210_mic_gain.ES7210_MIC_GAIN_36DB,
+    "37.5DB": es7210_mic_gain.ES7210_MIC_GAIN_37_5DB,
+}
+
+_validate_bits = cv.float_with_unit("bits", "bit")
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(ES7210),
+            cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All(
+                _validate_bits, cv.enum(ES7210_BITS_PER_SAMPLE_ENUM)
+            ),
+            cv.Optional(CONF_MIC_GAIN, default="24DB"): cv.enum(
+                ES7210_MIC_GAIN_ENUM, upper=True
+            ),
+            cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1),
+        }
+    )
+    .extend(cv.COMPONENT_SCHEMA)
+    .extend(i2c.i2c_device_schema(0x40))
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
+
+    cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
+    cg.add(var.set_mic_gain(config[CONF_MIC_GAIN]))
+    cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE]))
diff --git a/esphome/components/es7210/es7210.cpp b/esphome/components/es7210/es7210.cpp
new file mode 100644
index 0000000000..d2f2c3c1ff
--- /dev/null
+++ b/esphome/components/es7210/es7210.cpp
@@ -0,0 +1,201 @@
+#include "es7210.h"
+#include "es7210_const.h"
+#include "esphome/core/hal.h"
+#include "esphome/core/log.h"
+#include <cinttypes>
+
+namespace esphome {
+namespace es7210 {
+
+static const char *const TAG = "es7210";
+
+static const size_t MCLK_DIV_FRE = 256;
+
+// Mark the component as failed; use only in setup
+#define ES7210_ERROR_FAILED(func) \
+  if (!(func)) { \
+    this->mark_failed(); \
+    return; \
+  }
+
+// Return false; use outside of setup
+#define ES7210_ERROR_CHECK(func) \
+  if (!(func)) { \
+    return false; \
+  }
+
+void ES7210::dump_config() {
+  ESP_LOGCONFIG(TAG, "ES7210 ADC:");
+  ESP_LOGCONFIG(TAG, "  Bits Per Sample: %" PRIu8, this->bits_per_sample_);
+  ESP_LOGCONFIG(TAG, "  Sample Rate: %" PRIu32, this->sample_rate_);
+
+  if (this->is_failed()) {
+    ESP_LOGCONFIG(TAG, "  Failed to initialize!");
+    return;
+  }
+}
+
+void ES7210::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up ES7210...");
+
+  // Software reset
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0xff));
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x32));
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_CLOCK_OFF_REG01, 0x3f));
+
+  // Set initialization time when device powers up
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_TIME_CONTROL0_REG09, 0x30));
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_TIME_CONTROL1_REG0A, 0x30));
+
+  // Configure HFP for all ADC channels
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC12_HPF2_REG23, 0x2a));
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC12_HPF1_REG22, 0x0a));
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC34_HPF2_REG20, 0x0a));
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC34_HPF1_REG21, 0x2a));
+
+  // Secondary I2S mode settings
+  ES7210_ERROR_FAILED(this->es7210_update_reg_bit_(ES7210_MODE_CONFIG_REG08, 0x01, 0x00));
+
+  // Configure analog power
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_ANALOG_REG40, 0xC3));
+
+  // Set mic bias
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC12_BIAS_REG41, 0x70));
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC34_BIAS_REG42, 0x70));
+
+  // Configure I2S settings, sample rate, and microphone gains
+  ES7210_ERROR_FAILED(this->configure_i2s_format_());
+  ES7210_ERROR_FAILED(this->configure_sample_rate_());
+  ES7210_ERROR_FAILED(this->configure_mic_gain_());
+
+  // Power on mics 1 through 4
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC1_POWER_REG47, 0x08));
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC2_POWER_REG48, 0x08));
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC3_POWER_REG49, 0x08));
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC4_POWER_REG4A, 0x08));
+
+  // Power down DLL
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_POWER_DOWN_REG06, 0x04));
+
+  // Power on MIC1-4 bias & ADC1-4 & PGA1-4 Power
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x0F));
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x0F));
+
+  // Enable device
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x71));
+  ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x41));
+}
+
+bool ES7210::configure_sample_rate_() {
+  int mclk_fre = this->sample_rate_ * MCLK_DIV_FRE;
+  int coeff = -1;
+
+  for (int i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) {
+    if (ES7210_COEFFICIENTS[i].lrclk == this->sample_rate_ && ES7210_COEFFICIENTS[i].mclk == mclk_fre)
+      coeff = i;
+  }
+
+  if (coeff >= 0) {
+    // Set adc_div & doubler & dll
+    uint8_t regv;
+    ES7210_ERROR_CHECK(this->read_byte(ES7210_MAINCLK_REG02, &regv));
+    regv = regv & 0x00;
+    regv |= ES7210_COEFFICIENTS[coeff].adc_div;
+    regv |= ES7210_COEFFICIENTS[coeff].doubler << 6;
+    regv |= ES7210_COEFFICIENTS[coeff].dll << 7;
+
+    ES7210_ERROR_CHECK(this->write_byte(ES7210_MAINCLK_REG02, regv));
+
+    // Set osr
+    regv = ES7210_COEFFICIENTS[coeff].osr;
+    ES7210_ERROR_CHECK(this->write_byte(ES7210_OSR_REG07, regv));
+    // Set lrck
+    regv = ES7210_COEFFICIENTS[coeff].lrck_h;
+    ES7210_ERROR_CHECK(this->write_byte(ES7210_LRCK_DIVH_REG04, regv));
+    regv = ES7210_COEFFICIENTS[coeff].lrck_l;
+    ES7210_ERROR_CHECK(this->write_byte(ES7210_LRCK_DIVL_REG05, regv));
+  } else {
+    // Invalid sample frequency
+    ESP_LOGE(TAG, "Invalid sample rate");
+    return false;
+  }
+
+  return true;
+}
+bool ES7210::configure_mic_gain_() {
+  for (int i = 0; i < 4; ++i) {
+    this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43 + i, 0x10, 0x00);
+  }
+  ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0xff));
+  ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0xff));
+
+  // Configure mic 1
+  ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
+  ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x00));
+  ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43, 0x10, 0x10));
+  ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43, 0x0f, this->mic_gain_));
+
+  // Configure mic 2
+  ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
+  ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x00));
+  ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x10, 0x10));
+  ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, this->mic_gain_));
+
+  // Configure mic 3
+  ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
+  ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
+  ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10));
+  ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, this->mic_gain_));
+
+  // Configure mic 4
+  ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
+  ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
+  ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10));
+  ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, this->mic_gain_));
+
+  return true;
+}
+
+bool ES7210::configure_i2s_format_() {
+  // Configure bits per sample
+  uint8_t reg_val = 0;
+  switch (this->bits_per_sample_) {
+    case ES7210_BITS_PER_SAMPLE_16:
+      reg_val = 0x60;
+      break;
+    case ES7210_BITS_PER_SAMPLE_18:
+      reg_val = 0x40;
+      break;
+    case ES7210_BITS_PER_SAMPLE_20:
+      reg_val = 0x20;
+      break;
+    case ES7210_BITS_PER_SAMPLE_24:
+      reg_val = 0x00;
+      break;
+    case ES7210_BITS_PER_SAMPLE_32:
+      reg_val = 0x80;
+      break;
+    default:
+      return false;
+  }
+  ES7210_ERROR_CHECK(this->write_byte(ES7210_SDP_INTERFACE1_REG11, reg_val));
+
+  if (this->enable_tdm_) {
+    ES7210_ERROR_CHECK(this->write_byte(ES7210_SDP_INTERFACE2_REG12, 0x02));
+  } else {
+    // Microphones 1 and 2 output on SDOUT1, microphones 3 and 4 output on SDOUT2
+    ES7210_ERROR_CHECK(this->write_byte(ES7210_SDP_INTERFACE2_REG12, 0x00));
+  }
+
+  return true;
+}
+
+bool ES7210::es7210_update_reg_bit_(uint8_t reg_addr, uint8_t update_bits, uint8_t data) {
+  uint8_t regv;
+  ES7210_ERROR_CHECK(this->read_byte(reg_addr, &regv));
+  regv = (regv & (~update_bits)) | (update_bits & data);
+  return this->write_byte(reg_addr, regv);
+}
+
+}  // namespace es7210
+}  // namespace esphome
diff --git a/esphome/components/es7210/es7210.h b/esphome/components/es7210/es7210.h
new file mode 100644
index 0000000000..a40dde5aa5
--- /dev/null
+++ b/esphome/components/es7210/es7210.h
@@ -0,0 +1,69 @@
+#pragma once
+
+#include "esphome/components/i2c/i2c.h"
+#include "esphome/core/component.h"
+
+namespace esphome {
+namespace es7210 {
+
+enum ES7210BitsPerSample : uint8_t {
+  ES7210_BITS_PER_SAMPLE_16 = 16,
+  ES7210_BITS_PER_SAMPLE_18 = 18,
+  ES7210_BITS_PER_SAMPLE_20 = 20,
+  ES7210_BITS_PER_SAMPLE_24 = 24,
+  ES7210_BITS_PER_SAMPLE_32 = 32,
+};
+
+enum ES7210MicGain : uint8_t {
+  ES7210_MIC_GAIN_0DB = 0,
+  ES7210_MIC_GAIN_3DB,
+  ES7210_MIC_GAIN_6DB,
+  ES7210_MIC_GAIN_9DB,
+  ES7210_MIC_GAIN_12DB,
+  ES7210_MIC_GAIN_15DB,
+  ES7210_MIC_GAIN_18DB,
+  ES7210_MIC_GAIN_21DB,
+  ES7210_MIC_GAIN_24DB,
+  ES7210_MIC_GAIN_27DB,
+  ES7210_MIC_GAIN_30DB,
+  ES7210_MIC_GAIN_33DB,
+  ES7210_MIC_GAIN_34_5DB,
+  ES7210_MIC_GAIN_36DB,
+  ES7210_MIC_GAIN_37_5DB,
+};
+
+class ES7210 : public Component, public i2c::I2CDevice {
+  /* Class for configuring an ES7210 ADC for microphone input.
+   * Based on code from:
+   * - https://github.com/espressif/esp-bsp/ (accessed 20241219)
+   * - https://github.com/espressif/esp-adf/ (accessed 20241219)
+   */
+ public:
+  void setup() override;
+  float get_setup_priority() const override { return setup_priority::DATA; }
+  void dump_config() override;
+
+  void set_bits_per_sample(ES7210BitsPerSample bits_per_sample) { this->bits_per_sample_ = bits_per_sample; }
+  void set_mic_gain(ES7210MicGain mic_gain) { this->mic_gain_ = mic_gain; }
+  void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; }
+
+ protected:
+  /// @brief Updates an I2C registry address by modifying the current state
+  /// @param reg_addr I2C register address
+  /// @param update_bits Mask of allowed bits to be modified
+  /// @param data Bit values to be written
+  /// @return True if successful, false otherwise
+  bool es7210_update_reg_bit_(uint8_t reg_addr, uint8_t update_bits, uint8_t data);
+
+  bool configure_i2s_format_();
+  bool configure_mic_gain_();
+  bool configure_sample_rate_();
+
+  bool enable_tdm_{false};  // TDM is unsupported in ESPHome as of version 2024.12
+  ES7210MicGain mic_gain_;
+  ES7210BitsPerSample bits_per_sample_;
+  uint32_t sample_rate_;
+};
+
+}  // namespace es7210
+}  // namespace esphome
diff --git a/esphome/components/es7210/es7210_const.h b/esphome/components/es7210/es7210_const.h
new file mode 100644
index 0000000000..87fd6d86d2
--- /dev/null
+++ b/esphome/components/es7210/es7210_const.h
@@ -0,0 +1,126 @@
+#pragma once
+
+#include "es7210.h"
+
+namespace esphome {
+namespace es7210 {
+
+// ES7210 register addresses
+static const uint8_t ES7210_RESET_REG00 = 0x00;     /* Reset control */
+static const uint8_t ES7210_CLOCK_OFF_REG01 = 0x01; /* Used to turn off the ADC clock */
+static const uint8_t ES7210_MAINCLK_REG02 = 0x02;   /* Set ADC clock frequency division */
+
+static const uint8_t ES7210_MASTER_CLK_REG03 = 0x03; /* MCLK source $ SCLK division */
+static const uint8_t ES7210_LRCK_DIVH_REG04 = 0x04;  /* lrck_divh */
+static const uint8_t ES7210_LRCK_DIVL_REG05 = 0x05;  /* lrck_divl */
+static const uint8_t ES7210_POWER_DOWN_REG06 = 0x06; /* power down */
+static const uint8_t ES7210_OSR_REG07 = 0x07;
+static const uint8_t ES7210_MODE_CONFIG_REG08 = 0x08;     /* Set primary/secondary & channels */
+static const uint8_t ES7210_TIME_CONTROL0_REG09 = 0x09;   /* Set Chip intial state period*/
+static const uint8_t ES7210_TIME_CONTROL1_REG0A = 0x0A;   /* Set Power up state period */
+static const uint8_t ES7210_SDP_INTERFACE1_REG11 = 0x11;  /* Set sample & fmt */
+static const uint8_t ES7210_SDP_INTERFACE2_REG12 = 0x12;  /* Pins state */
+static const uint8_t ES7210_ADC_AUTOMUTE_REG13 = 0x13;    /* Set mute */
+static const uint8_t ES7210_ADC34_MUTERANGE_REG14 = 0x14; /* Set mute range */
+static const uint8_t ES7210_ADC12_MUTERANGE_REG15 = 0x15; /* Set mute range */
+static const uint8_t ES7210_ADC34_HPF2_REG20 = 0x20;      /* HPF */
+static const uint8_t ES7210_ADC34_HPF1_REG21 = 0x21;      /* HPF */
+static const uint8_t ES7210_ADC12_HPF1_REG22 = 0x22;      /* HPF */
+static const uint8_t ES7210_ADC12_HPF2_REG23 = 0x23;      /* HPF */
+static const uint8_t ES7210_ANALOG_REG40 = 0x40;          /* ANALOG Power */
+static const uint8_t ES7210_MIC12_BIAS_REG41 = 0x41;
+static const uint8_t ES7210_MIC34_BIAS_REG42 = 0x42;
+static const uint8_t ES7210_MIC1_GAIN_REG43 = 0x43;
+static const uint8_t ES7210_MIC2_GAIN_REG44 = 0x44;
+static const uint8_t ES7210_MIC3_GAIN_REG45 = 0x45;
+static const uint8_t ES7210_MIC4_GAIN_REG46 = 0x46;
+static const uint8_t ES7210_MIC1_POWER_REG47 = 0x47;
+static const uint8_t ES7210_MIC2_POWER_REG48 = 0x48;
+static const uint8_t ES7210_MIC3_POWER_REG49 = 0x49;
+static const uint8_t ES7210_MIC4_POWER_REG4A = 0x4A;
+static const uint8_t ES7210_MIC12_POWER_REG4B = 0x4B; /* MICBias & ADC & PGA Power */
+static const uint8_t ES7210_MIC34_POWER_REG4C = 0x4C;
+
+/*
+ * Clock coefficient structer
+ */
+struct ES7210Coefficient {
+  uint32_t mclk;  // mclk frequency
+  uint32_t lrclk;
+  uint8_t ss_ds;
+  uint8_t adc_div;
+  uint8_t dll;       // dll_bypass
+  uint8_t doubler;   // doubler_enable
+  uint8_t osr;       // adc osr
+  uint8_t mclk_src;  // sselect mclk source
+  uint8_t lrck_h;    // High 4 bits of lrck
+  uint8_t lrck_l;    // Low 8 bits of lrck
+};
+
+/* Codec hifi mclk clock divider coefficients
+ *           MEMBER      REG
+ *           mclk:       0x03
+ *           lrck:       standard
+ *           ss_ds:      --
+ *           adc_div:    0x02
+ *           dll:        0x06
+ *           doubler:    0x02
+ *           osr:        0x07
+ *           mclk_src:   0x03
+ *           lrckh:      0x04
+ *           lrckl:      0x05
+ */
+static const ES7210Coefficient ES7210_COEFFICIENTS[] = {
+    // mclk      lrck    ss_ds adc_div  dll  doubler osr  mclk_src  lrckh   lrckl
+    /* 8k */
+    {12288000, 8000, 0x00, 0x03, 0x01, 0x00, 0x20, 0x00, 0x06, 0x00},
+    {16384000, 8000, 0x00, 0x04, 0x01, 0x00, 0x20, 0x00, 0x08, 0x00},
+    {19200000, 8000, 0x00, 0x1e, 0x00, 0x01, 0x28, 0x00, 0x09, 0x60},
+    {4096000, 8000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
+
+    /* 11.025k */
+    {11289600, 11025, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00},
+
+    /* 12k */
+    {12288000, 12000, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00},
+    {19200000, 12000, 0x00, 0x14, 0x00, 0x01, 0x28, 0x00, 0x06, 0x40},
+
+    /* 16k */
+    {4096000, 16000, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
+    {19200000, 16000, 0x00, 0x0a, 0x00, 0x00, 0x1e, 0x00, 0x04, 0x80},
+    {16384000, 16000, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00},
+    {12288000, 16000, 0x00, 0x03, 0x01, 0x01, 0x20, 0x00, 0x03, 0x00},
+
+    /* 22.05k */
+    {11289600, 22050, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
+
+    /* 24k */
+    {12288000, 24000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
+    {19200000, 24000, 0x00, 0x0a, 0x00, 0x01, 0x28, 0x00, 0x03, 0x20},
+
+    /* 32k */
+    {12288000, 32000, 0x00, 0x03, 0x00, 0x00, 0x20, 0x00, 0x01, 0x80},
+    {16384000, 32000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
+    {19200000, 32000, 0x00, 0x05, 0x00, 0x00, 0x1e, 0x00, 0x02, 0x58},
+
+    /* 44.1k */
+    {11289600, 44100, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
+
+    /* 48k */
+    {12288000, 48000, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
+    {19200000, 48000, 0x00, 0x05, 0x00, 0x01, 0x28, 0x00, 0x01, 0x90},
+
+    /* 64k */
+    {16384000, 64000, 0x01, 0x01, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00},
+    {19200000, 64000, 0x00, 0x05, 0x00, 0x01, 0x1e, 0x00, 0x01, 0x2c},
+
+    /* 88.2k */
+    {11289600, 88200, 0x01, 0x01, 0x01, 0x01, 0x20, 0x00, 0x00, 0x80},
+
+    /* 96k */
+    {12288000, 96000, 0x01, 0x01, 0x01, 0x01, 0x20, 0x00, 0x00, 0x80},
+    {19200000, 96000, 0x01, 0x05, 0x00, 0x01, 0x28, 0x00, 0x00, 0xc8},
+};
+
+}  // namespace es7210
+}  // namespace esphome
diff --git a/esphome/components/es8311/audio_dac.py b/esphome/components/es8311/audio_dac.py
index 1b450c3c11..7d80cfd5fb 100644
--- a/esphome/components/es8311/audio_dac.py
+++ b/esphome/components/es8311/audio_dac.py
@@ -2,7 +2,7 @@ import esphome.codegen as cg
 from esphome.components import i2c
 from esphome.components.audio_dac import AudioDac
 import esphome.config_validation as cv
-from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_SAMPLE_RATE
+from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_MIC_GAIN, CONF_SAMPLE_RATE
 
 CODEOWNERS = ["@kroimon", "@kahrendt"]
 DEPENDENCIES = ["i2c"]
@@ -10,7 +10,6 @@ DEPENDENCIES = ["i2c"]
 es8311_ns = cg.esphome_ns.namespace("es8311")
 ES8311 = es8311_ns.class_("ES8311", AudioDac, cg.Component, i2c.I2CDevice)
 
-CONF_MIC_GAIN = "mic_gain"
 CONF_USE_MCLK = "use_mclk"
 CONF_USE_MICROPHONE = "use_microphone"
 
diff --git a/esphome/const.py b/esphome/const.py
index 0f41dc1aec..4c0203c6a9 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -490,6 +490,7 @@ CONF_MEMORY_BLOCKS = "memory_blocks"
 CONF_MESSAGE = "message"
 CONF_METHANE = "methane"
 CONF_METHOD = "method"
+CONF_MIC_GAIN = "mic_gain"
 CONF_MICROPHONE = "microphone"
 CONF_MIN_BRIGHTNESS = "min_brightness"
 CONF_MIN_COOLING_OFF_TIME = "min_cooling_off_time"
diff --git a/tests/components/es7210/common.yaml b/tests/components/es7210/common.yaml
new file mode 100644
index 0000000000..5c30f7e883
--- /dev/null
+++ b/tests/components/es7210/common.yaml
@@ -0,0 +1,6 @@
+i2c:
+  - id: i2c_aic3204
+    scl: ${scl_pin}
+    sda: ${sda_pin}
+
+es7210:
diff --git a/tests/components/es7210/test.esp32-ard.yaml b/tests/components/es7210/test.esp32-ard.yaml
new file mode 100644
index 0000000000..63c3bd6afd
--- /dev/null
+++ b/tests/components/es7210/test.esp32-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO16
+  sda_pin: GPIO17
+
+<<: !include common.yaml
diff --git a/tests/components/es7210/test.esp32-c3-ard.yaml b/tests/components/es7210/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/es7210/test.esp32-c3-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/es7210/test.esp32-c3-idf.yaml b/tests/components/es7210/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/es7210/test.esp32-c3-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/es7210/test.esp32-idf.yaml b/tests/components/es7210/test.esp32-idf.yaml
new file mode 100644
index 0000000000..63c3bd6afd
--- /dev/null
+++ b/tests/components/es7210/test.esp32-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO16
+  sda_pin: GPIO17
+
+<<: !include common.yaml

From c458fd18df52923b6b3fb35e2ea6efd4d6603f72 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Wed, 15 Jan 2025 16:49:58 +1300
Subject: [PATCH 5/7] Bump version to 2025.2.0-dev

---
 esphome/const.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/const.py b/esphome/const.py
index 4c0203c6a9..284f8d5f78 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,6 +1,6 @@
 """Constants used by esphome."""
 
-__version__ = "2025.1.0-dev"
+__version__ = "2025.2.0-dev"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 VALID_SUBSTITUTIONS_CHARACTERS = (

From e779a8bcb2e73eddb7eb3474814dd6c64bb98486 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Wed, 15 Jan 2025 16:54:45 +1300
Subject: [PATCH 6/7] [event] Store ``last_event_type`` in class (#8088)

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

diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp
index 061afcb026..d27b3b378e 100644
--- a/esphome/components/event/event.cpp
+++ b/esphome/components/event/event.cpp
@@ -8,11 +8,13 @@ namespace event {
 static const char *const TAG = "event";
 
 void Event::trigger(const std::string &event_type) {
-  if (types_.find(event_type) == types_.end()) {
+  auto found = types_.find(event_type);
+  if (found == types_.end()) {
     ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str());
     return;
   }
-  ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), event_type.c_str());
+  last_event_type = &(*found);
+  ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str());
   this->event_callback_.call(event_type);
 }
 
diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h
index 067a867360..03c3c8d95a 100644
--- a/esphome/components/event/event.h
+++ b/esphome/components/event/event.h
@@ -23,6 +23,8 @@ namespace event {
 
 class Event : public EntityBase, public EntityBase_DeviceClass {
  public:
+  const std::string *last_event_type;
+
   void trigger(const std::string &event_type);
   void set_event_types(const std::set<std::string> &event_types) { this->types_ = event_types; }
   std::set<std::string> get_event_types() const { return this->types_; }

From df26ace0f172d1bad7f019fc4d095f3a7be188f9 Mon Sep 17 00:00:00 2001
From: Jordan Zucker <jordan.zucker@gmail.com>
Date: Tue, 14 Jan 2025 19:56:22 -0800
Subject: [PATCH 7/7] [prometheus] Select, media_player, and number prometheus
 metrics (#7895)

---
 .../prometheus/prometheus_handler.cpp         | 168 ++++++++++++++++++
 .../prometheus/prometheus_handler.h           |  24 +++
 tests/components/prometheus/common.yaml       |  20 +++
 .../components/prometheus/test.esp32-ard.yaml |  33 ++++
 4 files changed, 245 insertions(+)

diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp
index 5d1861202a..2d39d8ef3f 100644
--- a/esphome/components/prometheus/prometheus_handler.cpp
+++ b/esphome/components/prometheus/prometheus_handler.cpp
@@ -59,6 +59,24 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) {
     this->text_sensor_row_(stream, obj, area, node, friendly_name);
 #endif
 
+#ifdef USE_NUMBER
+  this->number_type_(stream);
+  for (auto *obj : App.get_numbers())
+    this->number_row_(stream, obj, area, node, friendly_name);
+#endif
+
+#ifdef USE_SELECT
+  this->select_type_(stream);
+  for (auto *obj : App.get_selects())
+    this->select_row_(stream, obj, area, node, friendly_name);
+#endif
+
+#ifdef USE_MEDIA_PLAYER
+  this->media_player_type_(stream);
+  for (auto *obj : App.get_media_players())
+    this->media_player_row_(stream, obj, area, node, friendly_name);
+#endif
+
   req->send(stream);
 }
 
@@ -511,6 +529,156 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso
 }
 #endif
 
+// Type-specific implementation
+#ifdef USE_NUMBER
+void PrometheusHandler::number_type_(AsyncResponseStream *stream) {
+  stream->print(F("#TYPE esphome_number_value gauge\n"));
+  stream->print(F("#TYPE esphome_number_failed gauge\n"));
+}
+void PrometheusHandler::number_row_(AsyncResponseStream *stream, number::Number *obj, std::string &area,
+                                    std::string &node, std::string &friendly_name) {
+  if (obj->is_internal() && !this->include_internal_)
+    return;
+  if (!std::isnan(obj->state)) {
+    // We have a valid value, output this value
+    stream->print(F("esphome_number_failed{id=\""));
+    stream->print(relabel_id_(obj).c_str());
+    add_area_label_(stream, area);
+    add_node_label_(stream, node);
+    add_friendly_name_label_(stream, friendly_name);
+    stream->print(F("\",name=\""));
+    stream->print(relabel_name_(obj).c_str());
+    stream->print(F("\"} 0\n"));
+    // Data itself
+    stream->print(F("esphome_number_value{id=\""));
+    stream->print(relabel_id_(obj).c_str());
+    add_area_label_(stream, area);
+    add_node_label_(stream, node);
+    add_friendly_name_label_(stream, friendly_name);
+    stream->print(F("\",name=\""));
+    stream->print(relabel_name_(obj).c_str());
+    stream->print(F("\"} "));
+    stream->print(obj->state);
+    stream->print(F("\n"));
+  } else {
+    // Invalid state
+    stream->print(F("esphome_number_failed{id=\""));
+    stream->print(relabel_id_(obj).c_str());
+    add_area_label_(stream, area);
+    add_node_label_(stream, node);
+    add_friendly_name_label_(stream, friendly_name);
+    stream->print(F("\",name=\""));
+    stream->print(relabel_name_(obj).c_str());
+    stream->print(F("\"} 1\n"));
+  }
+}
+#endif
+
+#ifdef USE_SELECT
+void PrometheusHandler::select_type_(AsyncResponseStream *stream) {
+  stream->print(F("#TYPE esphome_select_value gauge\n"));
+  stream->print(F("#TYPE esphome_select_failed gauge\n"));
+}
+void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select *obj, std::string &area,
+                                    std::string &node, std::string &friendly_name) {
+  if (obj->is_internal() && !this->include_internal_)
+    return;
+  if (obj->has_state()) {
+    // We have a valid value, output this value
+    stream->print(F("esphome_select_failed{id=\""));
+    stream->print(relabel_id_(obj).c_str());
+    add_area_label_(stream, area);
+    add_node_label_(stream, node);
+    add_friendly_name_label_(stream, friendly_name);
+    stream->print(F("\",name=\""));
+    stream->print(relabel_name_(obj).c_str());
+    stream->print(F("\"} 0\n"));
+    // Data itself
+    stream->print(F("esphome_select_value{id=\""));
+    stream->print(relabel_id_(obj).c_str());
+    add_area_label_(stream, area);
+    add_node_label_(stream, node);
+    add_friendly_name_label_(stream, friendly_name);
+    stream->print(F("\",name=\""));
+    stream->print(relabel_name_(obj).c_str());
+    stream->print(F("\",value=\""));
+    stream->print(obj->state.c_str());
+    stream->print(F("\"} "));
+    stream->print(F("1.0"));
+    stream->print(F("\n"));
+  } else {
+    // Invalid state
+    stream->print(F("esphome_select_failed{id=\""));
+    stream->print(relabel_id_(obj).c_str());
+    add_area_label_(stream, area);
+    add_node_label_(stream, node);
+    add_friendly_name_label_(stream, friendly_name);
+    stream->print(F("\",name=\""));
+    stream->print(relabel_name_(obj).c_str());
+    stream->print(F("\"} 1\n"));
+  }
+}
+#endif
+
+#ifdef USE_MEDIA_PLAYER
+void PrometheusHandler::media_player_type_(AsyncResponseStream *stream) {
+  stream->print(F("#TYPE esphome_media_player_state_value gauge\n"));
+  stream->print(F("#TYPE esphome_media_player_volume gauge\n"));
+  stream->print(F("#TYPE esphome_media_player_is_muted gauge\n"));
+  stream->print(F("#TYPE esphome_media_player_failed gauge\n"));
+}
+void PrometheusHandler::media_player_row_(AsyncResponseStream *stream, media_player::MediaPlayer *obj,
+                                          std::string &area, std::string &node, std::string &friendly_name) {
+  if (obj->is_internal() && !this->include_internal_)
+    return;
+  stream->print(F("esphome_media_player_failed{id=\""));
+  stream->print(relabel_id_(obj).c_str());
+  add_area_label_(stream, area);
+  add_node_label_(stream, node);
+  add_friendly_name_label_(stream, friendly_name);
+  stream->print(F("\",name=\""));
+  stream->print(relabel_name_(obj).c_str());
+  stream->print(F("\"} 0\n"));
+  // Data itself
+  stream->print(F("esphome_media_player_state_value{id=\""));
+  stream->print(relabel_id_(obj).c_str());
+  add_area_label_(stream, area);
+  add_node_label_(stream, node);
+  add_friendly_name_label_(stream, friendly_name);
+  stream->print(F("\",name=\""));
+  stream->print(relabel_name_(obj).c_str());
+  stream->print(F("\",value=\""));
+  stream->print(media_player::media_player_state_to_string(obj->state));
+  stream->print(F("\"} "));
+  stream->print(F("1.0"));
+  stream->print(F("\n"));
+  stream->print(F("esphome_media_player_volume{id=\""));
+  stream->print(relabel_id_(obj).c_str());
+  add_area_label_(stream, area);
+  add_node_label_(stream, node);
+  add_friendly_name_label_(stream, friendly_name);
+  stream->print(F("\",name=\""));
+  stream->print(relabel_name_(obj).c_str());
+  stream->print(F("\"} "));
+  stream->print(obj->volume);
+  stream->print(F("\n"));
+  stream->print(F("esphome_media_player_is_muted{id=\""));
+  stream->print(relabel_id_(obj).c_str());
+  add_area_label_(stream, area);
+  add_node_label_(stream, node);
+  add_friendly_name_label_(stream, friendly_name);
+  stream->print(F("\",name=\""));
+  stream->print(relabel_name_(obj).c_str());
+  stream->print(F("\"} "));
+  if (obj->is_muted()) {
+    stream->print(F("1.0"));
+  } else {
+    stream->print(F("0.0"));
+  }
+  stream->print(F("\n"));
+}
+#endif
+
 }  // namespace prometheus
 }  // namespace esphome
 #endif
diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h
index 5d08aca63a..41a06537ed 100644
--- a/esphome/components/prometheus/prometheus_handler.h
+++ b/esphome/components/prometheus/prometheus_handler.h
@@ -128,6 +128,30 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
                         std::string &friendly_name);
 #endif
 
+#ifdef USE_NUMBER
+  /// Return the type for prometheus
+  void number_type_(AsyncResponseStream *stream);
+  /// Return the sensor state as prometheus data point
+  void number_row_(AsyncResponseStream *stream, number::Number *obj, std::string &area, std::string &node,
+                   std::string &friendly_name);
+#endif
+
+#ifdef USE_SELECT
+  /// Return the type for prometheus
+  void select_type_(AsyncResponseStream *stream);
+  /// Return the select state as prometheus data point
+  void select_row_(AsyncResponseStream *stream, select::Select *obj, std::string &area, std::string &node,
+                   std::string &friendly_name);
+#endif
+
+#ifdef USE_MEDIA_PLAYER
+  /// Return the type for prometheus
+  void media_player_type_(AsyncResponseStream *stream);
+  /// Return the select state as prometheus data point
+  void media_player_row_(AsyncResponseStream *stream, media_player::MediaPlayer *obj, std::string &area,
+                         std::string &node, std::string &friendly_name);
+#endif
+
   web_server_base::WebServerBase *base_;
   bool include_internal_{false};
   std::map<EntityBase *, std::string> relabel_map_id_;
diff --git a/tests/components/prometheus/common.yaml b/tests/components/prometheus/common.yaml
index 68ef2a2f58..1b87c1d6c1 100644
--- a/tests/components/prometheus/common.yaml
+++ b/tests/components/prometheus/common.yaml
@@ -78,6 +78,26 @@ lock:
       }
     optimistic: true
 
+select:
+  - platform: template
+    id: template_select1
+    name: "Template select"
+    optimistic: true
+    options:
+      - one
+      - two
+      - three
+    initial_option: two
+
+number:
+  - platform: template
+    id: template_number1
+    name: "Template number"
+    optimistic: true
+    min_value: 0
+    max_value: 100
+    step: 1
+
 prometheus:
   include_internal: true
   relabel:
diff --git a/tests/components/prometheus/test.esp32-ard.yaml b/tests/components/prometheus/test.esp32-ard.yaml
index dade44d145..3045a6db13 100644
--- a/tests/components/prometheus/test.esp32-ard.yaml
+++ b/tests/components/prometheus/test.esp32-ard.yaml
@@ -1 +1,34 @@
 <<: !include common.yaml
+
+i2s_audio:
+  i2s_lrclk_pin: 1
+  i2s_bclk_pin: 2
+  i2s_mclk_pin: 3
+
+media_player:
+  - platform: i2s_audio
+    name: "Media Player"
+    dac_type: external
+    i2s_dout_pin: 18
+    mute_pin: 19
+    on_state:
+      - media_player.play:
+      - media_player.play_media: http://localhost/media.mp3
+      - media_player.play_media: !lambda 'return "http://localhost/media.mp3";'
+    on_idle:
+      - media_player.pause:
+    on_play:
+      - media_player.stop:
+    on_pause:
+      - media_player.toggle:
+      - wait_until:
+          media_player.is_idle:
+      - wait_until:
+          media_player.is_playing:
+      - wait_until:
+          media_player.is_announcing:
+      - wait_until:
+          media_player.is_paused:
+      - media_player.volume_up:
+      - media_player.volume_down:
+      - media_player.volume_set: 50%