From 43a020641b89b86b7082f956e3c335de38bd97b0 Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Sun, 20 Oct 2024 21:16:08 +0200
Subject: [PATCH 001/282] Fix broken ibeacon_uuid config in ble_rssi (#7640)

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

diff --git a/esphome/components/ble_rssi/sensor.py b/esphome/components/ble_rssi/sensor.py
index e3ba1abfd7..c4e767aa21 100644
--- a/esphome/components/ble_rssi/sensor.py
+++ b/esphome/components/ble_rssi/sensor.py
@@ -45,7 +45,7 @@ CONFIG_SCHEMA = cv.All(
             cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
             cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t,
             cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t,
-            cv.Optional(CONF_IBEACON_UUID): cv.uuid,
+            cv.Optional(CONF_IBEACON_UUID): esp32_ble_tracker.bt_uuid,
         }
     )
     .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
@@ -79,7 +79,7 @@ async def to_code(config):
             cg.add(var.set_service_uuid128(uuid128))
 
     if ibeacon_uuid := config.get(CONF_IBEACON_UUID):
-        ibeacon_uuid = esp32_ble_tracker.as_hex_array(str(ibeacon_uuid))
+        ibeacon_uuid = esp32_ble_tracker.as_reversed_hex_array(ibeacon_uuid)
         cg.add(var.set_ibeacon_uuid(ibeacon_uuid))
 
         if (ibeacon_major := config.get(CONF_IBEACON_MAJOR)) is not None:

From f7543a7b8dd1b5f8984c4dc84fc087df3f392784 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Mon, 21 Oct 2024 11:28:52 +1300
Subject: [PATCH 002/282] Update Pull request template (#7620)

---
 .github/PULL_REQUEST_TEMPLATE.md | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 3bf9c4e1f6..5703d39be1 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -7,11 +7,16 @@
 - [ ] Bugfix (non-breaking change which fixes an issue)
 - [ ] New feature (non-breaking change which adds functionality)
 - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
+- [ ] Code quality improvements to existing code or addition of tests
 - [ ] Other
 
-**Related issue or feature (if applicable):** fixes <link to issue>
+**Related issue or feature (if applicable):**
 
-**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** esphome/esphome-docs#<esphome-docs PR number goes here>
+- fixes <link to issue>
+
+**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
+
+- esphome/esphome-docs#<esphome-docs PR number goes here>
 
 ## Test Environment
 
@@ -23,12 +28,6 @@
 - [ ] RTL87xx
 
 ## Example entry for `config.yaml`:
-<!--
-  Supplying a configuration snippet, makes it easier for a maintainer to test
-  your PR. Furthermore, for new integrations, it gives an impression of how
-  the configuration would look like.
-  Note: Remove this section if this PR does not have an example entry.
--->
 
 ```yaml
 # Example config.yaml

From 657527655d380554edfdd3274d7b1d5ac1b4a32a Mon Sep 17 00:00:00 2001
From: Samuel Sieb <samuel-github@sieb.net>
Date: Sun, 20 Oct 2024 17:40:43 -0700
Subject: [PATCH 003/282] auto-load preferences (#7642)

Co-authored-by: Samuel Sieb <samuel@sieb.net>
---
 esphome/components/host/__init__.py      | 2 +-
 esphome/components/libretiny/__init__.py | 2 +-
 esphome/components/rp2040/__init__.py    | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py
index e83bf2dba8..eb8cfbd984 100644
--- a/esphome/components/host/__init__.py
+++ b/esphome/components/host/__init__.py
@@ -16,7 +16,7 @@ from .const import KEY_HOST
 from .gpio import host_pin_to_code  # noqa
 
 CODEOWNERS = ["@esphome/core", "@clydebarrow"]
-AUTO_LOAD = ["network"]
+AUTO_LOAD = ["network", "preferences"]
 
 
 def set_core_data(config):
diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py
index cc7fae7e70..b29d2e309c 100644
--- a/esphome/components/libretiny/__init__.py
+++ b/esphome/components/libretiny/__init__.py
@@ -46,7 +46,7 @@ from .const import (
 
 _LOGGER = logging.getLogger(__name__)
 CODEOWNERS = ["@kuba2k2"]
-AUTO_LOAD = []
+AUTO_LOAD = ["preferences"]
 
 
 def _detect_variant(value):
diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py
index 925acb629d..f59962477f 100644
--- a/esphome/components/rp2040/__init__.py
+++ b/esphome/components/rp2040/__init__.py
@@ -26,7 +26,7 @@ from .gpio import rp2040_pin_to_code  # noqa
 
 _LOGGER = logging.getLogger(__name__)
 CODEOWNERS = ["@jesserockz"]
-AUTO_LOAD = []
+AUTO_LOAD = ["preferences"]
 
 
 def set_core_data(config):

From 5e8794175dceb777f928a2966c4c5e9a977c2acc Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 21 Oct 2024 17:46:41 -0500
Subject: [PATCH 004/282] [wifi] Support custom MAC on Arduino, too (#7644)

---
 esphome/components/esp32/__init__.py                 | 12 ++++++++++--
 .../components/wifi/wifi_component_esp32_arduino.cpp |  5 +++++
 2 files changed, 15 insertions(+), 2 deletions(-)

diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py
index 8a73f2020d..61fbb53e3a 100644
--- a/esphome/components/esp32/__init__.py
+++ b/esphome/components/esp32/__init__.py
@@ -395,6 +395,13 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All(
             cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
             cv.Optional(CONF_SOURCE): cv.string_strict,
             cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version,
+            cv.Optional(CONF_ADVANCED, default={}): cv.Schema(
+                {
+                    cv.Optional(
+                        CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False
+                    ): cv.boolean,
+                }
+            ),
         }
     ),
     _arduino_check_versions,
@@ -494,6 +501,9 @@ async def to_code(config):
     conf = config[CONF_FRAMEWORK]
     cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
 
+    if CONF_ADVANCED in conf and conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
+        cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
+
     add_extra_script(
         "post",
         "post_build.py",
@@ -540,8 +550,6 @@ async def to_code(config):
         for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
             add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
 
-        if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
-            cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
         if conf[CONF_ADVANCED].get(CONF_IGNORE_EFUSE_MAC_CRC):
             add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True)
             if (framework_ver.major, framework_ver.minor) >= (4, 4):
diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp
index b8724838c8..ef4308b28c 100644
--- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp
+++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp
@@ -34,6 +34,11 @@ static esp_netif_t *s_ap_netif = nullptr;  // NOLINT(cppcoreguidelines-avoid-non
 static bool s_sta_connecting = false;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
 void WiFiComponent::wifi_pre_setup_() {
+  uint8_t mac[6];
+  if (has_custom_mac_address()) {
+    get_mac_address_raw(mac);
+    set_mac_address(mac);
+  }
   auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2);
   WiFi.onEvent(f);
   WiFi.persistent(false);

From c8d0cde329a83dd042412651c033dd133c9a3330 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 22 Oct 2024 09:49:12 +1100
Subject: [PATCH 005/282] [config] Ensure user-supplied build flags don't get
 silently overwritten (#7622)

---
 esphome/core/config.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/esphome/core/config.py b/esphome/core/config.py
index f4253bee87..8c130eb6db 100644
--- a/esphome/core/config.py
+++ b/esphome/core/config.py
@@ -318,6 +318,8 @@ async def add_includes(includes):
 async def _add_platformio_options(pio_options):
     # Add includes at the very end, so that they override everything
     for key, val in pio_options.items():
+        if key == "build_flags" and not isinstance(val, list):
+            val = [val]
         cg.add_platformio_option(key, val)
 
 

From 612e2c164406fe0d4670b8af68478c3b3644c47e Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 22 Oct 2024 09:50:16 +1100
Subject: [PATCH 006/282] [lvgl] Defer display rotation reset until setup().
 (Bugfix) (#7627)

---
 esphome/components/lvgl/lvgl_esphome.cpp | 19 ++++++-------------
 1 file changed, 6 insertions(+), 13 deletions(-)

diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp
index 413b039af0..c8f7848a4f 100644
--- a/esphome/components/lvgl/lvgl_esphome.cpp
+++ b/esphome/components/lvgl/lvgl_esphome.cpp
@@ -84,6 +84,7 @@ lv_event_code_t lv_api_event;     // NOLINT
 lv_event_code_t lv_update_event;  // NOLINT
 void LvglComponent::dump_config() {
   ESP_LOGCONFIG(TAG, "LVGL:");
+  ESP_LOGCONFIG(TAG, "  Display width/height: %d x %d", this->disp_drv_.hor_res, this->disp_drv_.ver_res);
   ESP_LOGCONFIG(TAG, "  Rotation: %d", this->rotation);
   ESP_LOGCONFIG(TAG, "  Draw rounding: %d", (int) this->draw_rounding);
 }
@@ -426,19 +427,8 @@ LvglComponent::LvglComponent(std::vector<display::Display *> displays, float buf
   this->disp_drv_.full_refresh = this->full_refresh_;
   this->disp_drv_.flush_cb = static_flush_cb;
   this->disp_drv_.rounder_cb = rounder_cb;
-  // reset the display rotation since we will handle all rotations
-  display->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);
-  switch (this->rotation) {
-    default:
-      this->disp_drv_.hor_res = (lv_coord_t) display->get_width();
-      this->disp_drv_.ver_res = (lv_coord_t) display->get_height();
-      break;
-    case display::DISPLAY_ROTATION_90_DEGREES:
-    case display::DISPLAY_ROTATION_270_DEGREES:
-      this->disp_drv_.ver_res = (lv_coord_t) display->get_width();
-      this->disp_drv_.hor_res = (lv_coord_t) display->get_height();
-      break;
-  }
+  this->disp_drv_.hor_res = (lv_coord_t) display->get_width();
+  this->disp_drv_.ver_res = (lv_coord_t) display->get_height();
   this->disp_ = lv_disp_drv_register(&this->disp_drv_);
 }
 
@@ -459,6 +449,9 @@ void LvglComponent::setup() {
     esp_log_printf_(LVGL_LOG_LEVEL, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf);
   });
 #endif
+  // Rotation will be handled by our drawing function, so reset the display rotation.
+  for (auto *display : this->displays_)
+    display->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);
   this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0);
   lv_disp_trig_activity(this->disp_);
   ESP_LOGCONFIG(TAG, "LVGL Setup complete");

From 40ad6befa83f23674d09bf70198eb8761fa69c81 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 22 Oct 2024 09:51:40 +1100
Subject: [PATCH 007/282] [lvgl] Remove states from style definitions (Bugfix)
 (#7645)

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

diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py
index beaf279a9a..215fdecdb5 100644
--- a/esphome/components/lvgl/__init__.py
+++ b/esphome/components/lvgl/__init__.py
@@ -33,7 +33,7 @@ from .schemas import (
     FLEX_OBJ_SCHEMA,
     GRID_CELL_SCHEMA,
     LAYOUT_SCHEMAS,
-    STATE_SCHEMA,
+    STYLE_SCHEMA,
     WIDGET_TYPES,
     any_widget_schema,
     container_schema,
@@ -342,7 +342,7 @@ CONFIG_SCHEMA = (
             ),
             cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list(
                 cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)})
-                .extend(STATE_SCHEMA)
+                .extend(STYLE_SCHEMA)
                 .extend(
                     {
                         cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments,

From dc42427c604ed14ab94270df8148fad22e101ee4 Mon Sep 17 00:00:00 2001
From: Michael Hansen <mike@rhasspy.org>
Date: Mon, 21 Oct 2024 18:14:07 -0500
Subject: [PATCH 008/282] Move setting global voice assistant to constructor
 (#7630)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/voice_assistant/voice_assistant.cpp | 8 ++------
 esphome/components/voice_assistant/voice_assistant.h   | 3 ++-
 2 files changed, 4 insertions(+), 7 deletions(-)

diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp
index a2210f188d..0b53e74ba3 100644
--- a/esphome/components/voice_assistant/voice_assistant.cpp
+++ b/esphome/components/voice_assistant/voice_assistant.cpp
@@ -23,6 +23,8 @@ static const size_t SEND_BUFFER_SIZE = INPUT_BUFFER_SIZE * sizeof(int16_t);
 static const size_t RECEIVE_SIZE = 1024;
 static const size_t SPEAKER_BUFFER_SIZE = 16 * RECEIVE_SIZE;
 
+VoiceAssistant::VoiceAssistant() { global_voice_assistant = this; }
+
 float VoiceAssistant::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
 
 bool VoiceAssistant::start_udp_socket_() {
@@ -68,12 +70,6 @@ bool VoiceAssistant::start_udp_socket_() {
   return true;
 }
 
-void VoiceAssistant::setup() {
-  ESP_LOGCONFIG(TAG, "Setting up Voice Assistant...");
-
-  global_voice_assistant = this;
-}
-
 bool VoiceAssistant::allocate_buffers_() {
   if (this->send_buffer_ != nullptr) {
     return true;  // Already allocated
diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h
index 56ada0e75a..870e2acdaa 100644
--- a/esphome/components/voice_assistant/voice_assistant.h
+++ b/esphome/components/voice_assistant/voice_assistant.h
@@ -91,7 +91,8 @@ struct Configuration {
 
 class VoiceAssistant : public Component {
  public:
-  void setup() override;
+  VoiceAssistant();
+
   void loop() override;
   float get_setup_priority() const override;
   void start_streaming();

From 7004053538c16544740e5e866395e9f94da1a63a Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Thu, 17 Oct 2024 11:32:22 +1100
Subject: [PATCH 009/282] [config] Fix crash with empty substitutions block
 (#7612)

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

diff --git a/esphome/config.py b/esphome/config.py
index a2d0d15477..7d48569d2d 100644
--- a/esphome/config.py
+++ b/esphome/config.py
@@ -782,7 +782,7 @@ def validate_config(
         from esphome.components import substitutions
 
         result[CONF_SUBSTITUTIONS] = {
-            **config.get(CONF_SUBSTITUTIONS, {}),
+            **(config.get(CONF_SUBSTITUTIONS) or {}),
             **command_line_substitutions,
         }
         result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS)

From 3dd34f66284ff8c5b0a69e70d57f6273d3395544 Mon Sep 17 00:00:00 2001
From: Lennart <18233294+lennart-k@users.noreply.github.com>
Date: Sun, 20 Oct 2024 21:16:08 +0200
Subject: [PATCH 010/282] Fix broken ibeacon_uuid config in ble_rssi (#7640)

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

diff --git a/esphome/components/ble_rssi/sensor.py b/esphome/components/ble_rssi/sensor.py
index e3ba1abfd7..c4e767aa21 100644
--- a/esphome/components/ble_rssi/sensor.py
+++ b/esphome/components/ble_rssi/sensor.py
@@ -45,7 +45,7 @@ CONFIG_SCHEMA = cv.All(
             cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
             cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t,
             cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t,
-            cv.Optional(CONF_IBEACON_UUID): cv.uuid,
+            cv.Optional(CONF_IBEACON_UUID): esp32_ble_tracker.bt_uuid,
         }
     )
     .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
@@ -79,7 +79,7 @@ async def to_code(config):
             cg.add(var.set_service_uuid128(uuid128))
 
     if ibeacon_uuid := config.get(CONF_IBEACON_UUID):
-        ibeacon_uuid = esp32_ble_tracker.as_hex_array(str(ibeacon_uuid))
+        ibeacon_uuid = esp32_ble_tracker.as_reversed_hex_array(ibeacon_uuid)
         cg.add(var.set_ibeacon_uuid(ibeacon_uuid))
 
         if (ibeacon_major := config.get(CONF_IBEACON_MAJOR)) is not None:

From 10791db82e7784f2e8b22a69ca8ad01223f8f1d7 Mon Sep 17 00:00:00 2001
From: Samuel Sieb <samuel-github@sieb.net>
Date: Sun, 20 Oct 2024 17:40:43 -0700
Subject: [PATCH 011/282] auto-load preferences (#7642)

Co-authored-by: Samuel Sieb <samuel@sieb.net>
---
 esphome/components/host/__init__.py      | 2 +-
 esphome/components/libretiny/__init__.py | 2 +-
 esphome/components/rp2040/__init__.py    | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py
index e83bf2dba8..eb8cfbd984 100644
--- a/esphome/components/host/__init__.py
+++ b/esphome/components/host/__init__.py
@@ -16,7 +16,7 @@ from .const import KEY_HOST
 from .gpio import host_pin_to_code  # noqa
 
 CODEOWNERS = ["@esphome/core", "@clydebarrow"]
-AUTO_LOAD = ["network"]
+AUTO_LOAD = ["network", "preferences"]
 
 
 def set_core_data(config):
diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py
index cc7fae7e70..b29d2e309c 100644
--- a/esphome/components/libretiny/__init__.py
+++ b/esphome/components/libretiny/__init__.py
@@ -46,7 +46,7 @@ from .const import (
 
 _LOGGER = logging.getLogger(__name__)
 CODEOWNERS = ["@kuba2k2"]
-AUTO_LOAD = []
+AUTO_LOAD = ["preferences"]
 
 
 def _detect_variant(value):
diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py
index 925acb629d..f59962477f 100644
--- a/esphome/components/rp2040/__init__.py
+++ b/esphome/components/rp2040/__init__.py
@@ -26,7 +26,7 @@ from .gpio import rp2040_pin_to_code  # noqa
 
 _LOGGER = logging.getLogger(__name__)
 CODEOWNERS = ["@jesserockz"]
-AUTO_LOAD = []
+AUTO_LOAD = ["preferences"]
 
 
 def set_core_data(config):

From 748256b3eef8bcce7472983bd5d6f48dd55362cb Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 21 Oct 2024 17:46:41 -0500
Subject: [PATCH 012/282] [wifi] Support custom MAC on Arduino, too (#7644)

---
 esphome/components/esp32/__init__.py                 | 12 ++++++++++--
 .../components/wifi/wifi_component_esp32_arduino.cpp |  5 +++++
 2 files changed, 15 insertions(+), 2 deletions(-)

diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py
index 8a73f2020d..61fbb53e3a 100644
--- a/esphome/components/esp32/__init__.py
+++ b/esphome/components/esp32/__init__.py
@@ -395,6 +395,13 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All(
             cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
             cv.Optional(CONF_SOURCE): cv.string_strict,
             cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version,
+            cv.Optional(CONF_ADVANCED, default={}): cv.Schema(
+                {
+                    cv.Optional(
+                        CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False
+                    ): cv.boolean,
+                }
+            ),
         }
     ),
     _arduino_check_versions,
@@ -494,6 +501,9 @@ async def to_code(config):
     conf = config[CONF_FRAMEWORK]
     cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
 
+    if CONF_ADVANCED in conf and conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
+        cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
+
     add_extra_script(
         "post",
         "post_build.py",
@@ -540,8 +550,6 @@ async def to_code(config):
         for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
             add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
 
-        if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
-            cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
         if conf[CONF_ADVANCED].get(CONF_IGNORE_EFUSE_MAC_CRC):
             add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True)
             if (framework_ver.major, framework_ver.minor) >= (4, 4):
diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp
index b8724838c8..ef4308b28c 100644
--- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp
+++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp
@@ -34,6 +34,11 @@ static esp_netif_t *s_ap_netif = nullptr;  // NOLINT(cppcoreguidelines-avoid-non
 static bool s_sta_connecting = false;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
 void WiFiComponent::wifi_pre_setup_() {
+  uint8_t mac[6];
+  if (has_custom_mac_address()) {
+    get_mac_address_raw(mac);
+    set_mac_address(mac);
+  }
   auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2);
   WiFi.onEvent(f);
   WiFi.persistent(false);

From c26c96b8f469a3071c14ef91b5e7aa073a3aa9ca Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 22 Oct 2024 09:49:12 +1100
Subject: [PATCH 013/282] [config] Ensure user-supplied build flags don't get
 silently overwritten (#7622)

---
 esphome/core/config.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/esphome/core/config.py b/esphome/core/config.py
index f4253bee87..8c130eb6db 100644
--- a/esphome/core/config.py
+++ b/esphome/core/config.py
@@ -318,6 +318,8 @@ async def add_includes(includes):
 async def _add_platformio_options(pio_options):
     # Add includes at the very end, so that they override everything
     for key, val in pio_options.items():
+        if key == "build_flags" and not isinstance(val, list):
+            val = [val]
         cg.add_platformio_option(key, val)
 
 

From 3ebdd62c672dba8fa7d4c4ebd021750394c76596 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 22 Oct 2024 09:51:40 +1100
Subject: [PATCH 014/282] [lvgl] Remove states from style definitions (Bugfix)
 (#7645)

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

diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py
index ce3843567b..1a587edb57 100644
--- a/esphome/components/lvgl/__init__.py
+++ b/esphome/components/lvgl/__init__.py
@@ -33,7 +33,7 @@ from .schemas import (
     FLEX_OBJ_SCHEMA,
     GRID_CELL_SCHEMA,
     LAYOUT_SCHEMAS,
-    STATE_SCHEMA,
+    STYLE_SCHEMA,
     WIDGET_TYPES,
     any_widget_schema,
     container_schema,
@@ -323,7 +323,7 @@ CONFIG_SCHEMA = (
             ),
             cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list(
                 cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)})
-                .extend(STATE_SCHEMA)
+                .extend(STYLE_SCHEMA)
                 .extend(
                     {
                         cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments,

From d95b370998527e279ac3a9c3376ede984929cd5d Mon Sep 17 00:00:00 2001
From: Michael Hansen <mike@rhasspy.org>
Date: Mon, 21 Oct 2024 18:14:07 -0500
Subject: [PATCH 015/282] Move setting global voice assistant to constructor
 (#7630)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/voice_assistant/voice_assistant.cpp | 8 ++------
 esphome/components/voice_assistant/voice_assistant.h   | 3 ++-
 2 files changed, 4 insertions(+), 7 deletions(-)

diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp
index a2210f188d..0b53e74ba3 100644
--- a/esphome/components/voice_assistant/voice_assistant.cpp
+++ b/esphome/components/voice_assistant/voice_assistant.cpp
@@ -23,6 +23,8 @@ static const size_t SEND_BUFFER_SIZE = INPUT_BUFFER_SIZE * sizeof(int16_t);
 static const size_t RECEIVE_SIZE = 1024;
 static const size_t SPEAKER_BUFFER_SIZE = 16 * RECEIVE_SIZE;
 
+VoiceAssistant::VoiceAssistant() { global_voice_assistant = this; }
+
 float VoiceAssistant::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
 
 bool VoiceAssistant::start_udp_socket_() {
@@ -68,12 +70,6 @@ bool VoiceAssistant::start_udp_socket_() {
   return true;
 }
 
-void VoiceAssistant::setup() {
-  ESP_LOGCONFIG(TAG, "Setting up Voice Assistant...");
-
-  global_voice_assistant = this;
-}
-
 bool VoiceAssistant::allocate_buffers_() {
   if (this->send_buffer_ != nullptr) {
     return true;  // Already allocated
diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h
index 56ada0e75a..870e2acdaa 100644
--- a/esphome/components/voice_assistant/voice_assistant.h
+++ b/esphome/components/voice_assistant/voice_assistant.h
@@ -91,7 +91,8 @@ struct Configuration {
 
 class VoiceAssistant : public Component {
  public:
-  void setup() override;
+  VoiceAssistant();
+
   void loop() override;
   float get_setup_priority() const override;
   void start_streaming();

From 735c04cd6995e1cd220440af0fb79100a0b638f2 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Tue, 22 Oct 2024 12:57:17 +1300
Subject: [PATCH 016/282] Bump version to 2024.10.1

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

diff --git a/esphome/const.py b/esphome/const.py
index 5061b1a439..5fa457b25a 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,6 +1,6 @@
 """Constants used by esphome."""
 
-__version__ = "2024.10.0"
+__version__ = "2024.10.1"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 VALID_SUBSTITUTIONS_CHARACTERS = (

From 8bb4316956a39a8c7141a9504f56ee0f5c32812a Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 22 Oct 2024 14:03:32 +1100
Subject: [PATCH 017/282] [lvgl] light schema should require `widget:` not
 `led:` (Bugfix) (#7649)

---
 esphome/components/lvgl/light/__init__.py | 8 ++++----
 tests/components/lvgl/common.yaml         | 2 +-
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/esphome/components/lvgl/light/__init__.py b/esphome/components/lvgl/light/__init__.py
index a0eeded349..8031ae8221 100644
--- a/esphome/components/lvgl/light/__init__.py
+++ b/esphome/components/lvgl/light/__init__.py
@@ -2,9 +2,9 @@ import esphome.codegen as cg
 from esphome.components import light
 from esphome.components.light import LightOutput
 import esphome.config_validation as cv
-from esphome.const import CONF_GAMMA_CORRECT, CONF_LED, CONF_OUTPUT_ID
+from esphome.const import CONF_GAMMA_CORRECT, CONF_OUTPUT_ID
 
-from ..defines import CONF_LVGL_ID
+from ..defines import CONF_LVGL_ID, CONF_WIDGET
 from ..lvcode import LvContext
 from ..schemas import LVGL_SCHEMA
 from ..types import LvType, lvgl_ns
@@ -15,7 +15,7 @@ LVLight = lvgl_ns.class_("LVLight", LightOutput)
 CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
     {
         cv.Optional(CONF_GAMMA_CORRECT, default=0.0): cv.positive_float,
-        cv.Required(CONF_LED): cv.use_id(lv_led_t),
+        cv.Required(CONF_WIDGET): cv.use_id(lv_led_t),
         cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(LVLight),
     }
 ).extend(LVGL_SCHEMA)
@@ -26,7 +26,7 @@ async def to_code(config):
     await light.register_light(var, config)
 
     paren = await cg.get_variable(config[CONF_LVGL_ID])
-    widget = await get_widgets(config, CONF_LED)
+    widget = await get_widgets(config, CONF_WIDGET)
     widget = widget[0]
     await wait_for_widgets()
     async with LvContext(paren) as ctx:
diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml
index 5dcf30e0c1..cebc3caaa7 100644
--- a/tests/components/lvgl/common.yaml
+++ b/tests/components/lvgl/common.yaml
@@ -93,7 +93,7 @@ light:
   - platform: lvgl
     name: LVGL LED
     id: lv_light
-    led: lv_led
+    widget: lv_led
 
 binary_sensor:
   - platform: lvgl

From ff48f53989f7886c167364e04df72f79039a9bac Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 22 Oct 2024 14:05:39 +1100
Subject: [PATCH 018/282] [image] Fix compile time problem with host image not
 using lvgl (#7654)

---
 esphome/components/image/image.h     |  7 +------
 esphome/components/lvgl/lvgl_proxy.h | 17 +++++++++++++++++
 2 files changed, 18 insertions(+), 6 deletions(-)
 create mode 100644 esphome/components/lvgl/lvgl_proxy.h

diff --git a/esphome/components/image/image.h b/esphome/components/image/image.h
index ae5a7a814d..a8a8aab2c2 100644
--- a/esphome/components/image/image.h
+++ b/esphome/components/image/image.h
@@ -3,12 +3,7 @@
 #include "esphome/components/display/display.h"
 
 #ifdef USE_LVGL
-// required for clang-tidy
-#ifndef LV_CONF_H
-#define LV_CONF_SKIP 1  // NOLINT
-#endif                  // LV_CONF_H
-
-#include <lvgl.h>
+#include "esphome/components/lvgl/lvgl_proxy.h"
 #endif  // USE_LVGL
 
 namespace esphome {
diff --git a/esphome/components/lvgl/lvgl_proxy.h b/esphome/components/lvgl/lvgl_proxy.h
new file mode 100644
index 0000000000..0ccd80e541
--- /dev/null
+++ b/esphome/components/lvgl/lvgl_proxy.h
@@ -0,0 +1,17 @@
+#pragma once
+/**
+* This header is for use in components that might or might not use LVGL. There is a platformio bug where
+the mere mention of a header file, even if ifdefed, causes the build to fail. This is a workaround, since if this
+file is included in the build, LVGL is always included.
+*/
+#ifdef USE_LVGL
+// required for clang-tidy
+#ifndef LV_CONF_H
+#define LV_CONF_SKIP 1  // NOLINT
+#endif                  // LV_CONF_H
+
+#include <lvgl.h>
+namespace esphome {
+namespace lvgl {}  // namespace lvgl
+}  // namespace esphome
+#endif  // USE_LVGL

From 3ac730fb2f5b7231e77d5d7c3822e7cc59fb4e59 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 22 Oct 2024 14:06:58 +1100
Subject: [PATCH 019/282] [lvgl] Fix rotation code for 90deg (Bugfix) (#7653)

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

diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp
index c8f7848a4f..70cfb859de 100644
--- a/esphome/components/lvgl/lvgl_esphome.cpp
+++ b/esphome/components/lvgl/lvgl_esphome.cpp
@@ -146,7 +146,7 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
   lv_color_t *dst = this->rotate_buf_;
   switch (this->rotation) {
     case display::DISPLAY_ROTATION_90_DEGREES:
-      for (lv_coord_t x = height - 1; x-- != 0;) {
+      for (lv_coord_t x = height; x-- != 0;) {
         for (lv_coord_t y = 0; y != width; y++) {
           dst[y * height + x] = *ptr++;
         }

From 6330177d24b82060afec7628d6152fea6b54f963 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 22 Oct 2024 14:10:09 +1100
Subject: [PATCH 020/282] [lvgl] Allow strings to be interpreted as integers
 (Bugfix) (#7652)

---
 esphome/components/lvgl/lv_validation.py | 6 ++----
 tests/components/lvgl/lvgl-package.yaml  | 4 ++--
 2 files changed, 4 insertions(+), 6 deletions(-)

diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py
index fd840cc417..9af25a4e90 100644
--- a/esphome/components/lvgl/lv_validation.py
+++ b/esphome/components/lvgl/lv_validation.py
@@ -274,10 +274,8 @@ def size_validator(value):
         return ["SIZE_CONTENT", "number of pixels", "percentage"]
     if isinstance(value, str) and value.lower().endswith("px"):
         value = cv.int_(value[:-2])
-    if isinstance(value, str) and not value.endswith("%"):
-        if value.upper() == "SIZE_CONTENT":
-            return "LV_SIZE_CONTENT"
-        raise cv.Invalid("must be 'size_content', a percentage or an integer (pixels)")
+    if isinstance(value, str) and value.upper() == "SIZE_CONTENT":
+        return "LV_SIZE_CONTENT"
     return pixels_or_percent_validator(value)
 
 
diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml
index cef76396c2..7fd824a87b 100644
--- a/tests/components/lvgl/lvgl-package.yaml
+++ b/tests/components/lvgl/lvgl-package.yaml
@@ -422,7 +422,7 @@ lvgl:
             id: lv_image
             src: cat_image
             align: top_left
-            y: 50
+            y: "50"
         - tileview:
             id: tileview_id
             scrollbar_mode: active
@@ -461,7 +461,7 @@ lvgl:
               bg_opa: transp
             knob:
               radius: 1
-              width: 4
+              width: "4"
               height: 10%
               bg_color: 0x000000
             width: 100%

From 2597975ae00dde096522b80824dbf9915a0d2fd7 Mon Sep 17 00:00:00 2001
From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com>
Date: Tue, 22 Oct 2024 05:29:16 +0200
Subject: [PATCH 021/282] [rtttl] Add `get_gain()` (#7647)

---
 esphome/components/rtttl/rtttl.cpp | 5 ++++-
 esphome/components/rtttl/rtttl.h   | 1 +
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp
index 495b5c1c8a..db4cc731e4 100644
--- a/esphome/components/rtttl/rtttl.cpp
+++ b/esphome/components/rtttl/rtttl.cpp
@@ -26,7 +26,10 @@ inline double deg2rad(double degrees) {
   return degrees * PI_ON_180;
 }
 
-void Rtttl::dump_config() { ESP_LOGCONFIG(TAG, "Rtttl"); }
+void Rtttl::dump_config() {
+  ESP_LOGCONFIG(TAG, "Rtttl:");
+  ESP_LOGCONFIG(TAG, "  Gain: %f", gain_);
+}
 
 void Rtttl::play(std::string rtttl) {
   if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) {
diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h
index 3cb6e3f5fb..10c290c5fb 100644
--- a/esphome/components/rtttl/rtttl.h
+++ b/esphome/components/rtttl/rtttl.h
@@ -39,6 +39,7 @@ class Rtttl : public Component {
 #ifdef USE_SPEAKER
   void set_speaker(speaker::Speaker *speaker) { this->speaker_ = speaker; }
 #endif
+  float get_gain() { return gain_; }
   void set_gain(float gain) {
     if (gain < 0.1f)
       gain = 0.1f;

From a932ca2f64213e2af34c4d2e25f7f82766e98ccb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= <contact@rodrigomartin.dev>
Date: Tue, 22 Oct 2024 05:38:25 +0200
Subject: [PATCH 022/282] feat(MQTT): Add subscribe QoS to discovery (#7648)

---
 esphome/components/mqtt/__init__.py        | 3 +++
 esphome/components/mqtt/mqtt_component.cpp | 6 ++++++
 esphome/components/mqtt/mqtt_component.h   | 4 ++++
 esphome/components/mqtt/mqtt_const.h       | 2 ++
 esphome/config_validation.py               | 4 +++-
 esphome/const.py                           | 1 +
 tests/components/mqtt/common.yaml          | 1 +
 7 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py
index 336d928f71..8851581ea0 100644
--- a/esphome/components/mqtt/__init__.py
+++ b/esphome/components/mqtt/__init__.py
@@ -41,6 +41,7 @@ from esphome.const import (
     CONF_SHUTDOWN_MESSAGE,
     CONF_SSL_FINGERPRINTS,
     CONF_STATE_TOPIC,
+    CONF_SUBSCRIBE_QOS,
     CONF_TOPIC,
     CONF_TOPIC_PREFIX,
     CONF_TRIGGER_ID,
@@ -518,6 +519,8 @@ async def register_mqtt_component(var, config):
         cg.add(var.set_qos(config[CONF_QOS]))
     if CONF_RETAIN in config:
         cg.add(var.set_retain(config[CONF_RETAIN]))
+    if CONF_SUBSCRIBE_QOS in config:
+        cg.add(var.set_subscribe_qos(config[CONF_SUBSCRIBE_QOS]))
     if not config.get(CONF_DISCOVERY, True):
         cg.add(var.disable_discovery())
     if CONF_STATE_TOPIC in config:
diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp
index 295fbba5e5..3b9d367a7b 100644
--- a/esphome/components/mqtt/mqtt_component.cpp
+++ b/esphome/components/mqtt/mqtt_component.cpp
@@ -16,6 +16,8 @@ static const char *const TAG = "mqtt.component";
 
 void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; }
 
+void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; }
+
 void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; }
 
 std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const {
@@ -76,6 +78,10 @@ bool MQTTComponent::send_discovery_() {
         config.command_topic = true;
 
         this->send_discovery(root, config);
+        // Set subscription QoS (default is 0)
+        if (this->subscribe_qos_ != 0) {
+          root[MQTT_QOS] = this->subscribe_qos_;
+        }
 
         // Fields from EntityBase
         if (this->get_entity()->has_own_name()) {
diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h
index 147840d11f..01ba98ad40 100644
--- a/esphome/components/mqtt/mqtt_component.h
+++ b/esphome/components/mqtt/mqtt_component.h
@@ -89,6 +89,9 @@ class MQTTComponent : public Component {
   void disable_discovery();
   bool is_discovery_enabled() const;
 
+  /// Set the QOS for subscribe messages (used in discovery).
+  void set_subscribe_qos(uint8_t qos);
+
   /// Override this method to return the component type (e.g. "light", "sensor", ...)
   virtual std::string component_type() const = 0;
 
@@ -204,6 +207,7 @@ class MQTTComponent : public Component {
   bool command_retain_{false};
   bool retain_{true};
   uint8_t qos_{0};
+  uint8_t subscribe_qos_{0};
   bool discovery_enabled_{true};
   bool resend_state_{false};
 };
diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h
index 71f169fbe8..c1c40c4b6d 100644
--- a/esphome/components/mqtt/mqtt_const.h
+++ b/esphome/components/mqtt/mqtt_const.h
@@ -180,6 +180,7 @@ constexpr const char *const MQTT_PRESET_MODE_COMMAND_TOPIC = "pr_mode_cmd_t";
 constexpr const char *const MQTT_PRESET_MODE_STATE_TOPIC = "pr_mode_stat_t";
 constexpr const char *const MQTT_PRESET_MODE_VALUE_TEMPLATE = "pr_mode_val_tpl";
 constexpr const char *const MQTT_PRESET_MODES = "pr_modes";
+constexpr const char *const MQTT_QOS = "qos";
 constexpr const char *const MQTT_RED_TEMPLATE = "r_tpl";
 constexpr const char *const MQTT_RETAIN = "ret";
 constexpr const char *const MQTT_RGB_COMMAND_TEMPLATE = "rgb_cmd_tpl";
@@ -441,6 +442,7 @@ constexpr const char *const MQTT_PRESET_MODE_COMMAND_TOPIC = "preset_mode_comman
 constexpr const char *const MQTT_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic";
 constexpr const char *const MQTT_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template";
 constexpr const char *const MQTT_PRESET_MODES = "preset_modes";
+constexpr const char *const MQTT_QOS = "qos";
 constexpr const char *const MQTT_RED_TEMPLATE = "red_template";
 constexpr const char *const MQTT_RETAIN = "retain";
 constexpr const char *const MQTT_RGB_COMMAND_TEMPLATE = "rgb_command_template";
diff --git a/esphome/config_validation.py b/esphome/config_validation.py
index a7525a62dd..98b81ec328 100644
--- a/esphome/config_validation.py
+++ b/esphome/config_validation.py
@@ -40,6 +40,7 @@ from esphome.const import (
     CONF_SECOND,
     CONF_SETUP_PRIORITY,
     CONF_STATE_TOPIC,
+    CONF_SUBSCRIBE_QOS,
     CONF_TOPIC,
     CONF_TYPE,
     CONF_TYPE_ID,
@@ -1893,9 +1894,10 @@ MQTT_COMPONENT_AVAILABILITY_SCHEMA = Schema(
 
 MQTT_COMPONENT_SCHEMA = Schema(
     {
-        Optional(CONF_QOS): All(requires_component("mqtt"), int_range(min=0, max=2)),
+        Optional(CONF_QOS): All(requires_component("mqtt"), mqtt_qos),
         Optional(CONF_RETAIN): All(requires_component("mqtt"), boolean),
         Optional(CONF_DISCOVERY): All(requires_component("mqtt"), boolean),
+        Optional(CONF_SUBSCRIBE_QOS): All(requires_component("mqtt"), mqtt_qos),
         Optional(CONF_STATE_TOPIC): All(requires_component("mqtt"), publish_topic),
         Optional(CONF_AVAILABILITY): All(
             requires_component("mqtt"), Any(None, MQTT_COMPONENT_AVAILABILITY_SCHEMA)
diff --git a/esphome/const.py b/esphome/const.py
index a3a4318d69..54ebb2815f 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -819,6 +819,7 @@ CONF_STOP = "stop"
 CONF_STOP_ACTION = "stop_action"
 CONF_STORE_BASELINE = "store_baseline"
 CONF_SUBNET = "subnet"
+CONF_SUBSCRIBE_QOS = "subscribe_qos"
 CONF_SUBSTITUTIONS = "substitutions"
 CONF_SUM = "sum"
 CONF_SUPPLEMENTAL_COOLING_ACTION = "supplemental_cooling_action"
diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml
index f7a727ab2f..5ed6335d65 100644
--- a/tests/components/mqtt/common.yaml
+++ b/tests/components/mqtt/common.yaml
@@ -227,6 +227,7 @@ datetime:
     type: date
     state_topic: some/topic/date
     qos: 2
+    subscribe_qos: 2
     set_action:
       - logger.log: "set_value"
     on_value:

From 7c0543862ad593916e1230d1dc2f2dec8e61c507 Mon Sep 17 00:00:00 2001
From: Kyle Cascade <kyle@cascade.family>
Date: Mon, 21 Oct 2024 21:11:23 -0700
Subject: [PATCH 023/282] Humanized the missing MQTT log topic error message
 (#7634)

---
 esphome/mqtt.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/esphome/mqtt.py b/esphome/mqtt.py
index c1c45799cc..d55fb0202d 100644
--- a/esphome/mqtt.py
+++ b/esphome/mqtt.py
@@ -209,6 +209,12 @@ def show_logs(config, topic=None, username=None, password=None, client_id=None):
     elif CONF_MQTT in config:
         conf = config[CONF_MQTT]
         if CONF_LOG_TOPIC in conf:
+            if config[CONF_MQTT][CONF_LOG_TOPIC] is None:
+                _LOGGER.error("MQTT log topic set to null, can't start MQTT logs")
+                return 1
+            if CONF_TOPIC not in config[CONF_MQTT][CONF_LOG_TOPIC]:
+                _LOGGER.error("MQTT log topic not available, can't start MQTT logs")
+                return 1
             topic = config[CONF_MQTT][CONF_LOG_TOPIC][CONF_TOPIC]
         elif CONF_TOPIC_PREFIX in config[CONF_MQTT]:
             topic = f"{config[CONF_MQTT][CONF_TOPIC_PREFIX]}/debug"

From 68844c48698c6983a3d22498dbe70ee20c9835bf Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Wed, 23 Oct 2024 10:16:55 +1100
Subject: [PATCH 024/282] [lvgl] Some properties were not templatable (Bugfix)
 (#7655)

---
 esphome/components/lvgl/lv_validation.py |  4 ++++
 esphome/components/lvgl/schemas.py       | 14 +++++++-------
 tests/components/lvgl/lvgl-package.yaml  |  7 +++++++
 3 files changed, 18 insertions(+), 7 deletions(-)

diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py
index 9af25a4e90..b91b0905df 100644
--- a/esphome/components/lvgl/lv_validation.py
+++ b/esphome/components/lvgl/lv_validation.py
@@ -267,6 +267,9 @@ def angle(value):
     return int(cv.float_range(0.0, 360.0)(cv.angle(value)) * 10)
 
 
+lv_angle = LValidator(angle, uint32)
+
+
 @schema_extractor("one_of")
 def size_validator(value):
     """A size in one axis - one of "size_content", a number (pixels) or a percentage"""
@@ -401,6 +404,7 @@ class TextValidator(LValidator):
 lv_text = TextValidator()
 lv_float = LValidator(cv.float_, cg.float_)
 lv_int = LValidator(cv.int_, cg.int_)
+lv_positive_int = LValidator(cv.positive_int, cg.int_)
 lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255))
 
 
diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py
index 7599d64416..bb14c11ddd 100644
--- a/esphome/components/lvgl/schemas.py
+++ b/esphome/components/lvgl/schemas.py
@@ -91,7 +91,7 @@ STYLE_PROPS = {
     "arc_opa": lvalid.opacity,
     "arc_color": lvalid.lv_color,
     "arc_rounded": lvalid.lv_bool,
-    "arc_width": cv.positive_int,
+    "arc_width": lvalid.lv_positive_int,
     "anim_time": lvalid.lv_milliseconds,
     "bg_color": lvalid.lv_color,
     "bg_grad": lv_gradient,
@@ -111,7 +111,7 @@ STYLE_PROPS = {
     "border_side": df.LvConstant(
         "LV_BORDER_SIDE_", "NONE", "TOP", "BOTTOM", "LEFT", "RIGHT", "INTERNAL"
     ).several_of,
-    "border_width": cv.positive_int,
+    "border_width": lvalid.lv_positive_int,
     "clip_corner": lvalid.lv_bool,
     "color_filter_opa": lvalid.opacity,
     "height": lvalid.size,
@@ -134,11 +134,11 @@ STYLE_PROPS = {
     "pad_right": lvalid.pixels,
     "pad_top": lvalid.pixels,
     "shadow_color": lvalid.lv_color,
-    "shadow_ofs_x": cv.int_,
-    "shadow_ofs_y": cv.int_,
+    "shadow_ofs_x": lvalid.lv_int,
+    "shadow_ofs_y": lvalid.lv_int,
     "shadow_opa": lvalid.opacity,
-    "shadow_spread": cv.int_,
-    "shadow_width": cv.positive_int,
+    "shadow_spread": lvalid.lv_int,
+    "shadow_width": lvalid.lv_positive_int,
     "text_align": df.LvConstant(
         "LV_TEXT_ALIGN_", "LEFT", "CENTER", "RIGHT", "AUTO"
     ).one_of,
@@ -150,7 +150,7 @@ STYLE_PROPS = {
     "text_letter_space": cv.positive_int,
     "text_line_space": cv.positive_int,
     "text_opa": lvalid.opacity,
-    "transform_angle": lvalid.angle,
+    "transform_angle": lvalid.lv_angle,
     "transform_height": lvalid.pixels_or_percent,
     "transform_pivot_x": lvalid.pixels_or_percent,
     "transform_pivot_y": lvalid.pixels_or_percent,
diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml
index 7fd824a87b..4962a71596 100644
--- a/tests/components/lvgl/lvgl-package.yaml
+++ b/tests/components/lvgl/lvgl-package.yaml
@@ -323,6 +323,13 @@ lvgl:
             id: button_button
             width: 20%
             height: 10%
+            transform_angle: !lambda return 180*100;
+            arc_width: !lambda return 4;
+            border_width: !lambda return 6;
+            shadow_ofs_x: !lambda return 6;
+            shadow_ofs_y: !lambda return 6;
+            shadow_spread: !lambda return 6;
+            shadow_width: !lambda return 6;
             pressed:
               bg_color: light_blue
             checkable: true

From dd8d25e43f6d30f93391aeee207a912dd52907d4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?G=C3=A1bor=20Poczkodi?= <gabest11@gmail.com>
Date: Wed, 23 Oct 2024 05:23:10 +0200
Subject: [PATCH 025/282] i2c_device (#7641)

---
 CODEOWNERS                                    |  1 +
 esphome/components/i2c_device/__init__.py     | 26 +++++++++++++++++++
 esphome/components/i2c_device/i2c_device.cpp  | 17 ++++++++++++
 esphome/components/i2c_device/i2c_device.h    | 18 +++++++++++++
 .../components/i2c_device/test.esp32-ard.yaml |  8 ++++++
 .../i2c_device/test.esp32-c3-ard.yaml         |  8 ++++++
 .../i2c_device/test.esp32-c3-idf.yaml         |  8 ++++++
 .../components/i2c_device/test.esp32-idf.yaml |  8 ++++++
 .../i2c_device/test.esp8266-ard.yaml          |  8 ++++++
 .../i2c_device/test.rp2040-ard.yaml           |  8 ++++++
 10 files changed, 110 insertions(+)
 create mode 100644 esphome/components/i2c_device/__init__.py
 create mode 100644 esphome/components/i2c_device/i2c_device.cpp
 create mode 100644 esphome/components/i2c_device/i2c_device.h
 create mode 100644 tests/components/i2c_device/test.esp32-ard.yaml
 create mode 100644 tests/components/i2c_device/test.esp32-c3-ard.yaml
 create mode 100644 tests/components/i2c_device/test.esp32-c3-idf.yaml
 create mode 100644 tests/components/i2c_device/test.esp32-idf.yaml
 create mode 100644 tests/components/i2c_device/test.esp8266-ard.yaml
 create mode 100644 tests/components/i2c_device/test.rp2040-ard.yaml

diff --git a/CODEOWNERS b/CODEOWNERS
index 53300d6740..616b18293d 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -198,6 +198,7 @@ esphome/components/htu31d/* @betterengineering
 esphome/components/hydreon_rgxx/* @functionpointer
 esphome/components/hyt271/* @Philippe12
 esphome/components/i2c/* @esphome/core
+esphome/components/i2c_device/* @gabest11
 esphome/components/i2s_audio/* @jesserockz
 esphome/components/i2s_audio/media_player/* @jesserockz
 esphome/components/i2s_audio/microphone/* @jesserockz
diff --git a/esphome/components/i2c_device/__init__.py b/esphome/components/i2c_device/__init__.py
new file mode 100644
index 0000000000..e145ba56f8
--- /dev/null
+++ b/esphome/components/i2c_device/__init__.py
@@ -0,0 +1,26 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import i2c
+from esphome.const import CONF_ID
+
+DEPENDENCIES = ["i2c"]
+CODEOWNERS = ["@gabest11"]
+MULTI_CONF = True
+
+i2c_device_ns = cg.esphome_ns.namespace("i2c_device")
+
+I2CDeviceComponent = i2c_device_ns.class_(
+    "I2CDeviceComponent", cg.Component, i2c.I2CDevice
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(CONF_ID): cv.declare_id(I2CDeviceComponent),
+    }
+).extend(i2c.i2c_device_schema(None))
+
+
+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)
diff --git a/esphome/components/i2c_device/i2c_device.cpp b/esphome/components/i2c_device/i2c_device.cpp
new file mode 100644
index 0000000000..455c68fbed
--- /dev/null
+++ b/esphome/components/i2c_device/i2c_device.cpp
@@ -0,0 +1,17 @@
+#include "i2c_device.h"
+#include "esphome/core/log.h"
+#include "esphome/core/hal.h"
+#include <cinttypes>
+
+namespace esphome {
+namespace i2c_device {
+
+static const char *const TAG = "i2c_device";
+
+void I2CDeviceComponent::dump_config() {
+  ESP_LOGCONFIG(TAG, "I2CDevice");
+  LOG_I2C_DEVICE(this);
+}
+
+}  // namespace i2c_device
+}  // namespace esphome
diff --git a/esphome/components/i2c_device/i2c_device.h b/esphome/components/i2c_device/i2c_device.h
new file mode 100644
index 0000000000..ab118e3e89
--- /dev/null
+++ b/esphome/components/i2c_device/i2c_device.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/i2c/i2c.h"
+
+namespace esphome {
+namespace i2c_device {
+
+class I2CDeviceComponent : public Component, public i2c::I2CDevice {
+ public:
+  void dump_config() override;
+  float get_setup_priority() const override { return setup_priority::DATA; }
+
+ protected:
+};
+
+}  // namespace i2c_device
+}  // namespace esphome
diff --git a/tests/components/i2c_device/test.esp32-ard.yaml b/tests/components/i2c_device/test.esp32-ard.yaml
new file mode 100644
index 0000000000..6169d113f8
--- /dev/null
+++ b/tests/components/i2c_device/test.esp32-ard.yaml
@@ -0,0 +1,8 @@
+i2c:
+  - id: i2c_i2c
+    scl: 16
+    sda: 17
+
+i2c_device:
+  id: i2cdev
+  address: 0x2C
diff --git a/tests/components/i2c_device/test.esp32-c3-ard.yaml b/tests/components/i2c_device/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..5d53d12208
--- /dev/null
+++ b/tests/components/i2c_device/test.esp32-c3-ard.yaml
@@ -0,0 +1,8 @@
+i2c:
+  - id: i2c_i2c
+    scl: 5
+    sda: 4
+
+i2c_device:
+  id: i2cdev
+  address: 0x2C
diff --git a/tests/components/i2c_device/test.esp32-c3-idf.yaml b/tests/components/i2c_device/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..5d53d12208
--- /dev/null
+++ b/tests/components/i2c_device/test.esp32-c3-idf.yaml
@@ -0,0 +1,8 @@
+i2c:
+  - id: i2c_i2c
+    scl: 5
+    sda: 4
+
+i2c_device:
+  id: i2cdev
+  address: 0x2C
diff --git a/tests/components/i2c_device/test.esp32-idf.yaml b/tests/components/i2c_device/test.esp32-idf.yaml
new file mode 100644
index 0000000000..6169d113f8
--- /dev/null
+++ b/tests/components/i2c_device/test.esp32-idf.yaml
@@ -0,0 +1,8 @@
+i2c:
+  - id: i2c_i2c
+    scl: 16
+    sda: 17
+
+i2c_device:
+  id: i2cdev
+  address: 0x2C
diff --git a/tests/components/i2c_device/test.esp8266-ard.yaml b/tests/components/i2c_device/test.esp8266-ard.yaml
new file mode 100644
index 0000000000..5d53d12208
--- /dev/null
+++ b/tests/components/i2c_device/test.esp8266-ard.yaml
@@ -0,0 +1,8 @@
+i2c:
+  - id: i2c_i2c
+    scl: 5
+    sda: 4
+
+i2c_device:
+  id: i2cdev
+  address: 0x2C
diff --git a/tests/components/i2c_device/test.rp2040-ard.yaml b/tests/components/i2c_device/test.rp2040-ard.yaml
new file mode 100644
index 0000000000..5d53d12208
--- /dev/null
+++ b/tests/components/i2c_device/test.rp2040-ard.yaml
@@ -0,0 +1,8 @@
+i2c:
+  - id: i2c_i2c
+    scl: 5
+    sda: 4
+
+i2c_device:
+  id: i2cdev
+  address: 0x2C

From fdebf041967549866f438431284c0690bec3d6da Mon Sep 17 00:00:00 2001
From: Kevin Ahrendt <kevin.ahrendt@nabucasa.com>
Date: Wed, 23 Oct 2024 13:25:31 -0400
Subject: [PATCH 026/282] [voice_assistant] Bugfix: Fix crash on start (#7662)

---
 .../voice_assistant/voice_assistant.cpp       | 54 +++++++++++--------
 .../voice_assistant/voice_assistant.h         |  6 +--
 2 files changed, 34 insertions(+), 26 deletions(-)

diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp
index 0b53e74ba3..6f164f69d3 100644
--- a/esphome/components/voice_assistant/voice_assistant.cpp
+++ b/esphome/components/voice_assistant/voice_assistant.cpp
@@ -433,16 +433,18 @@ void VoiceAssistant::loop() {
 
 #ifdef USE_SPEAKER
 void VoiceAssistant::write_speaker_() {
-  if (this->speaker_buffer_size_ > 0) {
-    size_t write_chunk = std::min<size_t>(this->speaker_buffer_size_, 4 * 1024);
-    size_t written = this->speaker_->play(this->speaker_buffer_, write_chunk);
-    if (written > 0) {
-      memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written);
-      this->speaker_buffer_size_ -= written;
-      this->speaker_buffer_index_ -= written;
-      this->set_timeout("speaker-timeout", 5000, [this]() { this->speaker_->stop(); });
-    } else {
-      ESP_LOGV(TAG, "Speaker buffer full, trying again next loop");
+  if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) {
+    if (this->speaker_buffer_size_ > 0) {
+      size_t write_chunk = std::min<size_t>(this->speaker_buffer_size_, 4 * 1024);
+      size_t written = this->speaker_->play(this->speaker_buffer_, write_chunk);
+      if (written > 0) {
+        memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written);
+        this->speaker_buffer_size_ -= written;
+        this->speaker_buffer_index_ -= written;
+        this->set_timeout("speaker-timeout", 5000, [this]() { this->speaker_->stop(); });
+      } else {
+        ESP_LOGV(TAG, "Speaker buffer full, trying again next loop");
+      }
     }
   }
 }
@@ -772,16 +774,20 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
     }
     case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: {
 #ifdef USE_SPEAKER
-      this->wait_for_stream_end_ = true;
-      ESP_LOGD(TAG, "TTS stream start");
-      this->defer([this] { this->tts_stream_start_trigger_->trigger(); });
+      if (this->speaker_ != nullptr) {
+        this->wait_for_stream_end_ = true;
+        ESP_LOGD(TAG, "TTS stream start");
+        this->defer([this] { this->tts_stream_start_trigger_->trigger(); });
+      }
 #endif
       break;
     }
     case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: {
 #ifdef USE_SPEAKER
-      this->stream_ended_ = true;
-      ESP_LOGD(TAG, "TTS stream end");
+      if (this->speaker_ != nullptr) {
+        this->stream_ended_ = true;
+        ESP_LOGD(TAG, "TTS stream end");
+      }
 #endif
       break;
     }
@@ -802,14 +808,16 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
 
 void VoiceAssistant::on_audio(const api::VoiceAssistantAudio &msg) {
 #ifdef USE_SPEAKER  // We should never get to this function if there is no speaker anyway
-  if (this->speaker_buffer_index_ + msg.data.length() < SPEAKER_BUFFER_SIZE) {
-    memcpy(this->speaker_buffer_ + this->speaker_buffer_index_, msg.data.data(), msg.data.length());
-    this->speaker_buffer_index_ += msg.data.length();
-    this->speaker_buffer_size_ += msg.data.length();
-    this->speaker_bytes_received_ += msg.data.length();
-    ESP_LOGV(TAG, "Received audio: %u bytes from API", msg.data.length());
-  } else {
-    ESP_LOGE(TAG, "Cannot receive audio, buffer is full");
+  if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) {
+    if (this->speaker_buffer_index_ + msg.data.length() < SPEAKER_BUFFER_SIZE) {
+      memcpy(this->speaker_buffer_ + this->speaker_buffer_index_, msg.data.data(), msg.data.length());
+      this->speaker_buffer_index_ += msg.data.length();
+      this->speaker_buffer_size_ += msg.data.length();
+      this->speaker_bytes_received_ += msg.data.length();
+      ESP_LOGV(TAG, "Received audio: %u bytes from API", msg.data.length());
+    } else {
+      ESP_LOGE(TAG, "Cannot receive audio, buffer is full");
+    }
   }
 #endif
 }
diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h
index 870e2acdaa..0016d3157c 100644
--- a/esphome/components/voice_assistant/voice_assistant.h
+++ b/esphome/components/voice_assistant/voice_assistant.h
@@ -250,7 +250,7 @@ class VoiceAssistant : public Component {
 #ifdef USE_SPEAKER
   void write_speaker_();
   speaker::Speaker *speaker_{nullptr};
-  uint8_t *speaker_buffer_;
+  uint8_t *speaker_buffer_{nullptr};
   size_t speaker_buffer_index_{0};
   size_t speaker_buffer_size_{0};
   size_t speaker_bytes_received_{0};
@@ -282,8 +282,8 @@ class VoiceAssistant : public Component {
   float volume_multiplier_;
   uint32_t conversation_timeout_;
 
-  uint8_t *send_buffer_;
-  int16_t *input_buffer_;
+  uint8_t *send_buffer_{nullptr};
+  int16_t *input_buffer_{nullptr};
 
   bool continuous_{false};
   bool silence_detection_;

From 833565feb985e91c9512774a363ea2e5ca79d267 Mon Sep 17 00:00:00 2001
From: Kyle Cascade <kyle@cascade.family>
Date: Mon, 21 Oct 2024 21:11:23 -0700
Subject: [PATCH 027/282] Humanized the missing MQTT log topic error message
 (#7634)

---
 esphome/mqtt.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/esphome/mqtt.py b/esphome/mqtt.py
index c1c45799cc..d55fb0202d 100644
--- a/esphome/mqtt.py
+++ b/esphome/mqtt.py
@@ -209,6 +209,12 @@ def show_logs(config, topic=None, username=None, password=None, client_id=None):
     elif CONF_MQTT in config:
         conf = config[CONF_MQTT]
         if CONF_LOG_TOPIC in conf:
+            if config[CONF_MQTT][CONF_LOG_TOPIC] is None:
+                _LOGGER.error("MQTT log topic set to null, can't start MQTT logs")
+                return 1
+            if CONF_TOPIC not in config[CONF_MQTT][CONF_LOG_TOPIC]:
+                _LOGGER.error("MQTT log topic not available, can't start MQTT logs")
+                return 1
             topic = config[CONF_MQTT][CONF_LOG_TOPIC][CONF_TOPIC]
         elif CONF_TOPIC_PREFIX in config[CONF_MQTT]:
             topic = f"{config[CONF_MQTT][CONF_TOPIC_PREFIX]}/debug"

From 8d90d256bf2518c46676a0a7dfe37ef3dd1868c8 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Wed, 23 Oct 2024 10:16:55 +1100
Subject: [PATCH 028/282] [lvgl] Some properties were not templatable (Bugfix)
 (#7655)

---
 esphome/components/lvgl/lv_validation.py |  4 ++++
 esphome/components/lvgl/schemas.py       | 14 +++++++-------
 tests/components/lvgl/lvgl-package.yaml  |  7 +++++++
 3 files changed, 18 insertions(+), 7 deletions(-)

diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py
index fd840cc417..a58fbc33e0 100644
--- a/esphome/components/lvgl/lv_validation.py
+++ b/esphome/components/lvgl/lv_validation.py
@@ -267,6 +267,9 @@ def angle(value):
     return int(cv.float_range(0.0, 360.0)(cv.angle(value)) * 10)
 
 
+lv_angle = LValidator(angle, uint32)
+
+
 @schema_extractor("one_of")
 def size_validator(value):
     """A size in one axis - one of "size_content", a number (pixels) or a percentage"""
@@ -403,6 +406,7 @@ class TextValidator(LValidator):
 lv_text = TextValidator()
 lv_float = LValidator(cv.float_, cg.float_)
 lv_int = LValidator(cv.int_, cg.int_)
+lv_positive_int = LValidator(cv.positive_int, cg.int_)
 lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255))
 
 
diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py
index 780057623a..0d5a609a2d 100644
--- a/esphome/components/lvgl/schemas.py
+++ b/esphome/components/lvgl/schemas.py
@@ -91,7 +91,7 @@ STYLE_PROPS = {
     "arc_opa": lvalid.opacity,
     "arc_color": lvalid.lv_color,
     "arc_rounded": lvalid.lv_bool,
-    "arc_width": cv.positive_int,
+    "arc_width": lvalid.lv_positive_int,
     "anim_time": lvalid.lv_milliseconds,
     "bg_color": lvalid.lv_color,
     "bg_grad": lv_gradient,
@@ -111,7 +111,7 @@ STYLE_PROPS = {
     "border_side": df.LvConstant(
         "LV_BORDER_SIDE_", "NONE", "TOP", "BOTTOM", "LEFT", "RIGHT", "INTERNAL"
     ).several_of,
-    "border_width": cv.positive_int,
+    "border_width": lvalid.lv_positive_int,
     "clip_corner": lvalid.lv_bool,
     "color_filter_opa": lvalid.opacity,
     "height": lvalid.size,
@@ -134,11 +134,11 @@ STYLE_PROPS = {
     "pad_right": lvalid.pixels,
     "pad_top": lvalid.pixels,
     "shadow_color": lvalid.lv_color,
-    "shadow_ofs_x": cv.int_,
-    "shadow_ofs_y": cv.int_,
+    "shadow_ofs_x": lvalid.lv_int,
+    "shadow_ofs_y": lvalid.lv_int,
     "shadow_opa": lvalid.opacity,
-    "shadow_spread": cv.int_,
-    "shadow_width": cv.positive_int,
+    "shadow_spread": lvalid.lv_int,
+    "shadow_width": lvalid.lv_positive_int,
     "text_align": df.LvConstant(
         "LV_TEXT_ALIGN_", "LEFT", "CENTER", "RIGHT", "AUTO"
     ).one_of,
@@ -150,7 +150,7 @@ STYLE_PROPS = {
     "text_letter_space": cv.positive_int,
     "text_line_space": cv.positive_int,
     "text_opa": lvalid.opacity,
-    "transform_angle": lvalid.angle,
+    "transform_angle": lvalid.lv_angle,
     "transform_height": lvalid.pixels_or_percent,
     "transform_pivot_x": lvalid.pixels_or_percent,
     "transform_pivot_y": lvalid.pixels_or_percent,
diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml
index 1770c1bfbc..2e05cf10d3 100644
--- a/tests/components/lvgl/lvgl-package.yaml
+++ b/tests/components/lvgl/lvgl-package.yaml
@@ -299,6 +299,13 @@ lvgl:
             id: button_button
             width: 20%
             height: 10%
+            transform_angle: !lambda return 180*100;
+            arc_width: !lambda return 4;
+            border_width: !lambda return 6;
+            shadow_ofs_x: !lambda return 6;
+            shadow_ofs_y: !lambda return 6;
+            shadow_spread: !lambda return 6;
+            shadow_width: !lambda return 6;
             pressed:
               bg_color: light_blue
             checkable: true

From 156ad773c91edca8b14a8518363287e027ef8147 Mon Sep 17 00:00:00 2001
From: Kevin Ahrendt <kevin.ahrendt@nabucasa.com>
Date: Wed, 23 Oct 2024 13:25:31 -0400
Subject: [PATCH 029/282] [voice_assistant] Bugfix: Fix crash on start (#7662)

---
 .../voice_assistant/voice_assistant.cpp       | 54 +++++++++++--------
 .../voice_assistant/voice_assistant.h         |  6 +--
 2 files changed, 34 insertions(+), 26 deletions(-)

diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp
index 0b53e74ba3..6f164f69d3 100644
--- a/esphome/components/voice_assistant/voice_assistant.cpp
+++ b/esphome/components/voice_assistant/voice_assistant.cpp
@@ -433,16 +433,18 @@ void VoiceAssistant::loop() {
 
 #ifdef USE_SPEAKER
 void VoiceAssistant::write_speaker_() {
-  if (this->speaker_buffer_size_ > 0) {
-    size_t write_chunk = std::min<size_t>(this->speaker_buffer_size_, 4 * 1024);
-    size_t written = this->speaker_->play(this->speaker_buffer_, write_chunk);
-    if (written > 0) {
-      memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written);
-      this->speaker_buffer_size_ -= written;
-      this->speaker_buffer_index_ -= written;
-      this->set_timeout("speaker-timeout", 5000, [this]() { this->speaker_->stop(); });
-    } else {
-      ESP_LOGV(TAG, "Speaker buffer full, trying again next loop");
+  if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) {
+    if (this->speaker_buffer_size_ > 0) {
+      size_t write_chunk = std::min<size_t>(this->speaker_buffer_size_, 4 * 1024);
+      size_t written = this->speaker_->play(this->speaker_buffer_, write_chunk);
+      if (written > 0) {
+        memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written);
+        this->speaker_buffer_size_ -= written;
+        this->speaker_buffer_index_ -= written;
+        this->set_timeout("speaker-timeout", 5000, [this]() { this->speaker_->stop(); });
+      } else {
+        ESP_LOGV(TAG, "Speaker buffer full, trying again next loop");
+      }
     }
   }
 }
@@ -772,16 +774,20 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
     }
     case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: {
 #ifdef USE_SPEAKER
-      this->wait_for_stream_end_ = true;
-      ESP_LOGD(TAG, "TTS stream start");
-      this->defer([this] { this->tts_stream_start_trigger_->trigger(); });
+      if (this->speaker_ != nullptr) {
+        this->wait_for_stream_end_ = true;
+        ESP_LOGD(TAG, "TTS stream start");
+        this->defer([this] { this->tts_stream_start_trigger_->trigger(); });
+      }
 #endif
       break;
     }
     case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: {
 #ifdef USE_SPEAKER
-      this->stream_ended_ = true;
-      ESP_LOGD(TAG, "TTS stream end");
+      if (this->speaker_ != nullptr) {
+        this->stream_ended_ = true;
+        ESP_LOGD(TAG, "TTS stream end");
+      }
 #endif
       break;
     }
@@ -802,14 +808,16 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
 
 void VoiceAssistant::on_audio(const api::VoiceAssistantAudio &msg) {
 #ifdef USE_SPEAKER  // We should never get to this function if there is no speaker anyway
-  if (this->speaker_buffer_index_ + msg.data.length() < SPEAKER_BUFFER_SIZE) {
-    memcpy(this->speaker_buffer_ + this->speaker_buffer_index_, msg.data.data(), msg.data.length());
-    this->speaker_buffer_index_ += msg.data.length();
-    this->speaker_buffer_size_ += msg.data.length();
-    this->speaker_bytes_received_ += msg.data.length();
-    ESP_LOGV(TAG, "Received audio: %u bytes from API", msg.data.length());
-  } else {
-    ESP_LOGE(TAG, "Cannot receive audio, buffer is full");
+  if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) {
+    if (this->speaker_buffer_index_ + msg.data.length() < SPEAKER_BUFFER_SIZE) {
+      memcpy(this->speaker_buffer_ + this->speaker_buffer_index_, msg.data.data(), msg.data.length());
+      this->speaker_buffer_index_ += msg.data.length();
+      this->speaker_buffer_size_ += msg.data.length();
+      this->speaker_bytes_received_ += msg.data.length();
+      ESP_LOGV(TAG, "Received audio: %u bytes from API", msg.data.length());
+    } else {
+      ESP_LOGE(TAG, "Cannot receive audio, buffer is full");
+    }
   }
 #endif
 }
diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h
index 870e2acdaa..0016d3157c 100644
--- a/esphome/components/voice_assistant/voice_assistant.h
+++ b/esphome/components/voice_assistant/voice_assistant.h
@@ -250,7 +250,7 @@ class VoiceAssistant : public Component {
 #ifdef USE_SPEAKER
   void write_speaker_();
   speaker::Speaker *speaker_{nullptr};
-  uint8_t *speaker_buffer_;
+  uint8_t *speaker_buffer_{nullptr};
   size_t speaker_buffer_index_{0};
   size_t speaker_buffer_size_{0};
   size_t speaker_bytes_received_{0};
@@ -282,8 +282,8 @@ class VoiceAssistant : public Component {
   float volume_multiplier_;
   uint32_t conversation_timeout_;
 
-  uint8_t *send_buffer_;
-  int16_t *input_buffer_;
+  uint8_t *send_buffer_{nullptr};
+  int16_t *input_buffer_{nullptr};
 
   bool continuous_{false};
   bool silence_detection_;

From 127acfde6482ebb7e44894af0c17660263166fce Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Thu, 24 Oct 2024 07:15:40 +1300
Subject: [PATCH 030/282] Bump version to 2024.10.2

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

diff --git a/esphome/const.py b/esphome/const.py
index 5fa457b25a..032a4c79a0 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,6 +1,6 @@
 """Constants used by esphome."""
 
-__version__ = "2024.10.1"
+__version__ = "2024.10.2"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 VALID_SUBSTITUTIONS_CHARACTERS = (

From 4289e00ad0ee3f466a60d9044c97157a8070a047 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 24 Oct 2024 08:06:45 +1300
Subject: [PATCH 031/282] Bump actions/cache from 4.1.1 to 4.1.2 in
 /.github/actions/restore-python (#7659)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/actions/restore-python/action.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml
index 1f5812691e..c6978f68c5 100644
--- a/.github/actions/restore-python/action.yml
+++ b/.github/actions/restore-python/action.yml
@@ -22,7 +22,7 @@ runs:
         python-version: ${{ inputs.python-version }}
     - name: Restore Python virtual environment
       id: cache-venv
-      uses: actions/cache/restore@v4.1.1
+      uses: actions/cache/restore@v4.1.2
       with:
         path: venv
         # yamllint disable-line rule:line-length

From 2feffddc550c0241aede210bb273b8e0001084f2 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 24 Oct 2024 08:06:53 +1300
Subject: [PATCH 032/282] Bump actions/cache from 4.1.1 to 4.1.2 (#7660)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/workflows/ci.yml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 178b914a1c..0d2f1c877d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -46,7 +46,7 @@ jobs:
           python-version: ${{ env.DEFAULT_PYTHON }}
       - name: Restore Python virtual environment
         id: cache-venv
-        uses: actions/cache@v4.1.1
+        uses: actions/cache@v4.1.2
         with:
           path: venv
           # yamllint disable-line rule:line-length
@@ -302,14 +302,14 @@ jobs:
 
       - name: Cache platformio
         if: github.ref == 'refs/heads/dev'
-        uses: actions/cache@v4.1.1
+        uses: actions/cache@v4.1.2
         with:
           path: ~/.platformio
           key: platformio-${{ matrix.pio_cache_key }}
 
       - name: Cache platformio
         if: github.ref != 'refs/heads/dev'
-        uses: actions/cache/restore@v4.1.1
+        uses: actions/cache/restore@v4.1.2
         with:
           path: ~/.platformio
           key: platformio-${{ matrix.pio_cache_key }}

From bff0e81ed3863ee960d1612fd64bec78fe34c03b Mon Sep 17 00:00:00 2001
From: Kevin Ahrendt <kevin.ahrendt@nabucasa.com>
Date: Wed, 23 Oct 2024 16:37:38 -0400
Subject: [PATCH 033/282] [speaker, i2s_audio] Support audio_dac component,
 mute actions, and improved logging (#7664)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 CODEOWNERS                                    |  4 +-
 .../components/i2s_audio/speaker/__init__.py  |  2 +-
 .../i2s_audio/speaker/i2s_audio_speaker.cpp   | 72 +++++++++++++++----
 .../i2s_audio/speaker/i2s_audio_speaker.h     | 14 ++--
 esphome/components/speaker/__init__.py        | 31 ++++++--
 esphome/components/speaker/automation.h       | 20 ++++++
 esphome/components/speaker/speaker.h          | 42 ++++++++++-
 tests/components/speaker/test.esp32-ard.yaml  |  2 +
 .../components/speaker/test.esp32-c3-ard.yaml |  2 +
 .../components/speaker/test.esp32-c3-idf.yaml |  2 +
 tests/components/speaker/test.esp32-idf.yaml  | 15 +++-
 11 files changed, 177 insertions(+), 29 deletions(-)

diff --git a/CODEOWNERS b/CODEOWNERS
index 616b18293d..f96e43d5b7 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -202,7 +202,7 @@ esphome/components/i2c_device/* @gabest11
 esphome/components/i2s_audio/* @jesserockz
 esphome/components/i2s_audio/media_player/* @jesserockz
 esphome/components/i2s_audio/microphone/* @jesserockz
-esphome/components/i2s_audio/speaker/* @jesserockz
+esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
 esphome/components/iaqcore/* @yozik04
 esphome/components/ili9xxx/* @clydebarrow @nielsnl68
 esphome/components/improv_base/* @esphome/core
@@ -377,7 +377,7 @@ esphome/components/smt100/* @piechade
 esphome/components/sn74hc165/* @jesserockz
 esphome/components/socket/* @esphome/core
 esphome/components/sonoff_d1/* @anatoly-savchenkov
-esphome/components/speaker/* @jesserockz
+esphome/components/speaker/* @jesserockz @kahrendt
 esphome/components/spi/* @clydebarrow @esphome/core
 esphome/components/spi_device/* @clydebarrow
 esphome/components/spi_led_strip/* @clydebarrow
diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py
index 9fdaced64c..dd43d6cb39 100644
--- a/esphome/components/i2s_audio/speaker/__init__.py
+++ b/esphome/components/i2s_audio/speaker/__init__.py
@@ -17,7 +17,7 @@ from .. import (
 )
 
 AUTO_LOAD = ["audio"]
-CODEOWNERS = ["@jesserockz"]
+CODEOWNERS = ["@jesserockz", "@kahrendt"]
 DEPENDENCIES = ["i2s_audio"]
 
 I2SAudioSpeaker = i2s_audio_ns.class_(
diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
index 4fc489d0a3..cf6c3bbbba 100644
--- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
+++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
@@ -32,6 +32,7 @@ enum SpeakerEventGroupBits : uint32_t {
   STATE_RUNNING = (1 << 11),
   STATE_STOPPING = (1 << 12),
   STATE_STOPPED = (1 << 13),
+  ERR_INVALID_FORMAT = (1 << 14),
   ERR_TASK_FAILED_TO_START = (1 << 15),
   ERR_ESP_INVALID_STATE = (1 << 16),
   ERR_ESP_INVALID_ARG = (1 << 17),
@@ -104,16 +105,6 @@ void I2SAudioSpeaker::setup() {
 void I2SAudioSpeaker::loop() {
   uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
 
-  if (event_group_bits & SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START) {
-    this->status_set_error("Failed to start speaker task");
-  }
-
-  if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) {
-    uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS;
-    ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(err_bit_to_esp_err(error_bits)));
-    this->status_set_warning();
-  }
-
   if (event_group_bits & SpeakerEventGroupBits::STATE_STARTING) {
     ESP_LOGD(TAG, "Starting Speaker");
     this->state_ = speaker::STATE_STARTING;
@@ -139,12 +130,64 @@ void I2SAudioSpeaker::loop() {
       this->speaker_task_handle_ = nullptr;
     }
   }
+
+  if (event_group_bits & SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START) {
+    this->status_set_error("Failed to start speaker task");
+    xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START);
+  }
+
+  if (event_group_bits & SpeakerEventGroupBits::ERR_INVALID_FORMAT) {
+    this->status_set_error("Failed to adjust I2S bus to match the incoming audio");
+    ESP_LOGE(TAG,
+             "Incompatible audio format: sample rate = %" PRIu32 ", channels = %" PRIu8 ", bits per sample = %" PRIu8,
+             this->audio_stream_info_.sample_rate, this->audio_stream_info_.channels,
+             this->audio_stream_info_.bits_per_sample);
+  }
+
+  if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) {
+    uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS;
+    ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(err_bit_to_esp_err(error_bits)));
+    this->status_set_warning();
+  }
 }
 
 void I2SAudioSpeaker::set_volume(float volume) {
   this->volume_ = volume;
-  ssize_t decibel_index = remap<ssize_t, float>(volume, 0.0f, 1.0f, 0, Q15_VOLUME_SCALING_FACTORS.size() - 1);
-  this->q15_volume_factor_ = Q15_VOLUME_SCALING_FACTORS[decibel_index];
+#ifdef USE_AUDIO_DAC
+  if (this->audio_dac_ != nullptr) {
+    if (volume > 0.0) {
+      this->audio_dac_->set_mute_off();
+    }
+    this->audio_dac_->set_volume(volume);
+  } else
+#endif
+  {
+    // Fallback to software volume control by using a Q15 fixed point scaling factor
+    ssize_t decibel_index = remap<ssize_t, float>(volume, 0.0f, 1.0f, 0, Q15_VOLUME_SCALING_FACTORS.size() - 1);
+    this->q15_volume_factor_ = Q15_VOLUME_SCALING_FACTORS[decibel_index];
+  }
+}
+
+void I2SAudioSpeaker::set_mute_state(bool mute_state) {
+  this->mute_state_ = mute_state;
+#ifdef USE_AUDIO_DAC
+  if (this->audio_dac_) {
+    if (mute_state) {
+      this->audio_dac_->set_mute_on();
+    } else {
+      this->audio_dac_->set_mute_off();
+    }
+  } else
+#endif
+  {
+    if (mute_state) {
+      // Fallback to software volume control and scale by 0
+      this->q15_volume_factor_ = 0;
+    } else {
+      // Revert to previous volume when unmuting
+      this->set_volume(this->volume_);
+    }
+  }
 }
 
 size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) {
@@ -275,6 +318,9 @@ void I2SAudioSpeaker::speaker_task(void *params) {
         i2s_zero_dma_buffer(this_speaker->parent_->get_port());
       }
     }
+  } else {
+    // Couldn't configure the I2S port to be compatible with the incoming audio
+    xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_INVALID_FORMAT);
   }
   i2s_zero_dma_buffer(this_speaker->parent_->get_port());
 
@@ -288,7 +334,7 @@ void I2SAudioSpeaker::speaker_task(void *params) {
 }
 
 void I2SAudioSpeaker::start() {
-  if (this->is_failed())
+  if (this->is_failed() || this->status_has_error())
     return;
   if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING))
     return;
diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h
index 245f97d1e7..3c512d4d4d 100644
--- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h
+++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h
@@ -49,11 +49,17 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
 
   bool has_buffered_data() const override;
 
-  /// @brief Sets the volume of the speaker. It is implemented as a software volume control.
-  /// Overrides the default setter to convert the floating point volume to a Q15 fixed-point factor.
-  /// @param volume
+  /// @brief Sets the volume of the speaker. Uses the speaker's configured audio dac component. If unavailble, it is
+  /// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a
+  /// Q15 fixed-point factor.
+  /// @param volume between 0.0 and 1.0
   void set_volume(float volume) override;
-  float get_volume() override { return this->volume_; }
+
+  /// @brief Mutes or unmute the speaker. Uses the speaker's configured audio dac component. If unavailble, it is
+  /// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a
+  /// Q15 fixed-point factor.
+  /// @param mute_state true for muting, false for unmuting
+  void set_mute_state(bool mute_state) override;
 
  protected:
   /// @brief Function for the FreeRTOS task handling audio output.
diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py
index 1bbc0b02ef..7a668dc2f3 100644
--- a/esphome/components/speaker/__init__.py
+++ b/esphome/components/speaker/__init__.py
@@ -1,15 +1,18 @@
 from esphome import automation
 from esphome.automation import maybe_simple_id
 import esphome.codegen as cg
+from esphome.components import audio_dac
 import esphome.config_validation as cv
 from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME
 from esphome.core import CORE
 from esphome.coroutine import coroutine_with_priority
 
-CODEOWNERS = ["@jesserockz"]
+CODEOWNERS = ["@jesserockz", "@kahrendt"]
 
 IS_PLATFORM_COMPONENT = True
 
+CONF_AUDIO_DAC = "audio_dac"
+
 speaker_ns = cg.esphome_ns.namespace("speaker")
 
 Speaker = speaker_ns.class_("Speaker")
@@ -26,6 +29,12 @@ FinishAction = speaker_ns.class_(
 VolumeSetAction = speaker_ns.class_(
     "VolumeSetAction", automation.Action, cg.Parented.template(Speaker)
 )
+MuteOnAction = speaker_ns.class_(
+    "MuteOnAction", automation.Action, cg.Parented.template(Speaker)
+)
+MuteOffAction = speaker_ns.class_(
+    "MuteOffAction", automation.Action, cg.Parented.template(Speaker)
+)
 
 
 IsPlayingCondition = speaker_ns.class_("IsPlayingCondition", automation.Condition)
@@ -33,7 +42,9 @@ IsStoppedCondition = speaker_ns.class_("IsStoppedCondition", automation.Conditio
 
 
 async def setup_speaker_core_(var, config):
-    pass
+    if audio_dac_config := config.get(CONF_AUDIO_DAC):
+        aud_dac = await cg.get_variable(audio_dac_config)
+        cg.add(var.set_audio_dac(aud_dac))
 
 
 async def register_speaker(var, config):
@@ -42,8 +53,11 @@ async def register_speaker(var, config):
     await setup_speaker_core_(var, config)
 
 
-SPEAKER_SCHEMA = cv.Schema({})
-
+SPEAKER_SCHEMA = cv.Schema(
+    {
+        cv.Optional(CONF_AUDIO_DAC): cv.use_id(audio_dac.AudioDac),
+    }
+)
 
 SPEAKER_AUTOMATION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(Speaker)})
 
@@ -113,6 +127,15 @@ async def speaker_volume_set_action(config, action_id, template_arg, args):
     return var
 
 
+@automation.register_action(
+    "speaker.mute_off", MuteOffAction, SPEAKER_AUTOMATION_SCHEMA
+)
+@automation.register_action("speaker.mute_on", MuteOnAction, SPEAKER_AUTOMATION_SCHEMA)
+async def speaker_mute_action_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
+
+
 @coroutine_with_priority(100.0)
 async def to_code(config):
     cg.add_global(speaker_ns.using)
diff --git a/esphome/components/speaker/automation.h b/esphome/components/speaker/automation.h
index 9efda011f2..c083796eea 100644
--- a/esphome/components/speaker/automation.h
+++ b/esphome/components/speaker/automation.h
@@ -39,6 +39,26 @@ template<typename... Ts> class VolumeSetAction : public Action<Ts...>, public Pa
   void play(Ts... x) override { this->parent_->set_volume(this->volume_.value(x...)); }
 };
 
+template<typename... Ts> class MuteOnAction : public Action<Ts...> {
+ public:
+  explicit MuteOnAction(Speaker *speaker) : speaker_(speaker) {}
+
+  void play(Ts... x) override { this->speaker_->set_mute_state(true); }
+
+ protected:
+  Speaker *speaker_;
+};
+
+template<typename... Ts> class MuteOffAction : public Action<Ts...> {
+ public:
+  explicit MuteOffAction(Speaker *speaker) : speaker_(speaker) {}
+
+  void play(Ts... x) override { this->speaker_->set_mute_state(false); }
+
+ protected:
+  Speaker *speaker_;
+};
+
 template<typename... Ts> class StopAction : public Action<Ts...>, public Parented<Speaker> {
  public:
   void play(Ts... x) override { this->parent_->stop(); }
diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h
index 9390e4edb7..96843e2d5a 100644
--- a/esphome/components/speaker/speaker.h
+++ b/esphome/components/speaker/speaker.h
@@ -8,7 +8,12 @@
 #include <freertos/FreeRTOS.h>
 #endif
 
+#include "esphome/core/defines.h"
+
 #include "esphome/components/audio/audio.h"
+#ifdef USE_AUDIO_DAC
+#include "esphome/components/audio_dac/audio_dac.h"
+#endif
 
 namespace esphome {
 namespace speaker {
@@ -56,9 +61,35 @@ class Speaker {
   bool is_running() const { return this->state_ == STATE_RUNNING; }
   bool is_stopped() const { return this->state_ == STATE_STOPPED; }
 
-  // Volume control must be implemented by each speaker component, otherwise it will have no effect.
-  virtual void set_volume(float volume) { this->volume_ = volume; };
-  virtual float get_volume() { return this->volume_; }
+  // Volume control is handled by a configured audio dac component. Individual speaker components can
+  // override and implement in software if an audio dac isn't available.
+  virtual void set_volume(float volume) {
+    this->volume_ = volume;
+#ifdef USE_AUDIO_DAC
+    if (this->audio_dac_ != nullptr) {
+      this->audio_dac_->set_volume(volume);
+    }
+#endif
+  };
+  float get_volume() { return this->volume_; }
+
+  virtual void set_mute_state(bool mute_state) {
+    this->mute_state_ = mute_state;
+#ifdef USE_AUDIO_DAC
+    if (this->audio_dac_) {
+      if (mute_state) {
+        this->audio_dac_->set_mute_on();
+      } else {
+        this->audio_dac_->set_mute_off();
+      }
+    }
+#endif
+  }
+  bool get_mute_state() { return this->mute_state_; }
+
+#ifdef USE_AUDIO_DAC
+  void set_audio_dac(audio_dac::AudioDac *audio_dac) { this->audio_dac_ = audio_dac; }
+#endif
 
   void set_audio_stream_info(const audio::AudioStreamInfo &audio_stream_info) {
     this->audio_stream_info_ = audio_stream_info;
@@ -68,6 +99,11 @@ class Speaker {
   State state_{STATE_STOPPED};
   audio::AudioStreamInfo audio_stream_info_;
   float volume_{1.0f};
+  bool mute_state_{false};
+
+#ifdef USE_AUDIO_DAC
+  audio_dac::AudioDac *audio_dac_{nullptr};
+#endif
 };
 
 }  // namespace speaker
diff --git a/tests/components/speaker/test.esp32-ard.yaml b/tests/components/speaker/test.esp32-ard.yaml
index 9a24d00f68..396b4d95ea 100644
--- a/tests/components/speaker/test.esp32-ard.yaml
+++ b/tests/components/speaker/test.esp32-ard.yaml
@@ -1,6 +1,8 @@
 esphome:
   on_boot:
     then:
+      - speaker.mute_on:
+      - speaker.mute_off:
       - if:
           condition: speaker.is_stopped
           then:
diff --git a/tests/components/speaker/test.esp32-c3-ard.yaml b/tests/components/speaker/test.esp32-c3-ard.yaml
index f28014337c..636aeba766 100644
--- a/tests/components/speaker/test.esp32-c3-ard.yaml
+++ b/tests/components/speaker/test.esp32-c3-ard.yaml
@@ -1,6 +1,8 @@
 esphome:
   on_boot:
     then:
+      - speaker.mute_on:
+      - speaker.mute_off:
       - if:
           condition: speaker.is_stopped
           then:
diff --git a/tests/components/speaker/test.esp32-c3-idf.yaml b/tests/components/speaker/test.esp32-c3-idf.yaml
index f28014337c..636aeba766 100644
--- a/tests/components/speaker/test.esp32-c3-idf.yaml
+++ b/tests/components/speaker/test.esp32-c3-idf.yaml
@@ -1,6 +1,8 @@
 esphome:
   on_boot:
     then:
+      - speaker.mute_on:
+      - speaker.mute_off:
       - if:
           condition: speaker.is_stopped
           then:
diff --git a/tests/components/speaker/test.esp32-idf.yaml b/tests/components/speaker/test.esp32-idf.yaml
index 9a24d00f68..b69440b133 100644
--- a/tests/components/speaker/test.esp32-idf.yaml
+++ b/tests/components/speaker/test.esp32-idf.yaml
@@ -1,6 +1,8 @@
 esphome:
   on_boot:
     then:
+      - speaker.mute_on:
+      - speaker.mute_off:
       - if:
           condition: speaker.is_stopped
           then:
@@ -17,8 +19,17 @@ i2s_audio:
   i2s_bclk_pin: 17
   i2s_mclk_pin: 15
 
+i2c:
+  scl: 12
+  sda: 10
+
+audio_dac:
+  - platform: aic3204
+    id: internal_dac
+
 speaker:
   - platform: i2s_audio
-    id: speaker_id
+    id: speaker_with_audio_dac_id
+    audio_dac: internal_dac
     dac_type: external
-    i2s_dout_pin: 13
+    i2s_dout_pin: 14

From 9acc21e81a393a409236f29aeaf195cedb1ea472 Mon Sep 17 00:00:00 2001
From: tomaszduda23 <tomaszduda23@gmail.com>
Date: Wed, 23 Oct 2024 23:04:59 +0200
Subject: [PATCH 034/282] unified way how all platforms handle copy_files
 (#7614)

Co-authored-by: Tomasz Duda <tomaszduda23@gmai.com>
---
 esphome/components/rp2040/__init__.py |  9 ++++++---
 esphome/writer.py                     | 25 +++++++------------------
 2 files changed, 13 insertions(+), 21 deletions(-)

diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py
index f59962477f..d612631a4c 100644
--- a/esphome/components/rp2040/__init__.py
+++ b/esphome/components/rp2040/__init__.py
@@ -17,7 +17,7 @@ from esphome.const import (
     PLATFORM_RP2040,
 )
 from esphome.core import CORE, EsphomeError, coroutine_with_priority
-from esphome.helpers import copy_file_if_changed, mkdir_p, write_file
+from esphome.helpers import copy_file_if_changed, mkdir_p, write_file, read_file
 
 from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns
 
@@ -230,11 +230,14 @@ def generate_pio_files() -> bool:
 
 
 # Called by writer.py
-def copy_files() -> bool:
+def copy_files():
     dir = os.path.dirname(__file__)
     post_build_file = os.path.join(dir, "post_build.py.script")
     copy_file_if_changed(
         post_build_file,
         CORE.relative_build_path("post_build.py"),
     )
-    return generate_pio_files()
+    if generate_pio_files():
+        path = CORE.relative_src_path("esphome.h")
+        content = read_file(path).rstrip("\n")
+        write_file(path, content + '\n#include "pio_includes.h"\n')
diff --git a/esphome/writer.py b/esphome/writer.py
index 79ee72996c..90446ae4b1 100644
--- a/esphome/writer.py
+++ b/esphome/writer.py
@@ -1,3 +1,4 @@
+import importlib
 import logging
 import os
 from pathlib import Path
@@ -299,25 +300,13 @@ def copy_src_tree():
         CORE.relative_src_path("esphome", "core", "version.h"), generate_version_h()
     )
 
-    if CORE.is_esp32:
-        from esphome.components.esp32 import copy_files
-
+    platform = "esphome.components." + CORE.target_platform
+    try:
+        module = importlib.import_module(platform)
+        copy_files = getattr(module, "copy_files")
         copy_files()
-
-    elif CORE.is_esp8266:
-        from esphome.components.esp8266 import copy_files
-
-        copy_files()
-
-    elif CORE.is_rp2040:
-        from esphome.components.rp2040 import copy_files
-
-        (pio) = copy_files()
-        if pio:
-            write_file_if_changed(
-                CORE.relative_src_path("esphome.h"),
-                ESPHOME_H_FORMAT.format(include_s + '\n#include "pio_includes.h"'),
-            )
+    except AttributeError:
+        pass
 
 
 def generate_defines_h():

From 5b5c2fe71b8307aa692d3def1268837a1b26de51 Mon Sep 17 00:00:00 2001
From: Aaron Solochek <aarons@gmail.com>
Date: Wed, 23 Oct 2024 17:25:53 -0700
Subject: [PATCH 035/282] updating ESP32 board definitions (#7650)

---
 esphome/components/esp32/boards.py | 200 ++++++++++++++++++++++++++++-
 1 file changed, 199 insertions(+), 1 deletion(-)

diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py
index 60abcd447c..02744ecb6f 100644
--- a/esphome/components/esp32/boards.py
+++ b/esphome/components/esp32/boards.py
@@ -103,6 +103,173 @@ ESP32_BOARD_PINS = {
         "LED": 13,
         "LED_BUILTIN": 13,
     },
+    "adafruit_feather_esp32s3": {
+        "BUTTON": 0,
+        "A0": 18,
+        "A1": 17,
+        "A2": 16,
+        "A3": 15,
+        "A4": 14,
+        "A5": 8,
+        "SCK": 36,
+        "MOSI": 35,
+        "MISO": 37,
+        "RX": 38,
+        "TX": 39,
+        "SCL": 4,
+        "SDA": 3,
+        "NEOPIXEL": 33,
+        "PIN_NEOPIXEL": 33,
+        "NEOPIXEL_POWER": 21,
+        "I2C_POWER": 7,
+        "LED": 13,
+        "LED_BUILTIN": 13,
+    },
+    "adafruit_feather_esp32s3_nopsram": {
+        "BUTTON": 0,
+        "A0": 18,
+        "A1": 17,
+        "A2": 16,
+        "A3": 15,
+        "A4": 14,
+        "A5": 8,
+        "SCK": 36,
+        "MOSI": 35,
+        "MISO": 37,
+        "RX": 38,
+        "TX": 39,
+        "SCL": 4,
+        "SDA": 3,
+        "NEOPIXEL": 33,
+        "PIN_NEOPIXEL": 33,
+        "NEOPIXEL_POWER": 21,
+        "I2C_POWER": 7,
+        "LED": 13,
+        "LED_BUILTIN": 13,
+    },
+    "adafruit_feather_esp32s3_tft": {
+        "BUTTON": 0,
+        "A0": 18,
+        "A1": 17,
+        "A2": 16,
+        "A3": 15,
+        "A4": 14,
+        "A5": 8,
+        "SCK": 36,
+        "MOSI": 35,
+        "MISO": 37,
+        "RX": 2,
+        "TX": 1,
+        "SCL": 41,
+        "SDA": 42,
+        "NEOPIXEL": 33,
+        "PIN_NEOPIXEL": 33,
+        "NEOPIXEL_POWER": 34,
+        "TFT_I2C_POWER": 21,
+        "TFT_CS": 7,
+        "TFT_DC": 39,
+        "TFT_RESET": 40,
+        "TFT_BACKLIGHT": 45,
+        "LED": 13,
+        "LED_BUILTIN": 13,
+    },
+    "adafruit_funhouse_esp32s2": {
+        "BUTTON_UP": 5,
+        "BUTTON_DOWN": 3,
+        "BUTTON_SELECT": 4,
+        "DOTSTAR_DATA": 14,
+        "DOTSTAR_CLOCK": 15,
+        "PIR_SENSE": 16,
+        "A0": 17,
+        "A1": 2,
+        "A2": 1,
+        "CAP6": 6,
+        "CAP7": 7,
+        "CAP8": 8,
+        "CAP9": 9,
+        "CAP10": 10,
+        "CAP11": 11,
+        "CAP12": 12,
+        "CAP13": 13,
+        "SPEAKER": 42,
+        "LED": 37,
+        "LIGHT": 18,
+        "TFT_MOSI": 35,
+        "TFT_SCK": 36,
+        "TFT_CS": 40,
+        "TFT_DC": 39,
+        "TFT_RESET": 41,
+        "TFT_BACKLIGHT": 21,
+        "RED_LED": 31,
+        "BUTTON": 0,
+    },
+    "adafruit_itsybitsy_esp32": {
+        "A0": 25,
+        "A1": 26,
+        "A2": 4,
+        "A3": 38,
+        "A4": 37,
+        "A5": 36,
+        "SCK": 19,
+        "MOSI": 21,
+        "MISO": 22,
+        "SCL": 27,
+        "SDA": 15,
+        "TX": 20,
+        "RX": 8,
+        "NEOPIXEL": 0,
+        "PIN_NEOPIXEL": 0,
+        "NEOPIXEL_POWER": 2,
+        "BUTTON": 35,
+    },
+    "adafruit_magtag29_esp32s2": {
+        "A1": 18,
+        "BUTTON_A": 15,
+        "BUTTON_B": 14,
+        "BUTTON_C": 12,
+        "BUTTON_D": 11,
+        "SDA": 33,
+        "SCL": 34,
+        "SPEAKER": 17,
+        "SPEAKER_ENABLE": 16,
+        "VOLTAGE_MONITOR": 4,
+        "ACCELEROMETER_INT": 9,
+        "ACCELEROMETER_INTERRUPT": 9,
+        "LIGHT": 3,
+        "NEOPIXEL": 1,
+        "PIN_NEOPIXEL": 1,
+        "NEOPIXEL_POWER": 21,
+        "EPD_BUSY": 5,
+        "EPD_RESET": 6,
+        "EPD_DC": 7,
+        "EPD_CS": 8,
+        "EPD_MOSI": 35,
+        "EPD_SCK": 36,
+        "EPD_MISO": 37,
+        "BUTTON": 0,
+        "LED": 13,
+        "LED_BUILTIN": 13,
+    },
+    "adafruit_metro_esp32s2": {
+        "A0": 17,
+        "A1": 18,
+        "A2": 1,
+        "A3": 2,
+        "A4": 3,
+        "A5": 4,
+        "RX": 38,
+        "TX": 37,
+        "SCL": 34,
+        "SDA": 33,
+        "MISO": 37,
+        "SCK": 36,
+        "MOSI": 35,
+        "NEOPIXEL": 45,
+        "PIN_NEOPIXEL": 45,
+        "LED": 42,
+        "LED_BUILTIN": 42,
+        "BUTTON": 0,
+    },
     "adafruit_qtpy_esp32c3": {
         "A0": 4,
         "A1": 3,
@@ -141,6 +308,26 @@ ESP32_BOARD_PINS = {
         "BUTTON": 0,
         "SWITCH": 0,
     },
+    "adafruit_qtpy_esp32s3_nopsram": {
+        "A0": 18,
+        "A1": 17,
+        "A2": 9,
+        "A3": 8,
+        "SDA": 7,
+        "SCL": 6,
+        "MOSI": 35,
+        "MISO": 37,
+        "SCK": 36,
+        "RX": 16,
+        "TX": 5,
+        "SDA1": 41,
+        "SCL1": 40,
+        "NEOPIXEL": 39,
+        "PIN_NEOPIXEL": 39,
+        "NEOPIXEL_POWER": 38,
+        "BUTTON": 0,
+        "SWITCH": 0,
+    },
     "adafruit_qtpy_esp32": {
         "A0": 26,
         "A1": 25,
@@ -1068,7 +1255,18 @@ ESP32_BOARD_PINS = {
         "_VBAT": 35,
     },
     "wemosbat": {"LED": 16},
-    "wesp32": {"MISO": 32, "SCL": 4, "SDA": 15},
+    "wesp32": {
+        "MISO": 32,
+        "MOSI": 23,
+        "SCK": 18,
+        "SCL": 4,
+        "SDA": 15,
+        "MISO1": 12,
+        "MOSI1": 13,
+        "SCK1": 14,
+        "SCL1": 5,
+        "SDA1": 33,
+    },
     "widora-air": {
         "A1": 39,
         "A2": 35,

From ca5c73d170c57a256688f51c09b38dac70b1fe6e Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Fri, 25 Oct 2024 07:55:14 +1300
Subject: [PATCH 036/282] Support ignoring discovered devices from the
 dashboard (#7665)

---
 esphome/dashboard/core.py       | 25 ++++++++++++++++++++
 esphome/dashboard/web_server.py | 42 +++++++++++++++++++++++++++++++++
 esphome/storage_json.py         |  4 ++++
 3 files changed, 71 insertions(+)

diff --git a/esphome/dashboard/core.py b/esphome/dashboard/core.py
index eec0777da6..563ca1506d 100644
--- a/esphome/dashboard/core.py
+++ b/esphome/dashboard/core.py
@@ -5,10 +5,14 @@ from collections.abc import Coroutine
 import contextlib
 from dataclasses import dataclass
 from functools import partial
+import json
 import logging
+from pathlib import Path
 import threading
 from typing import TYPE_CHECKING, Any, Callable
 
+from esphome.storage_json import ignored_devices_storage_path
+
 from ..zeroconf import DiscoveredImport
 from .dns import DNSCache
 from .entries import DashboardEntries
@@ -20,6 +24,8 @@ if TYPE_CHECKING:
 
 _LOGGER = logging.getLogger(__name__)
 
+IGNORED_DEVICES_STORAGE_PATH = "ignored-devices.json"
+
 
 @dataclass
 class Event:
@@ -74,6 +80,7 @@ class ESPHomeDashboard:
         "settings",
         "dns_cache",
         "_background_tasks",
+        "ignored_devices",
     )
 
     def __init__(self) -> None:
@@ -89,12 +96,30 @@ class ESPHomeDashboard:
         self.settings = DashboardSettings()
         self.dns_cache = DNSCache()
         self._background_tasks: set[asyncio.Task] = set()
+        self.ignored_devices: set[str] = set()
 
     async def async_setup(self) -> None:
         """Setup the dashboard."""
         self.loop = asyncio.get_running_loop()
         self.ping_request = asyncio.Event()
         self.entries = DashboardEntries(self)
+        self.load_ignored_devices()
+
+    def load_ignored_devices(self) -> None:
+        storage_path = Path(ignored_devices_storage_path())
+        try:
+            with storage_path.open("r", encoding="utf-8") as f_handle:
+                data = json.load(f_handle)
+                self.ignored_devices = set(data.get("ignored_devices", set()))
+        except FileNotFoundError:
+            pass
+
+    def save_ignored_devices(self) -> None:
+        storage_path = Path(ignored_devices_storage_path())
+        with storage_path.open("w", encoding="utf-8") as f_handle:
+            json.dump(
+                {"ignored_devices": sorted(self.ignored_devices)}, indent=2, fp=f_handle
+            )
 
     async def async_run(self) -> None:
         """Run the dashboard."""
diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py
index e4b7b8d342..1a8b2ab83a 100644
--- a/esphome/dashboard/web_server.py
+++ b/esphome/dashboard/web_server.py
@@ -541,6 +541,46 @@ class ImportRequestHandler(BaseHandler):
         self.finish()
 
 
+class IgnoreDeviceRequestHandler(BaseHandler):
+    @authenticated
+    def post(self) -> None:
+        dashboard = DASHBOARD
+        try:
+            args = json.loads(self.request.body.decode())
+            device_name = args["name"]
+            ignore = args["ignore"]
+        except (json.JSONDecodeError, KeyError):
+            self.set_status(400)
+            self.set_header("content-type", "application/json")
+            self.write(json.dumps({"error": "Invalid payload"}))
+            return
+
+        ignored_device = next(
+            (
+                res
+                for res in dashboard.import_result.values()
+                if res.device_name == device_name
+            ),
+            None,
+        )
+
+        if ignored_device is None:
+            self.set_status(404)
+            self.set_header("content-type", "application/json")
+            self.write(json.dumps({"error": "Device not found"}))
+            return
+
+        if ignore:
+            dashboard.ignored_devices.add(ignored_device.device_name)
+        else:
+            dashboard.ignored_devices.discard(ignored_device.device_name)
+
+        dashboard.save_ignored_devices()
+
+        self.set_status(204)
+        self.finish()
+
+
 class DownloadListRequestHandler(BaseHandler):
     @authenticated
     @bind_config
@@ -688,6 +728,7 @@ class ListDevicesHandler(BaseHandler):
                             "project_name": res.project_name,
                             "project_version": res.project_version,
                             "network": res.network,
+                            "ignored": res.device_name in dashboard.ignored_devices,
                         }
                         for res in dashboard.import_result.values()
                         if res.device_name not in configured
@@ -1156,6 +1197,7 @@ def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application:
             (f"{rel}prometheus-sd", PrometheusServiceDiscoveryHandler),
             (f"{rel}boards/([a-z0-9]+)", BoardsRequestHandler),
             (f"{rel}version", EsphomeVersionHandler),
+            (f"{rel}ignore-device", IgnoreDeviceRequestHandler),
         ],
         **app_settings,
     )
diff --git a/esphome/storage_json.py b/esphome/storage_json.py
index 2d12ee01a0..97cf9ceadd 100644
--- a/esphome/storage_json.py
+++ b/esphome/storage_json.py
@@ -28,6 +28,10 @@ def esphome_storage_path() -> str:
     return os.path.join(CORE.data_dir, "esphome.json")
 
 
+def ignored_devices_storage_path() -> str:
+    return os.path.join(CORE.data_dir, "ignored-devices.json")
+
+
 def trash_storage_path() -> str:
     return CORE.relative_config_path("trash")
 

From 4fa3c6915ca7aaba27d001415aee5b613db33565 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Fri, 25 Oct 2024 08:10:30 +1300
Subject: [PATCH 037/282] Bump esphome-dashboard to 20241025.0 (#7669)

---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index c03a9f181a..8cc26e4da0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,7 +12,7 @@ pyserial==3.5
 platformio==6.1.16  # When updating platformio, also update Dockerfile
 esptool==4.7.0
 click==8.1.7
-esphome-dashboard==20240620.0
+esphome-dashboard==20241025.0
 aioesphomeapi==24.6.2
 zeroconf==0.132.2
 puremagic==1.27

From c20e1975d1acd9c99fefd1a1da7e446a2e403bc7 Mon Sep 17 00:00:00 2001
From: tomaszduda23 <tomaszduda23@gmail.com>
Date: Thu, 24 Oct 2024 23:25:19 +0200
Subject: [PATCH 038/282] unified way how all platforms handle
 get_download_types (#7617)

Co-authored-by: Tomasz Duda <tomaszduda23@gmai.com>
---
 esphome/dashboard/web_server.py | 27 ++++++++++-----------------
 1 file changed, 10 insertions(+), 17 deletions(-)

diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py
index 1a8b2ab83a..9aeece9aab 100644
--- a/esphome/dashboard/web_server.py
+++ b/esphome/dashboard/web_server.py
@@ -7,6 +7,7 @@ import datetime
 import functools
 import gzip
 import hashlib
+import importlib
 import json
 import logging
 import os
@@ -595,26 +596,18 @@ class DownloadListRequestHandler(BaseHandler):
 
         downloads = []
         platform: str = storage_json.target_platform.lower()
-        if platform == const.PLATFORM_RP2040:
-            from esphome.components.rp2040 import get_download_types as rp2040_types
 
-            downloads = rp2040_types(storage_json)
-        elif platform == const.PLATFORM_ESP8266:
-            from esphome.components.esp8266 import get_download_types as esp8266_types
-
-            downloads = esp8266_types(storage_json)
-        elif platform.upper() in ESP32_VARIANTS:
-            from esphome.components.esp32 import get_download_types as esp32_types
-
-            downloads = esp32_types(storage_json)
+        if platform.upper() in ESP32_VARIANTS:
+            platform = "esp32"
         elif platform in (const.PLATFORM_RTL87XX, const.PLATFORM_BK72XX):
-            from esphome.components.libretiny import (
-                get_download_types as libretiny_types,
-            )
+            platform = "libretiny"
 
-            downloads = libretiny_types(storage_json)
-        else:
-            raise ValueError(f"Unknown platform {platform}")
+        try:
+            module = importlib.import_module(f"esphome.components.{platform}")
+            get_download_types = getattr(module, "get_download_types")
+        except AttributeError as exc:
+            raise ValueError(f"Unknown platform {platform}") from exc
+        downloads = get_download_types(storage_json)
 
         self.set_status(200)
         self.set_header("content-type", "application/json")

From 4101d5dad15a5f3162ada989a536bf2cb6da0307 Mon Sep 17 00:00:00 2001
From: Kevin Ahrendt <kevin.ahrendt@nabucasa.com>
Date: Thu, 24 Oct 2024 17:26:39 -0400
Subject: [PATCH 039/282] [media_player] Add new media player conditions
 (#7667)

---
 esphome/components/media_player/__init__.py      | 11 +++++++++++
 esphome/components/media_player/automation.h     | 10 ++++++++++
 esphome/components/media_player/media_player.cpp |  4 ++++
 tests/components/media_player/common.yaml        |  4 ++++
 4 files changed, 29 insertions(+)

diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py
index 423cb065dc..a46b30db29 100644
--- a/esphome/components/media_player/__init__.py
+++ b/esphome/components/media_player/__init__.py
@@ -21,6 +21,7 @@ media_player_ns = cg.esphome_ns.namespace("media_player")
 
 MediaPlayer = media_player_ns.class_("MediaPlayer")
 
+
 PlayAction = media_player_ns.class_(
     "PlayAction", automation.Action, cg.Parented.template(MediaPlayer)
 )
@@ -60,7 +61,11 @@ AnnoucementTrigger = media_player_ns.class_(
     "AnnouncementTrigger", automation.Trigger.template()
 )
 IsIdleCondition = media_player_ns.class_("IsIdleCondition", automation.Condition)
+IsPausedCondition = media_player_ns.class_("IsPausedCondition", automation.Condition)
 IsPlayingCondition = media_player_ns.class_("IsPlayingCondition", automation.Condition)
+IsAnnouncingCondition = media_player_ns.class_(
+    "IsAnnouncingCondition", automation.Condition
+)
 
 
 async def setup_media_player_core_(var, config):
@@ -159,9 +164,15 @@ async def media_player_play_media_action(config, action_id, template_arg, args):
 @automation.register_condition(
     "media_player.is_idle", IsIdleCondition, MEDIA_PLAYER_ACTION_SCHEMA
 )
+@automation.register_condition(
+    "media_player.is_paused", IsPausedCondition, MEDIA_PLAYER_ACTION_SCHEMA
+)
 @automation.register_condition(
     "media_player.is_playing", IsPlayingCondition, MEDIA_PLAYER_ACTION_SCHEMA
 )
+@automation.register_condition(
+    "media_player.is_announcing", IsAnnouncingCondition, MEDIA_PLAYER_ACTION_SCHEMA
+)
 async def media_player_action(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
     await cg.register_parented(var, config[CONF_ID])
diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h
index f0e0a5dd31..7b9220c4a5 100644
--- a/esphome/components/media_player/automation.h
+++ b/esphome/components/media_player/automation.h
@@ -68,5 +68,15 @@ template<typename... Ts> class IsPlayingCondition : public Condition<Ts...>, pub
   bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING; }
 };
 
+template<typename... Ts> class IsPausedCondition : public Condition<Ts...>, public Parented<MediaPlayer> {
+ public:
+  bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PAUSED; }
+};
+
+template<typename... Ts> class IsAnnouncingCondition : public Condition<Ts...>, public Parented<MediaPlayer> {
+ public:
+  bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; }
+};
+
 }  // namespace media_player
 }  // namespace esphome
diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp
index 586345ac9f..b5190d8573 100644
--- a/esphome/components/media_player/media_player.cpp
+++ b/esphome/components/media_player/media_player.cpp
@@ -37,6 +37,10 @@ const char *media_player_command_to_string(MediaPlayerCommand command) {
       return "UNMUTE";
     case MEDIA_PLAYER_COMMAND_TOGGLE:
       return "TOGGLE";
+    case MEDIA_PLAYER_COMMAND_VOLUME_UP:
+      return "VOLUME_UP";
+    case MEDIA_PLAYER_COMMAND_VOLUME_DOWN:
+      return "VOLUME_DOWN";
     default:
       return "UNKNOWN";
   }
diff --git a/tests/components/media_player/common.yaml b/tests/components/media_player/common.yaml
index 24b85cd474..af0d5c7765 100644
--- a/tests/components/media_player/common.yaml
+++ b/tests/components/media_player/common.yaml
@@ -27,6 +27,10 @@ media_player:
           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%

From 7dbda12008830ba576c2e6cddca87fab112999c6 Mon Sep 17 00:00:00 2001
From: tomaszduda23 <tomaszduda23@gmail.com>
Date: Thu, 24 Oct 2024 23:55:58 +0200
Subject: [PATCH 040/282] [code-quality] weikai.h (#7601)

---
 esphome/components/weikai/weikai.h | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/esphome/components/weikai/weikai.h b/esphome/components/weikai/weikai.h
index 042c729162..175a067b27 100644
--- a/esphome/components/weikai/weikai.h
+++ b/esphome/components/weikai/weikai.h
@@ -209,7 +209,7 @@ class WeikaiComponent : public Component {
 
   /// @brief store the name for the component
   /// @param name the name as defined by the python code generator
-  void set_name(std::string name) { this->name_ = std::move(name); }
+  void set_name(std::string &&name) { this->name_ = std::move(name); }
 
   /// @brief Get the name of the component
   /// @return the name
@@ -308,7 +308,7 @@ class WeikaiChannel : public uart::UARTComponent {
 
   /// @brief The name as generated by the Python code generator
   /// @param name of the channel
-  void set_channel_name(std::string name) { this->name_ = std::move(name); }
+  void set_channel_name(std::string &&name) { this->name_ = std::move(name); }
 
   /// @brief Get the channel name
   /// @return the name

From 34a8eaddb227738ed1d22d43025a9af56ad5d4a0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 25 Oct 2024 10:56:48 +1300
Subject: [PATCH 041/282] Bump actions/setup-python from 5.2.0 to 5.3.0 in
 /.github/actions/restore-python (#7671)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/actions/restore-python/action.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml
index c6978f68c5..06c54578f5 100644
--- a/.github/actions/restore-python/action.yml
+++ b/.github/actions/restore-python/action.yml
@@ -17,7 +17,7 @@ runs:
   steps:
     - name: Set up Python ${{ inputs.python-version }}
       id: python
-      uses: actions/setup-python@v5.2.0
+      uses: actions/setup-python@v5.3.0
       with:
         python-version: ${{ inputs.python-version }}
     - name: Restore Python virtual environment

From 09f9d915773c9ad2d15d60a60f6886f9da553da5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 25 Oct 2024 10:57:09 +1300
Subject: [PATCH 042/282] Bump actions/setup-python from 5.2.0 to 5.3.0 (#7670)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 .github/workflows/ci-api-proto.yml        | 2 +-
 .github/workflows/ci-docker.yml           | 2 +-
 .github/workflows/ci.yml                  | 2 +-
 .github/workflows/release.yml             | 4 ++--
 .github/workflows/sync-device-classes.yml | 2 +-
 5 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml
index 8112c4e0ff..a6b2e2b2b3 100644
--- a/.github/workflows/ci-api-proto.yml
+++ b/.github/workflows/ci-api-proto.yml
@@ -23,7 +23,7 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v4.1.7
       - name: Set up Python
-        uses: actions/setup-python@v5.2.0
+        uses: actions/setup-python@v5.3.0
         with:
           python-version: "3.11"
 
diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml
index f003e5d24c..435a58498e 100644
--- a/.github/workflows/ci-docker.yml
+++ b/.github/workflows/ci-docker.yml
@@ -42,7 +42,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4.1.7
       - name: Set up Python
-        uses: actions/setup-python@v5.2.0
+        uses: actions/setup-python@v5.3.0
         with:
           python-version: "3.9"
       - name: Set up Docker Buildx
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0d2f1c877d..82a55d0e2a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -41,7 +41,7 @@ jobs:
         run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
       - name: Set up Python ${{ env.DEFAULT_PYTHON }}
         id: python
-        uses: actions/setup-python@v5.2.0
+        uses: actions/setup-python@v5.3.0
         with:
           python-version: ${{ env.DEFAULT_PYTHON }}
       - name: Restore Python virtual environment
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 26a213f170..5072bec222 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -53,7 +53,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4.1.7
       - name: Set up Python
-        uses: actions/setup-python@v5.2.0
+        uses: actions/setup-python@v5.3.0
         with:
           python-version: "3.x"
       - name: Set up python environment
@@ -85,7 +85,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4.1.7
       - name: Set up Python
-        uses: actions/setup-python@v5.2.0
+        uses: actions/setup-python@v5.3.0
         with:
           python-version: "3.9"
 
diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml
index c066ae9fb4..7a46d596a1 100644
--- a/.github/workflows/sync-device-classes.yml
+++ b/.github/workflows/sync-device-classes.yml
@@ -22,7 +22,7 @@ jobs:
           path: lib/home-assistant
 
       - name: Setup Python
-        uses: actions/setup-python@v5.2.0
+        uses: actions/setup-python@v5.3.0
         with:
           python-version: 3.12
 

From 33fdbbe30c4d9643300483bf2dfd85d992e2cf86 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Fri, 25 Oct 2024 09:05:25 +1100
Subject: [PATCH 043/282] [image][online_image][animation] Fix transparency in
 RGB565 (#7631)

---
 esphome/components/animation/__init__.py      | 13 +++---
 esphome/components/animation/animation.cpp    |  2 +-
 esphome/components/image/__init__.py          | 13 +++---
 esphome/components/image/image.cpp            | 28 ++++++-------
 esphome/components/image/image.h              | 37 ++++++++---------
 .../components/online_image/online_image.cpp  | 10 +----
 .../components/online_image/online_image.h    |  8 +---
 tests/components/image/common.yaml            | 38 ++++++++++++++++++
 tests/components/image/test.esp32-ard.yaml    | 40 +------------------
 tests/components/image/test.esp32-c3-ard.yaml | 39 +-----------------
 tests/components/image/test.esp32-c3-idf.yaml | 39 +-----------------
 tests/components/image/test.esp32-idf.yaml    | 39 +-----------------
 tests/components/image/test.esp8266-ard.yaml  | 39 +-----------------
 tests/components/image/test.host.yaml         |  8 ++++
 tests/components/image/test.rp2040-ard.yaml   | 39 +-----------------
 15 files changed, 101 insertions(+), 291 deletions(-)
 create mode 100644 tests/components/image/common.yaml
 create mode 100644 tests/components/image/test.host.yaml

diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py
index eb3d09ac96..5a308855de 100644
--- a/esphome/components/animation/__init__.py
+++ b/esphome/components/animation/__init__.py
@@ -271,7 +271,8 @@ async def to_code(config):
                 pos += 1
 
     elif config[CONF_TYPE] in ["RGB565", "TRANSPARENT_IMAGE"]:
-        data = [0 for _ in range(height * width * 2 * frames)]
+        bytes_per_pixel = 3 if transparent else 2
+        data = [0 for _ in range(height * width * bytes_per_pixel * frames)]
         pos = 0
         for frameIndex in range(frames):
             image.seek(frameIndex)
@@ -288,17 +289,13 @@ async def to_code(config):
                 G = g >> 2
                 B = b >> 3
                 rgb = (R << 11) | (G << 5) | B
-
-                if transparent:
-                    if rgb == 0x0020:
-                        rgb = 0
-                    if a < 0x80:
-                        rgb = 0x0020
-
                 data[pos] = rgb >> 8
                 pos += 1
                 data[pos] = rgb & 0xFF
                 pos += 1
+                if transparent:
+                    data[pos] = a
+                    pos += 1
 
     elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
         width8 = ((width + 7) // 8) * 8
diff --git a/esphome/components/animation/animation.cpp b/esphome/components/animation/animation.cpp
index 7e0efa97e0..1375dfe07e 100644
--- a/esphome/components/animation/animation.cpp
+++ b/esphome/components/animation/animation.cpp
@@ -62,7 +62,7 @@ void Animation::set_frame(int frame) {
 }
 
 void Animation::update_data_start_() {
-  const uint32_t image_size = image_type_to_width_stride(this->width_, this->type_) * this->height_;
+  const uint32_t image_size = this->get_width_stride() * this->height_;
   this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_;
 }
 
diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py
index c72417bcda..8742540067 100644
--- a/esphome/components/image/__init__.py
+++ b/esphome/components/image/__init__.py
@@ -361,24 +361,21 @@ async def to_code(config):
     elif config[CONF_TYPE] in ["RGB565"]:
         image = image.convert("RGBA")
         pixels = list(image.getdata())
-        data = [0 for _ in range(height * width * 2)]
+        bytes_per_pixel = 3 if transparent else 2
+        data = [0 for _ in range(height * width * bytes_per_pixel)]
         pos = 0
         for r, g, b, a in pixels:
             R = r >> 3
             G = g >> 2
             B = b >> 3
             rgb = (R << 11) | (G << 5) | B
-
-            if transparent:
-                if rgb == 0x0020:
-                    rgb = 0
-                if a < 0x80:
-                    rgb = 0x0020
-
             data[pos] = rgb >> 8
             pos += 1
             data[pos] = rgb & 0xFF
             pos += 1
+            if transparent:
+                data[pos] = a
+                pos += 1
 
     elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
         if transparent:
diff --git a/esphome/components/image/image.cpp b/esphome/components/image/image.cpp
index ded4c60d25..ca2f659fb0 100644
--- a/esphome/components/image/image.cpp
+++ b/esphome/components/image/image.cpp
@@ -88,7 +88,7 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
     this->dsc_.header.reserved = 0;
     this->dsc_.header.w = this->width_;
     this->dsc_.header.h = this->height_;
-    this->dsc_.data_size = image_type_to_width_stride(this->dsc_.header.w * this->dsc_.header.h, this->get_type());
+    this->dsc_.data_size = this->get_width_stride() * this->get_height();
     switch (this->get_type()) {
       case IMAGE_TYPE_BINARY:
         this->dsc_.header.cf = LV_IMG_CF_ALPHA_1BIT;
@@ -104,17 +104,17 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
 
       case IMAGE_TYPE_RGB565:
 #if LV_COLOR_DEPTH == 16
-        this->dsc_.header.cf = this->has_transparency() ? LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED : LV_IMG_CF_TRUE_COLOR;
+        this->dsc_.header.cf = this->has_transparency() ? LV_IMG_CF_TRUE_COLOR_ALPHA : LV_IMG_CF_TRUE_COLOR;
 #else
         this->dsc_.header.cf = LV_IMG_CF_RGB565;
 #endif
         break;
 
-      case image::IMAGE_TYPE_RGBA:
+      case IMAGE_TYPE_RGBA:
 #if LV_COLOR_DEPTH == 32
         this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR;
 #else
-        this->dsc_.header.cf = LV_IMG_CF_RGBA8888;
+        this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
 #endif
         break;
     }
@@ -147,21 +147,21 @@ Color Image::get_rgb24_pixel_(int x, int y) const {
   return color;
 }
 Color Image::get_rgb565_pixel_(int x, int y) const {
-  const uint32_t pos = (x + y * this->width_) * 2;
-  uint16_t rgb565 =
-      progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1);
+  const uint8_t *pos = this->data_start_;
+  if (this->transparent_) {
+    pos += (x + y * this->width_) * 3;
+  } else {
+    pos += (x + y * this->width_) * 2;
+  }
+  uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1));
   auto r = (rgb565 & 0xF800) >> 11;
   auto g = (rgb565 & 0x07E0) >> 5;
   auto b = rgb565 & 0x001F;
-  Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2));
-  if (rgb565 == 0x0020 && transparent_) {
-    // darkest green has been defined as transparent color for transparent RGB565 images.
-    color.w = 0;
-  } else {
-    color.w = 0xFF;
-  }
+  auto a = this->transparent_ ? progmem_read_byte(pos + 2) : 0xFF;
+  Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a);
   return color;
 }
+
 Color Image::get_grayscale_pixel_(int x, int y) const {
   const uint32_t pos = (x + y * this->width_);
   const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
diff --git a/esphome/components/image/image.h b/esphome/components/image/image.h
index a8a8aab2c2..40370d18da 100644
--- a/esphome/components/image/image.h
+++ b/esphome/components/image/image.h
@@ -17,24 +17,6 @@ enum ImageType {
   IMAGE_TYPE_RGBA = 4,
 };
 
-inline int image_type_to_bpp(ImageType type) {
-  switch (type) {
-    case IMAGE_TYPE_BINARY:
-      return 1;
-    case IMAGE_TYPE_GRAYSCALE:
-      return 8;
-    case IMAGE_TYPE_RGB565:
-      return 16;
-    case IMAGE_TYPE_RGB24:
-      return 24;
-    case IMAGE_TYPE_RGBA:
-      return 32;
-  }
-  return 0;
-}
-
-inline int image_type_to_width_stride(int width, ImageType type) { return (width * image_type_to_bpp(type) + 7u) / 8u; }
-
 class Image : public display::BaseImage {
  public:
   Image(const uint8_t *data_start, int width, int height, ImageType type);
@@ -44,6 +26,25 @@ class Image : public display::BaseImage {
   const uint8_t *get_data_start() const { return this->data_start_; }
   ImageType get_type() const;
 
+  int get_bpp() const {
+    switch (this->type_) {
+      case IMAGE_TYPE_BINARY:
+        return 1;
+      case IMAGE_TYPE_GRAYSCALE:
+        return 8;
+      case IMAGE_TYPE_RGB565:
+        return this->transparent_ ? 24 : 16;
+      case IMAGE_TYPE_RGB24:
+        return 24;
+      case IMAGE_TYPE_RGBA:
+        return 32;
+    }
+    return 0;
+  }
+
+  /// Return the stride of the image in bytes, that is, the distance in bytes
+  /// between two consecutive rows of pixels.
+  uint32_t get_width_stride() const { return (this->width_ * this->get_bpp() + 7u) / 8u; }
   void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
 
   void set_transparency(bool transparent) { transparent_ = transparent; }
diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp
index 480bad6aca..1786809dfa 100644
--- a/esphome/components/online_image/online_image.cpp
+++ b/esphome/components/online_image/online_image.cpp
@@ -215,16 +215,10 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
     }
     case ImageType::IMAGE_TYPE_RGB565: {
       uint16_t col565 = display::ColorUtil::color_to_565(color);
-      if (this->has_transparency()) {
-        if (col565 == 0x0020) {
-          col565 = 0;
-        }
-        if (color.w < 0x80) {
-          col565 = 0x0020;
-        }
-      }
       this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
       this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
+      if (this->has_transparency())
+        this->buffer_[pos + 2] = color.w;
       break;
     }
     case ImageType::IMAGE_TYPE_RGBA: {
diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h
index 51c11478cd..017402a088 100644
--- a/esphome/components/online_image/online_image.h
+++ b/esphome/components/online_image/online_image.h
@@ -86,13 +86,9 @@ class OnlineImage : public PollingComponent,
   Allocator allocator_{Allocator::Flags::ALLOW_FAILURE};
 
   uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_height_); }
-  int get_buffer_size_(int width, int height) const {
-    return std::ceil(image::image_type_to_bpp(this->type_) * width * height / 8.0);
-  }
+  int get_buffer_size_(int width, int height) const { return (this->get_bpp() * width + 7u) / 8u * height; }
 
-  int get_position_(int x, int y) const {
-    return ((x + y * this->buffer_width_) * image::image_type_to_bpp(this->type_)) / 8;
-  }
+  int get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; }
 
   ESPHOME_ALWAYS_INLINE bool auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; }
 
diff --git a/tests/components/image/common.yaml b/tests/components/image/common.yaml
new file mode 100644
index 0000000000..313da6bc0b
--- /dev/null
+++ b/tests/components/image/common.yaml
@@ -0,0 +1,38 @@
+image:
+  - id: binary_image
+    file: ../../pnglogo.png
+    type: BINARY
+    dither: FloydSteinberg
+  - id: transparent_transparent_image
+    file: ../../pnglogo.png
+    type: TRANSPARENT_BINARY
+  - id: rgba_image
+    file: ../../pnglogo.png
+    type: RGBA
+    resize: 50x50
+  - id: rgb24_image
+    file: ../../pnglogo.png
+    type: RGB24
+    use_transparency: yes
+  - id: rgb565_image
+    file: ../../pnglogo.png
+    type: RGB565
+    use_transparency: no
+  - id: web_svg_image
+    file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
+    resize: 256x48
+    type: TRANSPARENT_BINARY
+  - id: web_tiff_image
+    file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
+    type: RGB24
+    resize: 48x48
+  - id: web_redirect_image
+    file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
+    type: RGB24
+    resize: 48x48
+  - id: mdi_alert
+    file: mdi:alert-circle-outline
+    resize: 50x50
+  - id: another_alert_icon
+    file: mdi:alert-outline
+    type: BINARY
diff --git a/tests/components/image/test.esp32-ard.yaml b/tests/components/image/test.esp32-ard.yaml
index 9dd44d177f..818e720221 100644
--- a/tests/components/image/test.esp32-ard.yaml
+++ b/tests/components/image/test.esp32-ard.yaml
@@ -13,41 +13,5 @@ display:
     reset_pin: 21
     invert_colors: true
 
-image:
-  - id: binary_image
-    file: ../../pnglogo.png
-    type: BINARY
-    dither: FloydSteinberg
-  - id: transparent_transparent_image
-    file: ../../pnglogo.png
-    type: TRANSPARENT_BINARY
-  - id: rgba_image
-    file: ../../pnglogo.png
-    type: RGBA
-    resize: 50x50
-  - id: rgb24_image
-    file: ../../pnglogo.png
-    type: RGB24
-    use_transparency: yes
-  - id: rgb565_image
-    file: ../../pnglogo.png
-    type: RGB565
-    use_transparency: no
-  - id: web_svg_image
-    file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
-    resize: 256x48
-    type: TRANSPARENT_BINARY
-  - id: web_tiff_image
-    file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
-    type: RGB24
-    resize: 48x48
-  - id: web_redirect_image
-    file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
-    type: RGB24
-    resize: 48x48
-  - id: mdi_alert
-    file: mdi:alert-circle-outline
-    resize: 50x50
-  - id: another_alert_icon
-    file: mdi:alert-outline
-    type: BINARY
+<<: !include common.yaml
+
diff --git a/tests/components/image/test.esp32-c3-ard.yaml b/tests/components/image/test.esp32-c3-ard.yaml
index c0b2779773..4dae9cd5ec 100644
--- a/tests/components/image/test.esp32-c3-ard.yaml
+++ b/tests/components/image/test.esp32-c3-ard.yaml
@@ -13,41 +13,4 @@ display:
     reset_pin: 10
     invert_colors: true
 
-image:
-  - id: binary_image
-    file: ../../pnglogo.png
-    type: BINARY
-    dither: FloydSteinberg
-  - id: transparent_transparent_image
-    file: ../../pnglogo.png
-    type: TRANSPARENT_BINARY
-  - id: rgba_image
-    file: ../../pnglogo.png
-    type: RGBA
-    resize: 50x50
-  - id: rgb24_image
-    file: ../../pnglogo.png
-    type: RGB24
-    use_transparency: yes
-  - id: rgb565_image
-    file: ../../pnglogo.png
-    type: RGB565
-    use_transparency: no
-  - id: web_svg_image
-    file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
-    resize: 256x48
-    type: TRANSPARENT_BINARY
-  - id: web_tiff_image
-    file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
-    type: RGB24
-    resize: 48x48
-  - id: web_redirect_image
-    file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
-    type: RGB24
-    resize: 48x48
-  - id: mdi_alert
-    file: mdi:alert-circle-outline
-    resize: 50x50
-  - id: another_alert_icon
-    file: mdi:alert-outline
-    type: BINARY
+<<: !include common.yaml
diff --git a/tests/components/image/test.esp32-c3-idf.yaml b/tests/components/image/test.esp32-c3-idf.yaml
index c0b2779773..4dae9cd5ec 100644
--- a/tests/components/image/test.esp32-c3-idf.yaml
+++ b/tests/components/image/test.esp32-c3-idf.yaml
@@ -13,41 +13,4 @@ display:
     reset_pin: 10
     invert_colors: true
 
-image:
-  - id: binary_image
-    file: ../../pnglogo.png
-    type: BINARY
-    dither: FloydSteinberg
-  - id: transparent_transparent_image
-    file: ../../pnglogo.png
-    type: TRANSPARENT_BINARY
-  - id: rgba_image
-    file: ../../pnglogo.png
-    type: RGBA
-    resize: 50x50
-  - id: rgb24_image
-    file: ../../pnglogo.png
-    type: RGB24
-    use_transparency: yes
-  - id: rgb565_image
-    file: ../../pnglogo.png
-    type: RGB565
-    use_transparency: no
-  - id: web_svg_image
-    file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
-    resize: 256x48
-    type: TRANSPARENT_BINARY
-  - id: web_tiff_image
-    file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
-    type: RGB24
-    resize: 48x48
-  - id: web_redirect_image
-    file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
-    type: RGB24
-    resize: 48x48
-  - id: mdi_alert
-    file: mdi:alert-circle-outline
-    resize: 50x50
-  - id: another_alert_icon
-    file: mdi:alert-outline
-    type: BINARY
+<<: !include common.yaml
diff --git a/tests/components/image/test.esp32-idf.yaml b/tests/components/image/test.esp32-idf.yaml
index e903afea1f..814f16c36c 100644
--- a/tests/components/image/test.esp32-idf.yaml
+++ b/tests/components/image/test.esp32-idf.yaml
@@ -13,41 +13,4 @@ display:
     reset_pin: 21
     invert_colors: true
 
-image:
-  - id: binary_image
-    file: ../../pnglogo.png
-    type: BINARY
-    dither: FloydSteinberg
-  - id: transparent_transparent_image
-    file: ../../pnglogo.png
-    type: TRANSPARENT_BINARY
-  - id: rgba_image
-    file: ../../pnglogo.png
-    type: RGBA
-    resize: 50x50
-  - id: rgb24_image
-    file: ../../pnglogo.png
-    type: RGB24
-    use_transparency: yes
-  - id: rgb565_image
-    file: ../../pnglogo.png
-    type: RGB565
-    use_transparency: no
-  - id: web_svg_image
-    file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
-    resize: 256x48
-    type: TRANSPARENT_BINARY
-  - id: web_tiff_image
-    file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
-    type: RGB24
-    resize: 48x48
-  - id: web_redirect_image
-    file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
-    type: RGB24
-    resize: 48x48
-  - id: mdi_alert
-    file: mdi:alert-circle-outline
-    resize: 50x50
-  - id: another_alert_icon
-    file: mdi:alert-outline
-    type: BINARY
+<<: !include common.yaml
diff --git a/tests/components/image/test.esp8266-ard.yaml b/tests/components/image/test.esp8266-ard.yaml
index 5a96ed9497..f963022ff4 100644
--- a/tests/components/image/test.esp8266-ard.yaml
+++ b/tests/components/image/test.esp8266-ard.yaml
@@ -13,41 +13,4 @@ display:
     reset_pin: 16
     invert_colors: true
 
-image:
-  - id: binary_image
-    file: ../../pnglogo.png
-    type: BINARY
-    dither: FloydSteinberg
-  - id: transparent_transparent_image
-    file: ../../pnglogo.png
-    type: TRANSPARENT_BINARY
-  - id: rgba_image
-    file: ../../pnglogo.png
-    type: RGBA
-    resize: 50x50
-  - id: rgb24_image
-    file: ../../pnglogo.png
-    type: RGB24
-    use_transparency: yes
-  - id: rgb565_image
-    file: ../../pnglogo.png
-    type: RGB565
-    use_transparency: no
-  - id: web_svg_image
-    file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
-    resize: 256x48
-    type: TRANSPARENT_BINARY
-  - id: web_tiff_image
-    file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
-    type: RGB24
-    resize: 48x48
-  - id: web_redirect_image
-    file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
-    type: RGB24
-    resize: 48x48
-  - id: mdi_alert
-    file: mdi:alert-circle-outline
-    resize: 50x50
-  - id: another_alert_icon
-    file: mdi:alert-outline
-    type: BINARY
+<<: !include common.yaml
diff --git a/tests/components/image/test.host.yaml b/tests/components/image/test.host.yaml
new file mode 100644
index 0000000000..29509db66c
--- /dev/null
+++ b/tests/components/image/test.host.yaml
@@ -0,0 +1,8 @@
+display:
+  - platform: sdl
+    auto_clear_enabled: false
+    dimensions:
+      width: 480
+      height: 480
+
+<<: !include common.yaml
diff --git a/tests/components/image/test.rp2040-ard.yaml b/tests/components/image/test.rp2040-ard.yaml
index 4c40ca464f..5167c99a7d 100644
--- a/tests/components/image/test.rp2040-ard.yaml
+++ b/tests/components/image/test.rp2040-ard.yaml
@@ -13,41 +13,4 @@ display:
     reset_pin: 22
     invert_colors: true
 
-image:
-  - id: binary_image
-    file: ../../pnglogo.png
-    type: BINARY
-    dither: FloydSteinberg
-  - id: transparent_transparent_image
-    file: ../../pnglogo.png
-    type: TRANSPARENT_BINARY
-  - id: rgba_image
-    file: ../../pnglogo.png
-    type: RGBA
-    resize: 50x50
-  - id: rgb24_image
-    file: ../../pnglogo.png
-    type: RGB24
-    use_transparency: yes
-  - id: rgb565_image
-    file: ../../pnglogo.png
-    type: RGB565
-    use_transparency: no
-  - id: web_svg_image
-    file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
-    resize: 256x48
-    type: TRANSPARENT_BINARY
-  - id: web_tiff_image
-    file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
-    type: RGB24
-    resize: 48x48
-  - id: web_redirect_image
-    file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
-    type: RGB24
-    resize: 48x48
-  - id: mdi_alert
-    file: mdi:alert-circle-outline
-    resize: 50x50
-  - id: another_alert_icon
-    file: mdi:alert-outline
-    type: BINARY
+<<: !include common.yaml

From 21cb941bbe7afb7096ee342fae2c88bdaa27d28b Mon Sep 17 00:00:00 2001
From: Oleg Tarasov <me@olegtarasov.email>
Date: Fri, 25 Oct 2024 05:00:28 +0300
Subject: [PATCH 044/282] Add OpenTherm component (part 2.1: sensor platform)
 (#7529)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/opentherm/__init__.py      |  20 +-
 esphome/components/opentherm/const.py         |   5 +
 esphome/components/opentherm/generate.py      | 140 ++++++
 esphome/components/opentherm/hub.cpp          | 108 ++++-
 esphome/components/opentherm/hub.h            |  19 +-
 esphome/components/opentherm/opentherm.cpp    |   3 +
 esphome/components/opentherm/opentherm.h      |   3 +-
 .../components/opentherm/opentherm_macros.h   |  91 ++++
 esphome/components/opentherm/schema.py        | 438 ++++++++++++++++++
 .../components/opentherm/sensor/__init__.py   |  35 ++
 esphome/components/opentherm/validate.py      |  31 ++
 tests/components/opentherm/common.yaml        |  77 ++-
 12 files changed, 933 insertions(+), 37 deletions(-)
 create mode 100644 esphome/components/opentherm/const.py
 create mode 100644 esphome/components/opentherm/generate.py
 create mode 100644 esphome/components/opentherm/opentherm_macros.h
 create mode 100644 esphome/components/opentherm/schema.py
 create mode 100644 esphome/components/opentherm/sensor/__init__.py
 create mode 100644 esphome/components/opentherm/validate.py

diff --git a/esphome/components/opentherm/__init__.py b/esphome/components/opentherm/__init__.py
index 23443a4028..ee19818a29 100644
--- a/esphome/components/opentherm/__init__.py
+++ b/esphome/components/opentherm/__init__.py
@@ -1,9 +1,10 @@
 from typing import Any
 
-from esphome import pins
 import esphome.codegen as cg
 import esphome.config_validation as cv
+from esphome import pins
 from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266
+from . import generate
 
 CODEOWNERS = ["@olegtarasov"]
 MULTI_CONF = True
@@ -15,15 +16,14 @@ CONF_DHW_ENABLE = "dhw_enable"
 CONF_COOLING_ENABLE = "cooling_enable"
 CONF_OTC_ACTIVE = "otc_active"
 CONF_CH2_ACTIVE = "ch2_active"
+CONF_SUMMER_MODE_ACTIVE = "summer_mode_active"
+CONF_DHW_BLOCK = "dhw_block"
 CONF_SYNC_MODE = "sync_mode"
 
-opentherm_ns = cg.esphome_ns.namespace("opentherm")
-OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component)
-
 CONFIG_SCHEMA = cv.All(
     cv.Schema(
         {
-            cv.GenerateID(): cv.declare_id(OpenthermHub),
+            cv.GenerateID(): cv.declare_id(generate.OpenthermHub),
             cv.Required(CONF_IN_PIN): pins.internal_gpio_input_pin_schema,
             cv.Required(CONF_OUT_PIN): pins.internal_gpio_output_pin_schema,
             cv.Optional(CONF_CH_ENABLE, True): cv.boolean,
@@ -31,6 +31,8 @@ CONFIG_SCHEMA = cv.All(
             cv.Optional(CONF_COOLING_ENABLE, False): cv.boolean,
             cv.Optional(CONF_OTC_ACTIVE, False): cv.boolean,
             cv.Optional(CONF_CH2_ACTIVE, False): cv.boolean,
+            cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean,
+            cv.Optional(CONF_DHW_BLOCK, False): cv.boolean,
             cv.Optional(CONF_SYNC_MODE, False): cv.boolean,
         }
     ).extend(cv.COMPONENT_SCHEMA),
@@ -39,8 +41,6 @@ CONFIG_SCHEMA = cv.All(
 
 
 async def to_code(config: dict[str, Any]) -> None:
-    # Create the hub, passing the two callbacks defined below
-    # Since the hub is used in the callbacks, we need to define it first
     var = cg.new_Pvariable(config[CONF_ID])
     await cg.register_component(var, config)
 
@@ -53,5 +53,7 @@ async def to_code(config: dict[str, Any]) -> None:
 
     non_sensors = {CONF_ID, CONF_IN_PIN, CONF_OUT_PIN}
     for key, value in config.items():
-        if key not in non_sensors:
-            cg.add(getattr(var, f"set_{key}")(value))
+        if key in non_sensors:
+            continue
+
+        cg.add(getattr(var, f"set_{key}")(value))
diff --git a/esphome/components/opentherm/const.py b/esphome/components/opentherm/const.py
new file mode 100644
index 0000000000..1f997c5d9c
--- /dev/null
+++ b/esphome/components/opentherm/const.py
@@ -0,0 +1,5 @@
+OPENTHERM = "opentherm"
+
+CONF_OPENTHERM_ID = "opentherm_id"
+
+SENSOR = "sensor"
diff --git a/esphome/components/opentherm/generate.py b/esphome/components/opentherm/generate.py
new file mode 100644
index 0000000000..6a97835a57
--- /dev/null
+++ b/esphome/components/opentherm/generate.py
@@ -0,0 +1,140 @@
+from collections.abc import Awaitable
+from typing import Any, Callable
+
+import esphome.codegen as cg
+from esphome.const import CONF_ID
+from . import const
+from .schema import TSchema
+
+opentherm_ns = cg.esphome_ns.namespace("opentherm")
+OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component)
+
+
+def define_has_component(component_type: str, keys: list[str]) -> None:
+    cg.add_define(
+        f"OPENTHERM_{component_type.upper()}_LIST(F, sep)",
+        cg.RawExpression(
+            " sep ".join(map(lambda key: f"F({key}_{component_type.lower()})", keys))
+        ),
+    )
+    for key in keys:
+        cg.add_define(f"OPENTHERM_HAS_{component_type.upper()}_{key}")
+
+
+def define_message_handler(
+    component_type: str, keys: list[str], schemas: dict[str, TSchema]
+) -> None:
+    # The macros defined here should be able to generate things like this:
+    # // Parsing a message and publishing to sensors
+    # case MessageId::Message:
+    #     // Can have multiple sensors here, for example for a Status message with multiple flags
+    #     this->thing_binary_sensor->publish_state(parse_flag8_lb_0(response));
+    #     this->other_binary_sensor->publish_state(parse_flag8_lb_1(response));
+    #     break;
+    # // Building a message for a write request
+    # case MessageId::Message: {
+    #     unsigned int data = 0;
+    #     data = write_flag8_lb_0(some_input_switch->state, data); // Where input_sensor can also be a number/output/switch
+    #     data = write_u8_hb(some_number->state, data);
+    #     return opentherm_->build_request_(MessageType::WriteData, MessageId::Message, data);
+    # }
+
+    messages: dict[str, list[tuple[str, str]]] = {}
+    for key in keys:
+        msg = schemas[key].message
+        if msg not in messages:
+            messages[msg] = []
+        messages[msg].append((key, schemas[key].message_data))
+
+    cg.add_define(
+        f"OPENTHERM_{component_type.upper()}_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep)",
+        cg.RawExpression(
+            " msg_sep ".join(
+                [
+                    f"MESSAGE({msg}) "
+                    + " entity_sep ".join(
+                        [
+                            f"ENTITY({key}_{component_type.lower()}, {msg_data})"
+                            for key, msg_data in keys
+                        ]
+                    )
+                    + " postscript"
+                    for msg, keys in messages.items()
+                ]
+            )
+        ),
+    )
+
+
+def define_readers(component_type: str, keys: list[str]) -> None:
+    for key in keys:
+        cg.add_define(
+            f"OPENTHERM_READ_{key}",
+            cg.RawExpression(f"this->{key}_{component_type.lower()}->state"),
+        )
+
+
+def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]):
+    messages: set[tuple[str, bool]] = set()
+    for key in keys:
+        messages.add((schemas[key].message, schemas[key].keep_updated))
+    for msg, keep_updated in messages:
+        msg_expr = cg.RawExpression(f"esphome::opentherm::MessageId::{msg}")
+        if keep_updated:
+            cg.add(hub.add_repeating_message(msg_expr))
+        else:
+            cg.add(hub.add_initial_message(msg_expr))
+
+
+def add_property_set(var: cg.MockObj, config_key: str, config: dict[str, Any]) -> None:
+    if config_key in config:
+        cg.add(getattr(var, f"set_{config_key}")(config[config_key]))
+
+
+Create = Callable[[dict[str, Any], str, cg.MockObj], Awaitable[cg.Pvariable]]
+
+
+def create_only_conf(
+    create: Callable[[dict[str, Any]], Awaitable[cg.Pvariable]]
+) -> Create:
+    return lambda conf, _key, _hub: create(conf)
+
+
+async def component_to_code(
+    component_type: str,
+    schemas: dict[str, TSchema],
+    type: cg.MockObjClass,
+    create: Create,
+    config: dict[str, Any],
+) -> list[str]:
+    """Generate the code for each configured component in the schema of a component type.
+
+    Parameters:
+    - component_type: The type of component, e.g. "sensor" or "binary_sensor"
+    - schema_: The schema for that component type, a list of available components
+    - type: The type of the component, e.g. sensor.Sensor or OpenthermOutput
+    - create: A constructor function for the component, which receives the config,
+      the key and the hub and should asynchronously return the new component
+    - config: The configuration for this component type
+
+    Returns: The list of keys for the created components
+    """
+    cg.add_define(f"OPENTHERM_USE_{component_type.upper()}")
+
+    hub = await cg.get_variable(config[const.CONF_OPENTHERM_ID])
+
+    keys: list[str] = []
+    for key, conf in config.items():
+        if not isinstance(conf, dict):
+            continue
+        id = conf[CONF_ID]
+        if id and id.type == type:
+            entity = await create(conf, key, hub)
+            cg.add(getattr(hub, f"set_{key}_{component_type.lower()}")(entity))
+            keys.append(key)
+
+    define_has_component(component_type, keys)
+    define_message_handler(component_type, keys, schemas)
+    add_messages(hub, keys, schemas)
+
+    return keys
diff --git a/esphome/components/opentherm/hub.cpp b/esphome/components/opentherm/hub.cpp
index c26fbced32..770bbd82b7 100644
--- a/esphome/components/opentherm/hub.cpp
+++ b/esphome/components/opentherm/hub.cpp
@@ -7,50 +7,114 @@ namespace esphome {
 namespace opentherm {
 
 static const char *const TAG = "opentherm";
+namespace message_data {
+bool parse_flag8_lb_0(OpenthermData &data) { return read_bit(data.valueLB, 0); }
+bool parse_flag8_lb_1(OpenthermData &data) { return read_bit(data.valueLB, 1); }
+bool parse_flag8_lb_2(OpenthermData &data) { return read_bit(data.valueLB, 2); }
+bool parse_flag8_lb_3(OpenthermData &data) { return read_bit(data.valueLB, 3); }
+bool parse_flag8_lb_4(OpenthermData &data) { return read_bit(data.valueLB, 4); }
+bool parse_flag8_lb_5(OpenthermData &data) { return read_bit(data.valueLB, 5); }
+bool parse_flag8_lb_6(OpenthermData &data) { return read_bit(data.valueLB, 6); }
+bool parse_flag8_lb_7(OpenthermData &data) { return read_bit(data.valueLB, 7); }
+bool parse_flag8_hb_0(OpenthermData &data) { return read_bit(data.valueHB, 0); }
+bool parse_flag8_hb_1(OpenthermData &data) { return read_bit(data.valueHB, 1); }
+bool parse_flag8_hb_2(OpenthermData &data) { return read_bit(data.valueHB, 2); }
+bool parse_flag8_hb_3(OpenthermData &data) { return read_bit(data.valueHB, 3); }
+bool parse_flag8_hb_4(OpenthermData &data) { return read_bit(data.valueHB, 4); }
+bool parse_flag8_hb_5(OpenthermData &data) { return read_bit(data.valueHB, 5); }
+bool parse_flag8_hb_6(OpenthermData &data) { return read_bit(data.valueHB, 6); }
+bool parse_flag8_hb_7(OpenthermData &data) { return read_bit(data.valueHB, 7); }
+uint8_t parse_u8_lb(OpenthermData &data) { return data.valueLB; }
+uint8_t parse_u8_hb(OpenthermData &data) { return data.valueHB; }
+int8_t parse_s8_lb(OpenthermData &data) { return (int8_t) data.valueLB; }
+int8_t parse_s8_hb(OpenthermData &data) { return (int8_t) data.valueHB; }
+uint16_t parse_u16(OpenthermData &data) { return data.u16(); }
+int16_t parse_s16(OpenthermData &data) { return data.s16(); }
+float parse_f88(OpenthermData &data) { return data.f88(); }
 
-OpenthermData OpenthermHub::build_request_(MessageId request_id) {
+void write_flag8_lb_0(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 0, value); }
+void write_flag8_lb_1(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 1, value); }
+void write_flag8_lb_2(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 2, value); }
+void write_flag8_lb_3(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 3, value); }
+void write_flag8_lb_4(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 4, value); }
+void write_flag8_lb_5(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 5, value); }
+void write_flag8_lb_6(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 6, value); }
+void write_flag8_lb_7(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 7, value); }
+void write_flag8_hb_0(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 0, value); }
+void write_flag8_hb_1(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 1, value); }
+void write_flag8_hb_2(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 2, value); }
+void write_flag8_hb_3(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 3, value); }
+void write_flag8_hb_4(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 4, value); }
+void write_flag8_hb_5(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 5, value); }
+void write_flag8_hb_6(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 6, value); }
+void write_flag8_hb_7(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 7, value); }
+void write_u8_lb(const uint8_t value, OpenthermData &data) { data.valueLB = value; }
+void write_u8_hb(const uint8_t value, OpenthermData &data) { data.valueHB = value; }
+void write_s8_lb(const int8_t value, OpenthermData &data) { data.valueLB = (uint8_t) value; }
+void write_s8_hb(const int8_t value, OpenthermData &data) { data.valueHB = (uint8_t) value; }
+void write_u16(const uint16_t value, OpenthermData &data) { data.u16(value); }
+void write_s16(const int16_t value, OpenthermData &data) { data.s16(value); }
+void write_f88(const float value, OpenthermData &data) { data.f88(value); }
+
+}  // namespace message_data
+
+OpenthermData OpenthermHub::build_request_(MessageId request_id) const {
   OpenthermData data;
   data.type = 0;
   data.id = 0;
   data.valueHB = 0;
   data.valueLB = 0;
 
-  // First, handle the status request. This requires special logic, because we
-  // wouldn't want to inadvertently disable domestic hot water, for example.
-  // It is also included in the macro-generated code below, but that will
-  // never be executed, because we short-circuit it here.
+  // We need this special logic for STATUS message because we have two options for specifying boiler modes:
+  // with static config values in the hub, or with separate switches.
   if (request_id == MessageId::STATUS) {
-    bool const ch_enabled = this->ch_enable;
-    bool dhw_enabled = this->dhw_enable;
-    bool cooling_enabled = this->cooling_enable;
-    bool otc_enabled = this->otc_active;
-    bool ch2_enabled = this->ch2_active;
+    // NOLINTBEGIN
+    bool const ch_enabled = this->ch_enable && OPENTHERM_READ_ch_enable && OPENTHERM_READ_t_set > 0.0;
+    bool const dhw_enabled = this->dhw_enable && OPENTHERM_READ_dhw_enable;
+    bool const cooling_enabled =
+        this->cooling_enable && OPENTHERM_READ_cooling_enable && OPENTHERM_READ_cooling_control > 0.0;
+    bool const otc_enabled = this->otc_active && OPENTHERM_READ_otc_active;
+    bool const ch2_enabled = this->ch2_active && OPENTHERM_READ_ch2_active && OPENTHERM_READ_t_set_ch2 > 0.0;
+    bool const summer_mode_is_active = this->summer_mode_active && OPENTHERM_READ_summer_mode_active;
+    bool const dhw_blocked = this->dhw_block && OPENTHERM_READ_dhw_block;
+    // NOLINTEND
 
     data.type = MessageType::READ_DATA;
     data.id = MessageId::STATUS;
-    data.valueHB = ch_enabled | (dhw_enabled << 1) | (cooling_enabled << 2) | (otc_enabled << 3) | (ch2_enabled << 4);
+    data.valueHB = ch_enabled | (dhw_enabled << 1) | (cooling_enabled << 2) | (otc_enabled << 3) | (ch2_enabled << 4) |
+                   (summer_mode_is_active << 5) | (dhw_blocked << 6);
+
+    return data;
+  }
 
 // Disable incomplete switch statement warnings, because the cases in each
 // switch are generated based on the configured sensors and inputs.
 #pragma GCC diagnostic push
 #pragma GCC diagnostic ignored "-Wswitch"
 
-    // TODO: This is a placeholder for an auto-generated switch statement which builds request structure based on
-    // which sensors are enabled in config.
+  switch (request_id) { OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) }
 
 #pragma GCC diagnostic pop
 
-    return data;
-  }
-  return OpenthermData();
+  // And if we get here, a message was requested which somehow wasn't handled.
+  // This shouldn't happen due to the way the defines are configured, so we
+  // log an error and just return a 0 message.
+  ESP_LOGE(TAG, "Tried to create a request with unknown id %d. This should never happen, so please open an issue.",
+           request_id);
+  return {};
 }
 
-OpenthermHub::OpenthermHub() : Component() {}
+OpenthermHub::OpenthermHub() : Component(), in_pin_{}, out_pin_{} {}
 
 void OpenthermHub::process_response(OpenthermData &data) {
   ESP_LOGD(TAG, "Received OpenTherm response with id %d (%s)", data.id,
            this->opentherm_->message_id_to_str((MessageId) data.id));
   ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(data).c_str());
+
+  switch (data.id) {
+    OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_RESPONSE_MESSAGE, OPENTHERM_MESSAGE_RESPONSE_ENTITY, ,
+                                      OPENTHERM_MESSAGE_RESPONSE_POSTSCRIPT, )
+  }
 }
 
 void OpenthermHub::setup() {
@@ -254,15 +318,17 @@ void OpenthermHub::handle_timeout_error_() {
   this->stop_opentherm_();
 }
 
-#define ID(x) x
-#define SHOW2(x) #x
-#define SHOW(x) SHOW2(x)
-
 void OpenthermHub::dump_config() {
   ESP_LOGCONFIG(TAG, "OpenTherm:");
   LOG_PIN("  In: ", this->in_pin_);
   LOG_PIN("  Out: ", this->out_pin_);
   ESP_LOGCONFIG(TAG, "  Sync mode: %d", this->sync_mode_);
+  ESP_LOGCONFIG(TAG, "  Sensors: %s", SHOW(OPENTHERM_SENSOR_LIST(ID, )));
+  ESP_LOGCONFIG(TAG, "  Binary sensors: %s", SHOW(OPENTHERM_BINARY_SENSOR_LIST(ID, )));
+  ESP_LOGCONFIG(TAG, "  Switches: %s", SHOW(OPENTHERM_SWITCH_LIST(ID, )));
+  ESP_LOGCONFIG(TAG, "  Input sensors: %s", SHOW(OPENTHERM_INPUT_SENSOR_LIST(ID, )));
+  ESP_LOGCONFIG(TAG, "  Outputs: %s", SHOW(OPENTHERM_OUTPUT_LIST(ID, )));
+  ESP_LOGCONFIG(TAG, "  Numbers: %s", SHOW(OPENTHERM_NUMBER_LIST(ID, )));
   ESP_LOGCONFIG(TAG, "  Initial requests:");
   for (auto type : this->initial_messages_) {
     ESP_LOGCONFIG(TAG, "  - %d", type);
diff --git a/esphome/components/opentherm/hub.h b/esphome/components/opentherm/hub.h
index ce9f09fe33..3b90cdf427 100644
--- a/esphome/components/opentherm/hub.h
+++ b/esphome/components/opentherm/hub.h
@@ -7,11 +7,17 @@
 
 #include "opentherm.h"
 
+#ifdef OPENTHERM_USE_SENSOR
+#include "esphome/components/sensor/sensor.h"
+#endif
+
 #include <memory>
 #include <unordered_map>
 #include <unordered_set>
 #include <functional>
 
+#include "opentherm_macros.h"
+
 namespace esphome {
 namespace opentherm {
 
@@ -23,6 +29,8 @@ class OpenthermHub : public Component {
   // The OpenTherm interface
   std::unique_ptr<OpenTherm> opentherm_;
 
+  OPENTHERM_SENSOR_LIST(OPENTHERM_DECLARE_SENSOR, )
+
   // The set of initial messages to send on starting communication with the boiler
   std::unordered_set<MessageId> initial_messages_;
   // and the repeating messages which are sent repeatedly to update various sensors
@@ -44,7 +52,7 @@ class OpenthermHub : public Component {
   bool sync_mode_ = false;
 
   // Create OpenTherm messages based on the message id
-  OpenthermData build_request_(MessageId request_id);
+  OpenthermData build_request_(MessageId request_id) const;
   void handle_protocol_write_error_();
   void handle_protocol_read_error_();
   void handle_timeout_error_();
@@ -78,6 +86,8 @@ class OpenthermHub : public Component {
   void set_in_pin(InternalGPIOPin *in_pin) { this->in_pin_ = in_pin; }
   void set_out_pin(InternalGPIOPin *out_pin) { this->out_pin_ = out_pin; }
 
+  OPENTHERM_SENSOR_LIST(OPENTHERM_SET_SENSOR, )
+
   // Add a request to the set of initial requests
   void add_initial_message(MessageId message_id) { this->initial_messages_.insert(message_id); }
   // Add a request to the set of repeating requests. Note that a large number of repeating
@@ -86,9 +96,10 @@ class OpenthermHub : public Component {
   // will be processed.
   void add_repeating_message(MessageId message_id) { this->repeating_messages_.insert(message_id); }
 
-  // There are five status variables, which can either be set as a simple variable,
+  // There are seven status variables, which can either be set as a simple variable,
   // or using a switch. ch_enable and dhw_enable default to true, the others to false.
-  bool ch_enable = true, dhw_enable = true, cooling_enable = false, otc_active = false, ch2_active = false;
+  bool ch_enable = true, dhw_enable = true, cooling_enable = false, otc_active = false, ch2_active = false,
+       summer_mode_active = false, dhw_block = false;
 
   // Setters for the status variables
   void set_ch_enable(bool value) { this->ch_enable = value; }
@@ -96,6 +107,8 @@ class OpenthermHub : public Component {
   void set_cooling_enable(bool value) { this->cooling_enable = value; }
   void set_otc_active(bool value) { this->otc_active = value; }
   void set_ch2_active(bool value) { this->ch2_active = value; }
+  void set_summer_mode_active(bool value) { this->summer_mode_active = value; }
+  void set_dhw_block(bool value) { this->dhw_block = value; }
   void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; }
 
   float get_setup_priority() const override { return setup_priority::HARDWARE; }
diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp
index b830cc01d3..4a23bb94cf 100644
--- a/esphome/components/opentherm/opentherm.cpp
+++ b/esphome/components/opentherm/opentherm.cpp
@@ -283,6 +283,9 @@ bool OpenTherm::init_esp32_timer_() {
     .clk_src = TIMER_SRC_CLK_DEFAULT,
 #endif
     .divider = 80,
+#if defined(SOC_TIMER_GROUP_SUPPORT_XTAL) && ESP_IDF_VERSION_MAJOR < 5
+    .clk_src = TIMER_SRC_CLK_APB
+#endif
   };
 
   esp_err_t result;
diff --git a/esphome/components/opentherm/opentherm.h b/esphome/components/opentherm/opentherm.h
index 609cfb6243..23f4b39a1a 100644
--- a/esphome/components/opentherm/opentherm.h
+++ b/esphome/components/opentherm/opentherm.h
@@ -20,7 +20,6 @@
 namespace esphome {
 namespace opentherm {
 
-// TODO: Account for immutable semantics change in hub.cpp when doing later installments of OpenTherm PR
 template<class T> constexpr T read_bit(T value, uint8_t bit) { return (value >> bit) & 0x01; }
 
 template<class T> constexpr T set_bit(T value, uint8_t bit) { return value |= (1UL << bit); }
@@ -28,7 +27,7 @@ template<class T> constexpr T set_bit(T value, uint8_t bit) { return value |= (1
 template<class T> constexpr T clear_bit(T value, uint8_t bit) { return value &= ~(1UL << bit); }
 
 template<class T> constexpr T write_bit(T value, uint8_t bit, uint8_t bit_value) {
-  return bit_value ? setBit(value, bit) : clearBit(value, bit);
+  return bit_value ? set_bit(value, bit) : clear_bit(value, bit);
 }
 
 enum OperationMode {
diff --git a/esphome/components/opentherm/opentherm_macros.h b/esphome/components/opentherm/opentherm_macros.h
new file mode 100644
index 0000000000..0389e975ff
--- /dev/null
+++ b/esphome/components/opentherm/opentherm_macros.h
@@ -0,0 +1,91 @@
+#pragma once
+namespace esphome {
+namespace opentherm {
+
+// ===== hub.h macros =====
+
+// *_LIST macros will be generated in defines.h if at least one sensor from each platform is used.
+// These lists will look like this:
+// #define OPENTHERM_BINARY_SENSOR_LIST(F, sep) F(sensor_1) sep F(sensor_2)
+// These lists will be used in hub.h to define sensor fields (passing macros like OPENTHERM_DECLARE_SENSOR as F)
+// and setters (passing macros like OPENTHERM_SET_SENSOR as F) (see below)
+// In order for things not to break, we define empty lists here in case some platforms are not used in config.
+#ifndef OPENTHERM_SENSOR_LIST
+#define OPENTHERM_SENSOR_LIST(F, sep)
+#endif
+
+// Use macros to create fields for every entity specified in the ESPHome configuration
+#define OPENTHERM_DECLARE_SENSOR(entity) sensor::Sensor *entity;
+
+// Setter macros
+#define OPENTHERM_SET_SENSOR(entity) \
+  void set_##entity(sensor::Sensor *sensor) { this->entity = sensor; }
+
+// ===== hub.cpp macros =====
+
+// *_MESSAGE_HANDLERS are generated in defines.h and look like this:
+// OPENTHERM_NUMBER_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) MESSAGE(COOLING_CONTROL)
+// ENTITY(cooling_control_number, f88) postscript msg_sep They contain placeholders for message part and entities parts,
+// since one message can contain multiple entities. MESSAGE part is substituted with OPENTHERM_MESSAGE_WRITE_MESSAGE,
+// OPENTHERM_MESSAGE_READ_MESSAGE or OPENTHERM_MESSAGE_RESPONSE_MESSAGE. ENTITY part is substituted with
+// OPENTHERM_MESSAGE_WRITE_ENTITY or OPENTHERM_MESSAGE_RESPONSE_ENTITY. OPENTHERM_IGNORE is used for sensor read
+// requests since no data needs to be sent or processed, just the data id.
+
+// In order for things not to break, we define empty lists here in case some platforms are not used in config.
+#ifndef OPENTHERM_SENSOR_MESSAGE_HANDLERS
+#define OPENTHERM_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep)
+#endif
+
+// Read data request builder
+#define OPENTHERM_MESSAGE_READ_MESSAGE(msg) \
+  case MessageId::msg: \
+    data.type = MessageType::READ_DATA; \
+    data.id = request_id; \
+    return data;
+
+// Data processing builders
+#define OPENTHERM_MESSAGE_RESPONSE_MESSAGE(msg) case MessageId::msg:
+#define OPENTHERM_MESSAGE_RESPONSE_ENTITY(key, msg_data) this->key->publish_state(message_data::parse_##msg_data(data));
+#define OPENTHERM_MESSAGE_RESPONSE_POSTSCRIPT break;
+
+#define OPENTHERM_IGNORE(x, y)
+
+// Default macros for STATUS entities
+#ifndef OPENTHERM_READ_ch_enable
+#define OPENTHERM_READ_ch_enable true
+#endif
+#ifndef OPENTHERM_READ_dhw_enable
+#define OPENTHERM_READ_dhw_enable true
+#endif
+#ifndef OPENTHERM_READ_t_set
+#define OPENTHERM_READ_t_set 0.0
+#endif
+#ifndef OPENTHERM_READ_cooling_enable
+#define OPENTHERM_READ_cooling_enable false
+#endif
+#ifndef OPENTHERM_READ_cooling_control
+#define OPENTHERM_READ_cooling_control 0.0
+#endif
+#ifndef OPENTHERM_READ_otc_active
+#define OPENTHERM_READ_otc_active false
+#endif
+#ifndef OPENTHERM_READ_ch2_active
+#define OPENTHERM_READ_ch2_active false
+#endif
+#ifndef OPENTHERM_READ_t_set_ch2
+#define OPENTHERM_READ_t_set_ch2 0.0
+#endif
+#ifndef OPENTHERM_READ_summer_mode_active
+#define OPENTHERM_READ_summer_mode_active false
+#endif
+#ifndef OPENTHERM_READ_dhw_block
+#define OPENTHERM_READ_dhw_block false
+#endif
+
+// These macros utilize the structure of *_LIST macros in order
+#define ID(x) x
+#define SHOW_INNER(x) #x
+#define SHOW(x) SHOW_INNER(x)
+
+}  // namespace opentherm
+}  // namespace esphome
diff --git a/esphome/components/opentherm/schema.py b/esphome/components/opentherm/schema.py
new file mode 100644
index 0000000000..6ed0029437
--- /dev/null
+++ b/esphome/components/opentherm/schema.py
@@ -0,0 +1,438 @@
+# This file contains a schema for all supported sensors, binary sensors and
+# inputs of the OpenTherm component.
+
+from dataclasses import dataclass
+from typing import Optional, TypeVar
+
+from esphome.const import (
+    UNIT_CELSIUS,
+    UNIT_EMPTY,
+    UNIT_KILOWATT,
+    UNIT_MICROAMP,
+    UNIT_PERCENT,
+    UNIT_REVOLUTIONS_PER_MINUTE,
+    DEVICE_CLASS_CURRENT,
+    DEVICE_CLASS_EMPTY,
+    DEVICE_CLASS_PRESSURE,
+    DEVICE_CLASS_TEMPERATURE,
+    STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_NONE,
+    STATE_CLASS_TOTAL_INCREASING,
+)
+
+
+@dataclass
+class EntitySchema:
+    description: str
+    """Description of the item, based on the OpenTherm spec"""
+
+    message: str
+    """OpenTherm message id used to read or write the value"""
+
+    keep_updated: bool
+    """Whether the value should be read or write repeatedly (True) or only during
+    the initialization phase (False)
+    """
+
+    message_data: str
+    """Instructions on how to interpret the data in the message
+      - flag8_[hb|lb]_[0-7]: data is a byte of single bit flags,
+                             this flag is set in the high (hb) or low byte (lb),
+                             at position 0 to 7
+      - u8_[hb|lb]: data is an unsigned 8-bit integer,
+                    in the high (hb) or low byte (lb)
+      - s8_[hb|lb]: data is an signed 8-bit integer,
+                    in the high (hb) or low byte (lb)
+      - f88: data is a signed fixed point value with
+              1 sign bit, 7 integer bits, 8 fractional bits
+      - u16: data is an unsigned 16-bit integer
+      - s16: data is a signed 16-bit integer
+    """
+
+
+TSchema = TypeVar("TSchema", bound=EntitySchema)
+
+
+@dataclass
+class SensorSchema(EntitySchema):
+    accuracy_decimals: int
+    state_class: str
+    unit_of_measurement: Optional[str] = None
+    icon: Optional[str] = None
+    device_class: Optional[str] = None
+    disabled_by_default: bool = False
+
+
+SENSORS: dict[str, SensorSchema] = {
+    "rel_mod_level": SensorSchema(
+        description="Relative modulation level",
+        unit_of_measurement=UNIT_PERCENT,
+        accuracy_decimals=2,
+        icon="mdi:percent",
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="MODULATION_LEVEL",
+        keep_updated=True,
+        message_data="f88",
+    ),
+    "ch_pressure": SensorSchema(
+        description="Water pressure in CH circuit",
+        unit_of_measurement="bar",
+        accuracy_decimals=2,
+        device_class=DEVICE_CLASS_PRESSURE,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="CH_WATER_PRESSURE",
+        keep_updated=True,
+        message_data="f88",
+    ),
+    "dhw_flow_rate": SensorSchema(
+        description="Water flow rate in DHW circuit",
+        unit_of_measurement="l/min",
+        accuracy_decimals=2,
+        icon="mdi:waves-arrow-right",
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="DHW_FLOW_RATE",
+        keep_updated=True,
+        message_data="f88",
+    ),
+    "t_boiler": SensorSchema(
+        description="Boiler water temperature",
+        unit_of_measurement=UNIT_CELSIUS,
+        accuracy_decimals=2,
+        device_class=DEVICE_CLASS_TEMPERATURE,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="FEED_TEMP",
+        keep_updated=True,
+        message_data="f88",
+    ),
+    "t_dhw": SensorSchema(
+        description="DHW temperature",
+        unit_of_measurement=UNIT_CELSIUS,
+        accuracy_decimals=2,
+        device_class=DEVICE_CLASS_TEMPERATURE,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="DHW_TEMP",
+        keep_updated=True,
+        message_data="f88",
+    ),
+    "t_outside": SensorSchema(
+        description="Outside temperature",
+        unit_of_measurement=UNIT_CELSIUS,
+        accuracy_decimals=2,
+        device_class=DEVICE_CLASS_TEMPERATURE,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="OUTSIDE_TEMP",
+        keep_updated=True,
+        message_data="f88",
+    ),
+    "t_ret": SensorSchema(
+        description="Return water temperature",
+        unit_of_measurement=UNIT_CELSIUS,
+        accuracy_decimals=2,
+        device_class=DEVICE_CLASS_TEMPERATURE,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="RETURN_WATER_TEMP",
+        keep_updated=True,
+        message_data="f88",
+    ),
+    "t_storage": SensorSchema(
+        description="Solar storage temperature",
+        unit_of_measurement=UNIT_CELSIUS,
+        accuracy_decimals=2,
+        device_class=DEVICE_CLASS_TEMPERATURE,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="SOLAR_STORE_TEMP",
+        keep_updated=True,
+        message_data="f88",
+    ),
+    "t_collector": SensorSchema(
+        description="Solar collector temperature",
+        unit_of_measurement=UNIT_CELSIUS,
+        accuracy_decimals=0,
+        device_class=DEVICE_CLASS_TEMPERATURE,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="SOLAR_COLLECT_TEMP",
+        keep_updated=True,
+        message_data="s16",
+    ),
+    "t_flow_ch2": SensorSchema(
+        description="Flow water temperature CH2 circuit",
+        unit_of_measurement=UNIT_CELSIUS,
+        accuracy_decimals=2,
+        device_class=DEVICE_CLASS_TEMPERATURE,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="FEED_TEMP_CH2",
+        keep_updated=True,
+        message_data="f88",
+    ),
+    "t_dhw2": SensorSchema(
+        description="Domestic hot water temperature 2",
+        unit_of_measurement=UNIT_CELSIUS,
+        accuracy_decimals=2,
+        device_class=DEVICE_CLASS_TEMPERATURE,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="DHW2_TEMP",
+        keep_updated=True,
+        message_data="f88",
+    ),
+    "t_exhaust": SensorSchema(
+        description="Boiler exhaust temperature",
+        unit_of_measurement=UNIT_CELSIUS,
+        accuracy_decimals=0,
+        device_class=DEVICE_CLASS_TEMPERATURE,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="EXHAUST_TEMP",
+        keep_updated=True,
+        message_data="s16",
+    ),
+    "fan_speed": SensorSchema(
+        description="Boiler fan speed",
+        unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE,
+        accuracy_decimals=0,
+        device_class=DEVICE_CLASS_EMPTY,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="FAN_SPEED",
+        keep_updated=True,
+        message_data="u16",
+    ),
+    "flame_current": SensorSchema(
+        description="Boiler flame current",
+        unit_of_measurement=UNIT_MICROAMP,
+        accuracy_decimals=0,
+        device_class=DEVICE_CLASS_CURRENT,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="FLAME_CURRENT",
+        keep_updated=True,
+        message_data="f88",
+    ),
+    "burner_starts": SensorSchema(
+        description="Number of starts burner",
+        accuracy_decimals=0,
+        icon="mdi:gas-burner",
+        state_class=STATE_CLASS_TOTAL_INCREASING,
+        message="BURNER_STARTS",
+        keep_updated=True,
+        message_data="u16",
+    ),
+    "ch_pump_starts": SensorSchema(
+        description="Number of starts CH pump",
+        accuracy_decimals=0,
+        icon="mdi:pump",
+        state_class=STATE_CLASS_TOTAL_INCREASING,
+        message="CH_PUMP_STARTS",
+        keep_updated=True,
+        message_data="u16",
+    ),
+    "dhw_pump_valve_starts": SensorSchema(
+        description="Number of starts DHW pump/valve",
+        accuracy_decimals=0,
+        icon="mdi:water-pump",
+        state_class=STATE_CLASS_TOTAL_INCREASING,
+        message="DHW_PUMP_STARTS",
+        keep_updated=True,
+        message_data="u16",
+    ),
+    "dhw_burner_starts": SensorSchema(
+        description="Number of starts burner during DHW mode",
+        accuracy_decimals=0,
+        icon="mdi:gas-burner",
+        state_class=STATE_CLASS_TOTAL_INCREASING,
+        message="DHW_BURNER_STARTS",
+        keep_updated=True,
+        message_data="u16",
+    ),
+    "burner_operation_hours": SensorSchema(
+        description="Number of hours that burner is in operation",
+        accuracy_decimals=0,
+        icon="mdi:clock-outline",
+        state_class=STATE_CLASS_TOTAL_INCREASING,
+        message="BURNER_HOURS",
+        keep_updated=True,
+        message_data="u16",
+    ),
+    "ch_pump_operation_hours": SensorSchema(
+        description="Number of hours that CH pump has been running",
+        accuracy_decimals=0,
+        icon="mdi:clock-outline",
+        state_class=STATE_CLASS_TOTAL_INCREASING,
+        message="CH_PUMP_HOURS",
+        keep_updated=True,
+        message_data="u16",
+    ),
+    "dhw_pump_valve_operation_hours": SensorSchema(
+        description="Number of hours that DHW pump has been running or DHW valve has been opened",
+        accuracy_decimals=0,
+        icon="mdi:clock-outline",
+        state_class=STATE_CLASS_TOTAL_INCREASING,
+        message="DHW_PUMP_HOURS",
+        keep_updated=True,
+        message_data="u16",
+    ),
+    "dhw_burner_operation_hours": SensorSchema(
+        description="Number of hours that burner is in operation during DHW mode",
+        accuracy_decimals=0,
+        icon="mdi:clock-outline",
+        state_class=STATE_CLASS_TOTAL_INCREASING,
+        message="DHW_BURNER_HOURS",
+        keep_updated=True,
+        message_data="u16",
+    ),
+    "t_dhw_set_ub": SensorSchema(
+        description="Upper bound for adjustment of DHW setpoint",
+        unit_of_measurement=UNIT_CELSIUS,
+        accuracy_decimals=0,
+        device_class=DEVICE_CLASS_TEMPERATURE,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="DHW_BOUNDS",
+        keep_updated=False,
+        message_data="s8_hb",
+    ),
+    "t_dhw_set_lb": SensorSchema(
+        description="Lower bound for adjustment of DHW setpoint",
+        unit_of_measurement=UNIT_CELSIUS,
+        accuracy_decimals=0,
+        device_class=DEVICE_CLASS_TEMPERATURE,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="DHW_BOUNDS",
+        keep_updated=False,
+        message_data="s8_lb",
+    ),
+    "max_t_set_ub": SensorSchema(
+        description="Upper bound for adjustment of max CH setpoint",
+        unit_of_measurement=UNIT_CELSIUS,
+        accuracy_decimals=0,
+        device_class=DEVICE_CLASS_TEMPERATURE,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="CH_BOUNDS",
+        keep_updated=False,
+        message_data="s8_hb",
+    ),
+    "max_t_set_lb": SensorSchema(
+        description="Lower bound for adjustment of max CH setpoint",
+        unit_of_measurement=UNIT_CELSIUS,
+        accuracy_decimals=0,
+        device_class=DEVICE_CLASS_TEMPERATURE,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="CH_BOUNDS",
+        keep_updated=False,
+        message_data="s8_lb",
+    ),
+    "t_dhw_set": SensorSchema(
+        description="Domestic hot water temperature setpoint",
+        unit_of_measurement=UNIT_CELSIUS,
+        accuracy_decimals=2,
+        device_class=DEVICE_CLASS_TEMPERATURE,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="DHW_SETPOINT",
+        keep_updated=True,
+        message_data="f88",
+    ),
+    "max_t_set": SensorSchema(
+        description="Maximum allowable CH water setpoint",
+        unit_of_measurement=UNIT_CELSIUS,
+        accuracy_decimals=2,
+        device_class=DEVICE_CLASS_TEMPERATURE,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="MAX_CH_SETPOINT",
+        keep_updated=True,
+        message_data="f88",
+    ),
+    "oem_fault_code": SensorSchema(
+        description="OEM fault code",
+        unit_of_measurement=UNIT_EMPTY,
+        accuracy_decimals=0,
+        state_class=STATE_CLASS_NONE,
+        message="FAULT_FLAGS",
+        keep_updated=True,
+        message_data="u8_lb",
+    ),
+    "oem_diagnostic_code": SensorSchema(
+        description="OEM diagnostic code",
+        unit_of_measurement=UNIT_EMPTY,
+        accuracy_decimals=0,
+        state_class=STATE_CLASS_NONE,
+        message="OEM_DIAGNOSTIC",
+        keep_updated=True,
+        message_data="u16",
+    ),
+    "max_capacity": SensorSchema(
+        description="Maximum boiler capacity (KW)",
+        unit_of_measurement=UNIT_KILOWATT,
+        accuracy_decimals=0,
+        state_class=STATE_CLASS_MEASUREMENT,
+        disabled_by_default=True,
+        message="MAX_BOILER_CAPACITY",
+        keep_updated=False,
+        message_data="u8_hb",
+    ),
+    "min_mod_level": SensorSchema(
+        description="Minimum modulation level",
+        unit_of_measurement=UNIT_PERCENT,
+        accuracy_decimals=0,
+        icon="mdi:percent",
+        disabled_by_default=True,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="MAX_BOILER_CAPACITY",
+        keep_updated=False,
+        message_data="u8_lb",
+    ),
+    "opentherm_version_device": SensorSchema(
+        description="Version of OpenTherm implemented by device",
+        unit_of_measurement=UNIT_EMPTY,
+        accuracy_decimals=0,
+        state_class=STATE_CLASS_NONE,
+        disabled_by_default=True,
+        message="OT_VERSION_DEVICE",
+        keep_updated=False,
+        message_data="f88",
+    ),
+    "device_type": SensorSchema(
+        description="Device product type",
+        unit_of_measurement=UNIT_EMPTY,
+        accuracy_decimals=0,
+        state_class=STATE_CLASS_NONE,
+        disabled_by_default=True,
+        message="VERSION_DEVICE",
+        keep_updated=False,
+        message_data="u8_hb",
+    ),
+    "device_version": SensorSchema(
+        description="Device product version",
+        unit_of_measurement=UNIT_EMPTY,
+        accuracy_decimals=0,
+        state_class=STATE_CLASS_NONE,
+        disabled_by_default=True,
+        message="VERSION_DEVICE",
+        keep_updated=False,
+        message_data="u8_lb",
+    ),
+    "device_id": SensorSchema(
+        description="Device ID code",
+        unit_of_measurement=UNIT_EMPTY,
+        accuracy_decimals=0,
+        state_class=STATE_CLASS_NONE,
+        disabled_by_default=True,
+        message="DEVICE_CONFIG",
+        keep_updated=False,
+        message_data="u8_lb",
+    ),
+    "otc_hc_ratio_ub": SensorSchema(
+        description="OTC heat curve ratio upper bound",
+        unit_of_measurement=UNIT_EMPTY,
+        accuracy_decimals=0,
+        state_class=STATE_CLASS_NONE,
+        disabled_by_default=True,
+        message="OTC_CURVE_BOUNDS",
+        keep_updated=False,
+        message_data="u8_hb",
+    ),
+    "otc_hc_ratio_lb": SensorSchema(
+        description="OTC heat curve ratio lower bound",
+        unit_of_measurement=UNIT_EMPTY,
+        accuracy_decimals=0,
+        state_class=STATE_CLASS_NONE,
+        disabled_by_default=True,
+        message="OTC_CURVE_BOUNDS",
+        keep_updated=False,
+        message_data="u8_lb",
+    ),
+}
diff --git a/esphome/components/opentherm/sensor/__init__.py b/esphome/components/opentherm/sensor/__init__.py
new file mode 100644
index 0000000000..20224e0eda
--- /dev/null
+++ b/esphome/components/opentherm/sensor/__init__.py
@@ -0,0 +1,35 @@
+from typing import Any
+
+import esphome.config_validation as cv
+from esphome.components import sensor
+from .. import const, schema, validate, generate
+
+DEPENDENCIES = [const.OPENTHERM]
+COMPONENT_TYPE = const.SENSOR
+
+
+def get_entity_validation_schema(entity: schema.SensorSchema) -> cv.Schema:
+    return sensor.sensor_schema(
+        unit_of_measurement=entity.unit_of_measurement
+        or sensor._UNDEF,  # pylint: disable=protected-access
+        accuracy_decimals=entity.accuracy_decimals,
+        device_class=entity.device_class
+        or sensor._UNDEF,  # pylint: disable=protected-access
+        icon=entity.icon or sensor._UNDEF,  # pylint: disable=protected-access
+        state_class=entity.state_class,
+    )
+
+
+CONFIG_SCHEMA = validate.create_component_schema(
+    schema.SENSORS, get_entity_validation_schema
+)
+
+
+async def to_code(config: dict[str, Any]) -> None:
+    await generate.component_to_code(
+        COMPONENT_TYPE,
+        schema.SENSORS,
+        sensor.Sensor,
+        generate.create_only_conf(sensor.new_sensor),
+        config,
+    )
diff --git a/esphome/components/opentherm/validate.py b/esphome/components/opentherm/validate.py
new file mode 100644
index 0000000000..d4507672a5
--- /dev/null
+++ b/esphome/components/opentherm/validate.py
@@ -0,0 +1,31 @@
+from typing import Callable
+
+from voluptuous import Schema
+
+import esphome.config_validation as cv
+
+from . import const, schema, generate
+from .schema import TSchema
+
+
+def create_entities_schema(
+    entities: dict[str, schema.EntitySchema],
+    get_entity_validation_schema: Callable[[TSchema], cv.Schema],
+) -> Schema:
+    entity_schema = {}
+    for key, entity in entities.items():
+        entity_schema[cv.Optional(key)] = get_entity_validation_schema(entity)
+    return cv.Schema(entity_schema)
+
+
+def create_component_schema(
+    entities: dict[str, schema.EntitySchema],
+    get_entity_validation_schema: Callable[[TSchema], cv.Schema],
+) -> Schema:
+    return (
+        cv.Schema(
+            {cv.GenerateID(const.CONF_OPENTHERM_ID): cv.use_id(generate.OpenthermHub)}
+        )
+        .extend(create_entities_schema(entities, get_entity_validation_schema))
+        .extend(cv.COMPONENT_SCHEMA)
+    )
diff --git a/tests/components/opentherm/common.yaml b/tests/components/opentherm/common.yaml
index 4148b280d0..27cbae280a 100644
--- a/tests/components/opentherm/common.yaml
+++ b/tests/components/opentherm/common.yaml
@@ -1,3 +1,76 @@
+api:
+wifi:
+  ap:
+    ssid: "Thermostat"
+    password: "MySecretThemostat"
+
 opentherm:
-  in_pin: 1
-  out_pin: 2
+  in_pin: 4
+  out_pin: 5
+  ch_enable: true
+  dhw_enable: false
+  cooling_enable: false
+  otc_active: false
+  ch2_active: true
+  summer_mode_active: true
+  dhw_block: true
+  sync_mode: true
+
+sensor:
+  - platform: opentherm
+    rel_mod_level:
+      name: "Boiler Relative modulation level"
+    ch_pressure:
+      name: "Boiler Water pressure in CH circuit"
+    dhw_flow_rate:
+      name: "Boiler Water flow rate in DHW circuit"
+    t_boiler:
+      name: "Boiler water temperature"
+    t_dhw:
+      name: "Boiler DHW temperature"
+    t_outside:
+      name: "Boiler Outside temperature"
+    t_ret:
+      name: "Boiler Return water temperature"
+    t_storage:
+      name: "Boiler Solar storage temperature"
+    t_collector:
+      name: "Boiler Solar collector temperature"
+    t_flow_ch2:
+      name: "Boiler Flow water temperature CH2 circuit"
+    t_dhw2:
+      name: "Boiler Domestic hot water temperature 2"
+    t_exhaust:
+      name: "Boiler Exhaust temperature"
+    burner_starts:
+      name: "Boiler Number of starts burner"
+    ch_pump_starts:
+      name: "Boiler Number of starts CH pump"
+    dhw_pump_valve_starts:
+      name: "Boiler Number of starts DHW pump/valve"
+    dhw_burner_starts:
+      name: "Boiler Number of starts burner during DHW mode"
+    burner_operation_hours:
+      name: "Boiler Number of hours that burner is in operation (i.e. flame on)"
+    ch_pump_operation_hours:
+      name: "Boiler Number of hours that CH pump has been running"
+    dhw_pump_valve_operation_hours:
+      name: "Boiler Number of hours that DHW pump has been running or DHW valve has been opened"
+    dhw_burner_operation_hours:
+      name: "Boiler Number of hours that burner is in operation during DHW mode"
+    t_dhw_set_ub:
+      name: "Boiler Upper bound for adjustement of DHW setpoint"
+    t_dhw_set_lb:
+      name: "Boiler Lower bound for adjustement of DHW setpoint"
+    max_t_set_ub:
+      name: "Boiler Upper bound for adjustement of max CH setpoint"
+    max_t_set_lb:
+      name: "Boiler Lower bound for adjustement of max CH setpoint"
+    t_dhw_set:
+      name: "Boiler Domestic hot water temperature setpoint"
+    max_t_set:
+      name: "Boiler Maximum allowable CH water setpoint"
+    otc_hc_ratio_ub:
+      name: "OTC heat curve ratio upper bound"
+    otc_hc_ratio_lb:
+      name: "OTC heat curve ratio lower bound"

From 34de2bbe992b842ad8017daa94ba6500ffb26d1c Mon Sep 17 00:00:00 2001
From: SeByDocKy <sebydocky@hotmail.com>
Date: Sat, 26 Oct 2024 23:54:57 +0200
Subject: [PATCH 045/282] gp8403 : Add the possibility to use substitution for
 channel selection (#7681)

---
 esphome/components/gp8403/output/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/gp8403/output/__init__.py b/esphome/components/gp8403/output/__init__.py
index 1cf95ac6e5..7f17faa1b1 100644
--- a/esphome/components/gp8403/output/__init__.py
+++ b/esphome/components/gp8403/output/__init__.py
@@ -16,7 +16,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
     {
         cv.GenerateID(): cv.declare_id(GP8403Output),
         cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403),
-        cv.Required(CONF_CHANNEL): cv.one_of(0, 1),
+        cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=1),
     }
 ).extend(cv.COMPONENT_SCHEMA)
 

From 1e2497748d3a18847df868ac8f4b893800426652 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Sun, 27 Oct 2024 13:17:09 +1100
Subject: [PATCH 046/282] [rpi_dpi_rgb] Fix get_width and height (Bugfix)
 (#7675)

Co-authored-by: clydeps <U5yx99dok9>
---
 .../components/rpi_dpi_rgb/rpi_dpi_rgb.cpp    | 20 +++++++++++++++++++
 esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h  |  5 +++--
 2 files changed, 23 insertions(+), 2 deletions(-)

diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp
index 655b469b91..ba09171649 100644
--- a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp
+++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp
@@ -84,6 +84,26 @@ void RpiDpiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uin
     ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err));
 }
 
+int RpiDpiRgb::get_width() {
+  switch (this->rotation_) {
+    case display::DISPLAY_ROTATION_90_DEGREES:
+    case display::DISPLAY_ROTATION_270_DEGREES:
+      return this->get_height_internal();
+    default:
+      return this->get_width_internal();
+  }
+}
+
+int RpiDpiRgb::get_height() {
+  switch (this->rotation_) {
+    case display::DISPLAY_ROTATION_90_DEGREES:
+    case display::DISPLAY_ROTATION_270_DEGREES:
+      return this->get_width_internal();
+    default:
+      return this->get_height_internal();
+  }
+}
+
 void RpiDpiRgb::draw_pixel_at(int x, int y, Color color) {
   if (!this->get_clipping().inside(x, y))
     return;  // NOLINT
diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h
index 10f77a2624..7525040cd1 100644
--- a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h
+++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h
@@ -24,6 +24,7 @@ class RpiDpiRgb : public display::Display {
   void update() override { this->do_update_(); }
   void setup() override;
   void loop() override;
+  float get_setup_priority() const override { return setup_priority::HARDWARE; }
   void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
                       display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
   void draw_pixel_at(int x, int y, Color color) override;
@@ -44,8 +45,8 @@ class RpiDpiRgb : public display::Display {
     this->width_ = width;
     this->height_ = height;
   }
-  int get_width() override { return this->width_; }
-  int get_height() override { return this->height_; }
+  int get_width() override;
+  int get_height() override;
   void set_hsync_back_porch(uint16_t hsync_back_porch) { this->hsync_back_porch_ = hsync_back_porch; }
   void set_hsync_front_porch(uint16_t hsync_front_porch) { this->hsync_front_porch_ = hsync_front_porch; }
   void set_hsync_pulse_width(uint16_t hsync_pulse_width) { this->hsync_pulse_width_ = hsync_pulse_width; }

From 22f30d42a668e89b64318b1d8bf6c5cde38e2b21 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 29 Oct 2024 09:05:51 +1100
Subject: [PATCH 047/282] [lvgl] Implement qrcode (#7623)

---
 esphome/components/lvgl/__init__.py       |  2 +
 esphome/components/lvgl/widgets/qrcode.py | 54 +++++++++++++++++++++++
 tests/components/lvgl/lvgl-package.yaml   | 12 +++++
 3 files changed, 68 insertions(+)
 create mode 100644 esphome/components/lvgl/widgets/qrcode.py

diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py
index 215fdecdb5..4a1a26cc0b 100644
--- a/esphome/components/lvgl/__init__.py
+++ b/esphome/components/lvgl/__init__.py
@@ -71,6 +71,7 @@ from .widgets.meter import meter_spec
 from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code
 from .widgets.obj import obj_spec
 from .widgets.page import add_pages, generate_page_triggers, page_spec
+from .widgets.qrcode import qr_code_spec
 from .widgets.roller import roller_spec
 from .widgets.slider import slider_spec
 from .widgets.spinbox import spinbox_spec
@@ -109,6 +110,7 @@ for w_type in (
     spinbox_spec,
     keyboard_spec,
     tileview_spec,
+    qr_code_spec,
 ):
     WIDGET_TYPES[w_type.name] = w_type
 
diff --git a/esphome/components/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py
new file mode 100644
index 0000000000..742b538938
--- /dev/null
+++ b/esphome/components/lvgl/widgets/qrcode.py
@@ -0,0 +1,54 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_SIZE, CONF_TEXT
+from esphome.cpp_generator import MockObjClass
+
+from ..defines import CONF_MAIN, literal
+from ..lv_validation import color, color_retmapper, lv_text
+from ..lvcode import LocalVariable, lv, lv_expr
+from ..schemas import TEXT_SCHEMA
+from ..types import WidgetType, lv_obj_t
+from . import Widget
+
+CONF_QRCODE = "qrcode"
+CONF_DARK_COLOR = "dark_color"
+CONF_LIGHT_COLOR = "light_color"
+
+QRCODE_SCHEMA = TEXT_SCHEMA.extend(
+    {
+        cv.Optional(CONF_DARK_COLOR, default="black"): color,
+        cv.Optional(CONF_LIGHT_COLOR, default="white"): color,
+        cv.Required(CONF_SIZE): cv.int_,
+    }
+)
+
+
+class QrCodeType(WidgetType):
+    def __init__(self):
+        super().__init__(
+            CONF_QRCODE,
+            lv_obj_t,
+            (CONF_MAIN,),
+            QRCODE_SCHEMA,
+            modify_schema=TEXT_SCHEMA,
+        )
+
+    def get_uses(self):
+        return ("canvas", "img")
+
+    def obj_creator(self, parent: MockObjClass, config: dict):
+        dark_color = color_retmapper(config[CONF_DARK_COLOR])
+        light_color = color_retmapper(config[CONF_LIGHT_COLOR])
+        size = config[CONF_SIZE]
+        return lv_expr.call("qrcode_create", parent, size, dark_color, light_color)
+
+    async def to_code(self, w: Widget, config):
+        if (value := config.get(CONF_TEXT)) is not None:
+            value = await lv_text.process(value)
+            with LocalVariable(
+                "qr_text", cg.const_char_ptr, value, modifier=""
+            ) as str_obj:
+                lv.qrcode_update(w.obj, str_obj, literal(f"strlen({str_obj})"))
+
+
+qr_code_spec = QrCodeType()
diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml
index 4962a71596..9bfbb5fc95 100644
--- a/tests/components/lvgl/lvgl-package.yaml
+++ b/tests/components/lvgl/lvgl-package.yaml
@@ -458,6 +458,18 @@ lvgl:
 
     - id: page2
       widgets:
+        - qrcode:
+            id: lv_qr
+            align: left_mid
+            size: 100
+            light_color: whitesmoke
+            dark_color: steelblue
+            text: esphome.io
+            on_click:
+              lvgl.qrcode.update:
+                id: lv_qr
+                text: homeassistant.io
+
         - slider:
             min_value: 0
             max_value: 255

From 858d97ccefff68f92af1c8f722738424ff2db791 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 29 Oct 2024 09:08:29 +1100
Subject: [PATCH 048/282] [bytebuffer] Rework ByteBuffer using templates
 (#7638)

---
 CODEOWNERS                                    |   1 +
 esphome/components/bytebuffer/__init__.py     |   5 +
 esphome/components/bytebuffer/bytebuffer.h    | 421 ++++++++++++++++++
 esphome/core/bytebuffer.cpp                   | 167 -------
 esphome/core/bytebuffer.h                     | 144 ------
 tests/components/bytebuffer/common.yaml       | 161 +++++++
 .../components/bytebuffer/test.esp32-ard.yaml |   1 +
 .../bytebuffer/test.esp32-c3-ard.yaml         |   1 +
 .../bytebuffer/test.esp32-c3-idf.yaml         |   1 +
 .../components/bytebuffer/test.esp32-idf.yaml |   1 +
 .../bytebuffer/test.esp8266-ard.yaml          |   1 +
 tests/components/bytebuffer/test.host.yaml    |   1 +
 .../bytebuffer/test.rp2040-ard.yaml           |   1 +
 13 files changed, 595 insertions(+), 311 deletions(-)
 create mode 100644 esphome/components/bytebuffer/__init__.py
 create mode 100644 esphome/components/bytebuffer/bytebuffer.h
 delete mode 100644 esphome/core/bytebuffer.cpp
 delete mode 100644 esphome/core/bytebuffer.h
 create mode 100644 tests/components/bytebuffer/common.yaml
 create mode 100644 tests/components/bytebuffer/test.esp32-ard.yaml
 create mode 100644 tests/components/bytebuffer/test.esp32-c3-ard.yaml
 create mode 100644 tests/components/bytebuffer/test.esp32-c3-idf.yaml
 create mode 100644 tests/components/bytebuffer/test.esp32-idf.yaml
 create mode 100644 tests/components/bytebuffer/test.esp8266-ard.yaml
 create mode 100644 tests/components/bytebuffer/test.host.yaml
 create mode 100644 tests/components/bytebuffer/test.rp2040-ard.yaml

diff --git a/CODEOWNERS b/CODEOWNERS
index f96e43d5b7..5eb1f863f2 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -85,6 +85,7 @@ esphome/components/bmp581/* @kahrendt
 esphome/components/bp1658cj/* @Cossid
 esphome/components/bp5758d/* @Cossid
 esphome/components/button/* @esphome/core
+esphome/components/bytebuffer/* @clydebarrow
 esphome/components/canbus/* @danielschramm @mvturnho
 esphome/components/cap1188/* @mreditor97
 esphome/components/captive_portal/* @OttoWinter
diff --git a/esphome/components/bytebuffer/__init__.py b/esphome/components/bytebuffer/__init__.py
new file mode 100644
index 0000000000..3c7c695118
--- /dev/null
+++ b/esphome/components/bytebuffer/__init__.py
@@ -0,0 +1,5 @@
+CODEOWNERS = ["@clydebarrow"]
+
+# Allows bytebuffer to be configured in yaml, to allow use of the C++ api.
+
+CONFIG_SCHEMA = {}
diff --git a/esphome/components/bytebuffer/bytebuffer.h b/esphome/components/bytebuffer/bytebuffer.h
new file mode 100644
index 0000000000..030484ce32
--- /dev/null
+++ b/esphome/components/bytebuffer/bytebuffer.h
@@ -0,0 +1,421 @@
+#pragma once
+
+#include <utility>
+#include <vector>
+#include <cinttypes>
+#include <cstddef>
+#include "esphome/core/helpers.h"
+
+namespace esphome {
+namespace bytebuffer {
+
+enum Endian { LITTLE, BIG };
+
+/**
+ * A class modelled on the Java ByteBuffer class. It wraps a vector of bytes and permits putting and getting
+ * items of various sizes, with an automatically incremented position.
+ *
+ * There are three variables maintained pointing into the buffer:
+ *
+ * capacity: the maximum amount of data that can be stored - set on construction and cannot be changed
+ * limit: the limit of the data currently available to get or put
+ * position: the current insert or extract position
+ *
+ * 0 <= position <= limit <= capacity
+ *
+ * In addition a mark can be set to the current position with mark(). A subsequent call to reset() will restore
+ * the position to the mark.
+ *
+ * The buffer can be marked to be little-endian (default) or big-endian. All subsequent operations will use that order.
+ *
+ * The flip() operation will reset the position to 0 and limit to the current position. This is useful for reading
+ * data from a buffer after it has been written.
+ *
+ * The code is defined here in the header file rather than in a .cpp file, so that it does not get compiled if not used.
+ * The templated functions ensure that only those typed functions actually used are compiled. The functions
+ * are implicitly inline-able which will aid performance.
+ */
+class ByteBuffer {
+ public:
+  // Default constructor (compatibility with TEMPLATABLE_VALUE)
+  // Creates a zero-length ByteBuffer which is little use to anybody.
+  ByteBuffer() : ByteBuffer(std::vector<uint8_t>()) {}
+
+  /**
+   * Create a new Bytebuffer with the given capacity
+   */
+  ByteBuffer(size_t capacity, Endian endianness = LITTLE)
+      : data_(std::vector<uint8_t>(capacity)), endianness_(endianness), limit_(capacity){};
+
+  // templated functions to implement putting and getting data of various types. There are two flavours of all
+  // functions - one that uses the position as the offset, and updates the position accordingly, and one that
+  // takes an explicit offset and does not update the position.
+  // Separate temnplates are provided for types that fit into 32 bits and those that are bigger. These delegate
+  // the actual put/get to common code based around those sizes.
+  // This reduces the code size and execution time for smaller types. A similar structure for e.g. 16 bits is unlikely
+  // to provide any further benefit given that all target platforms are native 32 bit.
+
+  template<typename T>
+  T get(typename std::enable_if<std::is_integral<T>::value, T>::type * = 0,
+        typename std::enable_if<(sizeof(T) <= sizeof(uint32_t)), T>::type * = 0) {
+    // integral types that fit into 32 bit
+    return static_cast<T>(this->get_uint32_(sizeof(T)));
+  }
+
+  template<typename T>
+  T get(size_t offset, typename std::enable_if<std::is_integral<T>::value, T>::type * = 0,
+        typename std::enable_if<(sizeof(T) <= sizeof(uint32_t)), T>::type * = 0) {
+    return static_cast<T>(this->get_uint32_(offset, sizeof(T)));
+  }
+
+  template<typename T>
+  void put(const T &value, typename std::enable_if<std::is_integral<T>::value, T>::type * = 0,
+           typename std::enable_if<(sizeof(T) <= sizeof(uint32_t)), T>::type * = 0) {
+    this->put_uint32_(static_cast<uint32_t>(value), sizeof(T));
+  }
+
+  template<typename T>
+  void put(const T &value, size_t offset, typename std::enable_if<std::is_integral<T>::value, T>::type * = 0,
+           typename std::enable_if<(sizeof(T) <= sizeof(uint32_t)), T>::type * = 0) {
+    this->put_uint32_(static_cast<uint32_t>(value), offset, sizeof(T));
+  }
+
+  // integral types that do not fit into 32 bit (basically only 64 bit types)
+  template<typename T>
+  T get(typename std::enable_if<std::is_integral<T>::value, T>::type * = 0,
+        typename std::enable_if<(sizeof(T) == sizeof(uint64_t)), T>::type * = 0) {
+    return static_cast<T>(this->get_uint64_(sizeof(T)));
+  }
+
+  template<typename T>
+  T get(size_t offset, typename std::enable_if<std::is_integral<T>::value, T>::type * = 0,
+        typename std::enable_if<(sizeof(T) == sizeof(uint64_t)), T>::type * = 0) {
+    return static_cast<T>(this->get_uint64_(offset, sizeof(T)));
+  }
+
+  template<typename T>
+  void put(const T &value, typename std::enable_if<std::is_integral<T>::value, T>::type * = 0,
+           typename std::enable_if<(sizeof(T) == sizeof(uint64_t)), T>::type * = 0) {
+    this->put_uint64_(value, sizeof(T));
+  }
+
+  template<typename T>
+  void put(const T &value, size_t offset, typename std::enable_if<std::is_integral<T>::value, T>::type * = 0,
+           typename std::enable_if<(sizeof(T) == sizeof(uint64_t)), T>::type * = 0) {
+    this->put_uint64_(static_cast<uint64_t>(value), offset, sizeof(T));
+  }
+
+  // floating point types. Caters for 32 and 64 bit floating point.
+  template<typename T>
+  T get(typename std::enable_if<std::is_floating_point<T>::value, T>::type * = 0,
+        typename std::enable_if<(sizeof(T) == sizeof(uint32_t)), T>::type * = 0) {
+    return bit_cast<T>(this->get_uint32_(sizeof(T)));
+  }
+
+  template<typename T>
+  T get(typename std::enable_if<std::is_floating_point<T>::value, T>::type * = 0,
+        typename std::enable_if<(sizeof(T) == sizeof(uint64_t)), T>::type * = 0) {
+    return bit_cast<T>(this->get_uint64_(sizeof(T)));
+  }
+
+  template<typename T>
+  T get(size_t offset, typename std::enable_if<std::is_floating_point<T>::value, T>::type * = 0,
+        typename std::enable_if<(sizeof(T) == sizeof(uint32_t)), T>::type * = 0) {
+    return bit_cast<T>(this->get_uint32_(offset, sizeof(T)));
+  }
+
+  template<typename T>
+  T get(size_t offset, typename std::enable_if<std::is_floating_point<T>::value, T>::type * = 0,
+        typename std::enable_if<(sizeof(T) == sizeof(uint64_t)), T>::type * = 0) {
+    return bit_cast<T>(this->get_uint64_(offset, sizeof(T)));
+  }
+  template<typename T>
+  void put(const T &value, typename std::enable_if<std::is_floating_point<T>::value, T>::type * = 0,
+           typename std::enable_if<(sizeof(T) <= sizeof(uint32_t)), T>::type * = 0) {
+    this->put_uint32_(bit_cast<uint32_t>(value), sizeof(T));
+  }
+
+  template<typename T>
+  void put(const T &value, typename std::enable_if<std::is_floating_point<T>::value, T>::type * = 0,
+           typename std::enable_if<(sizeof(T) == sizeof(uint64_t)), T>::type * = 0) {
+    this->put_uint64_(bit_cast<uint64_t>(value), sizeof(T));
+  }
+
+  template<typename T>
+  void put(const T &value, size_t offset, typename std::enable_if<std::is_floating_point<T>::value, T>::type * = 0,
+           typename std::enable_if<(sizeof(T) <= sizeof(uint32_t)), T>::type * = 0) {
+    this->put_uint32_(bit_cast<uint32_t>(value), offset, sizeof(T));
+  }
+
+  template<typename T>
+  void put(const T &value, size_t offset, typename std::enable_if<std::is_floating_point<T>::value, T>::type * = 0,
+           typename std::enable_if<(sizeof(T) == sizeof(uint64_t)), T>::type * = 0) {
+    this->put_uint64_(bit_cast<uint64_t>(value), offset, sizeof(T));
+  }
+
+  template<typename T> static ByteBuffer wrap(T value, Endian endianness = LITTLE) {
+    ByteBuffer buffer = ByteBuffer(sizeof(T), endianness);
+    buffer.put(value);
+    buffer.flip();
+    return buffer;
+  }
+
+  static ByteBuffer wrap(std::vector<uint8_t> const &data, Endian endianness = LITTLE) {
+    ByteBuffer buffer = {data};
+    buffer.endianness_ = endianness;
+    return buffer;
+  }
+
+  static ByteBuffer wrap(const uint8_t *ptr, size_t len, Endian endianness = LITTLE) {
+    return wrap(std::vector<uint8_t>(ptr, ptr + len), endianness);
+  }
+
+  // convenience functions with explicit types named..
+  void put_float(float value) { this->put(value); }
+  void put_double(double value) { this->put(value); }
+
+  uint8_t get_uint8() { return this->data_[this->position_++]; }
+  // Get a 16 bit unsigned value, increment by 2
+  uint16_t get_uint16() { return this->get<uint16_t>(); }
+  // Get a 24 bit unsigned value, increment by 3
+  uint32_t get_uint24() { return this->get_uint32_(3); };
+  // Get a 32 bit unsigned value, increment by 4
+  uint32_t get_uint32() { return this->get<uint32_t>(); };
+  // Get a 64 bit unsigned value, increment by 8
+  uint64_t get_uint64() { return this->get<uint64_t>(); };
+  // Signed versions of the get functions
+  uint8_t get_int8() { return static_cast<int8_t>(this->get_uint8()); };
+  int16_t get_int16() { return this->get<uint16_t>(); }
+  int32_t get_int32() { return this->get<int32_t>(); }
+  int64_t get_int64() { return this->get<int64_t>(); }
+  // Get a float value, increment by 4
+  float get_float() { return this->get<float>(); }
+  // Get a double value, increment by 8
+  double get_double() { return this->get<double>(); }
+
+  // Get a bool value, increment by 1
+  bool get_bool() { return static_cast<bool>(this->get_uint8()); }
+
+  uint32_t get_int24(size_t offset) {
+    auto value = this->get_uint24(offset);
+    uint32_t mask = (~static_cast<uint32_t>(0)) << 23;
+    if ((value & mask) != 0)
+      value |= mask;
+    return value;
+  }
+
+  uint32_t get_int24() {
+    auto value = this->get_uint24();
+    uint32_t mask = (~static_cast<uint32_t>(0)) << 23;
+    if ((value & mask) != 0)
+      value |= mask;
+    return value;
+  }
+  std::vector<uint8_t> get_vector(size_t length, size_t offset) {
+    auto start = this->data_.begin() + offset;
+    return {start, start + length};
+  }
+
+  std::vector<uint8_t> get_vector(size_t length) {
+    auto result = this->get_vector(length, this->position_);
+    this->position_ += length;
+    return result;
+  }
+
+  // Convenience named functions
+  void put_uint8(uint8_t value) { this->data_[this->position_++] = value; }
+  void put_uint16(uint16_t value) { this->put(value); }
+  void put_uint24(uint32_t value) { this->put_uint32_(value, 3); }
+  void put_uint32(uint32_t value) { this->put(value); }
+  void put_uint64(uint64_t value) { this->put(value); }
+  // Signed versions of the put functions
+  void put_int8(int8_t value) { this->put_uint8(static_cast<uint8_t>(value)); }
+  void put_int16(int16_t value) { this->put(value); }
+  void put_int24(int32_t value) { this->put_uint32_(value, 3); }
+  void put_int32(int32_t value) { this->put(value); }
+  void put_int64(int64_t value) { this->put(value); }
+  // Extra put functions
+  void put_bool(bool value) { this->put_uint8(value); }
+
+  // versions of the above with an offset, these do not update the position
+
+  uint64_t get_uint64(size_t offset) { return this->get<uint64_t>(offset); }
+  uint32_t get_uint24(size_t offset) { return this->get_uint32_(offset, 3); };
+  double get_double(size_t offset) { return get<double>(offset); }
+
+  // Get one byte from the buffer, increment position by 1
+  uint8_t get_uint8(size_t offset) { return this->data_[offset]; }
+  // Get a 16 bit unsigned value, increment by 2
+  uint16_t get_uint16(size_t offset) { return get<uint16_t>(offset); }
+  // Get a 24 bit unsigned value, increment by 3
+  uint32_t get_uint32(size_t offset) { return this->get<uint32_t>(offset); };
+  // Get a 64 bit unsigned value, increment by 8
+  uint8_t get_int8(size_t offset) { return get<int8_t>(offset); }
+  int16_t get_int16(size_t offset) { return get<int16_t>(offset); }
+  int32_t get_int32(size_t offset) { return get<int32_t>(offset); }
+  int64_t get_int64(size_t offset) { return get<int64_t>(offset); }
+  // Get a float value, increment by 4
+  float get_float(size_t offset) { return get<float>(offset); }
+  // Get a double value, increment by 8
+
+  // Get a bool value, increment by 1
+  bool get_bool(size_t offset) { return this->get_uint8(offset); }
+
+  void put_uint8(uint8_t value, size_t offset) { this->data_[offset] = value; }
+  void put_uint16(uint16_t value, size_t offset) { this->put(value, offset); }
+  void put_uint24(uint32_t value, size_t offset) { this->put(value, offset); }
+  void put_uint32(uint32_t value, size_t offset) { this->put(value, offset); }
+  void put_uint64(uint64_t value, size_t offset) { this->put(value, offset); }
+  // Signed versions of the put functions
+  void put_int8(int8_t value, size_t offset) { this->put_uint8(static_cast<uint8_t>(value), offset); }
+  void put_int16(int16_t value, size_t offset) { this->put(value, offset); }
+  void put_int24(int32_t value, size_t offset) { this->put_uint32_(value, offset, 3); }
+  void put_int32(int32_t value, size_t offset) { this->put(value, offset); }
+  void put_int64(int64_t value, size_t offset) { this->put(value, offset); }
+  // Extra put functions
+  void put_float(float value, size_t offset) { this->put(value, offset); }
+  void put_double(double value, size_t offset) { this->put(value, offset); }
+  void put_bool(bool value, size_t offset) { this->put_uint8(value, offset); }
+  void put(const std::vector<uint8_t> &value, size_t offset) {
+    std::copy(value.begin(), value.end(), this->data_.begin() + offset);
+  }
+  void put_vector(const std::vector<uint8_t> &value, size_t offset) { this->put(value, offset); }
+  void put(const std::vector<uint8_t> &value) {
+    this->put_vector(value, this->position_);
+    this->position_ += value.size();
+  }
+  void put_vector(const std::vector<uint8_t> &value) { this->put(value); }
+
+  // Getters
+
+  inline size_t get_capacity() const { return this->data_.size(); }
+  inline size_t get_position() const { return this->position_; }
+  inline size_t get_limit() const { return this->limit_; }
+  inline size_t get_remaining() const { return this->get_limit() - this->get_position(); }
+  inline Endian get_endianness() const { return this->endianness_; }
+  inline void mark() { this->mark_ = this->position_; }
+  inline void big_endian() { this->endianness_ = BIG; }
+  inline void little_endian() { this->endianness_ = LITTLE; }
+  // retrieve a pointer to the underlying data.
+  std::vector<uint8_t> get_data() { return this->data_; };
+
+  void get_bytes(void *dest, size_t length) {
+    std::copy(this->data_.begin() + this->position_, this->data_.begin() + this->position_ + length, (uint8_t *) dest);
+    this->position_ += length;
+  }
+
+  void get_bytes(void *dest, size_t length, size_t offset) {
+    std::copy(this->data_.begin() + offset, this->data_.begin() + offset + length, (uint8_t *) dest);
+  }
+
+  void rewind() { this->position_ = 0; }
+  void reset() { this->position_ = this->mark_; }
+
+  void set_limit(size_t limit) { this->limit_ = limit; }
+  void set_position(size_t position) { this->position_ = position; }
+  void clear() {
+    this->limit_ = this->get_capacity();
+    this->position_ = 0;
+  }
+  void flip() {
+    this->limit_ = this->position_;
+    this->position_ = 0;
+  }
+
+ protected:
+  uint64_t get_uint64_(size_t offset, size_t length) const {
+    uint64_t value = 0;
+    if (this->endianness_ == LITTLE) {
+      offset += length;
+      while (length-- != 0) {
+        value <<= 8;
+        value |= this->data_[--offset];
+      }
+    } else {
+      while (length-- != 0) {
+        value <<= 8;
+        value |= this->data_[offset++];
+      }
+    }
+    return value;
+  }
+
+  uint64_t get_uint64_(size_t length) {
+    auto result = this->get_uint64_(this->position_, length);
+    this->position_ += length;
+    return result;
+  }
+  uint32_t get_uint32_(size_t offset, size_t length) const {
+    uint32_t value = 0;
+    if (this->endianness_ == LITTLE) {
+      offset += length;
+      while (length-- != 0) {
+        value <<= 8;
+        value |= this->data_[--offset];
+      }
+    } else {
+      while (length-- != 0) {
+        value <<= 8;
+        value |= this->data_[offset++];
+      }
+    }
+    return value;
+  }
+
+  uint32_t get_uint32_(size_t length) {
+    auto result = this->get_uint32_(this->position_, length);
+    this->position_ += length;
+    return result;
+  }
+
+  /// Putters
+
+  void put_uint64_(uint64_t value, size_t length) {
+    this->put_uint64_(value, this->position_, length);
+    this->position_ += length;
+  }
+  void put_uint32_(uint32_t value, size_t length) {
+    this->put_uint32_(value, this->position_, length);
+    this->position_ += length;
+  }
+
+  void put_uint64_(uint64_t value, size_t offset, size_t length) {
+    if (this->endianness_ == LITTLE) {
+      while (length-- != 0) {
+        this->data_[offset++] = static_cast<uint8_t>(value);
+        value >>= 8;
+      }
+    } else {
+      offset += length;
+      while (length-- != 0) {
+        this->data_[--offset] = static_cast<uint8_t>(value);
+        value >>= 8;
+      }
+    }
+  }
+
+  void put_uint32_(uint32_t value, size_t offset, size_t length) {
+    if (this->endianness_ == LITTLE) {
+      while (length-- != 0) {
+        this->data_[offset++] = static_cast<uint8_t>(value);
+        value >>= 8;
+      }
+    } else {
+      offset += length;
+      while (length-- != 0) {
+        this->data_[--offset] = static_cast<uint8_t>(value);
+        value >>= 8;
+      }
+    }
+  }
+  ByteBuffer(std::vector<uint8_t> const &data) : data_(data), limit_(data.size()) {}
+
+  std::vector<uint8_t> data_;
+  Endian endianness_{LITTLE};
+  size_t position_{0};
+  size_t mark_{0};
+  size_t limit_{0};
+};
+
+}  // namespace bytebuffer
+}  // namespace esphome
diff --git a/esphome/core/bytebuffer.cpp b/esphome/core/bytebuffer.cpp
deleted file mode 100644
index 9dd32bf87a..0000000000
--- a/esphome/core/bytebuffer.cpp
+++ /dev/null
@@ -1,167 +0,0 @@
-#include "bytebuffer.h"
-#include <cassert>
-#include "esphome/core/helpers.h"
-
-#include <list>
-#include <vector>
-
-namespace esphome {
-
-ByteBuffer ByteBuffer::wrap(const uint8_t *ptr, size_t len, Endian endianness) {
-  // there is a double copy happening here, could be optimized but at cost of clarity.
-  std::vector<uint8_t> data(ptr, ptr + len);
-  ByteBuffer buffer = {data};
-  buffer.endianness_ = endianness;
-  return buffer;
-}
-
-ByteBuffer ByteBuffer::wrap(std::vector<uint8_t> const &data, Endian endianness) {
-  ByteBuffer buffer = {data};
-  buffer.endianness_ = endianness;
-  return buffer;
-}
-
-ByteBuffer ByteBuffer::wrap(uint8_t value) {
-  ByteBuffer buffer = ByteBuffer(1);
-  buffer.put_uint8(value);
-  buffer.flip();
-  return buffer;
-}
-
-ByteBuffer ByteBuffer::wrap(uint16_t value, Endian endianness) {
-  ByteBuffer buffer = ByteBuffer(2, endianness);
-  buffer.put_uint16(value);
-  buffer.flip();
-  return buffer;
-}
-
-ByteBuffer ByteBuffer::wrap(uint32_t value, Endian endianness) {
-  ByteBuffer buffer = ByteBuffer(4, endianness);
-  buffer.put_uint32(value);
-  buffer.flip();
-  return buffer;
-}
-
-ByteBuffer ByteBuffer::wrap(uint64_t value, Endian endianness) {
-  ByteBuffer buffer = ByteBuffer(8, endianness);
-  buffer.put_uint64(value);
-  buffer.flip();
-  return buffer;
-}
-
-ByteBuffer ByteBuffer::wrap(float value, Endian endianness) {
-  ByteBuffer buffer = ByteBuffer(sizeof(float), endianness);
-  buffer.put_float(value);
-  buffer.flip();
-  return buffer;
-}
-
-ByteBuffer ByteBuffer::wrap(double value, Endian endianness) {
-  ByteBuffer buffer = ByteBuffer(sizeof(double), endianness);
-  buffer.put_double(value);
-  buffer.flip();
-  return buffer;
-}
-
-void ByteBuffer::set_limit(size_t limit) {
-  assert(limit <= this->get_capacity());
-  this->limit_ = limit;
-}
-void ByteBuffer::set_position(size_t position) {
-  assert(position <= this->get_limit());
-  this->position_ = position;
-}
-void ByteBuffer::clear() {
-  this->limit_ = this->get_capacity();
-  this->position_ = 0;
-}
-void ByteBuffer::flip() {
-  this->limit_ = this->position_;
-  this->position_ = 0;
-}
-
-/// Getters
-uint8_t ByteBuffer::get_uint8() {
-  assert(this->get_remaining() >= 1);
-  return this->data_[this->position_++];
-}
-uint64_t ByteBuffer::get_uint(size_t length) {
-  assert(this->get_remaining() >= length);
-  uint64_t value = 0;
-  if (this->endianness_ == LITTLE) {
-    this->position_ += length;
-    auto index = this->position_;
-    while (length-- != 0) {
-      value <<= 8;
-      value |= this->data_[--index];
-    }
-  } else {
-    while (length-- != 0) {
-      value <<= 8;
-      value |= this->data_[this->position_++];
-    }
-  }
-  return value;
-}
-
-uint32_t ByteBuffer::get_int24() {
-  auto value = this->get_uint24();
-  uint32_t mask = (~static_cast<uint32_t>(0)) << 23;
-  if ((value & mask) != 0)
-    value |= mask;
-  return value;
-}
-float ByteBuffer::get_float() {
-  assert(this->get_remaining() >= sizeof(float));
-  return bit_cast<float>(this->get_uint32());
-}
-double ByteBuffer::get_double() {
-  assert(this->get_remaining() >= sizeof(double));
-  return bit_cast<double>(this->get_uint64());
-}
-
-std::vector<uint8_t> ByteBuffer::get_vector(size_t length) {
-  assert(this->get_remaining() >= length);
-  auto start = this->data_.begin() + this->position_;
-  this->position_ += length;
-  return {start, start + length};
-}
-
-/// Putters
-void ByteBuffer::put_uint8(uint8_t value) {
-  assert(this->get_remaining() >= 1);
-  this->data_[this->position_++] = value;
-}
-
-void ByteBuffer::put_uint(uint64_t value, size_t length) {
-  assert(this->get_remaining() >= length);
-  if (this->endianness_ == LITTLE) {
-    while (length-- != 0) {
-      this->data_[this->position_++] = static_cast<uint8_t>(value);
-      value >>= 8;
-    }
-  } else {
-    this->position_ += length;
-    auto index = this->position_;
-    while (length-- != 0) {
-      this->data_[--index] = static_cast<uint8_t>(value);
-      value >>= 8;
-    }
-  }
-}
-void ByteBuffer::put_float(float value) {
-  static_assert(sizeof(float) == sizeof(uint32_t), "Float sizes other than 32 bit not supported");
-  assert(this->get_remaining() >= sizeof(float));
-  this->put_uint32(bit_cast<uint32_t>(value));
-}
-void ByteBuffer::put_double(double value) {
-  static_assert(sizeof(double) == sizeof(uint64_t), "Double sizes other than 64 bit not supported");
-  assert(this->get_remaining() >= sizeof(double));
-  this->put_uint64(bit_cast<uint64_t>(value));
-}
-void ByteBuffer::put_vector(const std::vector<uint8_t> &value) {
-  assert(this->get_remaining() >= value.size());
-  std::copy(value.begin(), value.end(), this->data_.begin() + this->position_);
-  this->position_ += value.size();
-}
-}  // namespace esphome
diff --git a/esphome/core/bytebuffer.h b/esphome/core/bytebuffer.h
deleted file mode 100644
index d44d01f275..0000000000
--- a/esphome/core/bytebuffer.h
+++ /dev/null
@@ -1,144 +0,0 @@
-#pragma once
-
-#include <utility>
-#include <vector>
-#include <cinttypes>
-#include <cstddef>
-
-namespace esphome {
-
-enum Endian { LITTLE, BIG };
-
-/**
- * A class modelled on the Java ByteBuffer class. It wraps a vector of bytes and permits putting and getting
- * items of various sizes, with an automatically incremented position.
- *
- * There are three variables maintained pointing into the buffer:
- *
- * capacity: the maximum amount of data that can be stored - set on construction and cannot be changed
- * limit: the limit of the data currently available to get or put
- * position: the current insert or extract position
- *
- * 0 <= position <= limit <= capacity
- *
- * In addition a mark can be set to the current position with mark(). A subsequent call to reset() will restore
- * the position to the mark.
- *
- * The buffer can be marked to be little-endian (default) or big-endian. All subsequent operations will use that order.
- *
- * The flip() operation will reset the position to 0 and limit to the current position. This is useful for reading
- * data from a buffer after it has been written.
- *
- */
-class ByteBuffer {
- public:
-  // Default constructor (compatibility with TEMPLATABLE_VALUE)
-  ByteBuffer() : ByteBuffer(std::vector<uint8_t>()) {}
-  /**
-   * Create a new Bytebuffer with the given capacity
-   */
-  ByteBuffer(size_t capacity, Endian endianness = LITTLE)
-      : data_(std::vector<uint8_t>(capacity)), endianness_(endianness), limit_(capacity){};
-  /**
-   * Wrap an existing vector in a ByteBufffer
-   */
-  static ByteBuffer wrap(std::vector<uint8_t> const &data, Endian endianness = LITTLE);
-  /**
-   * Wrap an existing array in a ByteBuffer. Note that this will create a copy of the data.
-   */
-  static ByteBuffer wrap(const uint8_t *ptr, size_t len, Endian endianness = LITTLE);
-  // Convenience functions to create a ByteBuffer from a value
-  static ByteBuffer wrap(uint8_t value);
-  static ByteBuffer wrap(uint16_t value, Endian endianness = LITTLE);
-  static ByteBuffer wrap(uint32_t value, Endian endianness = LITTLE);
-  static ByteBuffer wrap(uint64_t value, Endian endianness = LITTLE);
-  static ByteBuffer wrap(int8_t value) { return wrap(static_cast<uint8_t>(value)); }
-  static ByteBuffer wrap(int16_t value, Endian endianness = LITTLE) {
-    return wrap(static_cast<uint16_t>(value), endianness);
-  }
-  static ByteBuffer wrap(int32_t value, Endian endianness = LITTLE) {
-    return wrap(static_cast<uint32_t>(value), endianness);
-  }
-  static ByteBuffer wrap(int64_t value, Endian endianness = LITTLE) {
-    return wrap(static_cast<uint64_t>(value), endianness);
-  }
-  static ByteBuffer wrap(float value, Endian endianness = LITTLE);
-  static ByteBuffer wrap(double value, Endian endianness = LITTLE);
-  static ByteBuffer wrap(bool value) { return wrap(static_cast<uint8_t>(value)); }
-
-  // Get an integral value from the buffer, increment position by length
-  uint64_t get_uint(size_t length);
-  // Get one byte from the buffer, increment position by 1
-  uint8_t get_uint8();
-  // Get a 16 bit unsigned value, increment by 2
-  uint16_t get_uint16() { return static_cast<uint16_t>(this->get_uint(sizeof(uint16_t))); };
-  // Get a 24 bit unsigned value, increment by 3
-  uint32_t get_uint24() { return static_cast<uint32_t>(this->get_uint(3)); };
-  // Get a 32 bit unsigned value, increment by 4
-  uint32_t get_uint32() { return static_cast<uint32_t>(this->get_uint(sizeof(uint32_t))); };
-  // Get a 64 bit unsigned value, increment by 8
-  uint64_t get_uint64() { return this->get_uint(sizeof(uint64_t)); };
-  // Signed versions of the get functions
-  uint8_t get_int8() { return static_cast<int8_t>(this->get_uint8()); };
-  int16_t get_int16() { return static_cast<int16_t>(this->get_uint(sizeof(int16_t))); }
-  uint32_t get_int24();
-  int32_t get_int32() { return static_cast<int32_t>(this->get_uint(sizeof(int32_t))); }
-  int64_t get_int64() { return static_cast<int64_t>(this->get_uint(sizeof(int64_t))); }
-  // Get a float value, increment by 4
-  float get_float();
-  // Get a double value, increment by 8
-  double get_double();
-  // Get a bool value, increment by 1
-  bool get_bool() { return this->get_uint8(); }
-  // Get vector of bytes, increment by length
-  std::vector<uint8_t> get_vector(size_t length);
-
-  // Put values into the buffer, increment the position accordingly
-  // put any integral value, length represents the number of bytes
-  void put_uint(uint64_t value, size_t length);
-  void put_uint8(uint8_t value);
-  void put_uint16(uint16_t value) { this->put_uint(value, sizeof(uint16_t)); }
-  void put_uint24(uint32_t value) { this->put_uint(value, 3); }
-  void put_uint32(uint32_t value) { this->put_uint(value, sizeof(uint32_t)); }
-  void put_uint64(uint64_t value) { this->put_uint(value, sizeof(uint64_t)); }
-  // Signed versions of the put functions
-  void put_int8(int8_t value) { this->put_uint8(static_cast<uint8_t>(value)); }
-  void put_int16(int32_t value) { this->put_uint(static_cast<uint16_t>(value), sizeof(uint16_t)); }
-  void put_int24(int32_t value) { this->put_uint(static_cast<uint32_t>(value), 3); }
-  void put_int32(int32_t value) { this->put_uint(static_cast<uint32_t>(value), sizeof(uint32_t)); }
-  void put_int64(int64_t value) { this->put_uint(static_cast<uint64_t>(value), sizeof(uint64_t)); }
-  // Extra put functions
-  void put_float(float value);
-  void put_double(double value);
-  void put_bool(bool value) { this->put_uint8(value); }
-  void put_vector(const std::vector<uint8_t> &value);
-
-  inline size_t get_capacity() const { return this->data_.size(); }
-  inline size_t get_position() const { return this->position_; }
-  inline size_t get_limit() const { return this->limit_; }
-  inline size_t get_remaining() const { return this->get_limit() - this->get_position(); }
-  inline Endian get_endianness() const { return this->endianness_; }
-  inline void mark() { this->mark_ = this->position_; }
-  inline void big_endian() { this->endianness_ = BIG; }
-  inline void little_endian() { this->endianness_ = LITTLE; }
-  void set_limit(size_t limit);
-  void set_position(size_t position);
-  // set position to 0, limit to capacity.
-  void clear();
-  // set limit to current position, postition to zero. Used when swapping from write to read operations.
-  void flip();
-  // retrieve a pointer to the underlying data.
-  std::vector<uint8_t> get_data() { return this->data_; };
-  void rewind() { this->position_ = 0; }
-  void reset() { this->position_ = this->mark_; }
-
- protected:
-  ByteBuffer(std::vector<uint8_t> const &data) : data_(data), limit_(data.size()) {}
-  std::vector<uint8_t> data_;
-  Endian endianness_{LITTLE};
-  size_t position_{0};
-  size_t mark_{0};
-  size_t limit_{0};
-};
-
-}  // namespace esphome
diff --git a/tests/components/bytebuffer/common.yaml b/tests/components/bytebuffer/common.yaml
new file mode 100644
index 0000000000..177f487e1e
--- /dev/null
+++ b/tests/components/bytebuffer/common.yaml
@@ -0,0 +1,161 @@
+bytebuffer:
+
+esphome:
+  on_boot:
+    - lambda: |-
+        using namespace bytebuffer;
+        auto buf = ByteBuffer(16);
+        assert(buf.get_endianness() == LITTLE);
+        assert(buf.get_remaining() == 16);
+        buf.set_limit(10);
+        assert(buf.get_capacity() == 16);
+        buf.put_uint8(1);
+        assert(buf.get_remaining() == 9);
+        buf.put_uint16(0xABCD);
+        auto da = buf.get_data();
+        assert(buf.get_uint8(0) == 1);
+        auto x = buf.get_uint16(1);
+        assert(buf.get_uint16(1) == 0xABCD);
+        assert(buf.get_remaining() == 7);
+        buf.put_uint32(0x12345678UL);
+        assert(buf.get_uint32(3) == 0x12345678UL);
+        assert(buf.get_remaining() == 3);
+        assert(buf.get_data()[1] == 0xCD);
+        assert(buf.get_data()[2] == 0xAB);
+        assert(buf.get_data()[3] == 0x78);
+        assert(buf.get_data()[4] == 0x56);
+        assert(buf.get_data()[5] == 0x34);
+        assert(buf.get_data()[6] == 0x12);
+        buf.flip();
+        assert(buf.get_capacity() == 16);
+        assert(buf.get_uint32(3) == 0x12345678UL);
+        assert(buf.get_uint8(0) == 1);
+        assert(buf.get_uint16(1) == 0xABCD);
+        buf.put_uint16(0x1234, 1);
+        assert(buf.get_uint16(1) == 0x1234);
+        assert(buf.get_remaining() == 7);
+        assert(buf.get_uint8() == 1);
+        assert(buf.get_uint16() == 0x1234);
+        assert(buf.get_uint32() == 0x12345678ul);
+        assert(buf.get_remaining() == 0);
+        assert(buf.get_remaining() == 0);
+        buf.rewind();
+        buf.big_endian();
+        assert(buf.get_remaining() == 7);
+        assert(buf.get_uint8() == 1);
+        assert(buf.get_uint16() == 0x3412);
+        buf.mark();
+        assert(buf.get_uint32() == 0x78563412ul);
+        assert(buf.get_remaining() == 0);
+        buf.reset();
+        assert(buf.get_remaining() == 4);
+        assert(buf.get_uint32() == 0x78563412ul);
+        auto buf1 = ByteBuffer::wrap(buf.get_data().data(), buf.get_limit());
+        buf.clear();
+        assert(buf.get_position() == 0);
+        assert(buf.get_capacity() == 16);
+        assert(buf.get_limit() == 16);
+        assert(buf1.get_remaining() == 7);
+        assert(buf1.get_capacity() == 7);
+        buf1.set_position(3);
+        assert(buf1.get_uint32() == 0x12345678ul);
+        buf1.clear();
+        assert(buf1.get_limit() == 7);
+        assert(buf1.get_capacity() == 7);
+        assert(buf1.get_position() == 0);
+        float f = 1.2345;
+        buf1.put_float(f);
+        buf1.flip();
+        assert(buf1.get_remaining() == 4);
+        assert(buf1.get_float() == f);
+        buf1.clear();
+        buf1.put_uint16(-32760);
+        buf1.put_uint24(-302760);
+        buf1.flip();
+        assert(buf1.get_int16() == -32760);
+        assert(buf1.get_int24() == -302760);
+        uint8_t arr[4] = {0x10, 0x20, 0x30, 0x40};
+        buf1 = ByteBuffer::wrap(arr, 4);
+        assert(buf1.get_capacity() == 4);
+        assert(buf1.get_limit() == 4);
+        assert(buf1.get_position() == 0);
+        assert(buf1.get_uint32() == 0x40302010UL);
+        assert(buf1.get_position() == 4);
+        assert(buf1.get_remaining() == 0);
+        std::vector<uint8_t> vec{};
+        vec.push_back(0x10);
+        vec.push_back(0x20);
+        vec.push_back(0x30);
+        vec.push_back(0x40);
+        buf1 = ByteBuffer::wrap(vec);
+        assert(buf1.get_capacity() == 4);
+        assert(buf1.get_limit() == 4);
+        assert(buf1.get_position() == 0);
+        buf1.mark();
+        buf1.reset();
+        assert(buf1.get_uint32() == 0x40302010UL);
+        buf = ByteBuffer::wrap(true);
+        assert(buf.get_bool() == true);
+        buf = ByteBuffer::wrap((uint8_t)0xFE);
+        assert(buf.get_uint8() == 0xFE);
+        buf = ByteBuffer::wrap((uint16_t)0xA5A6, BIG);
+        assert(buf.get_remaining() == 2);
+        assert(buf.get_position() == 0);
+        assert(buf.get_capacity() == 2);
+        assert(buf.get_endianness() == BIG);
+        assert(buf.get_data()[0] == 0xA5);
+        assert(buf.get_uint16() == 0xA5A6);
+        buf.flip();
+        buf.little_endian();
+        assert(buf.get_uint16() == 0xA6A5);
+        buf = ByteBuffer::wrap(f, BIG);
+        assert(buf.get_float() == f);
+        double d = 1.2345678E7;
+        buf = ByteBuffer::wrap(d, BIG);
+        assert(buf.get_double() == d);
+        buf = ByteBuffer::wrap({1, 2, 3, 4}, BIG);
+        assert(buf.get_endianness() == BIG);
+        assert(buf.get_remaining() == 4);
+        assert(buf.get_data()[2] == 3);
+        buf.little_endian();
+        assert(buf.get_data()[2] == 3);
+        assert(buf.get_uint16() == 0x0201);
+        buf.big_endian();
+        assert(buf.get_uint16() == 0x0304);
+        buf.rewind();
+        vec = buf.get_vector(3);
+        assert(buf.get_remaining() == 1);
+        assert(vec[0] == 1);
+        assert(vec.size() == 3);
+        buf = ByteBuffer(10);
+        buf.put_vector(vec);
+        assert(buf.get_remaining() == 7);
+        buf.flip();
+        assert(buf.get_remaining() == 3);
+        assert(buf.get_uint24() == 0x030201);
+        buf = ByteBuffer(64);
+        buf.put_uint8(1, 1);
+        buf.put_uint16(16, 2);
+        buf.put_uint32(1232, 4);
+        buf.put_uint64(123432ul, 8);
+        buf.put_float(1.2f, 16);
+        buf.put_int24(0x678, 20);
+
+        assert(buf.get_uint8(1) == 1);
+        assert(buf.get<uint8_t>(1) == 1);
+        assert(buf.get_uint16(2) == 16);
+        assert(buf.get<uint16_t>(2) == 16);
+        assert(buf.get_uint32(4) == 1232);
+        assert(buf.get<uint32_t>(4) == 1232);
+        assert(buf.get_uint64(8) == 123432ul);
+        assert(buf.get<uint64_t>(8) == 123432ul);
+        assert(buf.get_float(16) == 1.2f);
+        assert(buf.get<float>(16) == 1.2f);
+        assert(buf.get_int24(20) == 0x678);
+        buf.clear();
+        buf.put(1.234, 10);
+        double dx = buf.get<double>(10);
+        assert(dx == 1.234);
+        buf.put((uint16_t)1, 10);
+        assert(buf.get_uint16(10) == 1);
+        ESP_LOGD("bytebuffer", "******************** All tests succeeded");
diff --git a/tests/components/bytebuffer/test.esp32-ard.yaml b/tests/components/bytebuffer/test.esp32-ard.yaml
new file mode 100644
index 0000000000..380ca87628
--- /dev/null
+++ b/tests/components/bytebuffer/test.esp32-ard.yaml
@@ -0,0 +1 @@
+!include common.yaml
diff --git a/tests/components/bytebuffer/test.esp32-c3-ard.yaml b/tests/components/bytebuffer/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..380ca87628
--- /dev/null
+++ b/tests/components/bytebuffer/test.esp32-c3-ard.yaml
@@ -0,0 +1 @@
+!include common.yaml
diff --git a/tests/components/bytebuffer/test.esp32-c3-idf.yaml b/tests/components/bytebuffer/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..380ca87628
--- /dev/null
+++ b/tests/components/bytebuffer/test.esp32-c3-idf.yaml
@@ -0,0 +1 @@
+!include common.yaml
diff --git a/tests/components/bytebuffer/test.esp32-idf.yaml b/tests/components/bytebuffer/test.esp32-idf.yaml
new file mode 100644
index 0000000000..380ca87628
--- /dev/null
+++ b/tests/components/bytebuffer/test.esp32-idf.yaml
@@ -0,0 +1 @@
+!include common.yaml
diff --git a/tests/components/bytebuffer/test.esp8266-ard.yaml b/tests/components/bytebuffer/test.esp8266-ard.yaml
new file mode 100644
index 0000000000..380ca87628
--- /dev/null
+++ b/tests/components/bytebuffer/test.esp8266-ard.yaml
@@ -0,0 +1 @@
+!include common.yaml
diff --git a/tests/components/bytebuffer/test.host.yaml b/tests/components/bytebuffer/test.host.yaml
new file mode 100644
index 0000000000..380ca87628
--- /dev/null
+++ b/tests/components/bytebuffer/test.host.yaml
@@ -0,0 +1 @@
+!include common.yaml
diff --git a/tests/components/bytebuffer/test.rp2040-ard.yaml b/tests/components/bytebuffer/test.rp2040-ard.yaml
new file mode 100644
index 0000000000..380ca87628
--- /dev/null
+++ b/tests/components/bytebuffer/test.rp2040-ard.yaml
@@ -0,0 +1 @@
+!include common.yaml

From 88627095fbc049ca342325be22d72e66e9b9c28a Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 29 Oct 2024 11:12:32 +1100
Subject: [PATCH 049/282] [http_request] Always return defined server response
 status (#7689)

---
 esphome/components/http_request/http_request_arduino.cpp       | 3 +--
 esphome/components/http_request/http_request_idf.cpp           | 3 +--
 esphome/components/http_request/ota/ota_http_request.cpp       | 2 +-
 esphome/components/http_request/update/http_request_update.cpp | 2 +-
 4 files changed, 4 insertions(+), 6 deletions(-)

diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp
index 2148d92ad2..f000082034 100644
--- a/esphome/components/http_request/http_request_arduino.cpp
+++ b/esphome/components/http_request/http_request_arduino.cpp
@@ -116,8 +116,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::start(std::string url, std::s
   if (container->status_code < 200 || container->status_code >= 300) {
     ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code);
     this->status_momentary_error("failed", 1000);
-    container->end();
-    return nullptr;
+    // Still return the container, so it can be used to get the status code and error message
   }
 
   int content_length = container->client_.getSize();
diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp
index 3819f5544e..e47e1d488e 100644
--- a/esphome/components/http_request/http_request_idf.cpp
+++ b/esphome/components/http_request/http_request_idf.cpp
@@ -172,8 +172,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
 
   ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code);
   this->status_momentary_error("failed", 1000);
-  esp_http_client_cleanup(client);
-  return nullptr;
+  return container;
 }
 
 int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp
index 1553de0bc1..f7c941da18 100644
--- a/esphome/components/http_request/ota/ota_http_request.cpp
+++ b/esphome/components/http_request/ota/ota_http_request.cpp
@@ -106,7 +106,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
 
   auto container = this->parent_->get(url_with_auth);
 
-  if (container == nullptr) {
+  if (container == nullptr || container->status_code != 200) {
     return OTA_CONNECTION_ERROR;
   }
 
diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp
index 059148e7e5..4d913f2158 100644
--- a/esphome/components/http_request/update/http_request_update.cpp
+++ b/esphome/components/http_request/update/http_request_update.cpp
@@ -31,7 +31,7 @@ void HttpRequestUpdate::setup() {
 void HttpRequestUpdate::update() {
   auto container = this->request_parent_->get(this->source_url_);
 
-  if (container == nullptr) {
+  if (container == nullptr || container->status_code != 200) {
     std::string msg = str_sprintf("Failed to fetch manifest from %s", this->source_url_.c_str());
     this->status_set_error(msg.c_str());
     return;

From 63e4d4b493ec6c704ac6d41273b4190614726578 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 29 Oct 2024 13:56:32 +1100
Subject: [PATCH 050/282] [font] Fix failure with bitmap fonts (#7691)

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

diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py
index dacd0779b1..a3f11df50e 100644
--- a/esphome/components/font/__init__.py
+++ b/esphome/components/font/__init__.py
@@ -344,7 +344,7 @@ class TrueTypeFontWrapper:
         return offset_x, offset_y
 
     def getmask(self, glyph, **kwargs):
-        return self.font.getmask(glyph, **kwargs)
+        return self.font.getmask(str(glyph), **kwargs)
 
     def getmetrics(self, glyphs):
         return self.font.getmetrics()
@@ -359,7 +359,7 @@ class BitmapFontWrapper:
         return 0, 0
 
     def getmask(self, glyph, **kwargs):
-        return self.font.getmask(glyph, **kwargs)
+        return self.font.getmask(str(glyph), **kwargs)
 
     def getmetrics(self, glyphs):
         max_height = 0

From df750d0d11ce31e793de06d0e00663c8e23a3680 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 29 Oct 2024 14:05:58 +1100
Subject: [PATCH 051/282] [http_request] Add enum for status codes (#7690)

---
 .../components/http_request/http_request.h    | 57 +++++++++++++++++++
 .../http_request/http_request_arduino.cpp     |  2 +-
 .../http_request/http_request_idf.cpp         | 17 ++----
 .../http_request/ota/ota_http_request.cpp     |  2 +-
 .../update/http_request_update.cpp            |  2 +-
 5 files changed, 65 insertions(+), 15 deletions(-)

diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h
index c01baf8644..d87d9b8a45 100644
--- a/esphome/components/http_request/http_request.h
+++ b/esphome/components/http_request/http_request.h
@@ -22,6 +22,63 @@ struct Header {
   const char *value;
 };
 
+// Some common HTTP status codes
+enum HttpStatus {
+  HTTP_STATUS_OK = 200,
+  HTTP_STATUS_NO_CONTENT = 204,
+  HTTP_STATUS_PARTIAL_CONTENT = 206,
+
+  /* 3xx - Redirection */
+  HTTP_STATUS_MULTIPLE_CHOICES = 300,
+  HTTP_STATUS_MOVED_PERMANENTLY = 301,
+  HTTP_STATUS_FOUND = 302,
+  HTTP_STATUS_SEE_OTHER = 303,
+  HTTP_STATUS_NOT_MODIFIED = 304,
+  HTTP_STATUS_TEMPORARY_REDIRECT = 307,
+  HTTP_STATUS_PERMANENT_REDIRECT = 308,
+
+  /* 4XX - CLIENT ERROR */
+  HTTP_STATUS_BAD_REQUEST = 400,
+  HTTP_STATUS_UNAUTHORIZED = 401,
+  HTTP_STATUS_FORBIDDEN = 403,
+  HTTP_STATUS_NOT_FOUND = 404,
+  HTTP_STATUS_METHOD_NOT_ALLOWED = 405,
+  HTTP_STATUS_NOT_ACCEPTABLE = 406,
+  HTTP_STATUS_LENGTH_REQUIRED = 411,
+
+  /* 5xx - Server Error */
+  HTTP_STATUS_INTERNAL_ERROR = 500
+};
+
+/**
+ * @brief Returns true if the HTTP status code is a redirect.
+ *
+ * @param status the HTTP status code to check
+ * @return true if the status code is a redirect, false otherwise
+ */
+inline bool is_redirect(int const status) {
+  switch (status) {
+    case HTTP_STATUS_MOVED_PERMANENTLY:
+    case HTTP_STATUS_FOUND:
+    case HTTP_STATUS_SEE_OTHER:
+    case HTTP_STATUS_TEMPORARY_REDIRECT:
+    case HTTP_STATUS_PERMANENT_REDIRECT:
+      return true;
+    default:
+      return false;
+  }
+}
+
+/**
+ * @brief Checks if the given HTTP status code indicates a successful request.
+ *
+ * A successful request is one where the status code is in the range 200-299
+ *
+ * @param status the HTTP status code to check
+ * @return true if the status code indicates a successful request, false otherwise
+ */
+inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; }
+
 class HttpRequestComponent;
 
 class HttpContainer : public Parented<HttpRequestComponent> {
diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp
index f000082034..af1eb6f459 100644
--- a/esphome/components/http_request/http_request_arduino.cpp
+++ b/esphome/components/http_request/http_request_arduino.cpp
@@ -113,7 +113,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::start(std::string url, std::s
     return nullptr;
   }
 
-  if (container->status_code < 200 || container->status_code >= 300) {
+  if (!is_success(container->status_code)) {
     ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code);
     this->status_momentary_error("failed", 1000);
     // Still return the container, so it can be used to get the status code and error message
diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp
index e47e1d488e..c6c567b620 100644
--- a/esphome/components/http_request/http_request_idf.cpp
+++ b/esphome/components/http_request/http_request_idf.cpp
@@ -6,7 +6,6 @@
 #include "esphome/components/watchdog/watchdog.h"
 
 #include "esphome/core/application.h"
-#include "esphome/core/defines.h"
 #include "esphome/core/log.h"
 
 #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
@@ -118,20 +117,14 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
     return nullptr;
   }
 
-  auto is_ok = [](int code) { return code >= HttpStatus_Ok && code < HttpStatus_MultipleChoices; };
-
   container->content_length = esp_http_client_fetch_headers(client);
   container->status_code = esp_http_client_get_status_code(client);
-  if (is_ok(container->status_code)) {
+  if (is_success(container->status_code)) {
     container->duration_ms = millis() - start;
     return container;
   }
 
   if (this->follow_redirects_) {
-    auto is_redirect = [](int code) {
-      return code == HttpStatus_MovedPermanently || code == HttpStatus_Found || code == HttpStatus_SeeOther ||
-             code == HttpStatus_TemporaryRedirect || code == HttpStatus_PermanentRedirect;
-    };
     auto num_redirects = this->redirect_limit_;
     while (is_redirect(container->status_code) && num_redirects > 0) {
       err = esp_http_client_set_redirection(client);
@@ -142,9 +135,9 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
         return nullptr;
       }
 #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
-      char url[256]{};
-      if (esp_http_client_get_url(client, url, sizeof(url) - 1) == ESP_OK) {
-        ESP_LOGV(TAG, "redirecting to url: %s", url);
+      char redirect_url[256]{};
+      if (esp_http_client_get_url(client, redirect_url, sizeof(redirect_url) - 1) == ESP_OK) {
+        ESP_LOGV(TAG, "redirecting to url: %s", redirect_url);
       }
 #endif
       err = esp_http_client_open(client, 0);
@@ -157,7 +150,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
 
       container->content_length = esp_http_client_fetch_headers(client);
       container->status_code = esp_http_client_get_status_code(client);
-      if (is_ok(container->status_code)) {
+      if (is_success(container->status_code)) {
         container->duration_ms = millis() - start;
         return container;
       }
diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp
index f7c941da18..cec30d72ec 100644
--- a/esphome/components/http_request/ota/ota_http_request.cpp
+++ b/esphome/components/http_request/ota/ota_http_request.cpp
@@ -106,7 +106,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
 
   auto container = this->parent_->get(url_with_auth);
 
-  if (container == nullptr || container->status_code != 200) {
+  if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
     return OTA_CONNECTION_ERROR;
   }
 
diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp
index 4d913f2158..0e0966c22b 100644
--- a/esphome/components/http_request/update/http_request_update.cpp
+++ b/esphome/components/http_request/update/http_request_update.cpp
@@ -31,7 +31,7 @@ void HttpRequestUpdate::setup() {
 void HttpRequestUpdate::update() {
   auto container = this->request_parent_->get(this->source_url_);
 
-  if (container == nullptr || container->status_code != 200) {
+  if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
     std::string msg = str_sprintf("Failed to fetch manifest from %s", this->source_url_.c_str());
     this->status_set_error(msg.c_str());
     return;

From 302ba2874e12960c76a624fa6066bf5df1ffac2e Mon Sep 17 00:00:00 2001
From: Satoshi YAMADA <slakichi@users.noreply.github.com>
Date: Tue, 29 Oct 2024 12:08:08 +0900
Subject: [PATCH 052/282] Support W5500 SPI-Ethernet polling mode if framework
 is supported (#7503)

---
 esphome/components/ethernet/__init__.py       | 56 ++++++++++++++++++-
 .../ethernet/ethernet_component.cpp           | 15 ++++-
 .../components/ethernet/ethernet_component.h  |  8 ++-
 esphome/const.py                              |  1 +
 4 files changed, 77 insertions(+), 3 deletions(-)

diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py
index 475d60df53..dca37b8dc2 100644
--- a/esphome/components/ethernet/__init__.py
+++ b/esphome/components/ethernet/__init__.py
@@ -1,3 +1,4 @@
+import logging
 from esphome import pins
 import esphome.codegen as cg
 from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
@@ -23,6 +24,7 @@ from esphome.const import (
     CONF_MISO_PIN,
     CONF_MOSI_PIN,
     CONF_PAGE_ID,
+    CONF_POLLING_INTERVAL,
     CONF_RESET_PIN,
     CONF_SPI,
     CONF_STATIC_IP,
@@ -30,13 +32,16 @@ from esphome.const import (
     CONF_TYPE,
     CONF_USE_ADDRESS,
     CONF_VALUE,
+    KEY_CORE,
+    KEY_FRAMEWORK_VERSION,
 )
-from esphome.core import CORE, coroutine_with_priority
+from esphome.core import CORE, TimePeriodMilliseconds, coroutine_with_priority
 import esphome.final_validate as fv
 
 CONFLICTS_WITH = ["wifi"]
 DEPENDENCIES = ["esp32"]
 AUTO_LOAD = ["network"]
+LOGGER = logging.getLogger(__name__)
 
 ethernet_ns = cg.esphome_ns.namespace("ethernet")
 PHYRegister = ethernet_ns.struct("PHYRegister")
@@ -63,6 +68,7 @@ ETHERNET_TYPES = {
 }
 
 SPI_ETHERNET_TYPES = ["W5500"]
+SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10)
 
 emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t")
 emac_rmii_clock_gpio_t = cg.global_ns.enum("emac_rmii_clock_gpio_t")
@@ -100,6 +106,24 @@ EthernetComponent = ethernet_ns.class_("EthernetComponent", cg.Component)
 ManualIP = ethernet_ns.struct("ManualIP")
 
 
+def _is_framework_spi_polling_mode_supported():
+    # SPI Ethernet without IRQ feature is added in
+    # esp-idf >= (5.3+ ,5.2.1+, 5.1.4) and arduino-esp32 >= 3.0.0
+    framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
+    if CORE.using_esp_idf:
+        if framework_version >= cv.Version(5, 3, 0):
+            return True
+        if cv.Version(5, 3, 0) > framework_version >= cv.Version(5, 2, 1):
+            return True
+        if cv.Version(5, 2, 0) > framework_version >= cv.Version(5, 1, 4):
+            return True
+        return False
+    if CORE.using_arduino:
+        return framework_version >= cv.Version(3, 0, 0)
+    # fail safe: Unknown framework
+    return False
+
+
 def _validate(config):
     if CONF_USE_ADDRESS not in config:
         if CONF_MANUAL_IP in config:
@@ -107,6 +131,27 @@ def _validate(config):
         else:
             use_address = CORE.name + config[CONF_DOMAIN]
         config[CONF_USE_ADDRESS] = use_address
+    if config[CONF_TYPE] in SPI_ETHERNET_TYPES:
+        if _is_framework_spi_polling_mode_supported():
+            if CONF_POLLING_INTERVAL in config and CONF_INTERRUPT_PIN in config:
+                raise cv.Invalid(
+                    f"Cannot specify more than one of {CONF_INTERRUPT_PIN}, {CONF_POLLING_INTERVAL}"
+                )
+            if CONF_POLLING_INTERVAL not in config and CONF_INTERRUPT_PIN not in config:
+                config[CONF_POLLING_INTERVAL] = SPI_ETHERNET_DEFAULT_POLLING_INTERVAL
+        else:
+            if CONF_POLLING_INTERVAL in config:
+                raise cv.Invalid(
+                    "In this version of the framework "
+                    f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), "
+                    f"'{CONF_POLLING_INTERVAL}' is not supported."
+                )
+            if CONF_INTERRUPT_PIN not in config:
+                raise cv.Invalid(
+                    "In this version of the framework "
+                    f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), "
+                    f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]."
+                )
     return config
 
 
@@ -157,6 +202,11 @@ SPI_SCHEMA = BASE_SCHEMA.extend(
             cv.Optional(CONF_CLOCK_SPEED, default="26.67MHz"): cv.All(
                 cv.frequency, cv.int_range(int(8e6), int(80e6))
             ),
+            # Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate()
+            cv.Optional(CONF_POLLING_INTERVAL): cv.All(
+                cv.positive_time_period_milliseconds,
+                cv.Range(min=TimePeriodMilliseconds(milliseconds=1)),
+            ),
         }
     ),
 )
@@ -234,6 +284,10 @@ async def to_code(config):
         cg.add(var.set_cs_pin(config[CONF_CS_PIN]))
         if CONF_INTERRUPT_PIN in config:
             cg.add(var.set_interrupt_pin(config[CONF_INTERRUPT_PIN]))
+        else:
+            cg.add(var.set_polling_interval(config[CONF_POLLING_INTERVAL]))
+        if _is_framework_spi_polling_mode_supported():
+            cg.add_define("USE_ETHERNET_SPI_POLLING_SUPPORT")
         if CONF_RESET_PIN in config:
             cg.add(var.set_reset_pin(config[CONF_RESET_PIN]))
         cg.add(var.set_clock_speed(config[CONF_CLOCK_SPEED]))
diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp
index 00c7ae4ab8..08f5fa6642 100644
--- a/esphome/components/ethernet/ethernet_component.cpp
+++ b/esphome/components/ethernet/ethernet_component.cpp
@@ -116,6 +116,9 @@ void EthernetComponent::setup() {
   eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(spi_handle);
 #endif
   w5500_config.int_gpio_num = this->interrupt_pin_;
+#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
+  w5500_config.poll_period_ms = this->polling_interval_;
+#endif
   phy_config.phy_addr = this->phy_addr_spi_;
   phy_config.reset_gpio_num = this->reset_pin_;
 
@@ -327,7 +330,14 @@ void EthernetComponent::dump_config() {
   ESP_LOGCONFIG(TAG, "  MISO Pin: %u", this->miso_pin_);
   ESP_LOGCONFIG(TAG, "  MOSI Pin: %u", this->mosi_pin_);
   ESP_LOGCONFIG(TAG, "  CS Pin: %u", this->cs_pin_);
-  ESP_LOGCONFIG(TAG, "  IRQ Pin: %u", this->interrupt_pin_);
+#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
+  if (this->polling_interval_ != 0) {
+    ESP_LOGCONFIG(TAG, "  Polling Interval: %lu ms", this->polling_interval_);
+  } else
+#endif
+  {
+    ESP_LOGCONFIG(TAG, "  IRQ Pin: %d", this->interrupt_pin_);
+  }
   ESP_LOGCONFIG(TAG, "  Reset Pin: %d", this->reset_pin_);
   ESP_LOGCONFIG(TAG, "  Clock Speed: %d MHz", this->clock_speed_ / 1000000);
 #else
@@ -536,6 +546,9 @@ void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
 void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
 void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; }
 void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; }
+#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
+void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; }
+#endif
 #else
 void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; }
 void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; }
diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h
index 5ee430c046..fb178431d5 100644
--- a/esphome/components/ethernet/ethernet_component.h
+++ b/esphome/components/ethernet/ethernet_component.h
@@ -67,6 +67,9 @@ class EthernetComponent : public Component {
   void set_interrupt_pin(uint8_t interrupt_pin);
   void set_reset_pin(uint8_t reset_pin);
   void set_clock_speed(int clock_speed);
+#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
+  void set_polling_interval(uint32_t polling_interval);
+#endif
 #else
   void set_phy_addr(uint8_t phy_addr);
   void set_power_pin(int power_pin);
@@ -108,10 +111,13 @@ class EthernetComponent : public Component {
   uint8_t miso_pin_;
   uint8_t mosi_pin_;
   uint8_t cs_pin_;
-  uint8_t interrupt_pin_;
+  int interrupt_pin_{-1};
   int reset_pin_{-1};
   int phy_addr_spi_{-1};
   int clock_speed_;
+#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
+  uint32_t polling_interval_{0};
+#endif
 #else
   uint8_t phy_addr_{0};
   int power_pin_{-1};
diff --git a/esphome/const.py b/esphome/const.py
index 54ebb2815f..c39061631b 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -666,6 +666,7 @@ CONF_PMC_1_0 = "pmc_1_0"
 CONF_PMC_10_0 = "pmc_10_0"
 CONF_PMC_2_5 = "pmc_2_5"
 CONF_PMC_4_0 = "pmc_4_0"
+CONF_POLLING_INTERVAL = "polling_interval"
 CONF_PORT = "port"
 CONF_POSITION = "position"
 CONF_POSITION_ACTION = "position_action"

From 444c0fc67f4284e42e00ad61466febe999122690 Mon Sep 17 00:00:00 2001
From: Jordan Zucker <jordan.zucker@gmail.com>
Date: Mon, 28 Oct 2024 20:09:22 -0700
Subject: [PATCH 053/282] Add asdf to gitignore (and dockerignore) (#7686)

---
 .dockerignore | 3 +++
 .gitignore    | 3 +++
 2 files changed, 6 insertions(+)

diff --git a/.dockerignore b/.dockerignore
index 9f14b98059..7998ff877f 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -75,6 +75,9 @@ target/
 # pyenv
 .python-version
 
+# asdf
+.tool-versions
+
 # celery beat schedule file
 celerybeat-schedule
 
diff --git a/.gitignore b/.gitignore
index 79820249ac..ad38e26fdd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -75,6 +75,9 @@ cov.xml
 # pyenv
 .python-version
 
+# asdf
+.tool-versions
+
 # Environments
 .env
 .venv

From 90b076eccd67253fb9ce612251130a76039e549d Mon Sep 17 00:00:00 2001
From: Jordan Zucker <jordan.zucker@gmail.com>
Date: Mon, 28 Oct 2024 20:43:02 -0700
Subject: [PATCH 054/282] Add more prometheus metrics (#7683)

---
 .../prometheus/prometheus_handler.cpp         | 43 +++++++++++++++++++
 .../prometheus/prometheus_handler.h           |  7 +++
 tests/components/prometheus/common.yaml       | 14 ++++++
 3 files changed, 64 insertions(+)

diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp
index 3e9cf81e6e..63b3fdf53f 100644
--- a/esphome/components/prometheus/prometheus_handler.cpp
+++ b/esphome/components/prometheus/prometheus_handler.cpp
@@ -50,6 +50,12 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) {
     this->lock_row_(stream, obj);
 #endif
 
+#ifdef USE_TEXT_SENSOR
+  this->text_sensor_type_(stream);
+  for (auto *obj : App.get_text_sensors())
+    this->text_sensor_row_(stream, obj);
+#endif
+
   req->send(stream);
 }
 
@@ -349,6 +355,43 @@ void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj)
 }
 #endif
 
+// Type-specific implementation
+#ifdef USE_TEXT_SENSOR
+void PrometheusHandler::text_sensor_type_(AsyncResponseStream *stream) {
+  stream->print(F("#TYPE esphome_text_sensor_value gauge\n"));
+  stream->print(F("#TYPE esphome_text_sensor_failed gauge\n"));
+}
+void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_sensor::TextSensor *obj) {
+  if (obj->is_internal() && !this->include_internal_)
+    return;
+  if (obj->has_state()) {
+    // We have a valid value, output this value
+    stream->print(F("esphome_text_sensor_failed{id=\""));
+    stream->print(relabel_id_(obj).c_str());
+    stream->print(F("\",name=\""));
+    stream->print(relabel_name_(obj).c_str());
+    stream->print(F("\"} 0\n"));
+    // Data itself
+    stream->print(F("esphome_text_sensor_value{id=\""));
+    stream->print(relabel_id_(obj).c_str());
+    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_text_sensor_failed{id=\""));
+    stream->print(relabel_id_(obj).c_str());
+    stream->print(F("\",name=\""));
+    stream->print(relabel_name_(obj).c_str());
+    stream->print(F("\"} 1\n"));
+  }
+}
+#endif
+
 }  // namespace prometheus
 }  // namespace esphome
 #endif
diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h
index f5e49a1419..512e1bee4f 100644
--- a/esphome/components/prometheus/prometheus_handler.h
+++ b/esphome/components/prometheus/prometheus_handler.h
@@ -110,6 +110,13 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
   void lock_row_(AsyncResponseStream *stream, lock::Lock *obj);
 #endif
 
+#ifdef USE_TEXT_SENSOR
+  /// Return the type for prometheus
+  void text_sensor_type_(AsyncResponseStream *stream);
+  /// Return the lock Values state as prometheus data point
+  void text_sensor_row_(AsyncResponseStream *stream, text_sensor::TextSensor *obj);
+#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 c8ce17da88..7aa509f8b2 100644
--- a/tests/components/prometheus/common.yaml
+++ b/tests/components/prometheus/common.yaml
@@ -13,9 +13,23 @@ sensor:
       }
     update_interval: 60s
 
+text_sensor:
+  - platform: template
+    id: template_text_sensor1
+    lambda: |-
+      if (millis() > 10000) {
+        return {"Hello World"};
+      } else {
+        return {"Goodbye (cruel) World"};
+      }
+    update_interval: 60s
+
 prometheus:
   include_internal: true
   relabel:
     template_sensor1:
       id: hellow_world
       name: Hello World
+    template_text_sensor1:
+      id: hello_text
+      name: Text Substitution

From 0dab280440de853d57d750463b3be4e065fd480d Mon Sep 17 00:00:00 2001
From: Sean Brogan <spbrogan@live.com>
Date: Mon, 28 Oct 2024 20:49:06 -0700
Subject: [PATCH 055/282] Mopeka Pro Check improvement to allow user to
 configure the sensor reporting for lower quality readings (#7475)

---
 .../mopeka_pro_check/mopeka_pro_check.cpp     | 63 ++++++++++++-------
 .../mopeka_pro_check/mopeka_pro_check.h       | 11 +++-
 esphome/components/mopeka_pro_check/sensor.py | 41 ++++++++++++
 tests/components/mopeka_pro_check/common.yaml | 17 +++++
 4 files changed, 108 insertions(+), 24 deletions(-)

diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp
index f79e40bb4e..9527f09f59 100644
--- a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp
+++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp
@@ -17,6 +17,8 @@ void MopekaProCheck::dump_config() {
   LOG_SENSOR("  ", "Temperature", this->temperature_);
   LOG_SENSOR("  ", "Battery Level", this->battery_level_);
   LOG_SENSOR("  ", "Reading Distance", this->distance_);
+  LOG_SENSOR("  ", "Read Quality", this->read_quality_);
+  LOG_SENSOR("  ", "Ignored Reads", this->ignored_reads_);
 }
 
 /**
@@ -66,34 +68,49 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
     this->battery_level_->publish_state(level);
   }
 
+  // Get the quality value
+  SensorReadQuality quality_value = this->parse_read_quality_(manu_data.data);
+  if (this->read_quality_ != nullptr) {
+    this->read_quality_->publish_state(static_cast<int>(quality_value));
+  }
+
+  // Determine if we have a good enough quality of read to report level and distance
+  // sensors.  This sensor is reported regardless of distance or level sensors being enabled
+  if (quality_value < this->min_signal_quality_) {
+    ESP_LOGW(TAG, "Read Quality too low to report distance or level");
+    this->ignored_read_count_++;
+  } else {
+    // reset to zero since read quality was sufficient
+    this->ignored_read_count_ = 0;
+  }
+  // Report number of contiguous ignored reads if sensor defined
+  if (this->ignored_reads_ != nullptr) {
+    this->ignored_reads_->publish_state(this->ignored_read_count_);
+  }
+
   // Get distance and level if either are sensors
   if ((this->distance_ != nullptr) || (this->level_ != nullptr)) {
     uint32_t distance_value = this->parse_distance_(manu_data.data);
-    SensorReadQuality quality_value = this->parse_read_quality_(manu_data.data);
     ESP_LOGD(TAG, "Distance Sensor: Quality (0x%X) Distance (%" PRId32 "mm)", quality_value, distance_value);
-    if (quality_value < QUALITY_HIGH) {
-      ESP_LOGW(TAG, "Poor read quality.");
-    }
-    if (quality_value < QUALITY_MED) {
-      // if really bad reading set to 0
-      ESP_LOGW(TAG, "Setting distance to 0");
-      distance_value = 0;
-    }
 
-    // update distance sensor
-    if (this->distance_ != nullptr) {
-      this->distance_->publish_state(distance_value);
-    }
-
-    // update level sensor
-    if (this->level_ != nullptr) {
-      uint8_t tank_level = 0;
-      if (distance_value >= this->full_mm_) {
-        tank_level = 100;  // cap at 100%
-      } else if (distance_value > this->empty_mm_) {
-        tank_level = ((100.0f / (this->full_mm_ - this->empty_mm_)) * (distance_value - this->empty_mm_));
+    // only update distance and level sensors if read quality was sufficient.  This can be determined by
+    // if the ignored_read_count is zero.
+    if (this->ignored_read_count_ == 0) {
+      // update distance sensor
+      if (this->distance_ != nullptr) {
+        this->distance_->publish_state(distance_value);
+      }
+
+      // update level sensor
+      if (this->level_ != nullptr) {
+        uint8_t tank_level = 0;
+        if (distance_value >= this->full_mm_) {
+          tank_level = 100;  // cap at 100%
+        } else if (distance_value > this->empty_mm_) {
+          tank_level = ((100.0f / (this->full_mm_ - this->empty_mm_)) * (distance_value - this->empty_mm_));
+        }
+        this->level_->publish_state(tank_level);
       }
-      this->level_->publish_state(tank_level);
     }
   }
 
@@ -131,6 +148,8 @@ uint32_t MopekaProCheck::parse_distance_(const std::vector<uint8_t> &message) {
 uint8_t MopekaProCheck::parse_temperature_(const std::vector<uint8_t> &message) { return (message[2] & 0x7F) - 40; }
 
 SensorReadQuality MopekaProCheck::parse_read_quality_(const std::vector<uint8_t> &message) {
+  // Since a 8 bit value is being shifted and truncated to 2 bits all possible values are defined as enumeration
+  //  value and the static cast is safe.
   return static_cast<SensorReadQuality>(message[4] >> 6);
 }
 
diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.h b/esphome/components/mopeka_pro_check/mopeka_pro_check.h
index 8b4d47e4c6..c58406ac18 100644
--- a/esphome/components/mopeka_pro_check/mopeka_pro_check.h
+++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.h
@@ -24,9 +24,9 @@ enum SensorType {
 };
 
 // Sensor read quality.  If sensor is poorly placed or tank level
-// gets too low the read quality will show and the distanace
+// gets too low the read quality will show and the distance
 // measurement may be inaccurate.
-enum SensorReadQuality { QUALITY_HIGH = 0x3, QUALITY_MED = 0x2, QUALITY_LOW = 0x1, QUALITY_NONE = 0x0 };
+enum SensorReadQuality { QUALITY_HIGH = 0x3, QUALITY_MED = 0x2, QUALITY_LOW = 0x1, QUALITY_ZERO = 0x0 };
 
 class MopekaProCheck : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
  public:
@@ -35,11 +35,14 @@ class MopekaProCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
   void dump_config() override;
   float get_setup_priority() const override { return setup_priority::DATA; }
+  void set_min_signal_quality(SensorReadQuality min) { this->min_signal_quality_ = min; };
 
   void set_level(sensor::Sensor *level) { level_ = level; };
   void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; };
   void set_battery_level(sensor::Sensor *bat) { battery_level_ = bat; };
   void set_distance(sensor::Sensor *distance) { distance_ = distance; };
+  void set_signal_quality(sensor::Sensor *rq) { read_quality_ = rq; };
+  void set_ignored_reads(sensor::Sensor *ir) { ignored_reads_ = ir; };
   void set_tank_full(float full) { full_mm_ = full; };
   void set_tank_empty(float empty) { empty_mm_ = empty; };
 
@@ -49,9 +52,13 @@ class MopekaProCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi
   sensor::Sensor *temperature_{nullptr};
   sensor::Sensor *distance_{nullptr};
   sensor::Sensor *battery_level_{nullptr};
+  sensor::Sensor *read_quality_{nullptr};
+  sensor::Sensor *ignored_reads_{nullptr};
 
   uint32_t full_mm_;
   uint32_t empty_mm_;
+  uint32_t ignored_read_count_ = 0;
+  SensorReadQuality min_signal_quality_ = QUALITY_MED;
 
   uint8_t parse_battery_level_(const std::vector<uint8_t> &message);
   uint32_t parse_distance_(const std::vector<uint8_t> &message);
diff --git a/esphome/components/mopeka_pro_check/sensor.py b/esphome/components/mopeka_pro_check/sensor.py
index 0ba33e94de..95ade53013 100644
--- a/esphome/components/mopeka_pro_check/sensor.py
+++ b/esphome/components/mopeka_pro_check/sensor.py
@@ -5,9 +5,12 @@ from esphome.const import (
     CONF_DISTANCE,
     CONF_MAC_ADDRESS,
     CONF_ID,
+    ICON_COUNTER,
     ICON_THERMOMETER,
     ICON_RULER,
+    ICON_SIGNAL,
     UNIT_PERCENT,
+    UNIT_EMPTY,
     CONF_LEVEL,
     CONF_TEMPERATURE,
     DEVICE_CLASS_TEMPERATURE,
@@ -16,11 +19,15 @@ from esphome.const import (
     STATE_CLASS_MEASUREMENT,
     CONF_BATTERY_LEVEL,
     DEVICE_CLASS_BATTERY,
+    ENTITY_CATEGORY_DIAGNOSTIC,
 )
 
 CONF_TANK_TYPE = "tank_type"
 CONF_CUSTOM_DISTANCE_FULL = "custom_distance_full"
 CONF_CUSTOM_DISTANCE_EMPTY = "custom_distance_empty"
+CONF_SIGNAL_QUALITY = "signal_quality"
+CONF_MINIMUM_SIGNAL_QUALITY = "minimum_signal_quality"
+CONF_IGNORED_READS = "ignored_reads"
 
 ICON_PROPANE_TANK = "mdi:propane-tank"
 
@@ -56,6 +63,14 @@ MopekaProCheck = mopeka_pro_check_ns.class_(
     "MopekaProCheck", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
 )
 
+SensorReadQuality = mopeka_pro_check_ns.enum("SensorReadQuality")
+SIGNAL_QUALITIES = {
+    "ZERO": SensorReadQuality.QUALITY_ZERO,
+    "LOW": SensorReadQuality.QUALITY_LOW,
+    "MEDIUM": SensorReadQuality.QUALITY_MED,
+    "HIGH": SensorReadQuality.QUALITY_HIGH,
+}
+
 CONFIG_SCHEMA = (
     cv.Schema(
         {
@@ -89,6 +104,21 @@ CONFIG_SCHEMA = (
                 device_class=DEVICE_CLASS_BATTERY,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
+            cv.Optional(CONF_SIGNAL_QUALITY): sensor.sensor_schema(
+                unit_of_measurement=UNIT_EMPTY,
+                icon=ICON_SIGNAL,
+                accuracy_decimals=0,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_IGNORED_READS): sensor.sensor_schema(
+                unit_of_measurement=UNIT_EMPTY,
+                icon=ICON_COUNTER,
+                accuracy_decimals=0,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+            ),
+            cv.Optional(CONF_MINIMUM_SIGNAL_QUALITY, default="MEDIUM"): cv.enum(
+                SIGNAL_QUALITIES, upper=True
+            ),
         }
     )
     .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
@@ -119,6 +149,11 @@ async def to_code(config):
         cg.add(var.set_tank_empty(CONF_SUPPORTED_TANKS_MAP[t][0]))
         cg.add(var.set_tank_full(CONF_SUPPORTED_TANKS_MAP[t][1]))
 
+    if (
+        minimum_signal_quality := config.get(CONF_MINIMUM_SIGNAL_QUALITY, None)
+    ) is not None:
+        cg.add(var.set_min_signal_quality(minimum_signal_quality))
+
     if CONF_TEMPERATURE in config:
         sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
@@ -131,3 +166,9 @@ async def to_code(config):
     if CONF_BATTERY_LEVEL in config:
         sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
         cg.add(var.set_battery_level(sens))
+    if CONF_SIGNAL_QUALITY in config:
+        sens = await sensor.new_sensor(config[CONF_SIGNAL_QUALITY])
+        cg.add(var.set_signal_quality(sens))
+    if CONF_IGNORED_READS in config:
+        sens = await sensor.new_sensor(config[CONF_IGNORED_READS])
+        cg.add(var.set_ignored_reads(sens))
diff --git a/tests/components/mopeka_pro_check/common.yaml b/tests/components/mopeka_pro_check/common.yaml
index 147cbcb9de..3533ecf631 100644
--- a/tests/components/mopeka_pro_check/common.yaml
+++ b/tests/components/mopeka_pro_check/common.yaml
@@ -14,3 +14,20 @@ sensor:
       name: Propane test distance
     battery_level:
       name: Propane test battery level
+
+  - platform: mopeka_pro_check
+    mac_address: AA:BB:CC:DD:EE:FF
+    tank_type: 20LB_V
+    temperature:
+      name: "Propane test2 temp"
+    level:
+      name: "Propane test2 level"
+    distance:
+      name: "Propane test2 distance"
+    battery_level:
+      name: "Propane test2 battery level"
+    signal_quality:
+      name: "propane test2 read quality"
+    ignored_reads:
+      name: "propane test2 ignored reads"
+    minimum_signal_quality: "LOW"

From aa0e155e22c422c789feeb0b1be9495f703fc8fa Mon Sep 17 00:00:00 2001
From: Bonne Eggleston <bonne@exciton.com.au>
Date: Mon, 28 Oct 2024 20:52:39 -0700
Subject: [PATCH 056/282] Fixes modbus timing error (#7674)

---
 esphome/components/modbus/modbus.cpp | 36 ++++++++++++++++++----------
 1 file changed, 24 insertions(+), 12 deletions(-)

diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp
index f8dd4c18b9..8544b50261 100644
--- a/esphome/components/modbus/modbus.cpp
+++ b/esphome/components/modbus/modbus.cpp
@@ -15,23 +15,33 @@ void Modbus::setup() {
 void Modbus::loop() {
   const uint32_t now = millis();
 
-  if (now - this->last_modbus_byte_ > 50) {
-    this->rx_buffer_.clear();
-    this->last_modbus_byte_ = now;
-  }
-  // stop blocking new send commands after send_wait_time_ ms regardless if a response has been received since then
-  if (now - this->last_send_ > send_wait_time_) {
-    waiting_for_response = 0;
-  }
-
   while (this->available()) {
     uint8_t byte;
     this->read_byte(&byte);
     if (this->parse_modbus_byte_(byte)) {
       this->last_modbus_byte_ = now;
     } else {
+      size_t at = this->rx_buffer_.size();
+      if (at > 0) {
+        ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse failed", at);
+        this->rx_buffer_.clear();
+      }
+    }
+  }
+
+  if (now - this->last_modbus_byte_ > 50) {
+    size_t at = this->rx_buffer_.size();
+    if (at > 0) {
+      ESP_LOGV(TAG, "Clearing buffer of %d bytes - timeout", at);
       this->rx_buffer_.clear();
     }
+
+    // stop blocking new send commands after sent_wait_time_ ms after response received
+    if (now - this->last_send_ > send_wait_time_) {
+      if (waiting_for_response > 0)
+        ESP_LOGV(TAG, "Stop waiting for response from %d", waiting_for_response);
+      waiting_for_response = 0;
+    }
   }
 }
 
@@ -39,7 +49,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
   size_t at = this->rx_buffer_.size();
   this->rx_buffer_.push_back(byte);
   const uint8_t *raw = &this->rx_buffer_[0];
-  ESP_LOGV(TAG, "Modbus received Byte  %d (0X%x)", byte, byte);
+  ESP_LOGVV(TAG, "Modbus received Byte  %d (0X%x)", byte, byte);
   // Byte 0: modbus address (match all)
   if (at == 0)
     return true;
@@ -144,8 +154,10 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
     ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X! ", address);
   }
 
-  // return false to reset buffer
-  return false;
+  // reset buffer
+  ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse succeeded", at);
+  this->rx_buffer_.clear();
+  return true;
 }
 
 void Modbus::dump_config() {

From abbd7faa641220fc3cbc2e77f76f3bfcf41546ab Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= <contact@rodrigomartin.dev>
Date: Tue, 29 Oct 2024 04:56:50 +0100
Subject: [PATCH 057/282] fix(WiFi): Fix strncpy missing NULL terminator
 [-Werror=stringop-truncation] (#7668)

---
 esphome/components/wifi/wifi_component.cpp               | 4 ++--
 esphome/components/wifi/wifi_component_esp32_arduino.cpp | 8 ++++----
 esphome/components/wifi/wifi_component_esp8266.cpp       | 8 ++++----
 esphome/components/wifi/wifi_component_esp_idf.cpp       | 4 ++--
 4 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp
index 583a27466a..8788711d5a 100644
--- a/esphome/components/wifi/wifi_component.cpp
+++ b/esphome/components/wifi/wifi_component.cpp
@@ -297,8 +297,8 @@ void WiFiComponent::set_sta(const WiFiAP &ap) {
 void WiFiComponent::clear_sta() { this->sta_.clear(); }
 void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) {
   SavedWifiSettings save{};
-  strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid));
-  strncpy(save.password, password.c_str(), sizeof(save.password));
+  snprintf(save.ssid, sizeof(save.ssid), "%s", ssid.c_str());
+  snprintf(save.password, sizeof(save.password), "%s", password.c_str());
   this->pref_.save(&save);
   // ensure it's written immediately
   global_preferences->sync();
diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp
index ef4308b28c..88648093c6 100644
--- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp
+++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp
@@ -137,8 +137,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
   // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t
   wifi_config_t conf;
   memset(&conf, 0, sizeof(conf));
-  strncpy(reinterpret_cast<char *>(conf.sta.ssid), ap.get_ssid().c_str(), sizeof(conf.sta.ssid));
-  strncpy(reinterpret_cast<char *>(conf.sta.password), ap.get_password().c_str(), sizeof(conf.sta.password));
+  snprintf(reinterpret_cast<char *>(conf.sta.ssid), sizeof(conf.sta.ssid), "%s", ap.get_ssid().c_str());
+  snprintf(reinterpret_cast<char *>(conf.sta.password), sizeof(conf.sta.password), "%s", ap.get_password().c_str());
 
   // The weakest authmode to accept in the fast scan mode
   if (ap.get_password().empty()) {
@@ -746,7 +746,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
 
   wifi_config_t conf;
   memset(&conf, 0, sizeof(conf));
-  strncpy(reinterpret_cast<char *>(conf.ap.ssid), ap.get_ssid().c_str(), sizeof(conf.ap.ssid));
+  snprintf(reinterpret_cast<char *>(conf.ap.ssid), sizeof(conf.ap.ssid), "%s", ap.get_ssid().c_str());
   conf.ap.channel = ap.get_channel().value_or(1);
   conf.ap.ssid_hidden = ap.get_ssid().size();
   conf.ap.max_connection = 5;
@@ -757,7 +757,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
     *conf.ap.password = 0;
   } else {
     conf.ap.authmode = WIFI_AUTH_WPA2_PSK;
-    strncpy(reinterpret_cast<char *>(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.password));
+    snprintf(reinterpret_cast<char *>(conf.ap.password), sizeof(conf.ap.password), "%s", ap.get_password().c_str());
   }
 
   // pairwise cipher of SoftAP, group cipher will be derived using this.
diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp
index 92f80c1e52..4568895950 100644
--- a/esphome/components/wifi/wifi_component_esp8266.cpp
+++ b/esphome/components/wifi/wifi_component_esp8266.cpp
@@ -236,8 +236,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
 
   struct station_config conf {};
   memset(&conf, 0, sizeof(conf));
-  strncpy(reinterpret_cast<char *>(conf.ssid), ap.get_ssid().c_str(), sizeof(conf.ssid));
-  strncpy(reinterpret_cast<char *>(conf.password), ap.get_password().c_str(), sizeof(conf.password));
+  snprintf(reinterpret_cast<char *>(conf.ssid), sizeof(conf.ssid), "%s", ap.get_ssid().c_str());
+  snprintf(reinterpret_cast<char *>(conf.password), sizeof(conf.password), "%s", ap.get_password().c_str());
 
   if (ap.get_bssid().has_value()) {
     conf.bssid_set = 1;
@@ -775,7 +775,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
     return false;
 
   struct softap_config conf {};
-  strncpy(reinterpret_cast<char *>(conf.ssid), ap.get_ssid().c_str(), sizeof(conf.ssid));
+  snprintf(reinterpret_cast<char *>(conf.ssid), sizeof(conf.ssid), "%s", ap.get_ssid().c_str());
   conf.ssid_len = static_cast<uint8>(ap.get_ssid().size());
   conf.channel = ap.get_channel().value_or(1);
   conf.ssid_hidden = ap.get_hidden();
@@ -787,7 +787,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
     *conf.password = 0;
   } else {
     conf.authmode = AUTH_WPA2_PSK;
-    strncpy(reinterpret_cast<char *>(conf.password), ap.get_password().c_str(), sizeof(conf.password));
+    snprintf(reinterpret_cast<char *>(conf.password), sizeof(conf.password), "%s", ap.get_password().c_str());
   }
 
   ETS_UART_INTR_DISABLE();
diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp
index 0f2e181e31..13870136d4 100644
--- a/esphome/components/wifi/wifi_component_esp_idf.cpp
+++ b/esphome/components/wifi/wifi_component_esp_idf.cpp
@@ -289,8 +289,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
   // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t
   wifi_config_t conf;
   memset(&conf, 0, sizeof(conf));
-  strncpy(reinterpret_cast<char *>(conf.sta.ssid), ap.get_ssid().c_str(), sizeof(conf.sta.ssid));
-  strncpy(reinterpret_cast<char *>(conf.sta.password), ap.get_password().c_str(), sizeof(conf.sta.password));
+  snprintf(reinterpret_cast<char *>(conf.sta.ssid), sizeof(conf.sta.ssid), "%s", ap.get_ssid().c_str());
+  snprintf(reinterpret_cast<char *>(conf.sta.password), sizeof(conf.sta.password), "%s", ap.get_password().c_str());
 
   // The weakest authmode to accept in the fast scan mode
   if (ap.get_password().empty()) {

From 71e1e3b5f8575a3c075b90dbf573c95927a9bb0a Mon Sep 17 00:00:00 2001
From: tomaszduda23 <tomaszduda23@gmail.com>
Date: Tue, 29 Oct 2024 04:58:36 +0100
Subject: [PATCH 058/282] let make new platform implementation in external
 components (#7615)

Co-authored-by: Tomasz Duda <tomaszduda23@gmai.com>
---
 esphome/core/helpers.cpp | 57 ++++++++++++++++++++++++----------------
 esphome/core/helpers.h   |  4 +++
 2 files changed, 39 insertions(+), 22 deletions(-)

diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp
index dca35819ff..8f94f624f1 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -10,6 +10,7 @@
 #include <cstdarg>
 #include <cstdio>
 #include <cstring>
+#include <strings.h>
 
 #ifdef USE_HOST
 #ifndef _WIN32
@@ -188,37 +189,39 @@ uint32_t fnv1_hash(const std::string &str) {
   return hash;
 }
 
-uint32_t random_uint32() {
 #ifdef USE_ESP32
-  return esp_random();
+uint32_t random_uint32() { return esp_random(); }
 #elif defined(USE_ESP8266)
-  return os_random();
+uint32_t random_uint32() { return os_random(); }
 #elif defined(USE_RP2040)
+uint32_t random_uint32() {
   uint32_t result = 0;
   for (uint8_t i = 0; i < 32; i++) {
     result <<= 1;
     result |= rosc_hw->randombit;
   }
   return result;
+}
 #elif defined(USE_LIBRETINY)
-  return rand();
+uint32_t random_uint32() { return rand(); }
 #elif defined(USE_HOST)
+uint32_t random_uint32() {
   std::random_device dev;
   std::mt19937 rng(dev());
   std::uniform_int_distribution<uint32_t> dist(0, std::numeric_limits<uint32_t>::max());
   return dist(rng);
-#else
-#error "No random source available for this configuration."
-#endif
 }
+#endif
 float random_float() { return static_cast<float>(random_uint32()) / static_cast<float>(UINT32_MAX); }
-bool random_bytes(uint8_t *data, size_t len) {
 #ifdef USE_ESP32
+bool random_bytes(uint8_t *data, size_t len) {
   esp_fill_random(data, len);
   return true;
+}
 #elif defined(USE_ESP8266)
-  return os_get_random(data, len) == 0;
+bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; }
 #elif defined(USE_RP2040)
+bool random_bytes(uint8_t *data, size_t len) {
   while (len-- != 0) {
     uint8_t result = 0;
     for (uint8_t i = 0; i < 8; i++) {
@@ -228,10 +231,14 @@ bool random_bytes(uint8_t *data, size_t len) {
     *data++ = result;
   }
   return true;
+}
 #elif defined(USE_LIBRETINY)
+bool random_bytes(uint8_t *data, size_t len) {
   lt_rand_bytes(data, len);
   return true;
+}
 #elif defined(USE_HOST)
+bool random_bytes(uint8_t *data, size_t len) {
   FILE *fp = fopen("/dev/urandom", "r");
   if (fp == nullptr) {
     ESP_LOGW(TAG, "Could not open /dev/urandom, errno=%d", errno);
@@ -244,10 +251,8 @@ bool random_bytes(uint8_t *data, size_t len) {
   }
   fclose(fp);
   return true;
-#else
-#error "No random source available for this configuration."
-#endif
 }
+#endif
 
 // Strings
 
@@ -619,11 +624,13 @@ void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green
 #if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_HOST)
 // ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS.
 Mutex::Mutex() {}
+Mutex::~Mutex() {}
 void Mutex::lock() {}
 bool Mutex::try_lock() { return true; }
 void Mutex::unlock() {}
 #elif defined(USE_ESP32) || defined(USE_LIBRETINY)
 Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); }
+Mutex::~Mutex() {}
 void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
 bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
 void Mutex::unlock() { xSemaphoreGive(this->handle_); }
@@ -657,11 +664,13 @@ void HighFrequencyLoopRequester::stop() {
 }
 bool HighFrequencyLoopRequester::is_high_frequency() { return num_requests > 0; }
 
-void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
 #if defined(USE_HOST)
+void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
   static const uint8_t esphome_host_mac_address[6] = USE_ESPHOME_HOST_MAC_ADDRESS;
   memcpy(mac, esphome_host_mac_address, sizeof(esphome_host_mac_address));
+}
 #elif defined(USE_ESP32)
+void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
 #if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
   // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default
   // returns the 802.15.4 EUI-64 address, so we read directly from eFuse instead.
@@ -677,16 +686,20 @@ void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parame
     esp_efuse_mac_get_default(mac);
   }
 #endif
-#elif defined(USE_ESP8266)
-  wifi_get_macaddr(STATION_IF, mac);
-#elif defined(USE_RP2040) && defined(USE_WIFI)
-  WiFi.macAddress(mac);
-#elif defined(USE_LIBRETINY)
-  WiFi.macAddress(mac);
-#else
-// this should be an error, but that messes with CI checks. #error No mac address method defined
-#endif
 }
+#elif defined(USE_ESP8266)
+void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
+  wifi_get_macaddr(STATION_IF, mac);
+}
+#elif defined(USE_RP2040) && defined(USE_WIFI)
+void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
+  WiFi.macAddress(mac);
+}
+#elif defined(USE_LIBRETINY)
+void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
+  WiFi.macAddress(mac);
+}
+#endif
 
 std::string get_mac_address() {
   uint8_t mac[6];
diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h
index 7f6fe9bfdc..43001bafdd 100644
--- a/esphome/core/helpers.h
+++ b/esphome/core/helpers.h
@@ -7,6 +7,7 @@
 #include <string>
 #include <type_traits>
 #include <vector>
+#include <limits>
 
 #include "esphome/core/optional.h"
 
@@ -545,6 +546,7 @@ class Mutex {
  public:
   Mutex();
   Mutex(const Mutex &) = delete;
+  ~Mutex();
   void lock();
   bool try_lock();
   void unlock();
@@ -554,6 +556,8 @@ class Mutex {
  private:
 #if defined(USE_ESP32) || defined(USE_LIBRETINY)
   SemaphoreHandle_t handle_;
+#else
+  void *handle_;  // d-pointer to store private data on new platforms
 #endif
 };
 

From 38dd566e0cbb51b4eb6f14a8fcb03b36b7b962dc Mon Sep 17 00:00:00 2001
From: Samuel Sieb <samuel-github@sieb.net>
Date: Mon, 28 Oct 2024 21:12:54 -0700
Subject: [PATCH 059/282] remove use of delay (#7680)

Co-authored-by: Samuel Sieb <samuel@sieb.net>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/sgp4x/sgp4x.cpp | 114 ++++++++++-------------------
 esphome/components/sgp4x/sgp4x.h   |   8 +-
 2 files changed, 45 insertions(+), 77 deletions(-)

diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp
index 7e474b9371..bf91c90832 100644
--- a/esphome/components/sgp4x/sgp4x.cpp
+++ b/esphome/components/sgp4x/sgp4x.cpp
@@ -111,7 +111,7 @@ void SGP4xComponent::setup() {
   number of records reported from being overwhelming.
   */
   ESP_LOGD(TAG, "Component requires sampling of 1Hz, setting up background sampler");
-  this->set_interval(1000, [this]() { this->update_gas_indices(); });
+  this->set_interval(1000, [this]() { this->take_sample(); });
 }
 
 void SGP4xComponent::self_test_() {
@@ -146,31 +146,15 @@ void SGP4xComponent::self_test_() {
   });
 }
 
-/**
- * @brief Combined the measured gasses, temperature, and humidity
- * to calculate the VOC Index
- *
- * @param temperature The measured temperature in degrees C
- * @param humidity The measured relative humidity in % rH
- * @return int32_t The VOC Index
- */
-bool SGP4xComponent::measure_gas_indices_(int32_t &voc, int32_t &nox) {
-  uint16_t voc_sraw;
-  uint16_t nox_sraw;
-  if (!measure_raw_(voc_sraw, nox_sraw))
-    return false;
-
-  this->status_clear_warning();
-
-  voc = voc_algorithm_.process(voc_sraw);
-  if (nox_sensor_) {
-    nox = nox_algorithm_.process(nox_sraw);
-  }
-  ESP_LOGV(TAG, "VOC = %" PRId32 ", NOx = %" PRId32, voc, nox);
+void SGP4xComponent::update_gas_indices_() {
+  this->voc_index_ = this->voc_algorithm_.process(this->voc_sraw_);
+  if (this->nox_sensor_ != nullptr)
+    this->nox_index_ = this->nox_algorithm_.process(this->nox_sraw_);
+  ESP_LOGV(TAG, "VOC = %" PRId32 ", NOx = %" PRId32, this->voc_index_, this->nox_index_);
   // Store baselines after defined interval or if the difference between current and stored baseline becomes too
   // much
   if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
-    voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_);
+    this->voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_);
     if (std::abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF ||
         std::abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF) {
       this->seconds_since_last_store_ = 0;
@@ -179,29 +163,27 @@ bool SGP4xComponent::measure_gas_indices_(int32_t &voc, int32_t &nox) {
 
       if (this->pref_.save(&this->voc_baselines_storage_)) {
         ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 " ,state1: 0x%04" PRIX32,
-                 this->voc_baselines_storage_.state0, voc_baselines_storage_.state1);
+                 this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
       } else {
         ESP_LOGW(TAG, "Could not store VOC baselines");
       }
     }
   }
 
-  return true;
+  if (this->samples_read_ < this->samples_to_stabilize_) {
+    this->samples_read_++;
+    ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %" PRIu32, this->samples_read_,
+             this->samples_to_stabilize_, this->voc_index_);
+  }
 }
-/**
- * @brief Return the raw gas measurement
- *
- * @param temperature The measured temperature in degrees C
- * @param humidity The measured relative humidity in % rH
- * @return uint16_t The current raw gas measurement
- */
-bool SGP4xComponent::measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw) {
+
+void SGP4xComponent::measure_raw_() {
   float humidity = NAN;
   static uint32_t nox_conditioning_start = millis();
 
   if (!this->self_test_complete_) {
     ESP_LOGD(TAG, "Self-test not yet complete");
-    return false;
+    return;
   }
   if (this->humidity_sensor_ != nullptr) {
     humidity = this->humidity_sensor_->state;
@@ -243,61 +225,45 @@ bool SGP4xComponent::measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw) {
   data[1] = tempticks;
 
   if (!this->write_command(command, data, 2)) {
-    this->status_set_warning();
     ESP_LOGD(TAG, "write error (%d)", this->last_error_);
-    return false;
+    this->status_set_warning("measurement request failed");
+    return;
   }
-  delay(measure_time_);
-  uint16_t raw_data[2];
-  raw_data[1] = 0;
-  if (!this->read_data(raw_data, response_words)) {
-    this->status_set_warning();
-    ESP_LOGD(TAG, "read error (%d)", this->last_error_);
-    return false;
-  }
-  voc_raw = raw_data[0];
-  nox_raw = raw_data[1];  // either 0 or the measured NOx ticks
-  return true;
+
+  this->set_timeout(this->measure_time_, [this, response_words]() {
+    uint16_t raw_data[2];
+    raw_data[1] = 0;
+    if (!this->read_data(raw_data, response_words)) {
+      ESP_LOGD(TAG, "read error (%d)", this->last_error_);
+      this->status_set_warning("measurement read failed");
+      this->voc_index_ = this->nox_index_ = UINT16_MAX;
+      return;
+    }
+    this->voc_sraw_ = raw_data[0];
+    this->nox_sraw_ = raw_data[1];  // either 0 or the measured NOx ticks
+    this->status_clear_warning();
+    this->update_gas_indices_();
+  });
 }
 
-void SGP4xComponent::update_gas_indices() {
+void SGP4xComponent::take_sample() {
   if (!this->self_test_complete_)
     return;
-
   this->seconds_since_last_store_ += 1;
-  if (!this->measure_gas_indices_(this->voc_index_, this->nox_index_)) {
-    // Set values to UINT16_MAX to indicate failure
-    this->voc_index_ = this->nox_index_ = UINT16_MAX;
-    ESP_LOGE(TAG, "measure gas indices failed");
-    return;
-  }
-  if (this->samples_read_ < this->samples_to_stabilize_) {
-    this->samples_read_++;
-    ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %" PRIu32, this->samples_read_,
-             this->samples_to_stabilize_, this->voc_index_);
-    return;
-  }
+  this->measure_raw_();
 }
 
 void SGP4xComponent::update() {
   if (this->samples_read_ < this->samples_to_stabilize_) {
     return;
   }
-  if (this->voc_sensor_) {
-    if (this->voc_index_ != UINT16_MAX) {
-      this->status_clear_warning();
+  if (this->voc_sensor_ != nullptr) {
+    if (this->voc_index_ != UINT16_MAX)
       this->voc_sensor_->publish_state(this->voc_index_);
-    } else {
-      this->status_set_warning();
-    }
   }
-  if (this->nox_sensor_) {
-    if (this->nox_index_ != UINT16_MAX) {
-      this->status_clear_warning();
+  if (this->nox_sensor_ != nullptr) {
+    if (this->nox_index_ != UINT16_MAX)
       this->nox_sensor_->publish_state(this->nox_index_);
-    } else {
-      this->status_set_warning();
-    }
   }
 }
 
@@ -329,7 +295,7 @@ void SGP4xComponent::dump_config() {
   }
   LOG_UPDATE_INTERVAL(this);
 
-  if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) {
+  if (this->humidity_sensor_ != nullptr || this->temperature_sensor_ != nullptr) {
     ESP_LOGCONFIG(TAG, "  Compensation:");
     LOG_SENSOR("    ", "Temperature Source:", this->temperature_sensor_);
     LOG_SENSOR("    ", "Humidity Source:", this->humidity_sensor_);
diff --git a/esphome/components/sgp4x/sgp4x.h b/esphome/components/sgp4x/sgp4x.h
index aa5ae4b9d2..959ff12c27 100644
--- a/esphome/components/sgp4x/sgp4x.h
+++ b/esphome/components/sgp4x/sgp4x.h
@@ -73,7 +73,7 @@ class SGP4xComponent : public PollingComponent, public sensor::Sensor, public se
 
   void setup() override;
   void update() override;
-  void update_gas_indices();
+  void take_sample();
   void dump_config() override;
   float get_setup_priority() const override { return setup_priority::DATA; }
   void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; }
@@ -108,8 +108,10 @@ class SGP4xComponent : public PollingComponent, public sensor::Sensor, public se
   sensor::Sensor *temperature_sensor_{nullptr};
   int16_t sensirion_init_sensors_();
 
-  bool measure_gas_indices_(int32_t &voc, int32_t &nox);
-  bool measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw);
+  void update_gas_indices_();
+  void measure_raw_();
+  uint16_t voc_sraw_;
+  uint16_t nox_sraw_;
 
   SgpType sgp_type_{SGP40};
   uint64_t serial_number_;

From 0982ab58ac1ff8c027bae542908e204b5d053728 Mon Sep 17 00:00:00 2001
From: tomaszduda23 <tomaszduda23@gmail.com>
Date: Tue, 29 Oct 2024 19:53:36 +0100
Subject: [PATCH 060/282] fix build error (#7694)

Co-authored-by: Tomasz Duda <tomaszduda23@gmai.com>
---
 esphome/core/helpers.cpp | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp
index 8f94f624f1..dae60a4e1d 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -691,9 +691,11 @@ void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parame
 void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
   wifi_get_macaddr(STATION_IF, mac);
 }
-#elif defined(USE_RP2040) && defined(USE_WIFI)
+#elif defined(USE_RP2040)
 void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
+#ifdef USE_WIFI
   WiFi.macAddress(mac);
+#endif
 }
 #elif defined(USE_LIBRETINY)
 void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)

From bac6880a1e9df93a38cb6a7ba467c28a78209c2f Mon Sep 17 00:00:00 2001
From: Ilia Sotnikov <hostcc@users.noreply.github.com>
Date: Wed, 30 Oct 2024 02:32:55 +0300
Subject: [PATCH 061/282] fix: [climate] Allow substitutions in
 `visual.temperature_step.{target_temperature,current_temperature}` (#7679)

---
 esphome/components/climate/__init__.py | 29 +++++++++++++-------------
 1 file changed, 15 insertions(+), 14 deletions(-)

diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py
index b302e2ab4e..ec68940726 100644
--- a/esphome/components/climate/__init__.py
+++ b/esphome/components/climate/__init__.py
@@ -119,10 +119,21 @@ visual_temperature = cv.float_with_unit(
 )
 
 
-def single_visual_temperature(value):
-    if isinstance(value, dict):
-        return value
+VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema(
+    {
+        cv.Required(CONF_TARGET_TEMPERATURE): visual_temperature,
+        cv.Required(CONF_CURRENT_TEMPERATURE): visual_temperature,
+    }
+)
 
+
+def visual_temperature_step(value):
+
+    # Allow defining target/current temperature steps separately
+    if isinstance(value, dict):
+        return VISUAL_TEMPERATURE_STEP_SCHEMA(value)
+
+    # Otherwise, use the single value for both properties
     value = visual_temperature(value)
     return VISUAL_TEMPERATURE_STEP_SCHEMA(
         {
@@ -141,16 +152,6 @@ ControlTrigger = climate_ns.class_(
     "ControlTrigger", automation.Trigger.template(ClimateCall.operator("ref"))
 )
 
-VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Any(
-    single_visual_temperature,
-    cv.Schema(
-        {
-            cv.Required(CONF_TARGET_TEMPERATURE): visual_temperature,
-            cv.Required(CONF_CURRENT_TEMPERATURE): visual_temperature,
-        }
-    ),
-)
-
 CLIMATE_SCHEMA = (
     cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
     .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
@@ -162,7 +163,7 @@ CLIMATE_SCHEMA = (
                 {
                     cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature,
                     cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature,
-                    cv.Optional(CONF_TEMPERATURE_STEP): VISUAL_TEMPERATURE_STEP_SCHEMA,
+                    cv.Optional(CONF_TEMPERATURE_STEP): visual_temperature_step,
                     cv.Optional(CONF_MIN_HUMIDITY): cv.percentage_int,
                     cv.Optional(CONF_MAX_HUMIDITY): cv.percentage_int,
                 }

From aae2ee2ecb34222ad61b52064f10006e86fc0fa3 Mon Sep 17 00:00:00 2001
From: Jordan Zucker <jordan.zucker@gmail.com>
Date: Tue, 29 Oct 2024 18:03:10 -0700
Subject: [PATCH 062/282] Add in area and device to the prometheus labels
 (#7692)

---
 .../prometheus/prometheus_handler.cpp         | 151 ++++++++++++++++--
 .../prometheus/prometheus_handler.h           |  27 +++-
 tests/components/prometheus/common.yaml       |  54 +++++++
 3 files changed, 208 insertions(+), 24 deletions(-)

diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp
index 63b3fdf53f..5d1861202a 100644
--- a/esphome/components/prometheus/prometheus_handler.cpp
+++ b/esphome/components/prometheus/prometheus_handler.cpp
@@ -7,53 +7,56 @@ namespace prometheus {
 
 void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) {
   AsyncResponseStream *stream = req->beginResponseStream("text/plain; version=0.0.4; charset=utf-8");
+  std::string area = App.get_area();
+  std::string node = App.get_name();
+  std::string friendly_name = App.get_friendly_name();
 
 #ifdef USE_SENSOR
   this->sensor_type_(stream);
   for (auto *obj : App.get_sensors())
-    this->sensor_row_(stream, obj);
+    this->sensor_row_(stream, obj, area, node, friendly_name);
 #endif
 
 #ifdef USE_BINARY_SENSOR
   this->binary_sensor_type_(stream);
   for (auto *obj : App.get_binary_sensors())
-    this->binary_sensor_row_(stream, obj);
+    this->binary_sensor_row_(stream, obj, area, node, friendly_name);
 #endif
 
 #ifdef USE_FAN
   this->fan_type_(stream);
   for (auto *obj : App.get_fans())
-    this->fan_row_(stream, obj);
+    this->fan_row_(stream, obj, area, node, friendly_name);
 #endif
 
 #ifdef USE_LIGHT
   this->light_type_(stream);
   for (auto *obj : App.get_lights())
-    this->light_row_(stream, obj);
+    this->light_row_(stream, obj, area, node, friendly_name);
 #endif
 
 #ifdef USE_COVER
   this->cover_type_(stream);
   for (auto *obj : App.get_covers())
-    this->cover_row_(stream, obj);
+    this->cover_row_(stream, obj, area, node, friendly_name);
 #endif
 
 #ifdef USE_SWITCH
   this->switch_type_(stream);
   for (auto *obj : App.get_switches())
-    this->switch_row_(stream, obj);
+    this->switch_row_(stream, obj, area, node, friendly_name);
 #endif
 
 #ifdef USE_LOCK
   this->lock_type_(stream);
   for (auto *obj : App.get_locks())
-    this->lock_row_(stream, obj);
+    this->lock_row_(stream, obj, area, node, friendly_name);
 #endif
 
 #ifdef USE_TEXT_SENSOR
   this->text_sensor_type_(stream);
   for (auto *obj : App.get_text_sensors())
-    this->text_sensor_row_(stream, obj);
+    this->text_sensor_row_(stream, obj, area, node, friendly_name);
 #endif
 
   req->send(stream);
@@ -69,25 +72,53 @@ std::string PrometheusHandler::relabel_name_(EntityBase *obj) {
   return item == relabel_map_name_.end() ? obj->get_name() : item->second;
 }
 
+void PrometheusHandler::add_area_label_(AsyncResponseStream *stream, std::string &area) {
+  if (!area.empty()) {
+    stream->print(F("\",area=\""));
+    stream->print(area.c_str());
+  }
+}
+
+void PrometheusHandler::add_node_label_(AsyncResponseStream *stream, std::string &node) {
+  if (!node.empty()) {
+    stream->print(F("\",node=\""));
+    stream->print(node.c_str());
+  }
+}
+
+void PrometheusHandler::add_friendly_name_label_(AsyncResponseStream *stream, std::string &friendly_name) {
+  if (!friendly_name.empty()) {
+    stream->print(F("\",friendly_name=\""));
+    stream->print(friendly_name.c_str());
+  }
+}
+
 // Type-specific implementation
 #ifdef USE_SENSOR
 void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) {
   stream->print(F("#TYPE esphome_sensor_value gauge\n"));
   stream->print(F("#TYPE esphome_sensor_failed gauge\n"));
 }
-void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj) {
+void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *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_sensor_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_sensor_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("\",unit=\""));
@@ -99,6 +130,9 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor
     // Invalid state
     stream->print(F("esphome_sensor_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"));
@@ -112,19 +146,26 @@ void PrometheusHandler::binary_sensor_type_(AsyncResponseStream *stream) {
   stream->print(F("#TYPE esphome_binary_sensor_value gauge\n"));
   stream->print(F("#TYPE esphome_binary_sensor_failed gauge\n"));
 }
-void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj) {
+void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *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_binary_sensor_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_binary_sensor_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("\"} "));
@@ -134,6 +175,9 @@ void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_s
     // Invalid state
     stream->print(F("esphome_binary_sensor_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"));
@@ -148,17 +192,24 @@ void PrometheusHandler::fan_type_(AsyncResponseStream *stream) {
   stream->print(F("#TYPE esphome_fan_speed gauge\n"));
   stream->print(F("#TYPE esphome_fan_oscillation gauge\n"));
 }
-void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj) {
+void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj, std::string &area, std::string &node,
+                                 std::string &friendly_name) {
   if (obj->is_internal() && !this->include_internal_)
     return;
   stream->print(F("esphome_fan_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_fan_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("\"} "));
@@ -168,6 +219,9 @@ void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj) {
   if (obj->get_traits().supports_speed()) {
     stream->print(F("esphome_fan_speed{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("\"} "));
@@ -178,6 +232,9 @@ void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj) {
   if (obj->get_traits().supports_oscillation()) {
     stream->print(F("esphome_fan_oscillation{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("\"} "));
@@ -193,12 +250,16 @@ void PrometheusHandler::light_type_(AsyncResponseStream *stream) {
   stream->print(F("#TYPE esphome_light_color gauge\n"));
   stream->print(F("#TYPE esphome_light_effect_active gauge\n"));
 }
-void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj) {
+void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj, std::string &area,
+                                   std::string &node, std::string &friendly_name) {
   if (obj->is_internal() && !this->include_internal_)
     return;
   // State
   stream->print(F("esphome_light_state{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("\"} "));
@@ -211,6 +272,9 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat
   color.as_rgbw(&r, &g, &b, &w);
   stream->print(F("esphome_light_color{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("\",channel=\"brightness\"} "));
@@ -218,6 +282,9 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat
   stream->print(F("\n"));
   stream->print(F("esphome_light_color{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("\",channel=\"r\"} "));
@@ -225,6 +292,9 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat
   stream->print(F("\n"));
   stream->print(F("esphome_light_color{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("\",channel=\"g\"} "));
@@ -232,6 +302,9 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat
   stream->print(F("\n"));
   stream->print(F("esphome_light_color{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("\",channel=\"b\"} "));
@@ -239,6 +312,9 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat
   stream->print(F("\n"));
   stream->print(F("esphome_light_color{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("\",channel=\"w\"} "));
@@ -249,12 +325,18 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat
   if (effect == "None") {
     stream->print(F("esphome_light_effect_active{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("\",effect=\"None\"} 0\n"));
   } else {
     stream->print(F("esphome_light_effect_active{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("\",effect=\""));
@@ -269,19 +351,26 @@ void PrometheusHandler::cover_type_(AsyncResponseStream *stream) {
   stream->print(F("#TYPE esphome_cover_value gauge\n"));
   stream->print(F("#TYPE esphome_cover_failed gauge\n"));
 }
-void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj) {
+void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj, std::string &area, std::string &node,
+                                   std::string &friendly_name) {
   if (obj->is_internal() && !this->include_internal_)
     return;
   if (!std::isnan(obj->position)) {
     // We have a valid value, output this value
     stream->print(F("esphome_cover_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_cover_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("\"} "));
@@ -290,6 +379,9 @@ void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *ob
     if (obj->get_traits().get_supports_tilt()) {
       stream->print(F("esphome_cover_tilt{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("\"} "));
@@ -300,6 +392,9 @@ void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *ob
     // Invalid state
     stream->print(F("esphome_cover_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"));
@@ -312,17 +407,24 @@ void PrometheusHandler::switch_type_(AsyncResponseStream *stream) {
   stream->print(F("#TYPE esphome_switch_value gauge\n"));
   stream->print(F("#TYPE esphome_switch_failed gauge\n"));
 }
-void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj) {
+void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj, std::string &area,
+                                    std::string &node, std::string &friendly_name) {
   if (obj->is_internal() && !this->include_internal_)
     return;
   stream->print(F("esphome_switch_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_switch_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("\"} "));
@@ -336,17 +438,24 @@ void PrometheusHandler::lock_type_(AsyncResponseStream *stream) {
   stream->print(F("#TYPE esphome_lock_value gauge\n"));
   stream->print(F("#TYPE esphome_lock_failed gauge\n"));
 }
-void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj) {
+void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj, std::string &area, std::string &node,
+                                  std::string &friendly_name) {
   if (obj->is_internal() && !this->include_internal_)
     return;
   stream->print(F("esphome_lock_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_lock_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("\"} "));
@@ -361,19 +470,26 @@ void PrometheusHandler::text_sensor_type_(AsyncResponseStream *stream) {
   stream->print(F("#TYPE esphome_text_sensor_value gauge\n"));
   stream->print(F("#TYPE esphome_text_sensor_failed gauge\n"));
 }
-void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_sensor::TextSensor *obj) {
+void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_sensor::TextSensor *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_text_sensor_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_text_sensor_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=\""));
@@ -385,6 +501,9 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso
     // Invalid state
     stream->print(F("esphome_text_sensor_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"));
diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h
index 512e1bee4f..5d08aca63a 100644
--- a/esphome/components/prometheus/prometheus_handler.h
+++ b/esphome/components/prometheus/prometheus_handler.h
@@ -60,61 +60,72 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
  protected:
   std::string relabel_id_(EntityBase *obj);
   std::string relabel_name_(EntityBase *obj);
+  void add_area_label_(AsyncResponseStream *stream, std::string &area);
+  void add_node_label_(AsyncResponseStream *stream, std::string &node);
+  void add_friendly_name_label_(AsyncResponseStream *stream, std::string &friendly_name);
 
 #ifdef USE_SENSOR
   /// Return the type for prometheus
   void sensor_type_(AsyncResponseStream *stream);
   /// Return the sensor state as prometheus data point
-  void sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj);
+  void sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj, std::string &area, std::string &node,
+                   std::string &friendly_name);
 #endif
 
 #ifdef USE_BINARY_SENSOR
   /// Return the type for prometheus
   void binary_sensor_type_(AsyncResponseStream *stream);
   /// Return the sensor state as prometheus data point
-  void binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj);
+  void binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj, std::string &area,
+                          std::string &node, std::string &friendly_name);
 #endif
 
 #ifdef USE_FAN
   /// Return the type for prometheus
   void fan_type_(AsyncResponseStream *stream);
   /// Return the sensor state as prometheus data point
-  void fan_row_(AsyncResponseStream *stream, fan::Fan *obj);
+  void fan_row_(AsyncResponseStream *stream, fan::Fan *obj, std::string &area, std::string &node,
+                std::string &friendly_name);
 #endif
 
 #ifdef USE_LIGHT
   /// Return the type for prometheus
   void light_type_(AsyncResponseStream *stream);
   /// Return the Light Values state as prometheus data point
-  void light_row_(AsyncResponseStream *stream, light::LightState *obj);
+  void light_row_(AsyncResponseStream *stream, light::LightState *obj, std::string &area, std::string &node,
+                  std::string &friendly_name);
 #endif
 
 #ifdef USE_COVER
   /// Return the type for prometheus
   void cover_type_(AsyncResponseStream *stream);
   /// Return the switch Values state as prometheus data point
-  void cover_row_(AsyncResponseStream *stream, cover::Cover *obj);
+  void cover_row_(AsyncResponseStream *stream, cover::Cover *obj, std::string &area, std::string &node,
+                  std::string &friendly_name);
 #endif
 
 #ifdef USE_SWITCH
   /// Return the type for prometheus
   void switch_type_(AsyncResponseStream *stream);
   /// Return the switch Values state as prometheus data point
-  void switch_row_(AsyncResponseStream *stream, switch_::Switch *obj);
+  void switch_row_(AsyncResponseStream *stream, switch_::Switch *obj, std::string &area, std::string &node,
+                   std::string &friendly_name);
 #endif
 
 #ifdef USE_LOCK
   /// Return the type for prometheus
   void lock_type_(AsyncResponseStream *stream);
   /// Return the lock Values state as prometheus data point
-  void lock_row_(AsyncResponseStream *stream, lock::Lock *obj);
+  void lock_row_(AsyncResponseStream *stream, lock::Lock *obj, std::string &area, std::string &node,
+                 std::string &friendly_name);
 #endif
 
 #ifdef USE_TEXT_SENSOR
   /// Return the type for prometheus
   void text_sensor_type_(AsyncResponseStream *stream);
   /// Return the lock Values state as prometheus data point
-  void text_sensor_row_(AsyncResponseStream *stream, text_sensor::TextSensor *obj);
+  void text_sensor_row_(AsyncResponseStream *stream, text_sensor::TextSensor *obj, std::string &area, std::string &node,
+                        std::string &friendly_name);
 #endif
 
   web_server_base::WebServerBase *base_;
diff --git a/tests/components/prometheus/common.yaml b/tests/components/prometheus/common.yaml
index 7aa509f8b2..68ef2a2f58 100644
--- a/tests/components/prometheus/common.yaml
+++ b/tests/components/prometheus/common.yaml
@@ -1,3 +1,8 @@
+esphome:
+  name: livingroomdevice
+  friendly_name: Living Room Device
+  area: Living Room
+
 wifi:
   ssid: MySSID
   password: password1
@@ -14,6 +19,9 @@ sensor:
     update_interval: 60s
 
 text_sensor:
+  - platform: version
+    name: "ESPHome Version"
+    hide_timestamp: true
   - platform: template
     id: template_text_sensor1
     lambda: |-
@@ -24,6 +32,52 @@ text_sensor:
       }
     update_interval: 60s
 
+binary_sensor:
+  - platform: template
+    id: template_binary_sensor1
+    lambda: |-
+      if (millis() > 10000) {
+        return true;
+      } else {
+        return false;
+      }
+
+switch:
+  - platform: template
+    id: template_switch1
+    lambda: |-
+      if (millis() > 10000) {
+        return true;
+      } else {
+        return false;
+      }
+    optimistic: true
+
+fan:
+  - platform: template
+    id: template_fan1
+
+cover:
+  - platform: template
+    id: template_cover1
+    lambda: |-
+      if (millis() > 10000) {
+        return COVER_OPEN;
+      } else {
+        return COVER_CLOSED;
+      }
+
+lock:
+  - platform: template
+    id: template_lock1
+    lambda: |-
+      if (millis() > 10000) {
+        return LOCK_STATE_LOCKED;
+      } else {
+        return LOCK_STATE_UNLOCKED;
+      }
+    optimistic: true
+
 prometheus:
   include_internal: true
   relabel:

From ee3ee3a63b784767ea907f2f52935db34f0e1267 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Wed, 30 Oct 2024 12:10:58 +1100
Subject: [PATCH 063/282] [http_request] Implement `on_error` trigger for
 requests (#7696)

---
 esphome/components/http_request/__init__.py    | 12 ++++++++++++
 esphome/components/http_request/http_request.h | 11 ++++++++---
 tests/components/http_request/common.yaml      |  2 ++
 3 files changed, 22 insertions(+), 3 deletions(-)

diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py
index 0407bbd326..78064fb4b4 100644
--- a/esphome/components/http_request/__init__.py
+++ b/esphome/components/http_request/__init__.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_ESP8266_DISABLE_SSL_SUPPORT,
     CONF_ID,
     CONF_METHOD,
+    CONF_ON_ERROR,
     CONF_TIMEOUT,
     CONF_TRIGGER_ID,
     CONF_URL,
@@ -185,6 +186,13 @@ HTTP_REQUEST_ACTION_SCHEMA = cv.Schema(
         cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(
             {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(HttpRequestResponseTrigger)}
         ),
+        cv.Optional(CONF_ON_ERROR): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
+                    automation.Trigger.template()
+                )
+            }
+        ),
         cv.Optional(CONF_MAX_RESPONSE_BUFFER_SIZE, default="1kB"): cv.validate_bytes,
     }
 )
@@ -272,5 +280,9 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
             ],
             conf,
         )
+    for conf in config.get(CONF_ON_ERROR, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
+        cg.add(var.register_error_trigger(trigger))
+        await automation.build_automation(trigger, [], conf)
 
     return var
diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h
index d87d9b8a45..4ed2c834f8 100644
--- a/esphome/components/http_request/http_request.h
+++ b/esphome/components/http_request/http_request.h
@@ -135,8 +135,8 @@ class HttpRequestComponent : public Component {
 
  protected:
   const char *useragent_{nullptr};
-  bool follow_redirects_;
-  uint16_t redirect_limit_;
+  bool follow_redirects_{};
+  uint16_t redirect_limit_{};
   uint16_t timeout_{4500};
   uint32_t watchdog_timeout_{0};
 };
@@ -157,6 +157,8 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
 
   void register_response_trigger(HttpRequestResponseTrigger *trigger) { this->response_triggers_.push_back(trigger); }
 
+  void register_error_trigger(Trigger<> *trigger) { this->error_triggers_.push_back(trigger); }
+
   void set_max_response_buffer_size(size_t max_response_buffer_size) {
     this->max_response_buffer_size_ = max_response_buffer_size;
   }
@@ -186,6 +188,8 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
     auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, headers);
 
     if (container == nullptr) {
+      for (auto *trigger : this->error_triggers_)
+        trigger->trigger(x...);
       return;
     }
 
@@ -237,7 +241,8 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
   std::map<const char *, TemplatableValue<const char *, Ts...>> headers_{};
   std::map<const char *, TemplatableValue<std::string, Ts...>> json_{};
   std::function<void(Ts..., JsonObject)> json_func_{nullptr};
-  std::vector<HttpRequestResponseTrigger *> response_triggers_;
+  std::vector<HttpRequestResponseTrigger *> response_triggers_{};
+  std::vector<Trigger<> *> error_triggers_{};
 
   size_t max_response_buffer_size_{SIZE_MAX};
 };
diff --git a/tests/components/http_request/common.yaml b/tests/components/http_request/common.yaml
index 589b7fb4b4..593b85e435 100644
--- a/tests/components/http_request/common.yaml
+++ b/tests/components/http_request/common.yaml
@@ -12,6 +12,8 @@ esphome:
           url: https://esphome.io
           headers:
             Content-Type: application/json
+          on_error:
+            logger.log: "Request failed"
           on_response:
             then:
               - logger.log:

From 6afd004ec54670ed27cd29202a7c290d258a8b64 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 31 Oct 2024 08:25:36 +1300
Subject: [PATCH 064/282] Bump pypa/gh-action-pypi-publish from 1.10.3 to
 1.11.0 (#7700)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/workflows/release.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5072bec222..82d7ae5ee8 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -65,7 +65,7 @@ jobs:
           pip3 install build
           python3 -m build
       - name: Publish
-        uses: pypa/gh-action-pypi-publish@v1.10.3
+        uses: pypa/gh-action-pypi-publish@v1.11.0
 
   deploy-docker:
     name: Build ESPHome ${{ matrix.platform }}

From 765579dabbd607975901b34b350c4252f80ca3ab Mon Sep 17 00:00:00 2001
From: Kevin Ahrendt <kevin.ahrendt@nabucasa.com>
Date: Wed, 30 Oct 2024 15:29:24 -0400
Subject: [PATCH 065/282] [es8311] Add es8311 dac component (#7693)

---
 CODEOWNERS                                    |   1 +
 esphome/components/es8311/__init__.py         |   0
 esphome/components/es8311/audio_dac.py        |  70 ++++++
 esphome/components/es8311/es8311.cpp          | 227 ++++++++++++++++++
 esphome/components/es8311/es8311.h            | 135 +++++++++++
 esphome/components/es8311/es8311_const.h      | 195 +++++++++++++++
 esphome/components/i2s_audio/__init__.py      |   4 +-
 esphome/const.py                              |   1 +
 tests/components/es8311/common.yaml           |  15 ++
 tests/components/es8311/test.esp32-ard.yaml   |   5 +
 .../components/es8311/test.esp32-c3-ard.yaml  |   5 +
 .../components/es8311/test.esp32-c3-idf.yaml  |   5 +
 tests/components/es8311/test.esp32-idf.yaml   |   5 +
 tests/components/es8311/test.esp8266-ard.yaml |   5 +
 14 files changed, 670 insertions(+), 3 deletions(-)
 create mode 100644 esphome/components/es8311/__init__.py
 create mode 100644 esphome/components/es8311/audio_dac.py
 create mode 100644 esphome/components/es8311/es8311.cpp
 create mode 100644 esphome/components/es8311/es8311.h
 create mode 100644 esphome/components/es8311/es8311_const.h
 create mode 100644 tests/components/es8311/common.yaml
 create mode 100644 tests/components/es8311/test.esp32-ard.yaml
 create mode 100644 tests/components/es8311/test.esp32-c3-ard.yaml
 create mode 100644 tests/components/es8311/test.esp32-c3-idf.yaml
 create mode 100644 tests/components/es8311/test.esp32-idf.yaml
 create mode 100644 tests/components/es8311/test.esp8266-ard.yaml

diff --git a/CODEOWNERS b/CODEOWNERS
index 5eb1f863f2..8fbbacef59 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/es8311/* @kahrendt @kroimon
 esphome/components/esp32/* @esphome/core
 esphome/components/esp32_ble/* @Rapsssito @jesserockz
 esphome/components/esp32_ble_client/* @jesserockz
diff --git a/esphome/components/es8311/__init__.py b/esphome/components/es8311/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/es8311/audio_dac.py b/esphome/components/es8311/audio_dac.py
new file mode 100644
index 0000000000..1b450c3c11
--- /dev/null
+++ b/esphome/components/es8311/audio_dac.py
@@ -0,0 +1,70 @@
+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
+
+CODEOWNERS = ["@kroimon", "@kahrendt"]
+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"
+
+es8311_resolution = es8311_ns.enum("ES8311Resolution")
+ES8311_BITS_PER_SAMPLE_ENUM = {
+    16: es8311_resolution.ES8311_RESOLUTION_16,
+    24: es8311_resolution.ES8311_RESOLUTION_24,
+    32: es8311_resolution.ES8311_RESOLUTION_32,
+}
+
+es8311_mic_gain = es8311_ns.enum("ES8311MicGain")
+ES8311_MIC_GAIN_ENUM = {
+    "MIN": es8311_mic_gain.ES8311_MIC_GAIN_MIN,
+    "0DB": es8311_mic_gain.ES8311_MIC_GAIN_0DB,
+    "6DB": es8311_mic_gain.ES8311_MIC_GAIN_6DB,
+    "12DB": es8311_mic_gain.ES8311_MIC_GAIN_12DB,
+    "18DB": es8311_mic_gain.ES8311_MIC_GAIN_18DB,
+    "24DB": es8311_mic_gain.ES8311_MIC_GAIN_24DB,
+    "30DB": es8311_mic_gain.ES8311_MIC_GAIN_30DB,
+    "36DB": es8311_mic_gain.ES8311_MIC_GAIN_36DB,
+    "42DB": es8311_mic_gain.ES8311_MIC_GAIN_42DB,
+    "MAX": es8311_mic_gain.ES8311_MIC_GAIN_MAX,
+}
+
+
+_validate_bits = cv.float_with_unit("bits", "bit")
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(ES8311),
+            cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All(
+                _validate_bits, cv.enum(ES8311_BITS_PER_SAMPLE_ENUM)
+            ),
+            cv.Optional(CONF_MIC_GAIN, default="42DB"): cv.enum(
+                ES8311_MIC_GAIN_ENUM, upper=True
+            ),
+            cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1),
+            cv.Optional(CONF_USE_MCLK, default=True): cv.boolean,
+            cv.Optional(CONF_USE_MICROPHONE, default=False): cv.boolean,
+        }
+    )
+    .extend(cv.COMPONENT_SCHEMA)
+    .extend(i2c.i2c_device_schema(0x18))
+)
+
+
+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_frequency(config[CONF_SAMPLE_RATE]))
+    cg.add(var.set_use_mclk(config[CONF_USE_MCLK]))
+    cg.add(var.set_use_mic(config[CONF_USE_MICROPHONE]))
diff --git a/esphome/components/es8311/es8311.cpp b/esphome/components/es8311/es8311.cpp
new file mode 100644
index 0000000000..1cb1fbbe08
--- /dev/null
+++ b/esphome/components/es8311/es8311.cpp
@@ -0,0 +1,227 @@
+#include "es8311.h"
+#include "es8311_const.h"
+#include "esphome/core/hal.h"
+#include "esphome/core/log.h"
+#include <cinttypes>
+
+namespace esphome {
+namespace es8311 {
+
+static const char *const TAG = "es8311";
+
+// Mark the component as failed; use only in setup
+#define ES8311_ERROR_FAILED(func) \
+  if (!(func)) { \
+    this->mark_failed(); \
+    return; \
+  }
+// Return false; use outside of setup
+#define ES8311_ERROR_CHECK(func) \
+  if (!(func)) { \
+    return false; \
+  }
+
+void ES8311::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up ES8311...");
+
+  // Reset
+  ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, 0x1F));
+  ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, 0x00));
+
+  ES8311_ERROR_FAILED(this->configure_clock_());
+  ES8311_ERROR_FAILED(this->configure_format_());
+  ES8311_ERROR_FAILED(this->configure_mic_());
+
+  // Set initial volume
+  this->set_volume(0.75);  // 0.75 = 0xBF = 0dB
+
+  // Power up analog circuitry
+  ES8311_ERROR_FAILED(this->write_byte(ES8311_REG0D_SYSTEM, 0x01));
+  // Enable analog PGA, enable ADC modulator
+  ES8311_ERROR_FAILED(this->write_byte(ES8311_REG0E_SYSTEM, 0x02));
+  // Power up DAC
+  ES8311_ERROR_FAILED(this->write_byte(ES8311_REG12_SYSTEM, 0x00));
+  // Enable output to HP drive
+  ES8311_ERROR_FAILED(this->write_byte(ES8311_REG13_SYSTEM, 0x10));
+  // ADC Equalizer bypass, cancel DC offset in digital domain
+  ES8311_ERROR_FAILED(this->write_byte(ES8311_REG1C_ADC, 0x6A));
+  // Bypass DAC equalizer
+  ES8311_ERROR_FAILED(this->write_byte(ES8311_REG37_DAC, 0x08));
+  // Power On
+  ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, 0x80));
+}
+
+void ES8311::dump_config() {
+  ESP_LOGCONFIG(TAG, "ES8311 Audio Codec:");
+  ESP_LOGCONFIG(TAG, "  Use MCLK: %s", YESNO(this->use_mclk_));
+  ESP_LOGCONFIG(TAG, "  Use Microphone: %s", YESNO(this->use_mic_));
+  ESP_LOGCONFIG(TAG, "  DAC Bits per Sample: %" PRIu8, this->resolution_out_);
+  ESP_LOGCONFIG(TAG, "  Sample Rate: %" PRIu32, this->sample_frequency_);
+
+  if (this->is_failed()) {
+    ESP_LOGCONFIG(TAG, "  Failed to initialize!");
+    return;
+  }
+}
+
+bool ES8311::set_volume(float volume) {
+  volume = clamp(volume, 0.0f, 1.0f);
+  uint8_t reg32 = remap<uint8_t, float>(volume, 0.0f, 1.0f, 0, 255);
+  return this->write_byte(ES8311_REG32_DAC, reg32);
+}
+
+float ES8311::volume() {
+  uint8_t reg32;
+  this->read_byte(ES8311_REG32_DAC, &reg32);
+  return remap<float, uint8_t>(reg32, 0, 255, 0.0f, 1.0f);
+}
+
+uint8_t ES8311::calculate_resolution_value(ES8311Resolution resolution) {
+  switch (resolution) {
+    case ES8311_RESOLUTION_16:
+      return (3 << 2);
+    case ES8311_RESOLUTION_18:
+      return (2 << 2);
+    case ES8311_RESOLUTION_20:
+      return (1 << 2);
+    case ES8311_RESOLUTION_24:
+      return (0 << 2);
+    case ES8311_RESOLUTION_32:
+      return (4 << 2);
+    default:
+      return 0;
+  }
+}
+
+const ES8311Coefficient *ES8311::get_coefficient(uint32_t mclk, uint32_t rate) {
+  for (const auto &coefficient : ES8311_COEFFICIENTS) {
+    if (coefficient.mclk == mclk && coefficient.rate == rate)
+      return &coefficient;
+  }
+  return nullptr;
+}
+
+bool ES8311::configure_clock_() {
+  // Register 0x01: select clock source for internal MCLK and determine its frequency
+  uint8_t reg01 = 0x3F;  // Enable all clocks
+
+  uint32_t mclk_frequency = this->sample_frequency_ * this->mclk_multiple_;
+  if (!this->use_mclk_) {
+    reg01 |= BIT(7);  // Use SCLK
+    mclk_frequency = this->sample_frequency_ * (int) this->resolution_out_ * 2;
+  }
+  if (this->mclk_inverted_) {
+    reg01 |= BIT(6);  // Invert MCLK pin
+  }
+  ES8311_ERROR_CHECK(this->write_byte(ES8311_REG01_CLK_MANAGER, reg01));
+
+  // Get clock coefficients from coefficient table
+  auto *coefficient = get_coefficient(mclk_frequency, this->sample_frequency_);
+  if (coefficient == nullptr) {
+    ESP_LOGE(TAG, "Unable to configure sample rate %" PRIu32 "Hz with %" PRIu32 "Hz MCLK", this->sample_frequency_,
+             mclk_frequency);
+    return false;
+  }
+
+  // Register 0x02
+  uint8_t reg02;
+  ES8311_ERROR_CHECK(this->read_byte(ES8311_REG02_CLK_MANAGER, &reg02));
+  reg02 &= 0x07;
+  reg02 |= (coefficient->pre_div - 1) << 5;
+  reg02 |= coefficient->pre_mult << 3;
+  ES8311_ERROR_CHECK(this->write_byte(ES8311_REG02_CLK_MANAGER, reg02));
+
+  // Register 0x03
+  const uint8_t reg03 = (coefficient->fs_mode << 6) | coefficient->adc_osr;
+  ES8311_ERROR_CHECK(this->write_byte(ES8311_REG03_CLK_MANAGER, reg03));
+
+  // Register 0x04
+  ES8311_ERROR_CHECK(this->write_byte(ES8311_REG04_CLK_MANAGER, coefficient->dac_osr));
+
+  // Register 0x05
+  const uint8_t reg05 = ((coefficient->adc_div - 1) << 4) | (coefficient->dac_div - 1);
+  ES8311_ERROR_CHECK(this->write_byte(ES8311_REG05_CLK_MANAGER, reg05));
+
+  // Register 0x06
+  uint8_t reg06;
+  ES8311_ERROR_CHECK(this->read_byte(ES8311_REG06_CLK_MANAGER, &reg06));
+  if (this->sclk_inverted_) {
+    reg06 |= BIT(5);
+  } else {
+    reg06 &= ~BIT(5);
+  }
+  reg06 &= 0xE0;
+  if (coefficient->bclk_div < 19) {
+    reg06 |= (coefficient->bclk_div - 1) << 0;
+  } else {
+    reg06 |= (coefficient->bclk_div) << 0;
+  }
+  ES8311_ERROR_CHECK(this->write_byte(ES8311_REG06_CLK_MANAGER, reg06));
+
+  // Register 0x07
+  uint8_t reg07;
+  ES8311_ERROR_CHECK(this->read_byte(ES8311_REG07_CLK_MANAGER, &reg07));
+  reg07 &= 0xC0;
+  reg07 |= coefficient->lrck_h << 0;
+  ES8311_ERROR_CHECK(this->write_byte(ES8311_REG07_CLK_MANAGER, reg07));
+
+  // Register 0x08
+  ES8311_ERROR_CHECK(this->write_byte(ES8311_REG08_CLK_MANAGER, coefficient->lrck_l));
+
+  // Successfully configured the clock
+  return true;
+}
+
+bool ES8311::configure_format_() {
+  // Configure I2S mode and format
+  uint8_t reg00;
+  ES8311_ERROR_CHECK(this->read_byte(ES8311_REG00_RESET, &reg00));
+  reg00 &= 0xBF;
+  ES8311_ERROR_CHECK(this->write_byte(ES8311_REG00_RESET, reg00));
+
+  // Configure SDP in resolution
+  uint8_t reg09 = calculate_resolution_value(this->resolution_in_);
+  ES8311_ERROR_CHECK(this->write_byte(ES8311_REG09_SDPIN, reg09));
+
+  // Configure SDP out resolution
+  uint8_t reg0a = calculate_resolution_value(this->resolution_out_);
+  ES8311_ERROR_CHECK(this->write_byte(ES8311_REG0A_SDPOUT, reg0a));
+
+  // Successfully configured the format
+  return true;
+}
+
+bool ES8311::configure_mic_() {
+  uint8_t reg14 = 0x1A;  // Enable analog MIC and max PGA gain
+  if (this->use_mic_) {
+    reg14 |= BIT(6);  // Enable PDM digital microphone
+  }
+  ES8311_ERROR_CHECK(this->write_byte(ES8311_REG14_SYSTEM, reg14));
+
+  ES8311_ERROR_CHECK(this->write_byte(ES8311_REG16_ADC, this->mic_gain_));  // ADC gain scale up
+  ES8311_ERROR_CHECK(this->write_byte(ES8311_REG17_ADC, 0xC8));             // Set ADC gain
+
+  // Successfully configured the microphones
+  return true;
+}
+
+bool ES8311::set_mute_state_(bool mute_state) {
+  uint8_t reg31;
+
+  this->is_muted_ = mute_state;
+
+  if (!this->read_byte(ES8311_REG31_DAC, &reg31)) {
+    return false;
+  }
+
+  if (mute_state) {
+    reg31 |= BIT(6) | BIT(5);
+  } else {
+    reg31 &= ~(BIT(6) | BIT(5));
+  }
+
+  return this->write_byte(ES8311_REG31_DAC, reg31);
+}
+
+}  // namespace es8311
+}  // namespace esphome
diff --git a/esphome/components/es8311/es8311.h b/esphome/components/es8311/es8311.h
new file mode 100644
index 0000000000..840a07204c
--- /dev/null
+++ b/esphome/components/es8311/es8311.h
@@ -0,0 +1,135 @@
+#pragma once
+
+#include "esphome/components/audio_dac/audio_dac.h"
+#include "esphome/components/i2c/i2c.h"
+#include "esphome/core/component.h"
+
+namespace esphome {
+namespace es8311 {
+
+enum ES8311MicGain {
+  ES8311_MIC_GAIN_MIN = -1,
+  ES8311_MIC_GAIN_0DB,
+  ES8311_MIC_GAIN_6DB,
+  ES8311_MIC_GAIN_12DB,
+  ES8311_MIC_GAIN_18DB,
+  ES8311_MIC_GAIN_24DB,
+  ES8311_MIC_GAIN_30DB,
+  ES8311_MIC_GAIN_36DB,
+  ES8311_MIC_GAIN_42DB,
+  ES8311_MIC_GAIN_MAX
+};
+
+enum ES8311Resolution : uint8_t {
+  ES8311_RESOLUTION_16 = 16,
+  ES8311_RESOLUTION_18 = 18,
+  ES8311_RESOLUTION_20 = 20,
+  ES8311_RESOLUTION_24 = 24,
+  ES8311_RESOLUTION_32 = 32
+};
+
+struct ES8311Coefficient {
+  uint32_t mclk;     // mclk frequency
+  uint32_t rate;     // sample rate
+  uint8_t pre_div;   // the pre divider with range from 1 to 8
+  uint8_t pre_mult;  // the pre multiplier with x1, x2, x4 and x8 selection
+  uint8_t adc_div;   // adcclk divider
+  uint8_t dac_div;   // dacclk divider
+  uint8_t fs_mode;   // single speed (0) or double speed (1)
+  uint8_t lrck_h;    // adc lrck divider and dac lrck divider
+  uint8_t lrck_l;    //
+  uint8_t bclk_div;  // sclk divider
+  uint8_t adc_osr;   // adc osr
+  uint8_t dac_osr;   // dac osr
+};
+
+class ES8311 : public audio_dac::AudioDac, public Component, public i2c::I2CDevice {
+ public:
+  /////////////////////////
+  // Component overrides //
+  /////////////////////////
+
+  void setup() override;
+  float get_setup_priority() const override { return setup_priority::DATA; }
+  void dump_config() override;
+
+  ////////////////////////
+  // AudioDac overrides //
+  ////////////////////////
+
+  /// @brief Writes the volume out to the DAC
+  /// @param volume floating point between 0.0 and 1.0
+  /// @return True if successful and false otherwise
+  bool set_volume(float volume) override;
+
+  /// @brief Gets the current volume out from the DAC
+  /// @return floating point between 0.0 and 1.0
+  float volume() override;
+
+  /// @brief Disables mute for audio out
+  /// @return True if successful and false otherwise
+  bool set_mute_off() override { return this->set_mute_state_(false); }
+
+  /// @brief Enables mute for audio out
+  /// @return True if successful and false otherwise
+  bool set_mute_on() override { return this->set_mute_state_(true); }
+
+  bool is_muted() override { return this->is_muted_; }
+
+  //////////////////////////////////
+  // ES8311 configuration setters //
+  //////////////////////////////////
+
+  void set_use_mclk(bool use_mclk) { this->use_mclk_ = use_mclk; }
+  void set_bits_per_sample(ES8311Resolution resolution) {
+    this->resolution_in_ = resolution;
+    this->resolution_out_ = resolution;
+  }
+  void set_sample_frequency(uint32_t sample_frequency) { this->sample_frequency_ = sample_frequency; }
+  void set_use_mic(bool use_mic) { this->use_mic_ = use_mic; }
+  void set_mic_gain(ES8311MicGain mic_gain) { this->mic_gain_ = mic_gain; }
+
+ protected:
+  /// @brief Computes the register value for the configured resolution (bits per sample)
+  /// @param resolution bits per sample enum for both audio in and audio out
+  /// @return register value
+  static uint8_t calculate_resolution_value(ES8311Resolution resolution);
+
+  /// @brief Retrieves the appropriate registers values for the configured mclk and rate
+  /// @param mclk mlck frequency in Hz
+  /// @param rate sample rate frequency in Hz
+  /// @return ES8311Coeffecient containing appropriate register values to configure the ES8311 or nullptr if impossible
+  static const ES8311Coefficient *get_coefficient(uint32_t mclk, uint32_t rate);
+
+  /// @brief Configures the ES8311 registers for the chosen sample rate
+  /// @return True if successful and false otherwise
+  bool configure_clock_();
+
+  /// @brief Configures the ES8311 registers for the chosen bits per sample
+  /// @return True if successful and false otherwise
+  bool configure_format_();
+
+  /// @brief Configures the ES8311 microphone registers
+  /// @return True if successful and false otherwise
+  bool configure_mic_();
+
+  /// @brief Mutes or unmute the DAC audio out
+  /// @param mute_state True to mute, false to unmute
+  /// @return
+  bool set_mute_state_(bool mute_state);
+
+  bool use_mic_;
+  ES8311MicGain mic_gain_;
+
+  bool use_mclk_;                // true = use dedicated MCLK pin, false = use SCLK
+  bool sclk_inverted_{false};    // SCLK is inverted
+  bool mclk_inverted_{false};    // MCLK is inverted (ignored if use_mclk_ == false)
+  uint32_t mclk_multiple_{256};  // MCLK frequency is sample rate * mclk_multiple_ (ignored if use_mclk_ == false)
+
+  uint32_t sample_frequency_;  // in Hz
+  ES8311Resolution resolution_in_;
+  ES8311Resolution resolution_out_;
+};
+
+}  // namespace es8311
+}  // namespace esphome
diff --git a/esphome/components/es8311/es8311_const.h b/esphome/components/es8311/es8311_const.h
new file mode 100644
index 0000000000..7463a92ef1
--- /dev/null
+++ b/esphome/components/es8311/es8311_const.h
@@ -0,0 +1,195 @@
+#pragma once
+
+#include "es8311.h"
+
+namespace esphome {
+namespace es8311 {
+
+// ES8311 register addresses
+static const uint8_t ES8311_REG00_RESET = 0x00;        // Reset
+static const uint8_t ES8311_REG01_CLK_MANAGER = 0x01;  // Clock Manager: select clk src for mclk, enable clock for codec
+static const uint8_t ES8311_REG02_CLK_MANAGER = 0x02;  // Clock Manager: clk divider and clk multiplier
+static const uint8_t ES8311_REG03_CLK_MANAGER = 0x03;  // Clock Manager: adc fsmode and osr
+static const uint8_t ES8311_REG04_CLK_MANAGER = 0x04;  // Clock Manager: dac osr
+static const uint8_t ES8311_REG05_CLK_MANAGER = 0x05;  // Clock Manager: clk divider for adc and dac
+static const uint8_t ES8311_REG06_CLK_MANAGER = 0x06;  // Clock Manager: bclk inverter BIT(5) and divider
+static const uint8_t ES8311_REG07_CLK_MANAGER = 0x07;  // Clock Manager: tri-state, lrck divider
+static const uint8_t ES8311_REG08_CLK_MANAGER = 0x08;  // Clock Manager: lrck divider
+static const uint8_t ES8311_REG09_SDPIN = 0x09;        // Serial Digital Port: DAC
+static const uint8_t ES8311_REG0A_SDPOUT = 0x0A;       // Serial Digital Port: ADC
+static const uint8_t ES8311_REG0B_SYSTEM = 0x0B;       // System
+static const uint8_t ES8311_REG0C_SYSTEM = 0x0C;       // System
+static const uint8_t ES8311_REG0D_SYSTEM = 0x0D;       // System: power up/down
+static const uint8_t ES8311_REG0E_SYSTEM = 0x0E;       // System: power up/down
+static const uint8_t ES8311_REG0F_SYSTEM = 0x0F;       // System: low power
+static const uint8_t ES8311_REG10_SYSTEM = 0x10;       // System
+static const uint8_t ES8311_REG11_SYSTEM = 0x11;       // System
+static const uint8_t ES8311_REG12_SYSTEM = 0x12;       // System: Enable DAC
+static const uint8_t ES8311_REG13_SYSTEM = 0x13;       // System
+static const uint8_t ES8311_REG14_SYSTEM = 0x14;       // System: select DMIC, select analog pga gain
+static const uint8_t ES8311_REG15_ADC = 0x15;          // ADC: adc ramp rate, dmic sense
+static const uint8_t ES8311_REG16_ADC = 0x16;          // ADC
+static const uint8_t ES8311_REG17_ADC = 0x17;          // ADC: volume
+static const uint8_t ES8311_REG18_ADC = 0x18;          // ADC: alc enable and winsize
+static const uint8_t ES8311_REG19_ADC = 0x19;          // ADC: alc maxlevel
+static const uint8_t ES8311_REG1A_ADC = 0x1A;          // ADC: alc automute
+static const uint8_t ES8311_REG1B_ADC = 0x1B;          // ADC: alc automute, adc hpf s1
+static const uint8_t ES8311_REG1C_ADC = 0x1C;          // ADC: equalizer, hpf s2
+static const uint8_t ES8311_REG1D_ADCEQ = 0x1D;        // ADCEQ: equalizer B0
+static const uint8_t ES8311_REG1E_ADCEQ = 0x1E;        // ADCEQ: equalizer B0
+static const uint8_t ES8311_REG1F_ADCEQ = 0x1F;        // ADCEQ: equalizer B0
+static const uint8_t ES8311_REG20_ADCEQ = 0x20;        // ADCEQ: equalizer B0
+static const uint8_t ES8311_REG21_ADCEQ = 0x21;        // ADCEQ: equalizer A1
+static const uint8_t ES8311_REG22_ADCEQ = 0x22;        // ADCEQ: equalizer A1
+static const uint8_t ES8311_REG23_ADCEQ = 0x23;        // ADCEQ: equalizer A1
+static const uint8_t ES8311_REG24_ADCEQ = 0x24;        // ADCEQ: equalizer A1
+static const uint8_t ES8311_REG25_ADCEQ = 0x25;        // ADCEQ: equalizer A2
+static const uint8_t ES8311_REG26_ADCEQ = 0x26;        // ADCEQ: equalizer A2
+static const uint8_t ES8311_REG27_ADCEQ = 0x27;        // ADCEQ: equalizer A2
+static const uint8_t ES8311_REG28_ADCEQ = 0x28;        // ADCEQ: equalizer A2
+static const uint8_t ES8311_REG29_ADCEQ = 0x29;        // ADCEQ: equalizer B1
+static const uint8_t ES8311_REG2A_ADCEQ = 0x2A;        // ADCEQ: equalizer B1
+static const uint8_t ES8311_REG2B_ADCEQ = 0x2B;        // ADCEQ: equalizer B1
+static const uint8_t ES8311_REG2C_ADCEQ = 0x2C;        // ADCEQ: equalizer B1
+static const uint8_t ES8311_REG2D_ADCEQ = 0x2D;        // ADCEQ: equalizer B2
+static const uint8_t ES8311_REG2E_ADCEQ = 0x2E;        // ADCEQ: equalizer B2
+static const uint8_t ES8311_REG2F_ADCEQ = 0x2F;        // ADCEQ: equalizer B2
+static const uint8_t ES8311_REG30_ADCEQ = 0x30;        // ADCEQ: equalizer B2
+static const uint8_t ES8311_REG31_DAC = 0x31;          // DAC: mute
+static const uint8_t ES8311_REG32_DAC = 0x32;          // DAC: volume
+static const uint8_t ES8311_REG33_DAC = 0x33;          // DAC: offset
+static const uint8_t ES8311_REG34_DAC = 0x34;          // DAC: drc enable, drc winsize
+static const uint8_t ES8311_REG35_DAC = 0x35;          // DAC: drc maxlevel, minilevel
+static const uint8_t ES8311_REG36_DAC = 0x36;          // DAC
+static const uint8_t ES8311_REG37_DAC = 0x37;          // DAC: ramprate
+static const uint8_t ES8311_REG38_DACEQ = 0x38;        // DACEQ: equalizer B0
+static const uint8_t ES8311_REG39_DACEQ = 0x39;        // DACEQ: equalizer B0
+static const uint8_t ES8311_REG3A_DACEQ = 0x3A;        // DACEQ: equalizer B0
+static const uint8_t ES8311_REG3B_DACEQ = 0x3B;        // DACEQ: equalizer B0
+static const uint8_t ES8311_REG3C_DACEQ = 0x3C;        // DACEQ: equalizer B1
+static const uint8_t ES8311_REG3D_DACEQ = 0x3D;        // DACEQ: equalizer B1
+static const uint8_t ES8311_REG3E_DACEQ = 0x3E;        // DACEQ: equalizer B1
+static const uint8_t ES8311_REG3F_DACEQ = 0x3F;        // DACEQ: equalizer B1
+static const uint8_t ES8311_REG40_DACEQ = 0x40;        // DACEQ: equalizer A1
+static const uint8_t ES8311_REG41_DACEQ = 0x41;        // DACEQ: equalizer A1
+static const uint8_t ES8311_REG42_DACEQ = 0x42;        // DACEQ: equalizer A1
+static const uint8_t ES8311_REG43_DACEQ = 0x43;        // DACEQ: equalizer A1
+static const uint8_t ES8311_REG44_GPIO = 0x44;         // GPIO: dac2adc for test
+static const uint8_t ES8311_REG45_GP = 0x45;           // GPIO: GP control
+static const uint8_t ES8311_REGFA_I2C = 0xFA;          // I2C: reset registers
+static const uint8_t ES8311_REGFC_FLAG = 0xFC;         // Flag
+static const uint8_t ES8311_REGFD_CHD1 = 0xFD;         // Chip: ID1
+static const uint8_t ES8311_REGFE_CHD2 = 0xFE;         // Chip: ID2
+static const uint8_t ES8311_REGFF_CHVER = 0xFF;        // Chip: Version
+
+// ES8311 clock divider coefficients
+static const ES8311Coefficient ES8311_COEFFICIENTS[] = {
+    // clang-format off
+
+  //   mclk,  rate, pre_  pre_  adc_  dac_  fs_   lrck  lrck bclk_  adc_  dac_
+  //                 div, mult,  div,  div, mode,   _h,   _l,  div,  osr,  osr
+
+  // 8k
+  {12288000,  8000, 0x06, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  {18432000,  8000, 0x03, 0x02, 0x03, 0x03, 0x00, 0x05, 0xff, 0x18, 0x10, 0x20},
+  {16384000,  8000, 0x08, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 8192000,  8000, 0x04, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 6144000,  8000, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 4096000,  8000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 3072000,  8000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 2048000,  8000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 1536000,  8000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 1024000,  8000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+
+  // 11.025k
+  {11289600, 11025, 0x04, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 5644800, 11025, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 2822400, 11025, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 1411200, 11025, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+
+  // 12k
+  {12288000, 12000, 0x04, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 6144000, 12000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 3072000, 12000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 1536000, 12000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+
+  // 16k
+  {12288000, 16000, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  {18432000, 16000, 0x03, 0x02, 0x03, 0x03, 0x00, 0x02, 0xff, 0x0c, 0x10, 0x20},
+  {16384000, 16000, 0x04, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 8192000, 16000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 6144000, 16000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 4096000, 16000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 3072000, 16000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 2048000, 16000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 1536000, 16000, 0x03, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+  { 1024000, 16000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20},
+
+  // 22.05k
+  {11289600, 22050, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 5644800, 22050, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 2822400, 22050, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 1411200, 22050, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+
+  // 24k
+  {12288000, 24000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  {18432000, 24000, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 6144000, 24000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 3072000, 24000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 1536000, 24000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+
+  // 32k
+  {12288000, 32000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  {18432000, 32000, 0x03, 0x04, 0x03, 0x03, 0x00, 0x02, 0xff, 0x0c, 0x10, 0x10},
+  {16384000, 32000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 8192000, 32000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 6144000, 32000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 4096000, 32000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 3072000, 32000, 0x03, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 2048000, 32000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 1536000, 32000, 0x03, 0x08, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10},
+  { 1024000, 32000, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+
+  // 44.1k
+  {11289600, 44100, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 5644800, 44100, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 2822400, 44100, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 1411200, 44100, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+
+  // 48k
+  {12288000, 48000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  {18432000, 48000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 6144000, 48000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 3072000, 48000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 1536000, 48000, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+
+  // 64k
+  {12288000, 64000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  {18432000, 64000, 0x03, 0x04, 0x03, 0x03, 0x01, 0x01, 0x7f, 0x06, 0x10, 0x10},
+  {16384000, 64000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 8192000, 64000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 6144000, 64000, 0x01, 0x04, 0x03, 0x03, 0x01, 0x01, 0x7f, 0x06, 0x10, 0x10},
+  { 4096000, 64000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 3072000, 64000, 0x01, 0x08, 0x03, 0x03, 0x01, 0x01, 0x7f, 0x06, 0x10, 0x10},
+  { 2048000, 64000, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 1536000, 64000, 0x01, 0x08, 0x01, 0x01, 0x01, 0x00, 0xbf, 0x03, 0x18, 0x18},
+  { 1024000, 64000, 0x01, 0x08, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10},
+
+  // 88.2k
+  {11289600, 88200, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 5644800, 88200, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 2822400, 88200, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 1411200, 88200, 0x01, 0x08, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10},
+
+  // 96k
+  {12288000, 96000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  {18432000, 96000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 6144000, 96000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 3072000, 96000, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
+  { 1536000, 96000, 0x01, 0x08, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10},
+
+    // clang-format on
+};
+
+}  // namespace es8311
+}  // namespace esphome
diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py
index d376907925..fa515a585f 100644
--- a/esphome/components/i2s_audio/__init__.py
+++ b/esphome/components/i2s_audio/__init__.py
@@ -8,7 +8,7 @@ from esphome.components.esp32.const import (
     VARIANT_ESP32S3,
 )
 import esphome.config_validation as cv
-from esphome.const import CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE
+from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE
 from esphome.cpp_generator import MockObjClass
 import esphome.final_validate as fv
 
@@ -25,13 +25,11 @@ CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin"
 CONF_I2S_AUDIO = "i2s_audio"
 CONF_I2S_AUDIO_ID = "i2s_audio_id"
 
-CONF_BITS_PER_SAMPLE = "bits_per_sample"
 CONF_I2S_MODE = "i2s_mode"
 CONF_PRIMARY = "primary"
 CONF_SECONDARY = "secondary"
 
 CONF_USE_APLL = "use_apll"
-CONF_BITS_PER_SAMPLE = "bits_per_sample"
 CONF_BITS_PER_CHANNEL = "bits_per_channel"
 CONF_MONO = "mono"
 CONF_LEFT = "left"
diff --git a/esphome/const.py b/esphome/const.py
index c39061631b..5645c9eaab 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -92,6 +92,7 @@ CONF_BINARY_SENSORS = "binary_sensors"
 CONF_BINDKEY = "bindkey"
 CONF_BIRTH_MESSAGE = "birth_message"
 CONF_BIT_DEPTH = "bit_depth"
+CONF_BITS_PER_SAMPLE = "bits_per_sample"
 CONF_BLOCK = "block"
 CONF_BLUE = "blue"
 CONF_BOARD = "board"
diff --git a/tests/components/es8311/common.yaml b/tests/components/es8311/common.yaml
new file mode 100644
index 0000000000..d833d1c043
--- /dev/null
+++ b/tests/components/es8311/common.yaml
@@ -0,0 +1,15 @@
+esphome:
+  on_boot:
+    then:
+      - audio_dac.mute_off:
+      - audio_dac.mute_on:
+      - audio_dac.set_volume:
+          volume: 50%
+
+i2c:
+  - id: i2c_aic3204
+    scl: ${scl_pin}
+    sda: ${sda_pin}
+
+audio_dac:
+  - platform: es8311
diff --git a/tests/components/es8311/test.esp32-ard.yaml b/tests/components/es8311/test.esp32-ard.yaml
new file mode 100644
index 0000000000..63c3bd6afd
--- /dev/null
+++ b/tests/components/es8311/test.esp32-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO16
+  sda_pin: GPIO17
+
+<<: !include common.yaml
diff --git a/tests/components/es8311/test.esp32-c3-ard.yaml b/tests/components/es8311/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/es8311/test.esp32-c3-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/es8311/test.esp32-c3-idf.yaml b/tests/components/es8311/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/es8311/test.esp32-c3-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/es8311/test.esp32-idf.yaml b/tests/components/es8311/test.esp32-idf.yaml
new file mode 100644
index 0000000000..63c3bd6afd
--- /dev/null
+++ b/tests/components/es8311/test.esp32-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO16
+  sda_pin: GPIO17
+
+<<: !include common.yaml
diff --git a/tests/components/es8311/test.esp8266-ard.yaml b/tests/components/es8311/test.esp8266-ard.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/es8311/test.esp8266-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml

From d3563e4e9782299c7820ad4783a1813a361a9575 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Thu, 31 Oct 2024 06:30:46 +1100
Subject: [PATCH 066/282] [sdl] Allow window to be resized. (#7698)

---
 esphome/components/sdl/sdl_esphome.cpp | 27 +++++++++++++++++++++-----
 esphome/components/sdl/sdl_esphome.h   |  1 +
 2 files changed, 23 insertions(+), 5 deletions(-)

diff --git a/esphome/components/sdl/sdl_esphome.cpp b/esphome/components/sdl/sdl_esphome.cpp
index 5e17ca5650..8f0821a2fa 100644
--- a/esphome/components/sdl/sdl_esphome.cpp
+++ b/esphome/components/sdl/sdl_esphome.cpp
@@ -9,8 +9,9 @@ void Sdl::setup() {
   ESP_LOGD(TAG, "Starting setup");
   SDL_Init(SDL_INIT_VIDEO);
   this->window_ = SDL_CreateWindow(App.get_name().c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
-                                   this->width_, this->height_, 0);
+                                   this->width_, this->height_, SDL_WINDOW_RESIZABLE);
   this->renderer_ = SDL_CreateRenderer(this->window_, -1, SDL_RENDERER_SOFTWARE);
+  SDL_RenderSetLogicalSize(this->renderer_, this->width_, this->height_);
   this->texture_ =
       SDL_CreateTexture(this->renderer_, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STATIC, this->width_, this->height_);
   SDL_SetTextureBlendMode(this->texture_, SDL_BLENDMODE_BLEND);
@@ -25,6 +26,10 @@ void Sdl::update() {
   this->y_low_ = this->height_;
   this->x_high_ = 0;
   this->y_high_ = 0;
+  this->redraw_(rect);
+}
+
+void Sdl::redraw_(SDL_Rect &rect) {
   SDL_RenderCopy(this->renderer_, this->texture_, &rect, &rect);
   SDL_RenderPresent(this->renderer_);
 }
@@ -33,15 +38,13 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *
                          display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
   SDL_Rect rect{x_start, y_start, w, h};
   if (this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || bitness != display::COLOR_BITNESS_565 || big_endian) {
-    display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset,
-                                     x_pad);
+    Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
   } else {
     auto stride = x_offset + w + x_pad;
     auto data = ptr + (stride * y_offset + x_offset) * 2;
     SDL_UpdateTexture(this->texture_, &rect, data, stride * 2);
   }
-  SDL_RenderCopy(this->renderer_, this->texture_, &rect, &rect);
-  SDL_RenderPresent(this->renderer_);
+  this->redraw_(rect);
 }
 
 void Sdl::draw_pixel_at(int x, int y, Color color) {
@@ -84,6 +87,20 @@ void Sdl::loop() {
         }
         break;
 
+      case SDL_WINDOWEVENT:
+        switch (e.window.event) {
+          case SDL_WINDOWEVENT_SIZE_CHANGED:
+          case SDL_WINDOWEVENT_EXPOSED:
+          case SDL_WINDOWEVENT_RESIZED: {
+            SDL_Rect rect{0, 0, this->width_, this->height_};
+            this->redraw_(rect);
+            break;
+          }
+          default:
+            break;
+        }
+        break;
+
       default:
         ESP_LOGV(TAG, "Event %d", e.type);
         break;
diff --git a/esphome/components/sdl/sdl_esphome.h b/esphome/components/sdl/sdl_esphome.h
index e4b2d9dd9f..4b0e59c9fe 100644
--- a/esphome/components/sdl/sdl_esphome.h
+++ b/esphome/components/sdl/sdl_esphome.h
@@ -38,6 +38,7 @@ class Sdl : public display::Display {
  protected:
   int get_width_internal() override { return this->width_; }
   int get_height_internal() override { return this->height_; }
+  void redraw_(SDL_Rect &rect);
   int width_{};
   int height_{};
   SDL_Renderer *renderer_{};

From e85157db4b246fd3c701c7b4195d4f52b735c554 Mon Sep 17 00:00:00 2001
From: Jason Nagin <33561705+JasonN3@users.noreply.github.com>
Date: Wed, 30 Oct 2024 15:34:33 -0400
Subject: [PATCH 067/282] Add config for current temperature precision (#7699)

---
 esphome/components/mqtt/mqtt_climate.cpp | 6 ++++--
 esphome/components/mqtt/mqtt_const.h     | 4 ++++
 tests/components/mqtt/common.yaml        | 4 ++++
 3 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp
index 49a8f06734..773d863835 100644
--- a/esphome/components/mqtt/mqtt_climate.cpp
+++ b/esphome/components/mqtt/mqtt_climate.cpp
@@ -71,8 +71,10 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
   root[MQTT_MIN_TEMP] = traits.get_visual_min_temperature();
   // max_temp
   root[MQTT_MAX_TEMP] = traits.get_visual_max_temperature();
-  // temp_step
-  root["temp_step"] = traits.get_visual_target_temperature_step();
+  // target_temp_step
+  root[MQTT_TARGET_TEMPERATURE_STEP] = traits.get_visual_target_temperature_step();
+  // current_temp_step
+  root[MQTT_CURRENT_TEMPERATURE_STEP] = traits.get_visual_current_temperature_step();
   // temperature units are always coerced to Celsius internally
   root[MQTT_TEMPERATURE_UNIT] = "C";
 
diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h
index c1c40c4b6d..445457a27f 100644
--- a/esphome/components/mqtt/mqtt_const.h
+++ b/esphome/components/mqtt/mqtt_const.h
@@ -51,6 +51,7 @@ constexpr const char *const MQTT_COMMAND_TOPIC = "cmd_t";
 constexpr const char *const MQTT_CONFIGURATION_URL = "cu";
 constexpr const char *const MQTT_CURRENT_HUMIDITY_TEMPLATE = "curr_hum_tpl";
 constexpr const char *const MQTT_CURRENT_HUMIDITY_TOPIC = "curr_hum_t";
+constexpr const char *const MQTT_CURRENT_TEMPERATURE_STEP = "precision";
 constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "curr_temp_tpl";
 constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "curr_temp_t";
 constexpr const char *const MQTT_DEVICE = "dev";
@@ -232,6 +233,7 @@ constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TEMPLATE = "hum_cmd_tpl
 constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TOPIC = "hum_cmd_t";
 constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TEMPLATE = "hum_state_tpl";
 constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TOPIC = "hum_stat_t";
+constexpr const char *const MQTT_TARGET_TEMPERATURE_STEP = "temp_step";
 constexpr const char *const MQTT_TEMPERATURE_COMMAND_TEMPLATE = "temp_cmd_tpl";
 constexpr const char *const MQTT_TEMPERATURE_COMMAND_TOPIC = "temp_cmd_t";
 constexpr const char *const MQTT_TEMPERATURE_HIGH_COMMAND_TEMPLATE = "temp_hi_cmd_tpl";
@@ -313,6 +315,7 @@ constexpr const char *const MQTT_COMMAND_TOPIC = "command_topic";
 constexpr const char *const MQTT_CONFIGURATION_URL = "configuration_url";
 constexpr const char *const MQTT_CURRENT_HUMIDITY_TEMPLATE = "current_humidity_template";
 constexpr const char *const MQTT_CURRENT_HUMIDITY_TOPIC = "current_humidity_topic";
+constexpr const char *const MQTT_CURRENT_TEMPERATURE_STEP = "precision";
 constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "current_temperature_template";
 constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "current_temperature_topic";
 constexpr const char *const MQTT_DEVICE = "device";
@@ -494,6 +497,7 @@ constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TEMPLATE = "target_humi
 constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic";
 constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template";
 constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic";
+constexpr const char *const MQTT_TARGET_TEMPERATURE_STEP = "temp_step";
 constexpr const char *const MQTT_TEMPERATURE_COMMAND_TEMPLATE = "temperature_command_template";
 constexpr const char *const MQTT_TEMPERATURE_COMMAND_TOPIC = "temperature_command_topic";
 constexpr const char *const MQTT_TEMPERATURE_HIGH_COMMAND_TEMPLATE = "temperature_high_command_template";
diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml
index 5ed6335d65..e154be8b5c 100644
--- a/tests/components/mqtt/common.yaml
+++ b/tests/components/mqtt/common.yaml
@@ -200,6 +200,10 @@ climate:
     fan_only_cooling: true
     fan_with_cooling: true
     fan_with_heating: true
+    visual:
+      temperature_step:
+        target_temperature: 0.1
+        current_temperature: 0.1
 
 cover:
   - platform: template

From 5a2fed35693f7191f81fe31236e69c2132c23d99 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Thu, 31 Oct 2024 09:28:18 +1100
Subject: [PATCH 068/282] [spi] Add mosi pin checks for displays (#7702)

---
 esphome/components/ili9xxx/display.py         |  4 ++++
 esphome/components/pcd8544/display.py         | 12 +++++++----
 esphome/components/ssd1306_spi/display.py     |  8 +++++--
 esphome/components/ssd1322_spi/display.py     |  8 +++++--
 esphome/components/ssd1325_spi/display.py     |  8 +++++--
 esphome/components/ssd1327_spi/display.py     |  8 +++++--
 esphome/components/ssd1331_spi/display.py     |  8 +++++--
 esphome/components/ssd1351_spi/display.py     |  8 +++++--
 esphome/components/st7567_spi/display.py      |  8 +++++--
 esphome/components/st7701s/display.py         |  4 ++++
 esphome/components/st7735/display.py          | 17 +++++++++------
 esphome/components/st7789v/display.py         | 21 ++++++++++++-------
 .../components/waveshare_epaper/display.py    |  8 +++++--
 13 files changed, 88 insertions(+), 34 deletions(-)

diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py
index 68e3aa953d..739ad07843 100644
--- a/esphome/components/ili9xxx/display.py
+++ b/esphome/components/ili9xxx/display.py
@@ -196,6 +196,10 @@ CONFIG_SCHEMA = cv.All(
     _validate,
 )
 
+FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
+    "ili9xxx", require_miso=False, require_mosi=True
+)
+
 
 async def to_code(config):
     rhs = MODELS[config[CONF_MODEL]].new()
diff --git a/esphome/components/pcd8544/display.py b/esphome/components/pcd8544/display.py
index d7e72d1c81..2c24b133da 100644
--- a/esphome/components/pcd8544/display.py
+++ b/esphome/components/pcd8544/display.py
@@ -1,15 +1,15 @@
-import esphome.codegen as cg
-import esphome.config_validation as cv
 from esphome import pins
+import esphome.codegen as cg
 from esphome.components import display, spi
+import esphome.config_validation as cv
 from esphome.const import (
+    CONF_CONTRAST,
+    CONF_CS_PIN,
     CONF_DC_PIN,
     CONF_ID,
     CONF_LAMBDA,
     CONF_PAGES,
     CONF_RESET_PIN,
-    CONF_CS_PIN,
-    CONF_CONTRAST,
 )
 
 DEPENDENCIES = ["spi"]
@@ -35,6 +35,10 @@ CONFIG_SCHEMA = cv.All(
     cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
 )
 
+FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
+    "pcd8544", require_miso=False, require_mosi=True
+)
+
 
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
diff --git a/esphome/components/ssd1306_spi/display.py b/esphome/components/ssd1306_spi/display.py
index 0af1168bde..4af41073d4 100644
--- a/esphome/components/ssd1306_spi/display.py
+++ b/esphome/components/ssd1306_spi/display.py
@@ -1,8 +1,8 @@
-import esphome.codegen as cg
-import esphome.config_validation as cv
 from esphome import pins
+import esphome.codegen as cg
 from esphome.components import spi, ssd1306_base
 from esphome.components.ssd1306_base import _validate
+import esphome.config_validation as cv
 from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES
 
 AUTO_LOAD = ["ssd1306_base"]
@@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All(
     _validate,
 )
 
+FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
+    "ssd1306_spi", require_miso=False, require_mosi=True
+)
+
 
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
diff --git a/esphome/components/ssd1322_spi/display.py b/esphome/components/ssd1322_spi/display.py
index 88b3a53355..849e71abee 100644
--- a/esphome/components/ssd1322_spi/display.py
+++ b/esphome/components/ssd1322_spi/display.py
@@ -1,7 +1,7 @@
-import esphome.codegen as cg
-import esphome.config_validation as cv
 from esphome import pins
+import esphome.codegen as cg
 from esphome.components import spi, ssd1322_base
+import esphome.config_validation as cv
 from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES
 
 CODEOWNERS = ["@kbx81"]
@@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All(
     cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
 )
 
+FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
+    "ssd1322_spi", require_miso=False, require_mosi=True
+)
+
 
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
diff --git a/esphome/components/ssd1325_spi/display.py b/esphome/components/ssd1325_spi/display.py
index a86dc751d5..e18db33c68 100644
--- a/esphome/components/ssd1325_spi/display.py
+++ b/esphome/components/ssd1325_spi/display.py
@@ -1,7 +1,7 @@
-import esphome.codegen as cg
-import esphome.config_validation as cv
 from esphome import pins
+import esphome.codegen as cg
 from esphome.components import spi, ssd1325_base
+import esphome.config_validation as cv
 from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES
 
 CODEOWNERS = ["@kbx81"]
@@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All(
     cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
 )
 
+FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
+    "ssd1325_spi", require_miso=False, require_mosi=True
+)
+
 
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
diff --git a/esphome/components/ssd1327_spi/display.py b/esphome/components/ssd1327_spi/display.py
index 138e85eecd..b622c098ec 100644
--- a/esphome/components/ssd1327_spi/display.py
+++ b/esphome/components/ssd1327_spi/display.py
@@ -1,7 +1,7 @@
-import esphome.codegen as cg
-import esphome.config_validation as cv
 from esphome import pins
+import esphome.codegen as cg
 from esphome.components import spi, ssd1327_base
+import esphome.config_validation as cv
 from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES
 
 CODEOWNERS = ["@kbx81"]
@@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All(
     cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
 )
 
+FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
+    "ssd1327_spi", require_miso=False, require_mosi=True
+)
+
 
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
diff --git a/esphome/components/ssd1331_spi/display.py b/esphome/components/ssd1331_spi/display.py
index c32ac60578..50895b3175 100644
--- a/esphome/components/ssd1331_spi/display.py
+++ b/esphome/components/ssd1331_spi/display.py
@@ -1,7 +1,7 @@
-import esphome.codegen as cg
-import esphome.config_validation as cv
 from esphome import pins
+import esphome.codegen as cg
 from esphome.components import spi, ssd1331_base
+import esphome.config_validation as cv
 from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES
 
 CODEOWNERS = ["@kbx81"]
@@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All(
     cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
 )
 
+FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
+    "ssd1331_spi", require_miso=False, require_mosi=True
+)
+
 
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
diff --git a/esphome/components/ssd1351_spi/display.py b/esphome/components/ssd1351_spi/display.py
index 3f3409226c..bd7033c3d4 100644
--- a/esphome/components/ssd1351_spi/display.py
+++ b/esphome/components/ssd1351_spi/display.py
@@ -1,7 +1,7 @@
-import esphome.codegen as cg
-import esphome.config_validation as cv
 from esphome import pins
+import esphome.codegen as cg
 from esphome.components import spi, ssd1351_base
+import esphome.config_validation as cv
 from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES
 
 CODEOWNERS = ["@kbx81"]
@@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All(
     cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
 )
 
+FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
+    "ssd1351_spi", require_miso=False, require_mosi=True
+)
+
 
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
diff --git a/esphome/components/st7567_spi/display.py b/esphome/components/st7567_spi/display.py
index aabe02a2d8..305aa35024 100644
--- a/esphome/components/st7567_spi/display.py
+++ b/esphome/components/st7567_spi/display.py
@@ -1,7 +1,7 @@
-import esphome.codegen as cg
-import esphome.config_validation as cv
 from esphome import pins
+import esphome.codegen as cg
 from esphome.components import spi, st7567_base
+import esphome.config_validation as cv
 from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES
 
 CODEOWNERS = ["@latonita"]
@@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All(
     cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
 )
 
+FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
+    "st7567_spi", require_miso=False, require_mosi=True
+)
+
 
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
diff --git a/esphome/components/st7701s/display.py b/esphome/components/st7701s/display.py
index e73c2467da..c6ad43c14c 100644
--- a/esphome/components/st7701s/display.py
+++ b/esphome/components/st7701s/display.py
@@ -167,6 +167,10 @@ CONFIG_SCHEMA = cv.All(
     cv.only_with_esp_idf,
 )
 
+FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
+    "st7701s", require_miso=False, require_mosi=True
+)
+
 
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
diff --git a/esphome/components/st7735/display.py b/esphome/components/st7735/display.py
index d5bb2fa3d6..2761214315 100644
--- a/esphome/components/st7735/display.py
+++ b/esphome/components/st7735/display.py
@@ -1,17 +1,17 @@
-import esphome.codegen as cg
-import esphome.config_validation as cv
 from esphome import pins
-from esphome.components import spi
-from esphome.components import display
+import esphome.codegen as cg
+from esphome.components import display, spi
+import esphome.config_validation as cv
 from esphome.const import (
     CONF_DC_PIN,
     CONF_ID,
+    CONF_INVERT_COLORS,
     CONF_LAMBDA,
     CONF_MODEL,
-    CONF_RESET_PIN,
     CONF_PAGES,
-    CONF_INVERT_COLORS,
+    CONF_RESET_PIN,
 )
+
 from . import st7735_ns
 
 CODEOWNERS = ["@SenexCrenshaw"]
@@ -68,6 +68,11 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
+FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
+    "st7735", require_miso=False, require_mosi=True
+)
+
+
 async def setup_st7735(var, config):
     await display.register_display(var, config)
 
diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py
index 04dce2cf6c..8259eacf2d 100644
--- a/esphome/components/st7789v/display.py
+++ b/esphome/components/st7789v/display.py
@@ -1,22 +1,23 @@
-import esphome.codegen as cg
-import esphome.config_validation as cv
 from esphome import pins
-from esphome.components import display, spi, power_supply
+import esphome.codegen as cg
+from esphome.components import display, power_supply, spi
+import esphome.config_validation as cv
 from esphome.const import (
     CONF_BACKLIGHT_PIN,
+    CONF_CS_PIN,
     CONF_DC_PIN,
     CONF_HEIGHT,
     CONF_ID,
     CONF_LAMBDA,
     CONF_MODEL,
-    CONF_RESET_PIN,
-    CONF_WIDTH,
-    CONF_POWER_SUPPLY,
-    CONF_ROTATION,
-    CONF_CS_PIN,
     CONF_OFFSET_HEIGHT,
     CONF_OFFSET_WIDTH,
+    CONF_POWER_SUPPLY,
+    CONF_RESET_PIN,
+    CONF_ROTATION,
+    CONF_WIDTH,
 )
+
 from . import st7789v_ns
 
 CONF_EIGHTBITCOLOR = "eightbitcolor"
@@ -168,6 +169,10 @@ CONFIG_SCHEMA = cv.All(
     validate_st7789v,
 )
 
+FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
+    "st7789v", require_miso=False, require_mosi=True
+)
+
 
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py
index 4d3965449f..8287788de5 100644
--- a/esphome/components/waveshare_epaper/display.py
+++ b/esphome/components/waveshare_epaper/display.py
@@ -1,7 +1,7 @@
-import esphome.codegen as cg
-import esphome.config_validation as cv
 from esphome import core, pins
+import esphome.codegen as cg
 from esphome.components import display, spi
+import esphome.config_validation as cv
 from esphome.const import (
     CONF_BUSY_PIN,
     CONF_DC_PIN,
@@ -187,6 +187,10 @@ CONFIG_SCHEMA = cv.All(
     cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
 )
 
+FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
+    "waveshare_epaper", require_miso=False, require_mosi=True
+)
+
 
 async def to_code(config):
     model_type, model = MODELS[config[CONF_MODEL]]

From 74ea1b60e35fa351b5a5fc380dfdc1879d22f043 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Thu, 31 Oct 2024 11:37:54 +1300
Subject: [PATCH 069/282] [CI] Fix webserver defines to be present based on
 platform, not just framework (#7703)

---
 esphome/core/defines.h | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/esphome/core/defines.h b/esphome/core/defines.h
index b5511b57eb..3798ddba6a 100644
--- a/esphome/core/defines.h
+++ b/esphome/core/defines.h
@@ -86,8 +86,6 @@
 #ifdef USE_ARDUINO
 #define USE_CAPTIVE_PORTAL
 #define USE_PROMETHEUS
-#define USE_WEBSERVER
-#define USE_WEBSERVER_PORT 80  // NOLINT
 #define USE_WIFI_WPA2_EAP
 #endif
 
@@ -111,6 +109,8 @@
 #define USE_SPEAKER
 #define USE_SPI
 #define USE_VOICE_ASSISTANT
+#define USE_WEBSERVER
+#define USE_WEBSERVER_PORT 80  // NOLINT
 #define USE_WIFI_11KV_SUPPORT
 
 #ifdef USE_ARDUINO
@@ -147,6 +147,8 @@
 #define USE_SHD_FIRMWARE_DATA \
   {}
 
+#define USE_WEBSERVER
+#define USE_WEBSERVER_PORT 80  // NOLINT
 #endif
 
 #ifdef USE_RP2040
@@ -158,6 +160,8 @@
 
 #ifdef USE_LIBRETINY
 #define USE_SOCKET_IMPL_LWIP_SOCKETS
+#define USE_WEBSERVER
+#define USE_WEBSERVER_PORT 80  // NOLINT
 #endif
 
 #ifdef USE_HOST

From 8b7e061f3ac5427dcc931feffdd0251100945a57 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Thu, 31 Oct 2024 13:15:39 +1100
Subject: [PATCH 070/282] [touchscreen] Calibration fixes (#7704)

---
 esphome/components/touchscreen/__init__.py    | 97 +++++++++----------
 esphome/components/touchscreen/touchscreen.h  | 12 +--
 .../xpt2046/touchscreen/__init__.py           | 39 ++------
 tests/components/xpt2046/test.esp32-ard.yaml  |  4 +-
 .../components/xpt2046/test.esp32-c3-ard.yaml |  2 +-
 .../components/xpt2046/test.esp32-c3-idf.yaml |  2 +-
 tests/components/xpt2046/test.esp32-idf.yaml  |  2 +-
 .../components/xpt2046/test.esp8266-ard.yaml  |  2 +-
 tests/components/xpt2046/test.rp2040-ard.yaml |  4 +-
 9 files changed, 63 insertions(+), 101 deletions(-)

diff --git a/esphome/components/touchscreen/__init__.py b/esphome/components/touchscreen/__init__.py
index b2d3f60d2b..01a271a34e 100644
--- a/esphome/components/touchscreen/__init__.py
+++ b/esphome/components/touchscreen/__init__.py
@@ -1,21 +1,18 @@
-import esphome.config_validation as cv
-import esphome.codegen as cg
-
-from esphome.components import display
 from esphome import automation
-
+import esphome.codegen as cg
+from esphome.components import display
+import esphome.config_validation as cv
 from esphome.const import (
+    CONF_CALIBRATION,
     CONF_DISPLAY,
-    CONF_ON_TOUCH,
-    CONF_ON_RELEASE,
-    CONF_ON_UPDATE,
-    CONF_SWAP_XY,
     CONF_MIRROR_X,
     CONF_MIRROR_Y,
+    CONF_ON_RELEASE,
+    CONF_ON_TOUCH,
+    CONF_ON_UPDATE,
+    CONF_SWAP_XY,
     CONF_TRANSFORM,
-    CONF_CALIBRATION,
 )
-
 from esphome.core import coroutine_with_priority
 
 CODEOWNERS = ["@jesserockz", "@nielsnl68"]
@@ -43,51 +40,45 @@ CONF_Y_MIN = "y_min"
 CONF_Y_MAX = "y_max"
 
 
-def validate_calibration(config):
-    if CONF_CALIBRATION in config:
-        calibration_config = config[CONF_CALIBRATION]
-        if (
-            cv.int_([CONF_X_MIN]) != 0
-            and cv.int_(calibration_config[CONF_X_MAX]) != 0
-            and abs(
-                cv.int_(calibration_config[CONF_X_MIN])
-                - cv.int_(calibration_config[CONF_X_MAX])
-            )
-            < 10
-        ):
-            raise cv.Invalid("Calibration X values difference must be more than 10")
-
-        if (
-            cv.int_(calibration_config[CONF_Y_MIN]) != 0
-            and cv.int_(calibration_config[CONF_Y_MAX]) != 0
-            and abs(
-                cv.int_(calibration_config[CONF_Y_MIN])
-                - cv.int_(calibration_config[CONF_Y_MAX])
-            )
-            < 10
-        ):
-            raise cv.Invalid("Calibration Y values difference must be more than 10")
-
-    return config
+def validate_calibration(calibration_config):
+    x_min = calibration_config[CONF_X_MIN]
+    x_max = calibration_config[CONF_X_MAX]
+    y_min = calibration_config[CONF_Y_MIN]
+    y_max = calibration_config[CONF_Y_MAX]
+    if x_max < x_min:
+        raise cv.Invalid(
+            "x_min must be smaller than x_max. To mirror the direction use the 'transform' options"
+        )
+    if y_max < y_min:
+        raise cv.Invalid(
+            "y_min must be smaller than y_max. To mirror the direction use the 'transform' options"
+        )
+    x_delta = x_max - x_min
+    y_delta = y_max - y_min
+    if x_delta < 10 or y_delta < 10:
+        raise cv.Invalid("Calibration value range must be greater than 10")
+    return calibration_config
 
 
-def calibration_schema(default_max_values):
-    return cv.Schema(
+CALIBRATION_SCHEMA = cv.All(
+    cv.Schema(
         {
-            cv.Optional(CONF_X_MIN, default=0): cv.int_range(min=0, max=4095),
-            cv.Optional(CONF_X_MAX, default=default_max_values): cv.int_range(
-                min=0, max=4095
-            ),
-            cv.Optional(CONF_Y_MIN, default=0): cv.int_range(min=0, max=4095),
-            cv.Optional(CONF_Y_MAX, default=default_max_values): cv.int_range(
-                min=0, max=4095
-            ),
-        },
-        validate_calibration,
+            cv.Required(CONF_X_MIN): cv.int_range(min=0, max=4095),
+            cv.Required(CONF_X_MAX): cv.int_range(min=0, max=4095),
+            cv.Required(CONF_Y_MIN): cv.int_range(min=0, max=4095),
+            cv.Required(CONF_Y_MAX): cv.int_range(min=0, max=4095),
+        }
+    ),
+    validate_calibration,
+)
+
+
+def touchscreen_schema(default_touch_timeout=cv.UNDEFINED, calibration_required=False):
+    calibration = (
+        cv.Required(CONF_CALIBRATION)
+        if calibration_required
+        else cv.Optional(CONF_CALIBRATION)
     )
-
-
-def touchscreen_schema(default_touch_timeout):
     return cv.Schema(
         {
             cv.GenerateID(CONF_DISPLAY): cv.use_id(display.Display),
@@ -102,7 +93,7 @@ def touchscreen_schema(default_touch_timeout):
                 cv.positive_time_period_milliseconds,
                 cv.Range(max=cv.TimePeriod(milliseconds=65535)),
             ),
-            cv.Optional(CONF_CALIBRATION): calibration_schema(0),
+            calibration: CALIBRATION_SCHEMA,
             cv.Optional(CONF_ON_TOUCH): automation.validate_automation(single=True),
             cv.Optional(CONF_ON_UPDATE): automation.validate_automation(single=True),
             cv.Optional(CONF_ON_RELEASE): automation.validate_automation(single=True),
diff --git a/esphome/components/touchscreen/touchscreen.h b/esphome/components/touchscreen/touchscreen.h
index 21111f87b3..8016323d49 100644
--- a/esphome/components/touchscreen/touchscreen.h
+++ b/esphome/components/touchscreen/touchscreen.h
@@ -53,14 +53,10 @@ class Touchscreen : public PollingComponent {
   void set_swap_xy(bool swap) { this->swap_x_y_ = swap; }
 
   void set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) {
-    this->x_raw_min_ = std::min(x_min, x_max);
-    this->x_raw_max_ = std::max(x_min, x_max);
-    this->y_raw_min_ = std::min(y_min, y_max);
-    this->y_raw_max_ = std::max(y_min, y_max);
-    if (x_min > x_max)
-      this->invert_x_ = true;
-    if (y_min > y_max)
-      this->invert_y_ = true;
+    this->x_raw_min_ = x_min;
+    this->x_raw_max_ = x_max;
+    this->y_raw_min_ = y_min;
+    this->y_raw_max_ = y_max;
   }
 
   Trigger<TouchPoint, const TouchPoints_t &> *get_touch_trigger() { return &this->touch_trigger_; }
diff --git a/esphome/components/xpt2046/touchscreen/__init__.py b/esphome/components/xpt2046/touchscreen/__init__.py
index d45f309a3b..d91ae44789 100644
--- a/esphome/components/xpt2046/touchscreen/__init__.py
+++ b/esphome/components/xpt2046/touchscreen/__init__.py
@@ -1,9 +1,8 @@
-import esphome.codegen as cg
-import esphome.config_validation as cv
-
 from esphome import pins
+import esphome.codegen as cg
 from esphome.components import spi, touchscreen
-from esphome.const import CONF_ID, CONF_THRESHOLD, CONF_INTERRUPT_PIN
+import esphome.config_validation as cv
+from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_THRESHOLD
 
 CODEOWNERS = ["@numo68", "@nielsnl68"]
 DEPENDENCIES = ["spi"]
@@ -15,13 +14,9 @@ XPT2046Component = XPT2046_ns.class_(
     spi.SPIDevice,
 )
 
-CONF_CALIBRATION_X_MIN = "calibration_x_min"
-CONF_CALIBRATION_X_MAX = "calibration_x_max"
-CONF_CALIBRATION_Y_MIN = "calibration_y_min"
-CONF_CALIBRATION_Y_MAX = "calibration_y_max"
-
 CONFIG_SCHEMA = cv.All(
-    touchscreen.TOUCHSCREEN_SCHEMA.extend(
+    touchscreen.touchscreen_schema(calibration_required=True)
+    .extend(
         cv.Schema(
             {
                 cv.GenerateID(): cv.declare_id(XPT2046Component),
@@ -29,30 +24,10 @@ CONFIG_SCHEMA = cv.All(
                     pins.internal_gpio_input_pin_schema
                 ),
                 cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095),
-                cv.Optional(
-                    touchscreen.CONF_CALIBRATION
-                ): touchscreen.calibration_schema(4095),
-                cv.Optional(CONF_CALIBRATION_X_MIN): cv.invalid(
-                    "Deprecated: use the new 'calibration' configuration variable"
-                ),
-                cv.Optional(CONF_CALIBRATION_X_MAX): cv.invalid(
-                    "Deprecated: use the new 'calibration' configuration variable"
-                ),
-                cv.Optional(CONF_CALIBRATION_Y_MIN): cv.invalid(
-                    "Deprecated: use the new 'calibration' configuration variable"
-                ),
-                cv.Optional(CONF_CALIBRATION_Y_MAX): cv.invalid(
-                    "Deprecated: use the new 'calibration' configuration variable"
-                ),
-                cv.Optional(CONF_CALIBRATION_Y_MAX): cv.invalid(
-                    "Deprecated: use the new 'calibration' configuration variable"
-                ),
-                cv.Optional("report_interval"): cv.invalid(
-                    "Deprecated: use the 'update_interval' configuration variable"
-                ),
             },
         )
-    ).extend(spi.spi_device_schema()),
+    )
+    .extend(spi.spi_device_schema()),
 )
 
 
diff --git a/tests/components/xpt2046/test.esp32-ard.yaml b/tests/components/xpt2046/test.esp32-ard.yaml
index f15d1f9b41..9e305791e0 100644
--- a/tests/components/xpt2046/test.esp32-ard.yaml
+++ b/tests/components/xpt2046/test.esp32-ard.yaml
@@ -25,8 +25,8 @@ touchscreen:
     update_interval: 50ms
     threshold: 400
     calibration:
-      x_min: 3860
-      x_max: 280
+      x_min: 280
+      x_max: 3860
       y_min: 340
       y_max: 3860
     on_touch:
diff --git a/tests/components/xpt2046/test.esp32-c3-ard.yaml b/tests/components/xpt2046/test.esp32-c3-ard.yaml
index ef4daa800d..c03fd6b345 100644
--- a/tests/components/xpt2046/test.esp32-c3-ard.yaml
+++ b/tests/components/xpt2046/test.esp32-c3-ard.yaml
@@ -25,7 +25,7 @@ touchscreen:
     update_interval: 50ms
     threshold: 400
     calibration:
-      x_min: 3860
+      x_min: 28
       x_max: 280
       y_min: 340
       y_max: 3860
diff --git a/tests/components/xpt2046/test.esp32-c3-idf.yaml b/tests/components/xpt2046/test.esp32-c3-idf.yaml
index ef4daa800d..787ca9b1ed 100644
--- a/tests/components/xpt2046/test.esp32-c3-idf.yaml
+++ b/tests/components/xpt2046/test.esp32-c3-idf.yaml
@@ -25,7 +25,7 @@ touchscreen:
     update_interval: 50ms
     threshold: 400
     calibration:
-      x_min: 3860
+      x_min: 50
       x_max: 280
       y_min: 340
       y_max: 3860
diff --git a/tests/components/xpt2046/test.esp32-idf.yaml b/tests/components/xpt2046/test.esp32-idf.yaml
index f15d1f9b41..e79997146b 100644
--- a/tests/components/xpt2046/test.esp32-idf.yaml
+++ b/tests/components/xpt2046/test.esp32-idf.yaml
@@ -25,7 +25,7 @@ touchscreen:
     update_interval: 50ms
     threshold: 400
     calibration:
-      x_min: 3860
+      x_min: 50
       x_max: 280
       y_min: 340
       y_max: 3860
diff --git a/tests/components/xpt2046/test.esp8266-ard.yaml b/tests/components/xpt2046/test.esp8266-ard.yaml
index 0daa25ad60..ab71f7b8bc 100644
--- a/tests/components/xpt2046/test.esp8266-ard.yaml
+++ b/tests/components/xpt2046/test.esp8266-ard.yaml
@@ -25,7 +25,7 @@ touchscreen:
     update_interval: 50ms
     threshold: 400
     calibration:
-      x_min: 3860
+      x_min: 50
       x_max: 280
       y_min: 340
       y_max: 3860
diff --git a/tests/components/xpt2046/test.rp2040-ard.yaml b/tests/components/xpt2046/test.rp2040-ard.yaml
index 8afc45d04d..622e69ac98 100644
--- a/tests/components/xpt2046/test.rp2040-ard.yaml
+++ b/tests/components/xpt2046/test.rp2040-ard.yaml
@@ -25,8 +25,8 @@ touchscreen:
     update_interval: 50ms
     threshold: 400
     calibration:
-      x_min: 3860
-      x_max: 280
+      x_min: 280
+      x_max: 3860
       y_min: 340
       y_max: 3860
     on_touch:

From a043022444609d11b2a8b040ae9515e54ce940f2 Mon Sep 17 00:00:00 2001
From: Faidon Liambotis <paravoid@debian.org>
Date: Thu, 31 Oct 2024 05:36:23 +0200
Subject: [PATCH 071/282] [font] Add support for "glyphsets" (#7429)

Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
---
 docker/Dockerfile                        |   70 +-
 esphome/components/font/__init__.py      |  303 +-
 requirements.txt                         |    3 +
 requirements_optional.txt                |    1 -
 script/ci-custom.py                      |    2 +-
 tests/components/font/.gitattributes     |    2 +
 tests/components/font/MatrixChunky8X.bdf | 7461 ++++++++++++++++++++++
 tests/components/font/common.yaml        |   24 +
 tests/components/font/test.host.yaml     |   34 +
 tests/components/font/x11.pcf            |  Bin 0 -> 13368 bytes
 10 files changed, 7771 insertions(+), 129 deletions(-)
 create mode 100644 tests/components/font/.gitattributes
 create mode 100644 tests/components/font/MatrixChunky8X.bdf
 create mode 100644 tests/components/font/x11.pcf

diff --git a/docker/Dockerfile b/docker/Dockerfile
index 52a4794f24..44ee879a12 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -40,25 +40,6 @@ RUN \
         libcairo2=1.16.0-7 \
         libmagic1=1:5.44-3 \
         patch=2.7.6-7 \
-    && ( \
-        ( \
-            [ "$TARGETARCH$TARGETVARIANT" = "armv7" ] && \
-                apt-get install -y --no-install-recommends \
-                build-essential=12.9 \
-                python3-dev=3.11.2-1+b1 \
-                zlib1g-dev=1:1.2.13.dfsg-1 \
-                libjpeg-dev=1:2.1.5-2 \
-                libfreetype-dev=2.12.1+dfsg-5+deb12u3 \
-                libssl-dev=3.0.14-1~deb12u2 \
-                libffi-dev=3.4.4-1 \
-                libopenjp2-7=2.5.0-2 \
-                libtiff6=4.5.0-6+deb12u1 \
-                cargo=0.66.0+ds1-1 \
-                pkg-config=1.8.1-1 \
-                gcc-arm-linux-gnueabihf=4:12.2.0-3 \
-        ) \
-        || [ "$TARGETARCH$TARGETVARIANT" != "armv7" ] \
-    ) \
     && rm -rf \
         /tmp/* \
         /var/{cache,log}/* \
@@ -97,15 +78,48 @@ RUN \
 # tmpfs is for https://github.com/rust-lang/cargo/issues/8719
 
 COPY requirements.txt requirements_optional.txt /
-RUN --mount=type=tmpfs,target=/root/.cargo if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
-        curl -L https://www.piwheels.org/cp311/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl -o /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl \
-        && pip3 install --break-system-packages --no-cache-dir /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl \
-        && rm /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl \
-        && export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
-    fi; \
-    CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo \
-    pip3 install \
-    --break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt
+RUN --mount=type=tmpfs,target=/root/.cargo <<END-OF-RUN
+# Fail on any non-zero status
+set -e
+
+if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]
+then
+    curl -L https://www.piwheels.org/cp311/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl -o /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl
+    pip3 install --break-system-packages --no-cache-dir /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl
+    rm /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl
+    export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple";
+fi
+
+# install build tools in case wheels are not available
+BUILD_DEPS="
+    build-essential=12.9
+    python3-dev=3.11.2-1+b1
+    zlib1g-dev=1:1.2.13.dfsg-1
+    libjpeg-dev=1:2.1.5-2
+    libfreetype-dev=2.12.1+dfsg-5+deb12u3
+    libssl-dev=3.0.14-1~deb12u2
+    libffi-dev=3.4.4-1
+    libopenjp2-7=2.5.0-2
+    libtiff6=4.5.0-6+deb12u1
+    cargo=0.66.0+ds1-1
+    pkg-config=1.8.1-1
+"
+if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ] || [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]
+then
+    apt-get update
+    apt-get install -y --no-install-recommends $BUILD_DEPS
+fi
+
+CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo
+pip3 install --break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt
+
+if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ] || [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]
+then
+    apt-get remove -y --purge --auto-remove $BUILD_DEPS
+    rm -rf /tmp/* /var/{cache,log}/* /var/lib/apt/lists/*
+fi
+END-OF-RUN
+
 
 COPY script/platformio_install_deps.py platformio.ini /
 RUN /platformio_install_deps.py /platformio.ini --libraries
diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py
index a3f11df50e..6fd2d7c310 100644
--- a/esphome/components/font/__init__.py
+++ b/esphome/components/font/__init__.py
@@ -1,3 +1,4 @@
+from collections.abc import Iterable
 import functools
 import hashlib
 import logging
@@ -5,6 +6,8 @@ import os
 from pathlib import Path
 import re
 
+import freetype
+import glyphsets
 from packaging import version
 import requests
 
@@ -43,6 +46,18 @@ GlyphData = font_ns.struct("GlyphData")
 CONF_BPP = "bpp"
 CONF_EXTRAS = "extras"
 CONF_FONTS = "fonts"
+CONF_GLYPHSETS = "glyphsets"
+CONF_IGNORE_MISSING_GLYPHS = "ignore_missing_glyphs"
+
+
+# Cache loaded freetype fonts
+class FontCache(dict):
+    def __missing__(self, key):
+        res = self[key] = freetype.Face(key)
+        return res
+
+
+FONT_CACHE = FontCache()
 
 
 def glyph_comparator(x, y):
@@ -59,36 +74,106 @@ def glyph_comparator(x, y):
         return -1
     if len(x_) > len(y_):
         return 1
-    raise cv.Invalid(f"Found duplicate glyph {x}")
+    return 0
 
 
-def validate_glyphs(value):
-    if isinstance(value, list):
-        value = cv.Schema([cv.string])(value)
-    value = cv.Schema([cv.string])(list(value))
+def flatten(lists) -> list:
+    """
+    Given a list of lists, flatten it to a single list of all elements of all lists.
+    This wraps itertools.chain.from_iterable to make it more readable, and return a list
+    rather than a single use iterable.
+    """
+    from itertools import chain
 
-    value.sort(key=functools.cmp_to_key(glyph_comparator))
-    return value
+    return list(chain.from_iterable(lists))
 
 
-font_map = {}
+def check_missing_glyphs(file, codepoints: Iterable, warning: bool = False):
+    """
+    Check that the given font file actually contains the requested glyphs
+    :param file: A Truetype font file
+    :param codepoints: A list of codepoints to check
+    :param warning: If true, log a warning instead of raising an exception
+    """
 
-
-def merge_glyphs(config):
-    glyphs = []
-    glyphs.extend(config[CONF_GLYPHS])
-    font_list = [(EFont(config[CONF_FILE], config[CONF_SIZE], config[CONF_GLYPHS]))]
-    if extras := config.get(CONF_EXTRAS):
-        extra_fonts = list(
-            map(
-                lambda x: EFont(x[CONF_FILE], config[CONF_SIZE], x[CONF_GLYPHS]), extras
-            )
+    font = FONT_CACHE[file]
+    missing = [chr(x) for x in codepoints if font.get_char_index(x) == 0]
+    if missing:
+        # Only list up to 10 missing glyphs
+        missing.sort(key=functools.cmp_to_key(glyph_comparator))
+        count = len(missing)
+        missing = missing[:10]
+        missing_str = "\n    ".join(
+            f"{x} ({x.encode('unicode_escape')})" for x in missing
         )
-        font_list.extend(extra_fonts)
-        for extra in extras:
-            glyphs.extend(extra[CONF_GLYPHS])
-        validate_glyphs(glyphs)
-    font_map[config[CONF_ID]] = font_list
+        if count > 10:
+            missing_str += f"\n    and {count - 10} more."
+        message = f"Font {Path(file).name} is missing {count} glyph{'s' if count != 1 else ''}:\n    {missing_str}"
+        if warning:
+            _LOGGER.warning(message)
+        else:
+            raise cv.Invalid(message)
+
+
+def validate_glyphs(config):
+    """
+    Check for duplicate codepoints, then check that all requested codepoints actually
+    have glyphs defined in the appropriate font file.
+    """
+
+    # Collect all glyph codepoints and flatten to a list of chars
+    glyphspoints = flatten(
+        [x[CONF_GLYPHS] for x in config[CONF_EXTRAS]] + config[CONF_GLYPHS]
+    )
+    # Convert a list of strings to a list of chars (one char strings)
+    glyphspoints = flatten([list(x) for x in glyphspoints])
+    if len(set(glyphspoints)) != len(glyphspoints):
+        duplicates = {x for x in glyphspoints if glyphspoints.count(x) > 1}
+        dup_str = ", ".join(f"{x} ({x.encode('unicode_escape')})" for x in duplicates)
+        raise cv.Invalid(
+            f"Found duplicate glyph{'s' if len(duplicates) != 1 else ''}: {dup_str}"
+        )
+    # convert to codepoints
+    glyphspoints = {ord(x) for x in glyphspoints}
+    fileconf = config[CONF_FILE]
+    setpoints = set(
+        flatten([glyphsets.unicodes_per_glyphset(x) for x in config[CONF_GLYPHSETS]])
+    )
+    # Make setpoints and glyphspoints disjoint
+    setpoints.difference_update(glyphspoints)
+    if fileconf[CONF_TYPE] == TYPE_LOCAL_BITMAP:
+        # Pillow only allows 256 glyphs per bitmap font. Not sure if that is a Pillow limitation
+        # or a file format limitation
+        if any(x >= 256 for x in setpoints.copy().union(glyphspoints)):
+            raise cv.Invalid("Codepoints in bitmap fonts must be in the range 0-255")
+    else:
+        # for TT fonts, check that glyphs are actually present
+        # Check extras against their own font, exclude from parent font codepoints
+        for extra in config[CONF_EXTRAS]:
+            points = {ord(x) for x in flatten(extra[CONF_GLYPHS])}
+            glyphspoints.difference_update(points)
+            setpoints.difference_update(points)
+            check_missing_glyphs(extra[CONF_FILE][CONF_PATH], points)
+
+        # A named glyph that can't be provided is an error
+        check_missing_glyphs(fileconf[CONF_PATH], glyphspoints)
+        # A missing glyph from a set is a warning.
+        if not config[CONF_IGNORE_MISSING_GLYPHS]:
+            check_missing_glyphs(fileconf[CONF_PATH], setpoints, warning=True)
+
+    # Populate the default after the above checks so that use of the default doesn't trigger errors
+    if not config[CONF_GLYPHS] and not config[CONF_GLYPHSETS]:
+        if fileconf[CONF_TYPE] == TYPE_LOCAL_BITMAP:
+            config[CONF_GLYPHS] = [DEFAULT_GLYPHS]
+        else:
+            # set a default glyphset, intersected with what the font actually offers
+            font = FONT_CACHE[fileconf[CONF_PATH]]
+            config[CONF_GLYPHS] = [
+                chr(x)
+                for x in glyphsets.unicodes_per_glyphset(DEFAULT_GLYPHSET)
+                if font.get_char_index(x) != 0
+            ]
+
     return config
 
 
@@ -120,7 +205,7 @@ def validate_truetype_file(value):
         )
     if not any(map(value.lower().endswith, FONT_EXTENSIONS)):
         raise cv.Invalid(f"Only {FONT_EXTENSIONS} files are supported.")
-    return cv.file_(value)
+    return CORE.relative_config_path(cv.file_(value))
 
 
 TYPE_LOCAL = "local"
@@ -139,6 +224,10 @@ LOCAL_BITMAP_SCHEMA = cv.Schema(
     }
 )
 
+FULLPATH_SCHEMA = cv.maybe_simple_value(
+    {cv.Required(CONF_PATH): cv.string}, key=CONF_PATH
+)
+
 CONF_ITALIC = "italic"
 FONT_WEIGHTS = {
     "thin": 100,
@@ -167,13 +256,13 @@ def _compute_local_font_path(value: dict) -> Path:
     return base_dir / key
 
 
-def get_font_path(value, type) -> Path:
-    if type == TYPE_GFONTS:
+def get_font_path(value, font_type) -> Path:
+    if font_type == TYPE_GFONTS:
         name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1"
         return external_files.compute_local_file_dir(DOMAIN) / f"{name}.ttf"
-    if type == TYPE_WEB:
+    if font_type == TYPE_WEB:
         return _compute_local_font_path(value) / "font.ttf"
-    return None
+    assert False
 
 
 def download_gfont(value):
@@ -203,7 +292,7 @@ def download_gfont(value):
     _LOGGER.debug("download_gfont: ttf_url=%s", ttf_url)
 
     external_files.download_content(ttf_url, path)
-    return value
+    return FULLPATH_SCHEMA(path)
 
 
 def download_web_font(value):
@@ -212,7 +301,7 @@ def download_web_font(value):
 
     external_files.download_content(url, path)
     _LOGGER.debug("download_web_font: path=%s", path)
-    return value
+    return FULLPATH_SCHEMA(path)
 
 
 EXTERNAL_FONT_SCHEMA = cv.Schema(
@@ -225,7 +314,6 @@ EXTERNAL_FONT_SCHEMA = cv.Schema(
     }
 )
 
-
 GFONTS_SCHEMA = cv.All(
     EXTERNAL_FONT_SCHEMA.extend(
         {
@@ -259,10 +347,10 @@ def validate_file_shorthand(value):
         }
         if weight is not None:
             data[CONF_WEIGHT] = weight[1:]
-        return FILE_SCHEMA(data)
+        return font_file_schema(data)
 
     if value.startswith("http://") or value.startswith("https://"):
-        return FILE_SCHEMA(
+        return font_file_schema(
             {
                 CONF_TYPE: TYPE_WEB,
                 CONF_URL: value,
@@ -270,14 +358,15 @@ def validate_file_shorthand(value):
         )
 
     if value.endswith(".pcf") or value.endswith(".bdf"):
-        return FILE_SCHEMA(
-            {
-                CONF_TYPE: TYPE_LOCAL_BITMAP,
-                CONF_PATH: value,
-            }
+        value = convert_bitmap_to_pillow_font(
+            CORE.relative_config_path(cv.file_(value))
         )
+        return {
+            CONF_TYPE: TYPE_LOCAL_BITMAP,
+            CONF_PATH: value,
+        }
 
-    return FILE_SCHEMA(
+    return font_file_schema(
         {
             CONF_TYPE: TYPE_LOCAL,
             CONF_PATH: value,
@@ -295,31 +384,35 @@ TYPED_FILE_SCHEMA = cv.typed_schema(
 )
 
 
-def _file_schema(value):
+def font_file_schema(value):
     if isinstance(value, str):
         return validate_file_shorthand(value)
     return TYPED_FILE_SCHEMA(value)
 
 
-FILE_SCHEMA = cv.All(_file_schema)
+# Default if no glyphs or glyphsets are provided
+DEFAULT_GLYPHSET = "GF_Latin_Kernel"
+# default for bitmap fonts
+DEFAULT_GLYPHS = ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz<C2><B0>'
 
-DEFAULT_GLYPHS = (
-    ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
-)
 CONF_RAW_GLYPH_ID = "raw_glyph_id"
 
 FONT_SCHEMA = cv.Schema(
     {
         cv.Required(CONF_ID): cv.declare_id(Font),
-        cv.Required(CONF_FILE): FILE_SCHEMA,
-        cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs,
+        cv.Required(CONF_FILE): font_file_schema,
+        cv.Optional(CONF_GLYPHS, default=[]): cv.ensure_list(cv.string_strict),
+        cv.Optional(CONF_GLYPHSETS, default=[]): cv.ensure_list(
+            cv.one_of(*glyphsets.defined_glyphsets())
+        ),
+        cv.Optional(CONF_IGNORE_MISSING_GLYPHS, default=False): cv.boolean,
         cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1),
         cv.Optional(CONF_BPP, default=1): cv.one_of(1, 2, 4, 8),
-        cv.Optional(CONF_EXTRAS): cv.ensure_list(
+        cv.Optional(CONF_EXTRAS, default=[]): cv.ensure_list(
             cv.Schema(
                 {
-                    cv.Required(CONF_FILE): FILE_SCHEMA,
-                    cv.Required(CONF_GLYPHS): validate_glyphs,
+                    cv.Required(CONF_FILE): font_file_schema,
+                    cv.Required(CONF_GLYPHS): cv.ensure_list(cv.string_strict),
                 }
             )
         ),
@@ -328,7 +421,7 @@ FONT_SCHEMA = cv.Schema(
     },
 )
 
-CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, merge_glyphs)
+CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, validate_glyphs)
 
 
 # PIL doesn't provide a consistent interface for both TrueType and bitmap
@@ -367,28 +460,20 @@ class BitmapFontWrapper:
             mask = self.getmask(glyph, mode="1")
             _, height = mask.size
             max_height = max(max_height, height)
-        return (max_height, 0)
+        return max_height, 0
 
 
 class EFont:
-    def __init__(self, file, size, glyphs):
-        self.glyphs = glyphs
+    def __init__(self, file, size, codepoints):
+        self.codepoints = codepoints
+        path = file[CONF_PATH]
+        self.name = Path(path).name
         ftype = file[CONF_TYPE]
         if ftype == TYPE_LOCAL_BITMAP:
-            font = load_bitmap_font(CORE.relative_config_path(file[CONF_PATH]))
-        elif ftype == TYPE_LOCAL:
-            path = CORE.relative_config_path(file[CONF_PATH])
-            font = load_ttf_font(path, size)
-        elif ftype in (TYPE_GFONTS, TYPE_WEB):
-            path = get_font_path(file, ftype)
-            font = load_ttf_font(path, size)
+            self.font = load_bitmap_font(path)
         else:
-            raise cv.Invalid(f"Could not load font: unknown type: {ftype}")
-        self.font = font
-        self.ascent, self.descent = font.getmetrics(glyphs)
-
-    def has_glyph(self, glyph):
-        return glyph in self.glyphs
+            self.font = load_ttf_font(path, size)
+        self.ascent, self.descent = self.font.getmetrics(codepoints)
 
 
 def convert_bitmap_to_pillow_font(filepath):
@@ -400,6 +485,7 @@ def convert_bitmap_to_pillow_font(filepath):
 
     copy_file_if_changed(filepath, local_bitmap_font_file)
 
+    local_pil_font_file = local_bitmap_font_file.with_suffix(".pil")
     with open(local_bitmap_font_file, "rb") as fp:
         try:
             try:
@@ -409,28 +495,22 @@ def convert_bitmap_to_pillow_font(filepath):
                 p = BdfFontFile.BdfFontFile(fp)
 
             # Convert to pillow-formatted fonts, which have a .pil and .pbm extension.
-            p.save(local_bitmap_font_file)
+            p.save(local_pil_font_file)
         except (SyntaxError, OSError) as err:
             raise core.EsphomeError(
                 f"Failed to parse as bitmap font: '{filepath}': {err}"
             )
 
-    local_pil_font_file = os.path.splitext(local_bitmap_font_file)[0] + ".pil"
-    return cv.file_(local_pil_font_file)
+    return str(local_pil_font_file)
 
 
 def load_bitmap_font(filepath):
     from PIL import ImageFont
 
-    # Convert bpf and pcf files to pillow fonts, first.
-    pil_font_path = convert_bitmap_to_pillow_font(filepath)
-
     try:
-        font = ImageFont.load(str(pil_font_path))
+        font = ImageFont.load(str(filepath))
     except Exception as e:
-        raise core.EsphomeError(
-            f"Failed to load bitmap font file: {pil_font_path} : {e}"
-        )
+        raise core.EsphomeError(f"Failed to load bitmap font file: {filepath}: {e}")
 
     return BitmapFontWrapper(font)
 
@@ -441,7 +521,7 @@ def load_ttf_font(path, size):
     try:
         font = ImageFont.truetype(str(path), size)
     except Exception as e:
-        raise core.EsphomeError(f"Could not load truetype file {path}: {e}")
+        raise core.EsphomeError(f"Could not load TrueType file {path}: {e}")
 
     return TrueTypeFontWrapper(font)
 
@@ -456,14 +536,35 @@ class GlyphInfo:
 
 
 async def to_code(config):
-    glyph_to_font_map = {}
-    font_list = font_map[config[CONF_ID]]
-    glyphs = []
-    for font in font_list:
-        glyphs.extend(font.glyphs)
-        for glyph in font.glyphs:
-            glyph_to_font_map[glyph] = font
-    glyphs.sort(key=functools.cmp_to_key(glyph_comparator))
+    """
+    Collect all glyph codepoints, construct a map from a codepoint to a font file.
+    Codepoints are either explicit (glyphs key in top level or extras) or part of a glyphset.
+    Codepoints listed in extras use the extra font and override codepoints from glyphsets.
+    Achieve this by processing the base codepoints first, then the extras
+    """
+
+    # get the codepoints from glyphsets and flatten to a set of chrs.
+    point_set: set[str] = {
+        chr(x)
+        for x in flatten(
+            [glyphsets.unicodes_per_glyphset(x) for x in config[CONF_GLYPHSETS]]
+        )
+    }
+    # get the codepoints from the glyphs key, flatten to a list of chrs and combine with the points from glyphsets
+    point_set.update(flatten(config[CONF_GLYPHS]))
+    size = config[CONF_SIZE]
+    # Create the codepoint to font file map
+    base_font = EFont(config[CONF_FILE], size, point_set)
+    point_font_map: dict[str, EFont] = {c: base_font for c in point_set}
+    # process extras, updating the map and extending the codepoint list
+    for extra in config[CONF_EXTRAS]:
+        extra_points = flatten(extra[CONF_GLYPHS])
+        point_set.update(extra_points)
+        extra_font = EFont(extra[CONF_FILE], size, extra_points)
+        point_font_map.update({c: extra_font for c in extra_points})
+
+    codepoints = list(point_set)
+    codepoints.sort(key=functools.cmp_to_key(glyph_comparator))
     glyph_args = {}
     data = []
     bpp = config[CONF_BPP]
@@ -473,10 +574,11 @@ async def to_code(config):
     else:
         mode = "L"
         scale = 256 // (1 << bpp)
-    for glyph in glyphs:
-        font = glyph_to_font_map[glyph].font
-        mask = font.getmask(glyph, mode=mode)
-        offset_x, offset_y = font.getoffset(glyph)
+    # create the data array for all glyphs
+    for codepoint in codepoints:
+        font = point_font_map[codepoint]
+        mask = font.font.getmask(codepoint, mode=mode)
+        offset_x, offset_y = font.font.getoffset(codepoint)
         width, height = mask.size
         glyph_data = [0] * ((height * width * bpp + 7) // 8)
         pos = 0
@@ -487,31 +589,34 @@ async def to_code(config):
                     if pixel & (1 << (bpp - bit_num - 1)):
                         glyph_data[pos // 8] |= 0x80 >> (pos % 8)
                     pos += 1
-        glyph_args[glyph] = GlyphInfo(len(data), offset_x, offset_y, width, height)
+        glyph_args[codepoint] = GlyphInfo(len(data), offset_x, offset_y, width, height)
         data += glyph_data
 
     rhs = [HexInt(x) for x in data]
     prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
 
+    # Create the glyph table that points to data in the above array.
     glyph_initializer = []
-    for glyph in glyphs:
+    for codepoint in codepoints:
         glyph_initializer.append(
             cg.StructInitializer(
                 GlyphData,
                 (
                     "a_char",
-                    cg.RawExpression(f"(const uint8_t *){cpp_string_escape(glyph)}"),
+                    cg.RawExpression(
+                        f"(const uint8_t *){cpp_string_escape(codepoint)}"
+                    ),
                 ),
                 (
                     "data",
                     cg.RawExpression(
-                        f"{str(prog_arr)} + {str(glyph_args[glyph].data_len)}"
+                        f"{str(prog_arr)} + {str(glyph_args[codepoint].data_len)}"
                     ),
                 ),
-                ("offset_x", glyph_args[glyph].offset_x),
-                ("offset_y", glyph_args[glyph].offset_y),
-                ("width", glyph_args[glyph].width),
-                ("height", glyph_args[glyph].height),
+                ("offset_x", glyph_args[codepoint].offset_x),
+                ("offset_y", glyph_args[codepoint].offset_y),
+                ("width", glyph_args[codepoint].width),
+                ("height", glyph_args[codepoint].height),
             )
         )
 
@@ -521,7 +626,7 @@ async def to_code(config):
         config[CONF_ID],
         glyphs,
         len(glyph_initializer),
-        font_list[0].ascent,
-        font_list[0].ascent + font_list[0].descent,
+        base_font.ascent,
+        base_font.ascent + base_font.descent,
         bpp,
     )
diff --git a/requirements.txt b/requirements.txt
index 8cc26e4da0..e11e629743 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -17,6 +17,9 @@ aioesphomeapi==24.6.2
 zeroconf==0.132.2
 puremagic==1.27
 ruamel.yaml==0.18.6 # dashboard_import
+glyphsets==1.0.0
+pillow==10.4.0
+freetype-py==2.5.1
 
 # esp-idf requires this, but doesn't bundle it by default
 # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24
diff --git a/requirements_optional.txt b/requirements_optional.txt
index 2d57c5fd96..7416753d55 100644
--- a/requirements_optional.txt
+++ b/requirements_optional.txt
@@ -1,2 +1 @@
-pillow==10.4.0
 cairosvg==2.7.1
diff --git a/script/ci-custom.py b/script/ci-custom.py
index 9a97d3e4a8..81e3da311a 100755
--- a/script/ci-custom.py
+++ b/script/ci-custom.py
@@ -58,7 +58,7 @@ file_types = (
 )
 cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc")
 py_include = ("*.py",)
-ignore_types = (".ico", ".png", ".woff", ".woff2", "", ".ttf", ".otf")
+ignore_types = (".ico", ".png", ".woff", ".woff2", "", ".ttf", ".otf", ".pcf")
 
 LINT_FILE_CHECKS = []
 LINT_CONTENT_CHECKS = []
diff --git a/tests/components/font/.gitattributes b/tests/components/font/.gitattributes
new file mode 100644
index 0000000000..18d9a389e8
--- /dev/null
+++ b/tests/components/font/.gitattributes
@@ -0,0 +1,2 @@
+*.pcf        -text
+
diff --git a/tests/components/font/MatrixChunky8X.bdf b/tests/components/font/MatrixChunky8X.bdf
new file mode 100644
index 0000000000..89b3683180
--- /dev/null
+++ b/tests/components/font/MatrixChunky8X.bdf
@@ -0,0 +1,7461 @@
+STARTFONT 2.1
+FONT -Trip5-MatrixChunky8X-Medium-R-Normal--8-80-75-75-P-40-ISO10646-1
+SIZE 8 75 75
+FONTBOUNDINGBOX 8 8 -1 0
+COMMENT "Generated by fontforge, http://fontforge.sourceforge.net"
+COMMENT "Trip5"
+COMMENT "Conventional Chaos"
+COMMENT "CC-BY"
+STARTPROPERTIES 25
+FOUNDRY "Conventional Chaos"
+FAMILY_NAME "MatrixChunky8X"
+FONT_NAME "MatrixChunky8X"
+FACE_NAME "MatrixChunky8X"
+COPYRIGHT "https://github.com/trip5/Matrix-Fonts"
+FONT_VERSION "001.000"
+WEIGHT_NAME "Medium"
+SLANT "R"
+SETWIDTH_NAME "Normal"
+ADD_STYLE_NAME ""
+PIXEL_SIZE 8
+POINT_SIZE 80
+RESOLUTION_X 75
+RESOLUTION_Y 75
+SPACING "P"
+AVERAGE_WIDTH 40
+CHARSET_REGISTRY "ISO10646"
+CHARSET_ENCODING "1"
+CHARSET_COLLECTIONS "ISO8859-2 ISO8859-9 ISO8859-4 ISO10646-1"
+FONT_ASCENT 8
+FONT_DESCENT 0
+UNDERLINE_POSITION 0
+UNDERLINE_THICKNESS 1
+X_HEIGHT 6
+CAP_HEIGHT 8
+ENDPROPERTIES
+CHARS 535
+STARTCHAR uni0000
+ENCODING 0
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 2
+BITMAP
+A0
+00
+40
+00
+A0
+ENDCHAR
+STARTCHAR space
+ENCODING 32
+SWIDTH 250 0
+DWIDTH 2 0
+BBX 1 1 0 0
+BITMAP
+00
+ENDCHAR
+STARTCHAR exclam
+ENCODING 33
+SWIDTH 250 0
+DWIDTH 2 0
+BBX 1 8 0 0
+BITMAP
+80
+80
+80
+80
+80
+80
+00
+80
+ENDCHAR
+STARTCHAR quotedbl
+ENCODING 34
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 2 0 6
+BITMAP
+A0
+A0
+ENDCHAR
+STARTCHAR numbersign
+ENCODING 35
+SWIDTH 750 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+50
+50
+F8
+50
+50
+F8
+50
+50
+ENDCHAR
+STARTCHAR dollar
+ENCODING 36
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+E0
+80
+E0
+20
+20
+E0
+40
+ENDCHAR
+STARTCHAR percent
+ENCODING 37
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+A0
+20
+40
+40
+80
+A0
+A0
+ENDCHAR
+STARTCHAR ampersand
+ENCODING 38
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+E0
+A0
+A0
+40
+B0
+A0
+B0
+D0
+ENDCHAR
+STARTCHAR quotesingle
+ENCODING 39
+SWIDTH 250 0
+DWIDTH 2 0
+BBX 1 3 0 5
+BITMAP
+80
+80
+80
+ENDCHAR
+STARTCHAR parenleft
+ENCODING 40
+SWIDTH 1000 0
+DWIDTH 3 0
+BBX 2 8 0 0
+BITMAP
+40
+40
+80
+80
+80
+80
+40
+40
+ENDCHAR
+STARTCHAR parenright
+ENCODING 41
+SWIDTH 1000 0
+DWIDTH 3 0
+BBX 2 8 0 0
+BITMAP
+80
+80
+40
+40
+40
+40
+80
+80
+ENDCHAR
+STARTCHAR asterisk
+ENCODING 42
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 4 0 4
+BITMAP
+90
+60
+60
+90
+ENDCHAR
+STARTCHAR plus
+ENCODING 43
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 3 0 3
+BITMAP
+40
+E0
+40
+ENDCHAR
+STARTCHAR comma
+ENCODING 44
+SWIDTH 1000 0
+DWIDTH 2 0
+BBX 1 2 0 0
+BITMAP
+80
+80
+ENDCHAR
+STARTCHAR hyphen
+ENCODING 45
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 1 0 4
+BITMAP
+E0
+ENDCHAR
+STARTCHAR period
+ENCODING 46
+SWIDTH 1000 0
+DWIDTH 2 0
+BBX 1 1 0 0
+BITMAP
+80
+ENDCHAR
+STARTCHAR slash
+ENCODING 47
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+20
+40
+40
+40
+80
+80
+80
+ENDCHAR
+STARTCHAR zero
+ENCODING 48
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+A0
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR one
+ENCODING 49
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+C0
+40
+40
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR two
+ENCODING 50
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+20
+20
+E0
+80
+80
+80
+E0
+ENDCHAR
+STARTCHAR three
+ENCODING 51
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+20
+20
+E0
+20
+20
+20
+E0
+ENDCHAR
+STARTCHAR four
+ENCODING 52
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+A0
+A0
+E0
+20
+20
+20
+20
+ENDCHAR
+STARTCHAR five
+ENCODING 53
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+80
+80
+E0
+20
+20
+20
+E0
+ENDCHAR
+STARTCHAR six
+ENCODING 54
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+80
+80
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR seven
+ENCODING 55
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+20
+20
+20
+20
+20
+20
+20
+ENDCHAR
+STARTCHAR eight
+ENCODING 56
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+A0
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR nine
+ENCODING 57
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+A0
+E0
+20
+20
+20
+E0
+ENDCHAR
+STARTCHAR colon
+ENCODING 58
+SWIDTH 1000 0
+DWIDTH 2 0
+BBX 1 3 0 3
+BITMAP
+80
+00
+80
+ENDCHAR
+STARTCHAR semicolon
+ENCODING 59
+SWIDTH 1000 0
+DWIDTH 2 0
+BBX 1 4 0 2
+BITMAP
+80
+00
+80
+80
+ENDCHAR
+STARTCHAR less
+ENCODING 60
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 2
+BITMAP
+20
+40
+80
+40
+20
+ENDCHAR
+STARTCHAR equal
+ENCODING 61
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 3 0 3
+BITMAP
+E0
+00
+E0
+ENDCHAR
+STARTCHAR greater
+ENCODING 62
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 2
+BITMAP
+80
+40
+20
+40
+80
+ENDCHAR
+STARTCHAR question
+ENCODING 63
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+20
+60
+40
+40
+00
+40
+ENDCHAR
+STARTCHAR at
+ENCODING 64
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+F0
+90
+90
+B0
+B0
+80
+80
+F0
+ENDCHAR
+STARTCHAR A
+ENCODING 65
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+A0
+E0
+A0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR B
+ENCODING 66
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+A0
+C0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR C
+ENCODING 67
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+80
+80
+80
+80
+A0
+E0
+ENDCHAR
+STARTCHAR D
+ENCODING 68
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+C0
+A0
+A0
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR E
+ENCODING 69
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+80
+80
+E0
+80
+80
+80
+E0
+ENDCHAR
+STARTCHAR F
+ENCODING 70
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+80
+80
+E0
+80
+80
+80
+80
+ENDCHAR
+STARTCHAR G
+ENCODING 71
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+E0
+80
+80
+B0
+90
+90
+90
+F0
+ENDCHAR
+STARTCHAR H
+ENCODING 72
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+90
+90
+90
+F0
+90
+90
+90
+90
+ENDCHAR
+STARTCHAR I
+ENCODING 73
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+40
+40
+40
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR J
+ENCODING 74
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+20
+20
+20
+20
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR K
+ENCODING 75
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+A0
+A0
+C0
+A0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR L
+ENCODING 76
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+80
+80
+80
+80
+80
+80
+80
+E0
+ENDCHAR
+STARTCHAR M
+ENCODING 77
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+88
+D8
+A8
+A8
+88
+88
+88
+88
+ENDCHAR
+STARTCHAR N
+ENCODING 78
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+90
+90
+D0
+B0
+90
+90
+90
+90
+ENDCHAR
+STARTCHAR O
+ENCODING 79
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+F0
+90
+90
+90
+90
+90
+90
+F0
+ENDCHAR
+STARTCHAR P
+ENCODING 80
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+A0
+E0
+80
+80
+80
+80
+ENDCHAR
+STARTCHAR Q
+ENCODING 81
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+F0
+90
+90
+90
+B0
+B0
+A0
+D0
+ENDCHAR
+STARTCHAR R
+ENCODING 82
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+A0
+C0
+A0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR S
+ENCODING 83
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+80
+E0
+20
+20
+A0
+E0
+ENDCHAR
+STARTCHAR T
+ENCODING 84
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+40
+40
+40
+40
+40
+40
+40
+ENDCHAR
+STARTCHAR U
+ENCODING 85
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+A0
+A0
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR V
+ENCODING 86
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+A0
+A0
+A0
+A0
+A0
+E0
+40
+ENDCHAR
+STARTCHAR W
+ENCODING 87
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+A8
+A8
+A8
+A8
+A8
+A8
+F8
+50
+ENDCHAR
+STARTCHAR X
+ENCODING 88
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+A0
+A0
+40
+A0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR Y
+ENCODING 89
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+A0
+A0
+E0
+40
+40
+40
+40
+ENDCHAR
+STARTCHAR Z
+ENCODING 90
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+20
+20
+40
+80
+80
+80
+E0
+ENDCHAR
+STARTCHAR bracketleft
+ENCODING 91
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+80
+80
+80
+80
+80
+80
+E0
+ENDCHAR
+STARTCHAR backslash
+ENCODING 92
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+80
+80
+40
+40
+40
+20
+20
+20
+ENDCHAR
+STARTCHAR bracketright
+ENCODING 93
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+20
+20
+20
+20
+20
+20
+E0
+ENDCHAR
+STARTCHAR asciicircum
+ENCODING 94
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 3 0 5
+BITMAP
+40
+E0
+A0
+ENDCHAR
+STARTCHAR underscore
+ENCODING 95
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 1 0 0
+BITMAP
+E0
+ENDCHAR
+STARTCHAR grave
+ENCODING 96
+SWIDTH 1000 0
+DWIDTH 3 0
+BBX 2 2 0 6
+BITMAP
+80
+40
+ENDCHAR
+STARTCHAR a
+ENCODING 97
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+20
+E0
+A0
+E0
+ENDCHAR
+STARTCHAR b
+ENCODING 98
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+80
+80
+80
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR c
+ENCODING 99
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+80
+80
+80
+E0
+ENDCHAR
+STARTCHAR d
+ENCODING 100
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+20
+20
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR e
+ENCODING 101
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+A0
+E0
+80
+E0
+ENDCHAR
+STARTCHAR f
+ENCODING 102
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+60
+40
+40
+E0
+40
+40
+40
+ENDCHAR
+STARTCHAR g
+ENCODING 103
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+A0
+E0
+20
+60
+ENDCHAR
+STARTCHAR h
+ENCODING 104
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+80
+80
+80
+E0
+A0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR i
+ENCODING 105
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+40
+00
+C0
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR j
+ENCODING 106
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+20
+00
+20
+20
+20
+A0
+E0
+ENDCHAR
+STARTCHAR k
+ENCODING 107
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+80
+80
+A0
+A0
+C0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR l
+ENCODING 108
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+C0
+40
+40
+40
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR m
+ENCODING 109
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 5 0 0
+BITMAP
+F8
+A8
+A8
+A8
+A8
+ENDCHAR
+STARTCHAR n
+ENCODING 110
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+A0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR o
+ENCODING 111
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR p
+ENCODING 112
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+A0
+E0
+80
+80
+ENDCHAR
+STARTCHAR q
+ENCODING 113
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+A0
+E0
+20
+20
+ENDCHAR
+STARTCHAR r
+ENCODING 114
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+80
+80
+80
+80
+ENDCHAR
+STARTCHAR s
+ENCODING 115
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+80
+E0
+20
+E0
+ENDCHAR
+STARTCHAR t
+ENCODING 116
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+40
+40
+E0
+40
+40
+40
+60
+ENDCHAR
+STARTCHAR u
+ENCODING 117
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR v
+ENCODING 118
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+A0
+A0
+A0
+E0
+40
+ENDCHAR
+STARTCHAR w
+ENCODING 119
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 5 0 0
+BITMAP
+A8
+A8
+A8
+F8
+50
+ENDCHAR
+STARTCHAR x
+ENCODING 120
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+A0
+A0
+40
+A0
+A0
+ENDCHAR
+STARTCHAR y
+ENCODING 121
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+A0
+A0
+E0
+20
+60
+ENDCHAR
+STARTCHAR z
+ENCODING 122
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+20
+40
+80
+E0
+ENDCHAR
+STARTCHAR braceleft
+ENCODING 123
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+60
+40
+40
+C0
+40
+40
+40
+60
+ENDCHAR
+STARTCHAR bar
+ENCODING 124
+SWIDTH 1000 0
+DWIDTH 2 0
+BBX 1 8 0 0
+BITMAP
+80
+80
+80
+80
+80
+80
+80
+80
+ENDCHAR
+STARTCHAR braceright
+ENCODING 125
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+C0
+40
+40
+60
+40
+40
+40
+C0
+ENDCHAR
+STARTCHAR asciitilde
+ENCODING 126
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 2 0 3
+BITMAP
+60
+C0
+ENDCHAR
+STARTCHAR exclamdown
+ENCODING 161
+SWIDTH 250 0
+DWIDTH 2 0
+BBX 1 8 0 0
+BITMAP
+80
+80
+00
+80
+80
+80
+80
+80
+ENDCHAR
+STARTCHAR cent
+ENCODING 162
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+40
+E0
+80
+80
+80
+E0
+40
+ENDCHAR
+STARTCHAR sterling
+ENCODING 163
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+70
+50
+40
+E0
+40
+40
+40
+F0
+ENDCHAR
+STARTCHAR currency
+ENCODING 164
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 6 0 1
+BITMAP
+F0
+60
+90
+90
+60
+F0
+ENDCHAR
+STARTCHAR yen
+ENCODING 165
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+A0
+E0
+40
+E0
+40
+40
+40
+ENDCHAR
+STARTCHAR brokenbar
+ENCODING 166
+SWIDTH 1000 0
+DWIDTH 2 0
+BBX 1 8 0 0
+BITMAP
+80
+80
+80
+00
+80
+80
+80
+80
+ENDCHAR
+STARTCHAR section
+ENCODING 167
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+60
+80
+E0
+A0
+A0
+E0
+20
+C0
+ENDCHAR
+STARTCHAR dieresis
+ENCODING 168
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+E0
+A0
+C0
+A0
+A0
+E0
+40
+ENDCHAR
+STARTCHAR copyright
+ENCODING 169
+SWIDTH 1000 0
+DWIDTH 7 0
+BBX 6 7 0 1
+BITMAP
+78
+CC
+B4
+A4
+B4
+CC
+78
+ENDCHAR
+STARTCHAR ordfeminine
+ENCODING 170
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 3
+BITMAP
+60
+A0
+E0
+00
+E0
+ENDCHAR
+STARTCHAR guillemotleft
+ENCODING 171
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 3 0 3
+BITMAP
+50
+F0
+50
+ENDCHAR
+STARTCHAR logicalnot
+ENCODING 172
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+B8
+A8
+20
+20
+20
+28
+28
+38
+ENDCHAR
+STARTCHAR uni00AD
+ENCODING 173
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 1 0 4
+BITMAP
+E0
+ENDCHAR
+STARTCHAR registered
+ENCODING 174
+SWIDTH 1000 0
+DWIDTH 7 0
+BBX 6 7 0 1
+BITMAP
+78
+CC
+B4
+A4
+A4
+CC
+78
+ENDCHAR
+STARTCHAR macron
+ENCODING 175
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+B8
+A0
+20
+38
+20
+20
+20
+20
+ENDCHAR
+STARTCHAR degree
+ENCODING 176
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 3 0 5
+BITMAP
+E0
+A0
+E0
+ENDCHAR
+STARTCHAR plusminus
+ENCODING 177
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 2
+BITMAP
+40
+E0
+40
+00
+E0
+ENDCHAR
+STARTCHAR uni00B2
+ENCODING 178
+SWIDTH 1000 0
+DWIDTH 3 0
+BBX 2 5 0 3
+BITMAP
+C0
+40
+C0
+80
+C0
+ENDCHAR
+STARTCHAR uni00B3
+ENCODING 179
+SWIDTH 1000 0
+DWIDTH 3 0
+BBX 2 5 0 3
+BITMAP
+C0
+40
+C0
+40
+C0
+ENDCHAR
+STARTCHAR acute
+ENCODING 180
+SWIDTH 1000 0
+DWIDTH 3 0
+BBX 2 2 0 6
+BITMAP
+40
+C0
+ENDCHAR
+STARTCHAR mu
+ENCODING 181
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 6 0 0
+BITMAP
+A0
+A0
+A0
+A0
+E0
+80
+ENDCHAR
+STARTCHAR paragraph
+ENCODING 182
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+F0
+D0
+F0
+30
+30
+30
+30
+30
+ENDCHAR
+STARTCHAR periodcentered
+ENCODING 183
+SWIDTH 1000 0
+DWIDTH 2 0
+BBX 1 1 0 4
+BITMAP
+80
+ENDCHAR
+STARTCHAR cedilla
+ENCODING 184
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+00
+00
+E0
+00
+00
+00
+E0
+ENDCHAR
+STARTCHAR uni00B9
+ENCODING 185
+SWIDTH 1000 0
+DWIDTH 2 0
+BBX 1 5 0 3
+BITMAP
+80
+80
+80
+80
+80
+ENDCHAR
+STARTCHAR ordmasculine
+ENCODING 186
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 5 0 3
+BITMAP
+E0
+A0
+E0
+00
+E0
+ENDCHAR
+STARTCHAR guillemotright
+ENCODING 187
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 3 0 3
+BITMAP
+A0
+F0
+A0
+ENDCHAR
+STARTCHAR onequarter
+ENCODING 188
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+30
+60
+40
+F0
+40
+40
+60
+30
+ENDCHAR
+STARTCHAR onehalf
+ENCODING 189
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 7 8 0 0
+BITMAP
+54
+54
+FE
+54
+54
+54
+7C
+28
+ENDCHAR
+STARTCHAR threequarters
+ENCODING 190
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+70
+50
+10
+F8
+40
+40
+50
+70
+ENDCHAR
+STARTCHAR questiondown
+ENCODING 191
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+00
+40
+40
+C0
+80
+A0
+E0
+ENDCHAR
+STARTCHAR Agrave
+ENCODING 192
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+80
+40
+00
+E0
+A0
+E0
+A0
+A0
+ENDCHAR
+STARTCHAR Aacute
+ENCODING 193
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+40
+00
+E0
+A0
+E0
+A0
+A0
+ENDCHAR
+STARTCHAR Acircumflex
+ENCODING 194
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+A0
+00
+E0
+A0
+E0
+A0
+A0
+ENDCHAR
+STARTCHAR Atilde
+ENCODING 195
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+00
+E0
+A0
+E0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR Adieresis
+ENCODING 196
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+00
+E0
+A0
+E0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR Aring
+ENCODING 197
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+00
+E0
+A0
+E0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR AE
+ENCODING 198
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+F8
+A0
+A0
+F8
+A0
+A0
+A0
+B8
+ENDCHAR
+STARTCHAR Ccedilla
+ENCODING 199
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+80
+80
+80
+80
+80
+E0
+40
+ENDCHAR
+STARTCHAR Egrave
+ENCODING 200
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+80
+40
+00
+E0
+80
+E0
+80
+E0
+ENDCHAR
+STARTCHAR Eacute
+ENCODING 201
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+40
+00
+E0
+80
+E0
+80
+E0
+ENDCHAR
+STARTCHAR Ecircumflex
+ENCODING 202
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+A0
+00
+E0
+80
+E0
+80
+E0
+ENDCHAR
+STARTCHAR Edieresis
+ENCODING 203
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+00
+E0
+80
+E0
+80
+80
+E0
+ENDCHAR
+STARTCHAR Igrave
+ENCODING 204
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+80
+40
+00
+E0
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR Iacute
+ENCODING 205
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+40
+00
+E0
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR Icircumflex
+ENCODING 206
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+A0
+00
+E0
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR Idieresis
+ENCODING 207
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+00
+E0
+40
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR Eth
+ENCODING 208
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+60
+50
+50
+F0
+50
+50
+50
+60
+ENDCHAR
+STARTCHAR Ntilde
+ENCODING 209
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+60
+00
+90
+D0
+B0
+90
+90
+90
+ENDCHAR
+STARTCHAR Ograve
+ENCODING 210
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+20
+00
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR Oacute
+ENCODING 211
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+80
+00
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR Ocircumflex
+ENCODING 212
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+A0
+00
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR Otilde
+ENCODING 213
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+00
+E0
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR Odieresis
+ENCODING 214
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+00
+E0
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR multiply
+ENCODING 215
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 3 0 3
+BITMAP
+A0
+40
+A0
+ENDCHAR
+STARTCHAR Oslash
+ENCODING 216
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+F0
+90
+90
+B0
+D0
+90
+90
+F0
+ENDCHAR
+STARTCHAR Ugrave
+ENCODING 217
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+80
+40
+00
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR Uacute
+ENCODING 218
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+40
+00
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR Ucircumflex
+ENCODING 219
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+A0
+00
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR Udieresis
+ENCODING 220
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+00
+A0
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR Yacute
+ENCODING 221
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+40
+00
+A0
+A0
+E0
+40
+40
+ENDCHAR
+STARTCHAR Thorn
+ENCODING 222
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+80
+80
+E0
+A0
+A0
+E0
+80
+80
+ENDCHAR
+STARTCHAR germandbls
+ENCODING 223
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+C0
+A0
+A0
+E0
+80
+80
+ENDCHAR
+STARTCHAR agrave
+ENCODING 224
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+20
+00
+E0
+20
+E0
+A0
+E0
+ENDCHAR
+STARTCHAR aacute
+ENCODING 225
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+80
+00
+E0
+20
+E0
+A0
+E0
+ENDCHAR
+STARTCHAR acircumflex
+ENCODING 226
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+A0
+00
+E0
+20
+E0
+A0
+E0
+ENDCHAR
+STARTCHAR atilde
+ENCODING 227
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+E0
+00
+E0
+20
+E0
+A0
+E0
+ENDCHAR
+STARTCHAR adieresis
+ENCODING 228
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+A0
+00
+E0
+20
+E0
+A0
+E0
+ENDCHAR
+STARTCHAR aring
+ENCODING 229
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+40
+00
+E0
+20
+E0
+A0
+E0
+ENDCHAR
+STARTCHAR ae
+ENCODING 230
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 5 0 0
+BITMAP
+F8
+28
+F8
+A0
+F8
+ENDCHAR
+STARTCHAR ccedilla
+ENCODING 231
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+80
+80
+E0
+40
+ENDCHAR
+STARTCHAR egrave
+ENCODING 232
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+20
+00
+E0
+A0
+E0
+80
+E0
+ENDCHAR
+STARTCHAR eacute
+ENCODING 233
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+80
+00
+E0
+A0
+E0
+80
+E0
+ENDCHAR
+STARTCHAR ecircumflex
+ENCODING 234
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+A0
+00
+E0
+A0
+E0
+80
+E0
+ENDCHAR
+STARTCHAR edieresis
+ENCODING 235
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+A0
+00
+E0
+A0
+E0
+80
+E0
+ENDCHAR
+STARTCHAR igrave
+ENCODING 236
+SWIDTH 375 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+80
+40
+00
+C0
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR iacute
+ENCODING 237
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+40
+00
+C0
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR icircumflex
+ENCODING 238
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+A0
+00
+C0
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR idieresis
+ENCODING 239
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+A0
+00
+C0
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR eth
+ENCODING 240
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+A0
+20
+E0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR ntilde
+ENCODING 241
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+E0
+00
+E0
+A0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR ograve
+ENCODING 242
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+80
+40
+00
+E0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR oacute
+ENCODING 243
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+20
+40
+00
+E0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR ocircumflex
+ENCODING 244
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+40
+A0
+00
+E0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR otilde
+ENCODING 245
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 6 0 0
+BITMAP
+E0
+00
+E0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR odieresis
+ENCODING 246
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 6 0 0
+BITMAP
+A0
+00
+E0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR divide
+ENCODING 247
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 2
+BITMAP
+40
+00
+E0
+00
+40
+ENDCHAR
+STARTCHAR oslash
+ENCODING 248
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 5 0 0
+BITMAP
+F0
+B0
+D0
+90
+F0
+ENDCHAR
+STARTCHAR ugrave
+ENCODING 249
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+80
+40
+00
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR uacute
+ENCODING 250
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+20
+40
+00
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR ucircumflex
+ENCODING 251
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+40
+A0
+00
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR udieresis
+ENCODING 252
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 6 0 0
+BITMAP
+A0
+00
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR yacute
+ENCODING 253
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+40
+00
+A0
+A0
+E0
+20
+60
+ENDCHAR
+STARTCHAR thorn
+ENCODING 254
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+80
+80
+80
+E0
+A0
+A0
+E0
+80
+ENDCHAR
+STARTCHAR ydieresis
+ENCODING 255
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+00
+A0
+A0
+A0
+E0
+20
+60
+ENDCHAR
+STARTCHAR Amacron
+ENCODING 256
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+00
+E0
+A0
+E0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR amacron
+ENCODING 257
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+E0
+00
+E0
+20
+E0
+A0
+E0
+ENDCHAR
+STARTCHAR Abreve
+ENCODING 258
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+E0
+A0
+E0
+A0
+A0
+ENDCHAR
+STARTCHAR abreve
+ENCODING 259
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+E0
+20
+E0
+A0
+E0
+ENDCHAR
+STARTCHAR Aogonek
+ENCODING 260
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 4 8 0 0
+BITMAP
+E0
+A0
+A0
+E0
+A0
+A0
+A0
+30
+ENDCHAR
+STARTCHAR aogonek
+ENCODING 261
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 4 6 0 0
+BITMAP
+E0
+20
+E0
+A0
+E0
+30
+ENDCHAR
+STARTCHAR Cacute
+ENCODING 262
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+80
+00
+E0
+80
+80
+80
+E0
+ENDCHAR
+STARTCHAR cacute
+ENCODING 263
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+40
+80
+00
+E0
+80
+80
+E0
+ENDCHAR
+STARTCHAR Ccircumflex
+ENCODING 264
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+A0
+00
+E0
+80
+80
+80
+E0
+ENDCHAR
+STARTCHAR ccircumflex
+ENCODING 265
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+40
+A0
+00
+E0
+80
+80
+E0
+ENDCHAR
+STARTCHAR Cdotaccent
+ENCODING 266
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+00
+E0
+80
+80
+80
+80
+E0
+ENDCHAR
+STARTCHAR cdotaccent
+ENCODING 267
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+40
+00
+E0
+80
+80
+80
+E0
+ENDCHAR
+STARTCHAR Ccaron
+ENCODING 268
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+E0
+80
+80
+80
+E0
+ENDCHAR
+STARTCHAR ccaron
+ENCODING 269
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+A0
+40
+00
+E0
+80
+80
+E0
+ENDCHAR
+STARTCHAR Dcaron
+ENCODING 270
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+C0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR dcaron
+ENCODING 271
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+30
+30
+20
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR Dcroat
+ENCODING 272
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+60
+50
+50
+F0
+50
+50
+50
+70
+ENDCHAR
+STARTCHAR dcroat
+ENCODING 273
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+20
+70
+20
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR Emacron
+ENCODING 274
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+00
+E0
+80
+E0
+80
+80
+E0
+ENDCHAR
+STARTCHAR emacron
+ENCODING 275
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+E0
+00
+E0
+A0
+E0
+80
+E0
+ENDCHAR
+STARTCHAR Ebreve
+ENCODING 276
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+E0
+80
+E0
+80
+E0
+ENDCHAR
+STARTCHAR ebreve
+ENCODING 277
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+E0
+A0
+E0
+80
+E0
+ENDCHAR
+STARTCHAR Edotaccent
+ENCODING 278
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+00
+E0
+80
+E0
+80
+80
+E0
+ENDCHAR
+STARTCHAR edotaccent
+ENCODING 279
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+40
+00
+E0
+A0
+E0
+80
+E0
+ENDCHAR
+STARTCHAR Eogonek
+ENCODING 280
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 4 8 0 0
+BITMAP
+E0
+80
+80
+E0
+80
+80
+E0
+30
+ENDCHAR
+STARTCHAR eogonek
+ENCODING 281
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 6 0 0
+BITMAP
+E0
+A0
+E0
+80
+E0
+40
+ENDCHAR
+STARTCHAR Ecaron
+ENCODING 282
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+E0
+80
+E0
+80
+E0
+ENDCHAR
+STARTCHAR ecaron
+ENCODING 283
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+E0
+A0
+E0
+80
+E0
+ENDCHAR
+STARTCHAR Gcircumflex
+ENCODING 284
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+40
+A0
+00
+E0
+80
+B0
+90
+F0
+ENDCHAR
+STARTCHAR gcircumflex
+ENCODING 285
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+A0
+00
+E0
+A0
+E0
+20
+60
+ENDCHAR
+STARTCHAR Gbreve
+ENCODING 286
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+A0
+40
+00
+E0
+80
+B0
+90
+F0
+ENDCHAR
+STARTCHAR gbreve
+ENCODING 287
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+E0
+A0
+E0
+20
+60
+ENDCHAR
+STARTCHAR Gdotaccent
+ENCODING 288
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+40
+00
+E0
+80
+80
+B0
+90
+F0
+ENDCHAR
+STARTCHAR gdotaccent
+ENCODING 289
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+40
+00
+E0
+A0
+E0
+20
+60
+ENDCHAR
+STARTCHAR uni0122
+ENCODING 290
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+E0
+80
+80
+B0
+90
+90
+F0
+40
+ENDCHAR
+STARTCHAR uni0123
+ENCODING 291
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+40
+00
+E0
+A0
+E0
+20
+60
+ENDCHAR
+STARTCHAR Hcircumflex
+ENCODING 292
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+A0
+00
+A0
+A0
+E0
+A0
+A0
+ENDCHAR
+STARTCHAR hcircumflex
+ENCODING 293
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+A0
+00
+80
+80
+E0
+A0
+A0
+ENDCHAR
+STARTCHAR Hbar
+ENCODING 294
+SWIDTH 750 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+50
+F8
+50
+70
+50
+50
+50
+50
+ENDCHAR
+STARTCHAR hbar
+ENCODING 295
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+40
+E0
+40
+40
+70
+50
+50
+50
+ENDCHAR
+STARTCHAR Itilde
+ENCODING 296
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+00
+E0
+40
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR itilde
+ENCODING 297
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+E0
+00
+C0
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR Imacron
+ENCODING 298
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+00
+E0
+40
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR imacron
+ENCODING 299
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+E0
+00
+C0
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR Ibreve
+ENCODING 300
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+E0
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR ibreve
+ENCODING 301
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+C0
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR Iogonek
+ENCODING 302
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+40
+40
+40
+40
+40
+E0
+40
+ENDCHAR
+STARTCHAR iogonek
+ENCODING 303
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+40
+00
+C0
+40
+40
+E0
+20
+ENDCHAR
+STARTCHAR Idotaccent
+ENCODING 304
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+00
+E0
+40
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR dotlessi
+ENCODING 305
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+C0
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR IJ
+ENCODING 306
+SWIDTH 875 0
+DWIDTH 7 0
+BBX 6 8 0 0
+BITMAP
+E4
+44
+44
+44
+44
+54
+54
+FC
+ENDCHAR
+STARTCHAR ij
+ENCODING 307
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 5 7 -1 0
+BITMAP
+48
+00
+C8
+48
+48
+68
+F8
+ENDCHAR
+STARTCHAR Jcircumflex
+ENCODING 308
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+20
+50
+00
+20
+20
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR jcircumflex
+ENCODING 309
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 7 0 0
+BITMAP
+20
+50
+00
+20
+20
+A0
+E0
+ENDCHAR
+STARTCHAR uni0136
+ENCODING 310
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+A0
+A0
+C0
+A0
+A0
+A0
+40
+ENDCHAR
+STARTCHAR uni0137
+ENCODING 311
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+80
+80
+A0
+A0
+C0
+A0
+A0
+40
+ENDCHAR
+STARTCHAR kgreenlandic
+ENCODING 312
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 6 0 0
+BITMAP
+A0
+A0
+C0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR Lacute
+ENCODING 313
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+80
+00
+80
+80
+80
+80
+E0
+ENDCHAR
+STARTCHAR lacute
+ENCODING 314
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+80
+00
+C0
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR uni013B
+ENCODING 315
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+80
+80
+80
+80
+80
+80
+E0
+40
+ENDCHAR
+STARTCHAR uni013C
+ENCODING 316
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+C0
+40
+40
+40
+40
+40
+E0
+40
+ENDCHAR
+STARTCHAR Lcaron
+ENCODING 317
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+A0
+80
+80
+80
+80
+80
+E0
+ENDCHAR
+STARTCHAR lcaron
+ENCODING 318
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+D0
+50
+40
+40
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR Ldot
+ENCODING 319
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+80
+80
+A0
+80
+80
+80
+80
+E0
+ENDCHAR
+STARTCHAR ldot
+ENCODING 320
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+C0
+40
+60
+40
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR Lslash
+ENCODING 321
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+40
+40
+60
+C0
+40
+40
+40
+70
+ENDCHAR
+STARTCHAR lslash
+ENCODING 322
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+C0
+40
+60
+C0
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR Nacute
+ENCODING 323
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+20
+40
+00
+90
+D0
+B0
+90
+90
+ENDCHAR
+STARTCHAR nacute
+ENCODING 324
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+40
+00
+E0
+A0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR uni0145
+ENCODING 325
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+90
+90
+D0
+B0
+90
+90
+90
+40
+ENDCHAR
+STARTCHAR uni0146
+ENCODING 326
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+A0
+A0
+A0
+40
+ENDCHAR
+STARTCHAR Ncaron
+ENCODING 327
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+A0
+40
+00
+90
+D0
+B0
+90
+90
+ENDCHAR
+STARTCHAR ncaron
+ENCODING 328
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+E0
+A0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR napostrophe
+ENCODING 329
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+80
+80
+00
+70
+50
+50
+50
+50
+ENDCHAR
+STARTCHAR Eng
+ENCODING 330
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+F0
+90
+90
+90
+90
+90
+90
+20
+ENDCHAR
+STARTCHAR eng
+ENCODING 331
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 6 0 0
+BITMAP
+E0
+A0
+A0
+A0
+A0
+20
+ENDCHAR
+STARTCHAR Omacron
+ENCODING 332
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+00
+E0
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR omacron
+ENCODING 333
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+E0
+00
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR Obreve
+ENCODING 334
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR obreve
+ENCODING 335
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+A0
+40
+00
+E0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR Ohungarumlaut
+ENCODING 336
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+00
+E0
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR ohungarumlaut
+ENCODING 337
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+A0
+00
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR OE
+ENCODING 338
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+F8
+A0
+A0
+B8
+A0
+A0
+A0
+F8
+ENDCHAR
+STARTCHAR oe
+ENCODING 339
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 5 0 0
+BITMAP
+F8
+A8
+B8
+A0
+F8
+ENDCHAR
+STARTCHAR Racute
+ENCODING 340
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+40
+00
+E0
+A0
+C0
+A0
+A0
+ENDCHAR
+STARTCHAR racute
+ENCODING 341
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+40
+00
+E0
+80
+80
+80
+80
+ENDCHAR
+STARTCHAR uni0156
+ENCODING 342
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+A0
+C0
+A0
+A0
+A0
+40
+ENDCHAR
+STARTCHAR uni0157
+ENCODING 343
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 6 0 0
+BITMAP
+E0
+80
+80
+80
+80
+40
+ENDCHAR
+STARTCHAR Rcaron
+ENCODING 344
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+E0
+A0
+C0
+A0
+A0
+ENDCHAR
+STARTCHAR rcaron
+ENCODING 345
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+E0
+80
+80
+80
+80
+ENDCHAR
+STARTCHAR Sacute
+ENCODING 346
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+80
+00
+E0
+80
+E0
+20
+E0
+ENDCHAR
+STARTCHAR sacute
+ENCODING 347
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+40
+00
+E0
+80
+E0
+20
+E0
+ENDCHAR
+STARTCHAR Scircumflex
+ENCODING 348
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+A0
+00
+E0
+80
+E0
+20
+E0
+ENDCHAR
+STARTCHAR scircumflex
+ENCODING 349
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+A0
+00
+E0
+80
+E0
+20
+E0
+ENDCHAR
+STARTCHAR Scedilla
+ENCODING 350
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+80
+80
+E0
+20
+20
+E0
+40
+ENDCHAR
+STARTCHAR scedilla
+ENCODING 351
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 6 0 0
+BITMAP
+E0
+80
+E0
+20
+E0
+40
+ENDCHAR
+STARTCHAR Scaron
+ENCODING 352
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+E0
+80
+E0
+20
+E0
+ENDCHAR
+STARTCHAR scaron
+ENCODING 353
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+E0
+80
+E0
+20
+E0
+ENDCHAR
+STARTCHAR uni0162
+ENCODING 354
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+40
+40
+40
+40
+40
+60
+20
+ENDCHAR
+STARTCHAR uni0163
+ENCODING 355
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+40
+E0
+40
+40
+40
+60
+20
+ENDCHAR
+STARTCHAR Tcaron
+ENCODING 356
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+00
+E0
+40
+40
+40
+40
+40
+ENDCHAR
+STARTCHAR tcaron
+ENCODING 357
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+50
+50
+40
+E0
+40
+40
+40
+60
+ENDCHAR
+STARTCHAR Tbar
+ENCODING 358
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+40
+40
+E0
+40
+40
+40
+40
+ENDCHAR
+STARTCHAR tbar
+ENCODING 359
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+40
+E0
+40
+E0
+40
+40
+60
+ENDCHAR
+STARTCHAR Utilde
+ENCODING 360
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+00
+A0
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR utilde
+ENCODING 361
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+E0
+00
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR Umacron
+ENCODING 362
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+00
+A0
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR umacron
+ENCODING 363
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+E0
+00
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR Ubreve
+ENCODING 364
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR ubreve
+ENCODING 365
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+A0
+40
+00
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR Uring
+ENCODING 366
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+00
+A0
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR uring
+ENCODING 367
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+40
+00
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR Uhungarumlaut
+ENCODING 368
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+00
+A0
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR uhungarumlaut
+ENCODING 369
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+A0
+00
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR Uogonek
+ENCODING 370
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+A0
+A0
+A0
+A0
+A0
+E0
+20
+ENDCHAR
+STARTCHAR uogonek
+ENCODING 371
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+A0
+A0
+A0
+E0
+20
+ENDCHAR
+STARTCHAR Wcircumflex
+ENCODING 372
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+20
+50
+00
+A8
+A8
+A8
+F8
+50
+ENDCHAR
+STARTCHAR wcircumflex
+ENCODING 373
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 7 0 0
+BITMAP
+20
+50
+00
+A8
+A8
+F8
+50
+ENDCHAR
+STARTCHAR Ycircumflex
+ENCODING 374
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+A0
+00
+A0
+A0
+E0
+40
+40
+ENDCHAR
+STARTCHAR ycircumflex
+ENCODING 375
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+A0
+00
+A0
+A0
+E0
+20
+60
+ENDCHAR
+STARTCHAR Ydieresis
+ENCODING 376
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+00
+A0
+A0
+E0
+40
+40
+40
+ENDCHAR
+STARTCHAR Zacute
+ENCODING 377
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+40
+00
+E0
+20
+40
+80
+E0
+ENDCHAR
+STARTCHAR zacute
+ENCODING 378
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+40
+00
+E0
+20
+40
+80
+E0
+ENDCHAR
+STARTCHAR Zdotaccent
+ENCODING 379
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+00
+E0
+20
+40
+80
+80
+E0
+ENDCHAR
+STARTCHAR zdotaccent
+ENCODING 380
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+40
+00
+E0
+20
+40
+80
+E0
+ENDCHAR
+STARTCHAR Zcaron
+ENCODING 381
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+E0
+20
+40
+80
+E0
+ENDCHAR
+STARTCHAR zcaron
+ENCODING 382
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+40
+00
+E0
+20
+40
+80
+E0
+ENDCHAR
+STARTCHAR longs
+ENCODING 383
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+80
+80
+80
+80
+80
+80
+80
+ENDCHAR
+STARTCHAR Alphatonos
+ENCODING 902
+SWIDTH 750 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+B8
+A8
+28
+38
+28
+28
+28
+28
+ENDCHAR
+STARTCHAR Epsilontonos
+ENCODING 904
+SWIDTH 750 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+B8
+A0
+20
+38
+20
+20
+20
+38
+ENDCHAR
+STARTCHAR Etatonos
+ENCODING 905
+SWIDTH 875 0
+DWIDTH 7 0
+BBX 6 8 0 0
+BITMAP
+A4
+A4
+24
+3C
+24
+24
+24
+24
+ENDCHAR
+STARTCHAR Iotatonos
+ENCODING 906
+SWIDTH 750 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+B8
+90
+10
+10
+10
+10
+10
+38
+ENDCHAR
+STARTCHAR Omicrontonos
+ENCODING 908
+SWIDTH 875 0
+DWIDTH 7 0
+BBX 6 8 0 0
+BITMAP
+BC
+A4
+24
+24
+24
+24
+24
+3C
+ENDCHAR
+STARTCHAR Upsilontonos
+ENCODING 910
+SWIDTH 750 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+A8
+A8
+28
+38
+10
+10
+10
+10
+ENDCHAR
+STARTCHAR Omegatonos
+ENCODING 911
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 7 8 0 0
+BITMAP
+BE
+A2
+22
+22
+22
+36
+14
+36
+ENDCHAR
+STARTCHAR Alpha
+ENCODING 913
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+A0
+E0
+A0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR Beta
+ENCODING 914
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+A0
+C0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR Gamma
+ENCODING 915
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+80
+80
+80
+80
+80
+80
+80
+ENDCHAR
+STARTCHAR uni0394
+ENCODING 916
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+20
+20
+50
+50
+88
+88
+88
+F8
+ENDCHAR
+STARTCHAR Epsilon
+ENCODING 917
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+80
+80
+E0
+80
+80
+80
+E0
+ENDCHAR
+STARTCHAR Zeta
+ENCODING 918
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+20
+20
+40
+80
+80
+80
+E0
+ENDCHAR
+STARTCHAR Eta
+ENCODING 919
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+90
+90
+90
+F0
+90
+90
+90
+90
+ENDCHAR
+STARTCHAR Theta
+ENCODING 920
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+F0
+90
+90
+F0
+90
+90
+90
+F0
+ENDCHAR
+STARTCHAR Iota
+ENCODING 921
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+40
+40
+40
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR Kappa
+ENCODING 922
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+A0
+A0
+C0
+A0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR Lambda
+ENCODING 923
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+20
+20
+50
+50
+50
+88
+88
+88
+ENDCHAR
+STARTCHAR Mu
+ENCODING 924
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+88
+D8
+A8
+A8
+88
+88
+88
+88
+ENDCHAR
+STARTCHAR Nu
+ENCODING 925
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+90
+90
+D0
+B0
+90
+90
+90
+90
+ENDCHAR
+STARTCHAR Xi
+ENCODING 926
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+00
+00
+E0
+00
+00
+00
+E0
+ENDCHAR
+STARTCHAR Omicron
+ENCODING 927
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+F0
+90
+90
+90
+90
+90
+90
+F0
+ENDCHAR
+STARTCHAR Pi
+ENCODING 928
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+F0
+90
+90
+90
+90
+90
+90
+90
+ENDCHAR
+STARTCHAR Rho
+ENCODING 929
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+A0
+E0
+80
+80
+80
+80
+ENDCHAR
+STARTCHAR Sigma
+ENCODING 931
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+80
+40
+20
+40
+80
+80
+E0
+ENDCHAR
+STARTCHAR Tau
+ENCODING 932
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+40
+40
+40
+40
+40
+40
+40
+ENDCHAR
+STARTCHAR Upsilon
+ENCODING 933
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+A0
+A0
+E0
+40
+40
+40
+40
+ENDCHAR
+STARTCHAR Phi
+ENCODING 934
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+20
+F8
+A8
+A8
+A8
+A8
+F8
+20
+ENDCHAR
+STARTCHAR Chi
+ENCODING 935
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+A0
+A0
+40
+A0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR Psi
+ENCODING 936
+SWIDTH 750 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+A8
+A8
+A8
+F8
+20
+20
+20
+20
+ENDCHAR
+STARTCHAR uni03A9
+ENCODING 937
+SWIDTH 750 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+F8
+88
+88
+88
+88
+D8
+50
+D8
+ENDCHAR
+STARTCHAR Iotadieresis
+ENCODING 938
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+00
+E0
+40
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR Upsilondieresis
+ENCODING 939
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+00
+A0
+A0
+E0
+40
+40
+40
+ENDCHAR
+STARTCHAR alphatonos
+ENCODING 940
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+20
+40
+00
+D0
+A0
+A0
+A0
+D0
+ENDCHAR
+STARTCHAR epsilontonos
+ENCODING 941
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+40
+00
+E0
+80
+40
+80
+E0
+ENDCHAR
+STARTCHAR etatonos
+ENCODING 942
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+40
+00
+E0
+A0
+A0
+A0
+20
+ENDCHAR
+STARTCHAR iotatonos
+ENCODING 943
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+40
+00
+C0
+40
+40
+40
+60
+ENDCHAR
+STARTCHAR alpha
+ENCODING 945
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 5 0 0
+BITMAP
+D0
+A0
+A0
+A0
+D0
+ENDCHAR
+STARTCHAR beta
+ENCODING 946
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+60
+A0
+A0
+C0
+A0
+A0
+E0
+80
+ENDCHAR
+STARTCHAR gamma
+ENCODING 947
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 5 0 0
+BITMAP
+D0
+50
+70
+20
+20
+ENDCHAR
+STARTCHAR delta
+ENCODING 948
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+80
+40
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR epsilon
+ENCODING 949
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+80
+40
+80
+E0
+ENDCHAR
+STARTCHAR zeta
+ENCODING 950
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+20
+C0
+80
+80
+80
+E0
+20
+ENDCHAR
+STARTCHAR eta
+ENCODING 951
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+A0
+A0
+A0
+20
+ENDCHAR
+STARTCHAR theta
+ENCODING 952
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+A0
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR iota
+ENCODING 953
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+C0
+40
+40
+40
+60
+ENDCHAR
+STARTCHAR kappa
+ENCODING 954
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+A0
+A0
+C0
+A0
+A0
+ENDCHAR
+STARTCHAR lambda
+ENCODING 955
+SWIDTH 750 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+60
+20
+20
+50
+50
+88
+88
+88
+ENDCHAR
+STARTCHAR uni03BC
+ENCODING 956
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+A0
+A0
+A0
+E0
+80
+ENDCHAR
+STARTCHAR nu
+ENCODING 957
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+A0
+A0
+A0
+E0
+40
+ENDCHAR
+STARTCHAR xi
+ENCODING 958
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+80
+E0
+80
+60
+80
+80
+E0
+20
+ENDCHAR
+STARTCHAR omicron
+ENCODING 959
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR pi
+ENCODING 960
+SWIDTH 750 0
+DWIDTH 6 0
+BBX 5 5 0 0
+BITMAP
+F8
+50
+50
+50
+58
+ENDCHAR
+STARTCHAR rho
+ENCODING 961
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+A0
+A0
+E0
+80
+ENDCHAR
+STARTCHAR sigma1
+ENCODING 962
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+80
+80
+E0
+20
+ENDCHAR
+STARTCHAR sigma
+ENCODING 963
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 5 0 0
+BITMAP
+F0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR tau
+ENCODING 964
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+40
+40
+40
+60
+ENDCHAR
+STARTCHAR upsilon
+ENCODING 965
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR phi
+ENCODING 966
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 5 0 0
+BITMAP
+B8
+A8
+A8
+F8
+20
+ENDCHAR
+STARTCHAR chi
+ENCODING 967
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+A0
+A0
+40
+A0
+A0
+ENDCHAR
+STARTCHAR psi
+ENCODING 968
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 5 0 0
+BITMAP
+A8
+A8
+A8
+F8
+20
+ENDCHAR
+STARTCHAR omega
+ENCODING 969
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 5 0 0
+BITMAP
+88
+A8
+A8
+F8
+50
+ENDCHAR
+STARTCHAR iotadieresis
+ENCODING 970
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+A0
+00
+C0
+40
+40
+40
+60
+ENDCHAR
+STARTCHAR upsilondieresis
+ENCODING 971
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+A0
+00
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR omicrontonos
+ENCODING 972
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+40
+00
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR upsilontonos
+ENCODING 973
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+20
+40
+00
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR omegatonos
+ENCODING 974
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+10
+20
+00
+88
+A8
+A8
+F8
+50
+ENDCHAR
+STARTCHAR uni0401
+ENCODING 1025
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+00
+E0
+80
+E0
+80
+80
+E0
+ENDCHAR
+STARTCHAR uni0404
+ENCODING 1028
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+70
+80
+80
+E0
+80
+80
+80
+70
+ENDCHAR
+STARTCHAR uni0406
+ENCODING 1030
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+40
+40
+40
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR uni0407
+ENCODING 1031
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+00
+E0
+40
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR uni040E
+ENCODING 1038
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+00
+A0
+A0
+E0
+20
+20
+60
+ENDCHAR
+STARTCHAR uni0410
+ENCODING 1040
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+A0
+E0
+A0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR uni0411
+ENCODING 1041
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+80
+80
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR uni0412
+ENCODING 1042
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+A0
+C0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR uni0413
+ENCODING 1043
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+80
+80
+80
+80
+80
+80
+80
+ENDCHAR
+STARTCHAR uni0414
+ENCODING 1044
+SWIDTH 750 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+30
+50
+50
+50
+50
+50
+F8
+88
+ENDCHAR
+STARTCHAR uni0415
+ENCODING 1045
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+80
+80
+E0
+80
+80
+80
+E0
+ENDCHAR
+STARTCHAR uni0416
+ENCODING 1046
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+A8
+A8
+A8
+F8
+A8
+A8
+A8
+A8
+ENDCHAR
+STARTCHAR uni0417
+ENCODING 1047
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+20
+40
+20
+20
+A0
+E0
+ENDCHAR
+STARTCHAR uni0418
+ENCODING 1048
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+88
+88
+98
+A8
+C8
+88
+88
+88
+ENDCHAR
+STARTCHAR uni0419
+ENCODING 1049
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+A8
+88
+98
+A8
+C8
+88
+88
+88
+ENDCHAR
+STARTCHAR uni041A
+ENCODING 1050
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+A0
+A0
+C0
+A0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR uni041B
+ENCODING 1051
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+30
+50
+50
+50
+50
+50
+50
+D0
+ENDCHAR
+STARTCHAR uni041C
+ENCODING 1052
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+88
+D8
+A8
+A8
+88
+88
+88
+88
+ENDCHAR
+STARTCHAR uni041D
+ENCODING 1053
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+90
+90
+90
+F0
+90
+90
+90
+90
+ENDCHAR
+STARTCHAR uni041E
+ENCODING 1054
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+F0
+90
+90
+90
+90
+90
+90
+F0
+ENDCHAR
+STARTCHAR uni041F
+ENCODING 1055
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+F0
+90
+90
+90
+90
+90
+90
+90
+ENDCHAR
+STARTCHAR uni0420
+ENCODING 1056
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+A0
+E0
+80
+80
+80
+80
+ENDCHAR
+STARTCHAR uni0421
+ENCODING 1057
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+80
+80
+80
+80
+A0
+E0
+ENDCHAR
+STARTCHAR uni0422
+ENCODING 1058
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+40
+40
+40
+40
+40
+40
+40
+ENDCHAR
+STARTCHAR uni0423
+ENCODING 1059
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+90
+90
+90
+F0
+10
+10
+10
+70
+ENDCHAR
+STARTCHAR uni0424
+ENCODING 1060
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+20
+F8
+A8
+A8
+A8
+A8
+F8
+20
+ENDCHAR
+STARTCHAR uni0425
+ENCODING 1061
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+A0
+A0
+40
+A0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR uni0426
+ENCODING 1062
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+A0
+A0
+A0
+A0
+A0
+A0
+A0
+F0
+ENDCHAR
+STARTCHAR uni0427
+ENCODING 1063
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+A0
+A0
+E0
+20
+20
+20
+20
+ENDCHAR
+STARTCHAR uni0428
+ENCODING 1064
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+A8
+A8
+A8
+A8
+A8
+A8
+A8
+F8
+ENDCHAR
+STARTCHAR uni0429
+ENCODING 1065
+SWIDTH 1000 0
+DWIDTH 7 0
+BBX 6 8 0 0
+BITMAP
+A8
+A8
+A8
+A8
+A8
+A8
+A8
+FC
+ENDCHAR
+STARTCHAR uni042A
+ENCODING 1066
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+C0
+40
+40
+70
+50
+50
+50
+70
+ENDCHAR
+STARTCHAR uni042B
+ENCODING 1067
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+88
+88
+88
+E8
+A8
+A8
+A8
+E8
+ENDCHAR
+STARTCHAR uni042C
+ENCODING 1068
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+80
+80
+80
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR uni042D
+ENCODING 1069
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+E0
+10
+10
+70
+10
+10
+10
+E0
+ENDCHAR
+STARTCHAR uni042E
+ENCODING 1070
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+B8
+A8
+A8
+E8
+A8
+A8
+A8
+B8
+ENDCHAR
+STARTCHAR uni042F
+ENCODING 1071
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+A0
+A0
+60
+A0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR uni0430
+ENCODING 1072
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+20
+E0
+A0
+E0
+ENDCHAR
+STARTCHAR uni0431
+ENCODING 1073
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+60
+80
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR uni0432
+ENCODING 1074
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+A0
+C0
+A0
+E0
+ENDCHAR
+STARTCHAR uni0433
+ENCODING 1075
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+80
+80
+80
+80
+ENDCHAR
+STARTCHAR uni0434
+ENCODING 1076
+SWIDTH 750 0
+DWIDTH 6 0
+BBX 5 5 0 0
+BITMAP
+30
+50
+50
+F8
+88
+ENDCHAR
+STARTCHAR uni0435
+ENCODING 1077
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+A0
+E0
+80
+E0
+ENDCHAR
+STARTCHAR uni0436
+ENCODING 1078
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 5 0 0
+BITMAP
+A8
+A8
+F8
+A8
+A8
+ENDCHAR
+STARTCHAR uni0437
+ENCODING 1079
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+20
+40
+20
+E0
+ENDCHAR
+STARTCHAR uni0438
+ENCODING 1080
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 5 0 0
+BITMAP
+90
+B0
+D0
+90
+90
+ENDCHAR
+STARTCHAR uni0439
+ENCODING 1081
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 7 0 0
+BITMAP
+60
+00
+90
+B0
+D0
+90
+90
+ENDCHAR
+STARTCHAR uni043A
+ENCODING 1082
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+A0
+A0
+C0
+A0
+A0
+ENDCHAR
+STARTCHAR uni043B
+ENCODING 1083
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 5 0 0
+BITMAP
+30
+50
+50
+50
+D0
+ENDCHAR
+STARTCHAR uni043C
+ENCODING 1084
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 5 0 0
+BITMAP
+88
+D8
+A8
+88
+88
+ENDCHAR
+STARTCHAR uni043D
+ENCODING 1085
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+A0
+A0
+E0
+A0
+A0
+ENDCHAR
+STARTCHAR uni043E
+ENCODING 1086
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR uni043F
+ENCODING 1087
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+A0
+A0
+A0
+A0
+ENDCHAR
+STARTCHAR uni0440
+ENCODING 1088
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+A0
+E0
+80
+80
+ENDCHAR
+STARTCHAR uni0441
+ENCODING 1089
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+80
+80
+80
+E0
+ENDCHAR
+STARTCHAR uni0442
+ENCODING 1090
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+40
+40
+40
+40
+ENDCHAR
+STARTCHAR uni0443
+ENCODING 1091
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+A0
+A0
+E0
+20
+60
+ENDCHAR
+STARTCHAR uni0444
+ENCODING 1092
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+20
+20
+20
+F8
+A8
+A8
+F8
+20
+ENDCHAR
+STARTCHAR uni0445
+ENCODING 1093
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+A0
+A0
+40
+A0
+A0
+ENDCHAR
+STARTCHAR uni0446
+ENCODING 1094
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 5 0 0
+BITMAP
+A0
+A0
+A0
+A0
+F0
+ENDCHAR
+STARTCHAR uni0447
+ENCODING 1095
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+A0
+A0
+E0
+20
+20
+ENDCHAR
+STARTCHAR uni0448
+ENCODING 1096
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 5 0 0
+BITMAP
+A8
+A8
+A8
+A8
+F8
+ENDCHAR
+STARTCHAR uni0449
+ENCODING 1097
+SWIDTH 1000 0
+DWIDTH 7 0
+BBX 6 5 0 0
+BITMAP
+A8
+A8
+A8
+A8
+FC
+ENDCHAR
+STARTCHAR uni044A
+ENCODING 1098
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 5 0 0
+BITMAP
+C0
+40
+70
+50
+70
+ENDCHAR
+STARTCHAR uni044B
+ENCODING 1099
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 5 0 0
+BITMAP
+88
+88
+E8
+A8
+E8
+ENDCHAR
+STARTCHAR uni044C
+ENCODING 1100
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+80
+80
+E0
+A0
+E0
+ENDCHAR
+STARTCHAR uni044D
+ENCODING 1101
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+C0
+20
+E0
+20
+C0
+ENDCHAR
+STARTCHAR uni044E
+ENCODING 1102
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 5 0 0
+BITMAP
+B8
+A8
+E8
+A8
+B8
+ENDCHAR
+STARTCHAR uni044F
+ENCODING 1103
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+E0
+A0
+60
+A0
+A0
+ENDCHAR
+STARTCHAR uni0451
+ENCODING 1105
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+A0
+00
+E0
+A0
+E0
+80
+E0
+ENDCHAR
+STARTCHAR uni0454
+ENCODING 1108
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 5 0 0
+BITMAP
+60
+80
+E0
+80
+60
+ENDCHAR
+STARTCHAR uni0456
+ENCODING 1110
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+40
+00
+C0
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR uni0457
+ENCODING 1111
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+A0
+00
+C0
+40
+40
+40
+E0
+ENDCHAR
+STARTCHAR uni045E
+ENCODING 1118
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+40
+00
+A0
+A0
+E0
+20
+60
+ENDCHAR
+STARTCHAR uni0490
+ENCODING 1168
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+10
+E0
+80
+80
+80
+80
+80
+80
+ENDCHAR
+STARTCHAR uni0491
+ENCODING 1169
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 6 0 0
+BITMAP
+20
+C0
+80
+80
+80
+80
+ENDCHAR
+STARTCHAR uni2002
+ENCODING 8194
+SWIDTH 375 0
+DWIDTH 3 0
+BBX 1 1 0 0
+BITMAP
+00
+ENDCHAR
+STARTCHAR uni2003
+ENCODING 8195
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 1 1 0 0
+BITMAP
+00
+ENDCHAR
+STARTCHAR uni2009
+ENCODING 8201
+SWIDTH 1000 0
+DWIDTH 1 0
+BBX 1 1 6 0
+BITMAP
+00
+ENDCHAR
+STARTCHAR uni2010
+ENCODING 8208
+SWIDTH 1000 0
+DWIDTH 1 0
+BBX 1 1 0 4
+BITMAP
+80
+ENDCHAR
+STARTCHAR endash
+ENCODING 8211
+SWIDTH 1000 0
+DWIDTH 3 0
+BBX 2 1 0 4
+BITMAP
+C0
+ENDCHAR
+STARTCHAR emdash
+ENCODING 8212
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 1 0 4
+BITMAP
+F0
+ENDCHAR
+STARTCHAR uni2015
+ENCODING 8213
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 4 1 0 4
+BITMAP
+F0
+ENDCHAR
+STARTCHAR bullet
+ENCODING 8226
+SWIDTH 1000 0
+DWIDTH 2 0
+BBX 1 1 0 4
+BITMAP
+80
+ENDCHAR
+STARTCHAR colonmonetary
+ENCODING 8353
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+10
+F0
+A0
+A0
+A0
+A0
+F0
+40
+ENDCHAR
+STARTCHAR uni20A2
+ENCODING 8354
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+F0
+80
+80
+B0
+A0
+A0
+A0
+F0
+ENDCHAR
+STARTCHAR uni20A6
+ENCODING 8358
+SWIDTH 875 0
+DWIDTH 7 0
+BBX 6 8 0 0
+BITMAP
+48
+48
+EC
+58
+CC
+48
+48
+48
+ENDCHAR
+STARTCHAR uni20A9
+ENCODING 8361
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 7 8 0 0
+BITMAP
+54
+54
+FE
+54
+54
+54
+7C
+28
+ENDCHAR
+STARTCHAR uni20AA
+ENCODING 8362
+SWIDTH 875 0
+DWIDTH 7 0
+BBX 6 6 0 0
+BITMAP
+F4
+94
+B4
+B4
+A4
+BC
+ENDCHAR
+STARTCHAR dong
+ENCODING 8363
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+20
+70
+20
+E0
+A0
+E0
+00
+E0
+ENDCHAR
+STARTCHAR Euro
+ENCODING 8364
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+30
+60
+40
+F0
+40
+40
+60
+30
+ENDCHAR
+STARTCHAR uni20AD
+ENCODING 8365
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+50
+50
+60
+F0
+60
+50
+50
+50
+ENDCHAR
+STARTCHAR uni20AE
+ENCODING 8366
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+40
+60
+40
+C0
+40
+40
+40
+ENDCHAR
+STARTCHAR uni20B1
+ENCODING 8369
+SWIDTH 750 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+70
+D8
+50
+70
+40
+40
+40
+40
+ENDCHAR
+STARTCHAR uni20B2
+ENCODING 8370
+SWIDTH 1000 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+40
+E0
+80
+B0
+90
+90
+F0
+40
+ENDCHAR
+STARTCHAR uni20B4
+ENCODING 8372
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+70
+50
+10
+F8
+40
+50
+50
+70
+ENDCHAR
+STARTCHAR uni20B5
+ENCODING 8373
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 7 0 0
+BITMAP
+40
+E0
+80
+80
+80
+E0
+40
+ENDCHAR
+STARTCHAR uni20B8
+ENCODING 8376
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+E0
+00
+E0
+40
+40
+40
+40
+40
+ENDCHAR
+STARTCHAR uni20B9
+ENCODING 8377
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+F0
+20
+F0
+80
+40
+20
+10
+10
+ENDCHAR
+STARTCHAR uni20BA
+ENCODING 8378
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+E0
+40
+E0
+40
+40
+50
+50
+70
+ENDCHAR
+STARTCHAR uni20BC
+ENCODING 8380
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 7 0 0
+BITMAP
+20
+20
+F8
+A8
+A8
+A8
+A8
+ENDCHAR
+STARTCHAR uni20BD
+ENCODING 8381
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+70
+50
+70
+40
+E0
+40
+40
+40
+ENDCHAR
+STARTCHAR uni20BE
+ENCODING 8382
+SWIDTH 750 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+20
+F8
+A8
+A8
+A0
+80
+40
+F8
+ENDCHAR
+STARTCHAR uni20BF
+ENCODING 8383
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+40
+E0
+A0
+C0
+A0
+A0
+E0
+40
+ENDCHAR
+STARTCHAR uni20C0
+ENCODING 8384
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 3 6 0 0
+BITMAP
+E0
+80
+80
+E0
+00
+E0
+ENDCHAR
+STARTCHAR uni2103
+ENCODING 8451
+SWIDTH 750 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+B8
+A8
+20
+20
+20
+20
+28
+38
+ENDCHAR
+STARTCHAR uni2109
+ENCODING 8457
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+B8
+A0
+20
+38
+20
+20
+20
+20
+ENDCHAR
+STARTCHAR uni2460
+ENCODING 9312
+SWIDTH 125 0
+DWIDTH 1 0
+BBX 1 1 4 0
+BITMAP
+00
+ENDCHAR
+STARTCHAR uni2461
+ENCODING 9313
+SWIDTH 250 0
+DWIDTH 2 0
+BBX 1 1 1 0
+BITMAP
+00
+ENDCHAR
+STARTCHAR uni2462
+ENCODING 9314
+SWIDTH 375 0
+DWIDTH 3 0
+BBX 1 1 3 0
+BITMAP
+00
+ENDCHAR
+STARTCHAR uni2463
+ENCODING 9315
+SWIDTH 500 0
+DWIDTH 4 0
+BBX 1 1 4 0
+BITMAP
+00
+ENDCHAR
+STARTCHAR uni2464
+ENCODING 9316
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 1 1 4 0
+BITMAP
+00
+ENDCHAR
+STARTCHAR uni2465
+ENCODING 9317
+SWIDTH 750 0
+DWIDTH 6 0
+BBX 1 1 1 0
+BITMAP
+00
+ENDCHAR
+STARTCHAR uni2466
+ENCODING 9318
+SWIDTH 875 0
+DWIDTH 7 0
+BBX 1 1 3 0
+BITMAP
+00
+ENDCHAR
+STARTCHAR uni2467
+ENCODING 9319
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 1 1 4 0
+BITMAP
+00
+ENDCHAR
+STARTCHAR uni2468
+ENCODING 9320
+SWIDTH 1125 0
+DWIDTH 9 0
+BBX 1 1 4 0
+BITMAP
+00
+ENDCHAR
+STARTCHAR uni2469
+ENCODING 9321
+SWIDTH 1250 0
+DWIDTH 10 0
+BBX 1 1 4 0
+BITMAP
+00
+ENDCHAR
+STARTCHAR uni24EA
+ENCODING 9450
+SWIDTH 0 0
+DWIDTH 0 0
+BBX 1 1 3 0
+BITMAP
+00
+ENDCHAR
+STARTCHAR H22073
+ENCODING 9633
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 6 0 1
+BITMAP
+E0
+A0
+A0
+A0
+A0
+E0
+ENDCHAR
+STARTCHAR uni4E00
+ENCODING 19968
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 7 2 0 3
+BITMAP
+04
+FE
+ENDCHAR
+STARTCHAR uni4E03
+ENCODING 19971
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 7 8 0 0
+BITMAP
+20
+20
+2E
+F0
+20
+20
+22
+1E
+ENDCHAR
+STARTCHAR uni4E09
+ENCODING 19977
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 7 8 0 0
+BITMAP
+7C
+00
+00
+38
+00
+00
+04
+FE
+ENDCHAR
+STARTCHAR uni4E0A
+ENCODING 19978
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+40
+40
+70
+40
+40
+40
+40
+F0
+ENDCHAR
+STARTCHAR uni4E0B
+ENCODING 19979
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 4 8 0 0
+BITMAP
+F0
+40
+60
+50
+40
+40
+40
+40
+ENDCHAR
+STARTCHAR uni4E5D
+ENCODING 20061
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 7 8 0 0
+BITMAP
+20
+F8
+28
+28
+48
+48
+4A
+8E
+ENDCHAR
+STARTCHAR uni4E8C
+ENCODING 20108
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 7 6 0 1
+BITMAP
+7C
+00
+00
+00
+04
+FE
+ENDCHAR
+STARTCHAR uni4E94
+ENCODING 20116
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 7 8 0 0
+BITMAP
+7C
+10
+10
+7C
+24
+24
+24
+FE
+ENDCHAR
+STARTCHAR uni516B
+ENCODING 20843
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 7 8 0 0
+BITMAP
+28
+28
+28
+28
+44
+44
+44
+82
+ENDCHAR
+STARTCHAR uni516D
+ENCODING 20845
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 7 8 0 0
+BITMAP
+10
+FE
+00
+28
+28
+44
+44
+82
+ENDCHAR
+STARTCHAR uni5341
+ENCODING 21313
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 7 8 0 0
+BITMAP
+10
+10
+FE
+10
+10
+10
+10
+10
+ENDCHAR
+STARTCHAR uni5348
+ENCODING 21320
+SWIDTH 625 0
+DWIDTH 5 0
+BBX 5 8 0 0
+BITMAP
+40
+70
+A0
+20
+F8
+20
+20
+20
+ENDCHAR
+STARTCHAR uni56DB
+ENCODING 22235
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 7 8 0 0
+BITMAP
+FE
+AA
+AA
+AE
+C2
+82
+FE
+82
+ENDCHAR
+STARTCHAR uni5929
+ENCODING 22825
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 7 8 0 0
+BITMAP
+7C
+10
+10
+FE
+10
+10
+28
+C6
+ENDCHAR
+STARTCHAR uni661F
+ENCODING 26143
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 7 8 0 0
+BITMAP
+FC
+84
+FC
+5E
+90
+7C
+10
+FE
+ENDCHAR
+STARTCHAR uni6708
+ENCODING 26376
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 7 8 0 0
+BITMAP
+3E
+22
+3E
+22
+3E
+22
+42
+86
+ENDCHAR
+STARTCHAR uni671F
+ENCODING 26399
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 7 8 0 0
+BITMAP
+5E
+FA
+5E
+7A
+5E
+EA
+12
+A6
+ENDCHAR
+STARTCHAR uniAE08
+ENCODING 44552
+SWIDTH 750 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+F8
+08
+00
+F8
+00
+F8
+88
+F8
+ENDCHAR
+STARTCHAR uniBAA9
+ENCODING 47785
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+F8
+88
+F8
+20
+F8
+00
+F8
+08
+ENDCHAR
+STARTCHAR uniC218
+ENCODING 49688
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+20
+20
+50
+88
+00
+F8
+20
+20
+ENDCHAR
+STARTCHAR uniC624
+ENCODING 50724
+SWIDTH 750 0
+DWIDTH 6 0
+BBX 5 7 0 1
+BITMAP
+70
+88
+88
+70
+20
+20
+F8
+ENDCHAR
+STARTCHAR uniC694
+ENCODING 50836
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 7 0 1
+BITMAP
+70
+88
+88
+70
+50
+50
+F8
+ENDCHAR
+STARTCHAR uniC6D4
+ENCODING 50900
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+48
+A8
+48
+E8
+58
+38
+C0
+F8
+ENDCHAR
+STARTCHAR uniC77C
+ENCODING 51068
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+48
+A8
+48
+00
+F8
+38
+C0
+F8
+ENDCHAR
+STARTCHAR uniC804
+ENCODING 51204
+SWIDTH 875 0
+DWIDTH 7 0
+BBX 6 8 0 0
+BITMAP
+F4
+24
+4C
+A4
+94
+44
+40
+7C
+ENDCHAR
+STARTCHAR uniD1A0
+ENCODING 53664
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 7 0 1
+BITMAP
+F8
+80
+F0
+80
+F8
+20
+F8
+ENDCHAR
+STARTCHAR uniD654
+ENCODING 54868
+SWIDTH 1000 0
+DWIDTH 8 0
+BBX 7 8 0 0
+BITMAP
+24
+FC
+54
+56
+24
+24
+FC
+04
+ENDCHAR
+STARTCHAR uniD6C4
+ENCODING 54980
+SWIDTH 1000 0
+DWIDTH 6 0
+BBX 5 8 0 0
+BITMAP
+20
+F8
+50
+20
+00
+F8
+20
+20
+ENDCHAR
+STARTCHAR uniFFE5
+ENCODING 65509
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 8 0 0
+BITMAP
+A0
+A0
+E0
+40
+E0
+40
+40
+40
+ENDCHAR
+STARTCHAR uniFFFD
+ENCODING 65533
+SWIDTH 1000 0
+DWIDTH 4 0
+BBX 3 6 0 1
+BITMAP
+A0
+A0
+40
+A0
+A0
+A0
+ENDCHAR
+ENDFONT
diff --git a/tests/components/font/common.yaml b/tests/components/font/common.yaml
index a81457a05d..5be9faf5be 100644
--- a/tests/components/font/common.yaml
+++ b/tests/components/font/common.yaml
@@ -1,4 +1,12 @@
 font:
+  - file:
+      type: gfonts
+      family: "Roboto"
+      weight: bold
+      italic: true
+    size: 32
+    id: roboto32
+
   - file: "gfonts://Roboto"
     id: roboto
     size: 20
@@ -9,6 +17,10 @@ font:
   - file: "gfonts://Roboto"
     id: roboto_web
     size: 20
+  - file: "gfonts://Roboto"
+    id: roboto_greek
+    size: 20
+    glyphs: ["\u0300", "\u00C5", "\U000000C7"]
   - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf"
     id: monocraft
     size: 20
@@ -20,6 +32,17 @@ font:
   - file: $component_dir/Monocraft.ttf
     id: monocraft3
     size: 28
+  - file: $component_dir/MatrixChunky8X.bdf
+    id: special_font
+    glyphs:
+      - '"'
+      - "'"
+      - '#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz°'
+
+  - file: $component_dir/MatrixChunky8X.bdf
+    id: default_font
+  - file: $component_dir/x11.pcf
+    id: pcf_font
 
 i2c:
   scl: ${i2c_scl}
@@ -36,3 +59,4 @@ display:
       it.print(0, 40, id(monocraft), "Hello, World!");
       it.print(0, 60, id(monocraft2), "Hello, World!");
       it.print(0, 80, id(monocraft3), "Hello, World!");
+      it.print(0, 100, id(roboto_greek), "Hello κόσμε!");
diff --git a/tests/components/font/test.host.yaml b/tests/components/font/test.host.yaml
index 017328ec83..c5399f2826 100644
--- a/tests/components/font/test.host.yaml
+++ b/tests/components/font/test.host.yaml
@@ -1,4 +1,12 @@
 font:
+  - file:
+      type: gfonts
+      family: "Roboto"
+      weight: bold
+      italic: true
+    size: 32
+    id: roboto32
+
   - file: "gfonts://Roboto"
     id: roboto
     size: 20
@@ -9,6 +17,10 @@ font:
   - file: "gfonts://Roboto"
     id: roboto_web
     size: 20
+  - file: "gfonts://Roboto"
+    id: roboto_greek
+    size: 20
+    glyphs: ["\u0300", "\u00C5", "\U000000C7"]
   - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf"
     id: monocraft
     size: 20
@@ -20,4 +32,26 @@ font:
   - file: $component_dir/Monocraft.ttf
     id: monocraft3
     size: 28
+  - file: $component_dir/MatrixChunky8X.bdf
+    id: special_font
+    glyphs:
+      - '"'
+      - "'"
+      - '#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz°'
 
+  - file: $component_dir/MatrixChunky8X.bdf
+    id: default_font
+
+display:
+  - platform: sdl
+    id: sdl_display
+    dimensions:
+      width: 800
+      height: 600
+    lambda: |-
+      it.print(0, 0, id(roboto), "Hello, World!");
+      it.print(0, 20, id(roboto_web), "Hello, World!");
+      it.print(0, 40, id(roboto_greek), "Hello κόσμε!");
+      it.print(0, 60, id(monocraft), "Hello, World!");
+      it.print(0, 80, id(monocraft2), "Hello, World!");
+      it.print(0, 100, id(monocraft3), "Hello, World!");
diff --git a/tests/components/font/x11.pcf b/tests/components/font/x11.pcf
new file mode 100644
index 0000000000000000000000000000000000000000..19a38d4e3918b6bb4796fb23d34a24e2a8fa9656
GIT binary patch
literal 13368
zcmbuG4Rl;bb;oD58-K)(tvF6BJBq!DgJZx-<zR@Mny|u>ZGj>gOHOPE=&g5G(&CkN
zvnx4r;@D4XSznfeo2E1=X=^AYfu_{7lm?o%56E#$j|m3S9JfG^&PgbtC*=r|5>e8)
zzyJGKFD8*)cqhL%cV_O~J9F>Md%N-`)S1Z5F~)>o0oY+<Mky=&dXjQZ#2EfF3s5>q
ze};<2NdNKW#;l_f_3M=koLps0%&*Tue`+;$gMM_U-%bTv8lQwY>pU5;8r-?K_BE+x
zaIJ(IxJg1C+#+EMv`Y90bbxE|qY`AcKM;z6@G&X>Av~;>7I;)GPk<xg+wfg5m%_hG
zm<>OYa1;DO!Y1Z7SHfw8N=Aj4VW>vJ%}|su2H%hlSLk2V@;v-ELd86YN_Y!cQW!_r
zErBIh?Ag@1y`^E>PE()E?n;?W^|v)O@9b!)zpc@1@5|=WadSsw)8;MhUekNCnWSlJ
zu5W2KO{T4}{f?%F_AOplOE%vV&zSm#hK{!Ooz0D2tJ&JLqp`W8t?7e}W@~FxOS>m-
zYiw(6-rnBS+S0MZlvQ?`wypIWn_4!Tt)~9=#%=YR8#`PdX5*InZ8+@M*0{N;t)2Nc
zwYApOy>or3qp@XUYlB9vH5==<c5HEHZFU60w#MfA_NLn#JAzr`)^G6^=&zFqZ1m^u
ztv{+YN1OWMnRLQzY~8wZo5Eo>W^;S<>8|d+=<38<qifgKtzW}^{o1G-$7tK$LSL$<
z5N+yBWb?UfKHir~-WZM6XEM=k8dZpHOBGW2J+zvZ*7l~2LAayoLNp%Dr@GP@$fuIg
zzI;5H>WSxfN3)$X88;1$ZS74hEV8x5bksMrzPGVsYulz)QsphAMho%R(b3Sj5pKtC
zhg6jBHnX+u-M_BtTBvbXRa4em(^TVSyryRDx|+4MwI#OJ++0`VXQyUuS$53_w%0fK
z=`5W;?L%Fc0jJ!A&%W;7&z_xDX%^CRncGA4|9k2AVVuE<!(+B>AGGbkLqp?sab$S>
zp24xvaa*^+;*p8*!NJ4hV|H=q;P}18;X~ua!9(1Nqa)+@4vvnFFm&jk+J^2MJUCRe
zZCPRLm_0bc%^sC>?&fyrRxC+g@6ZsYxD{Qa+zuUZrQ+x?_9hOG+xGaFe=ABFJWSi5
zD~&VV;^A?sN7OcQ2t97a!3ni#G4}mOxT#t!4U=@xGcf8Jz-Up9iW8DPrV)oF>E=2g
z)bfkNOvqLYH1YlR$jAY=Z7+^SJa2=e!+|t1GGZSXJv4sM9vc}sSRBKY;%6Vg)IoP@
zZnb#i2qVVFxGAPGrCx3$GUeWe4-rYJ5PNPDhY6S@-45DvS=6G72T4FMzeD@o)GDwz
zS3ca>kB*KWD2^)&QpTkcIy^q$E+22B`--EZ!#GkFlv*z?Bg10@Y=N<RF@@jZ;jw*#
zx*cTG>?K0@AQi>p(9qES;?VHm5V1l<_Sg*An;@nv)xTkYiI0yRQFPtggd}cb!R^5P
z6DX79Ay$4w%Xe=RL<ki!%`n{tM-MSfH+Nf*=?P`}Na^NQgPXfH?E{vDihrYb;;>Xa
zALI6Z{Q5ca55!?VzvrsTg3>)SI5MKG=iSibe0Z7Grm?xvrj4m!``gx){9CzY!tU;a
z(LvT5D36b!qAKb3W<}1CO3w<%KmXTLie|VIhTu4SpLZTbXfZTEAB@7Mfmd$mNq7og
z;=NZ1d<v-80sG(>&{y#y51si?3+<4D2LbzG2k;Szz#5=G!q^D*Bd6dMV=k$N2FL*8
zF2Uv{FBvmyF>HV&Ou!cbU$dFV>^iW3xz5JV>@(h{ojI#1w}K6if!gsgXWE##5s1PT
zz~)?R&P8V~^ShM3OV>a<i~=?<W$dMlork}9^v$Dh9&?}f5PT8npGW_E`sdR>AG`B&
zfZh2|!V5g27Xv<B=)WujN8xce4a{r7YN&-B&<)Ie0XhpF1newu0Nn*I86NF`&4t)p
zh`ogafc`>s7d{400J;mYTZv93I+Z#2G@LQ!@;Oik-S7}NfbOC-U;%y?(YELnW3E^Y
zJ75%!!=y2b7sD19fXCnrOc`@!6*Pkl=w6BLRp?%|0dnvVVD~E4u!Qy{v@hv~AwYi#
zb}(V8m`7DR>;vXpbsDCPSsDfWF2&Z;hu|r|)-r4@s|Cg^W6UzfEMv?v#$1i9t1a^T
zRpR{5$PXHG^XktX!wr`mb&Fps)sI0JsR#>VpeqKB&lyT_)JLC47x^5ZyxvEzXD08$
z<lI+tHKjgc69mJ=&8=`1JO|H09Yn#{#=Hl1B7O(dmv-2B7QSFr%$SX~H?R4|^S{n9
z=AE?NNBsxz4Cn*$Ui;kK5oOw?l2U87Am-yx+HXLNwy2NBolnqb(H8?7q;n_8$BPh!
z@^Ok!dEKHO5JR?M3LKv~l!1ONa@5D7jQQA<%i$f6@Ohf@gwH9;0Z_~~Kvfxvz3hr<
z2<*t#Y|wWi>0b-`LGzc5PM8H6uTq=<`3l-Jwi>R7HP8$iFMG1_9=H{vp!y~#w=+O}
zIRxuc{Xal{gzU-JevsV=1o4U@TQCVRkevc3KAjM(L3T7}+16ZFf%?U*Ae-`Y5)>oL
zCq}6&298e<N2=~+aSw<g+fV_Di{?G)BY75R4Z$VZhDlJr#suRWWX)Ieo&fb*;J_5<
zngj>*#H@?4?x-<6$Bg+V<-fxBV#a*f_F}(+GHg6%Q;K<T1*q>@pO+o)d{$A4rLe*n
zvmVyk9?F&GzFch(hZs0zd=0r1f<3U*_Uy@KtxsT2Ig`BuHtT>|xQY6$pwa^QNPzV8
z{HO|`t~ms{LH?#`)4b)w0tZ<<Z5#7-l@QEt0|BkFy!q8R*ayvVt+4S8FC%L$RX)K!
z2uh1~fhjjG^C<(}@-om@X@LXxO%h+*I|J*e%a7ROlc4;m{j2`#XbWuRkne&ZjvK1k
z=O~E9J9rKsWzQcs<^~3cRhBWYOkoeIqYyLZMpy+mz)IVro>G378!yI+gZQeC;wUzO
z=Bpei&svM(rhTHe99TZMyr8`xFD%1758A7Gj>vXEU|Xdqfaa<(*TF83-+iF@5Boet
zIRIyE?=LbBBUb^TD#f!FS!1W+myWk)ty_5zTC+IeBYQ79z8<oC3*}Oj+n%D5sPyQH
z8nau(JPJ1J0Qq?LB=bVnm*U{L5R_NYwh~@-@N0Ye^4ACQRqiiX#|p>$bfnxWp0X9}
z(OTMczRp50Hi{fzA;-?cp*{&VI1no%iX5O=vRv10l`pFvp#EqX7V;znV^$+)putBm
ze;pjKKzcgo?}ARBQ<Oh|pMZSIhqxJj7qnj#FX<=Y3DCY#-2w-+Ph=+tAyCX_f%eiW
zkgodA_m!co8K%Jjy;Ec(442qm%r)Qnpt+S(NnP_i4wG<=?afv5723-d=uDmP8KBgd
zDX^dyZU@cpC6NDrgYSd%gsvz|g5~2-o`y-7<wNm?^VFOJTgQ<np%QYS_8TA#-DP|W
z`4p7vS=0k!$PS!_Nw|^QtClf$$5;!TvU#r^1INqbZsWxyPpP$b+TJsLLr|wY0rDvn
zC(TRa3XlMeQ~q~=<6~1^1Hpchjc4FlP(0`RXdP?dHL!eQl)`~J$bbckiDGsJa-gvm
zO#7we7&vemCPDAC=OF=IpgBBed!Kt{_a$hCEpQToajP6YKS2_jZSoGmS-97DXGL=n
zOX2fy3iSTa9$QvMHFBUE*w=Ho1=LpyyFf9R0O>}3EJ|TRtPItK<Cl^zg#1*3?5h-W
z{8D|<G92XVU>}Tv`U4$ASJ$G9`8bq9_QVFzd-t@D@;(4jm<>zqb1~65)SNDd{qQ;{
z7K-Ok8LF%OZcx0|K^<t0nw$D8m;(DesJfTUy_^Nq7eOUxe$B86WM7||s$mP9gdAuN
zLUXQ%J8fegi1DoPk*qfLNoNCegT_q5^?qIUPJ?U<%{@}a6!Q5#Wcv|N`9Yi5gZ7c^
zT5!}ya;4AfWip~0+d9fMFdu3`zH?CJ*U$KM^;uv;6}P?MejcIJ9@jJFL&y}ReYga<
z&*v4&7vM!$4Rb&~gytuoj!%rzK2KJ^<`4x7rl1a*A(*3LC$yf7FCV3pu^e0rE2%1<
zFF1VbhebAjS)g1=S<*wi4w8;BKeU)DRNAaJ%Ja@K?jm!PRiGGrkEXmu&L*)L<F5#m
zmpfietBrSUpiqpp-W)syr(gggFx%$F7534&5cA*)SYiL__X!0nOq;HY9pn0lUA+D*
zoAp2iKFa4)niK6S>DTADA31!kq7=Gj`RMN*fxh<VO4tZLvb}jkC<EInrTeD(Y+(JR
zvBDu{CmA<QZbpeUHr5UiALLhQ(>Y!Xaj?KC<7>#BpuALq@-YW$eOY659kAJ7PzyTy
zw}MIwWH$jiPa#+cx-NE%)+xUitA91RLh;l4MRP5mlj^&CG^Z$Nzv)sbUqX82Sk%Q7
zC|}D#SNWJX8ka+78Wa;<7t*)Tkq<p5lo#n&`vh~Vqh3BY+1GljY;edG?1Ng+nG*%$
z<zQhH!ND$0s*$T>Gk^E0L|t>xr8T>I#rF2`n|%)}&-2BO3G#fg`pcPv{9LTQ_JsDs
z#p-iJ+~;Eu=PlILCclb7f-(i)@cAxn&%-8o4Kyx@+b<n2UKjcf7qU60pzC7Ctnkz?
zT0b}+vVWI-?tJQue%<E;rO-Yr&*jUucb0X2`uzU#y_lm-d+}DN0?loy{p;Vy6&vY=
zCRs0)S+E>=zU@C3C0mS*f#caaZM^fRvDKhU_TL4aK6*#|0Db~t&^tueOnMRerKe|v
zu9<Wd3$0DpOnN%ILf1@s^1H}Kvi9ejt!IV9_YRA-!ZWtF&SzuZyFu|#3@mWqq)(1A
z1{Opi=9kKuF6Af#&7gcZps^aKxTs&(OnP(t{_81Y7tqz(gsz!vI`nG}x}q=%mXAYu
z8YV$DE=Div>m8*W1=T0P@^L5wo2QV4u1c5!%V&~O@ljhXTm?GgBG`i`kOLb7$mM(H
zX6m~^dm|6-7~A{aD0{)ap2^;<i_}wIWG@N2X0lm6$C>oX_w`JAT8q&2npc$GSK3z^
zt9iA+bMOKvW{Ot~a!?1-)wA|_NI(}n1JBvs_fS1A!_+UNr~U;Hobj1#2G2*-XY?(<
zvZ?DXP;4&r-8I;+ip>r1b|`?>roE7aGvN5xl;ttMSU>)}&NAG+&i+;xTeyNSN6aN=
z79T(7n7RB^Y#tBI%Lu?i4$0*_#I7)l&6OO8B^=771mbEo-!;5!S8&*_<qN_}zQDbO
zKV03w7n--4x3Tm!{J8B#^E+&ewIt>pd?|e=4~Lsc{CaZ>e<s^t-p#}IJ#2`2^Ikp%
zH<(6VuA6x&H<|aD_w&%YjSbYwue3g3wwX4=Z;bfE(;fUVWvBU|`4Im=_F;Y_*YR5|
z-5qQ!KE&=KNlB9;O<jB~O`DH$kTa%-KX+x>u6J>q3%nBh`RaZ*+jy_J$K1=;>-+dx
ze!$pf&=k!<K0X}c&%h%*g2wm(++qG?am3tjKE~J2-!l)G-!~7MKQJHXr+N?ZQ@uYj
zpD>S@KQ@12{?r^ZpERE`e`Y>y{@i@VJZc^@pEaK|e_<Xsf61Ru{)#`>eZl;-`5W^^
z^S9<p=I{7(;+M@=%-@?Q%|DobH2+lErw*qj%7*w3Po-<%X}%q>6-C&loTBgNw133s
z720+nvu)kqFsS4x1!FE$y5krl8v&sc_~Q6i+yjrnG3q?hDte&_cpPYsp&|@XKMwDs
z?P1Cv!Y|-Kzx*sE$Ec#4ejaxfdnh0BO4r^;v7xo_Xi@2|g*AnaBDVu}LnkTYl;4Lt
zkuQaQ<Tkhs`4-B}*nI%`Uf>Z@AzNE$e*!)UUDUN6))D6MQ9+DCKL*7aKcTr^DgQz2
z&ySsSD(cW<Zef*AQm+NZDE4j}eWAPkHs%u&T8n%)AXj;HxBU^?7$1K89E!cGEB*5b
zt4=Q5xG5JtZXT2ipYVM8f;=b}<uF&(m5Xv*Tg0@Y4;1$|Abcf0$c38+<sv|ND8r3^
zkO$?W9QjxMytOG8ZXAE?KNlVHpo_J*AHSATUI#U>4mQ9h*amk3`87FU4V-GjSvH(t
z!^t(AS;Kkd&r9$rcoc}aISyZfC*iB`G<+M_-|nBnU!eRin1Y|dEAVrehW~-r;C0qc
z5vl;~DfU;0*oN4nA?6-pIiX5e1dHJ+sDfos4a*@4tSxjs)WR*W85C3YaVQC$&<#2-
z86fwe9OR)7_P_uT*U&!L4>k}7ozc)RjKVlf!2NI(9suGK`Zy4y5b+6p2EG8qCG<7;
zCVU6J2QR>j@G~F=6})0AE(7x8p7Wb1*FyumAGX1rz}$8AE0|lw0U$RO_^UVu<f7us
za1y=?_^Hr-_&NM9`!oW?FuVk=g&W{)@D3nW;d;0Y?f~KyP64msF#8~kuP|#59|6`G
z#$WhRV141Q0C^ANH~cNYZ<uovehFA#7~k%1xcKu;1m6*I6<G~8!dk$01mBS@&<^;H
z;5$NoBIF`M>?8P&;5&lv$R_~bk<Y^uz*9eR0=@~)!X*4B{1jg0UxGr(RA;<D)0gOu
z^XkgQ6DgA#NMzzY=C1xMKNQ}TF}?jgyHfc=x~tbDvzZKfxl}%p>g_Y}9;8CNH|Z%C
z(!E`ol*z^OsoqSgv(J;<UnZNlenOls>^8Yff59ZOJw0*Ly*JmL>Xo&0Hfai(c%j?e
zlgejJwl`(^?#`ON?tF?@ZMHveI@5bnrjQ;mh14F#r1ZmO)0;*yk<Da#Y3@mTQYKX>
znABbU@r>!pr}(jRp4p`eeQC7feZSF%h+X-3Vt1;~UDGZ^Z>oN4u)ah(ok-^s{XM3?
zH%VCdv2}_W#F_VR^07hIQ@+Q%acgCs`qG&s@hHnzGJAKgakHXGXGn!f^ykS$Vz1dt
z#CGMgyHmZpl;u>y4MsA}kGczKWyAmdxXI>|odnX2Ygd0dlS%bty=7;zUFk$T)0^!J
z(su55;ie~^$P?XUihz@UGK>w<Ob!eEZhG@xE=#p|qQ5U?diu3R;shzz?Qa9EK)*F-
zDM|dtwHHdGm|jo3kmzT1gf^I;7Z&u$d0y#8?2czTJ(qrCp|lCyK-GJRsrTkl?<KA?
zlNvDfUTEt}(W%cXmi3KhV=$#gZ*ZeGxUp<-V`)^Ar_<!=G?nQzm2?{Wx=oAcuGLd-
z_0(I-)LVUp(x@IzYdV+NYg)Z+xZNAN-5a{SZ0PpVn4Ml{dv`Y9Yr0bW-x0l>?1E9~
zwLiV?c-h#v=Qdt45qHB7=ez}BAln+8@&>2M29r8}Yo$G%w5Q`A5I4^0l1_>+^m^{H
zo_f|(&z70Y`U<5{$@HEyM<DA3q2C+W?+xrP8`xhOvDfSDbAzzA)ND$hi;>*xGVZ(&
J-u(Cf{|En5p|}75

literal 0
HcmV?d00001


From 749b9421329f0ca8453e7df0df0702a3d29e8276 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Thu, 31 Oct 2024 17:37:32 +1100
Subject: [PATCH 072/282] [lvlg] fix tests (#7708)

---
 tests/components/lvgl/common.yaml | 40 +------------------------------
 1 file changed, 1 insertion(+), 39 deletions(-)

diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml
index cebc3caaa7..c7d635db1c 100644
--- a/tests/components/lvgl/common.yaml
+++ b/tests/components/lvgl/common.yaml
@@ -4,56 +4,18 @@ touchscreen:
     display: tft_display
     update_interval: 50ms
     threshold: 1
-    calibration:
-      x_max: 240
-      y_max: 320
 
 font:
   - file: "$component_dir/roboto.ttf"
     id: roboto20
     size: 20
-    extras:
-      - file: '$component_dir/materialdesignicons-webfont.ttf'
-        glyphs: [
-          "\U000F004B",
-          "\U0000f0ed",
-          "\U000F006E",
-          "\U000F012C",
-          "\U000F179B",
-          "\U000F0748",
-          "\U000F1A1B",
-          "\U000F02DC",
-          "\U000F0A02",
-          "\U000F035F",
-          "\U000F0156",
-          "\U000F0C5F",
-          "\U000f0084",
-          "\U000f0091",
-        ]
+
   - file: "$component_dir/helvetica.ttf"
     id: helvetica20
   - file: "$component_dir/roboto.ttf"
     id: roboto10
     size: 10
     bpp: 4
-    extras:
-      - file: '$component_dir/materialdesignicons-webfont.ttf'
-        glyphs: [
-          "\U000F004B",
-          "\U0000f0ed",
-          "\U000F006E",
-          "\U000F012C",
-          "\U000F179B",
-          "\U000F0748",
-          "\U000F1A1B",
-          "\U000F02DC",
-          "\U000F0A02",
-          "\U000F035F",
-          "\U000F0156",
-          "\U000F0C5F",
-          "\U000f0084",
-          "\U000f0091",
-        ]
 
 sensor:
   - platform: lvgl

From cefbfb75bd4b5f8baaf5599c1c4221aab1919426 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Thu, 31 Oct 2024 23:46:35 +1300
Subject: [PATCH 073/282] [esp32_ble] Add disconnect as a virtual function to
 ``ESPBTClient`` (#7705)

---
 esphome/components/esp32_ble_client/ble_client_base.h    | 2 +-
 esphome/components/esp32_ble_tracker/esp32_ble_tracker.h | 3 ++-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h
index fd586e59d6..fca66c0b3c 100644
--- a/esphome/components/esp32_ble_client/ble_client_base.h
+++ b/esphome/components/esp32_ble_client/ble_client_base.h
@@ -35,7 +35,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
   void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
   void connect() override;
   esp_err_t pair();
-  void disconnect();
+  void disconnect() override;
   void release_services();
 
   bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; }
diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
index d2bb6a6e6d..2fc5da829d 100644
--- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
+++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
@@ -11,9 +11,9 @@
 
 #ifdef USE_ESP32
 
+#include <esp_bt_defs.h>
 #include <esp_gap_ble_api.h>
 #include <esp_gattc_api.h>
-#include <esp_bt_defs.h>
 
 #include <freertos/FreeRTOS.h>
 #include <freertos/semphr.h>
@@ -172,6 +172,7 @@ class ESPBTClient : public ESPBTDeviceListener {
                                    esp_ble_gattc_cb_param_t *param) = 0;
   virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0;
   virtual void connect() = 0;
+  virtual void disconnect() = 0;
   virtual void set_state(ClientState st) { this->state_ = st; }
   ClientState state() const { return state_; }
   int app_id;

From 77bb46ff3bf17afb7bfd3fe963a86af014a17d4b Mon Sep 17 00:00:00 2001
From: Samuel Sieb <samuel-github@sieb.net>
Date: Fri, 1 Nov 2024 02:54:34 -0700
Subject: [PATCH 074/282] handle bad pin schemas (#7711)

Co-authored-by: Samuel Sieb <samuel@sieb.net>
---
 esphome/components/esp32/gpio.py     |  4 +++-
 esphome/components/esp8266/gpio.py   | 12 +++++++-----
 esphome/components/host/gpio.py      | 10 ++++++----
 esphome/components/libretiny/gpio.py |  6 ++++--
 esphome/components/rp2040/gpio.py    |  8 +++++---
 5 files changed, 25 insertions(+), 15 deletions(-)

diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py
index 558ff51af8..df01769a66 100644
--- a/esphome/components/esp32/gpio.py
+++ b/esphome/components/esp32/gpio.py
@@ -67,8 +67,10 @@ def _translate_pin(value):
             "This variable only supports pin numbers, not full pin schemas "
             "(with inverted and mode)."
         )
-    if isinstance(value, int):
+    if isinstance(value, int) and not isinstance(value, bool):
         return value
+    if not isinstance(value, str):
+        raise cv.Invalid(f"Invalid pin number: {value}")
     try:
         return int(value)
     except ValueError:
diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py
index c42bc9204f..53016d2130 100644
--- a/esphome/components/esp8266/gpio.py
+++ b/esphome/components/esp8266/gpio.py
@@ -1,6 +1,9 @@
-import logging
 from dataclasses import dataclass
+import logging
 
+from esphome import pins
+import esphome.codegen as cg
+import esphome.config_validation as cv
 from esphome.const import (
     CONF_ANALOG,
     CONF_ID,
@@ -14,10 +17,7 @@ from esphome.const import (
     CONF_PULLUP,
     PLATFORM_ESP8266,
 )
-from esphome import pins
 from esphome.core import CORE, coroutine_with_priority
-import esphome.config_validation as cv
-import esphome.codegen as cg
 
 from . import boards
 from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns
@@ -48,8 +48,10 @@ def _translate_pin(value):
             "This variable only supports pin numbers, not full pin schemas "
             "(with inverted and mode)."
         )
-    if isinstance(value, int):
+    if isinstance(value, int) and not isinstance(value, bool):
         return value
+    if not isinstance(value, str):
+        raise cv.Invalid(f"Invalid pin number: {value}")
     try:
         return int(value)
     except ValueError:
diff --git a/esphome/components/host/gpio.py b/esphome/components/host/gpio.py
index 180919de4f..0f22a790bd 100644
--- a/esphome/components/host/gpio.py
+++ b/esphome/components/host/gpio.py
@@ -1,5 +1,8 @@
 import logging
 
+from esphome import pins
+import esphome.codegen as cg
+import esphome.config_validation as cv
 from esphome.const import (
     CONF_ID,
     CONF_INPUT,
@@ -11,9 +14,6 @@ from esphome.const import (
     CONF_PULLDOWN,
     CONF_PULLUP,
 )
-from esphome import pins
-import esphome.config_validation as cv
-import esphome.codegen as cg
 
 from .const import host_ns
 
@@ -28,8 +28,10 @@ def _translate_pin(value):
             "This variable only supports pin numbers, not full pin schemas "
             "(with inverted and mode)."
         )
-    if isinstance(value, int):
+    if isinstance(value, int) and not isinstance(value, bool):
         return value
+    if not isinstance(value, str):
+        raise cv.Invalid(f"Invalid pin number: {value}")
     try:
         return int(value)
     except ValueError:
diff --git a/esphome/components/libretiny/gpio.py b/esphome/components/libretiny/gpio.py
index 1d7b37cc9b..07eb0ce133 100644
--- a/esphome/components/libretiny/gpio.py
+++ b/esphome/components/libretiny/gpio.py
@@ -1,8 +1,8 @@
 import logging
 
+from esphome import pins
 import esphome.codegen as cg
 import esphome.config_validation as cv
-from esphome import pins
 from esphome.const import (
     CONF_ANALOG,
     CONF_ID,
@@ -103,8 +103,10 @@ def _translate_pin(value):
             "This variable only supports pin numbers, not full pin schemas "
             "(with inverted and mode)."
         )
-    if isinstance(value, int):
+    if isinstance(value, int) and not isinstance(value, bool):
         return value
+    if not isinstance(value, str):
+        raise cv.Invalid(f"Invalid pin number: {value}")
     try:
         return int(value)
     except ValueError:
diff --git a/esphome/components/rp2040/gpio.py b/esphome/components/rp2040/gpio.py
index 6ba0975a2c..58514f7db5 100644
--- a/esphome/components/rp2040/gpio.py
+++ b/esphome/components/rp2040/gpio.py
@@ -1,6 +1,8 @@
+from esphome import pins
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.const import (
+    CONF_ANALOG,
     CONF_ID,
     CONF_INPUT,
     CONF_INVERTED,
@@ -10,10 +12,8 @@ from esphome.const import (
     CONF_OUTPUT,
     CONF_PULLDOWN,
     CONF_PULLUP,
-    CONF_ANALOG,
 )
 from esphome.core import CORE
-from esphome import pins
 
 from . import boards
 from .const import KEY_BOARD, KEY_RP2040, rp2040_ns
@@ -41,8 +41,10 @@ def _translate_pin(value):
             "This variable only supports pin numbers, not full pin schemas "
             "(with inverted and mode)."
         )
-    if isinstance(value, int):
+    if isinstance(value, int) and not isinstance(value, bool):
         return value
+    if not isinstance(value, str):
+        raise cv.Invalid(f"Invalid pin number: {value}")
     try:
         return int(value)
     except ValueError:

From 01497c891d08c4a9a245eaadb982e302d43f0b58 Mon Sep 17 00:00:00 2001
From: tomaszduda23 <tomaszduda23@gmail.com>
Date: Sun, 3 Nov 2024 22:22:16 +0100
Subject: [PATCH 075/282] datetime fix build_language_schema (#7710)

Co-authored-by: Tomasz Duda <tomaszduda23@gmai.com>
---
 esphome/components/datetime/__init__.py    | 6 +++---
 tests/components/mqtt/common.yaml          | 1 +
 tests/components/web_server/common_v3.yaml | 8 ++++++++
 3 files changed, 12 insertions(+), 3 deletions(-)

diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py
index 7edf527e01..630bf6962c 100644
--- a/esphome/components/datetime/__init__.py
+++ b/esphome/components/datetime/__init__.py
@@ -70,8 +70,6 @@ def _validate_time_present(config):
 
 
 _DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
-    web_server.WEBSERVER_SORTING_SCHEMA,
-    cv.MQTT_COMMAND_COMPONENT_SCHEMA,
     cv.Schema(
         {
             cv.Optional(CONF_ON_VALUE): automation.validate_automation(
@@ -81,7 +79,9 @@ _DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
             ),
             cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
         }
-    ),
+    )
+    .extend(web_server.WEBSERVER_SORTING_SCHEMA)
+    .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
 ).add_extra(_validate_time_present)
 
 
diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml
index e154be8b5c..75c34bec56 100644
--- a/tests/components/mqtt/common.yaml
+++ b/tests/components/mqtt/common.yaml
@@ -230,6 +230,7 @@ datetime:
     id: test_date
     type: date
     state_topic: some/topic/date
+    command_topic: test_date/custom_command_topic
     qos: 2
     subscribe_qos: 2
     set_action:
diff --git a/tests/components/web_server/common_v3.yaml b/tests/components/web_server/common_v3.yaml
index 69f4b67f15..bdacaaddbe 100644
--- a/tests/components/web_server/common_v3.yaml
+++ b/tests/components/web_server/common_v3.yaml
@@ -35,3 +35,11 @@ switch:
     web_server:
       sorting_group_id: sorting_group_2
       sorting_weight: -10
+datetime:
+  - platform: template
+    name: Pick a Date
+    type: datetime
+    optimistic: yes
+    web_server:
+      sorting_group_id: sorting_group_3
+      sorting_weight: -5

From 2dca3d79e490abcf2a03e962cb7c88dc3bbfd83b Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 5 Nov 2024 11:32:18 +1100
Subject: [PATCH 076/282] [lvgl] Ensure images are configured before using
 them. (Bugfix) (#7721)

---
 esphome/components/lvgl/widgets/animimg.py | 7 ++++---
 esphome/components/lvgl/widgets/img.py     | 2 ++
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/esphome/components/lvgl/widgets/animimg.py b/esphome/components/lvgl/widgets/animimg.py
index 3b20008c3d..8adea72ad3 100644
--- a/esphome/components/lvgl/widgets/animimg.py
+++ b/esphome/components/lvgl/widgets/animimg.py
@@ -60,9 +60,10 @@ class AnimimgType(WidgetType):
         lvgl_components_required.add(CONF_IMAGE)
         lvgl_components_required.add(CONF_ANIMIMG)
         if CONF_SRC in config:
-            for x in config[CONF_SRC]:
-                await cg.get_variable(x)
-            srcs = [await lv_image.process(x) for x in config[CONF_SRC]]
+            srcs = [
+                await lv_image.process(await cg.get_variable(x))
+                for x in config[CONF_SRC]
+            ]
             src_id = cg.static_const_array(config[CONF_SRC_LIST_ID], srcs)
             count = len(config[CONF_SRC])
             lv.animimg_set_src(w.obj, src_id, count)
diff --git a/esphome/components/lvgl/widgets/img.py b/esphome/components/lvgl/widgets/img.py
index 59b2c97c63..931d0c0b5b 100644
--- a/esphome/components/lvgl/widgets/img.py
+++ b/esphome/components/lvgl/widgets/img.py
@@ -1,3 +1,4 @@
+import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.const import CONF_ANGLE, CONF_MODE
 
@@ -64,6 +65,7 @@ class ImgType(WidgetType):
 
     async def to_code(self, w: Widget, config):
         if src := config.get(CONF_SRC):
+            src = await cg.get_variable(src)
             lv.img_set_src(w.obj, await lv_image.process(src))
         if (cf_angle := config.get(CONF_ANGLE)) is not None:
             pivot_x = config[CONF_PIVOT_X]

From dcc537d0d43c2b8fdff7ecac86c8154c7ed78172 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Wed, 6 Nov 2024 10:45:40 +1300
Subject: [PATCH 077/282] [lvgl] Don't just throw key error if someone types a
 bad layout type (#7722)

Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
---
 esphome/components/lvgl/schemas.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py
index bb14c11ddd..516627708e 100644
--- a/esphome/components/lvgl/schemas.py
+++ b/esphome/components/lvgl/schemas.py
@@ -391,7 +391,9 @@ def container_validator(schema, widget_type: WidgetType):
             add_lv_use(ltype)
         if value == SCHEMA_EXTRACT:
             return result
-        result = result.extend(LAYOUT_SCHEMAS[ltype.lower()])
+        result = result.extend(
+            LAYOUT_SCHEMAS.get(ltype.lower(), LAYOUT_SCHEMAS[df.TYPE_NONE])
+        )
         return result(value)
 
     return validator

From 5bb4d042e48873812684fcad189c233c64698629 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Wed, 6 Nov 2024 11:54:47 +1100
Subject: [PATCH 078/282] [spi_device] rename mode to spi_mode (#7724)

---
 esphome/components/spi/__init__.py            | 29 ++++++++++---------
 esphome/components/spi_device/__init__.py     | 19 +++---------
 .../components/spi_device/test.esp32-ard.yaml |  2 +-
 .../spi_device/test.esp32-c3-ard.yaml         |  2 +-
 .../spi_device/test.esp32-c3-idf.yaml         |  2 +-
 .../components/spi_device/test.esp32-idf.yaml |  2 +-
 .../spi_device/test.esp8266-ard.yaml          |  2 +-
 .../spi_device/test.rp2040-ard.yaml           |  2 +-
 8 files changed, 25 insertions(+), 35 deletions(-)

diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py
index fdf19bb56e..52afbf365e 100644
--- a/esphome/components/spi/__init__.py
+++ b/esphome/components/spi/__init__.py
@@ -1,40 +1,37 @@
 import re
 
+from esphome import pins
 import esphome.codegen as cg
-import esphome.config_validation as cv
-import esphome.final_validate as fv
 from esphome.components.esp32.const import (
     KEY_ESP32,
-    VARIANT_ESP32S2,
-    VARIANT_ESP32S3,
     VARIANT_ESP32C2,
     VARIANT_ESP32C3,
     VARIANT_ESP32C6,
     VARIANT_ESP32H2,
+    VARIANT_ESP32S2,
+    VARIANT_ESP32S3,
 )
-from esphome import pins
+import esphome.config_validation as cv
 from esphome.const import (
     CONF_CLK_PIN,
+    CONF_CS_PIN,
+    CONF_DATA_PINS,
+    CONF_DATA_RATE,
     CONF_ID,
+    CONF_INVERTED,
     CONF_MISO_PIN,
     CONF_MOSI_PIN,
-    CONF_SPI_ID,
-    CONF_CS_PIN,
     CONF_NUMBER,
-    CONF_INVERTED,
+    CONF_SPI_ID,
     KEY_CORE,
     KEY_TARGET_PLATFORM,
     KEY_VARIANT,
-    CONF_DATA_RATE,
     PLATFORM_ESP32,
     PLATFORM_ESP8266,
     PLATFORM_RP2040,
-    CONF_DATA_PINS,
-)
-from esphome.core import (
-    coroutine_with_priority,
-    CORE,
 )
+from esphome.core import CORE, coroutine_with_priority
+import esphome.final_validate as fv
 
 CODEOWNERS = ["@esphome/core", "@clydebarrow"]
 spi_ns = cg.esphome_ns.namespace("spi")
@@ -69,6 +66,10 @@ SPI_MODE_OPTIONS = {
     1: SPIMode.MODE1,
     2: SPIMode.MODE2,
     3: SPIMode.MODE3,
+    "0": SPIMode.MODE0,
+    "1": SPIMode.MODE1,
+    "2": SPIMode.MODE2,
+    "3": SPIMode.MODE3,
 }
 
 CONF_SPI_MODE = "spi_mode"
diff --git a/esphome/components/spi_device/__init__.py b/esphome/components/spi_device/__init__.py
index 65e7ee6fc6..2f23d8a011 100644
--- a/esphome/components/spi_device/__init__.py
+++ b/esphome/components/spi_device/__init__.py
@@ -1,6 +1,6 @@
 import esphome.codegen as cg
-import esphome.config_validation as cv
 from esphome.components import spi
+import esphome.config_validation as cv
 from esphome.const import CONF_ID, CONF_MODE
 
 DEPENDENCIES = ["spi"]
@@ -11,18 +11,6 @@ spi_device_ns = cg.esphome_ns.namespace("spi_device")
 
 spi_device = spi_device_ns.class_("SPIDeviceComponent", cg.Component, spi.SPIDevice)
 
-Mode = spi.spi_ns.enum("SPIMode")
-MODES = {
-    "0": Mode.MODE0,
-    "1": Mode.MODE1,
-    "2": Mode.MODE2,
-    "3": Mode.MODE3,
-    "MODE0": Mode.MODE0,
-    "MODE1": Mode.MODE1,
-    "MODE2": Mode.MODE2,
-    "MODE3": Mode.MODE3,
-}
-
 BitOrder = spi.spi_ns.enum("SPIBitOrder")
 ORDERS = {
     "msb_first": BitOrder.BIT_ORDER_MSB_FIRST,
@@ -34,7 +22,9 @@ CONFIG_SCHEMA = cv.Schema(
     {
         cv.GenerateID(CONF_ID): cv.declare_id(spi_device),
         cv.Optional(CONF_BIT_ORDER, default="msb_first"): cv.enum(ORDERS, lower=True),
-        cv.Optional(CONF_MODE, default="0"): cv.enum(MODES, upper=True),
+        cv.Optional(CONF_MODE): cv.invalid(
+            "The 'mode' option has been renamed to 'spi_mode'."
+        ),
     }
 ).extend(spi.spi_device_schema(False, "1MHz"))
 
@@ -42,6 +32,5 @@ CONFIG_SCHEMA = cv.Schema(
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     await cg.register_component(var, config)
-    cg.add(var.set_mode(config[CONF_MODE]))
     cg.add(var.set_bit_order(config[CONF_BIT_ORDER]))
     await spi.register_spi_device(var, config)
diff --git a/tests/components/spi_device/test.esp32-ard.yaml b/tests/components/spi_device/test.esp32-ard.yaml
index cad8ca49f8..b539cb3ec4 100644
--- a/tests/components/spi_device/test.esp32-ard.yaml
+++ b/tests/components/spi_device/test.esp32-ard.yaml
@@ -7,5 +7,5 @@ spi:
 spi_device:
   id: spi_device_test
   data_rate: 2MHz
-  mode: 3
+  spi_mode: 3
   bit_order: lsb_first
diff --git a/tests/components/spi_device/test.esp32-c3-ard.yaml b/tests/components/spi_device/test.esp32-c3-ard.yaml
index 49e2733676..99c0ac1ebb 100644
--- a/tests/components/spi_device/test.esp32-c3-ard.yaml
+++ b/tests/components/spi_device/test.esp32-c3-ard.yaml
@@ -7,5 +7,5 @@ spi:
 spi_device:
   id: spi_device_test
   data_rate: 2MHz
-  mode: 3
+  spi_mode: 3
   bit_order: lsb_first
diff --git a/tests/components/spi_device/test.esp32-c3-idf.yaml b/tests/components/spi_device/test.esp32-c3-idf.yaml
index 49e2733676..99c0ac1ebb 100644
--- a/tests/components/spi_device/test.esp32-c3-idf.yaml
+++ b/tests/components/spi_device/test.esp32-c3-idf.yaml
@@ -7,5 +7,5 @@ spi:
 spi_device:
   id: spi_device_test
   data_rate: 2MHz
-  mode: 3
+  spi_mode: 3
   bit_order: lsb_first
diff --git a/tests/components/spi_device/test.esp32-idf.yaml b/tests/components/spi_device/test.esp32-idf.yaml
index cad8ca49f8..b539cb3ec4 100644
--- a/tests/components/spi_device/test.esp32-idf.yaml
+++ b/tests/components/spi_device/test.esp32-idf.yaml
@@ -7,5 +7,5 @@ spi:
 spi_device:
   id: spi_device_test
   data_rate: 2MHz
-  mode: 3
+  spi_mode: 3
   bit_order: lsb_first
diff --git a/tests/components/spi_device/test.esp8266-ard.yaml b/tests/components/spi_device/test.esp8266-ard.yaml
index 1b191bdb6a..988825ce2d 100644
--- a/tests/components/spi_device/test.esp8266-ard.yaml
+++ b/tests/components/spi_device/test.esp8266-ard.yaml
@@ -7,5 +7,5 @@ spi:
 spi_device:
   id: spi_device_test
   data_rate: 2MHz
-  mode: 3
+  spi_mode: 3
   bit_order: lsb_first
diff --git a/tests/components/spi_device/test.rp2040-ard.yaml b/tests/components/spi_device/test.rp2040-ard.yaml
index c70493c70d..6020643f21 100644
--- a/tests/components/spi_device/test.rp2040-ard.yaml
+++ b/tests/components/spi_device/test.rp2040-ard.yaml
@@ -7,5 +7,5 @@ spi:
 spi_device:
   id: spi_device_test
   data_rate: 2MHz
-  mode: 3
+  spi_mode: 3
   bit_order: lsb_first

From 80b4c264818df8ce79813d3f572e87b9c7d24bf6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= <contact@rodrigomartin.dev>
Date: Wed, 6 Nov 2024 01:56:48 +0100
Subject: [PATCH 079/282] feat(MQTT): Add `enable`, `disable` and
 `enable_on_boot` (#7716)

---
 esphome/components/mqtt/__init__.py     | 33 +++++++++++++++++++++++++
 esphome/components/mqtt/mqtt_client.cpp | 30 +++++++++++++++++++---
 esphome/components/mqtt/mqtt_client.h   | 29 ++++++++++++++++++++--
 tests/components/mqtt/common.yaml       |  3 +++
 4 files changed, 90 insertions(+), 5 deletions(-)

diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py
index 8851581ea0..86d163e61d 100644
--- a/esphome/components/mqtt/__init__.py
+++ b/esphome/components/mqtt/__init__.py
@@ -22,6 +22,7 @@ from esphome.const import (
     CONF_DISCOVERY_PREFIX,
     CONF_DISCOVERY_RETAIN,
     CONF_DISCOVERY_UNIQUE_ID_GENERATOR,
+    CONF_ENABLE_ON_BOOT,
     CONF_ID,
     CONF_KEEPALIVE,
     CONF_LEVEL,
@@ -99,6 +100,8 @@ MQTTMessage = mqtt_ns.struct("MQTTMessage")
 MQTTClientComponent = mqtt_ns.class_("MQTTClientComponent", cg.Component)
 MQTTPublishAction = mqtt_ns.class_("MQTTPublishAction", automation.Action)
 MQTTPublishJsonAction = mqtt_ns.class_("MQTTPublishJsonAction", automation.Action)
+MQTTEnableAction = mqtt_ns.class_("MQTTEnableAction", automation.Action)
+MQTTDisableAction = mqtt_ns.class_("MQTTDisableAction", automation.Action)
 MQTTMessageTrigger = mqtt_ns.class_(
     "MQTTMessageTrigger", automation.Trigger.template(cg.std_string), cg.Component
 )
@@ -208,6 +211,7 @@ CONFIG_SCHEMA = cv.All(
         {
             cv.GenerateID(): cv.declare_id(MQTTClientComponent),
             cv.Required(CONF_BROKER): cv.string_strict,
+            cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
             cv.Optional(CONF_PORT, default=1883): cv.port,
             cv.Optional(CONF_USERNAME, default=""): cv.string,
             cv.Optional(CONF_PASSWORD, default=""): cv.string,
@@ -325,6 +329,7 @@ async def to_code(config):
     cg.add_global(mqtt_ns.using)
 
     cg.add(var.set_broker_address(config[CONF_BROKER]))
+    cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT]))
     cg.add(var.set_broker_port(config[CONF_PORT]))
     cg.add(var.set_username(config[CONF_USERNAME]))
     cg.add(var.set_password(config[CONF_PASSWORD]))
@@ -555,3 +560,31 @@ async def register_mqtt_component(var, config):
 async def mqtt_connected_to_code(config, condition_id, template_arg, args):
     paren = await cg.get_variable(config[CONF_ID])
     return cg.new_Pvariable(condition_id, template_arg, paren)
+
+
+@automation.register_action(
+    "mqtt.enable",
+    MQTTEnableAction,
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.use_id(MQTTClientComponent),
+        }
+    ),
+)
+async def mqtt_enable_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
+
+
+@automation.register_action(
+    "mqtt.disable",
+    MQTTDisableAction,
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.use_id(MQTTClientComponent),
+        }
+    ),
+)
+async def mqtt_disable_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp
index b5ac285026..106192c0e3 100644
--- a/esphome/components/mqtt/mqtt_client.cpp
+++ b/esphome/components/mqtt/mqtt_client.cpp
@@ -50,6 +50,8 @@ void MQTTClientComponent::setup() {
         }
       });
   this->mqtt_backend_.set_on_disconnect([this](MQTTClientDisconnectReason reason) {
+    if (this->state_ == MQTT_CLIENT_DISABLED)
+      return;
     this->state_ = MQTT_CLIENT_DISCONNECTED;
     this->disconnect_reason_ = reason;
   });
@@ -77,8 +79,9 @@ void MQTTClientComponent::setup() {
         topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2);
   }
 
-  this->last_connected_ = millis();
-  this->start_dnslookup_();
+  if (this->enable_on_boot_) {
+    this->enable();
+  }
 }
 
 void MQTTClientComponent::send_device_info_() {
@@ -163,7 +166,9 @@ void MQTTClientComponent::dump_config() {
     ESP_LOGCONFIG(TAG, "  Availability: '%s'", this->availability_.topic.c_str());
   }
 }
-bool MQTTClientComponent::can_proceed() { return network::is_disabled() || this->is_connected(); }
+bool MQTTClientComponent::can_proceed() {
+  return network::is_disabled() || this->state_ == MQTT_CLIENT_DISABLED || this->is_connected();
+}
 
 void MQTTClientComponent::start_dnslookup_() {
   for (auto &subscription : this->subscriptions_) {
@@ -339,6 +344,8 @@ void MQTTClientComponent::loop() {
   const uint32_t now = millis();
 
   switch (this->state_) {
+    case MQTT_CLIENT_DISABLED:
+      return;  // Return to avoid a reboot when disabled
     case MQTT_CLIENT_DISCONNECTED:
       if (now - this->connect_begin_ > 5000) {
         this->start_dnslookup_();
@@ -501,6 +508,23 @@ bool MQTTClientComponent::publish_json(const std::string &topic, const json::jso
   return this->publish(topic, message, qos, retain);
 }
 
+void MQTTClientComponent::enable() {
+  if (this->state_ != MQTT_CLIENT_DISABLED)
+    return;
+  ESP_LOGD(TAG, "Enabling MQTT...");
+  this->state_ = MQTT_CLIENT_DISCONNECTED;
+  this->last_connected_ = millis();
+  this->start_dnslookup_();
+}
+
+void MQTTClientComponent::disable() {
+  if (this->state_ == MQTT_CLIENT_DISABLED)
+    return;
+  ESP_LOGD(TAG, "Disabling MQTT...");
+  this->state_ = MQTT_CLIENT_DISABLED;
+  this->on_shutdown();
+}
+
 /** Check if the message topic matches the given subscription topic
  *
  * INFO: MQTT spec mandates that topics must not be empty and must be valid NULL-terminated UTF-8 strings.
diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h
index 887800f201..7ae3a6c5e8 100644
--- a/esphome/components/mqtt/mqtt_client.h
+++ b/esphome/components/mqtt/mqtt_client.h
@@ -87,7 +87,8 @@ struct MQTTDiscoveryInfo {
 };
 
 enum MQTTClientState {
-  MQTT_CLIENT_DISCONNECTED = 0,
+  MQTT_CLIENT_DISABLED = 0,
+  MQTT_CLIENT_DISCONNECTED,
   MQTT_CLIENT_RESOLVING_ADDRESS,
   MQTT_CLIENT_CONNECTING,
   MQTT_CLIENT_CONNECTED,
@@ -247,6 +248,9 @@ class MQTTClientComponent : public Component {
   void register_mqtt_component(MQTTComponent *component);
 
   bool is_connected();
+  void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
+  void enable();
+  void disable();
 
   void on_shutdown() override;
 
@@ -314,10 +318,11 @@ class MQTTClientComponent : public Component {
   MQTTBackendLibreTiny mqtt_backend_;
 #endif
 
-  MQTTClientState state_{MQTT_CLIENT_DISCONNECTED};
+  MQTTClientState state_{MQTT_CLIENT_DISABLED};
   network::IPAddress ip_;
   bool dns_resolved_{false};
   bool dns_resolve_error_{false};
+  bool enable_on_boot_{true};
   std::vector<MQTTComponent *> children_;
   uint32_t reboot_timeout_{300000};
   uint32_t connect_begin_;
@@ -414,6 +419,26 @@ template<typename... Ts> class MQTTConnectedCondition : public Condition<Ts...>
   MQTTClientComponent *parent_;
 };
 
+template<typename... Ts> class MQTTEnableAction : public Action<Ts...> {
+ public:
+  MQTTEnableAction(MQTTClientComponent *parent) : parent_(parent) {}
+
+  void play(Ts... x) override { this->parent_->enable(); }
+
+ protected:
+  MQTTClientComponent *parent_;
+};
+
+template<typename... Ts> class MQTTDisableAction : public Action<Ts...> {
+ public:
+  MQTTDisableAction(MQTTClientComponent *parent) : parent_(parent) {}
+
+  void play(Ts... x) override { this->parent_->disable(); }
+
+ protected:
+  MQTTClientComponent *parent_;
+};
+
 }  // namespace mqtt
 }  // namespace esphome
 
diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml
index 75c34bec56..d22fe9579f 100644
--- a/tests/components/mqtt/common.yaml
+++ b/tests/components/mqtt/common.yaml
@@ -10,6 +10,7 @@ mqtt:
   port: 1883
   username: debug
   password: debug
+  enable_on_boot: false
   clean_session: True
   client_id: someclient
   use_abbreviations: false
@@ -87,6 +88,8 @@ button:
     state_topic: some/topic/button
     qos: 2
     on_press:
+      - mqtt.disable
+      - mqtt.enable
       - mqtt.publish:
           topic: some/topic/button
           payload: Hello

From 248b0bc378b93b8be87899a65f47b4d7a95b866d Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Fri, 8 Nov 2024 07:05:23 +1100
Subject: [PATCH 080/282] [lvgl] Allow multiple LVGL instances (#7712)

Co-authored-by: clydeps <U5yx99dok9>
---
 esphome/components/lvgl/__init__.py           | 241 +++++++++++-------
 esphome/components/lvgl/automation.py         |  14 +-
 .../components/lvgl/binary_sensor/__init__.py |  23 +-
 esphome/components/lvgl/light/__init__.py     |   8 +-
 esphome/components/lvgl/lvcode.py             |   4 +-
 esphome/components/lvgl/lvgl_esphome.cpp      |  25 +-
 esphome/components/lvgl/lvgl_esphome.h        |  14 +-
 esphome/components/lvgl/number/__init__.py    |  25 +-
 esphome/components/lvgl/select/__init__.py    |  21 +-
 esphome/components/lvgl/sensor/__init__.py    |  22 +-
 esphome/components/lvgl/switch/__init__.py    |  21 +-
 esphome/components/lvgl/text/__init__.py      |  11 +-
 .../components/lvgl/text_sensor/__init__.py   |  30 +--
 esphome/components/lvgl/touchscreens.py       |   5 +-
 esphome/components/lvgl/trigger.py            |  12 +-
 esphome/components/lvgl/widgets/page.py       |   5 +-
 tests/components/lvgl/test.host.yaml          |  32 +++
 17 files changed, 287 insertions(+), 226 deletions(-)

diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py
index 4a1a26cc0b..7476c0a09c 100644
--- a/esphome/components/lvgl/__init__.py
+++ b/esphome/components/lvgl/__init__.py
@@ -27,7 +27,7 @@ from .encoders import ENCODERS_CONFIG, encoders_to_code, initial_focus_to_code
 from .gradient import GRADIENT_SCHEMA, gradients_to_code
 from .hello_world import get_hello_world
 from .lv_validation import lv_bool, lv_images_used
-from .lvcode import LvContext, LvglComponent
+from .lvcode import LvContext, LvglComponent, lvgl_static
 from .schemas import (
     DISP_BG_SCHEMA,
     FLEX_OBJ_SCHEMA,
@@ -152,41 +152,70 @@ def generate_lv_conf_h():
     return LV_CONF_H_FORMAT.format("\n".join(definitions))
 
 
-def final_validation(config):
-    if pages := config.get(CONF_PAGES):
-        if all(p[df.CONF_SKIP] for p in pages):
-            raise cv.Invalid("At least one page must not be skipped")
+def multi_conf_validate(configs: list[dict]):
+    displays = [config[df.CONF_DISPLAYS] for config in configs]
+    # flatten the display list
+    display_list = [disp for disps in displays for disp in disps]
+    if len(display_list) != len(set(display_list)):
+        raise cv.Invalid("A display ID may be used in only one LVGL instance")
+    base_config = configs[0]
+    for config in configs[1:]:
+        for item in (
+            df.CONF_LOG_LEVEL,
+            df.CONF_COLOR_DEPTH,
+            df.CONF_BYTE_ORDER,
+            df.CONF_TRANSPARENCY_KEY,
+        ):
+            if base_config[item] != config[item]:
+                raise cv.Invalid(
+                    f"Config item '{item}' must be the same for all LVGL instances"
+                )
+
+
+def final_validation(configs):
+    multi_conf_validate(configs)
     global_config = full_config.get()
-    for display_id in config[df.CONF_DISPLAYS]:
-        path = global_config.get_path_for_id(display_id)[:-1]
-        display = global_config.get_config_for_path(path)
-        if CONF_LAMBDA in display:
-            raise cv.Invalid("Using lambda: in display config not compatible with LVGL")
-        if display[CONF_AUTO_CLEAR_ENABLED]:
-            raise cv.Invalid(
-                "Using auto_clear_enabled: true in display config not compatible with LVGL"
-            )
-    buffer_frac = config[CONF_BUFFER_SIZE]
-    if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config:
-        LOGGER.warning("buffer_size: may need to be reduced without PSRAM")
-    for image_id in lv_images_used:
-        path = global_config.get_path_for_id(image_id)[:-1]
-        image_conf = global_config.get_config_for_path(path)
-        if image_conf[CONF_TYPE] in ("RGBA", "RGB24"):
-            raise cv.Invalid(
-                "Using RGBA or RGB24 in image config not compatible with LVGL", path
-            )
-    for w in focused_widgets:
-        path = global_config.get_path_for_id(w)
-        widget_conf = global_config.get_config_for_path(path[:-1])
-        if df.CONF_ADJUSTABLE in widget_conf and not widget_conf[df.CONF_ADJUSTABLE]:
-            raise cv.Invalid(
-                "A non adjustable arc may not be focused",
-                path,
-            )
+    for config in configs:
+        if pages := config.get(CONF_PAGES):
+            if all(p[df.CONF_SKIP] for p in pages):
+                raise cv.Invalid("At least one page must not be skipped")
+        for display_id in config[df.CONF_DISPLAYS]:
+            path = global_config.get_path_for_id(display_id)[:-1]
+            display = global_config.get_config_for_path(path)
+            if CONF_LAMBDA in display:
+                raise cv.Invalid(
+                    "Using lambda: in display config not compatible with LVGL"
+                )
+            if display[CONF_AUTO_CLEAR_ENABLED]:
+                raise cv.Invalid(
+                    "Using auto_clear_enabled: true in display config not compatible with LVGL"
+                )
+        buffer_frac = config[CONF_BUFFER_SIZE]
+        if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config:
+            LOGGER.warning("buffer_size: may need to be reduced without PSRAM")
+        for image_id in lv_images_used:
+            path = global_config.get_path_for_id(image_id)[:-1]
+            image_conf = global_config.get_config_for_path(path)
+            if image_conf[CONF_TYPE] in ("RGBA", "RGB24"):
+                raise cv.Invalid(
+                    "Using RGBA or RGB24 in image config not compatible with LVGL", path
+                )
+        for w in focused_widgets:
+            path = global_config.get_path_for_id(w)
+            widget_conf = global_config.get_config_for_path(path[:-1])
+            if (
+                df.CONF_ADJUSTABLE in widget_conf
+                and not widget_conf[df.CONF_ADJUSTABLE]
+            ):
+                raise cv.Invalid(
+                    "A non adjustable arc may not be focused",
+                    path,
+                )
 
 
-async def to_code(config):
+async def to_code(configs):
+    config_0 = configs[0]
+    # Global configuration
     cg.add_library("lvgl/lvgl", "8.4.0")
     cg.add_define("USE_LVGL")
     # suppress default enabling of extra widgets
@@ -203,53 +232,33 @@ async def to_code(config):
     add_define("LV_MEM_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"')
 
     add_define(
-        "LV_LOG_LEVEL", f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config[df.CONF_LOG_LEVEL]]}"
+        "LV_LOG_LEVEL",
+        f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config_0[df.CONF_LOG_LEVEL]]}",
     )
     cg.add_define(
         "LVGL_LOG_LEVEL",
-        cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config[df.CONF_LOG_LEVEL]}"),
+        cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[df.CONF_LOG_LEVEL]}"),
     )
-    add_define("LV_COLOR_DEPTH", config[df.CONF_COLOR_DEPTH])
+    add_define("LV_COLOR_DEPTH", config_0[df.CONF_COLOR_DEPTH])
     for font in helpers.lv_fonts_used:
         add_define(f"LV_FONT_{font.upper()}")
 
-    if config[df.CONF_COLOR_DEPTH] == 16:
+    if config_0[df.CONF_COLOR_DEPTH] == 16:
         add_define(
             "LV_COLOR_16_SWAP",
-            "1" if config[df.CONF_BYTE_ORDER] == "big_endian" else "0",
+            "1" if config_0[df.CONF_BYTE_ORDER] == "big_endian" else "0",
         )
     add_define(
         "LV_COLOR_CHROMA_KEY",
-        await lvalid.lv_color.process(config[df.CONF_TRANSPARENCY_KEY]),
+        await lvalid.lv_color.process(config_0[df.CONF_TRANSPARENCY_KEY]),
     )
     cg.add_build_flag("-Isrc")
 
     cg.add_global(lvgl_ns.using)
-    frac = config[CONF_BUFFER_SIZE]
-    if frac >= 0.75:
-        frac = 1
-    elif frac >= 0.375:
-        frac = 2
-    elif frac > 0.19:
-        frac = 4
-    else:
-        frac = 8
-    displays = [await cg.get_variable(display) for display in config[df.CONF_DISPLAYS]]
-    lv_component = cg.new_Pvariable(
-        config[CONF_ID],
-        displays,
-        frac,
-        config[df.CONF_FULL_REFRESH],
-        config[df.CONF_DRAW_ROUNDING],
-        config[df.CONF_RESUME_ON_INPUT],
-    )
-    await cg.register_component(lv_component, config)
-    Widget.create(config[CONF_ID], lv_component, obj_spec, config)
-
     for font in helpers.esphome_fonts_used:
         await cg.get_variable(font)
         cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font))
-    default_font = config[df.CONF_DEFAULT_FONT]
+    default_font = config_0[df.CONF_DEFAULT_FONT]
     if not lvalid.is_lv_font(default_font):
         add_define(
             "LV_FONT_CUSTOM_DECLARE", f"LV_FONT_DECLARE(*{df.DEFAULT_ESPHOME_FONT})"
@@ -265,39 +274,71 @@ async def to_code(config):
         add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT)
     else:
         add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font))
+    cg.add(lvgl_static.esphome_lvgl_init())
 
-    lv_scr_act = get_scr_act(lv_component)
-    async with LvContext(lv_component):
-        await touchscreens_to_code(lv_component, config)
-        await encoders_to_code(lv_component, config)
-        await theme_to_code(config)
-        await styles_to_code(config)
-        await gradients_to_code(config)
-        await set_obj_properties(lv_scr_act, config)
-        await add_widgets(lv_scr_act, config)
-        await add_pages(lv_component, config)
-        await add_top_layer(lv_component, config)
-        await msgboxes_to_code(lv_component, config)
-        await disp_update(lv_component.get_disp(), config)
+    for config in configs:
+        frac = config[CONF_BUFFER_SIZE]
+        if frac >= 0.75:
+            frac = 1
+        elif frac >= 0.375:
+            frac = 2
+        elif frac > 0.19:
+            frac = 4
+        else:
+            frac = 8
+        displays = [
+            await cg.get_variable(display) for display in config[df.CONF_DISPLAYS]
+        ]
+        lv_component = cg.new_Pvariable(
+            config[CONF_ID],
+            displays,
+            frac,
+            config[df.CONF_FULL_REFRESH],
+            config[df.CONF_DRAW_ROUNDING],
+            config[df.CONF_RESUME_ON_INPUT],
+        )
+        await cg.register_component(lv_component, config)
+        Widget.create(config[CONF_ID], lv_component, obj_spec, config)
+
+        lv_scr_act = get_scr_act(lv_component)
+        async with LvContext():
+            await touchscreens_to_code(lv_component, config)
+            await encoders_to_code(lv_component, config)
+            await theme_to_code(config)
+            await styles_to_code(config)
+            await gradients_to_code(config)
+            await set_obj_properties(lv_scr_act, config)
+            await add_widgets(lv_scr_act, config)
+            await add_pages(lv_component, config)
+            await add_top_layer(lv_component, config)
+            await msgboxes_to_code(lv_component, config)
+            await disp_update(lv_component.get_disp(), config)
     # Set this directly since we are limited in how many methods can be added to the Widget class.
     Widget.widgets_completed = True
-    async with LvContext(lv_component):
-        await generate_triggers(lv_component)
-        await generate_page_triggers(lv_component, config)
-        await initial_focus_to_code(config)
-        for conf in config.get(CONF_ON_IDLE, ()):
-            templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32)
-            idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ)
-            await build_automation(idle_trigger, [], conf)
-        for conf in config.get(df.CONF_ON_PAUSE, ()):
-            pause_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, True)
-            await build_automation(pause_trigger, [], conf)
-        for conf in config.get(df.CONF_ON_RESUME, ()):
-            resume_trigger = cg.new_Pvariable(
-                conf[CONF_TRIGGER_ID], lv_component, False
-            )
-            await build_automation(resume_trigger, [], conf)
+    async with LvContext():
+        await generate_triggers()
+        for config in configs:
+            lv_component = await cg.get_variable(config[CONF_ID])
+            await generate_page_triggers(config)
+            await initial_focus_to_code(config)
+            for conf in config.get(CONF_ON_IDLE, ()):
+                templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32)
+                idle_trigger = cg.new_Pvariable(
+                    conf[CONF_TRIGGER_ID], lv_component, templ
+                )
+                await build_automation(idle_trigger, [], conf)
+            for conf in config.get(df.CONF_ON_PAUSE, ()):
+                pause_trigger = cg.new_Pvariable(
+                    conf[CONF_TRIGGER_ID], lv_component, True
+                )
+                await build_automation(pause_trigger, [], conf)
+            for conf in config.get(df.CONF_ON_RESUME, ()):
+                resume_trigger = cg.new_Pvariable(
+                    conf[CONF_TRIGGER_ID], lv_component, False
+                )
+                await build_automation(resume_trigger, [], conf)
 
+    # This must be done after all widgets are created
     for comp in helpers.lvgl_components_required:
         cg.add_define(f"USE_LVGL_{comp.upper()}")
     if "transform_angle" in styles_used:
@@ -312,7 +353,10 @@ async def to_code(config):
 
 def display_schema(config):
     value = cv.ensure_list(cv.use_id(Display))(config)
-    return value or [cv.use_id(Display)(config)]
+    value = value or [cv.use_id(Display)(config)]
+    if len(set(value)) != len(value):
+        raise cv.Invalid("Display IDs must be unique")
+    return value
 
 
 def add_hello_world(config):
@@ -324,7 +368,7 @@ def add_hello_world(config):
 
 FINAL_VALIDATE_SCHEMA = final_validation
 
-CONFIG_SCHEMA = (
+LVGL_SCHEMA = (
     cv.polling_component_schema("1s")
     .extend(obj_schema(obj_spec))
     .extend(
@@ -393,3 +437,16 @@ CONFIG_SCHEMA = (
     .extend(DISP_BG_SCHEMA)
     .add_extra(add_hello_world)
 )
+
+
+def lvgl_config_schema(config):
+    """
+    Can't use cv.ensure_list here because it converts an empty config to an empty list,
+    rather than a default config.
+    """
+    if not config or isinstance(config, dict):
+        return [LVGL_SCHEMA(config)]
+    return cv.Schema([LVGL_SCHEMA])(config)
+
+
+CONFIG_SCHEMA = lvgl_config_schema
diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py
index 48472354f8..58e3dd808b 100644
--- a/esphome/components/lvgl/automation.py
+++ b/esphome/components/lvgl/automation.py
@@ -137,20 +137,18 @@ async def disp_update(disp, config: dict):
         cv.maybe_simple_value(
             {
                 cv.Required(CONF_ID): cv.use_id(lv_obj_t),
-                cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent),
             },
             key=CONF_ID,
         ),
-        cv.Schema(
-            {
-                cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent),
-            }
-        ),
+        LVGL_SCHEMA,
     ),
 )
 async def obj_invalidate_to_code(config, action_id, template_arg, args):
-    lv_comp = await cg.get_variable(config[CONF_LVGL_ID])
-    widgets = await get_widgets(config) or [get_scr_act(lv_comp)]
+    if CONF_LVGL_ID in config:
+        lv_comp = await cg.get_variable(config[CONF_LVGL_ID])
+        widgets = [get_scr_act(lv_comp)]
+    else:
+        widgets = await get_widgets(config)
 
     async def do_invalidate(widget: Widget):
         lv_obj.invalidate(widget.obj)
diff --git a/esphome/components/lvgl/binary_sensor/__init__.py b/esphome/components/lvgl/binary_sensor/__init__.py
index 56984405aa..ffbdc977b2 100644
--- a/esphome/components/lvgl/binary_sensor/__init__.py
+++ b/esphome/components/lvgl/binary_sensor/__init__.py
@@ -1,4 +1,3 @@
-import esphome.codegen as cg
 from esphome.components.binary_sensor import (
     BinarySensor,
     binary_sensor_schema,
@@ -6,36 +5,30 @@ from esphome.components.binary_sensor import (
 )
 import esphome.config_validation as cv
 
-from ..defines import CONF_LVGL_ID, CONF_WIDGET
-from ..lvcode import EVENT_ARG, LambdaContext, LvContext
-from ..schemas import LVGL_SCHEMA
+from ..defines import CONF_WIDGET
+from ..lvcode import EVENT_ARG, LambdaContext, LvContext, lvgl_static
 from ..types import LV_EVENT, lv_pseudo_button_t
 from ..widgets import Widget, get_widgets, wait_for_widgets
 
-CONFIG_SCHEMA = (
-    binary_sensor_schema(BinarySensor)
-    .extend(LVGL_SCHEMA)
-    .extend(
-        {
-            cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t),
-        }
-    )
+CONFIG_SCHEMA = binary_sensor_schema(BinarySensor).extend(
+    {
+        cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t),
+    }
 )
 
 
 async def to_code(config):
     sensor = await new_binary_sensor(config)
-    paren = await cg.get_variable(config[CONF_LVGL_ID])
     widget = await get_widgets(config, CONF_WIDGET)
     widget = widget[0]
     assert isinstance(widget, Widget)
     await wait_for_widgets()
     async with LambdaContext(EVENT_ARG) as pressed_ctx:
         pressed_ctx.add(sensor.publish_state(widget.is_pressed()))
-    async with LvContext(paren) as ctx:
+    async with LvContext() as ctx:
         ctx.add(sensor.publish_initial_state(widget.is_pressed()))
         ctx.add(
-            paren.add_event_cb(
+            lvgl_static.add_event_cb(
                 widget.obj,
                 await pressed_ctx.get_lambda(),
                 LV_EVENT.PRESSING,
diff --git a/esphome/components/lvgl/light/__init__.py b/esphome/components/lvgl/light/__init__.py
index 8031ae8221..dcdf67a520 100644
--- a/esphome/components/lvgl/light/__init__.py
+++ b/esphome/components/lvgl/light/__init__.py
@@ -4,9 +4,8 @@ from esphome.components.light import LightOutput
 import esphome.config_validation as cv
 from esphome.const import CONF_GAMMA_CORRECT, CONF_OUTPUT_ID
 
-from ..defines import CONF_LVGL_ID, CONF_WIDGET
+from ..defines import CONF_WIDGET
 from ..lvcode import LvContext
-from ..schemas import LVGL_SCHEMA
 from ..types import LvType, lvgl_ns
 from ..widgets import get_widgets, wait_for_widgets
 
@@ -18,16 +17,15 @@ CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
         cv.Required(CONF_WIDGET): cv.use_id(lv_led_t),
         cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(LVLight),
     }
-).extend(LVGL_SCHEMA)
+)
 
 
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
     await light.register_light(var, config)
 
-    paren = await cg.get_variable(config[CONF_LVGL_ID])
     widget = await get_widgets(config, CONF_WIDGET)
     widget = widget[0]
     await wait_for_widgets()
-    async with LvContext(paren) as ctx:
+    async with LvContext() as ctx:
         ctx.add(var.set_obj(widget.obj))
diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py
index 37d6670b84..6b98cc4251 100644
--- a/esphome/components/lvgl/lvcode.py
+++ b/esphome/components/lvgl/lvcode.py
@@ -178,10 +178,9 @@ class LvContext(LambdaContext):
 
     added_lambda_count = 0
 
-    def __init__(self, lv_component, args=None):
+    def __init__(self, args=None):
         self.args = args or LVGL_COMP_ARG
         super().__init__(parameters=self.args)
-        self.lv_component = lv_component
 
     async def __aexit__(self, exc_type, exc_val, exc_tb):
         await super().__aexit__(exc_type, exc_val, exc_tb)
@@ -298,6 +297,7 @@ lv_expr = LvExpr("lv_")
 lv_obj = MockLv("lv_obj_")
 # Operations on the LVGL component
 lvgl_comp = MockObj(LVGL_COMP, "->")
+lvgl_static = MockObj("LvglComponent", "::")
 
 
 # equivalent to cg.add() for the current code context
diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp
index 70cfb859de..41346bc732 100644
--- a/esphome/components/lvgl/lvgl_esphome.cpp
+++ b/esphome/components/lvgl/lvgl_esphome.cpp
@@ -98,19 +98,24 @@ void LvglComponent::set_paused(bool paused, bool show_snow) {
   this->pause_callbacks_.call(paused);
 }
 
+void LvglComponent::esphome_lvgl_init() {
+  lv_init();
+  lv_update_event = static_cast<lv_event_code_t>(lv_event_register_id());
+  lv_api_event = static_cast<lv_event_code_t>(lv_event_register_id());
+}
 void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) {
-  lv_obj_add_event_cb(obj, callback, event, this);
+  lv_obj_add_event_cb(obj, callback, event, nullptr);
 }
 void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1,
                                  lv_event_code_t event2) {
-  this->add_event_cb(obj, callback, event1);
-  this->add_event_cb(obj, callback, event2);
+  add_event_cb(obj, callback, event1);
+  add_event_cb(obj, callback, event2);
 }
 void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1,
                                  lv_event_code_t event2, lv_event_code_t event3) {
-  this->add_event_cb(obj, callback, event1);
-  this->add_event_cb(obj, callback, event2);
-  this->add_event_cb(obj, callback, event3);
+  add_event_cb(obj, callback, event1);
+  add_event_cb(obj, callback, event2);
+  add_event_cb(obj, callback, event3);
 }
 void LvglComponent::add_page(LvPageType *page) {
   this->pages_.push_back(page);
@@ -218,8 +223,10 @@ PauseTrigger::PauseTrigger(LvglComponent *parent, TemplatableValue<bool> paused)
 }
 
 #ifdef USE_LVGL_TOUCHSCREEN
-LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time) {
+LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent) {
+  this->set_parent(parent);
   lv_indev_drv_init(&this->drv_);
+  this->drv_.disp = parent->get_disp();
   this->drv_.long_press_repeat_time = long_press_repeat_time;
   this->drv_.long_press_time = long_press_time;
   this->drv_.type = LV_INDEV_TYPE_POINTER;
@@ -235,6 +242,7 @@ LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_r
     }
   };
 }
+
 void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) {
   this->touch_pressed_ = !this->parent_->is_paused() && !tpoints.empty();
   if (this->touch_pressed_)
@@ -405,9 +413,6 @@ LvglComponent::LvglComponent(std::vector<display::Display *> displays, float buf
       buffer_frac_(buffer_frac),
       full_refresh_(full_refresh),
       resume_on_input_(resume_on_input) {
-  lv_init();
-  lv_update_event = static_cast<lv_event_code_t>(lv_event_register_id());
-  lv_api_event = static_cast<lv_event_code_t>(lv_event_register_id());
   auto *display = this->displays_[0];
   size_t buffer_pixels = display->get_width() * display->get_height() / this->buffer_frac_;
   auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8;
diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h
index f357c4950c..dae07d5153 100644
--- a/esphome/components/lvgl/lvgl_esphome.h
+++ b/esphome/components/lvgl/lvgl_esphome.h
@@ -146,10 +146,14 @@ class LvglComponent : public PollingComponent {
     }
   }
 
-  void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event);
-  void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2);
-  void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2,
-                    lv_event_code_t event3);
+  /**
+   * Initialize the LVGL library and register custom events.
+   */
+  static void esphome_lvgl_init();
+  static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event);
+  static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2);
+  static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2,
+                           lv_event_code_t event3);
   void add_page(LvPageType *page);
   void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time);
   void show_next_page(lv_scr_load_anim_t anim, uint32_t time);
@@ -231,7 +235,7 @@ template<typename... Ts> class LvglCondition : public Condition<Ts...>, public P
 #ifdef USE_LVGL_TOUCHSCREEN
 class LVTouchListener : public touchscreen::TouchListener, public Parented<LvglComponent> {
  public:
-  LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time);
+  LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent);
   void update(const touchscreen::TouchPoints_t &tpoints) override;
   void release() override {
     touch_pressed_ = false;
diff --git a/esphome/components/lvgl/number/__init__.py b/esphome/components/lvgl/number/__init__.py
index 07f92635b5..b41a36bc0f 100644
--- a/esphome/components/lvgl/number/__init__.py
+++ b/esphome/components/lvgl/number/__init__.py
@@ -3,7 +3,7 @@ from esphome.components import number
 import esphome.config_validation as cv
 from esphome.cpp_generator import MockObj
 
-from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_UPDATE_ON_RELEASE, CONF_WIDGET
+from ..defines import CONF_ANIMATED, CONF_UPDATE_ON_RELEASE, CONF_WIDGET
 from ..lv_validation import animated
 from ..lvcode import (
     API_EVENT,
@@ -13,28 +13,23 @@ from ..lvcode import (
     LvContext,
     lv,
     lv_add,
+    lvgl_static,
 )
-from ..schemas import LVGL_SCHEMA
 from ..types import LV_EVENT, LvNumber, lvgl_ns
 from ..widgets import get_widgets, wait_for_widgets
 
 LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number)
 
-CONFIG_SCHEMA = (
-    number.number_schema(LVGLNumber)
-    .extend(LVGL_SCHEMA)
-    .extend(
-        {
-            cv.Required(CONF_WIDGET): cv.use_id(LvNumber),
-            cv.Optional(CONF_ANIMATED, default=True): animated,
-            cv.Optional(CONF_UPDATE_ON_RELEASE, default=False): cv.boolean,
-        }
-    )
+CONFIG_SCHEMA = number.number_schema(LVGLNumber).extend(
+    {
+        cv.Required(CONF_WIDGET): cv.use_id(LvNumber),
+        cv.Optional(CONF_ANIMATED, default=True): animated,
+        cv.Optional(CONF_UPDATE_ON_RELEASE, default=False): cv.boolean,
+    }
 )
 
 
 async def to_code(config):
-    paren = await cg.get_variable(config[CONF_LVGL_ID])
     widget = await get_widgets(config, CONF_WIDGET)
     widget = widget[0]
     var = await number.new_number(
@@ -58,10 +53,10 @@ async def to_code(config):
         if not config[CONF_UPDATE_ON_RELEASE]
         else LV_EVENT.RELEASED
     )
-    async with LvContext(paren):
+    async with LvContext():
         lv_add(var.set_control_lambda(await control.get_lambda()))
         lv_add(
-            paren.add_event_cb(
+            lvgl_static.add_event_cb(
                 widget.obj, await event.get_lambda(), UPDATE_EVENT, event_code
             )
         )
diff --git a/esphome/components/lvgl/select/__init__.py b/esphome/components/lvgl/select/__init__.py
index 5e50b6b385..bd5ef8f237 100644
--- a/esphome/components/lvgl/select/__init__.py
+++ b/esphome/components/lvgl/select/__init__.py
@@ -1,25 +1,19 @@
-import esphome.codegen as cg
 from esphome.components import select
 import esphome.config_validation as cv
 from esphome.const import CONF_OPTIONS
 
-from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_WIDGET, literal
+from ..defines import CONF_ANIMATED, CONF_WIDGET, literal
 from ..lvcode import LvContext
-from ..schemas import LVGL_SCHEMA
 from ..types import LvSelect, lvgl_ns
 from ..widgets import get_widgets, wait_for_widgets
 
 LVGLSelect = lvgl_ns.class_("LVGLSelect", select.Select)
 
-CONFIG_SCHEMA = (
-    select.select_schema(LVGLSelect)
-    .extend(LVGL_SCHEMA)
-    .extend(
-        {
-            cv.Required(CONF_WIDGET): cv.use_id(LvSelect),
-            cv.Optional(CONF_ANIMATED, default=False): cv.boolean,
-        }
-    )
+CONFIG_SCHEMA = select.select_schema(LVGLSelect).extend(
+    {
+        cv.Required(CONF_WIDGET): cv.use_id(LvSelect),
+        cv.Optional(CONF_ANIMATED, default=False): cv.boolean,
+    }
 )
 
 
@@ -28,9 +22,8 @@ async def to_code(config):
     widget = widget[0]
     options = widget.config.get(CONF_OPTIONS, [])
     selector = await select.new_select(config, options=options)
-    paren = await cg.get_variable(config[CONF_LVGL_ID])
     await wait_for_widgets()
-    async with LvContext(paren) as ctx:
+    async with LvContext() as ctx:
         ctx.add(
             selector.set_widget(
                 widget.var,
diff --git a/esphome/components/lvgl/sensor/__init__.py b/esphome/components/lvgl/sensor/__init__.py
index a2a2298c27..03b2638ed0 100644
--- a/esphome/components/lvgl/sensor/__init__.py
+++ b/esphome/components/lvgl/sensor/__init__.py
@@ -1,8 +1,7 @@
-import esphome.codegen as cg
 from esphome.components.sensor import Sensor, new_sensor, sensor_schema
 import esphome.config_validation as cv
 
-from ..defines import CONF_LVGL_ID, CONF_WIDGET
+from ..defines import CONF_WIDGET
 from ..lvcode import (
     API_EVENT,
     EVENT_ARG,
@@ -11,34 +10,29 @@ from ..lvcode import (
     LambdaContext,
     LvContext,
     lv_add,
+    lvgl_static,
 )
-from ..schemas import LVGL_SCHEMA
 from ..types import LV_EVENT, LvNumber
 from ..widgets import Widget, get_widgets, wait_for_widgets
 
-CONFIG_SCHEMA = (
-    sensor_schema(Sensor)
-    .extend(LVGL_SCHEMA)
-    .extend(
-        {
-            cv.Required(CONF_WIDGET): cv.use_id(LvNumber),
-        }
-    )
+CONFIG_SCHEMA = sensor_schema(Sensor).extend(
+    {
+        cv.Required(CONF_WIDGET): cv.use_id(LvNumber),
+    }
 )
 
 
 async def to_code(config):
     sensor = await new_sensor(config)
-    paren = await cg.get_variable(config[CONF_LVGL_ID])
     widget = await get_widgets(config, CONF_WIDGET)
     widget = widget[0]
     assert isinstance(widget, Widget)
     await wait_for_widgets()
     async with LambdaContext(EVENT_ARG) as lamb:
         lv_add(sensor.publish_state(widget.get_value()))
-    async with LvContext(paren, LVGL_COMP_ARG):
+    async with LvContext(LVGL_COMP_ARG):
         lv_add(
-            paren.add_event_cb(
+            lvgl_static.add_event_cb(
                 widget.obj,
                 await lamb.get_lambda(),
                 LV_EVENT.VALUE_CHANGED,
diff --git a/esphome/components/lvgl/switch/__init__.py b/esphome/components/lvgl/switch/__init__.py
index 8c090543f9..4e1e7f72e0 100644
--- a/esphome/components/lvgl/switch/__init__.py
+++ b/esphome/components/lvgl/switch/__init__.py
@@ -3,7 +3,7 @@ from esphome.components.switch import Switch, new_switch, switch_schema
 import esphome.config_validation as cv
 from esphome.cpp_generator import MockObj
 
-from ..defines import CONF_LVGL_ID, CONF_WIDGET, literal
+from ..defines import CONF_WIDGET, literal
 from ..lvcode import (
     API_EVENT,
     EVENT_ARG,
@@ -13,26 +13,21 @@ from ..lvcode import (
     LvContext,
     lv,
     lv_add,
+    lvgl_static,
 )
-from ..schemas import LVGL_SCHEMA
 from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t, lvgl_ns
 from ..widgets import get_widgets, wait_for_widgets
 
 LVGLSwitch = lvgl_ns.class_("LVGLSwitch", Switch)
-CONFIG_SCHEMA = (
-    switch_schema(LVGLSwitch)
-    .extend(LVGL_SCHEMA)
-    .extend(
-        {
-            cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t),
-        }
-    )
+CONFIG_SCHEMA = switch_schema(LVGLSwitch).extend(
+    {
+        cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t),
+    }
 )
 
 
 async def to_code(config):
     switch = await new_switch(config)
-    paren = await cg.get_variable(config[CONF_LVGL_ID])
     widget = await get_widgets(config, CONF_WIDGET)
     widget = widget[0]
     await wait_for_widgets()
@@ -45,10 +40,10 @@ async def to_code(config):
             widget.clear_state(LV_STATE.CHECKED)
         lv.event_send(widget.obj, API_EVENT, cg.nullptr)
         control.add(switch.publish_state(literal("v")))
-    async with LvContext(paren) as ctx:
+    async with LvContext() as ctx:
         lv_add(switch.set_control_lambda(await control.get_lambda()))
         ctx.add(
-            paren.add_event_cb(
+            lvgl_static.add_event_cb(
                 widget.obj,
                 await checked_ctx.get_lambda(),
                 LV_EVENT.VALUE_CHANGED,
diff --git a/esphome/components/lvgl/text/__init__.py b/esphome/components/lvgl/text/__init__.py
index a59e703591..89db139a6a 100644
--- a/esphome/components/lvgl/text/__init__.py
+++ b/esphome/components/lvgl/text/__init__.py
@@ -3,7 +3,7 @@ from esphome.components import text
 from esphome.components.text import new_text
 import esphome.config_validation as cv
 
-from ..defines import CONF_LVGL_ID, CONF_WIDGET
+from ..defines import CONF_WIDGET
 from ..lvcode import (
     API_EVENT,
     EVENT_ARG,
@@ -12,14 +12,14 @@ from ..lvcode import (
     LvContext,
     lv,
     lv_add,
+    lvgl_static,
 )
-from ..schemas import LVGL_SCHEMA
 from ..types import LV_EVENT, LvText, lvgl_ns
 from ..widgets import get_widgets, wait_for_widgets
 
 LVGLText = lvgl_ns.class_("LVGLText", text.Text)
 
-CONFIG_SCHEMA = text.TEXT_SCHEMA.extend(LVGL_SCHEMA).extend(
+CONFIG_SCHEMA = text.TEXT_SCHEMA.extend(
     {
         cv.GenerateID(): cv.declare_id(LVGLText),
         cv.Required(CONF_WIDGET): cv.use_id(LvText),
@@ -29,7 +29,6 @@ CONFIG_SCHEMA = text.TEXT_SCHEMA.extend(LVGL_SCHEMA).extend(
 
 async def to_code(config):
     textvar = await new_text(config)
-    paren = await cg.get_variable(config[CONF_LVGL_ID])
     widget = await get_widgets(config, CONF_WIDGET)
     widget = widget[0]
     await wait_for_widgets()
@@ -39,10 +38,10 @@ async def to_code(config):
         control.add(textvar.publish_state(widget.get_value()))
     async with LambdaContext(EVENT_ARG) as lamb:
         lv_add(textvar.publish_state(widget.get_value()))
-    async with LvContext(paren):
+    async with LvContext():
         lv_add(textvar.set_control_lambda(await control.get_lambda()))
         lv_add(
-            paren.add_event_cb(
+            lvgl_static.add_event_cb(
                 widget.obj,
                 await lamb.get_lambda(),
                 LV_EVENT.VALUE_CHANGED,
diff --git a/esphome/components/lvgl/text_sensor/__init__.py b/esphome/components/lvgl/text_sensor/__init__.py
index ae39eec291..4728fd137a 100644
--- a/esphome/components/lvgl/text_sensor/__init__.py
+++ b/esphome/components/lvgl/text_sensor/__init__.py
@@ -1,4 +1,3 @@
-import esphome.codegen as cg
 from esphome.components.text_sensor import (
     TextSensor,
     new_text_sensor,
@@ -6,34 +5,35 @@ from esphome.components.text_sensor import (
 )
 import esphome.config_validation as cv
 
-from ..defines import CONF_LVGL_ID, CONF_WIDGET
-from ..lvcode import API_EVENT, EVENT_ARG, UPDATE_EVENT, LambdaContext, LvContext
-from ..schemas import LVGL_SCHEMA
+from ..defines import CONF_WIDGET
+from ..lvcode import (
+    API_EVENT,
+    EVENT_ARG,
+    UPDATE_EVENT,
+    LambdaContext,
+    LvContext,
+    lvgl_static,
+)
 from ..types import LV_EVENT, LvText
 from ..widgets import get_widgets, wait_for_widgets
 
-CONFIG_SCHEMA = (
-    text_sensor_schema(TextSensor)
-    .extend(LVGL_SCHEMA)
-    .extend(
-        {
-            cv.Required(CONF_WIDGET): cv.use_id(LvText),
-        }
-    )
+CONFIG_SCHEMA = text_sensor_schema(TextSensor).extend(
+    {
+        cv.Required(CONF_WIDGET): cv.use_id(LvText),
+    }
 )
 
 
 async def to_code(config):
     sensor = await new_text_sensor(config)
-    paren = await cg.get_variable(config[CONF_LVGL_ID])
     widget = await get_widgets(config, CONF_WIDGET)
     widget = widget[0]
     await wait_for_widgets()
     async with LambdaContext(EVENT_ARG) as pressed_ctx:
         pressed_ctx.add(sensor.publish_state(widget.get_value()))
-    async with LvContext(paren) as ctx:
+    async with LvContext() as ctx:
         ctx.add(
-            paren.add_event_cb(
+            lvgl_static.add_event_cb(
                 widget.obj,
                 await pressed_ctx.get_lambda(),
                 LV_EVENT.VALUE_CHANGED,
diff --git a/esphome/components/lvgl/touchscreens.py b/esphome/components/lvgl/touchscreens.py
index 4d430a428e..f2dd013f6d 100644
--- a/esphome/components/lvgl/touchscreens.py
+++ b/esphome/components/lvgl/touchscreens.py
@@ -33,13 +33,12 @@ def touchscreen_schema(config):
     return [TOUCHSCREENS_CONFIG(config)]
 
 
-async def touchscreens_to_code(var, config):
+async def touchscreens_to_code(lv_component, config):
     for tconf in config[CONF_TOUCHSCREENS]:
         lvgl_components_required.add(CONF_TOUCHSCREEN)
         touchscreen = await cg.get_variable(tconf[CONF_TOUCHSCREEN_ID])
         lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds
         lprt = tconf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds
-        listener = cg.new_Pvariable(tconf[CONF_ID], lpt, lprt)
-        await cg.register_parented(listener, var)
+        listener = cg.new_Pvariable(tconf[CONF_ID], lpt, lprt, lv_component)
         lv.indev_drv_register(listener.get_drv())
         cg.add(touchscreen.register_listener(listener))
diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py
index eb6e483203..fb856df04e 100644
--- a/esphome/components/lvgl/trigger.py
+++ b/esphome/components/lvgl/trigger.py
@@ -20,17 +20,16 @@ from .lvcode import (
     lv,
     lv_add,
     lv_event_t_ptr,
+    lvgl_static,
 )
 from .types import LV_EVENT
 from .widgets import widget_map
 
 
-async def generate_triggers(lv_component):
+async def generate_triggers():
     """
     Generate LVGL triggers for all defined widgets
     Must be done after all widgets completed
-    :param lv_component:  The parent component
-    :return:
     """
 
     for w in widget_map.values():
@@ -43,11 +42,10 @@ async def generate_triggers(lv_component):
                 conf = conf[0]
                 w.add_flag("LV_OBJ_FLAG_CLICKABLE")
                 event = literal("LV_EVENT_" + LV_EVENT_MAP[event[3:].upper()])
-                await add_trigger(conf, lv_component, w, event)
+                await add_trigger(conf, w, event)
             for conf in w.config.get(CONF_ON_VALUE, ()):
                 await add_trigger(
                     conf,
-                    lv_component,
                     w,
                     LV_EVENT.VALUE_CHANGED,
                     API_EVENT,
@@ -63,7 +61,7 @@ async def generate_triggers(lv_component):
                 lv.obj_align_to(w.obj, target, align, x, y)
 
 
-async def add_trigger(conf, lv_component, w, *events):
+async def add_trigger(conf, w, *events):
     tid = conf[CONF_TRIGGER_ID]
     trigger = cg.new_Pvariable(tid)
     args = w.get_args() + [(lv_event_t_ptr, "event")]
@@ -72,4 +70,4 @@ async def add_trigger(conf, lv_component, w, *events):
     async with LambdaContext(EVENT_ARG, where=tid) as context:
         with LvConditional(w.is_selected()):
             lv_add(trigger.trigger(*value, literal("event")))
-    lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), *events))
+    lv_add(lvgl_static.add_event_cb(w.obj, await context.get_lambda(), *events))
diff --git a/esphome/components/lvgl/widgets/page.py b/esphome/components/lvgl/widgets/page.py
index 0e84ab6791..a754a9cb9a 100644
--- a/esphome/components/lvgl/widgets/page.py
+++ b/esphome/components/lvgl/widgets/page.py
@@ -20,6 +20,7 @@ from ..lvcode import (
     add_line_marks,
     lv_add,
     lvgl_comp,
+    lvgl_static,
 )
 from ..schemas import LVGL_SCHEMA
 from ..types import LvglAction, lv_page_t
@@ -139,7 +140,7 @@ async def add_pages(lv_component, config):
         await add_widgets(page, pconf)
 
 
-async def generate_page_triggers(lv_component, config):
+async def generate_page_triggers(config):
     for pconf in config.get(CONF_PAGES, ()):
         page = (await get_widgets(pconf))[0]
         for ev in (CONF_ON_LOAD, CONF_ON_UNLOAD):
@@ -149,7 +150,7 @@ async def generate_page_triggers(lv_component, config):
                 async with LambdaContext(EVENT_ARG, where=id) as context:
                     lv_add(trigger.trigger())
                 lv_add(
-                    lv_component.add_event_cb(
+                    lvgl_static.add_event_cb(
                         page.obj,
                         await context.get_lambda(),
                         literal(f"LV_EVENT_SCREEN_{ev[3:].upper()}_START"),
diff --git a/tests/components/lvgl/test.host.yaml b/tests/components/lvgl/test.host.yaml
index 3a490bbe15..34918cb113 100644
--- a/tests/components/lvgl/test.host.yaml
+++ b/tests/components/lvgl/test.host.yaml
@@ -1,5 +1,12 @@
 display:
   - platform: sdl
+    id: sdl0
+    auto_clear_enabled: false
+    dimensions:
+      width: 480
+      height: 320
+  - platform: sdl
+    id: sdl1
     auto_clear_enabled: false
     dimensions:
       width: 480
@@ -7,5 +14,30 @@ display:
 
 touchscreen:
   - platform: sdl
+    display: sdl0
+    sdl_id: sdl0
 
 lvgl:
+  - id: lvgl_0
+    displays: sdl0
+  - id: lvgl_1
+    displays: sdl1
+    on_idle:
+      timeout: 8s
+      then:
+        if:
+          condition:
+            lvgl.is_idle:
+              lvgl_id: lvgl_1
+              timeout: 5s
+          then:
+            logger.log: Lvgl2 is idle
+    widgets:
+      - button:
+          align: center
+          widgets:
+            - label:
+                text: Click ME
+          on_click:
+            logger.log: Clicked
+

From c0658ffe2c31edcba1091dd05c922c36b3292c1c Mon Sep 17 00:00:00 2001
From: Ramil Valitov <ramilvalitov@gmail.com>
Date: Fri, 8 Nov 2024 01:10:58 +0300
Subject: [PATCH 081/282] [fix] deprecated legacy driver tsens (#7658)

Co-authored-by: luar123 <49960470+luar123@users.noreply.github.com>
---
 .../internal_temperature.cpp                  | 51 ++++++++++++++++++-
 .../internal_temperature.h                    |  1 +
 2 files changed, 50 insertions(+), 2 deletions(-)

diff --git a/esphome/components/internal_temperature/internal_temperature.cpp b/esphome/components/internal_temperature/internal_temperature.cpp
index 9ef5cbecd5..afa5583e59 100644
--- a/esphome/components/internal_temperature/internal_temperature.cpp
+++ b/esphome/components/internal_temperature/internal_temperature.cpp
@@ -8,8 +8,13 @@ extern "C" {
 uint8_t temprature_sens_read();
 }
 #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \
-    defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
+    defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || \
+    defined(USE_ESP32_VARIANT_ESP32C2)
+#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
 #include "driver/temp_sensor.h"
+#else
+#include "driver/temperature_sensor.h"
+#endif  // ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
 #endif  // USE_ESP32_VARIANT
 #endif  // USE_ESP32
 #ifdef USE_RP2040
@@ -25,6 +30,13 @@ namespace esphome {
 namespace internal_temperature {
 
 static const char *const TAG = "internal_temperature";
+#ifdef USE_ESP32
+#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) && \
+    (defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \
+     defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2))
+static temperature_sensor_handle_t tsensNew = NULL;
+#endif  // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) && USE_ESP32_VARIANT
+#endif  // USE_ESP32
 
 void InternalTemperatureSensor::update() {
   float temperature = NAN;
@@ -36,7 +48,9 @@ void InternalTemperatureSensor::update() {
   temperature = (raw - 32) / 1.8f;
   success = (raw != 128);
 #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \
-    defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
+    defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || \
+    defined(USE_ESP32_VARIANT_ESP32C2)
+#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
   temp_sensor_config_t tsens = TSENS_CONFIG_DEFAULT();
   temp_sensor_set_config(tsens);
   temp_sensor_start();
@@ -47,6 +61,13 @@ void InternalTemperatureSensor::update() {
   esp_err_t result = temp_sensor_read_celsius(&temperature);
   temp_sensor_stop();
   success = (result == ESP_OK);
+#else
+  esp_err_t result = temperature_sensor_get_celsius(tsensNew, &temperature);
+  success = (result == ESP_OK);
+  if (!success) {
+    ESP_LOGE(TAG, "Failed to get temperature: %d", result);
+  }
+#endif  // ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
 #endif  // USE_ESP32_VARIANT
 #endif  // USE_ESP32
 #ifdef USE_RP2040
@@ -75,6 +96,32 @@ void InternalTemperatureSensor::update() {
   }
 }
 
+void InternalTemperatureSensor::setup() {
+#ifdef USE_ESP32
+#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) && \
+    (defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \
+     defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2))
+  ESP_LOGCONFIG(TAG, "Setting up temperature sensor...");
+
+  temperature_sensor_config_t tsens_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80);
+
+  esp_err_t result = temperature_sensor_install(&tsens_config, &tsensNew);
+  if (result != ESP_OK) {
+    ESP_LOGE(TAG, "Failed to install temperature sensor: %d", result);
+    this->mark_failed();
+    return;
+  }
+
+  result = temperature_sensor_enable(tsensNew);
+  if (result != ESP_OK) {
+    ESP_LOGE(TAG, "Failed to enable temperature sensor: %d", result);
+    this->mark_failed();
+    return;
+  }
+#endif  // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) && USE_ESP32_VARIANT
+#endif  // USE_ESP32
+}
+
 void InternalTemperatureSensor::dump_config() { LOG_SENSOR("", "Internal Temperature Sensor", this); }
 
 }  // namespace internal_temperature
diff --git a/esphome/components/internal_temperature/internal_temperature.h b/esphome/components/internal_temperature/internal_temperature.h
index 0e46a69769..78e3bcef7d 100644
--- a/esphome/components/internal_temperature/internal_temperature.h
+++ b/esphome/components/internal_temperature/internal_temperature.h
@@ -8,6 +8,7 @@ namespace internal_temperature {
 
 class InternalTemperatureSensor : public sensor::Sensor, public PollingComponent {
  public:
+  void setup() override;
   void dump_config() override;
 
   void update() override;

From d189cc1fbe406dc971bfcbb33e302bedd806cc05 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Fri, 8 Nov 2024 10:39:01 +1100
Subject: [PATCH 082/282] [lvgl] Fix id config for the lvgl component (Bugfix)
 (#7731)

Co-authored-by: clydeps <U5yx99dok9>
---
 esphome/components/lvgl/automation.py | 52 +++++++++++++--------------
 1 file changed, 25 insertions(+), 27 deletions(-)

diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py
index 58e3dd808b..c26ae54892 100644
--- a/esphome/components/lvgl/automation.py
+++ b/esphome/components/lvgl/automation.py
@@ -1,5 +1,4 @@
-from collections.abc import Awaitable
-from typing import Callable
+from typing import Any, Callable
 
 from esphome import automation
 import esphome.codegen as cg
@@ -23,7 +22,6 @@ from .lvcode import (
     UPDATE_EVENT,
     LambdaContext,
     LocalVariable,
-    LvglComponent,
     ReturnStatement,
     add_line_marks,
     lv,
@@ -58,7 +56,7 @@ focused_widgets = set()
 
 async def action_to_code(
     widgets: list[Widget],
-    action: Callable[[Widget], Awaitable[None]],
+    action: Callable[[Widget], Any],
     action_id,
     template_arg,
     args,
@@ -159,14 +157,12 @@ async def obj_invalidate_to_code(config, action_id, template_arg, args):
 @automation.register_action(
     "lvgl.update",
     LvglAction,
-    DISP_BG_SCHEMA.extend(
-        {
-            cv.GenerateID(): cv.use_id(LvglComponent),
-        }
-    ).add_extra(cv.has_at_least_one_key(CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE)),
+    DISP_BG_SCHEMA.extend(LVGL_SCHEMA).add_extra(
+        cv.has_at_least_one_key(CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE)
+    ),
 )
 async def lvgl_update_to_code(config, action_id, template_arg, args):
-    widgets = await get_widgets(config)
+    widgets = await get_widgets(config, CONF_LVGL_ID)
     w = widgets[0]
     disp = literal(f"{w.obj}->get_disp()")
     async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context:
@@ -179,32 +175,33 @@ async def lvgl_update_to_code(config, action_id, template_arg, args):
 @automation.register_action(
     "lvgl.pause",
     LvglAction,
-    {
-        cv.GenerateID(): cv.use_id(LvglComponent),
-        cv.Optional(CONF_SHOW_SNOW, default=False): lv_bool,
-    },
+    LVGL_SCHEMA.extend(
+        {
+            cv.Optional(CONF_SHOW_SNOW, default=False): lv_bool,
+        }
+    ),
 )
 async def pause_action_to_code(config, action_id, template_arg, args):
+    lv_comp = await cg.get_variable(config[CONF_LVGL_ID])
     async with LambdaContext(LVGL_COMP_ARG) as context:
         add_line_marks(where=action_id)
         lv_add(lvgl_comp.set_paused(True, config[CONF_SHOW_SNOW]))
     var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
-    await cg.register_parented(var, config[CONF_ID])
+    await cg.register_parented(var, lv_comp)
     return var
 
 
 @automation.register_action(
     "lvgl.resume",
     LvglAction,
-    {
-        cv.GenerateID(): cv.use_id(LvglComponent),
-    },
+    LVGL_SCHEMA,
 )
 async def resume_action_to_code(config, action_id, template_arg, args):
+    lv_comp = await cg.get_variable(config[CONF_LVGL_ID])
     async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context:
         lv_add(lvgl_comp.set_paused(False, False))
     var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
-    await cg.register_parented(var, config[CONF_ID])
+    await cg.register_parented(var, lv_comp)
     return var
 
 
@@ -263,14 +260,15 @@ def focused_id(value):
     ObjUpdateAction,
     cv.Any(
         cv.maybe_simple_value(
-            {
-                cv.Optional(CONF_GROUP): cv.use_id(lv_group_t),
-                cv.Required(CONF_ACTION): cv.one_of(
-                    "MARK", "RESTORE", "NEXT", "PREVIOUS", upper=True
-                ),
-                cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent),
-                cv.Optional(CONF_FREEZE, default=False): cv.boolean,
-            },
+            LVGL_SCHEMA.extend(
+                {
+                    cv.Optional(CONF_GROUP): cv.use_id(lv_group_t),
+                    cv.Required(CONF_ACTION): cv.one_of(
+                        "MARK", "RESTORE", "NEXT", "PREVIOUS", upper=True
+                    ),
+                    cv.Optional(CONF_FREEZE, default=False): cv.boolean,
+                }
+            ),
             key=CONF_ACTION,
         ),
         cv.maybe_simple_value(

From 3f123d7542f850e7abe30d6196ec99356a9e343c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 8 Nov 2024 12:42:36 +1300
Subject: [PATCH 083/282] Bump pypa/gh-action-pypi-publish from 1.11.0 to
 1.12.2 (#7730)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/workflows/release.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 82d7ae5ee8..096b00f0f1 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -65,7 +65,7 @@ jobs:
           pip3 install build
           python3 -m build
       - name: Publish
-        uses: pypa/gh-action-pypi-publish@v1.11.0
+        uses: pypa/gh-action-pypi-publish@v1.12.2
 
   deploy-docker:
     name: Build ESPHome ${{ matrix.platform }}

From 2f77d316905f688fef7d899dfedc004ba6a73a49 Mon Sep 17 00:00:00 2001
From: David Woodhouse <dwmw2@infradead.org>
Date: Fri, 8 Nov 2024 03:38:13 +0000
Subject: [PATCH 084/282] OTA: Fix IPv6 and multiple address support (#7414)

---
 esphome/__main__.py |  4 +--
 esphome/espota2.py  | 69 +++++++++++++++++++-------------------
 esphome/helpers.py  | 82 ++++++++++++++++++++++++++++++++++++---------
 esphome/mqtt.py     | 11 ++++--
 esphome/zeroconf.py |  8 ++---
 5 files changed, 117 insertions(+), 57 deletions(-)

diff --git a/esphome/__main__.py b/esphome/__main__.py
index cf2741dbdb..85ab3cc00c 100644
--- a/esphome/__main__.py
+++ b/esphome/__main__.py
@@ -38,7 +38,7 @@ from esphome.const import (
     SECRETS_FILES,
 )
 from esphome.core import CORE, EsphomeError, coroutine
-from esphome.helpers import indent, is_ip_address, get_bool_env
+from esphome.helpers import get_bool_env, indent, is_ip_address
 from esphome.log import Fore, color, setup_log
 from esphome.util import (
     get_serial_ports,
@@ -378,7 +378,7 @@ def show_logs(config, args, port):
 
             port = mqtt.get_esphome_device_ip(
                 config, args.username, args.password, args.client_id
-            )
+            )[0]
 
         from esphome.components.api.client import run_logs
 
diff --git a/esphome/espota2.py b/esphome/espota2.py
index 580536153a..94b845b246 100644
--- a/esphome/espota2.py
+++ b/esphome/espota2.py
@@ -10,7 +10,7 @@ import sys
 import time
 
 from esphome.core import EsphomeError
-from esphome.helpers import is_ip_address, resolve_ip_address
+from esphome.helpers import resolve_ip_address
 
 RESPONSE_OK = 0x00
 RESPONSE_REQUEST_AUTH = 0x01
@@ -311,44 +311,45 @@ def perform_ota(
 
 
 def run_ota_impl_(remote_host, remote_port, password, filename):
-    if is_ip_address(remote_host):
-        _LOGGER.info("Connecting to %s", remote_host)
-        ip = remote_host
-    else:
-        _LOGGER.info("Resolving IP address of %s", remote_host)
-        try:
-            ip = resolve_ip_address(remote_host)
-        except EsphomeError as err:
-            _LOGGER.error(
-                "Error resolving IP address of %s. Is it connected to WiFi?",
-                remote_host,
-            )
-            _LOGGER.error(
-                "(If this error persists, please set a static IP address: "
-                "https://esphome.io/components/wifi.html#manual-ips)"
-            )
-            raise OTAError(err) from err
-        _LOGGER.info(" -> %s", ip)
-
-    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-    sock.settimeout(10.0)
     try:
-        sock.connect((ip, remote_port))
-    except OSError as err:
-        sock.close()
-        _LOGGER.error("Connecting to %s:%s failed: %s", remote_host, remote_port, err)
-        return 1
+        res = resolve_ip_address(remote_host, remote_port)
+    except EsphomeError as err:
+        _LOGGER.error(
+            "Error resolving IP address of %s. Is it connected to WiFi?",
+            remote_host,
+        )
+        _LOGGER.error(
+            "(If this error persists, please set a static IP address: "
+            "https://esphome.io/components/wifi.html#manual-ips)"
+        )
+        raise OTAError(err) from err
 
-    with open(filename, "rb") as file_handle:
+    for r in res:
+        af, socktype, _, _, sa = r
+        _LOGGER.info("Connecting to %s port %s...", sa[0], sa[1])
+        sock = socket.socket(af, socktype)
+        sock.settimeout(10.0)
         try:
-            perform_ota(sock, password, file_handle, filename)
-        except OTAError as err:
-            _LOGGER.error(str(err))
-            return 1
-        finally:
+            sock.connect(sa)
+        except OSError as err:
             sock.close()
+            _LOGGER.error("Connecting to %s port %s failed: %s", sa[0], sa[1], err)
+            continue
 
-    return 0
+        _LOGGER.info("Connected to %s", sa[0])
+        with open(filename, "rb") as file_handle:
+            try:
+                perform_ota(sock, password, file_handle, filename)
+            except OTAError as err:
+                _LOGGER.error(str(err))
+                return 1
+            finally:
+                sock.close()
+
+        return 0
+
+    _LOGGER.error("Connection failed.")
+    return 1
 
 
 def run_ota(remote_host, remote_port, password, filename):
diff --git a/esphome/helpers.py b/esphome/helpers.py
index 2a7e5cd9b6..8aae43c2bb 100644
--- a/esphome/helpers.py
+++ b/esphome/helpers.py
@@ -1,5 +1,6 @@
 import codecs
 from contextlib import suppress
+import ipaddress
 import logging
 import os
 from pathlib import Path
@@ -91,12 +92,8 @@ def mkdir_p(path):
 
 
 def is_ip_address(host):
-    parts = host.split(".")
-    if len(parts) != 4:
-        return False
     try:
-        for p in parts:
-            int(p)
+        ipaddress.ip_address(host)
         return True
     except ValueError:
         return False
@@ -127,25 +124,80 @@ def _resolve_with_zeroconf(host):
     return info
 
 
-def resolve_ip_address(host):
+def addr_preference_(res):
+    # Trivial alternative to RFC6724 sorting. Put sane IPv6 first, then
+    # Legacy IP, then IPv6 link-local addresses without an actual link.
+    sa = res[4]
+    ip = ipaddress.ip_address(sa[0])
+    if ip.version == 4:
+        return 2
+    if ip.is_link_local and sa[3] == 0:
+        return 3
+    return 1
+
+
+def resolve_ip_address(host, port):
     import socket
 
     from esphome.core import EsphomeError
 
+    # There are five cases here. The host argument could be one of:
+    #  • a *list* of IP addresses discovered by MQTT,
+    #  • a single IP address specified by the user,
+    #  • a .local hostname to be resolved by mDNS,
+    #  • a normal hostname to be resolved in DNS, or
+    #  • A URL from which we should extract the hostname.
+    #
+    # In each of the first three cases, we end up with IP addresses in
+    # string form which need to be converted to a 5-tuple to be used
+    # for the socket connection attempt. The easiest way to construct
+    # those is to pass the IP address string to getaddrinfo(). Which,
+    # coincidentally, is how we do hostname lookups in the other cases
+    # too. So first build a list which contains either IP addresses or
+    # a single hostname, then call getaddrinfo() on each element of
+    # that list.
+
     errs = []
+    if isinstance(host, list):
+        addr_list = host
+    elif is_ip_address(host):
+        addr_list = [host]
+    else:
+        url = urlparse(host)
+        if url.scheme != "":
+            host = url.hostname
 
-    if host.endswith(".local"):
+        addr_list = []
+        if host.endswith(".local"):
+            try:
+                _LOGGER.info("Resolving IP address of %s in mDNS", host)
+                addr_list = _resolve_with_zeroconf(host)
+            except EsphomeError as err:
+                errs.append(str(err))
+
+        # If not mDNS, or if mDNS failed, use normal DNS
+        if not addr_list:
+            addr_list = [host]
+
+    # Now we have a list containing either IP addresses or a hostname
+    res = []
+    for addr in addr_list:
+        if not is_ip_address(addr):
+            _LOGGER.info("Resolving IP address of %s", host)
         try:
-            return _resolve_with_zeroconf(host)
-        except EsphomeError as err:
+            r = socket.getaddrinfo(addr, port, proto=socket.IPPROTO_TCP)
+        except OSError as err:
             errs.append(str(err))
+            raise EsphomeError(
+                f"Error resolving IP address: {', '.join(errs)}"
+            ) from err
 
-    try:
-        host_url = host if (urlparse(host).scheme != "") else "http://" + host
-        return socket.gethostbyname(urlparse(host_url).hostname)
-    except OSError as err:
-        errs.append(str(err))
-        raise EsphomeError(f"Error resolving IP address: {', '.join(errs)}") from err
+        res = res + r
+
+    # Zeroconf tends to give us link-local IPv6 addresses without specifying
+    # the link. Put those last in the list to be attempted.
+    res.sort(key=addr_preference_)
+    return res
 
 
 def get_bool_env(var, default=False):
diff --git a/esphome/mqtt.py b/esphome/mqtt.py
index d55fb0202d..2f90c49025 100644
--- a/esphome/mqtt.py
+++ b/esphome/mqtt.py
@@ -175,8 +175,15 @@ def get_esphome_device_ip(
                 _LOGGER.Warn("Wrong device answer")
                 return
 
-            if "ip" in data:
-                dev_ip = data["ip"]
+            dev_ip = []
+            key = "ip"
+            n = 0
+            while key in data:
+                dev_ip.append(data[key])
+                n = n + 1
+                key = "ip" + str(n)
+
+            if dev_ip:
                 client.disconnect()
 
     def on_connect(client, userdata, flags, return_code):
diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py
index b3ee64e259..76049fa776 100644
--- a/esphome/zeroconf.py
+++ b/esphome/zeroconf.py
@@ -182,8 +182,8 @@ class EsphomeZeroconf(Zeroconf):
         if (
             info.load_from_cache(self)
             or (timeout and info.request(self, timeout * 1000))
-        ) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)):
-            return str(addresses[0])
+        ) and (addresses := info.parsed_scoped_addresses(IPVersion.All)):
+            return addresses
         return None
 
 
@@ -194,6 +194,6 @@ class AsyncEsphomeZeroconf(AsyncZeroconf):
         if (
             info.load_from_cache(self.zeroconf)
             or (timeout and await info.async_request(self.zeroconf, timeout * 1000))
-        ) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)):
-            return str(addresses[0])
+        ) and (addresses := info.parsed_scoped_addresses(IPVersion.All)):
+            return addresses
         return None

From 2ec17eed588f2e0ca2392337034981bdb83fcdcf Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Sun, 27 Oct 2024 13:17:09 +1100
Subject: [PATCH 085/282] [rpi_dpi_rgb] Fix get_width and height (Bugfix)
 (#7675)

Co-authored-by: clydeps <U5yx99dok9>
---
 .../components/rpi_dpi_rgb/rpi_dpi_rgb.cpp    | 20 +++++++++++++++++++
 esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h  |  5 +++--
 2 files changed, 23 insertions(+), 2 deletions(-)

diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp
index 655b469b91..ba09171649 100644
--- a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp
+++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp
@@ -84,6 +84,26 @@ void RpiDpiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uin
     ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err));
 }
 
+int RpiDpiRgb::get_width() {
+  switch (this->rotation_) {
+    case display::DISPLAY_ROTATION_90_DEGREES:
+    case display::DISPLAY_ROTATION_270_DEGREES:
+      return this->get_height_internal();
+    default:
+      return this->get_width_internal();
+  }
+}
+
+int RpiDpiRgb::get_height() {
+  switch (this->rotation_) {
+    case display::DISPLAY_ROTATION_90_DEGREES:
+    case display::DISPLAY_ROTATION_270_DEGREES:
+      return this->get_width_internal();
+    default:
+      return this->get_height_internal();
+  }
+}
+
 void RpiDpiRgb::draw_pixel_at(int x, int y, Color color) {
   if (!this->get_clipping().inside(x, y))
     return;  // NOLINT
diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h
index 10f77a2624..7525040cd1 100644
--- a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h
+++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h
@@ -24,6 +24,7 @@ class RpiDpiRgb : public display::Display {
   void update() override { this->do_update_(); }
   void setup() override;
   void loop() override;
+  float get_setup_priority() const override { return setup_priority::HARDWARE; }
   void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
                       display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
   void draw_pixel_at(int x, int y, Color color) override;
@@ -44,8 +45,8 @@ class RpiDpiRgb : public display::Display {
     this->width_ = width;
     this->height_ = height;
   }
-  int get_width() override { return this->width_; }
-  int get_height() override { return this->height_; }
+  int get_width() override;
+  int get_height() override;
   void set_hsync_back_porch(uint16_t hsync_back_porch) { this->hsync_back_porch_ = hsync_back_porch; }
   void set_hsync_front_porch(uint16_t hsync_front_porch) { this->hsync_front_porch_ = hsync_front_porch; }
   void set_hsync_pulse_width(uint16_t hsync_pulse_width) { this->hsync_pulse_width_ = hsync_pulse_width; }

From e85cbf26f881e91c4f02f4612d1b92d556ff56af Mon Sep 17 00:00:00 2001
From: Bonne Eggleston <bonne@exciton.com.au>
Date: Mon, 28 Oct 2024 20:52:39 -0700
Subject: [PATCH 086/282] Fixes modbus timing error (#7674)

---
 esphome/components/modbus/modbus.cpp | 36 ++++++++++++++++++----------
 1 file changed, 24 insertions(+), 12 deletions(-)

diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp
index f8dd4c18b9..8544b50261 100644
--- a/esphome/components/modbus/modbus.cpp
+++ b/esphome/components/modbus/modbus.cpp
@@ -15,23 +15,33 @@ void Modbus::setup() {
 void Modbus::loop() {
   const uint32_t now = millis();
 
-  if (now - this->last_modbus_byte_ > 50) {
-    this->rx_buffer_.clear();
-    this->last_modbus_byte_ = now;
-  }
-  // stop blocking new send commands after send_wait_time_ ms regardless if a response has been received since then
-  if (now - this->last_send_ > send_wait_time_) {
-    waiting_for_response = 0;
-  }
-
   while (this->available()) {
     uint8_t byte;
     this->read_byte(&byte);
     if (this->parse_modbus_byte_(byte)) {
       this->last_modbus_byte_ = now;
     } else {
+      size_t at = this->rx_buffer_.size();
+      if (at > 0) {
+        ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse failed", at);
+        this->rx_buffer_.clear();
+      }
+    }
+  }
+
+  if (now - this->last_modbus_byte_ > 50) {
+    size_t at = this->rx_buffer_.size();
+    if (at > 0) {
+      ESP_LOGV(TAG, "Clearing buffer of %d bytes - timeout", at);
       this->rx_buffer_.clear();
     }
+
+    // stop blocking new send commands after sent_wait_time_ ms after response received
+    if (now - this->last_send_ > send_wait_time_) {
+      if (waiting_for_response > 0)
+        ESP_LOGV(TAG, "Stop waiting for response from %d", waiting_for_response);
+      waiting_for_response = 0;
+    }
   }
 }
 
@@ -39,7 +49,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
   size_t at = this->rx_buffer_.size();
   this->rx_buffer_.push_back(byte);
   const uint8_t *raw = &this->rx_buffer_[0];
-  ESP_LOGV(TAG, "Modbus received Byte  %d (0X%x)", byte, byte);
+  ESP_LOGVV(TAG, "Modbus received Byte  %d (0X%x)", byte, byte);
   // Byte 0: modbus address (match all)
   if (at == 0)
     return true;
@@ -144,8 +154,10 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
     ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X! ", address);
   }
 
-  // return false to reset buffer
-  return false;
+  // reset buffer
+  ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse succeeded", at);
+  this->rx_buffer_.clear();
+  return true;
 }
 
 void Modbus::dump_config() {

From 3a25eaca3f90345dd45ffc7e03734f8a6c0c3c45 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 5 Nov 2024 11:32:18 +1100
Subject: [PATCH 087/282] [lvgl] Ensure images are configured before using
 them. (Bugfix) (#7721)

---
 esphome/components/lvgl/widgets/animimg.py | 7 ++++---
 esphome/components/lvgl/widgets/img.py     | 2 ++
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/esphome/components/lvgl/widgets/animimg.py b/esphome/components/lvgl/widgets/animimg.py
index 3b20008c3d..8adea72ad3 100644
--- a/esphome/components/lvgl/widgets/animimg.py
+++ b/esphome/components/lvgl/widgets/animimg.py
@@ -60,9 +60,10 @@ class AnimimgType(WidgetType):
         lvgl_components_required.add(CONF_IMAGE)
         lvgl_components_required.add(CONF_ANIMIMG)
         if CONF_SRC in config:
-            for x in config[CONF_SRC]:
-                await cg.get_variable(x)
-            srcs = [await lv_image.process(x) for x in config[CONF_SRC]]
+            srcs = [
+                await lv_image.process(await cg.get_variable(x))
+                for x in config[CONF_SRC]
+            ]
             src_id = cg.static_const_array(config[CONF_SRC_LIST_ID], srcs)
             count = len(config[CONF_SRC])
             lv.animimg_set_src(w.obj, src_id, count)
diff --git a/esphome/components/lvgl/widgets/img.py b/esphome/components/lvgl/widgets/img.py
index 59b2c97c63..931d0c0b5b 100644
--- a/esphome/components/lvgl/widgets/img.py
+++ b/esphome/components/lvgl/widgets/img.py
@@ -1,3 +1,4 @@
+import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.const import CONF_ANGLE, CONF_MODE
 
@@ -64,6 +65,7 @@ class ImgType(WidgetType):
 
     async def to_code(self, w: Widget, config):
         if src := config.get(CONF_SRC):
+            src = await cg.get_variable(src)
             lv.img_set_src(w.obj, await lv_image.process(src))
         if (cf_angle := config.get(CONF_ANGLE)) is not None:
             pivot_x = config[CONF_PIVOT_X]

From 551ea378824bc18df6523fa8ce810833f31b205e Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Fri, 8 Nov 2024 17:02:31 +1300
Subject: [PATCH 088/282] Bump version to 2024.10.3

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

diff --git a/esphome/const.py b/esphome/const.py
index 032a4c79a0..c9decd4fd2 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,6 +1,6 @@
 """Constants used by esphome."""
 
-__version__ = "2024.10.2"
+__version__ = "2024.10.3"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 VALID_SUBSTITUTIONS_CHARACTERS = (

From 335faf858baabee77a8104379e5fcba8da5ac58b Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Mon, 11 Nov 2024 08:55:19 +1300
Subject: [PATCH 089/282] Fix dashboard ip resolving (#7747)

---
 esphome/dashboard/status/mdns.py | 7 +++----
 esphome/dashboard/web_server.py  | 4 ++--
 esphome/zeroconf.py              | 6 ++++--
 3 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/esphome/dashboard/status/mdns.py b/esphome/dashboard/status/mdns.py
index bd212bc563..9f6399ca8b 100644
--- a/esphome/dashboard/status/mdns.py
+++ b/esphome/dashboard/status/mdns.py
@@ -26,7 +26,7 @@ class MDNSStatus:
         self.host_mdns_state: dict[str, bool | None] = {}
         self._loop = asyncio.get_running_loop()
 
-    async def async_resolve_host(self, host_name: str) -> str | None:
+    async def async_resolve_host(self, host_name: str) -> list[str] | None:
         """Resolve a host name to an address in a thread-safe manner."""
         if aiozc := self.aiozc:
             return await aiozc.async_resolve_host(host_name)
@@ -50,13 +50,12 @@ class MDNSStatus:
                 poll_names.setdefault(entry.name, set()).add(entry)
             elif (online := host_mdns_state.get(entry.name, SENTINEL)) != SENTINEL:
                 entries.async_set_state(entry, bool_to_entry_state(online))
-
         if poll_names and self.aiozc:
             results = await asyncio.gather(
                 *(self.aiozc.async_resolve_host(name) for name in poll_names)
             )
-            for name, address in zip(poll_names, results):
-                result = bool(address)
+            for name, address_list in zip(poll_names, results):
+                result = bool(address_list)
                 host_mdns_state[name] = result
                 for entry in poll_names[name]:
                     entries.async_set_state(entry, bool_to_entry_state(result))
diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py
index 9aeece9aab..07f7f019f8 100644
--- a/esphome/dashboard/web_server.py
+++ b/esphome/dashboard/web_server.py
@@ -320,12 +320,12 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
             and "api" in entry.loaded_integrations
         ):
             if (mdns := dashboard.mdns_status) and (
-                address := await mdns.async_resolve_host(entry.name)
+                address_list := await mdns.async_resolve_host(entry.name)
             ):
                 # Use the IP address if available but only
                 # if the API is loaded and the device is online
                 # since MQTT logging will not work otherwise
-                port = address
+                port = address_list[0]
             elif (
                 entry.address
                 and (
diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py
index 76049fa776..5a92a4ed7c 100644
--- a/esphome/zeroconf.py
+++ b/esphome/zeroconf.py
@@ -176,7 +176,7 @@ def _make_host_resolver(host: str) -> HostResolver:
 
 
 class EsphomeZeroconf(Zeroconf):
-    def resolve_host(self, host: str, timeout: float = 3.0) -> str | None:
+    def resolve_host(self, host: str, timeout: float = 3.0) -> list[str] | None:
         """Resolve a host name to an IP address."""
         info = _make_host_resolver(host)
         if (
@@ -188,7 +188,9 @@ class EsphomeZeroconf(Zeroconf):
 
 
 class AsyncEsphomeZeroconf(AsyncZeroconf):
-    async def async_resolve_host(self, host: str, timeout: float = 3.0) -> str | None:
+    async def async_resolve_host(
+        self, host: str, timeout: float = 3.0
+    ) -> list[str] | None:
         """Resolve a host name to an IP address."""
         info = _make_host_resolver(host)
         if (

From 7c00c5db7020f09a6e8b3ffc3cb637fa2aa2fdc6 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Mon, 11 Nov 2024 09:44:02 +1300
Subject: [PATCH 090/282] [docker] Bump curl, iputils-ping and libssl-dev
 (#7748)

---
 docker/Dockerfile | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index 44ee879a12..ed6ce083a8 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -32,9 +32,9 @@ RUN \
         python3-setuptools=66.1.1-1 \
         python3-venv=3.11.2-1+b1 \
         python3-wheel=0.38.4-2 \
-        iputils-ping=3:20221126-1 \
+        iputils-ping=3:20221126-1+deb12u1 \
         git=1:2.39.5-0+deb12u1 \
-        curl=7.88.1-10+deb12u7 \
+        curl=7.88.1-10+deb12u8 \
         openssh-client=1:9.2p1-2+deb12u3 \
         python3-cffi=1.15.1-5 \
         libcairo2=1.16.0-7 \
@@ -97,7 +97,7 @@ BUILD_DEPS="
     zlib1g-dev=1:1.2.13.dfsg-1
     libjpeg-dev=1:2.1.5-2
     libfreetype-dev=2.12.1+dfsg-5+deb12u3
-    libssl-dev=3.0.14-1~deb12u2
+    libssl-dev=3.0.15-1~deb12u1
     libffi-dev=3.4.4-1
     libopenjp2-7=2.5.0-2
     libtiff6=4.5.0-6+deb12u1

From c35240ca3207f7efef3cb0dcd146c32c5e33c6c7 Mon Sep 17 00:00:00 2001
From: Kyle Cascade <kyle@xkyle.com>
Date: Sun, 10 Nov 2024 17:13:43 -0800
Subject: [PATCH 091/282] Remove the choice for MQTT logging if it is disabled
 (#7723)

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

diff --git a/esphome/__main__.py b/esphome/__main__.py
index 85ab3cc00c..86d529e1bf 100644
--- a/esphome/__main__.py
+++ b/esphome/__main__.py
@@ -20,6 +20,8 @@ from esphome.const import (
     CONF_DEASSERT_RTS_DTR,
     CONF_DISABLED,
     CONF_ESPHOME,
+    CONF_LEVEL,
+    CONF_LOG_TOPIC,
     CONF_LOGGER,
     CONF_MDNS,
     CONF_MQTT,
@@ -30,6 +32,7 @@ from esphome.const import (
     CONF_PLATFORMIO_OPTIONS,
     CONF_PORT,
     CONF_SUBSTITUTIONS,
+    CONF_TOPIC,
     PLATFORM_BK72XX,
     PLATFORM_ESP32,
     PLATFORM_ESP8266,
@@ -95,8 +98,12 @@ def choose_upload_log_host(
         options.append((f"Over The Air ({CORE.address})", CORE.address))
         if default == "OTA":
             return CORE.address
-    if show_mqtt and CONF_MQTT in CORE.config:
-        options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT"))
+    if (
+        show_mqtt
+        and (mqtt_config := CORE.config.get(CONF_MQTT))
+        and mqtt_logging_enabled(mqtt_config)
+    ):
+        options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
         if default == "OTA":
             return "MQTT"
     if default is not None:
@@ -106,6 +113,17 @@ def choose_upload_log_host(
     return choose_prompt(options, purpose=purpose)
 
 
+def mqtt_logging_enabled(mqtt_config):
+    log_topic = mqtt_config[CONF_LOG_TOPIC]
+    if log_topic is None:
+        return False
+    if CONF_TOPIC not in log_topic:
+        return False
+    if log_topic.get(CONF_LEVEL, None) == "NONE":
+        return False
+    return True
+
+
 def get_port_type(port):
     if port.startswith("/") or port.startswith("COM"):
         return "SERIAL"

From d885d65c9bc7667c07afc1066f4bbc00efe09aff Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Mon, 11 Nov 2024 12:18:05 +1100
Subject: [PATCH 092/282] [sensor] Make some values templatable (#7735)

---
 esphome/components/sensor/__init__.py | 26 +++++++++++++------
 esphome/components/sensor/filter.cpp  | 37 ++++++++++++++-------------
 esphome/components/sensor/filter.h    | 19 +++++++-------
 tests/components/template/common.yaml | 19 ++++++++++++++
 4 files changed, 65 insertions(+), 36 deletions(-)

diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py
index 27338b8608..9dbad27102 100644
--- a/esphome/components/sensor/__init__.py
+++ b/esphome/components/sensor/__init__.py
@@ -335,19 +335,28 @@ def sensor_schema(
     return SENSOR_SCHEMA.extend(schema)
 
 
-@FILTER_REGISTRY.register("offset", OffsetFilter, cv.float_)
+@FILTER_REGISTRY.register("offset", OffsetFilter, cv.templatable(cv.float_))
 async def offset_filter_to_code(config, filter_id):
-    return cg.new_Pvariable(filter_id, config)
+    template_ = await cg.templatable(config, [], float)
+    return cg.new_Pvariable(filter_id, template_)
 
 
-@FILTER_REGISTRY.register("multiply", MultiplyFilter, cv.float_)
+@FILTER_REGISTRY.register("multiply", MultiplyFilter, cv.templatable(cv.float_))
 async def multiply_filter_to_code(config, filter_id):
-    return cg.new_Pvariable(filter_id, config)
+    template_ = await cg.templatable(config, [], float)
+    return cg.new_Pvariable(filter_id, template_)
 
 
-@FILTER_REGISTRY.register("filter_out", FilterOutValueFilter, cv.float_)
+@FILTER_REGISTRY.register(
+    "filter_out",
+    FilterOutValueFilter,
+    cv.Any(cv.templatable(cv.float_), [cv.templatable(cv.float_)]),
+)
 async def filter_out_filter_to_code(config, filter_id):
-    return cg.new_Pvariable(filter_id, config)
+    if not isinstance(config, list):
+        config = [config]
+    template_ = [await cg.templatable(x, [], float) for x in config]
+    return cg.new_Pvariable(filter_id, template_)
 
 
 QUANTILE_SCHEMA = cv.All(
@@ -573,7 +582,7 @@ async def heartbeat_filter_to_code(config, filter_id):
 TIMEOUT_SCHEMA = cv.maybe_simple_value(
     {
         cv.Required(CONF_TIMEOUT): cv.positive_time_period_milliseconds,
-        cv.Optional(CONF_VALUE, default="nan"): cv.float_,
+        cv.Optional(CONF_VALUE, default="nan"): cv.templatable(cv.float_),
     },
     key=CONF_TIMEOUT,
 )
@@ -581,7 +590,8 @@ TIMEOUT_SCHEMA = cv.maybe_simple_value(
 
 @FILTER_REGISTRY.register("timeout", TimeoutFilter, TIMEOUT_SCHEMA)
 async def timeout_filter_to_code(config, filter_id):
-    var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], config[CONF_VALUE])
+    template_ = await cg.templatable(config[CONF_VALUE], [], float)
+    var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_)
     await cg.register_component(var, {})
     return var
 
diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp
index bcf1fc8269..0a8740dd5b 100644
--- a/esphome/components/sensor/filter.cpp
+++ b/esphome/components/sensor/filter.cpp
@@ -288,36 +288,36 @@ optional<float> LambdaFilter::new_value(float value) {
 }
 
 // OffsetFilter
-OffsetFilter::OffsetFilter(float offset) : offset_(offset) {}
+OffsetFilter::OffsetFilter(TemplatableValue<float> offset) : offset_(std::move(offset)) {}
 
-optional<float> OffsetFilter::new_value(float value) { return value + this->offset_; }
+optional<float> OffsetFilter::new_value(float value) { return value + this->offset_.value(); }
 
 // MultiplyFilter
-MultiplyFilter::MultiplyFilter(float multiplier) : multiplier_(multiplier) {}
+MultiplyFilter::MultiplyFilter(TemplatableValue<float> multiplier) : multiplier_(std::move(multiplier)) {}
 
-optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_; }
+optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); }
 
 // FilterOutValueFilter
-FilterOutValueFilter::FilterOutValueFilter(float value_to_filter_out) : value_to_filter_out_(value_to_filter_out) {}
+FilterOutValueFilter::FilterOutValueFilter(std::vector<TemplatableValue<float>> values_to_filter_out)
+    : values_to_filter_out_(std::move(values_to_filter_out)) {}
 
 optional<float> FilterOutValueFilter::new_value(float value) {
-  if (std::isnan(this->value_to_filter_out_)) {
-    if (std::isnan(value)) {
-      return {};
-    } else {
-      return value;
+  int8_t accuracy = this->parent_->get_accuracy_decimals();
+  float accuracy_mult = powf(10.0f, accuracy);
+  for (auto filter_value : this->values_to_filter_out_) {
+    if (std::isnan(filter_value.value())) {
+      if (std::isnan(value)) {
+        return {};
+      }
+      continue;
     }
-  } else {
-    int8_t accuracy = this->parent_->get_accuracy_decimals();
-    float accuracy_mult = powf(10.0f, accuracy);
-    float rounded_filter_out = roundf(accuracy_mult * this->value_to_filter_out_);
+    float rounded_filter_out = roundf(accuracy_mult * filter_value.value());
     float rounded_value = roundf(accuracy_mult * value);
     if (rounded_filter_out == rounded_value) {
       return {};
-    } else {
-      return value;
     }
   }
+  return value;
 }
 
 // ThrottleFilter
@@ -383,11 +383,12 @@ void OrFilter::initialize(Sensor *parent, Filter *next) {
 
 // TimeoutFilter
 optional<float> TimeoutFilter::new_value(float value) {
-  this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_); });
+  this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_.value()); });
   return value;
 }
 
-TimeoutFilter::TimeoutFilter(uint32_t time_period, float new_value) : time_period_(time_period), value_(new_value) {}
+TimeoutFilter::TimeoutFilter(uint32_t time_period, TemplatableValue<float> new_value)
+    : time_period_(time_period), value_(std::move(new_value)) {}
 float TimeoutFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
 
 // DebounceFilter
diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h
index 92b1d8d240..86586b458d 100644
--- a/esphome/components/sensor/filter.h
+++ b/esphome/components/sensor/filter.h
@@ -5,6 +5,7 @@
 #include <vector>
 #include "esphome/core/component.h"
 #include "esphome/core/helpers.h"
+#include "esphome/core/automation.h"
 
 namespace esphome {
 namespace sensor {
@@ -273,34 +274,33 @@ class LambdaFilter : public Filter {
 /// A simple filter that adds `offset` to each value it receives.
 class OffsetFilter : public Filter {
  public:
-  explicit OffsetFilter(float offset);
+  explicit OffsetFilter(TemplatableValue<float> offset);
 
   optional<float> new_value(float value) override;
 
  protected:
-  float offset_;
+  TemplatableValue<float> offset_;
 };
 
 /// A simple filter that multiplies to each value it receives by `multiplier`.
 class MultiplyFilter : public Filter {
  public:
-  explicit MultiplyFilter(float multiplier);
-
+  explicit MultiplyFilter(TemplatableValue<float> multiplier);
   optional<float> new_value(float value) override;
 
  protected:
-  float multiplier_;
+  TemplatableValue<float> multiplier_;
 };
 
 /// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`.
 class FilterOutValueFilter : public Filter {
  public:
-  explicit FilterOutValueFilter(float value_to_filter_out);
+  explicit FilterOutValueFilter(std::vector<TemplatableValue<float>> values_to_filter_out);
 
   optional<float> new_value(float value) override;
 
  protected:
-  float value_to_filter_out_;
+  std::vector<TemplatableValue<float>> values_to_filter_out_;
 };
 
 class ThrottleFilter : public Filter {
@@ -316,8 +316,7 @@ class ThrottleFilter : public Filter {
 
 class TimeoutFilter : public Filter, public Component {
  public:
-  explicit TimeoutFilter(uint32_t time_period, float new_value);
-  void set_value(float new_value) { this->value_ = new_value; }
+  explicit TimeoutFilter(uint32_t time_period, TemplatableValue<float> new_value);
 
   optional<float> new_value(float value) override;
 
@@ -325,7 +324,7 @@ class TimeoutFilter : public Filter, public Component {
 
  protected:
   uint32_t time_period_;
-  float value_;
+  TemplatableValue<float> value_;
 };
 
 class DebounceFilter : public Filter, public Component {
diff --git a/tests/components/template/common.yaml b/tests/components/template/common.yaml
index 3565926933..79201fbe07 100644
--- a/tests/components/template/common.yaml
+++ b/tests/components/template/common.yaml
@@ -9,6 +9,25 @@ sensor:
         return 0.0;
       }
     update_interval: 60s
+    filters:
+      - offset: 10
+      - multiply: 1
+      - offset: !lambda return 10;
+      - multiply: !lambda return 2;
+      - filter_out:
+          - 10
+          - 20
+          - !lambda return 10;
+      - filter_out: 10
+      - filter_out: !lambda return NAN;
+      - timeout:
+          timeout: 10s
+          value: !lambda return 10;
+      - timeout:
+          timeout: 1h
+          value: 20.0
+      - timeout:
+          timeout: 1d
 
 esphome:
   on_boot:

From ffee2f0e8878edf6c5feafbd643a7ef6bcc3fc7a Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Mon, 11 Nov 2024 14:07:48 +1100
Subject: [PATCH 093/282] [lvgl] Implement keypads (#7719)

---
 esphome/components/lvgl/__init__.py     | 24 +++++++-
 esphome/components/lvgl/defines.py      |  1 +
 esphome/components/lvgl/encoders.py     | 19 +++---
 esphome/components/lvgl/keypads.py      | 77 +++++++++++++++++++++++++
 esphome/components/lvgl/lvgl_esphome.h  | 11 +---
 esphome/components/lvgl/types.py        |  1 +
 tests/components/lvgl/lvgl-package.yaml | 10 ++++
 7 files changed, 123 insertions(+), 20 deletions(-)
 create mode 100644 esphome/components/lvgl/keypads.py

diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py
index 7476c0a09c..d03adc9624 100644
--- a/esphome/components/lvgl/__init__.py
+++ b/esphome/components/lvgl/__init__.py
@@ -7,6 +7,7 @@ import esphome.config_validation as cv
 from esphome.const import (
     CONF_AUTO_CLEAR_ENABLED,
     CONF_BUFFER_SIZE,
+    CONF_GROUP,
     CONF_ID,
     CONF_LAMBDA,
     CONF_ON_IDLE,
@@ -23,9 +24,15 @@ from esphome.helpers import write_file_if_changed
 from . import defines as df, helpers, lv_validation as lvalid
 from .automation import disp_update, focused_widgets, update_to_code
 from .defines import add_define
-from .encoders import ENCODERS_CONFIG, encoders_to_code, initial_focus_to_code
+from .encoders import (
+    ENCODERS_CONFIG,
+    encoders_to_code,
+    get_default_group,
+    initial_focus_to_code,
+)
 from .gradient import GRADIENT_SCHEMA, gradients_to_code
 from .hello_world import get_hello_world
+from .keypads import KEYPADS_CONFIG, keypads_to_code
 from .lv_validation import lv_bool, lv_images_used
 from .lvcode import LvContext, LvglComponent, lvgl_static
 from .schemas import (
@@ -158,6 +165,13 @@ def multi_conf_validate(configs: list[dict]):
     display_list = [disp for disps in displays for disp in disps]
     if len(display_list) != len(set(display_list)):
         raise cv.Invalid("A display ID may be used in only one LVGL instance")
+    for config in configs:
+        for item in (df.CONF_ENCODERS, df.CONF_KEYPADS):
+            for enc in config.get(item, ()):
+                if CONF_GROUP not in enc:
+                    raise cv.Invalid(
+                        f"'{item}' must have an explicit group set when using multiple LVGL instances"
+                    )
     base_config = configs[0]
     for config in configs[1:]:
         for item in (
@@ -173,7 +187,8 @@ def multi_conf_validate(configs: list[dict]):
 
 
 def final_validation(configs):
-    multi_conf_validate(configs)
+    if len(configs) != 1:
+        multi_conf_validate(configs)
     global_config = full_config.get()
     for config in configs:
         if pages := config.get(CONF_PAGES):
@@ -275,6 +290,7 @@ async def to_code(configs):
     else:
         add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font))
     cg.add(lvgl_static.esphome_lvgl_init())
+    default_group = get_default_group(config_0)
 
     for config in configs:
         frac = config[CONF_BUFFER_SIZE]
@@ -303,7 +319,8 @@ async def to_code(configs):
         lv_scr_act = get_scr_act(lv_component)
         async with LvContext():
             await touchscreens_to_code(lv_component, config)
-            await encoders_to_code(lv_component, config)
+            await encoders_to_code(lv_component, config, default_group)
+            await keypads_to_code(lv_component, config, default_group)
             await theme_to_code(config)
             await styles_to_code(config)
             await gradients_to_code(config)
@@ -430,6 +447,7 @@ LVGL_SCHEMA = (
             cv.Optional(df.CONF_GRADIENTS): GRADIENT_SCHEMA,
             cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema,
             cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG,
+            cv.Optional(df.CONF_KEYPADS, default=None): KEYPADS_CONFIG,
             cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t),
             cv.Optional(df.CONF_RESUME_ON_INPUT, default=True): cv.boolean,
         }
diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py
index 4d48028611..ea345fa55c 100644
--- a/esphome/components/lvgl/defines.py
+++ b/esphome/components/lvgl/defines.py
@@ -438,6 +438,7 @@ CONF_HEADER_MODE = "header_mode"
 CONF_HOME = "home"
 CONF_INITIAL_FOCUS = "initial_focus"
 CONF_KEY_CODE = "key_code"
+CONF_KEYPADS = "keypads"
 CONF_LAYOUT = "layout"
 CONF_LEFT_BUTTON = "left_button"
 CONF_LINE_WIDTH = "line_width"
diff --git a/esphome/components/lvgl/encoders.py b/esphome/components/lvgl/encoders.py
index 81bcda95b4..952572df43 100644
--- a/esphome/components/lvgl/encoders.py
+++ b/esphome/components/lvgl/encoders.py
@@ -17,7 +17,7 @@ from .defines import (
 from .helpers import lvgl_components_required, requires_component
 from .lvcode import lv, lv_add, lv_assign, lv_expr, lv_Pvariable
 from .schemas import ENCODER_SCHEMA
-from .types import lv_group_t, lv_indev_type_t
+from .types import lv_group_t, lv_indev_type_t, lv_key_t
 
 ENCODERS_CONFIG = cv.ensure_list(
     ENCODER_SCHEMA.extend(
@@ -39,10 +39,13 @@ ENCODERS_CONFIG = cv.ensure_list(
 )
 
 
-async def encoders_to_code(var, config):
-    default_group = lv_Pvariable(lv_group_t, config[CONF_DEFAULT_GROUP])
-    lv_assign(default_group, lv_expr.group_create())
-    lv.group_set_default(default_group)
+def get_default_group(config):
+    default_group = cg.Pvariable(config[CONF_DEFAULT_GROUP], lv_expr.group_create())
+    cg.add(lv.group_set_default(default_group))
+    return default_group
+
+
+async def encoders_to_code(var, config, default_group):
     for enc_conf in config[CONF_ENCODERS]:
         lvgl_components_required.add("KEY_LISTENER")
         lpt = enc_conf[CONF_LONG_PRESS_TIME].total_milliseconds
@@ -54,14 +57,14 @@ async def encoders_to_code(var, config):
         if sensor_config := enc_conf.get(CONF_SENSOR):
             if isinstance(sensor_config, dict):
                 b_sensor = await cg.get_variable(sensor_config[CONF_LEFT_BUTTON])
-                cg.add(listener.set_left_button(b_sensor))
+                cg.add(listener.add_button(b_sensor, lv_key_t.LV_KEY_LEFT))
                 b_sensor = await cg.get_variable(sensor_config[CONF_RIGHT_BUTTON])
-                cg.add(listener.set_right_button(b_sensor))
+                cg.add(listener.add_button(b_sensor, lv_key_t.LV_KEY_RIGHT))
             else:
                 sensor_config = await cg.get_variable(sensor_config)
                 lv_add(listener.set_sensor(sensor_config))
         b_sensor = await cg.get_variable(enc_conf[CONF_ENTER_BUTTON])
-        cg.add(listener.set_enter_button(b_sensor))
+        cg.add(listener.add_button(b_sensor, lv_key_t.LV_KEY_ENTER))
         if group := enc_conf.get(CONF_GROUP):
             group = lv_Pvariable(lv_group_t, group)
             lv_assign(group, lv_expr.group_create())
diff --git a/esphome/components/lvgl/keypads.py b/esphome/components/lvgl/keypads.py
new file mode 100644
index 0000000000..5e2953d57f
--- /dev/null
+++ b/esphome/components/lvgl/keypads.py
@@ -0,0 +1,77 @@
+import esphome.codegen as cg
+from esphome.components.binary_sensor import BinarySensor
+import esphome.config_validation as cv
+from esphome.const import CONF_GROUP, CONF_ID
+
+from .defines import (
+    CONF_ENCODERS,
+    CONF_INITIAL_FOCUS,
+    CONF_KEYPADS,
+    CONF_LONG_PRESS_REPEAT_TIME,
+    CONF_LONG_PRESS_TIME,
+    literal,
+)
+from .helpers import lvgl_components_required
+from .lvcode import lv, lv_assign, lv_expr, lv_Pvariable
+from .schemas import ENCODER_SCHEMA
+from .types import lv_group_t, lv_indev_type_t
+
+KEYPAD_KEYS = (
+    "up",
+    "down",
+    "right",
+    "left",
+    "esc",
+    "del",
+    "backspace",
+    "enter",
+    "next",
+    "prev",
+    "home",
+    "end",
+    "0",
+    "1",
+    "2",
+    "3",
+    "4",
+    "5",
+    "6",
+    "7",
+    "8",
+    "9",
+    "#",
+    "*",
+)
+
+KEYPADS_CONFIG = cv.ensure_list(
+    ENCODER_SCHEMA.extend(
+        {cv.Optional(key): cv.use_id(BinarySensor) for key in KEYPAD_KEYS}
+    )
+)
+
+
+async def keypads_to_code(var, config, default_group):
+    for enc_conf in config[CONF_KEYPADS]:
+        lvgl_components_required.add("KEY_LISTENER")
+        lpt = enc_conf[CONF_LONG_PRESS_TIME].total_milliseconds
+        lprt = enc_conf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds
+        listener = cg.new_Pvariable(
+            enc_conf[CONF_ID], lv_indev_type_t.LV_INDEV_TYPE_KEYPAD, lpt, lprt
+        )
+        await cg.register_parented(listener, var)
+        for key in [x for x in enc_conf if x in KEYPAD_KEYS]:
+            b_sensor = await cg.get_variable(enc_conf[key])
+            cg.add(listener.add_button(b_sensor, literal(f"LV_KEY_{key.upper()}")))
+        if group := enc_conf.get(CONF_GROUP):
+            group = lv_Pvariable(lv_group_t, group)
+            lv_assign(group, lv_expr.group_create())
+        else:
+            group = default_group
+        lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group)
+
+
+async def initial_focus_to_code(config):
+    for enc_conf in config[CONF_ENCODERS]:
+        if default_focus := enc_conf.get(CONF_INITIAL_FOCUS):
+            obj = await cg.get_variable(default_focus)
+            lv.group_focus_obj(obj)
diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h
index dae07d5153..208cb1cbd5 100644
--- a/esphome/components/lvgl/lvgl_esphome.h
+++ b/esphome/components/lvgl/lvgl_esphome.h
@@ -256,15 +256,8 @@ class LVEncoderListener : public Parented<LvglComponent> {
   LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt);
 
 #ifdef USE_BINARY_SENSOR
-  void set_left_button(binary_sensor::BinarySensor *left_button) {
-    left_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_LEFT, state); });
-  }
-  void set_right_button(binary_sensor::BinarySensor *right_button) {
-    right_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_RIGHT, state); });
-  }
-
-  void set_enter_button(binary_sensor::BinarySensor *enter_button) {
-    enter_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_ENTER, state); });
+  void add_button(binary_sensor::BinarySensor *button, lv_key_t key) {
+    button->add_on_state_callback([this, key](bool state) { this->event(key, state); });
   }
 #endif
 
diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py
index b504f24674..40e69119f0 100644
--- a/esphome/components/lvgl/types.py
+++ b/esphome/components/lvgl/types.py
@@ -40,6 +40,7 @@ void_ptr = cg.void.operator("ptr")
 lv_coord_t = cg.global_ns.namespace("lv_coord_t")
 lv_event_code_t = cg.global_ns.enum("lv_event_code_t")
 lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t")
+lv_key_t = cg.global_ns.enum("lv_key_t")
 FontEngine = lvgl_ns.class_("FontEngine")
 IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template())
 PauseTrigger = lvgl_ns.class_("PauseTrigger", automation.Trigger.template())
diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml
index 9bfbb5fc95..db0443b3bb 100644
--- a/tests/components/lvgl/lvgl-package.yaml
+++ b/tests/components/lvgl/lvgl-package.yaml
@@ -11,6 +11,12 @@ substitutions:
   check: "\U000F012C"
   arrow_down: "\U000F004B"
 
+binary_sensor:
+  - id: enter_sensor
+    platform: template
+  - id: left_sensor
+    platform: template
+
 lvgl:
   log_level: debug
   resume_on_input: true
@@ -93,6 +99,10 @@ lvgl:
     - touchscreen_id: tft_touch
       long_press_repeat_time: 200ms
       long_press_time: 500ms
+  keypads:
+    - initial_focus: button_button
+      enter: enter_sensor
+      next: left_sensor
 
   msgboxes:
     - id: message_box

From a2dccc4730566536b700985310016fe1299ae371 Mon Sep 17 00:00:00 2001
From: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com>
Date: Mon, 11 Nov 2024 05:14:01 +0100
Subject: [PATCH 094/282] [midea] Add temperature validation in do_follow_me
 method (bugfix) (#7736)

---
 esphome/components/midea/air_conditioner.cpp | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp
index b5bf43b64f..a823680d03 100644
--- a/esphome/components/midea/air_conditioner.cpp
+++ b/esphome/components/midea/air_conditioner.cpp
@@ -3,6 +3,8 @@
 #include "esphome/core/log.h"
 #include "air_conditioner.h"
 #include "ac_adapter.h"
+#include <cmath>
+#include <cstdint>
 
 namespace esphome {
 namespace midea {
@@ -121,7 +123,21 @@ void AirConditioner::dump_config() {
 
 void AirConditioner::do_follow_me(float temperature, bool beeper) {
 #ifdef USE_REMOTE_TRANSMITTER
-  IrFollowMeData data(static_cast<uint8_t>(lroundf(temperature)), beeper);
+  // Check if temperature is finite (not NaN or infinite)
+  if (!std::isfinite(temperature)) {
+    ESP_LOGW(Constants::TAG, "Follow me action requires a finite temperature, got: %f", temperature);
+    return;
+  }
+
+  // Round and convert temperature to long, then clamp and convert it to uint8_t
+  uint8_t temp_uint8 =
+      static_cast<uint8_t>(std::max(0L, std::min(static_cast<long>(UINT8_MAX), std::lroundf(temperature))));
+
+  ESP_LOGD(Constants::TAG, "Follow me action called with temperature: %f °C, rounded to: %u °C", temperature,
+           temp_uint8);
+
+  // Create and transmit the data
+  IrFollowMeData data(temp_uint8, beeper);
   this->transmitter_.transmit(data);
 #else
   ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");

From 58d028ac1375511362fac3bf524b86567ef584b0 Mon Sep 17 00:00:00 2001
From: Oleg Tarasov <me@olegtarasov.email>
Date: Tue, 12 Nov 2024 06:19:42 +0300
Subject: [PATCH 095/282] Add OpenTherm component (part 3: rest of the sensors)
 (#7676)

Co-authored-by: FreeBear <freebear@tuxcnc.org>
Co-authored-by: FreeBear-nc <67865163+FreeBear-nc@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/opentherm/__init__.py      |  30 +-
 .../opentherm/binary_sensor/__init__.py       |  33 ++
 esphome/components/opentherm/const.py         |   6 +
 esphome/components/opentherm/generate.py      |   2 +
 esphome/components/opentherm/hub.cpp          |  42 +-
 esphome/components/opentherm/hub.h            |  52 ++-
 esphome/components/opentherm/input.h          |  18 +
 esphome/components/opentherm/input.py         |  51 +++
 .../components/opentherm/number/__init__.py   |  74 ++++
 .../components/opentherm/number/number.cpp    |  40 ++
 esphome/components/opentherm/number/number.h  |  31 ++
 esphome/components/opentherm/opentherm.h      |  33 ++
 .../components/opentherm/opentherm_macros.h   |  60 +++
 .../components/opentherm/output/__init__.py   |  47 +++
 .../components/opentherm/output/output.cpp    |  18 +
 esphome/components/opentherm/output/output.h  |  33 ++
 esphome/components/opentherm/schema.py        | 378 +++++++++++++++++-
 .../components/opentherm/sensor/__init__.py   |  16 +
 .../components/opentherm/switch/__init__.py   |  43 ++
 .../components/opentherm/switch/switch.cpp    |  28 ++
 esphome/components/opentherm/switch/switch.h  |  20 +
 tests/components/opentherm/common.yaml        |  84 ++++
 22 files changed, 1128 insertions(+), 11 deletions(-)
 create mode 100644 esphome/components/opentherm/binary_sensor/__init__.py
 create mode 100644 esphome/components/opentherm/input.h
 create mode 100644 esphome/components/opentherm/input.py
 create mode 100644 esphome/components/opentherm/number/__init__.py
 create mode 100644 esphome/components/opentherm/number/number.cpp
 create mode 100644 esphome/components/opentherm/number/number.h
 create mode 100644 esphome/components/opentherm/output/__init__.py
 create mode 100644 esphome/components/opentherm/output/output.cpp
 create mode 100644 esphome/components/opentherm/output/output.h
 create mode 100644 esphome/components/opentherm/switch/__init__.py
 create mode 100644 esphome/components/opentherm/switch/switch.cpp
 create mode 100644 esphome/components/opentherm/switch/switch.h

diff --git a/esphome/components/opentherm/__init__.py b/esphome/components/opentherm/__init__.py
index ee19818a29..81cd78af08 100644
--- a/esphome/components/opentherm/__init__.py
+++ b/esphome/components/opentherm/__init__.py
@@ -3,8 +3,9 @@ from typing import Any
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import pins
+from esphome.components import sensor
 from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266
-from . import generate
+from . import const, schema, validate, generate
 
 CODEOWNERS = ["@olegtarasov"]
 MULTI_CONF = True
@@ -19,6 +20,7 @@ CONF_CH2_ACTIVE = "ch2_active"
 CONF_SUMMER_MODE_ACTIVE = "summer_mode_active"
 CONF_DHW_BLOCK = "dhw_block"
 CONF_SYNC_MODE = "sync_mode"
+CONF_OPENTHERM_VERSION = "opentherm_version"
 
 CONFIG_SCHEMA = cv.All(
     cv.Schema(
@@ -34,8 +36,15 @@ CONFIG_SCHEMA = cv.All(
             cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean,
             cv.Optional(CONF_DHW_BLOCK, False): cv.boolean,
             cv.Optional(CONF_SYNC_MODE, False): cv.boolean,
+            cv.Optional(CONF_OPENTHERM_VERSION): cv.positive_float,
         }
-    ).extend(cv.COMPONENT_SCHEMA),
+    )
+    .extend(
+        validate.create_entities_schema(
+            schema.INPUTS, (lambda _: cv.use_id(sensor.Sensor))
+        )
+    )
+    .extend(cv.COMPONENT_SCHEMA),
     cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]),
 )
 
@@ -52,8 +61,23 @@ async def to_code(config: dict[str, Any]) -> None:
     cg.add(var.set_out_pin(out_pin))
 
     non_sensors = {CONF_ID, CONF_IN_PIN, CONF_OUT_PIN}
+    input_sensors = []
     for key, value in config.items():
         if key in non_sensors:
             continue
+        if key in schema.INPUTS:
+            input_sensor = await cg.get_variable(value)
+            cg.add(
+                getattr(var, f"set_{key}_{const.INPUT_SENSOR.lower()}")(input_sensor)
+            )
+            input_sensors.append(key)
+        else:
+            cg.add(getattr(var, f"set_{key}")(value))
 
-        cg.add(getattr(var, f"set_{key}")(value))
+    if len(input_sensors) > 0:
+        generate.define_has_component(const.INPUT_SENSOR, input_sensors)
+        generate.define_message_handler(
+            const.INPUT_SENSOR, input_sensors, schema.INPUTS
+        )
+        generate.define_readers(const.INPUT_SENSOR, input_sensors)
+        generate.add_messages(var, input_sensors, schema.INPUTS)
diff --git a/esphome/components/opentherm/binary_sensor/__init__.py b/esphome/components/opentherm/binary_sensor/__init__.py
new file mode 100644
index 0000000000..643734f90c
--- /dev/null
+++ b/esphome/components/opentherm/binary_sensor/__init__.py
@@ -0,0 +1,33 @@
+from typing import Any
+
+import esphome.config_validation as cv
+from esphome.components import binary_sensor
+from .. import const, schema, validate, generate
+
+DEPENDENCIES = [const.OPENTHERM]
+COMPONENT_TYPE = const.BINARY_SENSOR
+
+
+def get_entity_validation_schema(entity: schema.BinarySensorSchema) -> cv.Schema:
+    return binary_sensor.binary_sensor_schema(
+        device_class=(
+            entity.device_class
+            or binary_sensor._UNDEF  # pylint: disable=protected-access
+        ),
+        icon=(entity.icon or binary_sensor._UNDEF),  # pylint: disable=protected-access
+    )
+
+
+CONFIG_SCHEMA = validate.create_component_schema(
+    schema.BINARY_SENSORS, get_entity_validation_schema
+)
+
+
+async def to_code(config: dict[str, Any]) -> None:
+    await generate.component_to_code(
+        COMPONENT_TYPE,
+        schema.BINARY_SENSORS,
+        binary_sensor.BinarySensor,
+        generate.create_only_conf(binary_sensor.new_binary_sensor),
+        config,
+    )
diff --git a/esphome/components/opentherm/const.py b/esphome/components/opentherm/const.py
index 1f997c5d9c..a113331585 100644
--- a/esphome/components/opentherm/const.py
+++ b/esphome/components/opentherm/const.py
@@ -1,5 +1,11 @@
 OPENTHERM = "opentherm"
 
 CONF_OPENTHERM_ID = "opentherm_id"
+CONF_DATA_TYPE = "data_type"
 
 SENSOR = "sensor"
+BINARY_SENSOR = "binary_sensor"
+SWITCH = "switch"
+NUMBER = "number"
+OUTPUT = "output"
+INPUT_SENSOR = "input_sensor"
diff --git a/esphome/components/opentherm/generate.py b/esphome/components/opentherm/generate.py
index 6a97835a57..9716cab093 100644
--- a/esphome/components/opentherm/generate.py
+++ b/esphome/components/opentherm/generate.py
@@ -130,6 +130,8 @@ async def component_to_code(
         id = conf[CONF_ID]
         if id and id.type == type:
             entity = await create(conf, key, hub)
+            if const.CONF_DATA_TYPE in conf:
+                schemas[key].message_data = conf[const.CONF_DATA_TYPE]
             cg.add(getattr(hub, f"set_{key}_{component_type.lower()}")(entity))
             keys.append(key)
 
diff --git a/esphome/components/opentherm/hub.cpp b/esphome/components/opentherm/hub.cpp
index 770bbd82b7..432036d58d 100644
--- a/esphome/components/opentherm/hub.cpp
+++ b/esphome/components/opentherm/hub.cpp
@@ -29,6 +29,8 @@ uint8_t parse_u8_hb(OpenthermData &data) { return data.valueHB; }
 int8_t parse_s8_lb(OpenthermData &data) { return (int8_t) data.valueLB; }
 int8_t parse_s8_hb(OpenthermData &data) { return (int8_t) data.valueHB; }
 uint16_t parse_u16(OpenthermData &data) { return data.u16(); }
+uint16_t parse_u8_lb_60(OpenthermData &data) { return data.valueLB * 60; }
+uint16_t parse_u8_hb_60(OpenthermData &data) { return data.valueHB * 60; }
 int16_t parse_s16(OpenthermData &data) { return data.s16(); }
 float parse_f88(OpenthermData &data) { return data.f88(); }
 
@@ -87,13 +89,40 @@ OpenthermData OpenthermHub::build_request_(MessageId request_id) const {
     return data;
   }
 
+  // Another special case is OpenTherm version number which is configured at hub level as a constant
+  if (request_id == MessageId::OT_VERSION_CONTROLLER) {
+    data.type = MessageType::WRITE_DATA;
+    data.id = MessageId::OT_VERSION_CONTROLLER;
+    data.f88(this->opentherm_version_);
+
+    return data;
+  }
+
 // Disable incomplete switch statement warnings, because the cases in each
 // switch are generated based on the configured sensors and inputs.
 #pragma GCC diagnostic push
 #pragma GCC diagnostic ignored "-Wswitch"
 
-  switch (request_id) { OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) }
+  // Next, we start with the write requests from switches and other inputs,
+  // because we would want to write that data if it is available, rather than
+  // request a read for that type (in the case that both read and write are
+  // supported).
+  switch (request_id) {
+    OPENTHERM_SWITCH_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, ,
+                                      OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, )
+    OPENTHERM_NUMBER_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, ,
+                                      OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, )
+    OPENTHERM_OUTPUT_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, ,
+                                      OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, )
+    OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, ,
+                                            OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, )
+  }
 
+  // Finally, handle the simple read requests, which only change with the message id.
+  switch (request_id) { OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) }
+  switch (request_id) {
+    OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , )
+  }
 #pragma GCC diagnostic pop
 
   // And if we get here, a message was requested which somehow wasn't handled.
@@ -115,6 +144,10 @@ void OpenthermHub::process_response(OpenthermData &data) {
     OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_RESPONSE_MESSAGE, OPENTHERM_MESSAGE_RESPONSE_ENTITY, ,
                                       OPENTHERM_MESSAGE_RESPONSE_POSTSCRIPT, )
   }
+  switch (data.id) {
+    OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_RESPONSE_MESSAGE, OPENTHERM_MESSAGE_RESPONSE_ENTITY, ,
+                                             OPENTHERM_MESSAGE_RESPONSE_POSTSCRIPT, )
+  }
 }
 
 void OpenthermHub::setup() {
@@ -131,6 +164,13 @@ void OpenthermHub::setup() {
   // good practice anyway.
   this->add_repeating_message(MessageId::STATUS);
 
+  // Also ensure that we start communication with the STATUS message
+  this->initial_messages_.insert(this->initial_messages_.begin(), MessageId::STATUS);
+
+  if (this->opentherm_version_ > 0.0f) {
+    this->initial_messages_.insert(this->initial_messages_.begin(), MessageId::OT_VERSION_CONTROLLER);
+  }
+
   this->current_message_iterator_ = this->initial_messages_.begin();
 }
 
diff --git a/esphome/components/opentherm/hub.h b/esphome/components/opentherm/hub.h
index 3b90cdf427..1f536653e8 100644
--- a/esphome/components/opentherm/hub.h
+++ b/esphome/components/opentherm/hub.h
@@ -4,6 +4,7 @@
 #include "esphome/core/hal.h"
 #include "esphome/core/component.h"
 #include "esphome/core/log.h"
+#include <vector>
 
 #include "opentherm.h"
 
@@ -11,6 +12,22 @@
 #include "esphome/components/sensor/sensor.h"
 #endif
 
+#ifdef OPENTHERM_USE_BINARY_SENSOR
+#include "esphome/components/binary_sensor/binary_sensor.h"
+#endif
+
+#ifdef OPENTHERM_USE_SWITCH
+#include "esphome/components/opentherm/switch/switch.h"
+#endif
+
+#ifdef OPENTHERM_USE_OUTPUT
+#include "esphome/components/opentherm/output/output.h"
+#endif
+
+#ifdef OPENTHERM_USE_NUMBER
+#include "esphome/components/opentherm/number/number.h"
+#endif
+
 #include <memory>
 #include <unordered_map>
 #include <unordered_set>
@@ -31,15 +48,25 @@ class OpenthermHub : public Component {
 
   OPENTHERM_SENSOR_LIST(OPENTHERM_DECLARE_SENSOR, )
 
+  OPENTHERM_BINARY_SENSOR_LIST(OPENTHERM_DECLARE_BINARY_SENSOR, )
+
+  OPENTHERM_SWITCH_LIST(OPENTHERM_DECLARE_SWITCH, )
+
+  OPENTHERM_NUMBER_LIST(OPENTHERM_DECLARE_NUMBER, )
+
+  OPENTHERM_OUTPUT_LIST(OPENTHERM_DECLARE_OUTPUT, )
+
+  OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_DECLARE_INPUT_SENSOR, )
+
   // The set of initial messages to send on starting communication with the boiler
-  std::unordered_set<MessageId> initial_messages_;
+  std::vector<MessageId> initial_messages_;
   // and the repeating messages which are sent repeatedly to update various sensors
   // and boiler parameters (like the setpoint).
-  std::unordered_set<MessageId> repeating_messages_;
+  std::vector<MessageId> repeating_messages_;
   // Indicates if we are still working on the initial requests or not
   bool sending_initial_ = true;
   // Index for the current request in one of the _requests sets.
-  std::unordered_set<MessageId>::const_iterator current_message_iterator_;
+  std::vector<MessageId>::const_iterator current_message_iterator_;
 
   uint32_t last_conversation_start_ = 0;
   uint32_t last_conversation_end_ = 0;
@@ -51,6 +78,8 @@ class OpenthermHub : public Component {
   // Very likely to happen while using Dallas temperature sensors.
   bool sync_mode_ = false;
 
+  float opentherm_version_ = 0.0f;
+
   // Create OpenTherm messages based on the message id
   OpenthermData build_request_(MessageId request_id) const;
   void handle_protocol_write_error_();
@@ -88,13 +117,23 @@ class OpenthermHub : public Component {
 
   OPENTHERM_SENSOR_LIST(OPENTHERM_SET_SENSOR, )
 
-  // Add a request to the set of initial requests
-  void add_initial_message(MessageId message_id) { this->initial_messages_.insert(message_id); }
+  OPENTHERM_BINARY_SENSOR_LIST(OPENTHERM_SET_BINARY_SENSOR, )
+
+  OPENTHERM_SWITCH_LIST(OPENTHERM_SET_SWITCH, )
+
+  OPENTHERM_NUMBER_LIST(OPENTHERM_SET_NUMBER, )
+
+  OPENTHERM_OUTPUT_LIST(OPENTHERM_SET_OUTPUT, )
+
+  OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_SET_INPUT_SENSOR, )
+
+  // Add a request to the vector of initial requests
+  void add_initial_message(MessageId message_id) { this->initial_messages_.push_back(message_id); }
   // Add a request to the set of repeating requests. Note that a large number of repeating
   // requests will slow down communication with the boiler. Each request may take up to 1 second,
   // so with all sensors enabled, it may take about half a minute before a change in setpoint
   // will be processed.
-  void add_repeating_message(MessageId message_id) { this->repeating_messages_.insert(message_id); }
+  void add_repeating_message(MessageId message_id) { this->repeating_messages_.push_back(message_id); }
 
   // There are seven status variables, which can either be set as a simple variable,
   // or using a switch. ch_enable and dhw_enable default to true, the others to false.
@@ -110,6 +149,7 @@ class OpenthermHub : public Component {
   void set_summer_mode_active(bool value) { this->summer_mode_active = value; }
   void set_dhw_block(bool value) { this->dhw_block = value; }
   void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; }
+  void set_opentherm_version(float value) { this->opentherm_version_ = value; }
 
   float get_setup_priority() const override { return setup_priority::HARDWARE; }
 
diff --git a/esphome/components/opentherm/input.h b/esphome/components/opentherm/input.h
new file mode 100644
index 0000000000..3567138792
--- /dev/null
+++ b/esphome/components/opentherm/input.h
@@ -0,0 +1,18 @@
+#pragma once
+
+namespace esphome {
+namespace opentherm {
+
+class OpenthermInput {
+ public:
+  bool auto_min_value, auto_max_value;
+
+  virtual void set_min_value(float min_value) = 0;
+  virtual void set_max_value(float max_value) = 0;
+
+  virtual void set_auto_min_value(bool auto_min_value) { this->auto_min_value = auto_min_value; }
+  virtual void set_auto_max_value(bool auto_max_value) { this->auto_max_value = auto_max_value; }
+};
+
+}  // namespace opentherm
+}  // namespace esphome
diff --git a/esphome/components/opentherm/input.py b/esphome/components/opentherm/input.py
new file mode 100644
index 0000000000..7897747be1
--- /dev/null
+++ b/esphome/components/opentherm/input.py
@@ -0,0 +1,51 @@
+from typing import Any
+
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from . import schema, generate
+
+CONF_min_value = "min_value"
+CONF_max_value = "max_value"
+CONF_auto_min_value = "auto_min_value"
+CONF_auto_max_value = "auto_max_value"
+CONF_step = "step"
+
+OpenthermInput = generate.opentherm_ns.class_("OpenthermInput")
+
+
+def validate_min_value_less_than_max_value(conf):
+    if (
+        CONF_min_value in conf
+        and CONF_max_value in conf
+        and conf[CONF_min_value] > conf[CONF_max_value]
+    ):
+        raise cv.Invalid(f"{CONF_min_value} must be less than {CONF_max_value}")
+    return conf
+
+
+def input_schema(entity: schema.InputSchema) -> cv.Schema:
+    result = cv.Schema(
+        {
+            cv.Optional(CONF_min_value, entity.range[0]): cv.float_range(
+                entity.range[0], entity.range[1]
+            ),
+            cv.Optional(CONF_max_value, entity.range[1]): cv.float_range(
+                entity.range[0], entity.range[1]
+            ),
+        }
+    )
+    result = result.add_extra(validate_min_value_less_than_max_value)
+    result = result.extend({cv.Optional(CONF_step, False): cv.float_})
+    if entity.auto_min_value is not None:
+        result = result.extend({cv.Optional(CONF_auto_min_value, False): cv.boolean})
+    if entity.auto_max_value is not None:
+        result = result.extend({cv.Optional(CONF_auto_max_value, False): cv.boolean})
+
+    return result
+
+
+def generate_setters(entity: cg.MockObj, conf: dict[str, Any]) -> None:
+    generate.add_property_set(entity, CONF_min_value, conf)
+    generate.add_property_set(entity, CONF_max_value, conf)
+    generate.add_property_set(entity, CONF_auto_min_value, conf)
+    generate.add_property_set(entity, CONF_auto_max_value, conf)
diff --git a/esphome/components/opentherm/number/__init__.py b/esphome/components/opentherm/number/__init__.py
new file mode 100644
index 0000000000..bbf3e87586
--- /dev/null
+++ b/esphome/components/opentherm/number/__init__.py
@@ -0,0 +1,74 @@
+from typing import Any
+
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import number
+from esphome.const import (
+    CONF_ID,
+    CONF_UNIT_OF_MEASUREMENT,
+    CONF_STEP,
+    CONF_INITIAL_VALUE,
+    CONF_RESTORE_VALUE,
+)
+from .. import const, schema, validate, input, generate
+
+DEPENDENCIES = [const.OPENTHERM]
+COMPONENT_TYPE = const.NUMBER
+
+OpenthermNumber = generate.opentherm_ns.class_(
+    "OpenthermNumber", number.Number, cg.Component, input.OpenthermInput
+)
+
+
+async def new_openthermnumber(config: dict[str, Any]) -> cg.Pvariable:
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await number.register_number(
+        var,
+        config,
+        min_value=config[input.CONF_min_value],
+        max_value=config[input.CONF_max_value],
+        step=config[input.CONF_step],
+    )
+    input.generate_setters(var, config)
+
+    if CONF_INITIAL_VALUE in config:
+        cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE]))
+    if CONF_RESTORE_VALUE in config:
+        cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE]))
+
+    return var
+
+
+def get_entity_validation_schema(entity: schema.InputSchema) -> cv.Schema:
+    return (
+        number.NUMBER_SCHEMA.extend(
+            {
+                cv.GenerateID(): cv.declare_id(OpenthermNumber),
+                cv.Optional(
+                    CONF_UNIT_OF_MEASUREMENT, entity.unit_of_measurement
+                ): cv.string_strict,
+                cv.Optional(CONF_STEP, entity.step): cv.float_,
+                cv.Optional(CONF_INITIAL_VALUE): cv.float_,
+                cv.Optional(CONF_RESTORE_VALUE): cv.boolean,
+            }
+        )
+        .extend(input.input_schema(entity))
+        .extend(cv.COMPONENT_SCHEMA)
+    )
+
+
+CONFIG_SCHEMA = validate.create_component_schema(
+    schema.INPUTS, get_entity_validation_schema
+)
+
+
+async def to_code(config: dict[str, Any]) -> None:
+    keys = await generate.component_to_code(
+        COMPONENT_TYPE,
+        schema.INPUTS,
+        OpenthermNumber,
+        generate.create_only_conf(new_openthermnumber),
+        config,
+    )
+    generate.define_readers(COMPONENT_TYPE, keys)
diff --git a/esphome/components/opentherm/number/number.cpp b/esphome/components/opentherm/number/number.cpp
new file mode 100644
index 0000000000..d02b99ee9c
--- /dev/null
+++ b/esphome/components/opentherm/number/number.cpp
@@ -0,0 +1,40 @@
+#include "number.h"
+
+namespace esphome {
+namespace opentherm {
+
+static const char *const TAG = "opentherm.number";
+
+void OpenthermNumber::control(float value) {
+  this->publish_state(value);
+
+  if (this->restore_value_)
+    this->pref_.save(&value);
+}
+
+void OpenthermNumber::setup() {
+  float value;
+  if (!this->restore_value_) {
+    value = this->initial_value_;
+  } else {
+    this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
+    if (!this->pref_.load(&value)) {
+      if (!std::isnan(this->initial_value_)) {
+        value = this->initial_value_;
+      } else {
+        value = this->traits.get_min_value();
+      }
+    }
+  }
+  this->publish_state(value);
+}
+
+void OpenthermNumber::dump_config() {
+  LOG_NUMBER("", "OpenTherm Number", this);
+  ESP_LOGCONFIG(TAG, "  Restore value: %d", this->restore_value_);
+  ESP_LOGCONFIG(TAG, "  Initial value: %.2f", this->initial_value_);
+  ESP_LOGCONFIG(TAG, "  Current value: %.2f", this->state);
+}
+
+}  // namespace opentherm
+}  // namespace esphome
diff --git a/esphome/components/opentherm/number/number.h b/esphome/components/opentherm/number/number.h
new file mode 100644
index 0000000000..6f86072754
--- /dev/null
+++ b/esphome/components/opentherm/number/number.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "esphome/components/number/number.h"
+#include "esphome/core/preferences.h"
+#include "esphome/core/log.h"
+#include "esphome/components/opentherm/input.h"
+
+namespace esphome {
+namespace opentherm {
+
+// Just a simple number, which stores the number
+class OpenthermNumber : public number::Number, public Component, public OpenthermInput {
+ protected:
+  void control(float value) override;
+  void setup() override;
+  void dump_config() override;
+
+  float initial_value_{NAN};
+  bool restore_value_{false};
+
+  ESPPreferenceObject pref_;
+
+ public:
+  void set_min_value(float min_value) override { this->traits.set_min_value(min_value); }
+  void set_max_value(float max_value) override { this->traits.set_max_value(max_value); }
+  void set_initial_value(float initial_value) { initial_value_ = initial_value; }
+  void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; }
+};
+
+}  // namespace opentherm
+}  // namespace esphome
diff --git a/esphome/components/opentherm/opentherm.h b/esphome/components/opentherm/opentherm.h
index 23f4b39a1a..85f4611125 100644
--- a/esphome/components/opentherm/opentherm.h
+++ b/esphome/components/opentherm/opentherm.h
@@ -99,6 +99,8 @@ enum MessageId {
   EXHAUST_TEMP = 33,
   FAN_SPEED = 35,
   FLAME_CURRENT = 36,
+  ROOM_TEMP_CH2 = 37,
+  REL_HUMIDITY = 38,
   DHW_BOUNDS = 48,
   CH_BOUNDS = 49,
   OTC_CURVE_BOUNDS = 50,
@@ -110,15 +112,46 @@ enum MessageId {
   HVAC_STATUS = 70,
   REL_VENT_SETPOINT = 71,
   DEVICE_VENT = 74,
+  HVAC_VER_ID = 75,
   REL_VENTILATION = 77,
   REL_HUMID_EXHAUST = 78,
+  EXHAUST_CO2 = 79,
   SUPPLY_INLET_TEMP = 80,
   SUPPLY_OUTLET_TEMP = 81,
   EXHAUST_INLET_TEMP = 82,
   EXHAUST_OUTLET_TEMP = 83,
+  EXHAUST_FAN_SPEED = 84,
+  SUPPLY_FAN_SPEED = 85,
+  REMOTE_VENTILATION_PARAM = 86,
   NOM_REL_VENTILATION = 87,
+  HVAC_NUM_TSP = 88,
+  HVAC_IDX_TSP = 89,
+  HVAC_FHB_SIZE = 90,
+  HVAC_FHB_IDX = 91,
 
+  RF_SIGNAL = 98,
+  DHW_MODE = 99,
   OVERRIDE_FUNC = 100,
+
+  // Solar Specific Message IDs
+  SOLAR_MODE_FLAGS = 101,  // hb0-2 Controller storage mode
+                           // lb0   Device fault
+                           // lb1-3 Device mode status
+                           // lb4-5 Device status
+  SOLAR_ASF = 102,
+  SOLAR_VERSION_ID = 103,
+  SOLAR_PRODUCT_ID = 104,
+  SOLAR_NUM_TSP = 105,
+  SOLAR_IDX_TSP = 106,
+  SOLAR_FHB_SIZE = 107,
+  SOLAR_FHB_IDX = 108,
+  SOLAR_STARTS = 109,
+  SOLAR_HOURS = 110,
+  SOLAR_ENERGY = 111,
+  SOLAR_TOTAL_ENERGY = 112,
+
+  FAILED_BURNER_STARTS = 113,
+  BURNER_FLAME_LOW = 114,
   OEM_DIAGNOSTIC = 115,
   BURNER_STARTS = 116,
   CH_PUMP_STARTS = 117,
diff --git a/esphome/components/opentherm/opentherm_macros.h b/esphome/components/opentherm/opentherm_macros.h
index 0389e975ff..8aaec0b48a 100644
--- a/esphome/components/opentherm/opentherm_macros.h
+++ b/esphome/components/opentherm/opentherm_macros.h
@@ -13,14 +13,49 @@ namespace opentherm {
 #ifndef OPENTHERM_SENSOR_LIST
 #define OPENTHERM_SENSOR_LIST(F, sep)
 #endif
+#ifndef OPENTHERM_BINARY_SENSOR_LIST
+#define OPENTHERM_BINARY_SENSOR_LIST(F, sep)
+#endif
+#ifndef OPENTHERM_SWITCH_LIST
+#define OPENTHERM_SWITCH_LIST(F, sep)
+#endif
+#ifndef OPENTHERM_NUMBER_LIST
+#define OPENTHERM_NUMBER_LIST(F, sep)
+#endif
+#ifndef OPENTHERM_OUTPUT_LIST
+#define OPENTHERM_OUTPUT_LIST(F, sep)
+#endif
+#ifndef OPENTHERM_INPUT_SENSOR_LIST
+#define OPENTHERM_INPUT_SENSOR_LIST(F, sep)
+#endif
 
 // Use macros to create fields for every entity specified in the ESPHome configuration
 #define OPENTHERM_DECLARE_SENSOR(entity) sensor::Sensor *entity;
+#define OPENTHERM_DECLARE_BINARY_SENSOR(entity) binary_sensor::BinarySensor *entity;
+#define OPENTHERM_DECLARE_SWITCH(entity) OpenthermSwitch *entity;
+#define OPENTHERM_DECLARE_NUMBER(entity) OpenthermNumber *entity;
+#define OPENTHERM_DECLARE_OUTPUT(entity) OpenthermOutput *entity;
+#define OPENTHERM_DECLARE_INPUT_SENSOR(entity) sensor::Sensor *entity;
 
 // Setter macros
 #define OPENTHERM_SET_SENSOR(entity) \
   void set_##entity(sensor::Sensor *sensor) { this->entity = sensor; }
 
+#define OPENTHERM_SET_BINARY_SENSOR(entity) \
+  void set_##entity(binary_sensor::BinarySensor *binary_sensor) { this->entity = binary_sensor; }
+
+#define OPENTHERM_SET_SWITCH(entity) \
+  void set_##entity(OpenthermSwitch *sw) { this->entity = sw; }
+
+#define OPENTHERM_SET_NUMBER(entity) \
+  void set_##entity(OpenthermNumber *number) { this->entity = number; }
+
+#define OPENTHERM_SET_OUTPUT(entity) \
+  void set_##entity(OpenthermOutput *output) { this->entity = output; }
+
+#define OPENTHERM_SET_INPUT_SENSOR(entity) \
+  void set_##entity(sensor::Sensor *sensor) { this->entity = sensor; }
+
 // ===== hub.cpp macros =====
 
 // *_MESSAGE_HANDLERS are generated in defines.h and look like this:
@@ -35,6 +70,31 @@ namespace opentherm {
 #ifndef OPENTHERM_SENSOR_MESSAGE_HANDLERS
 #define OPENTHERM_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep)
 #endif
+#ifndef OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS
+#define OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep)
+#endif
+#ifndef OPENTHERM_SWITCH_MESSAGE_HANDLERS
+#define OPENTHERM_SWITCH_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep)
+#endif
+#ifndef OPENTHERM_NUMBER_MESSAGE_HANDLERS
+#define OPENTHERM_NUMBER_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep)
+#endif
+#ifndef OPENTHERM_OUTPUT_MESSAGE_HANDLERS
+#define OPENTHERM_OUTPUT_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep)
+#endif
+#ifndef OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS
+#define OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep)
+#endif
+
+// Write data request builders
+#define OPENTHERM_MESSAGE_WRITE_MESSAGE(msg) \
+  case MessageId::msg: { \
+    data.type = MessageType::WRITE_DATA; \
+    data.id = request_id;
+#define OPENTHERM_MESSAGE_WRITE_ENTITY(key, msg_data) message_data::write_##msg_data(this->key->state, data);
+#define OPENTHERM_MESSAGE_WRITE_POSTSCRIPT \
+  return data; \
+  }
 
 // Read data request builder
 #define OPENTHERM_MESSAGE_READ_MESSAGE(msg) \
diff --git a/esphome/components/opentherm/output/__init__.py b/esphome/components/opentherm/output/__init__.py
new file mode 100644
index 0000000000..3a53c9d4f4
--- /dev/null
+++ b/esphome/components/opentherm/output/__init__.py
@@ -0,0 +1,47 @@
+from typing import Any
+
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import output
+from esphome.const import CONF_ID
+from .. import const, schema, validate, input, generate
+
+DEPENDENCIES = [const.OPENTHERM]
+COMPONENT_TYPE = const.OUTPUT
+
+OpenthermOutput = generate.opentherm_ns.class_(
+    "OpenthermOutput", output.FloatOutput, cg.Component, input.OpenthermInput
+)
+
+
+async def new_openthermoutput(
+    config: dict[str, Any], key: str, _hub: cg.MockObj
+) -> cg.Pvariable:
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await output.register_output(var, config)
+    cg.add(getattr(var, "set_id")(cg.RawExpression(f'"{key}_{config[CONF_ID]}"')))
+    input.generate_setters(var, config)
+    return var
+
+
+def get_entity_validation_schema(entity: schema.InputSchema) -> cv.Schema:
+    return (
+        output.FLOAT_OUTPUT_SCHEMA.extend(
+            {cv.GenerateID(): cv.declare_id(OpenthermOutput)}
+        )
+        .extend(input.input_schema(entity))
+        .extend(cv.COMPONENT_SCHEMA)
+    )
+
+
+CONFIG_SCHEMA = validate.create_component_schema(
+    schema.INPUTS, get_entity_validation_schema
+)
+
+
+async def to_code(config: dict[str, Any]) -> None:
+    keys = await generate.component_to_code(
+        COMPONENT_TYPE, schema.INPUTS, OpenthermOutput, new_openthermoutput, config
+    )
+    generate.define_readers(COMPONENT_TYPE, keys)
diff --git a/esphome/components/opentherm/output/output.cpp b/esphome/components/opentherm/output/output.cpp
new file mode 100644
index 0000000000..f820dc76f1
--- /dev/null
+++ b/esphome/components/opentherm/output/output.cpp
@@ -0,0 +1,18 @@
+#include "esphome/core/helpers.h"  // for clamp() and lerp()
+#include "output.h"
+
+namespace esphome {
+namespace opentherm {
+
+static const char *const TAG = "opentherm.output";
+
+void opentherm::OpenthermOutput::write_state(float state) {
+  ESP_LOGD(TAG, "Received state: %.2f. Min value: %.2f, max value: %.2f", state, min_value_, max_value_);
+  this->state = state < 0.003 && this->zero_means_zero_
+                    ? 0.0
+                    : clamp(lerp(state, min_value_, max_value_), min_value_, max_value_);
+  this->has_state_ = true;
+  ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state);
+}
+}  // namespace opentherm
+}  // namespace esphome
diff --git a/esphome/components/opentherm/output/output.h b/esphome/components/opentherm/output/output.h
new file mode 100644
index 0000000000..8d6a0ee4ba
--- /dev/null
+++ b/esphome/components/opentherm/output/output.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "esphome/components/output/float_output.h"
+#include "esphome/components/opentherm/input.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace opentherm {
+
+class OpenthermOutput : public output::FloatOutput, public Component, public OpenthermInput {
+ protected:
+  bool has_state_ = false;
+  const char *id_ = nullptr;
+
+  float min_value_, max_value_;
+
+ public:
+  float state;
+
+  void set_id(const char *id) { this->id_ = id; }
+
+  void write_state(float state) override;
+
+  bool has_state() { return this->has_state_; };
+
+  void set_min_value(float min_value) override { this->min_value_ = min_value; }
+  void set_max_value(float max_value) override { this->max_value_ = max_value; }
+  float get_min_value() { return this->min_value_; }
+  float get_max_value() { return this->max_value_; }
+};
+
+}  // namespace opentherm
+}  // namespace esphome
diff --git a/esphome/components/opentherm/schema.py b/esphome/components/opentherm/schema.py
index 6ed0029437..fe0f2a77a3 100644
--- a/esphome/components/opentherm/schema.py
+++ b/esphome/components/opentherm/schema.py
@@ -11,9 +11,12 @@ from esphome.const import (
     UNIT_MICROAMP,
     UNIT_PERCENT,
     UNIT_REVOLUTIONS_PER_MINUTE,
+    DEVICE_CLASS_COLD,
     DEVICE_CLASS_CURRENT,
     DEVICE_CLASS_EMPTY,
+    DEVICE_CLASS_HEAT,
     DEVICE_CLASS_PRESSURE,
+    DEVICE_CLASS_PROBLEM,
     DEVICE_CLASS_TEMPERATURE,
     STATE_CLASS_MEASUREMENT,
     STATE_CLASS_NONE,
@@ -188,11 +191,23 @@ SENSORS: dict[str, SensorSchema] = {
         description="Boiler fan speed",
         unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE,
         accuracy_decimals=0,
+        icon="mdi:fan",
         device_class=DEVICE_CLASS_EMPTY,
         state_class=STATE_CLASS_MEASUREMENT,
         message="FAN_SPEED",
         keep_updated=True,
-        message_data="u16",
+        message_data="u8_lb_60",
+    ),
+    "fan_speed_setpoint": SensorSchema(
+        description="Boiler fan speed setpoint",
+        unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE,
+        accuracy_decimals=0,
+        icon="mdi:fan",
+        device_class=DEVICE_CLASS_EMPTY,
+        state_class=STATE_CLASS_MEASUREMENT,
+        message="FAN_SPEED",
+        keep_updated=True,
+        message_data="u8_hb_60",
     ),
     "flame_current": SensorSchema(
         description="Boiler flame current",
@@ -436,3 +451,364 @@ SENSORS: dict[str, SensorSchema] = {
         message_data="u8_lb",
     ),
 }
+
+
+@dataclass
+class BinarySensorSchema(EntitySchema):
+    icon: Optional[str] = None
+    device_class: Optional[str] = None
+
+
+BINARY_SENSORS: dict[str, BinarySensorSchema] = {
+    "fault_indication": BinarySensorSchema(
+        description="Status: Fault indication",
+        device_class=DEVICE_CLASS_PROBLEM,
+        message="STATUS",
+        keep_updated=True,
+        message_data="flag8_lb_0",
+    ),
+    "ch_active": BinarySensorSchema(
+        description="Status: Central Heating active",
+        device_class=DEVICE_CLASS_HEAT,
+        icon="mdi:radiator",
+        message="STATUS",
+        keep_updated=True,
+        message_data="flag8_lb_1",
+    ),
+    "dhw_active": BinarySensorSchema(
+        description="Status: Domestic Hot Water active",
+        device_class=DEVICE_CLASS_HEAT,
+        icon="mdi:faucet",
+        message="STATUS",
+        keep_updated=True,
+        message_data="flag8_lb_2",
+    ),
+    "flame_on": BinarySensorSchema(
+        description="Status: Flame on",
+        device_class=DEVICE_CLASS_HEAT,
+        icon="mdi:fire",
+        message="STATUS",
+        keep_updated=True,
+        message_data="flag8_lb_3",
+    ),
+    "cooling_active": BinarySensorSchema(
+        description="Status: Cooling active",
+        device_class=DEVICE_CLASS_COLD,
+        message="STATUS",
+        keep_updated=True,
+        message_data="flag8_lb_4",
+    ),
+    "ch2_active": BinarySensorSchema(
+        description="Status: Central Heating 2 active",
+        device_class=DEVICE_CLASS_HEAT,
+        icon="mdi:radiator",
+        message="STATUS",
+        keep_updated=True,
+        message_data="flag8_lb_5",
+    ),
+    "diagnostic_indication": BinarySensorSchema(
+        description="Status: Diagnostic event",
+        device_class=DEVICE_CLASS_PROBLEM,
+        message="STATUS",
+        keep_updated=True,
+        message_data="flag8_lb_6",
+    ),
+    "electricity_production": BinarySensorSchema(
+        description="Status: Electricity production",
+        device_class=DEVICE_CLASS_PROBLEM,
+        message="STATUS",
+        keep_updated=True,
+        message_data="flag8_lb_7",
+    ),
+    "dhw_present": BinarySensorSchema(
+        description="Configuration: DHW present",
+        message="DEVICE_CONFIG",
+        keep_updated=False,
+        message_data="flag8_hb_0",
+    ),
+    "control_type_on_off": BinarySensorSchema(
+        description="Configuration: Control type is on/off",
+        message="DEVICE_CONFIG",
+        keep_updated=False,
+        message_data="flag8_hb_1",
+    ),
+    "cooling_supported": BinarySensorSchema(
+        description="Configuration: Cooling supported",
+        message="DEVICE_CONFIG",
+        keep_updated=False,
+        message_data="flag8_hb_2",
+    ),
+    "dhw_storage_tank": BinarySensorSchema(
+        description="Configuration: DHW storage tank",
+        message="DEVICE_CONFIG",
+        keep_updated=False,
+        message_data="flag8_hb_3",
+    ),
+    "controller_pump_control_allowed": BinarySensorSchema(
+        description="Configuration: Controller pump control allowed",
+        message="DEVICE_CONFIG",
+        keep_updated=False,
+        message_data="flag8_hb_4",
+    ),
+    "ch2_present": BinarySensorSchema(
+        description="Configuration: CH2 present",
+        message="DEVICE_CONFIG",
+        keep_updated=False,
+        message_data="flag8_hb_5",
+    ),
+    "water_filling": BinarySensorSchema(
+        description="Configuration: Remote water filling",
+        message="DEVICE_CONFIG",
+        keep_updated=False,
+        message_data="flag8_hb_6",
+    ),
+    "heat_mode": BinarySensorSchema(
+        description="Configuration: Heating or cooling",
+        message="DEVICE_CONFIG",
+        keep_updated=False,
+        message_data="flag8_hb_7",
+    ),
+    "dhw_setpoint_transfer_enabled": BinarySensorSchema(
+        description="Remote boiler parameters: DHW setpoint transfer enabled",
+        message="REMOTE",
+        keep_updated=False,
+        message_data="flag8_hb_0",
+    ),
+    "max_ch_setpoint_transfer_enabled": BinarySensorSchema(
+        description="Remote boiler parameters: CH maximum setpoint transfer enabled",
+        message="REMOTE",
+        keep_updated=False,
+        message_data="flag8_hb_1",
+    ),
+    "dhw_setpoint_rw": BinarySensorSchema(
+        description="Remote boiler parameters: DHW setpoint read/write",
+        message="REMOTE",
+        keep_updated=False,
+        message_data="flag8_lb_0",
+    ),
+    "max_ch_setpoint_rw": BinarySensorSchema(
+        description="Remote boiler parameters: CH maximum setpoint read/write",
+        message="REMOTE",
+        keep_updated=False,
+        message_data="flag8_lb_1",
+    ),
+    "service_request": BinarySensorSchema(
+        description="Service required",
+        device_class=DEVICE_CLASS_PROBLEM,
+        message="FAULT_FLAGS",
+        keep_updated=True,
+        message_data="flag8_hb_0",
+    ),
+    "lockout_reset": BinarySensorSchema(
+        description="Lockout Reset",
+        device_class=DEVICE_CLASS_PROBLEM,
+        message="FAULT_FLAGS",
+        keep_updated=True,
+        message_data="flag8_hb_1",
+    ),
+    "low_water_pressure": BinarySensorSchema(
+        description="Low water pressure fault",
+        device_class=DEVICE_CLASS_PROBLEM,
+        message="FAULT_FLAGS",
+        keep_updated=True,
+        message_data="flag8_hb_2",
+    ),
+    "flame_fault": BinarySensorSchema(
+        description="Flame fault",
+        device_class=DEVICE_CLASS_PROBLEM,
+        message="FAULT_FLAGS",
+        keep_updated=True,
+        message_data="flag8_hb_3",
+    ),
+    "air_pressure_fault": BinarySensorSchema(
+        description="Air pressure fault",
+        device_class=DEVICE_CLASS_PROBLEM,
+        message="FAULT_FLAGS",
+        keep_updated=True,
+        message_data="flag8_hb_4",
+    ),
+    "water_over_temp": BinarySensorSchema(
+        description="Water overtemperature",
+        device_class=DEVICE_CLASS_PROBLEM,
+        message="FAULT_FLAGS",
+        keep_updated=True,
+        message_data="flag8_hb_5",
+    ),
+}
+
+
+@dataclass
+class SwitchSchema(EntitySchema):
+    default_mode: Optional[str] = None
+
+
+SWITCHES: dict[str, SwitchSchema] = {
+    "ch_enable": SwitchSchema(
+        description="Central Heating enabled",
+        message="STATUS",
+        keep_updated=True,
+        message_data="flag8_hb_0",
+        default_mode="restore_default_off",
+    ),
+    "dhw_enable": SwitchSchema(
+        description="Domestic Hot Water enabled",
+        message="STATUS",
+        keep_updated=True,
+        message_data="flag8_hb_1",
+        default_mode="restore_default_off",
+    ),
+    "cooling_enable": SwitchSchema(
+        description="Cooling enabled",
+        message="STATUS",
+        keep_updated=True,
+        message_data="flag8_hb_2",
+        default_mode="restore_default_off",
+    ),
+    "otc_active": SwitchSchema(
+        description="Outside temperature compensation active",
+        message="STATUS",
+        keep_updated=True,
+        message_data="flag8_hb_3",
+        default_mode="restore_default_off",
+    ),
+    "ch2_active": SwitchSchema(
+        description="Central Heating 2 active",
+        message="STATUS",
+        keep_updated=True,
+        message_data="flag8_hb_4",
+        default_mode="restore_default_off",
+    ),
+    "summer_mode_active": SwitchSchema(
+        description="Summer mode active",
+        message="STATUS",
+        keep_updated=True,
+        message_data="flag8_hb_5",
+        default_mode="restore_default_off",
+    ),
+    "dhw_block": SwitchSchema(
+        description="DHW blocked",
+        message="STATUS",
+        keep_updated=True,
+        message_data="flag8_hb_6",
+        default_mode="restore_default_off",
+    ),
+}
+
+
+@dataclass
+class AutoConfigure:
+    message: str
+    message_data: str
+
+
+@dataclass
+class InputSchema(EntitySchema):
+    unit_of_measurement: str
+    step: float
+    range: tuple[int, int]
+    icon: Optional[str] = None
+    auto_max_value: Optional[AutoConfigure] = None
+    auto_min_value: Optional[AutoConfigure] = None
+
+
+INPUTS: dict[str, InputSchema] = {
+    "t_set": InputSchema(
+        description="Control setpoint: temperature setpoint for the boiler's supply water",
+        unit_of_measurement=UNIT_CELSIUS,
+        step=0.1,
+        message="CH_SETPOINT",
+        keep_updated=True,
+        message_data="f88",
+        range=(0, 100),
+        auto_max_value=AutoConfigure(message="MAX_CH_SETPOINT", message_data="f88"),
+    ),
+    "t_set_ch2": InputSchema(
+        description="Control setpoint 2: temperature setpoint for the boiler's supply water on the second heating circuit",
+        unit_of_measurement=UNIT_CELSIUS,
+        step=0.1,
+        message="CH2_SETPOINT",
+        keep_updated=True,
+        message_data="f88",
+        range=(0, 100),
+        auto_max_value=AutoConfigure(message="MAX_CH_SETPOINT", message_data="f88"),
+    ),
+    "cooling_control": InputSchema(
+        description="Cooling control signal",
+        unit_of_measurement=UNIT_PERCENT,
+        step=1.0,
+        message="COOLING_CONTROL",
+        keep_updated=True,
+        message_data="f88",
+        range=(0, 100),
+    ),
+    "t_dhw_set": InputSchema(
+        description="Domestic hot water temperature setpoint",
+        unit_of_measurement=UNIT_CELSIUS,
+        step=0.1,
+        message="DHW_SETPOINT",
+        keep_updated=True,
+        message_data="f88",
+        range=(0, 127),
+        auto_min_value=AutoConfigure(message="DHW_BOUNDS", message_data="s8_lb"),
+        auto_max_value=AutoConfigure(message="DHW_BOUNDS", message_data="s8_hb"),
+    ),
+    "max_t_set": InputSchema(
+        description="Maximum allowable CH water setpoint",
+        unit_of_measurement=UNIT_CELSIUS,
+        step=0.1,
+        message="MAX_CH_SETPOINT",
+        keep_updated=True,
+        message_data="f88",
+        range=(0, 127),
+        auto_min_value=AutoConfigure(message="CH_BOUNDS", message_data="s8_lb"),
+        auto_max_value=AutoConfigure(message="CH_BOUNDS", message_data="s8_hb"),
+    ),
+    "t_room_set": InputSchema(
+        description="Current room temperature setpoint (informational)",
+        unit_of_measurement=UNIT_CELSIUS,
+        step=0.1,
+        message="ROOM_SETPOINT",
+        keep_updated=True,
+        message_data="f88",
+        range=(-40, 127),
+    ),
+    "t_room_set_ch2": InputSchema(
+        description="Current room temperature setpoint on CH2 (informational)",
+        unit_of_measurement=UNIT_CELSIUS,
+        step=0.1,
+        message="ROOM_SETPOINT_CH2",
+        keep_updated=True,
+        message_data="f88",
+        range=(-40, 127),
+    ),
+    "t_room": InputSchema(
+        description="Current sensed room temperature (informational)",
+        unit_of_measurement=UNIT_CELSIUS,
+        step=0.1,
+        message="ROOM_TEMP",
+        keep_updated=True,
+        message_data="f88",
+        range=(-40, 127),
+    ),
+    "max_rel_mod_level": InputSchema(
+        description="Maximum relative modulation level",
+        unit_of_measurement=UNIT_PERCENT,
+        step=1,
+        icon="mdi:percent",
+        message="MAX_MODULATION_LEVEL",
+        keep_updated=True,
+        message_data="f88",
+        range=(0, 100),
+    ),
+    "otc_hc_ratio": InputSchema(
+        description="OTC heat curve ratio",
+        unit_of_measurement=UNIT_CELSIUS,
+        step=0.1,
+        message="OTC_CURVE_RATIO",
+        keep_updated=True,
+        message_data="f88",
+        range=(0, 127),
+        auto_min_value=AutoConfigure(message="OTC_CURVE_BOUNDS", message_data="u8_lb"),
+        auto_max_value=AutoConfigure(message="OTC_CURVE_BOUNDS", message_data="u8_hb"),
+    ),
+}
diff --git a/esphome/components/opentherm/sensor/__init__.py b/esphome/components/opentherm/sensor/__init__.py
index 20224e0eda..546a79054b 100644
--- a/esphome/components/opentherm/sensor/__init__.py
+++ b/esphome/components/opentherm/sensor/__init__.py
@@ -7,6 +7,18 @@ from .. import const, schema, validate, generate
 DEPENDENCIES = [const.OPENTHERM]
 COMPONENT_TYPE = const.SENSOR
 
+MSG_DATA_TYPES = {
+    "u8_lb",
+    "u8_hb",
+    "s8_lb",
+    "s8_hb",
+    "u8_lb_60",
+    "u8_hb_60",
+    "u16",
+    "s16",
+    "f88",
+}
+
 
 def get_entity_validation_schema(entity: schema.SensorSchema) -> cv.Schema:
     return sensor.sensor_schema(
@@ -17,6 +29,10 @@ def get_entity_validation_schema(entity: schema.SensorSchema) -> cv.Schema:
         or sensor._UNDEF,  # pylint: disable=protected-access
         icon=entity.icon or sensor._UNDEF,  # pylint: disable=protected-access
         state_class=entity.state_class,
+    ).extend(
+        {
+            cv.Optional(const.CONF_DATA_TYPE): cv.one_of(*MSG_DATA_TYPES),
+        }
     )
 
 
diff --git a/esphome/components/opentherm/switch/__init__.py b/esphome/components/opentherm/switch/__init__.py
new file mode 100644
index 0000000000..94ec25e36c
--- /dev/null
+++ b/esphome/components/opentherm/switch/__init__.py
@@ -0,0 +1,43 @@
+from typing import Any
+
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import switch
+from esphome.const import CONF_ID
+from .. import const, schema, validate, generate
+
+DEPENDENCIES = [const.OPENTHERM]
+COMPONENT_TYPE = const.SWITCH
+
+OpenthermSwitch = generate.opentherm_ns.class_(
+    "OpenthermSwitch", switch.Switch, cg.Component
+)
+
+
+async def new_openthermswitch(config: dict[str, Any]) -> cg.Pvariable:
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await switch.register_switch(var, config)
+    return var
+
+
+def get_entity_validation_schema(entity: schema.SwitchSchema) -> cv.Schema:
+    return switch.SWITCH_SCHEMA.extend(
+        {cv.GenerateID(): cv.declare_id(OpenthermSwitch)}
+    ).extend(cv.COMPONENT_SCHEMA)
+
+
+CONFIG_SCHEMA = validate.create_component_schema(
+    schema.SWITCHES, get_entity_validation_schema
+)
+
+
+async def to_code(config: dict[str, Any]) -> None:
+    keys = await generate.component_to_code(
+        COMPONENT_TYPE,
+        schema.SWITCHES,
+        OpenthermSwitch,
+        generate.create_only_conf(new_openthermswitch),
+        config,
+    )
+    generate.define_readers(COMPONENT_TYPE, keys)
diff --git a/esphome/components/opentherm/switch/switch.cpp b/esphome/components/opentherm/switch/switch.cpp
new file mode 100644
index 0000000000..228d9ac8f3
--- /dev/null
+++ b/esphome/components/opentherm/switch/switch.cpp
@@ -0,0 +1,28 @@
+#include "switch.h"
+
+namespace esphome {
+namespace opentherm {
+
+static const char *const TAG = "opentherm.switch";
+
+void OpenthermSwitch::write_state(bool state) { this->publish_state(state); }
+
+void OpenthermSwitch::setup() {
+  auto restored = this->get_initial_state_with_restore_mode();
+  bool state = false;
+  if (!restored.has_value()) {
+    ESP_LOGD(TAG, "Couldn't restore state for OpenTherm switch '%s'", this->get_name().c_str());
+  } else {
+    ESP_LOGD(TAG, "Restored state for OpenTherm switch '%s': %d", this->get_name().c_str(), restored.value());
+    state = restored.value();
+  }
+  this->write_state(state);
+}
+
+void OpenthermSwitch::dump_config() {
+  LOG_SWITCH("", "OpenTherm Switch", this);
+  ESP_LOGCONFIG(TAG, "  Current state: %d", this->state);
+}
+
+}  // namespace opentherm
+}  // namespace esphome
diff --git a/esphome/components/opentherm/switch/switch.h b/esphome/components/opentherm/switch/switch.h
new file mode 100644
index 0000000000..0c20a0d9ed
--- /dev/null
+++ b/esphome/components/opentherm/switch/switch.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/switch/switch.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace opentherm {
+
+class OpenthermSwitch : public switch_::Switch, public Component {
+ protected:
+  void write_state(bool state) override;
+
+ public:
+  void setup() override;
+  void dump_config() override;
+};
+
+}  // namespace opentherm
+}  // namespace esphome
diff --git a/tests/components/opentherm/common.yaml b/tests/components/opentherm/common.yaml
index 27cbae280a..744580f18b 100644
--- a/tests/components/opentherm/common.yaml
+++ b/tests/components/opentherm/common.yaml
@@ -12,10 +12,41 @@ opentherm:
   cooling_enable: false
   otc_active: false
   ch2_active: true
+  t_room: boiler_sensor
   summer_mode_active: true
   dhw_block: true
   sync_mode: true
 
+output:
+  - platform: opentherm
+    t_set:
+      id: t_set
+      min_value: 20
+      auto_max_value: true
+      zero_means_zero: true
+    t_set_ch2:
+      id: t_set_ch2
+      min_value: 20
+      max_value: 40
+      zero_means_zero: true
+
+number:
+  - platform: opentherm
+    cooling_control:
+      name: "Boiler Cooling control signal"
+    t_dhw_set:
+      name: "Boiler DHW Setpoint"
+    max_t_set:
+      name: "Boiler Max Setpoint"
+    t_room_set:
+      name: "Boiler Room Setpoint"
+    t_room_set_ch2:
+      name: "Boiler Room Setpoint CH2"
+    max_rel_mod_level:
+      name: "Maximum relative modulation level"
+    otc_hc_ratio:
+      name: "OTC heat curve ratio"
+
 sensor:
   - platform: opentherm
     rel_mod_level:
@@ -25,6 +56,7 @@ sensor:
     dhw_flow_rate:
       name: "Boiler Water flow rate in DHW circuit"
     t_boiler:
+      id: "boiler_sensor"
       name: "Boiler water temperature"
     t_dhw:
       name: "Boiler DHW temperature"
@@ -74,3 +106,55 @@ sensor:
       name: "OTC heat curve ratio upper bound"
     otc_hc_ratio_lb:
       name: "OTC heat curve ratio lower bound"
+
+binary_sensor:
+  - platform: opentherm
+    fault_indication:
+      name: "Boiler Fault indication"
+    ch_active:
+      name: "Boiler Central Heating active"
+    dhw_active:
+      name: "Boiler Domestic Hot Water active"
+    flame_on:
+      name: "Boiler Flame on"
+    cooling_active:
+      name: "Boiler Cooling active"
+    ch2_active:
+      name: "Boiler Central Heating 2 active"
+    diagnostic_indication:
+      name: "Boiler Diagnostic event"
+    dhw_present:
+      name: "Boiler DHW present"
+    control_type_on_off:
+      name: "Boiler Control type is on/off"
+    cooling_supported:
+      name: "Boiler Cooling supported"
+    dhw_storage_tank:
+      name: "Boiler DHW storage tank"
+    controller_pump_control_allowed:
+      name: "Boiler Controller pump control allowed"
+    ch2_present:
+      name: "Boiler CH2 present"
+    dhw_setpoint_transfer_enabled:
+      name: "Boiler DHW setpoint transfer enabled"
+    max_ch_setpoint_transfer_enabled:
+      name: "Boiler CH maximum setpoint transfer enabled"
+    dhw_setpoint_rw:
+      name: "Boiler DHW setpoint read/write"
+    max_ch_setpoint_rw:
+      name: "Boiler CH maximum setpoint read/write"
+
+switch:
+  - platform: opentherm
+    ch_enable:
+      name: "Boiler Central Heating enabled"
+      restore_mode: RESTORE_DEFAULT_ON
+    dhw_enable:
+      name: "Boiler Domestic Hot Water enabled"
+    cooling_enable:
+      name: "Boiler Cooling enabled"
+      restore_mode: ALWAYS_OFF
+    otc_active:
+      name: "Boiler Outside temperature compensation active"
+    ch2_active:
+      name: "Boiler Central Heating 2 active"

From 928b39f4950536ff2d6501da8c35c139ea650b8b Mon Sep 17 00:00:00 2001
From: Kevin Ahrendt <kevin.ahrendt@nabucasa.com>
Date: Tue, 12 Nov 2024 13:20:12 -0500
Subject: [PATCH 096/282] [i2s_audio] I2S speaker improvements (#7749)

---
 .../components/i2s_audio/speaker/__init__.py  |  13 ++-
 .../i2s_audio/speaker/i2s_audio_speaker.cpp   | 106 +++++++++---------
 .../i2s_audio/speaker/i2s_audio_speaker.h     |  12 +-
 3 files changed, 72 insertions(+), 59 deletions(-)

diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py
index dd43d6cb39..0355c16321 100644
--- a/esphome/components/i2s_audio/speaker/__init__.py
+++ b/esphome/components/i2s_audio/speaker/__init__.py
@@ -24,9 +24,10 @@ I2SAudioSpeaker = i2s_audio_ns.class_(
     "I2SAudioSpeaker", cg.Component, speaker.Speaker, I2SAudioOut
 )
 
-
+CONF_BUFFER_DURATION = "buffer_duration"
 CONF_DAC_TYPE = "dac_type"
 CONF_I2S_COMM_FMT = "i2s_comm_fmt"
+CONF_NEVER = "never"
 
 i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t")
 INTERNAL_DAC_OPTIONS = {
@@ -73,8 +74,12 @@ BASE_SCHEMA = (
     .extend(
         {
             cv.Optional(
-                CONF_TIMEOUT, default="500ms"
+                CONF_BUFFER_DURATION, default="500ms"
             ): cv.positive_time_period_milliseconds,
+            cv.Optional(CONF_TIMEOUT, default="500ms"): cv.Any(
+                cv.positive_time_period_milliseconds,
+                cv.one_of(CONF_NEVER, lower=True),
+            ),
         }
     )
     .extend(cv.COMPONENT_SCHEMA)
@@ -116,4 +121,6 @@ async def to_code(config):
     else:
         cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
         cg.add(var.set_i2s_comm_fmt(config[CONF_I2S_COMM_FMT]))
-    cg.add(var.set_timeout(config[CONF_TIMEOUT]))
+    if config[CONF_TIMEOUT] != CONF_NEVER:
+        cg.add(var.set_timeout(config[CONF_TIMEOUT]))
+    cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION]))
diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
index cf6c3bbbba..c3f4566411 100644
--- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
+++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
@@ -13,21 +13,22 @@
 namespace esphome {
 namespace i2s_audio {
 
-static const size_t DMA_BUFFER_SIZE = 512;
+static const uint8_t DMA_BUFFER_DURATION_MS = 15;
 static const size_t DMA_BUFFERS_COUNT = 4;
-static const size_t FRAMES_IN_ALL_DMA_BUFFERS = DMA_BUFFER_SIZE * DMA_BUFFERS_COUNT;
-static const size_t RING_BUFFER_SAMPLES = 8192;
-static const size_t TASK_DELAY_MS = 10;
+
+static const size_t TASK_DELAY_MS = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT / 2;
+
 static const size_t TASK_STACK_SIZE = 4096;
 static const ssize_t TASK_PRIORITY = 23;
 
+static const size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT + 1;
+
 static const char *const TAG = "i2s_audio.speaker";
 
 enum SpeakerEventGroupBits : uint32_t {
-  COMMAND_START = (1 << 0),                           // Starts the main task purpose
-  COMMAND_STOP = (1 << 1),                            // stops the main task
-  COMMAND_STOP_GRACEFULLY = (1 << 2),                 // Stops the task once all data has been written
-  MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE = (1 << 5),  // Locks the ring buffer when not set
+  COMMAND_START = (1 << 0),            // starts the speaker task
+  COMMAND_STOP = (1 << 1),             // stops the speaker task
+  COMMAND_STOP_GRACEFULLY = (1 << 2),  // Stops the speaker task once all data has been written
   STATE_STARTING = (1 << 10),
   STATE_RUNNING = (1 << 11),
   STATE_STOPPING = (1 << 12),
@@ -91,15 +92,21 @@ static const std::vector<int16_t> Q15_VOLUME_SCALING_FACTORS = {
 void I2SAudioSpeaker::setup() {
   ESP_LOGCONFIG(TAG, "Setting up I2S Audio Speaker...");
 
-  if (this->event_group_ == nullptr) {
-    this->event_group_ = xEventGroupCreate();
-  }
+  this->event_group_ = xEventGroupCreate();
 
   if (this->event_group_ == nullptr) {
     ESP_LOGE(TAG, "Failed to create event group");
     this->mark_failed();
     return;
   }
+
+  this->i2s_event_queue_ = xQueueCreate(I2S_EVENT_QUEUE_COUNT, sizeof(i2s_event_t));
+
+  if (this->i2s_event_queue_ == nullptr) {
+    ESP_LOGE(TAG, "Failed to create I2S event queue");
+    this->mark_failed();
+    return;
+  }
 }
 
 void I2SAudioSpeaker::loop() {
@@ -199,23 +206,17 @@ size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t tick
     this->start();
   }
 
-  // Wait for the ring buffer to be available
-  uint32_t event_bits =
-      xEventGroupWaitBits(this->event_group_, SpeakerEventGroupBits::MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE, pdFALSE,
-                          pdFALSE, pdMS_TO_TICKS(TASK_DELAY_MS));
+  size_t bytes_written = 0;
+  if ((this->state_ == speaker::STATE_RUNNING) && (this->audio_ring_buffer_.use_count() == 1)) {
+    // Only one owner of the ring buffer (the speaker task), so the ring buffer is allocated and no other components are
+    // attempting to write to it.
 
-  if (event_bits & SpeakerEventGroupBits::MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE) {
-    // Ring buffer is available to write
-
-    // Lock the ring buffer, write to it, then unlock it
-    xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE);
-    size_t bytes_written = this->audio_ring_buffer_->write_without_replacement((void *) data, length, ticks_to_wait);
-    xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE);
-
-    return bytes_written;
+    // Temporarily share ownership of the ring buffer so it won't be deallocated while writing
+    std::shared_ptr<RingBuffer> temp_ring_buffer = this->audio_ring_buffer_;
+    bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, ticks_to_wait);
   }
 
-  return 0;
+  return bytes_written;
 }
 
 bool I2SAudioSpeaker::has_buffered_data() const {
@@ -246,10 +247,12 @@ void I2SAudioSpeaker::speaker_task(void *params) {
   const ssize_t bytes_per_sample = audio_stream_info.get_bytes_per_sample();
   const uint8_t number_of_channels = audio_stream_info.channels;
 
-  const size_t dma_buffers_size = FRAMES_IN_ALL_DMA_BUFFERS * bytes_per_sample * number_of_channels;
+  const size_t dma_buffers_size = DMA_BUFFERS_COUNT * DMA_BUFFER_DURATION_MS * this_speaker->sample_rate_ / 1000 *
+                                  bytes_per_sample * number_of_channels;
+  const size_t ring_buffer_size =
+      this_speaker->buffer_duration_ms_ * this_speaker->sample_rate_ / 1000 * bytes_per_sample * number_of_channels;
 
-  if (this_speaker->send_esp_err_to_event_group_(
-          this_speaker->allocate_buffers_(dma_buffers_size, RING_BUFFER_SAMPLES * bytes_per_sample))) {
+  if (this_speaker->send_esp_err_to_event_group_(this_speaker->allocate_buffers_(dma_buffers_size, ring_buffer_size))) {
     // Failed to allocate buffers
     xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
     this_speaker->delete_task_(dma_buffers_size);
@@ -258,9 +261,6 @@ void I2SAudioSpeaker::speaker_task(void *params) {
   if (this_speaker->send_esp_err_to_event_group_(this_speaker->start_i2s_driver_())) {
     // Failed to start I2S driver
     this_speaker->delete_task_(dma_buffers_size);
-  } else {
-    // Ring buffer is allocated, so indicate its can be written to
-    xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE);
   }
 
   if (!this_speaker->send_esp_err_to_event_group_(this_speaker->reconfigure_i2s_stream_info_(audio_stream_info))) {
@@ -270,8 +270,10 @@ void I2SAudioSpeaker::speaker_task(void *params) {
 
     bool stop_gracefully = false;
     uint32_t last_data_received_time = millis();
+    bool tx_dma_underflow = false;
 
-    while ((millis() - last_data_received_time) <= this_speaker->timeout_) {
+    while (!this_speaker->timeout_.has_value() ||
+           (millis() - last_data_received_time) <= this_speaker->timeout_.value()) {
       event_group_bits = xEventGroupGetBits(this_speaker->event_group_);
 
       if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) {
@@ -281,12 +283,18 @@ void I2SAudioSpeaker::speaker_task(void *params) {
         stop_gracefully = true;
       }
 
+      i2s_event_t i2s_event;
+      while (xQueueReceive(this_speaker->i2s_event_queue_, &i2s_event, 0)) {
+        if (i2s_event.type == I2S_EVENT_TX_Q_OVF) {
+          tx_dma_underflow = true;
+        }
+      }
+
       size_t bytes_to_read = dma_buffers_size;
       size_t bytes_read = this_speaker->audio_ring_buffer_->read((void *) this_speaker->data_buffer_, bytes_to_read,
                                                                  pdMS_TO_TICKS(TASK_DELAY_MS));
 
       if (bytes_read > 0) {
-        last_data_received_time = millis();
         size_t bytes_written = 0;
 
         if ((audio_stream_info.bits_per_sample == 16) && (this_speaker->q15_volume_factor_ < INT16_MAX)) {
@@ -307,15 +315,13 @@ void I2SAudioSpeaker::speaker_task(void *params) {
         if (bytes_written != bytes_read) {
           xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE);
         }
-
+        tx_dma_underflow = false;
+        last_data_received_time = millis();
       } else {
         // No data received
-
-        if (stop_gracefully) {
+        if (stop_gracefully && tx_dma_underflow) {
           break;
         }
-
-        i2s_zero_dma_buffer(this_speaker->parent_->get_port());
       }
     }
   } else {
@@ -326,7 +332,6 @@ void I2SAudioSpeaker::speaker_task(void *params) {
 
   xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STOPPING);
 
-  i2s_stop(this_speaker->parent_->get_port());
   i2s_driver_uninstall(this_speaker->parent_->get_port());
 
   this_speaker->parent_->unlock();
@@ -402,8 +407,8 @@ esp_err_t I2SAudioSpeaker::allocate_buffers_(size_t data_buffer_size, size_t rin
     return ESP_ERR_NO_MEM;
   }
 
-  if (this->audio_ring_buffer_ == nullptr) {
-    // Allocate ring buffer
+  if (this->audio_ring_buffer_.use_count() == 0) {
+    // Allocate ring buffer. Uses a shared_ptr to ensure it isn't improperly deallocated.
     this->audio_ring_buffer_ = RingBuffer::create(ring_buffer_size);
   }
 
@@ -419,6 +424,8 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_() {
     return ESP_ERR_INVALID_STATE;
   }
 
+  int dma_buffer_length = DMA_BUFFER_DURATION_MS * this->sample_rate_ / 1000;
+
   i2s_driver_config_t config = {
     .mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_TX),
     .sample_rate = this->sample_rate_,
@@ -427,7 +434,7 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_() {
     .communication_format = this->i2s_comm_fmt_,
     .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
     .dma_buf_count = DMA_BUFFERS_COUNT,
-    .dma_buf_len = DMA_BUFFER_SIZE,
+    .dma_buf_len = dma_buffer_length,
     .use_apll = this->use_apll_,
     .tx_desc_auto_clear = true,
     .fixed_mclk = I2S_PIN_NO_CHANGE,
@@ -448,7 +455,8 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_() {
   }
 #endif
 
-  esp_err_t err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr);
+  esp_err_t err =
+      i2s_driver_install(this->parent_->get_port(), &config, I2S_EVENT_QUEUE_COUNT, &this->i2s_event_queue_);
   if (err != ESP_OK) {
     // Failed to install the driver, so unlock the I2S port
     this->parent_->unlock();
@@ -502,16 +510,7 @@ esp_err_t I2SAudioSpeaker::reconfigure_i2s_stream_info_(audio::AudioStreamInfo &
 }
 
 void I2SAudioSpeaker::delete_task_(size_t buffer_size) {
-  if (this->audio_ring_buffer_ != nullptr) {
-    xEventGroupWaitBits(this->event_group_,
-                        MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE,  // Bit message to read
-                        pdFALSE,                                 // Don't clear the bits on exit
-                        pdTRUE,                                  // Don't wait for all the bits,
-                        portMAX_DELAY);                          // Block indefinitely until a command bit is set
-
-    this->audio_ring_buffer_.reset();  // Deallocates the ring buffer stored in the unique_ptr
-    this->audio_ring_buffer_ = nullptr;
-  }
+  this->audio_ring_buffer_.reset();  // Releases onwership of the shared_ptr
 
   if (this->data_buffer_ != nullptr) {
     ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
@@ -520,6 +519,7 @@ void I2SAudioSpeaker::delete_task_(size_t buffer_size) {
   }
 
   xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPED);
+  xQueueReset(this->i2s_event_queue_);
 
   this->task_created_ = false;
   vTaskDelete(nullptr);
diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h
index 3c512d4d4d..8b7386ba58 100644
--- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h
+++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h
@@ -7,6 +7,7 @@
 #include <driver/i2s.h>
 
 #include <freertos/event_groups.h>
+#include <freertos/queue.h>
 #include <freertos/FreeRTOS.h>
 
 #include "esphome/components/audio/audio.h"
@@ -27,6 +28,7 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
   void setup() override;
   void loop() override;
 
+  void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; }
   void set_timeout(uint32_t ms) { this->timeout_ = ms; }
   void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; }
 #if SOC_I2S_SUPPORTS_DAC
@@ -117,10 +119,14 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
   TaskHandle_t speaker_task_handle_{nullptr};
   EventGroupHandle_t event_group_{nullptr};
 
-  uint8_t *data_buffer_;
-  std::unique_ptr<RingBuffer> audio_ring_buffer_;
+  QueueHandle_t i2s_event_queue_;
 
-  uint32_t timeout_;
+  uint8_t *data_buffer_;
+  std::shared_ptr<RingBuffer> audio_ring_buffer_;
+
+  uint32_t buffer_duration_ms_;
+
+  optional<uint32_t> timeout_;
   uint8_t dout_pin_;
 
   bool task_created_{false};

From 1e80c4807eaab36d92c732a303664d29576d9e06 Mon Sep 17 00:00:00 2001
From: FreeBear-nc <67865163+FreeBear-nc@users.noreply.github.com>
Date: Tue, 12 Nov 2024 18:20:48 +0000
Subject: [PATCH 097/282] Message to string extend (#7755)

---
 esphome/components/opentherm/hub.cpp       |  4 ++--
 esphome/components/opentherm/opentherm.cpp | 27 ++++++++++++++++++++++
 2 files changed, 29 insertions(+), 2 deletions(-)

diff --git a/esphome/components/opentherm/hub.cpp b/esphome/components/opentherm/hub.cpp
index 432036d58d..dfa8ea95c5 100644
--- a/esphome/components/opentherm/hub.cpp
+++ b/esphome/components/opentherm/hub.cpp
@@ -371,11 +371,11 @@ void OpenthermHub::dump_config() {
   ESP_LOGCONFIG(TAG, "  Numbers: %s", SHOW(OPENTHERM_NUMBER_LIST(ID, )));
   ESP_LOGCONFIG(TAG, "  Initial requests:");
   for (auto type : this->initial_messages_) {
-    ESP_LOGCONFIG(TAG, "  - %d", type);
+    ESP_LOGCONFIG(TAG, "  - %d (%s)", type, this->opentherm_->message_id_to_str((type)));
   }
   ESP_LOGCONFIG(TAG, "  Repeating requests:");
   for (auto type : this->repeating_messages_) {
-    ESP_LOGCONFIG(TAG, "  - %d", type);
+    ESP_LOGCONFIG(TAG, "  - %d (%s)", type, this->opentherm_->message_id_to_str((type)));
   }
 }
 
diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp
index 4a23bb94cf..26c707f9a0 100644
--- a/esphome/components/opentherm/opentherm.cpp
+++ b/esphome/components/opentherm/opentherm.cpp
@@ -483,6 +483,8 @@ const char *OpenTherm::message_id_to_str(MessageId id) {
     TO_STRING_MEMBER(EXHAUST_TEMP)
     TO_STRING_MEMBER(FAN_SPEED)
     TO_STRING_MEMBER(FLAME_CURRENT)
+    TO_STRING_MEMBER(ROOM_TEMP_CH2)
+    TO_STRING_MEMBER(REL_HUMIDITY)
     TO_STRING_MEMBER(DHW_BOUNDS)
     TO_STRING_MEMBER(CH_BOUNDS)
     TO_STRING_MEMBER(OTC_CURVE_BOUNDS)
@@ -492,14 +494,39 @@ const char *OpenTherm::message_id_to_str(MessageId id) {
     TO_STRING_MEMBER(HVAC_STATUS)
     TO_STRING_MEMBER(REL_VENT_SETPOINT)
     TO_STRING_MEMBER(DEVICE_VENT)
+    TO_STRING_MEMBER(HVAC_VER_ID)
     TO_STRING_MEMBER(REL_VENTILATION)
     TO_STRING_MEMBER(REL_HUMID_EXHAUST)
+    TO_STRING_MEMBER(EXHAUST_CO2)
     TO_STRING_MEMBER(SUPPLY_INLET_TEMP)
     TO_STRING_MEMBER(SUPPLY_OUTLET_TEMP)
     TO_STRING_MEMBER(EXHAUST_INLET_TEMP)
     TO_STRING_MEMBER(EXHAUST_OUTLET_TEMP)
+    TO_STRING_MEMBER(EXHAUST_FAN_SPEED)
+    TO_STRING_MEMBER(SUPPLY_FAN_SPEED)
+    TO_STRING_MEMBER(REMOTE_VENTILATION_PARAM)
     TO_STRING_MEMBER(NOM_REL_VENTILATION)
+    TO_STRING_MEMBER(HVAC_NUM_TSP)
+    TO_STRING_MEMBER(HVAC_IDX_TSP)
+    TO_STRING_MEMBER(HVAC_FHB_SIZE)
+    TO_STRING_MEMBER(HVAC_FHB_IDX)
+    TO_STRING_MEMBER(RF_SIGNAL)
+    TO_STRING_MEMBER(DHW_MODE)
     TO_STRING_MEMBER(OVERRIDE_FUNC)
+    TO_STRING_MEMBER(SOLAR_MODE_FLAGS)
+    TO_STRING_MEMBER(SOLAR_ASF)
+    TO_STRING_MEMBER(SOLAR_VERSION_ID)
+    TO_STRING_MEMBER(SOLAR_PRODUCT_ID)
+    TO_STRING_MEMBER(SOLAR_NUM_TSP)
+    TO_STRING_MEMBER(SOLAR_IDX_TSP)
+    TO_STRING_MEMBER(SOLAR_FHB_SIZE)
+    TO_STRING_MEMBER(SOLAR_FHB_IDX)
+    TO_STRING_MEMBER(SOLAR_STARTS)
+    TO_STRING_MEMBER(SOLAR_HOURS)
+    TO_STRING_MEMBER(SOLAR_ENERGY)
+    TO_STRING_MEMBER(SOLAR_TOTAL_ENERGY)
+    TO_STRING_MEMBER(FAILED_BURNER_STARTS)
+    TO_STRING_MEMBER(BURNER_FLAME_LOW)
     TO_STRING_MEMBER(OEM_DIAGNOSTIC)
     TO_STRING_MEMBER(BURNER_STARTS)
     TO_STRING_MEMBER(CH_PUMP_STARTS)

From e6a1254e65d69ae0f362891409e7085768b6a479 Mon Sep 17 00:00:00 2001
From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com>
Date: Tue, 12 Nov 2024 19:23:00 +0100
Subject: [PATCH 098/282] [sun] Implements `is_above_horizon()` (#7754)

---
 esphome/components/sun/sun.h | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/esphome/components/sun/sun.h b/esphome/components/sun/sun.h
index de4801a655..77d62d34c3 100644
--- a/esphome/components/sun/sun.h
+++ b/esphome/components/sun/sun.h
@@ -59,6 +59,9 @@ class Sun {
   void set_latitude(double latitude) { location_.latitude = latitude; }
   void set_longitude(double longitude) { location_.longitude = longitude; }
 
+  // Check if the sun is above the horizon, with a default elevation angle of -0.83333 (standard for sunrise/set).
+  bool is_above_horizon(double elevation = -0.83333) { return this->elevation() > elevation; }
+
   optional<ESPTime> sunrise(double elevation);
   optional<ESPTime> sunset(double elevation);
   optional<ESPTime> sunrise(ESPTime date, double elevation);

From b367c01b4b27ac19c75d75774c7ce162894d6035 Mon Sep 17 00:00:00 2001
From: Kevin Ahrendt <kevin.ahrendt@nabucasa.com>
Date: Tue, 12 Nov 2024 13:48:03 -0500
Subject: [PATCH 099/282] [core] Ring buffer write functions use const pointer
 parameter (#7750)

---
 esphome/core/ring_buffer.cpp | 4 ++--
 esphome/core/ring_buffer.h   | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/esphome/core/ring_buffer.cpp b/esphome/core/ring_buffer.cpp
index f97c686684..6152ada314 100644
--- a/esphome/core/ring_buffer.cpp
+++ b/esphome/core/ring_buffer.cpp
@@ -46,7 +46,7 @@ size_t RingBuffer::read(void *data, size_t len, TickType_t ticks_to_wait) {
   return bytes_read;
 }
 
-size_t RingBuffer::write(void *data, size_t len) {
+size_t RingBuffer::write(const void *data, size_t len) {
   size_t free = this->free();
   if (free < len) {
     size_t needed = len - free;
@@ -56,7 +56,7 @@ size_t RingBuffer::write(void *data, size_t len) {
   return xStreamBufferSend(this->handle_, data, len, 0);
 }
 
-size_t RingBuffer::write_without_replacement(void *data, size_t len, TickType_t ticks_to_wait) {
+size_t RingBuffer::write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait) {
   return xStreamBufferSend(this->handle_, data, len, ticks_to_wait);
 }
 
diff --git a/esphome/core/ring_buffer.h b/esphome/core/ring_buffer.h
index c0511fb52e..aade1b5f49 100644
--- a/esphome/core/ring_buffer.h
+++ b/esphome/core/ring_buffer.h
@@ -37,7 +37,7 @@ class RingBuffer {
    * @param len Number of bytes to write
    * @return Number of bytes written
    */
-  size_t write(void *data, size_t len);
+  size_t write(const void *data, size_t len);
 
   /**
    * @brief Writes to the ring buffer without overwriting oldest data.
@@ -50,7 +50,7 @@ class RingBuffer {
    * @param ticks_to_wait Maximum number of FreeRTOS ticks to wait (default: 0)
    * @return Number of bytes written
    */
-  size_t write_without_replacement(void *data, size_t len, TickType_t ticks_to_wait = 0);
+  size_t write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait = 0);
 
   /**
    * @brief Returns the number of available bytes in the ring buffer.

From 7d75c9157bd9ddaa7c56389d6b3126a461b1e52b Mon Sep 17 00:00:00 2001
From: TFGF <terciofilho@gmail.com>
Date: Tue, 12 Nov 2024 17:48:40 -0300
Subject: [PATCH 100/282] [Modbus Controller] Added `on_online` and
 `on_offline` automation (#7417)

---
 .../components/modbus_controller/__init__.py  | 32 ++++++++++++++++++-
 .../components/modbus_controller/automation.h | 16 ++++++++++
 esphome/components/modbus_controller/const.py |  2 ++
 .../modbus_controller/modbus_controller.cpp   | 16 ++++++++--
 .../modbus_controller/modbus_controller.h     |  9 ++++++
 .../modbus_controller/test.esp32-ard.yaml     |  3 ++
 .../modbus_controller/test.esp32-idf.yaml     |  3 ++
 7 files changed, 78 insertions(+), 3 deletions(-)

diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py
index 488baa245a..5c407d6fff 100644
--- a/esphome/components/modbus_controller/__init__.py
+++ b/esphome/components/modbus_controller/__init__.py
@@ -25,6 +25,8 @@ from .const import (
     CONF_MODBUS_CONTROLLER_ID,
     CONF_OFFLINE_SKIP_UPDATES,
     CONF_ON_COMMAND_SENT,
+    CONF_ON_ONLINE,
+    CONF_ON_OFFLINE,
     CONF_REGISTER_COUNT,
     CONF_REGISTER_TYPE,
     CONF_RESPONSE_SIZE,
@@ -114,6 +116,14 @@ ModbusCommandSentTrigger = modbus_controller_ns.class_(
     "ModbusCommandSentTrigger", automation.Trigger.template(cg.int_, cg.int_)
 )
 
+ModbusOnlineTrigger = modbus_controller_ns.class_(
+    "ModbusOnlineTrigger", automation.Trigger.template(cg.int_, cg.int_)
+)
+
+ModbusOfflineTrigger = modbus_controller_ns.class_(
+    "ModbusOfflineTrigger", automation.Trigger.template(cg.int_, cg.int_)
+)
+
 _LOGGER = logging.getLogger(__name__)
 
 ModbusServerRegisterSchema = cv.Schema(
@@ -146,6 +156,16 @@ CONFIG_SCHEMA = cv.All(
                     ),
                 }
             ),
+            cv.Optional(CONF_ON_ONLINE): automation.validate_automation(
+                {
+                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOnlineTrigger),
+                }
+            ),
+            cv.Optional(CONF_ON_OFFLINE): automation.validate_automation(
+                {
+                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOnlineTrigger),
+                }
+            ),
         }
     )
     .extend(cv.polling_component_schema("60s"))
@@ -284,7 +304,17 @@ async def to_code(config):
     for conf in config.get(CONF_ON_COMMAND_SENT, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
         await automation.build_automation(
-            trigger, [(int, "function_code"), (int, "address")], conf
+            trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf
+        )
+    for conf in config.get(CONF_ON_ONLINE, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        await automation.build_automation(
+            trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf
+        )
+    for conf in config.get(CONF_ON_OFFLINE, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        await automation.build_automation(
+            trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf
         )
 
 
diff --git a/esphome/components/modbus_controller/automation.h b/esphome/components/modbus_controller/automation.h
index ad8de4b05d..b3338192cc 100644
--- a/esphome/components/modbus_controller/automation.h
+++ b/esphome/components/modbus_controller/automation.h
@@ -15,5 +15,21 @@ class ModbusCommandSentTrigger : public Trigger<int, int> {
   }
 };
 
+class ModbusOnlineTrigger : public Trigger<int, int> {
+ public:
+  ModbusOnlineTrigger(ModbusController *a_modbuscontroller) {
+    a_modbuscontroller->add_on_online_callback(
+        [this](int function_code, int address) { this->trigger(function_code, address); });
+  }
+};
+
+class ModbusOfflineTrigger : public Trigger<int, int> {
+ public:
+  ModbusOfflineTrigger(ModbusController *a_modbuscontroller) {
+    a_modbuscontroller->add_on_offline_callback(
+        [this](int function_code, int address) { this->trigger(function_code, address); });
+  }
+};
+
 }  // namespace modbus_controller
 }  // namespace esphome
diff --git a/esphome/components/modbus_controller/const.py b/esphome/components/modbus_controller/const.py
index 5cf7d230f1..4d39e48dcd 100644
--- a/esphome/components/modbus_controller/const.py
+++ b/esphome/components/modbus_controller/const.py
@@ -9,6 +9,8 @@ CONF_MAX_CMD_RETRIES = "max_cmd_retries"
 CONF_MODBUS_CONTROLLER_ID = "modbus_controller_id"
 CONF_MODBUS_FUNCTIONCODE = "modbus_functioncode"
 CONF_ON_COMMAND_SENT = "on_command_sent"
+CONF_ON_ONLINE = "on_online"
+CONF_ON_OFFLINE = "on_offline"
 CONF_RAW_ENCODE = "raw_encode"
 CONF_REGISTER_COUNT = "register_count"
 CONF_REGISTER_TYPE = "register_type"
diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp
index 1dcb533629..e1102516ca 100644
--- a/esphome/components/modbus_controller/modbus_controller.cpp
+++ b/esphome/components/modbus_controller/modbus_controller.cpp
@@ -32,8 +32,10 @@ bool ModbusController::send_next_command_() {
             r.skip_updates_counter = this->offline_skip_updates_;
           }
         }
+
+        this->module_offline_ = true;
+        this->offline_callback_.call((int) command->function_code, command->register_address);
       }
-      this->module_offline_ = true;
       ESP_LOGD(TAG, "Modbus command to device=%d register=0x%02X no response received - removed from send queue",
                this->address_, command->register_address);
       this->command_queue_.pop_front();
@@ -68,8 +70,10 @@ void ModbusController::on_modbus_data(const std::vector<uint8_t> &data) {
           r.skip_updates_counter = 0;
         }
       }
+      // Restore module online state
+      this->module_offline_ = false;
+      this->online_callback_.call((int) current_command->function_code, current_command->register_address);
     }
-    this->module_offline_ = false;
 
     // Move the commandItem to the response queue
     current_command->payload = data;
@@ -670,5 +674,13 @@ void ModbusController::add_on_command_sent_callback(std::function<void(int, int)
   this->command_sent_callback_.add(std::move(callback));
 }
 
+void ModbusController::add_on_online_callback(std::function<void(int, int)> &&callback) {
+  this->online_callback_.add(std::move(callback));
+}
+
+void ModbusController::add_on_offline_callback(std::function<void(int, int)> &&callback) {
+  this->offline_callback_.add(std::move(callback));
+}
+
 }  // namespace modbus_controller
 }  // namespace esphome
diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h
index 1fa35e1535..2a0b936bf5 100644
--- a/esphome/components/modbus_controller/modbus_controller.h
+++ b/esphome/components/modbus_controller/modbus_controller.h
@@ -468,6 +468,10 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
   bool get_module_offline() { return module_offline_; }
   /// Set callback for commands
   void add_on_command_sent_callback(std::function<void(int, int)> &&callback);
+  /// Set callback for online changes
+  void add_on_online_callback(std::function<void(int, int)> &&callback);
+  /// Set callback for offline changes
+  void add_on_offline_callback(std::function<void(int, int)> &&callback);
   /// called by esphome generated code to set the max_cmd_retries.
   void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; }
   /// get how many times a command will be (re)sent if no response is received
@@ -508,7 +512,12 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
   uint16_t offline_skip_updates_;
   /// How many times we will retry a command if we get no response
   uint8_t max_cmd_retries_{4};
+  /// Command sent callback
   CallbackManager<void(int, int)> command_sent_callback_{};
+  /// Server online callback
+  CallbackManager<void(int, int)> online_callback_{};
+  /// Server offline callback
+  CallbackManager<void(int, int)> offline_callback_{};
 };
 
 /** Convert vector<uint8_t> response payload to float.
diff --git a/tests/components/modbus_controller/test.esp32-ard.yaml b/tests/components/modbus_controller/test.esp32-ard.yaml
index cd95d149cb..f5c5c10125 100644
--- a/tests/components/modbus_controller/test.esp32-ard.yaml
+++ b/tests/components/modbus_controller/test.esp32-ard.yaml
@@ -21,6 +21,9 @@ modbus_controller:
     address: 0x2
     modbus_id: mod_bus1
     allow_duplicate_commands: false
+    on_online:
+      then:
+        logger.log: "Module Online"
   - id: modbus_controller2
     address: 0x2
     modbus_id: mod_bus2
diff --git a/tests/components/modbus_controller/test.esp32-idf.yaml b/tests/components/modbus_controller/test.esp32-idf.yaml
index ba28e94d73..0e1849dd88 100644
--- a/tests/components/modbus_controller/test.esp32-idf.yaml
+++ b/tests/components/modbus_controller/test.esp32-idf.yaml
@@ -13,4 +13,7 @@ modbus_controller:
     address: 0x2
     modbus_id: mod_bus1
     allow_duplicate_commands: true
+    on_offline:
+      then:
+        logger.log: "Module Offline"
     max_cmd_retries: 10

From 053465d3f627809fc890eb94271419df0368f369 Mon Sep 17 00:00:00 2001
From: Kyle Cascade <kyle@xkyle.com>
Date: Tue, 12 Nov 2024 14:54:25 -0800
Subject: [PATCH 101/282] Updated dfplayer logging to be more user-friendly
 (#7740)

---
 esphome/components/dfplayer/dfplayer.cpp | 138 ++++++++++++++++++++++-
 esphome/components/dfplayer/dfplayer.h   |  72 ++++--------
 2 files changed, 151 insertions(+), 59 deletions(-)

diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp
index aa2dc260e0..98c3e91e46 100644
--- a/esphome/components/dfplayer/dfplayer.cpp
+++ b/esphome/components/dfplayer/dfplayer.cpp
@@ -6,7 +6,104 @@ namespace dfplayer {
 
 static const char *const TAG = "dfplayer";
 
+void DFPlayer::next() {
+  this->ack_set_is_playing_ = true;
+  ESP_LOGD(TAG, "Playing next track");
+  this->send_cmd_(0x01);
+}
+
+void DFPlayer::previous() {
+  this->ack_set_is_playing_ = true;
+  ESP_LOGD(TAG, "Playing previous track");
+  this->send_cmd_(0x02);
+}
+void DFPlayer::play_mp3(uint16_t file) {
+  this->ack_set_is_playing_ = true;
+  ESP_LOGD(TAG, "Playing file %d in mp3 folder", file);
+  this->send_cmd_(0x12, file);
+}
+
+void DFPlayer::play_file(uint16_t file) {
+  this->ack_set_is_playing_ = true;
+  ESP_LOGD(TAG, "Playing file %d", file);
+  this->send_cmd_(0x03, file);
+}
+
+void DFPlayer::play_file_loop(uint16_t file) {
+  this->ack_set_is_playing_ = true;
+  ESP_LOGD(TAG, "Playing file %d in loop", file);
+  this->send_cmd_(0x08, file);
+}
+
+void DFPlayer::play_folder_loop(uint16_t folder) {
+  this->ack_set_is_playing_ = true;
+  ESP_LOGD(TAG, "Playing folder %d in loop", folder);
+  this->send_cmd_(0x17, folder);
+}
+
+void DFPlayer::volume_up() {
+  ESP_LOGD(TAG, "Increasing volume");
+  this->send_cmd_(0x04);
+}
+
+void DFPlayer::volume_down() {
+  ESP_LOGD(TAG, "Decreasing volume");
+  this->send_cmd_(0x05);
+}
+
+void DFPlayer::set_device(Device device) {
+  ESP_LOGD(TAG, "Setting device to %d", device);
+  this->send_cmd_(0x09, device);
+}
+
+void DFPlayer::set_volume(uint8_t volume) {
+  ESP_LOGD(TAG, "Setting volume to %d", volume);
+  this->send_cmd_(0x06, volume);
+}
+
+void DFPlayer::set_eq(EqPreset preset) {
+  ESP_LOGD(TAG, "Setting EQ to %d", preset);
+  this->send_cmd_(0x07, preset);
+}
+
+void DFPlayer::sleep() {
+  this->ack_reset_is_playing_ = true;
+  ESP_LOGD(TAG, "Putting DFPlayer to sleep");
+  this->send_cmd_(0x0A);
+}
+
+void DFPlayer::reset() {
+  this->ack_reset_is_playing_ = true;
+  ESP_LOGD(TAG, "Resetting DFPlayer");
+  this->send_cmd_(0x0C);
+}
+
+void DFPlayer::start() {
+  this->ack_set_is_playing_ = true;
+  ESP_LOGD(TAG, "Starting playback");
+  this->send_cmd_(0x0D);
+}
+
+void DFPlayer::pause() {
+  this->ack_reset_is_playing_ = true;
+  ESP_LOGD(TAG, "Pausing playback");
+  this->send_cmd_(0x0E);
+}
+
+void DFPlayer::stop() {
+  this->ack_reset_is_playing_ = true;
+  ESP_LOGD(TAG, "Stopping playback");
+  this->send_cmd_(0x16);
+}
+
+void DFPlayer::random() {
+  this->ack_set_is_playing_ = true;
+  ESP_LOGD(TAG, "Playing random file");
+  this->send_cmd_(0x18);
+}
+
 void DFPlayer::play_folder(uint16_t folder, uint16_t file) {
+  ESP_LOGD(TAG, "Playing file %d in folder %d", file, folder);
   if (folder < 100 && file < 256) {
     this->ack_set_is_playing_ = true;
     this->send_cmd_(0x0F, (uint8_t) folder, (uint8_t) file);
@@ -29,7 +126,7 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
 
   this->sent_cmd_ = cmd;
 
-  ESP_LOGD(TAG, "Send Command %#02x arg %#04x", cmd, argument);
+  ESP_LOGV(TAG, "Send Command %#02x arg %#04x", cmd, argument);
   this->write_array(buffer, 10);
 }
 
@@ -101,9 +198,37 @@ void DFPlayer::loop() {
             ESP_LOGV(TAG, "Nack");
             this->ack_set_is_playing_ = false;
             this->ack_reset_is_playing_ = false;
-            if (argument == 6) {
-              ESP_LOGV(TAG, "File not found");
-              this->is_playing_ = false;
+            switch (argument) {
+              case 0x01:
+                ESP_LOGE(TAG, "Module is busy or uninitialized");
+                break;
+              case 0x02:
+                ESP_LOGE(TAG, "Module is in sleep mode");
+                break;
+              case 0x03:
+                ESP_LOGE(TAG, "Serial receive error");
+                break;
+              case 0x04:
+                ESP_LOGE(TAG, "Checksum incorrect");
+                break;
+              case 0x05:
+                ESP_LOGE(TAG, "Specified track is out of current track scope");
+                this->is_playing_ = false;
+                break;
+              case 0x06:
+                ESP_LOGE(TAG, "Specified track is not found");
+                this->is_playing_ = false;
+                break;
+              case 0x07:
+                ESP_LOGE(TAG, "Insertion error (an inserting operation only can be done when a track is being played)");
+                break;
+              case 0x08:
+                ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
+                break;
+              case 0x09:
+                ESP_LOGE(TAG, "Entered into sleep mode");
+                this->is_playing_ = false;
+                break;
             }
             break;
           case 0x41:
@@ -113,12 +238,13 @@ void DFPlayer::loop() {
             this->ack_set_is_playing_ = false;
             this->ack_reset_is_playing_ = false;
             break;
-          case 0x3D:  // Playback finished
+          case 0x3D:
+            ESP_LOGV(TAG, "Playback finished");
             this->is_playing_ = false;
             this->on_finished_playback_callback_.call();
             break;
           default:
-            ESP_LOGD(TAG, "Command %#02x arg %#04x", cmd, argument);
+            ESP_LOGV(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
         }
         this->sent_cmd_ = 0;
         this->read_pos_ = 0;
diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h
index 26e90fd410..d2ec0a2310 100644
--- a/esphome/components/dfplayer/dfplayer.h
+++ b/esphome/components/dfplayer/dfplayer.h
@@ -23,64 +23,30 @@ enum Device {
   TF_CARD = 2,
 };
 
+// See the datasheet here:
+// https://github.com/DFRobot/DFRobotDFPlayerMini/blob/master/doc/FN-M16P%2BEmbedded%2BMP3%2BAudio%2BModule%2BDatasheet.pdf
 class DFPlayer : public uart::UARTDevice, public Component {
  public:
   void loop() override;
 
-  void next() {
-    this->ack_set_is_playing_ = true;
-    this->send_cmd_(0x01);
-  }
-  void previous() {
-    this->ack_set_is_playing_ = true;
-    this->send_cmd_(0x02);
-  }
-  void play_mp3(uint16_t file) {
-    this->ack_set_is_playing_ = true;
-    this->send_cmd_(0x12, file);
-  }
-  void play_file(uint16_t file) {
-    this->ack_set_is_playing_ = true;
-    this->send_cmd_(0x03, file);
-  }
-  void play_file_loop(uint16_t file) {
-    this->ack_set_is_playing_ = true;
-    this->send_cmd_(0x08, file);
-  }
+  void next();
+  void previous();
+  void play_mp3(uint16_t file);
+  void play_file(uint16_t file);
+  void play_file_loop(uint16_t file);
   void play_folder(uint16_t folder, uint16_t file);
-  void play_folder_loop(uint16_t folder) {
-    this->ack_set_is_playing_ = true;
-    this->send_cmd_(0x17, folder);
-  }
-  void volume_up() { this->send_cmd_(0x04); }
-  void volume_down() { this->send_cmd_(0x05); }
-  void set_device(Device device) { this->send_cmd_(0x09, device); }
-  void set_volume(uint8_t volume) { this->send_cmd_(0x06, volume); }
-  void set_eq(EqPreset preset) { this->send_cmd_(0x07, preset); }
-  void sleep() {
-    this->ack_reset_is_playing_ = true;
-    this->send_cmd_(0x0A);
-  }
-  void reset() {
-    this->ack_reset_is_playing_ = true;
-    this->send_cmd_(0x0C);
-  }
-  void start() {
-    this->ack_set_is_playing_ = true;
-    this->send_cmd_(0x0D);
-  }
-  void pause() {
-    this->ack_reset_is_playing_ = true;
-    this->send_cmd_(0x0E);
-  }
-  void stop() {
-    this->ack_reset_is_playing_ = true;
-    this->send_cmd_(0x16);
-  }
-  void random() {
-    this->ack_set_is_playing_ = true;
-    this->send_cmd_(0x18);
-  }
+  void play_folder_loop(uint16_t folder);
+  void volume_up();
+  void volume_down();
+  void set_device(Device device);
+  void set_volume(uint8_t volume);
+  void set_eq(EqPreset preset);
+  void sleep();
+  void reset();
+  void start();
+  void pause();
+  void stop();
+  void random();
 
   bool is_playing() { return is_playing_; }
   void dump_config() override;

From 80226694d5d0c5d44f0bb8c2c39b070802c4b073 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Wed, 13 Nov 2024 13:16:13 +1300
Subject: [PATCH 102/282] Bump version to 2024.11.0b1

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

diff --git a/esphome/const.py b/esphome/const.py
index 5645c9eaab..c3c8712677 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,6 +1,6 @@
 """Constants used by esphome."""
 
-__version__ = "2024.11.0-dev"
+__version__ = "2024.11.0b1"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 VALID_SUBSTITUTIONS_CHARACTERS = (

From 1f7f03f563e3500beeda9a3c163ee48e309e7617 Mon Sep 17 00:00:00 2001
From: luar123 <49960470+luar123@users.noreply.github.com>
Date: Wed, 13 Nov 2024 01:18:10 +0100
Subject: [PATCH 103/282] Fix temperature and humidity for bme680 with bsec2
 (#7728)

---
 .../components/bme68x_bsec2/bme68x_bsec2.cpp  | 95 ++++++++++---------
 .../components/bme68x_bsec2/bme68x_bsec2.h    |  2 -
 2 files changed, 49 insertions(+), 48 deletions(-)

diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp
index 5425bbd5b7..f83f20f1a5 100644
--- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp
+++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp
@@ -204,11 +204,11 @@ void BME68xBSEC2Component::update_subscription_() {
 }
 
 void BME68xBSEC2Component::run_() {
+  this->op_mode_ = this->bsec_settings_.op_mode;
   int64_t curr_time_ns = this->get_time_ns_();
-  if (curr_time_ns < this->next_call_ns_) {
+  if (curr_time_ns < this->bsec_settings_.next_call) {
     return;
   }
-  this->op_mode_ = this->bsec_settings_.op_mode;
   uint8_t status;
 
   ESP_LOGV(TAG, "Performing sensor run");
@@ -219,57 +219,60 @@ void BME68xBSEC2Component::run_() {
     ESP_LOGW(TAG, "Failed to fetch sensor control settings (BSEC2 error code %d)", this->bsec_status_);
     return;
   }
-  this->next_call_ns_ = this->bsec_settings_.next_call;
 
-  if (this->bsec_settings_.trigger_measurement) {
-    bme68x_get_conf(&bme68x_conf, &this->bme68x_);
+  switch (this->bsec_settings_.op_mode) {
+    case BME68X_FORCED_MODE:
+      bme68x_get_conf(&bme68x_conf, &this->bme68x_);
 
-    bme68x_conf.os_hum = this->bsec_settings_.humidity_oversampling;
-    bme68x_conf.os_temp = this->bsec_settings_.temperature_oversampling;
-    bme68x_conf.os_pres = this->bsec_settings_.pressure_oversampling;
-    bme68x_set_conf(&bme68x_conf, &this->bme68x_);
+      bme68x_conf.os_hum = this->bsec_settings_.humidity_oversampling;
+      bme68x_conf.os_temp = this->bsec_settings_.temperature_oversampling;
+      bme68x_conf.os_pres = this->bsec_settings_.pressure_oversampling;
+      bme68x_set_conf(&bme68x_conf, &this->bme68x_);
+      this->bme68x_heatr_conf_.enable = BME68X_ENABLE;
+      this->bme68x_heatr_conf_.heatr_temp = this->bsec_settings_.heater_temperature;
+      this->bme68x_heatr_conf_.heatr_dur = this->bsec_settings_.heater_duration;
+
+      // status = bme68x_set_op_mode(this->bsec_settings_.op_mode, &this->bme68x_);
+      status = bme68x_set_heatr_conf(BME68X_FORCED_MODE, &this->bme68x_heatr_conf_, &this->bme68x_);
+      status = bme68x_set_op_mode(BME68X_FORCED_MODE, &this->bme68x_);
+      this->op_mode_ = BME68X_FORCED_MODE;
+      ESP_LOGV(TAG, "Using forced mode");
+
+      break;
+    case BME68X_PARALLEL_MODE:
+      if (this->op_mode_ != this->bsec_settings_.op_mode) {
+        bme68x_get_conf(&bme68x_conf, &this->bme68x_);
+
+        bme68x_conf.os_hum = this->bsec_settings_.humidity_oversampling;
+        bme68x_conf.os_temp = this->bsec_settings_.temperature_oversampling;
+        bme68x_conf.os_pres = this->bsec_settings_.pressure_oversampling;
+        bme68x_set_conf(&bme68x_conf, &this->bme68x_);
 
-    switch (this->bsec_settings_.op_mode) {
-      case BME68X_FORCED_MODE:
         this->bme68x_heatr_conf_.enable = BME68X_ENABLE;
-        this->bme68x_heatr_conf_.heatr_temp = this->bsec_settings_.heater_temperature;
-        this->bme68x_heatr_conf_.heatr_dur = this->bsec_settings_.heater_duration;
+        this->bme68x_heatr_conf_.heatr_temp_prof = this->bsec_settings_.heater_temperature_profile;
+        this->bme68x_heatr_conf_.heatr_dur_prof = this->bsec_settings_.heater_duration_profile;
+        this->bme68x_heatr_conf_.profile_len = this->bsec_settings_.heater_profile_len;
+        this->bme68x_heatr_conf_.shared_heatr_dur =
+            BSEC_TOTAL_HEAT_DUR -
+            (bme68x_get_meas_dur(BME68X_PARALLEL_MODE, &bme68x_conf, &this->bme68x_) / INT64_C(1000));
 
-        status = bme68x_set_op_mode(this->bsec_settings_.op_mode, &this->bme68x_);
-        status = bme68x_set_heatr_conf(BME68X_FORCED_MODE, &this->bme68x_heatr_conf_, &this->bme68x_);
-        status = bme68x_set_op_mode(BME68X_FORCED_MODE, &this->bme68x_);
-        this->op_mode_ = BME68X_FORCED_MODE;
-        this->sleep_mode_ = false;
-        ESP_LOGV(TAG, "Using forced mode");
+        status = bme68x_set_heatr_conf(BME68X_PARALLEL_MODE, &this->bme68x_heatr_conf_, &this->bme68x_);
 
-        break;
-      case BME68X_PARALLEL_MODE:
-        if (this->op_mode_ != this->bsec_settings_.op_mode) {
-          this->bme68x_heatr_conf_.enable = BME68X_ENABLE;
-          this->bme68x_heatr_conf_.heatr_temp_prof = this->bsec_settings_.heater_temperature_profile;
-          this->bme68x_heatr_conf_.heatr_dur_prof = this->bsec_settings_.heater_duration_profile;
-          this->bme68x_heatr_conf_.profile_len = this->bsec_settings_.heater_profile_len;
-          this->bme68x_heatr_conf_.shared_heatr_dur =
-              BSEC_TOTAL_HEAT_DUR -
-              (bme68x_get_meas_dur(BME68X_PARALLEL_MODE, &bme68x_conf, &this->bme68x_) / INT64_C(1000));
-
-          status = bme68x_set_heatr_conf(BME68X_PARALLEL_MODE, &this->bme68x_heatr_conf_, &this->bme68x_);
-
-          status = bme68x_set_op_mode(BME68X_PARALLEL_MODE, &this->bme68x_);
-          this->op_mode_ = BME68X_PARALLEL_MODE;
-          this->sleep_mode_ = false;
-          ESP_LOGV(TAG, "Using parallel mode");
-        }
-        break;
-      case BME68X_SLEEP_MODE:
-        if (!this->sleep_mode_) {
-          bme68x_set_op_mode(BME68X_SLEEP_MODE, &this->bme68x_);
-          this->sleep_mode_ = true;
-          ESP_LOGV(TAG, "Using sleep mode");
-        }
-        break;
-    }
+        status = bme68x_set_op_mode(BME68X_PARALLEL_MODE, &this->bme68x_);
+        this->op_mode_ = BME68X_PARALLEL_MODE;
+        ESP_LOGV(TAG, "Using parallel mode");
+      }
+      break;
+    case BME68X_SLEEP_MODE:
+      if (this->op_mode_ != this->bsec_settings_.op_mode) {
+        bme68x_set_op_mode(BME68X_SLEEP_MODE, &this->bme68x_);
+        this->op_mode_ = BME68X_SLEEP_MODE;
+        ESP_LOGV(TAG, "Using sleep mode");
+      }
+      break;
+  }
 
+  if (this->bsec_settings_.trigger_measurement && this->bsec_settings_.op_mode != BME68X_SLEEP_MODE) {
     uint32_t meas_dur = 0;
     meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_);
     ESP_LOGV(TAG, "Queueing read in %uus", meas_dur);
diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.h b/esphome/components/bme68x_bsec2/bme68x_bsec2.h
index 7b9db2b7bf..86d3e5dfbf 100644
--- a/esphome/components/bme68x_bsec2/bme68x_bsec2.h
+++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.h
@@ -113,13 +113,11 @@ class BME68xBSEC2Component : public Component {
 
   struct bme68x_heatr_conf bme68x_heatr_conf_;
   uint8_t op_mode_;  // operating mode of sensor
-  bool sleep_mode_;
   bsec_library_return_t bsec_status_{BSEC_OK};
   int8_t bme68x_status_{BME68X_OK};
 
   int64_t last_time_ms_{0};
   uint32_t millis_overflow_counter_{0};
-  int64_t next_call_ns_{0};
 
   std::queue<std::function<void()>> queue_;
 

From a2cab960a9d2f138fe736ce8147b383f13146475 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Wed, 13 Nov 2024 13:16:13 +1300
Subject: [PATCH 104/282] Bump version to 2024.12.0-dev

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

diff --git a/esphome/const.py b/esphome/const.py
index 5645c9eaab..d42ee5ee72 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,6 +1,6 @@
 """Constants used by esphome."""
 
-__version__ = "2024.11.0-dev"
+__version__ = "2024.12.0-dev"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 VALID_SUBSTITUTIONS_CHARACTERS = (

From c7c8711c9c8380d9d90875973e2e8c0e70678667 Mon Sep 17 00:00:00 2001
From: Kevin Ahrendt <kevin.ahrendt@nabucasa.com>
Date: Wed, 13 Nov 2024 12:39:02 -0500
Subject: [PATCH 105/282] [i2s_audio] Bugfix: Adjust I2S speaker setup priority
 (#7759)

---
 .../i2s_audio/speaker/i2s_audio_speaker.cpp           | 11 +----------
 .../components/i2s_audio/speaker/i2s_audio_speaker.h  |  2 +-
 2 files changed, 2 insertions(+), 11 deletions(-)

diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
index c3f4566411..53b3cc8dc0 100644
--- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
+++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
@@ -99,14 +99,6 @@ void I2SAudioSpeaker::setup() {
     this->mark_failed();
     return;
   }
-
-  this->i2s_event_queue_ = xQueueCreate(I2S_EVENT_QUEUE_COUNT, sizeof(i2s_event_t));
-
-  if (this->i2s_event_queue_ == nullptr) {
-    ESP_LOGE(TAG, "Failed to create I2S event queue");
-    this->mark_failed();
-    return;
-  }
 }
 
 void I2SAudioSpeaker::loop() {
@@ -339,7 +331,7 @@ void I2SAudioSpeaker::speaker_task(void *params) {
 }
 
 void I2SAudioSpeaker::start() {
-  if (this->is_failed() || this->status_has_error())
+  if (!this->is_ready() || this->is_failed() || this->status_has_error())
     return;
   if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING))
     return;
@@ -519,7 +511,6 @@ void I2SAudioSpeaker::delete_task_(size_t buffer_size) {
   }
 
   xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPED);
-  xQueueReset(this->i2s_event_queue_);
 
   this->task_created_ = false;
   vTaskDelete(nullptr);
diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h
index 8b7386ba58..2b90f39399 100644
--- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h
+++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h
@@ -23,7 +23,7 @@ namespace i2s_audio {
 
 class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Component {
  public:
-  float get_setup_priority() const override { return esphome::setup_priority::LATE; }
+  float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; }
 
   void setup() override;
   void loop() override;

From 39c889e6625a212b582d519012c6b69a45862bc6 Mon Sep 17 00:00:00 2001
From: Roving Ronin <108674933+Roving-Ronin@users.noreply.github.com>
Date: Thu, 14 Nov 2024 11:43:21 +1100
Subject: [PATCH 106/282] Update UNIT_VOLT_AMPS_REACTIVE = "var" (Currently
 'VAR') (#7643)

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

diff --git a/esphome/const.py b/esphome/const.py
index d42ee5ee72..6a643e1e30 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1095,7 +1095,7 @@ UNIT_STEPS = "steps"
 UNIT_VOLT = "V"
 UNIT_VOLT_AMPS = "VA"
 UNIT_VOLT_AMPS_HOURS = "VAh"
-UNIT_VOLT_AMPS_REACTIVE = "VAR"
+UNIT_VOLT_AMPS_REACTIVE = "var"
 UNIT_VOLT_AMPS_REACTIVE_HOURS = "VARh"
 UNIT_WATT = "W"
 UNIT_WATT_HOURS = "Wh"

From d01508885531c31a2ebb518c8b07f550d5768ccc Mon Sep 17 00:00:00 2001
From: Felipe Santos <felipecassiors@gmail.com>
Date: Wed, 13 Nov 2024 21:44:18 -0300
Subject: [PATCH 107/282] Fix reactive power unit of measurement from VAR to
 var (#7757)

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

diff --git a/esphome/components/sdm_meter/sdm_meter.cpp b/esphome/components/sdm_meter/sdm_meter.cpp
index 9c35d306ad..18e06e2b04 100644
--- a/esphome/components/sdm_meter/sdm_meter.cpp
+++ b/esphome/components/sdm_meter/sdm_meter.cpp
@@ -38,7 +38,7 @@ void SDMMeter::on_modbus_data(const std::vector<uint8_t> &data) {
 
     ESP_LOGD(
         TAG,
-        "SDMMeter Phase %c: V=%.3f V, I=%.3f A, Active P=%.3f W, Apparent P=%.3f VA, Reactive P=%.3f VAR, PF=%.3f, "
+        "SDMMeter Phase %c: V=%.3f V, I=%.3f A, Active P=%.3f W, Apparent P=%.3f VA, Reactive P=%.3f var, PF=%.3f, "
         "PA=%.3f °",
         i + 'A', voltage, current, active_power, apparent_power, reactive_power, power_factor, phase_angle);
     if (phase.voltage_sensor_ != nullptr)

From 5e62c489b08a70295f3247c19c907de57507e263 Mon Sep 17 00:00:00 2001
From: Jordan Zucker <jordan.zucker@gmail.com>
Date: Wed, 13 Nov 2024 16:57:09 -0800
Subject: [PATCH 108/282] Disable bluetooth proxy during update (#7695)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp | 3 +++
 tests/components/esp32_ble_tracker/common.yaml             | 7 +++++++
 2 files changed, 10 insertions(+)

diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
index 74b4b9aa89..b86d32ee61 100644
--- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
+++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
@@ -65,6 +65,9 @@ void ESP32BLETracker::setup() {
       [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
         if (state == ota::OTA_STARTED) {
           this->stop_scan();
+          for (auto *client : this->clients_) {
+            client->disconnect();
+          }
         }
       });
 #endif
diff --git a/tests/components/esp32_ble_tracker/common.yaml b/tests/components/esp32_ble_tracker/common.yaml
index ef23635c9e..018bbb42b3 100644
--- a/tests/components/esp32_ble_tracker/common.yaml
+++ b/tests/components/esp32_ble_tracker/common.yaml
@@ -39,3 +39,10 @@ esp32_ble_tracker:
     - then:
         - lambda: |-
              ESP_LOGD("ble_auto", "The scan has ended!");
+
+wifi:
+  ssid: MySSID
+  password: password1
+
+ota:
+  - platform: esphome

From 0b51ec2c88793191f8d28fecfe9b514b9195312f Mon Sep 17 00:00:00 2001
From: Fabio Bonelli <fbonelli@gmail.com>
Date: Thu, 14 Nov 2024 01:57:51 +0100
Subject: [PATCH 109/282] ld2420: fix typo in log message (#7758)

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

diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp
index e57fdbc84e..9d628cc14f 100644
--- a/esphome/components/ld2420/ld2420.cpp
+++ b/esphome/components/ld2420/ld2420.cpp
@@ -180,7 +180,7 @@ void LD2420Component::apply_config_action() {
 }
 
 void LD2420Component::factory_reset_action() {
-  ESP_LOGCONFIG(TAG, "Setiing factory defaults...");
+  ESP_LOGCONFIG(TAG, "Setting factory defaults...");
   if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) {
     ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections.");
     this->mark_failed();

From 44545a18a0de2724d14e497357a4aea5a8f90d19 Mon Sep 17 00:00:00 2001
From: luar123 <49960470+luar123@users.noreply.github.com>
Date: Wed, 13 Nov 2024 01:18:10 +0100
Subject: [PATCH 110/282] Fix temperature and humidity for bme680 with bsec2
 (#7728)

---
 .../components/bme68x_bsec2/bme68x_bsec2.cpp  | 95 ++++++++++---------
 .../components/bme68x_bsec2/bme68x_bsec2.h    |  2 -
 2 files changed, 49 insertions(+), 48 deletions(-)

diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp
index 5425bbd5b7..f83f20f1a5 100644
--- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp
+++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp
@@ -204,11 +204,11 @@ void BME68xBSEC2Component::update_subscription_() {
 }
 
 void BME68xBSEC2Component::run_() {
+  this->op_mode_ = this->bsec_settings_.op_mode;
   int64_t curr_time_ns = this->get_time_ns_();
-  if (curr_time_ns < this->next_call_ns_) {
+  if (curr_time_ns < this->bsec_settings_.next_call) {
     return;
   }
-  this->op_mode_ = this->bsec_settings_.op_mode;
   uint8_t status;
 
   ESP_LOGV(TAG, "Performing sensor run");
@@ -219,57 +219,60 @@ void BME68xBSEC2Component::run_() {
     ESP_LOGW(TAG, "Failed to fetch sensor control settings (BSEC2 error code %d)", this->bsec_status_);
     return;
   }
-  this->next_call_ns_ = this->bsec_settings_.next_call;
 
-  if (this->bsec_settings_.trigger_measurement) {
-    bme68x_get_conf(&bme68x_conf, &this->bme68x_);
+  switch (this->bsec_settings_.op_mode) {
+    case BME68X_FORCED_MODE:
+      bme68x_get_conf(&bme68x_conf, &this->bme68x_);
 
-    bme68x_conf.os_hum = this->bsec_settings_.humidity_oversampling;
-    bme68x_conf.os_temp = this->bsec_settings_.temperature_oversampling;
-    bme68x_conf.os_pres = this->bsec_settings_.pressure_oversampling;
-    bme68x_set_conf(&bme68x_conf, &this->bme68x_);
+      bme68x_conf.os_hum = this->bsec_settings_.humidity_oversampling;
+      bme68x_conf.os_temp = this->bsec_settings_.temperature_oversampling;
+      bme68x_conf.os_pres = this->bsec_settings_.pressure_oversampling;
+      bme68x_set_conf(&bme68x_conf, &this->bme68x_);
+      this->bme68x_heatr_conf_.enable = BME68X_ENABLE;
+      this->bme68x_heatr_conf_.heatr_temp = this->bsec_settings_.heater_temperature;
+      this->bme68x_heatr_conf_.heatr_dur = this->bsec_settings_.heater_duration;
+
+      // status = bme68x_set_op_mode(this->bsec_settings_.op_mode, &this->bme68x_);
+      status = bme68x_set_heatr_conf(BME68X_FORCED_MODE, &this->bme68x_heatr_conf_, &this->bme68x_);
+      status = bme68x_set_op_mode(BME68X_FORCED_MODE, &this->bme68x_);
+      this->op_mode_ = BME68X_FORCED_MODE;
+      ESP_LOGV(TAG, "Using forced mode");
+
+      break;
+    case BME68X_PARALLEL_MODE:
+      if (this->op_mode_ != this->bsec_settings_.op_mode) {
+        bme68x_get_conf(&bme68x_conf, &this->bme68x_);
+
+        bme68x_conf.os_hum = this->bsec_settings_.humidity_oversampling;
+        bme68x_conf.os_temp = this->bsec_settings_.temperature_oversampling;
+        bme68x_conf.os_pres = this->bsec_settings_.pressure_oversampling;
+        bme68x_set_conf(&bme68x_conf, &this->bme68x_);
 
-    switch (this->bsec_settings_.op_mode) {
-      case BME68X_FORCED_MODE:
         this->bme68x_heatr_conf_.enable = BME68X_ENABLE;
-        this->bme68x_heatr_conf_.heatr_temp = this->bsec_settings_.heater_temperature;
-        this->bme68x_heatr_conf_.heatr_dur = this->bsec_settings_.heater_duration;
+        this->bme68x_heatr_conf_.heatr_temp_prof = this->bsec_settings_.heater_temperature_profile;
+        this->bme68x_heatr_conf_.heatr_dur_prof = this->bsec_settings_.heater_duration_profile;
+        this->bme68x_heatr_conf_.profile_len = this->bsec_settings_.heater_profile_len;
+        this->bme68x_heatr_conf_.shared_heatr_dur =
+            BSEC_TOTAL_HEAT_DUR -
+            (bme68x_get_meas_dur(BME68X_PARALLEL_MODE, &bme68x_conf, &this->bme68x_) / INT64_C(1000));
 
-        status = bme68x_set_op_mode(this->bsec_settings_.op_mode, &this->bme68x_);
-        status = bme68x_set_heatr_conf(BME68X_FORCED_MODE, &this->bme68x_heatr_conf_, &this->bme68x_);
-        status = bme68x_set_op_mode(BME68X_FORCED_MODE, &this->bme68x_);
-        this->op_mode_ = BME68X_FORCED_MODE;
-        this->sleep_mode_ = false;
-        ESP_LOGV(TAG, "Using forced mode");
+        status = bme68x_set_heatr_conf(BME68X_PARALLEL_MODE, &this->bme68x_heatr_conf_, &this->bme68x_);
 
-        break;
-      case BME68X_PARALLEL_MODE:
-        if (this->op_mode_ != this->bsec_settings_.op_mode) {
-          this->bme68x_heatr_conf_.enable = BME68X_ENABLE;
-          this->bme68x_heatr_conf_.heatr_temp_prof = this->bsec_settings_.heater_temperature_profile;
-          this->bme68x_heatr_conf_.heatr_dur_prof = this->bsec_settings_.heater_duration_profile;
-          this->bme68x_heatr_conf_.profile_len = this->bsec_settings_.heater_profile_len;
-          this->bme68x_heatr_conf_.shared_heatr_dur =
-              BSEC_TOTAL_HEAT_DUR -
-              (bme68x_get_meas_dur(BME68X_PARALLEL_MODE, &bme68x_conf, &this->bme68x_) / INT64_C(1000));
-
-          status = bme68x_set_heatr_conf(BME68X_PARALLEL_MODE, &this->bme68x_heatr_conf_, &this->bme68x_);
-
-          status = bme68x_set_op_mode(BME68X_PARALLEL_MODE, &this->bme68x_);
-          this->op_mode_ = BME68X_PARALLEL_MODE;
-          this->sleep_mode_ = false;
-          ESP_LOGV(TAG, "Using parallel mode");
-        }
-        break;
-      case BME68X_SLEEP_MODE:
-        if (!this->sleep_mode_) {
-          bme68x_set_op_mode(BME68X_SLEEP_MODE, &this->bme68x_);
-          this->sleep_mode_ = true;
-          ESP_LOGV(TAG, "Using sleep mode");
-        }
-        break;
-    }
+        status = bme68x_set_op_mode(BME68X_PARALLEL_MODE, &this->bme68x_);
+        this->op_mode_ = BME68X_PARALLEL_MODE;
+        ESP_LOGV(TAG, "Using parallel mode");
+      }
+      break;
+    case BME68X_SLEEP_MODE:
+      if (this->op_mode_ != this->bsec_settings_.op_mode) {
+        bme68x_set_op_mode(BME68X_SLEEP_MODE, &this->bme68x_);
+        this->op_mode_ = BME68X_SLEEP_MODE;
+        ESP_LOGV(TAG, "Using sleep mode");
+      }
+      break;
+  }
 
+  if (this->bsec_settings_.trigger_measurement && this->bsec_settings_.op_mode != BME68X_SLEEP_MODE) {
     uint32_t meas_dur = 0;
     meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_);
     ESP_LOGV(TAG, "Queueing read in %uus", meas_dur);
diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.h b/esphome/components/bme68x_bsec2/bme68x_bsec2.h
index 7b9db2b7bf..86d3e5dfbf 100644
--- a/esphome/components/bme68x_bsec2/bme68x_bsec2.h
+++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.h
@@ -113,13 +113,11 @@ class BME68xBSEC2Component : public Component {
 
   struct bme68x_heatr_conf bme68x_heatr_conf_;
   uint8_t op_mode_;  // operating mode of sensor
-  bool sleep_mode_;
   bsec_library_return_t bsec_status_{BSEC_OK};
   int8_t bme68x_status_{BME68X_OK};
 
   int64_t last_time_ms_{0};
   uint32_t millis_overflow_counter_{0};
-  int64_t next_call_ns_{0};
 
   std::queue<std::function<void()>> queue_;
 

From a0159a274662b22a3c791961742cdf019894f041 Mon Sep 17 00:00:00 2001
From: Kevin Ahrendt <kevin.ahrendt@nabucasa.com>
Date: Wed, 13 Nov 2024 12:39:02 -0500
Subject: [PATCH 111/282] [i2s_audio] Bugfix: Adjust I2S speaker setup priority
 (#7759)

---
 .../i2s_audio/speaker/i2s_audio_speaker.cpp           | 11 +----------
 .../components/i2s_audio/speaker/i2s_audio_speaker.h  |  2 +-
 2 files changed, 2 insertions(+), 11 deletions(-)

diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
index c3f4566411..53b3cc8dc0 100644
--- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
+++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
@@ -99,14 +99,6 @@ void I2SAudioSpeaker::setup() {
     this->mark_failed();
     return;
   }
-
-  this->i2s_event_queue_ = xQueueCreate(I2S_EVENT_QUEUE_COUNT, sizeof(i2s_event_t));
-
-  if (this->i2s_event_queue_ == nullptr) {
-    ESP_LOGE(TAG, "Failed to create I2S event queue");
-    this->mark_failed();
-    return;
-  }
 }
 
 void I2SAudioSpeaker::loop() {
@@ -339,7 +331,7 @@ void I2SAudioSpeaker::speaker_task(void *params) {
 }
 
 void I2SAudioSpeaker::start() {
-  if (this->is_failed() || this->status_has_error())
+  if (!this->is_ready() || this->is_failed() || this->status_has_error())
     return;
   if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING))
     return;
@@ -519,7 +511,6 @@ void I2SAudioSpeaker::delete_task_(size_t buffer_size) {
   }
 
   xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPED);
-  xQueueReset(this->i2s_event_queue_);
 
   this->task_created_ = false;
   vTaskDelete(nullptr);
diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h
index 8b7386ba58..2b90f39399 100644
--- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h
+++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h
@@ -23,7 +23,7 @@ namespace i2s_audio {
 
 class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Component {
  public:
-  float get_setup_priority() const override { return esphome::setup_priority::LATE; }
+  float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; }
 
   void setup() override;
   void loop() override;

From 15bfc4c91f5c567dc89fd3daced0db0339ee85ff Mon Sep 17 00:00:00 2001
From: Roving Ronin <108674933+Roving-Ronin@users.noreply.github.com>
Date: Thu, 14 Nov 2024 11:43:21 +1100
Subject: [PATCH 112/282] Update UNIT_VOLT_AMPS_REACTIVE = "var" (Currently
 'VAR') (#7643)

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

diff --git a/esphome/const.py b/esphome/const.py
index c3c8712677..bb0100d348 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1095,7 +1095,7 @@ UNIT_STEPS = "steps"
 UNIT_VOLT = "V"
 UNIT_VOLT_AMPS = "VA"
 UNIT_VOLT_AMPS_HOURS = "VAh"
-UNIT_VOLT_AMPS_REACTIVE = "VAR"
+UNIT_VOLT_AMPS_REACTIVE = "var"
 UNIT_VOLT_AMPS_REACTIVE_HOURS = "VARh"
 UNIT_WATT = "W"
 UNIT_WATT_HOURS = "Wh"

From 9bc7b74d0137aa351c906e0c38a913afa5835fee Mon Sep 17 00:00:00 2001
From: Felipe Santos <felipecassiors@gmail.com>
Date: Wed, 13 Nov 2024 21:44:18 -0300
Subject: [PATCH 113/282] Fix reactive power unit of measurement from VAR to
 var (#7757)

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

diff --git a/esphome/components/sdm_meter/sdm_meter.cpp b/esphome/components/sdm_meter/sdm_meter.cpp
index 9c35d306ad..18e06e2b04 100644
--- a/esphome/components/sdm_meter/sdm_meter.cpp
+++ b/esphome/components/sdm_meter/sdm_meter.cpp
@@ -38,7 +38,7 @@ void SDMMeter::on_modbus_data(const std::vector<uint8_t> &data) {
 
     ESP_LOGD(
         TAG,
-        "SDMMeter Phase %c: V=%.3f V, I=%.3f A, Active P=%.3f W, Apparent P=%.3f VA, Reactive P=%.3f VAR, PF=%.3f, "
+        "SDMMeter Phase %c: V=%.3f V, I=%.3f A, Active P=%.3f W, Apparent P=%.3f VA, Reactive P=%.3f var, PF=%.3f, "
         "PA=%.3f °",
         i + 'A', voltage, current, active_power, apparent_power, reactive_power, power_factor, phase_angle);
     if (phase.voltage_sensor_ != nullptr)

From 67a4e56fcfd7bfd8017c179c8004e6f0c67920d3 Mon Sep 17 00:00:00 2001
From: Jordan Zucker <jordan.zucker@gmail.com>
Date: Wed, 13 Nov 2024 16:57:09 -0800
Subject: [PATCH 114/282] Disable bluetooth proxy during update (#7695)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp | 3 +++
 tests/components/esp32_ble_tracker/common.yaml             | 7 +++++++
 2 files changed, 10 insertions(+)

diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
index 74b4b9aa89..b86d32ee61 100644
--- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
+++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
@@ -65,6 +65,9 @@ void ESP32BLETracker::setup() {
       [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
         if (state == ota::OTA_STARTED) {
           this->stop_scan();
+          for (auto *client : this->clients_) {
+            client->disconnect();
+          }
         }
       });
 #endif
diff --git a/tests/components/esp32_ble_tracker/common.yaml b/tests/components/esp32_ble_tracker/common.yaml
index ef23635c9e..018bbb42b3 100644
--- a/tests/components/esp32_ble_tracker/common.yaml
+++ b/tests/components/esp32_ble_tracker/common.yaml
@@ -39,3 +39,10 @@ esp32_ble_tracker:
     - then:
         - lambda: |-
              ESP_LOGD("ble_auto", "The scan has ended!");
+
+wifi:
+  ssid: MySSID
+  password: password1
+
+ota:
+  - platform: esphome

From 754352b4d7e717fa04615ff9ee86d43955ceaaa8 Mon Sep 17 00:00:00 2001
From: Fabio Bonelli <fbonelli@gmail.com>
Date: Thu, 14 Nov 2024 01:57:51 +0100
Subject: [PATCH 115/282] ld2420: fix typo in log message (#7758)

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

diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp
index e57fdbc84e..9d628cc14f 100644
--- a/esphome/components/ld2420/ld2420.cpp
+++ b/esphome/components/ld2420/ld2420.cpp
@@ -180,7 +180,7 @@ void LD2420Component::apply_config_action() {
 }
 
 void LD2420Component::factory_reset_action() {
-  ESP_LOGCONFIG(TAG, "Setiing factory defaults...");
+  ESP_LOGCONFIG(TAG, "Setting factory defaults...");
   if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) {
     ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections.");
     this->mark_failed();

From f4dc11477fb62014bc4397acdfbce61bfa57a90a Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Thu, 14 Nov 2024 14:21:43 +1300
Subject: [PATCH 116/282] Bump version to 2024.11.0b2

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

diff --git a/esphome/const.py b/esphome/const.py
index bb0100d348..51c8090e91 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,6 +1,6 @@
 """Constants used by esphome."""
 
-__version__ = "2024.11.0b1"
+__version__ = "2024.11.0b2"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 VALID_SUBSTITUTIONS_CHARACTERS = (

From b29c1194089f4770b7a3e9ab99b12ac890c3e2cb Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 15 Nov 2024 12:43:52 +0100
Subject: [PATCH 117/282] Bump codecov/codecov-action from 4 to 5 (#7771)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/workflows/ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 82a55d0e2a..f5af3ec9e9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -219,7 +219,7 @@ jobs:
           . venv/bin/activate
           pytest -vv --cov-report=xml --tb=native tests
       - name: Upload coverage to Codecov
-        uses: codecov/codecov-action@v4
+        uses: codecov/codecov-action@v5
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
 

From e81191ebd2583d44b4237db8b7dad3ed18877d0d Mon Sep 17 00:00:00 2001
From: pethans <38168907+pethans@users.noreply.github.com>
Date: Sun, 17 Nov 2024 10:47:29 -0800
Subject: [PATCH 118/282] TuyaFan control should use oscillation_type (#7776)

Co-authored-by: Peter Hanson <phanson@whistler.lan>
---
 esphome/components/tuya/fan/tuya_fan.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp
index 8a613d0bae..9b132e0de6 100644
--- a/esphome/components/tuya/fan/tuya_fan.cpp
+++ b/esphome/components/tuya/fan/tuya_fan.cpp
@@ -86,7 +86,7 @@ void TuyaFan::control(const fan::FanCall &call) {
   if (this->oscillation_id_.has_value() && call.get_oscillating().has_value()) {
     if (this->oscillation_type_ == TuyaDatapointType::ENUM) {
       this->parent_->set_enum_datapoint_value(*this->oscillation_id_, *call.get_oscillating());
-    } else if (this->speed_type_ == TuyaDatapointType::BOOLEAN) {
+    } else if (this->oscillation_type_ == TuyaDatapointType::BOOLEAN) {
       this->parent_->set_boolean_datapoint_value(*this->oscillation_id_, *call.get_oscillating());
     }
   }

From 6e41c22e9d7f777a5c556725b1a54cf33a669e39 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Mon, 18 Nov 2024 20:44:39 +1300
Subject: [PATCH 119/282] Bump esphome-dashboard to 20241118.0 (#7782)

---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index e11e629743..4bea8cf4ef 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,7 +12,7 @@ pyserial==3.5
 platformio==6.1.16  # When updating platformio, also update Dockerfile
 esptool==4.7.0
 click==8.1.7
-esphome-dashboard==20241025.0
+esphome-dashboard==20241118.0
 aioesphomeapi==24.6.2
 zeroconf==0.132.2
 puremagic==1.27

From 50aeefc66299f675f62a9eb09a77ceded7c61950 Mon Sep 17 00:00:00 2001
From: pethans <38168907+pethans@users.noreply.github.com>
Date: Sun, 17 Nov 2024 10:47:29 -0800
Subject: [PATCH 120/282] TuyaFan control should use oscillation_type (#7776)

Co-authored-by: Peter Hanson <phanson@whistler.lan>
---
 esphome/components/tuya/fan/tuya_fan.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp
index 8a613d0bae..9b132e0de6 100644
--- a/esphome/components/tuya/fan/tuya_fan.cpp
+++ b/esphome/components/tuya/fan/tuya_fan.cpp
@@ -86,7 +86,7 @@ void TuyaFan::control(const fan::FanCall &call) {
   if (this->oscillation_id_.has_value() && call.get_oscillating().has_value()) {
     if (this->oscillation_type_ == TuyaDatapointType::ENUM) {
       this->parent_->set_enum_datapoint_value(*this->oscillation_id_, *call.get_oscillating());
-    } else if (this->speed_type_ == TuyaDatapointType::BOOLEAN) {
+    } else if (this->oscillation_type_ == TuyaDatapointType::BOOLEAN) {
       this->parent_->set_boolean_datapoint_value(*this->oscillation_id_, *call.get_oscillating());
     }
   }

From 585586780bf2fe6645eff64ed9528da8bd57e042 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Mon, 18 Nov 2024 20:44:39 +1300
Subject: [PATCH 121/282] Bump esphome-dashboard to 20241118.0 (#7782)

---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index e11e629743..4bea8cf4ef 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,7 +12,7 @@ pyserial==3.5
 platformio==6.1.16  # When updating platformio, also update Dockerfile
 esptool==4.7.0
 click==8.1.7
-esphome-dashboard==20241025.0
+esphome-dashboard==20241118.0
 aioesphomeapi==24.6.2
 zeroconf==0.132.2
 puremagic==1.27

From 1ed27b7cc0556679344f8fbe5101f1e338cc21a0 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Tue, 19 Nov 2024 09:04:30 +1300
Subject: [PATCH 122/282] Bump version to 2024.11.0b3

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

diff --git a/esphome/const.py b/esphome/const.py
index 51c8090e91..e7edd8337e 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,6 +1,6 @@
 """Constants used by esphome."""
 
-__version__ = "2024.11.0b2"
+__version__ = "2024.11.0b3"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 VALID_SUBSTITUTIONS_CHARACTERS = (

From 49e9c4333979bfeae77f25ee32c3065bc610b7a5 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Wed, 20 Nov 2024 13:54:19 +1300
Subject: [PATCH 123/282] [http_request] Feed watchdog timeout around http
 request functions (#7786)

---
 esphome/components/http_request/http_request_arduino.cpp | 2 ++
 esphome/components/http_request/http_request_idf.cpp     | 6 ++++++
 2 files changed, 8 insertions(+)

diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp
index af1eb6f459..85a1312aaa 100644
--- a/esphome/components/http_request/http_request_arduino.cpp
+++ b/esphome/components/http_request/http_request_arduino.cpp
@@ -104,7 +104,9 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::start(std::string url, std::s
   static const size_t HEADER_COUNT = sizeof(header_keys) / sizeof(header_keys[0]);
   container->client_.collectHeaders(header_keys, HEADER_COUNT);
 
+  App.feed_wdt();
   container->status_code = container->client_.sendRequest(method.c_str(), body.c_str());
+  App.feed_wdt();
   if (container->status_code < 0) {
     ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url.c_str(),
              HTTPClient::errorToString(container->status_code).c_str());
diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp
index c6c567b620..b449f046ee 100644
--- a/esphome/components/http_request/http_request_idf.cpp
+++ b/esphome/components/http_request/http_request_idf.cpp
@@ -117,8 +117,11 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
     return nullptr;
   }
 
+  App.feed_wdt();
   container->content_length = esp_http_client_fetch_headers(client);
+  App.feed_wdt();
   container->status_code = esp_http_client_get_status_code(client);
+  App.feed_wdt();
   if (is_success(container->status_code)) {
     container->duration_ms = millis() - start;
     return container;
@@ -148,8 +151,11 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
         return nullptr;
       }
 
+      App.feed_wdt();
       container->content_length = esp_http_client_fetch_headers(client);
+      App.feed_wdt();
       container->status_code = esp_http_client_get_status_code(client);
+      App.feed_wdt();
       if (is_success(container->status_code)) {
         container->duration_ms = millis() - start;
         return container;

From cf63d627fee4224f3652a0d101fae4cad1039da4 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Wed, 20 Nov 2024 17:39:28 +1300
Subject: [PATCH 124/282] Bump esphome-dashboard to 20241120.0 (#7787)

---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 4bea8cf4ef..7bc1c895df 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,7 +12,7 @@ pyserial==3.5
 platformio==6.1.16  # When updating platformio, also update Dockerfile
 esptool==4.7.0
 click==8.1.7
-esphome-dashboard==20241118.0
+esphome-dashboard==20241120.0
 aioesphomeapi==24.6.2
 zeroconf==0.132.2
 puremagic==1.27

From eb8a2326ad2a9f32c52c94b52fbb3dfb4090c6c2 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Wed, 20 Nov 2024 13:54:19 +1300
Subject: [PATCH 125/282] [http_request] Feed watchdog timeout around http
 request functions (#7786)

---
 esphome/components/http_request/http_request_arduino.cpp | 2 ++
 esphome/components/http_request/http_request_idf.cpp     | 6 ++++++
 2 files changed, 8 insertions(+)

diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp
index af1eb6f459..85a1312aaa 100644
--- a/esphome/components/http_request/http_request_arduino.cpp
+++ b/esphome/components/http_request/http_request_arduino.cpp
@@ -104,7 +104,9 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::start(std::string url, std::s
   static const size_t HEADER_COUNT = sizeof(header_keys) / sizeof(header_keys[0]);
   container->client_.collectHeaders(header_keys, HEADER_COUNT);
 
+  App.feed_wdt();
   container->status_code = container->client_.sendRequest(method.c_str(), body.c_str());
+  App.feed_wdt();
   if (container->status_code < 0) {
     ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url.c_str(),
              HTTPClient::errorToString(container->status_code).c_str());
diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp
index c6c567b620..b449f046ee 100644
--- a/esphome/components/http_request/http_request_idf.cpp
+++ b/esphome/components/http_request/http_request_idf.cpp
@@ -117,8 +117,11 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
     return nullptr;
   }
 
+  App.feed_wdt();
   container->content_length = esp_http_client_fetch_headers(client);
+  App.feed_wdt();
   container->status_code = esp_http_client_get_status_code(client);
+  App.feed_wdt();
   if (is_success(container->status_code)) {
     container->duration_ms = millis() - start;
     return container;
@@ -148,8 +151,11 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
         return nullptr;
       }
 
+      App.feed_wdt();
       container->content_length = esp_http_client_fetch_headers(client);
+      App.feed_wdt();
       container->status_code = esp_http_client_get_status_code(client);
+      App.feed_wdt();
       if (is_success(container->status_code)) {
         container->duration_ms = millis() - start;
         return container;

From 872b8ee753ef5799894289ac941392383145d1d7 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Wed, 20 Nov 2024 17:39:28 +1300
Subject: [PATCH 126/282] Bump esphome-dashboard to 20241120.0 (#7787)

---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 4bea8cf4ef..7bc1c895df 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,7 +12,7 @@ pyserial==3.5
 platformio==6.1.16  # When updating platformio, also update Dockerfile
 esptool==4.7.0
 click==8.1.7
-esphome-dashboard==20241118.0
+esphome-dashboard==20241120.0
 aioesphomeapi==24.6.2
 zeroconf==0.132.2
 puremagic==1.27

From ae46dcef7e51eb148dd0858834b02dc855979a1b Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Wed, 20 Nov 2024 17:50:30 +1300
Subject: [PATCH 127/282] Bump version to 2024.11.0b4

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

diff --git a/esphome/const.py b/esphome/const.py
index e7edd8337e..659695465e 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,6 +1,6 @@
 """Constants used by esphome."""
 
-__version__ = "2024.11.0b3"
+__version__ = "2024.11.0b4"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 VALID_SUBSTITUTIONS_CHARACTERS = (

From ef78c404dd19bb35af412a9dab0bd8e294cd7469 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Wed, 20 Nov 2024 21:29:42 +1300
Subject: [PATCH 128/282] Bump version to 2024.11.0

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

diff --git a/esphome/const.py b/esphome/const.py
index 659695465e..408dc52869 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,6 +1,6 @@
 """Constants used by esphome."""
 
-__version__ = "2024.11.0b4"
+__version__ = "2024.11.0"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 VALID_SUBSTITUTIONS_CHARACTERS = (

From 372d68a177196d6efb688b9f15943c5fd05dd202 Mon Sep 17 00:00:00 2001
From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Date: Wed, 20 Nov 2024 13:27:23 -0500
Subject: [PATCH 129/282] [remote_base] Fix extra comma in dump raw (#7774)

Co-authored-by: Jonathan Swoboda <jonathan.swoboda>
---
 esphome/components/remote_base/raw_protocol.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/remote_base/raw_protocol.cpp b/esphome/components/remote_base/raw_protocol.cpp
index bdeb935dc4..ef0cb8454e 100644
--- a/esphome/components/remote_base/raw_protocol.cpp
+++ b/esphome/components/remote_base/raw_protocol.cpp
@@ -28,7 +28,7 @@ bool RawDumper::dump(RemoteReceiveData src) {
       ESP_LOGI(TAG, "%s", buffer);
       buffer_offset = 0;
       written = sprintf(buffer, "  ");
-      if (i + 1 < src.size()) {
+      if (i + 1 < src.size() - 1) {
         written += sprintf(buffer + written, "%" PRId32 ", ", value);
       } else {
         written += sprintf(buffer + written, "%" PRId32, value);

From 846b091aacbe72078996cd0c89419a40c32cb1f8 Mon Sep 17 00:00:00 2001
From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com>
Date: Wed, 20 Nov 2024 19:28:21 +0100
Subject: [PATCH 130/282] [nextion] New trigger `on_buffer_overflow` (#7772)

---
 esphome/components/nextion/automation.h         |  7 +++++++
 esphome/components/nextion/base_component.py    |  1 +
 esphome/components/nextion/display.py           | 15 +++++++++++++++
 esphome/components/nextion/nextion.cpp          |  8 +++++++-
 esphome/components/nextion/nextion.h            |  7 +++++++
 tests/components/nextion/test.esp32-ard.yaml    |  3 +++
 tests/components/nextion/test.esp32-c3-ard.yaml |  3 +++
 tests/components/nextion/test.esp32-c3-idf.yaml |  3 +++
 tests/components/nextion/test.esp32-idf.yaml    |  3 +++
 tests/components/nextion/test.esp8266-ard.yaml  |  3 +++
 tests/components/nextion/test.rp2040-ard.yaml   |  3 +++
 11 files changed, 55 insertions(+), 1 deletion(-)

diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h
index f51fe6b4f8..5182e07229 100644
--- a/esphome/components/nextion/automation.h
+++ b/esphome/components/nextion/automation.h
@@ -42,5 +42,12 @@ class TouchTrigger : public Trigger<uint8_t, uint8_t, bool> {
   }
 };
 
+class BufferOverflowTrigger : public Trigger<> {
+ public:
+  explicit BufferOverflowTrigger(Nextion *nextion) {
+    nextion->add_buffer_overflow_event_callback([this]() { this->trigger(); });
+  }
+};
+
 }  // namespace nextion
 }  // namespace esphome
diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py
index 2924f66d3c..9708379861 100644
--- a/esphome/components/nextion/base_component.py
+++ b/esphome/components/nextion/base_component.py
@@ -18,6 +18,7 @@ CONF_ON_SLEEP = "on_sleep"
 CONF_ON_WAKE = "on_wake"
 CONF_ON_SETUP = "on_setup"
 CONF_ON_PAGE = "on_page"
+CONF_ON_BUFFER_OVERFLOW = "on_buffer_overflow"
 CONF_TOUCH_SLEEP_TIMEOUT = "touch_sleep_timeout"
 CONF_WAKE_UP_PAGE = "wake_up_page"
 CONF_START_UP_PAGE = "start_up_page"
diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py
index e403ba7ae8..6f284376af 100644
--- a/esphome/components/nextion/display.py
+++ b/esphome/components/nextion/display.py
@@ -13,6 +13,7 @@ from esphome.const import (
 from esphome.core import CORE
 from . import Nextion, nextion_ns, nextion_ref
 from .base_component import (
+    CONF_ON_BUFFER_OVERFLOW,
     CONF_ON_SLEEP,
     CONF_ON_WAKE,
     CONF_ON_SETUP,
@@ -36,6 +37,9 @@ SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template())
 WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template())
 PageTrigger = nextion_ns.class_("PageTrigger", automation.Trigger.template())
 TouchTrigger = nextion_ns.class_("TouchTrigger", automation.Trigger.template())
+BufferOverflowTrigger = nextion_ns.class_(
+    "BufferOverflowTrigger", automation.Trigger.template()
+)
 
 CONFIG_SCHEMA = (
     display.BASIC_DISPLAY_SCHEMA.extend(
@@ -68,6 +72,13 @@ CONFIG_SCHEMA = (
                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TouchTrigger),
                 }
             ),
+            cv.Optional(CONF_ON_BUFFER_OVERFLOW): automation.validate_automation(
+                {
+                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
+                        BufferOverflowTrigger
+                    ),
+                }
+            ),
             cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.int_range(min=3, max=65535),
             cv.Optional(CONF_WAKE_UP_PAGE): cv.uint8_t,
             cv.Optional(CONF_START_UP_PAGE): cv.uint8_t,
@@ -151,3 +162,7 @@ async def to_code(config):
             ],
             conf,
         )
+
+    for conf in config.get(CONF_ON_BUFFER_OVERFLOW, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        await automation.build_automation(trigger, [], conf)
diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp
index a80f6efc91..984db09c57 100644
--- a/esphome/components/nextion/nextion.cpp
+++ b/esphome/components/nextion/nextion.cpp
@@ -190,6 +190,10 @@ void Nextion::add_touch_event_callback(std::function<void(uint8_t, uint8_t, bool
   this->touch_callback_.add(std::move(callback));
 }
 
+void Nextion::add_buffer_overflow_event_callback(std::function<void()> &&callback) {
+  this->buffer_overflow_callback_.add(std::move(callback));
+}
+
 void Nextion::update_all_components() {
   if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping())
     return;
@@ -458,7 +462,9 @@ void Nextion::process_nextion_commands_() {
         this->remove_from_q_();
         break;
       case 0x24:  //  Serial Buffer overflow occurs
-        ESP_LOGW(TAG, "Nextion reported Serial Buffer overflow!");
+        // Buffer will continue to receive the current instruction, all previous instructions are lost.
+        ESP_LOGE(TAG, "Nextion reported Serial Buffer overflow!");
+        this->buffer_overflow_callback_.call();
         break;
       case 0x65: {  // touch event return data
         if (to_process_length != 3) {
diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h
index 732ee9b455..f539c79718 100644
--- a/esphome/components/nextion/nextion.h
+++ b/esphome/components/nextion/nextion.h
@@ -1134,6 +1134,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
    */
   void add_touch_event_callback(std::function<void(uint8_t, uint8_t, bool)> &&callback);
 
+  /** Add a callback to be notified when the nextion reports a buffer overflow.
+   *
+   * @param callback The void() callback.
+   */
+  void add_buffer_overflow_event_callback(std::function<void()> &&callback);
+
   void update_all_components();
 
   /**
@@ -1323,6 +1329,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
   CallbackManager<void()> wake_callback_{};
   CallbackManager<void(uint8_t)> page_callback_{};
   CallbackManager<void(uint8_t, uint8_t, bool)> touch_callback_{};
+  CallbackManager<void()> buffer_overflow_callback_{};
 
   optional<nextion_writer_t> writer_;
   float brightness_{1.0};
diff --git a/tests/components/nextion/test.esp32-ard.yaml b/tests/components/nextion/test.esp32-ard.yaml
index 27568ebc2a..ba76236fc6 100644
--- a/tests/components/nextion/test.esp32-ard.yaml
+++ b/tests/components/nextion/test.esp32-ard.yaml
@@ -58,3 +58,6 @@ display:
     on_page:
       then:
         lambda: 'ESP_LOGD("display","Display shows new page %u", x);'
+    on_buffer_overflow:
+      then:
+        logger.log: "Nextion reported a buffer overflow!"
diff --git a/tests/components/nextion/test.esp32-c3-ard.yaml b/tests/components/nextion/test.esp32-c3-ard.yaml
index 5881d6e165..5d253268f8 100644
--- a/tests/components/nextion/test.esp32-c3-ard.yaml
+++ b/tests/components/nextion/test.esp32-c3-ard.yaml
@@ -58,3 +58,6 @@ display:
     on_page:
       then:
         lambda: 'ESP_LOGD("display","Display shows new page %u", x);'
+    on_buffer_overflow:
+      then:
+        logger.log: "Nextion reported a buffer overflow!"
diff --git a/tests/components/nextion/test.esp32-c3-idf.yaml b/tests/components/nextion/test.esp32-c3-idf.yaml
index 5881d6e165..5d253268f8 100644
--- a/tests/components/nextion/test.esp32-c3-idf.yaml
+++ b/tests/components/nextion/test.esp32-c3-idf.yaml
@@ -58,3 +58,6 @@ display:
     on_page:
       then:
         lambda: 'ESP_LOGD("display","Display shows new page %u", x);'
+    on_buffer_overflow:
+      then:
+        logger.log: "Nextion reported a buffer overflow!"
diff --git a/tests/components/nextion/test.esp32-idf.yaml b/tests/components/nextion/test.esp32-idf.yaml
index 27568ebc2a..ba76236fc6 100644
--- a/tests/components/nextion/test.esp32-idf.yaml
+++ b/tests/components/nextion/test.esp32-idf.yaml
@@ -58,3 +58,6 @@ display:
     on_page:
       then:
         lambda: 'ESP_LOGD("display","Display shows new page %u", x);'
+    on_buffer_overflow:
+      then:
+        logger.log: "Nextion reported a buffer overflow!"
diff --git a/tests/components/nextion/test.esp8266-ard.yaml b/tests/components/nextion/test.esp8266-ard.yaml
index 5881d6e165..5d253268f8 100644
--- a/tests/components/nextion/test.esp8266-ard.yaml
+++ b/tests/components/nextion/test.esp8266-ard.yaml
@@ -58,3 +58,6 @@ display:
     on_page:
       then:
         lambda: 'ESP_LOGD("display","Display shows new page %u", x);'
+    on_buffer_overflow:
+      then:
+        logger.log: "Nextion reported a buffer overflow!"
diff --git a/tests/components/nextion/test.rp2040-ard.yaml b/tests/components/nextion/test.rp2040-ard.yaml
index a1c5848ce6..9b04433095 100644
--- a/tests/components/nextion/test.rp2040-ard.yaml
+++ b/tests/components/nextion/test.rp2040-ard.yaml
@@ -53,3 +53,6 @@ display:
     on_page:
       then:
         lambda: 'ESP_LOGD("display","Display shows new page %u", x);'
+    on_buffer_overflow:
+      then:
+        logger.log: "Nextion reported a buffer overflow!"

From 5e27a8df1f5fc3978939521d421d6330d1296128 Mon Sep 17 00:00:00 2001
From: Kjell Braden <afflux@pentabarf.de>
Date: Wed, 20 Nov 2024 19:29:48 +0100
Subject: [PATCH 131/282] enable rp2040 for online_image (#7769)

---
 esphome/components/online_image/__init__.py   |  1 +
 .../online_image/common-rp2040.yaml           | 19 +++++++++++++++++++
 .../online_image/test.rp2040-ard.yaml         |  4 ++++
 3 files changed, 24 insertions(+)
 create mode 100644 tests/components/online_image/common-rp2040.yaml
 create mode 100644 tests/components/online_image/test.rp2040-ard.yaml

diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py
index dfb10137aa..be1bfb4a00 100644
--- a/esphome/components/online_image/__init__.py
+++ b/esphome/components/online_image/__init__.py
@@ -98,6 +98,7 @@ CONFIG_SCHEMA = cv.Schema(
             # esp8266_arduino=cv.Version(2, 7, 0),
             esp32_arduino=cv.Version(0, 0, 0),
             esp_idf=cv.Version(4, 0, 0),
+            rp2040_arduino=cv.Version(0, 0, 0),
         ),
     )
 )
diff --git a/tests/components/online_image/common-rp2040.yaml b/tests/components/online_image/common-rp2040.yaml
new file mode 100644
index 0000000000..16bb2b2c44
--- /dev/null
+++ b/tests/components/online_image/common-rp2040.yaml
@@ -0,0 +1,19 @@
+<<: !include common.yaml
+
+spi:
+  - id: spi_main_lcd
+    clk_pin: 18
+    mosi_pin: 19
+    miso_pin: 16
+
+display:
+  - platform: ili9xxx
+    id: main_lcd
+    model: ili9342
+    cs_pin: 20
+    dc_pin: 17
+    reset_pin: 21
+    invert_colors: true
+    lambda: |-
+      it.fill(Color(0, 0, 0));
+      it.image(0, 0, id(online_rgba_image));
diff --git a/tests/components/online_image/test.rp2040-ard.yaml b/tests/components/online_image/test.rp2040-ard.yaml
new file mode 100644
index 0000000000..d10f36b4e9
--- /dev/null
+++ b/tests/components/online_image/test.rp2040-ard.yaml
@@ -0,0 +1,4 @@
+<<: !include common-rp2040.yaml
+
+http_request:
+  verify_ssl: false

From 6d4f787f67b6d4dc16fcdbdca3a1984db8d9df62 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Thu, 21 Nov 2024 11:10:28 +1100
Subject: [PATCH 132/282] [http_request] Fix within context with parameters.
 (Bugfix) (#7790)

---
 esphome/components/http_request/http_request.h | 2 +-
 tests/components/http_request/common.yaml      | 8 ++++++++
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h
index 4ed2c834f8..b2ce718ec4 100644
--- a/esphome/components/http_request/http_request.h
+++ b/esphome/components/http_request/http_request.h
@@ -189,7 +189,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
 
     if (container == nullptr) {
       for (auto *trigger : this->error_triggers_)
-        trigger->trigger(x...);
+        trigger->trigger();
       return;
     }
 
diff --git a/tests/components/http_request/common.yaml b/tests/components/http_request/common.yaml
index 593b85e435..8408f27a05 100644
--- a/tests/components/http_request/common.yaml
+++ b/tests/components/http_request/common.yaml
@@ -39,6 +39,14 @@ http_request:
   timeout: 10s
   verify_ssl: ${verify_ssl}
 
+script:
+  - id: does_not_compile
+    parameters:
+      api_url: string
+    then:
+      - http_request.get:
+          url: "http://google.com"
+
 ota:
   - platform: http_request
     on_begin:

From fbb9967117d248951906b0bb19ef200e7f546360 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Thu, 21 Nov 2024 19:22:02 +1300
Subject: [PATCH 133/282] [rtttl] Clamp gain between 0 and 1 (#7793)

---
 esphome/components/rtttl/rtttl.h | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h
index 10c290c5fb..420948bfbf 100644
--- a/esphome/components/rtttl/rtttl.h
+++ b/esphome/components/rtttl/rtttl.h
@@ -40,13 +40,7 @@ class Rtttl : public Component {
   void set_speaker(speaker::Speaker *speaker) { this->speaker_ = speaker; }
 #endif
   float get_gain() { return gain_; }
-  void set_gain(float gain) {
-    if (gain < 0.1f)
-      gain = 0.1f;
-    if (gain > 1.0f)
-      gain = 1.0f;
-    this->gain_ = gain;
-  }
+  void set_gain(float gain) { this->gain_ = clamp(gain, 0.0f, 1.0f); }
   void play(std::string rtttl);
   void stop();
   void dump_config() override;

From 6bcbbcce02ca9441cffe6d7f559eceb92ab76811 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Thu, 21 Nov 2024 21:10:20 +1300
Subject: [PATCH 134/282] [speaker] Add missing auto-load for ``audio`` (#7794)

---
 esphome/components/speaker/__init__.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py
index 7a668dc2f3..948fe4b534 100644
--- a/esphome/components/speaker/__init__.py
+++ b/esphome/components/speaker/__init__.py
@@ -7,6 +7,7 @@ from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME
 from esphome.core import CORE
 from esphome.coroutine import coroutine_with_priority
 
+AUTO_LOAD = ["audio"]
 CODEOWNERS = ["@jesserockz", "@kahrendt"]
 
 IS_PLATFORM_COMPONENT = True

From 03ae6b2c1b6e4ae2a509c109c2381214e0cdb131 Mon Sep 17 00:00:00 2001
From: Manuel Kasper <mk@neon1.net>
Date: Thu, 21 Nov 2024 10:46:49 +0100
Subject: [PATCH 135/282] [qspi_dbi] Fix garbled graphics on RM690B0 (#7795)

---
 esphome/components/qspi_dbi/models.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/esphome/components/qspi_dbi/models.py b/esphome/components/qspi_dbi/models.py
index 071ea72d73..cbd9c4663f 100644
--- a/esphome/components/qspi_dbi/models.py
+++ b/esphome/components/qspi_dbi/models.py
@@ -55,6 +55,7 @@ chip.cmd(PAGESEL, 0x00)
 chip.cmd(0xC2, 0x00)
 chip.delay(10)
 chip.cmd(TEON, 0x00)
+chip.cmd(PIXFMT, 0x55)
 
 chip = DriverChip("AXS15231")
 chip.cmd(0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xA5)

From ccf2854b612a1659c27cedeb22eaf49b871f157c Mon Sep 17 00:00:00 2001
From: Spencer Owen <owenspencer@gmail.com>
Date: Thu, 21 Nov 2024 12:24:10 -0700
Subject: [PATCH 136/282] Check for min_version earlier in validation (#7797)

---
 esphome/core/config.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/esphome/core/config.py b/esphome/core/config.py
index 8c130eb6db..367e61c413 100644
--- a/esphome/core/config.py
+++ b/esphome/core/config.py
@@ -184,6 +184,9 @@ PRELOAD_CONFIG_SCHEMA = cv.Schema(
         cv.Optional(CONF_ESP8266_RESTORE_FROM_FLASH): cv.valid,
         cv.Optional(CONF_BOARD_FLASH_MODE): cv.valid,
         cv.Optional(CONF_ARDUINO_VERSION): cv.valid,
+        cv.Optional(CONF_MIN_VERSION, default=ESPHOME_VERSION): cv.All(
+            cv.version_number, cv.validate_esphome_version
+        ),
     },
     extra=cv.ALLOW_EXTRA,
 )

From 3232866dc337b0e3332f9b7283d834f025e2a642 Mon Sep 17 00:00:00 2001
From: Alain Turbide <7193213+Dilbert66@users.noreply.github.com>
Date: Thu, 21 Nov 2024 15:39:32 -0500
Subject: [PATCH 137/282] Fix for OTA mode not activating in safe_mode when OTA
 section has an on_xxxx action (#7796)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/esphome/ota/__init__.py | 8 ++++----
 esphome/components/ota/__init__.py         | 1 +
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py
index a852d8d001..86006e3e18 100644
--- a/esphome/components/esphome/ota/__init__.py
+++ b/esphome/components/esphome/ota/__init__.py
@@ -1,10 +1,9 @@
 import logging
 
 import esphome.codegen as cg
-import esphome.config_validation as cv
-import esphome.final_validate as fv
-from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent
+from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
 from esphome.config_helpers import merge_config
+import esphome.config_validation as cv
 from esphome.const import (
     CONF_ESPHOME,
     CONF_ID,
@@ -18,6 +17,7 @@ from esphome.const import (
     CONF_VERSION,
 )
 from esphome.core import coroutine_with_priority
+import esphome.final_validate as fv
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -124,7 +124,6 @@ FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
 @coroutine_with_priority(52.0)
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    await ota_to_code(var, config)
     cg.add(var.set_port(config[CONF_PORT]))
     if CONF_PASSWORD in config:
         cg.add(var.set_auth_password(config[CONF_PASSWORD]))
@@ -132,3 +131,4 @@ async def to_code(config):
     cg.add_define("USE_OTA_VERSION", config[CONF_VERSION])
 
     await cg.register_component(var, config)
+    await ota_to_code(var, config)
diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py
index d9917a2aae..627c55e910 100644
--- a/esphome/components/ota/__init__.py
+++ b/esphome/components/ota/__init__.py
@@ -92,6 +92,7 @@ async def to_code(config):
 
 
 async def ota_to_code(var, config):
+    await cg.past_safe_mode()
     use_state_callback = False
     for conf in config.get(CONF_ON_STATE_CHANGE, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

From 122ff731ef43206212976da35354494e928639f6 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Thu, 21 Nov 2024 14:41:31 -0600
Subject: [PATCH 138/282] Ensure storage I/O for ignored devices runs in the
 executor (#7792)

---
 esphome/dashboard/core.py       | 2 +-
 esphome/dashboard/web_server.py | 5 +++--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/esphome/dashboard/core.py b/esphome/dashboard/core.py
index 563ca1506d..f53cb7ffb1 100644
--- a/esphome/dashboard/core.py
+++ b/esphome/dashboard/core.py
@@ -103,7 +103,7 @@ class ESPHomeDashboard:
         self.loop = asyncio.get_running_loop()
         self.ping_request = asyncio.Event()
         self.entries = DashboardEntries(self)
-        self.load_ignored_devices()
+        await self.loop.run_in_executor(None, self.load_ignored_devices)
 
     def load_ignored_devices(self) -> None:
         storage_path = Path(ignored_devices_storage_path())
diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py
index 07f7f019f8..0fed8e9c53 100644
--- a/esphome/dashboard/web_server.py
+++ b/esphome/dashboard/web_server.py
@@ -544,7 +544,7 @@ class ImportRequestHandler(BaseHandler):
 
 class IgnoreDeviceRequestHandler(BaseHandler):
     @authenticated
-    def post(self) -> None:
+    async def post(self) -> None:
         dashboard = DASHBOARD
         try:
             args = json.loads(self.request.body.decode())
@@ -576,7 +576,8 @@ class IgnoreDeviceRequestHandler(BaseHandler):
         else:
             dashboard.ignored_devices.discard(ignored_device.device_name)
 
-        dashboard.save_ignored_devices()
+        loop = asyncio.get_running_loop()
+        await loop.run_in_executor(None, dashboard.save_ignored_devices)
 
         self.set_status(204)
         self.finish()

From 888b2379647049712a94143074b95c79e1f8392b Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Thu, 21 Nov 2024 11:10:28 +1100
Subject: [PATCH 139/282] [http_request] Fix within context with parameters.
 (Bugfix) (#7790)

---
 esphome/components/http_request/http_request.h | 2 +-
 tests/components/http_request/common.yaml      | 8 ++++++++
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h
index 4ed2c834f8..b2ce718ec4 100644
--- a/esphome/components/http_request/http_request.h
+++ b/esphome/components/http_request/http_request.h
@@ -189,7 +189,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
 
     if (container == nullptr) {
       for (auto *trigger : this->error_triggers_)
-        trigger->trigger(x...);
+        trigger->trigger();
       return;
     }
 
diff --git a/tests/components/http_request/common.yaml b/tests/components/http_request/common.yaml
index 593b85e435..8408f27a05 100644
--- a/tests/components/http_request/common.yaml
+++ b/tests/components/http_request/common.yaml
@@ -39,6 +39,14 @@ http_request:
   timeout: 10s
   verify_ssl: ${verify_ssl}
 
+script:
+  - id: does_not_compile
+    parameters:
+      api_url: string
+    then:
+      - http_request.get:
+          url: "http://google.com"
+
 ota:
   - platform: http_request
     on_begin:

From a0693060e401153409a2095b0a29248bd23dee0e Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Thu, 21 Nov 2024 19:22:02 +1300
Subject: [PATCH 140/282] [rtttl] Clamp gain between 0 and 1 (#7793)

---
 esphome/components/rtttl/rtttl.h | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h
index 10c290c5fb..420948bfbf 100644
--- a/esphome/components/rtttl/rtttl.h
+++ b/esphome/components/rtttl/rtttl.h
@@ -40,13 +40,7 @@ class Rtttl : public Component {
   void set_speaker(speaker::Speaker *speaker) { this->speaker_ = speaker; }
 #endif
   float get_gain() { return gain_; }
-  void set_gain(float gain) {
-    if (gain < 0.1f)
-      gain = 0.1f;
-    if (gain > 1.0f)
-      gain = 1.0f;
-    this->gain_ = gain;
-  }
+  void set_gain(float gain) { this->gain_ = clamp(gain, 0.0f, 1.0f); }
   void play(std::string rtttl);
   void stop();
   void dump_config() override;

From f04e3de7b8d103a21cbe06f1b5c78e7a78be3429 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Thu, 21 Nov 2024 21:10:20 +1300
Subject: [PATCH 141/282] [speaker] Add missing auto-load for ``audio`` (#7794)

---
 esphome/components/speaker/__init__.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py
index 7a668dc2f3..948fe4b534 100644
--- a/esphome/components/speaker/__init__.py
+++ b/esphome/components/speaker/__init__.py
@@ -7,6 +7,7 @@ from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME
 from esphome.core import CORE
 from esphome.coroutine import coroutine_with_priority
 
+AUTO_LOAD = ["audio"]
 CODEOWNERS = ["@jesserockz", "@kahrendt"]
 
 IS_PLATFORM_COMPONENT = True

From 489d0d20d2279257574038ab798755242441c18b Mon Sep 17 00:00:00 2001
From: Manuel Kasper <mk@neon1.net>
Date: Thu, 21 Nov 2024 10:46:49 +0100
Subject: [PATCH 142/282] [qspi_dbi] Fix garbled graphics on RM690B0 (#7795)

---
 esphome/components/qspi_dbi/models.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/esphome/components/qspi_dbi/models.py b/esphome/components/qspi_dbi/models.py
index 071ea72d73..cbd9c4663f 100644
--- a/esphome/components/qspi_dbi/models.py
+++ b/esphome/components/qspi_dbi/models.py
@@ -55,6 +55,7 @@ chip.cmd(PAGESEL, 0x00)
 chip.cmd(0xC2, 0x00)
 chip.delay(10)
 chip.cmd(TEON, 0x00)
+chip.cmd(PIXFMT, 0x55)
 
 chip = DriverChip("AXS15231")
 chip.cmd(0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xA5)

From ea424b069979e5c53438f95abdd43acdc521cd78 Mon Sep 17 00:00:00 2001
From: Spencer Owen <owenspencer@gmail.com>
Date: Thu, 21 Nov 2024 12:24:10 -0700
Subject: [PATCH 143/282] Check for min_version earlier in validation (#7797)

---
 esphome/core/config.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/esphome/core/config.py b/esphome/core/config.py
index 8c130eb6db..367e61c413 100644
--- a/esphome/core/config.py
+++ b/esphome/core/config.py
@@ -184,6 +184,9 @@ PRELOAD_CONFIG_SCHEMA = cv.Schema(
         cv.Optional(CONF_ESP8266_RESTORE_FROM_FLASH): cv.valid,
         cv.Optional(CONF_BOARD_FLASH_MODE): cv.valid,
         cv.Optional(CONF_ARDUINO_VERSION): cv.valid,
+        cv.Optional(CONF_MIN_VERSION, default=ESPHOME_VERSION): cv.All(
+            cv.version_number, cv.validate_esphome_version
+        ),
     },
     extra=cv.ALLOW_EXTRA,
 )

From 1c1f3f7c55607c504226f7a1ed52794a0482b123 Mon Sep 17 00:00:00 2001
From: Alain Turbide <7193213+Dilbert66@users.noreply.github.com>
Date: Thu, 21 Nov 2024 15:39:32 -0500
Subject: [PATCH 144/282] Fix for OTA mode not activating in safe_mode when OTA
 section has an on_xxxx action (#7796)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/esphome/ota/__init__.py | 8 ++++----
 esphome/components/ota/__init__.py         | 1 +
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py
index a852d8d001..86006e3e18 100644
--- a/esphome/components/esphome/ota/__init__.py
+++ b/esphome/components/esphome/ota/__init__.py
@@ -1,10 +1,9 @@
 import logging
 
 import esphome.codegen as cg
-import esphome.config_validation as cv
-import esphome.final_validate as fv
-from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent
+from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
 from esphome.config_helpers import merge_config
+import esphome.config_validation as cv
 from esphome.const import (
     CONF_ESPHOME,
     CONF_ID,
@@ -18,6 +17,7 @@ from esphome.const import (
     CONF_VERSION,
 )
 from esphome.core import coroutine_with_priority
+import esphome.final_validate as fv
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -124,7 +124,6 @@ FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
 @coroutine_with_priority(52.0)
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    await ota_to_code(var, config)
     cg.add(var.set_port(config[CONF_PORT]))
     if CONF_PASSWORD in config:
         cg.add(var.set_auth_password(config[CONF_PASSWORD]))
@@ -132,3 +131,4 @@ async def to_code(config):
     cg.add_define("USE_OTA_VERSION", config[CONF_VERSION])
 
     await cg.register_component(var, config)
+    await ota_to_code(var, config)
diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py
index d9917a2aae..627c55e910 100644
--- a/esphome/components/ota/__init__.py
+++ b/esphome/components/ota/__init__.py
@@ -92,6 +92,7 @@ async def to_code(config):
 
 
 async def ota_to_code(var, config):
+    await cg.past_safe_mode()
     use_state_callback = False
     for conf in config.get(CONF_ON_STATE_CHANGE, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

From e51f3d9498d377ed45d15665e1c91f91d6a2aae0 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Thu, 21 Nov 2024 14:41:31 -0600
Subject: [PATCH 145/282] Ensure storage I/O for ignored devices runs in the
 executor (#7792)

---
 esphome/dashboard/core.py       | 2 +-
 esphome/dashboard/web_server.py | 5 +++--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/esphome/dashboard/core.py b/esphome/dashboard/core.py
index 563ca1506d..f53cb7ffb1 100644
--- a/esphome/dashboard/core.py
+++ b/esphome/dashboard/core.py
@@ -103,7 +103,7 @@ class ESPHomeDashboard:
         self.loop = asyncio.get_running_loop()
         self.ping_request = asyncio.Event()
         self.entries = DashboardEntries(self)
-        self.load_ignored_devices()
+        await self.loop.run_in_executor(None, self.load_ignored_devices)
 
     def load_ignored_devices(self) -> None:
         storage_path = Path(ignored_devices_storage_path())
diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py
index 07f7f019f8..0fed8e9c53 100644
--- a/esphome/dashboard/web_server.py
+++ b/esphome/dashboard/web_server.py
@@ -544,7 +544,7 @@ class ImportRequestHandler(BaseHandler):
 
 class IgnoreDeviceRequestHandler(BaseHandler):
     @authenticated
-    def post(self) -> None:
+    async def post(self) -> None:
         dashboard = DASHBOARD
         try:
             args = json.loads(self.request.body.decode())
@@ -576,7 +576,8 @@ class IgnoreDeviceRequestHandler(BaseHandler):
         else:
             dashboard.ignored_devices.discard(ignored_device.device_name)
 
-        dashboard.save_ignored_devices()
+        loop = asyncio.get_running_loop()
+        await loop.run_in_executor(None, dashboard.save_ignored_devices)
 
         self.set_status(204)
         self.finish()

From 2cc2a2153b2c219d1bd07a9fe427b16d38e2e6f7 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Fri, 22 Nov 2024 10:08:00 +1300
Subject: [PATCH 146/282] Bump version to 2024.11.1

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

diff --git a/esphome/const.py b/esphome/const.py
index 408dc52869..d14cdecb23 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,6 +1,6 @@
 """Constants used by esphome."""
 
-__version__ = "2024.11.0"
+__version__ = "2024.11.1"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 VALID_SUBSTITUTIONS_CHARACTERS = (

From dea297c8d7d73f0c4f366e58994cd6a9a3995e11 Mon Sep 17 00:00:00 2001
From: Petr Kejval <petr.kejval6@gmail.com>
Date: Sat, 23 Nov 2024 05:52:02 +0100
Subject: [PATCH 147/282] [nextion] Add publish actions (#7646)

Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
---
 esphome/components/nextion/__init__.py        |   2 +
 esphome/components/nextion/automation.h       |  75 ++++-
 .../nextion/binary_sensor/__init__.py         |  45 ++-
 esphome/components/nextion/sensor/__init__.py |  43 ++-
 esphome/components/nextion/switch/__init__.py |  40 ++-
 .../nextion/text_sensor/__init__.py           |  39 ++-
 tests/components/nextion/common.yaml          | 293 ++++++++++++++++++
 tests/components/nextion/test.esp32-ard.yaml  |  65 +---
 .../components/nextion/test.esp32-c3-ard.yaml |  65 +---
 .../components/nextion/test.esp32-c3-idf.yaml |  65 +---
 tests/components/nextion/test.esp32-idf.yaml  |  65 +---
 .../components/nextion/test.esp8266-ard.yaml  |  65 +---
 tests/components/nextion/test.rp2040-ard.yaml |  61 +---
 13 files changed, 558 insertions(+), 365 deletions(-)
 create mode 100644 tests/components/nextion/common.yaml

diff --git a/esphome/components/nextion/__init__.py b/esphome/components/nextion/__init__.py
index 924d58198d..fb75daf4ba 100644
--- a/esphome/components/nextion/__init__.py
+++ b/esphome/components/nextion/__init__.py
@@ -6,3 +6,5 @@ Nextion = nextion_ns.class_("Nextion", cg.PollingComponent, uart.UARTDevice)
 nextion_ref = Nextion.operator("ref")
 
 CONF_NEXTION_ID = "nextion_id"
+CONF_PUBLISH_STATE = "publish_state"
+CONF_SEND_TO_NEXTION = "send_to_nextion"
diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h
index 5182e07229..65f1fd0058 100644
--- a/esphome/components/nextion/automation.h
+++ b/esphome/components/nextion/automation.h
@@ -5,6 +5,13 @@
 namespace esphome {
 namespace nextion {
 
+class BufferOverflowTrigger : public Trigger<> {
+ public:
+  explicit BufferOverflowTrigger(Nextion *nextion) {
+    nextion->add_buffer_overflow_event_callback([this]() { this->trigger(); });
+  }
+};
+
 class SetupTrigger : public Trigger<> {
  public:
   explicit SetupTrigger(Nextion *nextion) {
@@ -42,11 +49,73 @@ class TouchTrigger : public Trigger<uint8_t, uint8_t, bool> {
   }
 };
 
-class BufferOverflowTrigger : public Trigger<> {
+template<typename... Ts> class NextionPublishFloatAction : public Action<Ts...> {
  public:
-  explicit BufferOverflowTrigger(Nextion *nextion) {
-    nextion->add_buffer_overflow_event_callback([this]() { this->trigger(); });
+  explicit NextionPublishFloatAction(NextionComponent *component) : component_(component) {}
+
+  TEMPLATABLE_VALUE(float, state)
+  TEMPLATABLE_VALUE(bool, publish_state)
+  TEMPLATABLE_VALUE(bool, send_to_nextion)
+
+  void play(Ts... x) override {
+    this->component_->set_state(this->state_.value(x...), this->publish_state_.value(x...),
+                                this->send_to_nextion_.value(x...));
   }
+
+  void set_state(std::function<void(Ts..., float)> state) { this->state_ = state; }
+  void set_publish_state(std::function<void(Ts..., bool)> publish_state) { this->publish_state_ = publish_state; }
+  void set_send_to_nextion(std::function<void(Ts..., bool)> send_to_nextion) {
+    this->send_to_nextion_ = send_to_nextion;
+  }
+
+ protected:
+  NextionComponent *component_;
+};
+
+template<typename... Ts> class NextionPublishTextAction : public Action<Ts...> {
+ public:
+  explicit NextionPublishTextAction(NextionComponent *component) : component_(component) {}
+
+  TEMPLATABLE_VALUE(const char *, state)
+  TEMPLATABLE_VALUE(bool, publish_state)
+  TEMPLATABLE_VALUE(bool, send_to_nextion)
+
+  void play(Ts... x) override {
+    this->component_->set_state(this->state_.value(x...), this->publish_state_.value(x...),
+                                this->send_to_nextion_.value(x...));
+  }
+
+  void set_state(std::function<void(Ts..., const char *)> state) { this->state_ = state; }
+  void set_publish_state(std::function<void(Ts..., bool)> publish_state) { this->publish_state_ = publish_state; }
+  void set_send_to_nextion(std::function<void(Ts..., bool)> send_to_nextion) {
+    this->send_to_nextion_ = send_to_nextion;
+  }
+
+ protected:
+  NextionComponent *component_;
+};
+
+template<typename... Ts> class NextionPublishBoolAction : public Action<Ts...> {
+ public:
+  explicit NextionPublishBoolAction(NextionComponent *component) : component_(component) {}
+
+  TEMPLATABLE_VALUE(bool, state)
+  TEMPLATABLE_VALUE(bool, publish_state)
+  TEMPLATABLE_VALUE(bool, send_to_nextion)
+
+  void play(Ts... x) override {
+    this->component_->set_state(this->state_.value(x...), this->publish_state_.value(x...),
+                                this->send_to_nextion_.value(x...));
+  }
+
+  void set_state(std::function<void(Ts..., bool)> state) { this->state_ = state; }
+  void set_publish_state(std::function<void(Ts..., bool)> publish_state) { this->publish_state_ = publish_state; }
+  void set_send_to_nextion(std::function<void(Ts..., bool)> send_to_nextion) {
+    this->send_to_nextion_ = send_to_nextion;
+  }
+
+ protected:
+  NextionComponent *component_;
 };
 
 }  // namespace nextion
diff --git a/esphome/components/nextion/binary_sensor/__init__.py b/esphome/components/nextion/binary_sensor/__init__.py
index 8b4a45cc60..a257587e13 100644
--- a/esphome/components/nextion/binary_sensor/__init__.py
+++ b/esphome/components/nextion/binary_sensor/__init__.py
@@ -1,9 +1,16 @@
+from esphome import automation
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import binary_sensor
 
-from esphome.const import CONF_COMPONENT_ID, CONF_PAGE_ID, CONF_ID
-from .. import nextion_ns, CONF_NEXTION_ID
+from esphome.const import (
+    CONF_ID,
+    CONF_STATE,
+    CONF_COMPONENT_ID,
+    CONF_PAGE_ID,
+)
+
+from .. import nextion_ns, CONF_NEXTION_ID, CONF_PUBLISH_STATE, CONF_SEND_TO_NEXTION
 
 
 from ..base_component import (
@@ -19,6 +26,10 @@ NextionBinarySensor = nextion_ns.class_(
     "NextionBinarySensor", binary_sensor.BinarySensor, cg.PollingComponent
 )
 
+NextionPublishBoolAction = nextion_ns.class_(
+    "NextionPublishBoolAction", automation.Action
+)
+
 CONFIG_SCHEMA = cv.All(
     binary_sensor.binary_sensor_schema(NextionBinarySensor)
     .extend(
@@ -52,3 +63,33 @@ async def to_code(config):
     if CONF_COMPONENT_NAME in config or CONF_VARIABLE_NAME in config:
         await setup_component_core_(var, config, ".val")
         cg.add(hub.register_binarysensor_component(var))
+
+
+@automation.register_action(
+    "binary_sensor.nextion.publish",
+    NextionPublishBoolAction,
+    cv.Schema(
+        {
+            cv.Required(CONF_ID): cv.use_id(NextionBinarySensor),
+            cv.Required(CONF_STATE): cv.templatable(cv.boolean),
+            cv.Optional(CONF_PUBLISH_STATE, default="true"): cv.templatable(cv.boolean),
+            cv.Optional(CONF_SEND_TO_NEXTION, default="true"): cv.templatable(
+                cv.boolean
+            ),
+        }
+    ),
+)
+async def sensor_nextion_publish_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    var = cg.new_Pvariable(action_id, template_arg, paren)
+
+    template_ = await cg.templatable(config[CONF_STATE], args, bool)
+    cg.add(var.set_state(template_))
+
+    template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, bool)
+    cg.add(var.set_publish_state(template_))
+
+    template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, bool)
+    cg.add(var.set_send_to_nextion(template_))
+
+    return var
diff --git a/esphome/components/nextion/sensor/__init__.py b/esphome/components/nextion/sensor/__init__.py
index eefbe34d58..1058c2a04b 100644
--- a/esphome/components/nextion/sensor/__init__.py
+++ b/esphome/components/nextion/sensor/__init__.py
@@ -1,12 +1,11 @@
+from esphome import automation
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import sensor
 
-from esphome.const import (
-    CONF_ID,
-    CONF_COMPONENT_ID,
-)
-from .. import nextion_ns, CONF_NEXTION_ID
+from esphome.const import CONF_ID, CONF_COMPONENT_ID, CONF_STATE
+
+from .. import nextion_ns, CONF_NEXTION_ID, CONF_PUBLISH_STATE, CONF_SEND_TO_NEXTION
 
 from ..base_component import (
     setup_component_core_,
@@ -25,6 +24,10 @@ CODEOWNERS = ["@senexcrenshaw"]
 
 NextionSensor = nextion_ns.class_("NextionSensor", sensor.Sensor, cg.PollingComponent)
 
+NextionPublishFloatAction = nextion_ns.class_(
+    "NextionPublishFloatAction", automation.Action
+)
+
 
 def CheckWaveID(value):
     value = cv.int_(value)
@@ -95,3 +98,33 @@ async def to_code(config):
 
     if CONF_WAVE_MAX_LENGTH in config:
         cg.add(var.set_wave_max_length(config[CONF_WAVE_MAX_LENGTH]))
+
+
+@automation.register_action(
+    "sensor.nextion.publish",
+    NextionPublishFloatAction,
+    cv.Schema(
+        {
+            cv.Required(CONF_ID): cv.use_id(NextionSensor),
+            cv.Required(CONF_STATE): cv.templatable(cv.float_),
+            cv.Optional(CONF_PUBLISH_STATE, default="true"): cv.templatable(cv.boolean),
+            cv.Optional(CONF_SEND_TO_NEXTION, default="true"): cv.templatable(
+                cv.boolean
+            ),
+        }
+    ),
+)
+async def sensor_nextion_publish_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    var = cg.new_Pvariable(action_id, template_arg, paren)
+
+    template_ = await cg.templatable(config[CONF_STATE], args, float)
+    cg.add(var.set_state(template_))
+
+    template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, bool)
+    cg.add(var.set_publish_state(template_))
+
+    template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, bool)
+    cg.add(var.set_send_to_nextion(template_))
+
+    return var
diff --git a/esphome/components/nextion/switch/__init__.py b/esphome/components/nextion/switch/__init__.py
index 91ab0cc81f..de1a061478 100644
--- a/esphome/components/nextion/switch/__init__.py
+++ b/esphome/components/nextion/switch/__init__.py
@@ -1,9 +1,11 @@
+from esphome import automation
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import switch
 
-from esphome.const import CONF_ID
-from .. import nextion_ns, CONF_NEXTION_ID
+from esphome.const import CONF_ID, CONF_STATE
+
+from .. import nextion_ns, CONF_NEXTION_ID, CONF_PUBLISH_STATE, CONF_SEND_TO_NEXTION
 
 from ..base_component import (
     setup_component_core_,
@@ -16,6 +18,10 @@ CODEOWNERS = ["@senexcrenshaw"]
 
 NextionSwitch = nextion_ns.class_("NextionSwitch", switch.Switch, cg.PollingComponent)
 
+NextionPublishBoolAction = nextion_ns.class_(
+    "NextionPublishBoolAction", automation.Action
+)
+
 CONFIG_SCHEMA = cv.All(
     switch.switch_schema(NextionSwitch)
     .extend(CONFIG_SWITCH_COMPONENT_SCHEMA)
@@ -33,3 +39,33 @@ async def to_code(config):
     cg.add(hub.register_switch_component(var))
 
     await setup_component_core_(var, config, ".val")
+
+
+@automation.register_action(
+    "switch.nextion.publish",
+    NextionPublishBoolAction,
+    cv.Schema(
+        {
+            cv.Required(CONF_ID): cv.use_id(NextionSwitch),
+            cv.Required(CONF_STATE): cv.templatable(cv.boolean),
+            cv.Optional(CONF_PUBLISH_STATE, default="true"): cv.templatable(cv.boolean),
+            cv.Optional(CONF_SEND_TO_NEXTION, default="true"): cv.templatable(
+                cv.boolean
+            ),
+        }
+    ),
+)
+async def sensor_nextion_publish_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    var = cg.new_Pvariable(action_id, template_arg, paren)
+
+    template_ = await cg.templatable(config[CONF_STATE], args, bool)
+    cg.add(var.set_state(template_))
+
+    template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, bool)
+    cg.add(var.set_publish_state(template_))
+
+    template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, bool)
+    cg.add(var.set_send_to_nextion(template_))
+
+    return var
diff --git a/esphome/components/nextion/text_sensor/__init__.py b/esphome/components/nextion/text_sensor/__init__.py
index 826ff2354e..793397b1f4 100644
--- a/esphome/components/nextion/text_sensor/__init__.py
+++ b/esphome/components/nextion/text_sensor/__init__.py
@@ -1,9 +1,10 @@
+from esphome import automation
 from esphome.components import text_sensor
 import esphome.config_validation as cv
 import esphome.codegen as cg
-from esphome.const import CONF_ID
+from esphome.const import CONF_ID, CONF_STATE
 
-from .. import nextion_ns, CONF_NEXTION_ID
+from .. import nextion_ns, CONF_NEXTION_ID, CONF_PUBLISH_STATE, CONF_SEND_TO_NEXTION
 
 from ..base_component import (
     setup_component_core_,
@@ -16,6 +17,10 @@ NextionTextSensor = nextion_ns.class_(
     "NextionTextSensor", text_sensor.TextSensor, cg.PollingComponent
 )
 
+NextionPublishTextAction = nextion_ns.class_(
+    "NextionPublishTextAction", automation.Action
+)
+
 CONFIG_SCHEMA = (
     text_sensor.text_sensor_schema(NextionTextSensor)
     .extend(CONFIG_TEXT_COMPONENT_SCHEMA)
@@ -32,3 +37,33 @@ async def to_code(config):
     cg.add(hub.register_textsensor_component(var))
 
     await setup_component_core_(var, config, ".txt")
+
+
+@automation.register_action(
+    "text_sensor.nextion.publish",
+    NextionPublishTextAction,
+    cv.Schema(
+        {
+            cv.Required(CONF_ID): cv.use_id(NextionTextSensor),
+            cv.Required(CONF_STATE): cv.templatable(cv.string_strict),
+            cv.Optional(CONF_PUBLISH_STATE, default="true"): cv.templatable(cv.boolean),
+            cv.Optional(CONF_SEND_TO_NEXTION, default="true"): cv.templatable(
+                cv.boolean
+            ),
+        }
+    ),
+)
+async def sensor_nextion_publish_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    var = cg.new_Pvariable(action_id, template_arg, paren)
+
+    template_ = await cg.templatable(config[CONF_STATE], args, cg.const_char_ptr)
+    cg.add(var.set_state(template_))
+
+    template_ = await cg.templatable(config[CONF_PUBLISH_STATE], args, cg.bool_)
+    cg.add(var.set_publish_state(template_))
+
+    template_ = await cg.templatable(config[CONF_SEND_TO_NEXTION], args, cg.bool_)
+    cg.add(var.set_send_to_nextion(template_))
+
+    return var
diff --git a/tests/components/nextion/common.yaml b/tests/components/nextion/common.yaml
new file mode 100644
index 0000000000..e84cd08422
--- /dev/null
+++ b/tests/components/nextion/common.yaml
@@ -0,0 +1,293 @@
+esphome:
+  on_boot:
+    # Binary sensor publish action tests
+    - binary_sensor.nextion.publish:
+        id: r0_sensor
+        state: True
+
+    - binary_sensor.nextion.publish:
+        id: r0_sensor
+        state: True
+        publish_state: True
+        send_to_nextion: True
+
+    - binary_sensor.nextion.publish:
+        id: r0_sensor
+        state: True
+        publish_state: False
+        send_to_nextion: True
+
+    - binary_sensor.nextion.publish:
+        id: r0_sensor
+        state: True
+        publish_state: True
+        send_to_nextion: False
+
+    - binary_sensor.nextion.publish:
+        id: r0_sensor
+        state: True
+        publish_state: False
+        send_to_nextion: False
+
+    # Templated
+    - binary_sensor.nextion.publish:
+        id: r0_sensor
+        state: !lambda 'return true;'
+
+    - binary_sensor.nextion.publish:
+        id: r0_sensor
+        state: !lambda 'return true;'
+        publish_state: !lambda 'return true;'
+        send_to_nextion: !lambda 'return true;'
+
+    - binary_sensor.nextion.publish:
+        id: r0_sensor
+        state: !lambda 'return true;'
+        publish_state: !lambda 'return false;'
+        send_to_nextion: !lambda 'return true;'
+
+    - binary_sensor.nextion.publish:
+        id: r0_sensor
+        state: !lambda 'return true;'
+        publish_state: !lambda 'return true;'
+        send_to_nextion: !lambda 'return false;'
+
+    - binary_sensor.nextion.publish:
+        id: r0_sensor
+        state: !lambda 'return true;'
+        publish_state: !lambda 'return false;'
+        send_to_nextion: !lambda 'return false;'
+
+    # Sensor publish action tests
+    - sensor.nextion.publish:
+        id: testnumber
+        state: 42.0
+
+    - sensor.nextion.publish:
+        id: testnumber
+        state: 42.0
+        publish_state: True
+        send_to_nextion: True
+
+    - sensor.nextion.publish:
+        id: testnumber
+        state: 42.0
+        publish_state: False
+        send_to_nextion: True
+
+    - sensor.nextion.publish:
+        id: testnumber
+        state: 42.0
+        publish_state: True
+        send_to_nextion: False
+
+    - sensor.nextion.publish:
+        id: testnumber
+        state: 42.0
+        publish_state: False
+        send_to_nextion: False
+
+    # Templated
+    - sensor.nextion.publish:
+        id: testnumber
+        state: !lambda 'return 42.0;'
+
+    - sensor.nextion.publish:
+        id: testnumber
+        state: !lambda 'return 42.0;'
+        publish_state: !lambda 'return true;'
+        send_to_nextion: !lambda 'return true;'
+
+    - sensor.nextion.publish:
+        id: testnumber
+        state: !lambda 'return 42.0;'
+        publish_state: !lambda 'return false;'
+        send_to_nextion: !lambda 'return true;'
+
+    - sensor.nextion.publish:
+        id: testnumber
+        state: !lambda 'return 42.0;'
+        publish_state: !lambda 'return true;'
+        send_to_nextion: !lambda 'return false;'
+
+    - sensor.nextion.publish:
+        id: testnumber
+        state: !lambda 'return 42.0;'
+        publish_state: !lambda 'return false;'
+        send_to_nextion: !lambda 'return false;'
+
+    # Switch publish action tests
+    - switch.nextion.publish:
+        id: r0
+        state: True
+
+    - switch.nextion.publish:
+        id: r0
+        state: True
+        publish_state: true
+        send_to_nextion: true
+
+    - switch.nextion.publish:
+        id: r0
+        state: True
+        publish_state: false
+        send_to_nextion: true
+
+    - switch.nextion.publish:
+        id: r0
+        state: True
+        publish_state: true
+        send_to_nextion: false
+
+    - switch.nextion.publish:
+        id: r0
+        state: True
+        publish_state: false
+        send_to_nextion: false
+
+    # Templated
+    - switch.nextion.publish:
+        id: r0
+        state: !lambda 'return true;'
+
+    - switch.nextion.publish:
+        id: r0
+        state: !lambda 'return true;'
+        publish_state: !lambda 'return true;'
+        send_to_nextion: !lambda 'return true;'
+
+    - switch.nextion.publish:
+        id: r0
+        state: !lambda 'return true;'
+        publish_state: !lambda 'return false;'
+        send_to_nextion: !lambda 'return true;'
+
+    - switch.nextion.publish:
+        id: r0
+        state: !lambda 'return true;'
+        publish_state: !lambda 'return true;'
+        send_to_nextion: !lambda 'return false;'
+
+    - switch.nextion.publish:
+        id: r0
+        state: !lambda 'return true;'
+        publish_state: !lambda 'return false;'
+        send_to_nextion: !lambda 'return false;'
+
+    # Test sensor publish action tests
+    - text_sensor.nextion.publish:
+        id: text0
+        state: 'Test'
+        publish_state: true
+        send_to_nextion: true
+
+    - text_sensor.nextion.publish:
+        id: text0
+        state: 'Test'
+        publish_state: false
+        send_to_nextion: true
+
+    - text_sensor.nextion.publish:
+        id: text0
+        state: 'Test'
+        publish_state: true
+        send_to_nextion: false
+
+    - text_sensor.nextion.publish:
+        id: text0
+        state: 'Test'
+        publish_state: false
+        send_to_nextion: false
+
+    # Templated
+    - text_sensor.nextion.publish:
+        id: text0
+        state: !lambda 'return "Test";'
+
+    - text_sensor.nextion.publish:
+        id: text0
+        state: !lambda 'return "Test";'
+        publish_state: !lambda 'return true;'
+        send_to_nextion: !lambda 'return true;'
+
+    - text_sensor.nextion.publish:
+        id: text0
+        state: !lambda 'return "Test";'
+        publish_state: !lambda 'return false;'
+        send_to_nextion: !lambda 'return true;'
+
+    - text_sensor.nextion.publish:
+        id: text0
+        state: !lambda 'return "Test";'
+        publish_state: !lambda 'return true;'
+        send_to_nextion: !lambda 'return false;'
+
+    - text_sensor.nextion.publish:
+        id: text0
+        state: !lambda 'return "Test";'
+        publish_state: !lambda 'return false;'
+        send_to_nextion: !lambda 'return false;'
+
+wifi:
+  ssid: MySSID
+  password: password1
+
+uart:
+  - id: uart_nextion
+    tx_pin: ${tx_pin}
+    rx_pin: ${rx_pin}
+    baud_rate: 115200
+
+binary_sensor:
+  - platform: nextion
+    page_id: 0
+    component_id: 2
+    name: Nextion Touch Component
+  - platform: nextion
+    id: r0_sensor
+    name: R0 Sensor
+    component_name: page0.r0
+
+sensor:
+  - platform: nextion
+    id: testnumber
+    name: testnumber
+    variable_name: testnumber
+  - platform: nextion
+    id: testwave
+    name: testwave
+    component_id: 2
+    wave_channel_id: 1
+
+switch:
+  - platform: nextion
+    id: r0
+    name: R0 Switch
+    component_name: page0.r0
+
+text_sensor:
+  - platform: nextion
+    name: text0
+    id: text0
+    update_interval: 4s
+    component_name: text0
+
+display:
+  - platform: nextion
+    id: main_lcd
+    update_interval: 5s
+    on_sleep:
+      then:
+        lambda: 'ESP_LOGD("display","Display went to sleep");'
+    on_wake:
+      then:
+        lambda: 'ESP_LOGD("display","Display woke up");'
+    on_setup:
+      then:
+        lambda: 'ESP_LOGD("display","Display setup completed");'
+    on_page:
+      then:
+        lambda: 'ESP_LOGD("display","Display shows new page %u", x);'
+    on_buffer_overflow:
+      then:
+        logger.log: "Nextion reported a buffer overflow!"
diff --git a/tests/components/nextion/test.esp32-ard.yaml b/tests/components/nextion/test.esp32-ard.yaml
index ba76236fc6..d5e02b8b85 100644
--- a/tests/components/nextion/test.esp32-ard.yaml
+++ b/tests/components/nextion/test.esp32-ard.yaml
@@ -1,63 +1,10 @@
-wifi:
-  ssid: MySSID
-  password: password1
+substitutions:
+  tx_pin: GPIO17
+  rx_pin: GPIO16
 
-uart:
-  - id: uart_nextion
-    tx_pin: 17
-    rx_pin: 16
-    baud_rate: 115200
-
-binary_sensor:
-  - platform: nextion
-    page_id: 0
-    component_id: 2
-    name: Nextion Touch Component
-  - platform: nextion
-    id: r0_sensor
-    name: R0 Sensor
-    component_name: page0.r0
-
-sensor:
-  - platform: nextion
-    id: testnumber
-    name: testnumber
-    variable_name: testnumber
-  - platform: nextion
-    id: testwave
-    name: testwave
-    component_id: 2
-    wave_channel_id: 1
-
-switch:
-  - platform: nextion
-    id: r0
-    name: R0 Switch
-    component_name: page0.r0
-
-text_sensor:
-  - platform: nextion
-    name: text0
-    id: text0
-    update_interval: 4s
-    component_name: text0
+packages:
+  base: !include common.yaml
 
 display:
-  - platform: nextion
+  - id: !extend main_lcd
     tft_url: http://esphome.io/default35.tft
-    update_interval: 5s
-    on_sleep:
-      then:
-        lambda: 'ESP_LOGD("display","Display went to sleep");'
-    on_wake:
-      then:
-        lambda: 'ESP_LOGD("display","Display woke up");'
-    on_setup:
-      then:
-        lambda: 'ESP_LOGD("display","Display setup completed");'
-    on_page:
-      then:
-        lambda: 'ESP_LOGD("display","Display shows new page %u", x);'
-    on_buffer_overflow:
-      then:
-        logger.log: "Nextion reported a buffer overflow!"
diff --git a/tests/components/nextion/test.esp32-c3-ard.yaml b/tests/components/nextion/test.esp32-c3-ard.yaml
index 5d253268f8..5135c7e4f4 100644
--- a/tests/components/nextion/test.esp32-c3-ard.yaml
+++ b/tests/components/nextion/test.esp32-c3-ard.yaml
@@ -1,63 +1,10 @@
-wifi:
-  ssid: MySSID
-  password: password1
+substitutions:
+  tx_pin: GPIO4
+  rx_pin: GPIO5
 
-uart:
-  - id: uart_nextion
-    tx_pin: 4
-    rx_pin: 5
-    baud_rate: 115200
-
-binary_sensor:
-  - platform: nextion
-    page_id: 0
-    component_id: 2
-    name: Nextion Touch Component
-  - platform: nextion
-    id: r0_sensor
-    name: R0 Sensor
-    component_name: page0.r0
-
-sensor:
-  - platform: nextion
-    id: testnumber
-    name: testnumber
-    variable_name: testnumber
-  - platform: nextion
-    id: testwave
-    name: testwave
-    component_id: 2
-    wave_channel_id: 1
-
-switch:
-  - platform: nextion
-    id: r0
-    name: R0 Switch
-    component_name: page0.r0
-
-text_sensor:
-  - platform: nextion
-    name: text0
-    id: text0
-    update_interval: 4s
-    component_name: text0
+packages:
+  base: !include common.yaml
 
 display:
-  - platform: nextion
+  - id: !extend main_lcd
     tft_url: http://esphome.io/default35.tft
-    update_interval: 5s
-    on_sleep:
-      then:
-        lambda: 'ESP_LOGD("display","Display went to sleep");'
-    on_wake:
-      then:
-        lambda: 'ESP_LOGD("display","Display woke up");'
-    on_setup:
-      then:
-        lambda: 'ESP_LOGD("display","Display setup completed");'
-    on_page:
-      then:
-        lambda: 'ESP_LOGD("display","Display shows new page %u", x);'
-    on_buffer_overflow:
-      then:
-        logger.log: "Nextion reported a buffer overflow!"
diff --git a/tests/components/nextion/test.esp32-c3-idf.yaml b/tests/components/nextion/test.esp32-c3-idf.yaml
index 5d253268f8..5135c7e4f4 100644
--- a/tests/components/nextion/test.esp32-c3-idf.yaml
+++ b/tests/components/nextion/test.esp32-c3-idf.yaml
@@ -1,63 +1,10 @@
-wifi:
-  ssid: MySSID
-  password: password1
+substitutions:
+  tx_pin: GPIO4
+  rx_pin: GPIO5
 
-uart:
-  - id: uart_nextion
-    tx_pin: 4
-    rx_pin: 5
-    baud_rate: 115200
-
-binary_sensor:
-  - platform: nextion
-    page_id: 0
-    component_id: 2
-    name: Nextion Touch Component
-  - platform: nextion
-    id: r0_sensor
-    name: R0 Sensor
-    component_name: page0.r0
-
-sensor:
-  - platform: nextion
-    id: testnumber
-    name: testnumber
-    variable_name: testnumber
-  - platform: nextion
-    id: testwave
-    name: testwave
-    component_id: 2
-    wave_channel_id: 1
-
-switch:
-  - platform: nextion
-    id: r0
-    name: R0 Switch
-    component_name: page0.r0
-
-text_sensor:
-  - platform: nextion
-    name: text0
-    id: text0
-    update_interval: 4s
-    component_name: text0
+packages:
+  base: !include common.yaml
 
 display:
-  - platform: nextion
+  - id: !extend main_lcd
     tft_url: http://esphome.io/default35.tft
-    update_interval: 5s
-    on_sleep:
-      then:
-        lambda: 'ESP_LOGD("display","Display went to sleep");'
-    on_wake:
-      then:
-        lambda: 'ESP_LOGD("display","Display woke up");'
-    on_setup:
-      then:
-        lambda: 'ESP_LOGD("display","Display setup completed");'
-    on_page:
-      then:
-        lambda: 'ESP_LOGD("display","Display shows new page %u", x);'
-    on_buffer_overflow:
-      then:
-        logger.log: "Nextion reported a buffer overflow!"
diff --git a/tests/components/nextion/test.esp32-idf.yaml b/tests/components/nextion/test.esp32-idf.yaml
index ba76236fc6..d5e02b8b85 100644
--- a/tests/components/nextion/test.esp32-idf.yaml
+++ b/tests/components/nextion/test.esp32-idf.yaml
@@ -1,63 +1,10 @@
-wifi:
-  ssid: MySSID
-  password: password1
+substitutions:
+  tx_pin: GPIO17
+  rx_pin: GPIO16
 
-uart:
-  - id: uart_nextion
-    tx_pin: 17
-    rx_pin: 16
-    baud_rate: 115200
-
-binary_sensor:
-  - platform: nextion
-    page_id: 0
-    component_id: 2
-    name: Nextion Touch Component
-  - platform: nextion
-    id: r0_sensor
-    name: R0 Sensor
-    component_name: page0.r0
-
-sensor:
-  - platform: nextion
-    id: testnumber
-    name: testnumber
-    variable_name: testnumber
-  - platform: nextion
-    id: testwave
-    name: testwave
-    component_id: 2
-    wave_channel_id: 1
-
-switch:
-  - platform: nextion
-    id: r0
-    name: R0 Switch
-    component_name: page0.r0
-
-text_sensor:
-  - platform: nextion
-    name: text0
-    id: text0
-    update_interval: 4s
-    component_name: text0
+packages:
+  base: !include common.yaml
 
 display:
-  - platform: nextion
+  - id: !extend main_lcd
     tft_url: http://esphome.io/default35.tft
-    update_interval: 5s
-    on_sleep:
-      then:
-        lambda: 'ESP_LOGD("display","Display went to sleep");'
-    on_wake:
-      then:
-        lambda: 'ESP_LOGD("display","Display woke up");'
-    on_setup:
-      then:
-        lambda: 'ESP_LOGD("display","Display setup completed");'
-    on_page:
-      then:
-        lambda: 'ESP_LOGD("display","Display shows new page %u", x);'
-    on_buffer_overflow:
-      then:
-        logger.log: "Nextion reported a buffer overflow!"
diff --git a/tests/components/nextion/test.esp8266-ard.yaml b/tests/components/nextion/test.esp8266-ard.yaml
index 5d253268f8..5135c7e4f4 100644
--- a/tests/components/nextion/test.esp8266-ard.yaml
+++ b/tests/components/nextion/test.esp8266-ard.yaml
@@ -1,63 +1,10 @@
-wifi:
-  ssid: MySSID
-  password: password1
+substitutions:
+  tx_pin: GPIO4
+  rx_pin: GPIO5
 
-uart:
-  - id: uart_nextion
-    tx_pin: 4
-    rx_pin: 5
-    baud_rate: 115200
-
-binary_sensor:
-  - platform: nextion
-    page_id: 0
-    component_id: 2
-    name: Nextion Touch Component
-  - platform: nextion
-    id: r0_sensor
-    name: R0 Sensor
-    component_name: page0.r0
-
-sensor:
-  - platform: nextion
-    id: testnumber
-    name: testnumber
-    variable_name: testnumber
-  - platform: nextion
-    id: testwave
-    name: testwave
-    component_id: 2
-    wave_channel_id: 1
-
-switch:
-  - platform: nextion
-    id: r0
-    name: R0 Switch
-    component_name: page0.r0
-
-text_sensor:
-  - platform: nextion
-    name: text0
-    id: text0
-    update_interval: 4s
-    component_name: text0
+packages:
+  base: !include common.yaml
 
 display:
-  - platform: nextion
+  - id: !extend main_lcd
     tft_url: http://esphome.io/default35.tft
-    update_interval: 5s
-    on_sleep:
-      then:
-        lambda: 'ESP_LOGD("display","Display went to sleep");'
-    on_wake:
-      then:
-        lambda: 'ESP_LOGD("display","Display woke up");'
-    on_setup:
-      then:
-        lambda: 'ESP_LOGD("display","Display setup completed");'
-    on_page:
-      then:
-        lambda: 'ESP_LOGD("display","Display shows new page %u", x);'
-    on_buffer_overflow:
-      then:
-        logger.log: "Nextion reported a buffer overflow!"
diff --git a/tests/components/nextion/test.rp2040-ard.yaml b/tests/components/nextion/test.rp2040-ard.yaml
index 9b04433095..20347c6eff 100644
--- a/tests/components/nextion/test.rp2040-ard.yaml
+++ b/tests/components/nextion/test.rp2040-ard.yaml
@@ -1,58 +1,7 @@
-uart:
-  - id: uart_nextion
-    tx_pin: 4
-    rx_pin: 5
-    baud_rate: 115200
+substitutions:
+  tx_pin: GPIO4
+  rx_pin: GPIO5
 
-binary_sensor:
-  - platform: nextion
-    page_id: 0
-    component_id: 2
-    name: Nextion Touch Component
-  - platform: nextion
-    id: r0_sensor
-    name: R0 Sensor
-    component_name: page0.r0
+packages:
+  base: !include common.yaml
 
-sensor:
-  - platform: nextion
-    id: testnumber
-    name: testnumber
-    variable_name: testnumber
-  - platform: nextion
-    id: testwave
-    name: testwave
-    component_id: 2
-    wave_channel_id: 1
-
-switch:
-  - platform: nextion
-    id: r0
-    name: R0 Switch
-    component_name: page0.r0
-
-text_sensor:
-  - platform: nextion
-    name: text0
-    id: text0
-    update_interval: 4s
-    component_name: text0
-
-display:
-  - platform: nextion
-    update_interval: 5s
-    on_sleep:
-      then:
-        lambda: 'ESP_LOGD("display","Display went to sleep");'
-    on_wake:
-      then:
-        lambda: 'ESP_LOGD("display","Display woke up");'
-    on_setup:
-      then:
-        lambda: 'ESP_LOGD("display","Display setup completed");'
-    on_page:
-      then:
-        lambda: 'ESP_LOGD("display","Display shows new page %u", x);'
-    on_buffer_overflow:
-      then:
-        logger.log: "Nextion reported a buffer overflow!"

From 2ecd5cff0701b807e5fa5558355c7beb7e6204ce Mon Sep 17 00:00:00 2001
From: NP v/d Spek <github_mail@lumensoft.nl>
Date: Sun, 24 Nov 2024 21:16:51 +0100
Subject: [PATCH 148/282] [wifi] Make wifi_channel_() public (#7818)

---
 esphome/components/wifi/wifi_component.cpp               | 4 ++--
 esphome/components/wifi/wifi_component.h                 | 4 +++-
 esphome/components/wifi/wifi_component_esp32_arduino.cpp | 2 +-
 esphome/components/wifi/wifi_component_esp8266.cpp       | 2 +-
 esphome/components/wifi/wifi_component_esp_idf.cpp       | 2 +-
 esphome/components/wifi/wifi_component_libretiny.cpp     | 2 +-
 esphome/components/wifi/wifi_component_pico_w.cpp        | 2 +-
 7 files changed, 10 insertions(+), 8 deletions(-)

diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp
index 8788711d5a..eef962b8c4 100644
--- a/esphome/components/wifi/wifi_component.cpp
+++ b/esphome/components/wifi/wifi_component.cpp
@@ -444,7 +444,7 @@ void WiFiComponent::print_connect_params_() {
   if (this->selected_ap_.get_bssid().has_value()) {
     ESP_LOGV(TAG, "  Priority: %.1f", this->get_sta_priority(*this->selected_ap_.get_bssid()));
   }
-  ESP_LOGCONFIG(TAG, "  Channel: %" PRId32, wifi_channel_());
+  ESP_LOGCONFIG(TAG, "  Channel: %" PRId32, get_wifi_channel());
   ESP_LOGCONFIG(TAG, "  Subnet: %s", wifi_subnet_mask_().str().c_str());
   ESP_LOGCONFIG(TAG, "  Gateway: %s", wifi_gateway_ip_().str().c_str());
   ESP_LOGCONFIG(TAG, "  DNS1: %s", wifi_dns_ip_(0).str().c_str());
@@ -763,7 +763,7 @@ void WiFiComponent::load_fast_connect_settings_() {
 
 void WiFiComponent::save_fast_connect_settings_() {
   bssid_t bssid = wifi_bssid();
-  uint8_t channel = wifi_channel_();
+  uint8_t channel = get_wifi_channel();
 
   if (bssid != this->selected_ap_.get_bssid() || channel != this->selected_ap_.get_channel()) {
     SavedWifiFastConnectSettings fast_connect_save{};
diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h
index dde0d1d5a5..5995f72e0b 100644
--- a/esphome/components/wifi/wifi_component.h
+++ b/esphome/components/wifi/wifi_component.h
@@ -317,6 +317,8 @@ class WiFiComponent : public Component {
   Trigger<> *get_connect_trigger() const { return this->connect_trigger_; };
   Trigger<> *get_disconnect_trigger() const { return this->disconnect_trigger_; };
 
+  int32_t get_wifi_channel();
+
  protected:
   static std::string format_mac_addr(const uint8_t mac[6]);
 
@@ -344,7 +346,7 @@ class WiFiComponent : public Component {
 #endif  // USE_WIFI_AP
 
   bool wifi_disconnect_();
-  int32_t wifi_channel_();
+
   network::IPAddress wifi_subnet_mask_();
   network::IPAddress wifi_gateway_ip_();
   network::IPAddress wifi_dns_ip_(int num);
diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp
index 88648093c6..18c706cb01 100644
--- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp
+++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp
@@ -799,7 +799,7 @@ bssid_t WiFiComponent::wifi_bssid() {
 }
 std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
 int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); }
-int32_t WiFiComponent::wifi_channel_() { return WiFi.channel(); }
+int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); }
 network::IPAddress WiFiComponent::wifi_subnet_mask_() { return network::IPAddress(WiFi.subnetMask()); }
 network::IPAddress WiFiComponent::wifi_gateway_ip_() { return network::IPAddress(WiFi.gatewayIP()); }
 network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(WiFi.dnsIP(num)); }
diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp
index 4568895950..a18d078967 100644
--- a/esphome/components/wifi/wifi_component_esp8266.cpp
+++ b/esphome/components/wifi/wifi_component_esp8266.cpp
@@ -825,7 +825,7 @@ bssid_t WiFiComponent::wifi_bssid() {
 }
 std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
 int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); }
-int32_t WiFiComponent::wifi_channel_() { return WiFi.channel(); }
+int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); }
 network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; }
 network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; }
 network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {(const ip_addr_t *) WiFi.dnsIP(num)}; }
diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp
index 13870136d4..1bf14ff40b 100644
--- a/esphome/components/wifi/wifi_component_esp_idf.cpp
+++ b/esphome/components/wifi/wifi_component_esp_idf.cpp
@@ -973,7 +973,7 @@ int8_t WiFiComponent::wifi_rssi() {
   }
   return info.rssi;
 }
-int32_t WiFiComponent::wifi_channel_() {
+int32_t WiFiComponent::get_wifi_channel() {
   uint8_t primary;
   wifi_second_chan_t second;
   esp_err_t err = esp_wifi_get_channel(&primary, &second);
diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp
index afb30c3bcf..b02f8ef0ce 100644
--- a/esphome/components/wifi/wifi_component_libretiny.cpp
+++ b/esphome/components/wifi/wifi_component_libretiny.cpp
@@ -473,7 +473,7 @@ bssid_t WiFiComponent::wifi_bssid() {
 }
 std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
 int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); }
-int32_t WiFiComponent::wifi_channel_() { return WiFi.channel(); }
+int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); }
 network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; }
 network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; }
 network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {WiFi.dnsIP(num)}; }
diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp
index bac986d899..23fd766abe 100644
--- a/esphome/components/wifi/wifi_component_pico_w.cpp
+++ b/esphome/components/wifi/wifi_component_pico_w.cpp
@@ -189,7 +189,7 @@ bssid_t WiFiComponent::wifi_bssid() {
 }
 std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
 int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); }
-int32_t WiFiComponent::wifi_channel_() { return WiFi.channel(); }
+int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); }
 
 network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() {
   network::IPAddresses addresses;

From 4936ca17003404796d388363469badfab340d8b8 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Mon, 25 Nov 2024 07:25:16 +1100
Subject: [PATCH 149/282] [lvgl] Bugfixes (#7803)

---
 esphome/components/lvgl/__init__.py      | 2 +-
 esphome/components/lvgl/lv_validation.py | 3 ++-
 esphome/components/lvgl/schemas.py       | 1 +
 esphome/components/lvgl/widgets/line.py  | 5 ++++-
 4 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py
index d03adc9624..8fdd03f647 100644
--- a/esphome/components/lvgl/__init__.py
+++ b/esphome/components/lvgl/__init__.py
@@ -322,8 +322,8 @@ async def to_code(configs):
             await encoders_to_code(lv_component, config, default_group)
             await keypads_to_code(lv_component, config, default_group)
             await theme_to_code(config)
-            await styles_to_code(config)
             await gradients_to_code(config)
+            await styles_to_code(config)
             await set_obj_properties(lv_scr_act, config)
             await add_widgets(lv_scr_act, config)
             await add_pages(lv_component, config)
diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py
index b91b0905df..766c010244 100644
--- a/esphome/components/lvgl/lv_validation.py
+++ b/esphome/components/lvgl/lv_validation.py
@@ -30,7 +30,7 @@ from .defines import (
     call_lambda,
     literal,
 )
-from .helpers import esphome_fonts_used, lv_fonts_used, requires_component
+from .helpers import add_lv_use, esphome_fonts_used, lv_fonts_used, requires_component
 from .types import lv_font_t, lv_gradient_t, lv_img_t
 
 opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
@@ -326,6 +326,7 @@ def image_validator(value):
     value = requires_component("image")(value)
     value = cv.use_id(Image_)(value)
     lv_images_used.add(value)
+    add_lv_use("img", "label")
     return value
 
 
diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py
index 516627708e..3f56b3345f 100644
--- a/esphome/components/lvgl/schemas.py
+++ b/esphome/components/lvgl/schemas.py
@@ -341,6 +341,7 @@ FLEX_OBJ_SCHEMA = {
     cv.Optional(df.CONF_FLEX_GROW): cv.int_,
 }
 
+
 DISP_BG_SCHEMA = cv.Schema(
     {
         cv.Optional(df.CONF_DISP_BG_IMAGE): lv_image,
diff --git a/esphome/components/lvgl/widgets/line.py b/esphome/components/lvgl/widgets/line.py
index 4c6439fde4..548dfa8452 100644
--- a/esphome/components/lvgl/widgets/line.py
+++ b/esphome/components/lvgl/widgets/line.py
@@ -39,7 +39,10 @@ LINE_SCHEMA = {
 class LineType(WidgetType):
     def __init__(self):
         super().__init__(
-            CONF_LINE, LvType("lv_line_t"), (CONF_MAIN,), LINE_SCHEMA, modify_schema={}
+            CONF_LINE,
+            LvType("lv_line_t"),
+            (CONF_MAIN,),
+            LINE_SCHEMA,
         )
 
     async def to_code(self, w: Widget, config):

From 4001d82ca269472b6ef813fa4b894e1071d5571e Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Mon, 25 Nov 2024 07:25:51 +1100
Subject: [PATCH 150/282] [docker] Leave run-time required libraries installed.
 (#7804)

---
 docker/Dockerfile | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index ed6ce083a8..c2902a9dd1 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -99,15 +99,17 @@ BUILD_DEPS="
     libfreetype-dev=2.12.1+dfsg-5+deb12u3
     libssl-dev=3.0.15-1~deb12u1
     libffi-dev=3.4.4-1
-    libopenjp2-7=2.5.0-2
-    libtiff6=4.5.0-6+deb12u1
     cargo=0.66.0+ds1-1
     pkg-config=1.8.1-1
 "
+LIB_DEPS="
+    libtiff6=4.5.0-6+deb12u1
+    libopenjp2-7=2.5.0-2
+"
 if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ] || [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]
 then
     apt-get update
-    apt-get install -y --no-install-recommends $BUILD_DEPS
+    apt-get install -y --no-install-recommends $BUILD_DEPS $LIB_DEPS
 fi
 
 CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo

From 13077095c2ece197c41c41324d578cb2a284edd8 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Mon, 25 Nov 2024 07:27:09 +1100
Subject: [PATCH 151/282] [qspi_dbi] Fix init sequences (Bugfix) (#7805)

---
 esphome/components/qspi_dbi/models.py    | 2 ++
 esphome/components/qspi_dbi/qspi_dbi.cpp | 1 -
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/esphome/components/qspi_dbi/models.py b/esphome/components/qspi_dbi/models.py
index cbd9c4663f..c1fe434853 100644
--- a/esphome/components/qspi_dbi/models.py
+++ b/esphome/components/qspi_dbi/models.py
@@ -1,6 +1,7 @@
 # Commands
 SW_RESET_CMD = 0x01
 SLEEP_OUT = 0x11
+NORON = 0x13
 INVERT_OFF = 0x20
 INVERT_ON = 0x21
 ALL_ON = 0x23
@@ -56,6 +57,7 @@ chip.cmd(0xC2, 0x00)
 chip.delay(10)
 chip.cmd(TEON, 0x00)
 chip.cmd(PIXFMT, 0x55)
+chip.cmd(NORON)
 
 chip = DriverChip("AXS15231")
 chip.cmd(0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xA5)
diff --git a/esphome/components/qspi_dbi/qspi_dbi.cpp b/esphome/components/qspi_dbi/qspi_dbi.cpp
index a649a25ea6..785885d4ec 100644
--- a/esphome/components/qspi_dbi/qspi_dbi.cpp
+++ b/esphome/components/qspi_dbi/qspi_dbi.cpp
@@ -111,7 +111,6 @@ void QspiDbi::reset_params_(bool ready) {
     mad |= MADCTL_MY;
   this->write_command_(MADCTL_CMD, mad);
   this->write_command_(BRIGHTNESS, this->brightness_);
-  this->write_command_(NORON);
   this->write_command_(DISPLAY_ON);
 }
 

From e3e3d9234756b94a74f3788f64aa4d9b37d40f7a Mon Sep 17 00:00:00 2001
From: Samuel Sieb <samuel-github@sieb.net>
Date: Sun, 24 Nov 2024 10:42:46 -1000
Subject: [PATCH 152/282] fix modbus crashing when bad data returned (#7810)

Co-authored-by: Samuel Sieb <samuel@sieb.net>
---
 esphome/components/modbus/modbus.cpp          |  3 +-
 .../modbus_controller/modbus_controller.cpp   | 72 ++++++++++++++-----
 2 files changed, 56 insertions(+), 19 deletions(-)

diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp
index 8544b50261..47deea83e6 100644
--- a/esphome/components/modbus/modbus.cpp
+++ b/esphome/components/modbus/modbus.cpp
@@ -38,8 +38,9 @@ void Modbus::loop() {
 
     // stop blocking new send commands after sent_wait_time_ ms after response received
     if (now - this->last_send_ > send_wait_time_) {
-      if (waiting_for_response > 0)
+      if (waiting_for_response > 0) {
         ESP_LOGV(TAG, "Stop waiting for response from %d", waiting_for_response);
+      }
       waiting_for_response = 0;
     }
   }
diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp
index e1102516ca..f8b72af817 100644
--- a/esphome/components/modbus_controller/modbus_controller.cpp
+++ b/esphome/components/modbus_controller/modbus_controller.cpp
@@ -622,51 +622,87 @@ int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sens
                           uint32_t bitmask) {
   int64_t value = 0;  // int64_t because it can hold signed and unsigned 32 bits
 
+  size_t size = data.size() - offset;
+  bool error = false;
   switch (sensor_value_type) {
     case SensorValueType::U_WORD:
-      value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), bitmask);  // default is 0xFFFF ;
+      if (size >= 2) {
+        value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), bitmask);  // default is 0xFFFF ;
+      } else {
+        error = true;
+      }
       break;
     case SensorValueType::U_DWORD:
     case SensorValueType::FP32:
-      value = get_data<uint32_t>(data, offset);
-      value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
+      if (size >= 4) {
+        value = get_data<uint32_t>(data, offset);
+        value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
+      } else {
+        error = true;
+      }
       break;
     case SensorValueType::U_DWORD_R:
     case SensorValueType::FP32_R:
-      value = get_data<uint32_t>(data, offset);
-      value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
-      value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
+      if (size >= 4) {
+        value = get_data<uint32_t>(data, offset);
+        value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
+        value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
+      } else {
+        error = true;
+      }
       break;
     case SensorValueType::S_WORD:
-      value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset),
-                                         bitmask);  // default is 0xFFFF ;
+      if (size >= 2) {
+        value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset),
+                                           bitmask);  // default is 0xFFFF ;
+      } else {
+        error = true;
+      }
       break;
     case SensorValueType::S_DWORD:
-      value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
+      if (size >= 4) {
+        value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
+      } else {
+        error = true;
+      }
       break;
     case SensorValueType::S_DWORD_R: {
-      value = get_data<uint32_t>(data, offset);
-      // Currently the high word is at the low position
-      // the sign bit is therefore at low before the switch
-      uint32_t sign_bit = (value & 0x8000) << 16;
-      value = mask_and_shift_by_rightbit(
-          static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
+      if (size >= 4) {
+        value = get_data<uint32_t>(data, offset);
+        // Currently the high word is at the low position
+        // the sign bit is therefore at low before the switch
+        uint32_t sign_bit = (value & 0x8000) << 16;
+        value = mask_and_shift_by_rightbit(
+            static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
+      } else {
+        error = true;
+      }
     } break;
     case SensorValueType::U_QWORD:
     case SensorValueType::S_QWORD:
       // Ignore bitmask for QWORD
-      value = get_data<uint64_t>(data, offset);
+      if (size >= 8) {
+        value = get_data<uint64_t>(data, offset);
+      } else {
+        error = true;
+      }
       break;
     case SensorValueType::U_QWORD_R:
     case SensorValueType::S_QWORD_R: {
       // Ignore bitmask for QWORD
-      uint64_t tmp = get_data<uint64_t>(data, offset);
-      value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000);
+      if (size >= 8) {
+        uint64_t tmp = get_data<uint64_t>(data, offset);
+        value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000);
+      } else {
+        error = true;
+      }
     } break;
     case SensorValueType::RAW:
     default:
       break;
   }
+  if (error)
+    ESP_LOGE(TAG, "not enough data for value");
   return value;
 }
 

From 9fc1377b448fab25c91e3342834a0b38df0c4a80 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= <contact@rodrigomartin.dev>
Date: Sun, 24 Nov 2024 23:06:21 +0100
Subject: [PATCH 153/282] feat(WiFi): Add wifi.configure action (#7335)

---
 esphome/components/wifi/__init__.py      | 42 +++++++++++++
 esphome/components/wifi/wifi_component.h | 79 ++++++++++++++++++++++++
 tests/components/wifi/common.yaml        |  7 +++
 3 files changed, 128 insertions(+)

diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py
index ea03cc16d1..ad1a4f5262 100644
--- a/esphome/components/wifi/__init__.py
+++ b/esphome/components/wifi/__init__.py
@@ -27,6 +27,7 @@ from esphome.const import (
     CONF_NETWORKS,
     CONF_ON_CONNECT,
     CONF_ON_DISCONNECT,
+    CONF_ON_ERROR,
     CONF_PASSWORD,
     CONF_POWER_SAVE_MODE,
     CONF_PRIORITY,
@@ -34,6 +35,7 @@ from esphome.const import (
     CONF_SSID,
     CONF_STATIC_IP,
     CONF_SUBNET,
+    CONF_TIMEOUT,
     CONF_TTLS_PHASE_2,
     CONF_USE_ADDRESS,
     CONF_USERNAME,
@@ -46,6 +48,7 @@ from . import wpa2_eap
 AUTO_LOAD = ["network"]
 
 NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2]
+CONF_SAVE = "save"
 
 wifi_ns = cg.esphome_ns.namespace("wifi")
 EAPAuth = wifi_ns.struct("EAPAuth")
@@ -63,6 +66,9 @@ WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition)
 WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition)
 WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action)
 WiFiDisableAction = wifi_ns.class_("WiFiDisableAction", automation.Action)
+WiFiConfigureAction = wifi_ns.class_(
+    "WiFiConfigureAction", automation.Action, cg.Component
+)
 
 
 def validate_password(value):
@@ -483,3 +489,39 @@ async def wifi_enable_to_code(config, action_id, template_arg, args):
 @automation.register_action("wifi.disable", WiFiDisableAction, cv.Schema({}))
 async def wifi_disable_to_code(config, action_id, template_arg, args):
     return cg.new_Pvariable(action_id, template_arg)
+
+
+@automation.register_action(
+    "wifi.configure",
+    WiFiConfigureAction,
+    cv.Schema(
+        {
+            cv.Required(CONF_SSID): cv.templatable(cv.ssid),
+            cv.Required(CONF_PASSWORD): cv.templatable(validate_password),
+            cv.Optional(CONF_SAVE, default=True): cv.templatable(cv.boolean),
+            cv.Optional(CONF_TIMEOUT, default="30000ms"): cv.templatable(
+                cv.positive_time_period_milliseconds
+            ),
+            cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True),
+            cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True),
+        }
+    ),
+)
+async def wifi_set_sta_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    ssid = await cg.templatable(config[CONF_SSID], args, cg.std_string)
+    password = await cg.templatable(config[CONF_PASSWORD], args, cg.std_string)
+    save = await cg.templatable(config[CONF_SAVE], args, cg.bool_)
+    timeout = await cg.templatable(config.get(CONF_TIMEOUT), args, cg.uint32)
+    cg.add(var.set_ssid(ssid))
+    cg.add(var.set_password(password))
+    cg.add(var.set_save(save))
+    cg.add(var.set_connection_timeout(timeout))
+    if on_connect_config := config.get(CONF_ON_CONNECT):
+        await automation.build_automation(
+            var.get_connect_trigger(), [], on_connect_config
+        )
+    if on_error_config := config.get(CONF_ON_ERROR):
+        await automation.build_automation(var.get_error_trigger(), [], on_error_config)
+    await cg.register_component(var, config)
+    return var
diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h
index 5995f72e0b..abedfab3a6 100644
--- a/esphome/components/wifi/wifi_component.h
+++ b/esphome/components/wifi/wifi_component.h
@@ -209,6 +209,7 @@ class WiFiComponent : public Component {
   WiFiComponent();
 
   void set_sta(const WiFiAP &ap);
+  WiFiAP get_sta() { return this->selected_ap_; }
   void add_sta(const WiFiAP &ap);
   void clear_sta();
 
@@ -443,6 +444,84 @@ template<typename... Ts> class WiFiDisableAction : public Action<Ts...> {
   void play(Ts... x) override { global_wifi_component->disable(); }
 };
 
+template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, public Component {
+ public:
+  TEMPLATABLE_VALUE(std::string, ssid)
+  TEMPLATABLE_VALUE(std::string, password)
+  TEMPLATABLE_VALUE(bool, save)
+  TEMPLATABLE_VALUE(uint32_t, connection_timeout)
+
+  void play(Ts... x) override {
+    auto ssid = this->ssid_.value(x...);
+    auto password = this->password_.value(x...);
+    // Avoid multiple calls
+    if (this->connecting_)
+      return;
+    // If already connected to the same AP, do nothing
+    if (global_wifi_component->wifi_ssid() == ssid) {
+      // Callback to notify the user that the connection was successful
+      this->connect_trigger_->trigger();
+      return;
+    }
+    // Create a new WiFiAP object with the new SSID and password
+    this->new_sta_.set_ssid(ssid);
+    this->new_sta_.set_password(password);
+    // Save the current STA
+    this->old_sta_ = global_wifi_component->get_sta();
+    // Disable WiFi
+    global_wifi_component->disable();
+    // Set the state to connecting
+    this->connecting_ = true;
+    // Store the new STA so once the WiFi is enabled, it will connect to it
+    // This is necessary because the WiFiComponent will raise an error and fallback to the saved STA
+    // if trying to connect to a new STA while already connected to another one
+    if (this->save_.value(x...)) {
+      global_wifi_component->save_wifi_sta(new_sta_.get_ssid(), new_sta_.get_password());
+    } else {
+      global_wifi_component->set_sta(new_sta_);
+    }
+    // Enable WiFi
+    global_wifi_component->enable();
+    // Set timeout for the connection
+    this->set_timeout("wifi-connect-timeout", this->connection_timeout_.value(x...), [this]() {
+      this->connecting_ = false;
+      // If the timeout is reached, stop connecting and revert to the old AP
+      global_wifi_component->disable();
+      global_wifi_component->save_wifi_sta(old_sta_.get_ssid(), old_sta_.get_password());
+      global_wifi_component->enable();
+      // Callback to notify the user that the connection failed
+      this->error_trigger_->trigger();
+    });
+  }
+
+  Trigger<> *get_connect_trigger() const { return this->connect_trigger_; }
+  Trigger<> *get_error_trigger() const { return this->error_trigger_; }
+
+  void loop() override {
+    if (!this->connecting_)
+      return;
+    if (global_wifi_component->is_connected()) {
+      // The WiFi is connected, stop the timeout and reset the connecting flag
+      this->cancel_timeout("wifi-connect-timeout");
+      this->connecting_ = false;
+      if (global_wifi_component->wifi_ssid() == this->new_sta_.get_ssid()) {
+        // Callback to notify the user that the connection was successful
+        this->connect_trigger_->trigger();
+      } else {
+        // Callback to notify the user that the connection failed
+        this->error_trigger_->trigger();
+      }
+    }
+  }
+
+ protected:
+  bool connecting_{false};
+  WiFiAP new_sta_;
+  WiFiAP old_sta_;
+  Trigger<> *connect_trigger_{new Trigger<>()};
+  Trigger<> *error_trigger_{new Trigger<>()};
+};
+
 }  // namespace wifi
 }  // namespace esphome
 #endif
diff --git a/tests/components/wifi/common.yaml b/tests/components/wifi/common.yaml
index 003f6347be..343d44b177 100644
--- a/tests/components/wifi/common.yaml
+++ b/tests/components/wifi/common.yaml
@@ -3,6 +3,13 @@ esphome:
     then:
       - wifi.disable
       - wifi.enable
+      - wifi.configure:
+          ssid: MySSID
+          password: password1
+          on_connect:
+            - logger.log: "Connected to WiFi!"
+          on_error:
+            - logger.log: "Failed to connect to WiFi!"
 
 wifi:
   ssid: MySSID

From d4d630823ccc3bdddb6e6b39176c188d353d0807 Mon Sep 17 00:00:00 2001
From: TFGF <terciofilho@gmail.com>
Date: Sun, 24 Nov 2024 19:15:10 -0300
Subject: [PATCH 154/282] [Modbus Controller] Fix issue #6477. Online
 automation triggering Offline (#7801)

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

diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py
index 5c407d6fff..2a08075831 100644
--- a/esphome/components/modbus_controller/__init__.py
+++ b/esphome/components/modbus_controller/__init__.py
@@ -163,7 +163,7 @@ CONFIG_SCHEMA = cv.All(
             ),
             cv.Optional(CONF_ON_OFFLINE): automation.validate_automation(
                 {
-                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOnlineTrigger),
+                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOfflineTrigger),
                 }
             ),
         }

From e02f3cdac7b7e8a58711763b5d7f9dfac8ea6991 Mon Sep 17 00:00:00 2001
From: Ramil Valitov <ramilvalitov@gmail.com>
Date: Mon, 25 Nov 2024 01:23:30 +0300
Subject: [PATCH 155/282] [fix] Status sensor does not check if required
 network component is missing (#7734)

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

diff --git a/esphome/components/status/binary_sensor.py b/esphome/components/status/binary_sensor.py
index 1f2b7c9d18..adc342ed4d 100644
--- a/esphome/components/status/binary_sensor.py
+++ b/esphome/components/status/binary_sensor.py
@@ -6,6 +6,8 @@ from esphome.const import (
     ENTITY_CATEGORY_DIAGNOSTIC,
 )
 
+DEPENDENCIES = ["network"]
+
 status_ns = cg.esphome_ns.namespace("status")
 StatusBinarySensor = status_ns.class_(
     "StatusBinarySensor", binary_sensor.BinarySensor, cg.Component

From 59653ec7853a349e9e736710867daedc017a725e Mon Sep 17 00:00:00 2001
From: Samuel Sieb <samuel-github@sieb.net>
Date: Sun, 24 Nov 2024 12:40:28 -1000
Subject: [PATCH 156/282] allow multiple graphical menus (#7809)

Co-authored-by: Samuel Sieb <samuel@sieb.net>
---
 esphome/components/display_menu_base/__init__.py      | 2 --
 esphome/components/graphical_display_menu/__init__.py | 2 ++
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/esphome/components/display_menu_base/__init__.py b/esphome/components/display_menu_base/__init__.py
index 8ae9cbc2a4..f9c0424104 100644
--- a/esphome/components/display_menu_base/__init__.py
+++ b/esphome/components/display_menu_base/__init__.py
@@ -68,8 +68,6 @@ IsActiveCondition = display_menu_base_ns.class_(
     "IsActiveCondition", automation.Condition
 )
 
-MULTI_CONF = True
-
 MenuItemType = display_menu_base_ns.enum("MenuItemType")
 
 MENU_ITEM_TYPES = {
diff --git a/esphome/components/graphical_display_menu/__init__.py b/esphome/components/graphical_display_menu/__init__.py
index f4d59b22b8..56b720e75c 100644
--- a/esphome/components/graphical_display_menu/__init__.py
+++ b/esphome/components/graphical_display_menu/__init__.py
@@ -36,6 +36,8 @@ CODEOWNERS = ["@MrMDavidson"]
 
 AUTO_LOAD = ["display_menu_base"]
 
+MULTI_CONF = True
+
 CONFIG_SCHEMA = DISPLAY_MENU_BASE_SCHEMA.extend(
     cv.Schema(
         {

From b95b4a069417ea6fd31f96b5e9150ca16dd1c3f5 Mon Sep 17 00:00:00 2001
From: Samuel Sieb <samuel-github@sieb.net>
Date: Sun, 24 Nov 2024 12:40:51 -1000
Subject: [PATCH 157/282] keypad binary sensors should be initially off (#7808)

Co-authored-by: Samuel Sieb <samuel@sieb.net>
---
 esphome/components/binary_sensor/binary_sensor.h                | 2 +-
 .../matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h
index 301a472810..57cae9e2f5 100644
--- a/esphome/components/binary_sensor/binary_sensor.h
+++ b/esphome/components/binary_sensor/binary_sensor.h
@@ -58,7 +58,7 @@ class BinarySensor : public EntityBase, public EntityBase_DeviceClass {
   void publish_initial_state(bool state);
 
   /// The current reported state of the binary sensor.
-  bool state;
+  bool state{false};
 
   void add_filter(Filter *filter);
   void add_filters(const std::vector<Filter *> &filters);
diff --git a/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h b/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h
index d8a217f55e..2c1ce96f0a 100644
--- a/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h
+++ b/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h
@@ -6,7 +6,7 @@
 namespace esphome {
 namespace matrix_keypad {
 
-class MatrixKeypadBinarySensor : public MatrixKeypadListener, public binary_sensor::BinarySensor {
+class MatrixKeypadBinarySensor : public MatrixKeypadListener, public binary_sensor::BinarySensorInitiallyOff {
  public:
   MatrixKeypadBinarySensor(uint8_t key) : has_key_(true), key_(key){};
   MatrixKeypadBinarySensor(const char *key) : has_key_(true), key_((uint8_t) key[0]){};

From 71496574e9e104433ded1f4da497454b1b266e3d Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Mon, 25 Nov 2024 17:26:36 +1300
Subject: [PATCH 158/282] Move ``CONF_NAME_ADD_MAC_SUFFIX`` to ``const.py``
 (#7820)

---
 esphome/const.py       | 1 +
 esphome/core/config.py | 3 +--
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/esphome/const.py b/esphome/const.py
index 6a643e1e30..50528b7363 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -528,6 +528,7 @@ CONF_MULTIPLE = "multiple"
 CONF_MULTIPLEXER = "multiplexer"
 CONF_MULTIPLY = "multiply"
 CONF_NAME = "name"
+CONF_NAME_ADD_MAC_SUFFIX = "name_add_mac_suffix"
 CONF_NAME_FONT = "name_font"
 CONF_NBITS = "nbits"
 CONF_NEC = "nec"
diff --git a/esphome/core/config.py b/esphome/core/config.py
index 367e61c413..eee8b73934 100644
--- a/esphome/core/config.py
+++ b/esphome/core/config.py
@@ -21,6 +21,7 @@ from esphome.const import (
     CONF_LIBRARIES,
     CONF_MIN_VERSION,
     CONF_NAME,
+    CONF_NAME_ADD_MAC_SUFFIX,
     CONF_ON_BOOT,
     CONF_ON_LOOP,
     CONF_ON_SHUTDOWN,
@@ -59,8 +60,6 @@ ProjectUpdateTrigger = cg.esphome_ns.class_(
 
 VERSION_REGEX = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:[ab]\d+)?$")
 
-CONF_NAME_ADD_MAC_SUFFIX = "name_add_mac_suffix"
-
 
 VALID_INCLUDE_EXTS = {".h", ".hpp", ".tcc", ".ino", ".cpp", ".c"}
 

From c49f7293fef3c052691f388b6c9af6f8709b7aca Mon Sep 17 00:00:00 2001
From: Samuel Sieb <samuel-github@sieb.net>
Date: Sun, 24 Nov 2024 21:24:23 -1000
Subject: [PATCH 159/282] binary_sensor for switch state (#7819)

---
 CODEOWNERS                                    |  1 +
 .../switch/binary_sensor/__init__.py          | 31 +++++++++++++++++++
 .../binary_sensor/switch_binary_sensor.cpp    | 17 ++++++++++
 .../binary_sensor/switch_binary_sensor.h      | 22 +++++++++++++
 tests/components/switch/common.yaml           | 11 +++++++
 tests/components/switch/test.bk72xx-ard.yaml  |  2 ++
 tests/components/switch/test.esp32-ard.yaml   |  2 ++
 .../components/switch/test.esp32-c3-ard.yaml  |  2 ++
 .../components/switch/test.esp32-c3-idf.yaml  |  2 ++
 tests/components/switch/test.esp32-idf.yaml   |  2 ++
 .../components/switch/test.esp32-s3-idf.yaml  |  2 ++
 tests/components/switch/test.esp8266-ard.yaml |  2 ++
 tests/components/switch/test.rp2040-ard.yaml  |  2 ++
 13 files changed, 98 insertions(+)
 create mode 100644 esphome/components/switch/binary_sensor/__init__.py
 create mode 100644 esphome/components/switch/binary_sensor/switch_binary_sensor.cpp
 create mode 100644 esphome/components/switch/binary_sensor/switch_binary_sensor.h
 create mode 100644 tests/components/switch/common.yaml
 create mode 100644 tests/components/switch/test.bk72xx-ard.yaml
 create mode 100644 tests/components/switch/test.esp32-ard.yaml
 create mode 100644 tests/components/switch/test.esp32-c3-ard.yaml
 create mode 100644 tests/components/switch/test.esp32-c3-idf.yaml
 create mode 100644 tests/components/switch/test.esp32-idf.yaml
 create mode 100644 tests/components/switch/test.esp32-s3-idf.yaml
 create mode 100644 tests/components/switch/test.esp8266-ard.yaml
 create mode 100644 tests/components/switch/test.rp2040-ard.yaml

diff --git a/CODEOWNERS b/CODEOWNERS
index 8fbbacef59..dd3926d283 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -408,6 +408,7 @@ esphome/components/substitutions/* @esphome/core
 esphome/components/sun/* @OttoWinter
 esphome/components/sun_gtil2/* @Mat931
 esphome/components/switch/* @esphome/core
+esphome/components/switch/binary_sensor/* @ssieb
 esphome/components/t6615/* @tylermenezes
 esphome/components/tc74/* @sethgirvan
 esphome/components/tca9548a/* @andreashergert1984
diff --git a/esphome/components/switch/binary_sensor/__init__.py b/esphome/components/switch/binary_sensor/__init__.py
new file mode 100644
index 0000000000..61ca1a14a2
--- /dev/null
+++ b/esphome/components/switch/binary_sensor/__init__.py
@@ -0,0 +1,31 @@
+import esphome.codegen as cg
+from esphome.components import binary_sensor
+import esphome.config_validation as cv
+from esphome.const import CONF_SOURCE_ID
+
+from .. import Switch, switch_ns
+
+CODEOWNERS = ["@ssieb"]
+
+SwitchBinarySensor = switch_ns.class_(
+    "SwitchBinarySensor", binary_sensor.BinarySensor, cg.Component
+)
+
+
+CONFIG_SCHEMA = (
+    binary_sensor.binary_sensor_schema(SwitchBinarySensor)
+    .extend(
+        {
+            cv.Required(CONF_SOURCE_ID): cv.use_id(Switch),
+        }
+    )
+    .extend(cv.COMPONENT_SCHEMA)
+)
+
+
+async def to_code(config):
+    var = await binary_sensor.new_binary_sensor(config)
+    await cg.register_component(var, config)
+
+    source = await cg.get_variable(config[CONF_SOURCE_ID])
+    cg.add(var.set_source(source))
diff --git a/esphome/components/switch/binary_sensor/switch_binary_sensor.cpp b/esphome/components/switch/binary_sensor/switch_binary_sensor.cpp
new file mode 100644
index 0000000000..ba57154446
--- /dev/null
+++ b/esphome/components/switch/binary_sensor/switch_binary_sensor.cpp
@@ -0,0 +1,17 @@
+#include "switch_binary_sensor.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace switch_ {
+
+static const char *const TAG = "switch.binary_sensor";
+
+void SwitchBinarySensor::setup() {
+  source_->add_on_state_callback([this](bool value) { this->publish_state(value); });
+  this->publish_state(source_->state);
+}
+
+void SwitchBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Switch Binary Sensor", this); }
+
+}  // namespace switch_
+}  // namespace esphome
diff --git a/esphome/components/switch/binary_sensor/switch_binary_sensor.h b/esphome/components/switch/binary_sensor/switch_binary_sensor.h
new file mode 100644
index 0000000000..5a947c2fb4
--- /dev/null
+++ b/esphome/components/switch/binary_sensor/switch_binary_sensor.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "../switch.h"
+#include "esphome/core/component.h"
+#include "esphome/components/binary_sensor/binary_sensor.h"
+
+namespace esphome {
+namespace switch_ {
+
+class SwitchBinarySensor : public binary_sensor::BinarySensor, public Component {
+ public:
+  void set_source(Switch *source) { source_ = source; }
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override { return setup_priority::DATA; }
+
+ protected:
+  Switch *source_;
+};
+
+}  // namespace switch_
+}  // namespace esphome
diff --git a/tests/components/switch/common.yaml b/tests/components/switch/common.yaml
new file mode 100644
index 0000000000..8d6972f91b
--- /dev/null
+++ b/tests/components/switch/common.yaml
@@ -0,0 +1,11 @@
+binary_sensor:
+  - platform: switch
+    id: some_binary_sensor
+    name: "Template Switch State"
+    source_id: the_switch
+
+switch:
+  - platform: template
+    name: "Template Switch"
+    id: the_switch
+    optimistic: true
diff --git a/tests/components/switch/test.bk72xx-ard.yaml b/tests/components/switch/test.bk72xx-ard.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/switch/test.bk72xx-ard.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/switch/test.esp32-ard.yaml b/tests/components/switch/test.esp32-ard.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/switch/test.esp32-ard.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/switch/test.esp32-c3-ard.yaml b/tests/components/switch/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/switch/test.esp32-c3-ard.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/switch/test.esp32-c3-idf.yaml b/tests/components/switch/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/switch/test.esp32-c3-idf.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/switch/test.esp32-idf.yaml b/tests/components/switch/test.esp32-idf.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/switch/test.esp32-idf.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/switch/test.esp32-s3-idf.yaml b/tests/components/switch/test.esp32-s3-idf.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/switch/test.esp32-s3-idf.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/switch/test.esp8266-ard.yaml b/tests/components/switch/test.esp8266-ard.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/switch/test.esp8266-ard.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/switch/test.rp2040-ard.yaml b/tests/components/switch/test.rp2040-ard.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/switch/test.rp2040-ard.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml

From 7f75f2135d74c6daaaf5251c12a3a1f986ec6694 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 25 Nov 2024 02:22:50 -0600
Subject: [PATCH 160/282] [nextion] Remove assignment within `if` (#7824)

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

diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp
index b5bb5478c1..7541a57d56 100644
--- a/esphome/components/nextion/nextion_upload_idf.cpp
+++ b/esphome/components/nextion/nextion_upload_idf.cpp
@@ -36,8 +36,8 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r
   ESP_LOGV(TAG, "Requesting range: %s", range_header);
   esp_http_client_set_header(http_client, "Range", range_header);
   ESP_LOGV(TAG, "Opening HTTP connetion");
-  esp_err_t err;
-  if ((err = esp_http_client_open(http_client, 0)) != ESP_OK) {
+  esp_err_t err = esp_http_client_open(http_client, 0);
+  if (err != ESP_OK) {
     ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
     return -1;
   }

From 6c548a15966b0486226d0fd7c33a105b6776d3f4 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 25 Nov 2024 02:23:00 -0600
Subject: [PATCH 161/282] [ota] `void` functions should return nothing (#7825)

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

diff --git a/esphome/components/ota/automation.h b/esphome/components/ota/automation.h
index 4605193480..7e1a60f3ce 100644
--- a/esphome/components/ota/automation.h
+++ b/esphome/components/ota/automation.h
@@ -12,7 +12,7 @@ class OTAStateChangeTrigger : public Trigger<OTAState> {
   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);
+        trigger(state);
       }
     });
   }

From 46a435f5f2ec8f8dfb306f953280b5b2be265195 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 25 Nov 2024 02:24:35 -0600
Subject: [PATCH 162/282] [safe_mode] Remove unused capture (#7826)

---
 esphome/components/safe_mode/automation.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/safe_mode/automation.h b/esphome/components/safe_mode/automation.h
index d1388449ee..1ffa86a588 100644
--- a/esphome/components/safe_mode/automation.h
+++ b/esphome/components/safe_mode/automation.h
@@ -9,7 +9,7 @@ namespace safe_mode {
 class SafeModeTrigger : public Trigger<> {
  public:
   explicit SafeModeTrigger(SafeModeComponent *parent) {
-    parent->add_on_safe_mode_callback([this, parent]() { trigger(); });
+    parent->add_on_safe_mode_callback([this]() { trigger(); });
   }
 };
 

From ebf895990b494ec8c09b7dbab051b0d53cc2d029 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 25 Nov 2024 02:25:04 -0600
Subject: [PATCH 163/282] [stepper] Remove unnecessary ``#include`` (#7827)

---
 esphome/components/stepper/stepper.h | 1 -
 1 file changed, 1 deletion(-)

diff --git a/esphome/components/stepper/stepper.h b/esphome/components/stepper/stepper.h
index 560362e4d0..ba2b3182d7 100644
--- a/esphome/components/stepper/stepper.h
+++ b/esphome/components/stepper/stepper.h
@@ -2,7 +2,6 @@
 
 #include "esphome/core/component.h"
 #include "esphome/core/automation.h"
-#include "esphome/components/stepper/stepper.h"
 
 namespace esphome {
 namespace stepper {

From aa6cea6f7e4ea4b656fa6d3a152581e623227bd3 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 25 Nov 2024 02:27:36 -0600
Subject: [PATCH 164/282] [sx1509] Fix up includes (#7828)

---
 esphome/components/sx1509/sx1509_gpio_pin.cpp |  3 ++-
 esphome/components/sx1509/sx1509_gpio_pin.h   | 10 +++++-----
 2 files changed, 7 insertions(+), 6 deletions(-)

diff --git a/esphome/components/sx1509/sx1509_gpio_pin.cpp b/esphome/components/sx1509/sx1509_gpio_pin.cpp
index 56b51ae311..a74c8b60b8 100644
--- a/esphome/components/sx1509/sx1509_gpio_pin.cpp
+++ b/esphome/components/sx1509/sx1509_gpio_pin.cpp
@@ -1,5 +1,6 @@
 #include "esphome/core/helpers.h"
 #include "esphome/core/log.h"
+#include "sx1509.h"
 #include "sx1509_gpio_pin.h"
 
 namespace esphome {
@@ -13,7 +14,7 @@ bool SX1509GPIOPin::digital_read() { return this->parent_->digital_read(this->pi
 void SX1509GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
 std::string SX1509GPIOPin::dump_summary() const {
   char buffer[32];
-  snprintf(buffer, sizeof(buffer), "%u via sx1509", pin_);
+  snprintf(buffer, sizeof(buffer), "%u via sx1509", this->pin_);
   return buffer;
 }
 
diff --git a/esphome/components/sx1509/sx1509_gpio_pin.h b/esphome/components/sx1509/sx1509_gpio_pin.h
index 4d8aa5ec83..1cfa341ee7 100644
--- a/esphome/components/sx1509/sx1509_gpio_pin.h
+++ b/esphome/components/sx1509/sx1509_gpio_pin.h
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "sx1509.h"
+#include "esphome/core/gpio.h"
 
 namespace esphome {
 namespace sx1509 {
@@ -15,10 +15,10 @@ class SX1509GPIOPin : public GPIOPin {
   void digital_write(bool value) override;
   std::string dump_summary() const override;
 
-  void set_parent(SX1509Component *parent) { parent_ = parent; }
-  void set_pin(uint8_t pin) { pin_ = pin; }
-  void set_inverted(bool inverted) { inverted_ = inverted; }
-  void set_flags(gpio::Flags flags) { flags_ = flags; }
+  void set_parent(SX1509Component *parent) { this->parent_ = parent; }
+  void set_pin(uint8_t pin) { this->pin_ = pin; }
+  void set_inverted(bool inverted) { this->inverted_ = inverted; }
+  void set_flags(gpio::Flags flags) { this->flags_ = flags; }
 
  protected:
   SX1509Component *parent_;

From 1bd2d41ffd1273eefa03aa331944d7f47d5cd7c9 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 25 Nov 2024 02:39:22 -0600
Subject: [PATCH 165/282] [uart] `void` functions should return nothing (#7829)

---
 esphome/components/uart/uart.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h
index d41dbe26e6..dc6962fbae 100644
--- a/esphome/components/uart/uart.h
+++ b/esphome/components/uart/uart.h
@@ -40,7 +40,7 @@ class UARTDevice {
 
   int available() { return this->parent_->available(); }
 
-  void flush() { return this->parent_->flush(); }
+  void flush() { this->parent_->flush(); }
 
   // Compat APIs
   int read() {

From 17a09cd22151d95d510c858253b78dadae8e53e8 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 25 Nov 2024 03:50:18 -0600
Subject: [PATCH 166/282] [audio] Header modernization (#7832)

---
 esphome/components/audio/audio.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/audio/audio.h b/esphome/components/audio/audio.h
index b0968dc8da..caf325cf54 100644
--- a/esphome/components/audio/audio.h
+++ b/esphome/components/audio/audio.h
@@ -1,7 +1,7 @@
 #pragma once
 
+#include <cstddef>
 #include <cstdint>
-#include <stddef.h>
 
 namespace esphome {
 namespace audio {

From cf835d15806481c50361cd6d8d193cc7b0b0175e Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 25 Nov 2024 03:50:24 -0600
Subject: [PATCH 167/282] [opentherm] Follow variable naming convention (#7833)

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

diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp
index 26c707f9a0..78ecb53428 100644
--- a/esphome/components/opentherm/opentherm.cpp
+++ b/esphome/components/opentherm/opentherm.cpp
@@ -29,7 +29,7 @@ using std::to_string;
 static const char *const TAG = "opentherm";
 
 #ifdef ESP8266
-OpenTherm *OpenTherm::instance_ = nullptr;
+OpenTherm *OpenTherm::instance = nullptr;
 #endif
 
 OpenTherm::OpenTherm(InternalGPIOPin *in_pin, InternalGPIOPin *out_pin, int32_t device_timeout)
@@ -53,7 +53,7 @@ OpenTherm::OpenTherm(InternalGPIOPin *in_pin, InternalGPIOPin *out_pin, int32_t
 
 bool OpenTherm::initialize() {
 #ifdef ESP8266
-  OpenTherm::instance_ = this;
+  OpenTherm::instance = this;
 #endif
   this->in_pin_->pin_mode(gpio::FLAG_INPUT);
   this->out_pin_->pin_mode(gpio::FLAG_OUTPUT);
@@ -216,7 +216,7 @@ bool IRAM_ATTR OpenTherm::timer_isr(OpenTherm *arg) {
 }
 
 #ifdef ESP8266
-void IRAM_ATTR OpenTherm::esp8266_timer_isr() { OpenTherm::timer_isr(OpenTherm::instance_); }
+void IRAM_ATTR OpenTherm::esp8266_timer_isr() { OpenTherm::timer_isr(OpenTherm::instance); }
 #endif
 
 void IRAM_ATTR OpenTherm::bit_read_(uint8_t value) {
diff --git a/esphome/components/opentherm/opentherm.h b/esphome/components/opentherm/opentherm.h
index 85f4611125..5088bb2aa3 100644
--- a/esphome/components/opentherm/opentherm.h
+++ b/esphome/components/opentherm/opentherm.h
@@ -371,7 +371,7 @@ class OpenTherm {
 
 #ifdef ESP8266
   // ESP8266 timer can accept callback with no parameters, so we have this hack to save a static instance of OpenTherm
-  static OpenTherm *instance_;
+  static OpenTherm *instance;
 #endif
 };
 

From 89ecfc20049d2358ecdbe63dacaed9143c9d9ed5 Mon Sep 17 00:00:00 2001
From: Oleg Tarasov <me@olegtarasov.email>
Date: Tue, 26 Nov 2024 00:47:01 +0300
Subject: [PATCH 168/282] [opentherm] Fix out of memory errors on ESP8266
 (#7835)

---
 esphome/components/opentherm/hub.cpp       | 13 ++++----
 esphome/components/opentherm/opentherm.cpp | 36 ++++++----------------
 esphome/components/opentherm/opentherm.h   |  7 ++---
 esphome/core/helpers.cpp                   | 12 ++++++++
 esphome/core/helpers.h                     |  8 +++++
 5 files changed, 39 insertions(+), 37 deletions(-)

diff --git a/esphome/components/opentherm/hub.cpp b/esphome/components/opentherm/hub.cpp
index dfa8ea95c5..aac2966ed1 100644
--- a/esphome/components/opentherm/hub.cpp
+++ b/esphome/components/opentherm/hub.cpp
@@ -138,7 +138,7 @@ OpenthermHub::OpenthermHub() : Component(), in_pin_{}, out_pin_{} {}
 void OpenthermHub::process_response(OpenthermData &data) {
   ESP_LOGD(TAG, "Received OpenTherm response with id %d (%s)", data.id,
            this->opentherm_->message_id_to_str((MessageId) data.id));
-  ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(data).c_str());
+  this->opentherm_->debug_data(data);
 
   switch (data.id) {
     OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_RESPONSE_MESSAGE, OPENTHERM_MESSAGE_RESPONSE_ENTITY, ,
@@ -315,7 +315,7 @@ void OpenthermHub::start_conversation_() {
 
   ESP_LOGD(TAG, "Sending request with id %d (%s)", request.id,
            this->opentherm_->message_id_to_str((MessageId) request.id));
-  ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(request).c_str());
+  this->opentherm_->debug_data(request);
   // Send the request
   this->last_conversation_start_ = millis();
   this->opentherm_->send(request);
@@ -340,19 +340,18 @@ void OpenthermHub::stop_opentherm_() {
   this->opentherm_->stop();
   this->last_conversation_end_ = millis();
 }
-
 void OpenthermHub::handle_protocol_write_error_() {
   ESP_LOGW(TAG, "Error while sending request: %s",
            this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode()));
-  ESP_LOGW(TAG, "%s", this->opentherm_->debug_data(this->last_request_).c_str());
+  this->opentherm_->debug_data(this->last_request_);
 }
-
 void OpenthermHub::handle_protocol_read_error_() {
   OpenThermError error;
   this->opentherm_->get_protocol_error(error);
-  ESP_LOGW(TAG, "Protocol error occured while receiving response: %s", this->opentherm_->debug_error(error).c_str());
+  ESP_LOGW(TAG, "Protocol error occured while receiving response: %s",
+           this->opentherm_->protocol_error_to_to_str(error.error_type));
+  this->opentherm_->debug_error(error);
 }
-
 void OpenthermHub::handle_timeout_error_() {
   ESP_LOGW(TAG, "Receive response timed out at a protocol level");
   this->stop_opentherm_();
diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp
index 78ecb53428..e40fc66b7d 100644
--- a/esphome/components/opentherm/opentherm.cpp
+++ b/esphome/components/opentherm/opentherm.cpp
@@ -15,15 +15,11 @@
 #include "Arduino.h"
 #endif
 #include <string>
-#include <sstream>
-#include <bitset>
 
 namespace esphome {
 namespace opentherm {
 
 using std::string;
-using std::bitset;
-using std::stringstream;
 using std::to_string;
 
 static const char *const TAG = "opentherm";
@@ -545,29 +541,17 @@ const char *OpenTherm::message_id_to_str(MessageId id) {
   }
 }
 
-string OpenTherm::debug_data(OpenthermData &data) {
-  stringstream result;
-  result << bitset<8>(data.type) << " " << bitset<8>(data.id) << " " << bitset<8>(data.valueHB) << " "
-         << bitset<8>(data.valueLB) << "\n";
-  result << "type: " << this->message_type_to_str((MessageType) data.type) << "; ";
-  result << "id: " << to_string(data.id) << "; ";
-  result << "HB: " << to_string(data.valueHB) << "; ";
-  result << "LB: " << to_string(data.valueLB) << "; ";
-  result << "uint_16: " << to_string(data.u16()) << "; ";
-  result << "float: " << to_string(data.f88());
-
-  return result.str();
+void OpenTherm::debug_data(OpenthermData &data) {
+  ESP_LOGD(TAG, "%s %s %s %s", format_bin(data.type).c_str(), format_bin(data.id).c_str(),
+           format_bin(data.valueHB).c_str(), format_bin(data.valueLB).c_str());
+  ESP_LOGD(TAG, "type: %s; id: %s; HB: %s; LB: %s; uint_16: %s; float: %s",
+           this->message_type_to_str((MessageType) data.type), to_string(data.id).c_str(),
+           to_string(data.valueHB).c_str(), to_string(data.valueLB).c_str(), to_string(data.u16()).c_str(),
+           to_string(data.f88()).c_str());
 }
-std::string OpenTherm::debug_error(OpenThermError &error) {
-  stringstream result;
-  result << "type: " << this->protocol_error_to_to_str(error.error_type) << "; ";
-  result << "data: ";
-  result << format_hex(error.data);
-  result << "; clock: " << to_string(clock_);
-  result << "; capture: " << bitset<32>(error.capture);
-  result << "; bit_pos: " << to_string(error.bit_pos);
-
-  return result.str();
+void OpenTherm::debug_error(OpenThermError &error) const {
+  ESP_LOGD(TAG, "data: %s; clock: %s; capture: %s; bit_pos: %s", format_hex(error.data).c_str(),
+           to_string(clock_).c_str(), format_bin(error.capture).c_str(), to_string(error.bit_pos).c_str());
 }
 
 float OpenthermData::f88() { return ((float) this->s16()) / 256.0; }
diff --git a/esphome/components/opentherm/opentherm.h b/esphome/components/opentherm/opentherm.h
index 5088bb2aa3..9532a77821 100644
--- a/esphome/components/opentherm/opentherm.h
+++ b/esphome/components/opentherm/opentherm.h
@@ -8,10 +8,9 @@
 #pragma once
 
 #include <string>
-#include <sstream>
-#include <iomanip>
 #include "esphome/core/hal.h"
 #include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
 
 #if defined(ESP32) || defined(USE_ESP_IDF)
 #include "driver/timer.h"
@@ -318,8 +317,8 @@ class OpenTherm {
 
   OperationMode get_mode() { return mode_; }
 
-  std::string debug_data(OpenthermData &data);
-  std::string debug_error(OpenThermError &error);
+  void debug_data(OpenthermData &data);
+  void debug_error(OpenThermError &error) const;
 
   const char *protocol_error_to_to_str(ProtocolErrorType error_type);
   const char *message_type_to_str(MessageType message_type);
diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp
index dae60a4e1d..befc84516c 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -397,6 +397,18 @@ std::string format_hex_pretty(const uint16_t *data, size_t length) {
 }
 std::string format_hex_pretty(const std::vector<uint16_t> &data) { return format_hex_pretty(data.data(), data.size()); }
 
+std::string format_bin(const uint8_t *data, size_t length) {
+  std::string result;
+  result.resize(length * 8);
+  for (size_t byte_idx = 0; byte_idx < length; byte_idx++) {
+    for (size_t bit_idx = 0; bit_idx < 8; bit_idx++) {
+      result[byte_idx * 8 + bit_idx] = ((data[byte_idx] >> (7 - bit_idx)) & 1) + '0';
+    }
+  }
+
+  return result;
+}
+
 ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) {
   if (on == nullptr && strcasecmp(str, "on") == 0)
     return PARSE_ON;
diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h
index 43001bafdd..305ec47f76 100644
--- a/esphome/core/helpers.h
+++ b/esphome/core/helpers.h
@@ -420,6 +420,14 @@ template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::stri
   return format_hex_pretty(reinterpret_cast<uint8_t *>(&val), sizeof(T));
 }
 
+/// Format the byte array \p data of length \p len in binary.
+std::string format_bin(const uint8_t *data, size_t length);
+/// Format an unsigned integer in binary, starting with the most significant byte.
+template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_bin(T val) {
+  val = convert_big_endian(val);
+  return format_bin(reinterpret_cast<uint8_t *>(&val), sizeof(T));
+}
+
 /// Return values for parse_on_off().
 enum ParseOnOffState {
   PARSE_NONE = 0,

From b027b6a711be401153847ca6607a4f7b90bc6688 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 25 Nov 2024 15:57:40 -0600
Subject: [PATCH 169/282] [opentherm] Add nolint for 8266 static global (#7837)

---
 esphome/components/opentherm/opentherm.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/opentherm/opentherm.h b/esphome/components/opentherm/opentherm.h
index 9532a77821..3be0191c63 100644
--- a/esphome/components/opentherm/opentherm.h
+++ b/esphome/components/opentherm/opentherm.h
@@ -370,7 +370,7 @@ class OpenTherm {
 
 #ifdef ESP8266
   // ESP8266 timer can accept callback with no parameters, so we have this hack to save a static instance of OpenTherm
-  static OpenTherm *instance;
+  static OpenTherm *instance;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 #endif
 };
 

From bdb91112eada52df7c31a0b86e2f6f3b16ab9a49 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 25 Nov 2024 16:20:03 -0600
Subject: [PATCH 170/282] [helpers] Add NOLINT for Mutex private field
 ``handle_`` (#7838)

---
 esphome/core/helpers.h | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h
index 305ec47f76..fcbd8d8683 100644
--- a/esphome/core/helpers.h
+++ b/esphome/core/helpers.h
@@ -565,7 +565,8 @@ class Mutex {
 #if defined(USE_ESP32) || defined(USE_LIBRETINY)
   SemaphoreHandle_t handle_;
 #else
-  void *handle_;  // d-pointer to store private data on new platforms
+  // d-pointer to store private data on new platforms
+  void *handle_;  // NOLINT(clang-diagnostic-unused-private-field)
 #endif
 };
 

From d6f4f0509081ea38cfe799806c8225903e6cd1be Mon Sep 17 00:00:00 2001
From: programmingbgloDE <47243850+programmingbgloDE@users.noreply.github.com>
Date: Mon, 25 Nov 2024 23:26:48 +0100
Subject: [PATCH 171/282] Add waveshare 1 45 in v2 b support (#7052)

---
 .../components/waveshare_epaper/display.py    |  4 +
 .../waveshare_epaper/waveshare_epaper.cpp     | 84 +++++++++++++++++++
 .../waveshare_epaper/waveshare_epaper.h       | 18 ++++
 3 files changed, 106 insertions(+)

diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py
index 8287788de5..fbb5e1353d 100644
--- a/esphome/components/waveshare_epaper/display.py
+++ b/esphome/components/waveshare_epaper/display.py
@@ -27,6 +27,9 @@ WaveshareEPaperBWR = waveshare_epaper_ns.class_(
 WaveshareEPaperTypeA = waveshare_epaper_ns.class_(
     "WaveshareEPaperTypeA", WaveshareEPaper
 )
+WaveshareEpaper1P54INBV2 = waveshare_epaper_ns.class_(
+    "WaveshareEPaper1P54InBV2", WaveshareEPaperBWR
+)
 WaveshareEPaper2P7In = waveshare_epaper_ns.class_(
     "WaveshareEPaper2P7In", WaveshareEPaper
 )
@@ -105,6 +108,7 @@ WaveshareEPaperTypeBModel = waveshare_epaper_ns.enum("WaveshareEPaperTypeBModel"
 MODELS = {
     "1.54in": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_1_54_IN),
     "1.54inv2": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_1_54_IN_V2),
+    "1.54inv2-b": ("b", WaveshareEpaper1P54INBV2),
     "2.13in": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_13_IN),
     "2.13inv2": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_13_IN_V2),
     "2.13in-ttgo": ("a", WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN),
diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp
index 7c1d436673..1e27d594b8 100644
--- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp
+++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp
@@ -808,6 +808,90 @@ void WaveshareEPaper2P7InV2::dump_config() {
   LOG_UPDATE_INTERVAL(this);
 }
 
+// ========================================================
+//                          1.54inch_v2_e-paper_b
+// ========================================================
+// Datasheet:
+//  - https://files.waveshare.com/upload/9/9e/1.54inch-e-paper-b-v2-specification.pdf
+//  - https://www.waveshare.com/wiki/1.54inch_e-Paper_Module_(B)_Manual
+
+void WaveshareEPaper1P54InBV2::initialize() {
+  this->reset_();
+
+  this->wait_until_idle_();
+
+  this->command(0x12);
+  this->wait_until_idle_();
+
+  this->command(0x01);
+  this->data(0xC7);
+  this->data(0x00);
+  this->data(0x01);
+
+  this->command(0x11);  // data entry mode
+  this->data(0x01);
+
+  this->command(0x44);  // set Ram-X address start/end position
+  this->data(0x00);
+  this->data(0x18);  // 0x18-->(24+1)*8=200
+
+  this->command(0x45);  // set Ram-Y address start/end position
+  this->data(0xC7);     // 0xC7-->(199+1)=200
+  this->data(0x00);
+  this->data(0x00);
+  this->data(0x00);
+
+  this->command(0x3C);  // BorderWavefrom
+  this->data(0x05);
+
+  this->command(0x18);  // Read built-in temperature sensor
+  this->data(0x80);
+
+  this->command(0x4E);  // set RAM x address count to 0;
+  this->data(0x00);
+  this->command(0x4F);  // set RAM y address count to 0X199;
+  this->data(0xC7);
+  this->data(0x00);
+
+  this->wait_until_idle_();
+}
+
+void HOT WaveshareEPaper1P54InBV2::display() {
+  uint32_t buf_len_half = this->get_buffer_length_() >> 1;
+  this->initialize();
+
+  // COMMAND DATA START TRANSMISSION 1 (BLACK)
+  this->command(0x24);
+  delay(2);
+  for (uint32_t i = 0; i < buf_len_half; i++) {
+    this->data(~this->buffer_[i]);
+  }
+  delay(2);
+
+  // COMMAND DATA START TRANSMISSION 2  (RED)
+  this->command(0x26);
+  delay(2);
+  for (uint32_t i = buf_len_half; i < buf_len_half * 2u; i++) {
+    this->data(this->buffer_[i]);
+  }
+  this->command(0x22);
+  this->data(0xf7);
+  this->command(0x20);
+  this->wait_until_idle_();
+
+  this->deep_sleep();
+}
+int WaveshareEPaper1P54InBV2::get_height_internal() { return 200; }
+int WaveshareEPaper1P54InBV2::get_width_internal() { return 200; }
+void WaveshareEPaper1P54InBV2::dump_config() {
+  LOG_DISPLAY("", "Waveshare E-Paper", this);
+  ESP_LOGCONFIG(TAG, "  Model: 1.54in V2 B");
+  LOG_PIN("  Reset Pin: ", this->reset_pin_);
+  LOG_PIN("  DC Pin: ", this->dc_pin_);
+  LOG_PIN("  Busy Pin: ", this->busy_pin_);
+  LOG_UPDATE_INTERVAL(this);
+}
+
 // ========================================================
 //                          2.7inch_e-paper_b
 // ========================================================
diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h
index 7572982a20..a319b078d0 100644
--- a/esphome/components/waveshare_epaper/waveshare_epaper.h
+++ b/esphome/components/waveshare_epaper/waveshare_epaper.h
@@ -166,6 +166,24 @@ enum WaveshareEPaperTypeBModel {
   WAVESHARE_EPAPER_13_3_IN_K,
 };
 
+class WaveshareEPaper1P54InBV2 : public WaveshareEPaperBWR {
+ public:
+  void initialize() override;
+
+  void display() override;
+
+  void dump_config() override;
+
+  void deep_sleep() override {
+    this->command(0x10);
+    this->data(0x01);
+  }
+
+ protected:
+  int get_width_internal() override;
+  int get_height_internal() override;
+};
+
 class WaveshareEPaper2P7In : public WaveshareEPaper {
  public:
   void initialize() override;

From 140d77061b33895054b35c6e1245fe4217f81a1e Mon Sep 17 00:00:00 2001
From: JonasB2497 <45214989+JonasB2497@users.noreply.github.com>
Date: Mon, 25 Nov 2024 23:29:58 +0100
Subject: [PATCH 172/282] added Waveshare BWR Mode for the 7.5in Display
 (#7687)

---
 .../components/waveshare_epaper/display.py    |   4 +
 .../waveshare_epaper/waveshare_epaper.cpp     | 107 ++++++++++++++++++
 .../waveshare_epaper/waveshare_epaper.h       |  38 +++++++
 .../waveshare_epaper/test.esp32-ard.yaml      |  17 +++
 .../waveshare_epaper/test.esp32-c3-ard.yaml   |  16 +++
 .../waveshare_epaper/test.esp32-c3-idf.yaml   |  16 +++
 .../waveshare_epaper/test.esp32-idf.yaml      |  16 +++
 .../waveshare_epaper/test.esp8266-ard.yaml    |  16 +++
 .../waveshare_epaper/test.rp2040-ard.yaml     |  16 +++
 9 files changed, 246 insertions(+)

diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py
index fbb5e1353d..d5240b2674 100644
--- a/esphome/components/waveshare_epaper/display.py
+++ b/esphome/components/waveshare_epaper/display.py
@@ -79,6 +79,9 @@ WaveshareEPaper7P5InBV2 = waveshare_epaper_ns.class_(
 WaveshareEPaper7P5InBV3 = waveshare_epaper_ns.class_(
     "WaveshareEPaper7P5InBV3", WaveshareEPaper
 )
+WaveshareEPaper7P5InBV3BWR = waveshare_epaper_ns.class_(
+    "WaveshareEPaper7P5InBV3BWR", WaveshareEPaperBWR
+)
 WaveshareEPaper7P5InV2 = waveshare_epaper_ns.class_(
     "WaveshareEPaper7P5InV2", WaveshareEPaper
 )
@@ -133,6 +136,7 @@ MODELS = {
     "7.50in": ("b", WaveshareEPaper7P5In),
     "7.50in-bv2": ("b", WaveshareEPaper7P5InBV2),
     "7.50in-bv3": ("b", WaveshareEPaper7P5InBV3),
+    "7.50in-bv3-bwr": ("b", WaveshareEPaper7P5InBV3BWR),
     "7.50in-bc": ("b", WaveshareEPaper7P5InBC),
     "7.50inv2": ("b", WaveshareEPaper7P5InV2),
     "7.50inv2alt": ("b", WaveshareEPaper7P5InV2alt),
diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp
index 1e27d594b8..cb3b19aa1a 100644
--- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp
+++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp
@@ -2399,6 +2399,113 @@ void WaveshareEPaper7P5InBV3::dump_config() {
   LOG_UPDATE_INTERVAL(this);
 }
 
+void WaveshareEPaper7P5InBV3BWR::initialize() { this->init_display_(); }
+bool WaveshareEPaper7P5InBV3BWR::wait_until_idle_() {
+  if (this->busy_pin_ == nullptr) {
+    return true;
+  }
+
+  const uint32_t start = millis();
+  while (this->busy_pin_->digital_read()) {
+    this->command(0x71);
+    if (millis() - start > this->idle_timeout_()) {
+      ESP_LOGI(TAG, "Timeout while displaying image!");
+      return false;
+    }
+    App.feed_wdt();
+    delay(10);
+  }
+  delay(200);  // NOLINT
+  return true;
+};
+void WaveshareEPaper7P5InBV3BWR::init_display_() {
+  this->reset_();
+
+  // COMMAND POWER SETTING
+  this->command(0x01);
+
+  // 1-0=11: internal power
+  this->data(0x07);
+  this->data(0x17);  // VGH&VGL
+  this->data(0x3F);  // VSH
+  this->data(0x26);  // VSL
+  this->data(0x11);  // VSHR
+
+  // VCOM DC Setting
+  this->command(0x82);
+  this->data(0x24);  // VCOM
+
+  // Booster Setting
+  this->command(0x06);
+  this->data(0x27);
+  this->data(0x27);
+  this->data(0x2F);
+  this->data(0x17);
+
+  // POWER ON
+  this->command(0x04);
+
+  delay(100);  // NOLINT
+  this->wait_until_idle_();
+  // COMMAND PANEL SETTING
+  this->command(0x00);
+  this->data(0x0F);  // KW-3f   KWR-2F BWROTP 0f BWOTP 1f
+
+  // COMMAND RESOLUTION SETTING
+  this->command(0x61);
+  this->data(0x03);  // source 800
+  this->data(0x20);
+  this->data(0x01);  // gate 480
+  this->data(0xE0);
+  // COMMAND ...?
+  this->command(0x15);
+  this->data(0x00);
+  // COMMAND VCOM AND DATA INTERVAL SETTING
+  this->command(0x50);
+  this->data(0x20);
+  this->data(0x00);
+  // COMMAND TCON SETTING
+  this->command(0x60);
+  this->data(0x22);
+  // Resolution setting
+  this->command(0x65);
+  this->data(0x00);
+  this->data(0x00);  // 800*480
+  this->data(0x00);
+  this->data(0x00);
+};
+void HOT WaveshareEPaper7P5InBV3BWR::display() {
+  this->init_display_();
+  const uint32_t buf_len = this->get_buffer_length_() / 2u;
+
+  this->command(0x10);  // Send BW data Transmission
+  delay(2);
+  for (uint32_t i = 0; i < buf_len; i++) {
+    this->data(this->buffer_[i]);
+  }
+
+  this->command(0x13);  // Send red data Transmission
+  delay(2);
+  for (uint32_t i = 0; i < buf_len; i++) {
+    this->data(this->buffer_[i + buf_len]);
+  }
+
+  this->command(0x12);  // Display Refresh
+  delay(100);           // NOLINT
+  this->wait_until_idle_();
+  this->deep_sleep();
+}
+int WaveshareEPaper7P5InBV3BWR::get_width_internal() { return 800; }
+int WaveshareEPaper7P5InBV3BWR::get_height_internal() { return 480; }
+void WaveshareEPaper7P5InBV3BWR::dump_config() {
+  LOG_DISPLAY("", "Waveshare E-Paper", this);
+  ESP_LOGCONFIG(TAG, "  Model: 7.5in-bv3 BWR-Mode");
+  LOG_PIN("  Reset Pin: ", this->reset_pin_);
+  LOG_PIN("  DC Pin: ", this->dc_pin_);
+  LOG_PIN("  Busy Pin: ", this->busy_pin_);
+  LOG_UPDATE_INTERVAL(this);
+}
+
 void WaveshareEPaper7P5In::initialize() {
   // COMMAND POWER SETTING
   this->command(0x01);
diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h
index a319b078d0..4544f7df59 100644
--- a/esphome/components/waveshare_epaper/waveshare_epaper.h
+++ b/esphome/components/waveshare_epaper/waveshare_epaper.h
@@ -637,6 +637,44 @@ class WaveshareEPaper7P5InBV3 : public WaveshareEPaper {
   void init_display_();
 };
 
+class WaveshareEPaper7P5InBV3BWR : public WaveshareEPaperBWR {
+ public:
+  bool wait_until_idle_();
+
+  void initialize() override;
+
+  void display() override;
+
+  void dump_config() override;
+
+  void deep_sleep() override {
+    this->command(0x02);  // Power off
+    this->wait_until_idle_();
+    this->command(0x07);  // Deep sleep
+    this->data(0xA5);
+  }
+
+  void clear_screen();
+
+ protected:
+  int get_width_internal() override;
+
+  int get_height_internal() override;
+
+  void reset_() {
+    if (this->reset_pin_ != nullptr) {
+      this->reset_pin_->digital_write(true);
+      delay(200);  // NOLINT
+      this->reset_pin_->digital_write(false);
+      delay(5);
+      this->reset_pin_->digital_write(true);
+      delay(200);  // NOLINT
+    }
+  };
+
+  void init_display_();
+};
+
 class WaveshareEPaper7P5InBC : public WaveshareEPaper {
  public:
   void initialize() override;
diff --git a/tests/components/waveshare_epaper/test.esp32-ard.yaml b/tests/components/waveshare_epaper/test.esp32-ard.yaml
index 2f06c5c51b..944f98a1e9 100644
--- a/tests/components/waveshare_epaper/test.esp32-ard.yaml
+++ b/tests/components/waveshare_epaper/test.esp32-ard.yaml
@@ -188,3 +188,20 @@ display:
     full_update_every: 30
     lambda: |-
       it.rectangle(0, 0, it.get_width(), it.get_height());
+  - platform: waveshare_epaper
+    model: 7.50in-bv3-bwr
+    spi_id: spi_id_1
+    cs_pin:
+      allow_other_uses: true
+      number: GPIO25
+    dc_pin:
+      allow_other_uses: true
+      number: GPIO26
+    busy_pin:
+      allow_other_uses: true
+      number: GPIO27
+    reset_pin:
+      allow_other_uses: true
+      number: GPIO32
+    lambda: |-
+      it.rectangle(0, 0, it.get_width(), it.get_height());
diff --git a/tests/components/waveshare_epaper/test.esp32-c3-ard.yaml b/tests/components/waveshare_epaper/test.esp32-c3-ard.yaml
index 1c4547b7b4..5d651bd180 100644
--- a/tests/components/waveshare_epaper/test.esp32-c3-ard.yaml
+++ b/tests/components/waveshare_epaper/test.esp32-c3-ard.yaml
@@ -105,3 +105,19 @@ display:
     model: 1.54in-m5coreink-m09
     lambda: |-
       it.rectangle(0, 0, it.get_width(), it.get_height());
+  - platform: waveshare_epaper
+    cs_pin:
+      allow_other_uses: true
+      number: 4
+    dc_pin:
+      allow_other_uses: true
+      number: 4
+    busy_pin:
+      allow_other_uses: true
+      number: 4
+    reset_pin:
+      allow_other_uses: true
+      number: 4
+    model: 7.50in-bv3-bwr
+    lambda: |-
+      it.rectangle(0, 0, it.get_width(), it.get_height());
diff --git a/tests/components/waveshare_epaper/test.esp32-c3-idf.yaml b/tests/components/waveshare_epaper/test.esp32-c3-idf.yaml
index 1c4547b7b4..5d651bd180 100644
--- a/tests/components/waveshare_epaper/test.esp32-c3-idf.yaml
+++ b/tests/components/waveshare_epaper/test.esp32-c3-idf.yaml
@@ -105,3 +105,19 @@ display:
     model: 1.54in-m5coreink-m09
     lambda: |-
       it.rectangle(0, 0, it.get_width(), it.get_height());
+  - platform: waveshare_epaper
+    cs_pin:
+      allow_other_uses: true
+      number: 4
+    dc_pin:
+      allow_other_uses: true
+      number: 4
+    busy_pin:
+      allow_other_uses: true
+      number: 4
+    reset_pin:
+      allow_other_uses: true
+      number: 4
+    model: 7.50in-bv3-bwr
+    lambda: |-
+      it.rectangle(0, 0, it.get_width(), it.get_height());
diff --git a/tests/components/waveshare_epaper/test.esp32-idf.yaml b/tests/components/waveshare_epaper/test.esp32-idf.yaml
index b6082fcfbf..47f894d967 100644
--- a/tests/components/waveshare_epaper/test.esp32-idf.yaml
+++ b/tests/components/waveshare_epaper/test.esp32-idf.yaml
@@ -105,3 +105,19 @@ display:
     model: 1.54in-m5coreink-m09
     lambda: |-
       it.rectangle(0, 0, it.get_width(), it.get_height());
+  - platform: waveshare_epaper
+    cs_pin:
+      allow_other_uses: true
+      number: 4
+    dc_pin:
+      allow_other_uses: true
+      number: 4
+    busy_pin:
+      allow_other_uses: true
+      number: 4
+    reset_pin:
+      allow_other_uses: true
+      number: 4
+    model: 7.50in-bv3-bwr
+    lambda: |-
+      it.rectangle(0, 0, it.get_width(), it.get_height());
diff --git a/tests/components/waveshare_epaper/test.esp8266-ard.yaml b/tests/components/waveshare_epaper/test.esp8266-ard.yaml
index 1f076a67be..ceda328598 100644
--- a/tests/components/waveshare_epaper/test.esp8266-ard.yaml
+++ b/tests/components/waveshare_epaper/test.esp8266-ard.yaml
@@ -105,3 +105,19 @@ display:
     model: 1.54in-m5coreink-m09
     lambda: |-
       it.rectangle(0, 0, it.get_width(), it.get_height());
+  - platform: waveshare_epaper
+    cs_pin:
+      allow_other_uses: true
+      number: 4
+    dc_pin:
+      allow_other_uses: true
+      number: 4
+    busy_pin:
+      allow_other_uses: true
+      number: 4
+    reset_pin:
+      allow_other_uses: true
+      number: 4
+    model: 7.50in-bv3-bwr
+    lambda: |-
+      it.rectangle(0, 0, it.get_width(), it.get_height());
diff --git a/tests/components/waveshare_epaper/test.rp2040-ard.yaml b/tests/components/waveshare_epaper/test.rp2040-ard.yaml
index 6050062d7e..be7e780033 100644
--- a/tests/components/waveshare_epaper/test.rp2040-ard.yaml
+++ b/tests/components/waveshare_epaper/test.rp2040-ard.yaml
@@ -105,3 +105,19 @@ display:
     model: 1.54in-m5coreink-m09
     lambda: |-
       it.rectangle(0, 0, it.get_width(), it.get_height());
+  - platform: waveshare_epaper
+    cs_pin:
+      allow_other_uses: true
+      number: 5
+    dc_pin:
+      allow_other_uses: true
+      number: 5
+    busy_pin:
+      allow_other_uses: true
+      number: 5
+    reset_pin:
+      allow_other_uses: true
+      number: 5
+    model: 7.50in-bv3-bwr
+    lambda: |-
+      it.rectangle(0, 0, it.get_width(), it.get_height());

From 6ee02c47c29712c2b8cffa2ef5cebe25b29719a1 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 25 Nov 2024 16:42:12 -0600
Subject: [PATCH 173/282] [homeassistant.number] Return when value not set
 (#7839)

---
 .../components/homeassistant/number/homeassistant_number.cpp   | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/esphome/components/homeassistant/number/homeassistant_number.cpp b/esphome/components/homeassistant/number/homeassistant_number.cpp
index d3e285f4ac..a7f71c3244 100644
--- a/esphome/components/homeassistant/number/homeassistant_number.cpp
+++ b/esphome/components/homeassistant/number/homeassistant_number.cpp
@@ -27,6 +27,7 @@ void HomeassistantNumber::min_retrieved_(const std::string &min) {
   auto min_value = parse_number<float>(min);
   if (!min_value.has_value()) {
     ESP_LOGE(TAG, "'%s': Can't convert 'min' value '%s' to number!", this->entity_id_.c_str(), min.c_str());
+    return;
   }
   ESP_LOGD(TAG, "'%s': Min retrieved: %s", get_name().c_str(), min.c_str());
   this->traits.set_min_value(min_value.value());
@@ -36,6 +37,7 @@ void HomeassistantNumber::max_retrieved_(const std::string &max) {
   auto max_value = parse_number<float>(max);
   if (!max_value.has_value()) {
     ESP_LOGE(TAG, "'%s': Can't convert 'max' value '%s' to number!", this->entity_id_.c_str(), max.c_str());
+    return;
   }
   ESP_LOGD(TAG, "'%s': Max retrieved: %s", get_name().c_str(), max.c_str());
   this->traits.set_max_value(max_value.value());
@@ -45,6 +47,7 @@ void HomeassistantNumber::step_retrieved_(const std::string &step) {
   auto step_value = parse_number<float>(step);
   if (!step_value.has_value()) {
     ESP_LOGE(TAG, "'%s': Can't convert 'step' value '%s' to number!", this->entity_id_.c_str(), step.c_str());
+    return;
   }
   ESP_LOGD(TAG, "'%s': Step Retrieved %s", get_name().c_str(), step.c_str());
   this->traits.set_step(step_value.value());

From 4fbf41472a318bf923632508a3ba9473f06f3171 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 25 Nov 2024 17:41:27 -0600
Subject: [PATCH 174/282] [CI] Add/update some system include paths (#7831)

---
 .clang-tidy       |  6 ++++--
 script/clang-tidy | 11 +++++++----
 2 files changed, 11 insertions(+), 6 deletions(-)

diff --git a/.clang-tidy b/.clang-tidy
index 946f2950d8..994416b2f1 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -63,15 +63,16 @@ Checks: >-
   -misc-non-private-member-variables-in-classes,
   -misc-no-recursion,
   -misc-unused-parameters,
-  -modernize-avoid-c-arrays,
   -modernize-avoid-bind,
+  -modernize-avoid-c-arrays,
   -modernize-concat-nested-namespaces,
   -modernize-return-braced-init-list,
   -modernize-use-auto,
   -modernize-use-default-member-init,
   -modernize-use-equals-default,
-  -modernize-use-trailing-return-type,
   -modernize-use-nodiscard,
+  -modernize-use-nullptr,
+  -modernize-use-trailing-return-type,
   -mpi-*,
   -objc-*,
   -readability-container-data-pointer,
@@ -82,6 +83,7 @@ Checks: >-
   -readability-isolate-declaration,
   -readability-magic-numbers,
   -readability-make-member-function-const,
+  -readability-named-parameter,
   -readability-redundant-string-init,
   -readability-uppercase-literal-suffix,
   -readability-use-anyofallof,
diff --git a/script/clang-tidy b/script/clang-tidy
index 61199edce3..319fab70a2 100755
--- a/script/clang-tidy
+++ b/script/clang-tidy
@@ -100,10 +100,13 @@ def clang_options(idedata):
     # add library include directories using -isystem to suppress their errors
     for directory in list(idedata["includes"]["build"]):
         # skip our own directories, we add those later
-        if (
-            not directory.startswith(f"{root_path}/")
-            or directory.startswith(f"{root_path}/.pio/")
-            or directory.startswith(f"{root_path}/managed_components/")
+        if not directory.startswith(f"{root_path}") or directory.startswith(
+            (
+                f"{root_path}/.pio",
+                f"{root_path}/.platformio",
+                f"{root_path}/.temp",
+                f"{root_path}/managed_components",
+            )
         ):
             cmd.extend(["-isystem", directory])
 

From f4766ab74fda60c5e43eaea00c1f91f2a5e77ce2 Mon Sep 17 00:00:00 2001
From: Samuel Sieb <samuel-github@sieb.net>
Date: Mon, 25 Nov 2024 13:58:21 -1000
Subject: [PATCH 175/282] [wifi] fix 32 char SSIDs (#7834)

Co-authored-by: Samuel Sieb <samuel@sieb.net>
---
 .../wifi/wifi_component_esp32_arduino.cpp     | 24 +++++++++++++++----
 .../wifi/wifi_component_esp8266.cpp           | 24 +++++++++++++++----
 .../wifi/wifi_component_esp_idf.cpp           | 24 +++++++++++++++----
 3 files changed, 60 insertions(+), 12 deletions(-)

diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp
index 18c706cb01..bc10bbd1e5 100644
--- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp
+++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp
@@ -137,8 +137,16 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
   // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t
   wifi_config_t conf;
   memset(&conf, 0, sizeof(conf));
-  snprintf(reinterpret_cast<char *>(conf.sta.ssid), sizeof(conf.sta.ssid), "%s", ap.get_ssid().c_str());
-  snprintf(reinterpret_cast<char *>(conf.sta.password), sizeof(conf.sta.password), "%s", ap.get_password().c_str());
+  if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) {
+    ESP_LOGE(TAG, "SSID is too long");
+    return false;
+  }
+  if (ap.get_password().size() > sizeof(conf.sta.password)) {
+    ESP_LOGE(TAG, "password is too long");
+    return false;
+  }
+  memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
+  memcpy(reinterpret_cast<char *>(conf.sta.password), ap.get_password().c_str(), ap.get_password().size());
 
   // The weakest authmode to accept in the fast scan mode
   if (ap.get_password().empty()) {
@@ -746,7 +754,11 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
 
   wifi_config_t conf;
   memset(&conf, 0, sizeof(conf));
-  snprintf(reinterpret_cast<char *>(conf.ap.ssid), sizeof(conf.ap.ssid), "%s", ap.get_ssid().c_str());
+  if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) {
+    ESP_LOGE(TAG, "AP SSID is too long");
+    return false;
+  }
+  memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
   conf.ap.channel = ap.get_channel().value_or(1);
   conf.ap.ssid_hidden = ap.get_ssid().size();
   conf.ap.max_connection = 5;
@@ -757,7 +769,11 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
     *conf.ap.password = 0;
   } else {
     conf.ap.authmode = WIFI_AUTH_WPA2_PSK;
-    snprintf(reinterpret_cast<char *>(conf.ap.password), sizeof(conf.ap.password), "%s", ap.get_password().c_str());
+    if (ap.get_password().size() > sizeof(conf.ap.password)) {
+      ESP_LOGE(TAG, "AP password is too long");
+      return false;
+    }
+    memcpy(reinterpret_cast<char *>(conf.ap.password), ap.get_password().c_str(), ap.get_password().size());
   }
 
   // pairwise cipher of SoftAP, group cipher will be derived using this.
diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp
index a18d078967..8e1c2e70d8 100644
--- a/esphome/components/wifi/wifi_component_esp8266.cpp
+++ b/esphome/components/wifi/wifi_component_esp8266.cpp
@@ -236,8 +236,16 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
 
   struct station_config conf {};
   memset(&conf, 0, sizeof(conf));
-  snprintf(reinterpret_cast<char *>(conf.ssid), sizeof(conf.ssid), "%s", ap.get_ssid().c_str());
-  snprintf(reinterpret_cast<char *>(conf.password), sizeof(conf.password), "%s", ap.get_password().c_str());
+  if (ap.get_ssid().size() > sizeof(conf.ssid)) {
+    ESP_LOGE(TAG, "SSID is too long");
+    return false;
+  }
+  if (ap.get_password().size() > sizeof(conf.password)) {
+    ESP_LOGE(TAG, "password is too long");
+    return false;
+  }
+  memcpy(reinterpret_cast<char *>(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
+  memcpy(reinterpret_cast<char *>(conf.password), ap.get_password().c_str(), ap.get_password().size());
 
   if (ap.get_bssid().has_value()) {
     conf.bssid_set = 1;
@@ -775,7 +783,11 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
     return false;
 
   struct softap_config conf {};
-  snprintf(reinterpret_cast<char *>(conf.ssid), sizeof(conf.ssid), "%s", ap.get_ssid().c_str());
+  if (ap.get_ssid().size() > sizeof(conf.ssid)) {
+    ESP_LOGE(TAG, "AP SSID is too long");
+    return false;
+  }
+  memcpy(reinterpret_cast<char *>(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
   conf.ssid_len = static_cast<uint8>(ap.get_ssid().size());
   conf.channel = ap.get_channel().value_or(1);
   conf.ssid_hidden = ap.get_hidden();
@@ -787,7 +799,11 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
     *conf.password = 0;
   } else {
     conf.authmode = AUTH_WPA2_PSK;
-    snprintf(reinterpret_cast<char *>(conf.password), sizeof(conf.password), "%s", ap.get_password().c_str());
+    if (ap.get_password().size() > sizeof(conf.password)) {
+      ESP_LOGE(TAG, "AP password is too long");
+      return false;
+    }
+    memcpy(reinterpret_cast<char *>(conf.password), ap.get_password().c_str(), ap.get_password().size());
   }
 
   ETS_UART_INTR_DISABLE();
diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp
index 1bf14ff40b..1af271345f 100644
--- a/esphome/components/wifi/wifi_component_esp_idf.cpp
+++ b/esphome/components/wifi/wifi_component_esp_idf.cpp
@@ -289,8 +289,16 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
   // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t
   wifi_config_t conf;
   memset(&conf, 0, sizeof(conf));
-  snprintf(reinterpret_cast<char *>(conf.sta.ssid), sizeof(conf.sta.ssid), "%s", ap.get_ssid().c_str());
-  snprintf(reinterpret_cast<char *>(conf.sta.password), sizeof(conf.sta.password), "%s", ap.get_password().c_str());
+  if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) {
+    ESP_LOGE(TAG, "SSID is too long");
+    return false;
+  }
+  if (ap.get_password().size() > sizeof(conf.sta.password)) {
+    ESP_LOGE(TAG, "password is too long");
+    return false;
+  }
+  memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
+  memcpy(reinterpret_cast<char *>(conf.sta.password), ap.get_password().c_str(), ap.get_password().size());
 
   // The weakest authmode to accept in the fast scan mode
   if (ap.get_password().empty()) {
@@ -902,7 +910,11 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
 
   wifi_config_t conf;
   memset(&conf, 0, sizeof(conf));
-  strncpy(reinterpret_cast<char *>(conf.ap.ssid), ap.get_ssid().c_str(), sizeof(conf.ap.ssid));
+  if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) {
+    ESP_LOGE(TAG, "AP SSID is too long");
+    return false;
+  }
+  memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
   conf.ap.channel = ap.get_channel().value_or(1);
   conf.ap.ssid_hidden = ap.get_ssid().size();
   conf.ap.max_connection = 5;
@@ -913,7 +925,11 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
     *conf.ap.password = 0;
   } else {
     conf.ap.authmode = WIFI_AUTH_WPA2_PSK;
-    strncpy(reinterpret_cast<char *>(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.password));
+    if (ap.get_password().size() > sizeof(conf.ap.password)) {
+      ESP_LOGE(TAG, "AP password is too long");
+      return false;
+    }
+    memcpy(reinterpret_cast<char *>(conf.ap.password), ap.get_password().c_str(), ap.get_password().size());
   }
 
   // pairwise cipher of SoftAP, group cipher will be derived using this.

From a70cee1dc1162da1ee5f1f078991aa2c68d421a3 Mon Sep 17 00:00:00 2001
From: Samuel Sieb <samuel-github@sieb.net>
Date: Mon, 25 Nov 2024 14:15:01 -1000
Subject: [PATCH 176/282] fix local time timestamp calculation (#7807)

Co-authored-by: Samuel Sieb <samuel@sieb.net>
---
 .../components/datetime/datetime_entity.cpp   |  4 +--
 esphome/core/time.cpp                         | 34 +++++++++++--------
 esphome/core/time.h                           |  4 +--
 3 files changed, 21 insertions(+), 21 deletions(-)

diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp
index f215b7acb5..3d92194efa 100644
--- a/esphome/components/datetime/datetime_entity.cpp
+++ b/esphome/components/datetime/datetime_entity.cpp
@@ -60,9 +60,7 @@ ESPTime DateTimeEntity::state_as_esptime() const {
   obj.hour = this->hour_;
   obj.minute = this->minute_;
   obj.second = this->second_;
-  obj.day_of_week = 1;  // Required to be valid for recalc_timestamp_local but not used.
-  obj.day_of_year = 1;  // Required to be valid for recalc_timestamp_local but not used.
-  obj.recalc_timestamp_local(false);
+  obj.recalc_timestamp_local();
   return obj;
 }
 
diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp
index f7aa4fdddb..31977d972b 100644
--- a/esphome/core/time.cpp
+++ b/esphome/core/time.cpp
@@ -5,20 +5,18 @@
 
 namespace esphome {
 
-bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); }
-
 uint8_t days_in_month(uint8_t month, uint16_t year) {
   static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
-  uint8_t days = DAYS_IN_MONTH[month];
-  if (month == 2 && is_leap_year(year))
+  if (month == 2 && (year % 4 == 0))
     return 29;
-  return days;
+  return DAYS_IN_MONTH[month];
 }
 
 size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) {
   struct tm c_tm = this->to_c_tm();
   return ::strftime(buffer, buffer_len, format, &c_tm);
 }
+
 ESPTime ESPTime::from_c_tm(struct tm *c_tm, time_t c_time) {
   ESPTime res{};
   res.second = uint8_t(c_tm->tm_sec);
@@ -33,6 +31,7 @@ ESPTime ESPTime::from_c_tm(struct tm *c_tm, time_t c_time) {
   res.timestamp = c_time;
   return res;
 }
+
 struct tm ESPTime::to_c_tm() {
   struct tm c_tm {};
   c_tm.tm_sec = this->second;
@@ -46,6 +45,7 @@ struct tm ESPTime::to_c_tm() {
   c_tm.tm_isdst = this->is_dst;
   return c_tm;
 }
+
 std::string ESPTime::strftime(const std::string &format) {
   std::string timestr;
   timestr.resize(format.size() * 4);
@@ -142,6 +142,7 @@ void ESPTime::increment_second() {
     this->year++;
   }
 }
+
 void ESPTime::increment_day() {
   this->timestamp += 86400;
 
@@ -159,23 +160,22 @@ void ESPTime::increment_day() {
     this->year++;
   }
 }
+
 void ESPTime::recalc_timestamp_utc(bool use_day_of_year) {
   time_t res = 0;
-
   if (!this->fields_in_range()) {
     this->timestamp = -1;
     return;
   }
 
   for (int i = 1970; i < this->year; i++)
-    res += is_leap_year(i) ? 366 : 365;
+    res += (year % 4 == 0) ? 366 : 365;
 
   if (use_day_of_year) {
     res += this->day_of_year - 1;
   } else {
     for (int i = 1; i < this->month; i++)
       res += days_in_month(i, this->year);
-
     res += this->day_of_month - 1;
   }
 
@@ -188,13 +188,17 @@ void ESPTime::recalc_timestamp_utc(bool use_day_of_year) {
   this->timestamp = res;
 }
 
-void ESPTime::recalc_timestamp_local(bool use_day_of_year) {
-  this->recalc_timestamp_utc(use_day_of_year);
-  this->timestamp -= ESPTime::timezone_offset();
-  ESPTime temp = ESPTime::from_epoch_local(this->timestamp);
-  if (temp.is_dst) {
-    this->timestamp -= 3600;
-  }
+void ESPTime::recalc_timestamp_local() {
+  struct tm tm;
+
+  tm.tm_year = this->year - 1900;
+  tm.tm_mon = this->month - 1;
+  tm.tm_mday = this->day_of_month;
+  tm.tm_hour = this->hour;
+  tm.tm_min = this->minute;
+  tm.tm_sec = this->second;
+
+  this->timestamp = mktime(&tm);
 }
 
 int32_t ESPTime::timezone_offset() {
diff --git a/esphome/core/time.h b/esphome/core/time.h
index bce1108d93..5cbd9369fb 100644
--- a/esphome/core/time.h
+++ b/esphome/core/time.h
@@ -9,8 +9,6 @@ namespace esphome {
 
 template<typename T> bool increment_time_value(T &current, uint16_t begin, uint16_t end);
 
-bool is_leap_year(uint32_t year);
-
 uint8_t days_in_month(uint8_t month, uint16_t year);
 
 /// A more user-friendly version of struct tm from time.h
@@ -100,7 +98,7 @@ struct ESPTime {
   void recalc_timestamp_utc(bool use_day_of_year = true);
 
   /// Recalculate the timestamp field from the other fields of this ESPTime instance assuming local fields.
-  void recalc_timestamp_local(bool use_day_of_year = true);
+  void recalc_timestamp_local();
 
   /// Convert this ESPTime instance back to a tm struct.
   struct tm to_c_tm();

From d9d368d38eff2024bf50ab9fc034d19ef91d6799 Mon Sep 17 00:00:00 2001
From: Samuel Sieb <samuel-github@sieb.net>
Date: Mon, 25 Nov 2024 14:21:47 -1000
Subject: [PATCH 177/282] add on_key trigger to matrix_keypad (#7830)

Co-authored-by: Samuel Sieb <samuel@sieb.net>
---
 esphome/components/matrix_keypad/__init__.py   | 18 +++++++++++++++---
 .../components/matrix_keypad/matrix_keypad.cpp |  4 ++++
 .../components/matrix_keypad/matrix_keypad.h   |  5 +++++
 esphome/components/wiegand/__init__.py         |  7 +++----
 esphome/const.py                               |  1 +
 tests/components/matrix_keypad/common.yaml     |  8 ++++++++
 .../matrix_keypad/test.esp32-ard.yaml          | 12 ++++--------
 .../matrix_keypad/test.esp32-c3-ard.yaml       | 12 ++++--------
 .../matrix_keypad/test.esp32-c3-idf.yaml       | 12 ++++--------
 .../matrix_keypad/test.esp32-idf.yaml          | 12 ++++--------
 .../matrix_keypad/test.esp32-s3-idf.yaml       | 15 +++++++++++++++
 .../matrix_keypad/test.esp8266-ard.yaml        | 12 ++++--------
 .../matrix_keypad/test.rp2040-ard.yaml         | 12 ++++--------
 13 files changed, 75 insertions(+), 55 deletions(-)
 create mode 100644 tests/components/matrix_keypad/common.yaml
 create mode 100644 tests/components/matrix_keypad/test.esp32-s3-idf.yaml

diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py
index 5250a45732..b2bcde98ec 100644
--- a/esphome/components/matrix_keypad/__init__.py
+++ b/esphome/components/matrix_keypad/__init__.py
@@ -1,8 +1,8 @@
+from esphome import automation, pins
 import esphome.codegen as cg
-import esphome.config_validation as cv
-from esphome import pins
 from esphome.components import key_provider
-from esphome.const import CONF_ID, CONF_PIN
+import esphome.config_validation as cv
+from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID
 
 CODEOWNERS = ["@ssieb"]
 
@@ -14,6 +14,9 @@ matrix_keypad_ns = cg.esphome_ns.namespace("matrix_keypad")
 MatrixKeypad = matrix_keypad_ns.class_(
     "MatrixKeypad", key_provider.KeyProvider, cg.Component
 )
+MatrixKeyTrigger = matrix_keypad_ns.class_(
+    "MatrixKeyTrigger", automation.Trigger.template(cg.uint8)
+)
 
 CONF_KEYPAD_ID = "keypad_id"
 CONF_ROWS = "rows"
@@ -47,6 +50,11 @@ CONFIG_SCHEMA = cv.All(
             cv.Optional(CONF_DEBOUNCE_TIME, default=1): cv.int_range(min=1, max=100),
             cv.Optional(CONF_HAS_DIODES): cv.boolean,
             cv.Optional(CONF_HAS_PULLDOWNS): cv.boolean,
+            cv.Optional(CONF_ON_KEY): automation.validate_automation(
+                {
+                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MatrixKeyTrigger),
+                }
+            ),
         }
     ),
     check_keys,
@@ -73,3 +81,7 @@ async def to_code(config):
         cg.add(var.set_has_diodes(config[CONF_HAS_DIODES]))
     if CONF_HAS_PULLDOWNS in config:
         cg.add(var.set_has_pulldowns(config[CONF_HAS_PULLDOWNS]))
+    for conf in config.get(CONF_ON_KEY, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
+        cg.add(var.register_key_trigger(trigger))
+        await automation.build_automation(trigger, [(cg.uint8, "x")], conf)
diff --git a/esphome/components/matrix_keypad/matrix_keypad.cpp b/esphome/components/matrix_keypad/matrix_keypad.cpp
index f62c75c869..8537997935 100644
--- a/esphome/components/matrix_keypad/matrix_keypad.cpp
+++ b/esphome/components/matrix_keypad/matrix_keypad.cpp
@@ -86,6 +86,8 @@ void MatrixKeypad::loop() {
   if (!this->keys_.empty()) {
     uint8_t keycode = this->keys_[key];
     ESP_LOGD(TAG, "key '%c' pressed", keycode);
+    for (auto &trigger : this->key_triggers_)
+      trigger->trigger(keycode);
     for (auto &listener : this->listeners_)
       listener->key_pressed(keycode);
     this->send_key_(keycode);
@@ -107,5 +109,7 @@ void MatrixKeypad::dump_config() {
 
 void MatrixKeypad::register_listener(MatrixKeypadListener *listener) { this->listeners_.push_back(listener); }
 
+void MatrixKeypad::register_key_trigger(MatrixKeyTrigger *trig) { this->key_triggers_.push_back(trig); }
+
 }  // namespace matrix_keypad
 }  // namespace esphome
diff --git a/esphome/components/matrix_keypad/matrix_keypad.h b/esphome/components/matrix_keypad/matrix_keypad.h
index d506040b7c..8b309b42c2 100644
--- a/esphome/components/matrix_keypad/matrix_keypad.h
+++ b/esphome/components/matrix_keypad/matrix_keypad.h
@@ -1,6 +1,7 @@
 #pragma once
 
 #include "esphome/components/key_provider/key_provider.h"
+#include "esphome/core/automation.h"
 #include "esphome/core/component.h"
 #include "esphome/core/hal.h"
 #include "esphome/core/helpers.h"
@@ -18,6 +19,8 @@ class MatrixKeypadListener {
   virtual void key_released(uint8_t key){};
 };
 
+class MatrixKeyTrigger : public Trigger<uint8_t> {};
+
 class MatrixKeypad : public key_provider::KeyProvider, public Component {
  public:
   void setup() override;
@@ -31,6 +34,7 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component {
   void set_has_pulldowns(int has_pulldowns) { has_pulldowns_ = has_pulldowns; };
 
   void register_listener(MatrixKeypadListener *listener);
+  void register_key_trigger(MatrixKeyTrigger *trig);
 
  protected:
   std::vector<GPIOPin *> rows_;
@@ -42,6 +46,7 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component {
   int pressed_key_ = -1;
 
   std::vector<MatrixKeypadListener *> listeners_{};
+  std::vector<MatrixKeyTrigger *> key_triggers_;
 };
 
 }  // namespace matrix_keypad
diff --git a/esphome/components/wiegand/__init__.py b/esphome/components/wiegand/__init__.py
index 7b05c43198..962ac4c373 100644
--- a/esphome/components/wiegand/__init__.py
+++ b/esphome/components/wiegand/__init__.py
@@ -1,8 +1,8 @@
+from esphome import automation, pins
 import esphome.codegen as cg
-import esphome.config_validation as cv
-from esphome import pins, automation
 from esphome.components import key_provider
-from esphome.const import CONF_ID, CONF_ON_TAG, CONF_TRIGGER_ID
+import esphome.config_validation as cv
+from esphome.const import CONF_ID, CONF_ON_KEY, CONF_ON_TAG, CONF_TRIGGER_ID
 
 CODEOWNERS = ["@ssieb"]
 
@@ -25,7 +25,6 @@ WiegandKeyTrigger = wiegand_ns.class_(
 
 CONF_D0 = "d0"
 CONF_D1 = "d1"
-CONF_ON_KEY = "on_key"
 CONF_ON_RAW = "on_raw"
 
 CONFIG_SCHEMA = cv.Schema(
diff --git a/esphome/const.py b/esphome/const.py
index 50528b7363..d2df83aa43 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -575,6 +575,7 @@ CONF_ON_FINGER_SCAN_UNMATCHED = "on_finger_scan_unmatched"
 CONF_ON_FINISHED_WRITE = "on_finished_write"
 CONF_ON_IDLE = "on_idle"
 CONF_ON_JSON_MESSAGE = "on_json_message"
+CONF_ON_KEY = "on_key"
 CONF_ON_LOCK = "on_lock"
 CONF_ON_LOOP = "on_loop"
 CONF_ON_MESSAGE = "on_message"
diff --git a/tests/components/matrix_keypad/common.yaml b/tests/components/matrix_keypad/common.yaml
new file mode 100644
index 0000000000..32e334d890
--- /dev/null
+++ b/tests/components/matrix_keypad/common.yaml
@@ -0,0 +1,8 @@
+binary_sensor:
+  - platform: matrix_keypad
+    id: key4
+    row: 1
+    col: 1
+  - platform: matrix_keypad
+    id: key1
+    key: 1
diff --git a/tests/components/matrix_keypad/test.esp32-ard.yaml b/tests/components/matrix_keypad/test.esp32-ard.yaml
index c8e9b54534..70bb70638d 100644
--- a/tests/components/matrix_keypad/test.esp32-ard.yaml
+++ b/tests/components/matrix_keypad/test.esp32-ard.yaml
@@ -1,11 +1,5 @@
-binary_sensor:
-  - platform: matrix_keypad
-    id: key4
-    row: 1
-    col: 1
-  - platform: matrix_keypad
-    id: key1
-    key: 1
+packages:
+  common: !include common.yaml
 
 matrix_keypad:
   id: keypad
@@ -17,3 +11,5 @@ matrix_keypad:
     - pin: 15
   keys: "1234"
   has_pulldowns: true
+  on_key:
+    - lambda: ESP_LOGI("KEY", "key %d pressed", x);
diff --git a/tests/components/matrix_keypad/test.esp32-c3-ard.yaml b/tests/components/matrix_keypad/test.esp32-c3-ard.yaml
index d15e6af21a..75d9c0b263 100644
--- a/tests/components/matrix_keypad/test.esp32-c3-ard.yaml
+++ b/tests/components/matrix_keypad/test.esp32-c3-ard.yaml
@@ -1,11 +1,5 @@
-binary_sensor:
-  - platform: matrix_keypad
-    id: key4
-    row: 1
-    col: 1
-  - platform: matrix_keypad
-    id: key1
-    key: 1
+packages:
+  common: !include common.yaml
 
 matrix_keypad:
   id: keypad
@@ -17,3 +11,5 @@ matrix_keypad:
     - pin: 4
   keys: "1234"
   has_pulldowns: true
+  on_key:
+    - lambda: ESP_LOGI("KEY", "key %d pressed", x);
diff --git a/tests/components/matrix_keypad/test.esp32-c3-idf.yaml b/tests/components/matrix_keypad/test.esp32-c3-idf.yaml
index d15e6af21a..75d9c0b263 100644
--- a/tests/components/matrix_keypad/test.esp32-c3-idf.yaml
+++ b/tests/components/matrix_keypad/test.esp32-c3-idf.yaml
@@ -1,11 +1,5 @@
-binary_sensor:
-  - platform: matrix_keypad
-    id: key4
-    row: 1
-    col: 1
-  - platform: matrix_keypad
-    id: key1
-    key: 1
+packages:
+  common: !include common.yaml
 
 matrix_keypad:
   id: keypad
@@ -17,3 +11,5 @@ matrix_keypad:
     - pin: 4
   keys: "1234"
   has_pulldowns: true
+  on_key:
+    - lambda: ESP_LOGI("KEY", "key %d pressed", x);
diff --git a/tests/components/matrix_keypad/test.esp32-idf.yaml b/tests/components/matrix_keypad/test.esp32-idf.yaml
index c8e9b54534..70bb70638d 100644
--- a/tests/components/matrix_keypad/test.esp32-idf.yaml
+++ b/tests/components/matrix_keypad/test.esp32-idf.yaml
@@ -1,11 +1,5 @@
-binary_sensor:
-  - platform: matrix_keypad
-    id: key4
-    row: 1
-    col: 1
-  - platform: matrix_keypad
-    id: key1
-    key: 1
+packages:
+  common: !include common.yaml
 
 matrix_keypad:
   id: keypad
@@ -17,3 +11,5 @@ matrix_keypad:
     - pin: 15
   keys: "1234"
   has_pulldowns: true
+  on_key:
+    - lambda: ESP_LOGI("KEY", "key %d pressed", x);
diff --git a/tests/components/matrix_keypad/test.esp32-s3-idf.yaml b/tests/components/matrix_keypad/test.esp32-s3-idf.yaml
new file mode 100644
index 0000000000..a491f2ed59
--- /dev/null
+++ b/tests/components/matrix_keypad/test.esp32-s3-idf.yaml
@@ -0,0 +1,15 @@
+packages:
+  common: !include common.yaml
+
+matrix_keypad:
+  id: keypad
+  rows:
+    - pin: 10
+    - pin: 11
+  columns:
+    - pin: 12
+    - pin: 13
+  keys: "1234"
+  has_pulldowns: true
+  on_key:
+    - lambda: ESP_LOGI("KEY", "key %d pressed", x);
diff --git a/tests/components/matrix_keypad/test.esp8266-ard.yaml b/tests/components/matrix_keypad/test.esp8266-ard.yaml
index c8e9b54534..70bb70638d 100644
--- a/tests/components/matrix_keypad/test.esp8266-ard.yaml
+++ b/tests/components/matrix_keypad/test.esp8266-ard.yaml
@@ -1,11 +1,5 @@
-binary_sensor:
-  - platform: matrix_keypad
-    id: key4
-    row: 1
-    col: 1
-  - platform: matrix_keypad
-    id: key1
-    key: 1
+packages:
+  common: !include common.yaml
 
 matrix_keypad:
   id: keypad
@@ -17,3 +11,5 @@ matrix_keypad:
     - pin: 15
   keys: "1234"
   has_pulldowns: true
+  on_key:
+    - lambda: ESP_LOGI("KEY", "key %d pressed", x);
diff --git a/tests/components/matrix_keypad/test.rp2040-ard.yaml b/tests/components/matrix_keypad/test.rp2040-ard.yaml
index d15e6af21a..75d9c0b263 100644
--- a/tests/components/matrix_keypad/test.rp2040-ard.yaml
+++ b/tests/components/matrix_keypad/test.rp2040-ard.yaml
@@ -1,11 +1,5 @@
-binary_sensor:
-  - platform: matrix_keypad
-    id: key4
-    row: 1
-    col: 1
-  - platform: matrix_keypad
-    id: key1
-    key: 1
+packages:
+  common: !include common.yaml
 
 matrix_keypad:
   id: keypad
@@ -17,3 +11,5 @@ matrix_keypad:
     - pin: 4
   keys: "1234"
   has_pulldowns: true
+  on_key:
+    - lambda: ESP_LOGI("KEY", "key %d pressed", x);

From c0dcecc465fa8b0d3205880174ca820bc8bfd3d3 Mon Sep 17 00:00:00 2001
From: Citric Lee <37475446+limengdu@users.noreply.github.com>
Date: Tue, 26 Nov 2024 08:53:21 +0800
Subject: [PATCH 178/282] Add: Seeed Studio mr60fda2 mmwave sensor (#7576)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Spencer Yan <spencer@spenyan.com>
---
 CODEOWNERS                                    |   1 +
 esphome/components/seeed_mr60fda2/__init__.py |  41 ++
 .../seeed_mr60fda2/binary_sensor.py           |  33 ++
 .../seeed_mr60fda2/button/__init__.py         |  45 +++
 .../button/get_radar_parameters_button.cpp    |   9 +
 .../button/get_radar_parameters_button.h      |  18 +
 .../button/reset_radar_button.cpp             |   9 +
 .../button/reset_radar_button.h               |  18 +
 .../seeed_mr60fda2/seeed_mr60fda2.cpp         | 368 ++++++++++++++++++
 .../seeed_mr60fda2/seeed_mr60fda2.h           | 101 +++++
 .../seeed_mr60fda2/select/__init__.py         |  59 +++
 .../select/height_threshold_select.cpp        |  15 +
 .../select/height_threshold_select.h          |  18 +
 .../select/install_height_select.cpp          |  15 +
 .../select/install_height_select.h            |  18 +
 .../select/sensitivity_select.cpp             |  15 +
 .../select/sensitivity_select.h               |  18 +
 tests/components/seeed_mr60fda2/common.yaml   |  34 ++
 .../seeed_mr60fda2/test.esp32-c3-ard.yaml     |   5 +
 .../seeed_mr60fda2/test.esp32-c3-idf.yaml     |   5 +
 20 files changed, 845 insertions(+)
 create mode 100644 esphome/components/seeed_mr60fda2/__init__.py
 create mode 100644 esphome/components/seeed_mr60fda2/binary_sensor.py
 create mode 100644 esphome/components/seeed_mr60fda2/button/__init__.py
 create mode 100644 esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.cpp
 create mode 100644 esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.h
 create mode 100644 esphome/components/seeed_mr60fda2/button/reset_radar_button.cpp
 create mode 100644 esphome/components/seeed_mr60fda2/button/reset_radar_button.h
 create mode 100644 esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp
 create mode 100644 esphome/components/seeed_mr60fda2/seeed_mr60fda2.h
 create mode 100644 esphome/components/seeed_mr60fda2/select/__init__.py
 create mode 100644 esphome/components/seeed_mr60fda2/select/height_threshold_select.cpp
 create mode 100644 esphome/components/seeed_mr60fda2/select/height_threshold_select.h
 create mode 100644 esphome/components/seeed_mr60fda2/select/install_height_select.cpp
 create mode 100644 esphome/components/seeed_mr60fda2/select/install_height_select.h
 create mode 100644 esphome/components/seeed_mr60fda2/select/sensitivity_select.cpp
 create mode 100644 esphome/components/seeed_mr60fda2/select/sensitivity_select.h
 create mode 100644 tests/components/seeed_mr60fda2/common.yaml
 create mode 100644 tests/components/seeed_mr60fda2/test.esp32-c3-ard.yaml
 create mode 100644 tests/components/seeed_mr60fda2/test.esp32-c3-idf.yaml

diff --git a/CODEOWNERS b/CODEOWNERS
index dd3926d283..fb6d11d1fb 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -354,6 +354,7 @@ esphome/components/sdl/* @clydebarrow
 esphome/components/sdm_meter/* @jesserockz @polyfaces
 esphome/components/sdp3x/* @Azimath
 esphome/components/seeed_mr24hpc1/* @limengdu
+esphome/components/seeed_mr60fda2/* @limengdu
 esphome/components/selec_meter/* @sourabhjaiswal
 esphome/components/select/* @esphome/core
 esphome/components/sen0321/* @notjj
diff --git a/esphome/components/seeed_mr60fda2/__init__.py b/esphome/components/seeed_mr60fda2/__init__.py
new file mode 100644
index 0000000000..e79134deec
--- /dev/null
+++ b/esphome/components/seeed_mr60fda2/__init__.py
@@ -0,0 +1,41 @@
+import esphome.codegen as cg
+from esphome.components import uart
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+CODEOWNERS = ["@limengdu"]
+DEPENDENCIES = ["uart"]
+MULTI_CONF = True
+
+mr60fda2_ns = cg.esphome_ns.namespace("seeed_mr60fda2")
+
+MR60FDA2Component = mr60fda2_ns.class_(
+    "MR60FDA2Component", cg.Component, uart.UARTDevice
+)
+
+CONF_MR60FDA2_ID = "mr60fda2_id"
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(MR60FDA2Component),
+        }
+    )
+    .extend(uart.UART_DEVICE_SCHEMA)
+    .extend(cv.COMPONENT_SCHEMA)
+)
+
+FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
+    "seeed_mr60fda2",
+    require_tx=True,
+    require_rx=True,
+    baud_rate=115200,
+    parity="NONE",
+    stop_bits=1,
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
diff --git a/esphome/components/seeed_mr60fda2/binary_sensor.py b/esphome/components/seeed_mr60fda2/binary_sensor.py
new file mode 100644
index 0000000000..2860ac0100
--- /dev/null
+++ b/esphome/components/seeed_mr60fda2/binary_sensor.py
@@ -0,0 +1,33 @@
+import esphome.codegen as cg
+from esphome.components import binary_sensor
+import esphome.config_validation as cv
+from esphome.const import DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_SAFETY
+
+from . import CONF_MR60FDA2_ID, MR60FDA2Component
+
+DEPENDENCIES = ["seeed_mr60fda2"]
+
+CONF_PEOPLE_EXIST = "people_exist"
+CONF_FALL_DETECTED = "fall_detected"
+
+CONFIG_SCHEMA = {
+    cv.GenerateID(CONF_MR60FDA2_ID): cv.use_id(MR60FDA2Component),
+    cv.Optional(CONF_PEOPLE_EXIST): binary_sensor.binary_sensor_schema(
+        device_class=DEVICE_CLASS_OCCUPANCY, icon="mdi:motion-sensor"
+    ),
+    cv.Optional(CONF_FALL_DETECTED): binary_sensor.binary_sensor_schema(
+        device_class=DEVICE_CLASS_SAFETY, icon="mdi:emergency"
+    ),
+}
+
+
+async def to_code(config):
+    mr60fda2_component = await cg.get_variable(config[CONF_MR60FDA2_ID])
+
+    if people_exist_config := config.get(CONF_PEOPLE_EXIST):
+        sens = await binary_sensor.new_binary_sensor(people_exist_config)
+        cg.add(mr60fda2_component.set_people_exist_binary_sensor(sens))
+
+    if is_fall_config := config.get(CONF_FALL_DETECTED):
+        sens = await binary_sensor.new_binary_sensor(is_fall_config)
+        cg.add(mr60fda2_component.set_fall_detected_binary_sensor(sens))
diff --git a/esphome/components/seeed_mr60fda2/button/__init__.py b/esphome/components/seeed_mr60fda2/button/__init__.py
new file mode 100644
index 0000000000..1415dc27ca
--- /dev/null
+++ b/esphome/components/seeed_mr60fda2/button/__init__.py
@@ -0,0 +1,45 @@
+import esphome.codegen as cg
+from esphome.components import button
+import esphome.config_validation as cv
+from esphome.const import (
+    DEVICE_CLASS_RESTART,
+    DEVICE_CLASS_UPDATE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+    ENTITY_CATEGORY_NONE,
+    CONF_FACTORY_RESET,
+)
+
+from .. import CONF_MR60FDA2_ID, MR60FDA2Component, mr60fda2_ns
+
+DEPENDENCIES = ["seeed_mr60fda2"]
+
+GetRadarParametersButton = mr60fda2_ns.class_("GetRadarParametersButton", button.Button)
+ResetRadarButton = mr60fda2_ns.class_("ResetRadarButton", button.Button)
+
+CONF_GET_RADAR_PARAMETERS = "get_radar_parameters"
+
+CONFIG_SCHEMA = {
+    cv.GenerateID(CONF_MR60FDA2_ID): cv.use_id(MR60FDA2Component),
+    cv.Optional(CONF_GET_RADAR_PARAMETERS): button.button_schema(
+        GetRadarParametersButton,
+        device_class=DEVICE_CLASS_UPDATE,
+        entity_category=ENTITY_CATEGORY_NONE,
+    ),
+    cv.Optional(CONF_FACTORY_RESET): button.button_schema(
+        ResetRadarButton,
+        device_class=DEVICE_CLASS_RESTART,
+        entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+    ),
+}
+
+
+async def to_code(config):
+    mr60fda2_component = await cg.get_variable(config[CONF_MR60FDA2_ID])
+    if get_radar_parameters_config := config.get(CONF_GET_RADAR_PARAMETERS):
+        b = await button.new_button(get_radar_parameters_config)
+        await cg.register_parented(b, config[CONF_MR60FDA2_ID])
+        cg.add(mr60fda2_component.set_get_radar_parameters_button(b))
+    if factory_reset_config := config.get(CONF_FACTORY_RESET):
+        b = await button.new_button(factory_reset_config)
+        await cg.register_parented(b, config[CONF_MR60FDA2_ID])
+        cg.add(mr60fda2_component.set_factory_reset_button(b))
diff --git a/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.cpp b/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.cpp
new file mode 100644
index 0000000000..88be6dfe7c
--- /dev/null
+++ b/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.cpp
@@ -0,0 +1,9 @@
+#include "get_radar_parameters_button.h"
+
+namespace esphome {
+namespace seeed_mr60fda2 {
+
+void GetRadarParametersButton::press_action() { this->parent_->get_radar_parameters(); }
+
+}  // namespace seeed_mr60fda2
+}  // namespace esphome
diff --git a/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.h b/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.h
new file mode 100644
index 0000000000..9d6d507383
--- /dev/null
+++ b/esphome/components/seeed_mr60fda2/button/get_radar_parameters_button.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "esphome/components/button/button.h"
+#include "../seeed_mr60fda2.h"
+
+namespace esphome {
+namespace seeed_mr60fda2 {
+
+class GetRadarParametersButton : public button::Button, public Parented<MR60FDA2Component> {
+ public:
+  GetRadarParametersButton() = default;
+
+ protected:
+  void press_action() override;
+};
+
+}  // namespace seeed_mr60fda2
+}  // namespace esphome
diff --git a/esphome/components/seeed_mr60fda2/button/reset_radar_button.cpp b/esphome/components/seeed_mr60fda2/button/reset_radar_button.cpp
new file mode 100644
index 0000000000..0a5833a18c
--- /dev/null
+++ b/esphome/components/seeed_mr60fda2/button/reset_radar_button.cpp
@@ -0,0 +1,9 @@
+#include "reset_radar_button.h"
+
+namespace esphome {
+namespace seeed_mr60fda2 {
+
+void ResetRadarButton::press_action() { this->parent_->factory_reset(); }
+
+}  // namespace seeed_mr60fda2
+}  // namespace esphome
diff --git a/esphome/components/seeed_mr60fda2/button/reset_radar_button.h b/esphome/components/seeed_mr60fda2/button/reset_radar_button.h
new file mode 100644
index 0000000000..66780fb8af
--- /dev/null
+++ b/esphome/components/seeed_mr60fda2/button/reset_radar_button.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "esphome/components/button/button.h"
+#include "../seeed_mr60fda2.h"
+
+namespace esphome {
+namespace seeed_mr60fda2 {
+
+class ResetRadarButton : public button::Button, public Parented<MR60FDA2Component> {
+ public:
+  ResetRadarButton() = default;
+
+ protected:
+  void press_action() override;
+};
+
+}  // namespace seeed_mr60fda2
+}  // namespace esphome
diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp
new file mode 100644
index 0000000000..d183a1f77f
--- /dev/null
+++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp
@@ -0,0 +1,368 @@
+#include "seeed_mr60fda2.h"
+#include "esphome/core/log.h"
+
+#include <cinttypes>
+#include <utility>
+
+namespace esphome {
+namespace seeed_mr60fda2 {
+
+static const char *const TAG = "seeed_mr60fda2";
+
+// Prints the component's configuration data. dump_config() prints all of the component's configuration
+// items in an easy-to-read format, including the configuration key-value pairs.
+void MR60FDA2Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "MR60FDA2:");
+#ifdef USE_BINARY_SENSOR
+  LOG_BINARY_SENSOR(" ", "People Exist Binary Sensor", this->people_exist_binary_sensor_);
+  LOG_BINARY_SENSOR(" ", "Is Fall Binary Sensor", this->fall_detected_binary_sensor_);
+#endif
+#ifdef USE_BUTTON
+  LOG_BUTTON(" ", "Get Radar Parameters Button", this->get_radar_parameters_button_);
+  LOG_BUTTON(" ", "Reset Radar Button", this->factory_reset_button_);
+#endif
+#ifdef USE_SELECT
+  LOG_SELECT(" ", "Install Height Select", this->install_height_select_);
+  LOG_SELECT(" ", "Height Threshold Select", this->height_threshold_select_);
+  LOG_SELECT(" ", "Sensitivity Select", this->sensitivity_select_);
+#endif
+}
+
+// Initialisation functions
+void MR60FDA2Component::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up MR60FDA2...");
+  this->check_uart_settings(115200);
+
+  this->current_frame_locate_ = LOCATE_FRAME_HEADER;
+  this->current_frame_id_ = 0;
+  this->current_frame_len_ = 0;
+  this->current_data_frame_len_ = 0;
+  this->current_frame_type_ = 0;
+  this->get_radar_parameters();
+
+  memset(this->current_frame_buf_, 0, FRAME_BUF_MAX_SIZE);
+  memset(this->current_data_buf_, 0, DATA_BUF_MAX_SIZE);
+
+  ESP_LOGCONFIG(TAG, "Set up MR60FDA2 complete");
+}
+
+// main loop
+void MR60FDA2Component::loop() {
+  uint8_t byte;
+
+  // Is there data on the serial port
+  while (this->available()) {
+    this->read_byte(&byte);
+    this->split_frame_(byte);  // split data frame
+  }
+}
+
+/**
+ * @brief Calculate the checksum for a byte array.
+ *
+ * This function calculates the checksum for the provided byte array using an
+ * XOR-based checksum algorithm.
+ *
+ * @param data The byte array to calculate the checksum for.
+ * @param len The length of the byte array.
+ * @return The calculated checksum.
+ */
+static uint8_t calculate_checksum(const uint8_t *data, size_t len) {
+  uint8_t checksum = 0;
+  for (size_t i = 0; i < len; i++) {
+    checksum ^= data[i];
+  }
+  checksum = ~checksum;
+  return checksum;
+}
+
+/**
+ * @brief Validate the checksum of a byte array.
+ *
+ * This function validates the checksum of the provided byte array by comparing
+ * it to the expected checksum.
+ *
+ * @param data The byte array to validate.
+ * @param len The length of the byte array.
+ * @param expected_checksum The expected checksum.
+ * @return True if the checksum is valid, false otherwise.
+ */
+static bool validate_checksum(const uint8_t *data, size_t len, uint8_t expected_checksum) {
+  return calculate_checksum(data, len) == expected_checksum;
+}
+
+static uint8_t find_nearest_index(float value, const float *arr, int size) {
+  int nearest_index = 0;
+  float min_diff = std::abs(value - arr[0]);
+  for (int i = 1; i < size; ++i) {
+    float diff = std::abs(value - arr[i]);
+    if (diff < min_diff) {
+      min_diff = diff;
+      nearest_index = i;
+    }
+  }
+  return nearest_index;
+}
+
+/**
+ * @brief Convert a float value to a byte array.
+ *
+ * This function converts a float value to a byte array.
+ *
+ * @param value The float value to convert.
+ * @param bytes The byte array to store the converted value.
+ */
+static void float_to_bytes(float value, unsigned char *bytes) {
+  union {
+    float float_value;
+    unsigned char byte_array[4];
+  } u;
+
+  u.float_value = value;
+  memcpy(bytes, u.byte_array, 4);
+}
+
+/**
+ * @brief Convert a 32-bit unsigned integer to a byte array.
+ *
+ * This function converts a 32-bit unsigned integer to a byte array.
+ *
+ * @param value The 32-bit unsigned integer to convert.
+ * @param bytes The byte array to store the converted value.
+ */
+static void int_to_bytes(uint32_t value, unsigned char *bytes) {
+  bytes[0] = value & 0xFF;
+  bytes[1] = (value >> 8) & 0xFF;
+  bytes[2] = (value >> 16) & 0xFF;
+  bytes[3] = (value >> 24) & 0xFF;
+}
+
+void MR60FDA2Component::split_frame_(uint8_t buffer) {
+  switch (this->current_frame_locate_) {
+    case LOCATE_FRAME_HEADER:  // starting buffer
+      if (buffer == FRAME_HEADER_BUFFER) {
+        this->current_frame_len_ = 1;
+        this->current_frame_buf_[this->current_frame_len_ - 1] = buffer;
+        this->current_frame_locate_++;
+      }
+      break;
+    case LOCATE_ID_FRAME1:
+      this->current_frame_id_ = buffer << 8;
+      this->current_frame_len_++;
+      this->current_frame_buf_[this->current_frame_len_ - 1] = buffer;
+      this->current_frame_locate_++;
+      break;
+    case LOCATE_ID_FRAME2:
+      this->current_frame_id_ += buffer;
+      this->current_frame_len_++;
+      this->current_frame_buf_[this->current_frame_len_ - 1] = buffer;
+      this->current_frame_locate_++;
+      break;
+    case LOCATE_LENGTH_FRAME_H:
+      this->current_data_frame_len_ = buffer << 8;
+      if (this->current_data_frame_len_ == 0x00) {
+        this->current_frame_len_++;
+        this->current_frame_buf_[this->current_frame_len_ - 1] = buffer;
+        this->current_frame_locate_++;
+      } else {
+        this->current_frame_locate_ = LOCATE_FRAME_HEADER;
+      }
+      break;
+    case LOCATE_LENGTH_FRAME_L:
+      this->current_data_frame_len_ += buffer;
+      if (this->current_data_frame_len_ > DATA_BUF_MAX_SIZE) {
+        this->current_frame_locate_ = LOCATE_FRAME_HEADER;
+      } else {
+        this->current_frame_len_++;
+        this->current_frame_buf_[this->current_frame_len_ - 1] = buffer;
+        this->current_frame_locate_++;
+      }
+      break;
+    case LOCATE_TYPE_FRAME1:
+      this->current_frame_type_ = buffer << 8;
+      this->current_frame_len_++;
+      this->current_frame_buf_[this->current_frame_len_ - 1] = buffer;
+      this->current_frame_locate_++;
+      break;
+    case LOCATE_TYPE_FRAME2:
+      this->current_frame_type_ += buffer;
+      if ((this->current_frame_type_ == IS_FALL_TYPE_BUFFER) ||
+          (this->current_frame_type_ == PEOPLE_EXIST_TYPE_BUFFER) ||
+          (this->current_frame_type_ == RESULT_INSTALL_HEIGHT) || (this->current_frame_type_ == RESULT_PARAMETERS) ||
+          (this->current_frame_type_ == RESULT_HEIGHT_THRESHOLD) || (this->current_frame_type_ == RESULT_SENSITIVITY)) {
+        this->current_frame_len_++;
+        this->current_frame_buf_[this->current_frame_len_ - 1] = buffer;
+        this->current_frame_locate_++;
+      } else {
+        this->current_frame_locate_ = LOCATE_FRAME_HEADER;
+      }
+      break;
+    case LOCATE_HEAD_CKSUM_FRAME:
+      if (validate_checksum(this->current_frame_buf_, this->current_frame_len_, buffer)) {
+        this->current_frame_len_++;
+        this->current_frame_buf_[this->current_frame_len_ - 1] = buffer;
+        this->current_frame_locate_++;
+      } else {
+        ESP_LOGD(TAG, "HEAD_CKSUM_FRAME ERROR: 0x%02x", buffer);
+        ESP_LOGV(TAG, "CURRENT_FRAME: %s %s",
+                 format_hex_pretty(this->current_frame_buf_, this->current_frame_len_).c_str(),
+                 format_hex_pretty(&buffer, 1).c_str());
+        this->current_frame_locate_ = LOCATE_FRAME_HEADER;
+      }
+      break;
+    case LOCATE_DATA_FRAME:
+      this->current_frame_len_++;
+      this->current_frame_buf_[this->current_frame_len_ - 1] = buffer;
+      this->current_data_buf_[this->current_frame_len_ - LEN_TO_DATA_FRAME] = buffer;
+      if (this->current_frame_len_ - LEN_TO_HEAD_CKSUM == this->current_data_frame_len_) {
+        this->current_frame_locate_++;
+      }
+      if (this->current_frame_len_ > FRAME_BUF_MAX_SIZE) {
+        ESP_LOGD(TAG, "PRACTICE_DATA_FRAME_LEN ERROR: %d", this->current_frame_len_ - LEN_TO_HEAD_CKSUM);
+        this->current_frame_locate_ = LOCATE_FRAME_HEADER;
+      }
+      break;
+    case LOCATE_DATA_CKSUM_FRAME:
+      if (validate_checksum(this->current_data_buf_, this->current_data_frame_len_, buffer)) {
+        this->current_frame_len_++;
+        this->current_frame_buf_[this->current_frame_len_ - 1] = buffer;
+        this->current_frame_locate_++;
+        this->process_frame_();
+      } else {
+        ESP_LOGD(TAG, "DATA_CKSUM_FRAME ERROR: 0x%02x", buffer);
+        ESP_LOGV(TAG, "GET CURRENT_FRAME: %s %s",
+                 format_hex_pretty(this->current_frame_buf_, this->current_frame_len_).c_str(),
+                 format_hex_pretty(&buffer, 1).c_str());
+
+        this->current_frame_locate_ = LOCATE_FRAME_HEADER;
+      }
+      break;
+    default:
+      break;
+  }
+}
+
+void MR60FDA2Component::process_frame_() {
+  switch (this->current_frame_type_) {
+    case IS_FALL_TYPE_BUFFER:
+      if (this->fall_detected_binary_sensor_ != nullptr) {
+        this->fall_detected_binary_sensor_->publish_state(this->current_frame_buf_[LEN_TO_HEAD_CKSUM]);
+      }
+      this->current_frame_locate_ = LOCATE_FRAME_HEADER;
+      break;
+
+    case PEOPLE_EXIST_TYPE_BUFFER:
+      if (this->people_exist_binary_sensor_ != nullptr)
+        this->people_exist_binary_sensor_->publish_state(this->current_frame_buf_[LEN_TO_HEAD_CKSUM]);
+      this->current_frame_locate_ = LOCATE_FRAME_HEADER;
+      break;
+
+    case RESULT_INSTALL_HEIGHT:
+      if (this->current_data_buf_[0]) {
+        ESP_LOGD(TAG, "Successfully set the mounting height");
+      } else {
+        ESP_LOGD(TAG, "Failed to set the mounting height");
+      }
+      this->current_frame_locate_ = LOCATE_FRAME_HEADER;
+      break;
+
+    case RESULT_HEIGHT_THRESHOLD:
+      if (this->current_data_buf_[0]) {
+        ESP_LOGD(TAG, "Successfully set the height threshold");
+      } else {
+        ESP_LOGD(TAG, "Failed to set the height threshold");
+      }
+      this->current_frame_locate_ = LOCATE_FRAME_HEADER;
+      break;
+
+    case RESULT_SENSITIVITY:
+      if (this->current_data_buf_[0]) {
+        ESP_LOGD(TAG, "Successfully set the sensitivity");
+      } else {
+        ESP_LOGD(TAG, "Failed to set the sensitivity");
+      }
+      this->current_frame_locate_ = LOCATE_FRAME_HEADER;
+      break;
+
+    case RESULT_PARAMETERS: {
+      float install_height_float = 0;
+      float height_threshold_float = 0;
+      uint32_t current_sensitivity = 0;
+      if (this->install_height_select_ != nullptr) {
+        uint32_t current_install_height_int =
+            encode_uint32(current_data_buf_[3], current_data_buf_[2], current_data_buf_[1], current_data_buf_[0]);
+
+        install_height_float = bit_cast<float>(current_install_height_int);
+        uint32_t select_index = find_nearest_index(install_height_float, INSTALL_HEIGHT, 7);
+        this->install_height_select_->publish_state(this->install_height_select_->at(select_index).value());
+      }
+
+      if (this->height_threshold_select_ != nullptr) {
+        uint32_t current_height_threshold_int =
+            encode_uint32(current_data_buf_[7], current_data_buf_[6], current_data_buf_[5], current_data_buf_[4]);
+
+        height_threshold_float = bit_cast<float>(current_height_threshold_int);
+        size_t select_index = find_nearest_index(height_threshold_float, HEIGHT_THRESHOLD, 7);
+        this->height_threshold_select_->publish_state(this->height_threshold_select_->at(select_index).value());
+      }
+
+      if (this->sensitivity_select_ != nullptr) {
+        current_sensitivity =
+            encode_uint32(current_data_buf_[11], current_data_buf_[10], current_data_buf_[9], current_data_buf_[8]);
+
+        uint32_t select_index = find_nearest_index(current_sensitivity, SENSITIVITY, 3);
+        this->sensitivity_select_->publish_state(this->sensitivity_select_->at(select_index).value());
+      }
+
+      ESP_LOGD(TAG, "Mounting height: %.2f, Height threshold: %.2f, Sensitivity: %" PRIu32, install_height_float,
+               height_threshold_float, current_sensitivity);
+      this->current_frame_locate_ = LOCATE_FRAME_HEADER;
+      break;
+    }
+    default:
+      break;
+  }
+}
+
+// Send Heartbeat Packet Command
+void MR60FDA2Component::set_install_height(uint8_t index) {
+  uint8_t send_data[13] = {0x01, 0x00, 0x00, 0x00, 0x04, 0x0E, 0x04, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00};
+  float_to_bytes(INSTALL_HEIGHT[index], &send_data[8]);
+  send_data[12] = calculate_checksum(send_data + 8, 4);
+  this->write_array(send_data, 13);
+  ESP_LOGV(TAG, "SEND INSTALL HEIGHT FRAME: %s", format_hex_pretty(send_data, 13).c_str());
+}
+
+void MR60FDA2Component::set_height_threshold(uint8_t index) {
+  uint8_t send_data[13] = {0x01, 0x00, 0x00, 0x00, 0x04, 0x0E, 0x08, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00};
+  float_to_bytes(INSTALL_HEIGHT[index], &send_data[8]);
+  send_data[12] = calculate_checksum(send_data + 8, 4);
+  this->write_array(send_data, 13);
+  ESP_LOGV(TAG, "SEND HEIGHT THRESHOLD: %s", format_hex_pretty(send_data, 13).c_str());
+}
+
+void MR60FDA2Component::set_sensitivity(uint8_t index) {
+  uint8_t send_data[13] = {0x01, 0x00, 0x00, 0x00, 0x04, 0x0E, 0x0A, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+  int_to_bytes(SENSITIVITY[index], &send_data[8]);
+
+  send_data[12] = calculate_checksum(send_data + 8, 4);
+  this->write_array(send_data, 13);
+  ESP_LOGV(TAG, "SEND SET SENSITIVITY: %s", format_hex_pretty(send_data, 13).c_str());
+}
+
+void MR60FDA2Component::get_radar_parameters() {
+  uint8_t send_data[8] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x06, 0xF6};
+  this->write_array(send_data, 8);
+  ESP_LOGV(TAG, "SEND GET PARAMETERS: %s", format_hex_pretty(send_data, 8).c_str());
+}
+
+void MR60FDA2Component::factory_reset() {
+  uint8_t send_data[8] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x21, 0x10, 0xCF};
+  this->write_array(send_data, 8);
+  ESP_LOGV(TAG, "SEND RESET: %s", format_hex_pretty(send_data, 8).c_str());
+  this->get_radar_parameters();
+}
+
+}  // namespace seeed_mr60fda2
+}  // namespace esphome
diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.h b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.h
new file mode 100644
index 0000000000..e1ffa4f071
--- /dev/null
+++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.h
@@ -0,0 +1,101 @@
+#pragma once
+#include "esphome/core/component.h"
+#include "esphome/core/defines.h"
+#ifdef USE_BINARY_SENSOR
+#include "esphome/components/binary_sensor/binary_sensor.h"
+#endif
+#ifdef USE_BUTTON
+#include "esphome/components/button/button.h"
+#endif
+#ifdef USE_SELECT
+#include "esphome/components/select/select.h"
+#endif
+#ifdef USE_TEXT_SENSOR
+#include "esphome/components/text_sensor/text_sensor.h"
+#endif
+#include "esphome/components/uart/uart.h"
+#include "esphome/core/automation.h"
+#include "esphome/core/helpers.h"
+
+#include <map>
+
+namespace esphome {
+namespace seeed_mr60fda2 {
+
+static const uint8_t DATA_BUF_MAX_SIZE = 28;
+static const uint8_t FRAME_BUF_MAX_SIZE = 37;
+static const uint8_t LEN_TO_HEAD_CKSUM = 8;
+static const uint8_t LEN_TO_DATA_FRAME = 9;
+
+static const uint8_t FRAME_HEADER_BUFFER = 0x01;
+static const uint16_t IS_FALL_TYPE_BUFFER = 0x0E02;
+static const uint16_t PEOPLE_EXIST_TYPE_BUFFER = 0x0F09;
+static const uint16_t RESULT_INSTALL_HEIGHT = 0x0E04;
+static const uint16_t RESULT_PARAMETERS = 0x0E06;
+static const uint16_t RESULT_HEIGHT_THRESHOLD = 0x0E08;
+static const uint16_t RESULT_SENSITIVITY = 0x0E0A;
+
+enum FrameLocation {
+  LOCATE_FRAME_HEADER,
+  LOCATE_ID_FRAME1,
+  LOCATE_ID_FRAME2,
+  LOCATE_LENGTH_FRAME_H,
+  LOCATE_LENGTH_FRAME_L,
+  LOCATE_TYPE_FRAME1,
+  LOCATE_TYPE_FRAME2,
+  LOCATE_HEAD_CKSUM_FRAME,  // Header checksum: [from the first byte to the previous byte of the HEAD_CKSUM bit]
+  LOCATE_DATA_FRAME,
+  LOCATE_DATA_CKSUM_FRAME,  // Data checksum: [from the first to the previous byte of the DATA_CKSUM bit]
+  LOCATE_PROCESS_FRAME,
+};
+
+static const float INSTALL_HEIGHT[7] = {2.4f, 2.5f, 2.6f, 2.7f, 2.8f, 2.9f, 3.0f};
+static const float HEIGHT_THRESHOLD[7] = {0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f};
+static const float SENSITIVITY[3] = {3, 15, 30};
+
+static const char *const INSTALL_HEIGHT_STR[7] = {"2.4m", "2.5m", "2.6", "2.7m", "2.8", "2.9m", "3.0m"};
+static const char *const HEIGHT_THRESHOLD_STR[7] = {"0.0m", "0.1m", "0.2m", "0.3m", "0.4m", "0.5m", "0.6m"};
+static const char *const SENSITIVITY_STR[3] = {"1", "2", "3"};
+
+class MR60FDA2Component : public Component,
+                          public uart::UARTDevice {  // The class name must be the name defined by text_sensor.py
+#ifdef USE_BINARY_SENSOR
+  SUB_BINARY_SENSOR(people_exist)
+  SUB_BINARY_SENSOR(fall_detected)
+#endif
+#ifdef USE_BUTTON
+  SUB_BUTTON(get_radar_parameters)
+  SUB_BUTTON(factory_reset)
+#endif
+#ifdef USE_SELECT
+  SUB_SELECT(install_height)
+  SUB_SELECT(height_threshold)
+  SUB_SELECT(sensitivity)
+#endif
+
+ protected:
+  uint8_t current_frame_locate_;
+  uint8_t current_frame_buf_[FRAME_BUF_MAX_SIZE];
+  uint8_t current_data_buf_[DATA_BUF_MAX_SIZE];
+  uint16_t current_frame_id_;
+  size_t current_frame_len_;
+  size_t current_data_frame_len_;
+  uint16_t current_frame_type_;
+
+  void split_frame_(uint8_t buffer);
+  void process_frame_();
+
+ public:
+  float get_setup_priority() const override { return esphome::setup_priority::LATE; }
+  void setup() override;
+  void dump_config() override;
+  void loop() override;
+  void set_install_height(uint8_t index);
+  void set_height_threshold(uint8_t index);
+  void set_sensitivity(uint8_t index);
+  void get_radar_parameters();
+  void factory_reset();
+};
+
+}  // namespace seeed_mr60fda2
+}  // namespace esphome
diff --git a/esphome/components/seeed_mr60fda2/select/__init__.py b/esphome/components/seeed_mr60fda2/select/__init__.py
new file mode 100644
index 0000000000..a6f9eeb920
--- /dev/null
+++ b/esphome/components/seeed_mr60fda2/select/__init__.py
@@ -0,0 +1,59 @@
+import esphome.codegen as cg
+from esphome.components import select
+import esphome.config_validation as cv
+from esphome.const import CONF_SENSITIVITY, ENTITY_CATEGORY_CONFIG, ICON_ACCELERATION_Z
+
+from .. import CONF_MR60FDA2_ID, MR60FDA2Component, mr60fda2_ns
+
+
+DEPENDENCIES = ["seeed_mr60fda2"]
+
+InstallHeightSelect = mr60fda2_ns.class_("InstallHeightSelect", select.Select)
+HeightThresholdSelect = mr60fda2_ns.class_("HeightThresholdSelect", select.Select)
+SensitivitySelect = mr60fda2_ns.class_("SensitivitySelect", select.Select)
+
+CONF_INSTALL_HEIGHT = "install_height"
+CONF_HEIGHT_THRESHOLD = "height_threshold"
+
+CONFIG_SCHEMA = {
+    cv.GenerateID(CONF_MR60FDA2_ID): cv.use_id(MR60FDA2Component),
+    cv.Optional(CONF_INSTALL_HEIGHT): select.select_schema(
+        InstallHeightSelect,
+        entity_category=ENTITY_CATEGORY_CONFIG,
+        icon=ICON_ACCELERATION_Z,
+    ),
+    cv.Optional(CONF_HEIGHT_THRESHOLD): select.select_schema(
+        HeightThresholdSelect,
+        entity_category=ENTITY_CATEGORY_CONFIG,
+        icon=ICON_ACCELERATION_Z,
+    ),
+    cv.Optional(CONF_SENSITIVITY): select.select_schema(
+        SensitivitySelect,
+        entity_category=ENTITY_CATEGORY_CONFIG,
+    ),
+}
+
+
+async def to_code(config):
+    mr60fda2_component = await cg.get_variable(config[CONF_MR60FDA2_ID])
+    if install_height_config := config.get(CONF_INSTALL_HEIGHT):
+        s = await select.new_select(
+            install_height_config,
+            options=["2.4m", "2.5m", "2.6m", "2.7m", "2.8m", "2.9m", "3.0m"],
+        )
+        await cg.register_parented(s, config[CONF_MR60FDA2_ID])
+        cg.add(mr60fda2_component.set_install_height_select(s))
+    if height_threshold_config := config.get(CONF_HEIGHT_THRESHOLD):
+        s = await select.new_select(
+            height_threshold_config,
+            options=["0.0m", "0.1m", "0.2m", "0.3m", "0.4m", "0.5m", "0.6m"],
+        )
+        await cg.register_parented(s, config[CONF_MR60FDA2_ID])
+        cg.add(mr60fda2_component.set_height_threshold_select(s))
+    if sensitivity_config := config.get(CONF_SENSITIVITY):
+        s = await select.new_select(
+            sensitivity_config,
+            options=["1", "2", "3"],
+        )
+        await cg.register_parented(s, config[CONF_MR60FDA2_ID])
+        cg.add(mr60fda2_component.set_sensitivity_select(s))
diff --git a/esphome/components/seeed_mr60fda2/select/height_threshold_select.cpp b/esphome/components/seeed_mr60fda2/select/height_threshold_select.cpp
new file mode 100644
index 0000000000..037f8c6036
--- /dev/null
+++ b/esphome/components/seeed_mr60fda2/select/height_threshold_select.cpp
@@ -0,0 +1,15 @@
+#include "height_threshold_select.h"
+
+namespace esphome {
+namespace seeed_mr60fda2 {
+
+void HeightThresholdSelect::control(const std::string &value) {
+  this->publish_state(value);
+  auto index = this->index_of(value);
+  if (index.has_value()) {
+    this->parent_->set_height_threshold(index.value());
+  }
+}
+
+}  // namespace seeed_mr60fda2
+}  // namespace esphome
diff --git a/esphome/components/seeed_mr60fda2/select/height_threshold_select.h b/esphome/components/seeed_mr60fda2/select/height_threshold_select.h
new file mode 100644
index 0000000000..b856dbc89a
--- /dev/null
+++ b/esphome/components/seeed_mr60fda2/select/height_threshold_select.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "esphome/components/select/select.h"
+#include "../seeed_mr60fda2.h"
+
+namespace esphome {
+namespace seeed_mr60fda2 {
+
+class HeightThresholdSelect : public select::Select, public Parented<MR60FDA2Component> {
+ public:
+  HeightThresholdSelect() = default;
+
+ protected:
+  void control(const std::string &value) override;
+};
+
+}  // namespace seeed_mr60fda2
+}  // namespace esphome
diff --git a/esphome/components/seeed_mr60fda2/select/install_height_select.cpp b/esphome/components/seeed_mr60fda2/select/install_height_select.cpp
new file mode 100644
index 0000000000..e791911613
--- /dev/null
+++ b/esphome/components/seeed_mr60fda2/select/install_height_select.cpp
@@ -0,0 +1,15 @@
+#include "install_height_select.h"
+
+namespace esphome {
+namespace seeed_mr60fda2 {
+
+void InstallHeightSelect::control(const std::string &value) {
+  this->publish_state(value);
+  auto index = this->index_of(value);
+  if (index.has_value()) {
+    this->parent_->set_install_height(index.value());
+  }
+}
+
+}  // namespace seeed_mr60fda2
+}  // namespace esphome
diff --git a/esphome/components/seeed_mr60fda2/select/install_height_select.h b/esphome/components/seeed_mr60fda2/select/install_height_select.h
new file mode 100644
index 0000000000..7430da3493
--- /dev/null
+++ b/esphome/components/seeed_mr60fda2/select/install_height_select.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "esphome/components/select/select.h"
+#include "../seeed_mr60fda2.h"
+
+namespace esphome {
+namespace seeed_mr60fda2 {
+
+class InstallHeightSelect : public select::Select, public Parented<MR60FDA2Component> {
+ public:
+  InstallHeightSelect() = default;
+
+ protected:
+  void control(const std::string &value) override;
+};
+
+}  // namespace seeed_mr60fda2
+}  // namespace esphome
diff --git a/esphome/components/seeed_mr60fda2/select/sensitivity_select.cpp b/esphome/components/seeed_mr60fda2/select/sensitivity_select.cpp
new file mode 100644
index 0000000000..e2507fb7cc
--- /dev/null
+++ b/esphome/components/seeed_mr60fda2/select/sensitivity_select.cpp
@@ -0,0 +1,15 @@
+#include "sensitivity_select.h"
+
+namespace esphome {
+namespace seeed_mr60fda2 {
+
+void SensitivitySelect::control(const std::string &value) {
+  this->publish_state(value);
+  auto index = this->index_of(value);
+  if (index.has_value()) {
+    this->parent_->set_sensitivity(index.value());
+  }
+}
+
+}  // namespace seeed_mr60fda2
+}  // namespace esphome
diff --git a/esphome/components/seeed_mr60fda2/select/sensitivity_select.h b/esphome/components/seeed_mr60fda2/select/sensitivity_select.h
new file mode 100644
index 0000000000..d1accc1b5b
--- /dev/null
+++ b/esphome/components/seeed_mr60fda2/select/sensitivity_select.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "esphome/components/select/select.h"
+#include "../seeed_mr60fda2.h"
+
+namespace esphome {
+namespace seeed_mr60fda2 {
+
+class SensitivitySelect : public select::Select, public Parented<MR60FDA2Component> {
+ public:
+  SensitivitySelect() = default;
+
+ protected:
+  void control(const std::string &value) override;
+};
+
+}  // namespace seeed_mr60fda2
+}  // namespace esphome
diff --git a/tests/components/seeed_mr60fda2/common.yaml b/tests/components/seeed_mr60fda2/common.yaml
new file mode 100644
index 0000000000..55a7cc1ab3
--- /dev/null
+++ b/tests/components/seeed_mr60fda2/common.yaml
@@ -0,0 +1,34 @@
+uart:
+  - id: seeed_mr60fda2_uart
+    tx_pin: ${uart_tx_pin}
+    rx_pin: ${uart_rx_pin}
+    baud_rate: 115200
+    parity: NONE
+    stop_bits: 1
+
+seeed_mr60fda2:
+  id: my_seeed_mr60fda2
+  uart_id: seeed_mr60fda2_uart
+
+binary_sensor:
+  - platform: seeed_mr60fda2
+    people_exist:
+      name: "Person Information"
+    fall_detected:
+      name: "Falling Information"
+
+button:
+  - platform: seeed_mr60fda2
+    get_radar_parameters:
+      name: "Get Radar Parameters"
+    factory_reset:
+      name: "Reset"
+
+select:
+  - platform: seeed_mr60fda2
+    install_height:
+      name: "Set Install Height"
+    height_threshold:
+      name: "Set Height Threshold"
+    sensitivity:
+      name: "Set Sensitivity"
diff --git a/tests/components/seeed_mr60fda2/test.esp32-c3-ard.yaml b/tests/components/seeed_mr60fda2/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..4fb884abf4
--- /dev/null
+++ b/tests/components/seeed_mr60fda2/test.esp32-c3-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  uart_tx_pin: GPIO5
+  uart_rx_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/seeed_mr60fda2/test.esp32-c3-idf.yaml b/tests/components/seeed_mr60fda2/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..4fb884abf4
--- /dev/null
+++ b/tests/components/seeed_mr60fda2/test.esp32-c3-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  uart_tx_pin: GPIO5
+  uart_rx_pin: GPIO4
+
+<<: !include common.yaml

From ae6736311a47ddc5c63dd36b0d2cb4af29f4dd14 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 25 Nov 2024 22:29:36 -0600
Subject: [PATCH 179/282] [lvgl] clang-tidy fixes for #7822 (#7843)

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

diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp
index 41346bc732..61bdfe9755 100644
--- a/esphome/components/lvgl/lvgl_esphome.cpp
+++ b/esphome/components/lvgl/lvgl_esphome.cpp
@@ -279,7 +279,7 @@ std::string LvSelectable::get_selected_text() {
 static std::string join_string(std::vector<std::string> options) {
   return std::accumulate(
       options.begin(), options.end(), std::string(),
-      [](const std::string &a, const std::string &b) -> std::string { return a + (a.length() > 0 ? "\n" : "") + b; });
+      [](const std::string &a, const std::string &b) -> std::string { return a + (!a.empty() ? "\n" : "") + b; });
 }
 
 void LvSelectable::set_selected_text(const std::string &text, lv_anim_enable_t anim) {

From 72df3d1606aaaf650d49c342439bb2294d9ca76c Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 03:37:20 -0600
Subject: [PATCH 180/282] [xiaomi_ble] clang-tidy fixes for #7822 (#7860)

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

diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp
index 85434341cc..04e0724ba7 100644
--- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp
+++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp
@@ -249,7 +249,7 @@ optional<XiaomiParseResult> parse_xiaomi_header(const esp32_ble_tracker::Service
 }
 
 bool decrypt_xiaomi_payload(std::vector<uint8_t> &raw, const uint8_t *bindkey, const uint64_t &address) {
-  if (!((raw.size() == 19) || ((raw.size() >= 22) && (raw.size() <= 24)))) {
+  if ((raw.size() != 19) && ((raw.size() < 22) || (raw.size() > 24))) {
     ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): data packet has wrong size (%d)!", raw.size());
     ESP_LOGVV(TAG, "  Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str());
     return false;

From 11076e4614075109ce8cc51d4aae70559fc2394b Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 03:47:24 -0600
Subject: [PATCH 181/282] [wireguard] clang-tidy fixes for #7822 (#7859)

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

diff --git a/esphome/components/wireguard/wireguard.cpp b/esphome/components/wireguard/wireguard.cpp
index 7b4011cb79..431bf52227 100644
--- a/esphome/components/wireguard/wireguard.cpp
+++ b/esphome/components/wireguard/wireguard.cpp
@@ -37,7 +37,7 @@ void Wireguard::setup() {
   this->wg_config_.netmask = this->netmask_.c_str();
   this->wg_config_.persistent_keepalive = this->keepalive_;
 
-  if (this->preshared_key_.length() > 0)
+  if (!this->preshared_key_.empty())
     this->wg_config_.preshared_key = this->preshared_key_.c_str();
 
   this->publish_enabled_state();
@@ -137,7 +137,7 @@ void Wireguard::dump_config() {
   ESP_LOGCONFIG(TAG, "  Peer Port: " LOG_SECRET("%d"), this->peer_port_);
   ESP_LOGCONFIG(TAG, "  Peer Public Key: " LOG_SECRET("%s"), this->peer_public_key_.c_str());
   ESP_LOGCONFIG(TAG, "  Peer Pre-shared Key: " LOG_SECRET("%s"),
-                (this->preshared_key_.length() > 0 ? mask_key(this->preshared_key_).c_str() : "NOT IN USE"));
+                (!this->preshared_key_.empty() ? mask_key(this->preshared_key_).c_str() : "NOT IN USE"));
   ESP_LOGCONFIG(TAG, "  Peer Allowed IPs:");
   for (auto &allowed_ip : this->allowed_ips_) {
     ESP_LOGCONFIG(TAG, "    - %s/%s", std::get<0>(allowed_ip).c_str(), std::get<1>(allowed_ip).c_str());

From 841d278224d7a82ff8112cb3ee80dbe31347d84b Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 03:47:57 -0600
Subject: [PATCH 182/282] [dsmr] clang-tidy fixes for #7822 (#7848)

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

diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp
index 193ea1d4e5..c0a2883d79 100644
--- a/esphome/components/dsmr/dsmr.cpp
+++ b/esphome/components/dsmr/dsmr.cpp
@@ -296,7 +296,7 @@ void Dsmr::dump_config() {
 }
 
 void Dsmr::set_decryption_key(const std::string &decryption_key) {
-  if (decryption_key.length() == 0) {
+  if (decryption_key.empty()) {
     ESP_LOGI(TAG, "Disabling decryption");
     this->decryption_key_.clear();
     if (this->crypt_telegram_ != nullptr) {

From 6e50e2aa6534c31dbc9e425781c76581178ea68c Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Tue, 26 Nov 2024 22:50:16 +1300
Subject: [PATCH 183/282] Fix entity name validation to allow "Off" and "On"
 (#7821)

---
 esphome/config_validation.py | 26 +++++++++++++++-----------
 1 file changed, 15 insertions(+), 11 deletions(-)

diff --git a/esphome/config_validation.py b/esphome/config_validation.py
index 98b81ec328..ebfb2631c3 100644
--- a/esphome/config_validation.py
+++ b/esphome/config_validation.py
@@ -1839,8 +1839,6 @@ def validate_registry_entry(name, registry):
 def none(value):
     if value in ("none", "None"):
         return None
-    if boolean(value) is False:
-        return None
     raise Invalid("Must be none")
 
 
@@ -1912,17 +1910,23 @@ MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend(
     }
 )
 
+
+def _validate_entity_name(value):
+    value = string(value)
+    try:
+        value = none(value)  # pylint: disable=assignment-from-none
+    except Invalid:
+        pass
+    else:
+        requires_friendly_name(
+            "Name cannot be None when esphome->friendly_name is not set!"
+        )(value)
+    return value
+
+
 ENTITY_BASE_SCHEMA = Schema(
     {
-        Optional(CONF_NAME): Any(
-            All(
-                none,
-                requires_friendly_name(
-                    "Name cannot be None when esphome->friendly_name is not set!"
-                ),
-            ),
-            string,
-        ),
+        Optional(CONF_NAME): _validate_entity_name,
         Optional(CONF_INTERNAL): boolean,
         Optional(CONF_DISABLED_BY_DEFAULT, default=False): boolean,
         Optional(CONF_ICON): icon,

From 2eac8b6c4643494640aa6889025ba5791c09dc36 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 03:50:33 -0600
Subject: [PATCH 184/282] [camera_web_server] Use header instead of mock struct
 (#7823)

---
 esphome/components/esp32_camera_web_server/camera_web_server.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.h b/esphome/components/esp32_camera_web_server/camera_web_server.h
index f65625554c..3ba8f31dd7 100644
--- a/esphome/components/esp32_camera_web_server/camera_web_server.h
+++ b/esphome/components/esp32_camera_web_server/camera_web_server.h
@@ -11,7 +11,7 @@
 #include "esphome/core/helpers.h"
 #include "esphome/core/preferences.h"
 
-struct httpd_req;
+struct httpd_req;  // NOLINT(readability-identifier-naming)
 
 namespace esphome {
 namespace esp32_camera_web_server {

From 1c2d2bce5af36527abe758d88f68289b7bccd482 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 03:52:26 -0600
Subject: [PATCH 185/282] [display_menu_base] clang-tidy fixes for #7822
 (#7847)

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

diff --git a/esphome/components/display_menu_base/display_menu_base.cpp b/esphome/components/display_menu_base/display_menu_base.cpp
index 5502623607..2d8e6ae5fc 100644
--- a/esphome/components/display_menu_base/display_menu_base.cpp
+++ b/esphome/components/display_menu_base/display_menu_base.cpp
@@ -280,7 +280,7 @@ bool DisplayMenuComponent::cursor_down_() {
 bool DisplayMenuComponent::enter_menu_() {
   this->displayed_item_->on_leave();
   this->displayed_item_ = static_cast<MenuItemMenu *>(this->get_selected_item_());
-  this->selection_stack_.push_front({this->top_index_, this->cursor_index_});
+  this->selection_stack_.emplace_front(this->top_index_, this->cursor_index_);
   this->cursor_index_ = this->top_index_ = 0;
   this->displayed_item_->on_enter();
 

From 536bcab5def03c8177cdfc31c6deb3aa01d8ccf3 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 03:52:57 -0600
Subject: [PATCH 186/282] [nextion] clang-tidy fixes for #7822 (#7852)

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

diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp
index 984db09c57..7c41f8dfe2 100644
--- a/esphome/components/nextion/nextion.cpp
+++ b/esphome/components/nextion/nextion.cpp
@@ -343,7 +343,7 @@ void Nextion::process_serial_() {
 }
 // nextion.tech/instruction-set/
 void Nextion::process_nextion_commands_() {
-  if (this->command_data_.length() == 0) {
+  if (this->command_data_.empty()) {
     return;
   }
 

From 2d4688a2061e5734ad136f717ba77a7dc7a9fec7 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 03:53:23 -0600
Subject: [PATCH 187/282] [shelly_dimmer] clang-tidy fixes for #7822 (#7844)

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

diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp
index b415840bdc..d4229b2384 100644
--- a/esphome/components/shelly_dimmer/shelly_dimmer.cpp
+++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp
@@ -466,7 +466,7 @@ bool ShellyDimmer::handle_frame_() {
     }
     case SHELLY_DIMMER_PROTO_CMD_SWITCH:
     case SHELLY_DIMMER_PROTO_CMD_SETTINGS: {
-      return !(payload_len < 1 || payload[0] != 0x01);
+      return payload_len >= 1 && payload[0] == 0x01;
     }
     default: {
       return false;

From e6bd2238ce1fceef7db2cadbae3736a5759e16b2 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 03:54:16 -0600
Subject: [PATCH 188/282] [sim800l] clang-tidy fixes for #7822 (#7856)

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

diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp
index 4f7aa228e9..38775b0062 100644
--- a/esphome/components/sim800l/sim800l.cpp
+++ b/esphome/components/sim800l/sim800l.cpp
@@ -324,7 +324,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
         this->sms_received_callback_.call(this->message_, this->sender_);
         this->state_ = STATE_RECEIVED_SMS;
       } else {
-        if (this->message_.length() > 0)
+        if (!this->message_.empty())
           this->message_ += "\n";
         this->message_ += message;
       }

From 6b59f55a5097f40c3db18ded853cc27888bd2885 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 03:58:18 -0600
Subject: [PATCH 189/282] [nfc, pn532, pn7150, pn7160] clang-tidy fixes for
 #7822 (#7853)

---
 esphome/components/nfc/ndef_record.cpp                 | 6 +++---
 esphome/components/pn532/pn532_mifare_ultralight.cpp   | 4 ++--
 esphome/components/pn7150/pn7150_mifare_ultralight.cpp | 4 ++--
 esphome/components/pn7160/pn7160_mifare_ultralight.cpp | 4 ++--
 4 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/esphome/components/nfc/ndef_record.cpp b/esphome/components/nfc/ndef_record.cpp
index 8eb0c3b901..540ba62940 100644
--- a/esphome/components/nfc/ndef_record.cpp
+++ b/esphome/components/nfc/ndef_record.cpp
@@ -30,13 +30,13 @@ std::vector<uint8_t> NdefRecord::encode(bool first, bool last) {
     data.push_back(payload_length & 0xFF);
   }
 
-  if (this->id_.length()) {
+  if (!this->id_.empty()) {
     data.push_back(this->id_.length());
   }
 
   data.insert(data.end(), this->type_.begin(), this->type_.end());
 
-  if (this->id_.length()) {
+  if (!this->id_.empty()) {
     data.insert(data.end(), this->id_.begin(), this->id_.end());
   }
 
@@ -55,7 +55,7 @@ uint8_t NdefRecord::create_flag_byte(bool first, bool last, size_t payload_size)
   if (payload_size <= 255) {
     value = value | 0x10;  // Set SR bit
   }
-  if (this->id_.length()) {
+  if (!this->id_.empty()) {
     value = value | 0x08;  // Set IL bit
   }
   return value;
diff --git a/esphome/components/pn532/pn532_mifare_ultralight.cpp b/esphome/components/pn532/pn532_mifare_ultralight.cpp
index b08a7336c7..f823829a6c 100644
--- a/esphome/components/pn532/pn532_mifare_ultralight.cpp
+++ b/esphome/components/pn532/pn532_mifare_ultralight.cpp
@@ -80,8 +80,8 @@ bool PN532::is_mifare_ultralight_formatted_(const std::vector<uint8_t> &page_3_t
   const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;  // page 4 will begin 4 bytes into the vector
 
   return (page_3_to_6.size() > p4_offset + 3) &&
-         !((page_3_to_6[p4_offset + 0] == 0xFF) && (page_3_to_6[p4_offset + 1] == 0xFF) &&
-           (page_3_to_6[p4_offset + 2] == 0xFF) && (page_3_to_6[p4_offset + 3] == 0xFF));
+         ((page_3_to_6[p4_offset + 0] != 0xFF) || (page_3_to_6[p4_offset + 1] != 0xFF) ||
+          (page_3_to_6[p4_offset + 2] != 0xFF) || (page_3_to_6[p4_offset + 3] != 0xFF));
 }
 
 uint16_t PN532::read_mifare_ultralight_capacity_() {
diff --git a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp
index 791b0634d6..b107f6f79e 100644
--- a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp
+++ b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp
@@ -81,8 +81,8 @@ bool PN7150::is_mifare_ultralight_formatted_(const std::vector<uint8_t> &page_3_
   const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;  // page 4 will begin 4 bytes into the vector
 
   return (page_3_to_6.size() > p4_offset + 3) &&
-         !((page_3_to_6[p4_offset + 0] == 0xFF) && (page_3_to_6[p4_offset + 1] == 0xFF) &&
-           (page_3_to_6[p4_offset + 2] == 0xFF) && (page_3_to_6[p4_offset + 3] == 0xFF));
+         ((page_3_to_6[p4_offset + 0] != 0xFF) || (page_3_to_6[p4_offset + 1] != 0xFF) ||
+          (page_3_to_6[p4_offset + 2] != 0xFF) || (page_3_to_6[p4_offset + 3] != 0xFF));
 }
 
 uint16_t PN7150::read_mifare_ultralight_capacity_() {
diff --git a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp
index a74f23d4f2..65daac494f 100644
--- a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp
+++ b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp
@@ -81,8 +81,8 @@ bool PN7160::is_mifare_ultralight_formatted_(const std::vector<uint8_t> &page_3_
   const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;  // page 4 will begin 4 bytes into the vector
 
   return (page_3_to_6.size() > p4_offset + 3) &&
-         !((page_3_to_6[p4_offset + 0] == 0xFF) && (page_3_to_6[p4_offset + 1] == 0xFF) &&
-           (page_3_to_6[p4_offset + 2] == 0xFF) && (page_3_to_6[p4_offset + 3] == 0xFF));
+         ((page_3_to_6[p4_offset + 0] != 0xFF) || (page_3_to_6[p4_offset + 1] != 0xFF) ||
+          (page_3_to_6[p4_offset + 2] != 0xFF) || (page_3_to_6[p4_offset + 3] != 0xFF));
 }
 
 uint16_t PN7160::read_mifare_ultralight_capacity_() {

From 31c13e4c16f18e3ae241b223a529ee5ed2a08fd8 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 03:59:29 -0600
Subject: [PATCH 190/282] [output] clang-tidy fixes for #7822 (#7854)

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

diff --git a/esphome/components/output/float_output.cpp b/esphome/components/output/float_output.cpp
index f120f86f1f..e7dba1d81d 100644
--- a/esphome/components/output/float_output.cpp
+++ b/esphome/components/output/float_output.cpp
@@ -32,7 +32,7 @@ void FloatOutput::set_level(float state) {
   }
 #endif
 
-  if (!(state == 0.0f && this->zero_means_zero_))  // regardless of min_power_, 0.0 means off
+  if (state != 0.0f || !this->zero_means_zero_)  // regardless of min_power_, 0.0 means off
     state = (state * (this->max_power_ - this->min_power_)) + this->min_power_;
 
   if (this->is_inverted())

From bdc6302ea1b17319d2529d9d5e277ba416e18687 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 04:00:03 -0600
Subject: [PATCH 191/282] [sun_gtil2] clang-tidy fixes for #7822 (#7858)

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

diff --git a/esphome/components/sun_gtil2/sun_gtil2.cpp b/esphome/components/sun_gtil2/sun_gtil2.cpp
index 1653f937dd..46b4902654 100644
--- a/esphome/components/sun_gtil2/sun_gtil2.cpp
+++ b/esphome/components/sun_gtil2/sun_gtil2.cpp
@@ -83,7 +83,7 @@ void SunGTIL2::handle_char_(uint8_t c) {
   memcpy(&msg, this->rx_message_.data(), MESSAGE_SIZE);
   this->rx_message_.clear();
 
-  if (!((msg.end[0] == 0) && (msg.end[38] == 0x08)))
+  if ((msg.end[0] != 0) || (msg.end[38] != 0x08))
     return;
 
   ESP_LOGVV(TAG, "Frequency raw value: %02x", msg.frequency);

From 4c383906c490192a692cc74da82fca5d81fe17fc Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 04:00:40 -0600
Subject: [PATCH 192/282] [pipsolar] clang-tidy fixes for #7822 (#7855)

---
 esphome/components/pipsolar/pipsolar.cpp               | 4 ++--
 esphome/components/pipsolar/switch/pipsolar_switch.cpp | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp
index c4bc018b75..03c699e7d4 100644
--- a/esphome/components/pipsolar/pipsolar.cpp
+++ b/esphome/components/pipsolar/pipsolar.cpp
@@ -790,7 +790,7 @@ uint8_t Pipsolar::check_incoming_crc_() {
 // send next command used
 uint8_t Pipsolar::send_next_command_() {
   uint16_t crc16;
-  if (this->command_queue_[this->command_queue_position_].length() != 0) {
+  if (!this->command_queue_[this->command_queue_position_].empty()) {
     const char *command = this->command_queue_[this->command_queue_position_].c_str();
     uint8_t byte_command[16];
     uint8_t length = this->command_queue_[this->command_queue_position_].length();
@@ -846,7 +846,7 @@ void Pipsolar::queue_command_(const char *command, uint8_t length) {
   uint8_t next_position = command_queue_position_;
   for (uint8_t i = 0; i < COMMAND_QUEUE_LENGTH; i++) {
     uint8_t testposition = (next_position + i) % COMMAND_QUEUE_LENGTH;
-    if (command_queue_[testposition].length() == 0) {
+    if (command_queue_[testposition].empty()) {
       command_queue_[testposition] = command;
       ESP_LOGD(TAG, "Command queued successfully: %s with length %u at position %d", command,
                command_queue_[testposition].length(), testposition);
diff --git a/esphome/components/pipsolar/switch/pipsolar_switch.cpp b/esphome/components/pipsolar/switch/pipsolar_switch.cpp
index 7eaeac1c2d..be7763226b 100644
--- a/esphome/components/pipsolar/switch/pipsolar_switch.cpp
+++ b/esphome/components/pipsolar/switch/pipsolar_switch.cpp
@@ -10,11 +10,11 @@ static const char *const TAG = "pipsolar.switch";
 void PipsolarSwitch::dump_config() { LOG_SWITCH("", "Pipsolar Switch", this); }
 void PipsolarSwitch::write_state(bool state) {
   if (state) {
-    if (this->on_command_.length() > 0) {
+    if (!this->on_command_.empty()) {
       this->parent_->switch_command(this->on_command_);
     }
   } else {
-    if (this->off_command_.length() > 0) {
+    if (!this->off_command_.empty()) {
       this->parent_->switch_command(this->off_command_);
     }
   }

From 2fa8d907b3506a0db2dc4ed28940bb7d7e30ca4f Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 04:01:34 -0600
Subject: [PATCH 193/282] [ltr501] clang-tidy fixes for #7822 (#7850)

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

diff --git a/esphome/components/ltr501/ltr501.cpp b/esphome/components/ltr501/ltr501.cpp
index 4f4e26f44f..b30e520f2b 100644
--- a/esphome/components/ltr501/ltr501.cpp
+++ b/esphome/components/ltr501/ltr501.cpp
@@ -23,7 +23,7 @@ bool operator==(const GainTimePair &lhs, const GainTimePair &rhs) {
 }
 
 bool operator!=(const GainTimePair &lhs, const GainTimePair &rhs) {
-  return !(lhs.gain == rhs.gain && lhs.time == rhs.time);
+  return lhs.gain != rhs.gain || lhs.time != rhs.time;
 }
 
 template<typename T, size_t size> T get_next(const T (&array)[size], const T val) {

From cd1ee9660644177c99302d0698cefed6a9802361 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 04:04:50 -0600
Subject: [PATCH 194/282] [cse7766] clang-tidy fixes for #7822 (#7846)

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

diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp
index 48240464b3..3907c195d0 100644
--- a/esphome/components/cse7766/cse7766.cpp
+++ b/esphome/components/cse7766/cse7766.cpp
@@ -43,7 +43,7 @@ bool CSE7766Component::check_byte_() {
   uint8_t index = this->raw_data_index_;
   uint8_t byte = this->raw_data_[index];
   if (index == 0) {
-    return !((byte != 0x55) && ((byte & 0xF0) != 0xF0) && (byte != 0xAA));
+    return (byte == 0x55) || ((byte & 0xF0) == 0xF0) || (byte == 0xAA);
   }
 
   if (index == 1) {

From be7882727448093efd3043364607ad925058606b Mon Sep 17 00:00:00 2001
From: Samuel Sieb <samuel-github@sieb.net>
Date: Tue, 26 Nov 2024 00:05:20 -1000
Subject: [PATCH 195/282] [honeywell] use warning instead of failing (#7862)

Co-authored-by: Samuel Sieb <samuel@sieb.net>
---
 .../components/honeywellabp2_i2c/honeywellabp2.cpp    | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp b/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp
index e2910032cc..d111723669 100644
--- a/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp
+++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp
@@ -15,7 +15,7 @@ static const char *const TAG = "honeywellabp2";
 void HONEYWELLABP2Sensor::read_sensor_data() {
   if (this->read(raw_data_, 7) != i2c::ERROR_OK) {
     ESP_LOGE(TAG, "Communication with ABP2 failed!");
-    this->mark_failed();
+    this->status_set_warning("couldn't read sensor data");
     return;
   }
   float press_counts = encode_uint24(raw_data_[1], raw_data_[2], raw_data_[3]);  // calculate digital pressure counts
@@ -25,12 +25,13 @@ void HONEYWELLABP2Sensor::read_sensor_data() {
                           (this->max_pressure_ - this->min_pressure_)) +
                          this->min_pressure_;
   this->last_temperature_ = (temp_counts * 200 / 16777215) - 50;
+  this->status_clear_warning();
 }
 
 void HONEYWELLABP2Sensor::start_measurement() {
   if (this->write(i2c_cmd_, 3) != i2c::ERROR_OK) {
     ESP_LOGE(TAG, "Communication with ABP2 failed!");
-    this->mark_failed();
+    this->status_set_warning("couldn't start measurement");
     return;
   }
   this->measurement_running_ = true;
@@ -39,7 +40,7 @@ void HONEYWELLABP2Sensor::start_measurement() {
 bool HONEYWELLABP2Sensor::is_measurement_ready() {
   if (this->read(raw_data_, 1) != i2c::ERROR_OK) {
     ESP_LOGE(TAG, "Communication with ABP2 failed!");
-    this->mark_failed();
+    this->status_set_warning("couldn't check measurement");
     return false;
   }
   if ((raw_data_[0] & (0x1 << STATUS_BIT_BUSY)) > 0) {
@@ -52,7 +53,7 @@ bool HONEYWELLABP2Sensor::is_measurement_ready() {
 void HONEYWELLABP2Sensor::measurement_timeout() {
   ESP_LOGE(TAG, "Timeout!");
   this->measurement_running_ = false;
-  this->mark_failed();
+  this->status_set_warning("measurement timed out");
 }
 
 float HONEYWELLABP2Sensor::get_pressure() { return this->last_pressure_; }
@@ -79,7 +80,7 @@ void HONEYWELLABP2Sensor::update() {
   ESP_LOGV(TAG, "Update Honeywell ABP2 Sensor");
 
   this->start_measurement();
-  this->set_timeout("meas_timeout", 50, [this] { this->measurement_timeout(); });
+  this->set_timeout("meas_timeout", 100, [this] { this->measurement_timeout(); });
 }
 
 void HONEYWELLABP2Sensor::dump_config() {

From 2b9013699d841a7878a796f5621adfa3461f597d Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 04:05:39 -0600
Subject: [PATCH 196/282] [alarm_control_panel] clang-tidy fixes for #7822
 (#7845)

---
 .../alarm_control_panel/alarm_control_panel_call.cpp       | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp
index b1d2b2a097..7bb9b9989c 100644
--- a/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp
+++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp
@@ -72,10 +72,9 @@ void AlarmControlPanelCall::validate_() {
       this->state_.reset();
       return;
     }
-    if (state == ACP_STATE_DISARMED &&
-        !(this->parent_->is_state_armed(this->parent_->get_state()) ||
-          this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_ARMING ||
-          this->parent_->get_state() == ACP_STATE_TRIGGERED)) {
+    if (state == ACP_STATE_DISARMED && !this->parent_->is_state_armed(this->parent_->get_state()) &&
+        this->parent_->get_state() != ACP_STATE_PENDING && this->parent_->get_state() != ACP_STATE_ARMING &&
+        this->parent_->get_state() != ACP_STATE_TRIGGERED) {
       ESP_LOGW(TAG, "Cannot disarm when not armed");
       this->state_.reset();
       return;

From 3730b0310b23c989c8c27d5701c3d423b6c749d6 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 12:07:36 -0600
Subject: [PATCH 197/282] [sprinkler] clang-tidy fixes for #7822 (#7857)

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

diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp
index 59565251c3..5384d29871 100644
--- a/esphome/components/sprinkler/sprinkler.cpp
+++ b/esphome/components/sprinkler/sprinkler.cpp
@@ -419,7 +419,7 @@ void Sprinkler::add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControll
   SprinklerValve *new_valve = &this->valve_[new_valve_number];
 
   new_valve->controller_switch = valve_sw;
-  new_valve->controller_switch->set_state_lambda([=]() -> optional<bool> {
+  new_valve->controller_switch->set_state_lambda([this, new_valve_number]() -> optional<bool> {
     if (this->valve_pump_switch(new_valve_number) != nullptr) {
       return this->valve_switch(new_valve_number)->state() && this->valve_pump_switch(new_valve_number)->state();
     }
@@ -445,7 +445,7 @@ void Sprinkler::add_controller(Sprinkler *other_controller) { this->other_contro
 
 void Sprinkler::set_controller_main_switch(SprinklerControllerSwitch *controller_switch) {
   this->controller_sw_ = controller_switch;
-  controller_switch->set_state_lambda([=]() -> optional<bool> {
+  controller_switch->set_state_lambda([this]() -> optional<bool> {
     for (size_t valve_number = 0; valve_number < this->number_of_valves(); valve_number++) {
       if (this->valve_[valve_number].controller_switch->state) {
         return true;

From 53691d28a81644b84f82e4441da153b27cbfddd7 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 12:07:42 -0600
Subject: [PATCH 198/282] [haier] clang-tidy fixes for #7822 (#7849)

---
 esphome/components/haier/hon_climate.cpp | 135 ++++++++++-------------
 1 file changed, 60 insertions(+), 75 deletions(-)

diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp
index e7be1fa418..85e9cf37b9 100644
--- a/esphome/components/haier/hon_climate.cpp
+++ b/esphome/components/haier/hon_climate.cpp
@@ -1069,19 +1069,17 @@ void HonClimate::fill_control_messages_queue_() {
   climate_control = this->current_hvac_settings_;
   // Beeper command
   {
-    this->control_messages_queue_.push(
-        haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
-                                     (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
-                                         (uint8_t) hon_protocol::DataParameters::BEEPER_STATUS,
-                                     this->get_beeper_state() ? ZERO_BUF : ONE_BUF, 2));
+    this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
+                                          (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
+                                              (uint8_t) hon_protocol::DataParameters::BEEPER_STATUS,
+                                          this->get_beeper_state() ? ZERO_BUF : ONE_BUF, 2);
   }
   // Health mode
   {
-    this->control_messages_queue_.push(
-        haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
-                                     (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
-                                         (uint8_t) hon_protocol::DataParameters::HEALTH_MODE,
-                                     this->get_health_mode() ? ONE_BUF : ZERO_BUF, 2));
+    this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
+                                          (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
+                                              (uint8_t) hon_protocol::DataParameters::HEALTH_MODE,
+                                          this->get_health_mode() ? ONE_BUF : ZERO_BUF, 2);
     this->health_mode_ = (SwitchState) ((uint8_t) this->health_mode_ & 0b01);
   }
   // Climate mode
@@ -1099,51 +1097,46 @@ void HonClimate::fill_control_messages_queue_() {
       case CLIMATE_MODE_HEAT_COOL:
         new_power = true;
         buffer[1] = (uint8_t) hon_protocol::ConditioningMode::AUTO;
-        this->control_messages_queue_.push(
-            haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
-                                         (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
-                                             (uint8_t) hon_protocol::DataParameters::AC_MODE,
-                                         buffer, 2));
+        this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
+                                              (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
+                                                  (uint8_t) hon_protocol::DataParameters::AC_MODE,
+                                              buffer, 2);
         fan_mode_buf[1] = this->other_modes_fan_speed_;
         break;
       case CLIMATE_MODE_HEAT:
         new_power = true;
         buffer[1] = (uint8_t) hon_protocol::ConditioningMode::HEAT;
-        this->control_messages_queue_.push(
-            haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
-                                         (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
-                                             (uint8_t) hon_protocol::DataParameters::AC_MODE,
-                                         buffer, 2));
+        this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
+                                              (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
+                                                  (uint8_t) hon_protocol::DataParameters::AC_MODE,
+                                              buffer, 2);
         fan_mode_buf[1] = this->other_modes_fan_speed_;
         break;
       case CLIMATE_MODE_DRY:
         new_power = true;
         buffer[1] = (uint8_t) hon_protocol::ConditioningMode::DRY;
-        this->control_messages_queue_.push(
-            haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
-                                         (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
-                                             (uint8_t) hon_protocol::DataParameters::AC_MODE,
-                                         buffer, 2));
+        this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
+                                              (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
+                                                  (uint8_t) hon_protocol::DataParameters::AC_MODE,
+                                              buffer, 2);
         fan_mode_buf[1] = this->other_modes_fan_speed_;
         break;
       case CLIMATE_MODE_FAN_ONLY:
         new_power = true;
         buffer[1] = (uint8_t) hon_protocol::ConditioningMode::FAN;
-        this->control_messages_queue_.push(
-            haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
-                                         (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
-                                             (uint8_t) hon_protocol::DataParameters::AC_MODE,
-                                         buffer, 2));
+        this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
+                                              (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
+                                                  (uint8_t) hon_protocol::DataParameters::AC_MODE,
+                                              buffer, 2);
         fan_mode_buf[1] = this->other_modes_fan_speed_;  // Auto doesn't work in fan only mode
         break;
       case CLIMATE_MODE_COOL:
         new_power = true;
         buffer[1] = (uint8_t) hon_protocol::ConditioningMode::COOL;
-        this->control_messages_queue_.push(
-            haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
-                                         (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
-                                             (uint8_t) hon_protocol::DataParameters::AC_MODE,
-                                         buffer, 2));
+        this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
+                                              (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
+                                                  (uint8_t) hon_protocol::DataParameters::AC_MODE,
+                                              buffer, 2);
         fan_mode_buf[1] = this->other_modes_fan_speed_;
         break;
       default:
@@ -1153,11 +1146,10 @@ void HonClimate::fill_control_messages_queue_() {
   }
   // Climate power
   {
-    this->control_messages_queue_.push(
-        haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
-                                     (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
-                                         (uint8_t) hon_protocol::DataParameters::AC_POWER,
-                                     new_power ? ONE_BUF : ZERO_BUF, 2));
+    this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
+                                          (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
+                                              (uint8_t) hon_protocol::DataParameters::AC_POWER,
+                                          new_power ? ONE_BUF : ZERO_BUF, 2);
   }
   // CLimate preset
   {
@@ -1199,36 +1191,32 @@ void HonClimate::fill_control_messages_queue_() {
     }
     auto presets = this->traits_.get_supported_presets();
     if (quiet_mode_buf[1] != 0xFF) {
-      this->control_messages_queue_.push(
-          haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
-                                       (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
-                                           (uint8_t) hon_protocol::DataParameters::QUIET_MODE,
-                                       quiet_mode_buf, 2));
+      this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
+                                            (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
+                                                (uint8_t) hon_protocol::DataParameters::QUIET_MODE,
+                                            quiet_mode_buf, 2);
     }
     if ((fast_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_BOOST) != presets.end()))) {
-      this->control_messages_queue_.push(
-          haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
-                                       (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
-                                           (uint8_t) hon_protocol::DataParameters::FAST_MODE,
-                                       fast_mode_buf, 2));
+      this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
+                                            (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
+                                                (uint8_t) hon_protocol::DataParameters::FAST_MODE,
+                                            fast_mode_buf, 2);
     }
     if ((away_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_AWAY) != presets.end()))) {
-      this->control_messages_queue_.push(
-          haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
-                                       (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
-                                           (uint8_t) hon_protocol::DataParameters::TEN_DEGREE,
-                                       away_mode_buf, 2));
+      this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
+                                            (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
+                                                (uint8_t) hon_protocol::DataParameters::TEN_DEGREE,
+                                            away_mode_buf, 2);
     }
   }
   // Target temperature
   if (climate_control.target_temperature.has_value() && (this->mode != ClimateMode::CLIMATE_MODE_FAN_ONLY)) {
     uint8_t buffer[2] = {0x00, 0x00};
     buffer[1] = ((uint8_t) climate_control.target_temperature.value()) - 16;
-    this->control_messages_queue_.push(
-        haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
-                                     (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
-                                         (uint8_t) hon_protocol::DataParameters::SET_POINT,
-                                     buffer, 2));
+    this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
+                                          (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
+                                              (uint8_t) hon_protocol::DataParameters::SET_POINT,
+                                          buffer, 2);
   }
   // Vertical swing mode
   if (climate_control.swing_mode.has_value()) {
@@ -1248,16 +1236,14 @@ void HonClimate::fill_control_messages_queue_() {
       case CLIMATE_SWING_BOTH:
         break;
     }
-    this->control_messages_queue_.push(
-        haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
-                                     (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
-                                         (uint8_t) hon_protocol::DataParameters::HORIZONTAL_SWING_MODE,
-                                     horizontal_swing_buf, 2));
-    this->control_messages_queue_.push(
-        haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
-                                     (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
-                                         (uint8_t) hon_protocol::DataParameters::VERTICAL_SWING_MODE,
-                                     vertical_swing_buf, 2));
+    this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
+                                          (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
+                                              (uint8_t) hon_protocol::DataParameters::HORIZONTAL_SWING_MODE,
+                                          horizontal_swing_buf, 2);
+    this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
+                                          (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
+                                              (uint8_t) hon_protocol::DataParameters::VERTICAL_SWING_MODE,
+                                          vertical_swing_buf, 2);
   }
   // Fan mode
   if (climate_control.fan_mode.has_value()) {
@@ -1280,11 +1266,10 @@ void HonClimate::fill_control_messages_queue_() {
         break;
     }
     if (fan_mode_buf[1] != 0xFF) {
-      this->control_messages_queue_.push(
-          haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
-                                       (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
-                                           (uint8_t) hon_protocol::DataParameters::FAN_MODE,
-                                       fan_mode_buf, 2));
+      this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
+                                            (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
+                                                (uint8_t) hon_protocol::DataParameters::FAN_MODE,
+                                            fan_mode_buf, 2);
     }
   }
 }

From 39f3f795e2b927af404e80286d5af8100fc7c80d Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 12:07:53 -0600
Subject: [PATCH 199/282] [mqtt] clang-tidy fixes for #7822 (#7851)

---
 esphome/components/mqtt/mqtt_backend_esp32.cpp | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp
index ed500c6d44..2cccb957eb 100644
--- a/esphome/components/mqtt/mqtt_backend_esp32.cpp
+++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp
@@ -151,11 +151,11 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) {
       break;
     case MQTT_EVENT_DATA: {
       static std::string topic;
-      if (event.topic.length() > 0) {
+      if (!event.topic.empty()) {
         topic = event.topic;
       }
       ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", topic.c_str());
-      this->on_message_.call(event.topic.length() > 0 ? topic.c_str() : nullptr, event.data.data(), event.data.size(),
+      this->on_message_.call(!event.topic.empty() ? topic.c_str() : nullptr, event.data.data(), event.data.size(),
                              event.current_data_offset, event.total_data_len);
     } break;
     case MQTT_EVENT_ERROR:
@@ -184,7 +184,7 @@ void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t b
   // queue event to decouple processing
   if (instance) {
     auto event = *static_cast<esp_mqtt_event_t *>(event_data);
-    instance->mqtt_events_.push(Event(event));
+    instance->mqtt_events_.emplace(event);
   }
 }
 

From e3d673d16c766181c9cda046cca78bc17e0936a6 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 26 Nov 2024 12:08:02 -0600
Subject: [PATCH 200/282] [helpers, optional] clang-tidy fixes for #7822
 (#7841)

---
 esphome/core/helpers.cpp | 2 +-
 esphome/core/optional.h  | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp
index befc84516c..103173095b 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -293,7 +293,7 @@ std::string str_sanitize(const std::string &str) {
   std::replace_if(
       out.begin(), out.end(),
       [](const char &c) {
-        return !(c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'));
+        return c != '-' && c != '_' && (c < '0' || c > '9') && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z');
       },
       '_');
   return out;
diff --git a/esphome/core/optional.h b/esphome/core/optional.h
index 1e28ef1354..591bc7aa68 100644
--- a/esphome/core/optional.h
+++ b/esphome/core/optional.h
@@ -59,7 +59,7 @@ template<typename T> class optional {  // NOLINT
     return *this;
   }
 
-  void swap(optional &rhs) {
+  void swap(optional &rhs) noexcept {
     using std::swap;
     if (has_value() && rhs.has_value()) {
       swap(**this, *rhs);
@@ -206,7 +206,7 @@ template<typename T, typename U> inline bool operator>=(U const &v, optional<T>
 
 // Specialized algorithms
 
-template<typename T> void swap(optional<T> &x, optional<T> &y) { x.swap(y); }
+template<typename T> void swap(optional<T> &x, optional<T> &y) noexcept { x.swap(y); }
 
 // Convenience function to create an optional.
 

From 921be1a17c5b8fdd638518e8c3c92f87ed0cca2d Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Wed, 27 Nov 2024 07:09:16 +1300
Subject: [PATCH 201/282] Move ``USE_CAPTIVE_PORTAL`` into all define groups it
 can be used with (#7863)

---
 esphome/core/defines.h | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/esphome/core/defines.h b/esphome/core/defines.h
index 3798ddba6a..eb3b20d007 100644
--- a/esphome/core/defines.h
+++ b/esphome/core/defines.h
@@ -84,7 +84,6 @@
 
 // Arduino-specific feature flags
 #ifdef USE_ARDUINO
-#define USE_CAPTIVE_PORTAL
 #define USE_PROMETHEUS
 #define USE_WIFI_WPA2_EAP
 #endif
@@ -97,6 +96,7 @@
 // ESP32-specific feature flags
 #ifdef USE_ESP32
 #define USE_BLUETOOTH_PROXY
+#define USE_CAPTIVE_PORTAL
 #define USE_ESP32_BLE
 #define USE_ESP32_BLE_CLIENT
 #define USE_ESP32_BLE_SERVER
@@ -135,6 +135,7 @@
 #ifdef USE_ESP8266
 #define USE_ADC_SENSOR_VCC
 #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 1, 2)
+#define USE_CAPTIVE_PORTAL
 #define USE_ESP8266_PREFERENCES_FLASH
 #define USE_HTTP_REQUEST_ESP8266_HTTPS
 #define USE_SOCKET_IMPL_LWIP_TCP
@@ -159,6 +160,7 @@
 #endif
 
 #ifdef USE_LIBRETINY
+#define USE_CAPTIVE_PORTAL
 #define USE_SOCKET_IMPL_LWIP_SOCKETS
 #define USE_WEBSERVER
 #define USE_WEBSERVER_PORT 80  // NOLINT

From 3a8b41daa3297b77175a2303be3036fb93c16954 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 26 Nov 2024 21:06:56 +0100
Subject: [PATCH 202/282] Bump docker/build-push-action from 6.9.0 to 6.10.0 in
 /.github/actions/build-image (#7866)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/actions/build-image/action.yaml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml
index 5c686605c3..cc9894a657 100644
--- a/.github/actions/build-image/action.yaml
+++ b/.github/actions/build-image/action.yaml
@@ -46,7 +46,7 @@ runs:
 
     - name: Build and push to ghcr by digest
       id: build-ghcr
-      uses: docker/build-push-action@v6.9.0
+      uses: docker/build-push-action@v6.10.0
       env:
         DOCKER_BUILD_SUMMARY: false
         DOCKER_BUILD_RECORD_UPLOAD: false
@@ -72,7 +72,7 @@ runs:
 
     - name: Build and push to dockerhub by digest
       id: build-dockerhub
-      uses: docker/build-push-action@v6.9.0
+      uses: docker/build-push-action@v6.10.0
       env:
         DOCKER_BUILD_SUMMARY: false
         DOCKER_BUILD_RECORD_UPLOAD: false

From a3ef2ed7fd109fc6885de3dee7245d637d5ae2a9 Mon Sep 17 00:00:00 2001
From: tomaszduda23 <tomaszduda23@gmail.com>
Date: Tue, 26 Nov 2024 21:56:43 +0100
Subject: [PATCH 203/282] python lint for platform components (#7864)

---
 esphome/components/bk72xx/__init__.py  |  2 +-
 esphome/components/esp8266/__init__.py | 12 +++++-------
 esphome/components/rp2040/__init__.py  |  2 +-
 esphome/components/rtl87xx/__init__.py |  2 +-
 script/build_language_schema.py        | 16 +++++++---------
 5 files changed, 15 insertions(+), 19 deletions(-)

diff --git a/esphome/components/bk72xx/__init__.py b/esphome/components/bk72xx/__init__.py
index 6737631ac7..b5122de956 100644
--- a/esphome/components/bk72xx/__init__.py
+++ b/esphome/components/bk72xx/__init__.py
@@ -15,7 +15,7 @@ from esphome.components.libretiny.const import (
 )
 from esphome.core import CORE
 
-from .boards import BK72XX_BOARDS, BK72XX_BOARD_PINS
+from .boards import BK72XX_BOARD_PINS, BK72XX_BOARDS
 
 CODEOWNERS = ["@kuba2k2"]
 AUTO_LOAD = ["libretiny"]
diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py
index 64b127bda3..c73027fe1b 100644
--- a/esphome/components/esp8266/__init__.py
+++ b/esphome/components/esp8266/__init__.py
@@ -1,10 +1,13 @@
 import logging
 import os
 
+import esphome.codegen as cg
+import esphome.config_validation as cv
 from esphome.const import (
     CONF_BOARD,
     CONF_BOARD_FLASH_MODE,
     CONF_FRAMEWORK,
+    CONF_PLATFORM_VERSION,
     CONF_SOURCE,
     CONF_VERSION,
     KEY_CORE,
@@ -12,27 +15,22 @@ from esphome.const import (
     KEY_TARGET_FRAMEWORK,
     KEY_TARGET_PLATFORM,
     PLATFORM_ESP8266,
-    CONF_PLATFORM_VERSION,
 )
 from esphome.core import CORE, coroutine_with_priority
-import esphome.config_validation as cv
-import esphome.codegen as cg
 from esphome.helpers import copy_file_if_changed
 
+from .boards import BOARDS, ESP8266_LD_SCRIPTS
 from .const import (
-    CONF_RESTORE_FROM_FLASH,
     CONF_EARLY_PIN_INIT,
+    CONF_RESTORE_FROM_FLASH,
     KEY_BOARD,
     KEY_ESP8266,
     KEY_FLASH_SIZE,
     KEY_PIN_INITIAL_STATES,
     esp8266_ns,
 )
-from .boards import BOARDS, ESP8266_LD_SCRIPTS
-
 from .gpio import PinInitialState, add_pin_initial_states_array
 
-
 CODEOWNERS = ["@esphome/core"]
 _LOGGER = logging.getLogger(__name__)
 AUTO_LOAD = ["preferences"]
diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py
index d612631a4c..b04e539182 100644
--- a/esphome/components/rp2040/__init__.py
+++ b/esphome/components/rp2040/__init__.py
@@ -17,7 +17,7 @@ from esphome.const import (
     PLATFORM_RP2040,
 )
 from esphome.core import CORE, EsphomeError, coroutine_with_priority
-from esphome.helpers import copy_file_if_changed, mkdir_p, write_file, read_file
+from esphome.helpers import copy_file_if_changed, mkdir_p, read_file, write_file
 
 from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns
 
diff --git a/esphome/components/rtl87xx/__init__.py b/esphome/components/rtl87xx/__init__.py
index 9060a7c4a6..4c1956f0f4 100644
--- a/esphome/components/rtl87xx/__init__.py
+++ b/esphome/components/rtl87xx/__init__.py
@@ -15,7 +15,7 @@ from esphome.components.libretiny.const import (
 )
 from esphome.core import CORE
 
-from .boards import RTL87XX_BOARDS, RTL87XX_BOARD_PINS
+from .boards import RTL87XX_BOARD_PINS, RTL87XX_BOARDS
 
 CODEOWNERS = ["@kuba2k2"]
 AUTO_LOAD = ["libretiny"]
diff --git a/script/build_language_schema.py b/script/build_language_schema.py
index 8b2c28b06b..2023dc0402 100644
--- a/script/build_language_schema.py
+++ b/script/build_language_schema.py
@@ -394,9 +394,8 @@ def add_referenced_recursive(referenced_schemas, config_var, path, eat_schema=Fa
         for k in schema.get(S_EXTENDS, []):
             if k not in referenced_schemas:
                 referenced_schemas[k] = [path]
-            else:
-                if path not in referenced_schemas[k]:
-                    referenced_schemas[k].append(path)
+            elif path not in referenced_schemas[k]:
+                referenced_schemas[k].append(path)
 
             s1 = get_str_path_schema(k)
             p = k.split(".")
@@ -868,13 +867,12 @@ def convert(schema, config_var, path):
                     config_var[S_TYPE] = "use_id"
                 else:
                     print("TODO deferred?")
+            elif isinstance(data, str):
+                # TODO: Figure out why pipsolar does this
+                config_var["use_id_type"] = data
             else:
-                if isinstance(data, str):
-                    # TODO: Figure out why pipsolar does this
-                    config_var["use_id_type"] = data
-                else:
-                    config_var["use_id_type"] = str(data.base)
-                    config_var[S_TYPE] = "use_id"
+                config_var["use_id_type"] = str(data.base)
+                config_var[S_TYPE] = "use_id"
         else:
             raise TypeError("Unknown extracted schema type")
     elif config_var.get("key") == "GeneratedID":

From 4a97064b2cd2412414b2d57ec1b7e151ffcf92c2 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Mon, 25 Nov 2024 07:25:16 +1100
Subject: [PATCH 204/282] [lvgl] Bugfixes (#7803)

---
 esphome/components/lvgl/__init__.py      | 2 +-
 esphome/components/lvgl/lv_validation.py | 3 ++-
 esphome/components/lvgl/schemas.py       | 1 +
 esphome/components/lvgl/widgets/line.py  | 5 ++++-
 4 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py
index d03adc9624..8fdd03f647 100644
--- a/esphome/components/lvgl/__init__.py
+++ b/esphome/components/lvgl/__init__.py
@@ -322,8 +322,8 @@ async def to_code(configs):
             await encoders_to_code(lv_component, config, default_group)
             await keypads_to_code(lv_component, config, default_group)
             await theme_to_code(config)
-            await styles_to_code(config)
             await gradients_to_code(config)
+            await styles_to_code(config)
             await set_obj_properties(lv_scr_act, config)
             await add_widgets(lv_scr_act, config)
             await add_pages(lv_component, config)
diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py
index b91b0905df..766c010244 100644
--- a/esphome/components/lvgl/lv_validation.py
+++ b/esphome/components/lvgl/lv_validation.py
@@ -30,7 +30,7 @@ from .defines import (
     call_lambda,
     literal,
 )
-from .helpers import esphome_fonts_used, lv_fonts_used, requires_component
+from .helpers import add_lv_use, esphome_fonts_used, lv_fonts_used, requires_component
 from .types import lv_font_t, lv_gradient_t, lv_img_t
 
 opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
@@ -326,6 +326,7 @@ def image_validator(value):
     value = requires_component("image")(value)
     value = cv.use_id(Image_)(value)
     lv_images_used.add(value)
+    add_lv_use("img", "label")
     return value
 
 
diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py
index 516627708e..3f56b3345f 100644
--- a/esphome/components/lvgl/schemas.py
+++ b/esphome/components/lvgl/schemas.py
@@ -341,6 +341,7 @@ FLEX_OBJ_SCHEMA = {
     cv.Optional(df.CONF_FLEX_GROW): cv.int_,
 }
 
+
 DISP_BG_SCHEMA = cv.Schema(
     {
         cv.Optional(df.CONF_DISP_BG_IMAGE): lv_image,
diff --git a/esphome/components/lvgl/widgets/line.py b/esphome/components/lvgl/widgets/line.py
index 4c6439fde4..548dfa8452 100644
--- a/esphome/components/lvgl/widgets/line.py
+++ b/esphome/components/lvgl/widgets/line.py
@@ -39,7 +39,10 @@ LINE_SCHEMA = {
 class LineType(WidgetType):
     def __init__(self):
         super().__init__(
-            CONF_LINE, LvType("lv_line_t"), (CONF_MAIN,), LINE_SCHEMA, modify_schema={}
+            CONF_LINE,
+            LvType("lv_line_t"),
+            (CONF_MAIN,),
+            LINE_SCHEMA,
         )
 
     async def to_code(self, w: Widget, config):

From a4a71797d9790fe059210b79a35a95839ff2bef8 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Mon, 25 Nov 2024 07:25:51 +1100
Subject: [PATCH 205/282] [docker] Leave run-time required libraries installed.
 (#7804)

---
 docker/Dockerfile | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index ed6ce083a8..c2902a9dd1 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -99,15 +99,17 @@ BUILD_DEPS="
     libfreetype-dev=2.12.1+dfsg-5+deb12u3
     libssl-dev=3.0.15-1~deb12u1
     libffi-dev=3.4.4-1
-    libopenjp2-7=2.5.0-2
-    libtiff6=4.5.0-6+deb12u1
     cargo=0.66.0+ds1-1
     pkg-config=1.8.1-1
 "
+LIB_DEPS="
+    libtiff6=4.5.0-6+deb12u1
+    libopenjp2-7=2.5.0-2
+"
 if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ] || [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]
 then
     apt-get update
-    apt-get install -y --no-install-recommends $BUILD_DEPS
+    apt-get install -y --no-install-recommends $BUILD_DEPS $LIB_DEPS
 fi
 
 CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo

From 80fedbc1a556e121ca166519aa3aae7e44c05a1b Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Mon, 25 Nov 2024 07:27:09 +1100
Subject: [PATCH 206/282] [qspi_dbi] Fix init sequences (Bugfix) (#7805)

---
 esphome/components/qspi_dbi/models.py    | 2 ++
 esphome/components/qspi_dbi/qspi_dbi.cpp | 1 -
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/esphome/components/qspi_dbi/models.py b/esphome/components/qspi_dbi/models.py
index cbd9c4663f..c1fe434853 100644
--- a/esphome/components/qspi_dbi/models.py
+++ b/esphome/components/qspi_dbi/models.py
@@ -1,6 +1,7 @@
 # Commands
 SW_RESET_CMD = 0x01
 SLEEP_OUT = 0x11
+NORON = 0x13
 INVERT_OFF = 0x20
 INVERT_ON = 0x21
 ALL_ON = 0x23
@@ -56,6 +57,7 @@ chip.cmd(0xC2, 0x00)
 chip.delay(10)
 chip.cmd(TEON, 0x00)
 chip.cmd(PIXFMT, 0x55)
+chip.cmd(NORON)
 
 chip = DriverChip("AXS15231")
 chip.cmd(0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xA5)
diff --git a/esphome/components/qspi_dbi/qspi_dbi.cpp b/esphome/components/qspi_dbi/qspi_dbi.cpp
index a649a25ea6..785885d4ec 100644
--- a/esphome/components/qspi_dbi/qspi_dbi.cpp
+++ b/esphome/components/qspi_dbi/qspi_dbi.cpp
@@ -111,7 +111,6 @@ void QspiDbi::reset_params_(bool ready) {
     mad |= MADCTL_MY;
   this->write_command_(MADCTL_CMD, mad);
   this->write_command_(BRIGHTNESS, this->brightness_);
-  this->write_command_(NORON);
   this->write_command_(DISPLAY_ON);
 }
 

From e9851e7eb227ceebb4925b59e03c22c402b29d06 Mon Sep 17 00:00:00 2001
From: Samuel Sieb <samuel-github@sieb.net>
Date: Sun, 24 Nov 2024 10:42:46 -1000
Subject: [PATCH 207/282] fix modbus crashing when bad data returned (#7810)

Co-authored-by: Samuel Sieb <samuel@sieb.net>
---
 esphome/components/modbus/modbus.cpp          |  3 +-
 .../modbus_controller/modbus_controller.cpp   | 72 ++++++++++++++-----
 2 files changed, 56 insertions(+), 19 deletions(-)

diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp
index 8544b50261..47deea83e6 100644
--- a/esphome/components/modbus/modbus.cpp
+++ b/esphome/components/modbus/modbus.cpp
@@ -38,8 +38,9 @@ void Modbus::loop() {
 
     // stop blocking new send commands after sent_wait_time_ ms after response received
     if (now - this->last_send_ > send_wait_time_) {
-      if (waiting_for_response > 0)
+      if (waiting_for_response > 0) {
         ESP_LOGV(TAG, "Stop waiting for response from %d", waiting_for_response);
+      }
       waiting_for_response = 0;
     }
   }
diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp
index e1102516ca..f8b72af817 100644
--- a/esphome/components/modbus_controller/modbus_controller.cpp
+++ b/esphome/components/modbus_controller/modbus_controller.cpp
@@ -622,51 +622,87 @@ int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sens
                           uint32_t bitmask) {
   int64_t value = 0;  // int64_t because it can hold signed and unsigned 32 bits
 
+  size_t size = data.size() - offset;
+  bool error = false;
   switch (sensor_value_type) {
     case SensorValueType::U_WORD:
-      value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), bitmask);  // default is 0xFFFF ;
+      if (size >= 2) {
+        value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), bitmask);  // default is 0xFFFF ;
+      } else {
+        error = true;
+      }
       break;
     case SensorValueType::U_DWORD:
     case SensorValueType::FP32:
-      value = get_data<uint32_t>(data, offset);
-      value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
+      if (size >= 4) {
+        value = get_data<uint32_t>(data, offset);
+        value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
+      } else {
+        error = true;
+      }
       break;
     case SensorValueType::U_DWORD_R:
     case SensorValueType::FP32_R:
-      value = get_data<uint32_t>(data, offset);
-      value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
-      value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
+      if (size >= 4) {
+        value = get_data<uint32_t>(data, offset);
+        value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
+        value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
+      } else {
+        error = true;
+      }
       break;
     case SensorValueType::S_WORD:
-      value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset),
-                                         bitmask);  // default is 0xFFFF ;
+      if (size >= 2) {
+        value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset),
+                                           bitmask);  // default is 0xFFFF ;
+      } else {
+        error = true;
+      }
       break;
     case SensorValueType::S_DWORD:
-      value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
+      if (size >= 4) {
+        value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
+      } else {
+        error = true;
+      }
       break;
     case SensorValueType::S_DWORD_R: {
-      value = get_data<uint32_t>(data, offset);
-      // Currently the high word is at the low position
-      // the sign bit is therefore at low before the switch
-      uint32_t sign_bit = (value & 0x8000) << 16;
-      value = mask_and_shift_by_rightbit(
-          static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
+      if (size >= 4) {
+        value = get_data<uint32_t>(data, offset);
+        // Currently the high word is at the low position
+        // the sign bit is therefore at low before the switch
+        uint32_t sign_bit = (value & 0x8000) << 16;
+        value = mask_and_shift_by_rightbit(
+            static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
+      } else {
+        error = true;
+      }
     } break;
     case SensorValueType::U_QWORD:
     case SensorValueType::S_QWORD:
       // Ignore bitmask for QWORD
-      value = get_data<uint64_t>(data, offset);
+      if (size >= 8) {
+        value = get_data<uint64_t>(data, offset);
+      } else {
+        error = true;
+      }
       break;
     case SensorValueType::U_QWORD_R:
     case SensorValueType::S_QWORD_R: {
       // Ignore bitmask for QWORD
-      uint64_t tmp = get_data<uint64_t>(data, offset);
-      value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000);
+      if (size >= 8) {
+        uint64_t tmp = get_data<uint64_t>(data, offset);
+        value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000);
+      } else {
+        error = true;
+      }
     } break;
     case SensorValueType::RAW:
     default:
       break;
   }
+  if (error)
+    ESP_LOGE(TAG, "not enough data for value");
   return value;
 }
 

From 1b91e0027b8cf40f3ed5e897ccbada8fab5d3448 Mon Sep 17 00:00:00 2001
From: TFGF <terciofilho@gmail.com>
Date: Sun, 24 Nov 2024 19:15:10 -0300
Subject: [PATCH 208/282] [Modbus Controller] Fix issue #6477. Online
 automation triggering Offline (#7801)

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

diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py
index 5c407d6fff..2a08075831 100644
--- a/esphome/components/modbus_controller/__init__.py
+++ b/esphome/components/modbus_controller/__init__.py
@@ -163,7 +163,7 @@ CONFIG_SCHEMA = cv.All(
             ),
             cv.Optional(CONF_ON_OFFLINE): automation.validate_automation(
                 {
-                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOnlineTrigger),
+                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOfflineTrigger),
                 }
             ),
         }

From 72bf0086e486a71d5f44e99f3bed925f996e7aa3 Mon Sep 17 00:00:00 2001
From: Ramil Valitov <ramilvalitov@gmail.com>
Date: Mon, 25 Nov 2024 01:23:30 +0300
Subject: [PATCH 209/282] [fix] Status sensor does not check if required
 network component is missing (#7734)

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

diff --git a/esphome/components/status/binary_sensor.py b/esphome/components/status/binary_sensor.py
index 1f2b7c9d18..adc342ed4d 100644
--- a/esphome/components/status/binary_sensor.py
+++ b/esphome/components/status/binary_sensor.py
@@ -6,6 +6,8 @@ from esphome.const import (
     ENTITY_CATEGORY_DIAGNOSTIC,
 )
 
+DEPENDENCIES = ["network"]
+
 status_ns = cg.esphome_ns.namespace("status")
 StatusBinarySensor = status_ns.class_(
     "StatusBinarySensor", binary_sensor.BinarySensor, cg.Component

From 4c7552eca4af9c43f93e00e41e9ef7b1ed28db92 Mon Sep 17 00:00:00 2001
From: Samuel Sieb <samuel-github@sieb.net>
Date: Sun, 24 Nov 2024 12:40:51 -1000
Subject: [PATCH 210/282] keypad binary sensors should be initially off (#7808)

Co-authored-by: Samuel Sieb <samuel@sieb.net>
---
 esphome/components/binary_sensor/binary_sensor.h                | 2 +-
 .../matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h
index 301a472810..57cae9e2f5 100644
--- a/esphome/components/binary_sensor/binary_sensor.h
+++ b/esphome/components/binary_sensor/binary_sensor.h
@@ -58,7 +58,7 @@ class BinarySensor : public EntityBase, public EntityBase_DeviceClass {
   void publish_initial_state(bool state);
 
   /// The current reported state of the binary sensor.
-  bool state;
+  bool state{false};
 
   void add_filter(Filter *filter);
   void add_filters(const std::vector<Filter *> &filters);
diff --git a/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h b/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h
index d8a217f55e..2c1ce96f0a 100644
--- a/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h
+++ b/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h
@@ -6,7 +6,7 @@
 namespace esphome {
 namespace matrix_keypad {
 
-class MatrixKeypadBinarySensor : public MatrixKeypadListener, public binary_sensor::BinarySensor {
+class MatrixKeypadBinarySensor : public MatrixKeypadListener, public binary_sensor::BinarySensorInitiallyOff {
  public:
   MatrixKeypadBinarySensor(uint8_t key) : has_key_(true), key_(key){};
   MatrixKeypadBinarySensor(const char *key) : has_key_(true), key_((uint8_t) key[0]){};

From 5ddbe5cdba7faf510b15821e896b6d5b88d8af4d Mon Sep 17 00:00:00 2001
From: Samuel Sieb <samuel-github@sieb.net>
Date: Mon, 25 Nov 2024 13:58:21 -1000
Subject: [PATCH 211/282] [wifi] fix 32 char SSIDs (#7834)

Co-authored-by: Samuel Sieb <samuel@sieb.net>
---
 .../wifi/wifi_component_esp32_arduino.cpp     | 24 +++++++++++++++----
 .../wifi/wifi_component_esp8266.cpp           | 24 +++++++++++++++----
 .../wifi/wifi_component_esp_idf.cpp           | 24 +++++++++++++++----
 3 files changed, 60 insertions(+), 12 deletions(-)

diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp
index 88648093c6..ee00e2ac6c 100644
--- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp
+++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp
@@ -137,8 +137,16 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
   // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t
   wifi_config_t conf;
   memset(&conf, 0, sizeof(conf));
-  snprintf(reinterpret_cast<char *>(conf.sta.ssid), sizeof(conf.sta.ssid), "%s", ap.get_ssid().c_str());
-  snprintf(reinterpret_cast<char *>(conf.sta.password), sizeof(conf.sta.password), "%s", ap.get_password().c_str());
+  if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) {
+    ESP_LOGE(TAG, "SSID is too long");
+    return false;
+  }
+  if (ap.get_password().size() > sizeof(conf.sta.password)) {
+    ESP_LOGE(TAG, "password is too long");
+    return false;
+  }
+  memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
+  memcpy(reinterpret_cast<char *>(conf.sta.password), ap.get_password().c_str(), ap.get_password().size());
 
   // The weakest authmode to accept in the fast scan mode
   if (ap.get_password().empty()) {
@@ -746,7 +754,11 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
 
   wifi_config_t conf;
   memset(&conf, 0, sizeof(conf));
-  snprintf(reinterpret_cast<char *>(conf.ap.ssid), sizeof(conf.ap.ssid), "%s", ap.get_ssid().c_str());
+  if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) {
+    ESP_LOGE(TAG, "AP SSID is too long");
+    return false;
+  }
+  memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
   conf.ap.channel = ap.get_channel().value_or(1);
   conf.ap.ssid_hidden = ap.get_ssid().size();
   conf.ap.max_connection = 5;
@@ -757,7 +769,11 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
     *conf.ap.password = 0;
   } else {
     conf.ap.authmode = WIFI_AUTH_WPA2_PSK;
-    snprintf(reinterpret_cast<char *>(conf.ap.password), sizeof(conf.ap.password), "%s", ap.get_password().c_str());
+    if (ap.get_password().size() > sizeof(conf.ap.password)) {
+      ESP_LOGE(TAG, "AP password is too long");
+      return false;
+    }
+    memcpy(reinterpret_cast<char *>(conf.ap.password), ap.get_password().c_str(), ap.get_password().size());
   }
 
   // pairwise cipher of SoftAP, group cipher will be derived using this.
diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp
index 4568895950..14506f569c 100644
--- a/esphome/components/wifi/wifi_component_esp8266.cpp
+++ b/esphome/components/wifi/wifi_component_esp8266.cpp
@@ -236,8 +236,16 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
 
   struct station_config conf {};
   memset(&conf, 0, sizeof(conf));
-  snprintf(reinterpret_cast<char *>(conf.ssid), sizeof(conf.ssid), "%s", ap.get_ssid().c_str());
-  snprintf(reinterpret_cast<char *>(conf.password), sizeof(conf.password), "%s", ap.get_password().c_str());
+  if (ap.get_ssid().size() > sizeof(conf.ssid)) {
+    ESP_LOGE(TAG, "SSID is too long");
+    return false;
+  }
+  if (ap.get_password().size() > sizeof(conf.password)) {
+    ESP_LOGE(TAG, "password is too long");
+    return false;
+  }
+  memcpy(reinterpret_cast<char *>(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
+  memcpy(reinterpret_cast<char *>(conf.password), ap.get_password().c_str(), ap.get_password().size());
 
   if (ap.get_bssid().has_value()) {
     conf.bssid_set = 1;
@@ -775,7 +783,11 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
     return false;
 
   struct softap_config conf {};
-  snprintf(reinterpret_cast<char *>(conf.ssid), sizeof(conf.ssid), "%s", ap.get_ssid().c_str());
+  if (ap.get_ssid().size() > sizeof(conf.ssid)) {
+    ESP_LOGE(TAG, "AP SSID is too long");
+    return false;
+  }
+  memcpy(reinterpret_cast<char *>(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
   conf.ssid_len = static_cast<uint8>(ap.get_ssid().size());
   conf.channel = ap.get_channel().value_or(1);
   conf.ssid_hidden = ap.get_hidden();
@@ -787,7 +799,11 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
     *conf.password = 0;
   } else {
     conf.authmode = AUTH_WPA2_PSK;
-    snprintf(reinterpret_cast<char *>(conf.password), sizeof(conf.password), "%s", ap.get_password().c_str());
+    if (ap.get_password().size() > sizeof(conf.password)) {
+      ESP_LOGE(TAG, "AP password is too long");
+      return false;
+    }
+    memcpy(reinterpret_cast<char *>(conf.password), ap.get_password().c_str(), ap.get_password().size());
   }
 
   ETS_UART_INTR_DISABLE();
diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp
index 13870136d4..3074ffbe1b 100644
--- a/esphome/components/wifi/wifi_component_esp_idf.cpp
+++ b/esphome/components/wifi/wifi_component_esp_idf.cpp
@@ -289,8 +289,16 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
   // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t
   wifi_config_t conf;
   memset(&conf, 0, sizeof(conf));
-  snprintf(reinterpret_cast<char *>(conf.sta.ssid), sizeof(conf.sta.ssid), "%s", ap.get_ssid().c_str());
-  snprintf(reinterpret_cast<char *>(conf.sta.password), sizeof(conf.sta.password), "%s", ap.get_password().c_str());
+  if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) {
+    ESP_LOGE(TAG, "SSID is too long");
+    return false;
+  }
+  if (ap.get_password().size() > sizeof(conf.sta.password)) {
+    ESP_LOGE(TAG, "password is too long");
+    return false;
+  }
+  memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
+  memcpy(reinterpret_cast<char *>(conf.sta.password), ap.get_password().c_str(), ap.get_password().size());
 
   // The weakest authmode to accept in the fast scan mode
   if (ap.get_password().empty()) {
@@ -902,7 +910,11 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
 
   wifi_config_t conf;
   memset(&conf, 0, sizeof(conf));
-  strncpy(reinterpret_cast<char *>(conf.ap.ssid), ap.get_ssid().c_str(), sizeof(conf.ap.ssid));
+  if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) {
+    ESP_LOGE(TAG, "AP SSID is too long");
+    return false;
+  }
+  memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
   conf.ap.channel = ap.get_channel().value_or(1);
   conf.ap.ssid_hidden = ap.get_ssid().size();
   conf.ap.max_connection = 5;
@@ -913,7 +925,11 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
     *conf.ap.password = 0;
   } else {
     conf.ap.authmode = WIFI_AUTH_WPA2_PSK;
-    strncpy(reinterpret_cast<char *>(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.password));
+    if (ap.get_password().size() > sizeof(conf.ap.password)) {
+      ESP_LOGE(TAG, "AP password is too long");
+      return false;
+    }
+    memcpy(reinterpret_cast<char *>(conf.ap.password), ap.get_password().c_str(), ap.get_password().size());
   }
 
   // pairwise cipher of SoftAP, group cipher will be derived using this.

From 2539cba61047781de159d174f613d7ca0478b9e8 Mon Sep 17 00:00:00 2001
From: Samuel Sieb <samuel-github@sieb.net>
Date: Tue, 26 Nov 2024 00:05:20 -1000
Subject: [PATCH 212/282] [honeywell] use warning instead of failing (#7862)

Co-authored-by: Samuel Sieb <samuel@sieb.net>
---
 .../components/honeywellabp2_i2c/honeywellabp2.cpp    | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp b/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp
index e2910032cc..d111723669 100644
--- a/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp
+++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp
@@ -15,7 +15,7 @@ static const char *const TAG = "honeywellabp2";
 void HONEYWELLABP2Sensor::read_sensor_data() {
   if (this->read(raw_data_, 7) != i2c::ERROR_OK) {
     ESP_LOGE(TAG, "Communication with ABP2 failed!");
-    this->mark_failed();
+    this->status_set_warning("couldn't read sensor data");
     return;
   }
   float press_counts = encode_uint24(raw_data_[1], raw_data_[2], raw_data_[3]);  // calculate digital pressure counts
@@ -25,12 +25,13 @@ void HONEYWELLABP2Sensor::read_sensor_data() {
                           (this->max_pressure_ - this->min_pressure_)) +
                          this->min_pressure_;
   this->last_temperature_ = (temp_counts * 200 / 16777215) - 50;
+  this->status_clear_warning();
 }
 
 void HONEYWELLABP2Sensor::start_measurement() {
   if (this->write(i2c_cmd_, 3) != i2c::ERROR_OK) {
     ESP_LOGE(TAG, "Communication with ABP2 failed!");
-    this->mark_failed();
+    this->status_set_warning("couldn't start measurement");
     return;
   }
   this->measurement_running_ = true;
@@ -39,7 +40,7 @@ void HONEYWELLABP2Sensor::start_measurement() {
 bool HONEYWELLABP2Sensor::is_measurement_ready() {
   if (this->read(raw_data_, 1) != i2c::ERROR_OK) {
     ESP_LOGE(TAG, "Communication with ABP2 failed!");
-    this->mark_failed();
+    this->status_set_warning("couldn't check measurement");
     return false;
   }
   if ((raw_data_[0] & (0x1 << STATUS_BIT_BUSY)) > 0) {
@@ -52,7 +53,7 @@ bool HONEYWELLABP2Sensor::is_measurement_ready() {
 void HONEYWELLABP2Sensor::measurement_timeout() {
   ESP_LOGE(TAG, "Timeout!");
   this->measurement_running_ = false;
-  this->mark_failed();
+  this->status_set_warning("measurement timed out");
 }
 
 float HONEYWELLABP2Sensor::get_pressure() { return this->last_pressure_; }
@@ -79,7 +80,7 @@ void HONEYWELLABP2Sensor::update() {
   ESP_LOGV(TAG, "Update Honeywell ABP2 Sensor");
 
   this->start_measurement();
-  this->set_timeout("meas_timeout", 50, [this] { this->measurement_timeout(); });
+  this->set_timeout("meas_timeout", 100, [this] { this->measurement_timeout(); });
 }
 
 void HONEYWELLABP2Sensor::dump_config() {

From c894645747d742937cfedc572dcc319f2a8caaa5 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Wed, 27 Nov 2024 14:06:21 +1300
Subject: [PATCH 213/282] Bump version to 2024.11.2

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

diff --git a/esphome/const.py b/esphome/const.py
index d14cdecb23..4b19e2865d 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,6 +1,6 @@
 """Constants used by esphome."""
 
-__version__ = "2024.11.1"
+__version__ = "2024.11.2"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 VALID_SUBSTITUTIONS_CHARACTERS = (

From e6c730ab109ee2b896283399c3f7453c1591d1de Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 27 Nov 2024 16:16:54 -0600
Subject: [PATCH 214/282] [max31865] clang-tidy fixes for #7822 (#7876)

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

diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp
index b48aa2fdd3..4749874ac7 100644
--- a/esphome/components/max31865/max31865.cpp
+++ b/esphome/components/max31865/max31865.cpp
@@ -106,7 +106,8 @@ void MAX31865Sensor::read_data_() {
 
   // Check faults
   const uint8_t faults = this->read_register_(FAULT_STATUS_REG);
-  if ((has_fault_ = faults & 0b00111100)) {
+  has_fault_ = faults & 0b00111100;
+  if (has_fault_) {
     if (faults & (1 << 2)) {
       ESP_LOGE(TAG, "Overvoltage/undervoltage fault");
     }
@@ -125,7 +126,8 @@ void MAX31865Sensor::read_data_() {
   } else {
     this->status_clear_error();
   }
-  if ((has_warn_ = faults & 0b11000000)) {
+  has_warn_ = faults & 0b11000000;
+  if (has_warn_) {
     if (faults & (1 << 6)) {
       ESP_LOGW(TAG, "RTD Low Threshold");
     }

From 8439232b11ad720c0896fa6bf66ab3594ef91a53 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 27 Nov 2024 16:18:43 -0600
Subject: [PATCH 215/282] [esp32_ble] clang-tidy fixes for #7822 (#7883)

---
 esphome/components/esp32_ble/ble_uuid.cpp     | 19 +++++++++----------
 .../esp32_ble_tracker/esp32_ble_tracker.cpp   |  5 ++++-
 2 files changed, 13 insertions(+), 11 deletions(-)

diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp
index 07ac719434..aa1edd96b2 100644
--- a/esphome/components/esp32_ble/ble_uuid.cpp
+++ b/esphome/components/esp32_ble/ble_uuid.cpp
@@ -34,7 +34,7 @@ ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) {
 ESPBTUUID ESPBTUUID::from_raw_reversed(const uint8_t *data) {
   ESPBTUUID ret;
   ret.uuid_.len = ESP_UUID_LEN_128;
-  for (int i = 0; i < ESP_UUID_LEN_128; i++)
+  for (uint8_t i = 0; i < ESP_UUID_LEN_128; i++)
     ret.uuid_.uuid.uuid128[ESP_UUID_LEN_128 - 1 - i] = data[i];
   return ret;
 }
@@ -43,30 +43,30 @@ ESPBTUUID ESPBTUUID::from_raw(const std::string &data) {
   if (data.length() == 4) {
     ret.uuid_.len = ESP_UUID_LEN_16;
     ret.uuid_.uuid.uuid16 = 0;
-    for (int i = 0; i < data.length();) {
+    for (uint i = 0; i < data.length(); i += 2) {
       uint8_t msb = data.c_str()[i];
       uint8_t lsb = data.c_str()[i + 1];
+      uint8_t lsb_shift = i <= 2 ? (2 - i) * 4 : 0;
 
       if (msb > '9')
         msb -= 7;
       if (lsb > '9')
         lsb -= 7;
-      ret.uuid_.uuid.uuid16 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << (2 - i) * 4;
-      i += 2;
+      ret.uuid_.uuid.uuid16 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << lsb_shift;
     }
   } else if (data.length() == 8) {
     ret.uuid_.len = ESP_UUID_LEN_32;
     ret.uuid_.uuid.uuid32 = 0;
-    for (int i = 0; i < data.length();) {
+    for (uint i = 0; i < data.length(); i += 2) {
       uint8_t msb = data.c_str()[i];
       uint8_t lsb = data.c_str()[i + 1];
+      uint8_t lsb_shift = i <= 6 ? (6 - i) * 4 : 0;
 
       if (msb > '9')
         msb -= 7;
       if (lsb > '9')
         lsb -= 7;
-      ret.uuid_.uuid.uuid32 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << (6 - i) * 4;
-      i += 2;
+      ret.uuid_.uuid.uuid32 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << lsb_shift;
     }
   } else if (data.length() == 16) {  // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be
                                      // investigated (lack of time)
@@ -77,7 +77,7 @@ ESPBTUUID ESPBTUUID::from_raw(const std::string &data) {
     // UUID format.
     ret.uuid_.len = ESP_UUID_LEN_128;
     int n = 0;
-    for (int i = 0; i < data.length();) {
+    for (uint i = 0; i < data.length(); i += 2) {
       if (data.c_str()[i] == '-')
         i++;
       uint8_t msb = data.c_str()[i];
@@ -88,7 +88,6 @@ ESPBTUUID ESPBTUUID::from_raw(const std::string &data) {
       if (lsb > '9')
         lsb -= 7;
       ret.uuid_.uuid.uuid128[15 - n++] = ((msb & 0x0F) << 4) | (lsb & 0x0F);
-      i += 2;
     }
   } else {
     ESP_LOGE(TAG, "ERROR: UUID value not 2, 4, 16 or 36 bytes - %s", data.c_str());
@@ -155,7 +154,7 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const {
         }
         break;
       case ESP_UUID_LEN_128:
-        for (int i = 0; i < ESP_UUID_LEN_128; i++) {
+        for (uint8_t i = 0; i < ESP_UUID_LEN_128; i++) {
           if (uuid.uuid_.uuid.uuid128[i] != this->uuid_.uuid.uuid128[i]) {
             return false;
           }
diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
index b86d32ee61..6d051e3d4a 100644
--- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
+++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
@@ -432,7 +432,7 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e
 
 #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
   ESP_LOGVV(TAG, "Parse Result:");
-  const char *address_type = "";
+  const char *address_type;
   switch (this->address_type_) {
     case BLE_ADDR_TYPE_PUBLIC:
       address_type = "PUBLIC";
@@ -446,6 +446,9 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e
     case BLE_ADDR_TYPE_RPA_RANDOM:
       address_type = "RPA_RANDOM";
       break;
+    default:
+      address_type = "UNKNOWN";
+      break;
   }
   ESP_LOGVV(TAG, "  Address: %02X:%02X:%02X:%02X:%02X:%02X (%s)", this->address_[0], this->address_[1],
             this->address_[2], this->address_[3], this->address_[4], this->address_[5], address_type);

From f2e8e655ba6476301842f260197e8e52f4ce71eb Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 27 Nov 2024 16:19:41 -0600
Subject: [PATCH 216/282] [mqtt] clang-tidy fixes for #7822 (#7877)

---
 .../components/mqtt/mqtt_alarm_control_panel.cpp |  7 ++-----
 esphome/components/mqtt/mqtt_climate.cpp         | 16 +++++++++++++---
 2 files changed, 15 insertions(+), 8 deletions(-)

diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp
index 660a030d11..4cc4773bd3 100644
--- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp
+++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp
@@ -80,8 +80,7 @@ const EntityBase *MQTTAlarmControlPanelComponent::get_entity() const { return th
 
 bool MQTTAlarmControlPanelComponent::send_initial_state() { return this->publish_state(); }
 bool MQTTAlarmControlPanelComponent::publish_state() {
-  bool success = true;
-  const char *state_s = "";
+  const char *state_s;
   switch (this->alarm_control_panel_->get_state()) {
     case ACP_STATE_DISARMED:
       state_s = "disarmed";
@@ -116,9 +115,7 @@ bool MQTTAlarmControlPanelComponent::publish_state() {
     default:
       state_s = "unknown";
   }
-  if (!this->publish(this->get_state_topic_(), state_s))
-    success = false;
-  return success;
+  return this->publish(this->get_state_topic_(), state_s);
 }
 
 }  // namespace mqtt
diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp
index 773d863835..f06574fa26 100644
--- a/esphome/components/mqtt/mqtt_climate.cpp
+++ b/esphome/components/mqtt/mqtt_climate.cpp
@@ -257,7 +257,7 @@ const EntityBase *MQTTClimateComponent::get_entity() const { return this->device
 bool MQTTClimateComponent::publish_state_() {
   auto traits = this->device_->get_traits();
   // mode
-  const char *mode_s = "";
+  const char *mode_s;
   switch (this->device_->mode) {
     case CLIMATE_MODE_OFF:
       mode_s = "off";
@@ -280,6 +280,8 @@ bool MQTTClimateComponent::publish_state_() {
     case CLIMATE_MODE_HEAT_COOL:
       mode_s = "heat_cool";
       break;
+    default:
+      mode_s = "unknown";
   }
   bool success = true;
   if (!this->publish(this->get_mode_state_topic(), mode_s))
@@ -343,6 +345,8 @@ bool MQTTClimateComponent::publish_state_() {
         case CLIMATE_PRESET_ACTIVITY:
           payload = "activity";
           break;
+        default:
+          payload = "unknown";
       }
     }
     if (this->device_->custom_preset.has_value())
@@ -352,7 +356,7 @@ bool MQTTClimateComponent::publish_state_() {
   }
 
   if (traits.get_supports_action()) {
-    const char *payload = "unknown";
+    const char *payload;
     switch (this->device_->action) {
       case CLIMATE_ACTION_OFF:
         payload = "off";
@@ -372,6 +376,8 @@ bool MQTTClimateComponent::publish_state_() {
       case CLIMATE_ACTION_FAN:
         payload = "fan";
         break;
+      default:
+        payload = "unknown";
     }
     if (!this->publish(this->get_action_state_topic(), payload))
       success = false;
@@ -411,6 +417,8 @@ bool MQTTClimateComponent::publish_state_() {
         case CLIMATE_FAN_QUIET:
           payload = "quiet";
           break;
+        default:
+          payload = "unknown";
       }
     }
     if (this->device_->custom_fan_mode.has_value())
@@ -420,7 +428,7 @@ bool MQTTClimateComponent::publish_state_() {
   }
 
   if (traits.get_supports_swing_modes()) {
-    const char *payload = "";
+    const char *payload;
     switch (this->device_->swing_mode) {
       case CLIMATE_SWING_OFF:
         payload = "off";
@@ -434,6 +442,8 @@ bool MQTTClimateComponent::publish_state_() {
       case CLIMATE_SWING_HORIZONTAL:
         payload = "horizontal";
         break;
+      default:
+        payload = "unknown";
     }
     if (!this->publish(this->get_swing_mode_state_topic(), payload))
       success = false;

From 4da57c35d0c1b1a3ec413fe8352f71d7d7e12d0e Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 27 Nov 2024 16:20:51 -0600
Subject: [PATCH 217/282] [uln2003] clang-tidy fixes for #7822 (#7881)

---
 esphome/components/uln2003/uln2003.cpp | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/esphome/components/uln2003/uln2003.cpp b/esphome/components/uln2003/uln2003.cpp
index 1af9806906..991fe53487 100644
--- a/esphome/components/uln2003/uln2003.cpp
+++ b/esphome/components/uln2003/uln2003.cpp
@@ -40,7 +40,7 @@ void ULN2003::dump_config() {
   LOG_PIN("  Pin C: ", this->pin_c_);
   LOG_PIN("  Pin D: ", this->pin_d_);
   ESP_LOGCONFIG(TAG, "  Sleep when done: %s", YESNO(this->sleep_when_done_));
-  const char *step_mode_s = "";
+  const char *step_mode_s;
   switch (this->step_mode_) {
     case ULN2003_STEP_MODE_FULL_STEP:
       step_mode_s = "FULL STEP";
@@ -51,6 +51,9 @@ void ULN2003::dump_config() {
     case ULN2003_STEP_MODE_WAVE_DRIVE:
       step_mode_s = "WAVE DRIVE";
       break;
+    default:
+      step_mode_s = "UNKNOWN";
+      break;
   }
   ESP_LOGCONFIG(TAG, "  Step Mode: %s", step_mode_s);
 }

From 567256bd62cb39ec9706af6b36ca405e584eae5d Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 27 Nov 2024 16:21:10 -0600
Subject: [PATCH 218/282] [rotary_encoder] clang-tidy fixes for #7822 (#7880)

---
 esphome/components/rotary_encoder/rotary_encoder.cpp | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp
index a3631ffe27..e9a0eac3f5 100644
--- a/esphome/components/rotary_encoder/rotary_encoder.cpp
+++ b/esphome/components/rotary_encoder/rotary_encoder.cpp
@@ -162,7 +162,7 @@ void RotaryEncoderSensor::dump_config() {
   LOG_PIN("  Pin B: ", this->pin_b_);
   LOG_PIN("  Pin I: ", this->pin_i_);
 
-  const LogString *restore_mode = LOG_STR("");
+  const LogString *restore_mode;
   switch (this->restore_mode_) {
     case ROTARY_ENCODER_RESTORE_DEFAULT_ZERO:
       restore_mode = LOG_STR("Restore (Defaults to zero)");
@@ -170,6 +170,8 @@ void RotaryEncoderSensor::dump_config() {
     case ROTARY_ENCODER_ALWAYS_ZERO:
       restore_mode = LOG_STR("Always zero");
       break;
+    default:
+      restore_mode = LOG_STR("");
   }
   ESP_LOGCONFIG(TAG, "  Restore Mode: %s", LOG_STR_ARG(restore_mode));
 

From 65a5216d17b14fddb803906074faa3fb8ef1512f Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 27 Nov 2024 16:22:18 -0600
Subject: [PATCH 219/282] [pca6416a, pca9554] clang-tidy fixes for #7822
 (#7879)

---
 esphome/components/pca6416a/pca6416a.cpp | 8 +++++---
 esphome/components/pca9554/pca9554.cpp   | 8 ++++----
 2 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/esphome/components/pca6416a/pca6416a.cpp b/esphome/components/pca6416a/pca6416a.cpp
index 1f4e315644..53c0dcaf76 100644
--- a/esphome/components/pca6416a/pca6416a.cpp
+++ b/esphome/components/pca6416a/pca6416a.cpp
@@ -34,7 +34,7 @@ void PCA6416AComponent::setup() {
   }
 
   // Test to see if the device supports pull-up resistors
-  if (this->read_register(PCAL6416A_PULL_EN0, &value, 1, true) == esphome::i2c::ERROR_OK) {
+  if (this->read_register(PCAL6416A_PULL_EN0, &value, 1, true) == i2c::ERROR_OK) {
     this->has_pullup_ = true;
   }
 
@@ -106,7 +106,8 @@ bool PCA6416AComponent::read_register_(uint8_t reg, uint8_t *value) {
     return false;
   }
 
-  if ((this->last_error_ = this->read_register(reg, value, 1, true)) != esphome::i2c::ERROR_OK) {
+  this->last_error_ = this->read_register(reg, value, 1, true);
+  if (this->last_error_ != i2c::ERROR_OK) {
     this->status_set_warning();
     ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_);
     return false;
@@ -122,7 +123,8 @@ bool PCA6416AComponent::write_register_(uint8_t reg, uint8_t value) {
     return false;
   }
 
-  if ((this->last_error_ = this->write_register(reg, &value, 1, true)) != esphome::i2c::ERROR_OK) {
+  this->last_error_ = this->write_register(reg, &value, 1, true);
+  if (this->last_error_ != i2c::ERROR_OK) {
     this->status_set_warning();
     ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_);
     return false;
diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp
index c5a4bcfb09..78b877072a 100644
--- a/esphome/components/pca9554/pca9554.cpp
+++ b/esphome/components/pca9554/pca9554.cpp
@@ -95,8 +95,8 @@ bool PCA9554Component::read_inputs_() {
     return false;
   }
 
-  if ((this->last_error_ = this->read_register(INPUT_REG * this->reg_width_, inputs, this->reg_width_, true)) !=
-      esphome::i2c::ERROR_OK) {
+  this->last_error_ = this->read_register(INPUT_REG * this->reg_width_, inputs, this->reg_width_, true);
+  if (this->last_error_ != i2c::ERROR_OK) {
     this->status_set_warning();
     ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_);
     return false;
@@ -113,8 +113,8 @@ bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) {
   uint8_t outputs[2];
   outputs[0] = (uint8_t) value;
   outputs[1] = (uint8_t) (value >> 8);
-  if ((this->last_error_ = this->write_register(reg * this->reg_width_, outputs, this->reg_width_, true)) !=
-      esphome::i2c::ERROR_OK) {
+  this->last_error_ = this->write_register(reg * this->reg_width_, outputs, this->reg_width_, true);
+  if (this->last_error_ != i2c::ERROR_OK) {
     this->status_set_warning();
     ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_);
     return false;

From a825ef59d47d0550983c733d5730d41ff52eaef7 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 27 Nov 2024 16:22:37 -0600
Subject: [PATCH 220/282] [nextion] clang-tidy fixes for #7822 (#7878)

---
 esphome/components/nextion/nextion.cpp | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp
index 7c41f8dfe2..50a5834347 100644
--- a/esphome/components/nextion/nextion.cpp
+++ b/esphome/components/nextion/nextion.cpp
@@ -563,13 +563,10 @@ void Nextion::process_nextion_commands_() {
           break;
         }
 
-        int dataindex = 0;
-
         int value = 0;
 
         for (int i = 0; i < 4; ++i) {
           value += to_process[i] << (8 * i);
-          ++dataindex;
         }
 
         NextionQueue *nb = this->nextion_queue_.front();

From 12cdeca48a78169069e462c3dc8785268d9ac90f Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 27 Nov 2024 16:23:20 -0600
Subject: [PATCH 221/282] [various] clang-tidy fixes for #7822 (#7874)

---
 .../hitachi_ac344/hitachi_ac344.cpp           |  6 ++-
 .../hitachi_ac424/hitachi_ac424.cpp           |  6 ++-
 .../components/ili9xxx/ili9xxx_display.cpp    |  5 +-
 esphome/components/qspi_dbi/qspi_dbi.cpp      |  3 +-
 esphome/components/st7735/st7735.cpp          |  2 +-
 esphome/components/st7789v/st7789v.cpp        |  2 +-
 esphome/components/udp/udp_component.cpp      | 48 ++++++++++++-------
 esphome/core/component.cpp                    |  2 +-
 8 files changed, 48 insertions(+), 26 deletions(-)

diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.cpp b/esphome/components/hitachi_ac344/hitachi_ac344.cpp
index 2825e4f04c..2bcb205644 100644
--- a/esphome/components/hitachi_ac344/hitachi_ac344.cpp
+++ b/esphome/components/hitachi_ac344/hitachi_ac344.cpp
@@ -133,8 +133,10 @@ bool HitachiClimate::get_swing_v_() {
 }
 
 void HitachiClimate::set_swing_h_(uint8_t position) {
-  if (position > HITACHI_AC344_SWINGH_LEFT_MAX)
-    return set_swing_h_(HITACHI_AC344_SWINGH_MIDDLE);
+  if (position > HITACHI_AC344_SWINGH_LEFT_MAX) {
+    set_swing_h_(HITACHI_AC344_SWINGH_MIDDLE);
+    return;
+  }
   set_bits(&remote_state_[HITACHI_AC344_SWINGH_BYTE], HITACHI_AC344_SWINGH_OFFSET, HITACHI_AC344_SWINGH_SIZE, position);
   set_button_(HITACHI_AC344_BUTTON_SWINGH);
 }
diff --git a/esphome/components/hitachi_ac424/hitachi_ac424.cpp b/esphome/components/hitachi_ac424/hitachi_ac424.cpp
index 0bfc3ae564..64f23dfc17 100644
--- a/esphome/components/hitachi_ac424/hitachi_ac424.cpp
+++ b/esphome/components/hitachi_ac424/hitachi_ac424.cpp
@@ -133,8 +133,10 @@ bool HitachiClimate::get_swing_v_() {
 }
 
 void HitachiClimate::set_swing_h_(uint8_t position) {
-  if (position > HITACHI_AC424_SWINGH_LEFT_MAX)
-    return set_swing_h_(HITACHI_AC424_SWINGH_MIDDLE);
+  if (position > HITACHI_AC424_SWINGH_LEFT_MAX) {
+    set_swing_h_(HITACHI_AC424_SWINGH_MIDDLE);
+    return;
+  }
   set_bits(&remote_state_[HITACHI_AC424_SWINGH_BYTE], HITACHI_AC424_SWINGH_OFFSET, HITACHI_AC424_SWINGH_SIZE, position);
   set_button_(HITACHI_AC424_BUTTON_SWINGH);
 }
diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp
index 81976dd2c9..b9664067a9 100644
--- a/esphome/components/ili9xxx/ili9xxx_display.cpp
+++ b/esphome/components/ili9xxx/ili9xxx_display.cpp
@@ -313,8 +313,9 @@ void ILI9XXXDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, cons
   // do color conversion pixel-by-pixel into the buffer and draw it later. If this is happening the user has not
   // configured the renderer well.
   if (this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || bitness != display::COLOR_BITNESS_565 || !big_endian) {
-    return display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset,
-                                            x_pad);
+    display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset,
+                                     x_pad);
+    return;
   }
   this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1);
   // x_ and y_offset are offsets into the source buffer, unrelated to our own offsets into the display.
diff --git a/esphome/components/qspi_dbi/qspi_dbi.cpp b/esphome/components/qspi_dbi/qspi_dbi.cpp
index 785885d4ec..f8fd5dd374 100644
--- a/esphome/components/qspi_dbi/qspi_dbi.cpp
+++ b/esphome/components/qspi_dbi/qspi_dbi.cpp
@@ -146,7 +146,8 @@ void QspiDbi::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8
     return;
   if (bitness != display::COLOR_BITNESS_565 || order != this->color_mode_ ||
       big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST)) {
-    return Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
+    Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
+    return;
   } else if (this->draw_from_origin_) {
     auto stride = x_offset + w + x_pad;
     for (int y = 0; y != h; y++) {
diff --git a/esphome/components/st7735/st7735.cpp b/esphome/components/st7735/st7735.cpp
index a0c2d80d16..5985d8bfb3 100644
--- a/esphome/components/st7735/st7735.cpp
+++ b/esphome/components/st7735/st7735.cpp
@@ -483,7 +483,7 @@ void ST7735::spi_master_write_color_(uint16_t color, uint16_t size) {
   }
 
   this->dc_pin_->digital_write(true);
-  return write_array(byte, size * 2);
+  write_array(byte, size * 2);
 }
 
 }  // namespace st7735
diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp
index 74c7a4e9e3..0d2f35aef5 100644
--- a/esphome/components/st7789v/st7789v.cpp
+++ b/esphome/components/st7789v/st7789v.cpp
@@ -252,7 +252,7 @@ void ST7789V::write_color_(uint16_t color, uint16_t size) {
   }
 
   this->dc_pin_->digital_write(true);
-  return write_array(byte, size * 2);
+  write_array(byte, size * 2);
 }
 
 size_t ST7789V::get_buffer_length_() {
diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp
index a1c8889997..b8727ec423 100644
--- a/esphome/components/udp/udp_component.cpp
+++ b/esphome/components/udp/udp_component.cpp
@@ -434,7 +434,8 @@ static bool process_rolling_code(Provider &provider, uint8_t *&buf, const uint8_
 void UDPComponent::process_(uint8_t *buf, const size_t len) {
   auto ping_key_seen = !this->ping_pong_enable_;
   if (len < 8) {
-    return ESP_LOGV(TAG, "Bad length %zu", len);
+    ESP_LOGV(TAG, "Bad length %zu", len);
+    return;
   }
   char namebuf[256]{};
   uint8_t byte;
@@ -442,31 +443,40 @@ void UDPComponent::process_(uint8_t *buf, const size_t len) {
   const uint8_t *end = buf + len;
   FuData rdata{};
   auto magic = get_uint16(buf);
-  if (magic != MAGIC_NUMBER && magic != MAGIC_PING)
-    return ESP_LOGV(TAG, "Bad magic %X", magic);
+  if (magic != MAGIC_NUMBER && magic != MAGIC_PING) {
+    ESP_LOGV(TAG, "Bad magic %X", magic);
+    return;
+  }
 
   auto hlen = *buf++;
   if (hlen > len - 3) {
-    return ESP_LOGV(TAG, "Bad hostname length %u > %zu", hlen, len - 3);
+    ESP_LOGV(TAG, "Bad hostname length %u > %zu", hlen, len - 3);
+    return;
   }
   memcpy(namebuf, buf, hlen);
   if (strcmp(this->name_, namebuf) == 0) {
-    return ESP_LOGV(TAG, "Ignoring our own data");
+    ESP_LOGV(TAG, "Ignoring our own data");
+    return;
   }
   buf += hlen;
-  if (magic == MAGIC_PING)
-    return this->process_ping_request_(namebuf, buf, end - buf);
+  if (magic == MAGIC_PING) {
+    this->process_ping_request_(namebuf, buf, end - buf);
+    return;
+  }
   if (round4(len) != len) {
-    return ESP_LOGW(TAG, "Bad length %zu", len);
+    ESP_LOGW(TAG, "Bad length %zu", len);
+    return;
   }
   hlen = round4(hlen + 3);
   buf = start_ptr + hlen;
   if (buf == end) {
-    return ESP_LOGV(TAG, "No data after header");
+    ESP_LOGV(TAG, "No data after header");
+    return;
   }
 
   if (this->providers_.count(namebuf) == 0) {
-    return ESP_LOGVV(TAG, "Unknown hostname %s", namebuf);
+    ESP_LOGVV(TAG, "Unknown hostname %s", namebuf);
+    return;
   }
   auto &provider = this->providers_[namebuf];
   // if encryption not used with this host, ping check is pointless since it would be easily spoofed.
@@ -489,7 +499,8 @@ void UDPComponent::process_(uint8_t *buf, const size_t len) {
     if (!process_rolling_code(provider, buf, end))
       return;
   } else if (byte != DATA_KEY) {
-    return ESP_LOGV(TAG, "Expected rolling_key or data_key, got %X", byte);
+    ESP_LOGV(TAG, "Expected rolling_key or data_key, got %X", byte);
+    return;
   }
   while (buf < end) {
     byte = *buf++;
@@ -497,7 +508,8 @@ void UDPComponent::process_(uint8_t *buf, const size_t len) {
       continue;
     if (byte == PING_KEY) {
       if (end - buf < 4) {
-        return ESP_LOGV(TAG, "PING_KEY requires 4 more bytes");
+        ESP_LOGV(TAG, "PING_KEY requires 4 more bytes");
+        return;
       }
       auto key = get_uint32(buf);
       if (key == this->ping_key_) {
@@ -515,21 +527,25 @@ void UDPComponent::process_(uint8_t *buf, const size_t len) {
     }
     if (byte == BINARY_SENSOR_KEY) {
       if (end - buf < 3) {
-        return ESP_LOGV(TAG, "Binary sensor key requires at least 3 more bytes");
+        ESP_LOGV(TAG, "Binary sensor key requires at least 3 more bytes");
+        return;
       }
       rdata.u32 = *buf++;
     } else if (byte == SENSOR_KEY) {
       if (end - buf < 6) {
-        return ESP_LOGV(TAG, "Sensor key requires at least 6 more bytes");
+        ESP_LOGV(TAG, "Sensor key requires at least 6 more bytes");
+        return;
       }
       rdata.u32 = get_uint32(buf);
     } else {
-      return ESP_LOGW(TAG, "Unknown key byte %X", byte);
+      ESP_LOGW(TAG, "Unknown key byte %X", byte);
+      return;
     }
 
     hlen = *buf++;
     if (end - buf < hlen) {
-      return ESP_LOGV(TAG, "Name length of %u not available", hlen);
+      ESP_LOGV(TAG, "Name length of %u not available", hlen);
+      return;
     }
     memset(namebuf, 0, sizeof namebuf);
     memcpy(namebuf, buf, hlen);
diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp
index ae73a451d9..a6224a17c0 100644
--- a/esphome/core/component.cpp
+++ b/esphome/core/component.cpp
@@ -67,7 +67,7 @@ bool Component::cancel_retry(const std::string &name) {  // NOLINT
 }
 
 void Component::set_timeout(const std::string &name, uint32_t timeout, std::function<void()> &&f) {  // NOLINT
-  return App.scheduler.set_timeout(this, name, timeout, std::move(f));
+  App.scheduler.set_timeout(this, name, timeout, std::move(f));
 }
 
 bool Component::cancel_timeout(const std::string &name) {  // NOLINT

From e229ed0da3f7113757907b1b7db1934517390c11 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 27 Nov 2024 16:23:40 -0600
Subject: [PATCH 222/282] [logger] clang-tidy fixes for #7822 (#7875)

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

diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp
index dac08fbbce..36934c7459 100644
--- a/esphome/components/logger/logger.cpp
+++ b/esphome/components/logger/logger.cpp
@@ -47,7 +47,7 @@ void Logger::write_header_(int level, const char *tag, int line) {
   if (current_task == main_task_) {
     this->printf_to_buffer_("%s[%s][%s:%03u]: ", color, letter, tag, line);
   } else {
-    const char *thread_name = "";
+    const char *thread_name = "";  // NOLINT(clang-analyzer-deadcode.DeadStores)
 #if defined(USE_ESP32)
     thread_name = pcTaskGetName(current_task);
 #elif defined(USE_LIBRETINY)

From e124151e5c4c6ca925baf382e7dca19592a257c9 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 27 Nov 2024 16:24:43 -0600
Subject: [PATCH 223/282] [ezo] clang-tidy fixes for #7822 (#7873)

---
 esphome/components/ezo/ezo.cpp | 40 ++++++++++++----------------------
 1 file changed, 14 insertions(+), 26 deletions(-)

diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp
index 8e4486dbf2..10f3d530ce 100644
--- a/esphome/components/ezo/ezo.cpp
+++ b/esphome/components/ezo/ezo.cpp
@@ -111,11 +111,11 @@ void EZOSensor::loop() {
   if (buf[0] == 1) {
     std::string payload = reinterpret_cast<char *>(&buf[1]);
     if (!payload.empty()) {
+      auto start_location = payload.find(',');
       switch (to_run->command_type) {
         case EzoCommandType::EZO_READ: {
           // some sensors return multiple comma-separated values, terminate string after first one
-          int start_location = 0;
-          if ((start_location = payload.find(',')) != std::string::npos) {
+          if (start_location != std::string::npos) {
             payload.erase(start_location);
           }
           auto val = parse_number<float>(payload);
@@ -126,49 +126,37 @@ void EZOSensor::loop() {
           }
           break;
         }
-        case EzoCommandType::EZO_LED: {
+        case EzoCommandType::EZO_LED:
           this->led_callback_.call(payload.back() == '1');
           break;
-        }
-        case EzoCommandType::EZO_DEVICE_INFORMATION: {
-          int start_location = 0;
-          if ((start_location = payload.find(',')) != std::string::npos) {
+        case EzoCommandType::EZO_DEVICE_INFORMATION:
+          if (start_location != std::string::npos) {
             this->device_infomation_callback_.call(payload.substr(start_location + 1));
           }
           break;
-        }
-        case EzoCommandType::EZO_SLOPE: {
-          int start_location = 0;
-          if ((start_location = payload.find(',')) != std::string::npos) {
+        case EzoCommandType::EZO_SLOPE:
+          if (start_location != std::string::npos) {
             this->slope_callback_.call(payload.substr(start_location + 1));
           }
           break;
-        }
-        case EzoCommandType::EZO_CALIBRATION: {
-          int start_location = 0;
-          if ((start_location = payload.find(',')) != std::string::npos) {
+        case EzoCommandType::EZO_CALIBRATION:
+          if (start_location != std::string::npos) {
             this->calibration_callback_.call(payload.substr(start_location + 1));
           }
           break;
-        }
-        case EzoCommandType::EZO_T: {
-          int start_location = 0;
-          if ((start_location = payload.find(',')) != std::string::npos) {
+        case EzoCommandType::EZO_T:
+          if (start_location != std::string::npos) {
             this->t_callback_.call(payload.substr(start_location + 1));
           }
           break;
-        }
-        case EzoCommandType::EZO_CUSTOM: {
+        case EzoCommandType::EZO_CUSTOM:
           this->custom_callback_.call(payload);
           break;
-        }
-        default: {
+        default:
           break;
-        }
       }
     }
   }
-
   this->commands_.pop_front();
 }
 
@@ -178,7 +166,7 @@ void EZOSensor::add_command_(const std::string &command, EzoCommandType command_
   ezo_command->command_type = command_type;
   ezo_command->delay_ms = delay_ms;
   this->commands_.push_back(std::move(ezo_command));
-};
+}
 
 void EZOSensor::set_calibration_point_(EzoCalibrationType type, float value) {
   std::string payload = str_sprintf("Cal,%s,%0.2f", EZO_CALIBRATION_TYPE_STRINGS[type], value);

From 7aa3a1a1ccdb8e3cf2df837817e18a7307b90640 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 27 Nov 2024 16:25:00 -0600
Subject: [PATCH 224/282] [apds9306] clang-tidy fixes for #7822 (#7872)

---
 esphome/components/apds9306/apds9306.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/esphome/components/apds9306/apds9306.cpp b/esphome/components/apds9306/apds9306.cpp
index 7b79b0964c..bbb3ba1910 100644
--- a/esphome/components/apds9306/apds9306.cpp
+++ b/esphome/components/apds9306/apds9306.cpp
@@ -122,7 +122,8 @@ void APDS9306::update() {
 
   this->status_clear_warning();
 
-  if (!(status &= 0b00001000)) {  // No new data
+  status &= 0b00001000;
+  if (!status) {  // No new data
     return;
   }
 

From ff5004d7db6514536dfd0e28ec3d114bb14b120c Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 27 Nov 2024 16:25:15 -0600
Subject: [PATCH 225/282] [dht] clang-tidy fixes for #7822 (#7871)

---
 esphome/components/dht/dht.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp
index db1c851d5f..3f9f9c57f4 100644
--- a/esphome/components/dht/dht.cpp
+++ b/esphome/components/dht/dht.cpp
@@ -135,7 +135,8 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
 
       // Wait for falling edge
       while (this->pin_->digital_read()) {
-        if ((end_time = micros()) - start_time > 90) {
+        end_time = micros();
+        if (end_time - start_time > 90) {
           if (i < 0) {
             error_code = 3;
           } else {

From d30587028412d80a02ea1fc4ef7cd0085b1508d8 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 27 Nov 2024 16:25:34 -0600
Subject: [PATCH 226/282] [network] clang-tidy fixes for #7822 (#7870)

---
 esphome/components/network/ip_address.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h
index 941934cf0a..69d3788ca5 100644
--- a/esphome/components/network/ip_address.h
+++ b/esphome/components/network/ip_address.h
@@ -116,7 +116,7 @@ struct IPAddress {
   operator arduino_ns::IPAddress() const { return ip_addr_get_ip4_u32(&ip_addr_); }
 #endif
 
-  bool is_set() { return !ip_addr_isany(&ip_addr_); }
+  bool is_set() { return !ip_addr_isany(&ip_addr_); }  // NOLINT(readability-simplify-boolean-expr)
   bool is_ip4() { return IP_IS_V4(&ip_addr_); }
   bool is_ip6() { return IP_IS_V6(&ip_addr_); }
   std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); }

From c9b0490305fb402f13f7ee4ee449db9d207c978e Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Thu, 28 Nov 2024 14:48:48 +1100
Subject: [PATCH 227/282] [lvgl] Make image update via lambda work (#7886)

---
 esphome/components/lvgl/defines.py         |  8 +++++-
 esphome/components/lvgl/lv_validation.py   | 11 +++++---
 esphome/components/lvgl/lvgl_esphome.h     | 16 ++++++++++++
 esphome/components/lvgl/widgets/animimg.py | 29 +++++++++-------------
 esphome/components/lvgl/widgets/img.py     |  2 --
 tests/components/lvgl/lvgl-package.yaml    | 12 ++++++---
 tests/components/lvgl/test.esp32-ard.yaml  |  2 +-
 7 files changed, 52 insertions(+), 28 deletions(-)

diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py
index ea345fa55c..81984637bd 100644
--- a/esphome/components/lvgl/defines.py
+++ b/esphome/components/lvgl/defines.py
@@ -8,7 +8,7 @@ import logging
 
 from esphome import codegen as cg, config_validation as cv
 from esphome.const import CONF_ITEMS
-from esphome.core import Lambda
+from esphome.core import ID, Lambda
 from esphome.cpp_generator import LambdaExpression, MockObj
 from esphome.cpp_types import uint32
 from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
@@ -72,6 +72,12 @@ class LValidator:
             )
         if self.retmapper is not None:
             return self.retmapper(value)
+        if isinstance(value, ID):
+            return await cg.get_variable(value)
+        if isinstance(value, list):
+            value = [
+                await cg.get_variable(x) if isinstance(x, ID) else x for x in value
+            ]
         return cg.safe_exp(value)
 
 
diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py
index 766c010244..f91ed893f2 100644
--- a/esphome/components/lvgl/lv_validation.py
+++ b/esphome/components/lvgl/lv_validation.py
@@ -1,6 +1,7 @@
 from typing import Union
 
 import esphome.codegen as cg
+from esphome.components import image
 from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw
 from esphome.components.font import Font
 from esphome.components.image import Image_
@@ -31,7 +32,7 @@ from .defines import (
     literal,
 )
 from .helpers import add_lv_use, esphome_fonts_used, lv_fonts_used, requires_component
-from .types import lv_font_t, lv_gradient_t, lv_img_t
+from .types import lv_font_t, lv_gradient_t
 
 opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
 
@@ -332,8 +333,12 @@ def image_validator(value):
 
 lv_image = LValidator(
     image_validator,
-    lv_img_t,
-    retmapper=lambda x: MockObj(x, "->").get_lv_img_dsc(),
+    image.Image_.operator("ptr"),
+    requires="image",
+)
+lv_image_list = LValidator(
+    cv.ensure_list(image_validator),
+    cg.std_vector.template(image.Image_.operator("ptr")),
     requires="image",
 )
 lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal)
diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h
index 208cb1cbd5..921b7c109f 100644
--- a/esphome/components/lvgl/lvgl_esphome.h
+++ b/esphome/components/lvgl/lvgl_esphome.h
@@ -57,6 +57,22 @@ inline void lv_img_set_src(lv_obj_t *obj, esphome::image::Image *image) {
   lv_img_set_src(obj, image->get_lv_img_dsc());
 }
 #endif  // USE_LVGL_IMAGE
+#ifdef USE_LVGL_ANIMIMG
+inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images) {
+  auto *dsc = static_cast<std::vector<lv_img_dsc_t *> *>(lv_obj_get_user_data(img));
+  if (dsc == nullptr) {
+    // object will be lazily allocated but never freed.
+    dsc = new std::vector<lv_img_dsc_t *>(images.size());  // NOLINT
+    lv_obj_set_user_data(img, dsc);
+  }
+  dsc->clear();
+  for (auto &image : images) {
+    dsc->push_back(image->get_lv_img_dsc());
+  }
+  lv_animimg_set_src(img, (const void **) dsc->data(), dsc->size());
+}
+
+#endif  // USE_LVGL_ANIMIMG
 
 // Parent class for things that wrap an LVGL object
 class LvCompound {
diff --git a/esphome/components/lvgl/widgets/animimg.py b/esphome/components/lvgl/widgets/animimg.py
index 8adea72ad3..b824d28fb8 100644
--- a/esphome/components/lvgl/widgets/animimg.py
+++ b/esphome/components/lvgl/widgets/animimg.py
@@ -1,20 +1,18 @@
 from esphome import automation
-import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.const import CONF_DURATION, CONF_ID
 
 from ..automation import action_to_code
 from ..defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC
 from ..helpers import lvgl_components_required
-from ..lv_validation import lv_image, lv_milliseconds
+from ..lv_validation import lv_image_list, lv_milliseconds
 from ..lvcode import lv
-from ..types import LvType, ObjUpdateAction, void_ptr
+from ..types import LvType, ObjUpdateAction
 from . import Widget, WidgetType, get_widgets
 from .img import CONF_IMAGE
 from .label import CONF_LABEL
 
 CONF_ANIMIMG = "animimg"
-CONF_SRC_LIST_ID = "src_list_id"
 
 
 def lv_repeat_count(value):
@@ -32,14 +30,14 @@ ANIMIMG_BASE_SCHEMA = cv.Schema(
 ANIMIMG_SCHEMA = ANIMIMG_BASE_SCHEMA.extend(
     {
         cv.Required(CONF_DURATION): lv_milliseconds,
-        cv.Required(CONF_SRC): cv.ensure_list(lv_image),
-        cv.GenerateID(CONF_SRC_LIST_ID): cv.declare_id(void_ptr),
+        cv.Required(CONF_SRC): lv_image_list,
     }
 )
 
 ANIMIMG_MODIFY_SCHEMA = ANIMIMG_BASE_SCHEMA.extend(
     {
         cv.Optional(CONF_DURATION): lv_milliseconds,
+        cv.Optional(CONF_SRC): lv_image_list,
     }
 )
 
@@ -59,17 +57,14 @@ class AnimimgType(WidgetType):
     async def to_code(self, w: Widget, config):
         lvgl_components_required.add(CONF_IMAGE)
         lvgl_components_required.add(CONF_ANIMIMG)
-        if CONF_SRC in config:
-            srcs = [
-                await lv_image.process(await cg.get_variable(x))
-                for x in config[CONF_SRC]
-            ]
-            src_id = cg.static_const_array(config[CONF_SRC_LIST_ID], srcs)
-            count = len(config[CONF_SRC])
-            lv.animimg_set_src(w.obj, src_id, count)
-        lv.animimg_set_repeat_count(w.obj, config[CONF_REPEAT_COUNT])
-        lv.animimg_set_duration(w.obj, config[CONF_DURATION])
-        if config.get(CONF_AUTO_START):
+        if srcs := config.get(CONF_SRC):
+            srcs = await lv_image_list.process(srcs)
+            lv.animimg_set_src(w.obj, srcs)
+        if repeat_count := config.get(CONF_REPEAT_COUNT):
+            lv.animimg_set_repeat_count(w.obj, repeat_count)
+        if duration := config.get(CONF_DURATION):
+            lv.animimg_set_duration(w.obj, duration)
+        if config[CONF_AUTO_START]:
             lv.animimg_start(w.obj)
 
     def get_uses(self):
diff --git a/esphome/components/lvgl/widgets/img.py b/esphome/components/lvgl/widgets/img.py
index 931d0c0b5b..59b2c97c63 100644
--- a/esphome/components/lvgl/widgets/img.py
+++ b/esphome/components/lvgl/widgets/img.py
@@ -1,4 +1,3 @@
-import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.const import CONF_ANGLE, CONF_MODE
 
@@ -65,7 +64,6 @@ class ImgType(WidgetType):
 
     async def to_code(self, w: Widget, config):
         if src := config.get(CONF_SRC):
-            src = await cg.get_variable(src)
             lv.img_set_src(w.obj, await lv_image.process(src))
         if (cf_angle := config.get(CONF_ANGLE)) is not None:
             pivot_x = config[CONF_PIVOT_X]
diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml
index db0443b3bb..81b18c4ff8 100644
--- a/tests/components/lvgl/lvgl-package.yaml
+++ b/tests/components/lvgl/lvgl-package.yaml
@@ -171,9 +171,13 @@ lvgl:
             duration: 1s
             auto_start: true
             on_all_events:
-              logger.log:
-                format: "Event %s"
-                args: ['lv_event_code_name_for(event->code).c_str()']
+              - logger.log:
+                  format: "Event %s"
+                  args: ['lv_event_code_name_for(event->code).c_str()']
+              - lvgl.animimg.update:
+                  id: anim_img
+                  src: !lambda "return {dog_image, cat_image};"
+                  duration: 2s
         - label:
             id: hello_label
             text: Hello world
@@ -635,7 +639,7 @@ lvgl:
                   - image:
                       grid_cell_row_pos: 0
                       grid_cell_column_pos: 0
-                      src: dog_image
+                      src: !lambda return dog_image;
                       on_click:
                         then:
                           - lvgl.tabview.select:
diff --git a/tests/components/lvgl/test.esp32-ard.yaml b/tests/components/lvgl/test.esp32-ard.yaml
index 80d5ce503f..5b09147de7 100644
--- a/tests/components/lvgl/test.esp32-ard.yaml
+++ b/tests/components/lvgl/test.esp32-ard.yaml
@@ -55,5 +55,5 @@ lvgl:
 
 packages:
   lvgl: !include lvgl-package.yaml
+  xvgl: !include common.yaml
 
-<<: !include common.yaml

From 7cdf5b55ef637e76d13e31b596d7b7d76a5b4ab8 Mon Sep 17 00:00:00 2001
From: Max Slotov <max@slotov.dev>
Date: Thu, 28 Nov 2024 05:51:07 +0200
Subject: [PATCH 228/282] [deep_sleep] fix deep_sleep not keeping awake when
 sleep_duration is defined (#7885)

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

diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp
index d54046bc11..d647140865 100644
--- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp
+++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp
@@ -52,11 +52,11 @@ void DeepSleepComponent::dump_config_platform_() {
 
 bool DeepSleepComponent::prepare_to_sleep_() {
   if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_KEEP_AWAKE && this->wakeup_pin_ != nullptr &&
-      !this->sleep_duration_.has_value() && this->wakeup_pin_->digital_read()) {
+      this->wakeup_pin_->digital_read()) {
     // Defer deep sleep until inactive
     if (!this->next_enter_deep_sleep_) {
       this->status_set_warning();
-      ESP_LOGW(TAG, "Waiting for pin_ to switch state to enter deep sleep...");
+      ESP_LOGW(TAG, "Waiting wakeup pin state change to enter deep sleep...");
     }
     this->next_enter_deep_sleep_ = true;
     return false;

From beb8ab50e27f5df45e15af1cf3691309a5eb2e92 Mon Sep 17 00:00:00 2001
From: guillempages <guillempages@users.noreply.github.com>
Date: Thu, 28 Nov 2024 04:55:20 +0100
Subject: [PATCH 229/282] [online_image]Don't access decoder if not initialized
 (#7882)

---
 esphome/components/online_image/png_image.cpp | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/online_image/png_image.cpp
index c8e215a91d..4c4c22f9b7 100644
--- a/esphome/components/online_image/png_image.cpp
+++ b/esphome/components/online_image/png_image.cpp
@@ -49,6 +49,10 @@ void PngDecoder::prepare(uint32_t download_size) {
 }
 
 int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
+  if (!this->pngle_) {
+    ESP_LOGE(TAG, "PNG decoder engine not initialized!");
+    return -1;
+  }
   if (size < 256 && size < this->download_size_ - this->decoded_bytes_) {
     ESP_LOGD(TAG, "Waiting for data");
     return 0;

From 5486b40aab86f6505c62b9077b1d1737548ec554 Mon Sep 17 00:00:00 2001
From: FreeBear-nc <67865163+FreeBear-nc@users.noreply.github.com>
Date: Thu, 28 Nov 2024 03:56:37 +0000
Subject: [PATCH 230/282] Add IRAM_ATTR to all functions used during interrupts
 on esp8266 chips. (#7840)

---
 esphome/components/opentherm/opentherm.cpp | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp
index e40fc66b7d..62ab1d3860 100644
--- a/esphome/components/opentherm/opentherm.cpp
+++ b/esphome/components/opentherm/opentherm.cpp
@@ -220,7 +220,7 @@ void IRAM_ATTR OpenTherm::bit_read_(uint8_t value) {
   this->bit_pos_++;
 }
 
-ProtocolErrorType OpenTherm::verify_stop_bit_(uint8_t value) {
+ProtocolErrorType IRAM_ATTR OpenTherm::verify_stop_bit_(uint8_t value) {
   if (value) {  // stop bit detected
     return check_parity_(this->data_) ? ProtocolErrorType::NO_ERROR : ProtocolErrorType::PARITY_ERROR;
   } else {  // no stop bit detected, error
@@ -365,7 +365,7 @@ void IRAM_ATTR OpenTherm::stop_timer_() {
 
 #ifdef ESP8266
 // 5 kHz timer_
-void OpenTherm::start_read_timer_() {
+void IRAM_ATTR OpenTherm::start_read_timer_() {
   InterruptLock const lock;
   timer1_attachInterrupt(OpenTherm::esp8266_timer_isr);
   timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP);  // 5MHz (5 ticks/us - 1677721.4 us max)
@@ -373,14 +373,14 @@ void OpenTherm::start_read_timer_() {
 }
 
 // 2 kHz timer_
-void OpenTherm::start_write_timer_() {
+void IRAM_ATTR OpenTherm::start_write_timer_() {
   InterruptLock const lock;
   timer1_attachInterrupt(OpenTherm::esp8266_timer_isr);
   timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP);  // 5MHz (5 ticks/us - 1677721.4 us max)
   timer1_write(2500);                            // 2kHz
 }
 
-void OpenTherm::stop_timer_() {
+void IRAM_ATTR OpenTherm::stop_timer_() {
   InterruptLock const lock;
   timer1_disable();
   timer1_detachInterrupt();
@@ -389,7 +389,7 @@ void OpenTherm::stop_timer_() {
 #endif  // END ESP8266
 
 // https://stackoverflow.com/questions/21617970/how-to-check-if-value-has-even-parity-of-bits-or-odd
-bool OpenTherm::check_parity_(uint32_t val) {
+bool IRAM_ATTR OpenTherm::check_parity_(uint32_t val) {
   val ^= val >> 16;
   val ^= val >> 8;
   val ^= val >> 4;

From 217a80a1789d235705c47862748ef2c0a8af5a1c Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Thu, 28 Nov 2024 16:57:11 +1300
Subject: [PATCH 231/282] [st7920] Remove unnecessary warning when drawing
 outside display bounds (#7868)

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

diff --git a/esphome/components/st7920/st7920.cpp b/esphome/components/st7920/st7920.cpp
index f336d24e24..171e7095dd 100644
--- a/esphome/components/st7920/st7920.cpp
+++ b/esphome/components/st7920/st7920.cpp
@@ -1,7 +1,7 @@
 #include "st7920.h"
-#include "esphome/core/log.h"
-#include "esphome/core/application.h"
 #include "esphome/components/display/display_buffer.h"
+#include "esphome/core/application.h"
+#include "esphome/core/log.h"
 
 namespace esphome {
 namespace st7920 {
@@ -118,7 +118,6 @@ size_t ST7920::get_buffer_length_() {
 
 void HOT ST7920::draw_absolute_pixel_internal(int x, int y, Color color) {
   if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) {
-    ESP_LOGW(TAG, "Position out of area: %dx%d", x, y);
     return;
   }
   int width = this->get_width_internal() / 8u;

From 30477c764d9353381ef6e8bf186bae703cffbc7f Mon Sep 17 00:00:00 2001
From: Krzysztof Zdulski <krzys.zdulski@gmail.com>
Date: Fri, 29 Nov 2024 22:05:00 +0100
Subject: [PATCH 232/282] Fix recalc_timestamp_utc (#7894)

---
 esphome/core/time.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp
index 31977d972b..66a0e1c0a7 100644
--- a/esphome/core/time.cpp
+++ b/esphome/core/time.cpp
@@ -169,7 +169,7 @@ void ESPTime::recalc_timestamp_utc(bool use_day_of_year) {
   }
 
   for (int i = 1970; i < this->year; i++)
-    res += (year % 4 == 0) ? 366 : 365;
+    res += (i % 4 == 0) ? 366 : 365;
 
   if (use_day_of_year) {
     res += this->day_of_year - 1;

From 8f69d070612d9ed7e862fc31c340568dffabf9f7 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Sun, 1 Dec 2024 10:08:52 -0600
Subject: [PATCH 233/282] [hx711] clang-tidy fixes for #7822 (#7900)

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

diff --git a/esphome/components/hx711/hx711.cpp b/esphome/components/hx711/hx711.cpp
index 1a7169eed7..9643d0c411 100644
--- a/esphome/components/hx711/hx711.cpp
+++ b/esphome/components/hx711/hx711.cpp
@@ -53,7 +53,7 @@ bool HX711Sensor::read_sensor_(uint32_t *result) {
     }
 
     // Cycle clock pin for gain setting
-    for (uint8_t i = 0; i < this->gain_; i++) {
+    for (uint8_t i = 0; i < static_cast<uint8_t>(this->gain_); i++) {
       this->sck_pin_->digital_write(true);
       delayMicroseconds(1);
       this->sck_pin_->digital_write(false);
diff --git a/esphome/components/hx711/hx711.h b/esphome/components/hx711/hx711.h
index 0cb6868ab5..a92bb9945d 100644
--- a/esphome/components/hx711/hx711.h
+++ b/esphome/components/hx711/hx711.h
@@ -9,7 +9,7 @@
 namespace esphome {
 namespace hx711 {
 
-enum HX711Gain {
+enum HX711Gain : uint8_t {
   HX711_GAIN_128 = 1,
   HX711_GAIN_32 = 2,
   HX711_GAIN_64 = 3,

From 83d6834e277e9e9159f12653a2f39bc1113f9104 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Mon, 2 Dec 2024 05:10:18 +1300
Subject: [PATCH 234/282] Cast port to int for ota pushing (#7888)

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

diff --git a/esphome/__main__.py b/esphome/__main__.py
index 86d529e1bf..dce041e5ac 100644
--- a/esphome/__main__.py
+++ b/esphome/__main__.py
@@ -363,7 +363,7 @@ def upload_program(config, args, host):
 
     from esphome import espota2
 
-    remote_port = ota_conf[CONF_PORT]
+    remote_port = int(ota_conf[CONF_PORT])
     password = ota_conf.get(CONF_PASSWORD, "")
 
     if (

From edd847ea403c4a0fba05d652b8e30e09fd3e6a85 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Sun, 1 Dec 2024 18:27:32 -0600
Subject: [PATCH 235/282] [modbus_controller] Clang fixes (#7899)

---
 .../modbus_controller/modbus_controller.cpp   | 40 ++++++++---------
 .../modbus_controller/modbus_controller.h     | 44 +++++++++----------
 .../number/modbus_number.cpp                  | 16 +++----
 .../modbus_controller/number/modbus_number.h  |  8 ++--
 .../output/modbus_output.cpp                  | 13 +++---
 .../modbus_controller/output/modbus_output.h  | 10 ++---
 .../select/modbus_select.cpp                  |  7 +--
 .../modbus_controller/select/modbus_select.h  |  8 ++--
 .../switch/modbus_switch.cpp                  |  8 ++--
 .../modbus_controller/switch/modbus_switch.h  |  4 +-
 10 files changed, 80 insertions(+), 78 deletions(-)

diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp
index f8b72af817..641ba68223 100644
--- a/esphome/components/modbus_controller/modbus_controller.cpp
+++ b/esphome/components/modbus_controller/modbus_controller.cpp
@@ -152,11 +152,11 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t
 }
 
 SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const {
-  auto reg_it = find_if(begin(register_ranges_), end(register_ranges_), [=](RegisterRange const &r) {
+  auto reg_it = find_if(begin(this->register_ranges_), end(this->register_ranges_), [=](RegisterRange const &r) {
     return (r.start_address == start_address && r.register_type == register_type);
   });
 
-  if (reg_it == register_ranges_.end()) {
+  if (reg_it == this->register_ranges_.end()) {
     ESP_LOGE(TAG, "No matching range for sensor found - start_address : 0x%X", start_address);
   } else {
     return reg_it->sensors;
@@ -240,18 +240,18 @@ void ModbusController::update() {
 
 // walk through the sensors and determine the register ranges to read
 size_t ModbusController::create_register_ranges_() {
-  register_ranges_.clear();
-  if (this->parent_->role == modbus::ModbusRole::CLIENT && sensorset_.empty()) {
+  this->register_ranges_.clear();
+  if (this->parent_->role == modbus::ModbusRole::CLIENT && this->sensorset_.empty()) {
     ESP_LOGW(TAG, "No sensors registered");
     return 0;
   }
 
   // iterator is sorted see SensorItemsComparator for details
-  auto ix = sensorset_.begin();
+  auto ix = this->sensorset_.begin();
   RegisterRange r = {};
   uint8_t buffer_offset = 0;
   SensorItem *prev = nullptr;
-  while (ix != sensorset_.end()) {
+  while (ix != this->sensorset_.end()) {
     SensorItem *curr = *ix;
 
     ESP_LOGV(TAG, "Register: 0x%X %d %d %d offset=%u skip=%u addr=%p", curr->start_address, curr->register_count,
@@ -278,12 +278,12 @@ size_t ModbusController::create_register_ranges_() {
           // this register can re-use the data from the previous register
 
           // remove this sensore because start_address is changed (sort-order)
-          ix = sensorset_.erase(ix);
+          ix = this->sensorset_.erase(ix);
 
           curr->start_address = r.start_address;
           curr->offset += prev->offset;
 
-          sensorset_.insert(curr);
+          this->sensorset_.insert(curr);
           // move iterator backwards because it will be incremented later
           ix--;
 
@@ -293,14 +293,14 @@ size_t ModbusController::create_register_ranges_() {
           // this register can extend the current range
 
           // remove this sensore because start_address is changed (sort-order)
-          ix = sensorset_.erase(ix);
+          ix = this->sensorset_.erase(ix);
 
           curr->start_address = r.start_address;
           curr->offset += buffer_offset;
           buffer_offset += curr->get_register_size();
           r.register_count += curr->register_count;
 
-          sensorset_.insert(curr);
+          this->sensorset_.insert(curr);
           // move iterator backwards because it will be incremented later
           ix--;
 
@@ -327,7 +327,7 @@ size_t ModbusController::create_register_ranges_() {
       ix++;
     } else {
       ESP_LOGV(TAG, "Add range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates);
-      register_ranges_.push_back(r);
+      this->register_ranges_.push_back(r);
       r = {};
       buffer_offset = 0;
       // do not increment the iterator here because the current sensor has to be re-evaluated
@@ -339,10 +339,10 @@ size_t ModbusController::create_register_ranges_() {
   if (r.register_count > 0) {
     // Add the last range
     ESP_LOGV(TAG, "Add last range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates);
-    register_ranges_.push_back(r);
+    this->register_ranges_.push_back(r);
   }
 
-  return register_ranges_.size();
+  return this->register_ranges_.size();
 }
 
 void ModbusController::dump_config() {
@@ -352,18 +352,18 @@ void ModbusController::dump_config() {
   ESP_LOGCONFIG(TAG, "  Offline Skip Updates: %d", this->offline_skip_updates_);
 #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
   ESP_LOGCONFIG(TAG, "sensormap");
-  for (auto &it : sensorset_) {
+  for (auto &it : this->sensorset_) {
     ESP_LOGCONFIG(TAG, " Sensor type=%zu start=0x%X offset=0x%X count=%d size=%d",
                   static_cast<uint8_t>(it->register_type), it->start_address, it->offset, it->register_count,
                   it->get_register_size());
   }
   ESP_LOGCONFIG(TAG, "ranges");
-  for (auto &it : register_ranges_) {
+  for (auto &it : this->register_ranges_) {
     ESP_LOGCONFIG(TAG, "  Range type=%zu start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type),
                   it.start_address, it.register_count, it.skip_updates);
   }
   ESP_LOGCONFIG(TAG, "server registers");
-  for (auto &r : server_registers_) {
+  for (auto &r : this->server_registers_) {
     ESP_LOGCONFIG(TAG, "  Address=0x%02X value_type=%zu register_count=%u", r->address,
                   static_cast<uint8_t>(r->value_type), r->register_count);
   }
@@ -372,11 +372,11 @@ void ModbusController::dump_config() {
 
 void ModbusController::loop() {
   // Incoming data to process?
-  if (!incoming_queue_.empty()) {
-    auto &message = incoming_queue_.front();
+  if (!this->incoming_queue_.empty()) {
+    auto &message = this->incoming_queue_.front();
     if (message != nullptr)
       process_modbus_data_(message.get());
-    incoming_queue_.pop();
+    this->incoming_queue_.pop();
 
   } else {
     // all messages processed send pending commands
@@ -391,7 +391,7 @@ void ModbusController::on_write_register_response(ModbusRegisterType register_ty
 
 void ModbusController::dump_sensors_() {
   ESP_LOGV(TAG, "sensors");
-  for (auto &it : sensorset_) {
+  for (auto &it : this->sensorset_) {
     ESP_LOGV(TAG, "  Sensor start=0x%X count=%d size=%d offset=%d", it->start_address, it->register_count,
              it->get_register_size(), it->offset);
   }
diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h
index 2a0b936bf5..dfd52e44bc 100644
--- a/esphome/components/modbus_controller/modbus_controller.h
+++ b/esphome/components/modbus_controller/modbus_controller.h
@@ -240,14 +240,14 @@ class SensorItem {
   }
   // Override register size for modbus devices not using 1 register for one dword
   void set_register_size(uint8_t register_size) { response_bytes = register_size; }
-  ModbusRegisterType register_type;
-  SensorValueType sensor_value_type;
-  uint16_t start_address;
-  uint32_t bitmask;
-  uint8_t offset;
-  uint8_t register_count;
+  ModbusRegisterType register_type{ModbusRegisterType::CUSTOM};
+  SensorValueType sensor_value_type{SensorValueType::RAW};
+  uint16_t start_address{0};
+  uint32_t bitmask{0};
+  uint8_t offset{0};
+  uint8_t register_count{0};
   uint8_t response_bytes{0};
-  uint16_t skip_updates;
+  uint16_t skip_updates{0};
   std::vector<uint8_t> custom_data{};
   bool force_new_range{false};
 };
@@ -261,9 +261,9 @@ class ServerRegister {
     this->register_count = register_count;
     this->read_lambda = std::move(read_lambda);
   }
-  uint16_t address;
-  SensorValueType value_type;
-  uint8_t register_count;
+  uint16_t address{0};
+  SensorValueType value_type{SensorValueType::RAW};
+  uint8_t register_count{0};
   std::function<float()> read_lambda;
 };
 
@@ -312,11 +312,11 @@ struct RegisterRange {
 class ModbusCommandItem {
  public:
   static const size_t MAX_PAYLOAD_BYTES = 240;
-  ModbusController *modbusdevice;
-  uint16_t register_address;
-  uint16_t register_count;
-  ModbusFunctionCode function_code;
-  ModbusRegisterType register_type;
+  ModbusController *modbusdevice{nullptr};
+  uint16_t register_address{0};
+  uint16_t register_count{0};
+  ModbusFunctionCode function_code{ModbusFunctionCode::CUSTOM};
+  ModbusRegisterType register_type{ModbusRegisterType::CUSTOM};
   std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
       on_data_func;
   std::vector<uint8_t> payload = {};
@@ -493,23 +493,23 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
   /// Collection of all sensors for this component
   SensorSet sensorset_;
   /// Collection of all server registers for this component
-  std::vector<ServerRegister *> server_registers_;
+  std::vector<ServerRegister *> server_registers_{};
   /// Continuous range of modbus registers
-  std::vector<RegisterRange> register_ranges_;
+  std::vector<RegisterRange> register_ranges_{};
   /// Hold the pending requests to be sent
   std::list<std::unique_ptr<ModbusCommandItem>> command_queue_;
   /// modbus response data waiting to get processed
   std::queue<std::unique_ptr<ModbusCommandItem>> incoming_queue_;
   /// if duplicate commands can be sent
-  bool allow_duplicate_commands_;
+  bool allow_duplicate_commands_{false};
   /// when was the last send operation
-  uint32_t last_command_timestamp_;
+  uint32_t last_command_timestamp_{0};
   /// min time in ms between sending modbus commands
-  uint16_t command_throttle_;
+  uint16_t command_throttle_{0};
   /// if module didn't respond the last command
-  bool module_offline_;
+  bool module_offline_{false};
   /// how many updates to skip if module is offline
-  uint16_t offline_skip_updates_;
+  uint16_t offline_skip_updates_{0};
   /// How many times we will retry a command if we get no response
   uint8_t max_cmd_retries_{4};
   /// Command sent callback
diff --git a/esphome/components/modbus_controller/number/modbus_number.cpp b/esphome/components/modbus_controller/number/modbus_number.cpp
index 001cfb5787..ea8467d5a3 100644
--- a/esphome/components/modbus_controller/number/modbus_number.cpp
+++ b/esphome/components/modbus_controller/number/modbus_number.cpp
@@ -8,7 +8,7 @@ namespace modbus_controller {
 static const char *const TAG = "modbus.number";
 
 void ModbusNumber::parse_and_publish(const std::vector<uint8_t> &data) {
-  float result = payload_to_float(data, *this) / multiply_by_;
+  float result = payload_to_float(data, *this) / this->multiply_by_;
 
   // Is there a lambda registered
   // call it with the pre converted value and the raw data array
@@ -43,7 +43,7 @@ void ModbusNumber::control(float value) {
       return;
     }
   } else {
-    write_value = multiply_by_ * write_value;
+    write_value = this->multiply_by_ * write_value;
   }
 
   if (!data.empty()) {
@@ -63,21 +63,21 @@ void ModbusNumber::control(float value) {
     // Create and send the write command
     if (this->register_count == 1 && !this->use_write_multiple_) {
       // since offset is in bytes and a register is 16 bits we get the start by adding offset/2
-      write_cmd =
-          ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset / 2, data[0]);
+      write_cmd = ModbusCommandItem::create_write_single_command(this->parent_, this->start_address + this->offset / 2,
+                                                                 data[0]);
     } else {
-      write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset / 2,
-                                                                   this->register_count, data);
+      write_cmd = ModbusCommandItem::create_write_multiple_command(
+          this->parent_, this->start_address + this->offset / 2, this->register_count, data);
     }
     // publish new value
     write_cmd.on_data_func = [this, write_cmd, value](ModbusRegisterType register_type, uint16_t start_address,
                                                       const std::vector<uint8_t> &data) {
       // gets called when the write command is ack'd from the device
-      parent_->on_write_register_response(write_cmd.register_type, start_address, data);
+      this->parent_->on_write_register_response(write_cmd.register_type, start_address, data);
       this->publish_state(value);
     };
   }
-  parent_->queue_command(write_cmd);
+  this->parent_->queue_command(write_cmd);
   this->publish_state(value);
 }
 void ModbusNumber::dump_config() { LOG_NUMBER(TAG, "Modbus Number", this); }
diff --git a/esphome/components/modbus_controller/number/modbus_number.h b/esphome/components/modbus_controller/number/modbus_number.h
index 544d161cbc..8f77b2e014 100644
--- a/esphome/components/modbus_controller/number/modbus_number.h
+++ b/esphome/components/modbus_controller/number/modbus_number.h
@@ -29,7 +29,7 @@ class ModbusNumber : public number::Number, public Component, public SensorItem
   void parse_and_publish(const std::vector<uint8_t> &data) override;
   float get_setup_priority() const override { return setup_priority::HARDWARE; }
   void set_parent(ModbusController *parent) { this->parent_ = parent; }
-  void set_write_multiply(float factor) { multiply_by_ = factor; }
+  void set_write_multiply(float factor) { this->multiply_by_ = factor; }
 
   using transform_func_t = std::function<optional<float>(ModbusNumber *, float, const std::vector<uint8_t> &)>;
   using write_transform_func_t = std::function<optional<float>(ModbusNumber *, float, std::vector<uint16_t> &)>;
@@ -39,9 +39,9 @@ class ModbusNumber : public number::Number, public Component, public SensorItem
 
  protected:
   void control(float value) override;
-  optional<transform_func_t> transform_func_;
-  optional<write_transform_func_t> write_transform_func_;
-  ModbusController *parent_;
+  optional<transform_func_t> transform_func_{nullopt};
+  optional<write_transform_func_t> write_transform_func_{nullopt};
+  ModbusController *parent_{nullptr};
   float multiply_by_{1.0};
   bool use_write_multiple_{false};
 };
diff --git a/esphome/components/modbus_controller/output/modbus_output.cpp b/esphome/components/modbus_controller/output/modbus_output.cpp
index 79cd2d49c2..f0f6e64f10 100644
--- a/esphome/components/modbus_controller/output/modbus_output.cpp
+++ b/esphome/components/modbus_controller/output/modbus_output.cpp
@@ -27,7 +27,7 @@ void ModbusFloatOutput::write_state(float value) {
       return;
     }
   } else {
-    value = multiply_by_ * value;
+    value = this->multiply_by_ * value;
   }
   // lambda didn't set payload
   if (data.empty()) {
@@ -40,12 +40,13 @@ void ModbusFloatOutput::write_state(float value) {
   // Create and send the write command
   ModbusCommandItem write_cmd;
   if (this->register_count == 1 && !this->use_write_multiple_) {
-    write_cmd = ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset, data[0]);
+    write_cmd =
+        ModbusCommandItem::create_write_single_command(this->parent_, this->start_address + this->offset, data[0]);
   } else {
-    write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset,
+    write_cmd = ModbusCommandItem::create_write_multiple_command(this->parent_, this->start_address + this->offset,
                                                                  this->register_count, data);
   }
-  parent_->queue_command(write_cmd);
+  this->parent_->queue_command(write_cmd);
 }
 
 void ModbusFloatOutput::dump_config() {
@@ -90,9 +91,9 @@ void ModbusBinaryOutput::write_state(bool state) {
     // offset for coil and discrete inputs is the coil/register number not bytes
     if (this->use_write_multiple_) {
       std::vector<bool> states{state};
-      cmd = ModbusCommandItem::create_write_multiple_coils(parent_, this->start_address + this->offset, states);
+      cmd = ModbusCommandItem::create_write_multiple_coils(this->parent_, this->start_address + this->offset, states);
     } else {
-      cmd = ModbusCommandItem::create_write_single_coil(parent_, this->start_address + this->offset, state);
+      cmd = ModbusCommandItem::create_write_single_coil(this->parent_, this->start_address + this->offset, state);
     }
   }
   this->parent_->queue_command(cmd);
diff --git a/esphome/components/modbus_controller/output/modbus_output.h b/esphome/components/modbus_controller/output/modbus_output.h
index f424671cd1..bceb97affb 100644
--- a/esphome/components/modbus_controller/output/modbus_output.h
+++ b/esphome/components/modbus_controller/output/modbus_output.h
@@ -25,7 +25,7 @@ class ModbusFloatOutput : public output::FloatOutput, public Component, public S
   void dump_config() override;
 
   void set_parent(ModbusController *parent) { this->parent_ = parent; }
-  void set_write_multiply(float factor) { multiply_by_ = factor; }
+  void set_write_multiply(float factor) { this->multiply_by_ = factor; }
   // Do nothing
   void parse_and_publish(const std::vector<uint8_t> &data) override{};
 
@@ -37,9 +37,9 @@ class ModbusFloatOutput : public output::FloatOutput, public Component, public S
   void write_state(float value) override;
   optional<write_transform_func_t> write_transform_func_{nullopt};
 
-  ModbusController *parent_;
+  ModbusController *parent_{nullptr};
   float multiply_by_{1.0};
-  bool use_write_multiple_;
+  bool use_write_multiple_{false};
 };
 
 class ModbusBinaryOutput : public output::BinaryOutput, public Component, public SensorItem {
@@ -68,8 +68,8 @@ class ModbusBinaryOutput : public output::BinaryOutput, public Component, public
   void write_state(bool state) override;
   optional<write_transform_func_t> write_transform_func_{nullopt};
 
-  ModbusController *parent_;
-  bool use_write_multiple_;
+  ModbusController *parent_{nullptr};
+  bool use_write_multiple_{false};
 };
 
 }  // namespace modbus_controller
diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp
index 33cef39a18..56b8c783ed 100644
--- a/esphome/components/modbus_controller/select/modbus_select.cpp
+++ b/esphome/components/modbus_controller/select/modbus_select.cpp
@@ -74,12 +74,13 @@ void ModbusSelect::control(const std::string &value) {
   const uint16_t write_address = this->start_address + this->offset / 2;
   ModbusCommandItem write_cmd;
   if ((this->register_count == 1) && (!this->use_write_multiple_)) {
-    write_cmd = ModbusCommandItem::create_write_single_command(parent_, write_address, data[0]);
+    write_cmd = ModbusCommandItem::create_write_single_command(this->parent_, write_address, data[0]);
   } else {
-    write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, write_address, this->register_count, data);
+    write_cmd =
+        ModbusCommandItem::create_write_multiple_command(this->parent_, write_address, this->register_count, data);
   }
 
-  parent_->queue_command(write_cmd);
+  this->parent_->queue_command(write_cmd);
 
   if (this->optimistic_)
     this->publish_state(value);
diff --git a/esphome/components/modbus_controller/select/modbus_select.h b/esphome/components/modbus_controller/select/modbus_select.h
index 1c046b11d0..55fb2107dd 100644
--- a/esphome/components/modbus_controller/select/modbus_select.h
+++ b/esphome/components/modbus_controller/select/modbus_select.h
@@ -42,12 +42,12 @@ class ModbusSelect : public Component, public select::Select, public SensorItem
   void control(const std::string &value) override;
 
  protected:
-  std::vector<int64_t> mapping_;
-  ModbusController *parent_;
+  std::vector<int64_t> mapping_{};
+  ModbusController *parent_{nullptr};
   bool use_write_multiple_{false};
   bool optimistic_{false};
-  optional<transform_func_t> transform_func_;
-  optional<write_transform_func_t> write_transform_func_;
+  optional<transform_func_t> transform_func_{nullopt};
+  optional<write_transform_func_t> write_transform_func_{nullopt};
 };
 
 }  // namespace modbus_controller
diff --git a/esphome/components/modbus_controller/switch/modbus_switch.cpp b/esphome/components/modbus_controller/switch/modbus_switch.cpp
index 3a679fbeb8..ec29eca7f8 100644
--- a/esphome/components/modbus_controller/switch/modbus_switch.cpp
+++ b/esphome/components/modbus_controller/switch/modbus_switch.cpp
@@ -80,18 +80,18 @@ void ModbusSwitch::write_state(bool state) {
       // offset for coil and discrete inputs is the coil/register number not bytes
       if (this->use_write_multiple_) {
         std::vector<bool> states{state};
-        cmd = ModbusCommandItem::create_write_multiple_coils(parent_, this->start_address + this->offset, states);
+        cmd = ModbusCommandItem::create_write_multiple_coils(this->parent_, this->start_address + this->offset, states);
       } else {
-        cmd = ModbusCommandItem::create_write_single_coil(parent_, this->start_address + this->offset, state);
+        cmd = ModbusCommandItem::create_write_single_coil(this->parent_, this->start_address + this->offset, state);
       }
     } else {
       // since offset is in bytes and a register is 16 bits we get the start by adding offset/2
       if (this->use_write_multiple_) {
         std::vector<uint16_t> bool_states(1, state ? (0xFFFF & this->bitmask) : 0);
-        cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset / 2, 1,
+        cmd = ModbusCommandItem::create_write_multiple_command(this->parent_, this->start_address + this->offset / 2, 1,
                                                                bool_states);
       } else {
-        cmd = ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset / 2,
+        cmd = ModbusCommandItem::create_write_single_command(this->parent_, this->start_address + this->offset / 2,
                                                              state ? 0xFFFF & this->bitmask : 0u);
       }
     }
diff --git a/esphome/components/modbus_controller/switch/modbus_switch.h b/esphome/components/modbus_controller/switch/modbus_switch.h
index bfe46f3ac8..fe4b7c1ad5 100644
--- a/esphome/components/modbus_controller/switch/modbus_switch.h
+++ b/esphome/components/modbus_controller/switch/modbus_switch.h
@@ -40,8 +40,8 @@ class ModbusSwitch : public Component, public switch_::Switch, public SensorItem
   void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
 
  protected:
-  ModbusController *parent_;
-  bool use_write_multiple_;
+  ModbusController *parent_{nullptr};
+  bool use_write_multiple_{false};
   optional<transform_func_t> publish_transform_func_{nullopt};
   optional<write_transform_func_t> write_transform_func_{nullopt};
 };

From fb96e3588d1f771f7430beec570da2d09a83e2f2 Mon Sep 17 00:00:00 2001
From: David Woodhouse <dwmw2@infradead.org>
Date: Mon, 2 Dec 2024 08:16:58 +0000
Subject: [PATCH 236/282] Add H-Bridge switch component (#7421)

Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
---
 CODEOWNERS                                    |  1 +
 esphome/components/hbridge/switch/__init__.py | 44 +++++++++
 .../hbridge/switch/hbridge_switch.cpp         | 95 +++++++++++++++++++
 .../hbridge/switch/hbridge_switch.h           | 50 ++++++++++
 tests/components/hbridge/common.yaml          | 39 ++++++++
 tests/components/hbridge/test.esp32-ard.yaml  | 46 +++------
 .../components/hbridge/test.esp32-c3-ard.yaml | 45 +++------
 .../components/hbridge/test.esp32-c3-idf.yaml | 44 +++------
 tests/components/hbridge/test.esp32-idf.yaml  | 45 +++------
 .../components/hbridge/test.esp8266-ard.yaml  | 45 +++------
 tests/components/hbridge/test.rp2040-ard.yaml | 45 +++------
 11 files changed, 313 insertions(+), 186 deletions(-)
 create mode 100644 esphome/components/hbridge/switch/__init__.py
 create mode 100644 esphome/components/hbridge/switch/hbridge_switch.cpp
 create mode 100644 esphome/components/hbridge/switch/hbridge_switch.h
 create mode 100644 tests/components/hbridge/common.yaml

diff --git a/CODEOWNERS b/CODEOWNERS
index fb6d11d1fb..74c205b302 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -179,6 +179,7 @@ esphome/components/haier/text_sensor/* @paveldn
 esphome/components/havells_solar/* @sourabhjaiswal
 esphome/components/hbridge/fan/* @WeekendWarrior
 esphome/components/hbridge/light/* @DotNetDann
+esphome/components/hbridge/switch/* @dwmw2
 esphome/components/he60r/* @clydebarrow
 esphome/components/heatpumpir/* @rob-deutsch
 esphome/components/hitachi_ac424/* @sourabhjaiswal
diff --git a/esphome/components/hbridge/switch/__init__.py b/esphome/components/hbridge/switch/__init__.py
new file mode 100644
index 0000000000..e26bd6b1d8
--- /dev/null
+++ b/esphome/components/hbridge/switch/__init__.py
@@ -0,0 +1,44 @@
+from esphome import pins
+import esphome.codegen as cg
+from esphome.components import switch
+import esphome.config_validation as cv
+from esphome.const import CONF_OPTIMISTIC, CONF_PULSE_LENGTH, CONF_WAIT_TIME
+
+from .. import hbridge_ns
+
+HBridgeSwitch = hbridge_ns.class_("HBridgeSwitch", switch.Switch, cg.Component)
+
+CODEOWNERS = ["@dwmw2"]
+
+CONF_OFF_PIN = "off_pin"
+CONF_ON_PIN = "on_pin"
+
+CONFIG_SCHEMA = (
+    switch.switch_schema(HBridgeSwitch)
+    .extend(
+        {
+            cv.Required(CONF_ON_PIN): pins.gpio_output_pin_schema,
+            cv.Required(CONF_OFF_PIN): pins.gpio_output_pin_schema,
+            cv.Optional(
+                CONF_PULSE_LENGTH, default="100ms"
+            ): cv.positive_time_period_milliseconds,
+            cv.Optional(CONF_WAIT_TIME): cv.positive_time_period_milliseconds,
+            cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
+        }
+    )
+    .extend(cv.COMPONENT_SCHEMA)
+)
+
+
+async def to_code(config):
+    var = await switch.new_switch(config)
+    await cg.register_component(var, config)
+
+    on_pin = await cg.gpio_pin_expression(config[CONF_ON_PIN])
+    cg.add(var.set_on_pin(on_pin))
+    off_pin = await cg.gpio_pin_expression(config[CONF_OFF_PIN])
+    cg.add(var.set_off_pin(off_pin))
+    cg.add(var.set_pulse_length(config[CONF_PULSE_LENGTH]))
+    cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
+    if wait_time := config.get(CONF_WAIT_TIME):
+        cg.add(var.set_wait_time(wait_time))
diff --git a/esphome/components/hbridge/switch/hbridge_switch.cpp b/esphome/components/hbridge/switch/hbridge_switch.cpp
new file mode 100644
index 0000000000..12d1c01bca
--- /dev/null
+++ b/esphome/components/hbridge/switch/hbridge_switch.cpp
@@ -0,0 +1,95 @@
+#include "hbridge_switch.h"
+#include "esphome/core/log.h"
+
+#include <cinttypes>
+
+namespace esphome {
+namespace hbridge {
+
+static const char *const TAG = "switch.hbridge";
+
+float HBridgeSwitch::get_setup_priority() const { return setup_priority::HARDWARE; }
+void HBridgeSwitch::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up H-Bridge Switch '%s'...", this->name_.c_str());
+
+  optional<bool> initial_state = this->get_initial_state_with_restore_mode().value_or(false);
+
+  // Like GPIOSwitch does, set the pin state both before and after pin setup()
+  this->on_pin_->digital_write(false);
+  this->on_pin_->setup();
+  this->on_pin_->digital_write(false);
+
+  this->off_pin_->digital_write(false);
+  this->off_pin_->setup();
+  this->off_pin_->digital_write(false);
+
+  if (initial_state.has_value())
+    this->write_state(initial_state);
+}
+
+void HBridgeSwitch::dump_config() {
+  LOG_SWITCH("", "H-Bridge Switch", this);
+  LOG_PIN("  On Pin: ", this->on_pin_);
+  LOG_PIN("  Off Pin: ", this->off_pin_);
+  ESP_LOGCONFIG(TAG, "  Pulse length: %" PRId32 " ms", this->pulse_length_);
+  if (this->wait_time_)
+    ESP_LOGCONFIG(TAG, "  Wait time %" PRId32 " ms", this->wait_time_);
+}
+
+void HBridgeSwitch::write_state(bool state) {
+  this->desired_state_ = state;
+  if (!this->timer_running_)
+    this->timer_fn_();
+}
+
+void HBridgeSwitch::timer_fn_() {
+  uint32_t next_timeout = 0;
+
+  while ((uint8_t) this->desired_state_ != this->relay_state_) {
+    switch (this->relay_state_) {
+      case RELAY_STATE_ON:
+      case RELAY_STATE_OFF:
+      case RELAY_STATE_UNKNOWN:
+        if (this->desired_state_) {
+          this->on_pin_->digital_write(true);
+          this->relay_state_ = RELAY_STATE_SWITCHING_ON;
+        } else {
+          this->off_pin_->digital_write(true);
+          this->relay_state_ = RELAY_STATE_SWITCHING_OFF;
+        }
+        next_timeout = this->pulse_length_;
+        if (!this->optimistic_)
+          this->publish_state(this->desired_state_);
+        break;
+
+      case RELAY_STATE_SWITCHING_ON:
+        this->on_pin_->digital_write(false);
+        this->relay_state_ = RELAY_STATE_ON;
+        if (this->optimistic_)
+          this->publish_state(true);
+        next_timeout = this->wait_time_;
+        break;
+
+      case RELAY_STATE_SWITCHING_OFF:
+        this->off_pin_->digital_write(false);
+        this->relay_state_ = RELAY_STATE_OFF;
+        if (this->optimistic_)
+          this->publish_state(false);
+        next_timeout = this->wait_time_;
+        break;
+    }
+
+    if (next_timeout) {
+      this->timer_running_ = true;
+      this->set_timeout(next_timeout, [this]() { this->timer_fn_(); });
+      return;
+    }
+
+    // In the case where ON/OFF state has been reached but we need to
+    // immediately change back again to reach desired_state_, we loop.
+  }
+  this->timer_running_ = false;
+}
+
+}  // namespace hbridge
+}  // namespace esphome
diff --git a/esphome/components/hbridge/switch/hbridge_switch.h b/esphome/components/hbridge/switch/hbridge_switch.h
new file mode 100644
index 0000000000..ce00c6baa2
--- /dev/null
+++ b/esphome/components/hbridge/switch/hbridge_switch.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/hal.h"
+#include "esphome/components/switch/switch.h"
+
+#include <vector>
+
+namespace esphome {
+namespace hbridge {
+
+enum RelayState : uint8_t {
+  RELAY_STATE_OFF = 0,
+  RELAY_STATE_ON = 1,
+  RELAY_STATE_SWITCHING_ON = 2,
+  RELAY_STATE_SWITCHING_OFF = 3,
+  RELAY_STATE_UNKNOWN = 4,
+};
+
+class HBridgeSwitch : public switch_::Switch, public Component {
+ public:
+  void set_on_pin(GPIOPin *pin) { this->on_pin_ = pin; }
+  void set_off_pin(GPIOPin *pin) { this->off_pin_ = pin; }
+  void set_pulse_length(uint32_t pulse_length) { this->pulse_length_ = pulse_length; }
+  void set_wait_time(uint32_t wait_time) { this->wait_time_ = wait_time; }
+  void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
+
+  // ========== INTERNAL METHODS ==========
+  // (In most use cases you won't need these)
+  float get_setup_priority() const override;
+
+  void setup() override;
+  void dump_config() override;
+
+ protected:
+  void write_state(bool state) override;
+  void timer_fn_();
+
+  bool timer_running_{false};
+  bool desired_state_{false};
+  RelayState relay_state_{RELAY_STATE_UNKNOWN};
+  GPIOPin *on_pin_{nullptr};
+  GPIOPin *off_pin_{nullptr};
+  uint32_t pulse_length_{0};
+  uint32_t wait_time_{0};
+  bool optimistic_{false};
+};
+
+}  // namespace hbridge
+}  // namespace esphome
diff --git a/tests/components/hbridge/common.yaml b/tests/components/hbridge/common.yaml
new file mode 100644
index 0000000000..0504cdea03
--- /dev/null
+++ b/tests/components/hbridge/common.yaml
@@ -0,0 +1,39 @@
+output:
+  - platform: ${pwm_platform}
+    pin: ${output1_pin}
+    id: gpio_output1
+  - platform: ${pwm_platform}
+    pin: ${output2_pin}
+    id: gpio_output2
+  - platform: ${pwm_platform}
+    pin: ${output3_pin}
+    id: gpio_output3
+  - platform: ${pwm_platform}
+    pin: ${output4_pin}
+    id: gpio_output4
+
+light:
+  - platform: hbridge
+    name: Icicle Lights
+    pin_a: gpio_output3
+    pin_b: gpio_output4
+
+fan:
+  - platform: hbridge
+    id: fan_hbridge
+    speed_count: 4
+    name: H-bridge Fan with Presets
+    pin_a: gpio_output1
+    pin_b: gpio_output2
+    preset_modes:
+      - Preset 1
+      - Preset 2
+    on_preset_set:
+      then:
+        - logger.log: Preset mode was changed!
+
+switch:
+  - platform: hbridge
+    id: switch_hbridge
+    on_pin: ${hbridge_on_pin}
+    off_pin: ${hbridge_off_pin}
diff --git a/tests/components/hbridge/test.esp32-ard.yaml b/tests/components/hbridge/test.esp32-ard.yaml
index 6a80aaaf3b..e50d537749 100644
--- a/tests/components/hbridge/test.esp32-ard.yaml
+++ b/tests/components/hbridge/test.esp32-ard.yaml
@@ -1,33 +1,17 @@
-output:
-  - platform: ledc
-    pin: 14
-    id: gpio_output1
-  - platform: ledc
-    pin: 15
-    id: gpio_output2
-  - platform: ledc
-    pin: 12
-    id: gpio_output3
-  - platform: ledc
-    pin: 13
-    id: gpio_output4
+substitutions:
+  pwm_platform: ledc
+  output1_pin: "14"
+  output2_pin: "15"
+  output3_pin: "12"
+  output4_pin: "13"
+  hbridge_on_pin: "4"
+  hbridge_off_pin: "5"
 
-light:
-  - platform: hbridge
-    name: Icicle Lights
-    pin_a: gpio_output3
-    pin_b: gpio_output4
+packages:
+  common: !include common.yaml
 
-fan:
-  - platform: hbridge
-    id: fan_hbridge
-    speed_count: 4
-    name: H-bridge Fan with Presets
-    pin_a: gpio_output1
-    pin_b: gpio_output2
-    preset_modes:
-      - Preset 1
-      - Preset 2
-    on_preset_set:
-      then:
-        - logger.log: Preset mode was changed!
+switch:
+  - id: !extend switch_hbridge
+    pulse_length: 60ms
+    wait_time: 10ms
+    optimistic: false
diff --git a/tests/components/hbridge/test.esp32-c3-ard.yaml b/tests/components/hbridge/test.esp32-c3-ard.yaml
index 70cfd6ab6f..b9e8738442 100644
--- a/tests/components/hbridge/test.esp32-c3-ard.yaml
+++ b/tests/components/hbridge/test.esp32-c3-ard.yaml
@@ -1,33 +1,16 @@
-output:
-  - platform: ledc
-    pin: 4
-    id: gpio_output1
-  - platform: ledc
-    pin: 5
-    id: gpio_output2
-  - platform: ledc
-    pin: 6
-    id: gpio_output3
-  - platform: ledc
-    pin: 7
-    id: gpio_output4
+substitutions:
+  pwm_platform: "ledc"
+  output1_pin: "4"
+  output2_pin: "5"
+  output3_pin: "6"
+  output4_pin: "7"
+  hbridge_on_pin: "2"
+  hbridge_off_pin: "3"
 
-light:
-  - platform: hbridge
-    name: Icicle Lights
-    pin_a: gpio_output3
-    pin_b: gpio_output4
+packages:
+  common: !include common.yaml
 
-fan:
-  - platform: hbridge
-    id: fan_hbridge
-    speed_count: 4
-    name: H-bridge Fan with Presets
-    pin_a: gpio_output1
-    pin_b: gpio_output2
-    preset_modes:
-      - Preset 1
-      - Preset 2
-    on_preset_set:
-      then:
-        - logger.log: Preset mode was changed!
+switch:
+  - id: !extend switch_hbridge
+    wait_time: 10ms
+    optimistic: true
diff --git a/tests/components/hbridge/test.esp32-c3-idf.yaml b/tests/components/hbridge/test.esp32-c3-idf.yaml
index 70cfd6ab6f..c73f08b6de 100644
--- a/tests/components/hbridge/test.esp32-c3-idf.yaml
+++ b/tests/components/hbridge/test.esp32-c3-idf.yaml
@@ -1,33 +1,15 @@
-output:
-  - platform: ledc
-    pin: 4
-    id: gpio_output1
-  - platform: ledc
-    pin: 5
-    id: gpio_output2
-  - platform: ledc
-    pin: 6
-    id: gpio_output3
-  - platform: ledc
-    pin: 7
-    id: gpio_output4
+substitutions:
+  pwm_platform: "ledc"
+  output1_pin: "4"
+  output2_pin: "5"
+  output3_pin: "6"
+  output4_pin: "7"
+  hbridge_on_pin: "2"
+  hbridge_off_pin: "3"
 
-light:
-  - platform: hbridge
-    name: Icicle Lights
-    pin_a: gpio_output3
-    pin_b: gpio_output4
+packages:
+  common: !include common.yaml
 
-fan:
-  - platform: hbridge
-    id: fan_hbridge
-    speed_count: 4
-    name: H-bridge Fan with Presets
-    pin_a: gpio_output1
-    pin_b: gpio_output2
-    preset_modes:
-      - Preset 1
-      - Preset 2
-    on_preset_set:
-      then:
-        - logger.log: Preset mode was changed!
+switch:
+  - id: !extend switch_hbridge
+    pulse_length: 60ms
diff --git a/tests/components/hbridge/test.esp32-idf.yaml b/tests/components/hbridge/test.esp32-idf.yaml
index 6a80aaaf3b..dbbfa738c7 100644
--- a/tests/components/hbridge/test.esp32-idf.yaml
+++ b/tests/components/hbridge/test.esp32-idf.yaml
@@ -1,33 +1,16 @@
-output:
-  - platform: ledc
-    pin: 14
-    id: gpio_output1
-  - platform: ledc
-    pin: 15
-    id: gpio_output2
-  - platform: ledc
-    pin: 12
-    id: gpio_output3
-  - platform: ledc
-    pin: 13
-    id: gpio_output4
+substitutions:
+  pwm_platform: "ledc"
+  output1_pin: "14"
+  output2_pin: "15"
+  output3_pin: "12"
+  output4_pin: "13"
+  hbridge_on_pin: "4"
+  hbridge_off_pin: "5"
 
-light:
-  - platform: hbridge
-    name: Icicle Lights
-    pin_a: gpio_output3
-    pin_b: gpio_output4
+packages:
+  common: !include common.yaml
 
-fan:
-  - platform: hbridge
-    id: fan_hbridge
-    speed_count: 4
-    name: H-bridge Fan with Presets
-    pin_a: gpio_output1
-    pin_b: gpio_output2
-    preset_modes:
-      - Preset 1
-      - Preset 2
-    on_preset_set:
-      then:
-        - logger.log: Preset mode was changed!
+switch:
+  - id: !extend switch_hbridge
+    pulse_length: 60ms
+    wait_time: 10ms
diff --git a/tests/components/hbridge/test.esp8266-ard.yaml b/tests/components/hbridge/test.esp8266-ard.yaml
index 4f8915879d..f560da5d38 100644
--- a/tests/components/hbridge/test.esp8266-ard.yaml
+++ b/tests/components/hbridge/test.esp8266-ard.yaml
@@ -1,33 +1,16 @@
-output:
-  - platform: esp8266_pwm
-    pin: 4
-    id: gpio_output1
-  - platform: esp8266_pwm
-    pin: 5
-    id: gpio_output2
-  - platform: esp8266_pwm
-    pin: 12
-    id: gpio_output3
-  - platform: esp8266_pwm
-    pin: 13
-    id: gpio_output4
+substitutions:
+  pwm_platform: "esp8266_pwm"
+  output1_pin: "4"
+  output2_pin: "5"
+  output3_pin: "12"
+  output4_pin: "13"
+  hbridge_on_pin: "14"
+  hbridge_off_pin: "15"
 
-light:
-  - platform: hbridge
-    name: Icicle Lights
-    pin_a: gpio_output3
-    pin_b: gpio_output4
+packages:
+  common: !include common.yaml
 
-fan:
-  - platform: hbridge
-    id: fan_hbridge
-    speed_count: 4
-    name: H-bridge Fan with Presets
-    pin_a: gpio_output1
-    pin_b: gpio_output2
-    preset_modes:
-      - Preset 1
-      - Preset 2
-    on_preset_set:
-      then:
-        - logger.log: Preset mode was changed!
+switch:
+  - id: !extend switch_hbridge
+    pulse_length: 60ms
+    wait_time: 10ms
diff --git a/tests/components/hbridge/test.rp2040-ard.yaml b/tests/components/hbridge/test.rp2040-ard.yaml
index e21b55091d..aa6e290cab 100644
--- a/tests/components/hbridge/test.rp2040-ard.yaml
+++ b/tests/components/hbridge/test.rp2040-ard.yaml
@@ -1,33 +1,16 @@
-output:
-  - platform: rp2040_pwm
-    pin: 4
-    id: gpio_output1
-  - platform: rp2040_pwm
-    pin: 5
-    id: gpio_output2
-  - platform: rp2040_pwm
-    pin: 6
-    id: gpio_output3
-  - platform: rp2040_pwm
-    pin: 7
-    id: gpio_output4
+substitutions:
+  pwm_platform: "rp2040_pwm"
+  output1_pin: "4"
+  output2_pin: "5"
+  output3_pin: "6"
+  output4_pin: "7"
+  hbridge_on_pin: "2"
+  hbridge_off_pin: "3"
 
-light:
-  - platform: hbridge
-    name: Icicle Lights
-    pin_a: gpio_output3
-    pin_b: gpio_output4
+packages:
+  common: !include common.yaml
 
-fan:
-  - platform: hbridge
-    id: fan_hbridge
-    speed_count: 4
-    name: H-bridge Fan with Presets
-    pin_a: gpio_output1
-    pin_b: gpio_output2
-    preset_modes:
-      - Preset 1
-      - Preset 2
-    on_preset_set:
-      then:
-        - logger.log: Preset mode was changed!
+switch:
+  - id: !extend switch_hbridge
+    wait_time: 10ms
+    optimistic: true

From b79a3d672782fee909f929da8cb0a516ee52132d Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 2 Dec 2024 11:42:44 -0600
Subject: [PATCH 237/282] [CI] Bump GHA runners to ``ubuntu-24.04`` (#7905)

---
 .github/workflows/ci.yml | 26 +++++++++++++-------------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f5af3ec9e9..82e823adde 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,7 +30,7 @@ concurrency:
 jobs:
   common:
     name: Create common environment
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     outputs:
       cache-key: ${{ steps.cache-key.outputs.key }}
     steps:
@@ -62,7 +62,7 @@ jobs:
 
   black:
     name: Check black
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     needs:
       - common
     steps:
@@ -83,7 +83,7 @@ jobs:
 
   flake8:
     name: Check flake8
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     needs:
       - common
     steps:
@@ -104,7 +104,7 @@ jobs:
 
   pylint:
     name: Check pylint
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     needs:
       - common
     steps:
@@ -125,7 +125,7 @@ jobs:
 
   pyupgrade:
     name: Check pyupgrade
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     needs:
       - common
     steps:
@@ -146,7 +146,7 @@ jobs:
 
   ci-custom:
     name: Run script/ci-custom
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     needs:
       - common
     steps:
@@ -225,7 +225,7 @@ jobs:
 
   clang-format:
     name: Check clang-format
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     needs:
       - common
     steps:
@@ -251,7 +251,7 @@ jobs:
 
   clang-tidy:
     name: ${{ matrix.name }}
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     needs:
       - common
       - black
@@ -345,7 +345,7 @@ jobs:
         if: always()
 
   list-components:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     needs:
       - common
     if: github.event_name == 'pull_request'
@@ -387,7 +387,7 @@ jobs:
 
   test-build-components:
     name: Component test ${{ matrix.file }}
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     needs:
       - common
       - list-components
@@ -421,7 +421,7 @@ jobs:
 
   test-build-components-splitter:
     name: Split components for testing into 20 groups maximum
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     needs:
       - common
       - list-components
@@ -439,7 +439,7 @@ jobs:
 
   test-build-components-split:
     name: Test split components
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     needs:
       - common
       - list-components
@@ -483,7 +483,7 @@ jobs:
 
   ci-status:
     name: CI Status
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     needs:
       - common
       - black

From e08a9cc3a33ab431f6eeef869bdc49c46f17dcf1 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 3 Dec 2024 09:27:51 +1100
Subject: [PATCH 238/282] [font et. al.] Remove explicit check for pillow
 installed. (#7891)

---
 esphome/components/animation/__init__.py |  3 +--
 esphome/components/font/__init__.py      | 24 ++----------------------
 esphome/components/ili9xxx/display.py    |  3 +--
 esphome/components/image/__init__.py     |  3 +--
 4 files changed, 5 insertions(+), 28 deletions(-)

diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py
index 5a308855de..21a82649f0 100644
--- a/esphome/components/animation/__init__.py
+++ b/esphome/components/animation/__init__.py
@@ -2,7 +2,6 @@ import logging
 
 from esphome import automation, core
 import esphome.codegen as cg
-from esphome.components import font
 import esphome.components.image as espImage
 from esphome.components.image import (
     CONF_USE_TRANSPARENCY,
@@ -131,7 +130,7 @@ ANIMATION_SCHEMA = cv.Schema(
     )
 )
 
-CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA)
+CONFIG_SCHEMA = ANIMATION_SCHEMA
 
 NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
     {
diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py
index 6fd2d7c310..1b4fe4bff0 100644
--- a/esphome/components/font/__init__.py
+++ b/esphome/components/font/__init__.py
@@ -1,4 +1,3 @@
-from collections.abc import Iterable
 import functools
 import hashlib
 import logging
@@ -8,7 +7,6 @@ import re
 
 import freetype
 import glyphsets
-from packaging import version
 import requests
 
 from esphome import core, external_files
@@ -88,7 +86,7 @@ def flatten(lists) -> list:
     return list(chain.from_iterable(lists))
 
 
-def check_missing_glyphs(file, codepoints: Iterable, warning: bool = False):
+def check_missing_glyphs(file, codepoints, warning: bool = False):
     """
     Check that the given font file actually contains the requested glyphs
     :param file: A Truetype font file
@@ -177,24 +175,6 @@ def validate_glyphs(config):
     return config
 
 
-def validate_pillow_installed(value):
-    try:
-        import PIL
-    except ImportError as err:
-        raise cv.Invalid(
-            "Please install the pillow python package to use this feature. "
-            '(pip install "pillow==10.4.0")'
-        ) from err
-
-    if version.parse(PIL.__version__) != version.parse("10.4.0"):
-        raise cv.Invalid(
-            "Please update your pillow installation to 10.4.0. "
-            '(pip install "pillow==10.4.0")'
-        )
-
-    return value
-
-
 FONT_EXTENSIONS = (".ttf", ".woff", ".otf")
 
 
@@ -421,7 +401,7 @@ FONT_SCHEMA = cv.Schema(
     },
 )
 
-CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, validate_glyphs)
+CONFIG_SCHEMA = cv.All(FONT_SCHEMA, validate_glyphs)
 
 
 # PIL doesn't provide a consistent interface for both TrueType and bitmap
diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py
index 739ad07843..3c9dd2dab9 100644
--- a/esphome/components/ili9xxx/display.py
+++ b/esphome/components/ili9xxx/display.py
@@ -1,6 +1,6 @@
 from esphome import core, pins
 import esphome.codegen as cg
-from esphome.components import display, font, spi
+from esphome.components import display, spi
 from esphome.components.display import validate_rotation
 import esphome.config_validation as cv
 from esphome.const import (
@@ -147,7 +147,6 @@ def _validate(config):
 
 
 CONFIG_SCHEMA = cv.All(
-    font.validate_pillow_installed,
     display.FULL_DISPLAY_SCHEMA.extend(
         {
             cv.GenerateID(): cv.declare_id(ILI9XXXDisplay),
diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py
index 8742540067..4669a3418a 100644
--- a/esphome/components/image/__init__.py
+++ b/esphome/components/image/__init__.py
@@ -10,7 +10,6 @@ import puremagic
 
 from esphome import core, external_files
 import esphome.codegen as cg
-from esphome.components import font
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_DITHER,
@@ -233,7 +232,7 @@ IMAGE_SCHEMA = cv.Schema(
     )
 )
 
-CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA)
+CONFIG_SCHEMA = IMAGE_SCHEMA
 
 
 def load_svg_image(file: bytes, resize: tuple[int, int]):

From 9c8976be1328b4c5c1d019aa835f53f129262324 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Mon, 2 Dec 2024 16:29:45 -0600
Subject: [PATCH 239/282] [CI] Update clang-tidy to 18.1.3 (#7822)

---
 .clang-tidy              | 29 +++++++++++++++++++++++++++--
 .github/workflows/ci.yml |  5 -----
 docker/Dockerfile        |  8 ++++++--
 requirements_dev.txt     |  2 +-
 script/clang-tidy        |  8 +++-----
 5 files changed, 37 insertions(+), 15 deletions(-)

diff --git a/.clang-tidy b/.clang-tidy
index 994416b2f1..8dba033e2d 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -7,28 +7,38 @@ Checks: >-
   -boost-*,
   -bugprone-easily-swappable-parameters,
   -bugprone-implicit-widening-of-multiplication-result,
+  -bugprone-multi-level-implicit-pointer-conversion,
   -bugprone-narrowing-conversions,
   -bugprone-signed-char-misuse,
+  -bugprone-switch-missing-default-case,
   -cert-dcl50-cpp,
   -cert-err33-c,
   -cert-err58-cpp,
   -cert-oop57-cpp,
   -cert-str34-c,
+  -clang-analyzer-optin.core.EnumCastOutOfRange,
   -clang-analyzer-optin.cplusplus.UninitializedObject,
   -clang-analyzer-osx.*,
   -clang-diagnostic-delete-abstract-non-virtual-dtor,
   -clang-diagnostic-delete-non-abstract-non-virtual-dtor,
   -clang-diagnostic-ignored-optimization-argument,
+  -clang-diagnostic-missing-field-initializers,
   -clang-diagnostic-shadow-field,
   -clang-diagnostic-unused-const-variable,
   -clang-diagnostic-unused-parameter,
+  -clang-diagnostic-vla-cxx-extension,
   -concurrency-*,
   -cppcoreguidelines-avoid-c-arrays,
+  -cppcoreguidelines-avoid-const-or-ref-data-members,
+  -cppcoreguidelines-avoid-do-while,
   -cppcoreguidelines-avoid-magic-numbers,
   -cppcoreguidelines-init-variables,
+  -cppcoreguidelines-macro-to-enum,
   -cppcoreguidelines-macro-usage,
+  -cppcoreguidelines-missing-std-forward,
   -cppcoreguidelines-narrowing-conversions,
   -cppcoreguidelines-non-private-member-variables-in-classes,
+  -cppcoreguidelines-owning-memory,
   -cppcoreguidelines-prefer-member-initializer,
   -cppcoreguidelines-pro-bounds-array-to-pointer-decay,
   -cppcoreguidelines-pro-bounds-constant-array-index,
@@ -40,7 +50,9 @@ Checks: >-
   -cppcoreguidelines-pro-type-static-cast-downcast,
   -cppcoreguidelines-pro-type-union-access,
   -cppcoreguidelines-pro-type-vararg,
+  -cppcoreguidelines-rvalue-reference-param-not-moved,
   -cppcoreguidelines-special-member-functions,
+  -cppcoreguidelines-use-default-member-init,
   -cppcoreguidelines-virtual-class-destructor,
   -fuchsia-multiple-inheritance,
   -fuchsia-overloaded-operator,
@@ -60,21 +72,32 @@ Checks: >-
   -llvm-include-order,
   -llvm-qualified-auto,
   -llvmlibc-*,
-  -misc-non-private-member-variables-in-classes,
+  -misc-const-correctness,
+  -misc-include-cleaner,
   -misc-no-recursion,
+  -misc-non-private-member-variables-in-classes,
   -misc-unused-parameters,
+  -misc-use-anonymous-namespace,
   -modernize-avoid-bind,
   -modernize-avoid-c-arrays,
   -modernize-concat-nested-namespaces,
+  -modernize-macro-to-enum,
   -modernize-return-braced-init-list,
+  -modernize-type-traits,
   -modernize-use-auto,
+  -modernize-use-constraints,
   -modernize-use-default-member-init,
   -modernize-use-equals-default,
   -modernize-use-nodiscard,
   -modernize-use-nullptr,
+  -modernize-use-nodiscard,
+  -modernize-use-nullptr,
   -modernize-use-trailing-return-type,
   -mpi-*,
   -objc-*,
+  -performance-enum-size,
+  -readability-avoid-nested-conditional-operator,
+  -readability-container-contains,
   -readability-container-data-pointer,
   -readability-convert-member-functions-to-static,
   -readability-else-after-return,
@@ -84,11 +107,13 @@ Checks: >-
   -readability-magic-numbers,
   -readability-make-member-function-const,
   -readability-named-parameter,
+  -readability-redundant-casting,
+  -readability-redundant-inline-specifier,
+  -readability-redundant-member-init,
   -readability-redundant-string-init,
   -readability-uppercase-literal-suffix,
   -readability-use-anyofallof,
 WarningsAsErrors: '*'
-AnalyzeTemporaryDtors: false
 FormatStyle:     google
 CheckOptions:
   - key:             google-readability-function-size.StatementThreshold
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 82e823adde..e4d3934c59 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -314,11 +314,6 @@ jobs:
           path: ~/.platformio
           key: platformio-${{ matrix.pio_cache_key }}
 
-      - name: Install clang-tidy
-        run: |
-          sudo apt-get update
-          sudo apt-get install clang-tidy-14
-
       - name: Register problem matchers
         run: |
           echo "::add-matcher::.github/workflows/matchers/gcc.json"
diff --git a/docker/Dockerfile b/docker/Dockerfile
index c2902a9dd1..c53856d0e8 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -209,11 +209,12 @@ ENV \
   PLATFORMIO_CORE_DIR=/esphome/.temp/platformio
 
 RUN \
-    apt-get update \
+    curl -L https://apt.llvm.org/llvm-snapshot.gpg.key -o /etc/apt/trusted.gpg.d/apt.llvm.org.asc \
+    && echo "deb http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-18 main" > /etc/apt/sources.list.d/llvm.sources.list \
+    && apt-get update \
     # Use pinned versions so that we get updates with build caching
     && apt-get install -y --no-install-recommends \
         clang-format-13=1:13.0.1-11+b2 \
-        clang-tidy-14=1:14.0.6-12 \
         patch=2.7.6-7 \
         software-properties-common=0.99.30-4.1~deb12u1 \
         nano=7.2-1+deb12u1 \
@@ -227,6 +228,9 @@ RUN \
 COPY requirements_test.txt /
 RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
         export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
+  else \
+    # move this up into RUN above after armv7 is retired
+    apt-get install -y --no-install-recommends clang-tidy-18=1:18.1.8~++20240731024826+3b5b5c1ec4a3-1~exp1~20240731144843.145 ; \
   fi; \
   pip3 install \
   --break-system-packages --no-cache-dir -r /requirements_test.txt
diff --git a/requirements_dev.txt b/requirements_dev.txt
index eb749a861d..2ef8330215 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -1,4 +1,4 @@
 # Useful stuff when working in a development environment
 clang-format==13.0.1  # also change in .pre-commit-config.yaml and Dockerfile when updating
-clang-tidy==14.0.6  # When updating clang-tidy, also update Dockerfile
+clang-tidy==18.1.3  # When updating clang-tidy, also update Dockerfile
 yamllint==1.35.1  # also change in .pre-commit-config.yaml when updating
diff --git a/script/clang-tidy b/script/clang-tidy
index 319fab70a2..5c19f81043 100755
--- a/script/clang-tidy
+++ b/script/clang-tidy
@@ -63,8 +63,6 @@ def clang_options(idedata):
             "-Ddeprecated(x)=",
             # allow to condition code on the presence of clang-tidy
             "-DCLANG_TIDY",
-            # (esp-idf) Disable this header because they use asm with registers clang-tidy doesn't know
-            "-D__XTENSA_API_H__",
             # (esp-idf) Fix __once_callable in some libstdc++ headers
             "-D_GLIBCXX_HAVE_TLS",
         ]
@@ -238,7 +236,7 @@ def main():
 
     failed_files = []
     try:
-        executable = get_binary("clang-tidy", 14)
+        executable = get_binary("clang-tidy", 18)
         task_queue = queue.Queue(args.jobs)
         lock = threading.Lock()
         for _ in range(args.jobs):
@@ -283,12 +281,12 @@ def main():
         print("Applying fixes ...")
         try:
             try:
-                subprocess.call(["clang-apply-replacements-14", tmpdir])
+                subprocess.call(["clang-apply-replacements-18", tmpdir])
             except FileNotFoundError:
                 subprocess.call(["clang-apply-replacements", tmpdir])
         except FileNotFoundError:
             print(
-                "Error please install clang-apply-replacements-14 or clang-apply-replacements.\n",
+                "Error please install clang-apply-replacements-18 or clang-apply-replacements.\n",
                 file=sys.stderr,
             )
         except:

From 584dbf2668f888c774be7ad6ce4172d827965e9b Mon Sep 17 00:00:00 2001
From: kbullet <kbullet@users.noreply.github.com>
Date: Tue, 3 Dec 2024 06:50:05 +0700
Subject: [PATCH 240/282] MQTT sensors handling of publishing NaN values
 (#7768)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/mqtt/__init__.py     | 4 ++++
 esphome/components/mqtt/mqtt_client.cpp | 4 ++++
 esphome/components/mqtt/mqtt_client.h   | 6 ++++++
 esphome/components/mqtt/mqtt_sensor.cpp | 2 ++
 esphome/const.py                        | 1 +
 tests/components/mqtt/common.yaml       | 1 +
 6 files changed, 18 insertions(+)

diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py
index 86d163e61d..2b0d941220 100644
--- a/esphome/components/mqtt/__init__.py
+++ b/esphome/components/mqtt/__init__.py
@@ -49,6 +49,7 @@ from esphome.const import (
     CONF_USE_ABBREVIATIONS,
     CONF_USERNAME,
     CONF_WILL_MESSAGE,
+    CONF_PUBLISH_NAN_AS_NONE,
     PLATFORM_BK72XX,
     PLATFORM_ESP32,
     PLATFORM_ESP8266,
@@ -296,6 +297,7 @@ CONFIG_SCHEMA = cv.All(
                     cv.Optional(CONF_QOS, default=0): cv.mqtt_qos,
                 }
             ),
+            cv.Optional(CONF_PUBLISH_NAN_AS_NONE, default=False): cv.boolean,
         }
     ),
     validate_config,
@@ -449,6 +451,8 @@ async def to_code(config):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
         await automation.build_automation(trigger, [], conf)
 
+    cg.add(var.set_publish_nan_as_none(config[CONF_PUBLISH_NAN_AS_NONE]))
+
 
 MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema(
     {
diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp
index 106192c0e3..c7ace505a8 100644
--- a/esphome/components/mqtt/mqtt_client.cpp
+++ b/esphome/components/mqtt/mqtt_client.cpp
@@ -608,6 +608,10 @@ void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this
 const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; }
 void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix) { this->topic_prefix_ = topic_prefix; }
 const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; }
+void MQTTClientComponent::set_publish_nan_as_none(bool publish_nan_as_none) {
+  this->publish_nan_as_none_ = publish_nan_as_none;
+}
+bool MQTTClientComponent::is_publish_nan_as_none() const { return this->publish_nan_as_none_; }
 void MQTTClientComponent::disable_birth_message() {
   this->birth_message_.topic = "";
   this->recalculate_availability_();
diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h
index 7ae3a6c5e8..34eac29464 100644
--- a/esphome/components/mqtt/mqtt_client.h
+++ b/esphome/components/mqtt/mqtt_client.h
@@ -263,6 +263,10 @@ class MQTTClientComponent : public Component {
   void set_on_connect(mqtt_on_connect_callback_t &&callback);
   void set_on_disconnect(mqtt_on_disconnect_callback_t &&callback);
 
+  // Publish None state instead of NaN for Home Assistant
+  void set_publish_nan_as_none(bool publish_nan_as_none);
+  bool is_publish_nan_as_none() const;
+
  protected:
   void send_device_info_();
 
@@ -328,6 +332,8 @@ class MQTTClientComponent : public Component {
   uint32_t connect_begin_;
   uint32_t last_connected_{0};
   optional<MQTTClientDisconnectReason> disconnect_reason_{};
+
+  bool publish_nan_as_none_{false};
 };
 
 extern MQTTClientComponent *global_mqtt_client;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp
index fff75a3c00..2cbc291ccf 100644
--- a/esphome/components/mqtt/mqtt_sensor.cpp
+++ b/esphome/components/mqtt/mqtt_sensor.cpp
@@ -69,6 +69,8 @@ bool MQTTSensorComponent::send_initial_state() {
   }
 }
 bool MQTTSensorComponent::publish_state(float value) {
+  if (mqtt::global_mqtt_client->is_publish_nan_as_none() && std::isnan(value))
+    return this->publish(this->get_state_topic_(), "None");
   int8_t accuracy = this->sensor_->get_accuracy_decimals();
   return this->publish(this->get_state_topic_(), value_accuracy_to_string(value, accuracy));
 }
diff --git a/esphome/const.py b/esphome/const.py
index d2df83aa43..3d3bfcc244 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -692,6 +692,7 @@ CONF_PRIORITY = "priority"
 CONF_PROJECT = "project"
 CONF_PROTOCOL = "protocol"
 CONF_PUBLISH_INITIAL_STATE = "publish_initial_state"
+CONF_PUBLISH_NAN_AS_NONE = "publish_nan_as_none"
 CONF_PULL_MODE = "pull_mode"
 CONF_PULLDOWN = "pulldown"
 CONF_PULLUP = "pullup"
diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml
index d22fe9579f..a4bdf58809 100644
--- a/tests/components/mqtt/common.yaml
+++ b/tests/components/mqtt/common.yaml
@@ -60,6 +60,7 @@ mqtt:
     - mqtt.publish:
         topic: some/topic
         payload: Good-bye
+  publish_nan_as_none: false
 
 binary_sensor:
   - platform: template

From dc5942a59b521781279ff568057adf1dc01f11c8 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Tue, 3 Dec 2024 18:38:44 +1300
Subject: [PATCH 241/282] [ble] Allow setting shorter name for ble
 advertisements (#7867)

Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
---
 esphome/components/esp32_ble/__init__.py      | 24 +++++++++++++++++--
 esphome/components/esp32_ble/ble.cpp          | 18 ++++++++++----
 esphome/components/esp32_ble/ble.h            |  2 ++
 .../components/esp32_ble/ble_advertising.cpp  |  2 +-
 4 files changed, 38 insertions(+), 8 deletions(-)

diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py
index 75cf9d707d..08e30c9247 100644
--- a/esphome/components/esp32_ble/__init__.py
+++ b/esphome/components/esp32_ble/__init__.py
@@ -2,8 +2,10 @@ from esphome import automation
 import esphome.codegen as cg
 from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
 import esphome.config_validation as cv
-from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ID
+from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ESPHOME, CONF_ID, CONF_NAME
 from esphome.core import CORE
+from esphome.core.config import CONF_NAME_ADD_MAC_SUFFIX
+import esphome.final_validate as fv
 
 DEPENDENCIES = ["esp32"]
 CODEOWNERS = ["@jesserockz", "@Rapsssito"]
@@ -50,6 +52,7 @@ TX_POWER_LEVELS = {
 CONFIG_SCHEMA = cv.Schema(
     {
         cv.GenerateID(): cv.declare_id(ESP32BLE),
+        cv.Optional(CONF_NAME): cv.All(cv.string, cv.Length(max=20)),
         cv.Optional(CONF_IO_CAPABILITY, default="none"): cv.enum(
             IO_CAPABILITY, lower=True
         ),
@@ -67,7 +70,22 @@ def validate_variant(_):
         raise cv.Invalid(f"{variant} does not support Bluetooth")
 
 
-FINAL_VALIDATE_SCHEMA = validate_variant
+def final_validation(config):
+    validate_variant(config)
+    if (name := config.get(CONF_NAME)) is not None:
+        full_config = fv.full_config.get()
+        max_length = 20
+        if full_config[CONF_ESPHOME][CONF_NAME_ADD_MAC_SUFFIX]:
+            max_length -= 7  # "-AABBCC" is appended when add mac suffix option is used
+        if len(name) > max_length:
+            raise cv.Invalid(
+                f"Name '{name}' is too long, maximum length is {max_length} characters"
+            )
+
+    return config
+
+
+FINAL_VALIDATE_SCHEMA = final_validation
 
 
 async def to_code(config):
@@ -75,6 +93,8 @@ async def to_code(config):
     cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT]))
     cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY]))
     cg.add(var.set_advertising_cycle_time(config[CONF_ADVERTISING_CYCLE_TIME]))
+    if (name := config.get(CONF_NAME)) is not None:
+        cg.add(var.set_name(name))
     await cg.register_component(var, config)
 
     if CORE.using_esp_idf:
diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp
index 5d08b6e973..d7dcf93f86 100644
--- a/esphome/components/esp32_ble/ble.cpp
+++ b/esphome/components/esp32_ble/ble.cpp
@@ -188,12 +188,20 @@ bool ESP32BLE::ble_setup_() {
     }
   }
 
-  std::string name = App.get_name();
-  if (name.length() > 20) {
+  std::string name;
+  if (this->name_.has_value()) {
+    name = this->name_.value();
     if (App.is_name_add_mac_suffix_enabled()) {
-      name.erase(name.begin() + 13, name.end() - 7);  // Remove characters between 13 and the mac address
-    } else {
-      name = name.substr(0, 20);
+      name += "-" + get_mac_address().substr(6);
+    }
+  } else {
+    name = App.get_name();
+    if (name.length() > 20) {
+      if (App.is_name_add_mac_suffix_enabled()) {
+        name.erase(name.begin() + 13, name.end() - 7);  // Remove characters between 13 and the mac address
+      } else {
+        name = name.substr(0, 20);
+      }
     }
   }
 
diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h
index 7c55583852..ed7575f128 100644
--- a/esphome/components/esp32_ble/ble.h
+++ b/esphome/components/esp32_ble/ble.h
@@ -90,6 +90,7 @@ class ESP32BLE : public Component {
   void loop() override;
   void dump_config() override;
   float get_setup_priority() const override;
+  void set_name(const std::string &name) { this->name_ = name; }
 
   void advertising_start();
   void advertising_set_service_data(const std::vector<uint8_t> &data);
@@ -131,6 +132,7 @@ class ESP32BLE : public Component {
   esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
   uint32_t advertising_cycle_time_;
   bool enable_on_boot_;
+  optional<std::string> name_;
 };
 
 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp
index 1d340c76d9..92b7c60368 100644
--- a/esphome/components/esp32_ble/ble_advertising.cpp
+++ b/esphome/components/esp32_ble/ble_advertising.cpp
@@ -83,7 +83,7 @@ esp_err_t BLEAdvertising::services_advertisement_() {
   esp_err_t err;
 
   this->advertising_data_.set_scan_rsp = false;
-  this->advertising_data_.include_name = !this->scan_response_;
+  this->advertising_data_.include_name = true;
   this->advertising_data_.include_txpower = !this->scan_response_;
   err = esp_ble_gap_config_adv_data(&this->advertising_data_);
   if (err != ESP_OK) {

From c95887a14a61c8a5b9cab909ee9f9493eeef8cc3 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 3 Dec 2024 17:50:11 +1100
Subject: [PATCH 242/282] [lvgl] Bugfixes (#7896)

---
 esphome/components/lvgl/defines.py      | 2 +-
 esphome/components/lvgl/lvgl_esphome.h  | 3 +++
 esphome/components/lvgl/widgets/line.py | 6 ++++++
 tests/components/lvgl/lvgl-package.yaml | 4 ++--
 4 files changed, 12 insertions(+), 3 deletions(-)

diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py
index 81984637bd..5371f110a6 100644
--- a/esphome/components/lvgl/defines.py
+++ b/esphome/components/lvgl/defines.py
@@ -38,7 +38,7 @@ def literal(arg):
 def call_lambda(lamb: LambdaExpression):
     expr = lamb.content.strip()
     if expr.startswith("return") and expr.endswith(";"):
-        return expr[7:][:-1]
+        return expr[6:][:-1].strip()
     return f"{lamb}()"
 
 
diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h
index 921b7c109f..56413ad77e 100644
--- a/esphome/components/lvgl/lvgl_esphome.h
+++ b/esphome/components/lvgl/lvgl_esphome.h
@@ -56,6 +56,9 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT
 inline void lv_img_set_src(lv_obj_t *obj, esphome::image::Image *image) {
   lv_img_set_src(obj, image->get_lv_img_dsc());
 }
+inline void lv_disp_set_bg_image(lv_disp_t *disp, esphome::image::Image *image) {
+  lv_disp_set_bg_image(disp, image->get_lv_img_dsc());
+}
 #endif  // USE_LVGL_IMAGE
 #ifdef USE_LVGL_ANIMIMG
 inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images) {
diff --git a/esphome/components/lvgl/widgets/line.py b/esphome/components/lvgl/widgets/line.py
index 548dfa8452..0156fb1780 100644
--- a/esphome/components/lvgl/widgets/line.py
+++ b/esphome/components/lvgl/widgets/line.py
@@ -35,6 +35,11 @@ LINE_SCHEMA = {
     cv.GenerateID(CONF_POINT_LIST_ID): cv.declare_id(lv_point_t),
 }
 
+LINE_MODIFY_SCHEMA = {
+    cv.Optional(CONF_POINTS): cv_point_list,
+    cv.GenerateID(CONF_POINT_LIST_ID): cv.declare_id(lv_point_t),
+}
+
 
 class LineType(WidgetType):
     def __init__(self):
@@ -43,6 +48,7 @@ class LineType(WidgetType):
             LvType("lv_line_t"),
             (CONF_MAIN,),
             LINE_SCHEMA,
+            modify_schema=LINE_MODIFY_SCHEMA,
         )
 
     async def to_code(self, w: Widget, config):
diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml
index 81b18c4ff8..6611209756 100644
--- a/tests/components/lvgl/lvgl-package.yaml
+++ b/tests/components/lvgl/lvgl-package.yaml
@@ -337,7 +337,7 @@ lvgl:
             id: button_button
             width: 20%
             height: 10%
-            transform_angle: !lambda return 180*100;
+            transform_angle: !lambda return(180*100);
             arc_width: !lambda return 4;
             border_width: !lambda return 6;
             shadow_ofs_x: !lambda return 6;
@@ -581,7 +581,7 @@ lvgl:
               - 180, 60
               - 240, 10
             on_click:
-              - lvgl.widget.update:
+              - lvgl.line.update:
                   id: lv_line_id
                   line_color: 0xFFFF
               - lvgl.page.next:

From 00ddb0a427b2cae4a0bda8dfc26066e6857d8e6d Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 3 Dec 2024 17:50:56 +1100
Subject: [PATCH 243/282] [font] Restore correct default glyphs for bitmap
 fonts (#7907)

---
 esphome/components/font/__init__.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py
index 1b4fe4bff0..c397ba8306 100644
--- a/esphome/components/font/__init__.py
+++ b/esphome/components/font/__init__.py
@@ -373,7 +373,9 @@ def font_file_schema(value):
 # Default if no glyphs or glyphsets are provided
 DEFAULT_GLYPHSET = "GF_Latin_Kernel"
 # default for bitmap fonts
-DEFAULT_GLYPHS = ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz<C2><B0>'
+DEFAULT_GLYPHS = (
+    ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
+)
 
 CONF_RAW_GLYPH_ID = "raw_glyph_id"
 

From a37ff2dbd97d17c5b9b522c2a4515fd8311b7003 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Wed, 4 Dec 2024 07:48:50 +1100
Subject: [PATCH 244/282] [lvgl] Fix msgbox content (#7912)

---
 esphome/components/lvgl/widgets/msgbox.py | 3 ++-
 tests/components/lvgl/lvgl-package.yaml   | 7 +++++++
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/esphome/components/lvgl/widgets/msgbox.py b/esphome/components/lvgl/widgets/msgbox.py
index be0f2100d7..c3393940b6 100644
--- a/esphome/components/lvgl/widgets/msgbox.py
+++ b/esphome/components/lvgl/widgets/msgbox.py
@@ -29,7 +29,7 @@ from ..lvcode import (
 )
 from ..schemas import STYLE_SCHEMA, STYLED_TEXT_SCHEMA, container_schema, part_schema
 from ..types import LV_EVENT, char_ptr, lv_obj_t
-from . import Widget, set_obj_properties
+from . import Widget, add_widgets, set_obj_properties
 from .button import button_spec
 from .buttonmatrix import (
     BUTTONMATRIX_BUTTON_SCHEMA,
@@ -119,6 +119,7 @@ async def msgbox_to_code(top_layer, conf):
         button_style = {CONF_ITEMS: button_style}
         await set_obj_properties(buttonmatrix_widget, button_style)
     await set_obj_properties(msgbox_widget, conf)
+    await add_widgets(msgbox_widget, conf)
     async with LambdaContext(EVENT_ARG, where=messagebox_id) as close_action:
         outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN")
     if close_button:
diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml
index 6611209756..4b7e13db91 100644
--- a/tests/components/lvgl/lvgl-package.yaml
+++ b/tests/components/lvgl/lvgl-package.yaml
@@ -109,6 +109,10 @@ lvgl:
       close_button: true
       title: Messagebox
       bg_color: 0xffff
+      widgets:
+        - label:
+            text: Hello Msgbox
+            id: msgbox_label
       body:
         text: This is a sample messagebox
         bg_color: 0x808080
@@ -137,6 +141,9 @@ lvgl:
         - lvgl.widget.focus: mark
         - lvgl.widget.redraw: hello_label
         - lvgl.widget.redraw:
+        - lvgl.label.update:
+            id: msgbox_label
+            text: Unloaded
       on_all_events:
         logger.log:
           format: "Event %s"

From d00ec7e544f26b3ec3844f531f4027b9947c8d14 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 3 Dec 2024 17:23:17 -0600
Subject: [PATCH 245/282] [helpers] clang-tidy fix for #7706 (#7909)

---
 esphome/core/helpers.cpp | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp
index 103173095b..4e8caeae99 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -259,10 +259,15 @@ bool random_bytes(uint8_t *data, size_t len) {
 bool str_equals_case_insensitive(const std::string &a, const std::string &b) {
   return strcasecmp(a.c_str(), b.c_str()) == 0;
 }
+#if ESP_IDF_VERSION_MAJOR >= 5
+bool str_startswith(const std::string &str, const std::string &start) { return str.starts_with(start); }
+bool str_endswith(const std::string &str, const std::string &end) { return str.ends_with(end); }
+#else
 bool str_startswith(const std::string &str, const std::string &start) { return str.rfind(start, 0) == 0; }
 bool str_endswith(const std::string &str, const std::string &end) {
   return str.rfind(end) == (str.size() - end.size());
 }
+#endif
 std::string str_truncate(const std::string &str, size_t length) {
   return str.length() > length ? str.substr(0, length) : str;
 }

From dbed74b50d76cb7a6feb903908018aa3b96b9825 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Tue, 3 Dec 2024 17:26:27 -0600
Subject: [PATCH 246/282] [docker] Fix clang-tidy installation (#7910)

---
 docker/Dockerfile | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index c53856d0e8..cc05849271 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -220,7 +220,11 @@ RUN \
         nano=7.2-1+deb12u1 \
         build-essential=12.9 \
         python3-dev=3.11.2-1+b1 \
-    && rm -rf \
+    && if [ "$TARGETARCH$TARGETVARIANT" != "armv7" ]; then \
+    # move this up after armv7 is retired
+    apt-get install -y --no-install-recommends clang-tidy-18=1:18.1.8~++20240731024826+3b5b5c1ec4a3-1~exp1~20240731144843.145 ; \
+    fi; \
+    rm -rf \
         /tmp/* \
         /var/{cache,log}/* \
         /var/lib/apt/lists/*
@@ -228,9 +232,6 @@ RUN \
 COPY requirements_test.txt /
 RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
         export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
-  else \
-    # move this up into RUN above after armv7 is retired
-    apt-get install -y --no-install-recommends clang-tidy-18=1:18.1.8~++20240731024826+3b5b5c1ec4a3-1~exp1~20240731144843.145 ; \
   fi; \
   pip3 install \
   --break-system-packages --no-cache-dir -r /requirements_test.txt

From 79478cdb8a2731c23861da1c607e332117359179 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Wed, 4 Dec 2024 11:13:07 +1100
Subject: [PATCH 247/282] [sntp] Resolve warnings from ESP-IDF 5.x (#7913)

---
 esphome/components/sntp/sntp_component.cpp | 34 +++++++++-------------
 esphome/components/sntp/sntp_component.h   | 15 ++++------
 esphome/components/sntp/time.py            | 11 +++----
 3 files changed, 24 insertions(+), 36 deletions(-)

diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp
index 4ded98d483..21add1451d 100644
--- a/esphome/components/sntp/sntp_component.cpp
+++ b/esphome/components/sntp/sntp_component.cpp
@@ -9,11 +9,6 @@
 #include "lwip/apps/sntp.h"
 #endif
 
-// Yes, the server names are leaked, but that's fine.
-#ifdef CLANG_TIDY
-#define strdup(x) (const_cast<char *>(x))
-#endif
-
 namespace esphome {
 namespace sntp {
 
@@ -26,30 +21,29 @@ void SNTPComponent::setup() {
     esp_sntp_stop();
   }
   esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
+  size_t i = 0;
+  for (auto &server : this->servers_) {
+    esp_sntp_setservername(i++, server.c_str());
+  }
+  esp_sntp_set_sync_interval(this->get_update_interval());
+  esp_sntp_init();
 #else
   sntp_stop();
   sntp_setoperatingmode(SNTP_OPMODE_POLL);
-#endif
 
-  sntp_setservername(0, strdup(this->server_1_.c_str()));
-  if (!this->server_2_.empty()) {
-    sntp_setservername(1, strdup(this->server_2_.c_str()));
+  size_t i = 0;
+  for (auto &server : this->servers_) {
+    sntp_setservername(i++, server.c_str());
   }
-  if (!this->server_3_.empty()) {
-    sntp_setservername(2, strdup(this->server_3_.c_str()));
-  }
-#ifdef USE_ESP_IDF
-  esp_sntp_set_sync_interval(this->get_update_interval());
-#endif
-
   sntp_init();
+#endif
 }
 void SNTPComponent::dump_config() {
   ESP_LOGCONFIG(TAG, "SNTP Time:");
-  ESP_LOGCONFIG(TAG, "  Server 1: '%s'", this->server_1_.c_str());
-  ESP_LOGCONFIG(TAG, "  Server 2: '%s'", this->server_2_.c_str());
-  ESP_LOGCONFIG(TAG, "  Server 3: '%s'", this->server_3_.c_str());
-  ESP_LOGCONFIG(TAG, "  Timezone: '%s'", this->timezone_.c_str());
+  size_t i = 0;
+  for (auto &server : this->servers_) {
+    ESP_LOGCONFIG(TAG, "  Server %zu: '%s'", i++, server.c_str());
+  }
 }
 void SNTPComponent::update() {
 #if !defined(USE_ESP_IDF)
diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h
index 987dd23a19..a4e8267383 100644
--- a/esphome/components/sntp/sntp_component.h
+++ b/esphome/components/sntp/sntp_component.h
@@ -14,23 +14,20 @@ namespace sntp {
 /// \see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
 class SNTPComponent : public time::RealTimeClock {
  public:
+  SNTPComponent(const std::vector<std::string> &servers) : servers_(servers) {}
+
+  // Note: set_servers() has been removed and replaced by a constructor - calling set_servers after setup would
+  // have had no effect anyway, and making the strings immutable avoids the need to strdup their contents.
+
   void setup() override;
   void dump_config() override;
-  /// Change the servers used by SNTP for timekeeping
-  void set_servers(const std::string &server_1, const std::string &server_2, const std::string &server_3) {
-    this->server_1_ = server_1;
-    this->server_2_ = server_2;
-    this->server_3_ = server_3;
-  }
   float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; }
 
   void update() override;
   void loop() override;
 
  protected:
-  std::string server_1_;
-  std::string server_2_;
-  std::string server_3_;
+  std::vector<std::string> servers_;
   bool has_time_{false};
 };
 
diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py
index 7cc82e3dff..6f883d5bed 100644
--- a/esphome/components/sntp/time.py
+++ b/esphome/components/sntp/time.py
@@ -1,16 +1,16 @@
+import esphome.codegen as cg
 from esphome.components import time as time_
 import esphome.config_validation as cv
-import esphome.codegen as cg
-from esphome.core import CORE
 from esphome.const import (
     CONF_ID,
     CONF_SERVERS,
+    PLATFORM_BK72XX,
     PLATFORM_ESP32,
     PLATFORM_ESP8266,
     PLATFORM_RP2040,
     PLATFORM_RTL87XX,
-    PLATFORM_BK72XX,
 )
+from esphome.core import CORE
 
 DEPENDENCIES = ["network"]
 sntp_ns = cg.esphome_ns.namespace("sntp")
@@ -40,11 +40,8 @@ CONFIG_SCHEMA = cv.All(
 
 
 async def to_code(config):
-    var = cg.new_Pvariable(config[CONF_ID])
-
     servers = config[CONF_SERVERS]
-    servers += [""] * (3 - len(servers))
-    cg.add(var.set_servers(*servers))
+    var = cg.new_Pvariable(config[CONF_ID], servers)
 
     await cg.register_component(var, config)
     await time_.register_time(var, config)

From 016fac249649b475a918eb2c17f9daa0ad399485 Mon Sep 17 00:00:00 2001
From: mikosoft83 <63317931+mikosoft83@users.noreply.github.com>
Date: Wed, 4 Dec 2024 01:18:00 +0100
Subject: [PATCH 248/282] Add strftime variant with background color (#7714)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 esphome/components/display/display.cpp | 14 +++++++++-----
 esphome/components/display/display.h   | 14 ++++++++++++++
 2 files changed, 23 insertions(+), 5 deletions(-)

diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp
index 145a4f5278..1d996bd59b 100644
--- a/esphome/components/display/display.cpp
+++ b/esphome/components/display/display.cpp
@@ -662,20 +662,24 @@ void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) {
   if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to))
     this->trigger(from, to);
 }
-void Display::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time) {
+void Display::strftime(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format,
+                       ESPTime time) {
   char buffer[64];
   size_t ret = time.strftime(buffer, sizeof(buffer), format);
   if (ret > 0)
-    this->print(x, y, font, color, align, buffer);
+    this->print(x, y, font, color, align, buffer, background);
+}
+void Display::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time) {
+  this->strftime(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, time);
 }
 void Display::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) {
-  this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time);
+  this->strftime(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, time);
 }
 void Display::strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time) {
-  this->strftime(x, y, font, COLOR_ON, align, format, time);
+  this->strftime(x, y, font, COLOR_ON, COLOR_OFF, align, format, time);
 }
 void Display::strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) {
-  this->strftime(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, time);
+  this->strftime(x, y, font, COLOR_ON, COLOR_OFF, TextAlign::TOP_LEFT, format, time);
 }
 
 void Display::start_clipping(Rect rect) {
diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h
index 54e897cdec..43da08f4ac 100644
--- a/esphome/components/display/display.h
+++ b/esphome/components/display/display.h
@@ -437,6 +437,20 @@ class Display : public PollingComponent {
    */
   void printf(int x, int y, BaseFont *font, const char *format, ...) __attribute__((format(printf, 5, 6)));
 
+  /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
+   *
+   * @param x The x coordinate of the text alignment anchor point.
+   * @param y The y coordinate of the text alignment anchor point.
+   * @param font The font to draw the text with.
+   * @param color The color to draw the text with.
+   * @param background The background color to draw the text with.
+   * @param align The alignment of the text.
+   * @param format The format to use.
+   * @param ... The arguments to use for the text formatting.
+   */
+  void strftime(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format,
+                ESPTime time) __attribute__((format(strftime, 8, 0)));
+
   /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
    *
    * @param x The x coordinate of the text alignment anchor point.

From 472402745d2f3821802bbdadf2ebc45d2ee6ccf5 Mon Sep 17 00:00:00 2001
From: Kevin Ahrendt <kevin.ahrendt@nabucasa.com>
Date: Wed, 4 Dec 2024 16:18:14 -0500
Subject: [PATCH 249/282] [i2s_audio] Bugfix: Follow configured bits per sample
 (#7916)

---
 .../i2s_audio/speaker/i2s_audio_speaker.cpp   | 123 +++++++++---------
 .../i2s_audio/speaker/i2s_audio_speaker.h     |  25 ++--
 2 files changed, 70 insertions(+), 78 deletions(-)

diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
index 53b3cc8dc0..194cc06a60 100644
--- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
+++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
@@ -33,14 +33,15 @@ enum SpeakerEventGroupBits : uint32_t {
   STATE_RUNNING = (1 << 11),
   STATE_STOPPING = (1 << 12),
   STATE_STOPPED = (1 << 13),
-  ERR_INVALID_FORMAT = (1 << 14),
-  ERR_TASK_FAILED_TO_START = (1 << 15),
-  ERR_ESP_INVALID_STATE = (1 << 16),
+  ERR_TASK_FAILED_TO_START = (1 << 14),
+  ERR_ESP_INVALID_STATE = (1 << 15),
+  ERR_ESP_NOT_SUPPORTED = (1 << 16),
   ERR_ESP_INVALID_ARG = (1 << 17),
   ERR_ESP_INVALID_SIZE = (1 << 18),
   ERR_ESP_NO_MEM = (1 << 19),
   ERR_ESP_FAIL = (1 << 20),
-  ALL_ERR_ESP_BITS = ERR_ESP_INVALID_STATE | ERR_ESP_INVALID_ARG | ERR_ESP_INVALID_SIZE | ERR_ESP_NO_MEM | ERR_ESP_FAIL,
+  ALL_ERR_ESP_BITS = ERR_ESP_INVALID_STATE | ERR_ESP_NOT_SUPPORTED | ERR_ESP_INVALID_ARG | ERR_ESP_INVALID_SIZE |
+                     ERR_ESP_NO_MEM | ERR_ESP_FAIL,
   ALL_BITS = 0x00FFFFFF,  // All valid FreeRTOS event group bits
 };
 
@@ -55,6 +56,8 @@ static esp_err_t err_bit_to_esp_err(uint32_t bit) {
       return ESP_ERR_INVALID_SIZE;
     case SpeakerEventGroupBits::ERR_ESP_NO_MEM:
       return ESP_ERR_NO_MEM;
+    case SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED:
+      return ESP_ERR_NOT_SUPPORTED;
     default:
       return ESP_FAIL;
   }
@@ -135,19 +138,19 @@ void I2SAudioSpeaker::loop() {
     xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START);
   }
 
-  if (event_group_bits & SpeakerEventGroupBits::ERR_INVALID_FORMAT) {
+  if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) {
+    uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS;
+    ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(err_bit_to_esp_err(error_bits)));
+    this->status_set_warning();
+  }
+
+  if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED) {
     this->status_set_error("Failed to adjust I2S bus to match the incoming audio");
     ESP_LOGE(TAG,
              "Incompatible audio format: sample rate = %" PRIu32 ", channels = %" PRIu8 ", bits per sample = %" PRIu8,
              this->audio_stream_info_.sample_rate, this->audio_stream_info_.channels,
              this->audio_stream_info_.bits_per_sample);
   }
-
-  if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) {
-    uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS;
-    ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(err_bit_to_esp_err(error_bits)));
-    this->status_set_warning();
-  }
 }
 
 void I2SAudioSpeaker::set_volume(float volume) {
@@ -236,13 +239,14 @@ void I2SAudioSpeaker::speaker_task(void *params) {
   xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STARTING);
 
   audio::AudioStreamInfo audio_stream_info = this_speaker->audio_stream_info_;
-  const ssize_t bytes_per_sample = audio_stream_info.get_bytes_per_sample();
-  const uint8_t number_of_channels = audio_stream_info.channels;
 
-  const size_t dma_buffers_size = DMA_BUFFERS_COUNT * DMA_BUFFER_DURATION_MS * this_speaker->sample_rate_ / 1000 *
-                                  bytes_per_sample * number_of_channels;
-  const size_t ring_buffer_size =
-      this_speaker->buffer_duration_ms_ * this_speaker->sample_rate_ / 1000 * bytes_per_sample * number_of_channels;
+  const uint32_t bytes_per_ms =
+      audio_stream_info.channels * audio_stream_info.get_bytes_per_sample() * audio_stream_info.sample_rate / 1000;
+
+  const size_t dma_buffers_size = DMA_BUFFERS_COUNT * DMA_BUFFER_DURATION_MS * bytes_per_ms;
+
+  // Ensure ring buffer is at least as large as the total size of the DMA buffers
+  const size_t ring_buffer_size = std::min(dma_buffers_size, this_speaker->buffer_duration_ms_ * bytes_per_ms);
 
   if (this_speaker->send_esp_err_to_event_group_(this_speaker->allocate_buffers_(dma_buffers_size, ring_buffer_size))) {
     // Failed to allocate buffers
@@ -250,14 +254,7 @@ void I2SAudioSpeaker::speaker_task(void *params) {
     this_speaker->delete_task_(dma_buffers_size);
   }
 
-  if (this_speaker->send_esp_err_to_event_group_(this_speaker->start_i2s_driver_())) {
-    // Failed to start I2S driver
-    this_speaker->delete_task_(dma_buffers_size);
-  }
-
-  if (!this_speaker->send_esp_err_to_event_group_(this_speaker->reconfigure_i2s_stream_info_(audio_stream_info))) {
-    // Successfully set the I2S stream info, ready to write audio data to the I2S port
-
+  if (!this_speaker->send_esp_err_to_event_group_(this_speaker->start_i2s_driver_(audio_stream_info))) {
     xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_RUNNING);
 
     bool stop_gracefully = false;
@@ -275,6 +272,12 @@ void I2SAudioSpeaker::speaker_task(void *params) {
         stop_gracefully = true;
       }
 
+      if (this_speaker->audio_stream_info_ != audio_stream_info) {
+        // Audio stream info has changed, stop the speaker task so it will restart with the proper settings.
+
+        break;
+      }
+
       i2s_event_t i2s_event;
       while (xQueueReceive(this_speaker->i2s_event_queue_, &i2s_event, 0)) {
         if (i2s_event.type == I2S_EVENT_TX_Q_OVF) {
@@ -316,17 +319,14 @@ void I2SAudioSpeaker::speaker_task(void *params) {
         }
       }
     }
-  } else {
-    // Couldn't configure the I2S port to be compatible with the incoming audio
-    xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_INVALID_FORMAT);
+
+    xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STOPPING);
+
+    i2s_driver_uninstall(this_speaker->parent_->get_port());
+
+    this_speaker->parent_->unlock();
   }
-  i2s_zero_dma_buffer(this_speaker->parent_->get_port());
 
-  xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STOPPING);
-
-  i2s_driver_uninstall(this_speaker->parent_->get_port());
-
-  this_speaker->parent_->unlock();
   this_speaker->delete_task_(dma_buffers_size);
 }
 
@@ -382,6 +382,9 @@ bool I2SAudioSpeaker::send_esp_err_to_event_group_(esp_err_t err) {
     case ESP_ERR_NO_MEM:
       xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
       return true;
+    case ESP_ERR_NOT_SUPPORTED:
+      xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED);
+      return true;
     default:
       xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_FAIL);
       return true;
@@ -411,18 +414,40 @@ esp_err_t I2SAudioSpeaker::allocate_buffers_(size_t data_buffer_size, size_t rin
   return ESP_OK;
 }
 
-esp_err_t I2SAudioSpeaker::start_i2s_driver_() {
+esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info) {
+  if ((this->i2s_mode_ & I2S_MODE_SLAVE) && (this->sample_rate_ != audio_stream_info.sample_rate)) {  // NOLINT
+    //  Can't reconfigure I2S bus, so the sample rate must match the configured value
+    return ESP_ERR_NOT_SUPPORTED;
+  }
+
+  if ((i2s_bits_per_sample_t) audio_stream_info.bits_per_sample > this->bits_per_sample_) {
+    // Currently can't handle the case when the incoming audio has more bits per sample than the configured value
+    return ESP_ERR_NOT_SUPPORTED;
+  }
+
   if (!this->parent_->try_lock()) {
     return ESP_ERR_INVALID_STATE;
   }
 
+  i2s_channel_fmt_t channel = this->channel_;
+
+  if (audio_stream_info.channels == 1) {
+    if (this->channel_ == I2S_CHANNEL_FMT_ONLY_LEFT) {
+      channel = I2S_CHANNEL_FMT_ONLY_LEFT;
+    } else {
+      channel = I2S_CHANNEL_FMT_ONLY_RIGHT;
+    }
+  } else if (audio_stream_info.channels == 2) {
+    channel = I2S_CHANNEL_FMT_RIGHT_LEFT;
+  }
+
   int dma_buffer_length = DMA_BUFFER_DURATION_MS * this->sample_rate_ / 1000;
 
   i2s_driver_config_t config = {
     .mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_TX),
-    .sample_rate = this->sample_rate_,
+    .sample_rate = audio_stream_info.sample_rate,
     .bits_per_sample = this->bits_per_sample_,
-    .channel_format = this->channel_,
+    .channel_format = channel,
     .communication_format = this->i2s_comm_fmt_,
     .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
     .dma_buf_count = DMA_BUFFERS_COUNT,
@@ -477,30 +502,6 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_() {
   return err;
 }
 
-esp_err_t I2SAudioSpeaker::reconfigure_i2s_stream_info_(audio::AudioStreamInfo &audio_stream_info) {
-  if (this->i2s_mode_ & I2S_MODE_MASTER) {
-    // ESP controls for the the I2S bus, so adjust the sample rate and bits per sample to match the incoming audio
-    this->sample_rate_ = audio_stream_info.sample_rate;
-    this->bits_per_sample_ = (i2s_bits_per_sample_t) audio_stream_info.bits_per_sample;
-  } else if (this->sample_rate_ != audio_stream_info.sample_rate) {
-    // Can't reconfigure I2S bus, so the sample rate must match the configured value
-    return ESP_ERR_INVALID_ARG;
-  }
-
-  if ((i2s_bits_per_sample_t) audio_stream_info.bits_per_sample > this->bits_per_sample_) {
-    // Currently can't handle the case when the incoming audio has more bits per sample than the configured value
-    return ESP_ERR_INVALID_ARG;
-  }
-
-  if (audio_stream_info.channels == 1) {
-    return i2s_set_clk(this->parent_->get_port(), this->sample_rate_, this->bits_per_sample_, I2S_CHANNEL_MONO);
-  } else if (audio_stream_info.channels == 2) {
-    return i2s_set_clk(this->parent_->get_port(), this->sample_rate_, this->bits_per_sample_, I2S_CHANNEL_STEREO);
-  }
-
-  return ESP_ERR_INVALID_ARG;
-}
-
 void I2SAudioSpeaker::delete_task_(size_t buffer_size) {
   this->audio_ring_buffer_.reset();  // Releases onwership of the shared_ptr
 
diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h
index 2b90f39399..d706deb0f4 100644
--- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h
+++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h
@@ -91,24 +91,15 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
   esp_err_t allocate_buffers_(size_t data_buffer_size, size_t ring_buffer_size);
 
   /// @brief Starts the ESP32 I2S driver.
-  /// Attempts to lock the I2S port, starts the I2S driver, and sets the data out pin. If it fails, it will unlock
-  /// the I2S port and uninstall the driver, if necessary.
-  /// @return ESP_ERR_INVALID_STATE if the I2S port is already locked.
-  ///         ESP_ERR_INVALID_ARG if installing the driver or setting the data out pin fails due to a parameter error.
+  /// Attempts to lock the I2S port, starts the I2S driver using the passed in stream information, and sets the data out
+  /// pin. If it fails, it will unlock the I2S port and uninstall the driver, if necessary.
+  /// @param audio_stream_info Stream information for the I2S driver.
+  /// @return ESP_ERR_NOT_ALLOWED if the I2S port can't play the incoming audio stream.
+  ///         ESP_ERR_INVALID_STATE if the I2S port is already locked.
+  ///         ESP_ERR_INVALID_ARG if nstalling the driver or setting the data outpin fails due to a parameter error.
   ///         ESP_ERR_NO_MEM if the driver fails to install due to a memory allocation error.
-  ///         ESP_FAIL if setting the data out pin fails due to an IO error
-  ///         ESP_OK if successful
-  esp_err_t start_i2s_driver_();
-
-  /// @brief Adjusts the I2S driver configuration to match the incoming audio stream.
-  /// Modifies I2S driver's sample rate, bits per sample, and number of channel settings. If the I2S is in secondary
-  /// mode, it only modifies the number of channels.
-  /// @param audio_stream_info  Describes the incoming audio stream
-  /// @return ESP_ERR_INVALID_ARG if there is a parameter error, if there is more than 2 channels in the stream, or if
-  ///           the audio settings are incompatible with the configuration.
-  ///         ESP_ERR_NO_MEM if the driver fails to reconfigure due to a memory allocation error.
-  ///         ESP_OK if successful.
-  esp_err_t reconfigure_i2s_stream_info_(audio::AudioStreamInfo &audio_stream_info);
+  ///         ESP_FAIL if setting the data out pin fails due to an IO error ESP_OK if successful
+  esp_err_t start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info);
 
   /// @brief Deletes the speaker's task.
   /// Deallocates the data_buffer_ and audio_ring_buffer_, if necessary, and deletes the task. Should only be called by

From d429aa8bb8d9c8375996c0ab2335d66132cb3472 Mon Sep 17 00:00:00 2001
From: Pavlo Dudnytskyi <paveldudn@gmail.com>
Date: Wed, 4 Dec 2024 22:43:00 +0100
Subject: [PATCH 250/282] Haier AC quiet mode switch fix (#7902)

---
 esphome/components/haier/hon_climate.cpp | 24 +++++++++++++++++++-----
 1 file changed, 19 insertions(+), 5 deletions(-)

diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp
index 85e9cf37b9..c95a87223d 100644
--- a/esphome/components/haier/hon_climate.cpp
+++ b/esphome/components/haier/hon_climate.cpp
@@ -35,7 +35,9 @@ void HonClimate::set_beeper_state(bool state) {
   if (state != this->settings_.beeper_state) {
     this->settings_.beeper_state = state;
 #ifdef USE_SWITCH
-    this->beeper_switch_->publish_state(state);
+    if (this->beeper_switch_ != nullptr) {
+      this->beeper_switch_->publish_state(state);
+    }
 #endif
     this->hon_rtc_.save(&this->settings_);
   }
@@ -45,10 +47,17 @@ bool HonClimate::get_beeper_state() const { return this->settings_.beeper_state;
 
 void HonClimate::set_quiet_mode_state(bool state) {
   if (state != this->get_quiet_mode_state()) {
-    this->quiet_mode_state_ = state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF;
+    if ((this->mode != ClimateMode::CLIMATE_MODE_OFF) && (this->mode != ClimateMode::CLIMATE_MODE_FAN_ONLY)) {
+      this->quiet_mode_state_ = state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF;
+      this->force_send_control_ = true;
+    } else {
+      this->quiet_mode_state_ = state ? SwitchState::ON : SwitchState::OFF;
+    }
     this->settings_.quiet_mode_state = state;
 #ifdef USE_SWITCH
-    this->quiet_mode_switch_->publish_state(state);
+    if (this->quiet_mode_switch_ != nullptr) {
+      this->quiet_mode_switch_->publish_state(state);
+    }
 #endif
     this->hon_rtc_.save(&this->settings_);
   }
@@ -509,7 +518,7 @@ void HonClimate::initialization() {
   }
   this->current_vertical_swing_ = this->settings_.last_vertiacal_swing;
   this->current_horizontal_swing_ = this->settings_.last_horizontal_swing;
-  this->quiet_mode_state_ = this->settings_.quiet_mode_state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF;
+  this->quiet_mode_state_ = this->settings_.quiet_mode_state ? SwitchState::ON : SwitchState::OFF;
 }
 
 haier_protocol::HaierMessage HonClimate::get_control_message() {
@@ -932,7 +941,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
       if (this->mode == CLIMATE_MODE_OFF) {
         // AC just turned on from remote need to turn off display
         this->force_send_control_ = true;
-      } else if ((((uint8_t) this->health_mode_) & 0b10) == 0) {
+      } else if ((((uint8_t) this->display_status_) & 0b10) == 0) {
         this->display_status_ = disp_status ? SwitchState::ON : SwitchState::OFF;
       }
     }
@@ -1004,6 +1013,11 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
       if (new_quiet_mode != this->get_quiet_mode_state()) {
         this->quiet_mode_state_ = new_quiet_mode ? SwitchState::ON : SwitchState::OFF;
         this->settings_.quiet_mode_state = new_quiet_mode;
+#ifdef USE_SWITCH
+        if (this->quiet_mode_switch_ != nullptr) {
+          this->quiet_mode_switch_->publish_state(new_quiet_mode);
+        }
+#endif  // USE_SWITCH
         this->hon_rtc_.save(&this->settings_);
       }
     }

From 4e839d42d050f3d6c83e045512fcf5bfab8d27ac Mon Sep 17 00:00:00 2001
From: Sebastian Muszynski <basti@linkt.de>
Date: Wed, 4 Dec 2024 22:44:34 +0100
Subject: [PATCH 251/282] [CI] Update clang-tidy to 18.1.8 (#7915)

---
 requirements_dev.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements_dev.txt b/requirements_dev.txt
index 2ef8330215..1a98a15ab2 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -1,4 +1,4 @@
 # Useful stuff when working in a development environment
 clang-format==13.0.1  # also change in .pre-commit-config.yaml and Dockerfile when updating
-clang-tidy==18.1.3  # When updating clang-tidy, also update Dockerfile
+clang-tidy==18.1.8  # When updating clang-tidy, also update Dockerfile
 yamllint==1.35.1  # also change in .pre-commit-config.yaml when updating

From ece72c6b189e130bdc8a7dee7920abaa04cb61d9 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Wed, 4 Dec 2024 21:03:38 -0600
Subject: [PATCH 252/282] [i2s_audio] Speaker type fix (#7919)

---
 esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
index 194cc06a60..d2a582c2cc 100644
--- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
+++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
@@ -246,7 +246,8 @@ void I2SAudioSpeaker::speaker_task(void *params) {
   const size_t dma_buffers_size = DMA_BUFFERS_COUNT * DMA_BUFFER_DURATION_MS * bytes_per_ms;
 
   // Ensure ring buffer is at least as large as the total size of the DMA buffers
-  const size_t ring_buffer_size = std::min(dma_buffers_size, this_speaker->buffer_duration_ms_ * bytes_per_ms);
+  const size_t ring_buffer_size =
+      std::min((uint32_t) dma_buffers_size, this_speaker->buffer_duration_ms_ * bytes_per_ms);
 
   if (this_speaker->send_esp_err_to_event_group_(this_speaker->allocate_buffers_(dma_buffers_size, ring_buffer_size))) {
     // Failed to allocate buffers

From f3cc1e541a904d5ef44b431300e5457e4d782dd5 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Thu, 5 Dec 2024 16:44:59 +1300
Subject: [PATCH 253/282] [esp32_rmt_led_strip] Add ``COMPONENT_SCHEMA``
 extending (#7918)

---
 esphome/components/esp32_rmt_led_strip/light.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py
index 79f339e248..976f70e858 100644
--- a/esphome/components/esp32_rmt_led_strip/light.py
+++ b/esphome/components/esp32_rmt_led_strip/light.py
@@ -1,9 +1,9 @@
 from dataclasses import dataclass
 
-import esphome.codegen as cg
-import esphome.config_validation as cv
 from esphome import pins
+import esphome.codegen as cg
 from esphome.components import esp32_rmt, light
+import esphome.config_validation as cv
 from esphome.const import (
     CONF_CHIPSET,
     CONF_IS_RGBW,
@@ -103,7 +103,7 @@ CONFIG_SCHEMA = cv.All(
                 default="0 us",
             ): cv.positive_time_period_nanoseconds,
         }
-    ),
+    ).extend(cv.COMPONENT_SCHEMA),
     cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH),
 )
 

From acc8d24a325b34cd4af035bb2e26b26f1d6e4f83 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Thu, 5 Dec 2024 02:39:30 -0600
Subject: [PATCH 254/282] [esp32] Use pioarduino + IDF 5.1.5 as default for IDF
 builds (#7706)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 .clang-tidy                                   |   1 +
 esphome/components/esp32/__init__.py          | 106 ++++++++++++++++--
 platformio.ini                                |   4 +-
 ...build_components_base.esp32-c3-idf-51.yaml |  19 ----
 .../build_components_base.esp32-idf-51.yaml   |  19 ----
 ...build_components_base.esp32-s2-idf-51.yaml |  20 ----
 ...build_components_base.esp32-s3-idf-51.yaml |  20 ----
 7 files changed, 97 insertions(+), 92 deletions(-)
 delete mode 100644 tests/test_build_components/build_components_base.esp32-c3-idf-51.yaml
 delete mode 100644 tests/test_build_components/build_components_base.esp32-idf-51.yaml
 delete mode 100644 tests/test_build_components/build_components_base.esp32-s2-idf-51.yaml
 delete mode 100644 tests/test_build_components/build_components_base.esp32-s3-idf-51.yaml

diff --git a/.clang-tidy b/.clang-tidy
index 8dba033e2d..5878028f48 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -21,6 +21,7 @@ Checks: >-
   -clang-analyzer-osx.*,
   -clang-diagnostic-delete-abstract-non-virtual-dtor,
   -clang-diagnostic-delete-non-abstract-non-virtual-dtor,
+  -clang-diagnostic-deprecated-declarations,
   -clang-diagnostic-ignored-optimization-argument,
   -clang-diagnostic-missing-field-initializers,
   -clang-diagnostic-shadow-field,
diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py
index 61fbb53e3a..580c3fc081 100644
--- a/esphome/components/esp32/__init__.py
+++ b/esphome/components/esp32/__init__.py
@@ -65,6 +65,8 @@ _LOGGER = logging.getLogger(__name__)
 CODEOWNERS = ["@esphome/core"]
 AUTO_LOAD = ["preferences"]
 
+CONF_RELEASE = "release"
+
 
 def set_core_data(config):
     CORE.data[KEY_ESP32] = {}
@@ -216,11 +218,17 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
     return f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
 
 
-def _format_framework_espidf_version(ver: cv.Version) -> str:
+def _format_framework_espidf_version(
+    ver: cv.Version, release: str, for_platformio: bool
+) -> str:
     # format the given arduino (https://github.com/espressif/esp-idf/releases) version to
     # a PIO platformio/framework-espidf value
     # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
-    return f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
+    if for_platformio:
+        return f"platformio/framework-espidf@~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
+    if release:
+        return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip"
+    return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip"
 
 
 # NOTE: Keep this in mind when updating the recommended version:
@@ -241,11 +249,33 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0)
 # The default/recommended esp-idf framework version
 #  - https://github.com/espressif/esp-idf/releases
 #  - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
-RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 8)
+RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 1, 5)
 # The platformio/espressif32 version to use for esp-idf frameworks
 #  - https://github.com/platformio/platform-espressif32/releases
 #  - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
-ESP_IDF_PLATFORM_VERSION = cv.Version(5, 4, 0)
+ESP_IDF_PLATFORM_VERSION = cv.Version(51, 3, 7)
+
+# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions
+SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
+    cv.Version(5, 3, 1),
+    cv.Version(5, 3, 0),
+    cv.Version(5, 2, 2),
+    cv.Version(5, 2, 1),
+    cv.Version(5, 1, 2),
+    cv.Version(5, 1, 1),
+    cv.Version(5, 1, 0),
+    cv.Version(5, 0, 2),
+    cv.Version(5, 0, 1),
+    cv.Version(5, 0, 0),
+]
+
+# pioarduino versions that don't require a release number
+# List based on https://github.com/pioarduino/esp-idf/releases
+SUPPORTED_PIOARDUINO_ESP_IDF_5X = [
+    cv.Version(5, 3, 1),
+    cv.Version(5, 3, 0),
+    cv.Version(5, 1, 5),
+]
 
 
 def _arduino_check_versions(value):
@@ -286,8 +316,8 @@ def _arduino_check_versions(value):
 def _esp_idf_check_versions(value):
     value = value.copy()
     lookups = {
-        "dev": (cv.Version(5, 1, 2), "https://github.com/espressif/esp-idf.git"),
-        "latest": (cv.Version(5, 1, 2), None),
+        "dev": (cv.Version(5, 1, 5), "https://github.com/espressif/esp-idf.git"),
+        "latest": (cv.Version(5, 1, 5), None),
         "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
     }
 
@@ -305,13 +335,51 @@ def _esp_idf_check_versions(value):
     if version < cv.Version(4, 0, 0):
         raise cv.Invalid("Only ESP-IDF 4.0+ is supported.")
 
-    value[CONF_VERSION] = str(version)
-    value[CONF_SOURCE] = source or _format_framework_espidf_version(version)
+    # flag this for later *before* we set value[CONF_PLATFORM_VERSION] below
+    has_platform_ver = CONF_PLATFORM_VERSION in value
 
     value[CONF_PLATFORM_VERSION] = value.get(
         CONF_PLATFORM_VERSION, _parse_platform_version(str(ESP_IDF_PLATFORM_VERSION))
     )
 
+    if (
+        (is_platformio := _platform_is_platformio(value[CONF_PLATFORM_VERSION]))
+        and version.major >= 5
+        and version not in SUPPORTED_PLATFORMIO_ESP_IDF_5X
+    ):
+        raise cv.Invalid(
+            f"ESP-IDF {str(version)} not supported by platformio/espressif32"
+        )
+
+    if (
+        version.major < 5
+        or (
+            version in SUPPORTED_PLATFORMIO_ESP_IDF_5X
+            and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X
+        )
+    ) and not has_platform_ver:
+        raise cv.Invalid(
+            f"ESP-IDF {value[CONF_VERSION]} may be supported by platformio/espressif32; please specify '{CONF_PLATFORM_VERSION}'"
+        )
+
+    if (
+        not is_platformio
+        and CONF_RELEASE not in value
+        and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X
+    ):
+        raise cv.Invalid(
+            f"ESP-IDF {value[CONF_VERSION]} is not available with pioarduino; you may need to specify '{CONF_RELEASE}'"
+        )
+
+    value[CONF_VERSION] = str(version)
+    value[CONF_SOURCE] = source or _format_framework_espidf_version(
+        version, value.get(CONF_RELEASE, None), is_platformio
+    )
+
+    if value[CONF_SOURCE].startswith("http"):
+        # prefix is necessary or platformio will complain with a cryptic error
+        value[CONF_SOURCE] = f"framework-espidf@{value[CONF_SOURCE]}"
+
     if version != RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION:
         _LOGGER.warning(
             "The selected ESP-IDF framework version is not the recommended one. "
@@ -323,6 +391,12 @@ def _esp_idf_check_versions(value):
 
 def _parse_platform_version(value):
     try:
+        ver = cv.Version.parse(cv.version_number(value))
+        if ver.major >= 50:  # a pioarduino version
+            if "-" in value:
+                # maybe a release candidate?...definitely not our default, just use it as-is...
+                return f"https://github.com/pioarduino/platform-espressif32.git#{value}"
+            return f"https://github.com/pioarduino/platform-espressif32.git#{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
         # if platform version is a valid version constraint, prefix the default package
         cv.platformio_version_constraint(value)
         return f"platformio/espressif32@{value}"
@@ -330,6 +404,14 @@ def _parse_platform_version(value):
         return value
 
 
+def _platform_is_platformio(value):
+    try:
+        ver = cv.Version.parse(cv.version_number(value))
+        return ver.major < 50
+    except cv.Invalid:
+        return "platformio" in value
+
+
 def _detect_variant(value):
     board = value[CONF_BOARD]
     if board in BOARDS:
@@ -412,6 +494,7 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
     cv.Schema(
         {
             cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
+            cv.Optional(CONF_RELEASE): cv.string_strict,
             cv.Optional(CONF_SOURCE): cv.string_strict,
             cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version,
             cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): {
@@ -515,10 +598,9 @@ async def to_code(config):
         cg.add_build_flag("-DUSE_ESP_IDF")
         cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
         cg.add_build_flag("-Wno-nonnull-compare")
-        cg.add_platformio_option(
-            "platform_packages",
-            [f"platformio/framework-espidf@{conf[CONF_SOURCE]}"],
-        )
+
+        cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
+
         # platformio/toolchain-esp32ulp does not support linux_aarch64 yet and has not been updated for over 2 years
         # This is espressif's own published version which is more up to date.
         cg.add_platformio_option(
diff --git a/platformio.ini b/platformio.ini
index 04afc059af..b9b80e933f 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -137,9 +137,9 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
 ; This are common settings for the ESP32 (all variants) using IDF.
 [common:esp32-idf]
 extends = common:idf
-platform = platformio/espressif32@5.4.0
+platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.06/platform-espressif32.zip
 platform_packages =
-    platformio/framework-espidf@~3.40408.0
+    pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.1.5/esp-idf-v5.1.5.zip
 
 framework = espidf
 lib_deps =
diff --git a/tests/test_build_components/build_components_base.esp32-c3-idf-51.yaml b/tests/test_build_components/build_components_base.esp32-c3-idf-51.yaml
deleted file mode 100644
index eb5b23a4ec..0000000000
--- a/tests/test_build_components/build_components_base.esp32-c3-idf-51.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-esphome:
-  name: componenttestesp32c3idf51
-  friendly_name: $component_name
-
-esp32:
-  board: lolin_c3_mini
-  framework:
-    type: esp-idf
-    version: 5.1.2
-    platform_version: 6.5.0
-
-logger:
-  level: VERY_VERBOSE
-
-packages:
-  component_under_test: !include
-    file: $component_test_file
-    vars:
-      component_test_file: $component_test_file
diff --git a/tests/test_build_components/build_components_base.esp32-idf-51.yaml b/tests/test_build_components/build_components_base.esp32-idf-51.yaml
deleted file mode 100644
index b5e3dd6d83..0000000000
--- a/tests/test_build_components/build_components_base.esp32-idf-51.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-esphome:
-  name: componenttestesp32idf51
-  friendly_name: $component_name
-
-esp32:
-  board: nodemcu-32s
-  framework:
-    type: esp-idf
-    version: 5.1.2
-    platform_version: 6.5.0
-
-logger:
-  level: VERY_VERBOSE
-
-packages:
-  component_under_test: !include
-    file: $component_test_file
-    vars:
-      component_test_file: $component_test_file
diff --git a/tests/test_build_components/build_components_base.esp32-s2-idf-51.yaml b/tests/test_build_components/build_components_base.esp32-s2-idf-51.yaml
deleted file mode 100644
index 11b077509e..0000000000
--- a/tests/test_build_components/build_components_base.esp32-s2-idf-51.yaml
+++ /dev/null
@@ -1,20 +0,0 @@
-esphome:
-  name: componenttestesp32s2idf51
-  friendly_name: $component_name
-
-esp32:
-  board: esp32-s2-saola-1
-  variant: ESP32S2
-  framework:
-    type: esp-idf
-    version: 5.1.2
-    platform_version: 6.5.0
-
-logger:
-  level: VERY_VERBOSE
-
-packages:
-  component_under_test: !include
-    file: $component_test_file
-    vars:
-      component_test_file: $component_test_file
diff --git a/tests/test_build_components/build_components_base.esp32-s3-idf-51.yaml b/tests/test_build_components/build_components_base.esp32-s3-idf-51.yaml
deleted file mode 100644
index 4357b3581b..0000000000
--- a/tests/test_build_components/build_components_base.esp32-s3-idf-51.yaml
+++ /dev/null
@@ -1,20 +0,0 @@
-esphome:
-  name: componenttestesp32s3idf51
-  friendly_name: $component_name
-
-esp32:
-  board: esp32s3box
-  variant: ESP32S3
-  framework:
-    type: esp-idf
-    version: 5.1.2
-    platform_version: 6.5.0
-
-logger:
-  level: VERY_VERBOSE
-
-packages:
-  component_under_test: !include
-    file: $component_test_file
-    vars:
-      component_test_file: $component_test_file

From 555bdac604147b1f918c6c11609589ac88f5c368 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 6 Dec 2024 13:11:31 +1300
Subject: [PATCH 255/282] Bump actions/cache from 4.1.2 to 4.2.0 (#7926)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/workflows/ci.yml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e4d3934c59..6ce4159da0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -46,7 +46,7 @@ jobs:
           python-version: ${{ env.DEFAULT_PYTHON }}
       - name: Restore Python virtual environment
         id: cache-venv
-        uses: actions/cache@v4.1.2
+        uses: actions/cache@v4.2.0
         with:
           path: venv
           # yamllint disable-line rule:line-length
@@ -302,14 +302,14 @@ jobs:
 
       - name: Cache platformio
         if: github.ref == 'refs/heads/dev'
-        uses: actions/cache@v4.1.2
+        uses: actions/cache@v4.2.0
         with:
           path: ~/.platformio
           key: platformio-${{ matrix.pio_cache_key }}
 
       - name: Cache platformio
         if: github.ref != 'refs/heads/dev'
-        uses: actions/cache/restore@v4.1.2
+        uses: actions/cache/restore@v4.2.0
         with:
           path: ~/.platformio
           key: platformio-${{ matrix.pio_cache_key }}

From d3a71a1d45fa3958fee3aac4e1afacb5408e726a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 6 Dec 2024 13:11:46 +1300
Subject: [PATCH 256/282] Bump actions/cache from 4.1.2 to 4.2.0 in
 /.github/actions/restore-python (#7925)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/actions/restore-python/action.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml
index 06c54578f5..6b87cf0170 100644
--- a/.github/actions/restore-python/action.yml
+++ b/.github/actions/restore-python/action.yml
@@ -22,7 +22,7 @@ runs:
         python-version: ${{ inputs.python-version }}
     - name: Restore Python virtual environment
       id: cache-venv
-      uses: actions/cache/restore@v4.1.2
+      uses: actions/cache/restore@v4.2.0
       with:
         path: venv
         # yamllint disable-line rule:line-length

From 4e3195b474e5ffeda909e9d8933f8611da758779 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Fri, 6 Dec 2024 11:16:59 +1100
Subject: [PATCH 257/282] [esp32] Fix crash with empty `platformio_options:`
 value (#7920)

---
 esphome/components/esp32/__init__.py | 16 ++++++----------
 1 file changed, 6 insertions(+), 10 deletions(-)

diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py
index 580c3fc081..b0bde75451 100644
--- a/esphome/components/esp32/__init__.py
+++ b/esphome/components/esp32/__init__.py
@@ -437,24 +437,20 @@ def _detect_variant(value):
 
 
 def final_validate(config):
-    if CONF_PLATFORMIO_OPTIONS not in fv.full_config.get()[CONF_ESPHOME]:
+    if not (
+        pio_options := fv.full_config.get()[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS)
+    ):
+        # Not specified or empty
         return config
 
     pio_flash_size_key = "board_upload.flash_size"
     pio_partitions_key = "board_build.partitions"
-    if (
-        CONF_PARTITIONS in config
-        and pio_partitions_key
-        in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS]
-    ):
+    if CONF_PARTITIONS in config and pio_partitions_key in pio_options:
         raise cv.Invalid(
             f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32"
         )
 
-    if (
-        pio_flash_size_key
-        in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS]
-    ):
+    if pio_flash_size_key in pio_options:
         raise cv.Invalid(
             f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
         )

From bfd75d736c3693f7b055e890bf9a36d51543f646 Mon Sep 17 00:00:00 2001
From: alorente <gitmaster@passific.fr>
Date: Fri, 6 Dec 2024 01:21:14 +0100
Subject: [PATCH 258/282] Add OCI Image Labels  (#7924)

---
 docker/Dockerfile | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index cc05849271..1754d951e5 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -196,8 +196,16 @@ LABEL \
     io.hass.name="ESPHome" \
     io.hass.description="Manage and program ESP8266/ESP32 microcontrollers through YAML configuration files" \
     io.hass.type="addon" \
-    io.hass.version="${BUILD_VERSION}"
+    io.hass.version="${BUILD_VERSION}" \
     # io.hass.arch is inherited from addon-debian-base
+    org.opencontainers.image.authors="The ESPHome Authors" \
+    org.opencontainers.image.title="ESPHome" \
+    org.opencontainers.image.description="Manage and program ESP8266/ESP32 microcontrollers through YAML configuration files" \
+    org.opencontainers.image.url="https://esphome.io/" \
+    org.opencontainers.image.documentation="https://esphome.io/" \
+    org.opencontainers.image.source="https://github.com/esphome/esphome" \
+    org.opencontainers.image.licenses="ESPHome" \
+    org.opencontainers.image.version=${BUILD_VERSION}
 
 
 

From 58123845ff5f5a61139b9144b266fc1d32136926 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Fri, 6 Dec 2024 14:11:11 +1300
Subject: [PATCH 259/282] Move docker oci labels to correct image (#7927)

---
 docker/Dockerfile | 22 +++++++++++++---------
 1 file changed, 13 insertions(+), 9 deletions(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index 1754d951e5..947e410fe1 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -163,6 +163,18 @@ ENTRYPOINT ["/entrypoint.sh"]
 CMD ["dashboard", "/config"]
 
 
+ARG BUILD_VERSION=dev
+
+# Labels
+LABEL \
+    org.opencontainers.image.authors="The ESPHome Authors" \
+    org.opencontainers.image.title="ESPHome" \
+    org.opencontainers.image.description="Manage and program ESP8266/ESP32 microcontrollers through YAML configuration files" \
+    org.opencontainers.image.url="https://esphome.io/" \
+    org.opencontainers.image.documentation="https://esphome.io/" \
+    org.opencontainers.image.source="https://github.com/esphome/esphome" \
+    org.opencontainers.image.licenses="ESPHome" \
+    org.opencontainers.image.version=${BUILD_VERSION}
 
 
 # ======================= hassio-type image =======================
@@ -196,16 +208,8 @@ LABEL \
     io.hass.name="ESPHome" \
     io.hass.description="Manage and program ESP8266/ESP32 microcontrollers through YAML configuration files" \
     io.hass.type="addon" \
-    io.hass.version="${BUILD_VERSION}" \
+    io.hass.version="${BUILD_VERSION}"
     # io.hass.arch is inherited from addon-debian-base
-    org.opencontainers.image.authors="The ESPHome Authors" \
-    org.opencontainers.image.title="ESPHome" \
-    org.opencontainers.image.description="Manage and program ESP8266/ESP32 microcontrollers through YAML configuration files" \
-    org.opencontainers.image.url="https://esphome.io/" \
-    org.opencontainers.image.documentation="https://esphome.io/" \
-    org.opencontainers.image.source="https://github.com/esphome/esphome" \
-    org.opencontainers.image.licenses="ESPHome" \
-    org.opencontainers.image.version=${BUILD_VERSION}
 
 
 

From b0e3ac01e83258329244ca0efc557c2225c7d790 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Fri, 6 Dec 2024 15:24:20 +1300
Subject: [PATCH 260/282] Update project description (#7928)

---
 docker/Dockerfile | 4 ++--
 pyproject.toml    | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index 947e410fe1..0bb558d35e 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -169,7 +169,7 @@ ARG BUILD_VERSION=dev
 LABEL \
     org.opencontainers.image.authors="The ESPHome Authors" \
     org.opencontainers.image.title="ESPHome" \
-    org.opencontainers.image.description="Manage and program ESP8266/ESP32 microcontrollers through YAML configuration files" \
+    org.opencontainers.image.description="ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems" \
     org.opencontainers.image.url="https://esphome.io/" \
     org.opencontainers.image.documentation="https://esphome.io/" \
     org.opencontainers.image.source="https://github.com/esphome/esphome" \
@@ -206,7 +206,7 @@ RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
 # Labels
 LABEL \
     io.hass.name="ESPHome" \
-    io.hass.description="Manage and program ESP8266/ESP32 microcontrollers through YAML configuration files" \
+    io.hass.description="ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems" \
     io.hass.type="addon" \
     io.hass.version="${BUILD_VERSION}"
     # io.hass.arch is inherited from addon-debian-base
diff --git a/pyproject.toml b/pyproject.toml
index cfc279845f..7789f6d645 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
 [project]
 name        = "esphome"
 license     = {text = "MIT"}
-description = "Make creating custom firmwares for ESP32/ESP8266 super easy."
+description = "ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems."
 readme      = "README.md"
 authors     = [
   {name = "The ESPHome Authors", email = "esphome@nabucasa.com"}

From 749a5e3348e5da84b8a199ef49ea5122d993d6c1 Mon Sep 17 00:00:00 2001
From: Keith Burzinski <kbx81x@gmail.com>
Date: Thu, 5 Dec 2024 20:41:53 -0600
Subject: [PATCH 261/282] [modbus] More clean-up (#7921)

---
 .../components/modbus_controller/modbus_controller.cpp | 10 +++++-----
 .../modbus_controller/switch/modbus_switch.cpp         |  2 +-
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp
index 641ba68223..3f487abc94 100644
--- a/esphome/components/modbus_controller/modbus_controller.cpp
+++ b/esphome/components/modbus_controller/modbus_controller.cpp
@@ -152,9 +152,9 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t
 }
 
 SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const {
-  auto reg_it = find_if(begin(this->register_ranges_), end(this->register_ranges_), [=](RegisterRange const &r) {
-    return (r.start_address == start_address && r.register_type == register_type);
-  });
+  auto reg_it = std::find_if(
+      std::begin(this->register_ranges_), std::end(this->register_ranges_),
+      [=](RegisterRange const &r) { return (r.start_address == start_address && r.register_type == register_type); });
 
   if (reg_it == this->register_ranges_.end()) {
     ESP_LOGE(TAG, "No matching range for sensor found - start_address : 0x%X", start_address);
@@ -375,12 +375,12 @@ void ModbusController::loop() {
   if (!this->incoming_queue_.empty()) {
     auto &message = this->incoming_queue_.front();
     if (message != nullptr)
-      process_modbus_data_(message.get());
+      this->process_modbus_data_(message.get());
     this->incoming_queue_.pop();
 
   } else {
     // all messages processed send pending commands
-    send_next_command_();
+    this->send_next_command_();
   }
 }
 
diff --git a/esphome/components/modbus_controller/switch/modbus_switch.cpp b/esphome/components/modbus_controller/switch/modbus_switch.cpp
index ec29eca7f8..b729e2659f 100644
--- a/esphome/components/modbus_controller/switch/modbus_switch.cpp
+++ b/esphome/components/modbus_controller/switch/modbus_switch.cpp
@@ -97,7 +97,7 @@ void ModbusSwitch::write_state(bool state) {
     }
   }
   this->parent_->queue_command(cmd);
-  publish_state(state);
+  this->publish_state(state);
 }
 // ModbusSwitch end
 }  // namespace modbus_controller

From 39cbc6b183c69fce10eb77c1c9393687335b7a30 Mon Sep 17 00:00:00 2001
From: Oleg Tarasov <me@olegtarasov.email>
Date: Tue, 26 Nov 2024 00:47:01 +0300
Subject: [PATCH 262/282] [opentherm] Fix out of memory errors on ESP8266
 (#7835)

---
 esphome/components/opentherm/hub.cpp       | 13 ++++----
 esphome/components/opentherm/opentherm.cpp | 36 ++++++----------------
 esphome/components/opentherm/opentherm.h   |  7 ++---
 esphome/core/helpers.cpp                   | 12 ++++++++
 esphome/core/helpers.h                     |  8 +++++
 5 files changed, 39 insertions(+), 37 deletions(-)

diff --git a/esphome/components/opentherm/hub.cpp b/esphome/components/opentherm/hub.cpp
index dfa8ea95c5..aac2966ed1 100644
--- a/esphome/components/opentherm/hub.cpp
+++ b/esphome/components/opentherm/hub.cpp
@@ -138,7 +138,7 @@ OpenthermHub::OpenthermHub() : Component(), in_pin_{}, out_pin_{} {}
 void OpenthermHub::process_response(OpenthermData &data) {
   ESP_LOGD(TAG, "Received OpenTherm response with id %d (%s)", data.id,
            this->opentherm_->message_id_to_str((MessageId) data.id));
-  ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(data).c_str());
+  this->opentherm_->debug_data(data);
 
   switch (data.id) {
     OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_RESPONSE_MESSAGE, OPENTHERM_MESSAGE_RESPONSE_ENTITY, ,
@@ -315,7 +315,7 @@ void OpenthermHub::start_conversation_() {
 
   ESP_LOGD(TAG, "Sending request with id %d (%s)", request.id,
            this->opentherm_->message_id_to_str((MessageId) request.id));
-  ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(request).c_str());
+  this->opentherm_->debug_data(request);
   // Send the request
   this->last_conversation_start_ = millis();
   this->opentherm_->send(request);
@@ -340,19 +340,18 @@ void OpenthermHub::stop_opentherm_() {
   this->opentherm_->stop();
   this->last_conversation_end_ = millis();
 }
-
 void OpenthermHub::handle_protocol_write_error_() {
   ESP_LOGW(TAG, "Error while sending request: %s",
            this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode()));
-  ESP_LOGW(TAG, "%s", this->opentherm_->debug_data(this->last_request_).c_str());
+  this->opentherm_->debug_data(this->last_request_);
 }
-
 void OpenthermHub::handle_protocol_read_error_() {
   OpenThermError error;
   this->opentherm_->get_protocol_error(error);
-  ESP_LOGW(TAG, "Protocol error occured while receiving response: %s", this->opentherm_->debug_error(error).c_str());
+  ESP_LOGW(TAG, "Protocol error occured while receiving response: %s",
+           this->opentherm_->protocol_error_to_to_str(error.error_type));
+  this->opentherm_->debug_error(error);
 }
-
 void OpenthermHub::handle_timeout_error_() {
   ESP_LOGW(TAG, "Receive response timed out at a protocol level");
   this->stop_opentherm_();
diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp
index 26c707f9a0..62cfcdceea 100644
--- a/esphome/components/opentherm/opentherm.cpp
+++ b/esphome/components/opentherm/opentherm.cpp
@@ -15,15 +15,11 @@
 #include "Arduino.h"
 #endif
 #include <string>
-#include <sstream>
-#include <bitset>
 
 namespace esphome {
 namespace opentherm {
 
 using std::string;
-using std::bitset;
-using std::stringstream;
 using std::to_string;
 
 static const char *const TAG = "opentherm";
@@ -545,29 +541,17 @@ const char *OpenTherm::message_id_to_str(MessageId id) {
   }
 }
 
-string OpenTherm::debug_data(OpenthermData &data) {
-  stringstream result;
-  result << bitset<8>(data.type) << " " << bitset<8>(data.id) << " " << bitset<8>(data.valueHB) << " "
-         << bitset<8>(data.valueLB) << "\n";
-  result << "type: " << this->message_type_to_str((MessageType) data.type) << "; ";
-  result << "id: " << to_string(data.id) << "; ";
-  result << "HB: " << to_string(data.valueHB) << "; ";
-  result << "LB: " << to_string(data.valueLB) << "; ";
-  result << "uint_16: " << to_string(data.u16()) << "; ";
-  result << "float: " << to_string(data.f88());
-
-  return result.str();
+void OpenTherm::debug_data(OpenthermData &data) {
+  ESP_LOGD(TAG, "%s %s %s %s", format_bin(data.type).c_str(), format_bin(data.id).c_str(),
+           format_bin(data.valueHB).c_str(), format_bin(data.valueLB).c_str());
+  ESP_LOGD(TAG, "type: %s; id: %s; HB: %s; LB: %s; uint_16: %s; float: %s",
+           this->message_type_to_str((MessageType) data.type), to_string(data.id).c_str(),
+           to_string(data.valueHB).c_str(), to_string(data.valueLB).c_str(), to_string(data.u16()).c_str(),
+           to_string(data.f88()).c_str());
 }
-std::string OpenTherm::debug_error(OpenThermError &error) {
-  stringstream result;
-  result << "type: " << this->protocol_error_to_to_str(error.error_type) << "; ";
-  result << "data: ";
-  result << format_hex(error.data);
-  result << "; clock: " << to_string(clock_);
-  result << "; capture: " << bitset<32>(error.capture);
-  result << "; bit_pos: " << to_string(error.bit_pos);
-
-  return result.str();
+void OpenTherm::debug_error(OpenThermError &error) const {
+  ESP_LOGD(TAG, "data: %s; clock: %s; capture: %s; bit_pos: %s", format_hex(error.data).c_str(),
+           to_string(clock_).c_str(), format_bin(error.capture).c_str(), to_string(error.bit_pos).c_str());
 }
 
 float OpenthermData::f88() { return ((float) this->s16()) / 256.0; }
diff --git a/esphome/components/opentherm/opentherm.h b/esphome/components/opentherm/opentherm.h
index 85f4611125..76710af5f5 100644
--- a/esphome/components/opentherm/opentherm.h
+++ b/esphome/components/opentherm/opentherm.h
@@ -8,10 +8,9 @@
 #pragma once
 
 #include <string>
-#include <sstream>
-#include <iomanip>
 #include "esphome/core/hal.h"
 #include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
 
 #if defined(ESP32) || defined(USE_ESP_IDF)
 #include "driver/timer.h"
@@ -318,8 +317,8 @@ class OpenTherm {
 
   OperationMode get_mode() { return mode_; }
 
-  std::string debug_data(OpenthermData &data);
-  std::string debug_error(OpenThermError &error);
+  void debug_data(OpenthermData &data);
+  void debug_error(OpenThermError &error) const;
 
   const char *protocol_error_to_to_str(ProtocolErrorType error_type);
   const char *message_type_to_str(MessageType message_type);
diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp
index dae60a4e1d..befc84516c 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -397,6 +397,18 @@ std::string format_hex_pretty(const uint16_t *data, size_t length) {
 }
 std::string format_hex_pretty(const std::vector<uint16_t> &data) { return format_hex_pretty(data.data(), data.size()); }
 
+std::string format_bin(const uint8_t *data, size_t length) {
+  std::string result;
+  result.resize(length * 8);
+  for (size_t byte_idx = 0; byte_idx < length; byte_idx++) {
+    for (size_t bit_idx = 0; bit_idx < 8; bit_idx++) {
+      result[byte_idx * 8 + bit_idx] = ((data[byte_idx] >> (7 - bit_idx)) & 1) + '0';
+    }
+  }
+
+  return result;
+}
+
 ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) {
   if (on == nullptr && strcasecmp(str, "on") == 0)
     return PARSE_ON;
diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h
index 43001bafdd..305ec47f76 100644
--- a/esphome/core/helpers.h
+++ b/esphome/core/helpers.h
@@ -420,6 +420,14 @@ template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::stri
   return format_hex_pretty(reinterpret_cast<uint8_t *>(&val), sizeof(T));
 }
 
+/// Format the byte array \p data of length \p len in binary.
+std::string format_bin(const uint8_t *data, size_t length);
+/// Format an unsigned integer in binary, starting with the most significant byte.
+template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_bin(T val) {
+  val = convert_big_endian(val);
+  return format_bin(reinterpret_cast<uint8_t *>(&val), sizeof(T));
+}
+
 /// Return values for parse_on_off().
 enum ParseOnOffState {
   PARSE_NONE = 0,

From e623989878b3f73b5512c6380b4d4446183443da Mon Sep 17 00:00:00 2001
From: Samuel Sieb <samuel-github@sieb.net>
Date: Mon, 25 Nov 2024 14:15:01 -1000
Subject: [PATCH 263/282] fix local time timestamp calculation (#7807)

Co-authored-by: Samuel Sieb <samuel@sieb.net>
---
 .../components/datetime/datetime_entity.cpp   |  4 +--
 esphome/core/time.cpp                         | 34 +++++++++++--------
 esphome/core/time.h                           |  4 +--
 3 files changed, 21 insertions(+), 21 deletions(-)

diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp
index f215b7acb5..3d92194efa 100644
--- a/esphome/components/datetime/datetime_entity.cpp
+++ b/esphome/components/datetime/datetime_entity.cpp
@@ -60,9 +60,7 @@ ESPTime DateTimeEntity::state_as_esptime() const {
   obj.hour = this->hour_;
   obj.minute = this->minute_;
   obj.second = this->second_;
-  obj.day_of_week = 1;  // Required to be valid for recalc_timestamp_local but not used.
-  obj.day_of_year = 1;  // Required to be valid for recalc_timestamp_local but not used.
-  obj.recalc_timestamp_local(false);
+  obj.recalc_timestamp_local();
   return obj;
 }
 
diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp
index f7aa4fdddb..31977d972b 100644
--- a/esphome/core/time.cpp
+++ b/esphome/core/time.cpp
@@ -5,20 +5,18 @@
 
 namespace esphome {
 
-bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); }
-
 uint8_t days_in_month(uint8_t month, uint16_t year) {
   static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
-  uint8_t days = DAYS_IN_MONTH[month];
-  if (month == 2 && is_leap_year(year))
+  if (month == 2 && (year % 4 == 0))
     return 29;
-  return days;
+  return DAYS_IN_MONTH[month];
 }
 
 size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) {
   struct tm c_tm = this->to_c_tm();
   return ::strftime(buffer, buffer_len, format, &c_tm);
 }
+
 ESPTime ESPTime::from_c_tm(struct tm *c_tm, time_t c_time) {
   ESPTime res{};
   res.second = uint8_t(c_tm->tm_sec);
@@ -33,6 +31,7 @@ ESPTime ESPTime::from_c_tm(struct tm *c_tm, time_t c_time) {
   res.timestamp = c_time;
   return res;
 }
+
 struct tm ESPTime::to_c_tm() {
   struct tm c_tm {};
   c_tm.tm_sec = this->second;
@@ -46,6 +45,7 @@ struct tm ESPTime::to_c_tm() {
   c_tm.tm_isdst = this->is_dst;
   return c_tm;
 }
+
 std::string ESPTime::strftime(const std::string &format) {
   std::string timestr;
   timestr.resize(format.size() * 4);
@@ -142,6 +142,7 @@ void ESPTime::increment_second() {
     this->year++;
   }
 }
+
 void ESPTime::increment_day() {
   this->timestamp += 86400;
 
@@ -159,23 +160,22 @@ void ESPTime::increment_day() {
     this->year++;
   }
 }
+
 void ESPTime::recalc_timestamp_utc(bool use_day_of_year) {
   time_t res = 0;
-
   if (!this->fields_in_range()) {
     this->timestamp = -1;
     return;
   }
 
   for (int i = 1970; i < this->year; i++)
-    res += is_leap_year(i) ? 366 : 365;
+    res += (year % 4 == 0) ? 366 : 365;
 
   if (use_day_of_year) {
     res += this->day_of_year - 1;
   } else {
     for (int i = 1; i < this->month; i++)
       res += days_in_month(i, this->year);
-
     res += this->day_of_month - 1;
   }
 
@@ -188,13 +188,17 @@ void ESPTime::recalc_timestamp_utc(bool use_day_of_year) {
   this->timestamp = res;
 }
 
-void ESPTime::recalc_timestamp_local(bool use_day_of_year) {
-  this->recalc_timestamp_utc(use_day_of_year);
-  this->timestamp -= ESPTime::timezone_offset();
-  ESPTime temp = ESPTime::from_epoch_local(this->timestamp);
-  if (temp.is_dst) {
-    this->timestamp -= 3600;
-  }
+void ESPTime::recalc_timestamp_local() {
+  struct tm tm;
+
+  tm.tm_year = this->year - 1900;
+  tm.tm_mon = this->month - 1;
+  tm.tm_mday = this->day_of_month;
+  tm.tm_hour = this->hour;
+  tm.tm_min = this->minute;
+  tm.tm_sec = this->second;
+
+  this->timestamp = mktime(&tm);
 }
 
 int32_t ESPTime::timezone_offset() {
diff --git a/esphome/core/time.h b/esphome/core/time.h
index bce1108d93..5cbd9369fb 100644
--- a/esphome/core/time.h
+++ b/esphome/core/time.h
@@ -9,8 +9,6 @@ namespace esphome {
 
 template<typename T> bool increment_time_value(T &current, uint16_t begin, uint16_t end);
 
-bool is_leap_year(uint32_t year);
-
 uint8_t days_in_month(uint8_t month, uint16_t year);
 
 /// A more user-friendly version of struct tm from time.h
@@ -100,7 +98,7 @@ struct ESPTime {
   void recalc_timestamp_utc(bool use_day_of_year = true);
 
   /// Recalculate the timestamp field from the other fields of this ESPTime instance assuming local fields.
-  void recalc_timestamp_local(bool use_day_of_year = true);
+  void recalc_timestamp_local();
 
   /// Convert this ESPTime instance back to a tm struct.
   struct tm to_c_tm();

From 3bac45e737655af9f8fbf7734e3a17620fcdec47 Mon Sep 17 00:00:00 2001
From: guillempages <guillempages@users.noreply.github.com>
Date: Thu, 28 Nov 2024 04:55:20 +0100
Subject: [PATCH 264/282] [online_image]Don't access decoder if not initialized
 (#7882)

---
 esphome/components/online_image/png_image.cpp | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/online_image/png_image.cpp
index c8e215a91d..4c4c22f9b7 100644
--- a/esphome/components/online_image/png_image.cpp
+++ b/esphome/components/online_image/png_image.cpp
@@ -49,6 +49,10 @@ void PngDecoder::prepare(uint32_t download_size) {
 }
 
 int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
+  if (!this->pngle_) {
+    ESP_LOGE(TAG, "PNG decoder engine not initialized!");
+    return -1;
+  }
   if (size < 256 && size < this->download_size_ - this->decoded_bytes_) {
     ESP_LOGD(TAG, "Waiting for data");
     return 0;

From 5717d557f5f421ae3402a5d13510dda81ac4bebd Mon Sep 17 00:00:00 2001
From: FreeBear-nc <67865163+FreeBear-nc@users.noreply.github.com>
Date: Thu, 28 Nov 2024 03:56:37 +0000
Subject: [PATCH 265/282] Add IRAM_ATTR to all functions used during interrupts
 on esp8266 chips. (#7840)

---
 esphome/components/opentherm/opentherm.cpp | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp
index 62cfcdceea..c56b49ccb8 100644
--- a/esphome/components/opentherm/opentherm.cpp
+++ b/esphome/components/opentherm/opentherm.cpp
@@ -220,7 +220,7 @@ void IRAM_ATTR OpenTherm::bit_read_(uint8_t value) {
   this->bit_pos_++;
 }
 
-ProtocolErrorType OpenTherm::verify_stop_bit_(uint8_t value) {
+ProtocolErrorType IRAM_ATTR OpenTherm::verify_stop_bit_(uint8_t value) {
   if (value) {  // stop bit detected
     return check_parity_(this->data_) ? ProtocolErrorType::NO_ERROR : ProtocolErrorType::PARITY_ERROR;
   } else {  // no stop bit detected, error
@@ -365,7 +365,7 @@ void IRAM_ATTR OpenTherm::stop_timer_() {
 
 #ifdef ESP8266
 // 5 kHz timer_
-void OpenTherm::start_read_timer_() {
+void IRAM_ATTR OpenTherm::start_read_timer_() {
   InterruptLock const lock;
   timer1_attachInterrupt(OpenTherm::esp8266_timer_isr);
   timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP);  // 5MHz (5 ticks/us - 1677721.4 us max)
@@ -373,14 +373,14 @@ void OpenTherm::start_read_timer_() {
 }
 
 // 2 kHz timer_
-void OpenTherm::start_write_timer_() {
+void IRAM_ATTR OpenTherm::start_write_timer_() {
   InterruptLock const lock;
   timer1_attachInterrupt(OpenTherm::esp8266_timer_isr);
   timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP);  // 5MHz (5 ticks/us - 1677721.4 us max)
   timer1_write(2500);                            // 2kHz
 }
 
-void OpenTherm::stop_timer_() {
+void IRAM_ATTR OpenTherm::stop_timer_() {
   InterruptLock const lock;
   timer1_disable();
   timer1_detachInterrupt();
@@ -389,7 +389,7 @@ void OpenTherm::stop_timer_() {
 #endif  // END ESP8266
 
 // https://stackoverflow.com/questions/21617970/how-to-check-if-value-has-even-parity-of-bits-or-odd
-bool OpenTherm::check_parity_(uint32_t val) {
+bool IRAM_ATTR OpenTherm::check_parity_(uint32_t val) {
   val ^= val >> 16;
   val ^= val >> 8;
   val ^= val >> 4;

From 5fcd26bfe964f670759e08bc89386ee9c2b1c0d4 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Thu, 28 Nov 2024 16:57:11 +1300
Subject: [PATCH 266/282] [st7920] Remove unnecessary warning when drawing
 outside display bounds (#7868)

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

diff --git a/esphome/components/st7920/st7920.cpp b/esphome/components/st7920/st7920.cpp
index f336d24e24..171e7095dd 100644
--- a/esphome/components/st7920/st7920.cpp
+++ b/esphome/components/st7920/st7920.cpp
@@ -1,7 +1,7 @@
 #include "st7920.h"
-#include "esphome/core/log.h"
-#include "esphome/core/application.h"
 #include "esphome/components/display/display_buffer.h"
+#include "esphome/core/application.h"
+#include "esphome/core/log.h"
 
 namespace esphome {
 namespace st7920 {
@@ -118,7 +118,6 @@ size_t ST7920::get_buffer_length_() {
 
 void HOT ST7920::draw_absolute_pixel_internal(int x, int y, Color color) {
   if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) {
-    ESP_LOGW(TAG, "Position out of area: %dx%d", x, y);
     return;
   }
   int width = this->get_width_internal() / 8u;

From f042c6e643a0c1b67816cc1824611b98d6317ac4 Mon Sep 17 00:00:00 2001
From: Krzysztof Zdulski <krzys.zdulski@gmail.com>
Date: Fri, 29 Nov 2024 22:05:00 +0100
Subject: [PATCH 267/282] Fix recalc_timestamp_utc (#7894)

---
 esphome/core/time.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp
index 31977d972b..66a0e1c0a7 100644
--- a/esphome/core/time.cpp
+++ b/esphome/core/time.cpp
@@ -169,7 +169,7 @@ void ESPTime::recalc_timestamp_utc(bool use_day_of_year) {
   }
 
   for (int i = 1970; i < this->year; i++)
-    res += (year % 4 == 0) ? 366 : 365;
+    res += (i % 4 == 0) ? 366 : 365;
 
   if (use_day_of_year) {
     res += this->day_of_year - 1;

From 982ce1db727b103250b69c91e3edaac918bd5f37 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Mon, 2 Dec 2024 05:10:18 +1300
Subject: [PATCH 268/282] Cast port to int for ota pushing (#7888)

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

diff --git a/esphome/__main__.py b/esphome/__main__.py
index 86d529e1bf..dce041e5ac 100644
--- a/esphome/__main__.py
+++ b/esphome/__main__.py
@@ -363,7 +363,7 @@ def upload_program(config, args, host):
 
     from esphome import espota2
 
-    remote_port = ota_conf[CONF_PORT]
+    remote_port = int(ota_conf[CONF_PORT])
     password = ota_conf.get(CONF_PASSWORD, "")
 
     if (

From d0958f7cf28dd68e7149ae32d06b3489caf55c11 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 3 Dec 2024 17:50:11 +1100
Subject: [PATCH 269/282] [lvgl] Bugfixes (#7896)

---
 esphome/components/lvgl/defines.py      | 2 +-
 esphome/components/lvgl/lvgl_esphome.h  | 3 +++
 esphome/components/lvgl/widgets/line.py | 6 ++++++
 tests/components/lvgl/lvgl-package.yaml | 4 ++--
 4 files changed, 12 insertions(+), 3 deletions(-)

diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py
index ea345fa55c..bb7d25c2c7 100644
--- a/esphome/components/lvgl/defines.py
+++ b/esphome/components/lvgl/defines.py
@@ -38,7 +38,7 @@ def literal(arg):
 def call_lambda(lamb: LambdaExpression):
     expr = lamb.content.strip()
     if expr.startswith("return") and expr.endswith(";"):
-        return expr[7:][:-1]
+        return expr[6:][:-1].strip()
     return f"{lamb}()"
 
 
diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h
index 208cb1cbd5..7bc6b00cf5 100644
--- a/esphome/components/lvgl/lvgl_esphome.h
+++ b/esphome/components/lvgl/lvgl_esphome.h
@@ -56,6 +56,9 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT
 inline void lv_img_set_src(lv_obj_t *obj, esphome::image::Image *image) {
   lv_img_set_src(obj, image->get_lv_img_dsc());
 }
+inline void lv_disp_set_bg_image(lv_disp_t *disp, esphome::image::Image *image) {
+  lv_disp_set_bg_image(disp, image->get_lv_img_dsc());
+}
 #endif  // USE_LVGL_IMAGE
 
 // Parent class for things that wrap an LVGL object
diff --git a/esphome/components/lvgl/widgets/line.py b/esphome/components/lvgl/widgets/line.py
index 548dfa8452..0156fb1780 100644
--- a/esphome/components/lvgl/widgets/line.py
+++ b/esphome/components/lvgl/widgets/line.py
@@ -35,6 +35,11 @@ LINE_SCHEMA = {
     cv.GenerateID(CONF_POINT_LIST_ID): cv.declare_id(lv_point_t),
 }
 
+LINE_MODIFY_SCHEMA = {
+    cv.Optional(CONF_POINTS): cv_point_list,
+    cv.GenerateID(CONF_POINT_LIST_ID): cv.declare_id(lv_point_t),
+}
+
 
 class LineType(WidgetType):
     def __init__(self):
@@ -43,6 +48,7 @@ class LineType(WidgetType):
             LvType("lv_line_t"),
             (CONF_MAIN,),
             LINE_SCHEMA,
+            modify_schema=LINE_MODIFY_SCHEMA,
         )
 
     async def to_code(self, w: Widget, config):
diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml
index db0443b3bb..b83ff2a3d5 100644
--- a/tests/components/lvgl/lvgl-package.yaml
+++ b/tests/components/lvgl/lvgl-package.yaml
@@ -333,7 +333,7 @@ lvgl:
             id: button_button
             width: 20%
             height: 10%
-            transform_angle: !lambda return 180*100;
+            transform_angle: !lambda return(180*100);
             arc_width: !lambda return 4;
             border_width: !lambda return 6;
             shadow_ofs_x: !lambda return 6;
@@ -577,7 +577,7 @@ lvgl:
               - 180, 60
               - 240, 10
             on_click:
-              - lvgl.widget.update:
+              - lvgl.line.update:
                   id: lv_line_id
                   line_color: 0xFFFF
               - lvgl.page.next:

From 86ae1c59315261fc36c207b4b76d32385e68a3ba Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Wed, 4 Dec 2024 07:48:50 +1100
Subject: [PATCH 270/282] [lvgl] Fix msgbox content (#7912)

---
 esphome/components/lvgl/widgets/msgbox.py | 3 ++-
 tests/components/lvgl/lvgl-package.yaml   | 7 +++++++
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/esphome/components/lvgl/widgets/msgbox.py b/esphome/components/lvgl/widgets/msgbox.py
index be0f2100d7..c3393940b6 100644
--- a/esphome/components/lvgl/widgets/msgbox.py
+++ b/esphome/components/lvgl/widgets/msgbox.py
@@ -29,7 +29,7 @@ from ..lvcode import (
 )
 from ..schemas import STYLE_SCHEMA, STYLED_TEXT_SCHEMA, container_schema, part_schema
 from ..types import LV_EVENT, char_ptr, lv_obj_t
-from . import Widget, set_obj_properties
+from . import Widget, add_widgets, set_obj_properties
 from .button import button_spec
 from .buttonmatrix import (
     BUTTONMATRIX_BUTTON_SCHEMA,
@@ -119,6 +119,7 @@ async def msgbox_to_code(top_layer, conf):
         button_style = {CONF_ITEMS: button_style}
         await set_obj_properties(buttonmatrix_widget, button_style)
     await set_obj_properties(msgbox_widget, conf)
+    await add_widgets(msgbox_widget, conf)
     async with LambdaContext(EVENT_ARG, where=messagebox_id) as close_action:
         outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN")
     if close_button:
diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml
index b83ff2a3d5..e5df30f136 100644
--- a/tests/components/lvgl/lvgl-package.yaml
+++ b/tests/components/lvgl/lvgl-package.yaml
@@ -109,6 +109,10 @@ lvgl:
       close_button: true
       title: Messagebox
       bg_color: 0xffff
+      widgets:
+        - label:
+            text: Hello Msgbox
+            id: msgbox_label
       body:
         text: This is a sample messagebox
         bg_color: 0x808080
@@ -137,6 +141,9 @@ lvgl:
         - lvgl.widget.focus: mark
         - lvgl.widget.redraw: hello_label
         - lvgl.widget.redraw:
+        - lvgl.label.update:
+            id: msgbox_label
+            text: Unloaded
       on_all_events:
         logger.log:
           format: "Event %s"

From c8ec0bb7eab3d77cb5b62a227d4041677f1225ad Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Fri, 6 Dec 2024 11:16:59 +1100
Subject: [PATCH 271/282] [esp32] Fix crash with empty `platformio_options:`
 value (#7920)

---
 esphome/components/esp32/__init__.py | 16 ++++++----------
 1 file changed, 6 insertions(+), 10 deletions(-)

diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py
index 61fbb53e3a..aaef68fa27 100644
--- a/esphome/components/esp32/__init__.py
+++ b/esphome/components/esp32/__init__.py
@@ -355,24 +355,20 @@ def _detect_variant(value):
 
 
 def final_validate(config):
-    if CONF_PLATFORMIO_OPTIONS not in fv.full_config.get()[CONF_ESPHOME]:
+    if not (
+        pio_options := fv.full_config.get()[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS)
+    ):
+        # Not specified or empty
         return config
 
     pio_flash_size_key = "board_upload.flash_size"
     pio_partitions_key = "board_build.partitions"
-    if (
-        CONF_PARTITIONS in config
-        and pio_partitions_key
-        in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS]
-    ):
+    if CONF_PARTITIONS in config and pio_partitions_key in pio_options:
         raise cv.Invalid(
             f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32"
         )
 
-    if (
-        pio_flash_size_key
-        in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS]
-    ):
+    if pio_flash_size_key in pio_options:
         raise cv.Invalid(
             f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
         )

From c80e035bd56fbfe89475fdb149b0ff26fb279e61 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Fri, 6 Dec 2024 15:55:51 +1300
Subject: [PATCH 272/282] Bump version to 2024.11.3

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

diff --git a/esphome/const.py b/esphome/const.py
index 4b19e2865d..ae7feda6d8 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,6 +1,6 @@
 """Constants used by esphome."""
 
-__version__ = "2024.11.2"
+__version__ = "2024.11.3"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 VALID_SUBSTITUTIONS_CHARACTERS = (

From 9d000e9abf3832497e8a6640e66c0eb4c8568060 Mon Sep 17 00:00:00 2001
From: Citric Lee <37475446+limengdu@users.noreply.github.com>
Date: Mon, 9 Dec 2024 10:28:41 +0800
Subject: [PATCH 273/282] Add: Seeed Studio MR60BHA2 mmWave Sensor (#7589)

Co-authored-by: Spencer Yan <spencer@spenyan.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
---
 CODEOWNERS                                    |   1 +
 esphome/components/seeed_mr60bha2/__init__.py |  41 +++++
 .../seeed_mr60bha2/seeed_mr60bha2.cpp         | 173 ++++++++++++++++++
 .../seeed_mr60bha2/seeed_mr60bha2.h           |  61 ++++++
 esphome/components/seeed_mr60bha2/sensor.py   |  57 ++++++
 esphome/const.py                              |   2 +
 tests/components/seeed_mr60bha2/common.yaml   |  19 ++
 .../seeed_mr60bha2/test.esp32-c3-ard.yaml     |   5 +
 .../seeed_mr60bha2/test.esp32-c3-idf.yaml     |   5 +
 9 files changed, 364 insertions(+)
 create mode 100644 esphome/components/seeed_mr60bha2/__init__.py
 create mode 100644 esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp
 create mode 100644 esphome/components/seeed_mr60bha2/seeed_mr60bha2.h
 create mode 100644 esphome/components/seeed_mr60bha2/sensor.py
 create mode 100644 tests/components/seeed_mr60bha2/common.yaml
 create mode 100644 tests/components/seeed_mr60bha2/test.esp32-c3-ard.yaml
 create mode 100644 tests/components/seeed_mr60bha2/test.esp32-c3-idf.yaml

diff --git a/CODEOWNERS b/CODEOWNERS
index 74c205b302..404ad35efc 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -355,6 +355,7 @@ esphome/components/sdl/* @clydebarrow
 esphome/components/sdm_meter/* @jesserockz @polyfaces
 esphome/components/sdp3x/* @Azimath
 esphome/components/seeed_mr24hpc1/* @limengdu
+esphome/components/seeed_mr60bha2/* @limengdu
 esphome/components/seeed_mr60fda2/* @limengdu
 esphome/components/selec_meter/* @sourabhjaiswal
 esphome/components/select/* @esphome/core
diff --git a/esphome/components/seeed_mr60bha2/__init__.py b/esphome/components/seeed_mr60bha2/__init__.py
new file mode 100644
index 0000000000..87bdbbd003
--- /dev/null
+++ b/esphome/components/seeed_mr60bha2/__init__.py
@@ -0,0 +1,41 @@
+import esphome.codegen as cg
+from esphome.components import uart
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+CODEOWNERS = ["@limengdu"]
+DEPENDENCIES = ["uart"]
+MULTI_CONF = True
+
+mr60bha2_ns = cg.esphome_ns.namespace("seeed_mr60bha2")
+
+MR60BHA2Component = mr60bha2_ns.class_(
+    "MR60BHA2Component", cg.Component, uart.UARTDevice
+)
+
+CONF_MR60BHA2_ID = "mr60bha2_id"
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(MR60BHA2Component),
+        }
+    )
+    .extend(uart.UART_DEVICE_SCHEMA)
+    .extend(cv.COMPONENT_SCHEMA)
+)
+
+FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
+    "seeed_mr60bha2",
+    require_tx=True,
+    require_rx=True,
+    baud_rate=115200,
+    parity="NONE",
+    stop_bits=1,
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp
new file mode 100644
index 0000000000..50d709c3b0
--- /dev/null
+++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp
@@ -0,0 +1,173 @@
+#include "seeed_mr60bha2.h"
+#include "esphome/core/log.h"
+
+#include <utility>
+
+namespace esphome {
+namespace seeed_mr60bha2 {
+
+static const char *const TAG = "seeed_mr60bha2";
+
+// Prints the component's configuration data. dump_config() prints all of the component's configuration
+// items in an easy-to-read format, including the configuration key-value pairs.
+void MR60BHA2Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "MR60BHA2:");
+#ifdef USE_SENSOR
+  LOG_SENSOR(" ", "Breath Rate Sensor", this->breath_rate_sensor_);
+  LOG_SENSOR(" ", "Heart Rate Sensor", this->heart_rate_sensor_);
+  LOG_SENSOR(" ", "Distance Sensor", this->distance_sensor_);
+#endif
+}
+
+// main loop
+void MR60BHA2Component::loop() {
+  uint8_t byte;
+
+  // Is there data on the serial port
+  while (this->available()) {
+    this->read_byte(&byte);
+    this->rx_message_.push_back(byte);
+    if (!this->validate_message_()) {
+      this->rx_message_.clear();
+    }
+  }
+}
+
+/**
+ * @brief Calculate the checksum for a byte array.
+ *
+ * This function calculates the checksum for the provided byte array using an
+ * XOR-based checksum algorithm.
+ *
+ * @param data The byte array to calculate the checksum for.
+ * @param len The length of the byte array.
+ * @return The calculated checksum.
+ */
+static uint8_t calculate_checksum(const uint8_t *data, size_t len) {
+  uint8_t checksum = 0;
+  for (size_t i = 0; i < len; i++) {
+    checksum ^= data[i];
+  }
+  checksum = ~checksum;
+  return checksum;
+}
+
+/**
+ * @brief Validate the checksum of a byte array.
+ *
+ * This function validates the checksum of the provided byte array by comparing
+ * it to the expected checksum.
+ *
+ * @param data The byte array to validate.
+ * @param len The length of the byte array.
+ * @param expected_checksum The expected checksum.
+ * @return True if the checksum is valid, false otherwise.
+ */
+static bool validate_checksum(const uint8_t *data, size_t len, uint8_t expected_checksum) {
+  return calculate_checksum(data, len) == expected_checksum;
+}
+
+bool MR60BHA2Component::validate_message_() {
+  size_t at = this->rx_message_.size() - 1;
+  auto *data = &this->rx_message_[0];
+  uint8_t new_byte = data[at];
+
+  if (at == 0) {
+    return new_byte == FRAME_HEADER_BUFFER;
+  }
+
+  if (at <= 2) {
+    return true;
+  }
+  uint16_t frame_id = encode_uint16(data[1], data[2]);
+
+  if (at <= 4) {
+    return true;
+  }
+
+  uint16_t length = encode_uint16(data[3], data[4]);
+
+  if (at <= 6) {
+    return true;
+  }
+
+  uint16_t frame_type = encode_uint16(data[5], data[6]);
+
+  if (frame_type != BREATH_RATE_TYPE_BUFFER && frame_type != HEART_RATE_TYPE_BUFFER &&
+      frame_type != DISTANCE_TYPE_BUFFER) {
+    return false;
+  }
+
+  uint8_t header_checksum = new_byte;
+
+  if (at == 7) {
+    if (!validate_checksum(data, 7, header_checksum)) {
+      ESP_LOGE(TAG, "HEAD_CKSUM_FRAME ERROR: 0x%02x", header_checksum);
+      ESP_LOGV(TAG, "GET FRAME: %s", format_hex_pretty(data, 8).c_str());
+      return false;
+    }
+    return true;
+  }
+
+  // Wait until all data is read
+  if (at - 8 < length) {
+    return true;
+  }
+
+  uint8_t data_checksum = new_byte;
+  if (at == 8 + length) {
+    if (!validate_checksum(data + 8, length, data_checksum)) {
+      ESP_LOGE(TAG, "DATA_CKSUM_FRAME ERROR: 0x%02x", data_checksum);
+      ESP_LOGV(TAG, "GET FRAME: %s", format_hex_pretty(data, 8 + length).c_str());
+      return false;
+    }
+  }
+
+  const uint8_t *frame_data = data + 8;
+  ESP_LOGV(TAG, "Received Frame: ID: 0x%04x, Type: 0x%04x, Data: [%s] Raw Data: [%s]", frame_id, frame_type,
+           format_hex_pretty(frame_data, length).c_str(), format_hex_pretty(this->rx_message_).c_str());
+  this->process_frame_(frame_id, frame_type, data + 8, length);
+
+  // Return false to reset rx buffer
+  return false;
+}
+
+void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, const uint8_t *data, size_t length) {
+  switch (frame_type) {
+    case BREATH_RATE_TYPE_BUFFER:
+      if (this->breath_rate_sensor_ != nullptr && length >= 4) {
+        uint32_t current_breath_rate_int = encode_uint32(data[3], data[2], data[1], data[0]);
+        if (current_breath_rate_int != 0) {
+          float breath_rate_float;
+          memcpy(&breath_rate_float, &current_breath_rate_int, sizeof(float));
+          this->breath_rate_sensor_->publish_state(breath_rate_float);
+        }
+      }
+      break;
+    case HEART_RATE_TYPE_BUFFER:
+      if (this->heart_rate_sensor_ != nullptr && length >= 4) {
+        uint32_t current_heart_rate_int = encode_uint32(data[3], data[2], data[1], data[0]);
+        if (current_heart_rate_int != 0) {
+          float heart_rate_float;
+          memcpy(&heart_rate_float, &current_heart_rate_int, sizeof(float));
+          this->heart_rate_sensor_->publish_state(heart_rate_float);
+        }
+      }
+      break;
+    case DISTANCE_TYPE_BUFFER:
+      if (!data[0]) {
+        if (this->distance_sensor_ != nullptr && length >= 8) {
+          uint32_t current_distance_int = encode_uint32(data[7], data[6], data[5], data[4]);
+          float distance_float;
+          memcpy(&distance_float, &current_distance_int, sizeof(float));
+          this->distance_sensor_->publish_state(distance_float);
+        }
+      }
+      break;
+    default:
+      break;
+  }
+}
+
+}  // namespace seeed_mr60bha2
+}  // namespace esphome
diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.h b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.h
new file mode 100644
index 0000000000..0a4f21f1ad
--- /dev/null
+++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.h
@@ -0,0 +1,61 @@
+#pragma once
+#include "esphome/core/component.h"
+#include "esphome/core/defines.h"
+#ifdef USE_SENSOR
+#include "esphome/components/sensor/sensor.h"
+#endif
+#include "esphome/components/uart/uart.h"
+#include "esphome/core/automation.h"
+#include "esphome/core/helpers.h"
+
+#include <map>
+
+namespace esphome {
+namespace seeed_mr60bha2 {
+
+static const uint8_t DATA_BUF_MAX_SIZE = 12;
+static const uint8_t FRAME_BUF_MAX_SIZE = 21;
+static const uint8_t LEN_TO_HEAD_CKSUM = 8;
+static const uint8_t LEN_TO_DATA_FRAME = 9;
+
+static const uint8_t FRAME_HEADER_BUFFER = 0x01;
+static const uint16_t BREATH_RATE_TYPE_BUFFER = 0x0A14;
+static const uint16_t HEART_RATE_TYPE_BUFFER = 0x0A15;
+static const uint16_t DISTANCE_TYPE_BUFFER = 0x0A16;
+
+enum FrameLocation {
+  LOCATE_FRAME_HEADER,
+  LOCATE_ID_FRAME1,
+  LOCATE_ID_FRAME2,
+  LOCATE_LENGTH_FRAME_H,
+  LOCATE_LENGTH_FRAME_L,
+  LOCATE_TYPE_FRAME1,
+  LOCATE_TYPE_FRAME2,
+  LOCATE_HEAD_CKSUM_FRAME,  // Header checksum: [from the first byte to the previous byte of the HEAD_CKSUM bit]
+  LOCATE_DATA_FRAME,
+  LOCATE_DATA_CKSUM_FRAME,  // Data checksum: [from the first to the previous byte of the DATA_CKSUM bit]
+  LOCATE_PROCESS_FRAME,
+};
+
+class MR60BHA2Component : public Component,
+                          public uart::UARTDevice {  // The class name must be the name defined by text_sensor.py
+#ifdef USE_SENSOR
+  SUB_SENSOR(breath_rate);
+  SUB_SENSOR(heart_rate);
+  SUB_SENSOR(distance);
+#endif
+
+ public:
+  float get_setup_priority() const override { return esphome::setup_priority::LATE; }
+  void dump_config() override;
+  void loop() override;
+
+ protected:
+  bool validate_message_();
+  void process_frame_(uint16_t frame_id, uint16_t frame_type, const uint8_t *data, size_t length);
+
+  std::vector<uint8_t> rx_message_;
+};
+
+}  // namespace seeed_mr60bha2
+}  // namespace esphome
diff --git a/esphome/components/seeed_mr60bha2/sensor.py b/esphome/components/seeed_mr60bha2/sensor.py
new file mode 100644
index 0000000000..5f30b363bf
--- /dev/null
+++ b/esphome/components/seeed_mr60bha2/sensor.py
@@ -0,0 +1,57 @@
+import esphome.codegen as cg
+from esphome.components import sensor
+import esphome.config_validation as cv
+from esphome.const import (
+    CONF_DISTANCE,
+    DEVICE_CLASS_DISTANCE,
+    ICON_HEART_PULSE,
+    ICON_PULSE,
+    ICON_SIGNAL,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_BEATS_PER_MINUTE,
+    UNIT_CENTIMETER,
+)
+
+from . import CONF_MR60BHA2_ID, MR60BHA2Component
+
+DEPENDENCIES = ["seeed_mr60bha2"]
+
+CONF_BREATH_RATE = "breath_rate"
+CONF_HEART_RATE = "heart_rate"
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(CONF_MR60BHA2_ID): cv.use_id(MR60BHA2Component),
+        cv.Optional(CONF_BREATH_RATE): sensor.sensor_schema(
+            accuracy_decimals=2,
+            state_class=STATE_CLASS_MEASUREMENT,
+            icon=ICON_PULSE,
+        ),
+        cv.Optional(CONF_HEART_RATE): sensor.sensor_schema(
+            accuracy_decimals=0,
+            icon=ICON_HEART_PULSE,
+            state_class=STATE_CLASS_MEASUREMENT,
+            unit_of_measurement=UNIT_BEATS_PER_MINUTE,
+        ),
+        cv.Optional(CONF_DISTANCE): sensor.sensor_schema(
+            device_class=DEVICE_CLASS_DISTANCE,
+            state_class=STATE_CLASS_MEASUREMENT,
+            unit_of_measurement=UNIT_CENTIMETER,
+            accuracy_decimals=2,
+            icon=ICON_SIGNAL,
+        ),
+    }
+)
+
+
+async def to_code(config):
+    mr60bha2_component = await cg.get_variable(config[CONF_MR60BHA2_ID])
+    if breath_rate_config := config.get(CONF_BREATH_RATE):
+        sens = await sensor.new_sensor(breath_rate_config)
+        cg.add(mr60bha2_component.set_breath_rate_sensor(sens))
+    if heart_rate_config := config.get(CONF_HEART_RATE):
+        sens = await sensor.new_sensor(heart_rate_config)
+        cg.add(mr60bha2_component.set_heart_rate_sensor(sens))
+    if distance_config := config.get(CONF_DISTANCE):
+        sens = await sensor.new_sensor(distance_config)
+        cg.add(mr60bha2_component.set_distance_sensor(sens))
diff --git a/esphome/const.py b/esphome/const.py
index 3d3bfcc244..b9397aa1bd 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1001,6 +1001,7 @@ ICON_GRAIN = "mdi:grain"
 ICON_GYROSCOPE_X = "mdi:axis-x-rotate-clockwise"
 ICON_GYROSCOPE_Y = "mdi:axis-y-rotate-clockwise"
 ICON_GYROSCOPE_Z = "mdi:axis-z-rotate-clockwise"
+ICON_HEART_PULSE = "mdi:heart-pulse"
 ICON_HEATING_COIL = "mdi:heating-coil"
 ICON_KEY_PLUS = "mdi:key-plus"
 ICON_LIGHTBULB = "mdi:lightbulb"
@@ -1040,6 +1041,7 @@ ICON_WEATHER_WINDY = "mdi:weather-windy"
 ICON_WIFI = "mdi:wifi"
 
 UNIT_AMPERE = "A"
+UNIT_BEATS_PER_MINUTE = "bpm"
 UNIT_BECQUEREL_PER_CUBIC_METER = "Bq/m³"
 UNIT_BYTES = "B"
 UNIT_CELSIUS = "°C"
diff --git a/tests/components/seeed_mr60bha2/common.yaml b/tests/components/seeed_mr60bha2/common.yaml
new file mode 100644
index 0000000000..e9d0c735af
--- /dev/null
+++ b/tests/components/seeed_mr60bha2/common.yaml
@@ -0,0 +1,19 @@
+uart:
+  - id: seeed_mr60fda2_uart
+    tx_pin: ${uart_tx_pin}
+    rx_pin: ${uart_rx_pin}
+    baud_rate: 115200
+    parity: NONE
+    stop_bits: 1
+
+seeed_mr60bha2:
+  id: my_seeed_mr60bha2
+
+sensor:
+  - platform: seeed_mr60bha2
+    breath_rate:
+      name: "Real-time respiratory rate"
+    heart_rate:
+      name: "Real-time heart rate"
+    distance:
+      name: "Distance to detection object"
diff --git a/tests/components/seeed_mr60bha2/test.esp32-c3-ard.yaml b/tests/components/seeed_mr60bha2/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..4fb884abf4
--- /dev/null
+++ b/tests/components/seeed_mr60bha2/test.esp32-c3-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  uart_tx_pin: GPIO5
+  uart_rx_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/seeed_mr60bha2/test.esp32-c3-idf.yaml b/tests/components/seeed_mr60bha2/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..4fb884abf4
--- /dev/null
+++ b/tests/components/seeed_mr60bha2/test.esp32-c3-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  uart_tx_pin: GPIO5
+  uart_rx_pin: GPIO4
+
+<<: !include common.yaml

From f15e3cfb9b9cc26b402d1098549a79e87c00d9bc Mon Sep 17 00:00:00 2001
From: David Schneider <dnschneid@gmail.com>
Date: Sun, 8 Dec 2024 18:51:37 -0800
Subject: [PATCH 274/282] Optimize QMC5883L reads (#7889)

---
 esphome/components/qmc5883l/qmc5883l.cpp | 62 +++++++++++++++++-------
 esphome/components/qmc5883l/qmc5883l.h   |  2 +-
 2 files changed, 45 insertions(+), 19 deletions(-)

diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp
index 49a67d4e09..36286244fb 100644
--- a/esphome/components/qmc5883l/qmc5883l.cpp
+++ b/esphome/components/qmc5883l/qmc5883l.cpp
@@ -81,16 +81,39 @@ void QMC5883LComponent::dump_config() {
 }
 float QMC5883LComponent::get_setup_priority() const { return setup_priority::DATA; }
 void QMC5883LComponent::update() {
+  i2c::ErrorCode err;
   uint8_t status = false;
-  this->read_byte(QMC5883L_REGISTER_STATUS, &status);
+  // Status byte gets cleared when data is read, so we have to read this first.
+  // If status and two axes are desired, it's possible to save one byte of traffic by enabling
+  // ROL_PNT in setup and reading 7 bytes starting at the status register.
+  // If status and all three axes are desired, using ROL_PNT saves you 3 bytes.
+  // But simply not reading status saves you 4 bytes always and is much simpler.
+  if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG) {
+    err = this->read_register(QMC5883L_REGISTER_STATUS, &status, 1);
+    if (err != i2c::ERROR_OK) {
+      this->status_set_warning(str_sprintf("status read failed (%d)", err).c_str());
+      return;
+    }
+  }
 
-  // Always request X,Y,Z regardless if there are sensors for them
-  // to avoid https://github.com/esphome/issues/issues/5731
-  uint16_t raw_x, raw_y, raw_z;
-  if (!this->read_byte_16_(QMC5883L_REGISTER_DATA_X_LSB, &raw_x) ||
-      !this->read_byte_16_(QMC5883L_REGISTER_DATA_Y_LSB, &raw_y) ||
-      !this->read_byte_16_(QMC5883L_REGISTER_DATA_Z_LSB, &raw_z)) {
-    this->status_set_warning();
+  uint16_t raw[3] = {0};
+  // Z must always be requested, otherwise the data registers will remain locked against updates.
+  // Skipping the Y axis if X and Z are needed actually requires an additional byte of comms.
+  // Starting partway through the axes does save you traffic.
+  uint8_t start, dest;
+  if (this->heading_sensor_ != nullptr || this->x_sensor_ != nullptr) {
+    start = QMC5883L_REGISTER_DATA_X_LSB;
+    dest = 0;
+  } else if (this->y_sensor_ != nullptr) {
+    start = QMC5883L_REGISTER_DATA_Y_LSB;
+    dest = 1;
+  } else {
+    start = QMC5883L_REGISTER_DATA_Z_LSB;
+    dest = 2;
+  }
+  err = this->read_bytes_16_le_(start, &raw[dest], 3 - dest);
+  if (err != i2c::ERROR_OK) {
+    this->status_set_warning(str_sprintf("mag read failed (%d)", err).c_str());
     return;
   }
 
@@ -107,17 +130,18 @@ void QMC5883LComponent::update() {
   }
 
   // in µT
-  const float x = int16_t(raw_x) * mg_per_bit * 0.1f;
-  const float y = int16_t(raw_y) * mg_per_bit * 0.1f;
-  const float z = int16_t(raw_z) * mg_per_bit * 0.1f;
+  const float x = int16_t(raw[0]) * mg_per_bit * 0.1f;
+  const float y = int16_t(raw[1]) * mg_per_bit * 0.1f;
+  const float z = int16_t(raw[2]) * mg_per_bit * 0.1f;
 
   float heading = atan2f(0.0f - x, y) * 180.0f / M_PI;
 
   float temp = NAN;
   if (this->temperature_sensor_ != nullptr) {
     uint16_t raw_temp;
-    if (!this->read_byte_16_(QMC5883L_REGISTER_TEMPERATURE_LSB, &raw_temp)) {
-      this->status_set_warning();
+    err = this->read_bytes_16_le_(QMC5883L_REGISTER_TEMPERATURE_LSB, &raw_temp);
+    if (err != i2c::ERROR_OK) {
+      this->status_set_warning(str_sprintf("temp read failed (%d)", err).c_str());
       return;
     }
     temp = int16_t(raw_temp) * 0.01f;
@@ -138,11 +162,13 @@ void QMC5883LComponent::update() {
     this->temperature_sensor_->publish_state(temp);
 }
 
-bool QMC5883LComponent::read_byte_16_(uint8_t a_register, uint16_t *data) {
-  if (!this->read_byte_16(a_register, data))
-    return false;
-  *data = (*data & 0x00FF) << 8 | (*data & 0xFF00) >> 8;  // Flip Byte order, LSB first;
-  return true;
+i2c::ErrorCode QMC5883LComponent::read_bytes_16_le_(uint8_t a_register, uint16_t *data, uint8_t len) {
+  i2c::ErrorCode err = this->read_register(a_register, reinterpret_cast<uint8_t *>(data), len * 2);
+  if (err != i2c::ERROR_OK)
+    return err;
+  for (size_t i = 0; i < len; i++)
+    data[i] = convert_little_endian(data[i]);
+  return err;
 }
 
 }  // namespace qmc5883l
diff --git a/esphome/components/qmc5883l/qmc5883l.h b/esphome/components/qmc5883l/qmc5883l.h
index dd2008d453..3202e37780 100644
--- a/esphome/components/qmc5883l/qmc5883l.h
+++ b/esphome/components/qmc5883l/qmc5883l.h
@@ -55,7 +55,7 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice {
     NONE = 0,
     COMMUNICATION_FAILED,
   } error_code_;
-  bool read_byte_16_(uint8_t a_register, uint16_t *data);
+  i2c::ErrorCode read_bytes_16_le_(uint8_t a_register, uint16_t *data, uint8_t len = 1);
   HighFrequencyLoopRequester high_freq_;
 };
 

From 440080a753ffeed63616f3c3601acd911f239e56 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Mon, 9 Dec 2024 17:09:29 +1300
Subject: [PATCH 275/282] [display] Fix strftime overload ignoring alignment
 (#7937)

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

diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp
index 1d996bd59b..f00c2936a8 100644
--- a/esphome/components/display/display.cpp
+++ b/esphome/components/display/display.cpp
@@ -1,6 +1,6 @@
 #include "display.h"
-#include "display_color_utils.h"
 #include <utility>
+#include "display_color_utils.h"
 #include "esphome/core/hal.h"
 #include "esphome/core/log.h"
 
@@ -670,7 +670,7 @@ void Display::strftime(int x, int y, BaseFont *font, Color color, Color backgrou
     this->print(x, y, font, color, align, buffer, background);
 }
 void Display::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time) {
-  this->strftime(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, time);
+  this->strftime(x, y, font, color, COLOR_OFF, align, format, time);
 }
 void Display::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) {
   this->strftime(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, time);

From 132a096ae75c39e6d882b616b79bffd0cc50ea69 Mon Sep 17 00:00:00 2001
From: Yoonji Park <koreapyj@dcmys.kr>
Date: Mon, 9 Dec 2024 20:13:21 +0900
Subject: [PATCH 276/282] Add font anti-aliasing for grayscale display (#7934)

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

diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp
index aeca0f5cc0..8c4cba34b3 100644
--- a/esphome/components/font/font.cpp
+++ b/esphome/components/font/font.cpp
@@ -133,9 +133,11 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
     auto diff_r = (float) color.r - (float) background.r;
     auto diff_g = (float) color.g - (float) background.g;
     auto diff_b = (float) color.b - (float) background.b;
+    auto diff_w = (float) color.w - (float) background.w;
     auto b_r = (float) background.r;
     auto b_g = (float) background.g;
-    auto b_b = (float) background.g;
+    auto b_b = (float) background.b;
+    auto b_w = (float) background.w;
     for (int glyph_y = y_start + scan_y1; glyph_y != max_y; glyph_y++) {
       for (int glyph_x = x_at + scan_x1; glyph_x != max_x; glyph_x++) {
         uint8_t pixel = 0;
@@ -153,8 +155,8 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
           display->draw_pixel_at(glyph_x, glyph_y, color);
         } else if (pixel != 0) {
           auto on = (float) pixel / (float) bpp_max;
-          auto blended =
-              Color((uint8_t) (diff_r * on + b_r), (uint8_t) (diff_g * on + b_g), (uint8_t) (diff_b * on + b_b));
+          auto blended = Color((uint8_t) (diff_r * on + b_r), (uint8_t) (diff_g * on + b_g),
+                               (uint8_t) (diff_b * on + b_b), (uint8_t) (diff_w * on + b_w));
           display->draw_pixel_at(glyph_x, glyph_y, blended);
         }
       }

From 14eac3dbce15acf6857c68db1e931130ae6bd462 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 9 Dec 2024 23:44:39 +0100
Subject: [PATCH 277/282] Bump pypa/gh-action-pypi-publish from 1.12.2 to
 1.12.3 (#7941)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/workflows/release.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 096b00f0f1..a4e4305207 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -65,7 +65,7 @@ jobs:
           pip3 install build
           python3 -m build
       - name: Publish
-        uses: pypa/gh-action-pypi-publish@v1.12.2
+        uses: pypa/gh-action-pypi-publish@v1.12.3
 
   deploy-docker:
     name: Build ESPHome ${{ matrix.platform }}

From 437b236a4d5b7e2eb39a66b828b8dcfc1d51d162 Mon Sep 17 00:00:00 2001
From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com>
Date: Tue, 10 Dec 2024 01:38:45 +0100
Subject: [PATCH 278/282] [adc] Split files by platform (#7940)

---
 esphome/components/adc/adc_sensor.h           |  15 +-
 esphome/components/adc/adc_sensor_common.cpp  |  24 +++
 .../{adc_sensor.cpp => adc_sensor_esp32.cpp}  | 189 +-----------------
 esphome/components/adc/adc_sensor_esp8266.cpp |  58 ++++++
 esphome/components/adc/adc_sensor_rp2040.cpp  |  93 +++++++++
 5 files changed, 192 insertions(+), 187 deletions(-)
 create mode 100644 esphome/components/adc/adc_sensor_common.cpp
 rename esphome/components/adc/{adc_sensor.cpp => adc_sensor_esp32.cpp} (53%)
 create mode 100644 esphome/components/adc/adc_sensor_esp8266.cpp
 create mode 100644 esphome/components/adc/adc_sensor_rp2040.cpp

diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h
index b697d6dd7e..7a3e1c8da7 100644
--- a/esphome/components/adc/adc_sensor.h
+++ b/esphome/components/adc/adc_sensor.h
@@ -3,13 +3,12 @@
 #include "esphome/components/sensor/sensor.h"
 #include "esphome/components/voltage_sampler/voltage_sampler.h"
 #include "esphome/core/component.h"
-#include "esphome/core/defines.h"
 #include "esphome/core/hal.h"
 
 #ifdef USE_ESP32
 #include <esp_adc_cal.h>
 #include "driver/adc.h"
-#endif
+#endif  // USE_ESP32
 
 namespace esphome {
 namespace adc {
@@ -43,7 +42,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
     this->channel1_ = ADC1_CHANNEL_MAX;
   }
   void set_autorange(bool autorange) { this->autorange_ = autorange; }
-#endif
+#endif  // USE_ESP32
 
   /// Update ADC values
   void update() override;
@@ -59,11 +58,11 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
 
 #ifdef USE_ESP8266
   std::string unique_id() override;
-#endif
+#endif  // USE_ESP8266
 
 #ifdef USE_RP2040
   void set_is_temperature() { this->is_temperature_ = true; }
-#endif
+#endif  // USE_RP2040
 
  protected:
   InternalGPIOPin *pin_;
@@ -72,7 +71,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
 
 #ifdef USE_RP2040
   bool is_temperature_{false};
-#endif
+#endif  // USE_RP2040
 
 #ifdef USE_ESP32
   adc_atten_t attenuation_{ADC_ATTEN_DB_0};
@@ -83,8 +82,8 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
   esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {};
 #else
   esp_adc_cal_characteristics_t cal_characteristics_[ADC_ATTEN_MAX] = {};
-#endif
-#endif
+#endif  // ESP_IDF_VERSION_MAJOR
+#endif  // USE_ESP32
 };
 
 }  // namespace adc
diff --git a/esphome/components/adc/adc_sensor_common.cpp b/esphome/components/adc/adc_sensor_common.cpp
new file mode 100644
index 0000000000..2dccd55fcd
--- /dev/null
+++ b/esphome/components/adc/adc_sensor_common.cpp
@@ -0,0 +1,24 @@
+#include "adc_sensor.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace adc {
+
+static const char *const TAG = "adc.common";
+
+void ADCSensor::update() {
+  float value_v = this->sample();
+  ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v);
+  this->publish_state(value_v);
+}
+
+void ADCSensor::set_sample_count(uint8_t sample_count) {
+  if (sample_count != 0) {
+    this->sample_count_ = sample_count;
+  }
+}
+
+float ADCSensor::get_setup_priority() const { return setup_priority::DATA; }
+
+}  // namespace adc
+}  // namespace esphome
diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor_esp32.cpp
similarity index 53%
rename from esphome/components/adc/adc_sensor.cpp
rename to esphome/components/adc/adc_sensor_esp32.cpp
index 7257793016..24e3750091 100644
--- a/esphome/components/adc/adc_sensor.cpp
+++ b/esphome/components/adc/adc_sensor_esp32.cpp
@@ -1,30 +1,13 @@
+#ifdef USE_ESP32
+
 #include "adc_sensor.h"
-#include "esphome/core/helpers.h"
 #include "esphome/core/log.h"
 
-#ifdef USE_ESP8266
-#ifdef USE_ADC_SENSOR_VCC
-#include <Esp.h>
-ADC_MODE(ADC_VCC)
-#else
-#include <Arduino.h>
-#endif
-#endif
-
-#ifdef USE_RP2040
-#ifdef CYW43_USES_VSYS_PIN
-#include "pico/cyw43_arch.h"
-#endif
-#include <hardware/adc.h>
-#endif
-
 namespace esphome {
 namespace adc {
 
-static const char *const TAG = "adc";
+static const char *const TAG = "adc.esp32";
 
-// 13-bit for S2, 12-bit for all other ESP32 variants
-#ifdef USE_ESP32
 static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1);
 
 #ifndef SOC_ADC_RTC_MAX_BITWIDTH
@@ -32,24 +15,15 @@ static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_widt
 static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13;
 #else
 static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12;
-#endif
-#endif
+#endif  // USE_ESP32_VARIANT_ESP32S2
+#endif  // SOC_ADC_RTC_MAX_BITWIDTH
 
-static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1;    // 4095 (12 bit) or 8191 (13 bit)
-static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1;  // 2048 (12 bit) or 4096 (13 bit)
-#endif
+static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1;
+static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1;
 
-#ifdef USE_RP2040
-extern "C"
-#endif
-    void
-    ADCSensor::setup() {
+void ADCSensor::setup() {
   ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str());
-#if !defined(USE_ADC_SENSOR_VCC) && !defined(USE_RP2040)
-  this->pin_->setup();
-#endif
 
-#ifdef USE_ESP32
   if (this->channel1_ != ADC1_CHANNEL_MAX) {
     adc1_config_width(ADC_WIDTH_MAX_SOC_BITS);
     if (!this->autorange_) {
@@ -61,7 +35,6 @@ extern "C"
     }
   }
 
-  // load characteristics for each attenuation
   for (int32_t i = 0; i <= ADC_ATTEN_DB_12_COMPAT; i++) {
     auto adc_unit = this->channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2;
     auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS,
@@ -79,31 +52,10 @@ extern "C"
         break;
     }
   }
-
-#endif  // USE_ESP32
-
-#ifdef USE_RP2040
-  static bool initialized = false;
-  if (!initialized) {
-    adc_init();
-    initialized = true;
-  }
-#endif
-
-  ESP_LOGCONFIG(TAG, "ADC '%s' setup finished!", this->get_name().c_str());
 }
 
 void ADCSensor::dump_config() {
   LOG_SENSOR("", "ADC Sensor", this);
-#if defined(USE_ESP8266) || defined(USE_LIBRETINY)
-#ifdef USE_ADC_SENSOR_VCC
-  ESP_LOGCONFIG(TAG, "  Pin: VCC");
-#else
-  LOG_PIN("  Pin: ", this->pin_);
-#endif
-#endif  // USE_ESP8266 || USE_LIBRETINY
-
-#ifdef USE_ESP32
   LOG_PIN("  Pin: ", this->pin_);
   if (this->autorange_) {
     ESP_LOGCONFIG(TAG, "  Attenuation: auto");
@@ -125,55 +77,10 @@ void ADCSensor::dump_config() {
         break;
     }
   }
-#endif  // USE_ESP32
-
-#ifdef USE_RP2040
-  if (this->is_temperature_) {
-    ESP_LOGCONFIG(TAG, "  Pin: Temperature");
-  } else {
-#ifdef USE_ADC_SENSOR_VCC
-    ESP_LOGCONFIG(TAG, "  Pin: VCC");
-#else
-    LOG_PIN("  Pin: ", this->pin_);
-#endif  // USE_ADC_SENSOR_VCC
-  }
-#endif  // USE_RP2040
   ESP_LOGCONFIG(TAG, "  Samples: %i", this->sample_count_);
   LOG_UPDATE_INTERVAL(this);
 }
 
-float ADCSensor::get_setup_priority() const { return setup_priority::DATA; }
-void ADCSensor::update() {
-  float value_v = this->sample();
-  ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v);
-  this->publish_state(value_v);
-}
-
-void ADCSensor::set_sample_count(uint8_t sample_count) {
-  if (sample_count != 0) {
-    this->sample_count_ = sample_count;
-  }
-}
-
-#ifdef USE_ESP8266
-float ADCSensor::sample() {
-  uint32_t raw = 0;
-  for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
-#ifdef USE_ADC_SENSOR_VCC
-    raw += ESP.getVcc();  // NOLINT(readability-static-accessed-through-instance)
-#else
-    raw += analogRead(this->pin_->get_pin());  // NOLINT
-#endif
-  }
-  raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero)
-  if (this->output_raw_) {
-    return raw;
-  }
-  return raw / 1024.0f;
-}
-#endif
-
-#ifdef USE_ESP32
 float ADCSensor::sample() {
   if (!this->autorange_) {
     uint32_t sum = 0;
@@ -240,93 +147,17 @@ float ADCSensor::sample() {
   uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]);
   uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]);
 
-  // Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC)
   uint32_t c12 = std::min(raw12, ADC_HALF);
   uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF);
   uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF);
   uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF);
-  // max theoretical csum value is 4096*4 = 16384
   uint32_t csum = c12 + c6 + c2 + c0;
 
-  // each mv is max 3900; so max value is 3900*4096*4, fits in unsigned32
   uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0);
   return mv_scaled / (float) (csum * 1000U);
 }
-#endif  // USE_ESP32
-
-#ifdef USE_RP2040
-float ADCSensor::sample() {
-  if (this->is_temperature_) {
-    adc_set_temp_sensor_enabled(true);
-    delay(1);
-    adc_select_input(4);
-    uint32_t raw = 0;
-    for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
-      raw += adc_read();
-    }
-    raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero)
-    adc_set_temp_sensor_enabled(false);
-    if (this->output_raw_) {
-      return raw;
-    }
-    return raw * 3.3f / 4096.0f;
-  } else {
-    uint8_t pin = this->pin_->get_pin();
-#ifdef CYW43_USES_VSYS_PIN
-    if (pin == PICO_VSYS_PIN) {
-      // Measuring VSYS on Raspberry Pico W needs to be wrapped with
-      // `cyw43_thread_enter()`/`cyw43_thread_exit()` as discussed in
-      // https://github.com/raspberrypi/pico-sdk/issues/1222, since Wifi chip and
-      // VSYS ADC both share GPIO29
-      cyw43_thread_enter();
-    }
-#endif  // CYW43_USES_VSYS_PIN
-
-    adc_gpio_init(pin);
-    adc_select_input(pin - 26);
-
-    uint32_t raw = 0;
-    for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
-      raw += adc_read();
-    }
-    raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero)
-
-#ifdef CYW43_USES_VSYS_PIN
-    if (pin == PICO_VSYS_PIN) {
-      cyw43_thread_exit();
-    }
-#endif  // CYW43_USES_VSYS_PIN
-
-    if (this->output_raw_) {
-      return raw;
-    }
-    float coeff = pin == PICO_VSYS_PIN ? 3.0 : 1.0;
-    return raw * 3.3f / 4096.0f * coeff;
-  }
-}
-#endif
-
-#ifdef USE_LIBRETINY
-float ADCSensor::sample() {
-  uint32_t raw = 0;
-  if (this->output_raw_) {
-    for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
-      raw += analogRead(this->pin_->get_pin());  // NOLINT
-    }
-    raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero)
-    return raw;
-  }
-  for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
-    raw += analogReadVoltage(this->pin_->get_pin());  // NOLINT
-  }
-  raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero)
-  return raw / 1000.0f;
-}
-#endif  // USE_LIBRETINY
-
-#ifdef USE_ESP8266
-std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; }
-#endif
 
 }  // namespace adc
 }  // namespace esphome
+
+#endif  // USE_ESP32
diff --git a/esphome/components/adc/adc_sensor_esp8266.cpp b/esphome/components/adc/adc_sensor_esp8266.cpp
new file mode 100644
index 0000000000..c9b6f8b652
--- /dev/null
+++ b/esphome/components/adc/adc_sensor_esp8266.cpp
@@ -0,0 +1,58 @@
+#ifdef USE_ESP8266
+
+#include "adc_sensor.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/log.h"
+
+#ifdef USE_ADC_SENSOR_VCC
+#include <Esp.h>
+ADC_MODE(ADC_VCC)
+#else
+#include <Arduino.h>
+#endif  // USE_ADC_SENSOR_VCC
+
+namespace esphome {
+namespace adc {
+
+static const char *const TAG = "adc.esp8266";
+
+void ADCSensor::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str());
+#ifndef USE_ADC_SENSOR_VCC
+  this->pin_->setup();
+#endif
+}
+
+void ADCSensor::dump_config() {
+  LOG_SENSOR("", "ADC Sensor", this);
+#ifdef USE_ADC_SENSOR_VCC
+  ESP_LOGCONFIG(TAG, "  Pin: VCC");
+#else
+  LOG_PIN("  Pin: ", this->pin_);
+#endif  // USE_ADC_SENSOR_VCC
+  ESP_LOGCONFIG(TAG, "  Samples: %i", this->sample_count_);
+  LOG_UPDATE_INTERVAL(this);
+}
+
+float ADCSensor::sample() {
+  uint32_t raw = 0;
+  for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
+#ifdef USE_ADC_SENSOR_VCC
+    raw += ESP.getVcc();  // NOLINT(readability-static-accessed-through-instance)
+#else
+    raw += analogRead(this->pin_->get_pin());  // NOLINT
+#endif  // USE_ADC_SENSOR_VCC
+  }
+  raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero)
+  if (this->output_raw_) {
+    return raw;
+  }
+  return raw / 1024.0f;
+}
+
+std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; }
+
+}  // namespace adc
+}  // namespace esphome
+
+#endif  // USE_ESP8266
diff --git a/esphome/components/adc/adc_sensor_rp2040.cpp b/esphome/components/adc/adc_sensor_rp2040.cpp
new file mode 100644
index 0000000000..520ff3bacc
--- /dev/null
+++ b/esphome/components/adc/adc_sensor_rp2040.cpp
@@ -0,0 +1,93 @@
+#ifdef USE_RP2040
+
+#include "adc_sensor.h"
+#include "esphome/core/log.h"
+
+#ifdef CYW43_USES_VSYS_PIN
+#include "pico/cyw43_arch.h"
+#endif  // CYW43_USES_VSYS_PIN
+#include <hardware/adc.h>
+
+namespace esphome {
+namespace adc {
+
+static const char *const TAG = "adc.rp2040";
+
+void ADCSensor::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str());
+  static bool initialized = false;
+  if (!initialized) {
+    adc_init();
+    initialized = true;
+  }
+}
+
+void ADCSensor::dump_config() {
+  LOG_SENSOR("", "ADC Sensor", this);
+  if (this->is_temperature_) {
+    ESP_LOGCONFIG(TAG, "  Pin: Temperature");
+  } else {
+#ifdef USE_ADC_SENSOR_VCC
+    ESP_LOGCONFIG(TAG, "  Pin: VCC");
+#else
+    LOG_PIN("  Pin: ", this->pin_);
+#endif  // USE_ADC_SENSOR_VCC
+  }
+  ESP_LOGCONFIG(TAG, "  Samples: %i", this->sample_count_);
+  LOG_UPDATE_INTERVAL(this);
+}
+
+float ADCSensor::sample() {
+  if (this->is_temperature_) {
+    adc_set_temp_sensor_enabled(true);
+    delay(1);
+    adc_select_input(4);
+    uint32_t raw = 0;
+    for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
+      raw += adc_read();
+    }
+    raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero)
+    adc_set_temp_sensor_enabled(false);
+    if (this->output_raw_) {
+      return raw;
+    }
+    return raw * 3.3f / 4096.0f;
+  }
+
+  uint8_t pin = this->pin_->get_pin();
+#ifdef CYW43_USES_VSYS_PIN
+  if (pin == PICO_VSYS_PIN) {
+    // Measuring VSYS on Raspberry Pico W needs to be wrapped with
+    // `cyw43_thread_enter()`/`cyw43_thread_exit()` as discussed in
+    // https://github.com/raspberrypi/pico-sdk/issues/1222, since Wifi chip and
+    // VSYS ADC both share GPIO29
+    cyw43_thread_enter();
+  }
+#endif  // CYW43_USES_VSYS_PIN
+
+  adc_gpio_init(pin);
+  adc_select_input(pin - 26);
+
+  uint32_t raw = 0;
+  for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
+    raw += adc_read();
+  }
+  raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero)
+
+#ifdef CYW43_USES_VSYS_PIN
+  if (pin == PICO_VSYS_PIN) {
+    cyw43_thread_exit();
+  }
+#endif  // CYW43_USES_VSYS_PIN
+
+  if (this->output_raw_) {
+    return raw;
+  }
+  float coeff = pin == PICO_VSYS_PIN ? 3.0f : 1.0f;
+  return raw * 3.3f / 4096.0f * coeff;
+}
+
+}  // namespace adc
+}  // namespace esphome
+
+#endif  // USE_RP2040

From 5a92e2466238fa2627e9a16a1e29ec8d41375c92 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Tue, 10 Dec 2024 14:22:30 +1300
Subject: [PATCH 279/282] [const] Move ``CONF_TEMPERATURE_COMPENSATION`` to
 common const.py (#7943)

---
 esphome/components/sen5x/sensor.py    | 11 +++++------
 esphome/components/ufire_ec/sensor.py |  8 ++++----
 esphome/const.py                      |  1 +
 3 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py
index 67bd627f7f..a8a796853e 100644
--- a/esphome/components/sen5x/sensor.py
+++ b/esphome/components/sen5x/sensor.py
@@ -1,19 +1,19 @@
-import esphome.codegen as cg
-import esphome.config_validation as cv
-from esphome.components import i2c, sensor, sensirion_common
 from esphome import automation
 from esphome.automation import maybe_simple_id
-
+import esphome.codegen as cg
+from esphome.components import i2c, sensirion_common, sensor
+import esphome.config_validation as cv
 from esphome.const import (
     CONF_HUMIDITY,
     CONF_ID,
     CONF_OFFSET,
     CONF_PM_1_0,
-    CONF_PM_10_0,
     CONF_PM_2_5,
     CONF_PM_4_0,
+    CONF_PM_10_0,
     CONF_STORE_BASELINE,
     CONF_TEMPERATURE,
+    CONF_TEMPERATURE_COMPENSATION,
     DEVICE_CLASS_AQI,
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_PM1,
@@ -51,7 +51,6 @@ CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours"
 CONF_NORMALIZED_OFFSET_SLOPE = "normalized_offset_slope"
 CONF_NOX = "nox"
 CONF_STD_INITIAL = "std_initial"
-CONF_TEMPERATURE_COMPENSATION = "temperature_compensation"
 CONF_TIME_CONSTANT = "time_constant"
 CONF_VOC = "voc"
 CONF_VOC_BASELINE = "voc_baseline"
diff --git a/esphome/components/ufire_ec/sensor.py b/esphome/components/ufire_ec/sensor.py
index 9602d0c2d0..944fdfdee9 100644
--- a/esphome/components/ufire_ec/sensor.py
+++ b/esphome/components/ufire_ec/sensor.py
@@ -1,11 +1,12 @@
-import esphome.codegen as cg
 from esphome import automation
-import esphome.config_validation as cv
+import esphome.codegen as cg
 from esphome.components import i2c, sensor
+import esphome.config_validation as cv
 from esphome.const import (
-    CONF_ID,
     CONF_EC,
+    CONF_ID,
     CONF_TEMPERATURE,
+    CONF_TEMPERATURE_COMPENSATION,
     DEVICE_CLASS_EMPTY,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
@@ -18,7 +19,6 @@ DEPENDENCIES = ["i2c"]
 
 CONF_SOLUTION = "solution"
 CONF_TEMPERATURE_SENSOR = "temperature_sensor"
-CONF_TEMPERATURE_COMPENSATION = "temperature_compensation"
 CONF_TEMPERATURE_COEFFICIENT = "temperature_coefficient"
 
 ufire_ec_ns = cg.esphome_ns.namespace("ufire_ec")
diff --git a/esphome/const.py b/esphome/const.py
index b9397aa1bd..4d3818cb23 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -864,6 +864,7 @@ CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC = "target_temperature_low_command_topi
 CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC = "target_temperature_low_state_topic"
 CONF_TARGET_TEMPERATURE_STATE_TOPIC = "target_temperature_state_topic"
 CONF_TEMPERATURE = "temperature"
+CONF_TEMPERATURE_COMPENSATION = "temperature_compensation"
 CONF_TEMPERATURE_OFFSET = "temperature_offset"
 CONF_TEMPERATURE_SOURCE = "temperature_source"
 CONF_TEMPERATURE_STEP = "temperature_step"

From 517f659da86e111e5c203564aa91f54471e3ceca Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 10 Dec 2024 12:23:30 +1100
Subject: [PATCH 280/282] [lvgl] Fix image `mode` property (Bugfix) (#7938)

---
 esphome/components/lvgl/widgets/img.py  | 2 +-
 tests/components/lvgl/lvgl-package.yaml | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/esphome/components/lvgl/widgets/img.py b/esphome/components/lvgl/widgets/img.py
index 59b2c97c63..46077190d0 100644
--- a/esphome/components/lvgl/widgets/img.py
+++ b/esphome/components/lvgl/widgets/img.py
@@ -79,7 +79,7 @@ class ImgType(WidgetType):
         if CONF_ANTIALIAS in config:
             lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS])
         if mode := config.get(CONF_MODE):
-            lv.img_set_mode(w.obj, mode)
+            await w.set_property("size_mode", mode)
 
 
 img_spec = ImgType()
diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml
index 4b7e13db91..556ce105ee 100644
--- a/tests/components/lvgl/lvgl-package.yaml
+++ b/tests/components/lvgl/lvgl-package.yaml
@@ -451,6 +451,7 @@ lvgl:
             src: cat_image
             align: top_left
             y: "50"
+            mode: real
         - tileview:
             id: tileview_id
             scrollbar_mode: active
@@ -647,6 +648,7 @@ lvgl:
                       grid_cell_row_pos: 0
                       grid_cell_column_pos: 0
                       src: !lambda return dog_image;
+                      mode: virtual
                       on_click:
                         then:
                           - lvgl.tabview.select:

From bb27eaaf1e58316c6124f2d16a1fba6b0a033141 Mon Sep 17 00:00:00 2001
From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Date: Tue, 10 Dec 2024 12:25:29 +1100
Subject: [PATCH 281/282] [lvgl] Add `on_change` event (#7939)

---
 esphome/components/lvgl/defines.py      | 1 +
 tests/components/lvgl/lvgl-package.yaml | 5 +++++
 2 files changed, 6 insertions(+)

diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py
index 5371f110a6..02323f9655 100644
--- a/esphome/components/lvgl/defines.py
+++ b/esphome/components/lvgl/defines.py
@@ -168,6 +168,7 @@ LV_EVENT_MAP = {
     "READY": "READY",
     "CANCEL": "CANCEL",
     "ALL_EVENTS": "ALL",
+    "CHANGE": "VALUE_CHANGED",
 }
 
 LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT_MAP)
diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml
index 556ce105ee..b1b89adfe0 100644
--- a/tests/components/lvgl/lvgl-package.yaml
+++ b/tests/components/lvgl/lvgl-package.yaml
@@ -165,6 +165,11 @@ lvgl:
               - Nov
               - Dec
             selected_index: 1
+            on_change:
+              then:
+                - logger.log:
+                    format: "Roller changed = %d: %s"
+                    args: [x, text.c_str()]
             on_value:
               then:
                 - logger.log:

From 444e162c92c3148a14493f66d3de256679a6eedf Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Tue, 10 Dec 2024 19:39:00 +1300
Subject: [PATCH 282/282] Synchronise esp32 boards with platform version
 51.03.07 (#7945)

---
 esphome/components/esp32/boards.py | 236 +++++++++++++++++++++++++++--
 1 file changed, 222 insertions(+), 14 deletions(-)

diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py
index 02744ecb6f..81400eb9c3 100644
--- a/esphome/components/esp32/boards.py
+++ b/esphome/components/esp32/boards.py
@@ -1,4 +1,12 @@
-from .const import VARIANT_ESP32, VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3
+from .const import (
+    VARIANT_ESP32,
+    VARIANT_ESP32C2,
+    VARIANT_ESP32C3,
+    VARIANT_ESP32C6,
+    VARIANT_ESP32H2,
+    VARIANT_ESP32S2,
+    VARIANT_ESP32S3,
+)
 
 ESP32_BASE_PINS = {
     "TX": 1,
@@ -1344,6 +1352,26 @@ done | sort
 """
 
 BOARDS = {
+    "4d_systems_esp32s3_gen4_r8n16": {
+        "name": "4D Systems GEN4-ESP32 16MB (ESP32S3-R8N16)",
+        "variant": VARIANT_ESP32S3,
+    },
+    "adafruit_camera_esp32s3": {
+        "name": "Adafruit pyCamera S3",
+        "variant": VARIANT_ESP32S3,
+    },
+    "adafruit_feather_esp32c6": {
+        "name": "Adafruit Feather ESP32-C6",
+        "variant": VARIANT_ESP32C6,
+    },
+    "adafruit_feather_esp32s2": {
+        "name": "Adafruit Feather ESP32-S2",
+        "variant": VARIANT_ESP32S2,
+    },
+    "adafruit_feather_esp32s2_reversetft": {
+        "name": "Adafruit Feather ESP32-S2 Reverse TFT",
+        "variant": VARIANT_ESP32S2,
+    },
     "adafruit_feather_esp32s2_tft": {
         "name": "Adafruit Feather ESP32-S2 TFT",
         "variant": VARIANT_ESP32S2,
@@ -1356,6 +1384,10 @@ BOARDS = {
         "name": "Adafruit Feather ESP32-S3 No PSRAM",
         "variant": VARIANT_ESP32S3,
     },
+    "adafruit_feather_esp32s3_reversetft": {
+        "name": "Adafruit Feather ESP32-S3 Reverse TFT",
+        "variant": VARIANT_ESP32S3,
+    },
     "adafruit_feather_esp32s3_tft": {
         "name": "Adafruit Feather ESP32-S3 TFT",
         "variant": VARIANT_ESP32S3,
@@ -1376,10 +1408,18 @@ BOARDS = {
         "name": "Adafruit MagTag 2.9",
         "variant": VARIANT_ESP32S2,
     },
+    "adafruit_matrixportal_esp32s3": {
+        "name": "Adafruit MatrixPortal ESP32-S3",
+        "variant": VARIANT_ESP32S3,
+    },
     "adafruit_metro_esp32s2": {
         "name": "Adafruit Metro ESP32-S2",
         "variant": VARIANT_ESP32S2,
     },
+    "adafruit_metro_esp32s3": {
+        "name": "Adafruit Metro ESP32-S3",
+        "variant": VARIANT_ESP32S3,
+    },
     "adafruit_qtpy_esp32c3": {
         "name": "Adafruit QT Py ESP32-C3",
         "variant": VARIANT_ESP32C3,
@@ -1392,10 +1432,18 @@ BOARDS = {
         "name": "Adafruit QT Py ESP32-S2",
         "variant": VARIANT_ESP32S2,
     },
+    "adafruit_qtpy_esp32s3_n4r2": {
+        "name": "Adafruit QT Py ESP32-S3 (4M Flash 2M PSRAM)",
+        "variant": VARIANT_ESP32S3,
+    },
     "adafruit_qtpy_esp32s3_nopsram": {
         "name": "Adafruit QT Py ESP32-S3 No PSRAM",
         "variant": VARIANT_ESP32S3,
     },
+    "adafruit_qualia_s3_rgb666": {
+        "name": "Adafruit Qualia ESP32-S3 RGB666",
+        "variant": VARIANT_ESP32S3,
+    },
     "airm2m_core_esp32c3": {
         "name": "AirM2M CORE ESP32C3",
         "variant": VARIANT_ESP32C3,
@@ -1404,14 +1452,30 @@ BOARDS = {
         "name": "ALKS ESP32",
         "variant": VARIANT_ESP32,
     },
+    "arduino_nano_esp32": {
+        "name": "Arduino Nano ESP32",
+        "variant": VARIANT_ESP32S3,
+    },
+    "atd147_s3": {
+        "name": "ArtronShop ATD1.47-S3",
+        "variant": VARIANT_ESP32S3,
+    },
     "atmegazero_esp32s2": {
         "name": "EspinalLab ATMegaZero ESP32-S2",
         "variant": VARIANT_ESP32S2,
     },
+    "aventen_s3_sync": {
+        "name": "Aventen S3 Sync",
+        "variant": VARIANT_ESP32S3,
+    },
     "az-delivery-devkit-v4": {
         "name": "AZ-Delivery ESP-32 Dev Kit C V4",
         "variant": VARIANT_ESP32,
     },
+    "bee_data_logger": {
+        "name": "Smart Bee Data Logger",
+        "variant": VARIANT_ESP32S3,
+    },
     "bee_motion_mini": {
         "name": "Smart Bee Motion Mini",
         "variant": VARIANT_ESP32C3,
@@ -1436,14 +1500,6 @@ BOARDS = {
         "name": "BPI-Leaf-S3",
         "variant": VARIANT_ESP32S3,
     },
-    "briki_abc_esp32": {
-        "name": "Briki ABC (MBC-WB) - ESP32",
-        "variant": VARIANT_ESP32,
-    },
-    "briki_mbc-wb_esp32": {
-        "name": "Briki MBC-WB - ESP32",
-        "variant": VARIANT_ESP32,
-    },
     "cnrs_aw2eth": {
         "name": "CNRS AW2ETH",
         "variant": VARIANT_ESP32,
@@ -1496,18 +1552,38 @@ BOARDS = {
         "name": "DFRobot Beetle ESP32-C3",
         "variant": VARIANT_ESP32C3,
     },
+    "dfrobot_firebeetle2_esp32e": {
+        "name": "DFRobot Firebeetle 2 ESP32-E",
+        "variant": VARIANT_ESP32,
+    },
     "dfrobot_firebeetle2_esp32s3": {
         "name": "DFRobot Firebeetle 2 ESP32-S3",
         "variant": VARIANT_ESP32S3,
     },
+    "dfrobot_romeo_esp32s3": {
+        "name": "DFRobot Romeo ESP32-S3",
+        "variant": VARIANT_ESP32S3,
+    },
     "dpu_esp32": {
         "name": "TAMC DPU ESP32",
         "variant": VARIANT_ESP32,
     },
+    "edgebox-esp-100": {
+        "name": "Seeed Studio Edgebox-ESP-100",
+        "variant": VARIANT_ESP32S3,
+    },
     "esp320": {
         "name": "Electronic SweetPeas ESP320",
         "variant": VARIANT_ESP32,
     },
+    "esp32-c2-devkitm-1": {
+        "name": "Espressif ESP32-C2-DevKitM-1",
+        "variant": VARIANT_ESP32C2,
+    },
+    "esp32-c3-devkitc-02": {
+        "name": "Espressif ESP32-C3-DevKitC-02",
+        "variant": VARIANT_ESP32C3,
+    },
     "esp32-c3-devkitm-1": {
         "name": "Espressif ESP32-C3-DevKitM-1",
         "variant": VARIANT_ESP32C3,
@@ -1516,6 +1592,14 @@ BOARDS = {
         "name": "Ai-Thinker ESP-C3-M1-I-Kit",
         "variant": VARIANT_ESP32C3,
     },
+    "esp32-c6-devkitc-1": {
+        "name": "Espressif ESP32-C6-DevKitC-1",
+        "variant": VARIANT_ESP32C6,
+    },
+    "esp32-c6-devkitm-1": {
+        "name": "Espressif ESP32-C6-DevKitM-1",
+        "variant": VARIANT_ESP32C6,
+    },
     "esp32cam": {
         "name": "AI Thinker ESP32-CAM",
         "variant": VARIANT_ESP32,
@@ -1544,6 +1628,14 @@ BOARDS = {
         "name": "OLIMEX ESP32-GATEWAY",
         "variant": VARIANT_ESP32,
     },
+    "esp32-h2-devkitm-1": {
+        "name": "Espressif ESP32-H2-DevKit",
+        "variant": VARIANT_ESP32H2,
+    },
+    "esp32-pico-devkitm-2": {
+        "name": "Espressif ESP32-PICO-DevKitM-2",
+        "variant": VARIANT_ESP32,
+    },
     "esp32-poe-iso": {
         "name": "OLIMEX ESP32-PoE-ISO",
         "variant": VARIANT_ESP32,
@@ -1580,10 +1672,22 @@ BOARDS = {
         "name": "Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)",
         "variant": VARIANT_ESP32S3,
     },
-    "esp32-s3-korvo-2": {
-        "name": "Espressif ESP32-S3-Korvo-2",
+    "esp32-s3-devkitm-1": {
+        "name": "Espressif ESP32-S3-DevKitM-1",
         "variant": VARIANT_ESP32S3,
     },
+    "esp32s3_powerfeather": {
+        "name": "ESP32-S3 PowerFeather",
+        "variant": VARIANT_ESP32S3,
+    },
+    "esp32s3usbotg": {
+        "name": "Espressif ESP32-S3-USB-OTG",
+        "variant": VARIANT_ESP32S3,
+    },
+    "esp32-solo1": {
+        "name": "Espressif Generic ESP32-solo1 4M Flash",
+        "variant": VARIANT_ESP32,
+    },
     "esp32thing": {
         "name": "SparkFun ESP32 Thing",
         "variant": VARIANT_ESP32,
@@ -1652,9 +1756,9 @@ BOARDS = {
         "name": "Heltec WiFi Kit 32",
         "variant": VARIANT_ESP32,
     },
-    "heltec_wifi_kit_32_v2": {
-        "name": "Heltec WiFi Kit 32 (V2)",
-        "variant": VARIANT_ESP32,
+    "heltec_wifi_kit_32_V3": {
+        "name": "Heltec WiFi Kit 32 (V3)",
+        "variant": VARIANT_ESP32S3,
     },
     "heltec_wifi_lora_32": {
         "name": "Heltec WiFi LoRa 32",
@@ -1664,6 +1768,10 @@ BOARDS = {
         "name": "Heltec WiFi LoRa 32 (V2)",
         "variant": VARIANT_ESP32,
     },
+    "heltec_wifi_lora_32_V3": {
+        "name": "Heltec WiFi LoRa 32 (V3)",
+        "variant": VARIANT_ESP32S3,
+    },
     "heltec_wireless_stick_lite": {
         "name": "Heltec Wireless Stick Lite",
         "variant": VARIANT_ESP32,
@@ -1708,6 +1816,14 @@ BOARDS = {
         "name": "oddWires IoT-Bus Proteus",
         "variant": VARIANT_ESP32,
     },
+    "ioxesp32": {
+        "name": "ArtronShop IOXESP32",
+        "variant": VARIANT_ESP32,
+    },
+    "ioxesp32ps": {
+        "name": "ArtronShop IOXESP32PS",
+        "variant": VARIANT_ESP32,
+    },
     "kb32-ft": {
         "name": "MakerAsia KB32-FT",
         "variant": VARIANT_ESP32,
@@ -1720,10 +1836,26 @@ BOARDS = {
         "name": "Labplus mPython",
         "variant": VARIANT_ESP32,
     },
+    "lilka_v2": {
+        "name": "Lilka v2",
+        "variant": VARIANT_ESP32S3,
+    },
+    "lilygo-t-display": {
+        "name": "LilyGo T-Display",
+        "variant": VARIANT_ESP32,
+    },
+    "lilygo-t-display-s3": {
+        "name": "LilyGo T-Display-S3",
+        "variant": VARIANT_ESP32S3,
+    },
     "lionbit": {
         "name": "Lion:Bit Dev Board",
         "variant": VARIANT_ESP32,
     },
+    "lionbits3": {
+        "name": "Lion:Bit S3 STEM Dev Board",
+        "variant": VARIANT_ESP32S3,
+    },
     "lolin32_lite": {
         "name": "WEMOS LOLIN32 Lite",
         "variant": VARIANT_ESP32,
@@ -1752,10 +1884,18 @@ BOARDS = {
         "name": "WEMOS LOLIN S2 PICO",
         "variant": VARIANT_ESP32S2,
     },
+    "lolin_s3_mini": {
+        "name": "WEMOS LOLIN S3 Mini",
+        "variant": VARIANT_ESP32S3,
+    },
     "lolin_s3": {
         "name": "WEMOS LOLIN S3",
         "variant": VARIANT_ESP32S3,
     },
+    "lolin_s3_pro": {
+        "name": "WEMOS LOLIN S3 PRO",
+        "variant": VARIANT_ESP32S3,
+    },
     "lopy4": {
         "name": "Pycom LoPy4",
         "variant": VARIANT_ESP32,
@@ -1768,10 +1908,18 @@ BOARDS = {
         "name": "M5Stack-ATOM",
         "variant": VARIANT_ESP32,
     },
+    "m5stack-atoms3": {
+        "name": "M5Stack AtomS3",
+        "variant": VARIANT_ESP32S3,
+    },
     "m5stack-core2": {
         "name": "M5Stack Core2",
         "variant": VARIANT_ESP32,
     },
+    "m5stack-core-esp32-16M": {
+        "name": "M5Stack Core ESP32 16M",
+        "variant": VARIANT_ESP32,
+    },
     "m5stack-core-esp32": {
         "name": "M5Stack Core ESP32",
         "variant": VARIANT_ESP32,
@@ -1780,6 +1928,10 @@ BOARDS = {
         "name": "M5Stack-Core Ink",
         "variant": VARIANT_ESP32,
     },
+    "m5stack-cores3": {
+        "name": "M5Stack CoreS3",
+        "variant": VARIANT_ESP32S3,
+    },
     "m5stack-fire": {
         "name": "M5Stack FIRE",
         "variant": VARIANT_ESP32,
@@ -1788,6 +1940,14 @@ BOARDS = {
         "name": "M5Stack GREY ESP32",
         "variant": VARIANT_ESP32,
     },
+    "m5stack_paper": {
+        "name": "M5Stack Paper",
+        "variant": VARIANT_ESP32,
+    },
+    "m5stack-stamps3": {
+        "name": "M5Stack StampS3",
+        "variant": VARIANT_ESP32S3,
+    },
     "m5stack-station": {
         "name": "M5Stack Station",
         "variant": VARIANT_ESP32,
@@ -1796,6 +1956,10 @@ BOARDS = {
         "name": "M5Stack Timer CAM",
         "variant": VARIANT_ESP32,
     },
+    "m5stamp-pico": {
+        "name": "M5Stamp-Pico",
+        "variant": VARIANT_ESP32,
+    },
     "m5stick-c": {
         "name": "M5Stick-C",
         "variant": VARIANT_ESP32,
@@ -1832,10 +1996,26 @@ BOARDS = {
         "name": "Deparment of Alchemy MiniMain ESP32-S2",
         "variant": VARIANT_ESP32S2,
     },
+    "motorgo_mini_1": {
+        "name": "MotorGo Mini 1 (ESP32-S3)",
+        "variant": VARIANT_ESP32S3,
+    },
+    "namino_arancio": {
+        "name": "Namino Arancio",
+        "variant": VARIANT_ESP32S3,
+    },
+    "namino_rosso": {
+        "name": "Namino Rosso",
+        "variant": VARIANT_ESP32S3,
+    },
     "nano32": {
         "name": "MakerAsia Nano32",
         "variant": VARIANT_ESP32,
     },
+    "nebulas3": {
+        "name": "Kinetic Dynamics Nebula S3",
+        "variant": VARIANT_ESP32S3,
+    },
     "nina_w10": {
         "name": "u-blox NINA-W10 series",
         "variant": VARIANT_ESP32,
@@ -1896,10 +2076,22 @@ BOARDS = {
         "name": "Munich Labs RedPill ESP32-S3",
         "variant": VARIANT_ESP32S3,
     },
+    "roboheart_hercules": {
+        "name": "RoboHeart Hercules",
+        "variant": VARIANT_ESP32,
+    },
     "seeed_xiao_esp32c3": {
         "name": "Seeed Studio XIAO ESP32C3",
         "variant": VARIANT_ESP32C3,
     },
+    "seeed_xiao_esp32s3": {
+        "name": "Seeed Studio XIAO ESP32S3",
+        "variant": VARIANT_ESP32S3,
+    },
+    "sensebox_mcu_esp32s2": {
+        "name": "senseBox MCU-S2 ESP32-S2",
+        "variant": VARIANT_ESP32S2,
+    },
     "sensesiot_weizen": {
         "name": "LOGISENSES Senses Weizen",
         "variant": VARIANT_ESP32,
@@ -1912,6 +2104,10 @@ BOARDS = {
         "name": "S.ODI Ultra v1",
         "variant": VARIANT_ESP32,
     },
+    "sparkfun_esp32c6_thing_plus": {
+        "name": "Sparkfun ESP32-C6 Thing Plus",
+        "variant": VARIANT_ESP32C6,
+    },
     "sparkfun_esp32_iot_redboard": {
         "name": "SparkFun ESP32 IoT RedBoard",
         "variant": VARIANT_ESP32,
@@ -2004,6 +2200,10 @@ BOARDS = {
         "name": "Unexpected Maker FeatherS3",
         "variant": VARIANT_ESP32S3,
     },
+    "um_nanos3": {
+        "name": "Unexpected Maker NanoS3",
+        "variant": VARIANT_ESP32S3,
+    },
     "um_pros3": {
         "name": "Unexpected Maker PROS3",
         "variant": VARIANT_ESP32S3,
@@ -2040,6 +2240,14 @@ BOARDS = {
         "name": "uPesy ESP32 Wrover DevKit",
         "variant": VARIANT_ESP32,
     },
+    "valtrack_v4_mfw_esp32_c3": {
+        "name": "Valetron Systems VALTRACK-V4MVF",
+        "variant": VARIANT_ESP32C3,
+    },
+    "valtrack_v4_vts_esp32_c3": {
+        "name": "Valetron Systems VALTRACK-V4VTS",
+        "variant": VARIANT_ESP32C3,
+    },
     "vintlabs-devkit-v1": {
         "name": "VintLabs ESP32 Devkit",
         "variant": VARIANT_ESP32,