diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3baaf02506..c32dc11b61 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -52,6 +52,10 @@ jobs:
             file: tests/test6.yaml
             name: Test tests/test6.yaml
             pio_cache_key: test6
+          - id: test
+            file: tests/test7.yaml
+            name: Test tests/test7.yaml
+            pio_cache_key: test7
           - id: pytest
             name: Run pytest
           - id: clang-format
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d3b96141c4..3904834dc9 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -67,8 +67,10 @@ jobs:
       contents: read
       packages: write
     runs-on: ubuntu-latest
+    continue-on-error: ${{ matrix.image.title == 'lint' }}
     needs: [init]
     strategy:
+      fail-fast: false
       matrix:
         image:
           - title: "ha-addon"
@@ -136,14 +138,18 @@ jobs:
     runs-on: ubuntu-latest
     needs: [deploy-docker]
     steps:
-      - env:
-          TOKEN: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }}
-        # yamllint disable rule:line-length
-        run: |
-          curl \
-            -u ":$TOKEN" \
-            -X POST \
-            -H "Accept: application/vnd.github.v3+json" \
-            https://api.github.com/repos/esphome/home-assistant-addon/actions/workflows/bump-version.yml/dispatches \
-            -d '{"ref":"main","inputs":{"version":"${{ github.event.release.tag_name }}","content":${{ toJSON(github.event.release.body) }}}}'
-        # yamllint enable rule:line-length
+      - name: Trigger Workflow
+        uses: actions/github-script@v6
+        with:
+          github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }}
+          script: |
+            github.rest.actions.createWorkflowDispatch({
+              owner: "esphome",
+              repo: "home-assistant-addon",
+              workflow_id: "bump-version.yml",
+              ref: "main",
+              inputs: {
+                version: "${{ github.event.release.tag_name }}",
+                content: ${{ toJSON(github.event.release.body) }}
+              }
+            })
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 6cb3b659c4..a2ba086394 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -18,7 +18,7 @@ jobs:
   stale:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/stale@v6
+      - uses: actions/stale@v7
         with:
           days-before-pr-stale: 90
           days-before-pr-close: 7
@@ -38,7 +38,7 @@ jobs:
   close-issues:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/stale@v6
+      - uses: actions/stale@v7
         with:
           days-before-pr-stale: -1
           days-before-pr-close: -1
diff --git a/.gitignore b/.gitignore
index 110437c368..71b66b2499 100644
--- a/.gitignore
+++ b/.gitignore
@@ -128,3 +128,5 @@ tests/.esphome/
 
 sdkconfig.*
 !sdkconfig.defaults
+
+.tests/
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b8c0a22e81..e5ae80da3b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -3,7 +3,7 @@
 # See https://pre-commit.com/hooks.html for more hooks
 repos:
   - repo: https://github.com/ambv/black
-    rev: 22.10.0
+    rev: 22.12.0
     hooks:
       - id: black
         args:
diff --git a/CODEOWNERS b/CODEOWNERS
index d30060fe3b..ca1da2f153 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -41,6 +41,8 @@ esphome/components/ble_client/* @buxtronix
 esphome/components/bluetooth_proxy/* @jesserockz
 esphome/components/bme680_bsec/* @trvrnrth
 esphome/components/bmp3xx/* @martgras
+esphome/components/bp1658cj/* @Cossid
+esphome/components/bp5758d/* @Cossid
 esphome/components/button/* @esphome/core
 esphome/components/canbus/* @danielschramm @mvturnho
 esphome/components/cap1188/* @MrEditor97
@@ -69,6 +71,7 @@ esphome/components/display_menu_base/* @numo68
 esphome/components/dps310/* @kbx81
 esphome/components/ds1307/* @badbadc0ffee
 esphome/components/dsmr/* @glmnet @zuidwijk
+esphome/components/ee895/* @Stock-M
 esphome/components/ektf2232/* @jesserockz
 esphome/components/ens210/* @itn3rd77
 esphome/components/esp32/* @esphome/core
@@ -100,9 +103,11 @@ esphome/components/hitachi_ac424/* @sourabhjaiswal
 esphome/components/homeassistant/* @OttoWinter
 esphome/components/honeywellabp/* @RubyBailey
 esphome/components/hrxl_maxsonar_wr/* @netmikey
+esphome/components/hte501/* @Stock-M
 esphome/components/hydreon_rgxx/* @functionpointer
 esphome/components/i2c/* @esphome/core
 esphome/components/i2s_audio/* @jesserockz
+esphome/components/improv_base/* @esphome/core
 esphome/components/improv_serial/* @esphome/core
 esphome/components/ina260/* @MrEditor97
 esphome/components/inkbird_ibsth1_mini/* @fkirill
@@ -111,13 +116,17 @@ esphome/components/integration/* @OttoWinter
 esphome/components/interval/* @esphome/core
 esphome/components/json/* @OttoWinter
 esphome/components/kalman_combinator/* @Cat-Ion
+esphome/components/key_collector/* @ssieb
+esphome/components/key_provider/* @ssieb
 esphome/components/lcd_menu/* @numo68
+esphome/components/ld2410/* @sebcaps
 esphome/components/ledc/* @OttoWinter
 esphome/components/light/* @esphome/core
 esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
 esphome/components/lock/* @esphome/core
 esphome/components/logger/* @esphome/core
 esphome/components/ltr390/* @sjtrny
+esphome/components/matrix_keypad/* @ssieb
 esphome/components/max31865/* @DAVe3283
 esphome/components/max44009/* @berfenger
 esphome/components/max7219digit/* @rspaargaren
@@ -138,6 +147,7 @@ esphome/components/mcp9808/* @k7hpn
 esphome/components/md5/* @esphome/core
 esphome/components/mdns/* @esphome/core
 esphome/components/media_player/* @jesserockz
+esphome/components/mics_4514/* @jesserockz
 esphome/components/midea/* @dudanov
 esphome/components/midea_ir/* @dudanov
 esphome/components/mitsubishi/* @RubyBailey
@@ -164,6 +174,8 @@ esphome/components/nfc/* @jesserockz
 esphome/components/number/* @esphome/core
 esphome/components/ota/* @esphome/core
 esphome/components/output/* @esphome/core
+esphome/components/pca9554/* @hwstar
+esphome/components/pcf85063/* @brogon
 esphome/components/pid/* @OttoWinter
 esphome/components/pipsolar/* @andreashergert1984
 esphome/components/pm1006/* @habbie
@@ -204,8 +216,12 @@ esphome/components/sgp4x/* @SenexCrenshaw @martgras
 esphome/components/shelly_dimmer/* @edge90 @rnauber
 esphome/components/sht4x/* @sjtrny
 esphome/components/shutdown/* @esphome/core @jsuanet
+esphome/components/sigma_delta_output/* @Cat-Ion
 esphome/components/sim800l/* @glmnet
+esphome/components/sm10bit_base/* @Cossid
 esphome/components/sm2135/* @BoukeHaarsma23
+esphome/components/sm2235/* @Cossid
+esphome/components/sm2335/* @Cossid
 esphome/components/sml/* @alengwenus
 esphome/components/smt100/* @piechade
 esphome/components/sn74hc165/* @jesserockz
@@ -234,6 +250,7 @@ esphome/components/switch/* @esphome/core
 esphome/components/t6615/* @tylermenezes
 esphome/components/tca9548a/* @andreashergert1984
 esphome/components/tcl112/* @glmnet
+esphome/components/tee501/* @Stock-M
 esphome/components/teleinfo/* @0hax
 esphome/components/thermostat/* @kbx81
 esphome/components/time/* @OttoWinter
@@ -258,12 +275,15 @@ esphome/components/uart/* @esphome/core
 esphome/components/ufire_ec/* @pvizeli
 esphome/components/ufire_ise/* @pvizeli
 esphome/components/ultrasonic/* @OttoWinter
+esphome/components/vbus/* @ssieb
 esphome/components/version/* @esphome/core
 esphome/components/wake_on_lan/* @willwill2will54
 esphome/components/web_server_base/* @OttoWinter
 esphome/components/whirlpool/* @glmnet
 esphome/components/whynter/* @aeonsablaze
+esphome/components/wiegand/* @ssieb
 esphome/components/wl_134/* @hobbypunk90
+esphome/components/x9c/* @EtienneMD
 esphome/components/xiaomi_lywsd03mmc/* @ahpohl
 esphome/components/xiaomi_mhoc303/* @drug123
 esphome/components/xiaomi_mhoc401/* @vevsvevs
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 66b708f522..ddc666cf6a 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -26,7 +26,7 @@ RUN \
         python3-cryptography=3.3.2-1 \
         iputils-ping=3:20210202-1 \
         git=1:2.30.2-1 \
-        curl=7.74.0-1.3+deb11u3 \
+        curl=7.74.0-1.3+deb11u5 \
         openssh-client=1:8.4p1-5+deb11u1 \
     && rm -rf \
         /tmp/* \
diff --git a/docker/build.py b/docker/build.py
index ae977f87c1..47461ddf97 100755
--- a/docker/build.py
+++ b/docker/build.py
@@ -8,32 +8,49 @@ import re
 import sys
 
 
-CHANNEL_DEV = 'dev'
-CHANNEL_BETA = 'beta'
-CHANNEL_RELEASE = 'release'
+CHANNEL_DEV = "dev"
+CHANNEL_BETA = "beta"
+CHANNEL_RELEASE = "release"
 CHANNELS = [CHANNEL_DEV, CHANNEL_BETA, CHANNEL_RELEASE]
 
-ARCH_AMD64 = 'amd64'
-ARCH_ARMV7 = 'armv7'
-ARCH_AARCH64 = 'aarch64'
+ARCH_AMD64 = "amd64"
+ARCH_ARMV7 = "armv7"
+ARCH_AARCH64 = "aarch64"
 ARCHS = [ARCH_AMD64, ARCH_ARMV7, ARCH_AARCH64]
 
-TYPE_DOCKER = 'docker'
-TYPE_HA_ADDON = 'ha-addon'
-TYPE_LINT = 'lint'
+TYPE_DOCKER = "docker"
+TYPE_HA_ADDON = "ha-addon"
+TYPE_LINT = "lint"
 TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT]
 
 
 parser = argparse.ArgumentParser()
-parser.add_argument("--tag", type=str, required=True, help="The main docker tag to push to. If a version number also adds latest and/or beta tag")
-parser.add_argument("--arch", choices=ARCHS, required=False, help="The architecture to build for")
-parser.add_argument("--build-type", choices=TYPES, required=True, help="The type of build to run")
-parser.add_argument("--dry-run", action="store_true", help="Don't run any commands, just print them")
-subparsers = parser.add_subparsers(help="Action to perform", dest="command", required=True)
+parser.add_argument(
+    "--tag",
+    type=str,
+    required=True,
+    help="The main docker tag to push to. If a version number also adds latest and/or beta tag",
+)
+parser.add_argument(
+    "--arch", choices=ARCHS, required=False, help="The architecture to build for"
+)
+parser.add_argument(
+    "--build-type", choices=TYPES, required=True, help="The type of build to run"
+)
+parser.add_argument(
+    "--dry-run", action="store_true", help="Don't run any commands, just print them"
+)
+subparsers = parser.add_subparsers(
+    help="Action to perform", dest="command", required=True
+)
 build_parser = subparsers.add_parser("build", help="Build the image")
 build_parser.add_argument("--push", help="Also push the images", action="store_true")
-build_parser.add_argument("--load", help="Load the docker image locally", action="store_true")
-manifest_parser = subparsers.add_parser("manifest", help="Create a manifest from already pushed images")
+build_parser.add_argument(
+    "--load", help="Load the docker image locally", action="store_true"
+)
+manifest_parser = subparsers.add_parser(
+    "manifest", help="Create a manifest from already pushed images"
+)
 
 
 @dataclass(frozen=True)
@@ -49,7 +66,7 @@ class DockerParams:
         prefix = {
             TYPE_DOCKER: "esphome/esphome",
             TYPE_HA_ADDON: "esphome/esphome-hassio",
-            TYPE_LINT: "esphome/esphome-lint"
+            TYPE_LINT: "esphome/esphome-lint",
         }[build_type]
         build_to = f"{prefix}-{arch}"
         baseimgtype = {
@@ -128,13 +145,21 @@ def main():
 
         # 3. build
         cmd = [
-            "docker", "buildx", "build",
-            "--build-arg", f"BASEIMGTYPE={params.baseimgtype}",
-            "--build-arg", f"BUILD_VERSION={args.tag}",
-            "--cache-from", f"type=registry,ref={cache_img}",
-            "--file", "docker/Dockerfile",
-            "--platform", params.platform,
-            "--target", params.target,
+            "docker",
+            "buildx",
+            "build",
+            "--build-arg",
+            f"BASEIMGTYPE={params.baseimgtype}",
+            "--build-arg",
+            f"BUILD_VERSION={args.tag}",
+            "--cache-from",
+            f"type=registry,ref={cache_img}",
+            "--file",
+            "docker/Dockerfile",
+            "--platform",
+            params.platform,
+            "--target",
+            params.target,
         ]
         for img in imgs:
             cmd += ["--tag", img]
@@ -160,9 +185,7 @@ def main():
             run_command(*cmd)
         # 2. Push manifests
         for target in targets:
-            run_command(
-                "docker", "manifest", "push", target
-            )
+            run_command("docker", "manifest", "push", target)
 
 
 if __name__ == "__main__":
diff --git a/esphome/__main__.py b/esphome/__main__.py
index 9b6043ef50..24c2ce1d13 100644
--- a/esphome/__main__.py
+++ b/esphome/__main__.py
@@ -339,7 +339,7 @@ def command_config(args, config):
     _LOGGER.info("Configuration is valid!")
     if not CORE.verbose:
         config = strip_default_ids(config)
-    safe_print(yaml_util.dump(config))
+    safe_print(yaml_util.dump(config, args.show_secrets))
     return 0
 
 
@@ -665,6 +665,9 @@ def parse_args(argv):
     parser_config.add_argument(
         "configuration", help="Your YAML configuration file(s).", nargs="+"
     )
+    parser_config.add_argument(
+        "--show-secrets", help="Show secrets in output.", action="store_true"
+    )
 
     parser_compile = subparsers.add_parser(
         "compile", help="Read the configuration and compile a program."
diff --git a/esphome/components/ads1115/ads1115.cpp b/esphome/components/ads1115/ads1115.cpp
index beb379db93..c3f3c00c63 100644
--- a/esphome/components/ads1115/ads1115.cpp
+++ b/esphome/components/ads1115/ads1115.cpp
@@ -9,7 +9,7 @@ static const char *const TAG = "ads1115";
 static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00;
 static const uint8_t ADS1115_REGISTER_CONFIG = 0x01;
 
-static const uint8_t ADS1115_DATA_RATE_860_SPS = 0b111;
+static const uint8_t ADS1115_DATA_RATE_860_SPS = 0b111;  // 3300_SPS for ADS1015
 
 void ADS1115Component::setup() {
   ESP_LOGCONFIG(TAG, "Setting up ADS1115...");
@@ -18,6 +18,9 @@ void ADS1115Component::setup() {
     this->mark_failed();
     return;
   }
+
+  ESP_LOGCONFIG(TAG, "Configuring ADS1115...");
+
   uint16_t config = 0;
   // Clear single-shot bit
   //        0b0xxxxxxxxxxxxxxx
@@ -77,6 +80,7 @@ void ADS1115Component::dump_config() {
     LOG_SENSOR("  ", "Sensor", sensor);
     ESP_LOGCONFIG(TAG, "    Multiplexer: %u", sensor->get_multiplexer());
     ESP_LOGCONFIG(TAG, "    Gain: %u", sensor->get_gain());
+    ESP_LOGCONFIG(TAG, "    Resolution: %u", sensor->get_resolution());
   }
 }
 float ADS1115Component::request_measurement(ADS1115Sensor *sensor) {
@@ -127,27 +131,45 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) {
     this->status_set_warning();
     return NAN;
   }
+
+  if (sensor->get_resolution() == ADS1015_12_BITS) {
+    bool negative = (raw_conversion >> 15) == 1;
+
+    // shift raw_conversion as it's only 12-bits, left justified
+    raw_conversion = raw_conversion >> (16 - ADS1015_12_BITS);
+
+    // check if number was negative in order to keep the sign
+    if (negative) {
+      // the number was negative
+      // 1) set the negative bit back
+      raw_conversion |= 0x8000;
+      // 2) reset the former (shifted) negative bit
+      raw_conversion &= 0xF7FF;
+    }
+  }
+
   auto signed_conversion = static_cast<int16_t>(raw_conversion);
 
   float millivolts;
+  float divider = (sensor->get_resolution() == ADS1115_16_BITS) ? 32768.0f : 2048.0f;
   switch (sensor->get_gain()) {
     case ADS1115_GAIN_6P144:
-      millivolts = signed_conversion * 0.187500f;
+      millivolts = (signed_conversion * 6144) / divider;
       break;
     case ADS1115_GAIN_4P096:
-      millivolts = signed_conversion * 0.125000f;
+      millivolts = (signed_conversion * 4096) / divider;
       break;
     case ADS1115_GAIN_2P048:
-      millivolts = signed_conversion * 0.062500f;
+      millivolts = (signed_conversion * 2048) / divider;
       break;
     case ADS1115_GAIN_1P024:
-      millivolts = signed_conversion * 0.031250f;
+      millivolts = (signed_conversion * 1024) / divider;
       break;
     case ADS1115_GAIN_0P512:
-      millivolts = signed_conversion * 0.015625f;
+      millivolts = (signed_conversion * 512) / divider;
       break;
     case ADS1115_GAIN_0P256:
-      millivolts = signed_conversion * 0.007813f;
+      millivolts = (signed_conversion * 256) / divider;
       break;
     default:
       millivolts = NAN;
diff --git a/esphome/components/ads1115/ads1115.h b/esphome/components/ads1115/ads1115.h
index 17d5a910d8..0b8bfb339b 100644
--- a/esphome/components/ads1115/ads1115.h
+++ b/esphome/components/ads1115/ads1115.h
@@ -30,6 +30,11 @@ enum ADS1115Gain {
   ADS1115_GAIN_0P256 = 0b101,
 };
 
+enum ADS1115Resolution {
+  ADS1115_16_BITS = 16,
+  ADS1015_12_BITS = 12,
+};
+
 class ADS1115Sensor;
 
 class ADS1115Component : public Component, public i2c::I2CDevice {
@@ -58,15 +63,17 @@ class ADS1115Sensor : public sensor::Sensor, public PollingComponent, public vol
   void update() override;
   void set_multiplexer(ADS1115Multiplexer multiplexer) { multiplexer_ = multiplexer; }
   void set_gain(ADS1115Gain gain) { gain_ = gain; }
-
+  void set_resolution(ADS1115Resolution resolution) { resolution_ = resolution; }
   float sample() override;
   uint8_t get_multiplexer() const { return multiplexer_; }
   uint8_t get_gain() const { return gain_; }
+  uint8_t get_resolution() const { return resolution_; }
 
  protected:
   ADS1115Component *parent_;
   ADS1115Multiplexer multiplexer_;
   ADS1115Gain gain_;
+  ADS1115Resolution resolution_;
 };
 
 }  // namespace ads1115
diff --git a/esphome/components/ads1115/sensor.py b/esphome/components/ads1115/sensor.py
index 190e641ca3..f0d894e2af 100644
--- a/esphome/components/ads1115/sensor.py
+++ b/esphome/components/ads1115/sensor.py
@@ -4,6 +4,7 @@ from esphome.components import sensor, voltage_sampler
 from esphome.const import (
     CONF_GAIN,
     CONF_MULTIPLEXER,
+    CONF_RESOLUTION,
     DEVICE_CLASS_VOLTAGE,
     STATE_CLASS_MEASUREMENT,
     UNIT_VOLT,
@@ -35,6 +36,12 @@ GAIN = {
     "0.256": ADS1115Gain.ADS1115_GAIN_0P256,
 }
 
+ADS1115Resolution = ads1115_ns.enum("ADS1115Resolution")
+RESOLUTION = {
+    "16_BITS": ADS1115Resolution.ADS1115_16_BITS,
+    "12_BITS": ADS1115Resolution.ADS1015_12_BITS,
+}
+
 
 def validate_gain(value):
     if isinstance(value, float):
@@ -63,6 +70,9 @@ CONFIG_SCHEMA = (
             cv.GenerateID(CONF_ADS1115_ID): cv.use_id(ADS1115Component),
             cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"),
             cv.Required(CONF_GAIN): validate_gain,
+            cv.Optional(CONF_RESOLUTION, default="16_BITS"): cv.enum(
+                RESOLUTION, upper=True, space="_"
+            ),
         }
     )
     .extend(cv.polling_component_schema("60s"))
@@ -77,5 +87,6 @@ async def to_code(config):
 
     cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER]))
     cg.add(var.set_gain(config[CONF_GAIN]))
+    cg.add(var.set_resolution(config[CONF_RESOLUTION]))
 
     cg.add(paren.register_sensor(var))
diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py
index 87d72254e8..ce9f057496 100644
--- a/esphome/components/animation/__init__.py
+++ b/esphome/components/animation/__init__.py
@@ -117,7 +117,7 @@ async def to_code(config):
                 data[pos] = rgb & 255
                 pos += 1
 
-    elif config[CONF_TYPE] == "BINARY":
+    elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
         width8 = ((width + 7) // 8) * 8
         data = [0 for _ in range((height * width8 // 8) * frames)]
         for frameIndex in range(frames):
diff --git a/esphome/components/apds9960/apds9960.cpp b/esphome/components/apds9960/apds9960.cpp
index 05091f3f7d..1c6ec5c14a 100644
--- a/esphome/components/apds9960/apds9960.cpp
+++ b/esphome/components/apds9960/apds9960.cpp
@@ -23,7 +23,7 @@ void APDS9960::setup() {
     return;
   }
 
-  if (id != 0xAB && id != 0x9C) {  // APDS9960 all should have one of these IDs
+  if (id != 0xAB && id != 0x9C && id != 0xA8) {  // APDS9960 all should have one of these IDs
     this->error_code_ = WRONG_ID;
     this->mark_failed();
     return;
diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto
index e1bc7b0a57..ffb3bcb07e 100644
--- a/esphome/components/api/api.proto
+++ b/esphome/components/api/api.proto
@@ -206,6 +206,8 @@ message DeviceInfoResponse {
   uint32 bluetooth_proxy_version = 11;
 
   string manufacturer = 12;
+
+  string friendly_name = 13;
 }
 
 message ListEntitiesRequest {
@@ -785,6 +787,7 @@ enum ClimateFanMode {
   CLIMATE_FAN_MIDDLE = 6;
   CLIMATE_FAN_FOCUS = 7;
   CLIMATE_FAN_DIFFUSE = 8;
+  CLIMATE_FAN_QUIET = 9;
 }
 enum ClimateSwingMode {
   CLIMATE_SWING_OFF = 0;
diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp
index aac58587d1..65659941d6 100644
--- a/esphome/components/api/api_connection.cpp
+++ b/esphome/components/api/api_connection.cpp
@@ -930,6 +930,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
   DeviceInfoResponse resp{};
   resp.uses_password = this->parent_->uses_password();
   resp.name = App.get_name();
+  resp.friendly_name = App.get_friendly_name();
   resp.mac_address = get_mac_address_pretty();
   resp.esphome_version = ESPHOME_VERSION;
   resp.compilation_time = App.get_compilation_time();
diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp
index b19a55764f..c18e045a99 100644
--- a/esphome/components/api/api_frame_helper.cpp
+++ b/esphome/components/api/api_frame_helper.cpp
@@ -616,6 +616,9 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
   struct iovec iov[2];
   iov[0].iov_base = header;
   iov[0].iov_len = 3;
+  if (len == 0) {
+    return write_raw_(iov, 1);
+  }
   iov[1].iov_base = const_cast<uint8_t *>(data);
   iov[1].iov_len = len;
 
@@ -913,6 +916,9 @@ APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *pay
   struct iovec iov[2];
   iov[0].iov_base = &header[0];
   iov[0].iov_len = header.size();
+  if (payload_len == 0) {
+    return write_raw_(iov, 1);
+  }
   iov[1].iov_base = const_cast<uint8_t *>(payload);
   iov[1].iov_len = payload_len;
 
diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp
index f108d38e8f..9df05d2978 100644
--- a/esphome/components/api/api_pb2.cpp
+++ b/esphome/components/api/api_pb2.cpp
@@ -235,6 +235,8 @@ template<> const char *proto_enum_to_string<enums::ClimateFanMode>(enums::Climat
       return "CLIMATE_FAN_FOCUS";
     case enums::CLIMATE_FAN_DIFFUSE:
       return "CLIMATE_FAN_DIFFUSE";
+    case enums::CLIMATE_FAN_QUIET:
+      return "CLIMATE_FAN_QUIET";
     default:
       return "UNKNOWN";
   }
@@ -628,6 +630,10 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v
       this->manufacturer = value.as_string();
       return true;
     }
+    case 13: {
+      this->friendly_name = value.as_string();
+      return true;
+    }
     default:
       return false;
   }
@@ -645,6 +651,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_uint32(10, this->webserver_port);
   buffer.encode_uint32(11, this->bluetooth_proxy_version);
   buffer.encode_string(12, this->manufacturer);
+  buffer.encode_string(13, this->friendly_name);
 }
 #ifdef HAS_PROTO_MESSAGE_DUMP
 void DeviceInfoResponse::dump_to(std::string &out) const {
@@ -699,6 +706,10 @@ void DeviceInfoResponse::dump_to(std::string &out) const {
   out.append("  manufacturer: ");
   out.append("'").append(this->manufacturer).append("'");
   out.append("\n");
+
+  out.append("  friendly_name: ");
+  out.append("'").append(this->friendly_name).append("'");
+  out.append("\n");
   out.append("}");
 }
 #endif
diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h
index 8a78f1ad03..2db1c6fafa 100644
--- a/esphome/components/api/api_pb2.h
+++ b/esphome/components/api/api_pb2.h
@@ -99,6 +99,7 @@ enum ClimateFanMode : uint32_t {
   CLIMATE_FAN_MIDDLE = 6,
   CLIMATE_FAN_FOCUS = 7,
   CLIMATE_FAN_DIFFUSE = 8,
+  CLIMATE_FAN_QUIET = 9,
 };
 enum ClimateSwingMode : uint32_t {
   CLIMATE_SWING_OFF = 0,
@@ -276,6 +277,7 @@ class DeviceInfoResponse : public ProtoMessage {
   uint32_t webserver_port{0};
   uint32_t bluetooth_proxy_version{0};
   std::string manufacturer{};
+  std::string friendly_name{};
   void encode(ProtoWriteBuffer buffer) const override;
 #ifdef HAS_PROTO_MESSAGE_DUMP
   void dump_to(std::string &out) const override;
diff --git a/esphome/components/bedjet/fan/bedjet_fan.cpp b/esphome/components/bedjet/fan/bedjet_fan.cpp
index 02ac289e0e..e272241040 100644
--- a/esphome/components/bedjet/fan/bedjet_fan.cpp
+++ b/esphome/components/bedjet/fan/bedjet_fan.cpp
@@ -37,9 +37,13 @@ void BedJetFan::control(const fan::FanCall &call) {
 
   // ignore speed changes if not on or turning on
   if (this->state && call.get_speed().has_value()) {
-    this->speed = *call.get_speed();
-    this->parent_->set_fan_index(this->speed);
-    did_change = true;
+    auto speed = *call.get_speed();
+    if (speed >= 1) {
+      this->speed = speed;
+      // Fan.speed is 1-20, but Bedjet expects 0-19, so subtract 1
+      this->parent_->set_fan_index(this->speed - 1);
+      did_change = true;
+    }
   }
 
   if (did_change) {
@@ -57,8 +61,9 @@ void BedJetFan::on_status(const BedjetStatusPacket *data) {
     did_change = true;
   }
 
-  if (data->fan_step != this->speed) {
-    this->speed = data->fan_step;
+  // BedjetStatusPacket.fan_step is in range 0-19, but Fan.speed wants 1-20.
+  if (data->fan_step + 1 != this->speed) {
+    this->speed = data->fan_step + 1;
     did_change = true;
   }
 
diff --git a/esphome/components/bp1658cj/__init__.py b/esphome/components/bp1658cj/__init__.py
new file mode 100644
index 0000000000..8388b16df9
--- /dev/null
+++ b/esphome/components/bp1658cj/__init__.py
@@ -0,0 +1,44 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import pins
+from esphome.const import (
+    CONF_CLOCK_PIN,
+    CONF_DATA_PIN,
+    CONF_ID,
+)
+
+CODEOWNERS = ["@Cossid"]
+MULTI_CONF = True
+
+CONF_MAX_POWER_COLOR_CHANNELS = "max_power_color_channels"
+CONF_MAX_POWER_WHITE_CHANNELS = "max_power_white_channels"
+
+AUTO_LOAD = ["output"]
+bp1658cj_ns = cg.esphome_ns.namespace("bp1658cj")
+BP1658CJ = bp1658cj_ns.class_("BP1658CJ", cg.Component)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(BP1658CJ),
+        cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema,
+        cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema,
+        cv.Optional(CONF_MAX_POWER_COLOR_CHANNELS, default=2): cv.int_range(
+            min=0, max=15
+        ),
+        cv.Optional(CONF_MAX_POWER_WHITE_CHANNELS, default=4): cv.int_range(
+            min=0, max=15
+        ),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+
+    data = await cg.gpio_pin_expression(config[CONF_DATA_PIN])
+    cg.add(var.set_data_pin(data))
+    clock = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN])
+    cg.add(var.set_clock_pin(clock))
+    cg.add(var.set_max_power_color_channels(config[CONF_MAX_POWER_COLOR_CHANNELS]))
+    cg.add(var.set_max_power_white_channels(config[CONF_MAX_POWER_WHITE_CHANNELS]))
diff --git a/esphome/components/bp1658cj/bp1658cj.cpp b/esphome/components/bp1658cj/bp1658cj.cpp
new file mode 100644
index 0000000000..5b9e4a5a2c
--- /dev/null
+++ b/esphome/components/bp1658cj/bp1658cj.cpp
@@ -0,0 +1,110 @@
+#include "bp1658cj.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace bp1658cj {
+
+static const char *const TAG = "bp1658cj";
+
+static const uint8_t BP1658CJ_MODEL_ID = 0x80;
+static const uint8_t BP1658CJ_ADDR_STANDBY = 0x0;
+static const uint8_t BP1658CJ_ADDR_START_3CH = 0x10;
+static const uint8_t BP1658CJ_ADDR_START_2CH = 0x20;
+static const uint8_t BP1658CJ_ADDR_START_5CH = 0x30;
+
+void BP1658CJ::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up BP1658CJ Output Component...");
+  this->data_pin_->setup();
+  this->data_pin_->digital_write(false);
+  this->clock_pin_->setup();
+  this->clock_pin_->digital_write(false);
+  this->pwm_amounts_.resize(5, 0);
+}
+void BP1658CJ::dump_config() {
+  ESP_LOGCONFIG(TAG, "BP1658CJ:");
+  LOG_PIN("  Data Pin: ", this->data_pin_);
+  LOG_PIN("  Clock Pin: ", this->clock_pin_);
+  ESP_LOGCONFIG(TAG, "  Color Channels Max Power: %u", this->max_power_color_channels_);
+  ESP_LOGCONFIG(TAG, "  White Channels Max Power: %u", this->max_power_white_channels_);
+}
+
+void BP1658CJ::loop() {
+  if (!this->update_)
+    return;
+
+  uint8_t data[12];
+  if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 &&
+      this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) {
+    // Off / Sleep
+    data[0] = BP1658CJ_MODEL_ID + BP1658CJ_ADDR_STANDBY;
+    for (int i = 1; i < 12; i++)
+      data[i] = 0;
+    this->write_buffer_(data, 12);
+  } else if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 &&
+             (this->pwm_amounts_[3] > 0 || this->pwm_amounts_[4] > 0)) {
+    // Only data on white channels
+    data[0] = BP1658CJ_MODEL_ID + BP1658CJ_ADDR_START_2CH;
+    data[1] = 0 << 4 | this->max_power_white_channels_;
+    for (int i = 2, j = 0; i < 12; i += 2, j++) {
+      data[i] = this->pwm_amounts_[j] & 0x1F;
+      data[i + 1] = (this->pwm_amounts_[j] >> 5) & 0x1F;
+    }
+    this->write_buffer_(data, 12);
+  } else if ((this->pwm_amounts_[0] > 0 || this->pwm_amounts_[1] > 0 || this->pwm_amounts_[2] > 0) &&
+             this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) {
+    // Only data on RGB channels
+    data[0] = BP1658CJ_MODEL_ID + BP1658CJ_ADDR_START_3CH;
+    data[1] = this->max_power_color_channels_ << 4 | 0;
+    for (int i = 2, j = 0; i < 12; i += 2, j++) {
+      data[i] = this->pwm_amounts_[j] & 0x1F;
+      data[i + 1] = (this->pwm_amounts_[j] >> 5) & 0x1F;
+    }
+    this->write_buffer_(data, 12);
+  } else {
+    // All channels
+    data[0] = BP1658CJ_MODEL_ID + BP1658CJ_ADDR_START_5CH;
+    data[1] = this->max_power_color_channels_ << 4 | this->max_power_white_channels_;
+    for (int i = 2, j = 0; i < 12; i += 2, j++) {
+      data[i] = this->pwm_amounts_[j] & 0x1F;
+      data[i + 1] = (this->pwm_amounts_[j] >> 5) & 0x1F;
+    }
+    this->write_buffer_(data, 12);
+  }
+
+  this->update_ = false;
+}
+
+void BP1658CJ::set_channel_value_(uint8_t channel, uint16_t value) {
+  if (this->pwm_amounts_[channel] != value) {
+    this->update_ = true;
+    this->update_channel_ = channel;
+  }
+  this->pwm_amounts_[channel] = value;
+}
+void BP1658CJ::write_bit_(bool value) {
+  this->clock_pin_->digital_write(false);
+  this->data_pin_->digital_write(value);
+  this->clock_pin_->digital_write(true);
+}
+
+void BP1658CJ::write_byte_(uint8_t data) {
+  for (uint8_t mask = 0x80; mask; mask >>= 1) {
+    this->write_bit_(data & mask);
+  }
+  this->clock_pin_->digital_write(false);
+  this->data_pin_->digital_write(true);
+  this->clock_pin_->digital_write(true);
+}
+
+void BP1658CJ::write_buffer_(uint8_t *buffer, uint8_t size) {
+  this->data_pin_->digital_write(false);
+  for (uint32_t i = 0; i < size; i++) {
+    this->write_byte_(buffer[i]);
+  }
+  this->clock_pin_->digital_write(false);
+  this->clock_pin_->digital_write(true);
+  this->data_pin_->digital_write(true);
+}
+
+}  // namespace bp1658cj
+}  // namespace esphome
diff --git a/esphome/components/bp1658cj/bp1658cj.h b/esphome/components/bp1658cj/bp1658cj.h
new file mode 100644
index 0000000000..778f49b3e9
--- /dev/null
+++ b/esphome/components/bp1658cj/bp1658cj.h
@@ -0,0 +1,64 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/hal.h"
+#include "esphome/components/output/float_output.h"
+#include <vector>
+
+namespace esphome {
+namespace bp1658cj {
+
+class BP1658CJ : public Component {
+ public:
+  class Channel;
+
+  void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; }
+  void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; }
+  void set_max_power_color_channels(uint8_t max_power_color_channels) {
+    max_power_color_channels_ = max_power_color_channels;
+  }
+  void set_max_power_white_channels(uint8_t max_power_white_channels) {
+    max_power_white_channels_ = max_power_white_channels;
+  }
+
+  void setup() override;
+
+  void dump_config() override;
+
+  float get_setup_priority() const override { return setup_priority::HARDWARE; }
+
+  /// Send new values if they were updated.
+  void loop() override;
+
+  class Channel : public output::FloatOutput {
+   public:
+    void set_parent(BP1658CJ *parent) { parent_ = parent; }
+    void set_channel(uint8_t channel) { channel_ = channel; }
+
+   protected:
+    void write_state(float state) override {
+      auto amount = static_cast<uint16_t>(state * 0x3FF);
+      this->parent_->set_channel_value_(this->channel_, amount);
+    }
+
+    BP1658CJ *parent_;
+    uint8_t channel_;
+  };
+
+ protected:
+  void set_channel_value_(uint8_t channel, uint16_t value);
+  void write_bit_(bool value);
+  void write_byte_(uint8_t data);
+  void write_buffer_(uint8_t *buffer, uint8_t size);
+
+  GPIOPin *data_pin_;
+  GPIOPin *clock_pin_;
+  uint8_t max_power_color_channels_{4};
+  uint8_t max_power_white_channels_{6};
+  uint8_t update_channel_;
+  std::vector<uint16_t> pwm_amounts_;
+  bool update_{true};
+};
+
+}  // namespace bp1658cj
+}  // namespace esphome
diff --git a/esphome/components/bp1658cj/output.py b/esphome/components/bp1658cj/output.py
new file mode 100644
index 0000000000..3b89518621
--- /dev/null
+++ b/esphome/components/bp1658cj/output.py
@@ -0,0 +1,27 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import output
+from esphome.const import CONF_CHANNEL, CONF_ID
+from . import BP1658CJ
+
+DEPENDENCIES = ["bp1658cj"]
+
+Channel = BP1658CJ.class_("Channel", output.FloatOutput)
+
+CONF_BP1658CJ_ID = "bp1658cj_id"
+CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
+    {
+        cv.GenerateID(CONF_BP1658CJ_ID): cv.use_id(BP1658CJ),
+        cv.Required(CONF_ID): cv.declare_id(Channel),
+        cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await output.register_output(var, config)
+
+    parent = await cg.get_variable(config[CONF_BP1658CJ_ID])
+    cg.add(var.set_parent(parent))
+    cg.add(var.set_channel(config[CONF_CHANNEL]))
diff --git a/esphome/components/bp5758d/__init__.py b/esphome/components/bp5758d/__init__.py
new file mode 100644
index 0000000000..eeeab2a1bd
--- /dev/null
+++ b/esphome/components/bp5758d/__init__.py
@@ -0,0 +1,33 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import pins
+from esphome.const import (
+    CONF_CLOCK_PIN,
+    CONF_DATA_PIN,
+    CONF_ID,
+)
+
+CODEOWNERS = ["@Cossid"]
+MULTI_CONF = True
+
+AUTO_LOAD = ["output"]
+bp5758d_ns = cg.esphome_ns.namespace("bp5758d")
+BP5758D = bp5758d_ns.class_("BP5758D", cg.Component)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(BP5758D),
+        cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema,
+        cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema,
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+
+    data = await cg.gpio_pin_expression(config[CONF_DATA_PIN])
+    cg.add(var.set_data_pin(data))
+    clock = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN])
+    cg.add(var.set_clock_pin(clock))
diff --git a/esphome/components/bp5758d/bp5758d.cpp b/esphome/components/bp5758d/bp5758d.cpp
new file mode 100644
index 0000000000..111fd6b68e
--- /dev/null
+++ b/esphome/components/bp5758d/bp5758d.cpp
@@ -0,0 +1,147 @@
+#include "bp5758d.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace bp5758d {
+
+static const char *const TAG = "bp5758d";
+
+static const uint8_t BP5758D_MODEL_ID = 0b10000000;
+static const uint8_t BP5758D_ADDR_STANDBY = 0b00000000;
+// Note, channel start address seems ambiguous and mis-translated.
+// Documentation states all are "invalid sleep"
+// All 3 values appear to activate all 5 channels.  Using the mapping
+// from BP1658CJ ordering since it won't break anything.
+static const uint8_t BP5758D_ADDR_START_3CH = 0b00010000;
+static const uint8_t BP5758D_ADDR_START_2CH = 0b00100000;
+static const uint8_t BP5758D_ADDR_START_5CH = 0b00110000;
+static const uint8_t BP5758D_ALL_DATA_CHANNEL_ENABLEMENT = 0b00011111;
+
+void BP5758D::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up BP5758D Output Component...");
+  this->data_pin_->setup();
+  this->data_pin_->digital_write(false);
+  this->clock_pin_->setup();
+  this->clock_pin_->digital_write(false);
+  this->channel_current_.resize(5, 0);
+  this->pwm_amounts_.resize(5, 0);
+}
+void BP5758D::dump_config() {
+  ESP_LOGCONFIG(TAG, "BP5758D:");
+  LOG_PIN("  Data Pin: ", this->data_pin_);
+  LOG_PIN("  Clock Pin: ", this->clock_pin_);
+}
+
+void BP5758D::loop() {
+  if (!this->update_)
+    return;
+
+  uint8_t data[17];
+  if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 &&
+      this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) {
+    // Off / Sleep
+    data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_STANDBY;
+    for (int i = 1; i < 16; i++)
+      data[i] = 0;
+    this->write_buffer_(data, 17);
+  } else if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 &&
+             (this->pwm_amounts_[3] > 0 || this->pwm_amounts_[4] > 0)) {
+    // Only data on white channels
+    data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_START_2CH;
+    data[1] = BP5758D_ALL_DATA_CHANNEL_ENABLEMENT;
+    data[2] = 0;
+    data[3] = 0;
+    data[4] = 0;
+    data[5] = this->pwm_amounts_[3] > 0 ? correct_current_level_bits_(this->channel_current_[3]) : 0;
+    data[6] = this->pwm_amounts_[4] > 0 ? correct_current_level_bits_(this->channel_current_[4]) : 0;
+    for (int i = 7, j = 0; i <= 15; i += 2, j++) {
+      data[i] = this->pwm_amounts_[j] & 0x1F;
+      data[i + 1] = (this->pwm_amounts_[j] >> 5) & 0x1F;
+    }
+    this->write_buffer_(data, 17);
+  } else if ((this->pwm_amounts_[0] > 0 || this->pwm_amounts_[1] > 0 || this->pwm_amounts_[2] > 0) &&
+             this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) {
+    // Only data on RGB channels
+    data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_START_3CH;
+    data[1] = BP5758D_ALL_DATA_CHANNEL_ENABLEMENT;
+    data[2] = this->pwm_amounts_[0] > 0 ? correct_current_level_bits_(this->channel_current_[0]) : 0;
+    data[3] = this->pwm_amounts_[1] > 0 ? correct_current_level_bits_(this->channel_current_[1]) : 0;
+    data[4] = this->pwm_amounts_[2] > 0 ? correct_current_level_bits_(this->channel_current_[2]) : 0;
+    data[5] = 0;
+    data[6] = 0;
+    for (int i = 7, j = 0; i <= 15; i += 2, j++) {
+      data[i] = this->pwm_amounts_[j] & 0x1F;
+      data[i + 1] = (this->pwm_amounts_[j] >> 5) & 0x1F;
+    }
+    this->write_buffer_(data, 17);
+  } else {
+    // All channels
+    data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_START_5CH;
+    data[1] = BP5758D_ALL_DATA_CHANNEL_ENABLEMENT;
+    data[2] = this->pwm_amounts_[0] > 0 ? correct_current_level_bits_(this->channel_current_[0]) : 0;
+    data[3] = this->pwm_amounts_[1] > 0 ? correct_current_level_bits_(this->channel_current_[1]) : 0;
+    data[4] = this->pwm_amounts_[2] > 0 ? correct_current_level_bits_(this->channel_current_[2]) : 0;
+    data[5] = this->pwm_amounts_[3] > 0 ? correct_current_level_bits_(this->channel_current_[3]) : 0;
+    data[6] = this->pwm_amounts_[4] > 0 ? correct_current_level_bits_(this->channel_current_[4]) : 0;
+    for (int i = 7, j = 0; i <= 15; i += 2, j++) {
+      data[i] = this->pwm_amounts_[j] & 0x1F;
+      data[i + 1] = (this->pwm_amounts_[j] >> 5) & 0x1F;
+    }
+    this->write_buffer_(data, 17);
+  }
+
+  this->update_ = false;
+}
+
+uint8_t BP5758D::correct_current_level_bits_(uint8_t current) {
+  // Anything below 64 uses normal bitmapping.
+  if (current < 64) {
+    return current;
+  }
+
+  // Anything above 63 needs to be offset by +34 because the driver remaps bit 7 (normally 64) to 30.
+  // (no idea why(!) but it is documented)
+  // Example:
+  // integer 64 would normally put out 0b01000000 but here 0b01000000 = 30 whereas everything lower
+  // is normal, so we add 34 to the integer where
+  // integer 98 = 0b01100010 which is 30 (7th bit adjusted) + 34 (1st-6th bits).
+  return current + 34;
+}
+
+void BP5758D::set_channel_value_(uint8_t channel, uint16_t value) {
+  if (this->pwm_amounts_[channel] != value) {
+    this->update_ = true;
+    this->update_channel_ = channel;
+  }
+  this->pwm_amounts_[channel] = value;
+}
+
+void BP5758D::set_channel_current_(uint8_t channel, uint8_t current) { this->channel_current_[channel] = current; }
+
+void BP5758D::write_bit_(bool value) {
+  this->clock_pin_->digital_write(false);
+  this->data_pin_->digital_write(value);
+  this->clock_pin_->digital_write(true);
+}
+
+void BP5758D::write_byte_(uint8_t data) {
+  for (uint8_t mask = 0x80; mask; mask >>= 1) {
+    this->write_bit_(data & mask);
+  }
+  this->clock_pin_->digital_write(false);
+  this->data_pin_->digital_write(true);
+  this->clock_pin_->digital_write(true);
+}
+
+void BP5758D::write_buffer_(uint8_t *buffer, uint8_t size) {
+  this->data_pin_->digital_write(false);
+  for (uint32_t i = 0; i < size; i++) {
+    this->write_byte_(buffer[i]);
+  }
+  this->clock_pin_->digital_write(false);
+  this->clock_pin_->digital_write(true);
+  this->data_pin_->digital_write(true);
+}
+
+}  // namespace bp5758d
+}  // namespace esphome
diff --git a/esphome/components/bp5758d/bp5758d.h b/esphome/components/bp5758d/bp5758d.h
new file mode 100644
index 0000000000..cc7cc3d5f8
--- /dev/null
+++ b/esphome/components/bp5758d/bp5758d.h
@@ -0,0 +1,64 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/hal.h"
+#include "esphome/components/output/float_output.h"
+#include <vector>
+
+namespace esphome {
+namespace bp5758d {
+
+class BP5758D : public Component {
+ public:
+  class Channel;
+
+  void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; }
+  void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; }
+
+  void setup() override;
+
+  void dump_config() override;
+
+  float get_setup_priority() const override { return setup_priority::HARDWARE; }
+
+  /// Send new values if they were updated.
+  void loop() override;
+
+  class Channel : public output::FloatOutput {
+   public:
+    void set_parent(BP5758D *parent) { parent_ = parent; }
+    void set_channel(uint8_t channel) { channel_ = channel; }
+    void set_current(uint8_t current) { current_ = current; }
+
+   protected:
+    void write_state(float state) override {
+      auto amount = static_cast<uint16_t>(state * 0x3FF);
+      // We're enforcing channels start at 1 to mach OUT1-OUT5, we must adjust
+      // to our 0-based array internally here by subtracting 1.
+      this->parent_->set_channel_value_(this->channel_ - 1, amount);
+      this->parent_->set_channel_current_(this->channel_ - 1, this->current_);
+    }
+
+    BP5758D *parent_;
+    uint8_t channel_;
+    uint8_t current_;
+  };
+
+ protected:
+  uint8_t correct_current_level_bits_(uint8_t current);
+  void set_channel_value_(uint8_t channel, uint16_t value);
+  void set_channel_current_(uint8_t channel, uint8_t current);
+  void write_bit_(bool value);
+  void write_byte_(uint8_t data);
+  void write_buffer_(uint8_t *buffer, uint8_t size);
+
+  GPIOPin *data_pin_;
+  GPIOPin *clock_pin_;
+  uint8_t update_channel_;
+  std::vector<uint8_t> channel_current_;
+  std::vector<uint16_t> pwm_amounts_;
+  bool update_{true};
+};
+
+}  // namespace bp5758d
+}  // namespace esphome
diff --git a/esphome/components/bp5758d/output.py b/esphome/components/bp5758d/output.py
new file mode 100644
index 0000000000..d0083fb33f
--- /dev/null
+++ b/esphome/components/bp5758d/output.py
@@ -0,0 +1,29 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import output
+from esphome.const import CONF_CHANNEL, CONF_ID, CONF_CURRENT
+from . import BP5758D
+
+DEPENDENCIES = ["bp5758d"]
+
+Channel = BP5758D.class_("Channel", output.FloatOutput)
+
+CONF_BP5758D_ID = "bp5758d_id"
+CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
+    {
+        cv.GenerateID(CONF_BP5758D_ID): cv.use_id(BP5758D),
+        cv.Required(CONF_ID): cv.declare_id(Channel),
+        cv.Required(CONF_CHANNEL): cv.int_range(min=1, max=5),
+        cv.Optional(CONF_CURRENT, default=10): cv.int_range(min=0, max=90),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await output.register_output(var, config)
+
+    parent = await cg.get_variable(config[CONF_BP5758D_ID])
+    cg.add(var.set_parent(parent))
+    cg.add(var.set_channel(config[CONF_CHANNEL]))
+    cg.add(var.set_current(config[CONF_CURRENT]))
diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py
index 8a3cd38444..eaa87afcb1 100644
--- a/esphome/components/climate/__init__.py
+++ b/esphome/components/climate/__init__.py
@@ -22,6 +22,8 @@ from esphome.const import (
     CONF_MODE_STATE_TOPIC,
     CONF_ON_STATE,
     CONF_PRESET,
+    CONF_PRESET_COMMAND_TOPIC,
+    CONF_PRESET_STATE_TOPIC,
     CONF_SWING_MODE,
     CONF_SWING_MODE_COMMAND_TOPIC,
     CONF_SWING_MODE_STATE_TOPIC,
@@ -73,6 +75,7 @@ CLIMATE_FAN_MODES = {
     "MIDDLE": ClimateFanMode.CLIMATE_FAN_MIDDLE,
     "FOCUS": ClimateFanMode.CLIMATE_FAN_FOCUS,
     "DIFFUSE": ClimateFanMode.CLIMATE_FAN_DIFFUSE,
+    "QUIET": ClimateFanMode.CLIMATE_FAN_QUIET,
 }
 
 validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True)
@@ -142,6 +145,12 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).
         cv.Optional(CONF_MODE_STATE_TOPIC): cv.All(
             cv.requires_component("mqtt"), cv.publish_topic
         ),
+        cv.Optional(CONF_PRESET_COMMAND_TOPIC): cv.All(
+            cv.requires_component("mqtt"), cv.publish_topic
+        ),
+        cv.Optional(CONF_PRESET_STATE_TOPIC): cv.All(
+            cv.requires_component("mqtt"), cv.publish_topic
+        ),
         cv.Optional(CONF_SWING_MODE_COMMAND_TOPIC): cv.All(
             cv.requires_component("mqtt"), cv.publish_topic
         ),
@@ -216,7 +225,12 @@ async def setup_climate_core_(var, config):
             cg.add(mqtt_.set_custom_mode_command_topic(config[CONF_MODE_COMMAND_TOPIC]))
         if CONF_MODE_STATE_TOPIC in config:
             cg.add(mqtt_.set_custom_mode_state_topic(config[CONF_MODE_STATE_TOPIC]))
-
+        if CONF_PRESET_COMMAND_TOPIC in config:
+            cg.add(
+                mqtt_.set_custom_preset_command_topic(config[CONF_PRESET_COMMAND_TOPIC])
+            )
+        if CONF_PRESET_STATE_TOPIC in config:
+            cg.add(mqtt_.set_custom_preset_state_topic(config[CONF_PRESET_STATE_TOPIC]))
         if CONF_SWING_MODE_COMMAND_TOPIC in config:
             cg.add(
                 mqtt_.set_custom_swing_mode_command_topic(
diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp
index 512dbdd6dd..e1611d2fa9 100644
--- a/esphome/components/climate/climate.cpp
+++ b/esphome/components/climate/climate.cpp
@@ -174,6 +174,8 @@ ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
     this->set_fan_mode(CLIMATE_FAN_FOCUS);
   } else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) {
     this->set_fan_mode(CLIMATE_FAN_DIFFUSE);
+  } else if (str_equals_case_insensitive(fan_mode, "QUIET")) {
+    this->set_fan_mode(CLIMATE_FAN_QUIET);
   } else {
     if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
       this->custom_fan_mode_ = fan_mode;
diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp
index e46159a750..794f45ccd6 100644
--- a/esphome/components/climate/climate_mode.cpp
+++ b/esphome/components/climate/climate_mode.cpp
@@ -62,6 +62,8 @@ const LogString *climate_fan_mode_to_string(ClimateFanMode fan_mode) {
       return LOG_STR("FOCUS");
     case climate::CLIMATE_FAN_DIFFUSE:
       return LOG_STR("DIFFUSE");
+    case climate::CLIMATE_FAN_QUIET:
+      return LOG_STR("QUIET");
     default:
       return LOG_STR("UNKNOWN");
   }
diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h
index 139400a08a..c5245812c7 100644
--- a/esphome/components/climate/climate_mode.h
+++ b/esphome/components/climate/climate_mode.h
@@ -62,6 +62,8 @@ enum ClimateFanMode : uint8_t {
   CLIMATE_FAN_FOCUS = 7,
   /// The fan mode is set to Diffuse
   CLIMATE_FAN_DIFFUSE = 8,
+  /// The fan mode is set to Quiet
+  CLIMATE_FAN_QUIET = 9,
 };
 
 /// Enum for all modes a climate swing can be in
diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h
index 3ec51bc3c2..9da9bb7374 100644
--- a/esphome/components/climate/climate_traits.h
+++ b/esphome/components/climate/climate_traits.h
@@ -28,7 +28,7 @@ namespace climate {
  *  - supports action - if the climate device supports reporting the active
  *    current action of the device with the action property.
  *  - supports fan modes - optionally, if it has a fan which can be configured in different ways:
- *    - on, off, auto, high, medium, low, middle, focus, diffuse
+ *    - on, off, auto, high, medium, low, middle, focus, diffuse, quiet
  *  - supports swing modes - optionally, if it has a swing which can be configured in different ways:
  *    - off, both, vertical, horizontal
  *
diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py
index 0eb65579f9..e1bd6a7f08 100644
--- a/esphome/components/dashboard_import/__init__.py
+++ b/esphome/components/dashboard_import/__init__.py
@@ -1,14 +1,17 @@
+import base64
+import secrets
 from pathlib import Path
+from typing import Optional
+
 import requests
 
 import esphome.codegen as cg
 import esphome.config_validation as cv
+from esphome import git
 from esphome.components.packages import validate_source_shorthand
-from esphome.const import CONF_WIFI, CONF_REF
+from esphome.const import CONF_REF, CONF_WIFI
 from esphome.wizard import wizard_file
 from esphome.yaml_util import dump
-from esphome import git
-
 
 dashboard_import_ns = cg.esphome_ns.namespace("dashboard_import")
 
@@ -66,7 +69,13 @@ async def to_code(config):
 
 
 def import_config(
-    path: str, name: str, project_name: str, import_url: str, network: str = CONF_WIFI
+    path: str,
+    name: str,
+    friendly_name: Optional[str],
+    project_name: str,
+    import_url: str,
+    network: str = CONF_WIFI,
+    encryption: bool = False,
 ) -> None:
     p = Path(path)
 
@@ -74,14 +83,21 @@ def import_config(
         raise FileExistsError
 
     if project_name == "esphome.web":
+        kwargs = {
+            "name": name,
+            "friendly_name": friendly_name,
+            "platform": "ESP32" if "esp32" in import_url else "ESP8266",
+            "board": "esp32dev" if "esp32" in import_url else "esp01_1m",
+            "ssid": "!secret wifi_ssid",
+            "psk": "!secret wifi_password",
+        }
+        if encryption:
+            noise_psk = secrets.token_bytes(32)
+            key = base64.b64encode(noise_psk).decode()
+            kwargs["api_encryption_key"] = key
+
         p.write_text(
-            wizard_file(
-                name=name,
-                platform="ESP32" if "esp32" in import_url else "ESP8266",
-                board="esp32dev" if "esp32" in import_url else "esp01_1m",
-                ssid="!secret wifi_ssid",
-                psk="!secret wifi_password",
-            ),
+            wizard_file(**kwargs),
             encoding="utf8",
         )
     else:
@@ -98,14 +114,21 @@ def import_config(
             p.write_text(req.text, encoding="utf8")
 
         else:
+            substitutions = {"name": name}
+            esphome_core = {"name": "${name}", "name_add_mac_suffix": False}
+            if friendly_name:
+                substitutions["friendly_name"] = friendly_name
+                esphome_core["friendly_name"] = "${friendly_name}"
             config = {
-                "substitutions": {"name": name},
+                "substitutions": substitutions,
                 "packages": {project_name: import_url},
-                "esphome": {
-                    "name": "${name}",
-                    "name_add_mac_suffix": False,
-                },
+                "esphome": esphome_core,
             }
+            if encryption:
+                noise_psk = secrets.token_bytes(32)
+                key = base64.b64encode(noise_psk).decode()
+                config["api"] = {"encryption": {"key": key}}
+
             output = dump(config)
 
             if network == CONF_WIFI:
diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h
index 0cf48dd4ee..1ba80aabf5 100644
--- a/esphome/components/demo/demo_climate.h
+++ b/esphome/components/demo/demo_climate.h
@@ -111,6 +111,7 @@ class DemoClimate : public climate::Climate, public Component {
             climate::CLIMATE_FAN_MIDDLE,
             climate::CLIMATE_FAN_FOCUS,
             climate::CLIMATE_FAN_DIFFUSE,
+            climate::CLIMATE_FAN_QUIET,
         });
         traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"});
         traits.set_supported_swing_modes({
diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp
index 97c08dae24..9fe4137a14 100644
--- a/esphome/components/display/display_buffer.cpp
+++ b/esphome/components/display/display_buffer.cpp
@@ -15,6 +15,84 @@ static const char *const TAG = "display";
 const Color COLOR_OFF(0, 0, 0, 0);
 const Color COLOR_ON(255, 255, 255, 255);
 
+void Rect::expand(int16_t horizontal, int16_t vertical) {
+  if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) {
+    this->x = this->x - horizontal;
+    this->y = this->y - vertical;
+    this->w = this->w + (2 * horizontal);
+    this->h = this->h + (2 * vertical);
+  }
+}
+
+void Rect::extend(Rect rect) {
+  if (!this->is_set()) {
+    this->x = rect.x;
+    this->y = rect.y;
+    this->w = rect.w;
+    this->h = rect.h;
+  } else {
+    if (this->x > rect.x) {
+      this->x = rect.x;
+    }
+    if (this->y > rect.y) {
+      this->y = rect.y;
+    }
+    if (this->x2() < rect.x2()) {
+      this->w = rect.x2() - this->x;
+    }
+    if (this->y2() < rect.y2()) {
+      this->h = rect.y2() - this->y;
+    }
+  }
+}
+void Rect::shrink(Rect rect) {
+  if (!this->inside(rect)) {
+    (*this) = Rect();
+  } else {
+    if (this->x < rect.x) {
+      this->x = rect.x;
+    }
+    if (this->y < rect.y) {
+      this->y = rect.y;
+    }
+    if (this->x2() > rect.x2()) {
+      this->w = rect.x2() - this->x;
+    }
+    if (this->y2() > rect.y2()) {
+      this->h = rect.y2() - this->y;
+    }
+  }
+}
+
+bool Rect::inside(int16_t x, int16_t y, bool absolute) {  // NOLINT
+  if (!this->is_set()) {
+    return true;
+  }
+  if (absolute) {
+    return ((x >= 0) && (x <= this->w) && (y >= 0) && (y <= this->h));
+  } else {
+    return ((x >= this->x) && (x <= this->x2()) && (y >= this->y) && (y <= this->y2()));
+  }
+}
+
+bool Rect::inside(Rect rect, bool absolute) {
+  if (!this->is_set() || !rect.is_set()) {
+    return true;
+  }
+  if (absolute) {
+    return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0));
+  } else {
+    return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y));
+  }
+}
+
+void Rect::info(const std::string &prefix) {
+  if (this->is_set()) {
+    ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d]", prefix.c_str(), this->x, this->y, this->w, this->h);
+  } else
+    ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str());
+}
+
 void DisplayBuffer::init_internal_(uint32_t buffer_length) {
   ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
   this->buffer_ = allocator.allocate(buffer_length);
@@ -24,6 +102,7 @@ void DisplayBuffer::init_internal_(uint32_t buffer_length) {
   }
   this->clear();
 }
+
 void DisplayBuffer::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); }
 void DisplayBuffer::clear() { this->fill(COLOR_OFF); }
 int DisplayBuffer::get_width() {
@@ -50,6 +129,9 @@ int DisplayBuffer::get_height() {
 }
 void DisplayBuffer::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; }
 void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
+  if (!this->get_clipping().inside(x, y))
+    return;  // NOLINT
+
   switch (this->rotation_) {
     case DISPLAY_ROTATION_0_DEGREES:
       break;
@@ -368,6 +450,10 @@ void DisplayBuffer::do_update_() {
   } else if (this->writer_.has_value()) {
     (*this->writer_)(*this);
   }
+  // remove all not ended clipping regions
+  while (is_clipping()) {
+    end_clipping();
+  }
 }
 void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) {
   if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to))
@@ -392,6 +478,41 @@ void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, time:
 }
 #endif
 
+void DisplayBuffer::start_clipping(Rect rect) {
+  if (!this->clipping_rectangle_.empty()) {
+    Rect r = this->clipping_rectangle_.back();
+    rect.shrink(r);
+  }
+  this->clipping_rectangle_.push_back(rect);
+}
+void DisplayBuffer::end_clipping() {
+  if (this->clipping_rectangle_.empty()) {
+    ESP_LOGE(TAG, "clear: Clipping is not set.");
+  } else {
+    this->clipping_rectangle_.pop_back();
+  }
+}
+void DisplayBuffer::extend_clipping(Rect add_rect) {
+  if (this->clipping_rectangle_.empty()) {
+    ESP_LOGE(TAG, "add: Clipping is not set.");
+  } else {
+    this->clipping_rectangle_.back().extend(add_rect);
+  }
+}
+void DisplayBuffer::shrink_clipping(Rect add_rect) {
+  if (this->clipping_rectangle_.empty()) {
+    ESP_LOGE(TAG, "add: Clipping is not set.");
+  } else {
+    this->clipping_rectangle_.back().shrink(add_rect);
+  }
+}
+Rect DisplayBuffer::get_clipping() {
+  if (this->clipping_rectangle_.empty()) {
+    return Rect();
+  } else {
+    return this->clipping_rectangle_.back();
+  }
+}
 bool Glyph::get_pixel(int x, int y) const {
   const int x_data = x - this->glyph_data_->offset_x;
   const int y_data = y - this->glyph_data_->offset_y;
@@ -452,7 +573,7 @@ int Font::match_next_glyph(const char *str, int *match_length) {
 }
 void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) {
   *baseline = this->baseline_;
-  *height = this->bottom_;
+  *height = this->height_;
   int i = 0;
   int min_x = 0;
   bool has_char = false;
@@ -483,7 +604,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in
   *width = x - min_x;
 }
 const std::vector<Glyph> &Font::get_glyphs() const { return this->glyphs_; }
-Font::Font(const GlyphData *data, int data_nr, int baseline, int bottom) : baseline_(baseline), bottom_(bottom) {
+Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) {
   for (int i = 0; i < data_nr; ++i)
     glyphs_.emplace_back(data + i);
 }
@@ -527,6 +648,7 @@ int Image::get_height() const { return this->height_; }
 ImageType Image::get_type() const { return this->type_; }
 Image::Image(const uint8_t *data_start, int width, int height, ImageType type)
     : width_(width), height_(height), type_(type), data_start_(data_start) {}
+int Image::get_current_frame() const { return 0; }
 
 bool Animation::get_pixel(int x, int y) const {
   if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h
index 41052b3ffd..3763da8041 100644
--- a/esphome/components/display/display_buffer.h
+++ b/esphome/components/display/display_buffer.h
@@ -4,7 +4,6 @@
 #include "esphome/core/defines.h"
 #include "esphome/core/automation.h"
 #include "display_color_utils.h"
-
 #include <cstdarg>
 #include <vector>
 
@@ -100,6 +99,32 @@ enum DisplayRotation {
   DISPLAY_ROTATION_270_DEGREES = 270,
 };
 
+static const int16_t VALUE_NO_SET = 32766;
+
+class Rect {
+ public:
+  int16_t x;  ///< X coordinate of corner
+  int16_t y;  ///< Y coordinate of corner
+  int16_t w;  ///< Width of region
+  int16_t h;  ///< Height of region
+
+  Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {}  // NOLINT
+  inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ALWAYS_INLINE : x(x), y(y), w(w), h(h) {}
+  inline int16_t x2() { return this->x + this->w; };  ///< X coordinate of corner
+  inline int16_t y2() { return this->y + this->h; };  ///< Y coordinate of corner
+
+  inline bool is_set() ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); }
+
+  void expand(int16_t horizontal, int16_t vertical);
+
+  void extend(Rect rect);
+  void shrink(Rect rect);
+
+  bool inside(Rect rect, bool absolute = false);
+  bool inside(int16_t x, int16_t y, bool absolute = false);
+  void info(const std::string &prefix = "rect info:");
+};
+
 class Font;
 class Image;
 class DisplayBuffer;
@@ -126,6 +151,7 @@ class DisplayBuffer {
   int get_width();
   /// Get the height of the image in pixels with rotation applied.
   int get_height();
+
   /// Set a single pixel at the specified coordinates to the given color.
   void draw_pixel_at(int x, int y, Color color = COLOR_ON);
 
@@ -374,6 +400,49 @@ class DisplayBuffer {
    */
   virtual DisplayType get_display_type() = 0;
 
+  /** Set the clipping rectangle for further drawing
+   *
+   * @param[in]  rect:       Pointer to Rect for clipping (or NULL for entire screen)
+   *
+   * return true if success, false if error
+   */
+  void start_clipping(Rect rect);
+  void start_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) {
+    start_clipping(Rect(left, top, right - left, bottom - top));
+  };
+
+  /** Add a rectangular region to the invalidation region
+   * - This is usually called when an element has been modified
+   *
+   * @param[in]  rect: Rectangle to add to the invalidation region
+   */
+  void extend_clipping(Rect rect);
+  void extend_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) {
+    this->extend_clipping(Rect(left, top, right - left, bottom - top));
+  };
+
+  /** substract a rectangular region to the invalidation region
+   *  - This is usually called when an element has been modified
+   *
+   * @param[in]  rect: Rectangle to add to the invalidation region
+   */
+  void shrink_clipping(Rect rect);
+  void shrink_clipping(uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
+    this->shrink_clipping(Rect(left, top, right - left, bottom - top));
+  };
+
+  /** Reset the invalidation region
+   */
+  void end_clipping();
+
+  /** Get the current the clipping rectangle
+   *
+   * return rect for active clipping region
+   */
+  Rect get_clipping();
+
+  bool is_clipping() const { return !this->clipping_rectangle_.empty(); }
+
  protected:
   void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg);
 
@@ -390,6 +459,7 @@ class DisplayBuffer {
   DisplayPage *previous_page_{nullptr};
   std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;
   bool auto_clear_enabled_{true};
+  std::vector<Rect> clipping_rectangle_;
 };
 
 class DisplayPage {
@@ -448,18 +518,20 @@ class Font {
    * @param baseline The y-offset from the top of the text to the baseline.
    * @param bottom The y-offset from the top of the text to the bottom (i.e. height).
    */
-  Font(const GlyphData *data, int data_nr, int baseline, int bottom);
+  Font(const GlyphData *data, int data_nr, int baseline, int height);
 
   int match_next_glyph(const char *str, int *match_length);
 
   void measure(const char *str, int *width, int *x_offset, int *baseline, int *height);
+  inline int get_baseline() { return this->baseline_; }
+  inline int get_height() { return this->height_; }
 
   const std::vector<Glyph> &get_glyphs() const;
 
  protected:
   std::vector<Glyph> glyphs_;
   int baseline_;
-  int bottom_;
+  int height_;
 };
 
 class Image {
@@ -473,6 +545,8 @@ class Image {
   int get_height() const;
   ImageType get_type() const;
 
+  virtual int get_current_frame() const;
+
  protected:
   int width_;
   int height_;
@@ -489,7 +563,7 @@ class Animation : public Image {
   Color get_grayscale_pixel(int x, int y) const override;
 
   int get_animation_frame_count() const;
-  int get_current_frame() const;
+  int get_current_frame() const override;
   void next_frame();
   void prev_frame();
 
diff --git a/esphome/components/display_menu_base/__init__.py b/esphome/components/display_menu_base/__init__.py
index eb66737fdb..d7326cdc65 100644
--- a/esphome/components/display_menu_base/__init__.py
+++ b/esphome/components/display_menu_base/__init__.py
@@ -8,6 +8,7 @@ from esphome.const import (
     CONF_TRIGGER_ID,
     CONF_ON_VALUE,
     CONF_COMMAND,
+    CONF_CUSTOM,
     CONF_NUMBER,
     CONF_FORMAT,
     CONF_MODE,
@@ -32,7 +33,6 @@ CONF_BACK = "back"
 CONF_TEXT = "text"
 CONF_SELECT = "select"
 CONF_SWITCH = "switch"
-CONF_CUSTOM = "custom"
 CONF_ITEMS = "items"
 CONF_ON_TEXT = "on_text"
 CONF_OFF_TEXT = "off_text"
diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py
index 284733cca6..f4f8305ba6 100644
--- a/esphome/components/dsmr/__init__.py
+++ b/esphome/components/dsmr/__init__.py
@@ -10,6 +10,8 @@ from esphome.const import (
 
 CODEOWNERS = ["@glmnet", "@zuidwijk"]
 
+MULTI_CONF = True
+
 DEPENDENCIES = ["uart"]
 AUTO_LOAD = ["sensor", "text_sensor"]
 
diff --git a/esphome/components/ee895/__init__.py b/esphome/components/ee895/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp
new file mode 100644
index 0000000000..a7186ffbbc
--- /dev/null
+++ b/esphome/components/ee895/ee895.cpp
@@ -0,0 +1,115 @@
+#include "ee895.h"
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+
+namespace esphome {
+namespace ee895 {
+
+static const char *const TAG = "ee895";
+
+static const uint16_t CRC16_ONEWIRE_START = 0xFFFF;
+static const uint8_t FUNCTION_CODE_READ = 0x03;
+static const uint16_t SERIAL_NUMBER = 0x0000;
+static const uint16_t TEMPERATURE_ADDRESS = 0x03EA;
+static const uint16_t CO2_ADDRESS = 0x0424;
+static const uint16_t PRESSURE_ADDRESS = 0x04B0;
+
+void EE895Component::setup() {
+  uint16_t crc16_check = 0;
+  ESP_LOGCONFIG(TAG, "Setting up EE895...");
+  write_command_(SERIAL_NUMBER, 8);
+  uint8_t serial_number[20];
+  this->read(serial_number, 20);
+
+  crc16_check = (serial_number[19] << 8) + serial_number[18];
+  if (crc16_check != calc_crc16_(serial_number, 19)) {
+    this->error_code_ = CRC_CHECK_FAILED;
+    this->mark_failed();
+    return;
+  }
+  ESP_LOGV(TAG, "    Serial Number: 0x%s", format_hex(serial_number + 2, 16).c_str());
+}
+
+void EE895Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "EE895:");
+  LOG_I2C_DEVICE(this);
+  switch (this->error_code_) {
+    case COMMUNICATION_FAILED:
+      ESP_LOGE(TAG, "Communication with EE895 failed!");
+      break;
+    case CRC_CHECK_FAILED:
+      ESP_LOGE(TAG, "The crc check failed");
+      break;
+    case NONE:
+    default:
+      break;
+  }
+  LOG_UPDATE_INTERVAL(this);
+  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
+  LOG_SENSOR("  ", "CO2", this->co2_sensor_);
+  LOG_SENSOR("  ", "Pressure", this->pressure_sensor_);
+}
+
+float EE895Component::get_setup_priority() const { return setup_priority::DATA; }
+
+void EE895Component::update() {
+  write_command_(TEMPERATURE_ADDRESS, 2);
+  this->set_timeout(50, [this]() {
+    float temperature = read_float_();
+
+    write_command_(CO2_ADDRESS, 2);
+    float co2 = read_float_();
+
+    write_command_(PRESSURE_ADDRESS, 2);
+    float pressure = read_float_();
+    ESP_LOGD(TAG, "Got temperature=%.1f°C co2=%.0fppm pressure=%.1f%mbar", temperature, co2, pressure);
+    if (this->temperature_sensor_ != nullptr)
+      this->temperature_sensor_->publish_state(temperature);
+    if (this->co2_sensor_ != nullptr)
+      this->co2_sensor_->publish_state(co2);
+    if (this->pressure_sensor_ != nullptr)
+      this->pressure_sensor_->publish_state(pressure);
+    this->status_clear_warning();
+  });
+}
+
+void EE895Component::write_command_(uint16_t addr, uint16_t reg_cnt) {
+  uint8_t address[7];
+  uint16_t crc16 = 0;
+  address[0] = FUNCTION_CODE_READ;
+  address[1] = (addr >> 8) & 0xFF;
+  address[2] = addr & 0xFF;
+  address[3] = (reg_cnt >> 8) & 0xFF;
+  address[4] = reg_cnt & 0xFF;
+  crc16 = calc_crc16_(address, 6);
+  address[5] = crc16 & 0xFF;
+  address[6] = (crc16 >> 8) & 0xFF;
+  this->write(address, 7, true);
+}
+
+float EE895Component::read_float_() {
+  uint16_t crc16_check = 0;
+  uint8_t i2c_response[8];
+  this->read(i2c_response, 8);
+  crc16_check = (i2c_response[7] << 8) + i2c_response[6];
+  if (crc16_check != calc_crc16_(i2c_response, 7)) {
+    this->error_code_ = CRC_CHECK_FAILED;
+    this->status_set_warning();
+    return 0;
+  }
+  uint32_t x = encode_uint32(i2c_response[4], i2c_response[5], i2c_response[2], i2c_response[3]);
+  float value;
+  memcpy(&value, &x, sizeof(value));  // convert uin32_t IEEE-754 format to float
+  return value;
+}
+
+uint16_t EE895Component::calc_crc16_(const uint8_t buf[], uint8_t len) {
+  uint8_t crc_check_buf[22];
+  for (int i = 0; i < len; i++) {
+    crc_check_buf[i + 1] = buf[i];
+  }
+  crc_check_buf[0] = this->address_;
+  return crc16(crc_check_buf, len);
+}
+}  // namespace ee895
+}  // namespace esphome
diff --git a/esphome/components/ee895/ee895.h b/esphome/components/ee895/ee895.h
new file mode 100644
index 0000000000..83bd7c6e82
--- /dev/null
+++ b/esphome/components/ee895/ee895.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/i2c/i2c.h"
+
+namespace esphome {
+namespace ee895 {
+
+/// This class implements support for the ee895 of temperature i2c sensors.
+class EE895Component : public PollingComponent, public i2c::I2CDevice {
+ public:
+  void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; }
+  void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
+  void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
+
+  float get_setup_priority() const override;
+  void setup() override;
+  void dump_config() override;
+  void update() override;
+
+ protected:
+  void write_command_(uint16_t addr, uint16_t reg_cnt);
+  float read_float_();
+  uint16_t calc_crc16_(const uint8_t buf[], uint8_t len);
+  sensor::Sensor *co2_sensor_;
+  sensor::Sensor *temperature_sensor_;
+  sensor::Sensor *pressure_sensor_;
+
+  enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE};
+};
+
+}  // namespace ee895
+}  // namespace esphome
diff --git a/esphome/components/ee895/sensor.py b/esphome/components/ee895/sensor.py
new file mode 100644
index 0000000000..d06f9ca02f
--- /dev/null
+++ b/esphome/components/ee895/sensor.py
@@ -0,0 +1,69 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import i2c, sensor
+from esphome.const import (
+    CONF_ID,
+    CONF_PRESSURE,
+    CONF_TEMPERATURE,
+    CONF_CO2,
+    DEVICE_CLASS_TEMPERATURE,
+    DEVICE_CLASS_PRESSURE,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_HECTOPASCAL,
+    UNIT_CELSIUS,
+    ICON_MOLECULE_CO2,
+    UNIT_PARTS_PER_MILLION,
+)
+
+CODEOWNERS = ["@Stock-M"]
+
+DEPENDENCIES = ["i2c"]
+
+ee895_ns = cg.esphome_ns.namespace("ee895")
+EE895Component = ee895_ns.class_("EE895Component", cg.PollingComponent, i2c.I2CDevice)
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(EE895Component),
+            cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_CELSIUS,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_TEMPERATURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Required(CONF_CO2): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PARTS_PER_MILLION,
+                icon=ICON_MOLECULE_CO2,
+                accuracy_decimals=0,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Required(CONF_PRESSURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_HECTOPASCAL,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_PRESSURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+        }
+    )
+    .extend(cv.polling_component_schema("60s"))
+    .extend(i2c.i2c_device_schema(0x5F))
+)
+
+
+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)
+
+    if CONF_TEMPERATURE in config:
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
+        cg.add(var.set_temperature_sensor(sens))
+
+    if CONF_CO2 in config:
+        sens = await sensor.new_sensor(config[CONF_CO2])
+        cg.add(var.set_co2_sensor(sens))
+
+    if CONF_PRESSURE in config:
+        sens = await sensor.new_sensor(config[CONF_PRESSURE])
+        cg.add(var.set_pressure_sensor(sens))
diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py
index 3989b62842..f30fa9a7b2 100644
--- a/esphome/components/esp32/__init__.py
+++ b/esphome/components/esp32/__init__.py
@@ -356,9 +356,14 @@ async def to_code(config):
 
         if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_MAC_CRC]:
             cg.add_define("USE_ESP32_IGNORE_EFUSE_MAC_CRC")
-            add_idf_sdkconfig_option(
-                "CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE", False
-            )
+            if (framework_ver.major, framework_ver.minor) >= (4, 4):
+                add_idf_sdkconfig_option(
+                    "CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False
+                )
+            else:
+                add_idf_sdkconfig_option(
+                    "CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE", False
+                )
 
         cg.add_define(
             "USE_ESP_IDF_VERSION_CODE",
diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py
index b97a5e0457..7848d1d552 100644
--- a/esphome/components/esp32/gpio.py
+++ b/esphome/components/esp32/gpio.py
@@ -123,11 +123,8 @@ def validate_gpio_pin(value):
 
 def validate_supports(value):
     mode = value[CONF_MODE]
-    is_input = mode[CONF_INPUT]
     is_output = mode[CONF_OUTPUT]
     is_open_drain = mode[CONF_OPEN_DRAIN]
-    is_pullup = mode[CONF_PULLUP]
-    is_pulldown = mode[CONF_PULLDOWN]
     variant = CORE.data[KEY_ESP32][KEY_VARIANT]
     if variant not in _esp32_validations:
         raise cv.Invalid(f"Unsupported ESP32 variant {variant}")
@@ -138,26 +135,6 @@ def validate_supports(value):
         )
 
     value = _esp32_validations[variant].usage_validation(value)
-    if CORE.using_arduino:
-        # (input, output, open_drain, pullup, pulldown)
-        supported_modes = {
-            # INPUT
-            (True, False, False, False, False),
-            # OUTPUT
-            (False, True, False, False, False),
-            # INPUT_PULLUP
-            (True, False, False, True, False),
-            # INPUT_PULLDOWN
-            (True, False, False, False, True),
-            # OUTPUT_OPEN_DRAIN
-            (False, True, True, False, False),
-        }
-        key = (is_input, is_output, is_open_drain, is_pullup, is_pulldown)
-        if key not in supported_modes:
-            raise cv.Invalid(
-                "This pin mode is not supported on ESP32 for arduino frameworks",
-                [CONF_MODE],
-            )
     return value
 
 
diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py
index c6bb296cdc..f59d944dcf 100644
--- a/esphome/components/esp32_ble/__init__.py
+++ b/esphome/components/esp32_ble/__init__.py
@@ -6,13 +6,21 @@ from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
 
 DEPENDENCIES = ["esp32"]
 CODEOWNERS = ["@jesserockz"]
-CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"]
+CONFLICTS_WITH = ["esp32_ble_beacon"]
+
+CONF_BLE_ID = "ble_id"
+
+NO_BLUTOOTH_VARIANTS = [const.VARIANT_ESP32S2]
 
 NO_BLUTOOTH_VARIANTS = [const.VARIANT_ESP32S2]
 
 esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble")
 ESP32BLE = esp32_ble_ns.class_("ESP32BLE", cg.Component)
 
+GAPEventHandler = esp32_ble_ns.class_("GAPEventHandler")
+GATTcEventHandler = esp32_ble_ns.class_("GATTcEventHandler")
+GATTsEventHandler = esp32_ble_ns.class_("GATTsEventHandler")
+
 
 CONFIG_SCHEMA = cv.Schema(
     {
diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp
index ecd591d169..160084b913 100644
--- a/esphome/components/esp32_ble/ble.cpp
+++ b/esphome/components/esp32_ble/ble.cpp
@@ -4,13 +4,13 @@
 #include "esphome/core/application.h"
 #include "esphome/core/log.h"
 
-#include <nvs_flash.h>
-#include <freertos/FreeRTOSConfig.h>
-#include <esp_bt_main.h>
 #include <esp_bt.h>
-#include <freertos/FreeRTOS.h>
-#include <freertos/task.h>
+#include <esp_bt_main.h>
 #include <esp_gap_ble_api.h>
+#include <freertos/FreeRTOS.h>
+#include <freertos/FreeRTOSConfig.h>
+#include <freertos/task.h>
+#include <nvs_flash.h>
 
 #ifdef USE_ARDUINO
 #include <esp32-hal-bt.h>
@@ -31,24 +31,17 @@ void ESP32BLE::setup() {
     return;
   }
 
+#ifdef USE_ESP32_BLE_SERVER
   this->advertising_ = new BLEAdvertising();  // NOLINT(cppcoreguidelines-owning-memory)
 
   this->advertising_->set_scan_response(true);
   this->advertising_->set_min_preferred_interval(0x06);
   this->advertising_->start();
+#endif  // USE_ESP32_BLE_SERVER
 
   ESP_LOGD(TAG, "BLE setup complete");
 }
 
-void ESP32BLE::mark_failed() {
-  Component::mark_failed();
-#ifdef USE_ESP32_BLE_SERVER
-  if (this->server_ != nullptr) {
-    this->server_->mark_failed();
-  }
-#endif
-}
-
 bool ESP32BLE::ble_setup_() {
   esp_err_t err = nvs_flash_init();
   if (err != ESP_OK) {
@@ -100,13 +93,16 @@ bool ESP32BLE::ble_setup_() {
     ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err);
     return false;
   }
-  err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler);
-  if (err != ESP_OK) {
-    ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err);
-    return false;
+
+  if (!this->gap_event_handlers_.empty()) {
+    err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler);
+    if (err != ESP_OK) {
+      ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err);
+      return false;
+    }
   }
 
-  if (this->has_server()) {
+  if (!this->gatts_event_handlers_.empty()) {
     err = esp_ble_gatts_register_callback(ESP32BLE::gatts_event_handler);
     if (err != ESP_OK) {
       ESP_LOGE(TAG, "esp_ble_gatts_register_callback failed: %d", err);
@@ -114,7 +110,7 @@ bool ESP32BLE::ble_setup_() {
     }
   }
 
-  if (this->has_client()) {
+  if (!this->gattc_event_handlers_.empty()) {
     err = esp_ble_gattc_register_callback(ESP32BLE::gattc_event_handler);
     if (err != ESP_OK) {
       ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err);
@@ -158,6 +154,10 @@ void ESP32BLE::loop() {
         this->real_gatts_event_handler_(ble_event->event_.gatts.gatts_event, ble_event->event_.gatts.gatts_if,
                                         &ble_event->event_.gatts.gatts_param);
         break;
+      case BLEEvent::GATTC:
+        this->real_gattc_event_handler_(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if,
+                                        &ble_event->event_.gattc.gattc_param);
+        break;
       case BLEEvent::GAP:
         this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param);
         break;
@@ -176,9 +176,8 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
 
 void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
   ESP_LOGV(TAG, "(BLE) gap_event_handler - %d", event);
-  switch (event) {
-    default:
-      break;
+  for (auto *gap_handler : this->gap_event_handlers_) {
+    gap_handler->gap_event_handler(event, param);
   }
 }
 
@@ -191,14 +190,23 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat
 void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
                                          esp_ble_gatts_cb_param_t *param) {
   ESP_LOGV(TAG, "(BLE) gatts_event [esp_gatt_if: %d] - %d", gatts_if, event);
-#ifdef USE_ESP32_BLE_SERVER
-  this->server_->gatts_event_handler(event, gatts_if, param);
-#endif
+  for (auto *gatts_handler : this->gatts_event_handlers_) {
+    gatts_handler->gatts_event_handler(event, gatts_if, param);
+  }
 }
 
+void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
+                                   esp_ble_gattc_cb_param_t *param) {
+  BLEEvent *new_event = new BLEEvent(event, gattc_if, param);  // NOLINT(cppcoreguidelines-owning-memory)
+  global_ble->ble_events_.push(new_event);
+}  // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
+
 void ESP32BLE::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
                                          esp_ble_gattc_cb_param_t *param) {
-  // this->client_->gattc_event_handler(event, gattc_if, param);
+  ESP_LOGV(TAG, "(BLE) gattc_event [esp_gatt_if: %d] - %d", gattc_if, event);
+  for (auto *gattc_handler : this->gattc_event_handlers_) {
+    gattc_handler->gattc_event_handler(event, gattc_if, param);
+  }
 }
 
 float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; }
diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h
index 0477dee070..5970b43688 100644
--- a/esphome/components/esp32_ble/ble.h
+++ b/esphome/components/esp32_ble/ble.h
@@ -5,17 +5,16 @@
 #include "esphome/core/component.h"
 #include "esphome/core/defines.h"
 #include "esphome/core/helpers.h"
-#include "queue.h"
 
-#ifdef USE_ESP32_BLE_SERVER
-#include "esphome/components/esp32_ble_server/ble_server.h"
-#endif
+#include "queue.h"
+#include "ble_event.h"
 
 #ifdef USE_ESP32
 
 #include <esp_gap_ble_api.h>
 #include <esp_gatts_api.h>
 #include <esp_gattc_api.h>
+
 namespace esphome {
 namespace esp32_ble {
 
@@ -26,28 +25,36 @@ typedef struct {
   uint16_t mtu;
 } conn_status_t;
 
+class GAPEventHandler {
+ public:
+  virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0;
+};
+
+class GATTcEventHandler {
+ public:
+  virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
+                                   esp_ble_gattc_cb_param_t *param) = 0;
+};
+
+class GATTsEventHandler {
+ public:
+  virtual void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
+                                   esp_ble_gatts_cb_param_t *param) = 0;
+};
+
 class ESP32BLE : public Component {
  public:
   void setup() override;
   void loop() override;
   void dump_config() override;
   float get_setup_priority() const override;
-  void mark_failed() override;
-
-  bool has_server() {
-#ifdef USE_ESP32_BLE_SERVER
-    return this->server_ != nullptr;
-#else
-    return false;
-#endif
-  }
-  bool has_client() { return false; }
 
   BLEAdvertising *get_advertising() { return this->advertising_; }
 
-#ifdef USE_ESP32_BLE_SERVER
-  void set_server(esp32_ble_server::BLEServer *server) { this->server_ = server; }
-#endif
+  void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); }
+  void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); }
+  void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); }
+
  protected:
   static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
   static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param);
@@ -59,9 +66,10 @@ class ESP32BLE : public Component {
 
   bool ble_setup_();
 
-#ifdef USE_ESP32_BLE_SERVER
-  esp32_ble_server::BLEServer *server_{nullptr};
-#endif
+  std::vector<GAPEventHandler *> gap_event_handlers_;
+  std::vector<GATTcEventHandler *> gattc_event_handlers_;
+  std::vector<GATTsEventHandler *> gatts_event_handlers_;
+
   Queue<BLEEvent> ble_events_;
   BLEAdvertising *advertising_;
 };
diff --git a/esphome/components/esp32_ble_tracker/queue.h b/esphome/components/esp32_ble/ble_event.h
similarity index 50%
rename from esphome/components/esp32_ble_tracker/queue.h
rename to esphome/components/esp32_ble/ble_event.h
index f1dcc337e8..1cf63b2fab 100644
--- a/esphome/components/esp32_ble_tracker/queue.h
+++ b/esphome/components/esp32_ble/ble_event.h
@@ -1,69 +1,23 @@
 #pragma once
 
 #ifdef USE_ESP32
-#include "esphome/core/component.h"
-#include "esphome/core/helpers.h"
 
-#include <cstring>
-#include <mutex>
-#include <queue>
 #include <vector>
 
 #include <esp_gap_ble_api.h>
 #include <esp_gattc_api.h>
-#include <freertos/FreeRTOS.h>
-#include <freertos/semphr.h>
-
-/*
- * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather
- * than trying to deal with various locking strategies, all incoming GAP and GATT
- * events will simply be placed on a semaphore guarded queue. The next time the
- * component runs loop(), these events are popped off the queue and handed at
- * this safer time.
- */
+#include <esp_gatts_api.h>
 
 namespace esphome {
-namespace esp32_ble_tracker {
-
-template<class T> class Queue {
- public:
-  Queue() { m_ = xSemaphoreCreateMutex(); }
-
-  void push(T *element) {
-    if (element == nullptr)
-      return;
-    if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) {
-      q_.push(element);
-      xSemaphoreGive(m_);
-    }
-  }
-
-  T *pop() {
-    T *element = nullptr;
-
-    if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) {
-      if (!q_.empty()) {
-        element = q_.front();
-        q_.pop();
-      }
-      xSemaphoreGive(m_);
-    }
-    return element;
-  }
-
- protected:
-  std::queue<T *> q_;
-  SemaphoreHandle_t m_;
-};
-
-// Received GAP and GATTC events are only queued, and get processed in the main loop().
+namespace esp32_ble {
+// Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop().
 // This class stores each event in a single type.
 class BLEEvent {
  public:
   BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
     this->event_.gap.gap_event = e;
     memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t));
-    this->type_ = 0;
+    this->type_ = GAP;
   };
 
   BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
@@ -84,26 +38,57 @@ class BLEEvent {
       default:
         break;
     }
-    this->type_ = 1;
+    this->type_ = GATTC;
+  };
+
+  BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
+    this->event_.gatts.gatts_event = e;
+    this->event_.gatts.gatts_if = i;
+    memcpy(&this->event_.gatts.gatts_param, p, sizeof(esp_ble_gatts_cb_param_t));
+    // Need to also make a copy of relevant event data.
+    switch (e) {
+      case ESP_GATTS_WRITE_EVT:
+        this->data.assign(p->write.value, p->write.value + p->write.len);
+        this->event_.gatts.gatts_param.write.value = this->data.data();
+        break;
+      default:
+        break;
+    }
+    this->type_ = GATTS;
   };
 
   union {
-    struct gap_event {  // NOLINT(readability-identifier-naming)
+    // NOLINTNEXTLINE(readability-identifier-naming)
+    struct gap_event {
       esp_gap_ble_cb_event_t gap_event;
       esp_ble_gap_cb_param_t gap_param;
     } gap;
 
-    struct gattc_event {  // NOLINT(readability-identifier-naming)
+    // NOLINTNEXTLINE(readability-identifier-naming)
+    struct gattc_event {
       esp_gattc_cb_event_t gattc_event;
       esp_gatt_if_t gattc_if;
       esp_ble_gattc_cb_param_t gattc_param;
     } gattc;
+
+    // NOLINTNEXTLINE(readability-identifier-naming)
+    struct gatts_event {
+      esp_gatts_cb_event_t gatts_event;
+      esp_gatt_if_t gatts_if;
+      esp_ble_gatts_cb_param_t gatts_param;
+    } gatts;
   } event_;
+
   std::vector<uint8_t> data{};
-  uint8_t type_;  // 0=gap 1=gattc
+  // NOLINTNEXTLINE(readability-identifier-naming)
+  enum ble_event_t : uint8_t {
+    GAP,
+    GATTC,
+    GATTS,
+  } type_;
 };
 
-}  // namespace esp32_ble_tracker
+}  // namespace esp32_ble
 }  // namespace esphome
 
 #endif
diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp
index 8556aa87df..a50d3dbd42 100644
--- a/esphome/components/esp32_ble/ble_uuid.cpp
+++ b/esphome/components/esp32_ble/ble_uuid.cpp
@@ -27,8 +27,7 @@ ESPBTUUID ESPBTUUID::from_uint32(uint32_t uuid) {
 ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) {
   ESPBTUUID ret;
   ret.uuid_.len = ESP_UUID_LEN_128;
-  for (size_t i = 0; i < ESP_UUID_LEN_128; i++)
-    ret.uuid_.uuid.uuid128[i] = data[i];
+  memcpy(ret.uuid_.uuid.uuid128, data, ESP_UUID_LEN_128);
   return ret;
 }
 ESPBTUUID ESPBTUUID::from_raw(const std::string &data) {
@@ -91,10 +90,13 @@ ESPBTUUID ESPBTUUID::from_raw(const std::string &data) {
 ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) {
   ESPBTUUID ret;
   ret.uuid_.len = uuid.len;
-  ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16;
-  ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32;
-  for (size_t i = 0; i < ESP_UUID_LEN_128; i++)
-    ret.uuid_.uuid.uuid128[i] = uuid.uuid.uuid128[i];
+  if (uuid.len == ESP_UUID_LEN_16) {
+    ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16;
+  } else if (uuid.len == ESP_UUID_LEN_32) {
+    ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32;
+  } else if (uuid.len == ESP_UUID_LEN_128) {
+    memcpy(ret.uuid_.uuid.uuid128, uuid.uuid.uuid128, ESP_UUID_LEN_128);
+  }
   return ret;
 }
 ESPBTUUID ESPBTUUID::as_128bit() const {
@@ -158,30 +160,26 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const {
   }
   return false;
 }
-esp_bt_uuid_t ESPBTUUID::get_uuid() { return this->uuid_; }
-std::string ESPBTUUID::to_string() {
-  char sbuf[64];
+esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; }
+std::string ESPBTUUID::to_string() const {
   switch (this->uuid_.len) {
     case ESP_UUID_LEN_16:
-      sprintf(sbuf, "0x%02X%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff);
-      break;
+      return str_snprintf("0x%02X%02X", 6, this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff);
     case ESP_UUID_LEN_32:
-      sprintf(sbuf, "0x%02X%02X%02X%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff),
-              (this->uuid_.uuid.uuid32 >> 8 & 0xff), this->uuid_.uuid.uuid32 & 0xff);
-      break;
+      return str_snprintf("0x%02X%02X%02X%02X", 10, this->uuid_.uuid.uuid32 >> 24,
+                          (this->uuid_.uuid.uuid32 >> 16 & 0xff), (this->uuid_.uuid.uuid32 >> 8 & 0xff),
+                          this->uuid_.uuid.uuid32 & 0xff);
     default:
     case ESP_UUID_LEN_128:
-      char *bpos = sbuf;
+      std::string buf;
       for (int8_t i = 15; i >= 0; i--) {
-        sprintf(bpos, "%02X", this->uuid_.uuid.uuid128[i]);
-        bpos += 2;
+        buf += str_snprintf("%02X", 2, this->uuid_.uuid.uuid128[i]);
         if (i == 6 || i == 8 || i == 10 || i == 12)
-          sprintf(bpos++, "-");
+          buf += "-";
       }
-      sbuf[47] = '\0';
-      break;
+      return buf;
   }
-  return sbuf;
+  return "";
 }
 
 }  // namespace esp32_ble
diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h
index f953f9fede..790a57c59d 100644
--- a/esphome/components/esp32_ble/ble_uuid.h
+++ b/esphome/components/esp32_ble/ble_uuid.h
@@ -32,9 +32,9 @@ class ESPBTUUID {
   bool operator==(const ESPBTUUID &uuid) const;
   bool operator!=(const ESPBTUUID &uuid) const { return !(*this == uuid); }
 
-  esp_bt_uuid_t get_uuid();
+  esp_bt_uuid_t get_uuid() const;
 
-  std::string to_string();
+  std::string to_string() const;
 
  protected:
   esp_bt_uuid_t uuid_;
diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h
index 8d05eca058..5b31b97ae2 100644
--- a/esphome/components/esp32_ble/queue.h
+++ b/esphome/components/esp32_ble/queue.h
@@ -2,16 +2,9 @@
 
 #ifdef USE_ESP32
 
-#include "esphome/core/component.h"
-#include "esphome/core/helpers.h"
-
-#include <queue>
 #include <mutex>
-#include <cstring>
+#include <queue>
 
-#include <esp_gap_ble_api.h>
-#include <esp_gatts_api.h>
-#include <esp_gattc_api.h>
 #include <freertos/FreeRTOS.h>
 #include <freertos/semphr.h>
 
@@ -57,84 +50,6 @@ template<class T> class Queue {
   SemaphoreHandle_t m_;
 };
 
-// Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop().
-// This class stores each event in a single type.
-class BLEEvent {
- public:
-  BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
-    this->event_.gap.gap_event = e;
-    memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t));
-    this->type_ = GAP;
-  };
-
-  BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
-    this->event_.gattc.gattc_event = e;
-    this->event_.gattc.gattc_if = i;
-    memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t));
-    // Need to also make a copy of notify event data.
-    switch (e) {
-      case ESP_GATTC_NOTIFY_EVT:
-        memcpy(this->event_.gattc.data, p->notify.value, p->notify.value_len);
-        this->event_.gattc.gattc_param.notify.value = this->event_.gattc.data;
-        break;
-      case ESP_GATTC_READ_CHAR_EVT:
-      case ESP_GATTC_READ_DESCR_EVT:
-        memcpy(this->event_.gattc.data, p->read.value, p->read.value_len);
-        this->event_.gattc.gattc_param.read.value = this->event_.gattc.data;
-        break;
-      default:
-        break;
-    }
-    this->type_ = GATTC;
-  };
-
-  BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
-    this->event_.gatts.gatts_event = e;
-    this->event_.gatts.gatts_if = i;
-    memcpy(&this->event_.gatts.gatts_param, p, sizeof(esp_ble_gatts_cb_param_t));
-    // Need to also make a copy of write data.
-    switch (e) {
-      case ESP_GATTS_WRITE_EVT:
-        memcpy(this->event_.gatts.data, p->write.value, p->write.len);
-        this->event_.gatts.gatts_param.write.value = this->event_.gatts.data;
-        break;
-      default:
-        break;
-    }
-    this->type_ = GATTS;
-  };
-
-  union {
-    // NOLINTNEXTLINE(readability-identifier-naming)
-    struct gap_event {
-      esp_gap_ble_cb_event_t gap_event;
-      esp_ble_gap_cb_param_t gap_param;
-    } gap;
-
-    // NOLINTNEXTLINE(readability-identifier-naming)
-    struct gattc_event {
-      esp_gattc_cb_event_t gattc_event;
-      esp_gatt_if_t gattc_if;
-      esp_ble_gattc_cb_param_t gattc_param;
-      uint8_t data[64];
-    } gattc;
-
-    // NOLINTNEXTLINE(readability-identifier-naming)
-    struct gatts_event {
-      esp_gatts_cb_event_t gatts_event;
-      esp_gatt_if_t gatts_if;
-      esp_ble_gatts_cb_param_t gatts_param;
-      uint8_t data[64];
-    } gatts;
-  } event_;
-  // NOLINTNEXTLINE(readability-identifier-naming)
-  enum ble_event_t : uint8_t {
-    GAP,
-    GATTC,
-    GATTS,
-  } type_;
-};
-
 }  // namespace esp32_ble
 }  // namespace esphome
 
diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp
index 2337a5fe93..2793a74c5a 100644
--- a/esphome/components/esp32_ble_client/ble_client_base.cpp
+++ b/esphome/components/esp32_ble_client/ble_client_base.cpp
@@ -40,7 +40,7 @@ void BLEClientBase::loop() {
 float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
 
 bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
-  if (device.address_uint64() != this->address_)
+  if (this->address_ == 0 || device.address_uint64() != this->address_)
     return false;
   if (this->state_ != espbt::ClientState::IDLE && this->state_ != espbt::ClientState::SEARCHING)
     return false;
@@ -138,6 +138,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
                  this->address_str_.c_str(), ret);
       }
       if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
+        ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str());
         this->set_state(espbt::ClientState::CONNECTED);
         this->state_ = espbt::ClientState::ESTABLISHED;
         break;
@@ -189,6 +190,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
         ESP_LOGV(TAG, "[%d] [%s]  start_handle: 0x%x  end_handle: 0x%x", this->connection_index_,
                  this->address_str_.c_str(), svc->start_handle, svc->end_handle);
       }
+      ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str());
       this->set_state(espbt::ClientState::CONNECTED);
       this->state_ = espbt::ClientState::ESTABLISHED;
       break;
diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py
index 2fcc5c7743..0ddfa62c1b 100644
--- a/esphome/components/esp32_ble_server/__init__.py
+++ b/esphome/components/esp32_ble_server/__init__.py
@@ -7,21 +7,25 @@ from esphome.components.esp32 import add_idf_sdkconfig_option
 
 AUTO_LOAD = ["esp32_ble"]
 CODEOWNERS = ["@jesserockz"]
-CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"]
+CONFLICTS_WITH = ["esp32_ble_beacon"]
 DEPENDENCIES = ["esp32"]
 
 CONF_MANUFACTURER = "manufacturer"
-CONF_BLE_ID = "ble_id"
 
 esp32_ble_server_ns = cg.esphome_ns.namespace("esp32_ble_server")
-BLEServer = esp32_ble_server_ns.class_("BLEServer", cg.Component)
+BLEServer = esp32_ble_server_ns.class_(
+    "BLEServer",
+    cg.Component,
+    esp32_ble.GATTsEventHandler,
+    cg.Parented.template(esp32_ble.ESP32BLE),
+)
 BLEServiceComponent = esp32_ble_server_ns.class_("BLEServiceComponent")
 
 
 CONFIG_SCHEMA = cv.Schema(
     {
         cv.GenerateID(): cv.declare_id(BLEServer),
-        cv.GenerateID(CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
+        cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
         cv.Optional(CONF_MANUFACTURER, default="ESPHome"): cv.string,
         cv.Optional(CONF_MODEL): cv.string,
     }
@@ -29,16 +33,18 @@ CONFIG_SCHEMA = cv.Schema(
 
 
 async def to_code(config):
-    parent = await cg.get_variable(config[CONF_BLE_ID])
     var = cg.new_Pvariable(config[CONF_ID])
+
     await cg.register_component(var, config)
 
+    parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
+    cg.add(parent.register_gatts_event_handler(var))
+    cg.add(var.set_parent(parent))
+
     cg.add(var.set_manufacturer(config[CONF_MANUFACTURER]))
     if CONF_MODEL in config:
         cg.add(var.set_model(config[CONF_MODEL]))
     cg.add_define("USE_ESP32_BLE_SERVER")
 
-    cg.add(parent.set_server(var))
-
     if CORE.using_esp_idf:
         add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp
index 15bea07021..7cbf40c076 100644
--- a/esphome/components/esp32_ble_server/ble_server.cpp
+++ b/esphome/components/esp32_ble_server/ble_server.cpp
@@ -25,7 +25,8 @@ static const uint16_t VERSION_UUID = 0x2A26;
 static const uint16_t MANUFACTURER_UUID = 0x2A29;
 
 void BLEServer::setup() {
-  if (this->is_failed()) {
+  if (this->parent_->is_failed()) {
+    this->mark_failed();
     ESP_LOGE(TAG, "BLE Server was marked failed by ESP32BLE");
     return;
   }
diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h
index f82e854090..ac759f2dcd 100644
--- a/esphome/components/esp32_ble_server/ble_server.h
+++ b/esphome/components/esp32_ble_server/ble_server.h
@@ -3,6 +3,7 @@
 #include "ble_service.h"
 #include "ble_characteristic.h"
 
+#include "esphome/components/esp32_ble/ble.h"
 #include "esphome/components/esp32_ble/ble_advertising.h"
 #include "esphome/components/esp32_ble/ble_uuid.h"
 #include "esphome/components/esp32_ble/queue.h"
@@ -32,7 +33,7 @@ class BLEServiceComponent {
   virtual void stop();
 };
 
-class BLEServer : public Component {
+class BLEServer : public Component, public GATTsEventHandler, public Parented<ESP32BLE> {
  public:
   void setup() override;
   void loop() override;
@@ -55,7 +56,8 @@ class BLEServer : public Component {
   uint32_t get_connected_client_count() { return this->connected_clients_; }
   const std::map<uint16_t, void *> &get_clients() { return this->clients_; }
 
-  void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
+  void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
+                           esp_ble_gatts_cb_param_t *param) override;
 
   void register_service_component(BLEServiceComponent *component) { this->service_components_.push_back(component); }
 
diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py
index c20491e701..c5af845987 100644
--- a/esphome/components/esp32_ble_tracker/__init__.py
+++ b/esphome/components/esp32_ble_tracker/__init__.py
@@ -15,10 +15,12 @@ from esphome.const import (
     CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
     CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
 )
+from esphome.components import esp32_ble
 from esphome.core import CORE
 from esphome.components.esp32 import add_idf_sdkconfig_option
 from esphome.components import esp32_ble
 
+AUTO_LOAD = ["esp32_ble"]
 DEPENDENCIES = ["esp32"]
 
 CONF_ESP32_BLE_ID = "esp32_ble_id"
@@ -27,7 +29,13 @@ CONF_WINDOW = "window"
 CONF_CONTINUOUS = "continuous"
 CONF_ON_SCAN_END = "on_scan_end"
 esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker")
-ESP32BLETracker = esp32_ble_tracker_ns.class_("ESP32BLETracker", cg.Component)
+ESP32BLETracker = esp32_ble_tracker_ns.class_(
+    "ESP32BLETracker",
+    cg.Component,
+    esp32_ble.GAPEventHandler,
+    esp32_ble.GATTcEventHandler,
+    cg.Parented.template(esp32_ble.ESP32BLE),
+)
 ESPBTClient = esp32_ble_tracker_ns.class_("ESPBTClient")
 ESPBTDeviceListener = esp32_ble_tracker_ns.class_("ESPBTDeviceListener")
 ESPBTDevice = esp32_ble_tracker_ns.class_("ESPBTDevice")
@@ -138,6 +146,7 @@ def as_reversed_hex_array(value):
 CONFIG_SCHEMA = cv.Schema(
     {
         cv.GenerateID(): cv.declare_id(ESP32BLETracker),
+        cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
         cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All(
             cv.Schema(
                 {
@@ -200,6 +209,12 @@ ESP_BLE_DEVICE_SCHEMA = cv.Schema(
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     await cg.register_component(var, config)
+
+    parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
+    cg.add(parent.register_gap_event_handler(var))
+    cg.add(parent.register_gattc_event_handler(var))
+    cg.add(var.set_parent(parent))
+
     params = config[CONF_SCAN_PARAMETERS]
     cg.add(var.set_scan_duration(params[CONF_DURATION]))
     cg.add(var.set_scan_interval(int(params[CONF_INTERVAL].total_milliseconds / 0.625)))
@@ -248,6 +263,7 @@ async def to_code(config):
         add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192)
 
     cg.add_define("USE_OTA_STATE_CALLBACK")  # To be notified when an OTA update starts
+    cg.add_define("USE_ESP32_BLE_CLIENT")
 
 
 ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema(
diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
index b1d469025b..6b0f4dc897 100644
--- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
+++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
@@ -7,14 +7,14 @@
 #include "esphome/core/helpers.h"
 #include "esphome/core/log.h"
 
-#include <nvs_flash.h>
-#include <freertos/FreeRTOSConfig.h>
-#include <esp_bt_main.h>
 #include <esp_bt.h>
-#include <freertos/FreeRTOS.h>
-#include <freertos/task.h>
-#include <esp_gap_ble_api.h>
 #include <esp_bt_defs.h>
+#include <esp_bt_main.h>
+#include <esp_gap_ble_api.h>
+#include <freertos/FreeRTOS.h>
+#include <freertos/FreeRTOSConfig.h>
+#include <freertos/task.h>
+#include <nvs_flash.h>
 
 #ifdef USE_OTA
 #include "esphome/components/ota/ota_component.h"
@@ -45,17 +45,19 @@ uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) {
   return u;
 }
 
-float ESP32BLETracker::get_setup_priority() const { return setup_priority::BLUETOOTH; }
+float ESP32BLETracker::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
 
 void ESP32BLETracker::setup() {
+  if (this->parent_->is_failed()) {
+    this->mark_failed();
+    ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE");
+    return;
+  }
+
   global_esp32_ble_tracker = this;
   this->scan_result_lock_ = xSemaphoreCreateMutex();
   this->scan_end_lock_ = xSemaphoreCreateMutex();
   this->scanner_idle_ = true;
-  if (!ESP32BLETracker::ble_setup()) {
-    this->mark_failed();
-    return;
-  }
 
 #ifdef USE_OTA
   ota::global_ota_component->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t error) {
@@ -75,18 +77,6 @@ void ESP32BLETracker::setup() {
 }
 
 void ESP32BLETracker::loop() {
-  BLEEvent *ble_event = this->ble_events_.pop();
-  while (ble_event != nullptr) {
-    if (ble_event->type_) {
-      this->real_gattc_event_handler_(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if,
-                                      &ble_event->event_.gattc.gattc_param);
-    } else {
-      this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param);
-    }
-    delete ble_event;  // NOLINT(cppcoreguidelines-owning-memory)
-    ble_event = this->ble_events_.pop();
-  }
-
   int connecting = 0;
   int discovered = 0;
   int searching = 0;
@@ -238,85 +228,6 @@ void ESP32BLETracker::stop_scan() {
   this->cancel_timeout("scan");
 }
 
-bool ESP32BLETracker::ble_setup() {
-  // Initialize non-volatile storage for the bluetooth controller
-  esp_err_t err = nvs_flash_init();
-  if (err != ESP_OK) {
-    ESP_LOGE(TAG, "nvs_flash_init failed: %d", err);
-    return false;
-  }
-
-#ifdef USE_ARDUINO
-  if (!btStart()) {
-    ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status());
-    return false;
-  }
-#else
-  if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) {
-    // start bt controller
-    if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) {
-      esp_bt_controller_config_t cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
-      err = esp_bt_controller_init(&cfg);
-      if (err != ESP_OK) {
-        ESP_LOGE(TAG, "esp_bt_controller_init failed: %s", esp_err_to_name(err));
-        return false;
-      }
-      while (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE)
-        ;
-    }
-    if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_INITED) {
-      err = esp_bt_controller_enable(ESP_BT_MODE_BLE);
-      if (err != ESP_OK) {
-        ESP_LOGE(TAG, "esp_bt_controller_enable failed: %s", esp_err_to_name(err));
-        return false;
-      }
-    }
-    if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) {
-      ESP_LOGE(TAG, "esp bt controller enable failed");
-      return false;
-    }
-  }
-#endif
-
-  esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
-
-  err = esp_bluedroid_init();
-  if (err != ESP_OK) {
-    ESP_LOGE(TAG, "esp_bluedroid_init failed: %d", err);
-    return false;
-  }
-  err = esp_bluedroid_enable();
-  if (err != ESP_OK) {
-    ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err);
-    return false;
-  }
-  err = esp_ble_gap_register_callback(ESP32BLETracker::gap_event_handler);
-  if (err != ESP_OK) {
-    ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err);
-    return false;
-  }
-  err = esp_ble_gattc_register_callback(ESP32BLETracker::gattc_event_handler);
-  if (err != ESP_OK) {
-    ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err);
-    return false;
-  }
-
-  // Empty name
-  esp_ble_gap_set_device_name("");
-
-  esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE;
-  err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t));
-  if (err != ESP_OK) {
-    ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err);
-    return false;
-  }
-
-  // BLE takes some time to be fully set up, 200ms should be more than enough
-  delay(200);  // NOLINT
-
-  return true;
-}
-
 void ESP32BLETracker::start_scan_(bool first) {
   // The lock must be held when calling this function.
   if (xSemaphoreTake(this->scan_end_lock_, 0L)) {
@@ -369,11 +280,6 @@ void ESP32BLETracker::register_client(ESPBTClient *client) {
 }
 
 void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
-  BLEEvent *gap_event = new BLEEvent(event, param);  // NOLINT(cppcoreguidelines-owning-memory)
-  global_esp32_ble_tracker->ble_events_.push(gap_event);
-}  // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
-
-void ESP32BLETracker::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
   switch (event) {
     case ESP_GAP_BLE_SCAN_RESULT_EVT:
       this->gap_scan_result_(param->scan_rst);
@@ -428,204 +334,11 @@ void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_re
 
 void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
                                           esp_ble_gattc_cb_param_t *param) {
-  BLEEvent *gattc_event = new BLEEvent(event, gattc_if, param);  // NOLINT(cppcoreguidelines-owning-memory)
-  global_esp32_ble_tracker->ble_events_.push(gattc_event);
-}  // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
-
-void ESP32BLETracker::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
-                                                esp_ble_gattc_cb_param_t *param) {
   for (auto *client : this->clients_) {
     client->gattc_event_handler(event, gattc_if, param);
   }
 }
 
-ESPBTUUID::ESPBTUUID() : uuid_() {}
-ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) {
-  ESPBTUUID ret;
-  ret.uuid_.len = ESP_UUID_LEN_16;
-  ret.uuid_.uuid.uuid16 = uuid;
-  return ret;
-}
-ESPBTUUID ESPBTUUID::from_uint32(uint32_t uuid) {
-  ESPBTUUID ret;
-  ret.uuid_.len = ESP_UUID_LEN_32;
-  ret.uuid_.uuid.uuid32 = uuid;
-  return ret;
-}
-ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) {
-  ESPBTUUID ret;
-  ret.uuid_.len = ESP_UUID_LEN_128;
-  for (size_t i = 0; i < ESP_UUID_LEN_128; i++)
-    ret.uuid_.uuid.uuid128[i] = data[i];
-  return ret;
-}
-ESPBTUUID ESPBTUUID::from_raw(const std::string &data) {
-  ESPBTUUID ret;
-  if (data.length() == 4) {
-    ret.uuid_.len = ESP_UUID_LEN_16;
-    ret.uuid_.uuid.uuid16 = 0;
-    for (int i = 0; i < data.length();) {
-      uint8_t msb = data.c_str()[i];
-      uint8_t lsb = data.c_str()[i + 1];
-
-      if (msb > '9')
-        msb -= 7;
-      if (lsb > '9')
-        lsb -= 7;
-      ret.uuid_.uuid.uuid16 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << (2 - i) * 4;
-      i += 2;
-    }
-  } else if (data.length() == 8) {
-    ret.uuid_.len = ESP_UUID_LEN_32;
-    ret.uuid_.uuid.uuid32 = 0;
-    for (int i = 0; i < data.length();) {
-      uint8_t msb = data.c_str()[i];
-      uint8_t lsb = data.c_str()[i + 1];
-
-      if (msb > '9')
-        msb -= 7;
-      if (lsb > '9')
-        lsb -= 7;
-      ret.uuid_.uuid.uuid32 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << (6 - i) * 4;
-      i += 2;
-    }
-  } else if (data.length() == 16) {  // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be
-                                     // investigated (lack of time)
-    ret.uuid_.len = ESP_UUID_LEN_128;
-    memcpy(ret.uuid_.uuid.uuid128, (uint8_t *) data.data(), 16);
-  } else if (data.length() == 36) {
-    // If the length of the string is 36 bytes then we will assume it is a long hex string in
-    // UUID format.
-    ret.uuid_.len = ESP_UUID_LEN_128;
-    int n = 0;
-    for (int i = 0; i < data.length();) {
-      if (data.c_str()[i] == '-')
-        i++;
-      uint8_t msb = data.c_str()[i];
-      uint8_t lsb = data.c_str()[i + 1];
-
-      if (msb > '9')
-        msb -= 7;
-      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());
-  }
-  return ret;
-}
-ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) {
-  ESPBTUUID ret;
-  ret.uuid_.len = uuid.len;
-  if (uuid.len == ESP_UUID_LEN_16) {
-    ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16;
-  } else if (uuid.len == ESP_UUID_LEN_32) {
-    ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32;
-  } else if (uuid.len == ESP_UUID_LEN_128) {
-    memcpy(ret.uuid_.uuid.uuid128, uuid.uuid.uuid128, ESP_UUID_LEN_128);
-  }
-  return ret;
-}
-ESPBTUUID ESPBTUUID::as_128bit() const {
-  if (this->uuid_.len == ESP_UUID_LEN_128) {
-    return *this;
-  }
-  uint8_t data[] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
-  uint32_t uuid32;
-  if (this->uuid_.len == ESP_UUID_LEN_32) {
-    uuid32 = this->uuid_.uuid.uuid32;
-  } else {
-    uuid32 = this->uuid_.uuid.uuid16;
-  }
-  for (uint8_t i = 0; i < this->uuid_.len; i++) {
-    data[12 + i] = ((uuid32 >> i * 8) & 0xFF);
-  }
-  return ESPBTUUID::from_raw(data);
-}
-bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const {
-  if (this->uuid_.len == ESP_UUID_LEN_16) {
-    return (this->uuid_.uuid.uuid16 >> 8) == data2 && (this->uuid_.uuid.uuid16 & 0xFF) == data1;
-  } else if (this->uuid_.len == ESP_UUID_LEN_32) {
-    for (uint8_t i = 0; i < 3; i++) {
-      bool a = ((this->uuid_.uuid.uuid32 >> i * 8) & 0xFF) == data1;
-      bool b = ((this->uuid_.uuid.uuid32 >> (i + 1) * 8) & 0xFF) == data2;
-      if (a && b)
-        return true;
-    }
-  } else {
-    for (uint8_t i = 0; i < 15; i++) {
-      if (this->uuid_.uuid.uuid128[i] == data1 && this->uuid_.uuid.uuid128[i + 1] == data2)
-        return true;
-    }
-  }
-  return false;
-}
-bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const {
-  if (this->uuid_.len == uuid.uuid_.len) {
-    switch (this->uuid_.len) {
-      case ESP_UUID_LEN_16:
-        if (uuid.uuid_.uuid.uuid16 == this->uuid_.uuid.uuid16) {
-          return true;
-        }
-        break;
-      case ESP_UUID_LEN_32:
-        if (uuid.uuid_.uuid.uuid32 == this->uuid_.uuid.uuid32) {
-          return true;
-        }
-        break;
-      case ESP_UUID_LEN_128:
-        for (int i = 0; i < ESP_UUID_LEN_128; i++) {
-          if (uuid.uuid_.uuid.uuid128[i] != this->uuid_.uuid.uuid128[i]) {
-            return false;
-          }
-        }
-        return true;
-        break;
-    }
-  } else {
-    return this->as_128bit() == uuid.as_128bit();
-  }
-  return false;
-}
-esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; }
-std::string ESPBTUUID::to_string() const {
-  switch (this->uuid_.len) {
-    case ESP_UUID_LEN_16:
-      return str_snprintf("0x%02X%02X", 6, this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff);
-    case ESP_UUID_LEN_32:
-      return str_snprintf("0x%02X%02X%02X%02X", 10, this->uuid_.uuid.uuid32 >> 24,
-                          (this->uuid_.uuid.uuid32 >> 16 & 0xff), (this->uuid_.uuid.uuid32 >> 8 & 0xff),
-                          this->uuid_.uuid.uuid32 & 0xff);
-    default:
-    case ESP_UUID_LEN_128:
-      std::string buf;
-      for (int8_t i = 15; i >= 0; i--) {
-        buf += str_snprintf("%02X", 2, this->uuid_.uuid.uuid128[i]);
-        if (i == 6 || i == 8 || i == 10 || i == 12)
-          buf += "-";
-      }
-      return buf;
-  }
-  return "";
-}
-
-uint64_t ESPBTUUID::get_128bit_high() const {
-  esp_bt_uuid_t uuid = this->as_128bit().get_uuid();
-  return ((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) |
-         ((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) |
-         ((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) |
-         ((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]);
-}
-uint64_t ESPBTUUID::get_128bit_low() const {
-  esp_bt_uuid_t uuid = this->as_128bit().get_uuid();
-  return ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) |
-         ((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) |
-         ((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) |
-         ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0]);
-}
-
 ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); }
 optional<ESPBLEiBeacon> ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) {
   if (!data.uuid.contains(0x4C, 0x00))
diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
index e6f7829353..d1f72cf78d 100644
--- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
+++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
@@ -1,9 +1,8 @@
 #pragma once
 
-#include "esphome/core/component.h"
 #include "esphome/core/automation.h"
+#include "esphome/core/component.h"
 #include "esphome/core/helpers.h"
-#include "queue.h"
 
 #include <array>
 #include <string>
@@ -15,40 +14,16 @@
 #include <esp_gattc_api.h>
 #include <esp_bt_defs.h>
 
+#include <freertos/FreeRTOS.h>
+#include <freertos/semphr.h>
+
+#include "esphome/components/esp32_ble/ble.h"
+#include "esphome/components/esp32_ble/ble_uuid.h"
+
 namespace esphome {
 namespace esp32_ble_tracker {
 
-class ESPBTUUID {
- public:
-  ESPBTUUID();
-
-  static ESPBTUUID from_uint16(uint16_t uuid);
-
-  static ESPBTUUID from_uint32(uint32_t uuid);
-
-  static ESPBTUUID from_raw(const uint8_t *data);
-
-  static ESPBTUUID from_raw(const std::string &data);
-
-  static ESPBTUUID from_uuid(esp_bt_uuid_t uuid);
-
-  ESPBTUUID as_128bit() const;
-
-  bool contains(uint8_t data1, uint8_t data2) const;
-
-  bool operator==(const ESPBTUUID &uuid) const;
-  bool operator!=(const ESPBTUUID &uuid) const { return !(*this == uuid); }
-
-  esp_bt_uuid_t get_uuid() const;
-
-  std::string to_string() const;
-
-  uint64_t get_128bit_high() const;
-  uint64_t get_128bit_low() const;
-
- protected:
-  esp_bt_uuid_t uuid_;
-};
+using namespace esp32_ble;
 
 using adv_data_t = std::vector<uint8_t>;
 
@@ -191,7 +166,7 @@ class ESPBTClient : public ESPBTDeviceListener {
   ClientState state_;
 };
 
-class ESP32BLETracker : public Component {
+class ESP32BLETracker : public Component, public GAPEventHandler, public GATTcEventHandler, public Parented<ESP32BLE> {
  public:
   void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; }
   void set_scan_interval(uint32_t scan_interval) { scan_interval_ = scan_interval; }
@@ -218,16 +193,15 @@ class ESP32BLETracker : public Component {
   void start_scan();
   void stop_scan();
 
+  void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
+                           esp_ble_gattc_cb_param_t *param) override;
+  void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
+
  protected:
-  /// The FreeRTOS task managing the bluetooth interface.
-  static bool ble_setup();
   /// Start a single scan by setting up the parameters and doing some esp-idf calls.
   void start_scan_(bool first);
   /// Called when a scan ends
   void end_of_scan_();
-  /// Callback that will handle all GAP events and redistribute them to other callbacks.
-  static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
-  void real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
   /// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received.
   void gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param);
   /// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received.
@@ -238,9 +212,6 @@ class ESP32BLETracker : public Component {
   void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param &param);
 
   int app_id_;
-  /// Callback that will handle all GATTC events and redistribute them to other callbacks.
-  static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param);
-  void real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param);
 
   /// Vector of addresses that have already been printed in print_bt_device_info
   std::vector<uint64_t> already_discovered_;
@@ -263,8 +234,6 @@ class ESP32BLETracker : public Component {
   esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_buffer_[16];
   esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
   esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
-
-  Queue<BLEEvent> ble_events_;
 };
 
 // NOLINTNEXTLINE
diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py
index 753b6ed9da..b3abbd5c13 100644
--- a/esphome/components/esp32_camera/__init__.py
+++ b/esphome/components/esp32_camera/__init__.py
@@ -156,7 +156,7 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
         cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum(
             FRAME_SIZES, upper=True
         ),
-        cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=10, max=63),
+        cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63),
         cv.Optional(CONF_CONTRAST, default=0): camera_range_param,
         cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param,
         cv.Optional(CONF_SATURATION, default=0): camera_range_param,
diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py
index 1e50418e01..7170a6dabf 100644
--- a/esphome/components/esp32_improv/__init__.py
+++ b/esphome/components/esp32_improv/__init__.py
@@ -6,7 +6,7 @@ from esphome.const import CONF_ID
 
 AUTO_LOAD = ["binary_sensor", "output", "esp32_ble_server"]
 CODEOWNERS = ["@jesserockz"]
-CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"]
+CONFLICTS_WITH = ["esp32_ble_beacon"]
 DEPENDENCIES = ["wifi", "esp32"]
 
 CONF_AUTHORIZED_DURATION = "authorized_duration"
diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp
index 5ff4b0827d..85013c006b 100644
--- a/esphome/components/esp32_improv/esp32_improv_component.cpp
+++ b/esphome/components/esp32_improv/esp32_improv_component.cpp
@@ -195,7 +195,7 @@ void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &response) {
 }
 
 void ESP32ImprovComponent::start() {
-  if (this->state_ != improv::STATE_STOPPED)
+  if (this->should_start_ || this->state_ != improv::STATE_STOPPED)
     return;
 
   ESP_LOGD(TAG, "Setting Improv to start");
diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py
index b3614d8fcf..a0f8b557b0 100644
--- a/esphome/components/ethernet/__init__.py
+++ b/esphome/components/ethernet/__init__.py
@@ -33,6 +33,7 @@ ETHERNET_TYPES = {
     "RTL8201": EthernetType.ETHERNET_TYPE_RTL8201,
     "DP83848": EthernetType.ETHERNET_TYPE_DP83848,
     "IP101": EthernetType.ETHERNET_TYPE_IP101,
+    "JL1101": EthernetType.ETHERNET_TYPE_JL1101,
 }
 
 emac_rmii_clock_gpio_t = cg.global_ns.enum("emac_rmii_clock_gpio_t")
diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c
new file mode 100644
index 0000000000..6011795033
--- /dev/null
+++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c
@@ -0,0 +1,339 @@
+// Copyright 2019 Espressif Systems (Shanghai) PTE LTD
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifdef USE_ESP32
+
+#include <string.h>
+#include <stdlib.h>
+#include <sys/cdefs.h>
+#include "esp_log.h"
+#include "esp_eth.h"
+#include "eth_phy_regs_struct.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "driver/gpio.h"
+#include "esp_rom_gpio.h"
+#include "esp_rom_sys.h"
+
+static const char *TAG = "jl1101";
+#define PHY_CHECK(a, str, goto_tag, ...) \
+  do { \
+    if (!(a)) { \
+      ESP_LOGE(TAG, "%s(%d): " str, __FUNCTION__, __LINE__, ##__VA_ARGS__); \
+      goto goto_tag; \
+    } \
+  } while (0)
+
+/***************Vendor Specific Register***************/
+
+/**
+ * @brief PSR(Page Select Register)
+ *
+ */
+typedef union {
+  struct {
+    uint16_t page_select : 8; /* Select register page, default is 0 */
+    uint16_t reserved : 8;    /* Reserved */
+  };
+  uint16_t val;
+} psr_reg_t;
+#define ETH_PHY_PSR_REG_ADDR (0x1F)
+
+typedef struct {
+  esp_eth_phy_t parent;
+  esp_eth_mediator_t *eth;
+  int addr;
+  uint32_t reset_timeout_ms;
+  uint32_t autonego_timeout_ms;
+  eth_link_t link_status;
+  int reset_gpio_num;
+} phy_jl1101_t;
+
+static esp_err_t jl1101_page_select(phy_jl1101_t *jl1101, uint32_t page) {
+  esp_eth_mediator_t *eth = jl1101->eth;
+  psr_reg_t psr = {.page_select = page};
+  PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_PSR_REG_ADDR, psr.val) == ESP_OK, "write PSR failed", err);
+  return ESP_OK;
+err:
+  return ESP_FAIL;
+}
+
+static esp_err_t jl1101_update_link_duplex_speed(phy_jl1101_t *jl1101) {
+  esp_eth_mediator_t *eth = jl1101->eth;
+  eth_speed_t speed = ETH_SPEED_10M;
+  eth_duplex_t duplex = ETH_DUPLEX_HALF;
+  bmcr_reg_t bmcr;
+  bmsr_reg_t bmsr;
+  uint32_t peer_pause_ability = false;
+  anlpar_reg_t anlpar;
+  PHY_CHECK(jl1101_page_select(jl1101, 0) == ESP_OK, "select page 0 failed", err);
+  PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMSR_REG_ADDR, &(bmsr.val)) == ESP_OK, "read BMSR failed",
+            err);
+  PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_ANLPAR_REG_ADDR, &(anlpar.val)) == ESP_OK,
+            "read ANLPAR failed", err);
+  eth_link_t link = bmsr.link_status ? ETH_LINK_UP : ETH_LINK_DOWN;
+  /* check if link status changed */
+  if (jl1101->link_status != link) {
+    /* when link up, read negotiation result */
+    if (link == ETH_LINK_UP) {
+      PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed",
+                err);
+      if (bmcr.speed_select) {
+        speed = ETH_SPEED_100M;
+      } else {
+        speed = ETH_SPEED_10M;
+      }
+      if (bmcr.duplex_mode) {
+        duplex = ETH_DUPLEX_FULL;
+      } else {
+        duplex = ETH_DUPLEX_HALF;
+      }
+      PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_SPEED, (void *) speed) == ESP_OK, "change speed failed", err);
+      PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_DUPLEX, (void *) duplex) == ESP_OK, "change duplex failed", err);
+      /* if we're in duplex mode, and peer has the flow control ability */
+      if (duplex == ETH_DUPLEX_FULL && anlpar.symmetric_pause) {
+        peer_pause_ability = 1;
+      } else {
+        peer_pause_ability = 0;
+      }
+      PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_PAUSE, (void *) peer_pause_ability) == ESP_OK,
+                "change pause ability failed", err);
+    }
+    PHY_CHECK(eth->on_state_changed(eth, ETH_STATE_LINK, (void *) link) == ESP_OK, "change link failed", err);
+    jl1101->link_status = link;
+  }
+  return ESP_OK;
+err:
+  return ESP_FAIL;
+}
+
+static esp_err_t jl1101_set_mediator(esp_eth_phy_t *phy, esp_eth_mediator_t *eth) {
+  PHY_CHECK(eth, "can't set mediator to null", err);
+  phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent);
+  jl1101->eth = eth;
+  return ESP_OK;
+err:
+  return ESP_ERR_INVALID_ARG;
+}
+
+static esp_err_t jl1101_get_link(esp_eth_phy_t *phy) {
+  phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent);
+  /* Updata information about link, speed, duplex */
+  PHY_CHECK(jl1101_update_link_duplex_speed(jl1101) == ESP_OK, "update link duplex speed failed", err);
+  return ESP_OK;
+err:
+  return ESP_FAIL;
+}
+
+static esp_err_t jl1101_reset(esp_eth_phy_t *phy) {
+  phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent);
+  jl1101->link_status = ETH_LINK_DOWN;
+  esp_eth_mediator_t *eth = jl1101->eth;
+  bmcr_reg_t bmcr = {.reset = 1};
+  PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, bmcr.val) == ESP_OK, "write BMCR failed", err);
+  /* Wait for reset complete */
+  uint32_t to = 0;
+  for (to = 0; to < jl1101->reset_timeout_ms / 50; to++) {
+    vTaskDelay(pdMS_TO_TICKS(50));
+    PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed",
+              err);
+    if (!bmcr.reset) {
+      break;
+    }
+  }
+  PHY_CHECK(to < jl1101->reset_timeout_ms / 50, "reset timeout", err);
+  return ESP_OK;
+err:
+  return ESP_FAIL;
+}
+
+static esp_err_t jl1101_reset_hw(esp_eth_phy_t *phy) {
+  phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent);
+  if (jl1101->reset_gpio_num >= 0) {
+    esp_rom_gpio_pad_select_gpio(jl1101->reset_gpio_num);
+    gpio_set_direction(jl1101->reset_gpio_num, GPIO_MODE_OUTPUT);
+    gpio_set_level(jl1101->reset_gpio_num, 0);
+    esp_rom_delay_us(100);  // insert min input assert time
+    gpio_set_level(jl1101->reset_gpio_num, 1);
+  }
+  return ESP_OK;
+}
+
+static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy) {
+  phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent);
+  esp_eth_mediator_t *eth = jl1101->eth;
+  /* in case any link status has changed, let's assume we're in link down status */
+  jl1101->link_status = ETH_LINK_DOWN;
+  /* Restart auto negotiation */
+  bmcr_reg_t bmcr = {
+      .speed_select = 1,     /* 100Mbps */
+      .duplex_mode = 1,      /* Full Duplex */
+      .en_auto_nego = 1,     /* Auto Negotiation */
+      .restart_auto_nego = 1 /* Restart Auto Negotiation */
+  };
+  PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, bmcr.val) == ESP_OK, "write BMCR failed", err);
+  /* Wait for auto negotiation complete */
+  bmsr_reg_t bmsr;
+  uint32_t to = 0;
+  for (to = 0; to < jl1101->autonego_timeout_ms / 100; to++) {
+    vTaskDelay(pdMS_TO_TICKS(100));
+    PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMSR_REG_ADDR, &(bmsr.val)) == ESP_OK, "read BMSR failed",
+              err);
+    if (bmsr.auto_nego_complete) {
+      break;
+    }
+  }
+  /* Auto negotiation failed, maybe no network cable plugged in, so output a warning */
+  if (to >= jl1101->autonego_timeout_ms / 100) {
+    ESP_LOGW(TAG, "auto negotiation timeout");
+  }
+  return ESP_OK;
+err:
+  return ESP_FAIL;
+}
+
+static esp_err_t jl1101_pwrctl(esp_eth_phy_t *phy, bool enable) {
+  phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent);
+  esp_eth_mediator_t *eth = jl1101->eth;
+  bmcr_reg_t bmcr;
+  PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed",
+            err);
+  if (!enable) {
+    /* Enable IEEE Power Down Mode */
+    bmcr.power_down = 1;
+  } else {
+    /* Disable IEEE Power Down Mode */
+    bmcr.power_down = 0;
+  }
+  PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, bmcr.val) == ESP_OK, "write BMCR failed", err);
+  if (!enable) {
+    PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed",
+              err);
+    PHY_CHECK(bmcr.power_down == 1, "power down failed", err);
+  } else {
+    /* wait for power up complete */
+    uint32_t to = 0;
+    for (to = 0; to < jl1101->reset_timeout_ms / 10; to++) {
+      vTaskDelay(pdMS_TO_TICKS(10));
+      PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)) == ESP_OK, "read BMCR failed",
+                err);
+      if (bmcr.power_down == 0) {
+        break;
+      }
+    }
+    PHY_CHECK(to < jl1101->reset_timeout_ms / 10, "power up timeout", err);
+  }
+  return ESP_OK;
+err:
+  return ESP_FAIL;
+}
+
+static esp_err_t jl1101_set_addr(esp_eth_phy_t *phy, uint32_t addr) {
+  phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent);
+  jl1101->addr = addr;
+  return ESP_OK;
+}
+
+static esp_err_t jl1101_get_addr(esp_eth_phy_t *phy, uint32_t *addr) {
+  PHY_CHECK(addr, "addr can't be null", err);
+  phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent);
+  *addr = jl1101->addr;
+  return ESP_OK;
+err:
+  return ESP_ERR_INVALID_ARG;
+}
+
+static esp_err_t jl1101_del(esp_eth_phy_t *phy) {
+  phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent);
+  free(jl1101);
+  return ESP_OK;
+}
+
+static esp_err_t jl1101_advertise_pause_ability(esp_eth_phy_t *phy, uint32_t ability) {
+  phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent);
+  esp_eth_mediator_t *eth = jl1101->eth;
+  /* Set PAUSE function ability */
+  anar_reg_t anar;
+  PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_ANAR_REG_ADDR, &(anar.val)) == ESP_OK, "read ANAR failed",
+            err);
+  if (ability) {
+    anar.asymmetric_pause = 1;
+    anar.symmetric_pause = 1;
+  } else {
+    anar.asymmetric_pause = 0;
+    anar.symmetric_pause = 0;
+  }
+  PHY_CHECK(eth->phy_reg_write(eth, jl1101->addr, ETH_PHY_ANAR_REG_ADDR, anar.val) == ESP_OK, "write ANAR failed", err);
+  return ESP_OK;
+err:
+  return ESP_FAIL;
+}
+
+static esp_err_t jl1101_init(esp_eth_phy_t *phy) {
+  phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent);
+  esp_eth_mediator_t *eth = jl1101->eth;
+  // Detect PHY address
+  if (jl1101->addr == ESP_ETH_PHY_ADDR_AUTO) {
+    PHY_CHECK(esp_eth_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err);
+  }
+  /* Power on Ethernet PHY */
+  PHY_CHECK(jl1101_pwrctl(phy, true) == ESP_OK, "power control failed", err);
+  /* Reset Ethernet PHY */
+  PHY_CHECK(jl1101_reset(phy) == ESP_OK, "reset failed", err);
+  /* Check PHY ID */
+  phyidr1_reg_t id1;
+  phyidr2_reg_t id2;
+  PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_IDR1_REG_ADDR, &(id1.val)) == ESP_OK, "read ID1 failed", err);
+  PHY_CHECK(eth->phy_reg_read(eth, jl1101->addr, ETH_PHY_IDR2_REG_ADDR, &(id2.val)) == ESP_OK, "read ID2 failed", err);
+  PHY_CHECK(id1.oui_msb == 0x937C && id2.oui_lsb == 0x10 && id2.vendor_model == 0x2, "wrong chip ID", err);
+  return ESP_OK;
+err:
+  return ESP_FAIL;
+}
+
+static esp_err_t jl1101_deinit(esp_eth_phy_t *phy) {
+  /* Power off Ethernet PHY */
+  PHY_CHECK(jl1101_pwrctl(phy, false) == ESP_OK, "power control failed", err);
+  return ESP_OK;
+err:
+  return ESP_FAIL;
+}
+
+esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config) {
+  PHY_CHECK(config, "can't set phy config to null", err);
+  phy_jl1101_t *jl1101 = calloc(1, sizeof(phy_jl1101_t));
+  PHY_CHECK(jl1101, "calloc jl1101 failed", err);
+  jl1101->addr = config->phy_addr;
+  jl1101->reset_gpio_num = config->reset_gpio_num;
+  jl1101->reset_timeout_ms = config->reset_timeout_ms;
+  jl1101->link_status = ETH_LINK_DOWN;
+  jl1101->autonego_timeout_ms = config->autonego_timeout_ms;
+  jl1101->parent.reset = jl1101_reset;
+  jl1101->parent.reset_hw = jl1101_reset_hw;
+  jl1101->parent.init = jl1101_init;
+  jl1101->parent.deinit = jl1101_deinit;
+  jl1101->parent.set_mediator = jl1101_set_mediator;
+  jl1101->parent.negotiate = jl1101_negotiate;
+  jl1101->parent.get_link = jl1101_get_link;
+  jl1101->parent.pwrctl = jl1101_pwrctl;
+  jl1101->parent.get_addr = jl1101_get_addr;
+  jl1101->parent.set_addr = jl1101_set_addr;
+  jl1101->parent.advertise_pause_ability = jl1101_advertise_pause_ability;
+  jl1101->parent.del = jl1101_del;
+
+  return &(jl1101->parent);
+err:
+  return NULL;
+}
+#endif /* USE_ESP32 */
diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp
index fa66d824be..a3f0ae715f 100644
--- a/esphome/components/ethernet/ethernet_component.cpp
+++ b/esphome/components/ethernet/ethernet_component.cpp
@@ -71,6 +71,10 @@ void EthernetComponent::setup() {
       phy = esp_eth_phy_new_ip101(&phy_config);
       break;
     }
+    case ETHERNET_TYPE_JL1101: {
+      phy = esp_eth_phy_new_jl1101(&phy_config);
+      break;
+    }
     default: {
       this->mark_failed();
       return;
diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h
index ed784e1bc0..a538a5c77d 100644
--- a/esphome/components/ethernet/ethernet_component.h
+++ b/esphome/components/ethernet/ethernet_component.h
@@ -18,6 +18,7 @@ enum EthernetType {
   ETHERNET_TYPE_RTL8201,
   ETHERNET_TYPE_DP83848,
   ETHERNET_TYPE_IP101,
+  ETHERNET_TYPE_JL1101,
 };
 
 struct ManualIP {
@@ -82,6 +83,7 @@ class EthernetComponent : public Component {
 
 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
 extern EthernetComponent *global_eth_component;
+extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config);
 
 }  // namespace ethernet
 }  // namespace esphome
diff --git a/esphome/components/growatt_solar/sensor.py b/esphome/components/growatt_solar/sensor.py
index 4961595505..f95d679c3e 100644
--- a/esphome/components/growatt_solar/sensor.py
+++ b/esphome/components/growatt_solar/sensor.py
@@ -52,7 +52,7 @@ GrowattSolar = growatt_solar_ns.class_(
 PHASE_SENSORS = {
     CONF_VOLTAGE: sensor.sensor_schema(
         unit_of_measurement=UNIT_VOLT,
-        accuracy_decimals=2,
+        accuracy_decimals=1,
         device_class=DEVICE_CLASS_VOLTAGE,
     ),
     CONF_CURRENT: sensor.sensor_schema(
@@ -71,7 +71,7 @@ PHASE_SENSORS = {
 PV_SENSORS = {
     CONF_VOLTAGE: sensor.sensor_schema(
         unit_of_measurement=UNIT_VOLT,
-        accuracy_decimals=2,
+        accuracy_decimals=1,
         device_class=DEVICE_CLASS_VOLTAGE,
     ),
     CONF_CURRENT: sensor.sensor_schema(
@@ -135,13 +135,13 @@ CONFIG_SCHEMA = (
             ),
             cv.Optional(CONF_ENERGY_PRODUCTION_DAY): sensor.sensor_schema(
                 unit_of_measurement=UNIT_KILOWATT_HOURS,
-                accuracy_decimals=2,
+                accuracy_decimals=1,
                 device_class=DEVICE_CLASS_ENERGY,
                 state_class=STATE_CLASS_TOTAL_INCREASING,
             ),
             cv.Optional(CONF_TOTAL_ENERGY_PRODUCTION): sensor.sensor_schema(
                 unit_of_measurement=UNIT_KILOWATT_HOURS,
-                accuracy_decimals=0,
+                accuracy_decimals=1,
                 device_class=DEVICE_CLASS_ENERGY,
                 state_class=STATE_CLASS_TOTAL_INCREASING,
             ),
diff --git a/esphome/components/hte501/__init__.py b/esphome/components/hte501/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp
new file mode 100644
index 0000000000..68edd07a22
--- /dev/null
+++ b/esphome/components/hte501/hte501.cpp
@@ -0,0 +1,90 @@
+#include "hte501.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace hte501 {
+
+static const char *const TAG = "hte501";
+
+void HTE501Component::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up HTE501...");
+  uint8_t address[] = {0x70, 0x29};
+  this->write(address, 2, false);
+  uint8_t identification[9];
+  this->read(identification, 9);
+  if (identification[8] != calc_crc8_(identification, 0, 7)) {
+    this->error_code_ = CRC_CHECK_FAILED;
+    this->mark_failed();
+    return;
+  }
+  ESP_LOGV(TAG, "    Serial Number: 0x%s", format_hex(identification + 0, 7).c_str());
+}
+
+void HTE501Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "HTE501:");
+  LOG_I2C_DEVICE(this);
+  switch (this->error_code_) {
+    case COMMUNICATION_FAILED:
+      ESP_LOGE(TAG, "Communication with HTE501 failed!");
+      break;
+    case CRC_CHECK_FAILED:
+      ESP_LOGE(TAG, "The crc check failed");
+      break;
+    case NONE:
+    default:
+      break;
+  }
+  LOG_UPDATE_INTERVAL(this);
+  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
+  LOG_SENSOR("  ", "Humidity", this->humidity_sensor_);
+}
+
+float HTE501Component::get_setup_priority() const { return setup_priority::DATA; }
+void HTE501Component::update() {
+  uint8_t address_1[] = {0x2C, 0x1B};
+  this->write(address_1, 2, true);
+  this->set_timeout(50, [this]() {
+    uint8_t i2c_response[6];
+    this->read(i2c_response, 6);
+    if (i2c_response[2] != calc_crc8_(i2c_response, 0, 1) && i2c_response[5] != calc_crc8_(i2c_response, 3, 4)) {
+      this->error_code_ = CRC_CHECK_FAILED;
+      this->status_set_warning();
+      return;
+    }
+    float temperature = (float) encode_uint16(i2c_response[0], i2c_response[1]);
+    if (temperature > 55536) {
+      temperature = (temperature - 65536) / 100;
+    } else {
+      temperature = temperature / 100;
+    }
+    float humidity = ((float) encode_uint16(i2c_response[3], i2c_response[4])) / 100.0f;
+
+    ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity);
+    if (this->temperature_sensor_ != nullptr)
+      this->temperature_sensor_->publish_state(temperature);
+    if (this->humidity_sensor_ != nullptr)
+      this->humidity_sensor_->publish_state(humidity);
+    this->status_clear_warning();
+  });
+}
+
+unsigned char HTE501Component::calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to) {
+  unsigned char crc_val = 0xFF;
+  unsigned char i = 0;
+  unsigned char j = 0;
+  for (i = from; i <= to; i++) {
+    int cur_val = buf[i];
+    for (j = 0; j < 8; j++) {
+      if (((crc_val ^ cur_val) & 0x80) != 0)  // If MSBs are not equal
+      {
+        crc_val = ((crc_val << 1) ^ 0x31);
+      } else {
+        crc_val = (crc_val << 1);
+      }
+      cur_val = cur_val << 1;
+    }
+  }
+  return crc_val;
+}
+}  // namespace hte501
+}  // namespace esphome
diff --git a/esphome/components/hte501/hte501.h b/esphome/components/hte501/hte501.h
new file mode 100644
index 0000000000..0d2c952e81
--- /dev/null
+++ b/esphome/components/hte501/hte501.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/i2c/i2c.h"
+
+namespace esphome {
+namespace hte501 {
+
+/// This class implements support for the hte501 of temperature i2c sensors.
+class HTE501Component : public PollingComponent, public i2c::I2CDevice {
+ public:
+  void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
+  void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
+
+  float get_setup_priority() const override;
+  void setup() override;
+  void dump_config() override;
+  void update() override;
+
+ protected:
+  unsigned char calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to);
+  sensor::Sensor *temperature_sensor_;
+  sensor::Sensor *humidity_sensor_;
+
+  enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE};
+};
+
+}  // namespace hte501
+}  // namespace esphome
diff --git a/esphome/components/hte501/sensor.py b/esphome/components/hte501/sensor.py
new file mode 100644
index 0000000000..8bd6160038
--- /dev/null
+++ b/esphome/components/hte501/sensor.py
@@ -0,0 +1,58 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import i2c, sensor
+from esphome.const import (
+    CONF_ID,
+    CONF_HUMIDITY,
+    CONF_TEMPERATURE,
+    DEVICE_CLASS_TEMPERATURE,
+    DEVICE_CLASS_HUMIDITY,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+    UNIT_PERCENT,
+)
+
+CODEOWNERS = ["@Stock-M"]
+
+DEPENDENCIES = ["i2c"]
+
+hte501_ns = cg.esphome_ns.namespace("hte501")
+HTE501Component = hte501_ns.class_(
+    "HTE501Component", cg.PollingComponent, i2c.I2CDevice
+)
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(HTE501Component),
+            cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_CELSIUS,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_TEMPERATURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Required(CONF_HUMIDITY): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_HUMIDITY,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+        }
+    )
+    .extend(cv.polling_component_schema("60s"))
+    .extend(i2c.i2c_device_schema(0x40))
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
+
+    if CONF_TEMPERATURE in config:
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
+        cg.add(var.set_temperature_sensor(sens))
+
+    if CONF_HUMIDITY in config:
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
+        cg.add(var.set_humidity_sensor(sens))
diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py
index c8c0ca5369..0c3e249512 100644
--- a/esphome/components/http_request/__init__.py
+++ b/esphome/components/http_request/__init__.py
@@ -195,6 +195,8 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
     for conf in config.get(CONF_ON_RESPONSE, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
         cg.add(var.register_response_trigger(trigger))
-        await automation.build_automation(trigger, [(int, "status_code")], conf)
+        await automation.build_automation(
+            trigger, [(int, "status_code"), (cg.uint32, "duration_ms")], conf
+        )
 
     return var
diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp
index 4e1cfe94b3..46894a9afd 100644
--- a/esphome/components/http_request/http_request.cpp
+++ b/esphome/components/http_request/http_request.cpp
@@ -66,6 +66,9 @@ void HttpRequestComponent::send(const std::vector<HttpRequestResponseTrigger *>
   }
 
   this->client_.setTimeout(this->timeout_);
+#if defined(USE_ESP32)
+  this->client_.setConnectTimeout(this->timeout_);
+#endif
   if (this->useragent_ != nullptr) {
     this->client_.setUserAgent(this->useragent_);
   }
@@ -73,25 +76,27 @@ void HttpRequestComponent::send(const std::vector<HttpRequestResponseTrigger *>
     this->client_.addHeader(header.name, header.value, false, true);
   }
 
+  uint32_t start_time = millis();
   int http_code = this->client_.sendRequest(this->method_, this->body_.c_str());
+  uint32_t duration = millis() - start_time;
   for (auto *trigger : response_triggers)
-    trigger->process(http_code);
+    trigger->process(http_code, duration);
 
   if (http_code < 0) {
-    ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", this->url_.c_str(),
-             HTTPClient::errorToString(http_code).c_str());
+    ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s; Duration: %u ms", this->url_.c_str(),
+             HTTPClient::errorToString(http_code).c_str(), duration);
     this->status_set_warning();
     return;
   }
 
   if (http_code < 200 || http_code >= 300) {
-    ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Code: %d", this->url_.c_str(), http_code);
+    ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Code: %d; Duration: %u ms", this->url_.c_str(), http_code, duration);
     this->status_set_warning();
     return;
   }
 
   this->status_clear_warning();
-  ESP_LOGD(TAG, "HTTP Request completed; URL: %s; Code: %d", this->url_.c_str(), http_code);
+  ESP_LOGD(TAG, "HTTP Request completed; URL: %s; Code: %d; Duration: %u ms", this->url_.c_str(), http_code, duration);
 }
 
 #ifdef USE_ESP8266
diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h
index eab3045fdc..0958c07683 100644
--- a/esphome/components/http_request/http_request.h
+++ b/esphome/components/http_request/http_request.h
@@ -31,7 +31,10 @@ struct Header {
   const char *value;
 };
 
-class HttpRequestResponseTrigger;
+class HttpRequestResponseTrigger : public Trigger<int32_t, uint32_t> {
+ public:
+  void process(int32_t status_code, uint32_t duration_ms) { this->trigger(status_code, duration_ms); }
+};
 
 class HttpRequestComponent : public Component {
  public:
@@ -138,11 +141,6 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
   std::vector<HttpRequestResponseTrigger *> response_triggers_;
 };
 
-class HttpRequestResponseTrigger : public Trigger<int> {
- public:
-  void process(int status_code) { this->trigger(status_code); }
-};
-
 }  // namespace http_request
 }  // namespace esphome
 
diff --git a/esphome/components/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py
index 730499a493..c2dbbd6737 100644
--- a/esphome/components/hydreon_rgxx/sensor.py
+++ b/esphome/components/hydreon_rgxx/sensor.py
@@ -6,8 +6,10 @@ from esphome.const import (
     CONF_MODEL,
     CONF_MOISTURE,
     CONF_TEMPERATURE,
-    DEVICE_CLASS_HUMIDITY,
+    DEVICE_CLASS_PRECIPITATION_INTENSITY,
+    DEVICE_CLASS_PRECIPITATION,
     STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_TOTAL_INCREASING,
     UNIT_CELSIUS,
     ICON_THERMOMETER,
 )
@@ -70,31 +72,31 @@ CONFIG_SCHEMA = cv.All(
             cv.Optional(CONF_ACC): sensor.sensor_schema(
                 unit_of_measurement=UNIT_MILLIMETERS,
                 accuracy_decimals=2,
-                device_class=DEVICE_CLASS_HUMIDITY,
+                device_class=DEVICE_CLASS_PRECIPITATION,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_EVENT_ACC): sensor.sensor_schema(
                 unit_of_measurement=UNIT_MILLIMETERS,
                 accuracy_decimals=2,
-                device_class=DEVICE_CLASS_HUMIDITY,
+                device_class=DEVICE_CLASS_PRECIPITATION,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_TOTAL_ACC): sensor.sensor_schema(
                 unit_of_measurement=UNIT_MILLIMETERS,
                 accuracy_decimals=2,
-                device_class=DEVICE_CLASS_HUMIDITY,
-                state_class=STATE_CLASS_MEASUREMENT,
+                device_class=DEVICE_CLASS_PRECIPITATION,
+                state_class=STATE_CLASS_TOTAL_INCREASING,
             ),
             cv.Optional(CONF_R_INT): sensor.sensor_schema(
                 unit_of_measurement=UNIT_MILLIMETERS_PER_HOUR,
                 accuracy_decimals=2,
-                device_class=DEVICE_CLASS_HUMIDITY,
+                device_class=DEVICE_CLASS_PRECIPITATION_INTENSITY,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_MOISTURE): sensor.sensor_schema(
                 unit_of_measurement=UNIT_INTENSITY,
                 accuracy_decimals=0,
-                device_class=DEVICE_CLASS_HUMIDITY,
+                device_class=DEVICE_CLASS_PRECIPITATION_INTENSITY,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
diff --git a/esphome/components/ili9341/display.py b/esphome/components/ili9341/display.py
index 3122b45bbe..0b87a0c4eb 100644
--- a/esphome/components/ili9341/display.py
+++ b/esphome/components/ili9341/display.py
@@ -1,6 +1,6 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
-from esphome import pins
+from esphome import core, pins
 from esphome.components import display, spi
 from esphome.const import (
     CONF_COLOR_PALETTE,
@@ -12,10 +12,11 @@ from esphome.const import (
     CONF_RAW_DATA_ID,
     CONF_RESET_PIN,
 )
-from esphome.core import HexInt
+from esphome.core import CORE, HexInt
 
 DEPENDENCIES = ["spi"]
 
+CONF_COLOR_PALETTE_IMAGES = "color_palette_images"
 CONF_LED_PIN = "led_pin"
 
 ili9341_ns = cg.esphome_ns.namespace("ili9341")
@@ -37,7 +38,25 @@ MODELS = {
 
 ILI9341_MODEL = cv.enum(MODELS, upper=True, space="_")
 
-COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE")
+COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE")
+
+
+def _validate(config):
+    if config.get(CONF_COLOR_PALETTE) == "IMAGE_ADAPTIVE" and not config.get(
+        CONF_COLOR_PALETTE_IMAGES
+    ):
+        raise cv.Invalid(
+            "Color palette in IMAGE_ADAPTIVE mode requires at least one 'color_palette_images' entry to generate palette"
+        )
+    if (
+        config.get(CONF_COLOR_PALETTE_IMAGES)
+        and config.get(CONF_COLOR_PALETTE) != "IMAGE_ADAPTIVE"
+    ):
+        raise cv.Invalid(
+            "Providing color palette images requires palette mode to be 'IMAGE_ADAPTIVE'"
+        )
+    return config
+
 
 CONFIG_SCHEMA = cv.All(
     display.FULL_DISPLAY_SCHEMA.extend(
@@ -48,12 +67,16 @@ CONFIG_SCHEMA = cv.All(
             cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
             cv.Optional(CONF_LED_PIN): pins.gpio_output_pin_schema,
             cv.Optional(CONF_COLOR_PALETTE, default="NONE"): COLOR_PALETTE,
+            cv.Optional(CONF_COLOR_PALETTE_IMAGES, default=[]): cv.ensure_list(
+                cv.file_
+            ),
             cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
         }
     )
     .extend(cv.polling_component_schema("1s"))
     .extend(spi.spi_device_schema(False)),
     cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
+    _validate,
 )
 
 
@@ -86,12 +109,45 @@ async def to_code(config):
         led_pin = await cg.gpio_pin_expression(config[CONF_LED_PIN])
         cg.add(var.set_led_pin(led_pin))
 
+    rhs = None
     if config[CONF_COLOR_PALETTE] == "GRAYSCALE":
         cg.add(var.set_buffer_color_mode(ILI9341ColorMode.BITS_8_INDEXED))
         rhs = []
         for x in range(256):
             rhs.extend([HexInt(x), HexInt(x), HexInt(x)])
+    elif config[CONF_COLOR_PALETTE] == "IMAGE_ADAPTIVE":
+        cg.add(var.set_buffer_color_mode(ILI9341ColorMode.BITS_8_INDEXED))
+        from PIL import Image
+
+        def load_image(filename):
+            path = CORE.relative_config_path(filename)
+            try:
+                return Image.open(path)
+            except Exception as e:
+                raise core.EsphomeError(f"Could not load image file {path}: {e}")
+
+        # make a wide horizontal combined image.
+        images = [load_image(x) for x in config[CONF_COLOR_PALETTE_IMAGES]]
+        total_width = sum(i.width for i in images)
+        max_height = max(i.height for i in images)
+
+        ref_image = Image.new("RGB", (total_width, max_height))
+        x = 0
+        for i in images:
+            ref_image.paste(i, (x, 0))
+            x = x + i.width
+
+        # reduce the colors on combined image to 256.
+        converted = ref_image.convert("P", palette=Image.ADAPTIVE, colors=256)
+        # if you want to verify how the images look use
+        # ref_image.save("ref_in.png")
+        # converted.save("ref_out.png")
+        palette = converted.getpalette()
+        assert len(palette) == 256 * 3
+        rhs = palette
+    else:
+        cg.add(var.set_buffer_color_mode(ILI9341ColorMode.BITS_8))
+
+    if rhs is not None:
         prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
         cg.add(var.set_palette(prog_arr))
-    else:
-        pass
diff --git a/esphome/components/ili9341/ili9341_display.cpp b/esphome/components/ili9341/ili9341_display.cpp
index 117de3de89..9f9edcf21f 100644
--- a/esphome/components/ili9341/ili9341_display.cpp
+++ b/esphome/components/ili9341/ili9341_display.cpp
@@ -122,7 +122,12 @@ void ILI9341Display::display_() {
 }
 
 void ILI9341Display::fill(Color color) {
-  uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
+  uint8_t color332 = 0;
+  if (this->buffer_color_mode_ == BITS_8) {
+    color332 = display::ColorUtil::color_to_332(color);
+  } else {  // if (this->buffer_color_mode_ == BITS_8_INDEXED)
+    color332 = display::ColorUtil::color_to_index8_palette888(color, this->palette_);
+  }
   memset(this->buffer_, color332, this->get_buffer_length_());
   this->x_low_ = 0;
   this->y_low_ = 0;
diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py
index 0004391f20..88c625961b 100644
--- a/esphome/components/image/__init__.py
+++ b/esphome/components/image/__init__.py
@@ -26,6 +26,7 @@ IMAGE_TYPE = {
     "RGB24": ImageType.IMAGE_TYPE_RGB24,
     "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY,
     "RGB565": ImageType.IMAGE_TYPE_RGB565,
+    "TRANSPARENT_IMAGE": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY,
 }
 
 Image_ = display.display_ns.class_("Image")
@@ -105,7 +106,7 @@ async def to_code(config):
             data[pos] = rgb & 255
             pos += 1
 
-    elif config[CONF_TYPE] == "BINARY":
+    elif (config[CONF_TYPE] == "BINARY") or (config[CONF_TYPE] == "TRANSPARENT_BINARY"):
         image = image.convert("1", dither=dither)
         width8 = ((width + 7) // 8) * 8
         data = [0 for _ in range(height * width8 // 8)]
@@ -116,7 +117,7 @@ async def to_code(config):
                 pos = x + y * width8
                 data[pos // 8] |= 0x80 >> (pos % 8)
 
-    elif config[CONF_TYPE] == "TRANSPARENT_BINARY":
+    elif config[CONF_TYPE] == "TRANSPARENT_IMAGE":
         image = image.convert("RGBA")
         width8 = ((width + 7) // 8) * 8
         data = [0 for _ in range(height * width8 // 8)]
diff --git a/esphome/components/improv_base/__init__.py b/esphome/components/improv_base/__init__.py
new file mode 100644
index 0000000000..5c2853a5c6
--- /dev/null
+++ b/esphome/components/improv_base/__init__.py
@@ -0,0 +1,42 @@
+import re
+
+import esphome.config_validation as cv
+import esphome.codegen as cg
+
+from esphome.const import __version__
+
+CODEOWNERS = ["@esphome/core"]
+
+CONF_NEXT_URL = "next_url"
+
+VALID_SUBSTITUTIONS = ["esphome_version", "ip_address", "device_name"]
+
+
+def validate_next_url(value):
+    value = cv.url(value)
+    test = r"{{(?!" + r"\b|".join(VALID_SUBSTITUTIONS) + r"\b)(\w+)}}"
+    result = re.search(test, value)
+    if result:
+        raise cv.Invalid(
+            f"Invalid substitution(s) ({', '.join(result.groups())}) in next_url. Valid substitutions are: {', '.join(VALID_SUBSTITUTIONS)}"
+        )
+    return value
+
+
+IMPROV_SCHEMA = cv.Schema(
+    {
+        cv.Optional(CONF_NEXT_URL): validate_next_url,
+    }
+)
+
+
+def _process_next_url(url: str):
+    if "{{esphome_version}}" in url:
+        url = url.replace("{{esphome_version}}", __version__)
+    return url
+
+
+async def setup_improv_core(var, config):
+    if CONF_NEXT_URL in config:
+        cg.add(var.set_next_url(_process_next_url(config[CONF_NEXT_URL])))
+    cg.add_library("esphome/Improv", "1.2.3")
diff --git a/esphome/components/improv_base/improv_base.cpp b/esphome/components/improv_base/improv_base.cpp
new file mode 100644
index 0000000000..c95b3a77ec
--- /dev/null
+++ b/esphome/components/improv_base/improv_base.cpp
@@ -0,0 +1,32 @@
+#include "improv_base.h"
+
+#include "esphome/components/network/util.h"
+#include "esphome/core/application.h"
+
+namespace esphome {
+namespace improv_base {
+
+std::string ImprovBase::get_formatted_next_url_() {
+  if (this->next_url_.empty()) {
+    return "";
+  }
+  std::string copy = this->next_url_;
+  // Device name
+  std::size_t pos = this->next_url_.find("{{device_name}}");
+  if (pos != std::string::npos) {
+    const std::string &device_name = App.get_name();
+    copy.replace(pos, 15, device_name);
+  }
+
+  // Ip address
+  pos = this->next_url_.find("{{ip_address}}");
+  if (pos != std::string::npos) {
+    std::string ip = network::IPAddress(network::get_ip_address()).str();
+    copy.replace(pos, 14, ip);
+  }
+
+  return copy;
+}
+
+}  // namespace improv_base
+}  // namespace esphome
diff --git a/esphome/components/improv_base/improv_base.h b/esphome/components/improv_base/improv_base.h
new file mode 100644
index 0000000000..90cd02a4ab
--- /dev/null
+++ b/esphome/components/improv_base/improv_base.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <string>
+
+namespace esphome {
+namespace improv_base {
+
+class ImprovBase {
+ public:
+  void set_next_url(const std::string &next_url) { this->next_url_ = next_url; }
+
+ protected:
+  std::string get_formatted_next_url_();
+  std::string next_url_;
+};
+
+}  // namespace improv_base
+}  // namespace esphome
diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py
index 4a6da6bee0..311256804b 100644
--- a/esphome/components/improv_serial/__init__.py
+++ b/esphome/components/improv_serial/__init__.py
@@ -4,7 +4,9 @@ import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.core import CORE
 import esphome.final_validate as fv
+from esphome.components import improv_base
 
+AUTO_LOAD = ["improv_base"]
 CODEOWNERS = ["@esphome/core"]
 DEPENDENCIES = ["logger", "wifi"]
 
@@ -12,11 +14,15 @@ improv_serial_ns = cg.esphome_ns.namespace("improv_serial")
 
 ImprovSerialComponent = improv_serial_ns.class_("ImprovSerialComponent", cg.Component)
 
-CONFIG_SCHEMA = cv.Schema(
-    {
-        cv.GenerateID(): cv.declare_id(ImprovSerialComponent),
-    }
-).extend(cv.COMPONENT_SCHEMA)
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(ImprovSerialComponent),
+        }
+    )
+    .extend(improv_base.IMPROV_SCHEMA)
+    .extend(cv.COMPONENT_SCHEMA)
+)
 
 
 def validate_logger(config):
@@ -37,4 +43,4 @@ FINAL_VALIDATE_SCHEMA = validate_logger
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     await cg.register_component(var, config)
-    cg.add_library("esphome/Improv", "1.2.3")
+    await improv_base.setup_improv_core(var, config)
diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp
index 0dab71060c..fe19e2f085 100644
--- a/esphome/components/improv_serial/improv_serial_component.cpp
+++ b/esphome/components/improv_serial/improv_serial_component.cpp
@@ -95,6 +95,9 @@ void ImprovSerialComponent::loop() {
 
 std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) {
   std::vector<std::string> urls;
+  if (!this->next_url_.empty()) {
+    urls.push_back(this->get_formatted_next_url_());
+  }
 #ifdef USE_WEBSERVER
   auto ip = wifi::global_wifi_component->wifi_sta_ip();
   std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h
index 3d8478252d..731f9f9984 100644
--- a/esphome/components/improv_serial/improv_serial_component.h
+++ b/esphome/components/improv_serial/improv_serial_component.h
@@ -1,5 +1,6 @@
 #pragma once
 
+#include "esphome/components/improv_base/improv_base.h"
 #include "esphome/components/wifi/wifi_component.h"
 #include "esphome/core/component.h"
 #include "esphome/core/defines.h"
@@ -27,7 +28,7 @@ enum ImprovSerialType : uint8_t {
 
 static const uint8_t IMPROV_SERIAL_VERSION = 1;
 
-class ImprovSerialComponent : public Component {
+class ImprovSerialComponent : public Component, public improv_base::ImprovBase {
  public:
   void setup() override;
   void loop() override;
diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp
index 7e701af48b..f105280b23 100644
--- a/esphome/components/json/json_util.cpp
+++ b/esphome/components/json/json_util.cpp
@@ -7,6 +7,9 @@
 #ifdef USE_ESP32
 #include <esp_heap_caps.h>
 #endif
+#ifdef USE_RP2040
+#include <Arduino.h>
+#endif
 
 namespace esphome {
 namespace json {
@@ -24,6 +27,8 @@ std::string build_json(const json_build_t &f) {
   const size_t free_heap = ESP.getMaxFreeBlockSize();  // NOLINT(readability-static-accessed-through-instance)
 #elif defined(USE_ESP32)
   const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
+#elif defined(USE_RP2040)
+  const size_t free_heap = rp2040.getFreeHeap();
 #endif
 
   size_t request_size = std::min(free_heap, (size_t) 512);
@@ -64,6 +69,8 @@ void parse_json(const std::string &data, const json_parse_t &f) {
   const size_t free_heap = ESP.getMaxFreeBlockSize();  // NOLINT(readability-static-accessed-through-instance)
 #elif defined(USE_ESP32)
   const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
+#elif defined(USE_RP2040)
+  const size_t free_heap = rp2040.getFreeHeap();
 #endif
   bool pass = false;
   size_t request_size = std::min(free_heap, (size_t)(data.size() * 1.5));
diff --git a/esphome/components/key_collector/__init__.py b/esphome/components/key_collector/__init__.py
new file mode 100644
index 0000000000..2099e28109
--- /dev/null
+++ b/esphome/components/key_collector/__init__.py
@@ -0,0 +1,95 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import automation
+from esphome.components import key_provider
+from esphome.const import (
+    CONF_ID,
+    CONF_MAX_LENGTH,
+    CONF_MIN_LENGTH,
+    CONF_ON_TIMEOUT,
+    CONF_SOURCE_ID,
+    CONF_TIMEOUT,
+)
+
+CODEOWNERS = ["@ssieb"]
+
+MULTI_CONF = True
+
+AUTO_LOAD = ["key_provider"]
+
+CONF_START_KEYS = "start_keys"
+CONF_END_KEYS = "end_keys"
+CONF_END_KEY_REQUIRED = "end_key_required"
+CONF_BACK_KEYS = "back_keys"
+CONF_CLEAR_KEYS = "clear_keys"
+CONF_ALLOWED_KEYS = "allowed_keys"
+CONF_ON_PROGRESS = "on_progress"
+CONF_ON_RESULT = "on_result"
+
+key_collector_ns = cg.esphome_ns.namespace("key_collector")
+KeyCollector = key_collector_ns.class_("KeyCollector", cg.Component)
+
+CONFIG_SCHEMA = cv.All(
+    cv.COMPONENT_SCHEMA.extend(
+        {
+            cv.GenerateID(): cv.declare_id(KeyCollector),
+            cv.GenerateID(CONF_SOURCE_ID): cv.use_id(key_provider.KeyProvider),
+            cv.Optional(CONF_MIN_LENGTH): cv.int_,
+            cv.Optional(CONF_MAX_LENGTH): cv.int_,
+            cv.Optional(CONF_START_KEYS): cv.string,
+            cv.Optional(CONF_END_KEYS): cv.string,
+            cv.Optional(CONF_END_KEY_REQUIRED): cv.boolean,
+            cv.Optional(CONF_BACK_KEYS): cv.string,
+            cv.Optional(CONF_CLEAR_KEYS): cv.string,
+            cv.Optional(CONF_ALLOWED_KEYS): cv.string,
+            cv.Optional(CONF_ON_PROGRESS): automation.validate_automation(single=True),
+            cv.Optional(CONF_ON_RESULT): automation.validate_automation(single=True),
+            cv.Optional(CONF_ON_TIMEOUT): automation.validate_automation(single=True),
+            cv.Optional(CONF_TIMEOUT): cv.positive_time_period_milliseconds,
+        }
+    ),
+    cv.has_at_least_one_key(CONF_END_KEYS, CONF_MAX_LENGTH),
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    source = await cg.get_variable(config[CONF_SOURCE_ID])
+    cg.add(var.set_provider(source))
+    if CONF_MIN_LENGTH in config:
+        cg.add(var.set_min_length(config[CONF_MIN_LENGTH]))
+    if CONF_MAX_LENGTH in config:
+        cg.add(var.set_max_length(config[CONF_MAX_LENGTH]))
+    if CONF_START_KEYS in config:
+        cg.add(var.set_start_keys(config[CONF_START_KEYS]))
+    if CONF_END_KEYS in config:
+        cg.add(var.set_end_keys(config[CONF_END_KEYS]))
+    if CONF_END_KEY_REQUIRED in config:
+        cg.add(var.set_end_key_required(config[CONF_END_KEY_REQUIRED]))
+    if CONF_BACK_KEYS in config:
+        cg.add(var.set_back_keys(config[CONF_BACK_KEYS]))
+    if CONF_CLEAR_KEYS in config:
+        cg.add(var.set_clear_keys(config[CONF_CLEAR_KEYS]))
+    if CONF_ALLOWED_KEYS in config:
+        cg.add(var.set_allowed_keys(config[CONF_ALLOWED_KEYS]))
+    if CONF_ON_PROGRESS in config:
+        await automation.build_automation(
+            var.get_progress_trigger(),
+            [(cg.std_string, "x"), (cg.uint8, "start")],
+            config[CONF_ON_PROGRESS],
+        )
+    if CONF_ON_RESULT in config:
+        await automation.build_automation(
+            var.get_result_trigger(),
+            [(cg.std_string, "x"), (cg.uint8, "start"), (cg.uint8, "end")],
+            config[CONF_ON_RESULT],
+        )
+    if CONF_ON_TIMEOUT in config:
+        await automation.build_automation(
+            var.get_timeout_trigger(),
+            [(cg.std_string, "x"), (cg.uint8, "start")],
+            config[CONF_ON_TIMEOUT],
+        )
+    if CONF_TIMEOUT in config:
+        cg.add(var.set_timeout(config[CONF_TIMEOUT]))
diff --git a/esphome/components/key_collector/key_collector.cpp b/esphome/components/key_collector/key_collector.cpp
new file mode 100644
index 0000000000..a9213890ee
--- /dev/null
+++ b/esphome/components/key_collector/key_collector.cpp
@@ -0,0 +1,95 @@
+#include "key_collector.h"
+#include "esphome/core/hal.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace key_collector {
+
+static const char *const TAG = "key_collector";
+
+KeyCollector::KeyCollector()
+    : progress_trigger_(new Trigger<std::string, uint8_t>()),
+      result_trigger_(new Trigger<std::string, uint8_t, uint8_t>()),
+      timeout_trigger_(new Trigger<std::string, uint8_t>()) {}
+
+void KeyCollector::loop() {
+  if ((this->timeout_ == 0) || this->result_.empty() || (millis() - this->last_key_time_ < this->timeout_))
+    return;
+  this->timeout_trigger_->trigger(this->result_, this->start_key_);
+  this->clear();
+}
+
+void KeyCollector::dump_config() {
+  ESP_LOGCONFIG(TAG, "Key Collector:");
+  if (this->min_length_ > 0)
+    ESP_LOGCONFIG(TAG, "  min length: %d", this->min_length_);
+  if (this->max_length_ > 0)
+    ESP_LOGCONFIG(TAG, "  max length: %d", this->max_length_);
+  if (!this->back_keys_.empty())
+    ESP_LOGCONFIG(TAG, "  erase keys '%s'", this->back_keys_.c_str());
+  if (!this->clear_keys_.empty())
+    ESP_LOGCONFIG(TAG, "  clear keys '%s'", this->clear_keys_.c_str());
+  if (!this->start_keys_.empty())
+    ESP_LOGCONFIG(TAG, "  start keys '%s'", this->start_keys_.c_str());
+  if (!this->end_keys_.empty()) {
+    ESP_LOGCONFIG(TAG, "  end keys '%s'", this->end_keys_.c_str());
+    ESP_LOGCONFIG(TAG, "  end key is required: %s", ONOFF(this->end_key_required_));
+  }
+  if (!this->allowed_keys_.empty())
+    ESP_LOGCONFIG(TAG, "  allowed keys '%s'", this->allowed_keys_.c_str());
+  if (this->timeout_ > 0)
+    ESP_LOGCONFIG(TAG, "  entry timeout: %0.1f", this->timeout_ / 1000.0);
+}
+
+void KeyCollector::set_provider(key_provider::KeyProvider *provider) {
+  provider->add_on_key_callback([this](uint8_t key) { this->key_pressed_(key); });
+}
+
+void KeyCollector::clear(bool progress_update) {
+  this->result_.clear();
+  this->start_key_ = 0;
+  if (progress_update)
+    this->progress_trigger_->trigger(this->result_, 0);
+}
+
+void KeyCollector::key_pressed_(uint8_t key) {
+  this->last_key_time_ = millis();
+  if (!this->start_keys_.empty() && !this->start_key_) {
+    if (this->start_keys_.find(key) != std::string::npos) {
+      this->start_key_ = key;
+      this->progress_trigger_->trigger(this->result_, this->start_key_);
+    }
+    return;
+  }
+  if (this->back_keys_.find(key) != std::string::npos) {
+    if (!this->result_.empty()) {
+      this->result_.pop_back();
+      this->progress_trigger_->trigger(this->result_, this->start_key_);
+    }
+    return;
+  }
+  if (this->clear_keys_.find(key) != std::string::npos) {
+    if (!this->result_.empty())
+      this->clear();
+    return;
+  }
+  if (this->end_keys_.find(key) != std::string::npos) {
+    if ((this->min_length_ == 0) || (this->result_.size() >= this->min_length_)) {
+      this->result_trigger_->trigger(this->result_, this->start_key_, key);
+      this->clear();
+    }
+    return;
+  }
+  if (!this->allowed_keys_.empty() && (this->allowed_keys_.find(key) == std::string::npos))
+    return;
+  if ((this->max_length_ == 0) || (this->result_.size() < this->max_length_))
+    this->result_.push_back(key);
+  if ((this->max_length_ > 0) && (this->result_.size() == this->max_length_) && (!this->end_key_required_)) {
+    this->result_trigger_->trigger(this->result_, this->start_key_, 0);
+    this->clear(false);
+  }
+  this->progress_trigger_->trigger(this->result_, this->start_key_);
+}
+
+}  // namespace key_collector
+}  // namespace esphome
diff --git a/esphome/components/key_collector/key_collector.h b/esphome/components/key_collector/key_collector.h
new file mode 100644
index 0000000000..5e63397839
--- /dev/null
+++ b/esphome/components/key_collector/key_collector.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include <utility>
+#include "esphome/components/key_provider/key_provider.h"
+#include "esphome/core/automation.h"
+
+namespace esphome {
+namespace key_collector {
+
+class KeyCollector : public Component {
+ public:
+  KeyCollector();
+  void loop() override;
+  void dump_config() override;
+  void set_provider(key_provider::KeyProvider *provider);
+  void set_min_length(int min_length) { this->min_length_ = min_length; };
+  void set_max_length(int max_length) { this->max_length_ = max_length; };
+  void set_start_keys(std::string start_keys) { this->start_keys_ = std::move(start_keys); };
+  void set_end_keys(std::string end_keys) { this->end_keys_ = std::move(end_keys); };
+  void set_end_key_required(bool end_key_required) { this->end_key_required_ = end_key_required; };
+  void set_back_keys(std::string back_keys) { this->back_keys_ = std::move(back_keys); };
+  void set_clear_keys(std::string clear_keys) { this->clear_keys_ = std::move(clear_keys); };
+  void set_allowed_keys(std::string allowed_keys) { this->allowed_keys_ = std::move(allowed_keys); };
+  Trigger<std::string, uint8_t> *get_progress_trigger() const { return this->progress_trigger_; };
+  Trigger<std::string, uint8_t, uint8_t> *get_result_trigger() const { return this->result_trigger_; };
+  Trigger<std::string, uint8_t> *get_timeout_trigger() const { return this->timeout_trigger_; };
+  void set_timeout(int timeout) { this->timeout_ = timeout; };
+
+  void clear(bool progress_update = true);
+
+ protected:
+  void key_pressed_(uint8_t key);
+
+  int min_length_{0};
+  int max_length_{0};
+  std::string start_keys_;
+  std::string end_keys_;
+  bool end_key_required_{false};
+  std::string back_keys_;
+  std::string clear_keys_;
+  std::string allowed_keys_;
+  std::string result_;
+  uint8_t start_key_{0};
+  Trigger<std::string, uint8_t> *progress_trigger_;
+  Trigger<std::string, uint8_t, uint8_t> *result_trigger_;
+  Trigger<std::string, uint8_t> *timeout_trigger_;
+  uint32_t last_key_time_;
+  uint32_t timeout_{0};
+};
+
+}  // namespace key_collector
+}  // namespace esphome
diff --git a/esphome/components/key_provider/__init__.py b/esphome/components/key_provider/__init__.py
new file mode 100644
index 0000000000..f397382ff2
--- /dev/null
+++ b/esphome/components/key_provider/__init__.py
@@ -0,0 +1,6 @@
+import esphome.codegen as cg
+
+CODEOWNERS = ["@ssieb"]
+
+key_provider_ns = cg.esphome_ns.namespace("key_provider")
+KeyProvider = key_provider_ns.class_("KeyProvider")
diff --git a/esphome/components/key_provider/key_provider.cpp b/esphome/components/key_provider/key_provider.cpp
new file mode 100644
index 0000000000..5a0e24b13f
--- /dev/null
+++ b/esphome/components/key_provider/key_provider.cpp
@@ -0,0 +1,13 @@
+#include "key_provider.h"
+
+namespace esphome {
+namespace key_provider {
+
+void KeyProvider::add_on_key_callback(std::function<void(uint8_t)> &&callback) {
+  this->key_callback_.add(std::move(callback));
+}
+
+void KeyProvider::send_key_(uint8_t key) { this->key_callback_.call(key); }
+
+}  // namespace key_provider
+}  // namespace esphome
diff --git a/esphome/components/key_provider/key_provider.h b/esphome/components/key_provider/key_provider.h
new file mode 100644
index 0000000000..272d3eecad
--- /dev/null
+++ b/esphome/components/key_provider/key_provider.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "esphome/core/automation.h"
+#include "esphome/core/component.h"
+
+namespace esphome {
+namespace key_provider {
+
+/// interface for components that provide keypresses
+class KeyProvider {
+ public:
+  void add_on_key_callback(std::function<void(uint8_t)> &&callback);
+
+ protected:
+  void send_key_(uint8_t key);
+
+  CallbackManager<void(uint8_t)> key_callback_{};
+};
+
+}  // namespace key_provider
+}  // namespace esphome
diff --git a/esphome/components/ld2410/__init__.py b/esphome/components/ld2410/__init__.py
new file mode 100644
index 0000000000..be39cc2979
--- /dev/null
+++ b/esphome/components/ld2410/__init__.py
@@ -0,0 +1,158 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import uart
+from esphome.const import CONF_ID, CONF_TIMEOUT
+from esphome import automation
+from esphome.automation import maybe_simple_id
+
+DEPENDENCIES = ["uart"]
+CODEOWNERS = ["@sebcaps"]
+MULTI_CONF = True
+
+ld2410_ns = cg.esphome_ns.namespace("ld2410")
+LD2410Component = ld2410_ns.class_("LD2410Component", cg.Component, uart.UARTDevice)
+LD2410Restart = ld2410_ns.class_("LD2410Restart", automation.Action)
+CONF_LD2410_ID = "ld2410_id"
+CONF_MAX_MOVE_DISTANCE = "max_move_distance"
+CONF_MAX_STILL_DISTANCE = "max_still_distance"
+CONF_G0_MOVE_THRESHOLD = "g0_move_threshold"
+CONF_G0_STILL_THRESHOLD = "g0_still_threshold"
+CONF_G1_MOVE_THRESHOLD = "g1_move_threshold"
+CONF_G1_STILL_THRESHOLD = "g1_still_threshold"
+CONF_G2_MOVE_THRESHOLD = "g2_move_threshold"
+CONF_G2_STILL_THRESHOLD = "g2_still_threshold"
+CONF_G3_MOVE_THRESHOLD = "g3_move_threshold"
+CONF_G3_STILL_THRESHOLD = "g3_still_threshold"
+CONF_G4_MOVE_THRESHOLD = "g4_move_threshold"
+CONF_G4_STILL_THRESHOLD = "g4_still_threshold"
+CONF_G5_MOVE_THRESHOLD = "g5_move_threshold"
+CONF_G5_STILL_THRESHOLD = "g5_still_threshold"
+CONF_G6_MOVE_THRESHOLD = "g6_move_threshold"
+CONF_G6_STILL_THRESHOLD = "g6_still_threshold"
+CONF_G7_MOVE_THRESHOLD = "g7_move_threshold"
+CONF_G7_STILL_THRESHOLD = "g7_still_threshold"
+CONF_G8_MOVE_THRESHOLD = "g8_move_threshold"
+CONF_G8_STILL_THRESHOLD = "g8_still_threshold"
+
+DISTANCES = [0.75, 1.5, 2.25, 3, 3.75, 4.5, 5.25, 6]
+
+CONFIG_SCHEMA = cv.All(
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(LD2410Component),
+            cv.Optional(CONF_MAX_MOVE_DISTANCE, default="4.5m"): cv.All(
+                cv.distance, cv.one_of(*DISTANCES, float=True)
+            ),
+            cv.Optional(CONF_MAX_STILL_DISTANCE, default="4.5m"): cv.All(
+                cv.distance, cv.one_of(*DISTANCES, float=True)
+            ),
+            cv.Optional(CONF_TIMEOUT, default="5s"): cv.All(
+                cv.positive_time_period_seconds,
+                cv.Range(max=cv.TimePeriod(seconds=32767)),
+            ),
+            cv.Optional(CONF_G0_MOVE_THRESHOLD, default=50): cv.int_range(
+                min=0, max=100
+            ),
+            cv.Optional(CONF_G0_STILL_THRESHOLD, default=0): cv.int_range(
+                min=0, max=100
+            ),
+            cv.Optional(CONF_G1_MOVE_THRESHOLD, default=50): cv.int_range(
+                min=0, max=100
+            ),
+            cv.Optional(CONF_G1_STILL_THRESHOLD, default=0): cv.int_range(
+                min=0, max=100
+            ),
+            cv.Optional(CONF_G2_MOVE_THRESHOLD, default=40): cv.int_range(
+                min=0, max=100
+            ),
+            cv.Optional(CONF_G2_STILL_THRESHOLD, default=40): cv.int_range(
+                min=0, max=100
+            ),
+            cv.Optional(CONF_G3_MOVE_THRESHOLD, default=40): cv.int_range(
+                min=0, max=100
+            ),
+            cv.Optional(CONF_G3_STILL_THRESHOLD, default=40): cv.int_range(
+                min=0, max=100
+            ),
+            cv.Optional(CONF_G4_MOVE_THRESHOLD, default=40): cv.int_range(
+                min=0, max=100
+            ),
+            cv.Optional(CONF_G4_STILL_THRESHOLD, default=40): cv.int_range(
+                min=0, max=100
+            ),
+            cv.Optional(CONF_G5_MOVE_THRESHOLD, default=40): cv.int_range(
+                min=0, max=100
+            ),
+            cv.Optional(CONF_G5_STILL_THRESHOLD, default=40): cv.int_range(
+                min=0, max=100
+            ),
+            cv.Optional(CONF_G6_MOVE_THRESHOLD, default=30): cv.int_range(
+                min=0, max=100
+            ),
+            cv.Optional(CONF_G6_STILL_THRESHOLD, default=15): cv.int_range(
+                min=0, max=100
+            ),
+            cv.Optional(CONF_G7_MOVE_THRESHOLD, default=30): cv.int_range(
+                min=0, max=100
+            ),
+            cv.Optional(CONF_G7_STILL_THRESHOLD, default=15): cv.int_range(
+                min=0, max=100
+            ),
+            cv.Optional(CONF_G8_MOVE_THRESHOLD, default=30): cv.int_range(
+                min=0, max=100
+            ),
+            cv.Optional(CONF_G8_STILL_THRESHOLD, default=15): cv.int_range(
+                min=0, max=100
+            ),
+        }
+    )
+    .extend(uart.UART_DEVICE_SCHEMA)
+    .extend(cv.COMPONENT_SCHEMA)
+)
+
+FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
+    "ld2410",
+    baud_rate=256000,
+    require_tx=True,
+    require_rx=True,
+    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)
+    cg.add(var.set_timeout(config[CONF_TIMEOUT]))
+    cg.add(var.set_max_move_distance(int(config[CONF_MAX_MOVE_DISTANCE] / 0.75)))
+    cg.add(var.set_max_still_distance(int(config[CONF_MAX_STILL_DISTANCE] / 0.75)))
+    cg.add(
+        var.set_range_config(
+            config[CONF_G0_MOVE_THRESHOLD],
+            config[CONF_G0_STILL_THRESHOLD],
+            config[CONF_G1_MOVE_THRESHOLD],
+            config[CONF_G1_STILL_THRESHOLD],
+            config[CONF_G2_MOVE_THRESHOLD],
+            config[CONF_G2_STILL_THRESHOLD],
+            config[CONF_G3_MOVE_THRESHOLD],
+            config[CONF_G3_STILL_THRESHOLD],
+            config[CONF_G4_MOVE_THRESHOLD],
+            config[CONF_G4_STILL_THRESHOLD],
+            config[CONF_G5_MOVE_THRESHOLD],
+            config[CONF_G5_STILL_THRESHOLD],
+            config[CONF_G6_MOVE_THRESHOLD],
+            config[CONF_G6_STILL_THRESHOLD],
+            config[CONF_G7_MOVE_THRESHOLD],
+            config[CONF_G7_STILL_THRESHOLD],
+            config[CONF_G8_MOVE_THRESHOLD],
+            config[CONF_G8_STILL_THRESHOLD],
+        )
+    )
+
+
+CALIBRATION_ACTION_SCHEMA = maybe_simple_id(
+    {
+        cv.Required(CONF_ID): cv.use_id(LD2410Component),
+    }
+)
diff --git a/esphome/components/ld2410/binary_sensor.py b/esphome/components/ld2410/binary_sensor.py
new file mode 100644
index 0000000000..02f73d57b7
--- /dev/null
+++ b/esphome/components/ld2410/binary_sensor.py
@@ -0,0 +1,36 @@
+import esphome.codegen as cg
+from esphome.components import binary_sensor
+import esphome.config_validation as cv
+from esphome.const import DEVICE_CLASS_MOTION, DEVICE_CLASS_OCCUPANCY
+from . import CONF_LD2410_ID, LD2410Component
+
+DEPENDENCIES = ["ld2410"]
+CONF_HAS_TARGET = "has_target"
+CONF_HAS_MOVING_TARGET = "has_moving_target"
+CONF_HAS_STILL_TARGET = "has_still_target"
+
+CONFIG_SCHEMA = {
+    cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
+    cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema(
+        device_class=DEVICE_CLASS_OCCUPANCY
+    ),
+    cv.Optional(CONF_HAS_MOVING_TARGET): binary_sensor.binary_sensor_schema(
+        device_class=DEVICE_CLASS_MOTION
+    ),
+    cv.Optional(CONF_HAS_STILL_TARGET): binary_sensor.binary_sensor_schema(
+        device_class=DEVICE_CLASS_OCCUPANCY
+    ),
+}
+
+
+async def to_code(config):
+    ld2410_component = await cg.get_variable(config[CONF_LD2410_ID])
+    if CONF_HAS_TARGET in config:
+        sens = await binary_sensor.new_binary_sensor(config[CONF_HAS_TARGET])
+        cg.add(ld2410_component.set_target_sensor(sens))
+    if CONF_HAS_MOVING_TARGET in config:
+        sens = await binary_sensor.new_binary_sensor(config[CONF_HAS_MOVING_TARGET])
+        cg.add(ld2410_component.set_moving_target_sensor(sens))
+    if CONF_HAS_STILL_TARGET in config:
+        sens = await binary_sensor.new_binary_sensor(config[CONF_HAS_STILL_TARGET])
+        cg.add(ld2410_component.set_still_target_sensor(sens))
diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp
new file mode 100644
index 0000000000..8e67ba54d7
--- /dev/null
+++ b/esphome/components/ld2410/ld2410.cpp
@@ -0,0 +1,315 @@
+#include "ld2410.h"
+
+#define highbyte(val) (uint8_t)((val) >> 8)
+#define lowbyte(val) (uint8_t)((val) &0xff)
+
+namespace esphome {
+namespace ld2410 {
+
+static const char *const TAG = "ld2410";
+
+void LD2410Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "LD2410:");
+#ifdef USE_BINARY_SENSOR
+  LOG_BINARY_SENSOR("  ", "HasTargetSensor", this->target_binary_sensor_);
+  LOG_BINARY_SENSOR("  ", "MovingSensor", this->moving_binary_sensor_);
+  LOG_BINARY_SENSOR("  ", "StillSensor", this->still_binary_sensor_);
+#endif
+#ifdef USE_SENSOR
+  LOG_SENSOR("  ", "Moving Distance", this->moving_target_distance_sensor_);
+  LOG_SENSOR("  ", "Still Distance", this->still_target_distance_sensor_);
+  LOG_SENSOR("  ", "Moving Energy", this->moving_target_energy_sensor_);
+  LOG_SENSOR("  ", "Still Energy", this->still_target_energy_sensor_);
+  LOG_SENSOR("  ", "Detection Distance", this->detection_distance_sensor_);
+#endif
+  this->set_config_mode_(true);
+  this->get_version_();
+  this->set_config_mode_(false);
+  ESP_LOGCONFIG(TAG, "  Firmware Version : %u.%u.%u%u%u%u", this->version_[0], this->version_[1], this->version_[2],
+                this->version_[3], this->version_[4], this->version_[5]);
+}
+
+void LD2410Component::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up LD2410...");
+  this->set_config_mode_(true);
+  this->set_max_distances_timeout_(this->max_move_distance_, this->max_still_distance_, this->timeout_);
+  // Configure Gates sensitivity
+  this->set_gate_threshold_(0, this->rg0_move_threshold_, this->rg0_still_threshold_);
+  this->set_gate_threshold_(1, this->rg1_move_threshold_, this->rg1_still_threshold_);
+  this->set_gate_threshold_(2, this->rg2_move_threshold_, this->rg2_still_threshold_);
+  this->set_gate_threshold_(3, this->rg3_move_threshold_, this->rg3_still_threshold_);
+  this->set_gate_threshold_(4, this->rg4_move_threshold_, this->rg4_still_threshold_);
+  this->set_gate_threshold_(5, this->rg5_move_threshold_, this->rg5_still_threshold_);
+  this->set_gate_threshold_(6, this->rg6_move_threshold_, this->rg6_still_threshold_);
+  this->set_gate_threshold_(7, this->rg7_move_threshold_, this->rg7_still_threshold_);
+  this->set_gate_threshold_(8, this->rg8_move_threshold_, this->rg8_still_threshold_);
+  this->get_version_();
+  this->set_config_mode_(false);
+  ESP_LOGCONFIG(TAG, "Firmware Version : %u.%u.%u%u%u%u", this->version_[0], this->version_[1], this->version_[2],
+                this->version_[3], this->version_[4], this->version_[5]);
+  ESP_LOGCONFIG(TAG, "LD2410 setup complete.");
+}
+
+void LD2410Component::loop() {
+  const int max_line_length = 80;
+  static uint8_t buffer[max_line_length];
+
+  while (available()) {
+    this->readline_(read(), buffer, max_line_length);
+  }
+}
+
+void LD2410Component::send_command_(uint8_t command, uint8_t *command_value, int command_value_len) {
+  // lastCommandSuccess->publish_state(false);
+
+  // frame start bytes
+  this->write_array(CMD_FRAME_HEADER, 4);
+  // length bytes
+  int len = 2;
+  if (command_value != nullptr)
+    len += command_value_len;
+  this->write_byte(lowbyte(len));
+  this->write_byte(highbyte(len));
+
+  // command
+  this->write_byte(lowbyte(command));
+  this->write_byte(highbyte(command));
+
+  // command value bytes
+  if (command_value != nullptr) {
+    for (int i = 0; i < command_value_len; i++) {
+      this->write_byte(command_value[i]);
+    }
+  }
+  // frame end bytes
+  this->write_array(CMD_FRAME_END, 4);
+  // FIXME to remove
+  delay(50);  // NOLINT
+}
+
+void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) {
+  if (len < 12)
+    return;  // 4 frame start bytes + 2 length bytes + 1 data end byte + 1 crc byte + 4 frame end bytes
+  if (buffer[0] != 0xF4 || buffer[1] != 0xF3 || buffer[2] != 0xF2 || buffer[3] != 0xF1)  // check 4 frame start bytes
+    return;
+  if (buffer[7] != HEAD || buffer[len - 6] != END || buffer[len - 5] != CHECK)  // Check constant values
+    return;  // data head=0xAA, data end=0x55, crc=0x00
+
+    /*
+      Data Type: 6th
+      0x01: Engineering mode
+      0x02: Normal mode
+    */
+    // char data_type = buffer[DATA_TYPES];
+    /*
+      Target states: 9th
+      0x00 = No target
+      0x01 = Moving targets
+      0x02 = Still targets
+      0x03 = Moving+Still targets
+    */
+#ifdef USE_BINARY_SENSOR
+  char target_state = buffer[TARGET_STATES];
+  if (this->target_binary_sensor_ != nullptr) {
+    this->target_binary_sensor_->publish_state(target_state != 0x00);
+  }
+#endif
+
+  /*
+    Reduce data update rate to prevent home assistant database size grow fast
+  */
+  int32_t current_millis = millis();
+  if (current_millis - last_periodic_millis < 1000)
+    return;
+  last_periodic_millis = current_millis;
+
+#ifdef USE_BINARY_SENSOR
+  if (this->moving_binary_sensor_ != nullptr) {
+    this->moving_binary_sensor_->publish_state(CHECK_BIT(target_state, 0));
+  }
+  if (this->still_binary_sensor_ != nullptr) {
+    this->still_binary_sensor_->publish_state(CHECK_BIT(target_state, 1));
+  }
+#endif
+  /*
+    Moving target distance: 10~11th bytes
+    Moving target energy: 12th byte
+    Still target distance: 13~14th bytes
+    Still target energy: 15th byte
+    Detect distance: 16~17th bytes
+  */
+#ifdef USE_SENSOR
+  if (this->moving_target_distance_sensor_ != nullptr) {
+    int new_moving_target_distance = this->two_byte_to_int_(buffer[MOVING_TARGET_LOW], buffer[MOVING_TARGET_HIGH]);
+    if (this->moving_target_distance_sensor_->get_state() != new_moving_target_distance)
+      this->moving_target_distance_sensor_->publish_state(new_moving_target_distance);
+  }
+  if (this->moving_target_energy_sensor_ != nullptr) {
+    int new_moving_target_energy = buffer[MOVING_ENERGY];
+    if (this->moving_target_energy_sensor_->get_state() != new_moving_target_energy)
+      this->moving_target_energy_sensor_->publish_state(new_moving_target_energy);
+  }
+  if (this->still_target_distance_sensor_ != nullptr) {
+    int new_still_target_distance = this->two_byte_to_int_(buffer[STILL_TARGET_LOW], buffer[STILL_TARGET_HIGH]);
+    if (this->still_target_distance_sensor_->get_state() != new_still_target_distance)
+      this->still_target_distance_sensor_->publish_state(new_still_target_distance);
+  }
+  if (this->still_target_energy_sensor_ != nullptr) {
+    int new_still_target_energy = buffer[STILL_ENERGY];
+    if (this->still_target_energy_sensor_->get_state() != new_still_target_energy)
+      this->still_target_energy_sensor_->publish_state(new_still_target_energy);
+  }
+  if (this->detection_distance_sensor_ != nullptr) {
+    int new_detect_distance = this->two_byte_to_int_(buffer[DETECT_DISTANCE_LOW], buffer[DETECT_DISTANCE_HIGH]);
+    if (this->detection_distance_sensor_->get_state() != new_detect_distance)
+      this->detection_distance_sensor_->publish_state(new_detect_distance);
+  }
+#endif
+}
+
+void LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
+  ESP_LOGV(TAG, "Handling ACK DATA for COMMAND");
+  if (len < 10) {
+    ESP_LOGE(TAG, "Error with last command : incorrect length");
+    return;
+  }
+  if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) {  // check 4 frame start bytes
+    ESP_LOGE(TAG, "Error with last command : incorrect Header");
+    return;
+  }
+  if (buffer[COMMAND_STATUS] != 0x01) {
+    ESP_LOGE(TAG, "Error with last command : status != 0x01");
+    return;
+  }
+  if (this->two_byte_to_int_(buffer[8], buffer[9]) != 0x00) {
+    ESP_LOGE(TAG, "Error with last command , last buffer was: %u , %u", buffer[8], buffer[9]);
+    return;
+  }
+
+  switch (buffer[COMMAND]) {
+    case lowbyte(CMD_ENABLE_CONF):
+      ESP_LOGV(TAG, "Handled Enable conf command");
+      break;
+    case lowbyte(CMD_DISABLE_CONF):
+      ESP_LOGV(TAG, "Handled Disabled conf command");
+      break;
+    case lowbyte(CMD_VERSION):
+      ESP_LOGV(TAG, "FW Version is: %u.%u.%u%u%u%u", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15],
+               buffer[14]);
+      this->version_[0] = buffer[13];
+      this->version_[1] = buffer[12];
+      this->version_[2] = buffer[17];
+      this->version_[3] = buffer[16];
+      this->version_[4] = buffer[15];
+      this->version_[5] = buffer[14];
+
+      break;
+    case lowbyte(CMD_GATE_SENS):
+      ESP_LOGV(TAG, "Handled sensitivity command");
+      break;
+    case lowbyte(CMD_QUERY):  // Query parameters response
+    {
+      if (buffer[10] != 0xAA)
+        return;  // value head=0xAA
+      /*
+        Moving distance range: 13th byte
+        Still distance range: 14th byte
+      */
+      // TODO
+      // maxMovingDistanceRange->publish_state(buffer[12]);
+      // maxStillDistanceRange->publish_state(buffer[13]);
+      /*
+        Moving Sensitivities: 15~23th bytes
+        Still Sensitivities: 24~32th bytes
+      */
+      for (int i = 0; i < 9; i++) {
+        moving_sensitivities[i] = buffer[14 + i];
+      }
+      for (int i = 0; i < 9; i++) {
+        still_sensitivities[i] = buffer[23 + i];
+      }
+      /*
+        None Duration: 33~34th bytes
+      */
+      // noneDuration->publish_state(this->two_byte_to_int_(buffer[32], buffer[33]));
+    } break;
+    default:
+      break;
+  }
+}
+
+void LD2410Component::readline_(int readch, uint8_t *buffer, int len) {
+  static int pos = 0;
+
+  if (readch >= 0) {
+    if (pos < len - 1) {
+      buffer[pos++] = readch;
+      buffer[pos] = 0;
+    } else {
+      pos = 0;
+    }
+    if (pos >= 4) {
+      if (buffer[pos - 4] == 0xF8 && buffer[pos - 3] == 0xF7 && buffer[pos - 2] == 0xF6 && buffer[pos - 1] == 0xF5) {
+        ESP_LOGV(TAG, "Will handle Periodic Data");
+        this->handle_periodic_data_(buffer, pos);
+        pos = 0;  // Reset position index ready for next time
+      } else if (buffer[pos - 4] == 0x04 && buffer[pos - 3] == 0x03 && buffer[pos - 2] == 0x02 &&
+                 buffer[pos - 1] == 0x01) {
+        ESP_LOGV(TAG, "Will handle ACK Data");
+        this->handle_ack_data_(buffer, pos);
+        pos = 0;  // Reset position index ready for next time
+      }
+    }
+  }
+}
+
+void LD2410Component::set_config_mode_(bool enable) {
+  uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
+  uint8_t cmd_value[2] = {0x01, 0x00};
+  this->send_command_(cmd, enable ? cmd_value : nullptr, 2);
+}
+
+void LD2410Component::query_parameters_() { this->send_command_(CMD_QUERY, nullptr, 0); }
+void LD2410Component::get_version_() { this->send_command_(CMD_VERSION, nullptr, 0); }
+
+void LD2410Component::set_max_distances_timeout_(uint8_t max_moving_distance_range, uint8_t max_still_distance_range,
+                                                 uint16_t timeout) {
+  uint8_t value[18] = {0x00,
+                       0x00,
+                       lowbyte(max_moving_distance_range),
+                       highbyte(max_moving_distance_range),
+                       0x00,
+                       0x00,
+                       0x01,
+                       0x00,
+                       lowbyte(max_still_distance_range),
+                       highbyte(max_still_distance_range),
+                       0x00,
+                       0x00,
+                       0x02,
+                       0x00,
+                       lowbyte(timeout),
+                       highbyte(timeout),
+                       0x00,
+                       0x00};
+  this->send_command_(CMD_MAXDIST_DURATION, value, 18);
+  this->query_parameters_();
+}
+void LD2410Component::set_gate_threshold_(uint8_t gate, uint8_t motionsens, uint8_t stillsens) {
+  // reference
+  // https://drive.google.com/drive/folders/1p4dhbEJA3YubyIjIIC7wwVsSo8x29Fq-?spm=a2g0o.detail.1000023.17.93465697yFwVxH
+  //   Send data: configure the motion sensitivity of distance gate 3 to 40, and the static sensitivity of 40
+  // 00 00 (gate)
+  // 03 00 00 00 (gate number)
+  // 01 00 (motion sensitivity)
+  // 28 00 00 00 (value)
+  // 02 00 (still sensitivtiy)
+  // 28 00 00 00 (value)
+  uint8_t value[18] = {0x00, 0x00, lowbyte(gate),       highbyte(gate),       0x00, 0x00,
+                       0x01, 0x00, lowbyte(motionsens), highbyte(motionsens), 0x00, 0x00,
+                       0x02, 0x00, lowbyte(stillsens),  highbyte(stillsens),  0x00, 0x00};
+  this->send_command_(CMD_GATE_SENS, value, 18);
+}
+
+}  // namespace ld2410
+}  // namespace esphome
diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h
new file mode 100644
index 0000000000..5a35798bc2
--- /dev/null
+++ b/esphome/components/ld2410/ld2410.h
@@ -0,0 +1,146 @@
+#pragma once
+#include "esphome/core/defines.h"
+#include "esphome/core/component.h"
+#ifdef USE_BINARY_SENSOR
+#include "esphome/components/binary_sensor/binary_sensor.h"
+#endif
+#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"
+
+namespace esphome {
+namespace ld2410 {
+
+#define CHECK_BIT(var, pos) (((var) >> (pos)) & 1)
+
+// Commands
+static const uint8_t CMD_ENABLE_CONF = 0x00FF;
+static const uint8_t CMD_DISABLE_CONF = 0x00FE;
+static const uint8_t CMD_MAXDIST_DURATION = 0x0060;
+static const uint8_t CMD_QUERY = 0x0061;
+static const uint8_t CMD_GATE_SENS = 0x0064;
+static const uint8_t CMD_VERSION = 0x00A0;
+
+// Commands values
+static const uint8_t CMD_MAX_MOVE_VALUE = 0x0000;
+static const uint8_t CMD_MAX_STILL_VALUE = 0x0001;
+static const uint8_t CMD_DURATION_VALUE = 0x0002;
+// Command Header & Footer
+static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA};
+static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01};
+// Data Header & Footer
+static const uint8_t DATA_FRAME_HEADER[4] = {0xF4, 0xF3, 0xF2, 0xF1};
+static const uint8_t DATA_FRAME_END[4] = {0xF8, 0xF7, 0xF6, 0xF5};
+/*
+Data Type: 6th byte
+Target states: 9th byte
+    Moving target distance: 10~11th bytes
+    Moving target energy: 12th byte
+    Still target distance: 13~14th bytes
+    Still target energy: 15th byte
+    Detect distance: 16~17th bytes
+*/
+enum PeriodicDataStructure : uint8_t {
+  DATA_TYPES = 5,
+  TARGET_STATES = 8,
+  MOVING_TARGET_LOW = 9,
+  MOVING_TARGET_HIGH = 10,
+  MOVING_ENERGY = 11,
+  STILL_TARGET_LOW = 12,
+  STILL_TARGET_HIGH = 13,
+  STILL_ENERGY = 14,
+  DETECT_DISTANCE_LOW = 15,
+  DETECT_DISTANCE_HIGH = 16,
+};
+enum PeriodicDataValue : uint8_t { HEAD = 0XAA, END = 0x55, CHECK = 0x00 };
+
+enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 };
+
+//  char cmd[2] = {enable ? 0xFF : 0xFE, 0x00};
+class LD2410Component : public Component, public uart::UARTDevice {
+#ifdef USE_SENSOR
+  SUB_SENSOR(moving_target_distance)
+  SUB_SENSOR(still_target_distance)
+  SUB_SENSOR(moving_target_energy)
+  SUB_SENSOR(still_target_energy)
+  SUB_SENSOR(detection_distance)
+#endif
+
+ public:
+  void setup() override;
+  void dump_config() override;
+  void loop() override;
+
+#ifdef USE_BINARY_SENSOR
+  void set_target_sensor(binary_sensor::BinarySensor *sens) { this->target_binary_sensor_ = sens; };
+  void set_moving_target_sensor(binary_sensor::BinarySensor *sens) { this->moving_binary_sensor_ = sens; };
+  void set_still_target_sensor(binary_sensor::BinarySensor *sens) { this->still_binary_sensor_ = sens; };
+#endif
+
+  void set_timeout(uint16_t value) { this->timeout_ = value; };
+  void set_max_move_distance(uint8_t value) { this->max_move_distance_ = value; };
+  void set_max_still_distance(uint8_t value) { this->max_still_distance_ = value; };
+  void set_range_config(int rg0_move, int rg0_still, int rg1_move, int rg1_still, int rg2_move, int rg2_still,
+                        int rg3_move, int rg3_still, int rg4_move, int rg4_still, int rg5_move, int rg5_still,
+                        int rg6_move, int rg6_still, int rg7_move, int rg7_still, int rg8_move, int rg8_still) {
+    this->rg0_move_threshold_ = rg0_move;
+    this->rg0_still_threshold_ = rg0_still;
+    this->rg1_move_threshold_ = rg1_move;
+    this->rg1_still_threshold_ = rg1_still;
+    this->rg2_move_threshold_ = rg2_move;
+    this->rg2_still_threshold_ = rg2_still;
+    this->rg3_move_threshold_ = rg3_move;
+    this->rg3_still_threshold_ = rg3_still;
+    this->rg4_move_threshold_ = rg4_move;
+    this->rg4_still_threshold_ = rg4_still;
+    this->rg5_move_threshold_ = rg5_move;
+    this->rg5_still_threshold_ = rg5_still;
+    this->rg6_move_threshold_ = rg6_move;
+    this->rg6_still_threshold_ = rg6_still;
+    this->rg7_move_threshold_ = rg7_move;
+    this->rg7_still_threshold_ = rg7_still;
+    this->rg8_move_threshold_ = rg8_move;
+    this->rg8_still_threshold_ = rg8_still;
+  };
+  int moving_sensitivities[9] = {0};
+  int still_sensitivities[9] = {0};
+
+  int32_t last_periodic_millis = millis();
+
+ protected:
+#ifdef USE_BINARY_SENSOR
+  binary_sensor::BinarySensor *target_binary_sensor_{nullptr};
+  binary_sensor::BinarySensor *moving_binary_sensor_{nullptr};
+  binary_sensor::BinarySensor *still_binary_sensor_{nullptr};
+#endif
+
+  std::vector<uint8_t> rx_buffer_;
+  int two_byte_to_int_(char firstbyte, char secondbyte) { return (int16_t)(secondbyte << 8) + firstbyte; }
+  void send_command_(uint8_t command_str, uint8_t *command_value, int command_value_len);
+
+  void set_max_distances_timeout_(uint8_t max_moving_distance_range, uint8_t max_still_distance_range,
+                                  uint16_t timeout);
+  void set_gate_threshold_(uint8_t gate, uint8_t motionsens, uint8_t stillsens);
+  void set_config_mode_(bool enable);
+  void handle_periodic_data_(uint8_t *buffer, int len);
+  void handle_ack_data_(uint8_t *buffer, int len);
+  void readline_(int readch, uint8_t *buffer, int len);
+  void query_parameters_();
+  void get_version_();
+
+  uint16_t timeout_;
+  uint8_t max_move_distance_;
+  uint8_t max_still_distance_;
+
+  uint8_t version_[6];
+  uint8_t rg0_move_threshold_, rg0_still_threshold_, rg1_move_threshold_, rg1_still_threshold_, rg2_move_threshold_,
+      rg2_still_threshold_, rg3_move_threshold_, rg3_still_threshold_, rg4_move_threshold_, rg4_still_threshold_,
+      rg5_move_threshold_, rg5_still_threshold_, rg6_move_threshold_, rg6_still_threshold_, rg7_move_threshold_,
+      rg7_still_threshold_, rg8_move_threshold_, rg8_still_threshold_;
+};
+
+}  // namespace ld2410
+}  // namespace esphome
diff --git a/esphome/components/ld2410/sensor.py b/esphome/components/ld2410/sensor.py
new file mode 100644
index 0000000000..b941263134
--- /dev/null
+++ b/esphome/components/ld2410/sensor.py
@@ -0,0 +1,55 @@
+import esphome.codegen as cg
+from esphome.components import sensor
+import esphome.config_validation as cv
+from esphome.const import (
+    DEVICE_CLASS_DISTANCE,
+    DEVICE_CLASS_ENERGY,
+    UNIT_CENTIMETER,
+    UNIT_PERCENT,
+)
+from . import CONF_LD2410_ID, LD2410Component
+
+DEPENDENCIES = ["ld2410"]
+CONF_MOVING_DISTANCE = "moving_distance"
+CONF_STILL_DISTANCE = "still_distance"
+CONF_MOVING_ENERGY = "moving_energy"
+CONF_STILL_ENERGY = "still_energy"
+CONF_DETECTION_DISTANCE = "detection_distance"
+
+CONFIG_SCHEMA = {
+    cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
+    cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema(
+        device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_CENTIMETER
+    ),
+    cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema(
+        device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_CENTIMETER
+    ),
+    cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema(
+        device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=UNIT_PERCENT
+    ),
+    cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
+        device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=UNIT_PERCENT
+    ),
+    cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema(
+        device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_CENTIMETER
+    ),
+}
+
+
+async def to_code(config):
+    ld2410_component = await cg.get_variable(config[CONF_LD2410_ID])
+    if CONF_MOVING_DISTANCE in config:
+        sens = await sensor.new_sensor(config[CONF_MOVING_DISTANCE])
+        cg.add(ld2410_component.set_moving_target_distance_sensor(sens))
+    if CONF_STILL_DISTANCE in config:
+        sens = await sensor.new_sensor(config[CONF_STILL_DISTANCE])
+        cg.add(ld2410_component.set_still_target_distance_sensor(sens))
+    if CONF_MOVING_ENERGY in config:
+        sens = await sensor.new_sensor(config[CONF_MOVING_ENERGY])
+        cg.add(ld2410_component.set_moving_target_energy_sensor(sens))
+    if CONF_STILL_ENERGY in config:
+        sens = await sensor.new_sensor(config[CONF_STILL_ENERGY])
+        cg.add(ld2410_component.set_still_target_energy_sensor(sens))
+    if CONF_DETECTION_DISTANCE in config:
+        sens = await sensor.new_sensor(config[CONF_DETECTION_DISTANCE])
+        cg.add(ld2410_component.set_detection_distance_sensor(sens))
diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp
index a56dccfd72..d6241dd5b0 100644
--- a/esphome/components/ledc/ledc_output.cpp
+++ b/esphome/components/ledc/ledc_output.cpp
@@ -6,17 +6,29 @@
 #ifdef USE_ARDUINO
 #include <esp32-hal-ledc.h>
 #endif
-#ifdef USE_ESP_IDF
 #include <driver/ledc.h>
+
+#define CLOCK_FREQUENCY 80e6f
+
+#ifdef USE_ARDUINO
+#ifdef SOC_LEDC_SUPPORT_XTAL_CLOCK
+#undef CLOCK_FREQUENCY
+// starting with ESP32 Arduino 2.0.2, the 40MHz crystal is used as clock by default if supported
+#define CLOCK_FREQUENCY 40e6f
 #endif
+#else
+#define DEFAULT_CLK LEDC_USE_APB_CLK
+#endif
+
+static const uint8_t SETUP_ATTEMPT_COUNT_MAX = 5;
 
 namespace esphome {
 namespace ledc {
 
 static const char *const TAG = "ledc.output";
 
-#ifdef USE_ESP_IDF
 static const int MAX_RES_BITS = LEDC_TIMER_BIT_MAX - 1;
+#ifdef USE_ESP_IDF
 #if SOC_LEDC_SUPPORT_HS_MODE
 // Only ESP32 has LEDC_HIGH_SPEED_MODE
 inline ledc_mode_t get_speed_mode(uint8_t channel) { return channel < 8 ? LEDC_HIGH_SPEED_MODE : LEDC_LOW_SPEED_MODE; }
@@ -26,15 +38,13 @@ inline ledc_mode_t get_speed_mode(uint8_t channel) { return channel < 8 ? LEDC_H
 // https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/api-reference/peripherals/ledc.html#functionality-overview
 inline ledc_mode_t get_speed_mode(uint8_t) { return LEDC_LOW_SPEED_MODE; }
 #endif
-#else
-static const int MAX_RES_BITS = 20;
 #endif
 
-float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) { return 80e6f / float(1 << bit_depth); }
+float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) { return CLOCK_FREQUENCY / float(1 << bit_depth); }
 
 float ledc_min_frequency_for_bit_depth(uint8_t bit_depth, bool low_frequency) {
   const float max_div_num = ((1 << MAX_RES_BITS) - 1) / (low_frequency ? 32.0f : 256.0f);
-  return 80e6f / (max_div_num * float(1 << bit_depth));
+  return CLOCK_FREQUENCY / (max_div_num * float(1 << bit_depth));
 }
 
 optional<uint8_t> ledc_bit_depth_for_frequency(float frequency) {
@@ -50,6 +60,38 @@ optional<uint8_t> ledc_bit_depth_for_frequency(float frequency) {
   return {};
 }
 
+#ifdef USE_ESP_IDF
+esp_err_t configure_timer_frequency(ledc_mode_t speed_mode, ledc_timer_t timer_num, ledc_channel_t chan_num,
+                                    uint8_t channel, uint8_t &bit_depth, float frequency) {
+  bit_depth = *ledc_bit_depth_for_frequency(frequency);
+  if (bit_depth < 1) {
+    ESP_LOGE(TAG, "Frequency %f can't be achieved with any bit depth", frequency);
+  }
+
+  ledc_timer_config_t timer_conf{};
+  timer_conf.speed_mode = speed_mode;
+  timer_conf.duty_resolution = static_cast<ledc_timer_bit_t>(bit_depth);
+  timer_conf.timer_num = timer_num;
+  timer_conf.freq_hz = (uint32_t) frequency;
+  timer_conf.clk_cfg = DEFAULT_CLK;
+
+  // Configure the time with fallback in case of error
+  int attempt_count_max = SETUP_ATTEMPT_COUNT_MAX;
+  esp_err_t init_result = ESP_FAIL;
+  while (attempt_count_max > 0 && init_result != ESP_OK) {
+    init_result = ledc_timer_config(&timer_conf);
+    if (init_result != ESP_OK) {
+      ESP_LOGW(TAG, "Unable to initialize timer with frequency %.1f and bit depth of %u", frequency, bit_depth);
+      // try again with a lower bit depth
+      timer_conf.duty_resolution = static_cast<ledc_timer_bit_t>(--bit_depth);
+    }
+    attempt_count_max--;
+  }
+
+  return init_result;
+}
+#endif
+
 void LEDCOutput::write_state(float state) {
   if (!initialized_) {
     ESP_LOGW(TAG, "LEDC output hasn't been initialized yet!");
@@ -65,6 +107,7 @@ void LEDCOutput::write_state(float state) {
   auto duty = static_cast<uint32_t>(duty_rounded);
 
 #ifdef USE_ARDUINO
+  ESP_LOGV(TAG, "Setting duty: %u on channel %u", duty, this->channel_);
   ledcWrite(this->channel_, duty);
 #endif
 #ifdef USE_ESP_IDF
@@ -76,6 +119,7 @@ void LEDCOutput::write_state(float state) {
 }
 
 void LEDCOutput::setup() {
+  ESP_LOGV(TAG, "Entering setup...");
 #ifdef USE_ARDUINO
   this->update_frequency(this->frequency_);
   this->turn_off();
@@ -87,19 +131,16 @@ void LEDCOutput::setup() {
   auto timer_num = static_cast<ledc_timer_t>((channel_ % 8) / 2);
   auto chan_num = static_cast<ledc_channel_t>(channel_ % 8);
 
-  bit_depth_ = *ledc_bit_depth_for_frequency(frequency_);
-  if (bit_depth_ < 1) {
-    ESP_LOGW(TAG, "Frequency %f can't be achieved with any bit depth", frequency_);
-    this->status_set_warning();
+  esp_err_t timer_init_result =
+      configure_timer_frequency(speed_mode, timer_num, chan_num, this->channel_, this->bit_depth_, this->frequency_);
+
+  if (timer_init_result != ESP_OK) {
+    ESP_LOGE(TAG, "Frequency %f can't be achieved with computed bit depth %u", this->frequency_, this->bit_depth_);
+    this->status_set_error();
+    return;
   }
 
-  ledc_timer_config_t timer_conf{};
-  timer_conf.speed_mode = speed_mode;
-  timer_conf.duty_resolution = static_cast<ledc_timer_bit_t>(bit_depth_);
-  timer_conf.timer_num = timer_num;
-  timer_conf.freq_hz = (uint32_t) frequency_;
-  timer_conf.clk_cfg = LEDC_AUTO_CLK;
-  ledc_timer_config(&timer_conf);
+  ESP_LOGV(TAG, "Configured frequency %f with a bit depth of %u bits", this->frequency_, this->bit_depth_);
 
   ledc_channel_config_t chan_conf{};
   chan_conf.gpio_num = pin_->get_pin();
@@ -111,6 +152,7 @@ void LEDCOutput::setup() {
   chan_conf.hpoint = 0;
   ledc_channel_config(&chan_conf);
   initialized_ = true;
+  this->status_clear_error();
 #endif
 }
 
@@ -118,36 +160,80 @@ void LEDCOutput::dump_config() {
   ESP_LOGCONFIG(TAG, "LEDC Output:");
   LOG_PIN("  Pin ", this->pin_);
   ESP_LOGCONFIG(TAG, "  LEDC Channel: %u", this->channel_);
-  ESP_LOGCONFIG(TAG, "  Frequency: %.1f Hz", this->frequency_);
+  ESP_LOGCONFIG(TAG, "  PWM Frequency: %.1f Hz", this->frequency_);
+  ESP_LOGCONFIG(TAG, "  Bit depth: %u", this->bit_depth_);
+  ESP_LOGV(TAG, "  Max frequency for bit depth: %f", ledc_max_frequency_for_bit_depth(this->bit_depth_));
+  ESP_LOGV(TAG, "  Min frequency for bit depth: %f",
+           ledc_min_frequency_for_bit_depth(this->bit_depth_, (this->frequency_ < 100)));
+  ESP_LOGV(TAG, "  Max frequency for bit depth-1: %f", ledc_max_frequency_for_bit_depth(this->bit_depth_ - 1));
+  ESP_LOGV(TAG, "  Min frequency for bit depth-1: %f",
+           ledc_min_frequency_for_bit_depth(this->bit_depth_ - 1, (this->frequency_ < 100)));
+  ESP_LOGV(TAG, "  Max frequency for bit depth+1: %f", ledc_max_frequency_for_bit_depth(this->bit_depth_ + 1));
+  ESP_LOGV(TAG, "  Min frequency for bit depth+1: %f",
+           ledc_min_frequency_for_bit_depth(this->bit_depth_ + 1, (this->frequency_ < 100)));
+  ESP_LOGV(TAG, "  Max res bits: %d", MAX_RES_BITS);
+  ESP_LOGV(TAG, "  Clock frequency: %f", CLOCK_FREQUENCY);
 }
 
 void LEDCOutput::update_frequency(float frequency) {
   auto bit_depth_opt = ledc_bit_depth_for_frequency(frequency);
   if (!bit_depth_opt.has_value()) {
-    ESP_LOGW(TAG, "Frequency %f can't be achieved with any bit depth", frequency);
-    this->status_set_warning();
+    ESP_LOGE(TAG, "Frequency %f can't be achieved with any bit depth", this->frequency_);
+    this->status_set_error();
   }
   this->bit_depth_ = bit_depth_opt.value_or(8);
   this->frequency_ = frequency;
 #ifdef USE_ARDUINO
-  ledcSetup(this->channel_, frequency, this->bit_depth_);
-  initialized_ = true;
+  ESP_LOGV(TAG, "Using Arduino API - Trying to define channel, frequency and bit depth...");
+  u_int32_t configured_frequency = 0;
+
+  // Configure LEDC channel, frequency and bit depth with fallback
+  int attempt_count_max = SETUP_ATTEMPT_COUNT_MAX;
+  while (attempt_count_max > 0 && configured_frequency == 0) {
+    ESP_LOGV(TAG, "Trying initialize channel %u with frequency %.1f and bit depth of %u...", this->channel_,
+             this->frequency_, this->bit_depth_);
+    configured_frequency = ledcSetup(this->channel_, frequency, this->bit_depth_);
+    if (configured_frequency != 0) {
+      initialized_ = true;
+      this->status_clear_error();
+      ESP_LOGV(TAG, "Configured frequency: %u with bit depth: %u", configured_frequency, this->bit_depth_);
+    } else {
+      ESP_LOGW(TAG, "Unable to initialize channel %u with frequency %.1f and bit depth of %u", this->channel_,
+               this->frequency_, this->bit_depth_);
+      // try again with a lower bit depth
+      this->bit_depth_--;
+    }
+    attempt_count_max--;
+  }
+
+  if (configured_frequency == 0) {
+    ESP_LOGE(TAG, "Permanently failed to initialize channel %u with frequency %.1f and bit depth of %u", this->channel_,
+             this->frequency_, this->bit_depth_);
+    this->status_set_error();
+    return;
+  }
+
 #endif  // USE_ARDUINO
 #ifdef USE_ESP_IDF
   if (!initialized_) {
     ESP_LOGW(TAG, "LEDC output hasn't been initialized yet!");
     return;
   }
+
   auto speed_mode = get_speed_mode(channel_);
   auto timer_num = static_cast<ledc_timer_t>((channel_ % 8) / 2);
+  auto chan_num = static_cast<ledc_channel_t>(channel_ % 8);
 
-  ledc_timer_config_t timer_conf{};
-  timer_conf.speed_mode = speed_mode;
-  timer_conf.duty_resolution = static_cast<ledc_timer_bit_t>(bit_depth_);
-  timer_conf.timer_num = timer_num;
-  timer_conf.freq_hz = (uint32_t) frequency_;
-  timer_conf.clk_cfg = LEDC_AUTO_CLK;
-  ledc_timer_config(&timer_conf);
+  esp_err_t timer_init_result =
+      configure_timer_frequency(speed_mode, timer_num, chan_num, this->channel_, this->bit_depth_, this->frequency_);
+
+  if (timer_init_result != ESP_OK) {
+    ESP_LOGE(TAG, "Frequency %f can't be achieved with computed bit depth %u", this->frequency_, this->bit_depth_);
+    this->status_set_error();
+    return;
+  }
+
+  this->status_clear_error();
 #endif
   // re-apply duty
   this->write_state(this->duty_);
diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp
index 00c03eb5f0..64c29a346b 100644
--- a/esphome/components/light/light_state.cpp
+++ b/esphome/components/light/light_state.cpp
@@ -276,7 +276,15 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot
 void LightState::save_remote_values_() {
   LightStateRTCState saved;
   saved.color_mode = this->remote_values.get_color_mode();
-  saved.state = this->remote_values.is_on();
+  switch (this->restore_mode_) {
+    case LIGHT_RESTORE_AND_OFF:
+    case LIGHT_RESTORE_AND_ON:
+      saved.state = (this->restore_mode_ == LIGHT_RESTORE_AND_ON);
+      break;
+    default:
+      saved.state = this->remote_values.is_on();
+      break;
+  }
   saved.brightness = this->remote_values.get_brightness();
   saved.color_brightness = this->remote_values.get_color_brightness();
   saved.red = this->remote_values.get_red();
diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py
new file mode 100644
index 0000000000..1c549007b9
--- /dev/null
+++ b/esphome/components/matrix_keypad/__init__.py
@@ -0,0 +1,71 @@
+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
+
+CODEOWNERS = ["@ssieb"]
+
+AUTO_LOAD = ["key_provider"]
+
+MULTI_CONF = True
+
+matrix_keypad_ns = cg.esphome_ns.namespace("matrix_keypad")
+MatrixKeypad = matrix_keypad_ns.class_(
+    "MatrixKeypad", key_provider.KeyProvider, cg.Component
+)
+
+CONF_KEYPAD_ID = "keypad_id"
+CONF_ROWS = "rows"
+CONF_COLUMNS = "columns"
+CONF_KEYS = "keys"
+CONF_DEBOUNCE_TIME = "debounce_time"
+CONF_HAS_DIODES = "has_diodes"
+
+
+def check_keys(obj):
+    if CONF_KEYS in obj:
+        if len(obj[CONF_KEYS]) != len(obj[CONF_ROWS]) * len(obj[CONF_COLUMNS]):
+            raise cv.Invalid("The number of key codes must equal the number of buttons")
+    return obj
+
+
+CONFIG_SCHEMA = cv.All(
+    cv.COMPONENT_SCHEMA.extend(
+        {
+            cv.GenerateID(): cv.declare_id(MatrixKeypad),
+            cv.Required(CONF_ROWS): cv.All(
+                cv.ensure_list({cv.Required(CONF_PIN): pins.gpio_output_pin_schema}),
+                cv.Length(min=1),
+            ),
+            cv.Required(CONF_COLUMNS): cv.All(
+                cv.ensure_list({cv.Required(CONF_PIN): pins.gpio_input_pin_schema}),
+                cv.Length(min=1),
+            ),
+            cv.Optional(CONF_KEYS): cv.string,
+            cv.Optional(CONF_DEBOUNCE_TIME, default=1): cv.int_range(min=1, max=100),
+            cv.Optional(CONF_HAS_DIODES): cv.boolean,
+        }
+    ),
+    check_keys,
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    row_pins = []
+    for conf in config[CONF_ROWS]:
+        pin = await cg.gpio_pin_expression(conf[CONF_PIN])
+        row_pins.append(pin)
+    cg.add(var.set_rows(row_pins))
+    col_pins = []
+    for conf in config[CONF_COLUMNS]:
+        pin = await cg.gpio_pin_expression(conf[CONF_PIN])
+        col_pins.append(pin)
+    cg.add(var.set_columns(col_pins))
+    if CONF_KEYS in config:
+        cg.add(var.set_keys(config[CONF_KEYS]))
+    cg.add(var.set_debounce_time(config[CONF_DEBOUNCE_TIME]))
+    if CONF_HAS_DIODES in config:
+        cg.add(var.set_has_diodes(config[CONF_HAS_DIODES]))
diff --git a/esphome/components/matrix_keypad/binary_sensor/__init__.py b/esphome/components/matrix_keypad/binary_sensor/__init__.py
new file mode 100644
index 0000000000..204db98650
--- /dev/null
+++ b/esphome/components/matrix_keypad/binary_sensor/__init__.py
@@ -0,0 +1,53 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import binary_sensor
+from esphome.const import CONF_ID, CONF_KEY
+from .. import MatrixKeypad, matrix_keypad_ns, CONF_KEYPAD_ID
+
+CONF_ROW = "row"
+CONF_COL = "col"
+
+DEPENDENCIES = ["matrix_keypad"]
+
+MatrixKeypadBinarySensor = matrix_keypad_ns.class_(
+    "MatrixKeypadBinarySensor", binary_sensor.BinarySensor
+)
+
+
+def check_button(obj):
+    if CONF_ROW in obj or CONF_COL in obj:
+        if CONF_KEY in obj:
+            raise cv.Invalid("You can't provide both a key and a position")
+        if CONF_ROW not in obj:
+            raise cv.Invalid("Missing row")
+        if CONF_COL not in obj:
+            raise cv.Invalid("Missing col")
+    elif CONF_KEY not in obj:
+        raise cv.Invalid("Missing key or position")
+    elif len(obj[CONF_KEY]) != 1:
+        raise cv.Invalid("Key must be one character")
+    return obj
+
+
+CONFIG_SCHEMA = cv.All(
+    binary_sensor.BINARY_SENSOR_SCHEMA.extend(
+        {
+            cv.GenerateID(): cv.declare_id(MatrixKeypadBinarySensor),
+            cv.GenerateID(CONF_KEYPAD_ID): cv.use_id(MatrixKeypad),
+            cv.Optional(CONF_ROW): cv.int_,
+            cv.Optional(CONF_COL): cv.int_,
+            cv.Optional(CONF_KEY): cv.string,
+        }
+    ),
+    check_button,
+)
+
+
+async def to_code(config):
+    if CONF_KEY in config:
+        var = cg.new_Pvariable(config[CONF_ID], config[CONF_KEY][0])
+    else:
+        var = cg.new_Pvariable(config[CONF_ID], config[CONF_ROW], config[CONF_COL])
+    await binary_sensor.register_binary_sensor(var, config)
+    matrix_keypad = await cg.get_variable(config[CONF_KEYPAD_ID])
+    cg.add(matrix_keypad.register_listener(var))
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
new file mode 100644
index 0000000000..d8a217f55e
--- /dev/null
+++ b/esphome/components/matrix_keypad/binary_sensor/matrix_keypad_binary_sensor.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "esphome/components/matrix_keypad/matrix_keypad.h"
+#include "esphome/components/binary_sensor/binary_sensor.h"
+
+namespace esphome {
+namespace matrix_keypad {
+
+class MatrixKeypadBinarySensor : public MatrixKeypadListener, public binary_sensor::BinarySensor {
+ public:
+  MatrixKeypadBinarySensor(uint8_t key) : has_key_(true), key_(key){};
+  MatrixKeypadBinarySensor(const char *key) : has_key_(true), key_((uint8_t) key[0]){};
+  MatrixKeypadBinarySensor(int row, int col) : has_key_(false), row_(row), col_(col){};
+
+  void key_pressed(uint8_t key) override {
+    if (!this->has_key_)
+      return;
+    if (key == this->key_)
+      this->publish_state(true);
+  }
+
+  void key_released(uint8_t key) override {
+    if (!this->has_key_)
+      return;
+    if (key == this->key_)
+      this->publish_state(false);
+  }
+
+  void button_pressed(int row, int col) override {
+    if (this->has_key_)
+      return;
+    if ((row == this->row_) && (col == this->col_))
+      this->publish_state(true);
+  }
+
+  void button_released(int row, int col) override {
+    if (this->has_key_)
+      return;
+    if ((row == this->row_) && (col == this->col_))
+      this->publish_state(false);
+  }
+
+ protected:
+  bool has_key_;
+  uint8_t key_;
+  int row_;
+  int col_;
+};
+
+}  // namespace matrix_keypad
+}  // namespace esphome
diff --git a/esphome/components/matrix_keypad/matrix_keypad.cpp b/esphome/components/matrix_keypad/matrix_keypad.cpp
new file mode 100644
index 0000000000..f4e7bf4d23
--- /dev/null
+++ b/esphome/components/matrix_keypad/matrix_keypad.cpp
@@ -0,0 +1,102 @@
+#include "matrix_keypad.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace matrix_keypad {
+
+static const char *const TAG = "matrix_keypad";
+
+void MatrixKeypad::setup() {
+  for (auto *pin : this->rows_) {
+    if (!has_diodes_) {
+      pin->pin_mode(gpio::FLAG_INPUT);
+    } else {
+      pin->digital_write(true);
+    }
+  }
+  for (auto *pin : this->columns_)
+    pin->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
+}
+
+void MatrixKeypad::loop() {
+  static uint32_t active_start = 0;
+  static int active_key = -1;
+  uint32_t now = millis();
+  int key = -1;
+  bool error = false;
+  int pos = 0, row, col;
+  for (auto *row : this->rows_) {
+    if (!has_diodes_)
+      row->pin_mode(gpio::FLAG_OUTPUT);
+    row->digital_write(false);
+    for (auto *col : this->columns_) {
+      if (!col->digital_read()) {
+        if (key != -1) {
+          error = true;
+        } else {
+          key = pos;
+        }
+      }
+      pos++;
+    }
+    row->digital_write(true);
+    if (!has_diodes_)
+      row->pin_mode(gpio::FLAG_INPUT);
+  }
+  if (error)
+    return;
+
+  if (key != active_key) {
+    if ((active_key != -1) && (this->pressed_key_ == active_key)) {
+      row = this->pressed_key_ / this->columns_.size();
+      col = this->pressed_key_ % this->columns_.size();
+      ESP_LOGD(TAG, "key @ row %d, col %d released", row, col);
+      for (auto &listener : this->listeners_)
+        listener->button_released(row, col);
+      if (!this->keys_.empty()) {
+        uint8_t keycode = this->keys_[this->pressed_key_];
+        ESP_LOGD(TAG, "key '%c' released", keycode);
+        for (auto &listener : this->listeners_)
+          listener->key_released(keycode);
+      }
+      this->pressed_key_ = -1;
+    }
+
+    active_key = key;
+    if (key == -1)
+      return;
+    active_start = now;
+  }
+
+  if ((this->pressed_key_ == key) || (now - active_start < this->debounce_time_))
+    return;
+
+  row = key / this->columns_.size();
+  col = key % this->columns_.size();
+  ESP_LOGD(TAG, "key @ row %d, col %d pressed", row, col);
+  for (auto &listener : this->listeners_)
+    listener->button_pressed(row, col);
+  if (!this->keys_.empty()) {
+    uint8_t keycode = this->keys_[key];
+    ESP_LOGD(TAG, "key '%c' pressed", keycode);
+    for (auto &listener : this->listeners_)
+      listener->key_pressed(keycode);
+    this->send_key_(keycode);
+  }
+  this->pressed_key_ = key;
+}
+
+void MatrixKeypad::dump_config() {
+  ESP_LOGCONFIG(TAG, "Matrix Keypad:");
+  ESP_LOGCONFIG(TAG, " Rows:");
+  for (auto &pin : this->rows_)
+    LOG_PIN("  Pin: ", pin);
+  ESP_LOGCONFIG(TAG, " Cols:");
+  for (auto &pin : this->columns_)
+    LOG_PIN("  Pin: ", pin);
+}
+
+void MatrixKeypad::register_listener(MatrixKeypadListener *listener) { this->listeners_.push_back(listener); }
+
+}  // namespace matrix_keypad
+}  // namespace esphome
diff --git a/esphome/components/matrix_keypad/matrix_keypad.h b/esphome/components/matrix_keypad/matrix_keypad.h
new file mode 100644
index 0000000000..9f5942be9a
--- /dev/null
+++ b/esphome/components/matrix_keypad/matrix_keypad.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include "esphome/components/key_provider/key_provider.h"
+#include "esphome/core/component.h"
+#include "esphome/core/hal.h"
+#include "esphome/core/helpers.h"
+#include <cstdlib>
+#include <utility>
+
+namespace esphome {
+namespace matrix_keypad {
+
+class MatrixKeypadListener {
+ public:
+  virtual void button_pressed(int row, int col){};
+  virtual void button_released(int row, int col){};
+  virtual void key_pressed(uint8_t key){};
+  virtual void key_released(uint8_t key){};
+};
+
+class MatrixKeypad : public key_provider::KeyProvider, public Component {
+ public:
+  void setup() override;
+  void loop() override;
+  void dump_config() override;
+  void set_columns(std::vector<GPIOPin *> pins) { columns_ = std::move(pins); };
+  void set_rows(std::vector<GPIOPin *> pins) { rows_ = std::move(pins); };
+  void set_keys(std::string keys) { keys_ = std::move(keys); };
+  void set_debounce_time(int debounce_time) { debounce_time_ = debounce_time; };
+  void set_has_diodes(int has_diodes) { has_diodes_ = has_diodes; };
+
+  void register_listener(MatrixKeypadListener *listener);
+
+ protected:
+  std::vector<GPIOPin *> rows_;
+  std::vector<GPIOPin *> columns_;
+  std::string keys_;
+  int debounce_time_ = 0;
+  bool has_diodes_{false};
+  int pressed_key_ = -1;
+
+  std::vector<MatrixKeypadListener *> listeners_{};
+};
+
+}  // namespace matrix_keypad
+}  // namespace esphome
diff --git a/esphome/components/mcp9600/sensor.py b/esphome/components/mcp9600/sensor.py
index 4c10df2dab..392ee4e773 100644
--- a/esphome/components/mcp9600/sensor.py
+++ b/esphome/components/mcp9600/sensor.py
@@ -58,7 +58,7 @@ CONFIG_SCHEMA = (
 )
 
 FINAL_VALIDATE_SCHEMA = i2c.final_validate_device_schema(
-    "mcp9600", min_frequency="100khz"
+    "mcp9600", min_frequency="10khz"
 )
 
 
diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py
index 154e6b099f..e27786a98b 100644
--- a/esphome/components/mdns/__init__.py
+++ b/esphome/components/mdns/__init__.py
@@ -1,4 +1,10 @@
-from esphome.const import CONF_ID
+from esphome.const import (
+    CONF_ID,
+    CONF_PORT,
+    CONF_PROTOCOL,
+    CONF_SERVICES,
+    CONF_SERVICE,
+)
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.core import CORE, coroutine_with_priority
@@ -8,6 +14,8 @@ DEPENDENCIES = ["network"]
 
 mdns_ns = cg.esphome_ns.namespace("mdns")
 MDNSComponent = mdns_ns.class_("MDNSComponent", cg.Component)
+MDNSTXTRecord = mdns_ns.struct("MDNSTXTRecord")
+MDNSService = mdns_ns.struct("MDNSService")
 
 
 def _remove_id_if_disabled(value):
@@ -17,18 +25,50 @@ def _remove_id_if_disabled(value):
     return value
 
 
+CONF_TXT = "txt"
+
+SERVICE_SCHEMA = cv.Schema(
+    {
+        cv.Required(CONF_SERVICE): cv.string,
+        cv.Required(CONF_PROTOCOL): cv.string,
+        cv.Optional(CONF_PORT, default=0): cv.Any(0, cv.port),
+        cv.Optional(CONF_TXT, default={}): {cv.string: cv.string},
+    }
+)
+
 CONF_DISABLED = "disabled"
 CONFIG_SCHEMA = cv.All(
     cv.Schema(
         {
             cv.GenerateID(): cv.declare_id(MDNSComponent),
             cv.Optional(CONF_DISABLED, default=False): cv.boolean,
+            cv.Optional(CONF_SERVICES, default=[]): cv.ensure_list(SERVICE_SCHEMA),
         }
     ),
     _remove_id_if_disabled,
 )
 
 
+def mdns_txt_record(key: str, value: str):
+    return cg.StructInitializer(
+        MDNSTXTRecord,
+        ("key", key),
+        ("value", value),
+    )
+
+
+def mdns_service(
+    service: str, proto: str, port: int, txt_records: list[dict[str, str]]
+):
+    return cg.StructInitializer(
+        MDNSService,
+        ("service_type", service),
+        ("proto", proto),
+        ("port", port),
+        ("txt_records", txt_records),
+    )
+
+
 @coroutine_with_priority(55.0)
 async def to_code(config):
     if CORE.using_arduino:
@@ -46,3 +86,15 @@ async def to_code(config):
 
     var = cg.new_Pvariable(config[CONF_ID])
     await cg.register_component(var, config)
+
+    for service in config[CONF_SERVICES]:
+        txt = [
+            mdns_txt_record(txt_key, txt_value)
+            for txt_key, txt_value in service[CONF_TXT].items()
+        ]
+
+        exp = mdns_service(
+            service[CONF_SERVICE], service[CONF_PROTOCOL], service[CONF_PORT], txt
+        )
+
+        cg.add(var.add_extra_service(exp))
diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp
index a3f38322b3..cdb9aa8e74 100644
--- a/esphome/components/mdns/mdns_component.cpp
+++ b/esphome/components/mdns/mdns_component.cpp
@@ -30,6 +30,9 @@ void MDNSComponent::compile_records_() {
     service.service_type = "_esphomelib";
     service.proto = "_tcp";
     service.port = api::global_api_server->get_port();
+    if (!App.get_friendly_name().empty()) {
+      service.txt_records.push_back({"friendly_name", App.get_friendly_name()});
+    }
     service.txt_records.push_back({"version", ESPHOME_VERSION});
     service.txt_records.push_back({"mac", get_mac_address()});
     const char *platform = nullptr;
@@ -87,6 +90,8 @@ void MDNSComponent::compile_records_() {
   }
 #endif
 
+  this->services_.insert(this->services_.end(), this->services_extra_.begin(), this->services_extra_.end());
+
   if (this->services_.empty()) {
     // Publish "http" service if not using native API
     // This is just to have *some* mDNS service so that .local resolution works
diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h
index dbf7482f1f..b2cb10db62 100644
--- a/esphome/components/mdns/mdns_component.h
+++ b/esphome/components/mdns/mdns_component.h
@@ -33,7 +33,12 @@ class MDNSComponent : public Component {
 #endif
   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
 
+  void add_extra_service(MDNSService service) { services_extra_.push_back(std::move(service)); }
+
+  void on_shutdown() override;
+
  protected:
+  std::vector<MDNSService> services_extra_{};
   std::vector<MDNSService> services_{};
   std::string hostname_;
   void compile_records_();
diff --git a/esphome/components/mdns/mdns_esp_idf.cpp b/esphome/components/mdns/mdns_esp32.cpp
similarity index 87%
rename from esphome/components/mdns/mdns_esp_idf.cpp
rename to esphome/components/mdns/mdns_esp32.cpp
index 40d9f1d5f3..6081c96637 100644
--- a/esphome/components/mdns/mdns_esp_idf.cpp
+++ b/esphome/components/mdns/mdns_esp32.cpp
@@ -1,9 +1,10 @@
-#ifdef USE_ESP_IDF
+#ifdef USE_ESP32
 
-#include "mdns_component.h"
-#include "esphome/core/log.h"
 #include <mdns.h>
 #include <cstring>
+#include "esphome/core/hal.h"
+#include "esphome/core/log.h"
+#include "mdns_component.h"
 
 namespace esphome {
 namespace mdns {
@@ -47,7 +48,12 @@ void MDNSComponent::setup() {
   }
 }
 
+void MDNSComponent::on_shutdown() {
+  mdns_free();
+  delay(40);  // Allow the mdns packets announcing service removal to be sent
+}
+
 }  // namespace mdns
 }  // namespace esphome
 
-#endif
+#endif  // USE_ESP32
diff --git a/esphome/components/mdns/mdns_esp32_arduino.cpp b/esphome/components/mdns/mdns_esp32_arduino.cpp
deleted file mode 100644
index 6a66beef92..0000000000
--- a/esphome/components/mdns/mdns_esp32_arduino.cpp
+++ /dev/null
@@ -1,26 +0,0 @@
-#ifdef USE_ESP32_FRAMEWORK_ARDUINO
-
-#include "mdns_component.h"
-#include "esphome/core/log.h"
-#include <ESPmDNS.h>
-
-namespace esphome {
-namespace mdns {
-
-void MDNSComponent::setup() {
-  this->compile_records_();
-
-  MDNS.begin(this->hostname_.c_str());
-
-  for (const auto &service : this->services_) {
-    MDNS.addService(service.service_type.c_str(), service.proto.c_str(), service.port);
-    for (const auto &record : service.txt_records) {
-      MDNS.addServiceTxt(service.service_type.c_str(), service.proto.c_str(), record.key.c_str(), record.value.c_str());
-    }
-  }
-}
-
-}  // namespace mdns
-}  // namespace esphome
-
-#endif  // USE_ESP32_FRAMEWORK_ARDUINO
diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp
index b91f60cb6b..4ccfe42baa 100644
--- a/esphome/components/mdns/mdns_esp8266.cpp
+++ b/esphome/components/mdns/mdns_esp8266.cpp
@@ -1,10 +1,11 @@
 #if defined(USE_ESP8266) && defined(USE_ARDUINO)
 
-#include "mdns_component.h"
-#include "esphome/core/log.h"
+#include <ESP8266mDNS.h>
 #include "esphome/components/network/ip_address.h"
 #include "esphome/components/network/util.h"
-#include <ESP8266mDNS.h>
+#include "esphome/core/hal.h"
+#include "esphome/core/log.h"
+#include "mdns_component.h"
 
 namespace esphome {
 namespace mdns {
@@ -37,6 +38,11 @@ void MDNSComponent::setup() {
 
 void MDNSComponent::loop() { MDNS.update(); }
 
+void MDNSComponent::on_shutdown() {
+  MDNS.close();
+  delay(10);
+}
+
 }  // namespace mdns
 }  // namespace esphome
 
diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp
index e443a78c48..b30a3a4ee7 100644
--- a/esphome/components/mdns/mdns_rp2040.cpp
+++ b/esphome/components/mdns/mdns_rp2040.cpp
@@ -38,6 +38,11 @@ void MDNSComponent::setup() {
 
 void MDNSComponent::loop() { MDNS.update(); }
 
+void MDNSComponent::on_shutdown() {
+  MDNS.close();
+  delay(40);
+}
+
 }  // namespace mdns
 }  // namespace esphome
 
diff --git a/esphome/components/mics_4514/__init__.py b/esphome/components/mics_4514/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/mics_4514/mics_4514.cpp b/esphome/components/mics_4514/mics_4514.cpp
new file mode 100644
index 0000000000..a14d7f2f80
--- /dev/null
+++ b/esphome/components/mics_4514/mics_4514.cpp
@@ -0,0 +1,145 @@
+#include "mics_4514.h"
+
+#include "esphome/core/helpers.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace mics_4514 {
+
+static const char *const TAG = "mics_4514";
+
+static const uint8_t SENSOR_REGISTER = 0x04;
+static const uint8_t POWER_MODE_REGISTER = 0x0a;
+
+void MICS4514Component::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up MICS 4514...");
+  uint8_t power_mode;
+  this->read_register(POWER_MODE_REGISTER, &power_mode, 1);
+  if (power_mode == 0x00) {
+    ESP_LOGCONFIG(TAG, "Waking up MICS 4514, sensors will have data after 3 minutes...");
+    power_mode = 0x01;
+    this->write_register(POWER_MODE_REGISTER, &power_mode, 1);
+    delay(100);  // NOLINT
+    this->set_timeout("warmup", 3 * 60 * 1000, [this]() {
+      this->warmed_up_ = true;
+      ESP_LOGCONFIG(TAG, "MICS 4514 setup complete.");
+    });
+    this->status_set_warning();
+    return;
+  }
+  ESP_LOGCONFIG(TAG, "Device already awake.");
+  this->warmed_up_ = true;
+  ESP_LOGCONFIG(TAG, "MICS 4514 setup complete.");
+}
+void MICS4514Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "MICS 4514:");
+  LOG_I2C_DEVICE(this);
+  LOG_UPDATE_INTERVAL(this);
+  LOG_SENSOR("  ", "Nitrogen Dioxide", this->nitrogen_dioxide_sensor_);
+  LOG_SENSOR("  ", "Carbon Monoxide", this->carbon_monoxide_sensor_);
+  LOG_SENSOR("  ", "Methane", this->methane_sensor_);
+  LOG_SENSOR("  ", "Ethanol", this->ethanol_sensor_);
+  LOG_SENSOR("  ", "Hydrogen", this->hydrogen_sensor_);
+  LOG_SENSOR("  ", "Ammonia", this->ammonia_sensor_);
+}
+float MICS4514Component::get_setup_priority() const { return setup_priority::DATA; }
+void MICS4514Component::update() {
+  if (!this->warmed_up_) {
+    return;
+  }
+  uint8_t data[6];
+  if (this->read_register(SENSOR_REGISTER, data, 6) != i2c::ERROR_OK) {
+    this->status_set_warning();
+    return;
+  }
+  this->status_clear_warning();
+  ESP_LOGV(TAG, "Got data: %02X %02X %02X %02X %02X %02X", data[0], data[1], data[2], data[3], data[4], data[5]);
+  uint16_t ox = encode_uint16(data[0], data[1]);
+  uint16_t red = encode_uint16(data[2], data[3]);
+  uint16_t power = encode_uint16(data[4], data[5]);
+
+  if (this->initial_) {
+    this->initial_ = false;
+    this->ox_calibration_ = (float) (power - ox);
+    this->red_calibration_ = (float) (power - red);
+    return;
+  }
+
+  float red_f = (float) (power - red) / this->red_calibration_;
+  float ox_f = (float) (power - ox) / this->ox_calibration_;
+
+  if (this->carbon_monoxide_sensor_ != nullptr) {
+    float co = 0.0f;
+    if (red_f <= 0.425f) {
+      co = (0.425f - red_f) / 0.000405f;
+      if (co < 1.0f)
+        co = 0.0f;
+      if (co > 1000.0f)
+        co = 1000.0f;
+    }
+    this->carbon_monoxide_sensor_->publish_state(co);
+  }
+
+  if (this->nitrogen_dioxide_sensor_ != nullptr) {
+    float nitrogendioxide = 0.0f;
+    if (ox_f >= 1.1f) {
+      nitrogendioxide = (ox_f - 0.045f) / 6.13f;
+      if (nitrogendioxide < 0.1f)
+        nitrogendioxide = 0.0f;
+      if (nitrogendioxide > 10.0f)
+        nitrogendioxide = 10.0f;
+    }
+    this->nitrogen_dioxide_sensor_->publish_state(nitrogendioxide);
+  }
+
+  if (this->methane_sensor_ != nullptr) {
+    float methane = 0.0f;
+    if (red_f <= 0.786f) {
+      methane = (0.786f - red_f) / 0.000023f;
+      if (methane < 1000.0f)
+        methane = 0.0f;
+      if (methane > 25000.0f)
+        methane = 25000.0f;
+    }
+    this->methane_sensor_->publish_state(methane);
+  }
+
+  if (this->ethanol_sensor_ != nullptr) {
+    float ethanol = 0.0f;
+    if (red_f <= 0.306f) {
+      ethanol = (0.306f - red_f) / 0.00057f;
+      if (ethanol < 10.0f)
+        ethanol = 0.0f;
+      if (ethanol > 500.0f)
+        ethanol = 500.0f;
+    }
+    this->ethanol_sensor_->publish_state(ethanol);
+  }
+
+  if (this->hydrogen_sensor_ != nullptr) {
+    float hydrogen = 0.0f;
+    if (red_f <= 0.279f) {
+      hydrogen = (0.279f - red_f) / 0.00026f;
+      if (hydrogen < 1.0f)
+        hydrogen = 0.0f;
+      if (hydrogen > 1000.0f)
+        hydrogen = 1000.0f;
+    }
+    this->hydrogen_sensor_->publish_state(hydrogen);
+  }
+
+  if (this->ammonia_sensor_ != nullptr) {
+    float ammonia = 0.0f;
+    if (red_f <= 0.8f) {
+      ammonia = (0.8f - red_f) / 0.0015f;
+      if (ammonia < 1.0f)
+        ammonia = 0.0f;
+      if (ammonia > 500.0f)
+        ammonia = 500.0f;
+    }
+    this->ammonia_sensor_->publish_state(ammonia);
+  }
+}
+
+}  // namespace mics_4514
+}  // namespace esphome
diff --git a/esphome/components/mics_4514/mics_4514.h b/esphome/components/mics_4514/mics_4514.h
new file mode 100644
index 0000000000..d2fefc3630
--- /dev/null
+++ b/esphome/components/mics_4514/mics_4514.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "esphome/components/i2c/i2c.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/core/component.h"
+#include "esphome/core/hal.h"
+
+namespace esphome {
+namespace mics_4514 {
+
+class MICS4514Component : public PollingComponent, public i2c::I2CDevice {
+  SUB_SENSOR(carbon_monoxide)
+  SUB_SENSOR(nitrogen_dioxide)
+  SUB_SENSOR(methane)
+  SUB_SENSOR(ethanol)
+  SUB_SENSOR(hydrogen)
+  SUB_SENSOR(ammonia)
+
+ public:
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override;
+  void update() override;
+
+ protected:
+  bool warmed_up_{false};
+  bool initial_{true};
+
+  float ox_calibration_{0};
+  float red_calibration_{0};
+};
+
+}  // namespace mics_4514
+}  // namespace esphome
diff --git a/esphome/components/mics_4514/sensor.py b/esphome/components/mics_4514/sensor.py
new file mode 100644
index 0000000000..80c3524f66
--- /dev/null
+++ b/esphome/components/mics_4514/sensor.py
@@ -0,0 +1,71 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+
+from esphome.components import sensor, i2c
+
+from esphome.const import (
+    CONF_ID,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_PARTS_PER_MILLION,
+)
+
+CODEOWNERS = ["@jesserockz"]
+DEPENDENCIES = ["i2c"]
+
+CONF_CARBON_MONOXIDE = "carbon_monoxide"
+CONF_NITROGEN_DIOXIDE = "nitrogen_dioxide"
+CONF_METHANE = "methane"
+CONF_ETHANOL = "ethanol"
+CONF_HYDROGEN = "hydrogen"
+CONF_AMMONIA = "ammonia"
+
+
+mics_4514_ns = cg.esphome_ns.namespace("mics_4514")
+MICS4514Component = mics_4514_ns.class_(
+    "MICS4514Component", cg.PollingComponent, i2c.I2CDevice
+)
+
+SENSORS = [
+    CONF_CARBON_MONOXIDE,
+    CONF_METHANE,
+    CONF_ETHANOL,
+    CONF_HYDROGEN,
+    CONF_AMMONIA,
+]
+
+common_sensor_schema = sensor.sensor_schema(
+    unit_of_measurement=UNIT_PARTS_PER_MILLION,
+    state_class=STATE_CLASS_MEASUREMENT,
+    accuracy_decimals=2,
+)
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(MICS4514Component),
+            cv.Optional(CONF_NITROGEN_DIOXIDE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PARTS_PER_MILLION,
+                state_class=STATE_CLASS_MEASUREMENT,
+                accuracy_decimals=2,
+            ),
+        }
+    )
+    .extend({cv.Optional(sensor_type): common_sensor_schema for sensor_type in SENSORS})
+    .extend(i2c.i2c_device_schema(0x75))
+    .extend(cv.polling_component_schema("60s"))
+)
+
+
+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)
+
+    for sensor_type in SENSORS:
+        if sensor_type in config:
+            sens = await sensor.new_sensor(config[sensor_type])
+            cg.add(getattr(var, f"set_{sensor_type}_sensor")(sens))
+
+    if CONF_NITROGEN_DIOXIDE in config:
+        sens = await sensor.new_sensor(config[CONF_NITROGEN_DIOXIDE])
+        cg.add(var.set_nitrogen_dioxide_sensor(sens))
diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp
index 1ad5ade53d..b5bf43b64f 100644
--- a/esphome/components/midea/air_conditioner.cpp
+++ b/esphome/components/midea/air_conditioner.cpp
@@ -84,18 +84,18 @@ ClimateTraits AirConditioner::traits() {
   traits.set_supported_custom_presets(this->supported_custom_presets_);
   traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_);
   /* + MINIMAL SET OF CAPABILITIES */
-  traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF);
-  traits.add_supported_mode(ClimateMode::CLIMATE_MODE_FAN_ONLY);
   traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO);
   traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW);
   traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM);
   traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH);
-  traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF);
-  traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_VERTICAL);
-  traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE);
-  traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_SLEEP);
   if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK)
     Converters::to_climate_traits(traits, this->base_.getCapabilities());
+  if (!traits.get_supported_modes().empty())
+    traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF);
+  if (!traits.get_supported_swing_modes().empty())
+    traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF);
+  if (!traits.get_supported_presets().empty())
+    traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE);
   return traits;
 }
 
diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py
index 254322d097..6fea7033f2 100644
--- a/esphome/components/modbus/__init__.py
+++ b/esphome/components/modbus/__init__.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_FLOW_CONTROL_PIN,
     CONF_ID,
     CONF_ADDRESS,
+    CONF_DISABLE_CRC,
 )
 from esphome import pins
 
@@ -27,6 +28,7 @@ CONFIG_SCHEMA = (
             cv.Optional(
                 CONF_SEND_WAIT_TIME, default="250ms"
             ): cv.positive_time_period_milliseconds,
+            cv.Optional(CONF_DISABLE_CRC, default=False): cv.boolean,
         }
     )
     .extend(cv.COMPONENT_SCHEMA)
@@ -45,8 +47,8 @@ async def to_code(config):
         pin = await gpio_pin_expression(config[CONF_FLOW_CONTROL_PIN])
         cg.add(var.set_flow_control_pin(pin))
 
-    if CONF_SEND_WAIT_TIME in config:
-        cg.add(var.set_send_wait_time(config[CONF_SEND_WAIT_TIME]))
+    cg.add(var.set_send_wait_time(config[CONF_SEND_WAIT_TIME]))
+    cg.add(var.set_disable_crc(config[CONF_DISABLE_CRC]))
 
 
 def modbus_device_schema(default_address):
diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp
index 4d75675d0f..137fb0b26b 100644
--- a/esphome/components/modbus/modbus.cpp
+++ b/esphome/components/modbus/modbus.cpp
@@ -102,8 +102,12 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
     uint16_t computed_crc = crc16(raw, data_offset + data_len);
     uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
     if (computed_crc != remote_crc) {
-      ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc);
-      return false;
+      if (this->disable_crc_) {
+        ESP_LOGD(TAG, "Modbus CRC Check failed, but ignored! %02X!=%02X", computed_crc, remote_crc);
+      } else {
+        ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc);
+        return false;
+      }
     }
   }
   std::vector<uint8_t> data(this->rx_buffer_.begin() + data_offset, this->rx_buffer_.begin() + data_offset + data_len);
@@ -139,6 +143,7 @@ void Modbus::dump_config() {
   ESP_LOGCONFIG(TAG, "Modbus:");
   LOG_PIN("  Flow Control Pin: ", this->flow_control_pin_);
   ESP_LOGCONFIG(TAG, "  Send Wait Time: %d ms", this->send_wait_time_);
+  ESP_LOGCONFIG(TAG, "  CRC Disabled: %s", YESNO(this->disable_crc_));
 }
 float Modbus::get_setup_priority() const {
   // After UART bus
diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h
index 95414ba090..dd8732c6e9 100644
--- a/esphome/components/modbus/modbus.h
+++ b/esphome/components/modbus/modbus.h
@@ -30,12 +30,14 @@ class Modbus : public uart::UARTDevice, public Component {
   void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; }
   uint8_t waiting_for_response{0};
   void set_send_wait_time(uint16_t time_in_ms) { send_wait_time_ = time_in_ms; }
+  void set_disable_crc(bool disable_crc) { disable_crc_ = disable_crc; }
 
  protected:
   GPIOPin *flow_control_pin_{nullptr};
 
   bool parse_modbus_byte_(uint8_t byte);
   uint16_t send_wait_time_{250};
+  bool disable_crc_;
   std::vector<uint8_t> rx_buffer_;
   uint32_t last_modbus_byte_{0};
   uint32_t last_send_{0};
diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp
index fc57318a81..60592b00ad 100644
--- a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp
+++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp
@@ -53,6 +53,7 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
   // Now parse the data - See Datasheet for definition
 
   if (static_cast<SensorType>(manu_data.data[0]) != STANDARD_BOTTOM_UP &&
+      static_cast<SensorType>(manu_data.data[0]) != LIPPERT_BOTTOM_UP &&
       static_cast<SensorType>(manu_data.data[0]) != PLUS_BOTTOM_UP) {
     ESP_LOGE(TAG, "Unsupported Sensor Type (0x%X)", manu_data.data[0]);
     return false;
diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.h b/esphome/components/mopeka_pro_check/mopeka_pro_check.h
index e6cc1fd6f1..8b126a204c 100644
--- a/esphome/components/mopeka_pro_check/mopeka_pro_check.h
+++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.h
@@ -15,6 +15,7 @@ enum SensorType {
   STANDARD_BOTTOM_UP = 0x03,
   TOP_DOWN_AIR_ABOVE = 0x04,
   BOTTOM_UP_WATER = 0x05,
+  LIPPERT_BOTTOM_UP = 0x06,
   PLUS_BOTTOM_UP = 0x08
   // all other values are reserved
 };
diff --git a/esphome/components/mqtt/mqtt_backend_idf.cpp b/esphome/components/mqtt/mqtt_backend_idf.cpp
index 0726f72567..812e36d522 100644
--- a/esphome/components/mqtt/mqtt_backend_idf.cpp
+++ b/esphome/components/mqtt/mqtt_backend_idf.cpp
@@ -69,7 +69,7 @@ void MQTTBackendIDF::loop() {
   }
 }
 
-void MQTTBackendIDF::mqtt_event_handler_(const esp_mqtt_event_t &event) {
+void MQTTBackendIDF::mqtt_event_handler_(const Event &event) {
   ESP_LOGV(TAG, "Event dispatched from event loop event_id=%d", event.event_id);
   switch (event.event_id) {
     case MQTT_EVENT_BEFORE_CONNECT:
@@ -104,28 +104,24 @@ void MQTTBackendIDF::mqtt_event_handler_(const esp_mqtt_event_t &event) {
       break;
     case MQTT_EVENT_DATA: {
       static std::string topic;
-      if (event.topic) {
-        // not 0 terminated - create a string from it
-        topic = std::string(event.topic, event.topic_len);
+      if (event.topic.length() > 0) {
+        topic = event.topic;
       }
       ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", topic.c_str());
-      auto data_len = event.data_len;
-      if (data_len == 0)
-        data_len = strlen(event.data);
-      this->on_message_.call(event.topic ? const_cast<char *>(topic.c_str()) : nullptr, event.data, data_len,
+      this->on_message_.call(event.topic.length() > 0 ? topic.c_str() : nullptr, event.data.data(), event.data.size(),
                              event.current_data_offset, event.total_data_len);
     } break;
     case MQTT_EVENT_ERROR:
       ESP_LOGE(TAG, "MQTT_EVENT_ERROR");
-      if (event.error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) {
-        ESP_LOGE(TAG, "Last error code reported from esp-tls: 0x%x", event.error_handle->esp_tls_last_esp_err);
-        ESP_LOGE(TAG, "Last tls stack error number: 0x%x", event.error_handle->esp_tls_stack_err);
-        ESP_LOGE(TAG, "Last captured errno : %d (%s)", event.error_handle->esp_transport_sock_errno,
-                 strerror(event.error_handle->esp_transport_sock_errno));
-      } else if (event.error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) {
-        ESP_LOGE(TAG, "Connection refused error: 0x%x", event.error_handle->connect_return_code);
+      if (event.error_handle.error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) {
+        ESP_LOGE(TAG, "Last error code reported from esp-tls: 0x%x", event.error_handle.esp_tls_last_esp_err);
+        ESP_LOGE(TAG, "Last tls stack error number: 0x%x", event.error_handle.esp_tls_stack_err);
+        ESP_LOGE(TAG, "Last captured errno : %d (%s)", event.error_handle.esp_transport_sock_errno,
+                 strerror(event.error_handle.esp_transport_sock_errno));
+      } else if (event.error_handle.error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) {
+        ESP_LOGE(TAG, "Connection refused error: 0x%x", event.error_handle.connect_return_code);
       } else {
-        ESP_LOGE(TAG, "Unknown error type: 0x%x", event.error_handle->error_type);
+        ESP_LOGE(TAG, "Unknown error type: 0x%x", event.error_handle.error_type);
       }
       break;
     default:
@@ -140,7 +136,7 @@ void MQTTBackendIDF::mqtt_event_handler(void *handler_args, esp_event_base_t bas
   // queue event to decouple processing
   if (instance) {
     auto event = *static_cast<esp_mqtt_event_t *>(event_data);
-    instance->mqtt_events_.push(event);
+    instance->mqtt_events_.push(Event(event));
   }
 }
 
diff --git a/esphome/components/mqtt/mqtt_backend_idf.h b/esphome/components/mqtt/mqtt_backend_idf.h
index 77b5592d72..900ee9709b 100644
--- a/esphome/components/mqtt/mqtt_backend_idf.h
+++ b/esphome/components/mqtt/mqtt_backend_idf.h
@@ -12,6 +12,33 @@
 namespace esphome {
 namespace mqtt {
 
+struct Event {
+  esp_mqtt_event_id_t event_id;
+  std::vector<char> data;
+  int total_data_len;
+  int current_data_offset;
+  std::string topic;
+  int msg_id;
+  bool retain;
+  int qos;
+  bool dup;
+  esp_mqtt_error_codes_t error_handle;
+
+  // Construct from esp_mqtt_event_t
+  // Any pointer values that are unsafe to keep are converted to safe copies
+  Event(const esp_mqtt_event_t &event)
+      : event_id(event.event_id),
+        data(event.data, event.data + event.data_len),
+        total_data_len(event.total_data_len),
+        current_data_offset(event.current_data_offset),
+        topic(event.topic, event.topic_len),
+        msg_id(event.msg_id),
+        retain(event.retain),
+        qos(event.qos),
+        dup(event.dup),
+        error_handle(*event.error_handle) {}
+};
+
 class MQTTBackendIDF final : public MQTTBackend {
  public:
   static const size_t MQTT_BUFFER_SIZE = 4096;
@@ -99,7 +126,7 @@ class MQTTBackendIDF final : public MQTTBackend {
 
  protected:
   bool initialize_();
-  void mqtt_event_handler_(const esp_mqtt_event_t &event);
+  void mqtt_event_handler_(const Event &event);
   static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data);
 
   struct MqttClientDeleter {
@@ -134,7 +161,7 @@ class MQTTBackendIDF final : public MQTTBackend {
   CallbackManager<on_unsubscribe_callback_t> on_unsubscribe_;
   CallbackManager<on_message_callback_t> on_message_;
   CallbackManager<on_publish_user_callback_t> on_publish_;
-  std::queue<esp_mqtt_event_t> mqtt_events_;
+  std::queue<Event> mqtt_events_;
 };
 
 }  // namespace mqtt
diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp
index 7c3c414b3a..8dd03dd5c8 100644
--- a/esphome/components/mqtt/mqtt_climate.cpp
+++ b/esphome/components/mqtt/mqtt_climate.cpp
@@ -66,18 +66,42 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
   // temperature units are always coerced to Celsius internally
   root[MQTT_TEMPERATURE_UNIT] = "C";
 
-  if (traits.supports_preset(CLIMATE_PRESET_AWAY)) {
-    // away_mode_command_topic
-    root[MQTT_AWAY_MODE_COMMAND_TOPIC] = this->get_away_command_topic();
-    // away_mode_state_topic
-    root[MQTT_AWAY_MODE_STATE_TOPIC] = this->get_away_state_topic();
+  if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
+    // preset_mode_command_topic
+    root[MQTT_PRESET_MODE_COMMAND_TOPIC] = this->get_preset_command_topic();
+    // preset_mode_state_topic
+    root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic();
+    // presets
+    JsonArray presets = root.createNestedArray("presets");
+    if (traits.supports_preset(CLIMATE_PRESET_HOME))
+      presets.add("home");
+    if (traits.supports_preset(CLIMATE_PRESET_AWAY)) {
+      // away_mode_command_topic
+      root[MQTT_AWAY_MODE_COMMAND_TOPIC] = this->get_away_command_topic();
+      // away_mode_state_topic
+      root[MQTT_AWAY_MODE_STATE_TOPIC] = this->get_away_state_topic();
+      presets.add("away");
+    }
+    if (traits.supports_preset(CLIMATE_PRESET_BOOST))
+      presets.add("boost");
+    if (traits.supports_preset(CLIMATE_PRESET_COMFORT))
+      presets.add("comfort");
+    if (traits.supports_preset(CLIMATE_PRESET_ECO))
+      presets.add("eco");
+    if (traits.supports_preset(CLIMATE_PRESET_SLEEP))
+      presets.add("sleep");
+    if (traits.supports_preset(CLIMATE_PRESET_ACTIVITY))
+      presets.add("activity");
+    for (const auto &preset : traits.get_supported_custom_presets())
+      presets.add(preset);
   }
+
   if (traits.get_supports_action()) {
     // action_topic
     root[MQTT_ACTION_TOPIC] = this->get_action_state_topic();
   }
 
-  if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) {
+  if (traits.get_supports_fan_modes()) {
     // fan_mode_command_topic
     root[MQTT_FAN_MODE_COMMAND_TOPIC] = this->get_fan_mode_command_topic();
     // fan_mode_state_topic
@@ -102,6 +126,8 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
       fan_modes.add("focus");
     if (traits.supports_fan_mode(CLIMATE_FAN_DIFFUSE))
       fan_modes.add("diffuse");
+    if (traits.supports_fan_mode(CLIMATE_FAN_QUIET))
+      fan_modes.add("quiet");
     for (const auto &fan_mode : traits.get_supported_custom_fan_modes())
       fan_modes.add(fan_mode);
   }
@@ -194,6 +220,14 @@ void MQTTClimateComponent::setup() {
     });
   }
 
+  if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
+    this->subscribe(this->get_preset_command_topic(), [this](const std::string &topic, const std::string &payload) {
+      auto call = this->device_->make_call();
+      call.set_preset(payload);
+      call.perform();
+    });
+  }
+
   if (traits.get_supports_fan_modes()) {
     this->subscribe(this->get_fan_mode_command_topic(), [this](const std::string &topic, const std::string &payload) {
       auto call = this->device_->make_call();
@@ -271,6 +305,42 @@ bool MQTTClimateComponent::publish_state_() {
     if (!this->publish(this->get_away_state_topic(), payload))
       success = false;
   }
+  if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
+    std::string payload;
+    if (this->device_->preset.has_value()) {
+      switch (this->device_->preset.value()) {
+        case CLIMATE_PRESET_NONE:
+          payload = "none";
+          break;
+        case CLIMATE_PRESET_HOME:
+          payload = "home";
+          break;
+        case CLIMATE_PRESET_AWAY:
+          payload = "away";
+          break;
+        case CLIMATE_PRESET_BOOST:
+          payload = "boost";
+          break;
+        case CLIMATE_PRESET_COMFORT:
+          payload = "comfort";
+          break;
+        case CLIMATE_PRESET_ECO:
+          payload = "eco";
+          break;
+        case CLIMATE_PRESET_SLEEP:
+          payload = "sleep";
+          break;
+        case CLIMATE_PRESET_ACTIVITY:
+          payload = "activity";
+          break;
+      }
+    }
+    if (this->device_->custom_preset.has_value())
+      payload = this->device_->custom_preset.value();
+    if (!this->publish(this->get_preset_state_topic(), payload))
+      success = false;
+  }
+
   if (traits.get_supports_action()) {
     const char *payload = "unknown";
     switch (this->device_->action) {
@@ -328,6 +398,9 @@ bool MQTTClimateComponent::publish_state_() {
         case CLIMATE_FAN_DIFFUSE:
           payload = "diffuse";
           break;
+        case CLIMATE_FAN_QUIET:
+          payload = "quiet";
+          break;
       }
     }
     if (this->device_->custom_fan_mode.has_value())
diff --git a/esphome/components/mqtt/mqtt_climate.h b/esphome/components/mqtt/mqtt_climate.h
index ea3e2ab3fa..a93070fe66 100644
--- a/esphome/components/mqtt/mqtt_climate.h
+++ b/esphome/components/mqtt/mqtt_climate.h
@@ -35,6 +35,8 @@ class MQTTClimateComponent : public mqtt::MQTTComponent {
   MQTT_COMPONENT_CUSTOM_TOPIC(fan_mode, command)
   MQTT_COMPONENT_CUSTOM_TOPIC(swing_mode, state)
   MQTT_COMPONENT_CUSTOM_TOPIC(swing_mode, command)
+  MQTT_COMPONENT_CUSTOM_TOPIC(preset, state)
+  MQTT_COMPONENT_CUSTOM_TOPIC(preset, command)
 
  protected:
   const EntityBase *get_entity() const override;
diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py
index ec4dead42e..463557e3b3 100644
--- a/esphome/components/number/__init__.py
+++ b/esphome/components/number/__init__.py
@@ -41,6 +41,7 @@ from esphome.const import (
     DEVICE_CLASS_PM25,
     DEVICE_CLASS_POWER,
     DEVICE_CLASS_POWER_FACTOR,
+    DEVICE_CLASS_PRECIPITATION,
     DEVICE_CLASS_PRECIPITATION_INTENSITY,
     DEVICE_CLASS_PRESSURE,
     DEVICE_CLASS_REACTIVE_POWER,
@@ -84,6 +85,7 @@ DEVICE_CLASSES = [
     DEVICE_CLASS_PM25,
     DEVICE_CLASS_POWER_FACTOR,
     DEVICE_CLASS_POWER,
+    DEVICE_CLASS_PRECIPITATION,
     DEVICE_CLASS_PRECIPITATION_INTENSITY,
     DEVICE_CLASS_PRESSURE,
     DEVICE_CLASS_REACTIVE_POWER,
diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py
index 8392008222..98483e8ae9 100644
--- a/esphome/components/packages/__init__.py
+++ b/esphome/components/packages/__init__.py
@@ -165,7 +165,7 @@ def do_packages_pass(config: dict):
                 f"Packages must be a key to value mapping, got {type(packages)} instead"
             )
 
-        for package_name, package_config in packages.items():
+        for package_name, package_config in reversed(packages.items()):
             with cv.prepend_path(package_name):
                 recursive_package = package_config
                 if CONF_URL in package_config:
diff --git a/esphome/components/pca9554/__init__.py b/esphome/components/pca9554/__init__.py
new file mode 100644
index 0000000000..76d6ddaf32
--- /dev/null
+++ b/esphome/components/pca9554/__init__.py
@@ -0,0 +1,76 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import pins
+from esphome.components import i2c
+from esphome.const import (
+    CONF_ID,
+    CONF_INPUT,
+    CONF_NUMBER,
+    CONF_MODE,
+    CONF_INVERTED,
+    CONF_OUTPUT,
+)
+
+CODEOWNERS = ["@hwstar"]
+DEPENDENCIES = ["i2c"]
+MULTI_CONF = True
+pca9554_ns = cg.esphome_ns.namespace("pca9554")
+
+PCA9554Component = pca9554_ns.class_("PCA9554Component", cg.Component, i2c.I2CDevice)
+PCA9554GPIOPin = pca9554_ns.class_(
+    "PCA9554GPIOPin", cg.GPIOPin, cg.Parented.template(PCA9554Component)
+)
+
+CONF_PCA9554 = "pca9554"
+CONFIG_SCHEMA = (
+    cv.Schema({cv.Required(CONF_ID): cv.declare_id(PCA9554Component)})
+    .extend(cv.COMPONENT_SCHEMA)
+    .extend(
+        i2c.i2c_device_schema(0x20)
+    )  # Note: 0x20 for the non-A part. The PCA9554A parts start at addess 0x38
+)
+
+
+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)
+
+
+def validate_mode(value):
+    if not (value[CONF_INPUT] or value[CONF_OUTPUT]):
+        raise cv.Invalid("Mode must be either input or output")
+    if value[CONF_INPUT] and value[CONF_OUTPUT]:
+        raise cv.Invalid("Mode must be either input or output")
+    return value
+
+
+PCA9554_PIN_SCHEMA = cv.All(
+    {
+        cv.GenerateID(): cv.declare_id(PCA9554GPIOPin),
+        cv.Required(CONF_PCA9554): cv.use_id(PCA9554Component),
+        cv.Required(CONF_NUMBER): cv.int_range(min=0, max=8),
+        cv.Optional(CONF_MODE, default={}): cv.All(
+            {
+                cv.Optional(CONF_INPUT, default=False): cv.boolean,
+                cv.Optional(CONF_OUTPUT, default=False): cv.boolean,
+            },
+            validate_mode,
+        ),
+        cv.Optional(CONF_INVERTED, default=False): cv.boolean,
+    }
+)
+
+
+@pins.PIN_SCHEMA_REGISTRY.register("pca9554", PCA9554_PIN_SCHEMA)
+async def pca9554_pin_to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    parent = await cg.get_variable(config[CONF_PCA9554])
+
+    cg.add(var.set_parent(parent))
+
+    num = config[CONF_NUMBER]
+    cg.add(var.set_pin(num))
+    cg.add(var.set_inverted(config[CONF_INVERTED]))
+    cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
+    return var
diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp
new file mode 100644
index 0000000000..39093fcf54
--- /dev/null
+++ b/esphome/components/pca9554/pca9554.cpp
@@ -0,0 +1,112 @@
+#include "pca9554.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace pca9554 {
+
+const uint8_t INPUT_REG = 0;
+const uint8_t OUTPUT_REG = 1;
+const uint8_t INVERT_REG = 2;
+const uint8_t CONFIG_REG = 3;
+
+static const char *const TAG = "pca9554";
+
+void PCA9554Component::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up PCA9554/PCA9554A...");
+  // Test to see if device exists
+  if (!this->read_inputs_()) {
+    ESP_LOGE(TAG, "PCA9554 not available under 0x%02X", this->address_);
+    this->mark_failed();
+    return;
+  }
+
+  // No polarity inversion
+  this->write_register_(INVERT_REG, 0);
+  // All inputs at initialization
+  this->config_mask_ = 0;
+  // Invert mask as the part sees a 1 as an input
+  this->write_register_(CONFIG_REG, ~this->config_mask_);
+  // All ouputs low
+  this->output_mask_ = 0;
+  this->write_register_(OUTPUT_REG, this->output_mask_);
+  // Read the inputs
+  this->read_inputs_();
+  ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
+           this->status_has_error());
+}
+void PCA9554Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "PCA9554:");
+  LOG_I2C_DEVICE(this)
+  if (this->is_failed()) {
+    ESP_LOGE(TAG, "Communication with PCA9554 failed!");
+  }
+}
+
+bool PCA9554Component::digital_read(uint8_t pin) {
+  this->read_inputs_();
+  return this->input_mask_ & (1 << pin);
+}
+
+void PCA9554Component::digital_write(uint8_t pin, bool value) {
+  if (value) {
+    this->output_mask_ |= (1 << pin);
+  } else {
+    this->output_mask_ &= ~(1 << pin);
+  }
+  this->write_register_(OUTPUT_REG, this->output_mask_);
+}
+
+void PCA9554Component::pin_mode(uint8_t pin, gpio::Flags flags) {
+  if (flags == gpio::FLAG_INPUT) {
+    // Clear mode mask bit
+    this->config_mask_ &= ~(1 << pin);
+  } else if (flags == gpio::FLAG_OUTPUT) {
+    // Set mode mask bit
+    this->config_mask_ |= 1 << pin;
+  }
+  this->write_register_(CONFIG_REG, ~this->config_mask_);
+}
+
+bool PCA9554Component::read_inputs_() {
+  uint8_t inputs;
+
+  if (this->is_failed()) {
+    ESP_LOGD(TAG, "Device marked failed");
+    return false;
+  }
+
+  if ((this->last_error_ = this->read_register(INPUT_REG, &inputs, 1, true)) != esphome::i2c::ERROR_OK) {
+    this->status_set_warning();
+    ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_);
+    return false;
+  }
+  this->status_clear_warning();
+  this->input_mask_ = inputs;
+  return true;
+}
+
+bool PCA9554Component::write_register_(uint8_t reg, uint8_t value) {
+  if ((this->last_error_ = this->write_register(reg, &value, 1, true)) != esphome::i2c::ERROR_OK) {
+    this->status_set_warning();
+    ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_);
+    return false;
+  }
+
+  this->status_clear_warning();
+  return true;
+}
+
+float PCA9554Component::get_setup_priority() const { return setup_priority::IO; }
+
+void PCA9554GPIOPin::setup() { pin_mode(flags_); }
+void PCA9554GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
+bool PCA9554GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
+void PCA9554GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
+std::string PCA9554GPIOPin::dump_summary() const {
+  char buffer[32];
+  snprintf(buffer, sizeof(buffer), "%u via PCA9554", pin_);
+  return buffer;
+}
+
+}  // namespace pca9554
+}  // namespace esphome
diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h
new file mode 100644
index 0000000000..d1bfc36bec
--- /dev/null
+++ b/esphome/components/pca9554/pca9554.h
@@ -0,0 +1,64 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/hal.h"
+#include "esphome/components/i2c/i2c.h"
+
+namespace esphome {
+namespace pca9554 {
+
+class PCA9554Component : public Component, public i2c::I2CDevice {
+ public:
+  PCA9554Component() = default;
+
+  /// Check i2c availability and setup masks
+  void setup() override;
+  /// Helper function to read the value of a pin.
+  bool digital_read(uint8_t pin);
+  /// Helper function to write the value of a pin.
+  void digital_write(uint8_t pin, bool value);
+  /// Helper function to set the pin mode of a pin.
+  void pin_mode(uint8_t pin, gpio::Flags flags);
+
+  float get_setup_priority() const override;
+
+  void dump_config() override;
+
+ protected:
+  bool read_inputs_();
+
+  bool write_register_(uint8_t reg, uint8_t value);
+
+  /// Mask for the pin config - 1 means OUTPUT, 0 means INPUT
+  uint8_t config_mask_{0x00};
+  /// The mask to write as output state - 1 means HIGH, 0 means LOW
+  uint8_t output_mask_{0x00};
+  /// The state of the actual input pin states - 1 means HIGH, 0 means LOW
+  uint8_t input_mask_{0x00};
+  /// Storage for last I2C error seen
+  esphome::i2c::ErrorCode last_error_;
+};
+
+/// Helper class to expose a PCA9554 pin as an internal input GPIO pin.
+class PCA9554GPIOPin : public GPIOPin {
+ public:
+  void setup() override;
+  void pin_mode(gpio::Flags flags) override;
+  bool digital_read() override;
+  void digital_write(bool value) override;
+  std::string dump_summary() const override;
+
+  void set_parent(PCA9554Component *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; }
+
+ protected:
+  PCA9554Component *parent_;
+  uint8_t pin_;
+  bool inverted_;
+  gpio::Flags flags_;
+};
+
+}  // namespace pca9554
+}  // namespace esphome
diff --git a/esphome/components/pca9685/__init__.py b/esphome/components/pca9685/__init__.py
index 1a5ccc3247..b22577bf9f 100644
--- a/esphome/components/pca9685/__init__.py
+++ b/esphome/components/pca9685/__init__.py
@@ -1,7 +1,7 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import i2c
-from esphome.const import CONF_FREQUENCY, CONF_ID
+from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_EXTERNAL_CLOCK_INPUT
 
 DEPENDENCIES = ["i2c"]
 MULTI_CONF = True
@@ -9,21 +9,39 @@ MULTI_CONF = True
 pca9685_ns = cg.esphome_ns.namespace("pca9685")
 PCA9685Output = pca9685_ns.class_("PCA9685Output", cg.Component, i2c.I2CDevice)
 
-CONFIG_SCHEMA = (
+
+def validate_frequency(config):
+    if config[CONF_EXTERNAL_CLOCK_INPUT]:
+        if CONF_FREQUENCY in config:
+            raise cv.Invalid(
+                "Frequency cannot be set when using an external clock input"
+            )
+        return config
+    if CONF_FREQUENCY not in config:
+        raise cv.Invalid("Frequency is required")
+    return config
+
+
+CONFIG_SCHEMA = cv.All(
     cv.Schema(
         {
             cv.GenerateID(): cv.declare_id(PCA9685Output),
-            cv.Required(CONF_FREQUENCY): cv.All(
+            cv.Optional(CONF_FREQUENCY): cv.All(
                 cv.frequency, cv.Range(min=23.84, max=1525.88)
             ),
+            cv.Optional(CONF_EXTERNAL_CLOCK_INPUT, default=False): cv.boolean,
         }
     )
     .extend(cv.COMPONENT_SCHEMA)
-    .extend(i2c.i2c_device_schema(0x40))
+    .extend(i2c.i2c_device_schema(0x40)),
+    validate_frequency,
 )
 
 
 async def to_code(config):
-    var = cg.new_Pvariable(config[CONF_ID], config[CONF_FREQUENCY])
+    var = cg.new_Pvariable(config[CONF_ID])
+    if CONF_FREQUENCY in config:
+        cg.add(var.set_frequency(config[CONF_FREQUENCY]))
+    cg.add(var.set_extclk(config[CONF_EXTERNAL_CLOCK_INPUT]))
     await cg.register_component(var, config)
     await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/pca9685/pca9685_output.cpp b/esphome/components/pca9685/pca9685_output.cpp
index 957f4062fc..c61251b66f 100644
--- a/esphome/components/pca9685/pca9685_output.cpp
+++ b/esphome/components/pca9685/pca9685_output.cpp
@@ -21,6 +21,7 @@ static const uint8_t PCA9685_REGISTER_LED0 = 0x06;
 static const uint8_t PCA9685_REGISTER_PRE_SCALE = 0xFE;
 
 static const uint8_t PCA9685_MODE1_RESTART = 0b10000000;
+static const uint8_t PCA9685_MODE1_EXTCLK = 0b01000000;
 static const uint8_t PCA9685_MODE1_AUTOINC = 0b00100000;
 static const uint8_t PCA9685_MODE1_SLEEP = 0b00010000;
 
@@ -28,10 +29,13 @@ void PCA9685Output::setup() {
   ESP_LOGCONFIG(TAG, "Setting up PCA9685OutputComponent...");
 
   ESP_LOGV(TAG, "  Resetting devices...");
+  uint8_t address_tmp = this->address_;
+  this->set_i2c_address(0x00);
   if (!this->write_bytes(PCA9685_REGISTER_SOFTWARE_RESET, nullptr, 0)) {
     this->mark_failed();
     return;
   }
+  this->set_i2c_address(address_tmp);
 
   if (!this->write_byte(PCA9685_REGISTER_MODE1, PCA9685_MODE1_RESTART | PCA9685_MODE1_AUTOINC)) {
     this->mark_failed();
@@ -42,14 +46,6 @@ void PCA9685Output::setup() {
     return;
   }
 
-  int pre_scaler = static_cast<int>((25000000 / (4096 * this->frequency_)) - 1);
-  if (pre_scaler > 255)
-    pre_scaler = 255;
-  if (pre_scaler < 3)
-    pre_scaler = 3;
-
-  ESP_LOGV(TAG, "  -> Prescaler: %d", pre_scaler);
-
   uint8_t mode1;
   if (!this->read_byte(PCA9685_REGISTER_MODE1, &mode1)) {
     this->mark_failed();
@@ -60,6 +56,20 @@ void PCA9685Output::setup() {
     this->mark_failed();
     return;
   }
+
+  int pre_scaler = 3;
+  if (this->extclk_) {
+    mode1 = mode1 | PCA9685_MODE1_EXTCLK;
+    if (!this->write_byte(PCA9685_REGISTER_MODE1, mode1)) {
+      this->mark_failed();
+      return;
+    }
+  } else {
+    pre_scaler = static_cast<int>((25000000 / (4096 * this->frequency_)) - 1);
+    pre_scaler = clamp(pre_scaler, 3, 255);
+
+    ESP_LOGV(TAG, "  -> Prescaler: %d", pre_scaler);
+  }
   if (!this->write_byte(PCA9685_REGISTER_PRE_SCALE, pre_scaler)) {
     this->mark_failed();
     return;
@@ -78,7 +88,12 @@ void PCA9685Output::setup() {
 void PCA9685Output::dump_config() {
   ESP_LOGCONFIG(TAG, "PCA9685:");
   ESP_LOGCONFIG(TAG, "  Mode: 0x%02X", this->mode_);
-  ESP_LOGCONFIG(TAG, "  Frequency: %.0f Hz", this->frequency_);
+  if (this->extclk_) {
+    ESP_LOGCONFIG(TAG, "  EXTCLK: enabled");
+  } else {
+    ESP_LOGCONFIG(TAG, "  EXTCLK: disabled");
+    ESP_LOGCONFIG(TAG, "  Frequency: %.0f Hz", this->frequency_);
+  }
   if (this->is_failed()) {
     ESP_LOGE(TAG, "Setting up PCA9685 failed!");
   }
diff --git a/esphome/components/pca9685/pca9685_output.h b/esphome/components/pca9685/pca9685_output.h
index 5dd52b5510..8e547d0032 100644
--- a/esphome/components/pca9685/pca9685_output.h
+++ b/esphome/components/pca9685/pca9685_output.h
@@ -37,8 +37,7 @@ class PCA9685Channel : public output::FloatOutput {
 /// PCA9685 float output component.
 class PCA9685Output : public Component, public i2c::I2CDevice {
  public:
-  PCA9685Output(float frequency, uint8_t mode = PCA9685_MODE_OUTPUT_ONACK | PCA9685_MODE_OUTPUT_TOTEM_POLE)
-      : frequency_(frequency), mode_(mode) {}
+  PCA9685Output(uint8_t mode = PCA9685_MODE_OUTPUT_ONACK | PCA9685_MODE_OUTPUT_TOTEM_POLE) : mode_(mode) {}
 
   void register_channel(PCA9685Channel *channel);
 
@@ -46,6 +45,8 @@ class PCA9685Output : public Component, public i2c::I2CDevice {
   void dump_config() override;
   float get_setup_priority() const override { return setup_priority::HARDWARE; }
   void loop() override;
+  void set_extclk(bool extclk) { this->extclk_ = extclk; }
+  void set_frequency(float frequency) { this->frequency_ = frequency; }
 
  protected:
   friend PCA9685Channel;
@@ -58,6 +59,7 @@ class PCA9685Output : public Component, public i2c::I2CDevice {
 
   float frequency_;
   uint8_t mode_;
+  bool extclk_ = false;
 
   uint8_t min_channel_{0xFF};
   uint8_t max_channel_{0x00};
diff --git a/esphome/components/pcf85063/__init__.py b/esphome/components/pcf85063/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp
new file mode 100644
index 0000000000..c6a8624ca7
--- /dev/null
+++ b/esphome/components/pcf85063/pcf85063.cpp
@@ -0,0 +1,105 @@
+#include "pcf85063.h"
+#include "esphome/core/log.h"
+
+// Datasheet:
+// - https://datasheets.maximintegrated.com/en/ds/DS1307.pdf
+
+namespace esphome {
+namespace pcf85063 {
+
+static const char *const TAG = "pcf85063";
+
+void PCF85063Component::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up PCF85063...");
+  if (!this->read_rtc_()) {
+    this->mark_failed();
+  }
+}
+
+void PCF85063Component::update() { this->read_time(); }
+
+void PCF85063Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "PCF85063:");
+  LOG_I2C_DEVICE(this);
+  if (this->is_failed()) {
+    ESP_LOGE(TAG, "Communication with PCF85063 failed!");
+  }
+  ESP_LOGCONFIG(TAG, "  Timezone: '%s'", this->timezone_.c_str());
+}
+
+float PCF85063Component::get_setup_priority() const { return setup_priority::DATA; }
+
+void PCF85063Component::read_time() {
+  if (!this->read_rtc_()) {
+    return;
+  }
+  if (pcf85063_.reg.osc_stop) {
+    ESP_LOGW(TAG, "RTC halted, not syncing to system clock.");
+    return;
+  }
+  time::ESPTime rtc_time{.second = uint8_t(pcf85063_.reg.second + 10 * pcf85063_.reg.second_10),
+                         .minute = uint8_t(pcf85063_.reg.minute + 10u * pcf85063_.reg.minute_10),
+                         .hour = uint8_t(pcf85063_.reg.hour + 10u * pcf85063_.reg.hour_10),
+                         .day_of_week = uint8_t(pcf85063_.reg.weekday),
+                         .day_of_month = uint8_t(pcf85063_.reg.day + 10u * pcf85063_.reg.day_10),
+                         .day_of_year = 1,  // ignored by recalc_timestamp_utc(false)
+                         .month = uint8_t(pcf85063_.reg.month + 10u * pcf85063_.reg.month_10),
+                         .year = uint16_t(pcf85063_.reg.year + 10u * pcf85063_.reg.year_10 + 2000)};
+  rtc_time.recalc_timestamp_utc(false);
+  if (!rtc_time.is_valid()) {
+    ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
+    return;
+  }
+  time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp);
+}
+
+void PCF85063Component::write_time() {
+  auto now = time::RealTimeClock::utcnow();
+  if (!now.is_valid()) {
+    ESP_LOGE(TAG, "Invalid system time, not syncing to RTC.");
+    return;
+  }
+  pcf85063_.reg.year = (now.year - 2000) % 10;
+  pcf85063_.reg.year_10 = (now.year - 2000) / 10 % 10;
+  pcf85063_.reg.month = now.month % 10;
+  pcf85063_.reg.month_10 = now.month / 10;
+  pcf85063_.reg.day = now.day_of_month % 10;
+  pcf85063_.reg.day_10 = now.day_of_month / 10;
+  pcf85063_.reg.weekday = now.day_of_week;
+  pcf85063_.reg.hour = now.hour % 10;
+  pcf85063_.reg.hour_10 = now.hour / 10;
+  pcf85063_.reg.minute = now.minute % 10;
+  pcf85063_.reg.minute_10 = now.minute / 10;
+  pcf85063_.reg.second = now.second % 10;
+  pcf85063_.reg.second_10 = now.second / 10;
+  pcf85063_.reg.osc_stop = false;
+
+  this->write_rtc_();
+}
+
+bool PCF85063Component::read_rtc_() {
+  if (!this->read_bytes(0, this->pcf85063_.raw, sizeof(this->pcf85063_.raw))) {
+    ESP_LOGE(TAG, "Can't read I2C data.");
+    return false;
+  }
+  ESP_LOGD(TAG, "Read  %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u  OSC:%s CLKOUT:%0u", pcf85063_.reg.hour_10,
+           pcf85063_.reg.hour, pcf85063_.reg.minute_10, pcf85063_.reg.minute, pcf85063_.reg.second_10,
+           pcf85063_.reg.second, pcf85063_.reg.year_10, pcf85063_.reg.year, pcf85063_.reg.month_10, pcf85063_.reg.month,
+           pcf85063_.reg.day_10, pcf85063_.reg.day, ONOFF(!pcf85063_.reg.osc_stop), pcf85063_.reg.clkout_control);
+
+  return true;
+}
+
+bool PCF85063Component::write_rtc_() {
+  if (!this->write_bytes(0, this->pcf85063_.raw, sizeof(this->pcf85063_.raw))) {
+    ESP_LOGE(TAG, "Can't write I2C data.");
+    return false;
+  }
+  ESP_LOGD(TAG, "Write %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u  OSC:%s CLKOUT:%0u", pcf85063_.reg.hour_10,
+           pcf85063_.reg.hour, pcf85063_.reg.minute_10, pcf85063_.reg.minute, pcf85063_.reg.second_10,
+           pcf85063_.reg.second, pcf85063_.reg.year_10, pcf85063_.reg.year, pcf85063_.reg.month_10, pcf85063_.reg.month,
+           pcf85063_.reg.day_10, pcf85063_.reg.day, ONOFF(!pcf85063_.reg.osc_stop), pcf85063_.reg.clkout_control);
+  return true;
+}
+}  // namespace pcf85063
+}  // namespace esphome
diff --git a/esphome/components/pcf85063/pcf85063.h b/esphome/components/pcf85063/pcf85063.h
new file mode 100644
index 0000000000..1a3fd704e5
--- /dev/null
+++ b/esphome/components/pcf85063/pcf85063.h
@@ -0,0 +1,96 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/i2c/i2c.h"
+#include "esphome/components/time/real_time_clock.h"
+
+namespace esphome {
+namespace pcf85063 {
+
+class PCF85063Component : public time::RealTimeClock, public i2c::I2CDevice {
+ public:
+  void setup() override;
+  void update() override;
+  void dump_config() override;
+  float get_setup_priority() const override;
+  void read_time();
+  void write_time();
+
+ protected:
+  bool read_rtc_();
+  bool write_rtc_();
+  union PCF85063Reg {
+    struct {
+      // Control_1 register
+      bool cap_12pf : 1;
+      bool am_pm : 1;
+      bool correction_int_enable : 1;
+      bool : 1;
+      bool soft_reset : 1;
+      bool stop : 1;
+      bool : 1;
+      bool ext_test : 1;
+
+      // Control_2 register
+      uint8_t clkout_control : 3;
+      bool timer_flag : 1;
+      bool halfminute_int : 1;
+      bool minute_int : 1;
+      bool alarm_flag : 1;
+      bool alarm_int : 1;
+
+      // Offset register
+      uint8_t offset : 7;
+      bool coarse_mode : 1;
+
+      // nvRAM register
+      uint8_t nvram : 8;
+
+      // Seconds register
+      uint8_t second : 4;
+      uint8_t second_10 : 3;
+      bool osc_stop : 1;
+
+      // Minutes register
+      uint8_t minute : 4;
+      uint8_t minute_10 : 3;
+      uint8_t : 1;
+
+      // Hours register
+      uint8_t hour : 4;
+      uint8_t hour_10 : 2;
+      uint8_t : 2;
+
+      // Days register
+      uint8_t day : 4;
+      uint8_t day_10 : 2;
+      uint8_t : 2;
+
+      // Weekdays register
+      uint8_t weekday : 3;
+      uint8_t unused_3 : 5;
+
+      // Months register
+      uint8_t month : 4;
+      uint8_t month_10 : 1;
+      uint8_t : 3;
+
+      // Years register
+      uint8_t year : 4;
+      uint8_t year_10 : 4;
+    } reg;
+    mutable uint8_t raw[sizeof(reg)];
+  } pcf85063_;
+};
+
+template<typename... Ts> class WriteAction : public Action<Ts...>, public Parented<PCF85063Component> {
+ public:
+  void play(Ts... x) override { this->parent_->write_time(); }
+};
+
+template<typename... Ts> class ReadAction : public Action<Ts...>, public Parented<PCF85063Component> {
+ public:
+  void play(Ts... x) override { this->parent_->read_time(); }
+};
+}  // namespace pcf85063
+}  // namespace esphome
diff --git a/esphome/components/pcf85063/time.py b/esphome/components/pcf85063/time.py
new file mode 100644
index 0000000000..67ec230b5f
--- /dev/null
+++ b/esphome/components/pcf85063/time.py
@@ -0,0 +1,60 @@
+import esphome.config_validation as cv
+import esphome.codegen as cg
+from esphome import automation
+from esphome.components import i2c, time
+from esphome.const import CONF_ID
+
+
+CODEOWNERS = ["@brogon"]
+DEPENDENCIES = ["i2c"]
+pcf85063_ns = cg.esphome_ns.namespace("pcf85063")
+PCF85063Component = pcf85063_ns.class_(
+    "PCF85063Component", time.RealTimeClock, i2c.I2CDevice
+)
+WriteAction = pcf85063_ns.class_("WriteAction", automation.Action)
+ReadAction = pcf85063_ns.class_("ReadAction", automation.Action)
+
+
+CONFIG_SCHEMA = time.TIME_SCHEMA.extend(
+    {
+        cv.GenerateID(): cv.declare_id(PCF85063Component),
+    }
+).extend(i2c.i2c_device_schema(0x51))
+
+
+@automation.register_action(
+    "pcf85063.write_time",
+    WriteAction,
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.use_id(PCF85063Component),
+        }
+    ),
+)
+async def pcf85063_write_time_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    await cg.register_parented(var, config[CONF_ID])
+    return var
+
+
+@automation.register_action(
+    "pcf85063.read_time",
+    ReadAction,
+    automation.maybe_simple_id(
+        {
+            cv.GenerateID(): cv.use_id(PCF85063Component),
+        }
+    ),
+)
+async def pcf85063_read_time_to_code(config, action_id, template_arg, args):
+    var = cg.new_Pvariable(action_id, template_arg)
+    await cg.register_parented(var, config[CONF_ID])
+    return var
+
+
+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)
+    await time.register_time(var, config)
diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp
index fc012aaa39..1b3ddebcc5 100644
--- a/esphome/components/pid/pid_autotuner.cpp
+++ b/esphome/components/pid/pid_autotuner.cpp
@@ -1,6 +1,10 @@
 #include "pid_autotuner.h"
 #include "esphome/core/log.h"
 
+#ifndef M_PI
+#define M_PI 3.1415926535897932384626433
+#endif
+
 namespace esphome {
 namespace pid {
 
@@ -73,7 +77,7 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce
   }
 
   if (!std::isnan(this->setpoint_) && this->setpoint_ != setpoint) {
-    ESP_LOGW(TAG, "Setpoint changed during autotune! The result will not be accurate!");
+    ESP_LOGW(TAG, "%s: Setpoint changed during autotune! The result will not be accurate!", this->id_.c_str());
   }
   this->setpoint_ = setpoint;
 
@@ -87,7 +91,7 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce
 
   if (!this->frequency_detector_.has_enough_data() || !this->amplitude_detector_.has_enough_data()) {
     // not enough data for calculation yet
-    ESP_LOGV(TAG, "  Not enough data yet for aututuner");
+    ESP_LOGV(TAG, "%s:   Not enough data yet for autotuner", this->id_.c_str());
     return res;
   }
 
@@ -97,12 +101,13 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce
     // The frequency/amplitude is not fully accurate yet, try to wait
     // until the fault clears, or terminate after a while anyway
     if (zc_symmetrical) {
-      ESP_LOGVV(TAG, "  ZC is not symmetrical");
+      ESP_LOGVV(TAG, "%s:   ZC is not symmetrical", this->id_.c_str());
     }
     if (amplitude_convergent) {
-      ESP_LOGVV(TAG, "  Amplitude is not convergent");
+      ESP_LOGVV(TAG, "%s:   Amplitude is not convergent", this->id_.c_str());
     }
     uint32_t phase = this->relay_function_.phase_count;
+    ESP_LOGVV(TAG, "%s: >", this->id_.c_str());
     ESP_LOGVV(TAG, "  Phase %u, enough=%u", phase, enough_data_phase_);
 
     if (this->enough_data_phase_ == 0) {
@@ -116,7 +121,7 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce
     }
   }
 
-  ESP_LOGI(TAG, "PID Autotune finished!");
+  ESP_LOGI(TAG, "%s: PID Autotune finished!", this->id_.c_str());
 
   float osc_ampl = this->amplitude_detector_.get_mean_oscillation_amplitude();
   float d = (this->relay_function_.output_positive - this->relay_function_.output_negative) / 2.0f;
@@ -131,12 +136,12 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce
   return res;
 }
 void PIDAutotuner::dump_config() {
-  ESP_LOGI(TAG, "PID Autotune:");
   if (this->state_ == AUTOTUNE_SUCCEEDED) {
+    ESP_LOGI(TAG, "%s: PID Autotune:", this->id_.c_str());
     ESP_LOGI(TAG, "  State: Succeeded!");
     bool has_issue = false;
     if (!this->amplitude_detector_.is_amplitude_convergent()) {
-      ESP_LOGW(TAG, "  Could not reliable determine oscillation amplitude, PID parameters may be inaccurate!");
+      ESP_LOGW(TAG, "  Could not reliably determine oscillation amplitude, PID parameters may be inaccurate!");
       ESP_LOGW(TAG, "    Please make sure you eliminate all outside influences on the measured temperature.");
       has_issue = true;
     }
@@ -173,10 +178,12 @@ void PIDAutotuner::dump_config() {
     print_rule_("Pessen Integral PID", 0.7f, 1.75f, 0.105f);
     print_rule_("Some Overshoot PID", 0.333f, 0.667f, 0.111f);
     print_rule_("No Overshoot PID", 0.2f, 0.4f, 0.0625f);
+    ESP_LOGI(TAG, "%s: Autotune completed", this->id_.c_str());
   }
 
   if (this->state_ == AUTOTUNE_RUNNING) {
-    ESP_LOGI(TAG, "  Autotune is still running!");
+    ESP_LOGD(TAG, "%s: PID Autotune:", this->id_.c_str());
+    ESP_LOGD(TAG, "  Autotune is still running!");
     ESP_LOGD(TAG, "  Status: Trying to reach %.2f °C", setpoint_ - relay_function_.current_target_error());
     ESP_LOGD(TAG, "  Stats so far:");
     ESP_LOGD(TAG, "    Phases: %u", relay_function_.phase_count);
@@ -221,7 +228,6 @@ float PIDAutotuner::RelayFunction::update(float error) {
   float output = state == RELAY_FUNCTION_POSITIVE ? output_positive : output_negative;
   if (change) {
     this->phase_count++;
-    ESP_LOGV(TAG, "Autotune: Turning output to %.1f%%", output * 100);
   }
 
   return output;
@@ -245,10 +251,8 @@ void PIDAutotuner::OscillationFrequencyDetector::update(uint32_t now, float erro
 
   if (had_crossing) {
     // Had crossing above hysteresis threshold, record
-    ESP_LOGV(TAG, "Autotune: Detected Zero-Cross at %u", now);
     if (this->last_zerocross != 0) {
       uint32_t dt = now - this->last_zerocross;
-      ESP_LOGV(TAG, "  dt: %u", dt);
       this->zerocrossing_intervals.push_back(dt);
     }
     this->last_zerocross = now;
@@ -297,13 +301,11 @@ void PIDAutotuner::OscillationAmplitudeDetector::update(float error,
       // The positive error peak must have been in previous segment (180° shifted)
       // record phase_max
       this->phase_maxs.push_back(phase_max);
-      ESP_LOGV(TAG, "Autotune: Phase Max: %f", phase_max);
     } else if (last_relay_state == RelayFunction::RELAY_FUNCTION_NEGATIVE) {
       // Transitioned from negative error to positive error.
       // The negative error peak must have been in previous segment (180° shifted)
       // record phase_min
       this->phase_mins.push_back(phase_min);
-      ESP_LOGV(TAG, "Autotune: Phase Min: %f", phase_min);
     }
     // reset phase values for next phase
     this->phase_min = error;
diff --git a/esphome/components/pid/pid_autotuner.h b/esphome/components/pid/pid_autotuner.h
index 88716d2b89..98dc02bcc4 100644
--- a/esphome/components/pid/pid_autotuner.h
+++ b/esphome/components/pid/pid_autotuner.h
@@ -31,6 +31,8 @@ class PIDAutotuner {
 
   void dump_config();
 
+  void set_autotuner_id(std::string id) { this->id_ = std::move(id); }
+
   void set_noiseband(float noiseband) {
     relay_function_.noiseband = noiseband;
     // ZC detector uses 1/4 the noiseband of relay function (noise suppression)
@@ -106,6 +108,7 @@ class PIDAutotuner {
   } state_ = AUTOTUNE_RUNNING;
   float ku_;
   float pu_;
+  std::string id_;
 };
 
 }  // namespace pid
diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp
index 81c3e1f12e..dab4502d40 100644
--- a/esphome/components/pid/pid_climate.cpp
+++ b/esphome/components/pid/pid_climate.cpp
@@ -130,9 +130,6 @@ void PIDClimate::update_pid_() {
         // keep autotuner instance so that subsequent dump_configs will print the long result message.
       } else {
         value = res.output;
-        if (mode != climate::CLIMATE_MODE_HEAT_COOL) {
-          ESP_LOGW(TAG, "For PID autotuner you need to set AUTO (also called heat/cool) mode!");
-        }
       }
     }
   }
@@ -151,10 +148,24 @@ void PIDClimate::start_autotune(std::unique_ptr<PIDAutotuner> &&autotune) {
   float min_value = this->supports_cool_() ? -1.0f : 0.0f;
   float max_value = this->supports_heat_() ? 1.0f : 0.0f;
   this->autotuner_->config(min_value, max_value);
+  this->autotuner_->set_autotuner_id(this->get_object_id());
+
+  ESP_LOGI(TAG,
+           "%s: Autotune has started. This can take a long time depending on the "
+           "responsiveness of your system. Your system "
+           "output will be altered to deliberately oscillate above and below the setpoint multiple times. "
+           "Until your sensor provides a reading, the autotuner may display \'nan\'",
+           this->get_object_id().c_str());
+
   this->set_interval("autotune-progress", 10000, [this]() {
     if (this->autotuner_ != nullptr && !this->autotuner_->is_finished())
       this->autotuner_->dump_config();
   });
+
+  if (mode != climate::CLIMATE_MODE_HEAT_COOL) {
+    ESP_LOGW(TAG, "%s: !!! For PID autotuner you need to set AUTO (also called heat/cool) mode!",
+             this->get_object_id().c_str());
+  }
 }
 
 void PIDClimate::reset_integral_term() { this->controller_.reset_accumulated_integral(); }
diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp
index 52b7261f8b..d0c627313c 100644
--- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp
+++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp
@@ -11,43 +11,48 @@ void PulseMeterSensor::setup() {
   this->isr_pin_ = pin_->to_isr();
   this->pin_->attach_interrupt(PulseMeterSensor::gpio_intr, this, gpio::INTERRUPT_ANY_EDGE);
 
-  this->pulse_width_us_ = 0;
   this->last_detected_edge_us_ = 0;
-  this->last_valid_high_edge_us_ = 0;
-  this->last_valid_low_edge_us_ = 0;
+  this->last_valid_edge_us_ = 0;
+  this->pulse_width_us_ = 0;
   this->sensor_is_high_ = this->isr_pin_.digital_read();
-  this->has_valid_high_edge_ = false;
-  this->has_valid_low_edge_ = false;
+  this->has_valid_edge_ = false;
+  this->pending_state_change_ = NONE;
 }
+// In PULSE mode we set a flag (pending_state_change_) for every interrupt
+// that constitutes a state change. In the loop() method we check if a time
+// interval greater than the internal_filter time has passed without any
+// interrupts.
 
 void PulseMeterSensor::loop() {
-  // Get a local copy of the volatile sensor values, to make sure they are not
-  // modified by the ISR. This could cause overflow in the following arithmetic
-  const uint32_t last_valid_high_edge_us = this->last_valid_high_edge_us_;
-  const bool has_valid_high_edge = this->has_valid_high_edge_;
+  // Get a snapshot of the needed volatile sensor values, to make sure they are not
+  // modified by the ISR while we are in the loop() method. If they are changed
+  // after we the variable "now" has been set, overflow will occur in the
+  // subsequent arithmetic
+  const bool has_valid_edge = this->has_valid_edge_;
+  const uint32_t last_detected_edge_us = this->last_detected_edge_us_;
+  const uint32_t last_valid_edge_us = this->last_valid_edge_us_;
+  // Get the current time after the snapshot of saved times
   const uint32_t now = micros();
 
-  // If we've exceeded our timeout interval without receiving any pulses, assume
-  // 0 pulses/min until we get at least two valid pulses.
-  const uint32_t time_since_valid_edge_us = now - last_valid_high_edge_us;
-  if ((has_valid_high_edge) && (time_since_valid_edge_us > this->timeout_us_)) {
+  this->handle_state_change_(now, last_detected_edge_us, last_valid_edge_us, has_valid_edge);
+
+  // If we've exceeded our timeout interval without receiving any pulses, assume 0 pulses/min until
+  // we get at least two valid pulses.
+  const uint32_t time_since_valid_edge_us = now - last_detected_edge_us;
+  if ((has_valid_edge) && (time_since_valid_edge_us > this->timeout_us_)) {
     ESP_LOGD(TAG, "No pulse detected for %us, assuming 0 pulses/min", time_since_valid_edge_us / 1000000);
 
+    this->last_valid_edge_us_ = 0;
     this->pulse_width_us_ = 0;
+    this->has_valid_edge_ = false;
     this->last_detected_edge_us_ = 0;
-    this->last_valid_high_edge_us_ = 0;
-    this->last_valid_low_edge_us_ = 0;
-    this->has_detected_edge_ = false;
-    this->has_valid_high_edge_ = false;
-    this->has_valid_low_edge_ = false;
   }
 
   // We quantize our pulse widths to 1 ms to avoid unnecessary jitter
   const uint32_t pulse_width_ms = this->pulse_width_us_ / 1000;
   if (this->pulse_width_dedupe_.next(pulse_width_ms)) {
     if (pulse_width_ms == 0) {
-      // Treat 0 pulse width as 0 pulses/min (normally because we've not
-      // detected any pulses for a while)
+      // Treat 0 pulse width as 0 pulses/min (normally because we've not detected any pulses for a while)
       this->publish_state(0);
     } else {
       // Calculate pulses/min from the pulse width in ms
@@ -77,58 +82,95 @@ void PulseMeterSensor::dump_config() {
 }
 
 void IRAM_ATTR PulseMeterSensor::gpio_intr(PulseMeterSensor *sensor) {
-  // This is an interrupt handler - we can't call any virtual method from this
-  // method
-
-  // Get the current time before we do anything else so the measurements are
-  // consistent
+  // This is an interrupt handler - we can't call any virtual method from this method
+  // Get the current time before we do anything else so the measurements are consistent
   const uint32_t now = micros();
+  const bool pin_val = sensor->isr_pin_.digital_read();
 
-  // We only look at rising edges in EDGE mode, and all edges in PULSE mode
   if (sensor->filter_mode_ == FILTER_EDGE) {
-    if (sensor->isr_pin_.digital_read()) {
-      sensor->last_detected_edge_us_ = now;
-    }
-  }
-
-  // Check to see if we should filter this edge out
-  if (sensor->filter_mode_ == FILTER_EDGE) {
-    if ((sensor->last_detected_edge_us_ - sensor->last_valid_high_edge_us_) >= sensor->filter_us_) {
-      // Don't measure the first valid pulse (we need at least two pulses to
-      // measure the width)
-      if (sensor->has_valid_high_edge_) {
-        sensor->pulse_width_us_ = (sensor->last_detected_edge_us_ - sensor->last_valid_high_edge_us_);
-      }
-      sensor->total_pulses_++;
-      sensor->last_valid_high_edge_us_ = sensor->last_detected_edge_us_;
-      sensor->has_valid_high_edge_ = true;
-    }
-  } else {
-    // Filter Mode is PULSE
-    bool pin_val = sensor->isr_pin_.digital_read();
-    // Ignore false edges that may be caused by bouncing and exit the ISR ASAP
-    if (pin_val == sensor->sensor_is_high_) {
+    // We only look at rising edges
+    if (!pin_val) {
       return;
     }
-    // Make sure the signal has been stable long enough
-    if (sensor->has_detected_edge_ && (now - sensor->last_detected_edge_us_ >= sensor->filter_us_)) {
-      if (pin_val) {
-        sensor->has_valid_high_edge_ = true;
-        sensor->last_valid_high_edge_us_ = sensor->last_detected_edge_us_;
-        sensor->sensor_is_high_ = true;
-      } else {
-        // Count pulses when a sufficiently long high pulse is concluded.
-        sensor->total_pulses_++;
-        if (sensor->has_valid_low_edge_) {
-          sensor->pulse_width_us_ = sensor->last_detected_edge_us_ - sensor->last_valid_low_edge_us_;
-        }
-        sensor->has_valid_low_edge_ = true;
-        sensor->last_valid_low_edge_us_ = sensor->last_detected_edge_us_;
-        sensor->sensor_is_high_ = false;
+    // Check to see if we should filter this edge out
+    if ((now - sensor->last_detected_edge_us_) >= sensor->filter_us_) {
+      // Don't measure the first valid pulse (we need at least two pulses to measure the width)
+      if (sensor->has_valid_edge_) {
+        sensor->pulse_width_us_ = (now - sensor->last_valid_edge_us_);
       }
+      sensor->total_pulses_++;
+      sensor->last_valid_edge_us_ = now;
+      sensor->has_valid_edge_ = true;
     }
-    sensor->has_detected_edge_ = true;
     sensor->last_detected_edge_us_ = now;
+  } else {
+    // Filter Mode is PULSE
+    const uint32_t delta_t_us = now - sensor->last_detected_edge_us_;
+    // We need to check if we have missed to handle a state change in the
+    // loop() function. This can happen when the filter_us value is less than
+    // the loop() interval, which is ~50-60ms
+    // The section below is essentially a modified repeat of the
+    // handle_state_change method. Ideally i would refactor and call the
+    // method here as well. However functions called in ISRs need to meet
+    // strict criteria and I don't think the methos would meet them.
+    if (sensor->pending_state_change_ != NONE && (delta_t_us > sensor->filter_us_)) {
+      // We have missed to handle a state change in the loop function.
+      sensor->sensor_is_high_ = sensor->pending_state_change_ == TO_HIGH;
+      if (sensor->sensor_is_high_) {
+        // We need to handle a pulse that would have been missed by the loop function
+        sensor->total_pulses_++;
+        if (sensor->has_valid_edge_) {
+          sensor->pulse_width_us_ = sensor->last_detected_edge_us_ - sensor->last_valid_edge_us_;
+          sensor->has_valid_edge_ = true;
+          sensor->last_valid_edge_us_ = sensor->last_detected_edge_us_;
+        }
+      }
+    }  // End of checking for and handling of change in state
+
+    // Ignore false edges that may be caused by bouncing and exit the ISR ASAP
+    if (pin_val == sensor->sensor_is_high_) {
+      sensor->pending_state_change_ = NONE;
+      return;
+    }
+    sensor->pending_state_change_ = pin_val ? TO_HIGH : TO_LOW;
+    sensor->last_detected_edge_us_ = now;
+  }
+}
+
+void PulseMeterSensor::handle_state_change_(uint32_t now, uint32_t last_detected_edge_us, uint32_t last_valid_edge_us,
+                                            bool has_valid_edge) {
+  if (this->pending_state_change_ == NONE) {
+    return;
+  }
+
+  const bool pin_val = this->isr_pin_.digital_read();
+  if (pin_val == this->sensor_is_high_) {
+    // Most likely caused by high frequency bouncing. Theoretically we should
+    // expect interrupts of alternating state. Here we are registering an
+    // interrupt with no change in state. Another interrupt will likely trigger
+    // just after this one and have an alternate state.
+    this->pending_state_change_ = NONE;
+    return;
+  }
+
+  if ((now - last_detected_edge_us) > this->filter_us_) {
+    this->sensor_is_high_ = pin_val;
+    ESP_LOGVV(TAG, "State is now %s", pin_val ? "high" : "low");
+
+    // Increment with valid rising edges only
+    if (pin_val) {
+      this->total_pulses_++;
+      ESP_LOGVV(TAG, "Incremented pulses to %u", this->total_pulses_);
+
+      if (has_valid_edge) {
+        this->pulse_width_us_ = last_detected_edge_us - last_valid_edge_us;
+        ESP_LOGVV(TAG, "Set pulse width to %u", this->pulse_width_us_);
+      }
+      this->has_valid_edge_ = true;
+      this->last_valid_edge_us_ = last_detected_edge_us;
+      ESP_LOGVV(TAG, "last_valid_edge_us_ is now %u", this->last_valid_edge_us_);
+    }
+    this->pending_state_change_ = NONE;
   }
 }
 
diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.h b/esphome/components/pulse_meter/pulse_meter_sensor.h
index ed4fb2a1f4..47af6e2398 100644
--- a/esphome/components/pulse_meter/pulse_meter_sensor.h
+++ b/esphome/components/pulse_meter/pulse_meter_sensor.h
@@ -29,7 +29,11 @@ class PulseMeterSensor : public sensor::Sensor, public Component {
   void dump_config() override;
 
  protected:
+  enum StateChange { TO_LOW = 0, TO_HIGH, NONE };
+
   static void gpio_intr(PulseMeterSensor *sensor);
+  void handle_state_change_(uint32_t now, uint32_t last_detected_edge_us, uint32_t last_valid_edge_us,
+                            bool has_valid_edge);
 
   InternalGPIOPin *pin_{nullptr};
   ISRInternalGPIOPin isr_pin_;
@@ -42,14 +46,12 @@ class PulseMeterSensor : public sensor::Sensor, public Component {
   Deduplicator<uint32_t> total_dedupe_;
 
   volatile uint32_t last_detected_edge_us_ = 0;
-  volatile uint32_t last_valid_high_edge_us_ = 0;
-  volatile uint32_t last_valid_low_edge_us_ = 0;
+  volatile uint32_t last_valid_edge_us_ = 0;
   volatile uint32_t pulse_width_us_ = 0;
   volatile uint32_t total_pulses_ = 0;
   volatile bool sensor_is_high_ = false;
-  volatile bool has_detected_edge_ = false;
-  volatile bool has_valid_high_edge_ = false;
-  volatile bool has_valid_low_edge_ = false;
+  volatile bool has_valid_edge_ = false;
+  volatile StateChange pending_state_change_{NONE};
 };
 
 }  // namespace pulse_meter
diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py
index b979a050db..c3149ce430 100644
--- a/esphome/components/remote_base/__init__.py
+++ b/esphome/components/remote_base/__init__.py
@@ -237,6 +237,107 @@ async def build_dumpers(config):
     return dumpers
 
 
+# CanalSat
+(
+    CanalSatData,
+    CanalSatBinarySensor,
+    CanalSatTrigger,
+    CanalSatAction,
+    CanalSatDumper,
+) = declare_protocol("CanalSat")
+CANALSAT_SCHEMA = cv.Schema(
+    {
+        cv.Required(CONF_DEVICE): cv.hex_uint8_t,
+        cv.Optional(CONF_ADDRESS, default=0): cv.hex_uint8_t,
+        cv.Required(CONF_COMMAND): cv.hex_uint8_t,
+    }
+)
+
+
+@register_binary_sensor("canalsat", CanalSatBinarySensor, CANALSAT_SCHEMA)
+def canalsat_binary_sensor(var, config):
+    cg.add(
+        var.set_data(
+            cg.StructInitializer(
+                CanalSatData,
+                ("device", config[CONF_DEVICE]),
+                ("address", config[CONF_ADDRESS]),
+                ("command", config[CONF_COMMAND]),
+            )
+        )
+    )
+
+
+@register_trigger("canalsat", CanalSatTrigger, CanalSatData)
+def canalsat_trigger(var, config):
+    pass
+
+
+@register_dumper("canalsat", CanalSatDumper)
+def canalsat_dumper(var, config):
+    pass
+
+
+@register_action("canalsat", CanalSatAction, CANALSAT_SCHEMA)
+async def canalsat_action(var, config, args):
+    template_ = await cg.templatable(config[CONF_DEVICE], args, cg.uint8)
+    cg.add(var.set_device(template_))
+    template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8)
+    cg.add(var.set_address(template_))
+    template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8)
+    cg.add(var.set_command(template_))
+
+
+(
+    CanalSatLDData,
+    CanalSatLDBinarySensor,
+    CanalSatLDTrigger,
+    CanalSatLDAction,
+    CanalSatLDDumper,
+) = declare_protocol("CanalSatLD")
+CANALSATLD_SCHEMA = cv.Schema(
+    {
+        cv.Required(CONF_DEVICE): cv.hex_uint8_t,
+        cv.Optional(CONF_ADDRESS, default=0): cv.hex_uint8_t,
+        cv.Required(CONF_COMMAND): cv.hex_uint8_t,
+    }
+)
+
+
+@register_binary_sensor("canalsatld", CanalSatLDBinarySensor, CANALSAT_SCHEMA)
+def canalsatld_binary_sensor(var, config):
+    cg.add(
+        var.set_data(
+            cg.StructInitializer(
+                CanalSatLDData,
+                ("device", config[CONF_DEVICE]),
+                ("address", config[CONF_ADDRESS]),
+                ("command", config[CONF_COMMAND]),
+            )
+        )
+    )
+
+
+@register_trigger("canalsatld", CanalSatLDTrigger, CanalSatLDData)
+def canalsatld_trigger(var, config):
+    pass
+
+
+@register_dumper("canalsatld", CanalSatLDDumper)
+def canalsatld_dumper(var, config):
+    pass
+
+
+@register_action("canalsatld", CanalSatLDAction, CANALSATLD_SCHEMA)
+async def canalsatld_action(var, config, args):
+    template_ = await cg.templatable(config[CONF_DEVICE], args, cg.uint8)
+    cg.add(var.set_device(template_))
+    template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8)
+    cg.add(var.set_address(template_))
+    template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8)
+    cg.add(var.set_command(template_))
+
+
 # Coolix
 (
     CoolixData,
diff --git a/esphome/components/remote_base/canalsat_protocol.cpp b/esphome/components/remote_base/canalsat_protocol.cpp
new file mode 100644
index 0000000000..1ea47750fd
--- /dev/null
+++ b/esphome/components/remote_base/canalsat_protocol.cpp
@@ -0,0 +1,108 @@
+#include "canalsat_protocol.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace remote_base {
+
+static const char *const CANALSAT_TAG = "remote.canalsat";
+static const char *const CANALSATLD_TAG = "remote.canalsatld";
+
+static const uint16_t CANALSAT_FREQ = 55500;
+static const uint16_t CANALSATLD_FREQ = 56000;
+static const uint16_t CANALSAT_UNIT = 250;
+static const uint16_t CANALSATLD_UNIT = 320;
+
+CanalSatProtocol::CanalSatProtocol() {
+  this->frequency_ = CANALSAT_FREQ;
+  this->unit_ = CANALSAT_UNIT;
+  this->tag_ = CANALSAT_TAG;
+}
+
+CanalSatLDProtocol::CanalSatLDProtocol() {
+  this->frequency_ = CANALSATLD_FREQ;
+  this->unit_ = CANALSATLD_UNIT;
+  this->tag_ = CANALSATLD_TAG;
+}
+
+void CanalSatBaseProtocol::encode(RemoteTransmitData *dst, const CanalSatData &data) {
+  dst->reserve(48);
+  dst->set_carrier_frequency(this->frequency_);
+
+  uint32_t raw{
+      static_cast<uint32_t>((1 << 23) | (data.device << 16) | (data.address << 10) | (0 << 9) | (data.command << 1))};
+  bool was_high{true};
+
+  for (uint32_t mask = 0x800000; mask; mask >>= 1) {
+    if (raw & mask) {
+      if (was_high) {
+        dst->mark(this->unit_);
+      }
+      was_high = true;
+      if (raw & mask >> 1) {
+        dst->space(this->unit_);
+      } else {
+        dst->space(this->unit_ * 2);
+      }
+    } else {
+      if (!was_high) {
+        dst->space(this->unit_);
+      }
+      was_high = false;
+      if (raw & mask >> 1) {
+        dst->mark(this->unit_ * 2);
+      } else {
+        dst->mark(this->unit_);
+      }
+    }
+  }
+}
+
+optional<CanalSatData> CanalSatBaseProtocol::decode(RemoteReceiveData src) {
+  CanalSatData data{
+      .device = 0,
+      .address = 0,
+      .repeat = 0,
+      .command = 0,
+  };
+
+  // Check if initial mark and spaces match
+  if (!src.peek_mark(this->unit_) || !(src.peek_space(this->unit_, 1) || src.peek_space(this->unit_ * 2, 1))) {
+    return {};
+  }
+
+  uint8_t bit{1};
+  uint8_t offset{1};
+  uint32_t buffer{0};
+
+  while (offset < 24) {
+    buffer = buffer | (bit << (24 - offset++));
+    src.advance();
+    if (src.peek_mark(this->unit_) || src.peek_space(this->unit_)) {
+      src.advance();
+    } else if (src.peek_mark(this->unit_ * 2) || src.peek_space(this->unit_ * 2)) {
+      bit = !bit;
+    } else if (offset != 24 && bit != 1) {  // If last bit is high, final space is indistinguishable
+      return {};
+    }
+  }
+
+  data.device = (0xFF0000 & buffer) >> 16;
+  data.address = (0x00FF00 & buffer) >> 10;
+  data.repeat = (0x00FF00 & buffer) >> 9;
+  data.command = (0x0000FF & buffer) >> 1;
+
+  return data;
+}
+
+void CanalSatBaseProtocol::dump(const CanalSatData &data) {
+  if (this->tag_ == CANALSATLD_TAG) {
+    ESP_LOGD(this->tag_, "Received CanalSatLD: device=0x%02X, address=0x%02X, command=0x%02X, repeat=0x%X", data.device,
+             data.address, data.command, data.repeat);
+  } else {
+    ESP_LOGD(this->tag_, "Received CanalSat: device=0x%02X, address=0x%02X, command=0x%02X, repeat=0x%X", data.device,
+             data.address, data.command, data.repeat);
+  }
+}
+
+}  // namespace remote_base
+}  // namespace esphome
diff --git a/esphome/components/remote_base/canalsat_protocol.h b/esphome/components/remote_base/canalsat_protocol.h
new file mode 100644
index 0000000000..180989ef99
--- /dev/null
+++ b/esphome/components/remote_base/canalsat_protocol.h
@@ -0,0 +1,78 @@
+#pragma once
+
+#include "remote_base.h"
+
+namespace esphome {
+namespace remote_base {
+
+struct CanalSatData {
+  uint8_t device : 7;
+  uint8_t address : 6;
+  uint8_t repeat : 1;
+  uint8_t command : 7;
+
+  bool operator==(const CanalSatData &rhs) const {
+    return device == rhs.device && address == rhs.address && command == rhs.command;
+  }
+};
+
+struct CanalSatLDData : public CanalSatData {};
+
+class CanalSatBaseProtocol : public RemoteProtocol<CanalSatData> {
+ public:
+  void encode(RemoteTransmitData *dst, const CanalSatData &data) override;
+  optional<CanalSatData> decode(RemoteReceiveData src) override;
+  void dump(const CanalSatData &data) override;
+
+ protected:
+  uint16_t frequency_;
+  uint16_t unit_;
+  const char *tag_;
+};
+
+class CanalSatProtocol : public CanalSatBaseProtocol {
+ public:
+  CanalSatProtocol();
+};
+
+class CanalSatLDProtocol : public CanalSatBaseProtocol {
+ public:
+  CanalSatLDProtocol();
+};
+
+DECLARE_REMOTE_PROTOCOL(CanalSat)
+
+template<typename... Ts> class CanalSatAction : public RemoteTransmitterActionBase<Ts...> {
+ public:
+  TEMPLATABLE_VALUE(uint8_t, device)
+  TEMPLATABLE_VALUE(uint8_t, address)
+  TEMPLATABLE_VALUE(uint8_t, command)
+
+  void encode(RemoteTransmitData *dst, Ts... x) {
+    CanalSatData data{};
+    data.device = this->device_.value(x...);
+    data.address = this->address_.value(x...);
+    data.command = this->command_.value(x...);
+    CanalSatProtocol().encode(dst, data);
+  }
+};
+
+DECLARE_REMOTE_PROTOCOL(CanalSatLD)
+
+template<typename... Ts> class CanalSatLDAction : public RemoteTransmitterActionBase<Ts...> {
+ public:
+  TEMPLATABLE_VALUE(uint8_t, device)
+  TEMPLATABLE_VALUE(uint8_t, address)
+  TEMPLATABLE_VALUE(uint8_t, command)
+
+  void encode(RemoteTransmitData *dst, Ts... x) {
+    CanalSatData data{};
+    data.device = this->device_.value(x...);
+    data.address = this->address_.value(x...);
+    data.command = this->command_.value(x...);
+    CanalSatLDProtocol().encode(dst, data);
+  }
+};
+
+}  // namespace remote_base
+}  // namespace esphome
diff --git a/esphome/components/rp2040_pwm/rp2040_pwm.cpp b/esphome/components/rp2040_pwm/rp2040_pwm.cpp
index 664a3734f7..3c5591885e 100644
--- a/esphome/components/rp2040_pwm/rp2040_pwm.cpp
+++ b/esphome/components/rp2040_pwm/rp2040_pwm.cpp
@@ -9,6 +9,7 @@
 #include <hardware/clocks.h>
 #include <hardware/gpio.h>
 #include <hardware/pwm.h>
+#include <cmath>
 
 namespace esphome {
 namespace rp2040_pwm {
@@ -23,8 +24,14 @@ void RP2040PWM::setup() {
 
 void RP2040PWM::setup_pwm_() {
   pwm_config config = pwm_get_default_config();
-  pwm_config_set_clkdiv(&config, clock_get_hz(clk_sys) / (255.0f * this->frequency_));
-  pwm_config_set_wrap(&config, 254);
+
+  uint32_t clock = clock_get_hz(clk_sys);
+  float divider = ceil(clock / (4096 * this->frequency_)) / 16.0f;
+  uint16_t wrap = clock / divider / this->frequency_ - 1;
+  this->wrap_ = wrap;
+
+  pwm_config_set_clkdiv(&config, divider);
+  pwm_config_set_wrap(&config, wrap);
   pwm_init(pwm_gpio_to_slice_num(this->pin_->get_pin()), &config, true);
 }
 
@@ -48,7 +55,7 @@ void HOT RP2040PWM::write_state(float state) {
   }
 
   gpio_set_function(this->pin_->get_pin(), GPIO_FUNC_PWM);
-  pwm_set_gpio_level(this->pin_->get_pin(), state * 255.0f);
+  pwm_set_gpio_level(this->pin_->get_pin(), state * this->wrap_);
 }
 
 }  // namespace rp2040_pwm
diff --git a/esphome/components/rp2040_pwm/rp2040_pwm.h b/esphome/components/rp2040_pwm/rp2040_pwm.h
index 9c88826ae9..e499e72b06 100644
--- a/esphome/components/rp2040_pwm/rp2040_pwm.h
+++ b/esphome/components/rp2040_pwm/rp2040_pwm.h
@@ -34,6 +34,7 @@ class RP2040PWM : public output::FloatOutput, public Component {
 
   InternalGPIOPin *pin_;
   float frequency_{1000.0};
+  uint16_t wrap_{65535};
   /// Cache last output level for dynamic frequency updating
   float last_output_{0.0};
   bool frequency_changed_{false};
diff --git a/esphome/components/scd30/automation.h b/esphome/components/scd30/automation.h
new file mode 100644
index 0000000000..37b3bc1674
--- /dev/null
+++ b/esphome/components/scd30/automation.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/automation.h"
+#include "scd30.h"
+
+namespace esphome {
+namespace scd30 {
+
+template<typename... Ts> class ForceRecalibrationWithReference : public Action<Ts...>, public Parented<SCD30Component> {
+ public:
+  void play(Ts... x) override {
+    if (this->value_.has_value()) {
+      this->parent_->force_recalibration_with_reference(this->value_.value(x...));
+    }
+  }
+
+ protected:
+  TEMPLATABLE_VALUE(uint16_t, value)
+};
+
+}  // namespace scd30
+}  // namespace esphome
diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp
index 103b7a255d..c25ed107b7 100644
--- a/esphome/components/scd30/scd30.cpp
+++ b/esphome/components/scd30/scd30.cpp
@@ -202,5 +202,27 @@ bool SCD30Component::is_data_ready_() {
   return is_data_ready == 1;
 }
 
+bool SCD30Component::force_recalibration_with_reference(uint16_t co2_reference) {
+  ESP_LOGD(TAG, "Performing CO2 force recalibration with reference %dppm.", co2_reference);
+  if (this->write_command(SCD30_CMD_FORCED_CALIBRATION, co2_reference)) {
+    ESP_LOGD(TAG, "Force recalibration complete.");
+    return true;
+  } else {
+    ESP_LOGE(TAG, "Failed to force recalibration with reference.");
+    this->error_code_ = FORCE_RECALIBRATION_FAILED;
+    this->status_set_warning();
+    return false;
+  }
+}
+
+uint16_t SCD30Component::get_forced_calibration_reference() {
+  uint16_t forced_calibration_reference;
+  // Get current CO2 calibration
+  if (!this->get_register(SCD30_CMD_FORCED_CALIBRATION, forced_calibration_reference)) {
+    ESP_LOGE(TAG, "Unable to read forced calibration reference.");
+  }
+  return forced_calibration_reference;
+}
+
 }  // namespace scd30
 }  // namespace esphome
diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h
index c434bf0dea..4a4ca832bf 100644
--- a/esphome/components/scd30/scd30.h
+++ b/esphome/components/scd30/scd30.h
@@ -20,6 +20,8 @@ class SCD30Component : public Component, public sensirion_common::SensirionI2CDe
   }
   void set_temperature_offset(float offset) { temperature_offset_ = offset; }
   void set_update_interval(uint16_t interval) { update_interval_ = interval; }
+  bool force_recalibration_with_reference(uint16_t co2_reference);
+  uint16_t get_forced_calibration_reference();
 
   void setup() override;
   void update();
@@ -33,6 +35,7 @@ class SCD30Component : public Component, public sensirion_common::SensirionI2CDe
     COMMUNICATION_FAILED,
     FIRMWARE_IDENTIFICATION_FAILED,
     MEASUREMENT_INIT_FAILED,
+    FORCE_RECALIBRATION_FAILED,
     UNKNOWN
   } error_code_{UNKNOWN};
   bool enable_asc_{true};
diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py
index 3cfd861a63..ffbf90338f 100644
--- a/esphome/components/scd30/sensor.py
+++ b/esphome/components/scd30/sensor.py
@@ -1,4 +1,4 @@
-from esphome import core
+from esphome import automation, core
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import i2c, sensor
@@ -9,6 +9,7 @@ from esphome.const import (
     CONF_TEMPERATURE,
     CONF_CO2,
     CONF_UPDATE_INTERVAL,
+    CONF_VALUE,
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
     STATE_CLASS_MEASUREMENT,
@@ -26,6 +27,11 @@ SCD30Component = scd30_ns.class_(
     "SCD30Component", cg.Component, sensirion_common.SensirionI2CDevice
 )
 
+# Actions
+ForceRecalibrationWithReference = scd30_ns.class_(
+    "ForceRecalibrationWithReference", automation.Action
+)
+
 CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration"
 CONF_ALTITUDE_COMPENSATION = "altitude_compensation"
 CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation"
@@ -106,3 +112,26 @@ async def to_code(config):
     if CONF_TEMPERATURE in config:
         sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature_sensor(sens))
+
+
+@automation.register_action(
+    "scd30.force_recalibration_with_reference",
+    ForceRecalibrationWithReference,
+    cv.maybe_simple_value(
+        {
+            cv.GenerateID(): cv.use_id(SCD30Component),
+            cv.Required(CONF_VALUE): cv.templatable(
+                cv.int_range(min=400, max=2000, max_included=True)
+            ),
+        },
+        key=CONF_VALUE,
+    ),
+)
+async def scd30_force_recalibration_with_reference_to_code(
+    config, action_id, template_arg, args
+):
+    var = cg.new_Pvariable(action_id, template_arg)
+    await cg.register_parented(var, config[CONF_ID])
+    template_ = await cg.templatable(config[CONF_VALUE], args, cg.uint16)
+    cg.add(var.set_value(template_))
+    return var
diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp
index 681324fa18..117b92b901 100644
--- a/esphome/components/scd4x/scd4x.cpp
+++ b/esphome/components/scd4x/scd4x.cpp
@@ -149,9 +149,9 @@ void SCD4XComponent::update() {
   }
 
   if (this->ambient_pressure_source_ != nullptr) {
-    float pressure = this->ambient_pressure_source_->state / 1000.0f;
+    float pressure = this->ambient_pressure_source_->state;
     if (!std::isnan(pressure)) {
-      set_ambient_pressure_compensation(this->ambient_pressure_source_->state / 1000.0f);
+      set_ambient_pressure_compensation(pressure);
     }
   }
 
@@ -254,12 +254,15 @@ bool SCD4XComponent::factory_reset() {
   return true;
 }
 
-// Note pressure in bar here. Convert to hPa
-void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_bar) {
+void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_hpa) {
   ambient_pressure_compensation_ = true;
-  uint16_t new_ambient_pressure = (uint16_t)(pressure_in_bar * 1000);
-  // remove millibar from comparison to avoid frequent updates +/- 10 millibar doesn't matter
-  if (initialized_ && (new_ambient_pressure / 10 != ambient_pressure_ / 10)) {
+  uint16_t new_ambient_pressure = (uint16_t) pressure_in_hpa;
+  if (!initialized_) {
+    ambient_pressure_ = new_ambient_pressure;
+    return;
+  }
+  // Only send pressure value if it has changed since last update
+  if (new_ambient_pressure != ambient_pressure_) {
     update_ambient_pressure_compensation_(new_ambient_pressure);
     ambient_pressure_ = new_ambient_pressure;
   } else {
diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h
index 23c3766e60..22055e78d0 100644
--- a/esphome/components/scd4x/scd4x.h
+++ b/esphome/components/scd4x/scd4x.h
@@ -26,7 +26,7 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri
 
   void set_automatic_self_calibration(bool asc) { enable_asc_ = asc; }
   void set_altitude_compensation(uint16_t altitude) { altitude_compensation_ = altitude; }
-  void set_ambient_pressure_compensation(float pressure_in_bar);
+  void set_ambient_pressure_compensation(float pressure_in_hpa);
   void set_ambient_pressure_source(sensor::Sensor *pressure) { ambient_pressure_source_ = pressure; }
   void set_temperature_offset(float offset) { temperature_offset_ = offset; };
 
diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py
index 2859dc6e5a..7842cef4de 100644
--- a/esphome/components/sensor/__init__.py
+++ b/esphome/components/sensor/__init__.py
@@ -55,6 +55,7 @@ from esphome.const import (
     DEVICE_CLASS_PM25,
     DEVICE_CLASS_POWER,
     DEVICE_CLASS_POWER_FACTOR,
+    DEVICE_CLASS_PRECIPITATION,
     DEVICE_CLASS_PRECIPITATION_INTENSITY,
     DEVICE_CLASS_PRESSURE,
     DEVICE_CLASS_REACTIVE_POWER,
@@ -103,6 +104,7 @@ DEVICE_CLASSES = [
     DEVICE_CLASS_PM25,
     DEVICE_CLASS_POWER,
     DEVICE_CLASS_POWER_FACTOR,
+    DEVICE_CLASS_PRECIPITATION,
     DEVICE_CLASS_PRECIPITATION_INTENSITY,
     DEVICE_CLASS_PRESSURE,
     DEVICE_CLASS_REACTIVE_POWER,
@@ -115,8 +117,8 @@ DEVICE_CLASSES = [
     DEVICE_CLASS_VOLTAGE,
     DEVICE_CLASS_VOLUME,
     DEVICE_CLASS_WATER,
-    DEVICE_CLASS_WIND_SPEED,
     DEVICE_CLASS_WEIGHT,
+    DEVICE_CLASS_WIND_SPEED,
 ]
 
 sensor_ns = cg.esphome_ns.namespace("sensor")
diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h
index 958230cb3c..bba69ed0bb 100644
--- a/esphome/components/sensor/sensor.h
+++ b/esphome/components/sensor/sensor.h
@@ -31,6 +31,13 @@ namespace sensor {
     } \
   }
 
+#define SUB_SENSOR(name) \
+ protected: \
+  sensor::Sensor *name##_sensor_{nullptr}; \
+\
+ public: \
+  void set_##name##_sensor(sensor::Sensor *sensor) { this->name##_sensor_ = sensor; }
+
 /**
  * Sensor state classes
  */
diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py
index 1f6c5006a5..3d24f6c409 100644
--- a/esphome/components/sgp4x/sensor.py
+++ b/esphome/components/sgp4x/sensor.py
@@ -6,8 +6,7 @@ from esphome.const import (
     CONF_STORE_BASELINE,
     CONF_TEMPERATURE_SOURCE,
     ICON_RADIATOR,
-    DEVICE_CLASS_NITROUS_OXIDE,
-    DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
+    DEVICE_CLASS_AQI,
     STATE_CLASS_MEASUREMENT,
 )
 
@@ -67,13 +66,13 @@ CONFIG_SCHEMA = cv.All(
             cv.Optional(CONF_VOC): sensor.sensor_schema(
                 icon=ICON_RADIATOR,
                 accuracy_decimals=0,
-                device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
+                device_class=DEVICE_CLASS_AQI,
                 state_class=STATE_CLASS_MEASUREMENT,
             ).extend(GAS_SENSOR),
             cv.Optional(CONF_NOX): sensor.sensor_schema(
                 icon=ICON_RADIATOR,
                 accuracy_decimals=0,
-                device_class=DEVICE_CLASS_NITROUS_OXIDE,
+                device_class=DEVICE_CLASS_AQI,
                 state_class=STATE_CLASS_MEASUREMENT,
             ).extend(GAS_SENSOR),
             cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,
diff --git a/esphome/components/shelly_dimmer/light.py b/esphome/components/shelly_dimmer/light.py
index 9050a872b1..20e0e8156b 100644
--- a/esphome/components/shelly_dimmer/light.py
+++ b/esphome/components/shelly_dimmer/light.py
@@ -22,6 +22,7 @@ from esphome.const import (
     UNIT_WATT,
     DEVICE_CLASS_POWER,
     DEVICE_CLASS_VOLTAGE,
+    DEVICE_CLASS_CURRENT,
 )
 from esphome.core import HexInt, CORE
 
@@ -169,7 +170,7 @@ CONFIG_SCHEMA = (
             ),
             cv.Optional(CONF_CURRENT): sensor.sensor_schema(
                 unit_of_measurement=UNIT_AMPERE,
-                device_class=DEVICE_CLASS_POWER,
+                device_class=DEVICE_CLASS_CURRENT,
                 accuracy_decimals=2,
             ),
             # Change the default gamma_correct setting.
diff --git a/esphome/components/sigma_delta_output/__init__.py b/esphome/components/sigma_delta_output/__init__.py
new file mode 100644
index 0000000000..3356e61bb2
--- /dev/null
+++ b/esphome/components/sigma_delta_output/__init__.py
@@ -0,0 +1 @@
+CODEOWNERS = ["@Cat-Ion"]
diff --git a/esphome/components/sigma_delta_output/output.py b/esphome/components/sigma_delta_output/output.py
new file mode 100644
index 0000000000..49ac9e685a
--- /dev/null
+++ b/esphome/components/sigma_delta_output/output.py
@@ -0,0 +1,66 @@
+from esphome import automation, pins
+from esphome.components import output
+import esphome.config_validation as cv
+import esphome.codegen as cg
+from esphome.const import (
+    CONF_ID,
+    CONF_PIN,
+    CONF_TURN_ON_ACTION,
+    CONF_TURN_OFF_ACTION,
+)
+
+DEPENDENCIES = []
+
+
+sigma_delta_output_ns = cg.esphome_ns.namespace("sigma_delta_output")
+SigmaDeltaOutput = sigma_delta_output_ns.class_(
+    "SigmaDeltaOutput", output.FloatOutput, cg.PollingComponent
+)
+
+CONF_STATE_CHANGE_ACTION = "state_change_action"
+
+CONFIG_SCHEMA = cv.All(
+    output.FLOAT_OUTPUT_SCHEMA.extend(cv.polling_component_schema("60s")).extend(
+        {
+            cv.Required(CONF_ID): cv.declare_id(SigmaDeltaOutput),
+            cv.Optional(CONF_PIN): pins.gpio_output_pin_schema,
+            cv.Optional(CONF_STATE_CHANGE_ACTION): automation.validate_automation(
+                single=True
+            ),
+            cv.Inclusive(
+                CONF_TURN_ON_ACTION,
+                "on_off",
+                f"{CONF_TURN_ON_ACTION} and {CONF_TURN_OFF_ACTION} must both be defined",
+            ): automation.validate_automation(single=True),
+            cv.Inclusive(
+                CONF_TURN_OFF_ACTION,
+                "on_off",
+                f"{CONF_TURN_ON_ACTION} and {CONF_TURN_OFF_ACTION} must both be defined",
+            ): automation.validate_automation(single=True),
+        }
+    ),
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await output.register_output(var, config)
+
+    if CONF_PIN in config:
+        pin = await cg.gpio_pin_expression(config[CONF_PIN])
+        cg.add(var.set_pin(pin))
+
+    if CONF_STATE_CHANGE_ACTION in config:
+        await automation.build_automation(
+            var.get_state_change_trigger(),
+            [(bool, "state")],
+            config[CONF_STATE_CHANGE_ACTION],
+        )
+    if CONF_TURN_ON_ACTION in config:
+        await automation.build_automation(
+            var.get_turn_on_trigger(), [], config[CONF_TURN_ON_ACTION]
+        )
+        await automation.build_automation(
+            var.get_turn_off_trigger(), [], config[CONF_TURN_OFF_ACTION]
+        )
diff --git a/esphome/components/sigma_delta_output/sigma_delta_output.h b/esphome/components/sigma_delta_output/sigma_delta_output.h
new file mode 100644
index 0000000000..5a5acd2dfb
--- /dev/null
+++ b/esphome/components/sigma_delta_output/sigma_delta_output.h
@@ -0,0 +1,66 @@
+#pragma once
+#include "esphome/core/component.h"
+#include "esphome/components/output/float_output.h"
+
+namespace esphome {
+namespace sigma_delta_output {
+class SigmaDeltaOutput : public PollingComponent, public output::FloatOutput {
+ public:
+  Trigger<> *get_turn_on_trigger() {
+    if (!this->turn_on_trigger_)
+      this->turn_on_trigger_ = make_unique<Trigger<>>();
+    return this->turn_on_trigger_.get();
+  }
+  Trigger<> *get_turn_off_trigger() {
+    if (!this->turn_off_trigger_)
+      this->turn_off_trigger_ = make_unique<Trigger<>>();
+    return this->turn_off_trigger_.get();
+  }
+
+  Trigger<bool> *get_state_change_trigger() {
+    if (!this->state_change_trigger_)
+      this->state_change_trigger_ = make_unique<Trigger<bool>>();
+    return this->state_change_trigger_.get();
+  }
+
+  void set_pin(GPIOPin *pin) { this->pin_ = pin; };
+  void write_state(float state) override { this->state_ = state; }
+  void update() override {
+    this->accum_ += this->state_;
+    const bool next_value = this->accum_ > 0;
+
+    if (next_value) {
+      this->accum_ -= 1.;
+    }
+
+    if (next_value != this->value_) {
+      this->value_ = next_value;
+      if (this->pin_) {
+        this->pin_->digital_write(next_value);
+      }
+
+      if (this->state_change_trigger_) {
+        this->state_change_trigger_->trigger(next_value);
+      }
+
+      if (next_value && this->turn_on_trigger_) {
+        this->turn_on_trigger_->trigger();
+      } else if (!next_value && this->turn_off_trigger_) {
+        this->turn_off_trigger_->trigger();
+      }
+    }
+  }
+
+ protected:
+  GPIOPin *pin_{nullptr};
+
+  std::unique_ptr<Trigger<>> turn_on_trigger_{nullptr};
+  std::unique_ptr<Trigger<>> turn_off_trigger_{nullptr};
+  std::unique_ptr<Trigger<bool>> state_change_trigger_{nullptr};
+
+  float accum_{0};
+  float state_{0.};
+  bool value_{false};
+};
+}  // namespace sigma_delta_output
+}  // namespace esphome
diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp
index 7015933e73..4f7aa228e9 100644
--- a/esphome/components/sim800l/sim800l.cpp
+++ b/esphome/components/sim800l/sim800l.cpp
@@ -156,6 +156,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
     case STATE_SEND_USSD1:
       this->send_cmd_("AT+CUSD=1, \"" + this->ussd_ + "\"");
       this->state_ = STATE_SEND_USSD2;
+      this->expect_ack_ = true;
       break;
     case STATE_SEND_USSD2:
       ESP_LOGD(TAG, "SendUssd2: '%s'", message.c_str());
diff --git a/esphome/components/slow_pwm/slow_pwm_output.cpp b/esphome/components/slow_pwm/slow_pwm_output.cpp
index 81c721a866..d6b2cdfe12 100644
--- a/esphome/components/slow_pwm/slow_pwm_output.cpp
+++ b/esphome/components/slow_pwm/slow_pwm_output.cpp
@@ -70,7 +70,7 @@ void SlowPWMOutput::dump_config() {
 void SlowPWMOutput::write_state(float state) {
   this->state_ = state;
   if (this->restart_cycle_on_state_change_)
-    this->period_start_time_ = millis();
+    this->restart_cycle();
 }
 
 }  // namespace slow_pwm
diff --git a/esphome/components/slow_pwm/slow_pwm_output.h b/esphome/components/slow_pwm/slow_pwm_output.h
index be45736864..3e5a3e2a40 100644
--- a/esphome/components/slow_pwm/slow_pwm_output.h
+++ b/esphome/components/slow_pwm/slow_pwm_output.h
@@ -14,6 +14,7 @@ class SlowPWMOutput : public output::FloatOutput, public Component {
   void set_restart_cycle_on_state_change(bool restart_cycle_on_state_change) {
     restart_cycle_on_state_change_ = restart_cycle_on_state_change;
   }
+  void restart_cycle() { this->period_start_time_ = millis(); }
 
   /// Initialize pin
   void setup() override;
diff --git a/esphome/components/sm10bit_base/__init__.py b/esphome/components/sm10bit_base/__init__.py
new file mode 100644
index 0000000000..8722bd35a9
--- /dev/null
+++ b/esphome/components/sm10bit_base/__init__.py
@@ -0,0 +1,44 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import pins
+from esphome.const import (
+    CONF_CLOCK_PIN,
+    CONF_DATA_PIN,
+    CONF_ID,
+)
+
+CODEOWNERS = ["@Cossid"]
+MULTI_CONF = True
+
+CONF_MAX_POWER_COLOR_CHANNELS = "max_power_color_channels"
+CONF_MAX_POWER_WHITE_CHANNELS = "max_power_white_channels"
+
+sm10bit_base_ns = cg.esphome_ns.namespace("sm10bit_base")
+Sm10BitBase = sm10bit_base_ns.class_("Sm10BitBase", cg.Component)
+
+SM10BIT_BASE_CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema,
+        cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema,
+        cv.Optional(CONF_MAX_POWER_COLOR_CHANNELS, default=2): cv.int_range(
+            min=0, max=15
+        ),
+        cv.Optional(CONF_MAX_POWER_WHITE_CHANNELS, default=4): cv.int_range(
+            min=0, max=15
+        ),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def register_sm10bit_base(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+
+    data = await cg.gpio_pin_expression(config[CONF_DATA_PIN])
+    cg.add(var.set_data_pin(data))
+    clock = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN])
+    cg.add(var.set_clock_pin(clock))
+    cg.add(var.set_max_power_color_channels(config[CONF_MAX_POWER_COLOR_CHANNELS]))
+    cg.add(var.set_max_power_white_channels(config[CONF_MAX_POWER_WHITE_CHANNELS]))
+
+    return var
diff --git a/esphome/components/sm10bit_base/sm10bit_base.cpp b/esphome/components/sm10bit_base/sm10bit_base.cpp
new file mode 100644
index 0000000000..9c7abb48e2
--- /dev/null
+++ b/esphome/components/sm10bit_base/sm10bit_base.cpp
@@ -0,0 +1,112 @@
+#include "sm10bit_base.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace sm10bit_base {
+
+static const char *const TAG = "sm10bit_base";
+
+static const uint8_t SM10BIT_ADDR_STANDBY = 0x0;
+static const uint8_t SM10BIT_ADDR_START_3CH = 0x8;
+static const uint8_t SM10BIT_ADDR_START_2CH = 0x10;
+static const uint8_t SM10BIT_ADDR_START_5CH = 0x18;
+
+// Power current values
+// HEX | Binary | RGB level | White level | Config value
+// 0x0 | 0000   | RGB 10mA  | CW 5mA      | 0
+// 0x1 | 0001   | RGB 20mA  | CW 10mA     | 1
+// 0x2 | 0010   | RGB 30mA  | CW 15mA     | 2 - Default spec color value
+// 0x3 | 0011   | RGB 40mA  | CW 20mA     | 3
+// 0x4 | 0100   | RGB 50mA  | CW 25mA     | 4 - Default spec white value
+// 0x5 | 0101   | RGB 60mA  | CW 30mA     | 5
+// 0x6 | 0110   | RGB 70mA  | CW 35mA     | 6
+// 0x7 | 0111   | RGB 80mA  | CW 40mA     | 7
+// 0x8 | 1000   | RGB 90mA  | CW 45mA     | 8
+// 0x9 | 1001   | RGB 100mA | CW 50mA     | 9
+// 0xA | 1010   | RGB 110mA | CW 55mA     | 10
+// 0xB | 1011   | RGB 120mA | CW 60mA     | 11
+// 0xC | 1100   | RGB 130mA | CW 65mA     | 12
+// 0xD | 1101   | RGB 140mA | CW 70mA     | 13
+// 0xE | 1110   | RGB 150mA | CW 75mA     | 14
+// 0xF | 1111   | RGB 160mA | CW 80mA     | 15
+
+void Sm10BitBase::loop() {
+  if (!this->update_)
+    return;
+
+  uint8_t data[12];
+  if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 &&
+      this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) {
+    // Off / Sleep
+    data[0] = this->model_id_ + SM10BIT_ADDR_STANDBY;
+    for (int i = 1; i < 12; i++)
+      data[i] = 0;
+    this->write_buffer_(data, 12);
+  } else if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 &&
+             (this->pwm_amounts_[3] > 0 || this->pwm_amounts_[4] > 0)) {
+    // Only data on white channels
+    data[0] = this->model_id_ + SM10BIT_ADDR_START_2CH;
+    data[1] = 0 << 4 | this->max_power_white_channels_;
+    for (int i = 2, j = 0; i < 12; i += 2, j++) {
+      data[i] = this->pwm_amounts_[j] >> 0x8;
+      data[i + 1] = this->pwm_amounts_[j] & 0xFF;
+    }
+    this->write_buffer_(data, 12);
+  } else if ((this->pwm_amounts_[0] > 0 || this->pwm_amounts_[1] > 0 || this->pwm_amounts_[2] > 0) &&
+             this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) {
+    // Only data on RGB channels
+    data[0] = this->model_id_ + SM10BIT_ADDR_START_3CH;
+    data[1] = this->max_power_color_channels_ << 4 | 0;
+    for (int i = 2, j = 0; i < 12; i += 2, j++) {
+      data[i] = this->pwm_amounts_[j] >> 0x8;
+      data[i + 1] = this->pwm_amounts_[j] & 0xFF;
+    }
+    this->write_buffer_(data, 12);
+  } else {
+    // All channels
+    data[0] = this->model_id_ + SM10BIT_ADDR_START_5CH;
+    data[1] = this->max_power_color_channels_ << 4 | this->max_power_white_channels_;
+    for (int i = 2, j = 0; i < 12; i += 2, j++) {
+      data[i] = this->pwm_amounts_[j] >> 0x8;
+      data[i + 1] = this->pwm_amounts_[j] & 0xFF;
+    }
+    this->write_buffer_(data, 12);
+  }
+
+  this->update_ = false;
+}
+
+void Sm10BitBase::set_channel_value_(uint8_t channel, uint16_t value) {
+  if (this->pwm_amounts_[channel] != value) {
+    this->update_ = true;
+    this->update_channel_ = channel;
+  }
+  this->pwm_amounts_[channel] = value;
+}
+void Sm10BitBase::write_bit_(bool value) {
+  this->clock_pin_->digital_write(false);
+  this->data_pin_->digital_write(value);
+  this->clock_pin_->digital_write(true);
+}
+
+void Sm10BitBase::write_byte_(uint8_t data) {
+  for (uint8_t mask = 0x80; mask; mask >>= 1) {
+    this->write_bit_(data & mask);
+  }
+  this->clock_pin_->digital_write(false);
+  this->data_pin_->digital_write(true);
+  this->clock_pin_->digital_write(true);
+}
+
+void Sm10BitBase::write_buffer_(uint8_t *buffer, uint8_t size) {
+  this->data_pin_->digital_write(false);
+  for (uint32_t i = 0; i < size; i++) {
+    this->write_byte_(buffer[i]);
+  }
+  this->clock_pin_->digital_write(false);
+  this->clock_pin_->digital_write(true);
+  this->data_pin_->digital_write(true);
+}
+
+}  // namespace sm10bit_base
+}  // namespace esphome
diff --git a/esphome/components/sm10bit_base/sm10bit_base.h b/esphome/components/sm10bit_base/sm10bit_base.h
new file mode 100644
index 0000000000..c8e92e352f
--- /dev/null
+++ b/esphome/components/sm10bit_base/sm10bit_base.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/hal.h"
+#include "esphome/components/output/float_output.h"
+#include <vector>
+
+namespace esphome {
+namespace sm10bit_base {
+
+class Sm10BitBase : public Component {
+ public:
+  class Channel;
+
+  void set_model(uint8_t model_id) { model_id_ = model_id; }
+  void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; }
+  void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; }
+  void set_max_power_color_channels(uint8_t max_power_color_channels) {
+    max_power_color_channels_ = max_power_color_channels;
+  }
+  void set_max_power_white_channels(uint8_t max_power_white_channels) {
+    max_power_white_channels_ = max_power_white_channels;
+  }
+
+  float get_setup_priority() const override { return setup_priority::HARDWARE; }
+
+  void setup() override;
+  void dump_config() override;
+  void loop() override;
+
+  class Channel : public output::FloatOutput {
+   public:
+    void set_parent(Sm10BitBase *parent) { parent_ = parent; }
+    void set_channel(uint8_t channel) { channel_ = channel; }
+
+   protected:
+    void write_state(float state) override {
+      auto amount = static_cast<uint16_t>(state * 0x3FF);
+      this->parent_->set_channel_value_(this->channel_, amount);
+    }
+
+    Sm10BitBase *parent_;
+    uint8_t channel_;
+  };
+
+ protected:
+  void set_channel_value_(uint8_t channel, uint16_t value);
+  void write_bit_(bool value);
+  void write_byte_(uint8_t data);
+  void write_buffer_(uint8_t *buffer, uint8_t size);
+
+  GPIOPin *data_pin_;
+  GPIOPin *clock_pin_;
+  uint8_t model_id_;
+  uint8_t max_power_color_channels_{2};
+  uint8_t max_power_white_channels_{4};
+  uint8_t update_channel_;
+  std::vector<uint16_t> pwm_amounts_;
+  bool update_{true};
+};
+
+}  // namespace sm10bit_base
+}  // namespace esphome
diff --git a/esphome/components/sm2235/__init__.py b/esphome/components/sm2235/__init__.py
new file mode 100644
index 0000000000..ae6cb336ad
--- /dev/null
+++ b/esphome/components/sm2235/__init__.py
@@ -0,0 +1,22 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sm10bit_base
+
+AUTO_LOAD = ["sm10bit_base", "output"]
+CODEOWNERS = ["@Cossid"]
+MULTI_CONF = True
+
+sm2235_ns = cg.esphome_ns.namespace("sm2235")
+
+SM2235 = sm2235_ns.class_("SM2235", sm10bit_base.Sm10BitBase)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(SM2235),
+    }
+).extend(sm10bit_base.SM10BIT_BASE_CONFIG_SCHEMA)
+
+
+async def to_code(config):
+    var = await sm10bit_base.register_sm10bit_base(config)
+    cg.add(var.set_model(0xC0))
diff --git a/esphome/components/sm2235/output.py b/esphome/components/sm2235/output.py
new file mode 100644
index 0000000000..c4f63b451a
--- /dev/null
+++ b/esphome/components/sm2235/output.py
@@ -0,0 +1,28 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import output
+from esphome.const import CONF_CHANNEL, CONF_ID
+from . import SM2235
+
+DEPENDENCIES = ["sm2235"]
+CODEOWNERS = ["@Cossid"]
+
+Channel = SM2235.class_("Channel", output.FloatOutput)
+
+CONF_SM2235_ID = "sm2235_id"
+CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
+    {
+        cv.GenerateID(CONF_SM2235_ID): cv.use_id(SM2235),
+        cv.Required(CONF_ID): cv.declare_id(Channel),
+        cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await output.register_output(var, config)
+
+    parent = await cg.get_variable(config[CONF_SM2235_ID])
+    cg.add(var.set_parent(parent))
+    cg.add(var.set_channel(config[CONF_CHANNEL]))
diff --git a/esphome/components/sm2235/sm2235.cpp b/esphome/components/sm2235/sm2235.cpp
new file mode 100644
index 0000000000..f953d41957
--- /dev/null
+++ b/esphome/components/sm2235/sm2235.cpp
@@ -0,0 +1,27 @@
+#include "sm2235.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace sm2235 {
+
+static const char *const TAG = "sm2235";
+
+void SM2235::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up sm2235 Output Component...");
+  this->data_pin_->setup();
+  this->data_pin_->digital_write(true);
+  this->clock_pin_->setup();
+  this->clock_pin_->digital_write(true);
+  this->pwm_amounts_.resize(5, 0);
+}
+
+void SM2235::dump_config() {
+  ESP_LOGCONFIG(TAG, "sm2235:");
+  LOG_PIN("  Data Pin: ", this->data_pin_);
+  LOG_PIN("  Clock Pin: ", this->clock_pin_);
+  ESP_LOGCONFIG(TAG, "  Color Channels Max Power: %u", this->max_power_color_channels_);
+  ESP_LOGCONFIG(TAG, "  White Channels Max Power: %u", this->max_power_white_channels_);
+}
+
+}  // namespace sm2235
+}  // namespace esphome
diff --git a/esphome/components/sm2235/sm2235.h b/esphome/components/sm2235/sm2235.h
new file mode 100644
index 0000000000..56d1782055
--- /dev/null
+++ b/esphome/components/sm2235/sm2235.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sm10bit_base/sm10bit_base.h"
+#include "esphome/core/hal.h"
+
+namespace esphome {
+namespace sm2235 {
+
+class SM2235 : public sm10bit_base::Sm10BitBase {
+ public:
+  SM2235() = default;
+
+  void setup() override;
+  void dump_config() override;
+};
+
+}  // namespace sm2235
+}  // namespace esphome
diff --git a/esphome/components/sm2335/__init__.py b/esphome/components/sm2335/__init__.py
new file mode 100644
index 0000000000..6d6e0e311c
--- /dev/null
+++ b/esphome/components/sm2335/__init__.py
@@ -0,0 +1,22 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sm10bit_base
+
+AUTO_LOAD = ["sm10bit_base", "output"]
+CODEOWNERS = ["@Cossid"]
+MULTI_CONF = True
+
+sm2335_ns = cg.esphome_ns.namespace("sm2335")
+
+SM2335 = sm2335_ns.class_("SM2335", sm10bit_base.Sm10BitBase)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(SM2335),
+    }
+).extend(sm10bit_base.SM10BIT_BASE_CONFIG_SCHEMA)
+
+
+async def to_code(config):
+    var = await sm10bit_base.register_sm10bit_base(config)
+    cg.add(var.set_model(0xC0))
diff --git a/esphome/components/sm2335/output.py b/esphome/components/sm2335/output.py
new file mode 100644
index 0000000000..52b6321db1
--- /dev/null
+++ b/esphome/components/sm2335/output.py
@@ -0,0 +1,28 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import output
+from esphome.const import CONF_CHANNEL, CONF_ID
+from . import SM2335
+
+DEPENDENCIES = ["sm2335"]
+CODEOWNERS = ["@Cossid"]
+
+Channel = SM2335.class_("Channel", output.FloatOutput)
+
+CONF_SM2335_ID = "sm2335_id"
+CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
+    {
+        cv.GenerateID(CONF_SM2335_ID): cv.use_id(SM2335),
+        cv.Required(CONF_ID): cv.declare_id(Channel),
+        cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await output.register_output(var, config)
+
+    parent = await cg.get_variable(config[CONF_SM2335_ID])
+    cg.add(var.set_parent(parent))
+    cg.add(var.set_channel(config[CONF_CHANNEL]))
diff --git a/esphome/components/sm2335/sm2335.cpp b/esphome/components/sm2335/sm2335.cpp
new file mode 100644
index 0000000000..b6c482b5bb
--- /dev/null
+++ b/esphome/components/sm2335/sm2335.cpp
@@ -0,0 +1,27 @@
+#include "sm2335.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace sm2335 {
+
+static const char *const TAG = "sm2335";
+
+void SM2335::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up sm2335 Output Component...");
+  this->data_pin_->setup();
+  this->data_pin_->digital_write(true);
+  this->clock_pin_->setup();
+  this->clock_pin_->digital_write(true);
+  this->pwm_amounts_.resize(5, 0);
+}
+
+void SM2335::dump_config() {
+  ESP_LOGCONFIG(TAG, "sm2335:");
+  LOG_PIN("  Data Pin: ", this->data_pin_);
+  LOG_PIN("  Clock Pin: ", this->clock_pin_);
+  ESP_LOGCONFIG(TAG, "  Color Channels Max Power: %u", this->max_power_color_channels_);
+  ESP_LOGCONFIG(TAG, "  White Channels Max Power: %u", this->max_power_white_channels_);
+}
+
+}  // namespace sm2335
+}  // namespace esphome
diff --git a/esphome/components/sm2335/sm2335.h b/esphome/components/sm2335/sm2335.h
new file mode 100644
index 0000000000..c8cf825189
--- /dev/null
+++ b/esphome/components/sm2335/sm2335.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sm10bit_base/sm10bit_base.h"
+#include "esphome/core/hal.h"
+
+namespace esphome {
+namespace sm2335 {
+
+class SM2335 : public sm10bit_base::Sm10BitBase {
+ public:
+  SM2335() = default;
+
+  void setup() override;
+  void dump_config() override;
+};
+
+}  // namespace sm2335
+}  // namespace esphome
diff --git a/esphome/components/tee501/__init__.py b/esphome/components/tee501/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/tee501/sensor.py b/esphome/components/tee501/sensor.py
new file mode 100644
index 0000000000..329fc724bd
--- /dev/null
+++ b/esphome/components/tee501/sensor.py
@@ -0,0 +1,36 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import i2c, sensor
+from esphome.const import (
+    DEVICE_CLASS_TEMPERATURE,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+)
+
+CODEOWNERS = ["@Stock-M"]
+
+DEPENDENCIES = ["i2c"]
+
+tee501_ns = cg.esphome_ns.namespace("tee501")
+
+TEE501Component = tee501_ns.class_(
+    "TEE501Component", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice
+)
+
+CONFIG_SCHEMA = (
+    sensor.sensor_schema(
+        TEE501Component,
+        unit_of_measurement=UNIT_CELSIUS,
+        accuracy_decimals=1,
+        device_class=DEVICE_CLASS_TEMPERATURE,
+        state_class=STATE_CLASS_MEASUREMENT,
+    )
+    .extend(cv.polling_component_schema("60s"))
+    .extend(i2c.i2c_device_schema(0x48))
+)
+
+
+async def to_code(config):
+    var = await sensor.new_sensor(config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/tee501/tee501.cpp b/esphome/components/tee501/tee501.cpp
new file mode 100644
index 0000000000..22329d40cd
--- /dev/null
+++ b/esphome/components/tee501/tee501.cpp
@@ -0,0 +1,85 @@
+#include "tee501.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace tee501 {
+
+static const char *const TAG = "tee501";
+
+void TEE501Component::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up TEE501...");
+  uint8_t address[] = {0x70, 0x29};
+  this->write(address, 2, false);
+  uint8_t identification[9];
+  this->read(identification, 9);
+  if (identification[8] != calc_crc8_(identification, 0, 7)) {
+    this->error_code_ = CRC_CHECK_FAILED;
+    this->mark_failed();
+    return;
+  }
+  ESP_LOGV(TAG, "    Serial Number: 0x%s", format_hex(identification + 0, 7).c_str());
+}
+
+void TEE501Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "TEE501:");
+  LOG_I2C_DEVICE(this);
+  switch (this->error_code_) {
+    case COMMUNICATION_FAILED:
+      ESP_LOGE(TAG, "Communication with TEE501 failed!");
+      break;
+    case CRC_CHECK_FAILED:
+      ESP_LOGE(TAG, "The crc check failed");
+      break;
+    case NONE:
+    default:
+      break;
+  }
+  LOG_UPDATE_INTERVAL(this);
+  LOG_SENSOR("  ", "TEE501", this);
+}
+
+float TEE501Component::get_setup_priority() const { return setup_priority::DATA; }
+void TEE501Component::update() {
+  uint8_t address_1[] = {0x2C, 0x1B};
+  this->write(address_1, 2, true);
+  this->set_timeout(50, [this]() {
+    uint8_t i2c_response[3];
+    this->read(i2c_response, 3);
+    if (i2c_response[2] != calc_crc8_(i2c_response, 0, 1)) {
+      this->error_code_ = CRC_CHECK_FAILED;
+      this->status_set_warning();
+      return;
+    }
+    float temperature = (float) encode_uint16(i2c_response[0], i2c_response[1]);
+    if (temperature > 55536) {
+      temperature = (temperature - 65536) / 100;
+    } else {
+      temperature = temperature / 100;
+    }
+    ESP_LOGD(TAG, "Got temperature=%.2f°C", temperature);
+    this->publish_state(temperature);
+    this->status_clear_warning();
+  });
+}
+
+unsigned char TEE501Component::calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to) {
+  unsigned char crc_val = 0xFF;
+  unsigned char i = 0;
+  unsigned char j = 0;
+  for (i = from; i <= to; i++) {
+    int cur_val = buf[i];
+    for (j = 0; j < 8; j++) {
+      if (((crc_val ^ cur_val) & 0x80) != 0)  // If MSBs are not equal
+      {
+        crc_val = ((crc_val << 1) ^ 0x31);
+      } else {
+        crc_val = (crc_val << 1);
+      }
+      cur_val = cur_val << 1;
+    }
+  }
+  return crc_val;
+}
+
+}  // namespace tee501
+}  // namespace esphome
diff --git a/esphome/components/tee501/tee501.h b/esphome/components/tee501/tee501.h
new file mode 100644
index 0000000000..fc655e58c9
--- /dev/null
+++ b/esphome/components/tee501/tee501.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/i2c/i2c.h"
+
+namespace esphome {
+namespace tee501 {
+
+/// This class implements support for the tee501 of temperature i2c sensors.
+class TEE501Component : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
+ public:
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override;
+  void update() override;
+
+ protected:
+  unsigned char calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to);
+
+  enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE};
+};
+
+}  // namespace tee501
+}  // namespace esphome
diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp
index b3e545d3e9..5db346b99f 100644
--- a/esphome/components/template/switch/template_switch.cpp
+++ b/esphome/components/template/switch/template_switch.cpp
@@ -43,15 +43,16 @@ void TemplateSwitch::setup() {
   if (!this->restore_state_)
     return;
 
-  auto restored = this->get_initial_state();
-  if (!restored.has_value())
-    return;
+  optional<bool> initial_state = this->get_initial_state_with_restore_mode();
 
-  ESP_LOGD(TAG, "  Restored state %s", ONOFF(*restored));
-  if (*restored) {
-    this->turn_on();
-  } else {
-    this->turn_off();
+  if (initial_state.has_value()) {
+    ESP_LOGD(TAG, "  Restored state %s", ONOFF(initial_state.value()));
+    // if it has a value, restore_mode is not "DISABLED", therefore act on the switch:
+    if (initial_state.value()) {
+      this->turn_on();
+    } else {
+      this->turn_off();
+    }
   }
 }
 void TemplateSwitch::dump_config() {
diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py
index 8aa61dbb93..9a57f6a337 100644
--- a/esphome/components/thermostat/climate.py
+++ b/esphome/components/thermostat/climate.py
@@ -24,6 +24,7 @@ from esphome.const import (
     CONF_FAN_MODE_MIDDLE_ACTION,
     CONF_FAN_MODE_FOCUS_ACTION,
     CONF_FAN_MODE_DIFFUSE_ACTION,
+    CONF_FAN_MODE_QUIET_ACTION,
     CONF_FAN_ONLY_ACTION,
     CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER,
     CONF_FAN_ONLY_COOLING,
@@ -273,6 +274,7 @@ def validate_thermostat(config):
             CONF_FAN_MODE_MIDDLE_ACTION,
             CONF_FAN_MODE_FOCUS_ACTION,
             CONF_FAN_MODE_DIFFUSE_ACTION,
+            CONF_FAN_MODE_QUIET_ACTION,
         ],
     }
     for req_config_item, config_triggers in requirements.items():
@@ -413,6 +415,7 @@ def validate_thermostat(config):
             "MIDDLE": [CONF_FAN_MODE_MIDDLE_ACTION],
             "FOCUS": [CONF_FAN_MODE_FOCUS_ACTION],
             "DIFFUSE": [CONF_FAN_MODE_DIFFUSE_ACTION],
+            "QUIET": [CONF_FAN_MODE_QUIET_ACTION],
         }
 
         for preset_config in config[CONF_PRESET]:
@@ -500,12 +503,13 @@ def validate_thermostat(config):
             CONF_FAN_MODE_MIDDLE_ACTION,
             CONF_FAN_MODE_FOCUS_ACTION,
             CONF_FAN_MODE_DIFFUSE_ACTION,
+            CONF_FAN_MODE_QUIET_ACTION,
         ]
         for config_req_action in requirements:
             if config_req_action in config:
                 return config
         raise cv.Invalid(
-            f"At least one of {CONF_FAN_MODE_ON_ACTION}, {CONF_FAN_MODE_OFF_ACTION}, {CONF_FAN_MODE_AUTO_ACTION}, {CONF_FAN_MODE_LOW_ACTION}, {CONF_FAN_MODE_MEDIUM_ACTION}, {CONF_FAN_MODE_HIGH_ACTION}, {CONF_FAN_MODE_MIDDLE_ACTION}, {CONF_FAN_MODE_FOCUS_ACTION}, {CONF_FAN_MODE_DIFFUSE_ACTION} must be defined to use {CONF_MIN_FAN_MODE_SWITCHING_TIME}"
+            f"At least one of {CONF_FAN_MODE_ON_ACTION}, {CONF_FAN_MODE_OFF_ACTION}, {CONF_FAN_MODE_AUTO_ACTION}, {CONF_FAN_MODE_LOW_ACTION}, {CONF_FAN_MODE_MEDIUM_ACTION}, {CONF_FAN_MODE_HIGH_ACTION}, {CONF_FAN_MODE_MIDDLE_ACTION}, {CONF_FAN_MODE_FOCUS_ACTION}, {CONF_FAN_MODE_DIFFUSE_ACTION}, {CONF_FAN_MODE_QUIET_ACTION} must be defined to use {CONF_MIN_FAN_MODE_SWITCHING_TIME}"
         )
     return config
 
@@ -563,6 +567,9 @@ CONFIG_SCHEMA = cv.All(
             cv.Optional(CONF_FAN_MODE_DIFFUSE_ACTION): automation.validate_automation(
                 single=True
             ),
+            cv.Optional(CONF_FAN_MODE_QUIET_ACTION): automation.validate_automation(
+                single=True
+            ),
             cv.Optional(CONF_SWING_BOTH_ACTION): automation.validate_automation(
                 single=True
             ),
@@ -836,6 +843,11 @@ async def to_code(config):
             var.get_fan_mode_diffuse_trigger(), [], config[CONF_FAN_MODE_DIFFUSE_ACTION]
         )
         cg.add(var.set_supports_fan_mode_diffuse(True))
+    if CONF_FAN_MODE_QUIET_ACTION in config:
+        await automation.build_automation(
+            var.get_fan_mode_quiet_trigger(), [], config[CONF_FAN_MODE_QUIET_ACTION]
+        )
+        cg.add(var.set_supports_fan_mode_quiet(True))
     if CONF_SWING_BOTH_ACTION in config:
         await automation.build_automation(
             var.get_swing_mode_both_trigger(), [], config[CONF_SWING_BOTH_ACTION]
diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp
index 1fbbbe022c..51da663a0c 100644
--- a/esphome/components/thermostat/thermostat_climate.cpp
+++ b/esphome/components/thermostat/thermostat_climate.cpp
@@ -247,6 +247,8 @@ climate::ClimateTraits ThermostatClimate::traits() {
     traits.add_supported_fan_mode(climate::CLIMATE_FAN_FOCUS);
   if (supports_fan_mode_diffuse_)
     traits.add_supported_fan_mode(climate::CLIMATE_FAN_DIFFUSE);
+  if (supports_fan_mode_quiet_)
+    traits.add_supported_fan_mode(climate::CLIMATE_FAN_QUIET);
 
   if (supports_swing_mode_both_)
     traits.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH);
@@ -594,6 +596,10 @@ void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bo
         trig = this->fan_mode_diffuse_trigger_;
         ESP_LOGVV(TAG, "Switching to FAN_DIFFUSE mode");
         break;
+      case climate::CLIMATE_FAN_QUIET:
+        trig = this->fan_mode_quiet_trigger_;
+        ESP_LOGVV(TAG, "Switching to FAN_QUIET mode");
+        break;
       default:
         // we cannot report an invalid mode back to HA (even if it asked for one)
         //  and must assume some valid value
@@ -1093,6 +1099,7 @@ ThermostatClimate::ThermostatClimate()
       fan_mode_middle_trigger_(new Trigger<>()),
       fan_mode_focus_trigger_(new Trigger<>()),
       fan_mode_diffuse_trigger_(new Trigger<>()),
+      fan_mode_quiet_trigger_(new Trigger<>()),
       swing_mode_both_trigger_(new Trigger<>()),
       swing_mode_off_trigger_(new Trigger<>()),
       swing_mode_horizontal_trigger_(new Trigger<>()),
@@ -1208,6 +1215,9 @@ void ThermostatClimate::set_supports_fan_mode_focus(bool supports_fan_mode_focus
 void ThermostatClimate::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) {
   this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse;
 }
+void ThermostatClimate::set_supports_fan_mode_quiet(bool supports_fan_mode_quiet) {
+  this->supports_fan_mode_quiet_ = supports_fan_mode_quiet;
+}
 void ThermostatClimate::set_supports_swing_mode_both(bool supports_swing_mode_both) {
   this->supports_swing_mode_both_ = supports_swing_mode_both;
 }
@@ -1250,6 +1260,7 @@ Trigger<> *ThermostatClimate::get_fan_mode_high_trigger() const { return this->f
 Trigger<> *ThermostatClimate::get_fan_mode_middle_trigger() const { return this->fan_mode_middle_trigger_; }
 Trigger<> *ThermostatClimate::get_fan_mode_focus_trigger() const { return this->fan_mode_focus_trigger_; }
 Trigger<> *ThermostatClimate::get_fan_mode_diffuse_trigger() const { return this->fan_mode_diffuse_trigger_; }
+Trigger<> *ThermostatClimate::get_fan_mode_quiet_trigger() const { return this->fan_mode_quiet_trigger_; }
 Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() const { return this->swing_mode_both_trigger_; }
 Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; }
 Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; }
@@ -1294,7 +1305,8 @@ void ThermostatClimate::dump_config() {
   }
   if (this->supports_fan_mode_on_ || this->supports_fan_mode_off_ || this->supports_fan_mode_auto_ ||
       this->supports_fan_mode_low_ || this->supports_fan_mode_medium_ || this->supports_fan_mode_high_ ||
-      this->supports_fan_mode_middle_ || this->supports_fan_mode_focus_ || this->supports_fan_mode_diffuse_) {
+      this->supports_fan_mode_middle_ || this->supports_fan_mode_focus_ || this->supports_fan_mode_diffuse_ ||
+      this->supports_fan_mode_quiet_) {
     ESP_LOGCONFIG(TAG, "  Minimum Fan Mode Switching Time: %us",
                   this->timer_duration_(thermostat::TIMER_FAN_MODE) / 1000);
   }
@@ -1323,6 +1335,7 @@ void ThermostatClimate::dump_config() {
   ESP_LOGCONFIG(TAG, "  Supports FAN MODE MIDDLE: %s", YESNO(this->supports_fan_mode_middle_));
   ESP_LOGCONFIG(TAG, "  Supports FAN MODE FOCUS: %s", YESNO(this->supports_fan_mode_focus_));
   ESP_LOGCONFIG(TAG, "  Supports FAN MODE DIFFUSE: %s", YESNO(this->supports_fan_mode_diffuse_));
+  ESP_LOGCONFIG(TAG, "  Supports FAN MODE QUIET: %s", YESNO(this->supports_fan_mode_quiet_));
   ESP_LOGCONFIG(TAG, "  Supports SWING MODE BOTH: %s", YESNO(this->supports_swing_mode_both_));
   ESP_LOGCONFIG(TAG, "  Supports SWING MODE OFF: %s", YESNO(this->supports_swing_mode_off_));
   ESP_LOGCONFIG(TAG, "  Supports SWING MODE HORIZONTAL: %s", YESNO(this->supports_swing_mode_horizontal_));
diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h
index 124f95de33..677b4ad324 100644
--- a/esphome/components/thermostat/thermostat_climate.h
+++ b/esphome/components/thermostat/thermostat_climate.h
@@ -101,6 +101,7 @@ class ThermostatClimate : public climate::Climate, public Component {
   void set_supports_fan_mode_middle(bool supports_fan_mode_middle);
   void set_supports_fan_mode_focus(bool supports_fan_mode_focus);
   void set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse);
+  void set_supports_fan_mode_quiet(bool supports_fan_mode_quiet);
   void set_supports_swing_mode_both(bool supports_swing_mode_both);
   void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal);
   void set_supports_swing_mode_off(bool supports_swing_mode_off);
@@ -132,6 +133,7 @@ class ThermostatClimate : public climate::Climate, public Component {
   Trigger<> *get_fan_mode_middle_trigger() const;
   Trigger<> *get_fan_mode_focus_trigger() const;
   Trigger<> *get_fan_mode_diffuse_trigger() const;
+  Trigger<> *get_fan_mode_quiet_trigger() const;
   Trigger<> *get_swing_mode_both_trigger() const;
   Trigger<> *get_swing_mode_horizontal_trigger() const;
   Trigger<> *get_swing_mode_off_trigger() const;
@@ -277,6 +279,7 @@ class ThermostatClimate : public climate::Climate, public Component {
   bool supports_fan_mode_middle_{false};
   bool supports_fan_mode_focus_{false};
   bool supports_fan_mode_diffuse_{false};
+  bool supports_fan_mode_quiet_{false};
 
   /// Whether the controller supports various swing modes.
   ///
@@ -372,6 +375,9 @@ class ThermostatClimate : public climate::Climate, public Component {
   /// The trigger to call when the controller should switch the fan to "diffuse" position.
   Trigger<> *fan_mode_diffuse_trigger_{nullptr};
 
+  /// The trigger to call when the controller should switch the fan to "quiet" position.
+  Trigger<> *fan_mode_quiet_trigger_{nullptr};
+
   /// The trigger to call when the controller should switch the swing mode to "both".
   Trigger<> *swing_mode_both_trigger_{nullptr};
 
diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp
index 104e885aab..a070ccceb2 100644
--- a/esphome/components/toshiba/toshiba.cpp
+++ b/esphome/components/toshiba/toshiba.cpp
@@ -124,9 +124,6 @@ void ToshibaClimate::setup() {
   // Set supported modes & temperatures based on model
   this->minimum_temperature_ = this->temperature_min_();
   this->maximum_temperature_ = this->temperature_max_();
-  this->supports_dry_ = this->toshiba_supports_dry_();
-  this->supports_fan_only_ = this->toshiba_supports_fan_only_();
-  this->fan_modes_ = this->toshiba_fan_modes_();
   this->swing_modes_ = this->toshiba_swing_modes_();
   // Never send nan to HA
   if (std::isnan(this->target_temperature))
@@ -178,12 +175,39 @@ void ToshibaClimate::transmit_generic_() {
       mode = TOSHIBA_MODE_COOL;
       break;
 
+    case climate::CLIMATE_MODE_DRY:
+      mode = TOSHIBA_MODE_DRY;
+      break;
+
+    case climate::CLIMATE_MODE_FAN_ONLY:
+      mode = TOSHIBA_MODE_FAN_ONLY;
+      break;
+
     case climate::CLIMATE_MODE_HEAT_COOL:
     default:
       mode = TOSHIBA_MODE_AUTO;
   }
 
-  message[6] |= mode | TOSHIBA_FAN_SPEED_AUTO;
+  uint8_t fan;
+  switch (this->fan_mode.value()) {
+    case climate::CLIMATE_FAN_LOW:
+      fan = TOSHIBA_FAN_SPEED_1;
+      break;
+
+    case climate::CLIMATE_FAN_MEDIUM:
+      fan = TOSHIBA_FAN_SPEED_3;
+      break;
+
+    case climate::CLIMATE_FAN_HIGH:
+      fan = TOSHIBA_FAN_SPEED_5;
+      break;
+
+    case climate::CLIMATE_FAN_AUTO:
+    default:
+      fan = TOSHIBA_FAN_SPEED_AUTO;
+      break;
+  }
+  message[6] = fan | mode;
 
   // Zero
   message[7] = 0x00;
diff --git a/esphome/components/toshiba/toshiba.h b/esphome/components/toshiba/toshiba.h
index 36e8760169..729548e747 100644
--- a/esphome/components/toshiba/toshiba.h
+++ b/esphome/components/toshiba/toshiba.h
@@ -22,7 +22,10 @@ const float TOSHIBA_RAC_PT1411HWRU_TEMP_F_MAX = 86.0;
 
 class ToshibaClimate : public climate_ir::ClimateIR {
  public:
-  ToshibaClimate() : climate_ir::ClimateIR(TOSHIBA_GENERIC_TEMP_C_MIN, TOSHIBA_GENERIC_TEMP_C_MAX, 1.0f) {}
+  ToshibaClimate()
+      : climate_ir::ClimateIR(TOSHIBA_GENERIC_TEMP_C_MIN, TOSHIBA_GENERIC_TEMP_C_MAX, 1.0f, true, true,
+                              {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
+                               climate::CLIMATE_FAN_HIGH}) {}
 
   void setup() override;
   void set_model(Model model) { this->model_ = model; }
@@ -46,18 +49,6 @@ class ToshibaClimate : public climate_ir::ClimateIR {
   float temperature_max_() {
     return (this->model_ == MODEL_GENERIC) ? TOSHIBA_GENERIC_TEMP_C_MAX : TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX;
   }
-  bool toshiba_supports_dry_() {
-    return ((this->model_ == MODEL_RAC_PT1411HWRU_C) || (this->model_ == MODEL_RAC_PT1411HWRU_F));
-  }
-  bool toshiba_supports_fan_only_() {
-    return ((this->model_ == MODEL_RAC_PT1411HWRU_C) || (this->model_ == MODEL_RAC_PT1411HWRU_F));
-  }
-  std::set<climate::ClimateFanMode> toshiba_fan_modes_() {
-    return (this->model_ == MODEL_GENERIC)
-               ? std::set<climate::ClimateFanMode>{}
-               : std::set<climate::ClimateFanMode>{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW,
-                                                   climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH};
-  }
   std::set<climate::ClimateSwingMode> toshiba_swing_modes_() {
     return (this->model_ == MODEL_GENERIC)
                ? std::set<climate::ClimateSwingMode>{}
diff --git a/esphome/components/tsl2591/sensor.py b/esphome/components/tsl2591/sensor.py
index 63a0733365..5435ed4b62 100644
--- a/esphome/components/tsl2591/sensor.py
+++ b/esphome/components/tsl2591/sensor.py
@@ -24,6 +24,7 @@ import esphome.config_validation as cv
 from esphome.components import i2c, sensor
 from esphome.const import (
     CONF_GAIN,
+    CONF_ACTUAL_GAIN,
     CONF_ID,
     CONF_NAME,
     CONF_INTEGRATION_TIME,
@@ -79,7 +80,6 @@ TSL2591Component = tsl2591_ns.class_(
     "TSL2591Component", cg.PollingComponent, i2c.I2CDevice
 )
 
-
 CONFIG_SCHEMA = (
     cv.Schema(
         {
@@ -106,6 +106,12 @@ CONFIG_SCHEMA = (
                 device_class=DEVICE_CLASS_ILLUMINANCE,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
+            cv.Optional(CONF_ACTUAL_GAIN): sensor.sensor_schema(
+                icon=ICON_BRIGHTNESS_6,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_ILLUMINANCE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
             cv.Optional(
                 CONF_INTEGRATION_TIME, default="100ms"
             ): validate_integration_time,
@@ -150,6 +156,11 @@ async def to_code(config):
         sens = await sensor.new_sensor(conf)
         cg.add(var.set_calculated_lux_sensor(sens))
 
+    if CONF_ACTUAL_GAIN in config:
+        conf = config[CONF_ACTUAL_GAIN]
+        sens = await sensor.new_sensor(conf)
+        cg.add(var.set_actual_gain_sensor(sens))
+
     cg.add(var.set_name(config[CONF_NAME]))
     cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
     cg.add(var.set_integration_time(config[CONF_INTEGRATION_TIME]))
diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp
index f8c59a53c6..5086a38408 100644
--- a/esphome/components/tsl2591/tsl2591.cpp
+++ b/esphome/components/tsl2591/tsl2591.cpp
@@ -130,6 +130,7 @@ void TSL2591Component::dump_config() {
   LOG_SENSOR("  ", "Infrared:", this->infrared_sensor_);
   LOG_SENSOR("  ", "Visible:", this->visible_sensor_);
   LOG_SENSOR("  ", "Calculated lux:", this->calculated_lux_sensor_);
+  LOG_SENSOR("  ", "Actual gain:", this->actual_gain_sensor_);
 
   LOG_UPDATE_INTERVAL(this);
 }
@@ -140,8 +141,9 @@ void TSL2591Component::process_update_() {
   uint16_t infrared = this->get_illuminance(TSL2591_SENSOR_CHANNEL_INFRARED, combined);
   uint16_t full = this->get_illuminance(TSL2591_SENSOR_CHANNEL_FULL_SPECTRUM, combined);
   float lux = this->get_calculated_lux(full, infrared);
-  ESP_LOGD(TAG, "Got illuminance: combined 0x%X, full %d, IR %d, vis %d. Calc lux: %f", combined, full, infrared,
-           visible, lux);
+  uint16_t actual_gain = this->get_actual_gain();
+  ESP_LOGD(TAG, "Got illuminance: combined 0x%X, full %d, IR %d, vis %d. Calc lux: %f. Actual gain: %d.", combined,
+           full, infrared, visible, lux, actual_gain);
   if (this->full_spectrum_sensor_ != nullptr) {
     this->full_spectrum_sensor_->publish_state(full);
   }
@@ -154,9 +156,14 @@ void TSL2591Component::process_update_() {
   if (this->calculated_lux_sensor_ != nullptr) {
     this->calculated_lux_sensor_->publish_state(lux);
   }
+
   if (this->component_gain_ == TSL2591_CGAIN_AUTO) {
     this->automatic_gain_update(full);
   }
+
+  if (this->actual_gain_sensor_ != nullptr) {
+    this->actual_gain_sensor_->publish_state(actual_gain);
+  }
   this->status_clear_warning();
 }
 
@@ -207,6 +214,10 @@ void TSL2591Component::set_calculated_lux_sensor(sensor::Sensor *calculated_lux_
   this->calculated_lux_sensor_ = calculated_lux_sensor;
 }
 
+void TSL2591Component::set_actual_gain_sensor(sensor::Sensor *actual_gain_sensor) {
+  this->actual_gain_sensor_ = actual_gain_sensor;
+}
+
 void TSL2591Component::set_integration_time(TSL2591IntegrationTime integration_time) {
   this->integration_time_ = integration_time;
 }
@@ -347,8 +358,8 @@ float TSL2591Component::get_calculated_lux(uint16_t full_spectrum, uint16_t infr
   uint16_t max_count = (this->integration_time_ == TSL2591_INTEGRATION_TIME_100MS ? 36863 : 65535);
   if ((full_spectrum == max_count) || (infrared == max_count)) {
     // Signal an overflow
-    ESP_LOGW(TAG, "Apparent saturation on TSL2591 (%s). You could reduce the gain.", this->name_);
-    return -1.0F;
+    ESP_LOGW(TAG, "Apparent saturation on TSL2591 (%s). You could reduce the gain or integration time.", this->name_);
+    return NAN;
   }
 
   if ((full_spectrum == 0) && (infrared == 0)) {
@@ -377,7 +388,6 @@ float TSL2591Component::get_calculated_lux(uint16_t full_spectrum, uint16_t infr
       again = 1.0F;
       break;
   }
-
   // This lux equation is copied from the Adafruit TSL2591 v1.4.0 and modified slightly.
   // See: https://github.com/adafruit/Adafruit_TSL2591_Library/issues/14
   // and that library code.
@@ -448,5 +458,25 @@ void TSL2591Component::automatic_gain_update(uint16_t full_spectrum) {
   ESP_LOGD(TAG, "Gain setting: %d", this->gain_);
 }
 
+/** Reads the actual gain used
+ *
+ * Useful for exposing the real gain used when configured in "auto" gain mode
+ */
+float TSL2591Component::get_actual_gain() {
+  switch (this->gain_) {
+    case TSL2591_GAIN_LOW:
+      return 1.0F;
+    case TSL2591_GAIN_MED:
+      return 25.0F;
+    case TSL2591_GAIN_HIGH:
+      return 400.0F;
+    case TSL2591_GAIN_MAX:
+      return 9500.0F;
+    default:
+      // Shouldn't get here, but just in case.
+      return NAN;
+  }
+}
+
 }  // namespace tsl2591
 }  // namespace esphome
diff --git a/esphome/components/tsl2591/tsl2591.h b/esphome/components/tsl2591/tsl2591.h
index 5b7eea35ec..d7c5230276 100644
--- a/esphome/components/tsl2591/tsl2591.h
+++ b/esphome/components/tsl2591/tsl2591.h
@@ -217,14 +217,21 @@ class TSL2591Component : public PollingComponent, public i2c::I2CDevice {
    *
    * This gets called on update and tries to keep the ADC readings in the middle of the range
    */
-
   void automatic_gain_update(uint16_t full_spectrum);
 
+  /** Reads the actual gain used
+   *
+   * Useful for exposing the real gain used when configured in "auto" gain mode
+   */
+  float get_actual_gain();
+
   // ========== INTERNAL METHODS ==========
   // (In most use cases you won't need these. They're for ESPHome integration use.)
   /** Used by ESPHome framework. */
   void set_full_spectrum_sensor(sensor::Sensor *full_spectrum_sensor);
   /** Used by ESPHome framework. */
+  void set_actual_gain_sensor(sensor::Sensor *actual_gain_sensor);
+  /** Used by ESPHome framework. */
   void set_infrared_sensor(sensor::Sensor *infrared_sensor);
   /** Used by ESPHome framework. */
   void set_visible_sensor(sensor::Sensor *visible_sensor);
@@ -249,6 +256,7 @@ class TSL2591Component : public PollingComponent, public i2c::I2CDevice {
   sensor::Sensor *infrared_sensor_{nullptr};
   sensor::Sensor *visible_sensor_{nullptr};
   sensor::Sensor *calculated_lux_sensor_{nullptr};
+  sensor::Sensor *actual_gain_sensor_{nullptr};
   TSL2591IntegrationTime integration_time_;
   TSL2591ComponentGain component_gain_;
   TSL2591Gain gain_;
diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp
index 7b580986e1..89a687e5a6 100644
--- a/esphome/components/tuya/tuya.cpp
+++ b/esphome/components/tuya/tuya.cpp
@@ -66,7 +66,6 @@ void Tuya::dump_config() {
     LOG_PIN("  Status Pin: ", this->status_pin_.value());
   }
   ESP_LOGCONFIG(TAG, "  Product: '%s'", this->product_.c_str());
-  this->check_uart_settings(9600);
 }
 
 bool Tuya::validate_message_() {
diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py
index 01a1049568..ed60a9f880 100644
--- a/esphome/components/uart/__init__.py
+++ b/esphome/components/uart/__init__.py
@@ -246,11 +246,13 @@ def final_validate_device_schema(
     baud_rate: Optional[int] = None,
     require_tx: bool = False,
     require_rx: bool = False,
+    parity: Optional[str] = None,
+    stop_bits: Optional[int] = None,
 ):
     def validate_baud_rate(value):
         if value != baud_rate:
             raise cv.Invalid(
-                f"Component {name} required baud rate {baud_rate} for the uart bus"
+                f"Component {name} requires baud rate {baud_rate} for the uart bus"
             )
         return value
 
@@ -266,6 +268,20 @@ def final_validate_device_schema(
 
         return validator
 
+    def validate_parity(value):
+        if value != parity:
+            raise cv.Invalid(
+                f"Component {name} requires parity {parity} for the uart bus"
+            )
+        return value
+
+    def validate_stop_bits(value):
+        if value != stop_bits:
+            raise cv.Invalid(
+                f"Component {name} requires stop bits {stop_bits} for the uart bus"
+            )
+        return value
+
     def validate_hub(hub_config):
         hub_schema = {}
         uart_id = hub_config[CONF_ID]
@@ -288,6 +304,10 @@ def final_validate_device_schema(
             ] = validate_pin(CONF_RX_PIN, device)
         if baud_rate is not None:
             hub_schema[cv.Required(CONF_BAUD_RATE)] = validate_baud_rate
+        if parity is not None:
+            hub_schema[cv.Required(CONF_PARITY)] = validate_parity
+        if stop_bits is not None:
+            hub_schema[cv.Required(CONF_STOP_BITS)] = validate_stop_bits
         return cv.Schema(hub_schema, extra=cv.ALLOW_EXTRA)(hub_config)
 
     return cv.Schema(
diff --git a/esphome/components/uptime/sensor.py b/esphome/components/uptime/sensor.py
index 07d7d8f2cf..50e584f5d5 100644
--- a/esphome/components/uptime/sensor.py
+++ b/esphome/components/uptime/sensor.py
@@ -3,7 +3,6 @@ import esphome.config_validation as cv
 from esphome.components import sensor
 from esphome.const import (
     ENTITY_CATEGORY_DIAGNOSTIC,
-    STATE_CLASS_TOTAL_INCREASING,
     UNIT_SECOND,
     ICON_TIMER,
     DEVICE_CLASS_DURATION,
@@ -17,7 +16,6 @@ CONFIG_SCHEMA = sensor.sensor_schema(
     unit_of_measurement=UNIT_SECOND,
     icon=ICON_TIMER,
     accuracy_decimals=0,
-    state_class=STATE_CLASS_TOTAL_INCREASING,
     device_class=DEVICE_CLASS_DURATION,
     entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
 ).extend(cv.polling_component_schema("60s"))
diff --git a/esphome/components/vbus/__init__.py b/esphome/components/vbus/__init__.py
new file mode 100644
index 0000000000..70f130e23b
--- /dev/null
+++ b/esphome/components/vbus/__init__.py
@@ -0,0 +1,32 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import uart
+from esphome.const import CONF_ID
+
+CODEOWNERS = ["@ssieb"]
+
+DEPENDENCIES = ["uart"]
+
+MULTI_CONF = True
+
+vbus_ns = cg.esphome_ns.namespace("vbus")
+VBus = vbus_ns.class_("VBus", uart.UARTDevice, cg.Component)
+
+CONF_VBUS_ID = "vbus_id"
+
+CONF_DELTASOL_BS_PLUS = "deltasol_bs_plus"
+CONF_DELTASOL_C = "deltasol_c"
+CONF_DELTASOL_CS2 = "deltasol_cs2"
+CONF_DELTASOL_CS_PLUS = "deltasol_cs_plus"
+
+CONFIG_SCHEMA = uart.UART_DEVICE_SCHEMA.extend(
+    {
+        cv.GenerateID(): cv.declare_id(VBus),
+    }
+)
+
+
+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/vbus/binary_sensor/__init__.py b/esphome/components/vbus/binary_sensor/__init__.py
new file mode 100644
index 0000000000..9901fb2724
--- /dev/null
+++ b/esphome/components/vbus/binary_sensor/__init__.py
@@ -0,0 +1,296 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import binary_sensor
+from esphome.const import (
+    CONF_ID,
+    CONF_BINARY_SENSORS,
+    CONF_COMMAND,
+    CONF_CUSTOM,
+    CONF_DEST,
+    CONF_LAMBDA,
+    CONF_MODEL,
+    CONF_SOURCE,
+    DEVICE_CLASS_PROBLEM,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+)
+from .. import (
+    vbus_ns,
+    VBus,
+    CONF_VBUS_ID,
+    CONF_DELTASOL_BS_PLUS,
+    CONF_DELTASOL_C,
+    CONF_DELTASOL_CS2,
+    CONF_DELTASOL_CS_PLUS,
+)
+
+DeltaSol_BS_Plus = vbus_ns.class_("DeltaSolBSPlusBSensor", cg.Component)
+DeltaSol_C = vbus_ns.class_("DeltaSolCBSensor", cg.Component)
+DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2BSensor", cg.Component)
+DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusBSensor", cg.Component)
+VBusCustom = vbus_ns.class_("VBusCustomBSensor", cg.Component)
+VBusCustomSub = vbus_ns.class_("VBusCustomSubBSensor", cg.Component)
+
+CONF_RELAY1 = "relay1"
+CONF_RELAY2 = "relay2"
+CONF_SENSOR1_ERROR = "sensor1_error"
+CONF_SENSOR2_ERROR = "sensor2_error"
+CONF_SENSOR3_ERROR = "sensor3_error"
+CONF_SENSOR4_ERROR = "sensor4_error"
+CONF_COLLECTOR_MAX = "collector_max"
+CONF_COLLECTOR_MIN = "collector_min"
+CONF_COLLECTOR_FROST = "collector_frost"
+CONF_TUBE_COLLECTOR = "tube_collector"
+CONF_RECOOLING = "recooling"
+CONF_HQM = "hqm"
+
+CONFIG_SCHEMA = cv.typed_schema(
+    {
+        CONF_DELTASOL_BS_PLUS: cv.COMPONENT_SCHEMA.extend(
+            {
+                cv.GenerateID(): cv.declare_id(DeltaSol_BS_Plus),
+                cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus),
+                cv.Optional(CONF_RELAY1): binary_sensor.binary_sensor_schema(),
+                cv.Optional(CONF_RELAY2): binary_sensor.binary_sensor_schema(),
+                cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema(
+                    device_class=DEVICE_CLASS_PROBLEM,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema(
+                    device_class=DEVICE_CLASS_PROBLEM,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema(
+                    device_class=DEVICE_CLASS_PROBLEM,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema(
+                    device_class=DEVICE_CLASS_PROBLEM,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_COLLECTOR_MAX): binary_sensor.binary_sensor_schema(
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_COLLECTOR_MIN): binary_sensor.binary_sensor_schema(
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_COLLECTOR_FROST): binary_sensor.binary_sensor_schema(
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_TUBE_COLLECTOR): binary_sensor.binary_sensor_schema(
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_RECOOLING): binary_sensor.binary_sensor_schema(
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_HQM): binary_sensor.binary_sensor_schema(
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+            }
+        ),
+        CONF_DELTASOL_C: cv.COMPONENT_SCHEMA.extend(
+            {
+                cv.GenerateID(): cv.declare_id(DeltaSol_C),
+                cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus),
+                cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema(
+                    device_class=DEVICE_CLASS_PROBLEM,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema(
+                    device_class=DEVICE_CLASS_PROBLEM,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema(
+                    device_class=DEVICE_CLASS_PROBLEM,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema(
+                    device_class=DEVICE_CLASS_PROBLEM,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+            }
+        ),
+        CONF_DELTASOL_CS2: cv.COMPONENT_SCHEMA.extend(
+            {
+                cv.GenerateID(): cv.declare_id(DeltaSol_CS2),
+                cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus),
+                cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema(
+                    device_class=DEVICE_CLASS_PROBLEM,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema(
+                    device_class=DEVICE_CLASS_PROBLEM,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema(
+                    device_class=DEVICE_CLASS_PROBLEM,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema(
+                    device_class=DEVICE_CLASS_PROBLEM,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+            }
+        ),
+        CONF_DELTASOL_CS_PLUS: cv.COMPONENT_SCHEMA.extend(
+            {
+                cv.GenerateID(): cv.declare_id(DeltaSol_CS_Plus),
+                cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus),
+                cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema(
+                    device_class=DEVICE_CLASS_PROBLEM,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema(
+                    device_class=DEVICE_CLASS_PROBLEM,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema(
+                    device_class=DEVICE_CLASS_PROBLEM,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema(
+                    device_class=DEVICE_CLASS_PROBLEM,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+            }
+        ),
+        CONF_CUSTOM: cv.COMPONENT_SCHEMA.extend(
+            {
+                cv.GenerateID(): cv.declare_id(VBusCustom),
+                cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus),
+                cv.Optional(CONF_COMMAND): cv.uint16_t,
+                cv.Optional(CONF_SOURCE): cv.uint16_t,
+                cv.Optional(CONF_DEST): cv.uint16_t,
+                cv.Optional(CONF_BINARY_SENSORS): cv.ensure_list(
+                    binary_sensor.binary_sensor_schema().extend(
+                        {
+                            cv.GenerateID(): cv.declare_id(VBusCustomSub),
+                            cv.Required(CONF_LAMBDA): cv.lambda_,
+                        }
+                    )
+                ),
+            }
+        ),
+    },
+    key=CONF_MODEL,
+    lower=True,
+    space="_",
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+
+    if config[CONF_MODEL] == CONF_DELTASOL_BS_PLUS:
+        cg.add(var.set_command(0x0100))
+        cg.add(var.set_source(0x4221))
+        cg.add(var.set_dest(0x0010))
+        if CONF_RELAY1 in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_RELAY1])
+            cg.add(var.set_relay1_bsensor(sens))
+        if CONF_RELAY2 in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_RELAY2])
+            cg.add(var.set_relay2_bsensor(sens))
+        if CONF_SENSOR1_ERROR in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR])
+            cg.add(var.set_s1_error_bsensor(sens))
+        if CONF_SENSOR2_ERROR in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR])
+            cg.add(var.set_s2_error_bsensor(sens))
+        if CONF_SENSOR3_ERROR in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR])
+            cg.add(var.set_s3_error_bsensor(sens))
+        if CONF_SENSOR4_ERROR in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR])
+            cg.add(var.set_s4_error_bsensor(sens))
+        if CONF_COLLECTOR_MAX in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_COLLECTOR_MAX])
+            cg.add(var.set_collector_max_bsensor(sens))
+        if CONF_COLLECTOR_MIN in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_COLLECTOR_MIN])
+            cg.add(var.set_collector_min_bsensor(sens))
+        if CONF_COLLECTOR_FROST in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_COLLECTOR_FROST])
+            cg.add(var.set_collector_frost_bsensor(sens))
+        if CONF_TUBE_COLLECTOR in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_TUBE_COLLECTOR])
+            cg.add(var.set_tube_collector_bsensor(sens))
+        if CONF_RECOOLING in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_RECOOLING])
+            cg.add(var.set_recooling_bsensor(sens))
+        if CONF_HQM in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_HQM])
+            cg.add(var.set_hqm_bsensor(sens))
+
+    elif config[CONF_MODEL] == CONF_DELTASOL_C:
+        cg.add(var.set_command(0x0100))
+        cg.add(var.set_source(0x4212))
+        cg.add(var.set_dest(0x0010))
+        if CONF_SENSOR1_ERROR in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR])
+            cg.add(var.set_s1_error_bsensor(sens))
+        if CONF_SENSOR2_ERROR in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR])
+            cg.add(var.set_s2_error_bsensor(sens))
+        if CONF_SENSOR3_ERROR in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR])
+            cg.add(var.set_s3_error_bsensor(sens))
+        if CONF_SENSOR4_ERROR in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR])
+            cg.add(var.set_s4_error_bsensor(sens))
+
+    elif config[CONF_MODEL] == CONF_DELTASOL_CS2:
+        cg.add(var.set_command(0x0100))
+        cg.add(var.set_source(0x1121))
+        cg.add(var.set_dest(0x0010))
+        if CONF_SENSOR1_ERROR in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR])
+            cg.add(var.set_s1_error_bsensor(sens))
+        if CONF_SENSOR2_ERROR in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR])
+            cg.add(var.set_s2_error_bsensor(sens))
+        if CONF_SENSOR3_ERROR in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR])
+            cg.add(var.set_s3_error_bsensor(sens))
+        if CONF_SENSOR4_ERROR in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR])
+            cg.add(var.set_s4_error_bsensor(sens))
+
+    elif config[CONF_MODEL] == CONF_DELTASOL_CS_PLUS:
+        cg.add(var.set_command(0x0100))
+        cg.add(var.set_source(0x2211))
+        cg.add(var.set_dest(0x0010))
+        if CONF_SENSOR1_ERROR in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR])
+            cg.add(var.set_s1_error_bsensor(sens))
+        if CONF_SENSOR2_ERROR in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR])
+            cg.add(var.set_s2_error_bsensor(sens))
+        if CONF_SENSOR3_ERROR in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR])
+            cg.add(var.set_s3_error_bsensor(sens))
+        if CONF_SENSOR4_ERROR in config:
+            sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR])
+            cg.add(var.set_s4_error_bsensor(sens))
+
+    elif config[CONF_MODEL] == CONF_CUSTOM:
+        if CONF_COMMAND in config:
+            cg.add(var.set_command(config[CONF_COMMAND]))
+        if CONF_SOURCE in config:
+            cg.add(var.set_source(config[CONF_SOURCE]))
+        if CONF_DEST in config:
+            cg.add(var.set_dest(config[CONF_DEST]))
+        bsensors = []
+        for conf in config[CONF_BINARY_SENSORS]:
+            bsens = await binary_sensor.new_binary_sensor(conf)
+            lambda_ = await cg.process_lambda(
+                conf[CONF_LAMBDA],
+                [(cg.std_vector.template(cg.uint8), "x")],
+                return_type=cg.bool_,
+            )
+            cg.add(bsens.set_message_parser(lambda_))
+            bsensors.append(bsens)
+        cg.add(var.set_bsensors(bsensors))
+
+    vbus = await cg.get_variable(config[CONF_VBUS_ID])
+    cg.add(vbus.register_listener(var))
diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp
new file mode 100644
index 0000000000..6edbae22ba
--- /dev/null
+++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp
@@ -0,0 +1,142 @@
+#include "vbus_binary_sensor.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace vbus {
+
+static const char *const TAG = "vbus.binary_sensor";
+
+void DeltaSolBSPlusBSensor::dump_config() {
+  ESP_LOGCONFIG(TAG, "Deltasol BS Plus:");
+  LOG_BINARY_SENSOR("  ", "Relay 1 On", this->relay1_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Relay 2 On", this->relay2_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Sensor 1 Error", this->s1_error_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Sensor 2 Error", this->s2_error_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Sensor 3 Error", this->s3_error_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Sensor 4 Error", this->s4_error_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Option Collector Max", this->collector_max_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Option Collector Min", this->collector_min_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Option Collector Frost", this->collector_frost_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Option Tube Collector", this->tube_collector_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Option Recooling", this->recooling_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Option Heat Quantity Measurement", this->hqm_bsensor_);
+}
+
+void DeltaSolBSPlusBSensor::handle_message(std::vector<uint8_t> &message) {
+  if (this->relay1_bsensor_ != nullptr)
+    this->relay1_bsensor_->publish_state(message[10] & 1);
+  if (this->relay2_bsensor_ != nullptr)
+    this->relay2_bsensor_->publish_state(message[10] & 2);
+  if (this->s1_error_bsensor_ != nullptr)
+    this->s1_error_bsensor_->publish_state(message[11] & 1);
+  if (this->s2_error_bsensor_ != nullptr)
+    this->s2_error_bsensor_->publish_state(message[11] & 2);
+  if (this->s3_error_bsensor_ != nullptr)
+    this->s3_error_bsensor_->publish_state(message[11] & 4);
+  if (this->s4_error_bsensor_ != nullptr)
+    this->s4_error_bsensor_->publish_state(message[11] & 8);
+  if (this->collector_max_bsensor_ != nullptr)
+    this->collector_max_bsensor_->publish_state(message[15] & 1);
+  if (this->collector_min_bsensor_ != nullptr)
+    this->collector_min_bsensor_->publish_state(message[15] & 2);
+  if (this->collector_frost_bsensor_ != nullptr)
+    this->collector_frost_bsensor_->publish_state(message[15] & 4);
+  if (this->tube_collector_bsensor_ != nullptr)
+    this->tube_collector_bsensor_->publish_state(message[15] & 8);
+  if (this->recooling_bsensor_ != nullptr)
+    this->recooling_bsensor_->publish_state(message[15] & 0x10);
+  if (this->hqm_bsensor_ != nullptr)
+    this->hqm_bsensor_->publish_state(message[15] & 0x20);
+}
+
+void DeltaSolCBSensor::dump_config() {
+  ESP_LOGCONFIG(TAG, "Deltasol C:");
+  LOG_BINARY_SENSOR("  ", "Sensor 1 Error", this->s1_error_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Sensor 2 Error", this->s2_error_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Sensor 3 Error", this->s3_error_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Sensor 4 Error", this->s4_error_bsensor_);
+}
+
+void DeltaSolCBSensor::handle_message(std::vector<uint8_t> &message) {
+  if (this->s1_error_bsensor_ != nullptr)
+    this->s1_error_bsensor_->publish_state(message[10] & 1);
+  if (this->s2_error_bsensor_ != nullptr)
+    this->s2_error_bsensor_->publish_state(message[10] & 2);
+  if (this->s3_error_bsensor_ != nullptr)
+    this->s3_error_bsensor_->publish_state(message[10] & 4);
+  if (this->s4_error_bsensor_ != nullptr)
+    this->s4_error_bsensor_->publish_state(message[10] & 8);
+}
+
+void DeltaSolCS2BSensor::dump_config() {
+  ESP_LOGCONFIG(TAG, "Deltasol CS2:");
+  LOG_BINARY_SENSOR("  ", "Sensor 1 Error", this->s1_error_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Sensor 2 Error", this->s2_error_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Sensor 3 Error", this->s3_error_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Sensor 4 Error", this->s4_error_bsensor_);
+}
+
+void DeltaSolCS2BSensor::handle_message(std::vector<uint8_t> &message) {
+  if (this->s1_error_bsensor_ != nullptr)
+    this->s1_error_bsensor_->publish_state(message[18] & 1);
+  if (this->s2_error_bsensor_ != nullptr)
+    this->s2_error_bsensor_->publish_state(message[18] & 2);
+  if (this->s3_error_bsensor_ != nullptr)
+    this->s3_error_bsensor_->publish_state(message[18] & 4);
+  if (this->s4_error_bsensor_ != nullptr)
+    this->s4_error_bsensor_->publish_state(message[18] & 8);
+}
+
+void DeltaSolCSPlusBSensor::dump_config() {
+  ESP_LOGCONFIG(TAG, "Deltasol CS Plus:");
+  LOG_BINARY_SENSOR("  ", "Sensor 1 Error", this->s1_error_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Sensor 2 Error", this->s2_error_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Sensor 3 Error", this->s3_error_bsensor_);
+  LOG_BINARY_SENSOR("  ", "Sensor 4 Error", this->s4_error_bsensor_);
+}
+
+void DeltaSolCSPlusBSensor::handle_message(std::vector<uint8_t> &message) {
+  if (this->s1_error_bsensor_ != nullptr)
+    this->s1_error_bsensor_->publish_state(message[20] & 1);
+  if (this->s2_error_bsensor_ != nullptr)
+    this->s2_error_bsensor_->publish_state(message[20] & 2);
+  if (this->s3_error_bsensor_ != nullptr)
+    this->s3_error_bsensor_->publish_state(message[20] & 4);
+  if (this->s4_error_bsensor_ != nullptr)
+    this->s4_error_bsensor_->publish_state(message[20] & 8);
+}
+
+void VBusCustomBSensor::dump_config() {
+  ESP_LOGCONFIG(TAG, "VBus Custom Binary Sensor:");
+  if (this->source_ == 0xffff) {
+    ESP_LOGCONFIG(TAG, "  Source address: ANY");
+  } else {
+    ESP_LOGCONFIG(TAG, "  Source address: 0x%04x", this->source_);
+  }
+  if (this->dest_ == 0xffff) {
+    ESP_LOGCONFIG(TAG, "  Dest address: ANY");
+  } else {
+    ESP_LOGCONFIG(TAG, "  Dest address: 0x%04x", this->dest_);
+  }
+  if (this->command_ == 0xffff) {
+    ESP_LOGCONFIG(TAG, "  Command: ANY");
+  } else {
+    ESP_LOGCONFIG(TAG, "  Command: 0x%04x", this->command_);
+  }
+  ESP_LOGCONFIG(TAG, "  Binary Sensors:");
+  for (VBusCustomSubBSensor *bsensor : this->bsensors_)
+    LOG_BINARY_SENSOR("  ", "-", bsensor);
+}
+
+void VBusCustomBSensor::handle_message(std::vector<uint8_t> &message) {
+  for (VBusCustomSubBSensor *bsensor : this->bsensors_)
+    bsensor->parse_message(message);
+}
+
+void VBusCustomSubBSensor::parse_message(std::vector<uint8_t> &message) {
+  this->publish_state(this->message_parser_(message));
+}
+
+}  // namespace vbus
+}  // namespace esphome
diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h
new file mode 100644
index 0000000000..c0a823a0ab
--- /dev/null
+++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h
@@ -0,0 +1,115 @@
+#pragma once
+
+#include "../vbus.h"
+#include "esphome/components/binary_sensor/binary_sensor.h"
+
+namespace esphome {
+namespace vbus {
+
+class DeltaSolBSPlusBSensor : public VBusListener, public Component {
+ public:
+  void dump_config() override;
+  void set_relay1_bsensor(binary_sensor::BinarySensor *bsensor) { this->relay1_bsensor_ = bsensor; }
+  void set_relay2_bsensor(binary_sensor::BinarySensor *bsensor) { this->relay2_bsensor_ = bsensor; }
+  void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; }
+  void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; }
+  void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; }
+  void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; }
+  void set_collector_max_bsensor(binary_sensor::BinarySensor *bsensor) { this->collector_max_bsensor_ = bsensor; }
+  void set_collector_min_bsensor(binary_sensor::BinarySensor *bsensor) { this->collector_min_bsensor_ = bsensor; }
+  void set_collector_frost_bsensor(binary_sensor::BinarySensor *bsensor) { this->collector_frost_bsensor_ = bsensor; }
+  void set_tube_collector_bsensor(binary_sensor::BinarySensor *bsensor) { this->tube_collector_bsensor_ = bsensor; }
+  void set_recooling_bsensor(binary_sensor::BinarySensor *bsensor) { this->recooling_bsensor_ = bsensor; }
+  void set_hqm_bsensor(binary_sensor::BinarySensor *bsensor) { this->hqm_bsensor_ = bsensor; }
+
+ protected:
+  binary_sensor::BinarySensor *relay1_bsensor_{nullptr};
+  binary_sensor::BinarySensor *relay2_bsensor_{nullptr};
+  binary_sensor::BinarySensor *s1_error_bsensor_{nullptr};
+  binary_sensor::BinarySensor *s2_error_bsensor_{nullptr};
+  binary_sensor::BinarySensor *s3_error_bsensor_{nullptr};
+  binary_sensor::BinarySensor *s4_error_bsensor_{nullptr};
+  binary_sensor::BinarySensor *collector_max_bsensor_{nullptr};
+  binary_sensor::BinarySensor *collector_min_bsensor_{nullptr};
+  binary_sensor::BinarySensor *collector_frost_bsensor_{nullptr};
+  binary_sensor::BinarySensor *tube_collector_bsensor_{nullptr};
+  binary_sensor::BinarySensor *recooling_bsensor_{nullptr};
+  binary_sensor::BinarySensor *hqm_bsensor_{nullptr};
+
+  void handle_message(std::vector<uint8_t> &message) override;
+};
+
+class DeltaSolCBSensor : public VBusListener, public Component {
+ public:
+  void dump_config() override;
+  void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; }
+  void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; }
+  void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; }
+  void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; }
+
+ protected:
+  binary_sensor::BinarySensor *s1_error_bsensor_{nullptr};
+  binary_sensor::BinarySensor *s2_error_bsensor_{nullptr};
+  binary_sensor::BinarySensor *s3_error_bsensor_{nullptr};
+  binary_sensor::BinarySensor *s4_error_bsensor_{nullptr};
+
+  void handle_message(std::vector<uint8_t> &message) override;
+};
+
+class DeltaSolCS2BSensor : public VBusListener, public Component {
+ public:
+  void dump_config() override;
+  void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; }
+  void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; }
+  void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; }
+  void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; }
+
+ protected:
+  binary_sensor::BinarySensor *s1_error_bsensor_{nullptr};
+  binary_sensor::BinarySensor *s2_error_bsensor_{nullptr};
+  binary_sensor::BinarySensor *s3_error_bsensor_{nullptr};
+  binary_sensor::BinarySensor *s4_error_bsensor_{nullptr};
+
+  void handle_message(std::vector<uint8_t> &message) override;
+};
+
+class DeltaSolCSPlusBSensor : public VBusListener, public Component {
+ public:
+  void dump_config() override;
+  void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; }
+  void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; }
+  void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; }
+  void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; }
+
+ protected:
+  binary_sensor::BinarySensor *s1_error_bsensor_{nullptr};
+  binary_sensor::BinarySensor *s2_error_bsensor_{nullptr};
+  binary_sensor::BinarySensor *s3_error_bsensor_{nullptr};
+  binary_sensor::BinarySensor *s4_error_bsensor_{nullptr};
+
+  void handle_message(std::vector<uint8_t> &message) override;
+};
+
+class VBusCustomSubBSensor;
+
+class VBusCustomBSensor : public VBusListener, public Component {
+ public:
+  void dump_config() override;
+  void set_bsensors(std::vector<VBusCustomSubBSensor *> bsensors) { this->bsensors_ = std::move(bsensors); };
+
+ protected:
+  std::vector<VBusCustomSubBSensor *> bsensors_;
+  void handle_message(std::vector<uint8_t> &message) override;
+};
+
+class VBusCustomSubBSensor : public binary_sensor::BinarySensor, public Component {
+ public:
+  void set_message_parser(message_parser_t parser) { this->message_parser_ = std::move(parser); };
+  void parse_message(std::vector<uint8_t> &message);
+
+ protected:
+  message_parser_t message_parser_;
+};
+
+}  // namespace vbus
+}  // namespace esphome
diff --git a/esphome/components/vbus/sensor/__init__.py b/esphome/components/vbus/sensor/__init__.py
new file mode 100644
index 0000000000..bce28758ce
--- /dev/null
+++ b/esphome/components/vbus/sensor/__init__.py
@@ -0,0 +1,568 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor
+from esphome.const import (
+    CONF_ID,
+    CONF_COMMAND,
+    CONF_CUSTOM,
+    CONF_DEST,
+    CONF_LAMBDA,
+    CONF_MODEL,
+    CONF_SENSORS,
+    CONF_SOURCE,
+    CONF_TIME,
+    CONF_VERSION,
+    DEVICE_CLASS_DURATION,
+    DEVICE_CLASS_EMPTY,
+    DEVICE_CLASS_ENERGY,
+    DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+    ICON_PERCENT,
+    ICON_RADIATOR,
+    ICON_THERMOMETER,
+    ICON_TIMER,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+    UNIT_HOUR,
+    UNIT_MINUTE,
+    UNIT_PERCENT,
+    UNIT_WATT_HOURS,
+)
+from .. import (
+    vbus_ns,
+    VBus,
+    CONF_VBUS_ID,
+    CONF_DELTASOL_BS_PLUS,
+    CONF_DELTASOL_C,
+    CONF_DELTASOL_CS2,
+    CONF_DELTASOL_CS_PLUS,
+)
+
+DeltaSol_BS_Plus = vbus_ns.class_("DeltaSolBSPlusSensor", cg.Component)
+DeltaSol_C = vbus_ns.class_("DeltaSolCSensor", cg.Component)
+DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2Sensor", cg.Component)
+DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusSensor", cg.Component)
+VBusCustom = vbus_ns.class_("VBusCustomSensor", cg.Component)
+VBusCustomSub = vbus_ns.class_("VBusCustomSubSensor", cg.Component)
+
+CONF_FLOW_RATE = "flow_rate"
+CONF_HEAT_QUANTITY = "heat_quantity"
+CONF_OPERATING_HOURS = "operating_hours"
+CONF_OPERATING_HOURS_1 = "operating_hours_1"
+CONF_OPERATING_HOURS_2 = "operating_hours_2"
+CONF_PUMP_SPEED = "pump_speed"
+CONF_PUMP_SPEED_1 = "pump_speed_1"
+CONF_PUMP_SPEED_2 = "pump_speed_2"
+CONF_TEMPERATURE_1 = "temperature_1"
+CONF_TEMPERATURE_2 = "temperature_2"
+CONF_TEMPERATURE_3 = "temperature_3"
+CONF_TEMPERATURE_4 = "temperature_4"
+CONF_TEMPERATURE_5 = "temperature_5"
+
+CONFIG_SCHEMA = cv.typed_schema(
+    {
+        CONF_DELTASOL_BS_PLUS: cv.COMPONENT_SCHEMA.extend(
+            {
+                cv.GenerateID(): cv.declare_id(DeltaSol_BS_Plus),
+                cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus),
+                cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_CELSIUS,
+                    icon=ICON_THERMOMETER,
+                    accuracy_decimals=1,
+                    device_class=DEVICE_CLASS_TEMPERATURE,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_CELSIUS,
+                    icon=ICON_THERMOMETER,
+                    accuracy_decimals=1,
+                    device_class=DEVICE_CLASS_TEMPERATURE,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_CELSIUS,
+                    icon=ICON_THERMOMETER,
+                    accuracy_decimals=1,
+                    device_class=DEVICE_CLASS_TEMPERATURE,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_CELSIUS,
+                    icon=ICON_THERMOMETER,
+                    accuracy_decimals=1,
+                    device_class=DEVICE_CLASS_TEMPERATURE,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_PUMP_SPEED_1): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_PERCENT,
+                    icon=ICON_PERCENT,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_EMPTY,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_PUMP_SPEED_2): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_PERCENT,
+                    icon=ICON_PERCENT,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_EMPTY,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_OPERATING_HOURS_1): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_HOUR,
+                    icon=ICON_TIMER,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_DURATION,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_OPERATING_HOURS_2): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_HOUR,
+                    icon=ICON_TIMER,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_DURATION,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_WATT_HOURS,
+                    icon=ICON_RADIATOR,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_ENERGY,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_TIME): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_MINUTE,
+                    icon=ICON_TIMER,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_DURATION,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_VERSION): sensor.sensor_schema(
+                    accuracy_decimals=2,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+            }
+        ),
+        CONF_DELTASOL_C: cv.COMPONENT_SCHEMA.extend(
+            {
+                cv.GenerateID(): cv.declare_id(DeltaSol_C),
+                cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus),
+                cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_CELSIUS,
+                    icon=ICON_THERMOMETER,
+                    accuracy_decimals=1,
+                    device_class=DEVICE_CLASS_TEMPERATURE,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_CELSIUS,
+                    icon=ICON_THERMOMETER,
+                    accuracy_decimals=1,
+                    device_class=DEVICE_CLASS_TEMPERATURE,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_CELSIUS,
+                    icon=ICON_THERMOMETER,
+                    accuracy_decimals=1,
+                    device_class=DEVICE_CLASS_TEMPERATURE,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_CELSIUS,
+                    icon=ICON_THERMOMETER,
+                    accuracy_decimals=1,
+                    device_class=DEVICE_CLASS_TEMPERATURE,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_PUMP_SPEED_1): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_PERCENT,
+                    icon=ICON_PERCENT,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_EMPTY,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_PUMP_SPEED_2): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_PERCENT,
+                    icon=ICON_PERCENT,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_EMPTY,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_OPERATING_HOURS_1): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_HOUR,
+                    icon=ICON_TIMER,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_DURATION,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_OPERATING_HOURS_2): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_HOUR,
+                    icon=ICON_TIMER,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_DURATION,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_WATT_HOURS,
+                    icon=ICON_RADIATOR,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_ENERGY,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_TIME): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_MINUTE,
+                    icon=ICON_TIMER,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_DURATION,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+            }
+        ),
+        CONF_DELTASOL_CS2: cv.COMPONENT_SCHEMA.extend(
+            {
+                cv.GenerateID(): cv.declare_id(DeltaSol_CS2),
+                cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus),
+                cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_CELSIUS,
+                    icon=ICON_THERMOMETER,
+                    accuracy_decimals=1,
+                    device_class=DEVICE_CLASS_TEMPERATURE,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_CELSIUS,
+                    icon=ICON_THERMOMETER,
+                    accuracy_decimals=1,
+                    device_class=DEVICE_CLASS_TEMPERATURE,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_CELSIUS,
+                    icon=ICON_THERMOMETER,
+                    accuracy_decimals=1,
+                    device_class=DEVICE_CLASS_TEMPERATURE,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_CELSIUS,
+                    icon=ICON_THERMOMETER,
+                    accuracy_decimals=1,
+                    device_class=DEVICE_CLASS_TEMPERATURE,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_PUMP_SPEED): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_PERCENT,
+                    icon=ICON_PERCENT,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_EMPTY,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_OPERATING_HOURS): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_HOUR,
+                    icon=ICON_TIMER,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_DURATION,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_WATT_HOURS,
+                    icon=ICON_RADIATOR,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_ENERGY,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_VERSION): sensor.sensor_schema(
+                    accuracy_decimals=2,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+            }
+        ),
+        CONF_DELTASOL_CS_PLUS: cv.COMPONENT_SCHEMA.extend(
+            {
+                cv.GenerateID(): cv.declare_id(DeltaSol_CS_Plus),
+                cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus),
+                cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_CELSIUS,
+                    icon=ICON_THERMOMETER,
+                    accuracy_decimals=1,
+                    device_class=DEVICE_CLASS_TEMPERATURE,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_CELSIUS,
+                    icon=ICON_THERMOMETER,
+                    accuracy_decimals=1,
+                    device_class=DEVICE_CLASS_TEMPERATURE,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_CELSIUS,
+                    icon=ICON_THERMOMETER,
+                    accuracy_decimals=1,
+                    device_class=DEVICE_CLASS_TEMPERATURE,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_CELSIUS,
+                    icon=ICON_THERMOMETER,
+                    accuracy_decimals=1,
+                    device_class=DEVICE_CLASS_TEMPERATURE,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_TEMPERATURE_5): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_CELSIUS,
+                    icon=ICON_THERMOMETER,
+                    accuracy_decimals=1,
+                    device_class=DEVICE_CLASS_TEMPERATURE,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_PUMP_SPEED_1): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_PERCENT,
+                    icon=ICON_PERCENT,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_EMPTY,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_PUMP_SPEED_2): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_PERCENT,
+                    icon=ICON_PERCENT,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_EMPTY,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_OPERATING_HOURS_1): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_HOUR,
+                    icon=ICON_TIMER,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_DURATION,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_OPERATING_HOURS_2): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_HOUR,
+                    icon=ICON_TIMER,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_DURATION,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_WATT_HOURS,
+                    icon=ICON_RADIATOR,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_ENERGY,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+                cv.Optional(CONF_TIME): sensor.sensor_schema(
+                    unit_of_measurement=UNIT_MINUTE,
+                    icon=ICON_TIMER,
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_DURATION,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_VERSION): sensor.sensor_schema(
+                    accuracy_decimals=2,
+                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+                ),
+                cv.Optional(CONF_FLOW_RATE): sensor.sensor_schema(
+                    accuracy_decimals=0,
+                    device_class=DEVICE_CLASS_EMPTY,
+                    state_class=STATE_CLASS_MEASUREMENT,
+                ),
+            }
+        ),
+        CONF_CUSTOM: cv.COMPONENT_SCHEMA.extend(
+            {
+                cv.GenerateID(): cv.declare_id(VBusCustom),
+                cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus),
+                cv.Optional(CONF_COMMAND): cv.uint16_t,
+                cv.Optional(CONF_SOURCE): cv.uint16_t,
+                cv.Optional(CONF_DEST): cv.uint16_t,
+                cv.Optional(CONF_SENSORS): cv.ensure_list(
+                    sensor.sensor_schema().extend(
+                        {
+                            cv.GenerateID(): cv.declare_id(VBusCustomSub),
+                            cv.Required(CONF_LAMBDA): cv.lambda_,
+                        }
+                    )
+                ),
+            }
+        ),
+    },
+    key=CONF_MODEL,
+    lower=True,
+    space="_",
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+
+    if config[CONF_MODEL] == CONF_DELTASOL_BS_PLUS:
+        cg.add(var.set_command(0x0100))
+        cg.add(var.set_source(0x4221))
+        cg.add(var.set_dest(0x0010))
+        if CONF_TEMPERATURE_1 in config:
+            sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1])
+            cg.add(var.set_temperature1_sensor(sens))
+        if CONF_TEMPERATURE_2 in config:
+            sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2])
+            cg.add(var.set_temperature2_sensor(sens))
+        if CONF_TEMPERATURE_3 in config:
+            sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3])
+            cg.add(var.set_temperature3_sensor(sens))
+        if CONF_TEMPERATURE_4 in config:
+            sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4])
+            cg.add(var.set_temperature4_sensor(sens))
+        if CONF_PUMP_SPEED_1 in config:
+            sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_1])
+            cg.add(var.set_pump_speed1_sensor(sens))
+        if CONF_PUMP_SPEED_2 in config:
+            sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_2])
+            cg.add(var.set_pump_speed2_sensor(sens))
+        if CONF_OPERATING_HOURS_1 in config:
+            sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_1])
+            cg.add(var.set_operating_hours1_sensor(sens))
+        if CONF_OPERATING_HOURS_2 in config:
+            sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_2])
+            cg.add(var.set_operating_hours2_sensor(sens))
+        if CONF_HEAT_QUANTITY in config:
+            sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY])
+            cg.add(var.set_heat_quantity_sensor(sens))
+        if CONF_TIME in config:
+            sens = await sensor.new_sensor(config[CONF_TIME])
+            cg.add(var.set_time_sensor(sens))
+        if CONF_VERSION in config:
+            sens = await sensor.new_sensor(config[CONF_VERSION])
+            cg.add(var.set_version_sensor(sens))
+
+    elif config[CONF_MODEL] == CONF_DELTASOL_C:
+        cg.add(var.set_command(0x0100))
+        cg.add(var.set_source(0x4212))
+        cg.add(var.set_dest(0x0010))
+        if CONF_TEMPERATURE_1 in config:
+            sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1])
+            cg.add(var.set_temperature1_sensor(sens))
+        if CONF_TEMPERATURE_2 in config:
+            sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2])
+            cg.add(var.set_temperature2_sensor(sens))
+        if CONF_TEMPERATURE_3 in config:
+            sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3])
+            cg.add(var.set_temperature3_sensor(sens))
+        if CONF_TEMPERATURE_4 in config:
+            sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4])
+            cg.add(var.set_temperature4_sensor(sens))
+        if CONF_PUMP_SPEED_1 in config:
+            sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_1])
+            cg.add(var.set_pump_speed1_sensor(sens))
+        if CONF_PUMP_SPEED_2 in config:
+            sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_2])
+            cg.add(var.set_pump_speed2_sensor(sens))
+        if CONF_OPERATING_HOURS_1 in config:
+            sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_1])
+            cg.add(var.set_operating_hours1_sensor(sens))
+        if CONF_OPERATING_HOURS_2 in config:
+            sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_2])
+            cg.add(var.set_operating_hours2_sensor(sens))
+        if CONF_HEAT_QUANTITY in config:
+            sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY])
+            cg.add(var.set_heat_quantity_sensor(sens))
+        if CONF_TIME in config:
+            sens = await sensor.new_sensor(config[CONF_TIME])
+            cg.add(var.set_time_sensor(sens))
+
+    elif config[CONF_MODEL] == CONF_DELTASOL_CS2:
+        cg.add(var.set_command(0x0100))
+        cg.add(var.set_source(0x1121))
+        cg.add(var.set_dest(0x0010))
+        if CONF_TEMPERATURE_1 in config:
+            sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1])
+            cg.add(var.set_temperature1_sensor(sens))
+        if CONF_TEMPERATURE_2 in config:
+            sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2])
+            cg.add(var.set_temperature2_sensor(sens))
+        if CONF_TEMPERATURE_3 in config:
+            sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3])
+            cg.add(var.set_temperature3_sensor(sens))
+        if CONF_TEMPERATURE_4 in config:
+            sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4])
+            cg.add(var.set_temperature4_sensor(sens))
+        if CONF_PUMP_SPEED in config:
+            sens = await sensor.new_sensor(config[CONF_PUMP_SPEED])
+            cg.add(var.set_pump_speed_sensor(sens))
+        if CONF_OPERATING_HOURS in config:
+            sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS])
+            cg.add(var.set_operating_hours_sensor(sens))
+        if CONF_HEAT_QUANTITY in config:
+            sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY])
+            cg.add(var.set_heat_quantity_sensor(sens))
+        if CONF_VERSION in config:
+            sens = await sensor.new_sensor(config[CONF_VERSION])
+            cg.add(var.set_version_sensor(sens))
+
+    if config[CONF_MODEL] == CONF_DELTASOL_CS_PLUS:
+        cg.add(var.set_command(0x0100))
+        cg.add(var.set_source(0x2211))
+        cg.add(var.set_dest(0x0010))
+        if CONF_TEMPERATURE_1 in config:
+            sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1])
+            cg.add(var.set_temperature1_sensor(sens))
+        if CONF_TEMPERATURE_2 in config:
+            sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2])
+            cg.add(var.set_temperature2_sensor(sens))
+        if CONF_TEMPERATURE_3 in config:
+            sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3])
+            cg.add(var.set_temperature3_sensor(sens))
+        if CONF_TEMPERATURE_4 in config:
+            sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4])
+            cg.add(var.set_temperature4_sensor(sens))
+        if CONF_TEMPERATURE_5 in config:
+            sens = await sensor.new_sensor(config[CONF_TEMPERATURE_5])
+            cg.add(var.set_temperature5_sensor(sens))
+        if CONF_PUMP_SPEED_1 in config:
+            sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_1])
+            cg.add(var.set_pump_speed1_sensor(sens))
+        if CONF_PUMP_SPEED_2 in config:
+            sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_2])
+            cg.add(var.set_pump_speed2_sensor(sens))
+        if CONF_OPERATING_HOURS_1 in config:
+            sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_1])
+            cg.add(var.set_operating_hours1_sensor(sens))
+        if CONF_OPERATING_HOURS_2 in config:
+            sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_2])
+            cg.add(var.set_operating_hours2_sensor(sens))
+        if CONF_HEAT_QUANTITY in config:
+            sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY])
+            cg.add(var.set_heat_quantity_sensor(sens))
+        if CONF_TIME in config:
+            sens = await sensor.new_sensor(config[CONF_TIME])
+            cg.add(var.set_time_sensor(sens))
+        if CONF_VERSION in config:
+            sens = await sensor.new_sensor(config[CONF_VERSION])
+            cg.add(var.set_version_sensor(sens))
+        if CONF_FLOW_RATE in config:
+            sens = await sensor.new_sensor(config[CONF_FLOW_RATE])
+            cg.add(var.set_flow_rate_sensor(sens))
+
+    elif config[CONF_MODEL] == CONF_CUSTOM:
+        if CONF_COMMAND in config:
+            cg.add(var.set_command(config[CONF_COMMAND]))
+        if CONF_SOURCE in config:
+            cg.add(var.set_source(config[CONF_SOURCE]))
+        if CONF_DEST in config:
+            cg.add(var.set_dest(config[CONF_DEST]))
+        sensors = []
+        for conf in config[CONF_SENSORS]:
+            sens = await sensor.new_sensor(conf)
+            lambda_ = await cg.process_lambda(
+                conf[CONF_LAMBDA],
+                [(cg.std_vector.template(cg.uint8), "x")],
+                return_type=cg.float_,
+            )
+            cg.add(sens.set_message_parser(lambda_))
+            sensors.append(sens)
+        cg.add(var.set_sensors(sensors))
+
+    vbus = await cg.get_variable(config[CONF_VBUS_ID])
+    cg.add(vbus.register_listener(var))
diff --git a/esphome/components/vbus/sensor/vbus_sensor.cpp b/esphome/components/vbus/sensor/vbus_sensor.cpp
new file mode 100644
index 0000000000..57d5a355ad
--- /dev/null
+++ b/esphome/components/vbus/sensor/vbus_sensor.cpp
@@ -0,0 +1,208 @@
+#include "vbus_sensor.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace vbus {
+
+static const char *const TAG = "vbus.sensor";
+
+static inline uint16_t get_u16(std::vector<uint8_t> &message, int start) {
+  return (message[start + 1] << 8) + message[start];
+}
+
+static inline int16_t get_i16(std::vector<uint8_t> &message, int start) {
+  return (int16_t)((message[start + 1] << 8) + message[start]);
+}
+
+void DeltaSolBSPlusSensor::dump_config() {
+  ESP_LOGCONFIG(TAG, "Deltasol BS Plus:");
+  LOG_SENSOR("  ", "Temperature 1", this->temperature1_sensor_);
+  LOG_SENSOR("  ", "Temperature 2", this->temperature2_sensor_);
+  LOG_SENSOR("  ", "Temperature 3", this->temperature3_sensor_);
+  LOG_SENSOR("  ", "Temperature 4", this->temperature4_sensor_);
+  LOG_SENSOR("  ", "Pump Speed 1", this->pump_speed1_sensor_);
+  LOG_SENSOR("  ", "Pump Speed 2", this->pump_speed2_sensor_);
+  LOG_SENSOR("  ", "Operating Hours 1", this->operating_hours1_sensor_);
+  LOG_SENSOR("  ", "Operating Hours 2", this->operating_hours2_sensor_);
+  LOG_SENSOR("  ", "Heat Quantity", this->heat_quantity_sensor_);
+  LOG_SENSOR("  ", "System Time", this->time_sensor_);
+  LOG_SENSOR("  ", "FW Version", this->version_sensor_);
+}
+
+void DeltaSolBSPlusSensor::handle_message(std::vector<uint8_t> &message) {
+  if (this->temperature1_sensor_ != nullptr)
+    this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f);
+  if (this->temperature2_sensor_ != nullptr)
+    this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f);
+  if (this->temperature3_sensor_ != nullptr)
+    this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f);
+  if (this->temperature4_sensor_ != nullptr)
+    this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f);
+  if (this->pump_speed1_sensor_ != nullptr)
+    this->pump_speed1_sensor_->publish_state(message[8]);
+  if (this->pump_speed2_sensor_ != nullptr)
+    this->pump_speed2_sensor_->publish_state(message[9]);
+  if (this->operating_hours1_sensor_ != nullptr)
+    this->operating_hours1_sensor_->publish_state(get_u16(message, 16));
+  if (this->operating_hours2_sensor_ != nullptr)
+    this->operating_hours2_sensor_->publish_state(get_u16(message, 18));
+  if (this->heat_quantity_sensor_ != nullptr) {
+    this->heat_quantity_sensor_->publish_state(get_u16(message, 20) + get_u16(message, 22) * 1000 +
+                                               get_u16(message, 24) * 1000000);
+  }
+  if (this->time_sensor_ != nullptr)
+    this->time_sensor_->publish_state(get_u16(message, 12));
+  if (this->version_sensor_ != nullptr)
+    this->version_sensor_->publish_state(get_u16(message, 26) * 0.01f);
+}
+
+void DeltaSolCSensor::dump_config() {
+  ESP_LOGCONFIG(TAG, "Deltasol C:");
+  LOG_SENSOR("  ", "Temperature 1", this->temperature1_sensor_);
+  LOG_SENSOR("  ", "Temperature 2", this->temperature2_sensor_);
+  LOG_SENSOR("  ", "Temperature 3", this->temperature3_sensor_);
+  LOG_SENSOR("  ", "Temperature 4", this->temperature4_sensor_);
+  LOG_SENSOR("  ", "Pump Speed 1", this->pump_speed1_sensor_);
+  LOG_SENSOR("  ", "Pump Speed 2", this->pump_speed2_sensor_);
+  LOG_SENSOR("  ", "Operating Hours 1", this->operating_hours1_sensor_);
+  LOG_SENSOR("  ", "Operating Hours 2", this->operating_hours2_sensor_);
+  LOG_SENSOR("  ", "Heat Quantity", this->heat_quantity_sensor_);
+  LOG_SENSOR("  ", "System Time", this->time_sensor_);
+}
+
+void DeltaSolCSensor::handle_message(std::vector<uint8_t> &message) {
+  if (this->temperature1_sensor_ != nullptr)
+    this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f);
+  if (this->temperature2_sensor_ != nullptr)
+    this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f);
+  if (this->temperature3_sensor_ != nullptr)
+    this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f);
+  if (this->temperature4_sensor_ != nullptr)
+    this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f);
+  if (this->pump_speed1_sensor_ != nullptr)
+    this->pump_speed1_sensor_->publish_state(message[8]);
+  if (this->pump_speed2_sensor_ != nullptr)
+    this->pump_speed2_sensor_->publish_state(message[9]);
+  if (this->operating_hours1_sensor_ != nullptr)
+    this->operating_hours1_sensor_->publish_state(get_u16(message, 12));
+  if (this->operating_hours2_sensor_ != nullptr)
+    this->operating_hours2_sensor_->publish_state(get_u16(message, 14));
+  if (this->heat_quantity_sensor_ != nullptr) {
+    this->heat_quantity_sensor_->publish_state(get_u16(message, 16) + get_u16(message, 18) * 1000 +
+                                               get_u16(message, 20) * 1000000);
+  }
+  if (this->time_sensor_ != nullptr)
+    this->time_sensor_->publish_state(get_u16(message, 22));
+}
+
+void DeltaSolCS2Sensor::dump_config() {
+  ESP_LOGCONFIG(TAG, "Deltasol CS2:");
+  LOG_SENSOR("  ", "Temperature 1", this->temperature1_sensor_);
+  LOG_SENSOR("  ", "Temperature 2", this->temperature2_sensor_);
+  LOG_SENSOR("  ", "Temperature 3", this->temperature3_sensor_);
+  LOG_SENSOR("  ", "Temperature 4", this->temperature4_sensor_);
+  LOG_SENSOR("  ", "Pump Speed", this->pump_speed_sensor_);
+  LOG_SENSOR("  ", "Operating Hours", this->operating_hours_sensor_);
+  LOG_SENSOR("  ", "Heat Quantity", this->heat_quantity_sensor_);
+  LOG_SENSOR("  ", "FW Version", this->version_sensor_);
+}
+
+void DeltaSolCS2Sensor::handle_message(std::vector<uint8_t> &message) {
+  if (this->temperature1_sensor_ != nullptr)
+    this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f);
+  if (this->temperature2_sensor_ != nullptr)
+    this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f);
+  if (this->temperature3_sensor_ != nullptr)
+    this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f);
+  if (this->temperature4_sensor_ != nullptr)
+    this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f);
+  if (this->pump_speed_sensor_ != nullptr)
+    this->pump_speed_sensor_->publish_state(message[12]);
+  if (this->operating_hours_sensor_ != nullptr)
+    this->operating_hours_sensor_->publish_state(get_u16(message, 14));
+  if (this->heat_quantity_sensor_ != nullptr)
+    this->heat_quantity_sensor_->publish_state((get_u16(message, 26) << 16) + get_u16(message, 24));
+  if (this->version_sensor_ != nullptr)
+    this->version_sensor_->publish_state(get_u16(message, 28) * 0.01f);
+}
+
+void DeltaSolCSPlusSensor::dump_config() {
+  ESP_LOGCONFIG(TAG, "Deltasol CS Plus:");
+  LOG_SENSOR("  ", "Temperature 1", this->temperature1_sensor_);
+  LOG_SENSOR("  ", "Temperature 2", this->temperature2_sensor_);
+  LOG_SENSOR("  ", "Temperature 3", this->temperature3_sensor_);
+  LOG_SENSOR("  ", "Temperature 4", this->temperature4_sensor_);
+  LOG_SENSOR("  ", "Temperature 5", this->temperature5_sensor_);
+  LOG_SENSOR("  ", "Pump Speed 1", this->pump_speed1_sensor_);
+  LOG_SENSOR("  ", "Pump Speed 2", this->pump_speed2_sensor_);
+  LOG_SENSOR("  ", "Operating Hours 1", this->operating_hours1_sensor_);
+  LOG_SENSOR("  ", "Operating Hours 2", this->operating_hours2_sensor_);
+  LOG_SENSOR("  ", "Heat Quantity", this->heat_quantity_sensor_);
+  LOG_SENSOR("  ", "System Time", this->time_sensor_);
+  LOG_SENSOR("  ", "FW Version", this->version_sensor_);
+  LOG_SENSOR("  ", "Flow Rate", this->flow_rate_sensor_);
+}
+
+void DeltaSolCSPlusSensor::handle_message(std::vector<uint8_t> &message) {
+  if (this->temperature1_sensor_ != nullptr)
+    this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f);
+  if (this->temperature2_sensor_ != nullptr)
+    this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f);
+  if (this->temperature3_sensor_ != nullptr)
+    this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f);
+  if (this->temperature4_sensor_ != nullptr)
+    this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f);
+  if (this->temperature5_sensor_ != nullptr)
+    this->temperature5_sensor_->publish_state(get_i16(message, 36) * 0.1f);
+  if (this->pump_speed1_sensor_ != nullptr)
+    this->pump_speed1_sensor_->publish_state(message[8]);
+  if (this->pump_speed2_sensor_ != nullptr)
+    this->pump_speed2_sensor_->publish_state(message[12]);
+  if (this->operating_hours1_sensor_ != nullptr)
+    this->operating_hours1_sensor_->publish_state(get_u16(message, 10));
+  if (this->operating_hours2_sensor_ != nullptr)
+    this->operating_hours2_sensor_->publish_state(get_u16(message, 14));
+  if (this->heat_quantity_sensor_ != nullptr)
+    this->heat_quantity_sensor_->publish_state((get_u16(message, 30) << 16) + get_u16(message, 28));
+  if (this->time_sensor_ != nullptr)
+    this->time_sensor_->publish_state(get_u16(message, 12));
+  if (this->version_sensor_ != nullptr)
+    this->version_sensor_->publish_state(get_u16(message, 26) * 0.01f);
+  if (this->flow_rate_sensor_ != nullptr)
+    this->flow_rate_sensor_->publish_state(get_u16(message, 38));
+}
+
+void VBusCustomSensor::dump_config() {
+  ESP_LOGCONFIG(TAG, "VBus Custom Sensor:");
+  if (this->source_ == 0xffff) {
+    ESP_LOGCONFIG(TAG, "  Source address: ANY");
+  } else {
+    ESP_LOGCONFIG(TAG, "  Source address: 0x%04x", this->source_);
+  }
+  if (this->dest_ == 0xffff) {
+    ESP_LOGCONFIG(TAG, "  Dest address: ANY");
+  } else {
+    ESP_LOGCONFIG(TAG, "  Dest address: 0x%04x", this->dest_);
+  }
+  if (this->command_ == 0xffff) {
+    ESP_LOGCONFIG(TAG, "  Command: ANY");
+  } else {
+    ESP_LOGCONFIG(TAG, "  Command: 0x%04x", this->command_);
+  }
+  ESP_LOGCONFIG(TAG, "  Sensors:");
+  for (VBusCustomSubSensor *sensor : this->sensors_)
+    LOG_SENSOR("  ", "-", sensor);
+}
+
+void VBusCustomSensor::handle_message(std::vector<uint8_t> &message) {
+  for (VBusCustomSubSensor *sensor : this->sensors_)
+    sensor->parse_message(message);
+}
+
+void VBusCustomSubSensor::parse_message(std::vector<uint8_t> &message) {
+  this->publish_state(this->message_parser_(message));
+}
+
+}  // namespace vbus
+}  // namespace esphome
diff --git a/esphome/components/vbus/sensor/vbus_sensor.h b/esphome/components/vbus/sensor/vbus_sensor.h
new file mode 100644
index 0000000000..6ba752b68c
--- /dev/null
+++ b/esphome/components/vbus/sensor/vbus_sensor.h
@@ -0,0 +1,151 @@
+#pragma once
+
+#include "../vbus.h"
+#include "esphome/components/sensor/sensor.h"
+
+namespace esphome {
+namespace vbus {
+
+class DeltaSolBSPlusSensor : public VBusListener, public Component {
+ public:
+  void dump_config() override;
+  void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; }
+  void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; }
+  void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; }
+  void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; }
+  void set_pump_speed1_sensor(sensor::Sensor *sensor) { this->pump_speed1_sensor_ = sensor; }
+  void set_pump_speed2_sensor(sensor::Sensor *sensor) { this->pump_speed2_sensor_ = sensor; }
+  void set_operating_hours1_sensor(sensor::Sensor *sensor) { this->operating_hours1_sensor_ = sensor; }
+  void set_operating_hours2_sensor(sensor::Sensor *sensor) { this->operating_hours2_sensor_ = sensor; }
+  void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; }
+  void set_time_sensor(sensor::Sensor *sensor) { this->time_sensor_ = sensor; }
+  void set_version_sensor(sensor::Sensor *sensor) { this->version_sensor_ = sensor; }
+
+ protected:
+  sensor::Sensor *temperature1_sensor_{nullptr};
+  sensor::Sensor *temperature2_sensor_{nullptr};
+  sensor::Sensor *temperature3_sensor_{nullptr};
+  sensor::Sensor *temperature4_sensor_{nullptr};
+  sensor::Sensor *pump_speed1_sensor_{nullptr};
+  sensor::Sensor *pump_speed2_sensor_{nullptr};
+  sensor::Sensor *operating_hours1_sensor_{nullptr};
+  sensor::Sensor *operating_hours2_sensor_{nullptr};
+  sensor::Sensor *heat_quantity_sensor_{nullptr};
+  sensor::Sensor *time_sensor_{nullptr};
+  sensor::Sensor *version_sensor_{nullptr};
+
+  void handle_message(std::vector<uint8_t> &message) override;
+};
+
+class DeltaSolCSensor : public VBusListener, public Component {
+ public:
+  void dump_config() override;
+  void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; }
+  void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; }
+  void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; }
+  void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; }
+  void set_pump_speed1_sensor(sensor::Sensor *sensor) { this->pump_speed1_sensor_ = sensor; }
+  void set_pump_speed2_sensor(sensor::Sensor *sensor) { this->pump_speed2_sensor_ = sensor; }
+  void set_operating_hours1_sensor(sensor::Sensor *sensor) { this->operating_hours1_sensor_ = sensor; }
+  void set_operating_hours2_sensor(sensor::Sensor *sensor) { this->operating_hours2_sensor_ = sensor; }
+  void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; }
+  void set_time_sensor(sensor::Sensor *sensor) { this->time_sensor_ = sensor; }
+
+ protected:
+  sensor::Sensor *temperature1_sensor_{nullptr};
+  sensor::Sensor *temperature2_sensor_{nullptr};
+  sensor::Sensor *temperature3_sensor_{nullptr};
+  sensor::Sensor *temperature4_sensor_{nullptr};
+  sensor::Sensor *pump_speed1_sensor_{nullptr};
+  sensor::Sensor *pump_speed2_sensor_{nullptr};
+  sensor::Sensor *operating_hours1_sensor_{nullptr};
+  sensor::Sensor *operating_hours2_sensor_{nullptr};
+  sensor::Sensor *heat_quantity_sensor_{nullptr};
+  sensor::Sensor *time_sensor_{nullptr};
+
+  void handle_message(std::vector<uint8_t> &message) override;
+};
+
+class DeltaSolCS2Sensor : public VBusListener, public Component {
+ public:
+  void dump_config() override;
+  void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; }
+  void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; }
+  void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; }
+  void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; }
+  void set_pump_speed_sensor(sensor::Sensor *sensor) { this->pump_speed_sensor_ = sensor; }
+  void set_operating_hours_sensor(sensor::Sensor *sensor) { this->operating_hours_sensor_ = sensor; }
+  void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; }
+  void set_version_sensor(sensor::Sensor *sensor) { this->version_sensor_ = sensor; }
+
+ protected:
+  sensor::Sensor *temperature1_sensor_{nullptr};
+  sensor::Sensor *temperature2_sensor_{nullptr};
+  sensor::Sensor *temperature3_sensor_{nullptr};
+  sensor::Sensor *temperature4_sensor_{nullptr};
+  sensor::Sensor *pump_speed_sensor_{nullptr};
+  sensor::Sensor *operating_hours_sensor_{nullptr};
+  sensor::Sensor *heat_quantity_sensor_{nullptr};
+  sensor::Sensor *version_sensor_{nullptr};
+
+  void handle_message(std::vector<uint8_t> &message) override;
+};
+
+class DeltaSolCSPlusSensor : public VBusListener, public Component {
+ public:
+  void dump_config() override;
+  void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; }
+  void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; }
+  void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; }
+  void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; }
+  void set_temperature5_sensor(sensor::Sensor *sensor) { this->temperature5_sensor_ = sensor; }
+  void set_pump_speed1_sensor(sensor::Sensor *sensor) { this->pump_speed1_sensor_ = sensor; }
+  void set_pump_speed2_sensor(sensor::Sensor *sensor) { this->pump_speed2_sensor_ = sensor; }
+  void set_operating_hours1_sensor(sensor::Sensor *sensor) { this->operating_hours1_sensor_ = sensor; }
+  void set_operating_hours2_sensor(sensor::Sensor *sensor) { this->operating_hours2_sensor_ = sensor; }
+  void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; }
+  void set_time_sensor(sensor::Sensor *sensor) { this->time_sensor_ = sensor; }
+  void set_version_sensor(sensor::Sensor *sensor) { this->version_sensor_ = sensor; }
+  void set_flow_rate_sensor(sensor::Sensor *sensor) { this->flow_rate_sensor_ = sensor; }
+
+ protected:
+  sensor::Sensor *temperature1_sensor_{nullptr};
+  sensor::Sensor *temperature2_sensor_{nullptr};
+  sensor::Sensor *temperature3_sensor_{nullptr};
+  sensor::Sensor *temperature4_sensor_{nullptr};
+  sensor::Sensor *temperature5_sensor_{nullptr};
+  sensor::Sensor *pump_speed1_sensor_{nullptr};
+  sensor::Sensor *pump_speed2_sensor_{nullptr};
+  sensor::Sensor *operating_hours1_sensor_{nullptr};
+  sensor::Sensor *operating_hours2_sensor_{nullptr};
+  sensor::Sensor *heat_quantity_sensor_{nullptr};
+  sensor::Sensor *time_sensor_{nullptr};
+  sensor::Sensor *version_sensor_{nullptr};
+  sensor::Sensor *flow_rate_sensor_{nullptr};
+
+  void handle_message(std::vector<uint8_t> &message) override;
+};
+
+class VBusCustomSubSensor;
+
+class VBusCustomSensor : public VBusListener, public Component {
+ public:
+  void dump_config() override;
+  void set_sensors(std::vector<VBusCustomSubSensor *> sensors) { this->sensors_ = std::move(sensors); };
+
+ protected:
+  std::vector<VBusCustomSubSensor *> sensors_;
+  void handle_message(std::vector<uint8_t> &message) override;
+};
+
+class VBusCustomSubSensor : public sensor::Sensor, public Component {
+ public:
+  void set_message_parser(message_parser_t parser) { this->message_parser_ = std::move(parser); };
+  void parse_message(std::vector<uint8_t> &message);
+
+ protected:
+  message_parser_t message_parser_;
+};
+
+}  // namespace vbus
+}  // namespace esphome
diff --git a/esphome/components/vbus/vbus.cpp b/esphome/components/vbus/vbus.cpp
new file mode 100644
index 0000000000..c9758891cc
--- /dev/null
+++ b/esphome/components/vbus/vbus.cpp
@@ -0,0 +1,124 @@
+#include "vbus.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace vbus {
+
+static const char *const TAG = "vbus";
+
+void VBus::dump_config() {
+  ESP_LOGCONFIG(TAG, "VBus:");
+  check_uart_settings(9600);
+}
+
+static void septet_spread(uint8_t *data, int start, int count, uint8_t septet) {
+  for (int i = 0; i < count; i++, septet >>= 1) {
+    if (septet & 1)
+      data[start + i] |= 0x80;
+  }
+}
+
+static bool checksum(const uint8_t *data, int start, int count) {
+  uint8_t csum = 0x7f;
+  for (int i = 0; i < count; i++)
+    csum = (csum - data[start + i]) & 0x7f;
+  return csum == 0;
+}
+
+void VBus::loop() {
+  if (!available())
+    return;
+
+  while (available()) {
+    uint8_t c;
+    read_byte(&c);
+
+    if (c == 0xaa) {
+      this->state_ = 1;
+      this->buffer_.clear();
+      continue;
+    }
+    if (c & 0x80) {
+      this->state_ = 0;
+      continue;
+    }
+    if (this->state_ == 0)
+      continue;
+
+    if (this->state_ == 1) {
+      this->buffer_.push_back(c);
+      if (this->buffer_.size() == 7) {
+        this->protocol_ = this->buffer_[4];
+        this->source_ = (this->buffer_[3] << 8) + this->buffer_[2];
+        this->dest_ = (this->buffer_[1] << 8) + this->buffer_[0];
+        this->command_ = (this->buffer_[6] << 8) + this->buffer_[5];
+      }
+      if ((this->protocol_ == 0x20) && (this->buffer_.size() == 15)) {
+        this->state_ = 0;
+        if (!checksum(this->buffer_.data(), 0, 15)) {
+          ESP_LOGE(TAG, "P2 checksum failed");
+          continue;
+        }
+        septet_spread(this->buffer_.data(), 7, 6, this->buffer_[13]);
+        uint16_t id = (this->buffer_[8] << 8) + this->buffer_[7];
+        uint32_t value =
+            (this->buffer_[12] << 24) + (this->buffer_[11] << 16) + (this->buffer_[10] << 8) + this->buffer_[9];
+        ESP_LOGV(TAG, "P1 C%04x %04x->%04x: %04x %04x (%d)", this->command_, this->source_, this->dest_, id, value,
+                 value);
+      } else if ((this->protocol_ == 0x10) && (this->buffer_.size() == 9)) {
+        if (!checksum(this->buffer_.data(), 0, 9)) {
+          ESP_LOGE(TAG, "P1 checksum failed");
+          this->state_ = 0;
+          continue;
+        }
+        this->frames_ = this->buffer_[7];
+        if (this->frames_) {
+          this->state_ = 2;
+          this->cframe_ = 0;
+          this->fbcount_ = 0;
+          this->buffer_.clear();
+        } else {
+          this->state_ = 0;
+          ESP_LOGD(TAG, "P1 empty message");
+        }
+      }
+      continue;
+    }
+
+    if (this->state_ == 2) {
+      this->fbytes_[this->fbcount_++] = c;
+      if (this->fbcount_ < 6)
+        continue;
+      this->fbcount_ = 0;
+      if (!checksum(this->fbytes_, 0, 6)) {
+        ESP_LOGE(TAG, "frame checksum failed");
+        continue;
+      }
+      septet_spread(this->fbytes_, 0, 4, this->fbytes_[4]);
+      for (int i = 0; i < 4; i++)
+        this->buffer_.push_back(this->fbytes_[i]);
+      if (++this->cframe_ < this->frames_)
+        continue;
+      ESP_LOGV(TAG, "P2 C%04x %04x->%04x: %s", this->command_, this->source_, this->dest_,
+               format_hex(this->buffer_).c_str());
+      for (auto &listener : this->listeners_)
+        listener->on_message(this->command_, this->source_, this->dest_, this->buffer_);
+      this->state_ = 0;
+      continue;
+    }
+  }
+}
+
+void VBusListener::on_message(uint16_t command, uint16_t source, uint16_t dest, std::vector<uint8_t> &message) {
+  if ((this->command_ != 0xffff) && (this->command_ != command))
+    return;
+  if ((this->source_ != 0xffff) && (this->source_ != source))
+    return;
+  if ((this->dest_ != 0xffff) && (this->dest_ != dest))
+    return;
+  this->handle_message(message);
+}
+
+}  // namespace vbus
+}  // namespace esphome
diff --git a/esphome/components/vbus/vbus.h b/esphome/components/vbus/vbus.h
new file mode 100644
index 0000000000..7e97b5049a
--- /dev/null
+++ b/esphome/components/vbus/vbus.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/uart/uart.h"
+
+namespace esphome {
+namespace vbus {
+
+using message_parser_t = std::function<float(std::vector<uint8_t> &)>;
+
+class VBus;
+
+class VBusListener {
+ public:
+  void set_command(uint16_t command) { this->command_ = command; }
+  void set_source(uint16_t source) { this->source_ = source; }
+  void set_dest(uint16_t dest) { this->dest_ = dest; }
+
+  void on_message(uint16_t command, uint16_t source, uint16_t dest, std::vector<uint8_t> &message);
+
+ protected:
+  uint16_t command_{0xffff};
+  uint16_t source_{0xffff};
+  uint16_t dest_{0xffff};
+
+  virtual void handle_message(std::vector<uint8_t> &message) = 0;
+};
+
+class VBus : public uart::UARTDevice, public Component {
+ public:
+  void dump_config() override;
+  void loop() override;
+  float get_setup_priority() const override { return setup_priority::DATA; }
+
+  void register_listener(VBusListener *listener) { this->listeners_.push_back(listener); }
+
+ protected:
+  int state_{0};
+  std::vector<uint8_t> buffer_;
+  uint8_t protocol_;
+  uint16_t source_;
+  uint16_t dest_;
+  uint16_t command_;
+  uint8_t frames_;
+  uint8_t cframe_;
+  uint8_t fbytes_[6];
+  int fbcount_;
+  std::vector<VBusListener *> listeners_{};
+};
+
+}  // namespace vbus
+}  // namespace esphome
diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp
index 30ac959e43..513399e257 100644
--- a/esphome/components/web_server/web_server.cpp
+++ b/esphome/components/web_server/web_server.cpp
@@ -2,12 +2,12 @@
 
 #include "web_server.h"
 
-#include "esphome/core/log.h"
-#include "esphome/core/application.h"
-#include "esphome/core/entity_base.h"
-#include "esphome/core/util.h"
 #include "esphome/components/json/json_util.h"
 #include "esphome/components/network/util.h"
+#include "esphome/core/application.h"
+#include "esphome/core/entity_base.h"
+#include "esphome/core/log.h"
+#include "esphome/core/util.h"
 
 #include "StreamString.h"
 
@@ -83,6 +83,13 @@ UrlMatch match_url(const std::string &url, bool only_domain = false) {
   return match;
 }
 
+WebServer::WebServer(web_server_base::WebServerBase *base)
+    : base_(base), entities_iterator_(ListEntitiesIterator(this)) {
+#ifdef USE_ESP32
+  to_schedule_lock_ = xSemaphoreCreateMutex();
+#endif
+}
+
 void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; }
 void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; }
 void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; }
@@ -97,7 +104,8 @@ void WebServer::setup() {
     // Configure reconnect timeout and send config
 
     client->send(json::build_json([this](JsonObject root) {
-                   root["title"] = App.get_name();
+                   root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name();
+                   root["comment"] = App.get_comment();
                    root["ota"] = this->allow_ota_;
                    root["lang"] = "en";
                  }).c_str(),
@@ -120,7 +128,25 @@ void WebServer::setup() {
 
   this->set_interval(10000, [this]() { this->events_.send("", "ping", millis(), 30000); });
 }
-void WebServer::loop() { this->entities_iterator_.advance(); }
+void WebServer::loop() {
+#ifdef USE_ESP32
+  if (xSemaphoreTake(this->to_schedule_lock_, 0L)) {
+    std::function<void()> fn;
+    if (!to_schedule_.empty()) {
+      // scheduler execute things out of order which may lead to incorrect state
+      // this->defer(std::move(to_schedule_.front()));
+      // let's execute it directly from the loop
+      fn = std::move(to_schedule_.front());
+      to_schedule_.pop_front();
+    }
+    xSemaphoreGive(this->to_schedule_lock_);
+    if (fn) {
+      fn();
+    }
+  }
+#endif
+  this->entities_iterator_.advance();
+}
 void WebServer::dump_config() {
   ESP_LOGCONFIG(TAG, "Web Server:");
   ESP_LOGCONFIG(TAG, "  Address: %s:%u", network::get_use_address().c_str(), this->base_->get_port());
@@ -413,13 +439,13 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
       std::string data = this->switch_json(obj, obj->state, DETAIL_STATE);
       request->send(200, "application/json", data.c_str());
     } else if (match.method == "toggle") {
-      this->defer([obj]() { obj->toggle(); });
+      this->schedule_([obj]() { obj->toggle(); });
       request->send(200);
     } else if (match.method == "turn_on") {
-      this->defer([obj]() { obj->turn_on(); });
+      this->schedule_([obj]() { obj->turn_on(); });
       request->send(200);
     } else if (match.method == "turn_off") {
-      this->defer([obj]() { obj->turn_off(); });
+      this->schedule_([obj]() { obj->turn_off(); });
       request->send(200);
     } else {
       request->send(404);
@@ -441,7 +467,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
     if (obj->get_object_id() != match.id)
       continue;
     if (request->method() == HTTP_POST && match.method == "press") {
-      this->defer([obj]() { obj->press(); });
+      this->schedule_([obj]() { obj->press(); });
       request->send(200);
       return;
     } else {
@@ -497,7 +523,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
       std::string data = this->fan_json(obj, DETAIL_STATE);
       request->send(200, "application/json", data.c_str());
     } else if (match.method == "toggle") {
-      this->defer([obj]() { obj->toggle().perform(); });
+      this->schedule_([obj]() { obj->toggle().perform(); });
       request->send(200);
     } else if (match.method == "turn_on") {
       auto call = obj->turn_on();
@@ -531,10 +557,10 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
             return;
         }
       }
-      this->defer([call]() mutable { call.perform(); });
+      this->schedule_([call]() mutable { call.perform(); });
       request->send(200);
     } else if (match.method == "turn_off") {
-      this->defer([obj]() { obj->turn_off().perform(); });
+      this->schedule_([obj]() { obj->turn_off().perform(); });
       request->send(200);
     } else {
       request->send(404);
@@ -558,7 +584,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
       std::string data = this->light_json(obj, DETAIL_STATE);
       request->send(200, "application/json", data.c_str());
     } else if (match.method == "toggle") {
-      this->defer([obj]() { obj->toggle().perform(); });
+      this->schedule_([obj]() { obj->toggle().perform(); });
       request->send(200);
     } else if (match.method == "turn_on") {
       auto call = obj->turn_on();
@@ -590,7 +616,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
         call.set_effect(effect);
       }
 
-      this->defer([call]() mutable { call.perform(); });
+      this->schedule_([call]() mutable { call.perform(); });
       request->send(200);
     } else if (match.method == "turn_off") {
       auto call = obj->turn_off();
@@ -598,7 +624,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
         auto length = (uint32_t) request->getParam("transition")->value().toFloat() * 1000;
         call.set_transition_length(length);
       }
-      this->defer([call]() mutable { call.perform(); });
+      this->schedule_([call]() mutable { call.perform(); });
       request->send(200);
     } else {
       request->send(404);
@@ -663,7 +689,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
     if (request->hasParam("tilt"))
       call.set_tilt(request->getParam("tilt")->value().toFloat());
 
-    this->defer([call]() mutable { call.perform(); });
+    this->schedule_([call]() mutable { call.perform(); });
     request->send(200);
     return;
   }
@@ -708,7 +734,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
         call.set_value(*value_f);
     }
 
-    this->defer([call]() mutable { call.perform(); });
+    this->schedule_([call]() mutable { call.perform(); });
     request->send(200);
     return;
   }
@@ -765,7 +791,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
       call.set_option(option.c_str());  // NOLINT(clang-diagnostic-deprecated-declarations)
     }
 
-    this->defer([call]() mutable { call.perform(); });
+    this->schedule_([call]() mutable { call.perform(); });
     request->send(200);
     return;
   }
@@ -833,7 +859,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
         call.set_target_temperature(*value_f);
     }
 
-    this->defer([call]() mutable { call.perform(); });
+    this->schedule_([call]() mutable { call.perform(); });
     request->send(200);
     return;
   }
@@ -949,13 +975,13 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
       std::string data = this->lock_json(obj, obj->state, DETAIL_STATE);
       request->send(200, "application/json", data.c_str());
     } else if (match.method == "lock") {
-      this->defer([obj]() { obj->lock(); });
+      this->schedule_([obj]() { obj->lock(); });
       request->send(200);
     } else if (match.method == "unlock") {
-      this->defer([obj]() { obj->unlock(); });
+      this->schedule_([obj]() { obj->unlock(); });
       request->send(200);
     } else if (match.method == "open") {
-      this->defer([obj]() { obj->open(); });
+      this->schedule_([obj]() { obj->open(); });
       request->send(200);
     } else {
       request->send(404);
@@ -1154,6 +1180,16 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
 
 bool WebServer::isRequestHandlerTrivial() { return false; }
 
+void WebServer::schedule_(std::function<void()> &&f) {
+#ifdef USE_ESP32
+  xSemaphoreTake(this->to_schedule_lock_, portMAX_DELAY);
+  to_schedule_.push_back(std::move(f));
+  xSemaphoreGive(this->to_schedule_lock_);
+#else
+  this->defer(std::move(f));
+#endif
+}
+
 }  // namespace web_server
 }  // namespace esphome
 
diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h
index 78d0597e61..f4122ef754 100644
--- a/esphome/components/web_server/web_server.h
+++ b/esphome/components/web_server/web_server.h
@@ -9,7 +9,11 @@
 #include "esphome/core/controller.h"
 
 #include <vector>
-
+#ifdef USE_ESP32
+#include <deque>
+#include <freertos/FreeRTOS.h>
+#include <freertos/semphr.h>
+#endif
 namespace esphome {
 namespace web_server {
 
@@ -34,7 +38,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE };
  */
 class WebServer : public Controller, public Component, public AsyncWebHandler {
  public:
-  WebServer(web_server_base::WebServerBase *base) : base_(base), entities_iterator_(ListEntitiesIterator(this)) {}
+  WebServer(web_server_base::WebServerBase *base);
 
   /** Set the URL to the CSS <link> that's sent to each client. Defaults to
    * https://esphome.io/_static/webserver-v1.min.css
@@ -220,6 +224,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
   bool isRequestHandlerTrivial() override;
 
  protected:
+  void schedule_(std::function<void()> &&f);
   friend ListEntitiesIterator;
   web_server_base::WebServerBase *base_;
   AsyncEventSource events_{"/events"};
@@ -230,6 +235,10 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
   const char *js_include_{nullptr};
   bool include_internal_{false};
   bool allow_ota_{true};
+#ifdef USE_ESP32
+  std::deque<std::function<void()>> to_schedule_;
+  SemaphoreHandle_t to_schedule_lock_;
+#endif
 };
 
 }  // namespace web_server
diff --git a/esphome/components/wiegand/__init__.py b/esphome/components/wiegand/__init__.py
new file mode 100644
index 0000000000..7b05c43198
--- /dev/null
+++ b/esphome/components/wiegand/__init__.py
@@ -0,0 +1,78 @@
+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
+
+CODEOWNERS = ["@ssieb"]
+
+AUTO_LOAD = ["key_provider"]
+
+MULTI_CONF = True
+
+wiegand_ns = cg.esphome_ns.namespace("wiegand")
+
+Wiegand = wiegand_ns.class_("Wiegand", key_provider.KeyProvider, cg.Component)
+WiegandTagTrigger = wiegand_ns.class_(
+    "WiegandTagTrigger", automation.Trigger.template(cg.std_string)
+)
+WiegandRawTrigger = wiegand_ns.class_(
+    "WiegandRawTrigger", automation.Trigger.template(cg.uint8, cg.uint64)
+)
+WiegandKeyTrigger = wiegand_ns.class_(
+    "WiegandKeyTrigger", automation.Trigger.template(cg.uint8)
+)
+
+CONF_D0 = "d0"
+CONF_D1 = "d1"
+CONF_ON_KEY = "on_key"
+CONF_ON_RAW = "on_raw"
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(Wiegand),
+        cv.Required(CONF_D0): pins.internal_gpio_input_pin_schema,
+        cv.Required(CONF_D1): pins.internal_gpio_input_pin_schema,
+        cv.Optional(CONF_ON_TAG): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WiegandTagTrigger),
+            }
+        ),
+        cv.Optional(CONF_ON_RAW): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WiegandRawTrigger),
+            }
+        ),
+        cv.Optional(CONF_ON_KEY): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WiegandKeyTrigger),
+            }
+        ),
+    }
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    pin = await cg.gpio_pin_expression(config[CONF_D0])
+    cg.add(var.set_d0_pin(pin))
+    pin = await cg.gpio_pin_expression(config[CONF_D1])
+    cg.add(var.set_d1_pin(pin))
+
+    for conf in config.get(CONF_ON_TAG, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
+        cg.add(var.register_tag_trigger(trigger))
+        await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
+
+    for conf in config.get(CONF_ON_RAW, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
+        cg.add(var.register_raw_trigger(trigger))
+        await automation.build_automation(
+            trigger, [(cg.uint8, "bits"), (cg.uint64, "value")], conf
+        )
+
+    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/wiegand/wiegand.cpp b/esphome/components/wiegand/wiegand.cpp
new file mode 100644
index 0000000000..67558da731
--- /dev/null
+++ b/esphome/components/wiegand/wiegand.cpp
@@ -0,0 +1,117 @@
+#include "wiegand.h"
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+
+namespace esphome {
+namespace wiegand {
+
+static const char *const TAG = "wiegand";
+static const char *const KEYS = "0123456789*#";
+
+void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) {
+  if (arg->d0.digital_read())
+    return;
+  arg->count++;
+  arg->value <<= 1;
+  arg->last_bit_time = millis();
+  arg->done = false;
+}
+
+void IRAM_ATTR HOT WiegandStore::d1_gpio_intr(WiegandStore *arg) {
+  if (arg->d1.digital_read())
+    return;
+  arg->count++;
+  arg->value = (arg->value << 1) | 1;
+  arg->last_bit_time = millis();
+  arg->done = false;
+}
+
+void Wiegand::setup() {
+  this->d0_pin_->setup();
+  this->store_.d0 = this->d0_pin_->to_isr();
+  this->d1_pin_->setup();
+  this->store_.d1 = this->d1_pin_->to_isr();
+  this->d0_pin_->attach_interrupt(WiegandStore::d0_gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE);
+  this->d1_pin_->attach_interrupt(WiegandStore::d1_gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE);
+}
+
+bool check_eparity(uint64_t value, int start, int length) {
+  int parity = 0;
+  uint64_t mask = 1LL << start;
+  for (int i = 0; i <= length; i++, mask <<= 1) {
+    if (value & i)
+      parity++;
+  }
+  return !(parity & 1);
+}
+
+bool check_oparity(uint64_t value, int start, int length) {
+  int parity = 0;
+  uint64_t mask = 1LL << start;
+  for (int i = 0; i <= length; i++, mask <<= 1) {
+    if (value & i)
+      parity++;
+  }
+  return parity & 1;
+}
+
+void Wiegand::loop() {
+  if (this->store_.done)
+    return;
+  if (millis() - this->store_.last_bit_time < 100)
+    return;
+  uint8_t count = this->store_.count;
+  uint64_t value = this->store_.value;
+  this->store_.count = 0;
+  this->store_.value = 0;
+  this->store_.done = true;
+  ESP_LOGV(TAG, "received %d-bit value: %llx", count, value);
+  for (auto *trigger : this->raw_triggers_)
+    trigger->trigger(count, value);
+  if (count == 26) {
+    std::string tag = to_string((value >> 1) & 0xffffff);
+    ESP_LOGD(TAG, "received 26-bit tag: %s", tag.c_str());
+    if (!check_eparity(value, 13, 13) || !check_oparity(value, 0, 13)) {
+      ESP_LOGW(TAG, "invalid parity");
+      return;
+    }
+    for (auto *trigger : this->tag_triggers_)
+      trigger->trigger(tag);
+  } else if (count == 34) {
+    std::string tag = to_string((value >> 1) & 0xffffffff);
+    ESP_LOGD(TAG, "received 34-bit tag: %s", tag.c_str());
+    if (!check_eparity(value, 17, 17) || !check_oparity(value, 0, 17)) {
+      ESP_LOGW(TAG, "invalid parity");
+      return;
+    }
+    for (auto *trigger : this->tag_triggers_)
+      trigger->trigger(tag);
+  } else if (count == 37) {
+    std::string tag = to_string((value >> 1) & 0x7ffffffff);
+    ESP_LOGD(TAG, "received 37-bit tag: %s", tag.c_str());
+    if (!check_eparity(value, 18, 19) || !check_oparity(value, 0, 19)) {
+      ESP_LOGW(TAG, "invalid parity");
+      return;
+    }
+    for (auto *trigger : this->tag_triggers_)
+      trigger->trigger(tag);
+  } else if (count == 4) {
+    for (auto *trigger : this->key_triggers_)
+      trigger->trigger(value);
+    if (value < 12) {
+      uint8_t key = KEYS[value];
+      this->send_key_(key);
+    }
+  } else {
+    ESP_LOGD(TAG, "received unknown %d-bit value: %llx", count, value);
+  }
+}
+
+void Wiegand::dump_config() {
+  ESP_LOGCONFIG(TAG, "Wiegand reader:");
+  LOG_PIN("  D0 pin: ", this->d0_pin_);
+  LOG_PIN("  D1 pin: ", this->d1_pin_);
+}
+
+}  // namespace wiegand
+}  // namespace esphome
diff --git a/esphome/components/wiegand/wiegand.h b/esphome/components/wiegand/wiegand.h
new file mode 100644
index 0000000000..994631a3a3
--- /dev/null
+++ b/esphome/components/wiegand/wiegand.h
@@ -0,0 +1,54 @@
+#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"
+
+namespace esphome {
+namespace wiegand {
+
+class Wiegand;
+
+struct WiegandStore {
+  ISRInternalGPIOPin d0;
+  ISRInternalGPIOPin d1;
+  volatile uint64_t value{0};
+  volatile uint32_t last_bit_time{0};
+  volatile bool done{true};
+  volatile uint8_t count{0};
+
+  static void d0_gpio_intr(WiegandStore *arg);
+  static void d1_gpio_intr(WiegandStore *arg);
+};
+
+class WiegandTagTrigger : public Trigger<std::string> {};
+
+class WiegandRawTrigger : public Trigger<uint8_t, uint64_t> {};
+
+class WiegandKeyTrigger : public Trigger<uint8_t> {};
+
+class Wiegand : public key_provider::KeyProvider, public Component {
+ public:
+  float get_setup_priority() const override { return setup_priority::HARDWARE; }
+  void setup() override;
+  void loop() override;
+  void dump_config() override;
+
+  void set_d0_pin(InternalGPIOPin *pin) { this->d0_pin_ = pin; };
+  void set_d1_pin(InternalGPIOPin *pin) { this->d1_pin_ = pin; };
+  void register_tag_trigger(WiegandTagTrigger *trig) { this->tag_triggers_.push_back(trig); }
+  void register_raw_trigger(WiegandRawTrigger *trig) { this->raw_triggers_.push_back(trig); }
+  void register_key_trigger(WiegandKeyTrigger *trig) { this->key_triggers_.push_back(trig); }
+
+ protected:
+  InternalGPIOPin *d0_pin_;
+  InternalGPIOPin *d1_pin_;
+  WiegandStore store_{};
+  std::vector<WiegandTagTrigger *> tag_triggers_;
+  std::vector<WiegandRawTrigger *> raw_triggers_;
+  std::vector<WiegandKeyTrigger *> key_triggers_;
+};
+
+}  // namespace wiegand
+}  // namespace esphome
diff --git a/esphome/components/x9c/__init__.py b/esphome/components/x9c/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/x9c/output.py b/esphome/components/x9c/output.py
new file mode 100644
index 0000000000..44e9d729b3
--- /dev/null
+++ b/esphome/components/x9c/output.py
@@ -0,0 +1,46 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import pins
+from esphome.components import output
+from esphome.const import (
+    CONF_ID,
+    CONF_CS_PIN,
+    CONF_INC_PIN,
+    CONF_UD_PIN,
+    CONF_INITIAL_VALUE,
+)
+
+CODEOWNERS = ["@EtienneMD"]
+
+x9c_ns = cg.esphome_ns.namespace("x9c")
+
+X9cOutput = x9c_ns.class_("X9cOutput", output.FloatOutput, cg.Component)
+
+CONFIG_SCHEMA = cv.All(
+    output.FLOAT_OUTPUT_SCHEMA.extend(
+        {
+            cv.GenerateID(): cv.declare_id(X9cOutput),
+            cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_schema,
+            cv.Required(CONF_INC_PIN): pins.internal_gpio_output_pin_schema,
+            cv.Required(CONF_UD_PIN): pins.internal_gpio_output_pin_schema,
+            cv.Optional(CONF_INITIAL_VALUE, default=1.0): cv.float_range(
+                min=0.01, max=1.0
+            ),
+        }
+    )
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await output.register_output(var, config)
+
+    cs_pin = await cg.gpio_pin_expression(config[CONF_CS_PIN])
+    cg.add(var.set_cs_pin(cs_pin))
+    inc_pin = await cg.gpio_pin_expression(config[CONF_INC_PIN])
+    cg.add(var.set_inc_pin(inc_pin))
+    ud_pin = await cg.gpio_pin_expression(config[CONF_UD_PIN])
+    cg.add(var.set_ud_pin(ud_pin))
+
+    cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE]))
diff --git a/esphome/components/x9c/x9c.cpp b/esphome/components/x9c/x9c.cpp
new file mode 100644
index 0000000000..ff7777e71f
--- /dev/null
+++ b/esphome/components/x9c/x9c.cpp
@@ -0,0 +1,72 @@
+#include "x9c.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace x9c {
+
+static const char *const TAG = "x9c.output";
+
+void X9cOutput::trim_value(int change_amount) {
+  if (change_amount > 0) {  // Set change direction
+    this->ud_pin_->digital_write(true);
+  } else {
+    this->ud_pin_->digital_write(false);
+  }
+
+  this->inc_pin_->digital_write(true);
+  this->cs_pin_->digital_write(false);  // Select chip
+
+  for (int i = 0; i < abs(change_amount); i++) {  // Move wiper
+    this->inc_pin_->digital_write(true);
+    delayMicroseconds(1);
+    this->inc_pin_->digital_write(false);
+    delayMicroseconds(1);
+  }
+
+  delayMicroseconds(100);  // Let value settle
+
+  this->inc_pin_->digital_write(false);
+  this->cs_pin_->digital_write(true);  // Deselect chip safely (no save)
+}
+
+void X9cOutput::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up X9C Potentiometer with initial value of %f", this->initial_value_);
+
+  this->inc_pin_->get_pin();
+  this->inc_pin_->setup();
+  this->inc_pin_->digital_write(false);
+
+  this->cs_pin_->get_pin();
+  this->cs_pin_->setup();
+  this->cs_pin_->digital_write(true);
+
+  this->ud_pin_->get_pin();
+  this->ud_pin_->setup();
+
+  if (this->initial_value_ <= 0.50) {
+    this->trim_value(-101);  // Set min value (beyond 0)
+    this->trim_value((int) (this->initial_value_ * 100));
+  } else {
+    this->trim_value(101);  // Set max value (beyond 100)
+    this->trim_value((int) (this->initial_value_ * 100) - 100);
+  }
+  this->pot_value_ = this->initial_value_;
+  this->write_state(this->initial_value_);
+}
+
+void X9cOutput::write_state(float state) {
+  this->trim_value((int) ((state - this->pot_value_) * 100));
+  this->pot_value_ = state;
+}
+
+void X9cOutput::dump_config() {
+  ESP_LOGCONFIG(TAG, "X9C Potentiometer Output:");
+  LOG_PIN("  Chip Select Pin: ", this->cs_pin_);
+  LOG_PIN("  Increment Pin: ", this->inc_pin_);
+  LOG_PIN("  Up/Down Pin: ", this->ud_pin_);
+  ESP_LOGCONFIG(TAG, "  Initial Value: %f", this->initial_value_);
+  LOG_FLOAT_OUTPUT(this);
+}
+
+}  // namespace x9c
+}  // namespace esphome
diff --git a/esphome/components/x9c/x9c.h b/esphome/components/x9c/x9c.h
new file mode 100644
index 0000000000..924460c841
--- /dev/null
+++ b/esphome/components/x9c/x9c.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/hal.h"
+#include "esphome/components/output/float_output.h"
+
+namespace esphome {
+namespace x9c {
+
+class X9cOutput : public output::FloatOutput, public Component {
+ public:
+  void set_cs_pin(InternalGPIOPin *pin) { cs_pin_ = pin; }
+  void set_inc_pin(InternalGPIOPin *pin) { inc_pin_ = pin; }
+  void set_ud_pin(InternalGPIOPin *pin) { ud_pin_ = pin; }
+  void set_initial_value(float initial_value) { initial_value_ = initial_value; }
+
+  void setup() override;
+  void dump_config() override;
+
+  void trim_value(int change_amount);
+
+ protected:
+  void write_state(float state) override;
+  InternalGPIOPin *cs_pin_;
+  InternalGPIOPin *inc_pin_;
+  InternalGPIOPin *ud_pin_;
+  float initial_value_;
+  float pot_value_;
+};
+
+}  // namespace x9c
+}  // namespace esphome
diff --git a/esphome/config.py b/esphome/config.py
index e63cddf347..b04de020e0 100644
--- a/esphome/config.py
+++ b/esphome/config.py
@@ -14,6 +14,7 @@ from esphome import core, yaml_util, loader
 import esphome.core.config as core_config
 from esphome.const import (
     CONF_ESPHOME,
+    CONF_ID,
     CONF_PLATFORM,
     CONF_PACKAGES,
     CONF_SUBSTITUTIONS,
@@ -24,6 +25,7 @@ from esphome.core import CORE, EsphomeError
 from esphome.helpers import indent
 from esphome.util import safe_print, OrderedDict
 
+from esphome.config_helpers import Extend
 from esphome.loader import get_component, get_platform, ComponentManifest
 from esphome.yaml_util import is_secret, ESPHomeDataBase, ESPForceValue
 from esphome.voluptuous_schema import ExtraKeysInvalid
@@ -334,6 +336,13 @@ class LoadValidationStep(ConfigValidationStep):
                 continue
             p_name = p_config.get("platform")
             if p_name is None:
+                p_id = p_config.get(CONF_ID)
+                if isinstance(p_id, Extend):
+                    result.add_str_error(
+                        f"Source for extension of ID '{p_id.value}' was not found.",
+                        path + [CONF_ID],
+                    )
+                    continue
                 result.add_str_error("No platform specified! See 'platform' key.", path)
                 continue
             # Remove temp output path and construct new one
diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py
index d36a2f1e7f..e1d63775bb 100644
--- a/esphome/config_helpers.py
+++ b/esphome/config_helpers.py
@@ -1,10 +1,27 @@
 import json
 import os
 
+from esphome.const import CONF_ID
 from esphome.core import CORE
 from esphome.helpers import read_file
 
 
+class Extend:
+    def __init__(self, value):
+        self.value = value
+
+    def __str__(self):
+        return f"!extend {self.value}"
+
+    def __eq__(self, b):
+        """
+        Check if two Extend objects contain the same ID.
+
+        Only used in unit tests.
+        """
+        return isinstance(b, Extend) and self.value == b.value
+
+
 def read_config_file(path: str) -> str:
     if CORE.vscode and (
         not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path)
@@ -36,7 +53,25 @@ def merge_config(full_old, full_new):
         if isinstance(new, list):
             if not isinstance(old, list):
                 return new
-            return old + new
+            res = old.copy()
+            ids = {
+                v[CONF_ID]: i
+                for i, v in enumerate(res)
+                if CONF_ID in v and isinstance(v[CONF_ID], str)
+            }
+            for v in new:
+                if CONF_ID in v:
+                    new_id = v[CONF_ID]
+                    if isinstance(new_id, Extend):
+                        new_id = new_id.value
+                        if new_id in ids:
+                            v[CONF_ID] = new_id
+                            res[ids[new_id]] = merge(res[ids[new_id]], v)
+                            continue
+                    else:
+                        ids[new_id] = len(res)
+                res.append(v)
+            return res
         if new is None:
             return old
 
diff --git a/esphome/config_validation.py b/esphome/config_validation.py
index 054ee194c1..7440f71790 100644
--- a/esphome/config_validation.py
+++ b/esphome/config_validation.py
@@ -13,6 +13,7 @@ import voluptuous as vol
 
 from esphome import core
 import esphome.codegen as cg
+from esphome.config_helpers import Extend
 from esphome.const import (
     ALLOWED_NAME_CHARS,
     CONF_AVAILABILITY,
@@ -490,6 +491,8 @@ def declare_id(type):
         if value is None:
             return core.ID(None, is_declaration=True, type=type)
 
+        if isinstance(value, Extend):
+            raise Invalid(f"Source for extension of ID '{value.value}' was not found.")
         return core.ID(validate_id_name(value), is_declaration=True, type=type)
 
     return validator
@@ -1240,7 +1243,7 @@ def enum(mapping, **kwargs):
     return validator
 
 
-LAMBDA_ENTITY_ID_PROG = re.compile(r"id\(\s*([a-zA-Z0-9_]+\.[.a-zA-Z0-9_]+)\s*\)")
+LAMBDA_ENTITY_ID_PROG = re.compile(r"\Wid\(\s*([a-zA-Z0-9_]+\.[.a-zA-Z0-9_]+)\s*\)")
 
 
 def lambda_(value):
diff --git a/esphome/const.py b/esphome/const.py
index 163ddc75b2..ce3eac9ed6 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,6 +1,6 @@
 """Constants used by esphome."""
 
-__version__ = "2022.12.8"
+__version__ = "2023.2.0b5"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 
@@ -12,7 +12,7 @@ TARGET_PLATFORMS = [PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]
 
 SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"}
 HEADER_FILE_EXTENSIONS = {".h", ".hpp", ".tcc"}
-SECRETS_FILES = {"secrets.yaml", "secrets.yml"}
+SECRETS_FILES = ("secrets.yaml", "secrets.yml")
 
 
 CONF_ABOVE = "above"
@@ -26,6 +26,7 @@ CONF_ACTION_ID = "action_id"
 CONF_ACTION_STATE_TOPIC = "action_state_topic"
 CONF_ACTIVE = "active"
 CONF_ACTIVE_POWER = "active_power"
+CONF_ACTUAL_GAIN = "actual_gain"
 CONF_ADDRESS = "address"
 CONF_ADDRESSABLE_LIGHT_ID = "addressable_light_id"
 CONF_ADVANCED = "advanced"
@@ -140,6 +141,7 @@ CONF_CURRENT = "current"
 CONF_CURRENT_OPERATION = "current_operation"
 CONF_CURRENT_RESISTOR = "current_resistor"
 CONF_CURRENT_TEMPERATURE_STATE_TOPIC = "current_temperature_state_topic"
+CONF_CUSTOM = "custom"
 CONF_CUSTOM_FAN_MODE = "custom_fan_mode"
 CONF_CUSTOM_FAN_MODES = "custom_fan_modes"
 CONF_CUSTOM_PRESET = "custom_preset"
@@ -166,6 +168,7 @@ CONF_DEFAULT_TRANSITION_LENGTH = "default_transition_length"
 CONF_DELAY = "delay"
 CONF_DELIMITER = "delimiter"
 CONF_DELTA = "delta"
+CONF_DEST = "dest"
 CONF_DEVICE = "device"
 CONF_DEVICE_CLASS = "device_class"
 CONF_DEVICE_FACTOR = "device_factor"
@@ -175,6 +178,7 @@ CONF_DIO_PIN = "dio_pin"
 CONF_DIR_PIN = "dir_pin"
 CONF_DIRECTION = "direction"
 CONF_DIRECTION_OUTPUT = "direction_output"
+CONF_DISABLE_CRC = "disable_crc"
 CONF_DISABLED_BY_DEFAULT = "disabled_by_default"
 CONF_DISCONNECT_DELAY = "disconnect_delay"
 CONF_DISCOVERY = "discovery"
@@ -217,6 +221,7 @@ CONF_EVENT = "event"
 CONF_EXPIRE_AFTER = "expire_after"
 CONF_EXPORT_ACTIVE_ENERGY = "export_active_energy"
 CONF_EXPORT_REACTIVE_ENERGY = "export_reactive_energy"
+CONF_EXTERNAL_CLOCK_INPUT = "external_clock_input"
 CONF_EXTERNAL_COMPONENTS = "external_components"
 CONF_EXTERNAL_VCC = "external_vcc"
 CONF_FALLING_EDGE = "falling_edge"
@@ -232,6 +237,7 @@ CONF_FAN_MODE_MEDIUM_ACTION = "fan_mode_medium_action"
 CONF_FAN_MODE_MIDDLE_ACTION = "fan_mode_middle_action"
 CONF_FAN_MODE_OFF_ACTION = "fan_mode_off_action"
 CONF_FAN_MODE_ON_ACTION = "fan_mode_on_action"
+CONF_FAN_MODE_QUIET_ACTION = "fan_mode_quiet_action"
 CONF_FAN_MODE_STATE_TOPIC = "fan_mode_state_topic"
 CONF_FAN_ONLY_ACTION = "fan_only_action"
 CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER = "fan_only_action_uses_fan_mode_timer"
@@ -259,6 +265,7 @@ CONF_FRAGMENTATION = "fragmentation"
 CONF_FRAMEWORK = "framework"
 CONF_FREE = "free"
 CONF_FREQUENCY = "frequency"
+CONF_FRIENDLY_NAME = "friendly_name"
 CONF_FROM = "from"
 CONF_FULL_SPECTRUM = "full_spectrum"
 CONF_FULL_UPDATE_EVERY = "full_update_every"
@@ -306,6 +313,7 @@ CONF_ILLUMINANCE = "illuminance"
 CONF_IMPEDANCE = "impedance"
 CONF_IMPORT_ACTIVE_ENERGY = "import_active_energy"
 CONF_IMPORT_REACTIVE_ENERGY = "import_reactive_energy"
+CONF_INC_PIN = "inc_pin"
 CONF_INCLUDE_INTERNAL = "include_internal"
 CONF_INCLUDES = "includes"
 CONF_INDEX = "index"
@@ -462,6 +470,7 @@ CONF_ON_TAG = "on_tag"
 CONF_ON_TAG_REMOVED = "on_tag_removed"
 CONF_ON_TIME = "on_time"
 CONF_ON_TIME_SYNC = "on_time_sync"
+CONF_ON_TIMEOUT = "on_timeout"
 CONF_ON_TOUCH = "on_touch"
 CONF_ON_TURN_OFF = "on_turn_off"
 CONF_ON_TURN_ON = "on_turn_on"
@@ -542,8 +551,10 @@ CONF_POWER_SAVE_MODE = "power_save_mode"
 CONF_POWER_SUPPLY = "power_supply"
 CONF_PRESET = "preset"
 CONF_PRESET_BOOST = "preset_boost"
+CONF_PRESET_COMMAND_TOPIC = "preset_command_topic"
 CONF_PRESET_ECO = "preset_eco"
 CONF_PRESET_SLEEP = "preset_sleep"
+CONF_PRESET_STATE_TOPIC = "preset_state_topic"
 CONF_PRESSURE = "pressure"
 CONF_PRIORITY = "priority"
 CONF_PROJECT = "project"
@@ -741,6 +752,7 @@ CONF_TX_POWER = "tx_power"
 CONF_TYPE = "type"
 CONF_TYPE_ID = "type_id"
 CONF_UART_ID = "uart_id"
+CONF_UD_PIN = "ud_pin"
 CONF_UID = "uid"
 CONF_UNIQUE = "unique"
 CONF_UNIT_OF_MEASUREMENT = "unit_of_measurement"
@@ -855,6 +867,7 @@ UNIT_AMPERE = "A"
 UNIT_BECQUEREL_PER_CUBIC_METER = "Bq/m³"
 UNIT_BYTES = "B"
 UNIT_CELSIUS = "°C"
+UNIT_CENTIMETER = "cm"
 UNIT_COUNT_DECILITRE = "/dL"
 UNIT_COUNTS_PER_CUBIC_METER = "#/m³"
 UNIT_CUBIC_METER = "m³"
@@ -866,6 +879,7 @@ UNIT_EMPTY = ""
 UNIT_G = "G"
 UNIT_HECTOPASCAL = "hPa"
 UNIT_HERTZ = "Hz"
+UNIT_HOUR = "h"
 UNIT_KELVIN = "K"
 UNIT_KILOGRAM = "kg"
 UNIT_KILOMETER = "km"
@@ -953,6 +967,7 @@ DEVICE_CLASS_PM1 = "pm1"
 DEVICE_CLASS_PM10 = "pm10"
 DEVICE_CLASS_PM25 = "pm25"
 DEVICE_CLASS_POWER_FACTOR = "power_factor"
+DEVICE_CLASS_PRECIPITATION = "precipitation"
 DEVICE_CLASS_PRECIPITATION_INTENSITY = "precipitation_intensity"
 DEVICE_CLASS_PRESSURE = "pressure"
 DEVICE_CLASS_REACTIVE_POWER = "reactive_power"
@@ -993,6 +1008,7 @@ KEY_TARGET_PLATFORM = "target_platform"
 KEY_TARGET_FRAMEWORK = "target_framework"
 KEY_FRAMEWORK_VERSION = "framework_version"
 KEY_NAME = "name"
+KEY_VARIANT = "variant"
 
 # Entity categories
 ENTITY_CATEGORY_NONE = ""
diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py
index 09115c2791..545fae381f 100644
--- a/esphome/core/__init__.py
+++ b/esphome/core/__init__.py
@@ -453,6 +453,8 @@ class EsphomeCore:
         self.ace = False
         # The name of the node
         self.name: Optional[str] = None
+        # The friendly name of the node
+        self.friendly_name: Optional[str] = None
         # Additional data components can store temporary data in
         # The first key to this dict should always be the integration name
         self.data = {}
@@ -492,6 +494,7 @@ class EsphomeCore:
     def reset(self):
         self.dashboard = False
         self.name = None
+        self.friendly_name = None
         self.data = {}
         self.config_path = None
         self.build_path = None
diff --git a/esphome/core/application.h b/esphome/core/application.h
index 6376987f66..0992a4df39 100644
--- a/esphome/core/application.h
+++ b/esphome/core/application.h
@@ -2,11 +2,11 @@
 
 #include <string>
 #include <vector>
-#include "esphome/core/defines.h"
-#include "esphome/core/preferences.h"
 #include "esphome/core/component.h"
+#include "esphome/core/defines.h"
 #include "esphome/core/hal.h"
 #include "esphome/core/helpers.h"
+#include "esphome/core/preferences.h"
 #include "esphome/core/scheduler.h"
 
 #ifdef USE_BINARY_SENSOR
@@ -53,14 +53,22 @@ namespace esphome {
 
 class Application {
  public:
-  void pre_setup(const std::string &name, const char *compilation_time, bool name_add_mac_suffix) {
+  void pre_setup(const std::string &name, const std::string &friendly_name, const std::string &comment,
+                 const char *compilation_time, bool name_add_mac_suffix) {
     arch_init();
     this->name_add_mac_suffix_ = name_add_mac_suffix;
     if (name_add_mac_suffix) {
       this->name_ = name + "-" + get_mac_address().substr(6);
+      if (friendly_name.empty()) {
+        this->friendly_name_ = "";
+      } else {
+        this->friendly_name_ = friendly_name + " " + get_mac_address().substr(6);
+      }
     } else {
       this->name_ = name;
+      this->friendly_name_ = friendly_name;
     }
+    this->comment_ = comment;
     this->compilation_time_ = compilation_time;
   }
 
@@ -131,9 +139,14 @@ class Application {
   /// Make a loop iteration. Call this in your loop() function.
   void loop();
 
-  /// Get the name of this Application set by set_name().
+  /// Get the name of this Application set by pre_setup().
   const std::string &get_name() const { return this->name_; }
 
+  /// Get the friendly name of this Application set by pre_setup().
+  const std::string &get_friendly_name() const { return this->friendly_name_; }
+  /// Get the comment of this Application set by pre_setup().
+  const std::string &get_comment() const { return this->comment_; }
+
   bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; }
 
   const std::string &get_compilation_time() const { return this->compilation_time_; }
@@ -338,6 +351,8 @@ class Application {
 #endif
 
   std::string name_;
+  std::string friendly_name_;
+  std::string comment_;
   std::string compilation_time_;
   bool name_add_mac_suffix_;
   uint32_t last_loop_{0};
diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp
index 7505e1f39b..b1ace8b2d1 100644
--- a/esphome/core/component.cpp
+++ b/esphome/core/component.cpp
@@ -57,7 +57,7 @@ bool Component::cancel_interval(const std::string &name) {  // NOLINT
 }
 
 void Component::set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
-                          std::function<RetryResult()> &&f, float backoff_increase_factor) {  // NOLINT
+                          std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) {  // NOLINT
   App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
 }
 
@@ -130,7 +130,7 @@ void Component::set_timeout(uint32_t timeout, std::function<void()> &&f) {  // N
 void Component::set_interval(uint32_t interval, std::function<void()> &&f) {  // NOLINT
   App.scheduler.set_interval(this, "", interval, std::move(f));
 }
-void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult()> &&f,
+void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f,
                           float backoff_increase_factor) {  // NOLINT
   App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
 }
diff --git a/esphome/core/component.h b/esphome/core/component.h
index 1d8499e262..769e74e645 100644
--- a/esphome/core/component.h
+++ b/esphome/core/component.h
@@ -185,26 +185,39 @@ class Component {
 
   /** Set an retry function with a unique name. Empty name means no cancelling possible.
    *
-   * This will call f. If f returns RetryResult::RETRY f is called again after initial_wait_time ms.
-   * f should return RetryResult::DONE if no repeat is required. The initial wait time will be increased
-   * by backoff_increase_factor for each iteration. Default is doubling the time between iterations
-   * Can be cancelled via cancel_retry().
+   * This will call the retry function f on the next scheduler loop. f should return RetryResult::DONE if
+   * it is successful and no repeat is required. Otherwise, returning RetryResult::RETRY will call f
+   * again in the future.
+   *
+   * The first retry of f happens after `initial_wait_time` milliseconds. The delay between retries is
+   * increased by multipling by `backoff_increase_factor` each time. If no backoff_increase_factor is
+   * supplied (default = 1.0), the wait time will stay constant.
+   *
+   * The retry function f needs to accept a single argument: the number of attempts remaining. On the
+   * final retry of f, this value will be 0.
+   *
+   * This retry function can also be cancelled by name via cancel_retry().
    *
    * IMPORTANT: Do not rely on this having correct timing. This is only called from
    * loop() and therefore can be significantly delayed.
    *
+   * REMARK: It is an error to supply a negative or zero `backoff_increase_factor`, and 1.0 will be used instead.
+   *
+   * REMARK: The interval between retries is stored into a `uint32_t`, so this doesn't behave correctly
+   * if `initial_wait_time * (backoff_increase_factor ** (max_attempts - 2))` overflows.
+   *
    * @param name The identifier for this retry function.
    * @param initial_wait_time The time in ms before f is called again
-   * @param max_attempts The maximum number of retries
+   * @param max_attempts The maximum number of executions
    * @param f The function (or lambda) that should be called
-   * @param backoff_increase_factor time between retries is increased by this factor on every retry
+   * @param backoff_increase_factor time between retries is multiplied by this factor on every retry after the first
    * @see cancel_retry()
    */
-  void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,  // NOLINT
-                 std::function<RetryResult()> &&f, float backoff_increase_factor = 1.0f);    // NOLINT
+  void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,       // NOLINT
+                 std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f);  // NOLINT
 
-  void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult()> &&f,  // NOLINT
-                 float backoff_increase_factor = 1.0f);                                               // NOLINT
+  void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f,  // NOLINT
+                 float backoff_increase_factor = 1.0f);                                                      // NOLINT
 
   /** Cancel a retry function.
    *
diff --git a/esphome/core/config.py b/esphome/core/config.py
index bf0beea2ef..ef6553026e 100644
--- a/esphome/core/config.py
+++ b/esphome/core/config.py
@@ -19,6 +19,7 @@ from esphome.const import (
     CONF_LIBRARIES,
     CONF_MIN_VERSION,
     CONF_NAME,
+    CONF_FRIENDLY_NAME,
     CONF_ON_BOOT,
     CONF_ON_LOOP,
     CONF_ON_SHUTDOWN,
@@ -124,6 +125,7 @@ CONFIG_SCHEMA = cv.All(
     cv.Schema(
         {
             cv.Required(CONF_NAME): cv.valid_name,
+            cv.Optional(CONF_FRIENDLY_NAME, ""): cv.string,
             cv.Optional(CONF_COMMENT): cv.string,
             cv.Required(CONF_BUILD_PATH): cv.string,
             cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema(
@@ -192,6 +194,7 @@ def preload_core_config(config, result):
         conf = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME])
 
     CORE.name = conf[CONF_NAME]
+    CORE.friendly_name = conf.get(CONF_FRIENDLY_NAME)
     CORE.data[KEY_CORE] = {}
 
     if CONF_BUILD_PATH not in conf:
@@ -346,6 +349,8 @@ async def to_code(config):
     cg.add(
         cg.App.pre_setup(
             config[CONF_NAME],
+            config[CONF_FRIENDLY_NAME],
+            config.get(CONF_COMMENT, ""),
             cg.RawExpression('__DATE__ ", " __TIME__'),
             config[CONF_NAME_ADD_MAC_SUFFIX],
         )
diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp
index cc4074b94d..d880f0fda4 100644
--- a/esphome/core/scheduler.cpp
+++ b/esphome/core/scheduler.cpp
@@ -74,7 +74,7 @@ bool HOT Scheduler::cancel_interval(Component *component, const std::string &nam
 }
 
 struct RetryArgs {
-  std::function<RetryResult()> func;
+  std::function<RetryResult(uint8_t)> func;
   uint8_t retry_countdown;
   uint32_t current_interval;
   Component *component;
@@ -84,15 +84,18 @@ struct RetryArgs {
 };
 
 static void retry_handler(const std::shared_ptr<RetryArgs> &args) {
-  RetryResult retry_result = args->func();
-  if (retry_result == RetryResult::DONE || --args->retry_countdown <= 0)
+  RetryResult const retry_result = args->func(--args->retry_countdown);
+  if (retry_result == RetryResult::DONE || args->retry_countdown <= 0)
     return;
-  args->current_interval *= args->backoff_increase_factor;
+  // second execution of `func` happens after `initial_wait_time`
   args->scheduler->set_timeout(args->component, args->name, args->current_interval, [args]() { retry_handler(args); });
+  // backoff_increase_factor applied to third & later executions
+  args->current_interval *= args->backoff_increase_factor;
 }
 
 void HOT Scheduler::set_retry(Component *component, const std::string &name, uint32_t initial_wait_time,
-                              uint8_t max_attempts, std::function<RetryResult()> func, float backoff_increase_factor) {
+                              uint8_t max_attempts, std::function<RetryResult(uint8_t)> func,
+                              float backoff_increase_factor) {
   if (!name.empty())
     this->cancel_retry(component, name);
 
@@ -102,6 +105,13 @@ void HOT Scheduler::set_retry(Component *component, const std::string &name, uin
   ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%u, max_attempts=%u, backoff_factor=%0.1f)", name.c_str(),
             initial_wait_time, max_attempts, backoff_increase_factor);
 
+  if (backoff_increase_factor < 0.0001) {
+    ESP_LOGE(TAG,
+             "set_retry(name='%s'): backoff_factor cannot be close to zero nor negative (%0.1f). Using 1.0 instead",
+             name.c_str(), backoff_increase_factor);
+    backoff_increase_factor = 1;
+  }
+
   auto args = std::make_shared<RetryArgs>();
   args->func = std::move(func);
   args->retry_countdown = max_attempts;
@@ -111,7 +121,8 @@ void HOT Scheduler::set_retry(Component *component, const std::string &name, uin
   args->backoff_increase_factor = backoff_increase_factor;
   args->scheduler = this;
 
-  this->set_timeout(component, args->name, initial_wait_time, [args]() { retry_handler(args); });
+  // First exectuion of `func` immediately
+  this->set_timeout(component, args->name, 0, [args]() { retry_handler(args); });
 }
 bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) {
   return this->cancel_timeout(component, "retry$" + name);
diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h
index 111bee1df2..a758198b8d 100644
--- a/esphome/core/scheduler.h
+++ b/esphome/core/scheduler.h
@@ -16,7 +16,7 @@ class Scheduler {
   bool cancel_interval(Component *component, const std::string &name);
 
   void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
-                 std::function<RetryResult()> func, float backoff_increase_factor = 1.0f);
+                 std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
   bool cancel_retry(Component *component, const std::string &name);
 
   optional<uint32_t> next_schedule_in();
diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py
index 2cf826dea9..f7d471586d 100644
--- a/esphome/dashboard/dashboard.py
+++ b/esphome/dashboard/dashboard.py
@@ -25,10 +25,10 @@ import tornado.netutil
 import tornado.process
 import tornado.web
 import tornado.websocket
+import yaml
 from tornado.log import access_log
 
 from esphome import const, platformio_api, util, yaml_util
-from esphome.core import EsphomeError
 from esphome.helpers import get_bool_env, mkdir_p, run_system_command
 from esphome.storage_json import (
     EsphomeStorageJSON,
@@ -40,7 +40,7 @@ from esphome.storage_json import (
 from esphome.util import get_serial_ports, shlex_quote
 from esphome.zeroconf import DashboardImportDiscovery, DashboardStatus, EsphomeZeroconf
 
-from .util import password_hash
+from .util import friendly_name_slugify, password_hash
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -55,6 +55,7 @@ class DashboardSettings:
         self.using_password = False
         self.on_ha_addon = False
         self.cookie_secret = None
+        self.absolute_config_dir = None
 
     def parse_args(self, args):
         self.on_ha_addon = args.ha_addon
@@ -65,6 +66,7 @@ class DashboardSettings:
         if self.using_password:
             self.password_hash = password_hash(password)
         self.config_dir = args.configuration
+        self.absolute_config_dir = Path(self.config_dir).resolve()
 
     @property
     def relative_url(self):
@@ -94,7 +96,10 @@ class DashboardSettings:
         return hmac.compare_digest(self.password_hash, password_hash(password))
 
     def rel_path(self, *args):
-        return os.path.join(self.config_dir, *args)
+        joined_path = os.path.join(self.config_dir, *args)
+        # Raises ValueError if not relative to ESPHome config folder
+        Path(joined_path).resolve().relative_to(self.absolute_config_dir)
+        return joined_path
 
     def list_yaml_files(self):
         return util.list_yaml_files([self.config_dir])
@@ -281,8 +286,11 @@ class EsphomeLogsHandler(EsphomeCommandWebSocket):
 
 
 class EsphomeRenameHandler(EsphomeCommandWebSocket):
+    old_name: str
+
     def build_command(self, json_message):
         config_file = settings.rel_path(json_message["configuration"])
+        self.old_name = json_message["configuration"]
         return [
             "esphome",
             "--dashboard",
@@ -291,8 +299,30 @@ class EsphomeRenameHandler(EsphomeCommandWebSocket):
             json_message["newName"],
         ]
 
+    def _proc_on_exit(self, returncode):
+        super()._proc_on_exit(returncode)
+
+        if returncode != 0:
+            return
+
+        # Remove the old ping result from the cache
+        PING_RESULT.pop(self.old_name, None)
+
 
 class EsphomeUploadHandler(EsphomeCommandWebSocket):
+    def build_command(self, json_message):
+        config_file = settings.rel_path(json_message["configuration"])
+        return [
+            "esphome",
+            "--dashboard",
+            "upload",
+            config_file,
+            "--device",
+            json_message["port"],
+        ]
+
+
+class EsphomeRunHandler(EsphomeCommandWebSocket):
     def build_command(self, json_message):
         config_file = settings.rel_path(json_message["configuration"])
         return [
@@ -378,12 +408,24 @@ class WizardRequestHandler(BaseHandler):
             for k, v in json.loads(self.request.body.decode()).items()
             if k in ("name", "platform", "board", "ssid", "psk", "password")
         }
+        if not kwargs["name"]:
+            self.set_status(422)
+            self.set_header("content-type", "application/json")
+            self.write(json.dumps({"error": "Name is required"}))
+            return
+
+        kwargs["friendly_name"] = kwargs["name"]
+        kwargs["name"] = friendly_name_slugify(kwargs["friendly_name"])
+
         kwargs["ota_password"] = secrets.token_hex(16)
         noise_psk = secrets.token_bytes(32)
         kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode()
-        destination = settings.rel_path(f"{kwargs['name']}.yaml")
+        filename = f"{kwargs['name']}.yaml"
+        destination = settings.rel_path(filename)
         wizard.wizard_write(path=destination, **kwargs)
         self.set_status(200)
+        self.set_header("content-type", "application/json")
+        self.write(json.dumps({"configuration": filename}))
         self.finish()
 
 
@@ -395,6 +437,8 @@ class ImportRequestHandler(BaseHandler):
         args = json.loads(self.request.body.decode())
         try:
             name = args["name"]
+            friendly_name = args.get("friendly_name")
+            encryption = args.get("encryption", False)
 
             imported_device = next(
                 (res for res in IMPORT_RESULT.values() if res.device_name == name), None
@@ -402,15 +446,19 @@ class ImportRequestHandler(BaseHandler):
 
             if imported_device is not None:
                 network = imported_device.network
+                if friendly_name is None:
+                    friendly_name = imported_device.friendly_name
             else:
                 network = const.CONF_WIFI
 
             import_config(
                 settings.rel_path(f"{name}.yaml"),
                 name,
+                friendly_name,
                 args["project_name"],
                 args["package_import_url"],
                 network,
+                encryption,
             )
         except FileExistsError:
             self.set_status(500)
@@ -422,6 +470,8 @@ class ImportRequestHandler(BaseHandler):
             return
 
         self.set_status(200)
+        self.set_header("content-type", "application/json")
+        self.write(json.dumps({"configuration": f"{name}.yaml"}))
         self.finish()
 
 
@@ -495,35 +545,11 @@ class DownloadBinaryRequestHandler(BaseHandler):
         self.finish()
 
 
-class ManifestRequestHandler(BaseHandler):
+class EsphomeVersionHandler(BaseHandler):
     @authenticated
-    @bind_config
-    def get(self, configuration=None):
-        args = ["esphome", "idedata", settings.rel_path(configuration)]
-        rc, stdout, _ = run_system_command(*args)
-
-        if rc != 0:
-            self.send_error(404 if rc == 2 else 500)
-            return
-
-        idedata = platformio_api.IDEData(json.loads(stdout))
-
-        firmware_offset = "0x10000" if idedata.extra_flash_images else "0x0"
-        flash_images = [
-            {
-                "path": f"./download.bin?configuration={configuration}&type=firmware.bin",
-                "offset": firmware_offset,
-            }
-        ] + [
-            {
-                "path": f"./download.bin?configuration={configuration}&type={os.path.basename(image.path)}",
-                "offset": image.offset,
-            }
-            for image in idedata.extra_flash_images
-        ]
-
+    def get(self):
         self.set_header("Content-Type", "application/json")
-        self.write(json.dumps(flash_images))
+        self.write(json.dumps({"version": const.__version__}))
         self.finish()
 
 
@@ -569,6 +595,12 @@ class DashboardEntry:
             return self.filename.replace(".yml", "").replace(".yaml", "")
         return self.storage.name
 
+    @property
+    def friendly_name(self):
+        if self.storage is None:
+            return self.name
+        return self.storage.friendly_name
+
     @property
     def comment(self):
         if self.storage is None:
@@ -616,6 +648,7 @@ class ListDevicesHandler(BaseHandler):
                     "configured": [
                         {
                             "name": entry.name,
+                            "friendly_name": entry.friendly_name,
                             "configuration": entry.filename,
                             "loaded_integrations": entry.loaded_integrations,
                             "deployed_version": entry.update_old,
@@ -631,6 +664,7 @@ class ListDevicesHandler(BaseHandler):
                     "importable": [
                         {
                             "name": res.device_name,
+                            "friendly_name": res.friendly_name,
                             "package_import_url": res.package_import_url,
                             "project_name": res.project_name,
                             "project_version": res.project_version,
@@ -695,20 +729,34 @@ class PrometheusServiceDiscoveryHandler(BaseHandler):
 
 class BoardsRequestHandler(BaseHandler):
     @authenticated
-    def get(self):
+    def get(self, platform: str):
         from esphome.components.esp32.boards import BOARDS as ESP32_BOARDS
         from esphome.components.esp8266.boards import BOARDS as ESP8266_BOARDS
         from esphome.components.rp2040.boards import BOARDS as RP2040_BOARDS
 
-        boards = {
-            "esp32": {key: val[const.KEY_NAME] for key, val in ESP32_BOARDS.items()},
-            "esp8266": {
-                key: val[const.KEY_NAME] for key, val in ESP8266_BOARDS.items()
-            },
-            "rp2040": {key: val[const.KEY_NAME] for key, val in RP2040_BOARDS.items()},
+        platform_to_boards = {
+            "esp32": ESP32_BOARDS,
+            "esp8266": ESP8266_BOARDS,
+            "rp2040": RP2040_BOARDS,
         }
+        # filter all ESP32 variants by requested platform
+        if platform.startswith("esp32"):
+            boards = {
+                k: v
+                for k, v in platform_to_boards["esp32"].items()
+                if v[const.KEY_VARIANT] == platform.upper()
+            }
+        else:
+            boards = platform_to_boards[platform]
+
+        # map to a {board_name: board_title} dict
+        platform_boards = {key: val[const.KEY_NAME] for key, val in boards.items()}
+        # sort by board title
+        boards_items = sorted(platform_boards.items(), key=lambda item: item[1])
+        output = [dict(items=dict(boards_items))]
+
         self.set_header("content-type", "application/json")
-        self.write(json.dumps(boards))
+        self.write(json.dumps(output))
 
 
 class MDNSStatusThread(threading.Thread):
@@ -845,6 +893,9 @@ class DeleteRequestHandler(BaseHandler):
             if build_folder is not None:
                 shutil.rmtree(build_folder, os.path.join(trash_path, name))
 
+        # Remove the old ping result from the cache
+        PING_RESULT.pop(configuration, None)
+
 
 class UndoDeleteRequestHandler(BaseHandler):
     @authenticated
@@ -953,6 +1004,20 @@ class SecretKeysRequestHandler(BaseHandler):
         self.write(json.dumps(secret_keys))
 
 
+class SafeLoaderIgnoreUnknown(yaml.SafeLoader):
+    def ignore_unknown(self, node):
+        return f"{node.tag} {node.value}"
+
+    def construct_yaml_binary(self, node) -> str:
+        return super().construct_yaml_binary(node).decode("ascii")
+
+
+SafeLoaderIgnoreUnknown.add_constructor(None, SafeLoaderIgnoreUnknown.ignore_unknown)
+SafeLoaderIgnoreUnknown.add_constructor(
+    "tag:yaml.org,2002:binary", SafeLoaderIgnoreUnknown.construct_yaml_binary
+)
+
+
 class JsonConfigRequestHandler(BaseHandler):
     @authenticated
     @bind_config
@@ -962,16 +1027,18 @@ class JsonConfigRequestHandler(BaseHandler):
             self.send_error(404)
             return
 
-        try:
-            content = yaml_util.load_yaml(filename, clear_secrets=False)
-            json_content = json.dumps(
-                content, default=lambda o: {"__type": str(type(o)), "repr": repr(o)}
-            )
-            self.set_header("content-type", "application/json")
-            self.write(json_content)
-        except EsphomeError as err:
-            _LOGGER.warning("Error translating file %s to JSON: %s", filename, err)
-            self.send_error(500)
+        args = ["esphome", "config", settings.rel_path(configuration), "--show-secrets"]
+
+        rc, stdout, _ = run_system_command(*args)
+
+        if rc != 0:
+            self.send_error(422)
+            return
+
+        data = yaml.load(stdout, Loader=SafeLoaderIgnoreUnknown)
+        self.set_header("content-type", "application/json")
+        self.write(json.dumps(data))
+        self.finish()
 
 
 def get_base_frontend_path():
@@ -1058,6 +1125,7 @@ def make_app(debug=get_bool_env(ENV_DEV)):
             (f"{rel}logout", LogoutHandler),
             (f"{rel}logs", EsphomeLogsHandler),
             (f"{rel}upload", EsphomeUploadHandler),
+            (f"{rel}run", EsphomeRunHandler),
             (f"{rel}compile", EsphomeCompileHandler),
             (f"{rel}validate", EsphomeValidateHandler),
             (f"{rel}clean-mqtt", EsphomeCleanMqttHandler),
@@ -1068,7 +1136,6 @@ def make_app(debug=get_bool_env(ENV_DEV)):
             (f"{rel}info", InfoRequestHandler),
             (f"{rel}edit", EditRequestHandler),
             (f"{rel}download.bin", DownloadBinaryRequestHandler),
-            (f"{rel}manifest.json", ManifestRequestHandler),
             (f"{rel}serial-ports", SerialPortRequestHandler),
             (f"{rel}ping", PingRequestHandler),
             (f"{rel}delete", DeleteRequestHandler),
@@ -1081,7 +1148,8 @@ def make_app(debug=get_bool_env(ENV_DEV)):
             (f"{rel}json-config", JsonConfigRequestHandler),
             (f"{rel}rename", EsphomeRenameHandler),
             (f"{rel}prometheus-sd", PrometheusServiceDiscoveryHandler),
-            (f"{rel}boards", BoardsRequestHandler),
+            (f"{rel}boards/([a-z0-9]+)", BoardsRequestHandler),
+            (f"{rel}version", EsphomeVersionHandler),
         ],
         **app_settings,
     )
diff --git a/esphome/dashboard/util.py b/esphome/dashboard/util.py
index 3e3864aa17..a2ad530b74 100644
--- a/esphome/dashboard/util.py
+++ b/esphome/dashboard/util.py
@@ -1,4 +1,7 @@
 import hashlib
+import unicodedata
+
+from esphome.const import ALLOWED_NAME_CHARS
 
 
 def password_hash(password: str) -> bytes:
@@ -7,3 +10,23 @@ def password_hash(password: str) -> bytes:
     Note this is not meant for secure storage, but for securely comparing passwords.
     """
     return hashlib.sha256(password.encode()).digest()
+
+
+def strip_accents(value):
+    return "".join(
+        c
+        for c in unicodedata.normalize("NFD", str(value))
+        if unicodedata.category(c) != "Mn"
+    )
+
+
+def friendly_name_slugify(value):
+    value = (
+        strip_accents(value)
+        .lower()
+        .replace(" ", "-")
+        .replace("_", "-")
+        .replace("--", "-")
+        .strip("-")
+    )
+    return "".join(c for c in value if c in ALLOWED_NAME_CHARS)
diff --git a/esphome/pins.py b/esphome/pins.py
index 2b3adce86d..2ac4cd4b54 100644
--- a/esphome/pins.py
+++ b/esphome/pins.py
@@ -49,6 +49,11 @@ def _set_mode(value, default_mode):
             CONF_INPUT: True,
             CONF_PULLDOWN: True,
         },
+        "INPUT_OUTPUT_OPEN_DRAIN": {
+            CONF_INPUT: True,
+            CONF_OUTPUT: True,
+            CONF_OPEN_DRAIN: True,
+        },
     }
     if mode.upper() not in PIN_MODES:
         raise cv.Invalid(f"Unknown pin mode {mode}", [CONF_MODE])
diff --git a/esphome/storage_json.py b/esphome/storage_json.py
index c2c8b91a36..bbdfbbc8a2 100644
--- a/esphome/storage_json.py
+++ b/esphome/storage_json.py
@@ -36,6 +36,7 @@ class StorageJSON:
         self,
         storage_version,
         name,
+        friendly_name,
         comment,
         esphome_version,
         src_version,
@@ -51,6 +52,8 @@ class StorageJSON:
         self.storage_version: int = storage_version
         # The name of the node
         self.name: str = name
+        # The friendly name of the node
+        self.friendly_name: str = friendly_name
         # The comment of the node
         self.comment: str = comment
         # The esphome version this was compiled with
@@ -77,6 +80,7 @@ class StorageJSON:
         return {
             "storage_version": self.storage_version,
             "name": self.name,
+            "friendly_name": self.friendly_name,
             "comment": self.comment,
             "esphome_version": self.esphome_version,
             "src_version": self.src_version,
@@ -106,6 +110,7 @@ class StorageJSON:
         return StorageJSON(
             storage_version=1,
             name=esph.name,
+            friendly_name=esph.friendly_name,
             comment=esph.comment,
             esphome_version=const.__version__,
             src_version=1,
@@ -118,10 +123,13 @@ class StorageJSON:
         )
 
     @staticmethod
-    def from_wizard(name: str, address: str, platform: str) -> "StorageJSON":
+    def from_wizard(
+        name: str, friendly_name: str, address: str, platform: str
+    ) -> "StorageJSON":
         return StorageJSON(
             storage_version=1,
             name=name,
+            friendly_name=friendly_name,
             comment=None,
             esphome_version=None,
             src_version=1,
@@ -139,6 +147,7 @@ class StorageJSON:
             storage = json.load(f_handle)
         storage_version = storage["storage_version"]
         name = storage.get("name")
+        friendly_name = storage.get("friendly_name")
         comment = storage.get("comment")
         esphome_version = storage.get(
             "esphome_version", storage.get("esphomeyaml_version")
@@ -153,6 +162,7 @@ class StorageJSON:
         return StorageJSON(
             storage_version,
             name,
+            friendly_name,
             comment,
             esphome_version,
             src_version,
diff --git a/esphome/wizard.py b/esphome/wizard.py
index 0fcccfc3f6..fd661af639 100644
--- a/esphome/wizard.py
+++ b/esphome/wizard.py
@@ -46,6 +46,11 @@ BASE_CONFIG = """esphome:
   name: {name}
 """
 
+BASE_CONFIG_FRIENDLY = """esphome:
+  name: {name}
+  friendly_name: {friendly_name}
+"""
+
 LOGGER_API_CONFIG = """
 # Enable logging
 logger:
@@ -110,7 +115,12 @@ def wizard_file(**kwargs):
     kwargs["fallback_name"] = ap_name
     kwargs["fallback_psk"] = "".join(random.choice(letters) for _ in range(12))
 
-    config = BASE_CONFIG.format(**kwargs)
+    if kwargs.get("friendly_name"):
+        base = BASE_CONFIG_FRIENDLY
+    else:
+        base = BASE_CONFIG
+
+    config = base.format(**kwargs)
 
     config += HARDWARE_BASE_CONFIGS[kwargs["platform"]].format(**kwargs)
 
@@ -192,7 +202,7 @@ def wizard_write(path, **kwargs):
     hardware = kwargs["platform"]
 
     write_file(path, wizard_file(**kwargs))
-    storage = StorageJSON.from_wizard(name, f"{name}.local", hardware)
+    storage = StorageJSON.from_wizard(name, name, f"{name}.local", hardware)
     storage_path = ext_storage_path(os.path.dirname(path), os.path.basename(path))
     storage.save(storage_path)
 
diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py
index 2689ce9245..8a03c431a7 100644
--- a/esphome/yaml_util.py
+++ b/esphome/yaml_util.py
@@ -10,7 +10,7 @@ import yaml
 import yaml.constructor
 
 from esphome import core
-from esphome.config_helpers import read_config_file
+from esphome.config_helpers import read_config_file, Extend
 from esphome.core import (
     EsphomeError,
     IPAddress,
@@ -338,6 +338,10 @@ class ESPHomeLoader(yaml.SafeLoader):
         obj = self.construct_scalar(node)
         return add_class_to_obj(obj, ESPForceValue)
 
+    @_add_data_ref
+    def construct_extend(self, node):
+        return Extend(str(node.value))
+
 
 ESPHomeLoader.add_constructor("tag:yaml.org,2002:int", ESPHomeLoader.construct_yaml_int)
 ESPHomeLoader.add_constructor(
@@ -369,6 +373,7 @@ ESPHomeLoader.add_constructor(
 )
 ESPHomeLoader.add_constructor("!lambda", ESPHomeLoader.construct_lambda)
 ESPHomeLoader.add_constructor("!force", ESPHomeLoader.construct_force)
+ESPHomeLoader.add_constructor("!extend", ESPHomeLoader.construct_extend)
 
 
 def load_yaml(fname, clear_secrets=True):
@@ -390,8 +395,11 @@ def _load_yaml_internal(fname):
         loader.dispose()
 
 
-def dump(dict_):
+def dump(dict_, show_secrets=False):
     """Dump YAML to a string and remove null."""
+    if show_secrets:
+        _SECRET_VALUES.clear()
+        _SECRET_CACHE.clear()
     return yaml.dump(
         dict_, default_flow_style=False, allow_unicode=True, Dumper=ESPHomeDumper
     )
diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py
index 3743f650f3..b0dddfd152 100644
--- a/esphome/zeroconf.py
+++ b/esphome/zeroconf.py
@@ -119,10 +119,12 @@ TXT_RECORD_PACKAGE_IMPORT_URL = b"package_import_url"
 TXT_RECORD_PROJECT_NAME = b"project_name"
 TXT_RECORD_PROJECT_VERSION = b"project_version"
 TXT_RECORD_NETWORK = b"network"
+TXT_RECORD_FRIENDLY_NAME = b"friendly_name"
 
 
 @dataclass
 class DiscoveredImport:
+    friendly_name: Optional[str]
     device_name: str
     package_import_url: str
     project_name: str
@@ -155,6 +157,11 @@ class DashboardImportDiscovery:
             return
         if state_change == ServiceStateChange.Removed:
             self.import_state.pop(name, None)
+            return
+
+        if state_change == ServiceStateChange.Updated and name not in self.import_state:
+            # Ignore updates for devices that are not in the import state
+            return
 
         info = zeroconf.get_service_info(service_type, name)
         _LOGGER.debug("-> resolved info: %s", info)
@@ -174,8 +181,12 @@ class DashboardImportDiscovery:
         project_name = info.properties[TXT_RECORD_PROJECT_NAME].decode()
         project_version = info.properties[TXT_RECORD_PROJECT_VERSION].decode()
         network = info.properties.get(TXT_RECORD_NETWORK, b"wifi").decode()
+        friendly_name = info.properties.get(TXT_RECORD_FRIENDLY_NAME)
+        if friendly_name is not None:
+            friendly_name = friendly_name.decode()
 
         self.import_state[name] = DiscoveredImport(
+            friendly_name=friendly_name,
             device_name=node_name,
             package_import_url=import_url,
             project_name=project_name,
diff --git a/requirements.txt b/requirements.txt
index 9c12253160..9f4d34528c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,9 +9,9 @@ pyserial==3.5
 platformio==6.1.5  # When updating platformio, also update Dockerfile
 esptool==4.4
 click==8.1.3
-esphome-dashboard==20221213.0
-aioesphomeapi==13.0.1
-zeroconf==0.39.4
+esphome-dashboard==20230214.0
+aioesphomeapi==13.1.0
+zeroconf==0.47.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_test.txt b/requirements_test.txt
index fbc8f63613..8404818c95 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -1,13 +1,13 @@
-pylint==2.15.8
+pylint==2.15.10
 flake8==6.0.0  # also change in .pre-commit-config.yaml when updating
-black==22.10.0  # also change in .pre-commit-config.yaml when updating
+black==22.12.0  # also change in .pre-commit-config.yaml when updating
 pyupgrade==3.3.0  # also change in .pre-commit-config.yaml when updating
 pre-commit
 
 # Unit tests
-pytest==7.2.0
+pytest==7.2.1
 pytest-cov==4.0.0
 pytest-mock==3.10.0
-pytest-asyncio==0.20.2
+pytest-asyncio==0.20.3
 asyncmock==0.4.2
 hypothesis==5.49.0
diff --git a/tests/README.md b/tests/README.md
index ed78b3e7d1..3238acaa79 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -25,3 +25,4 @@ Current test_.yaml file contents.
 | test4.yaml | ESP32 | ethernet | None
 | test5.yaml | ESP32 | wifi | ble_server
 | test6.yaml | RP2040 | wifi | N/A
+| test7.yaml | ESP32-C3 | wifi | N/A
diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py
new file mode 100644
index 0000000000..0e24d78f5c
--- /dev/null
+++ b/tests/component_tests/packages/test_packages.py
@@ -0,0 +1,351 @@
+"""Tests for the packages component."""
+
+import pytest
+
+
+from esphome.const import (
+    CONF_DOMAIN,
+    CONF_ESPHOME,
+    CONF_FILTERS,
+    CONF_ID,
+    CONF_MULTIPLY,
+    CONF_NAME,
+    CONF_OFFSET,
+    CONF_PACKAGES,
+    CONF_PASSWORD,
+    CONF_PLATFORM,
+    CONF_SENSOR,
+    CONF_SSID,
+    CONF_UPDATE_INTERVAL,
+    CONF_WIFI,
+)
+from esphome.components.packages import do_packages_pass
+from esphome.config_helpers import Extend
+import esphome.config_validation as cv
+
+# Test strings
+TEST_DEVICE_NAME = "test_device_name"
+TEST_PLATFORM = "test_platform"
+TEST_WIFI_SSID = "test_wifi_ssid"
+TEST_PACKAGE_WIFI_SSID = "test_package_wifi_ssid"
+TEST_PACKAGE_WIFI_PASSWORD = "test_package_wifi_password"
+TEST_DOMAIN = "test_domain_name"
+TEST_SENSOR_PLATFORM_1 = "test_sensor_platform_1"
+TEST_SENSOR_PLATFORM_2 = "test_sensor_platform_2"
+TEST_SENSOR_NAME_1 = "test_sensor_name_1"
+TEST_SENSOR_NAME_2 = "test_sensor_name_2"
+TEST_SENSOR_ID_1 = "test_sensor_id_1"
+TEST_SENSOR_ID_2 = "test_sensor_id_2"
+TEST_SENSOR_UPDATE_INTERVAL = "test_sensor_update_interval"
+
+
+@pytest.fixture(name="basic_wifi")
+def fixture_basic_wifi():
+    return {
+        CONF_SSID: TEST_PACKAGE_WIFI_SSID,
+        CONF_PASSWORD: TEST_PACKAGE_WIFI_PASSWORD,
+    }
+
+
+@pytest.fixture(name="basic_esphome")
+def fixture_basic_esphome():
+    return {CONF_NAME: TEST_DEVICE_NAME, CONF_PLATFORM: TEST_PLATFORM}
+
+
+def test_package_unused(basic_esphome, basic_wifi):
+    """
+    Ensures do_package_pass does not change a config if packages aren't used.
+    """
+    config = {CONF_ESPHOME: basic_esphome, CONF_WIFI: basic_wifi}
+
+    actual = do_packages_pass(config)
+    assert actual == config
+
+
+def test_package_invalid_dict(basic_esphome, basic_wifi):
+    """
+    Ensures an error is raised if packages is not valid.
+
+    """
+    config = {CONF_ESPHOME: basic_esphome, CONF_PACKAGES: basic_wifi}
+
+    with pytest.raises(cv.Invalid):
+        do_packages_pass(config)
+
+
+def test_package_include(basic_wifi, basic_esphome):
+    """
+    Tests the simple case where an independent config present in a package is added to the top-level config as is.
+
+    In this test, the CONF_WIFI config is expected to be simply added to the top-level config.
+    """
+    config = {
+        CONF_ESPHOME: basic_esphome,
+        CONF_PACKAGES: {"network": {CONF_WIFI: basic_wifi}},
+    }
+
+    expected = {CONF_ESPHOME: basic_esphome, CONF_WIFI: basic_wifi}
+
+    actual = do_packages_pass(config)
+    assert actual == expected
+
+
+def test_package_append(basic_wifi, basic_esphome):
+    """
+    Tests the case where a key is present in both a package and top-level config.
+
+    In this test, CONF_WIFI is defined in a package, and CONF_DOMAIN is added to it at the top level.
+    """
+    config = {
+        CONF_ESPHOME: basic_esphome,
+        CONF_PACKAGES: {"network": {CONF_WIFI: basic_wifi}},
+        CONF_WIFI: {CONF_DOMAIN: TEST_DOMAIN},
+    }
+
+    expected = {
+        CONF_ESPHOME: basic_esphome,
+        CONF_WIFI: {
+            CONF_SSID: TEST_PACKAGE_WIFI_SSID,
+            CONF_PASSWORD: TEST_PACKAGE_WIFI_PASSWORD,
+            CONF_DOMAIN: TEST_DOMAIN,
+        },
+    }
+
+    actual = do_packages_pass(config)
+    assert actual == expected
+
+
+def test_package_override(basic_wifi, basic_esphome):
+    """
+    Ensures that the top-level configuration takes precedence over duplicate keys defined in a package.
+
+    In this test, CONF_SSID should be overwritten by that defined in the top-level config.
+    """
+    config = {
+        CONF_ESPHOME: basic_esphome,
+        CONF_PACKAGES: {"network": {CONF_WIFI: basic_wifi}},
+        CONF_WIFI: {CONF_SSID: TEST_WIFI_SSID},
+    }
+
+    expected = {
+        CONF_ESPHOME: basic_esphome,
+        CONF_WIFI: {
+            CONF_SSID: TEST_WIFI_SSID,
+            CONF_PASSWORD: TEST_PACKAGE_WIFI_PASSWORD,
+        },
+    }
+
+    actual = do_packages_pass(config)
+    assert actual == expected
+
+
+def test_multiple_package_order():
+    """
+    Ensures that mutiple packages are merged in order.
+    """
+    config = {
+        CONF_PACKAGES: {
+            "package1": {
+                "logger": {
+                    "level": "DEBUG",
+                },
+            },
+            "package2": {
+                "logger": {
+                    "level": "VERBOSE",
+                },
+            },
+        },
+    }
+
+    expected = {
+        "logger": {
+            "level": "VERBOSE",
+        },
+    }
+
+    actual = do_packages_pass(config)
+    assert actual == expected
+
+
+def test_package_list_merge():
+    """
+    Ensures lists defined in both a package and the top-level config are merged correctly
+    """
+    config = {
+        CONF_PACKAGES: {
+            "package_sensors": {
+                CONF_SENSOR: [
+                    {
+                        CONF_PLATFORM: TEST_SENSOR_PLATFORM_1,
+                        CONF_NAME: TEST_SENSOR_NAME_1,
+                    },
+                    {
+                        CONF_PLATFORM: TEST_SENSOR_PLATFORM_1,
+                        CONF_NAME: TEST_SENSOR_NAME_2,
+                    },
+                ]
+            }
+        },
+        CONF_SENSOR: [
+            {CONF_PLATFORM: TEST_SENSOR_PLATFORM_2, CONF_NAME: TEST_SENSOR_NAME_1},
+            {CONF_PLATFORM: TEST_SENSOR_PLATFORM_2, CONF_NAME: TEST_SENSOR_NAME_2},
+        ],
+    }
+
+    expected = {
+        CONF_SENSOR: [
+            {CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, CONF_NAME: TEST_SENSOR_NAME_1},
+            {CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, CONF_NAME: TEST_SENSOR_NAME_2},
+            {CONF_PLATFORM: TEST_SENSOR_PLATFORM_2, CONF_NAME: TEST_SENSOR_NAME_1},
+            {CONF_PLATFORM: TEST_SENSOR_PLATFORM_2, CONF_NAME: TEST_SENSOR_NAME_2},
+        ]
+    }
+
+    actual = do_packages_pass(config)
+    assert actual == expected
+
+
+def test_package_list_merge_by_id():
+    """
+    Ensures that components with matching IDs are merged correctly.
+
+    In this test, a sensor is defined in a package, and a CONF_UPDATE_INTERVAL is added at the top level,
+    and a sensor name is overridden in another sensor.
+    """
+    config = {
+        CONF_PACKAGES: {
+            "package_sensors": {
+                CONF_SENSOR: [
+                    {
+                        CONF_ID: TEST_SENSOR_ID_1,
+                        CONF_PLATFORM: TEST_SENSOR_PLATFORM_1,
+                        CONF_NAME: TEST_SENSOR_NAME_1,
+                    },
+                    {
+                        CONF_ID: TEST_SENSOR_ID_2,
+                        CONF_PLATFORM: TEST_SENSOR_PLATFORM_1,
+                        CONF_NAME: TEST_SENSOR_NAME_2,
+                    },
+                ]
+            },
+            "package2": {
+                CONF_SENSOR: [
+                    {
+                        CONF_ID: Extend(TEST_SENSOR_ID_1),
+                        CONF_DOMAIN: "2",
+                    }
+                ],
+            },
+            "package3": {
+                CONF_SENSOR: [
+                    {
+                        CONF_ID: Extend(TEST_SENSOR_ID_1),
+                        CONF_DOMAIN: "3",
+                    }
+                ],
+            },
+        },
+        CONF_SENSOR: [
+            {
+                CONF_ID: Extend(TEST_SENSOR_ID_1),
+                CONF_UPDATE_INTERVAL: TEST_SENSOR_UPDATE_INTERVAL,
+            },
+            {CONF_ID: Extend(TEST_SENSOR_ID_2), CONF_NAME: TEST_SENSOR_NAME_1},
+            {CONF_PLATFORM: TEST_SENSOR_PLATFORM_2, CONF_NAME: TEST_SENSOR_NAME_2},
+        ],
+    }
+
+    expected = {
+        CONF_SENSOR: [
+            {
+                CONF_ID: TEST_SENSOR_ID_1,
+                CONF_PLATFORM: TEST_SENSOR_PLATFORM_1,
+                CONF_NAME: TEST_SENSOR_NAME_1,
+                CONF_UPDATE_INTERVAL: TEST_SENSOR_UPDATE_INTERVAL,
+                CONF_DOMAIN: "3",
+            },
+            {
+                CONF_ID: TEST_SENSOR_ID_2,
+                CONF_PLATFORM: TEST_SENSOR_PLATFORM_1,
+                CONF_NAME: TEST_SENSOR_NAME_1,
+            },
+            {CONF_PLATFORM: TEST_SENSOR_PLATFORM_2, CONF_NAME: TEST_SENSOR_NAME_2},
+        ]
+    }
+
+    actual = do_packages_pass(config)
+    assert actual == expected
+
+
+def test_package_merge_by_id_with_list():
+    """
+    Ensures that components with matching IDs are merged correctly when their configuration contains lists.
+
+    For example, a sensor with filters defined in both a package and the top level config should be merged.
+    """
+
+    config = {
+        CONF_PACKAGES: {
+            "sensors": {
+                CONF_SENSOR: [
+                    {CONF_ID: TEST_SENSOR_ID_1, CONF_FILTERS: [{CONF_MULTIPLY: 42.0}]}
+                ]
+            }
+        },
+        CONF_SENSOR: [
+            {CONF_ID: Extend(TEST_SENSOR_ID_1), CONF_FILTERS: [{CONF_OFFSET: 146.0}]}
+        ],
+    }
+
+    expected = {
+        CONF_SENSOR: [
+            {
+                CONF_ID: TEST_SENSOR_ID_1,
+                CONF_FILTERS: [{CONF_MULTIPLY: 42.0}, {CONF_OFFSET: 146.0}],
+            }
+        ]
+    }
+
+    actual = do_packages_pass(config)
+    assert actual == expected
+
+
+def test_package_merge_by_missing_id():
+    """
+    Ensures that components with missing IDs are not merged.
+    """
+
+    config = {
+        CONF_PACKAGES: {
+            "sensors": {
+                CONF_SENSOR: [
+                    {CONF_ID: TEST_SENSOR_ID_1, CONF_FILTERS: [{CONF_MULTIPLY: 42.0}]},
+                ]
+            }
+        },
+        CONF_SENSOR: [
+            {CONF_ID: TEST_SENSOR_ID_1, CONF_FILTERS: [{CONF_MULTIPLY: 10.0}]},
+            {CONF_ID: Extend(TEST_SENSOR_ID_2), CONF_FILTERS: [{CONF_OFFSET: 146.0}]},
+        ],
+    }
+
+    expected = {
+        CONF_SENSOR: [
+            {
+                CONF_ID: TEST_SENSOR_ID_1,
+                CONF_FILTERS: [{CONF_MULTIPLY: 42.0}],
+            },
+            {
+                CONF_ID: TEST_SENSOR_ID_1,
+                CONF_FILTERS: [{CONF_MULTIPLY: 10.0}],
+            },
+            {
+                CONF_ID: Extend(TEST_SENSOR_ID_2),
+                CONF_FILTERS: [{CONF_OFFSET: 146.0}],
+            },
+        ]
+    }
+
+    actual = do_packages_pass(config)
+    assert actual == expected
diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp
index 8cc1838d94..236b9f5fc2 100644
--- a/tests/dummy_main.cpp
+++ b/tests/dummy_main.cpp
@@ -3,16 +3,16 @@
 // matter at all, as long as it compiles).
 // Not used during runtime nor for CI.
 
-#include <esphome/core/application.h>
-#include <esphome/components/logger/logger.h>
-#include <esphome/components/wifi/wifi_component.h>
-#include <esphome/components/ota/ota_component.h>
 #include <esphome/components/gpio/switch/gpio_switch.h>
+#include <esphome/components/logger/logger.h>
+#include <esphome/components/ota/ota_component.h>
+#include <esphome/components/wifi/wifi_component.h>
+#include <esphome/core/application.h>
 
 using namespace esphome;
 
 void setup() {
-  App.pre_setup("livingroom", __DATE__ ", " __TIME__, false);
+  App.pre_setup("livingroom", "LivingRoom", "comment", __DATE__ ", " __TIME__, false);
   auto *log = new logger::Logger(115200, 512);  // NOLINT
   log->pre_setup();
   log->set_uart_selection(logger::UART_SELECTION_UART0);
diff --git a/tests/test1.yaml b/tests/test1.yaml
index 88cdb43fca..a77f8802b9 100644
--- a/tests/test1.yaml
+++ b/tests/test1.yaml
@@ -222,6 +222,12 @@ uart:
     rx_pin: GPIO26
     baud_rate: 115200
     rx_buffer_size: 1024
+  - id: ld2410_uart
+    tx_pin: 18
+    rx_pin: 23
+    baud_rate: 256000
+    parity: NONE
+    stop_bits: 1
 
 ota:
   safe_mode: true
@@ -568,6 +574,15 @@ sensor:
   - platform: duty_cycle
     pin: GPIO25
     name: Duty Cycle Sensor
+  - platform: ee895
+    co2:
+      name: Office CO2 1
+    temperature:
+      name: Office Temperature 1
+    pressure:
+      name: Office Pressure 1
+    address: 0x5F
+    i2c_id: i2c_bus
   - platform: esp32_hall
     name: ESP32 Hall Sensor
     update_interval: 15s
@@ -638,6 +653,13 @@ sensor:
     temperature:
       name: Honeywell temperature
     cs_pin: GPIO5
+  - platform: hte501
+    temperature:
+      name: Office Temperature 2
+    humidity:
+      name: Office Humidity 1
+    address: 0x40
+    i2c_id: i2c_bus
   - platform: qmc5883l
     address: 0x0D
     field_strength_x:
@@ -1040,6 +1062,10 @@ sensor:
       name: tsl2591 calculated_lux
       id: tsl2591_cl
     i2c_id: i2c_bus
+  - platform: tee501
+    name: Office Temperature 3
+    address: 0x48
+    i2c_id: i2c_bus
   - platform: ultrasonic
     trigger_pin: GPIO25
     echo_pin:
@@ -1050,6 +1076,8 @@ sensor:
     id: ultrasonic_sensor1
   - platform: uptime
     name: Uptime Sensor
+  - id: !extend ${devicename}_uptime_pcg
+    unit_of_measurement: s
   - platform: wifi_signal
     name: WiFi Signal Sensor
     update_interval: 15s
@@ -1180,6 +1208,17 @@ sensor:
     pressure:
       name: "MPL3115A2 Pressure"
     update_interval: 10s
+  - platform: ld2410
+    moving_distance:
+      name: "Moving distance (cm)"
+    still_distance:
+      name: "Still Distance (cm)"
+    moving_energy:
+      name: "Move Energy"
+    still_energy:
+      name: "Still Energy"
+    detection_distance:
+      name: "Distance Detection"
 
 esp32_touch:
   setup_mode: false
@@ -1284,6 +1323,11 @@ binary_sensor:
       number: GPIO9
       mode: INPUT_PULLUP
     name: Living Room Window 2
+  - platform: gpio
+    pin:
+      number: GPIO9
+      mode: INPUT_OUTPUT_OPEN_DRAIN
+    name: Living Room Button
   - platform: status
     name: Living Room Status
   - platform: esp32_touch
@@ -1341,6 +1385,13 @@ binary_sensor:
       number: 1
       mode: INPUT
       inverted: true
+  - platform: gpio
+    name: PCA9554 binary sensor
+    pin:
+      pca9554: pca9554_hub
+      number: 1
+      mode: INPUT
+      inverted: true
   - platform: gpio
     name: MCP21 binary sensor
     pin:
@@ -1434,6 +1485,13 @@ binary_sensor:
     id: close_sensor
   - platform: template
     id: close_obstacle_sensor
+  - platform: ld2410
+    has_target:
+      name: presence
+    has_moving_target:
+      name: movement
+    has_still_target:
+      name: still
 
 pca9685:
   frequency: 500
@@ -1458,6 +1516,28 @@ my9231:
   num_chips: 2
   bit_depth: 16
 
+sm2235:
+  data_pin: GPIO4
+  clock_pin: GPIO5
+  max_power_color_channels: 9
+  max_power_white_channels: 9
+
+sm2335:
+  data_pin: GPIO4
+  clock_pin: GPIO5
+  max_power_color_channels: 9
+  max_power_white_channels: 9
+
+bp1658cj:
+  data_pin: GPIO3
+  clock_pin: GPIO5
+  max_power_color_channels: 4
+  max_power_white_channels: 6
+
+bp5758d:
+  data_pin: GPIO3
+  clock_pin: GPIO5
+
 output:
   - platform: gpio
     pin: GPIO26
@@ -1537,6 +1617,13 @@ output:
       number: 0
       mode: OUTPUT
       inverted: false
+  - platform: gpio
+    id: id26
+    pin:
+      pca9554: pca9554_hub
+      number: 0
+      mode: OUTPUT
+      inverted: false
   - platform: gpio
     id: id22
     pin:
@@ -1576,6 +1663,36 @@ output:
   - platform: my9231
     id: my_5
     channel: 5
+  - platform: sm2235
+    id: sm2235_red
+    channel: 1
+  - platform: sm2235
+    id: sm2235_green
+    channel: 0
+  - platform: sm2235
+    id: sm2235_blue
+    channel: 2
+  - platform: sm2235
+    id: sm2235_coldwhite
+    channel: 4
+  - platform: sm2235
+    id: sm2235_warmwhite
+    channel: 3
+  - platform: sm2335
+    id: sm2335_red
+    channel: 1
+  - platform: sm2335
+    id: sm2335_green
+    channel: 0
+  - platform: sm2335
+    id: sm2335_blue
+    channel: 2
+  - platform: sm2335
+    id: sm2335_coldwhite
+    channel: 4
+  - platform: sm2335
+    id: sm2335_warmwhite
+    channel: 3
   - platform: slow_pwm
     id: id24
     pin: GPIO26
@@ -1612,6 +1729,47 @@ output:
     vref: internal
     gain: X2
     power_down: gnd_500k
+  - platform: bp1658cj
+    id: bp1658cj_red
+    channel: 1
+  - platform: bp1658cj
+    id: bp1658cj_green
+    channel: 2
+  - platform: bp1658cj
+    id: bp1658cj_blue
+    channel: 0
+  - platform: bp1658cj
+    id: bp1658cj_coldwhite
+    channel: 3
+  - platform: bp1658cj
+    id: bp1658cj_warmwhite
+    channel: 4
+  - platform: bp5758d
+    id: bp5758d_red
+    channel: 2
+    current: 10
+  - platform: bp5758d
+    id: bp5758d_green
+    channel: 3
+    current: 10
+  - platform: bp5758d
+    id: bp5758d_blue
+    channel: 1
+    current: 10
+  - platform: bp5758d
+    id: bp5758d_coldwhite
+    channel: 5
+    current: 10
+  - platform: bp5758d
+    id: bp5758d_warmwhite
+    channel: 4
+    current: 10
+  - platform: x9c
+    id: test_x9c
+    cs_pin: GPIO25
+    inc_pin: GPIO26
+    ud_pin: GPIO27
+    initial_value: 0.5
 
 e131:
 
@@ -2716,6 +2874,11 @@ pcf8574:
     pcf8575: false
     i2c_id: i2c_bus
 
+pca9554:
+  - id: pca9554_hub
+    address: 0x3F
+    i2c_id: i2c_bus
+
 mcp23017:
   - id: mcp23017_hub
     open_drain_interrupt: true
@@ -3011,6 +3174,19 @@ button:
     on_press:
       midea_ac.power_toggle:
 
+ld2410:
+  id: my_ld2410
+  uart_id: ld2410_uart
+  timeout: 150s
+  max_move_distance: 6m
+  max_still_distance: 0.75m
+  g0_move_threshold: 10
+  g0_still_threshold: 20
+  g2_move_threshold: 20
+  g2_still_threshold: 21
+  g8_move_threshold: 80
+  g8_still_threshold: 81
+
 lcd_menu:
   display_id: my_lcd_gpio
   mark_back: 0x5e
diff --git a/tests/test2.yaml b/tests/test2.yaml
index 7d4cb4cbb2..3e6a186320 100644
--- a/tests/test2.yaml
+++ b/tests/test2.yaml
@@ -381,6 +381,20 @@ sensor:
     temperature_sensor: ha_hello_world_temperature
     ph:
       name: Ufire pH
+  - platform: mics_4514
+    update_interval: 60s
+    nitrogen_dioxide:
+      name: MICS-4514 NO2
+    carbon_monoxide:
+      name: MICS-4514 CO
+    methane:
+      name: MICS-4514 CH4
+    hydrogen:
+      name: MICS-4514 H2
+    ethanol:
+      name: MICS-4514 C2H5OH
+    ammonia:
+      name: MICS-4514 NH3
 
 time:
   - platform: homeassistant
@@ -462,6 +476,15 @@ binary_sensor:
       name: Mi Motion Sensor 2 Light
     button:
       name: Mi Motion Sensor 2 Button
+  - platform: gpio
+    id: gpio_set_retry_test
+    pin: GPIO9
+    on_press:
+      then:
+        - lambda: |-
+            App.scheduler.set_retry(id(gpio_set_retry_test), "set_retry_test", 100, 3, [](const uint8_t remaining) {
+              return remaining ? RetryResult::RETRY : RetryResult::DONE; // just to reference both symbols
+            }, 5.0f);
 
 esp32_ble_tracker:
   on_ble_advertise:
diff --git a/tests/test3.yaml b/tests/test3.yaml
index 9b4e530fbd..4827b7cbcd 100644
--- a/tests/test3.yaml
+++ b/tests/test3.yaml
@@ -287,6 +287,9 @@ uart:
 modbus:
   uart_id: uart1
 
+vbus:
+  uart_id: uart4
+
 ota:
   safe_mode: true
   port: 3286
@@ -298,6 +301,7 @@ logger:
   esp8266_store_log_strings_in_flash: true
 
 improv_serial:
+  next_url: https://esphome.io/?name={{device_name}}&version={{esphome_version}}&ip={{ip_address}}
 
 deep_sleep:
   run_duration: 20s
@@ -798,6 +802,11 @@ sensor:
     id: adc128s102_channel_0
     channel: 0
 
+  - platform: vbus
+    model: deltasol c
+    temperature_1:
+      name: Temperature 1
+
 time:
   - platform: homeassistant
 
@@ -903,6 +912,11 @@ binary_sensor:
       then:
         - pzemac.reset_energy: pzemac1
 
+  - platform: vbus
+    model: deltasol_bs_plus
+    relay1:
+      name: Relay 1 On
+
 globals:
   - id: my_global_string
     type: std::string
@@ -1122,14 +1136,16 @@ climate:
       - switch.turn_on: gpio_switch1
     fan_mode_diffuse_action:
       - switch.turn_on: gpio_switch2
+    fan_mode_quiet_action:
+      - switch.turn_on: gpio_switch1
     swing_off_action:
-      - switch.turn_on: gpio_switch1
+      - switch.turn_on: gpio_switch2
     swing_horizontal_action:
-      - switch.turn_on: gpio_switch2
-    swing_vertical_action:
       - switch.turn_on: gpio_switch1
-    swing_both_action:
+    swing_vertical_action:
       - switch.turn_on: gpio_switch2
+    swing_both_action:
+      - switch.turn_on: gpio_switch1
     startup_delay: true
     supplemental_cooling_delta: 2.0
     cool_deadband: 0.5
@@ -1313,6 +1329,21 @@ output:
       return {s};
     outputs:
       - id: custom_binary
+  - platform: sigma_delta_output
+    id: sddac
+    update_interval: 60s
+    pin: D4
+    turn_on_action:
+      then:
+        - logger.log: "Turned on"
+    turn_off_action:
+      then:
+        - logger.log: "Turned off"
+    state_change_action:
+      then:
+        - logger.log:
+            format: "Changed state: %d"
+            args: [ 'state' ]
   - platform: custom
     type: float
     lambda: |-
diff --git a/tests/test5.yaml b/tests/test5.yaml
index 596ba5de4e..5f72579d08 100644
--- a/tests/test5.yaml
+++ b/tests/test5.yaml
@@ -66,6 +66,9 @@ mqtt:
           ESP_LOGD("Mqtt Test", "testing/sensor/testing_sensor/state=[%s]", x.c_str());
       # yamllint enable rule:line-length
 
+vbus:
+  - uart_id: uart2
+
 binary_sensor:
   - platform: gpio
     pin: GPIO0
@@ -168,14 +171,38 @@ binary_sensor:
       sn74hc165: sn74hc165_hub
       number: 0
 
-
-
   - platform: ezo_pmp
     pump_state:
       name: "Pump State"
     is_paused:
       name: "Is Paused"
 
+  - platform: matrix_keypad
+    keypad_id: keypad
+    id: key4
+    row: 1
+    col: 1
+  - platform: matrix_keypad
+    id: key1
+    key: 1
+
+  - platform: vbus
+    model: deltasol_bs_plus
+    relay2:
+      name: Relay 2 On
+    sensor1_error:
+      name: Sensor 1 Error
+
+  - platform: vbus
+    model: custom
+    command: 0x100
+    source: 0x1234
+    dest: 0x10
+    binary_sensors:
+      - id: vcustom_b
+        name: VBus Custom Binary Sensor
+        lambda: return x[0] & 1;
+
 tlc5947:
   data_pin: GPIO12
   clock_pin: GPIO14
@@ -470,6 +497,27 @@ sensor:
     max_flow_rate:
       name: Max Flow Rate
 
+  - platform: vbus
+    model: deltasol c
+    temperature_3:
+      name: Temperature 3
+    operating_hours_1:
+      name: Operating Hours 1
+    heat_quantity:
+      name: Heat Quantity
+    time:
+      name: System Time
+
+  - platform: vbus
+    model: custom
+    command: 0x100
+    source: 0x1234
+    dest: 0x10
+    sensors:
+      - id: vcustom
+        name: VBus Custom Sensor
+        lambda: return x[0] / 10.0;
+
 script:
   - id: automation_test
     then:
@@ -517,6 +565,9 @@ display:
     lambda: |-
       it.print("81818181");
 
+time:
+  - platform: pcf85063
+
 text_sensor:
   - platform: ezo_pmp
     dosing_mode:
@@ -555,3 +606,21 @@ sn74hc165:
   load_pin: GPIO27
   clock_inhibit_pin: GPIO26
   sr_count: 4
+
+
+matrix_keypad:
+  id: keypad
+  rows:
+    - pin: 21
+    - pin: 19
+  columns:
+    - pin: 17
+    - pin: 16
+  keys: "1234"
+
+key_collector:
+  - id: reader
+    source_id: keypad
+    min_length: 4
+    max_length: 4
+
diff --git a/tests/test7.yaml b/tests/test7.yaml
new file mode 100644
index 0000000000..10e1b035ab
--- /dev/null
+++ b/tests/test7.yaml
@@ -0,0 +1,33 @@
+# Tests for ESP32-C3 boards which use toolchain-riscv32-esp
+---
+wifi:
+  ssid: 'ssid'
+
+esp32:
+  board: lolin_c3_mini
+  framework:
+    type: arduino
+
+esphome:
+  name: 'on-response-test'
+  on_boot:
+    then:
+      - http_request.send:
+          method: PUT
+          url: https://esphome.io
+          headers:
+            Content-Type: application/json
+          body: Some data
+          verify_ssl: false
+          on_response:
+            then:
+              - logger.log:
+                  format: "Response status: %d"
+                  args:
+                    - status_code
+
+logger:
+
+http_request:
+  useragent: esphome/tagreader
+  timeout: 10s