diff --git a/.clang-tidy b/.clang-tidy
index 5e486e6a0c..1202d12c27 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -4,14 +4,35 @@ Checks: >-
   -abseil-*,
   -android-*,
   -boost-*,
+  -bugprone-branch-clone,
   -bugprone-macro-parentheses,
+  -bugprone-narrowing-conversions,
+  -bugprone-reserved-identifier,
+  -bugprone-signed-char-misuse,
+  -bugprone-suspicious-include,
+  -bugprone-too-small-loop-variable,
+  -bugprone-unhandled-self-assignment,
+  -cert-dcl37-c,
   -cert-dcl50-cpp,
+  -cert-dcl51-cpp,
   -cert-err58-cpp,
+  -cert-oop54-cpp,
+  -cert-oop57-cpp,
+  -cert-str34-c,
   -clang-analyzer-core.CallAndMessage,
+  -clang-analyzer-optin.*,
   -clang-analyzer-osx.*,
   -clang-analyzer-security.*,
+  -clang-diagnostic-shadow-field,
+  -cppcoreguidelines-avoid-c-arrays,
   -cppcoreguidelines-avoid-goto,
+  -cppcoreguidelines-avoid-magic-numbers,
+  -cppcoreguidelines-avoid-non-const-global-variables,
   -cppcoreguidelines-c-copy-assignment-signature,
+  -cppcoreguidelines-init-variables,
+  -cppcoreguidelines-macro-usage,
+  -cppcoreguidelines-narrowing-conversions,
+  -cppcoreguidelines-non-private-member-variables-in-classes,
   -cppcoreguidelines-owning-memory,
   -cppcoreguidelines-pro-bounds-array-to-pointer-decay,
   -cppcoreguidelines-pro-bounds-constant-array-index,
@@ -37,10 +58,16 @@ Checks: >-
   -google-runtime-int,
   -google-runtime-references,
   -hicpp-*,
+  -llvm-else-after-return,
   -llvm-header-guard,
   -llvm-include-order,
+  -llvm-qualified-auto,
+  -llvmlibc-*,
+  -misc-non-private-member-variables-in-classes,
+  -misc-no-recursion,
   -misc-unconventional-assign-operator,
   -misc-unused-parameters,
+  -modernize-avoid-c-arrays,
   -modernize-deprecated-headers,
   -modernize-pass-by-value,
   -modernize-pass-by-value,
@@ -48,14 +75,25 @@ Checks: >-
   -modernize-use-auto,
   -modernize-use-default-member-init,
   -modernize-use-equals-default,
+  -modernize-use-trailing-return-type,
   -mpi-*,
   -objc-*,
   -performance-unnecessary-value-param,
   -readability-braces-around-statements,
+  -readability-const-return-type,
+  -readability-convert-member-functions-to-static,
   -readability-else-after-return,
   -readability-implicit-bool-conversion,
+  -readability-isolate-declaration,
+  -readability-magic-numbers,
+  -readability-make-member-function-const,
   -readability-named-parameter,
+  -readability-qualified-auto,
+  -readability-redundant-access-specifiers,
   -readability-redundant-member-init,
+  -readability-redundant-string-init,
+  -readability-uppercase-literal-suffix,
+  -readability-use-anyofallof,
   -warnings-as-errors,
   -zircon-*
 WarningsAsErrors: '*'
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 0d77eee7aa..aa90ef365f 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,25 +1,22 @@
 # What does this implement/fix? 
 
-Quick description 
+Quick description and explanation of changes
 
 ## Types of changes
 
 - [ ] Bugfix (non-breaking change which fixes an issue)
 - [ ] New feature (non-breaking change which adds functionality)
 - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
-- [ ] Configuration change (this will require users to update their yaml configuration files to keep working)
+- [ ] Other
 
 **Related issue or feature (if applicable):** fixes <link to issue>
 
 **Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** esphome/esphome-docs#<esphome-docs PR number goes here>
-  
-# Test Environment
+
+## Test Environment
 
 - [ ] ESP32
 - [ ] ESP8266
-- [ ] Windows
-- [ ] Mac OS
-- [ ] Linux
 
 ## Example entry for `config.yaml`:
 <!--
@@ -34,11 +31,6 @@ Quick description
 
 ```
 
-# Explain your changes
-
-Describe your changes here to communicate to the maintainers **why we should accept this pull request**.
-Very important to fill if no issue linked
-
 ## Checklist:
   - [ ] The code change is tested and works locally.
   - [ ] Tests have been added to verify that the new code works (under `tests/` folder).
diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml
index 7da8a3f8f1..b5f8b7b0e0 100644
--- a/.github/workflows/ci-docker.yml
+++ b/.github/workflows/ci-docker.yml
@@ -26,7 +26,7 @@ jobs:
       - uses: actions/checkout@v2
       - name: Set up env variables
         run: |
-          base_version="3.0.0"
+          base_version="3.4.0"
 
           if [[ "${{ matrix.build_type }}" == "hassio" ]]; then
             build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d2230b3da7..a27cbe67b0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,7 +15,7 @@ jobs:
     runs-on: ubuntu-latest
     # cpp lint job runs with esphome-lint docker image so that clang-format-*
     # doesn't have to be installed
-    container: esphome/esphome-lint:latest
+    container: esphome/esphome-lint:1.1
     steps:
       - uses: actions/checkout@v2
       # Set up the pio project so that the cpp checks know how files are compiled
@@ -32,7 +32,7 @@ jobs:
     runs-on: ubuntu-latest
     # cpp lint job runs with esphome-lint docker image so that clang-format-*
     # doesn't have to be installed
-    container: esphome/esphome-lint:latest
+    container: esphome/esphome-lint:1.1
     # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files
     strategy:
       fail-fast: false
@@ -97,6 +97,7 @@ jobs:
           - test2
           - test3
           - test4
+          - test5
     steps:
       - uses: actions/checkout@v2
       - name: Set up Python
@@ -126,7 +127,7 @@ jobs:
         run: |
           echo "::add-matcher::.github/workflows/matchers/gcc.json"
           echo "::add-matcher::.github/workflows/matchers/python.json"
-      - run: esphome tests/${{ matrix.test }}.yaml compile
+      - run: esphome compile tests/${{ matrix.test }}.yaml
 
   pytest:
     runs-on: ubuntu-latest
diff --git a/.github/workflows/docker-lint-build.yml b/.github/workflows/docker-lint-build.yml
index f148d98d65..d254ac332a 100644
--- a/.github/workflows/docker-lint-build.yml
+++ b/.github/workflows/docker-lint-build.yml
@@ -7,6 +7,7 @@ on:
     paths:
       - 'docker/Dockerfile.lint'
       - 'requirements.txt'
+      - 'requirements_optional.txt'
       - 'requirements_test.txt'
       - 'platformio.ini'
       - '.github/workflows/docker-lint-build.yml'
@@ -17,6 +18,9 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
+      - name: Set TAG
+        run: |
+          echo "TAG=1.1" >> $GITHUB_ENV
       - name: Pull for cache
         run: |
           docker pull "esphome/esphome-lint:latest" || true
@@ -26,6 +30,7 @@ jobs:
             --cache-from "esphome/esphome-lint:latest" \
             --file "docker/Dockerfile.lint" \
             --tag "esphome/esphome-lint:latest" \
+            --tag "esphome/esphome-lint:${TAG}" \
             .
       - name: Log in to docker hub
         env:
@@ -33,4 +38,5 @@ jobs:
           DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
         run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}"
       - run: |
+          docker push "esphome/esphome-lint:${TAG}"
           docker push "esphome/esphome-lint:latest"
diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml
index f0dc4bd0c0..f8b90d524f 100644
--- a/.github/workflows/release-dev.yml
+++ b/.github/workflows/release-dev.yml
@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-latest
     # cpp lint job runs with esphome-lint docker image so that clang-format-*
     # doesn't have to be installed
-    container: esphome/esphome-lint:latest
+    container: esphome/esphome-lint:1.1
     steps:
       - uses: actions/checkout@v2
       # Set up the pio project so that the cpp checks know how files are compiled
@@ -29,7 +29,7 @@ jobs:
     runs-on: ubuntu-latest
     # cpp lint job runs with esphome-lint docker image so that clang-format-*
     # doesn't have to be installed
-    container: esphome/esphome-lint:latest
+    container: esphome/esphome-lint:1.1
     # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files
     strategy:
       fail-fast: false
@@ -94,6 +94,7 @@ jobs:
           - test2
           - test3
           - test4
+          - test5
     steps:
       - uses: actions/checkout@v2
       - name: Set up Python
@@ -123,7 +124,7 @@ jobs:
         run: |
           echo "::add-matcher::.github/workflows/matchers/gcc.json"
           echo "::add-matcher::.github/workflows/matchers/python.json"
-      - run: esphome tests/${{ matrix.test }}.yaml compile
+      - run: esphome compile tests/${{ matrix.test }}.yaml
 
   pytest:
     runs-on: ubuntu-latest
@@ -174,7 +175,7 @@ jobs:
           echo "TAG=${TAG}" >> $GITHUB_ENV
       - name: Set up env variables
         run: |
-          base_version="3.0.0"
+          base_version="3.4.0"
 
           if [[ "${{ matrix.build_type }}" == "hassio" ]]; then
             build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1eca3be269..9523a2164c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -11,7 +11,7 @@ jobs:
     runs-on: ubuntu-latest
     # cpp lint job runs with esphome-lint docker image so that clang-format-*
     # doesn't have to be installed
-    container: esphome/esphome-lint:latest
+    container: esphome/esphome-lint:1.1
     steps:
       - uses: actions/checkout@v2
       # Set up the pio project so that the cpp checks know how files are compiled
@@ -28,7 +28,7 @@ jobs:
     runs-on: ubuntu-latest
     # cpp lint job runs with esphome-lint docker image so that clang-format-*
     # doesn't have to be installed
-    container: esphome/esphome-lint:latest
+    container: esphome/esphome-lint:1.1
     # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files
     strategy:
       fail-fast: false
@@ -93,6 +93,7 @@ jobs:
           - test2
           - test3
           - test4
+          - test5
     steps:
       - uses: actions/checkout@v2
       - name: Set up Python
@@ -121,7 +122,7 @@ jobs:
         run: |
           echo "::add-matcher::.github/workflows/matchers/gcc.json"
           echo "::add-matcher::.github/workflows/matchers/python.json"
-      - run: esphome tests/${{ matrix.test }}.yaml compile
+      - run: esphome compile tests/${{ matrix.test }}.yaml
 
   pytest:
     runs-on: ubuntu-latest
@@ -194,7 +195,7 @@ jobs:
           echo "TAG=${TAG}" >> $GITHUB_ENV
       - name: Set up env variables
         run: |
-          base_version="3.0.0"
+          base_version="3.4.0"
 
           if [[ "${{ matrix.build_type }}" == "hassio" ]]; then
             build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}"
diff --git a/.gitignore b/.gitignore
index 86a27eb4b4..a24550ad54 100644
--- a/.gitignore
+++ b/.gitignore
@@ -100,6 +100,8 @@ CMakeLists.txt
 
 # CMake
 cmake-build-debug/
+cmake-build-livingroom8266/
+cmake-build-livingroom32/
 cmake-build-release/
 
 CMakeCache.txt
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index e11600b093..cc83d8bcdf 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -4,7 +4,7 @@
         {
             "label": "run",
             "type": "shell",
-            "command": "python3 -m esphome config dashboard",
+            "command": "python3 -m esphome dashboard config",
             "problemMatcher": []
         }
     ]
diff --git a/CODEOWNERS b/CODEOWNERS
index 2269730017..3f1abae5df 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -29,11 +29,14 @@ esphome/components/climate/* @esphome/core
 esphome/components/climate_ir/* @glmnet
 esphome/components/coolix/* @glmnet
 esphome/components/cover/* @esphome/core
+esphome/components/cs5460a/* @balrog-kun
 esphome/components/ct_clamp/* @jesserockz
 esphome/components/debug/* @OttoWinter
 esphome/components/dfplayer/* @glmnet
 esphome/components/dht/* @OttoWinter
 esphome/components/ds1307/* @badbadc0ffee
+esphome/components/esp32_ble/* @jesserockz
+esphome/components/esp32_improv/* @jesserockz
 esphome/components/exposure_notifications/* @OttoWinter
 esphome/components/ezo/* @ssieb
 esphome/components/fastled_base/* @OttoWinter
@@ -43,6 +46,7 @@ esphome/components/gpio/* @esphome/core
 esphome/components/gps/* @coogle
 esphome/components/homeassistant/* @OttoWinter
 esphome/components/i2c/* @esphome/core
+esphome/components/improv/* @jesserockz
 esphome/components/inkbird_ibsth1_mini/* @fkirill
 esphome/components/inkplate6/* @jesserockz
 esphome/components/integration/* @OttoWinter
@@ -63,6 +67,7 @@ esphome/components/mcp2515/* @danielschramm @mvturnho
 esphome/components/mcp9808/* @k7hpn
 esphome/components/midea_ac/* @dudanov
 esphome/components/midea_dongle/* @dudanov
+esphome/components/mitsubishi/* @RubyBailey
 esphome/components/network/* @esphome/core
 esphome/components/nfc/* @jesserockz
 esphome/components/ota/* @esphome/core
@@ -80,6 +85,7 @@ esphome/components/restart/* @esphome/core
 esphome/components/rf_bridge/* @jesserockz
 esphome/components/rtttl/* @glmnet
 esphome/components/script/* @esphome/core
+esphome/components/sdm_meter/* @jesserockz @polyfaces
 esphome/components/sensor/* @esphome/core
 esphome/components/sgp40/* @SenexCrenshaw
 esphome/components/sht4x/* @sjtrny
@@ -122,3 +128,4 @@ esphome/components/web_server_base/* @OttoWinter
 esphome/components/whirlpool/* @glmnet
 esphome/components/xiaomi_lywsd03mmc/* @ahpohl
 esphome/components/xiaomi_mhoc401/* @vevsvevs
+esphome/components/xpt2046/* @numo68
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 7364299ba7..0d126a2944 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,10 +1,10 @@
-ARG BUILD_FROM=esphome/esphome-base-amd64:3.0.0
+ARG BUILD_FROM=esphome/esphome-base-amd64:3.4.0
 FROM ${BUILD_FROM}
 
 # First install requirements to leverage caching when requirements don't change
-COPY requirements.txt docker/platformio_install_deps.py platformio.ini /
+COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini /
 RUN \
-    pip3 install --no-cache-dir -r /requirements.txt \
+    pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \
     && /platformio_install_deps.py /platformio.ini
 
 # Then copy esphome and install
@@ -27,4 +27,4 @@ WORKDIR /config
 # in every docker command twice
 ENTRYPOINT ["esphome"]
 # When no arguments given, start the dashboard in the workdir
-CMD ["/config", "dashboard"]
+CMD ["dashboard", "/config"]
diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev
index 52495fc8ae..ebcf14d1bc 100644
--- a/docker/Dockerfile.dev
+++ b/docker/Dockerfile.dev
@@ -1,4 +1,4 @@
-FROM esphome/esphome-base-amd64:3.0.0
+FROM esphome/esphome-base-amd64:3.4.0
 
 COPY . .
 
diff --git a/docker/Dockerfile.hassio b/docker/Dockerfile.hassio
index b6ff037c35..5dd9339b18 100644
--- a/docker/Dockerfile.hassio
+++ b/docker/Dockerfile.hassio
@@ -2,9 +2,9 @@ ARG BUILD_FROM
 FROM ${BUILD_FROM}
 
 # First install requirements to leverage caching when requirements don't change
-COPY requirements.txt docker/platformio_install_deps.py platformio.ini /
+COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini /
 RUN \
-    pip3 install --no-cache-dir -r /requirements.txt \
+    pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \
     && /platformio_install_deps.py /platformio.ini
 
 # Copy root filesystem
diff --git a/docker/Dockerfile.lint b/docker/Dockerfile.lint
index 5488e68c91..60d63152a0 100644
--- a/docker/Dockerfile.lint
+++ b/docker/Dockerfile.lint
@@ -1,8 +1,8 @@
-FROM esphome/esphome-lint-base:3.0.0
+FROM esphome/esphome-lint-base:3.4.0
 
-COPY requirements.txt requirements_test.txt docker/platformio_install_deps.py  platformio.ini /
+COPY requirements.txt requirements_optional.txt requirements_test.txt docker/platformio_install_deps.py  platformio.ini /
 RUN \
-    pip3 install --no-cache-dir -r /requirements.txt -r /requirements_test.txt \
+    pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt -r /requirements_test.txt \
     && /platformio_install_deps.py /platformio.ini
 
 VOLUME ["/esphome"]
diff --git a/docker/rootfs/etc/services.d/esphome/run b/docker/rootfs/etc/services.d/esphome/run
index 6257bec6a3..f806c50929 100755
--- a/docker/rootfs/etc/services.d/esphome/run
+++ b/docker/rootfs/etc/services.d/esphome/run
@@ -23,4 +23,4 @@ if bashio::config.has_value 'relative_url'; then
 fi
 
 bashio::log.info "Starting ESPHome dashboard..."
-exec esphome /config/esphome dashboard --socket /var/run/esphome.sock --hassio
+exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --hassio
diff --git a/esphome/__main__.py b/esphome/__main__.py
index 1ec72d9255..9af08a8f21 100644
--- a/esphome/__main__.py
+++ b/esphome/__main__.py
@@ -18,7 +18,7 @@ from esphome.const import (
     CONF_ESPHOME,
     CONF_PLATFORMIO_OPTIONS,
 )
-from esphome.core import CORE, EsphomeError, coroutine, coroutine_with_priority
+from esphome.core import CORE, EsphomeError, coroutine
 from esphome.helpers import indent
 from esphome.util import (
     run_external_command,
@@ -127,15 +127,16 @@ def wrap_to_code(name, comp):
     coro = coroutine(comp.to_code)
 
     @functools.wraps(comp.to_code)
-    @coroutine_with_priority(coro.priority)
-    def wrapped(conf):
+    async def wrapped(conf):
         cg.add(cg.LineComment(f"{name}:"))
         if comp.config_schema is not None:
             conf_str = yaml_util.dump(conf)
             conf_str = conf_str.replace("//", "")
             cg.add(cg.LineComment(indent(conf_str)))
-        yield coro(conf)
+        await coro(conf)
 
+    if hasattr(coro, "priority"):
+        wrapped.priority = coro.priority
     return wrapped
 
 
@@ -267,7 +268,7 @@ def clean_mqtt(config, args):
 def command_wizard(args):
     from esphome import wizard
 
-    return wizard.wizard(args.configuration[0])
+    return wizard.wizard(args.configuration)
 
 
 def command_config(args, config):
@@ -283,7 +284,7 @@ def command_vscode(args):
 
     logging.disable(logging.INFO)
     logging.disable(logging.WARNING)
-    CORE.config_path = args.configuration[0]
+    CORE.config_path = args.configuration
     vscode.read_config(args)
 
 
@@ -303,7 +304,7 @@ def command_compile(args, config):
 
 def command_upload(args, config):
     port = choose_upload_log_host(
-        default=args.upload_port,
+        default=args.device,
         check_default=None,
         show_ota=True,
         show_mqtt=False,
@@ -318,7 +319,7 @@ def command_upload(args, config):
 
 def command_logs(args, config):
     port = choose_upload_log_host(
-        default=args.serial_port,
+        default=args.device,
         check_default=None,
         show_ota=False,
         show_mqtt=True,
@@ -336,7 +337,7 @@ def command_run(args, config):
         return exit_code
     _LOGGER.info("Successfully compiled program.")
     port = choose_upload_log_host(
-        default=args.upload_port,
+        default=args.device,
         check_default=None,
         show_ota=True,
         show_mqtt=False,
@@ -349,7 +350,7 @@ def command_run(args, config):
     if args.no_logs:
         return 0
     port = choose_upload_log_host(
-        default=args.upload_port,
+        default=args.device,
         check_default=port,
         show_ota=False,
         show_mqtt=True,
@@ -393,7 +394,7 @@ def command_update_all(args):
     import click
 
     success = {}
-    files = list_yaml_files(args.configuration[0])
+    files = list_yaml_files(args.configuration)
     twidth = 60
 
     def print_bar(middle_text):
@@ -407,7 +408,7 @@ def command_update_all(args):
         print("-" * twidth)
         print()
         rc = run_external_process(
-            "esphome", "--dashboard", f, "run", "--no-logs", "--upload-port", "OTA"
+            "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
         )
         if rc == 0:
             print_bar("[{}] {}".format(color(Fore.BOLD_GREEN, "SUCCESS"), f))
@@ -452,15 +453,17 @@ POST_CONFIG_ACTIONS = {
 
 
 def parse_args(argv):
-    parser = argparse.ArgumentParser(description=f"ESPHome v{const.__version__}")
-    parser.add_argument(
-        "-v", "--verbose", help="Enable verbose esphome logs.", action="store_true"
+    options_parser = argparse.ArgumentParser(add_help=False)
+    options_parser.add_argument(
+        "-v", "--verbose", help="Enable verbose ESPHome logs.", action="store_true"
     )
-    parser.add_argument(
-        "-q", "--quiet", help="Disable all esphome logs.", action="store_true"
+    options_parser.add_argument(
+        "-q", "--quiet", help="Disable all ESPHome logs.", action="store_true"
     )
-    parser.add_argument("--dashboard", help=argparse.SUPPRESS, action="store_true")
-    parser.add_argument(
+    options_parser.add_argument(
+        "--dashboard", help=argparse.SUPPRESS, action="store_true"
+    )
+    options_parser.add_argument(
         "-s",
         "--substitution",
         nargs=2,
@@ -468,17 +471,87 @@ def parse_args(argv):
         help="Add a substitution",
         metavar=("key", "value"),
     )
-    parser.add_argument(
-        "configuration", help="Your YAML configuration file.", nargs="*"
+
+    # Keep backward compatibility with the old command line format of
+    # esphome <config> <command>.
+    #
+    # Unfortunately this can't be done by adding another configuration argument to the
+    # main config parser, as argparse is greedy when parsing arguments, so in regular
+    # usage it'll eat the command as the configuration argument and error out out
+    # because it can't parse the configuration as a command.
+    #
+    # Instead, construct an ad-hoc parser for the old format that doesn't actually
+    # process the arguments, but parses them enough to let us figure out if the old
+    # format is used. In that case, swap the command and configuration in the arguments
+    # and continue on with the normal parser (after raising a deprecation warning).
+    #
+    # Disable argparse's built-in help option and add it manually to prevent this
+    # parser from printing the help messagefor the old format when invoked with -h.
+    compat_parser = argparse.ArgumentParser(parents=[options_parser], add_help=False)
+    compat_parser.add_argument("-h", "--help")
+    compat_parser.add_argument("configuration", nargs="*")
+    compat_parser.add_argument(
+        "command",
+        choices=[
+            "config",
+            "compile",
+            "upload",
+            "logs",
+            "run",
+            "clean-mqtt",
+            "wizard",
+            "mqtt-fingerprint",
+            "version",
+            "clean",
+            "dashboard",
+        ],
     )
 
-    subparsers = parser.add_subparsers(help="Commands", dest="command")
+    # on Python 3.9+ we can simply set exit_on_error=False in the constructor
+    def _raise(x):
+        raise argparse.ArgumentError(None, x)
+
+    compat_parser.error = _raise
+
+    try:
+        result, unparsed = compat_parser.parse_known_args(argv[1:])
+        last_option = len(argv) - len(unparsed) - 1 - len(result.configuration)
+        argv = argv[0:last_option] + [result.command] + result.configuration + unparsed
+        deprecated_argv_suggestion = argv
+    except argparse.ArgumentError:
+        # This is not an old-style command line, so we don't have to do anything.
+        deprecated_argv_suggestion = None
+
+    # And continue on with regular parsing
+    parser = argparse.ArgumentParser(
+        description=f"ESPHome v{const.__version__}", parents=[options_parser]
+    )
+    parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion)
+
+    mqtt_options = argparse.ArgumentParser(add_help=False)
+    mqtt_options.add_argument("--topic", help="Manually set the MQTT topic.")
+    mqtt_options.add_argument("--username", help="Manually set the MQTT username.")
+    mqtt_options.add_argument("--password", help="Manually set the MQTT password.")
+    mqtt_options.add_argument("--client-id", help="Manually set the MQTT client id.")
+
+    subparsers = parser.add_subparsers(
+        help="Command to run:", dest="command", metavar="command"
+    )
     subparsers.required = True
-    subparsers.add_parser("config", help="Validate the configuration and spit it out.")
+
+    parser_config = subparsers.add_parser(
+        "config", help="Validate the configuration and spit it out."
+    )
+    parser_config.add_argument(
+        "configuration", help="Your YAML configuration file(s).", nargs="+"
+    )
 
     parser_compile = subparsers.add_parser(
         "compile", help="Read the configuration and compile a program."
     )
+    parser_compile.add_argument(
+        "configuration", help="Your YAML configuration file(s).", nargs="+"
+    )
     parser_compile.add_argument(
         "--only-generate",
         help="Only generate source code, do not compile.",
@@ -486,106 +559,124 @@ def parse_args(argv):
     )
 
     parser_upload = subparsers.add_parser(
-        "upload", help="Validate the configuration " "and upload the latest binary."
+        "upload", help="Validate the configuration and upload the latest binary."
     )
     parser_upload.add_argument(
-        "--upload-port",
-        help="Manually specify the upload port to use. "
-        "For example /dev/cu.SLAB_USBtoUART.",
+        "configuration", help="Your YAML configuration file(s).", nargs="+"
+    )
+    parser_upload.add_argument(
+        "--device",
+        help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
     )
 
     parser_logs = subparsers.add_parser(
-        "logs", help="Validate the configuration " "and show all MQTT logs."
+        "logs",
+        help="Validate the configuration and show all logs.",
+        parents=[mqtt_options],
     )
-    parser_logs.add_argument("--topic", help="Manually set the topic to subscribe to.")
-    parser_logs.add_argument("--username", help="Manually set the username.")
-    parser_logs.add_argument("--password", help="Manually set the password.")
-    parser_logs.add_argument("--client-id", help="Manually set the client id.")
     parser_logs.add_argument(
-        "--serial-port",
-        help="Manually specify a serial port to use"
-        "For example /dev/cu.SLAB_USBtoUART.",
+        "configuration", help="Your YAML configuration file.", nargs=1
+    )
+    parser_logs.add_argument(
+        "--device",
+        help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
     )
 
     parser_run = subparsers.add_parser(
         "run",
-        help="Validate the configuration, create a binary, "
-        "upload it, and start MQTT logs.",
+        help="Validate the configuration, create a binary, upload it, and start logs.",
+        parents=[mqtt_options],
     )
     parser_run.add_argument(
-        "--upload-port",
-        help="Manually specify the upload port/ip to use. "
-        "For example /dev/cu.SLAB_USBtoUART.",
+        "configuration", help="Your YAML configuration file(s).", nargs="+"
     )
     parser_run.add_argument(
-        "--no-logs", help="Disable starting MQTT logs.", action="store_true"
+        "--device",
+        help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
     )
     parser_run.add_argument(
-        "--topic", help="Manually set the topic to subscribe to for logs."
+        "--no-logs", help="Disable starting logs.", action="store_true"
     )
-    parser_run.add_argument(
-        "--username", help="Manually set the MQTT username for logs."
-    )
-    parser_run.add_argument(
-        "--password", help="Manually set the MQTT password for logs."
-    )
-    parser_run.add_argument("--client-id", help="Manually set the client id for logs.")
 
     parser_clean = subparsers.add_parser(
-        "clean-mqtt", help="Helper to clear an MQTT topic from " "retain messages."
+        "clean-mqtt",
+        help="Helper to clear retained messages from an MQTT topic.",
+        parents=[mqtt_options],
+    )
+    parser_clean.add_argument(
+        "configuration", help="Your YAML configuration file(s).", nargs="+"
     )
-    parser_clean.add_argument("--topic", help="Manually set the topic to subscribe to.")
-    parser_clean.add_argument("--username", help="Manually set the username.")
-    parser_clean.add_argument("--password", help="Manually set the password.")
-    parser_clean.add_argument("--client-id", help="Manually set the client id.")
 
-    subparsers.add_parser(
+    parser_wizard = subparsers.add_parser(
         "wizard",
-        help="A helpful setup wizard that will guide "
-        "you through setting up esphome.",
+        help="A helpful setup wizard that will guide you through setting up ESPHome.",
+    )
+    parser_wizard.add_argument(
+        "configuration",
+        help="Your YAML configuration file.",
     )
 
-    subparsers.add_parser(
+    parser_fingerprint = subparsers.add_parser(
         "mqtt-fingerprint", help="Get the SSL fingerprint from a MQTT broker."
     )
+    parser_fingerprint.add_argument(
+        "configuration", help="Your YAML configuration file(s).", nargs="+"
+    )
 
-    subparsers.add_parser("version", help="Print the esphome version and exit.")
+    subparsers.add_parser("version", help="Print the ESPHome version and exit.")
 
-    subparsers.add_parser("clean", help="Delete all temporary build files.")
+    parser_clean = subparsers.add_parser(
+        "clean", help="Delete all temporary build files."
+    )
+    parser_clean.add_argument(
+        "configuration", help="Your YAML configuration file(s).", nargs="+"
+    )
 
-    dashboard = subparsers.add_parser(
+    parser_dashboard = subparsers.add_parser(
         "dashboard", help="Create a simple web server for a dashboard."
     )
-    dashboard.add_argument(
+    parser_dashboard.add_argument(
+        "configuration",
+        help="Your YAML configuration file directory.",
+    )
+    parser_dashboard.add_argument(
         "--port",
         help="The HTTP port to open connections on. Defaults to 6052.",
         type=int,
         default=6052,
     )
-    dashboard.add_argument(
+    parser_dashboard.add_argument(
         "--username",
-        help="The optional username to require " "for authentication.",
+        help="The optional username to require for authentication.",
         type=str,
         default="",
     )
-    dashboard.add_argument(
+    parser_dashboard.add_argument(
         "--password",
-        help="The optional password to require " "for authentication.",
+        help="The optional password to require for authentication.",
         type=str,
         default="",
     )
-    dashboard.add_argument(
+    parser_dashboard.add_argument(
         "--open-ui", help="Open the dashboard UI in a browser.", action="store_true"
     )
-    dashboard.add_argument("--hassio", help=argparse.SUPPRESS, action="store_true")
-    dashboard.add_argument(
+    parser_dashboard.add_argument(
+        "--hassio", help=argparse.SUPPRESS, action="store_true"
+    )
+    parser_dashboard.add_argument(
         "--socket", help="Make the dashboard serve under a unix socket", type=str
     )
 
-    vscode = subparsers.add_parser("vscode", help=argparse.SUPPRESS)
-    vscode.add_argument("--ace", action="store_true")
+    parser_vscode = subparsers.add_parser("vscode")
+    parser_vscode.add_argument(
+        "configuration", help="Your YAML configuration file.", nargs=1
+    )
+    parser_vscode.add_argument("--ace", action="store_true")
 
-    subparsers.add_parser("update-all", help=argparse.SUPPRESS)
+    parser_update = subparsers.add_parser("update-all")
+    parser_update.add_argument(
+        "configuration", help="Your YAML configuration file directory.", nargs=1
+    )
 
     return parser.parse_args(argv[1:])
 
@@ -595,9 +686,13 @@ def run_esphome(argv):
     CORE.dashboard = args.dashboard
 
     setup_log(args.verbose, args.quiet)
-    if args.command != "version" and not args.configuration:
-        _LOGGER.error("Missing configuration parameter, see esphome --help.")
-        return 1
+    if args.deprecated_argv_suggestion is not None:
+        _LOGGER.warning(
+            "Calling ESPHome with the configuration before the command is deprecated "
+            "and will be removed in the future. "
+        )
+        _LOGGER.warning("Please instead use:")
+        _LOGGER.warning("   esphome %s", " ".join(args.deprecated_argv_suggestion[1:]))
 
     if sys.version_info < (3, 7, 0):
         _LOGGER.error(
@@ -610,7 +705,7 @@ def run_esphome(argv):
         try:
             return PRE_CONFIG_ACTIONS[args.command](args)
         except EsphomeError as e:
-            _LOGGER.error(e)
+            _LOGGER.error(e, exc_info=args.verbose)
             return 1
 
     for conf_path in args.configuration:
@@ -628,7 +723,7 @@ def run_esphome(argv):
         try:
             rc = POST_CONFIG_ACTIONS[args.command](args, config)
         except EsphomeError as e:
-            _LOGGER.error(e)
+            _LOGGER.error(e, exc_info=args.verbose)
             return 1
         if rc != 0:
             return rc
diff --git a/esphome/automation.py b/esphome/automation.py
index eb6cb02532..71c564b906 100644
--- a/esphome/automation.py
+++ b/esphome/automation.py
@@ -10,7 +10,6 @@ from esphome.const import (
     CONF_TYPE_ID,
     CONF_TIME,
 )
-from esphome.core import coroutine
 from esphome.jsonschema import jschema_extractor
 from esphome.util import Registry
 
@@ -142,27 +141,27 @@ NotCondition = cg.esphome_ns.class_("NotCondition", Condition)
 
 
 @register_condition("and", AndCondition, validate_condition_list)
-def and_condition_to_code(config, condition_id, template_arg, args):
-    conditions = yield build_condition_list(config, template_arg, args)
-    yield cg.new_Pvariable(condition_id, template_arg, conditions)
+async def and_condition_to_code(config, condition_id, template_arg, args):
+    conditions = await build_condition_list(config, template_arg, args)
+    return cg.new_Pvariable(condition_id, template_arg, conditions)
 
 
 @register_condition("or", OrCondition, validate_condition_list)
-def or_condition_to_code(config, condition_id, template_arg, args):
-    conditions = yield build_condition_list(config, template_arg, args)
-    yield cg.new_Pvariable(condition_id, template_arg, conditions)
+async def or_condition_to_code(config, condition_id, template_arg, args):
+    conditions = await build_condition_list(config, template_arg, args)
+    return cg.new_Pvariable(condition_id, template_arg, conditions)
 
 
 @register_condition("not", NotCondition, validate_potentially_and_condition)
-def not_condition_to_code(config, condition_id, template_arg, args):
-    condition = yield build_condition(config, template_arg, args)
-    yield cg.new_Pvariable(condition_id, template_arg, condition)
+async def not_condition_to_code(config, condition_id, template_arg, args):
+    condition = await build_condition(config, template_arg, args)
+    return cg.new_Pvariable(condition_id, template_arg, condition)
 
 
-@register_condition("lambda", LambdaCondition, cv.lambda_)
-def lambda_condition_to_code(config, condition_id, template_arg, args):
-    lambda_ = yield cg.process_lambda(config, args, return_type=bool)
-    yield cg.new_Pvariable(condition_id, template_arg, lambda_)
+@register_condition("lambda", LambdaCondition, cv.returning_lambda)
+async def lambda_condition_to_code(config, condition_id, template_arg, args):
+    lambda_ = await cg.process_lambda(config, args, return_type=bool)
+    return cg.new_Pvariable(condition_id, template_arg, lambda_)
 
 
 @register_condition(
@@ -177,26 +176,26 @@ def lambda_condition_to_code(config, condition_id, template_arg, args):
         }
     ).extend(cv.COMPONENT_SCHEMA),
 )
-def for_condition_to_code(config, condition_id, template_arg, args):
-    condition = yield build_condition(
+async def for_condition_to_code(config, condition_id, template_arg, args):
+    condition = await build_condition(
         config[CONF_CONDITION], cg.TemplateArguments(), []
     )
     var = cg.new_Pvariable(condition_id, template_arg, condition)
-    yield cg.register_component(var, config)
-    templ = yield cg.templatable(config[CONF_TIME], args, cg.uint32)
+    await cg.register_component(var, config)
+    templ = await cg.templatable(config[CONF_TIME], args, cg.uint32)
     cg.add(var.set_time(templ))
-    yield var
+    return var
 
 
 @register_action(
     "delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
 )
-def delay_action_to_code(config, action_id, template_arg, args):
+async def delay_action_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_component(var, {})
-    template_ = yield cg.templatable(config, args, cg.uint32)
+    await cg.register_component(var, {})
+    template_ = await cg.templatable(config, args, cg.uint32)
     cg.add(var.set_delay(template_))
-    yield var
+    return var
 
 
 @register_action(
@@ -211,16 +210,16 @@ def delay_action_to_code(config, action_id, template_arg, args):
         cv.has_at_least_one_key(CONF_THEN, CONF_ELSE),
     ),
 )
-def if_action_to_code(config, action_id, template_arg, args):
-    conditions = yield build_condition(config[CONF_CONDITION], template_arg, args)
+async def if_action_to_code(config, action_id, template_arg, args):
+    conditions = await build_condition(config[CONF_CONDITION], template_arg, args)
     var = cg.new_Pvariable(action_id, template_arg, conditions)
     if CONF_THEN in config:
-        actions = yield build_action_list(config[CONF_THEN], template_arg, args)
+        actions = await build_action_list(config[CONF_THEN], template_arg, args)
         cg.add(var.add_then(actions))
     if CONF_ELSE in config:
-        actions = yield build_action_list(config[CONF_ELSE], template_arg, args)
+        actions = await build_action_list(config[CONF_ELSE], template_arg, args)
         cg.add(var.add_else(actions))
-    yield var
+    return var
 
 
 @register_action(
@@ -233,12 +232,12 @@ def if_action_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def while_action_to_code(config, action_id, template_arg, args):
-    conditions = yield build_condition(config[CONF_CONDITION], template_arg, args)
+async def while_action_to_code(config, action_id, template_arg, args):
+    conditions = await build_condition(config[CONF_CONDITION], template_arg, args)
     var = cg.new_Pvariable(action_id, template_arg, conditions)
-    actions = yield build_action_list(config[CONF_THEN], template_arg, args)
+    actions = await build_action_list(config[CONF_THEN], template_arg, args)
     cg.add(var.add_then(actions))
-    yield var
+    return var
 
 
 def validate_wait_until(value):
@@ -253,17 +252,17 @@ def validate_wait_until(value):
 
 
 @register_action("wait_until", WaitUntilAction, validate_wait_until)
-def wait_until_action_to_code(config, action_id, template_arg, args):
-    conditions = yield build_condition(config[CONF_CONDITION], template_arg, args)
+async def wait_until_action_to_code(config, action_id, template_arg, args):
+    conditions = await build_condition(config[CONF_CONDITION], template_arg, args)
     var = cg.new_Pvariable(action_id, template_arg, conditions)
-    yield cg.register_component(var, {})
-    yield var
+    await cg.register_component(var, {})
+    return var
 
 
 @register_action("lambda", LambdaAction, cv.lambda_)
-def lambda_action_to_code(config, action_id, template_arg, args):
-    lambda_ = yield cg.process_lambda(config, args, return_type=cg.void)
-    yield cg.new_Pvariable(action_id, template_arg, lambda_)
+async def lambda_action_to_code(config, action_id, template_arg, args):
+    lambda_ = await cg.process_lambda(config, args, return_type=cg.void)
+    return cg.new_Pvariable(action_id, template_arg, lambda_)
 
 
 @register_action(
@@ -275,54 +274,51 @@ def lambda_action_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def component_update_action_to_code(config, action_id, template_arg, args):
-    comp = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, comp)
+async def component_update_action_to_code(config, action_id, template_arg, args):
+    comp = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, comp)
 
 
-@coroutine
-def build_action(full_config, template_arg, args):
+async def build_action(full_config, template_arg, args):
     registry_entry, config = cg.extract_registry_entry_config(
         ACTION_REGISTRY, full_config
     )
     action_id = full_config[CONF_TYPE_ID]
     builder = registry_entry.coroutine_fun
-    yield builder(config, action_id, template_arg, args)
+    ret = await builder(config, action_id, template_arg, args)
+    return ret
 
 
-@coroutine
-def build_action_list(config, templ, arg_type):
+async def build_action_list(config, templ, arg_type):
     actions = []
     for conf in config:
-        action = yield build_action(conf, templ, arg_type)
+        action = await build_action(conf, templ, arg_type)
         actions.append(action)
-    yield actions
+    return actions
 
 
-@coroutine
-def build_condition(full_config, template_arg, args):
+async def build_condition(full_config, template_arg, args):
     registry_entry, config = cg.extract_registry_entry_config(
         CONDITION_REGISTRY, full_config
     )
     action_id = full_config[CONF_TYPE_ID]
     builder = registry_entry.coroutine_fun
-    yield builder(config, action_id, template_arg, args)
+    ret = await builder(config, action_id, template_arg, args)
+    return ret
 
 
-@coroutine
-def build_condition_list(config, templ, args):
+async def build_condition_list(config, templ, args):
     conditions = []
     for conf in config:
-        condition = yield build_condition(conf, templ, args)
+        condition = await build_condition(conf, templ, args)
         conditions.append(condition)
-    yield conditions
+    return conditions
 
 
-@coroutine
-def build_automation(trigger, args, config):
+async def build_automation(trigger, args, config):
     arg_types = [arg[0] for arg in args]
     templ = cg.TemplateArguments(*arg_types)
     obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, trigger)
-    actions = yield build_action_list(config[CONF_THEN], templ, args)
+    actions = await build_action_list(config[CONF_THEN], templ, args)
     cg.add(obj.add_actions(actions))
-    yield obj
+    return obj
diff --git a/esphome/codegen.py b/esphome/codegen.py
index 90f59f75bc..8361faeb81 100644
--- a/esphome/codegen.py
+++ b/esphome/codegen.py
@@ -19,6 +19,7 @@ from esphome.cpp_generator import (  # noqa
     Statement,
     LineComment,
     progmem_array,
+    static_const_array,
     statement,
     variable,
     new_variable,
diff --git a/esphome/components/a4988/stepper.py b/esphome/components/a4988/stepper.py
index 1de3562ff7..7f53856c7b 100644
--- a/esphome/components/a4988/stepper.py
+++ b/esphome/components/a4988/stepper.py
@@ -18,16 +18,16 @@ CONFIG_SCHEMA = stepper.STEPPER_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield stepper.register_stepper(var, config)
+    await cg.register_component(var, config)
+    await stepper.register_stepper(var, config)
 
-    step_pin = yield cg.gpio_pin_expression(config[CONF_STEP_PIN])
+    step_pin = await cg.gpio_pin_expression(config[CONF_STEP_PIN])
     cg.add(var.set_step_pin(step_pin))
-    dir_pin = yield cg.gpio_pin_expression(config[CONF_DIR_PIN])
+    dir_pin = await cg.gpio_pin_expression(config[CONF_DIR_PIN])
     cg.add(var.set_dir_pin(dir_pin))
 
     if CONF_SLEEP_PIN in config:
-        sleep_pin = yield cg.gpio_pin_expression(config[CONF_SLEEP_PIN])
+        sleep_pin = await cg.gpio_pin_expression(config[CONF_SLEEP_PIN])
         cg.add(var.set_sleep_pin(sleep_pin))
diff --git a/esphome/components/ac_dimmer/output.py b/esphome/components/ac_dimmer/output.py
index 2f06a0b6fc..2c37d325eb 100644
--- a/esphome/components/ac_dimmer/output.py
+++ b/esphome/components/ac_dimmer/output.py
@@ -32,18 +32,18 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
     # override default min power to 10%
     if CONF_MIN_POWER not in config:
         config[CONF_MIN_POWER] = 0.1
-    yield output.register_output(var, config)
+    await output.register_output(var, config)
 
-    pin = yield cg.gpio_pin_expression(config[CONF_GATE_PIN])
+    pin = await cg.gpio_pin_expression(config[CONF_GATE_PIN])
     cg.add(var.set_gate_pin(pin))
-    pin = yield cg.gpio_pin_expression(config[CONF_ZERO_CROSS_PIN])
+    pin = await cg.gpio_pin_expression(config[CONF_ZERO_CROSS_PIN])
     cg.add(var.set_zero_cross_pin(pin))
     cg.add(var.set_init_with_half_cycle(config[CONF_INIT_WITH_HALF_CYCLE]))
     cg.add(var.set_method(config[CONF_METHOD]))
diff --git a/esphome/components/adalight/__init__.py b/esphome/components/adalight/__init__.py
index 169a137165..919ffecbea 100644
--- a/esphome/components/adalight/__init__.py
+++ b/esphome/components/adalight/__init__.py
@@ -21,8 +21,7 @@ CONFIG_SCHEMA = cv.Schema({})
     "Adalight",
     {cv.GenerateID(CONF_UART_ID): cv.use_id(uart.UARTComponent)},
 )
-def adalight_light_effect_to_code(config, effect_id):
+async def adalight_light_effect_to_code(config, effect_id):
     effect = cg.new_Pvariable(effect_id, config[CONF_NAME])
-    yield uart.register_uart_device(effect, config)
-
-    yield effect
+    await uart.register_uart_device(effect, config)
+    return effect
diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py
index 2e36c6179a..90561679b7 100644
--- a/esphome/components/adc/sensor.py
+++ b/esphome/components/adc/sensor.py
@@ -8,6 +8,7 @@ from esphome.const import (
     CONF_PIN,
     DEVICE_CLASS_VOLTAGE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_VOLT,
 )
 
@@ -35,7 +36,9 @@ ADCSensor = adc_ns.class_(
 )
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE)
+    sensor.sensor_schema(
+        UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(ADCSensor),
@@ -49,10 +52,10 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
 
     if config[CONF_PIN] == "VCC":
         cg.add_define("USE_ADC_SENSOR_VCC")
diff --git a/esphome/components/addressable_light/display.py b/esphome/components/addressable_light/display.py
index e5d3ca3034..0684bf8dfc 100644
--- a/esphome/components/addressable_light/display.py
+++ b/esphome/components/addressable_light/display.py
@@ -38,18 +38,18 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    wrapped_light = yield cg.get_variable(config[CONF_ADDRESSABLE_LIGHT_ID])
+    wrapped_light = await cg.get_variable(config[CONF_ADDRESSABLE_LIGHT_ID])
     cg.add(var.set_width(config[CONF_WIDTH]))
     cg.add(var.set_height(config[CONF_HEIGHT]))
     cg.add(var.set_light(wrapped_light))
 
-    yield cg.register_component(var, config)
-    yield display.register_display(var, config)
+    await cg.register_component(var, config)
+    await display.register_display(var, config)
 
     if CONF_PIXEL_MAPPER in config:
-        pixel_mapper_template_ = yield cg.process_lambda(
+        pixel_mapper_template_ = await cg.process_lambda(
             config[CONF_PIXEL_MAPPER],
             [(int, "x"), (int, "y")],
             return_type=cg.int_,
@@ -57,7 +57,7 @@ def to_code(config):
         cg.add(var.set_pixel_mapper(pixel_mapper_template_))
 
     if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
+        lambda_ = await cg.process_lambda(
             config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void
         )
         cg.add(var.set_writer(lambda_))
diff --git a/esphome/components/ade7953/sensor.py b/esphome/components/ade7953/sensor.py
index 6bc9917806..90873f1a5e 100644
--- a/esphome/components/ade7953/sensor.py
+++ b/esphome/components/ade7953/sensor.py
@@ -9,6 +9,7 @@ from esphome.const import (
     DEVICE_CLASS_POWER,
     DEVICE_CLASS_VOLTAGE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_VOLT,
     UNIT_AMPERE,
     UNIT_WATT,
@@ -31,19 +32,27 @@ CONFIG_SCHEMA = (
             cv.GenerateID(): cv.declare_id(ADE7953),
             cv.Optional(CONF_IRQ_PIN): pins.input_pin,
             cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
-                UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE
+                UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT
             ),
             cv.Optional(CONF_CURRENT_A): sensor.sensor_schema(
-                UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT
+                UNIT_AMPERE,
+                ICON_EMPTY,
+                2,
+                DEVICE_CLASS_CURRENT,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_CURRENT_B): sensor.sensor_schema(
-                UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT
+                UNIT_AMPERE,
+                ICON_EMPTY,
+                2,
+                DEVICE_CLASS_CURRENT,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_ACTIVE_POWER_A): sensor.sensor_schema(
-                UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER
+                UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
             ),
             cv.Optional(CONF_ACTIVE_POWER_B): sensor.sensor_schema(
-                UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER
+                UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
             ),
         }
     )
@@ -52,10 +61,10 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     if CONF_IRQ_PIN in config:
         cg.add(var.set_irq_pin(config[CONF_IRQ_PIN]))
@@ -70,5 +79,5 @@ def to_code(config):
         if key not in config:
             continue
         conf = config[key]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(getattr(var, f"set_{key}_sensor")(sens))
diff --git a/esphome/components/ads1115/__init__.py b/esphome/components/ads1115/__init__.py
index 32ab17db31..e8861a2f67 100644
--- a/esphome/components/ads1115/__init__.py
+++ b/esphome/components/ads1115/__init__.py
@@ -23,9 +23,9 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     cg.add(var.set_continuous_mode(config[CONF_CONTINUOUS_MODE]))
diff --git a/esphome/components/ads1115/sensor.py b/esphome/components/ads1115/sensor.py
index 6eb39bdd1a..c521769279 100644
--- a/esphome/components/ads1115/sensor.py
+++ b/esphome/components/ads1115/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_MULTIPLEXER,
     DEVICE_CLASS_VOLTAGE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_VOLT,
     CONF_ID,
 )
@@ -51,7 +52,9 @@ ADS1115Sensor = ads1115_ns.class_(
 
 CONF_ADS1115_ID = "ads1115_id"
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE)
+    sensor.sensor_schema(
+        UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(ADS1115Sensor),
@@ -64,11 +67,11 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
-    paren = yield cg.get_variable(config[CONF_ADS1115_ID])
+async def to_code(config):
+    paren = await cg.get_variable(config[CONF_ADS1115_ID])
     var = cg.new_Pvariable(config[CONF_ID], paren)
-    yield sensor.register_sensor(var, config)
-    yield cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
 
     cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER]))
     cg.add(var.set_gain(config[CONF_GAIN]))
diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp
index 6951254e0d..d3a77fee5f 100644
--- a/esphome/components/aht10/aht10.cpp
+++ b/esphome/components/aht10/aht10.cpp
@@ -60,6 +60,7 @@ void AHT10Component::update() {
     delay = AHT10_HUMIDITY_DELAY;
   for (int i = 0; i < AHT10_ATTEMPS; ++i) {
     ESP_LOGVV(TAG, "Attemps %u at %6ld", i, millis());
+    delay_microseconds_accurate(4);
     if (!this->read_bytes(0, data, 6, delay)) {
       ESP_LOGD(TAG, "Communication with AHT10 failed, waiting...");
     } else if ((data[0] & 0x80) == 0x80) {  // Bit[7] = 0b1, device is busy
diff --git a/esphome/components/aht10/sensor.py b/esphome/components/aht10/sensor.py
index e335934a8e..35168be54a 100644
--- a/esphome/components/aht10/sensor.py
+++ b/esphome/components/aht10/sensor.py
@@ -8,6 +8,7 @@ from esphome.const import (
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
 )
@@ -22,10 +23,18 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(AHT10Component),
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 2, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                2,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 2, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                2,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -34,15 +43,15 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature_sensor(sens))
 
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity_sensor(sens))
diff --git a/esphome/components/am2320/sensor.py b/esphome/components/am2320/sensor.py
index 6d1cc42581..5d6cb9eded 100644
--- a/esphome/components/am2320/sensor.py
+++ b/esphome/components/am2320/sensor.py
@@ -7,6 +7,7 @@ from esphome.const import (
     CONF_TEMPERATURE,
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     ICON_EMPTY,
     UNIT_PERCENT,
@@ -24,10 +25,18 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(AM2320Component),
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -36,15 +45,15 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature_sensor(sens))
 
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity_sensor(sens))
diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py
index fbcb2b4c1f..3ae3aa94f9 100644
--- a/esphome/components/animation/__init__.py
+++ b/esphome/components/animation/__init__.py
@@ -34,7 +34,7 @@ CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA)
 CODEOWNERS = ["@syndlex"]
 
 
-def to_code(config):
+async def to_code(config):
     from PIL import Image
 
     path = CORE.relative_config_path(config[CONF_FILE])
diff --git a/esphome/components/apds9960/__init__.py b/esphome/components/apds9960/__init__.py
index 9346b5ece1..8de83251b7 100644
--- a/esphome/components/apds9960/__init__.py
+++ b/esphome/components/apds9960/__init__.py
@@ -23,7 +23,7 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/apds9960/binary_sensor.py b/esphome/components/apds9960/binary_sensor.py
index 0433061385..4a5c69f6a9 100644
--- a/esphome/components/apds9960/binary_sensor.py
+++ b/esphome/components/apds9960/binary_sensor.py
@@ -24,8 +24,8 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 )
 
 
-def to_code(config):
-    hub = yield cg.get_variable(config[CONF_APDS9960_ID])
-    var = yield binary_sensor.new_binary_sensor(config)
+async def to_code(config):
+    hub = await cg.get_variable(config[CONF_APDS9960_ID])
+    var = await binary_sensor.new_binary_sensor(config)
     func = getattr(hub, DIRECTIONS[config[CONF_DIRECTION]])
     cg.add(func(var))
diff --git a/esphome/components/apds9960/sensor.py b/esphome/components/apds9960/sensor.py
index a1ebd9b5c3..cb0c52735d 100644
--- a/esphome/components/apds9960/sensor.py
+++ b/esphome/components/apds9960/sensor.py
@@ -1,7 +1,13 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import sensor
-from esphome.const import CONF_TYPE, DEVICE_CLASS_EMPTY, UNIT_PERCENT, ICON_LIGHTBULB
+from esphome.const import (
+    CONF_TYPE,
+    DEVICE_CLASS_EMPTY,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_PERCENT,
+    ICON_LIGHTBULB,
+)
 from . import APDS9960, CONF_APDS9960_ID
 
 DEPENDENCIES = ["apds9960"]
@@ -15,7 +21,7 @@ TYPES = {
 }
 
 CONFIG_SCHEMA = sensor.sensor_schema(
-    UNIT_PERCENT, ICON_LIGHTBULB, 1, DEVICE_CLASS_EMPTY
+    UNIT_PERCENT, ICON_LIGHTBULB, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
 ).extend(
     {
         cv.Required(CONF_TYPE): cv.one_of(*TYPES, upper=True),
@@ -24,8 +30,8 @@ CONFIG_SCHEMA = sensor.sensor_schema(
 )
 
 
-def to_code(config):
-    hub = yield cg.get_variable(config[CONF_APDS9960_ID])
-    var = yield sensor.new_sensor(config)
+async def to_code(config):
+    hub = await cg.get_variable(config[CONF_APDS9960_ID])
+    var = await sensor.new_sensor(config)
     func = getattr(hub, TYPES[config[CONF_TYPE]])
     cg.add(func(var))
diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py
index d12c01e614..559f8f649c 100644
--- a/esphome/components/api/__init__.py
+++ b/esphome/components/api/__init__.py
@@ -68,9 +68,9 @@ CONFIG_SCHEMA = cv.Schema(
 
 
 @coroutine_with_priority(40.0)
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
     cg.add(var.set_port(config[CONF_PORT]))
     cg.add(var.set_password(config[CONF_PASSWORD]))
@@ -90,7 +90,7 @@ def to_code(config):
             conf[CONF_TRIGGER_ID], templ, conf[CONF_SERVICE], service_arg_names
         )
         cg.add(var.register_user_service(trigger))
-        yield automation.build_automation(trigger, func_args, conf)
+        await automation.build_automation(trigger, func_args, conf)
 
     cg.add_define("USE_API")
     cg.add_global(api_ns.using)
@@ -116,21 +116,21 @@ HOMEASSISTANT_SERVICE_ACTION_SCHEMA = cv.Schema(
     HomeAssistantServiceCallAction,
     HOMEASSISTANT_SERVICE_ACTION_SCHEMA,
 )
-def homeassistant_service_to_code(config, action_id, template_arg, args):
-    serv = yield cg.get_variable(config[CONF_ID])
+async def homeassistant_service_to_code(config, action_id, template_arg, args):
+    serv = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, serv, False)
-    templ = yield cg.templatable(config[CONF_SERVICE], args, None)
+    templ = await cg.templatable(config[CONF_SERVICE], args, None)
     cg.add(var.set_service(templ))
     for key, value in config[CONF_DATA].items():
-        templ = yield cg.templatable(value, args, None)
+        templ = await cg.templatable(value, args, None)
         cg.add(var.add_data(key, templ))
     for key, value in config[CONF_DATA_TEMPLATE].items():
-        templ = yield cg.templatable(value, args, None)
+        templ = await cg.templatable(value, args, None)
         cg.add(var.add_data_template(key, templ))
     for key, value in config[CONF_VARIABLES].items():
-        templ = yield cg.templatable(value, args, None)
+        templ = await cg.templatable(value, args, None)
         cg.add(var.add_variable(key, templ))
-    yield var
+    return var
 
 
 def validate_homeassistant_event(value):
@@ -159,21 +159,21 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
     HomeAssistantServiceCallAction,
     HOMEASSISTANT_EVENT_ACTION_SCHEMA,
 )
-def homeassistant_event_to_code(config, action_id, template_arg, args):
-    serv = yield cg.get_variable(config[CONF_ID])
+async def homeassistant_event_to_code(config, action_id, template_arg, args):
+    serv = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, serv, True)
-    templ = yield cg.templatable(config[CONF_EVENT], args, None)
+    templ = await cg.templatable(config[CONF_EVENT], args, None)
     cg.add(var.set_service(templ))
     for key, value in config[CONF_DATA].items():
-        templ = yield cg.templatable(value, args, None)
+        templ = await cg.templatable(value, args, None)
         cg.add(var.add_data(key, templ))
     for key, value in config[CONF_DATA_TEMPLATE].items():
-        templ = yield cg.templatable(value, args, None)
+        templ = await cg.templatable(value, args, None)
         cg.add(var.add_data_template(key, templ))
     for key, value in config[CONF_VARIABLES].items():
-        templ = yield cg.templatable(value, args, None)
+        templ = await cg.templatable(value, args, None)
         cg.add(var.add_variable(key, templ))
-    yield var
+    return var
 
 
 HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
@@ -190,15 +190,15 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
     HomeAssistantServiceCallAction,
     HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
 )
-def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args):
-    serv = yield cg.get_variable(config[CONF_ID])
+async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args):
+    serv = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, serv, True)
     cg.add(var.set_service("esphome.tag_scanned"))
-    templ = yield cg.templatable(config[CONF_TAG], args, cg.std_string)
+    templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
     cg.add(var.add_data("tag_id", templ))
-    yield var
+    return var
 
 
 @automation.register_condition("api.connected", APIConnectedCondition, {})
-def api_connected_to_code(config, condition_id, template_arg, args):
-    yield cg.new_Pvariable(condition_id, template_arg)
+async def api_connected_to_code(config, condition_id, template_arg, args):
+    return cg.new_Pvariable(condition_id, template_arg)
diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto
index ede2cc6205..bdb94b3d9b 100644
--- a/esphome/components/api/api.proto
+++ b/esphome/components/api/api.proto
@@ -176,6 +176,10 @@ message DeviceInfoResponse {
   string model = 6;
 
   bool has_deep_sleep = 7;
+
+  // The esphome project details if set
+  string project_name = 8;
+  string project_version = 9;
 }
 
 message ListEntitiesRequest {
@@ -409,6 +413,11 @@ message LightCommandRequest {
 }
 
 // ==================== SENSOR ====================
+enum SensorStateClass {
+  STATE_CLASS_NONE = 0;
+  STATE_CLASS_MEASUREMENT = 1;
+}
+
 message ListEntitiesSensorResponse {
   option (id) = 16;
   option (source) = SOURCE_SERVER;
@@ -424,6 +433,7 @@ message ListEntitiesSensorResponse {
   int32 accuracy_decimals = 7;
   bool force_update = 8;
   string device_class = 9;
+  SensorStateClass state_class = 10;
 }
 message SensorStateResponse {
   option (id) = 25;
@@ -561,6 +571,7 @@ message SubscribeHomeAssistantStateResponse {
   option (id) = 39;
   option (source) = SOURCE_SERVER;
   string entity_id = 1;
+  string attribute = 2;
 }
 
 message HomeAssistantStateResponse {
@@ -570,6 +581,7 @@ message HomeAssistantStateResponse {
 
   string entity_id = 1;
   string state = 2;
+  string attribute = 3;
 }
 
 // ==================== IMPORT TIME ====================
@@ -664,11 +676,12 @@ message CameraImageRequest {
 // ==================== CLIMATE ====================
 enum ClimateMode {
   CLIMATE_MODE_OFF = 0;
-  CLIMATE_MODE_AUTO = 1;
+  CLIMATE_MODE_HEAT_COOL = 1;
   CLIMATE_MODE_COOL = 2;
   CLIMATE_MODE_HEAT = 3;
   CLIMATE_MODE_FAN_ONLY = 4;
   CLIMATE_MODE_DRY = 5;
+  CLIMATE_MODE_AUTO = 6;
 }
 enum ClimateFanMode {
   CLIMATE_FAN_ON = 0;
@@ -696,6 +709,15 @@ enum ClimateAction {
   CLIMATE_ACTION_DRYING = 5;
   CLIMATE_ACTION_FAN = 6;
 }
+enum ClimatePreset {
+  CLIMATE_PRESET_ECO = 0;
+  CLIMATE_PRESET_AWAY = 1;
+  CLIMATE_PRESET_BOOST = 2;
+  CLIMATE_PRESET_COMFORT = 3;
+  CLIMATE_PRESET_HOME = 4;
+  CLIMATE_PRESET_SLEEP = 5;
+  CLIMATE_PRESET_ACTIVITY = 6;
+}
 message ListEntitiesClimateResponse {
   option (id) = 46;
   option (source) = SOURCE_SERVER;
@@ -716,6 +738,9 @@ message ListEntitiesClimateResponse {
   bool supports_action = 12;
   repeated ClimateFanMode supported_fan_modes = 13;
   repeated ClimateSwingMode supported_swing_modes = 14;
+  repeated string supported_custom_fan_modes = 15;
+  repeated ClimatePreset supported_presets = 16;
+  repeated string supported_custom_presets = 17;
 }
 message ClimateStateResponse {
   option (id) = 47;
@@ -733,6 +758,9 @@ message ClimateStateResponse {
   ClimateAction action = 8;
   ClimateFanMode fan_mode = 9;
   ClimateSwingMode swing_mode = 10;
+  string custom_fan_mode = 11;
+  ClimatePreset preset = 12;
+  string custom_preset = 13;
 }
 message ClimateCommandRequest {
   option (id) = 48;
@@ -755,4 +783,10 @@ message ClimateCommandRequest {
   ClimateFanMode fan_mode = 13;
   bool has_swing_mode = 14;
   ClimateSwingMode swing_mode = 15;
+  bool has_custom_fan_mode = 16;
+  string custom_fan_mode = 17;
+  bool has_preset = 18;
+  ClimatePreset preset = 19;
+  bool has_custom_preset = 20;
+  string custom_preset = 21;
 }
diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp
index 8098c93781..175c3c4487 100644
--- a/esphome/components/api/api_connection.cpp
+++ b/esphome/components/api/api_connection.cpp
@@ -395,6 +395,8 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) {
   msg.accuracy_decimals = sensor->get_accuracy_decimals();
   msg.force_update = sensor->get_force_update();
   msg.device_class = sensor->get_device_class();
+  msg.state_class = static_cast<enums::SensorStateClass>(sensor->state_class);
+
   return this->send_list_entities_sensor_response(msg);
 }
 #endif
@@ -475,8 +477,14 @@ bool APIConnection::send_climate_state(climate::Climate *climate) {
   }
   if (traits.get_supports_away())
     resp.away = climate->away;
-  if (traits.get_supports_fan_modes())
-    resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode);
+  if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
+    resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
+  if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value())
+    resp.custom_fan_mode = climate->custom_fan_mode.value();
+  if (traits.get_supports_presets() && climate->preset.has_value())
+    resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
+  if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value())
+    resp.custom_preset = climate->custom_preset.value();
   if (traits.get_supports_swing_modes())
     resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
   return this->send_climate_state_response(resp);
@@ -490,8 +498,9 @@ bool APIConnection::send_climate_info(climate::Climate *climate) {
   msg.unique_id = get_default_unique_id("climate", climate);
   msg.supports_current_temperature = traits.get_supports_current_temperature();
   msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
-  for (auto mode : {climate::CLIMATE_MODE_AUTO, climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL,
-                    climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_DRY, climate::CLIMATE_MODE_FAN_ONLY}) {
+  for (auto mode :
+       {climate::CLIMATE_MODE_AUTO, climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT,
+        climate::CLIMATE_MODE_DRY, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_HEAT_COOL}) {
     if (traits.supports_mode(mode))
       msg.supported_modes.push_back(static_cast<enums::ClimateMode>(mode));
   }
@@ -506,6 +515,18 @@ bool APIConnection::send_climate_info(climate::Climate *climate) {
     if (traits.supports_fan_mode(fan_mode))
       msg.supported_fan_modes.push_back(static_cast<enums::ClimateFanMode>(fan_mode));
   }
+  for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) {
+    msg.supported_custom_fan_modes.push_back(custom_fan_mode);
+  }
+  for (auto preset : {climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_AWAY, climate::CLIMATE_PRESET_BOOST,
+                      climate::CLIMATE_PRESET_COMFORT, climate::CLIMATE_PRESET_HOME, climate::CLIMATE_PRESET_SLEEP,
+                      climate::CLIMATE_PRESET_ACTIVITY}) {
+    if (traits.supports_preset(preset))
+      msg.supported_presets.push_back(static_cast<enums::ClimatePreset>(preset));
+  }
+  for (auto const &custom_preset : traits.get_supported_custom_presets()) {
+    msg.supported_custom_presets.push_back(custom_preset);
+  }
   for (auto swing_mode : {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL,
                           climate::CLIMATE_SWING_HORIZONTAL}) {
     if (traits.supports_swing_mode(swing_mode))
@@ -531,6 +552,12 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
     call.set_away(msg.away);
   if (msg.has_fan_mode)
     call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
+  if (msg.has_custom_fan_mode)
+    call.set_fan_mode(msg.custom_fan_mode);
+  if (msg.has_preset)
+    call.set_preset(static_cast<climate::ClimatePreset>(msg.preset));
+  if (msg.has_custom_preset)
+    call.set_preset(msg.custom_preset);
   if (msg.has_swing_mode)
     call.set_swing_mode(static_cast<climate::ClimateSwingMode>(msg.swing_mode));
   call.perform();
@@ -637,13 +664,18 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
 #endif
 #ifdef USE_DEEP_SLEEP
   resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;
+#endif
+#ifdef ESPHOME_PROJECT_NAME
+  resp.project_name = ESPHOME_PROJECT_NAME;
+  resp.project_version = ESPHOME_PROJECT_VERSION;
 #endif
   return resp;
 }
 void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
   for (auto &it : this->parent_->get_state_subs())
-    if (it.entity_id == msg.entity_id)
+    if (it.entity_id == msg.entity_id && it.attribute.value() == msg.attribute) {
       it.callback(msg.state);
+    }
 }
 void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
   bool found = false;
@@ -660,6 +692,7 @@ void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistant
   for (auto &it : this->parent_->get_state_subs()) {
     SubscribeHomeAssistantStateResponse resp;
     resp.entity_id = it.entity_id;
+    resp.attribute = it.attribute.value();
     if (!this->send_subscribe_home_assistant_state_response(resp)) {
       this->on_fatal_error();
       return;
diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp
index 23538b77bf..a9e9d64bc1 100644
--- a/esphome/components/api/api_pb2.cpp
+++ b/esphome/components/api/api_pb2.cpp
@@ -62,6 +62,16 @@ template<> const char *proto_enum_to_string<enums::FanDirection>(enums::FanDirec
       return "UNKNOWN";
   }
 }
+template<> const char *proto_enum_to_string<enums::SensorStateClass>(enums::SensorStateClass value) {
+  switch (value) {
+    case enums::STATE_CLASS_NONE:
+      return "STATE_CLASS_NONE";
+    case enums::STATE_CLASS_MEASUREMENT:
+      return "STATE_CLASS_MEASUREMENT";
+    default:
+      return "UNKNOWN";
+  }
+}
 template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel value) {
   switch (value) {
     case enums::LOG_LEVEL_NONE:
@@ -108,8 +118,8 @@ template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMo
   switch (value) {
     case enums::CLIMATE_MODE_OFF:
       return "CLIMATE_MODE_OFF";
-    case enums::CLIMATE_MODE_AUTO:
-      return "CLIMATE_MODE_AUTO";
+    case enums::CLIMATE_MODE_HEAT_COOL:
+      return "CLIMATE_MODE_HEAT_COOL";
     case enums::CLIMATE_MODE_COOL:
       return "CLIMATE_MODE_COOL";
     case enums::CLIMATE_MODE_HEAT:
@@ -118,6 +128,8 @@ template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMo
       return "CLIMATE_MODE_FAN_ONLY";
     case enums::CLIMATE_MODE_DRY:
       return "CLIMATE_MODE_DRY";
+    case enums::CLIMATE_MODE_AUTO:
+      return "CLIMATE_MODE_AUTO";
     default:
       return "UNKNOWN";
   }
@@ -178,6 +190,26 @@ template<> const char *proto_enum_to_string<enums::ClimateAction>(enums::Climate
       return "UNKNOWN";
   }
 }
+template<> const char *proto_enum_to_string<enums::ClimatePreset>(enums::ClimatePreset value) {
+  switch (value) {
+    case enums::CLIMATE_PRESET_ECO:
+      return "CLIMATE_PRESET_ECO";
+    case enums::CLIMATE_PRESET_AWAY:
+      return "CLIMATE_PRESET_AWAY";
+    case enums::CLIMATE_PRESET_BOOST:
+      return "CLIMATE_PRESET_BOOST";
+    case enums::CLIMATE_PRESET_COMFORT:
+      return "CLIMATE_PRESET_COMFORT";
+    case enums::CLIMATE_PRESET_HOME:
+      return "CLIMATE_PRESET_HOME";
+    case enums::CLIMATE_PRESET_SLEEP:
+      return "CLIMATE_PRESET_SLEEP";
+    case enums::CLIMATE_PRESET_ACTIVITY:
+      return "CLIMATE_PRESET_ACTIVITY";
+    default:
+      return "UNKNOWN";
+  }
+}
 bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
   switch (field_id) {
     case 1: {
@@ -328,6 +360,14 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v
       this->model = value.as_string();
       return true;
     }
+    case 8: {
+      this->project_name = value.as_string();
+      return true;
+    }
+    case 9: {
+      this->project_version = value.as_string();
+      return true;
+    }
     default:
       return false;
   }
@@ -340,6 +380,8 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_string(5, this->compilation_time);
   buffer.encode_string(6, this->model);
   buffer.encode_bool(7, this->has_deep_sleep);
+  buffer.encode_string(8, this->project_name);
+  buffer.encode_string(9, this->project_version);
 }
 void DeviceInfoResponse::dump_to(std::string &out) const {
   char buffer[64];
@@ -371,6 +413,14 @@ void DeviceInfoResponse::dump_to(std::string &out) const {
   out.append("  has_deep_sleep: ");
   out.append(YESNO(this->has_deep_sleep));
   out.append("\n");
+
+  out.append("  project_name: ");
+  out.append("'").append(this->project_name).append("'");
+  out.append("\n");
+
+  out.append("  project_version: ");
+  out.append("'").append(this->project_version).append("'");
+  out.append("\n");
   out.append("}");
 }
 void ListEntitiesRequest::encode(ProtoWriteBuffer buffer) const {}
@@ -1507,6 +1557,10 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va
       this->force_update = value.as_bool();
       return true;
     }
+    case 10: {
+      this->state_class = value.as_enum<enums::SensorStateClass>();
+      return true;
+    }
     default:
       return false;
   }
@@ -1561,6 +1615,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_int32(7, this->accuracy_decimals);
   buffer.encode_bool(8, this->force_update);
   buffer.encode_string(9, this->device_class);
+  buffer.encode_enum<enums::SensorStateClass>(10, this->state_class);
 }
 void ListEntitiesSensorResponse::dump_to(std::string &out) const {
   char buffer[64];
@@ -1602,6 +1657,10 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const {
   out.append("  device_class: ");
   out.append("'").append(this->device_class).append("'");
   out.append("\n");
+
+  out.append("  state_class: ");
+  out.append(proto_enum_to_string<enums::SensorStateClass>(this->state_class));
+  out.append("\n");
   out.append("}");
 }
 bool SensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
@@ -2123,12 +2182,17 @@ bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, Proto
       this->entity_id = value.as_string();
       return true;
     }
+    case 2: {
+      this->attribute = value.as_string();
+      return true;
+    }
     default:
       return false;
   }
 }
 void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_string(1, this->entity_id);
+  buffer.encode_string(2, this->attribute);
 }
 void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const {
   char buffer[64];
@@ -2136,6 +2200,10 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const {
   out.append("  entity_id: ");
   out.append("'").append(this->entity_id).append("'");
   out.append("\n");
+
+  out.append("  attribute: ");
+  out.append("'").append(this->attribute).append("'");
+  out.append("\n");
   out.append("}");
 }
 bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
@@ -2148,6 +2216,10 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel
       this->state = value.as_string();
       return true;
     }
+    case 3: {
+      this->attribute = value.as_string();
+      return true;
+    }
     default:
       return false;
   }
@@ -2155,6 +2227,7 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel
 void HomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_string(1, this->entity_id);
   buffer.encode_string(2, this->state);
+  buffer.encode_string(3, this->attribute);
 }
 void HomeAssistantStateResponse::dump_to(std::string &out) const {
   char buffer[64];
@@ -2166,6 +2239,10 @@ void HomeAssistantStateResponse::dump_to(std::string &out) const {
   out.append("  state: ");
   out.append("'").append(this->state).append("'");
   out.append("\n");
+
+  out.append("  attribute: ");
+  out.append("'").append(this->attribute).append("'");
+  out.append("\n");
   out.append("}");
 }
 void GetTimeRequest::encode(ProtoWriteBuffer buffer) const {}
@@ -2610,6 +2687,10 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v
       this->supported_swing_modes.push_back(value.as_enum<enums::ClimateSwingMode>());
       return true;
     }
+    case 16: {
+      this->supported_presets.push_back(value.as_enum<enums::ClimatePreset>());
+      return true;
+    }
     default:
       return false;
   }
@@ -2628,6 +2709,14 @@ bool ListEntitiesClimateResponse::decode_length(uint32_t field_id, ProtoLengthDe
       this->unique_id = value.as_string();
       return true;
     }
+    case 15: {
+      this->supported_custom_fan_modes.push_back(value.as_string());
+      return true;
+    }
+    case 17: {
+      this->supported_custom_presets.push_back(value.as_string());
+      return true;
+    }
     default:
       return false;
   }
@@ -2675,6 +2764,15 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
   for (auto &it : this->supported_swing_modes) {
     buffer.encode_enum<enums::ClimateSwingMode>(14, it, true);
   }
+  for (auto &it : this->supported_custom_fan_modes) {
+    buffer.encode_string(15, it, true);
+  }
+  for (auto &it : this->supported_presets) {
+    buffer.encode_enum<enums::ClimatePreset>(16, it, true);
+  }
+  for (auto &it : this->supported_custom_presets) {
+    buffer.encode_string(17, it, true);
+  }
 }
 void ListEntitiesClimateResponse::dump_to(std::string &out) const {
   char buffer[64];
@@ -2744,6 +2842,24 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
     out.append(proto_enum_to_string<enums::ClimateSwingMode>(it));
     out.append("\n");
   }
+
+  for (const auto &it : this->supported_custom_fan_modes) {
+    out.append("  supported_custom_fan_modes: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
+
+  for (const auto &it : this->supported_presets) {
+    out.append("  supported_presets: ");
+    out.append(proto_enum_to_string<enums::ClimatePreset>(it));
+    out.append("\n");
+  }
+
+  for (const auto &it : this->supported_custom_presets) {
+    out.append("  supported_custom_presets: ");
+    out.append("'").append(it).append("'");
+    out.append("\n");
+  }
   out.append("}");
 }
 bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
@@ -2768,6 +2884,24 @@ bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
       this->swing_mode = value.as_enum<enums::ClimateSwingMode>();
       return true;
     }
+    case 12: {
+      this->preset = value.as_enum<enums::ClimatePreset>();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ClimateStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 11: {
+      this->custom_fan_mode = value.as_string();
+      return true;
+    }
+    case 13: {
+      this->custom_preset = value.as_string();
+      return true;
+    }
     default:
       return false;
   }
@@ -2809,6 +2943,9 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_enum<enums::ClimateAction>(8, this->action);
   buffer.encode_enum<enums::ClimateFanMode>(9, this->fan_mode);
   buffer.encode_enum<enums::ClimateSwingMode>(10, this->swing_mode);
+  buffer.encode_string(11, this->custom_fan_mode);
+  buffer.encode_enum<enums::ClimatePreset>(12, this->preset);
+  buffer.encode_string(13, this->custom_preset);
 }
 void ClimateStateResponse::dump_to(std::string &out) const {
   char buffer[64];
@@ -2857,6 +2994,18 @@ void ClimateStateResponse::dump_to(std::string &out) const {
   out.append("  swing_mode: ");
   out.append(proto_enum_to_string<enums::ClimateSwingMode>(this->swing_mode));
   out.append("\n");
+
+  out.append("  custom_fan_mode: ");
+  out.append("'").append(this->custom_fan_mode).append("'");
+  out.append("\n");
+
+  out.append("  preset: ");
+  out.append(proto_enum_to_string<enums::ClimatePreset>(this->preset));
+  out.append("\n");
+
+  out.append("  custom_preset: ");
+  out.append("'").append(this->custom_preset).append("'");
+  out.append("\n");
   out.append("}");
 }
 bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
@@ -2905,6 +3054,36 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
       this->swing_mode = value.as_enum<enums::ClimateSwingMode>();
       return true;
     }
+    case 16: {
+      this->has_custom_fan_mode = value.as_bool();
+      return true;
+    }
+    case 18: {
+      this->has_preset = value.as_bool();
+      return true;
+    }
+    case 19: {
+      this->preset = value.as_enum<enums::ClimatePreset>();
+      return true;
+    }
+    case 20: {
+      this->has_custom_preset = value.as_bool();
+      return true;
+    }
+    default:
+      return false;
+  }
+}
+bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
+  switch (field_id) {
+    case 17: {
+      this->custom_fan_mode = value.as_string();
+      return true;
+    }
+    case 21: {
+      this->custom_preset = value.as_string();
+      return true;
+    }
     default:
       return false;
   }
@@ -2947,6 +3126,12 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_enum<enums::ClimateFanMode>(13, this->fan_mode);
   buffer.encode_bool(14, this->has_swing_mode);
   buffer.encode_enum<enums::ClimateSwingMode>(15, this->swing_mode);
+  buffer.encode_bool(16, this->has_custom_fan_mode);
+  buffer.encode_string(17, this->custom_fan_mode);
+  buffer.encode_bool(18, this->has_preset);
+  buffer.encode_enum<enums::ClimatePreset>(19, this->preset);
+  buffer.encode_bool(20, this->has_custom_preset);
+  buffer.encode_string(21, this->custom_preset);
 }
 void ClimateCommandRequest::dump_to(std::string &out) const {
   char buffer[64];
@@ -3014,6 +3199,30 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
   out.append("  swing_mode: ");
   out.append(proto_enum_to_string<enums::ClimateSwingMode>(this->swing_mode));
   out.append("\n");
+
+  out.append("  has_custom_fan_mode: ");
+  out.append(YESNO(this->has_custom_fan_mode));
+  out.append("\n");
+
+  out.append("  custom_fan_mode: ");
+  out.append("'").append(this->custom_fan_mode).append("'");
+  out.append("\n");
+
+  out.append("  has_preset: ");
+  out.append(YESNO(this->has_preset));
+  out.append("\n");
+
+  out.append("  preset: ");
+  out.append(proto_enum_to_string<enums::ClimatePreset>(this->preset));
+  out.append("\n");
+
+  out.append("  has_custom_preset: ");
+  out.append(YESNO(this->has_custom_preset));
+  out.append("\n");
+
+  out.append("  custom_preset: ");
+  out.append("'").append(this->custom_preset).append("'");
+  out.append("\n");
   out.append("}");
 }
 
diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h
index f70ac74a79..04d5834572 100644
--- a/esphome/components/api/api_pb2.h
+++ b/esphome/components/api/api_pb2.h
@@ -32,6 +32,10 @@ enum FanDirection : uint32_t {
   FAN_DIRECTION_FORWARD = 0,
   FAN_DIRECTION_REVERSE = 1,
 };
+enum SensorStateClass : uint32_t {
+  STATE_CLASS_NONE = 0,
+  STATE_CLASS_MEASUREMENT = 1,
+};
 enum LogLevel : uint32_t {
   LOG_LEVEL_NONE = 0,
   LOG_LEVEL_ERROR = 1,
@@ -53,11 +57,12 @@ enum ServiceArgType : uint32_t {
 };
 enum ClimateMode : uint32_t {
   CLIMATE_MODE_OFF = 0,
-  CLIMATE_MODE_AUTO = 1,
+  CLIMATE_MODE_HEAT_COOL = 1,
   CLIMATE_MODE_COOL = 2,
   CLIMATE_MODE_HEAT = 3,
   CLIMATE_MODE_FAN_ONLY = 4,
   CLIMATE_MODE_DRY = 5,
+  CLIMATE_MODE_AUTO = 6,
 };
 enum ClimateFanMode : uint32_t {
   CLIMATE_FAN_ON = 0,
@@ -84,12 +89,21 @@ enum ClimateAction : uint32_t {
   CLIMATE_ACTION_DRYING = 5,
   CLIMATE_ACTION_FAN = 6,
 };
+enum ClimatePreset : uint32_t {
+  CLIMATE_PRESET_ECO = 0,
+  CLIMATE_PRESET_AWAY = 1,
+  CLIMATE_PRESET_BOOST = 2,
+  CLIMATE_PRESET_COMFORT = 3,
+  CLIMATE_PRESET_HOME = 4,
+  CLIMATE_PRESET_SLEEP = 5,
+  CLIMATE_PRESET_ACTIVITY = 6,
+};
 
 }  // namespace enums
 
 class HelloRequest : public ProtoMessage {
  public:
-  std::string client_info{};  // NOLINT
+  std::string client_info{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -98,9 +112,9 @@ class HelloRequest : public ProtoMessage {
 };
 class HelloResponse : public ProtoMessage {
  public:
-  uint32_t api_version_major{0};  // NOLINT
-  uint32_t api_version_minor{0};  // NOLINT
-  std::string server_info{};      // NOLINT
+  uint32_t api_version_major{0};
+  uint32_t api_version_minor{0};
+  std::string server_info{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -110,7 +124,7 @@ class HelloResponse : public ProtoMessage {
 };
 class ConnectRequest : public ProtoMessage {
  public:
-  std::string password{};  // NOLINT
+  std::string password{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -119,7 +133,7 @@ class ConnectRequest : public ProtoMessage {
 };
 class ConnectResponse : public ProtoMessage {
  public:
-  bool invalid_password{false};  // NOLINT
+  bool invalid_password{false};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -163,13 +177,15 @@ class DeviceInfoRequest : public ProtoMessage {
 };
 class DeviceInfoResponse : public ProtoMessage {
  public:
-  bool uses_password{false};       // NOLINT
-  std::string name{};              // NOLINT
-  std::string mac_address{};       // NOLINT
-  std::string esphome_version{};   // NOLINT
-  std::string compilation_time{};  // NOLINT
-  std::string model{};             // NOLINT
-  bool has_deep_sleep{false};      // NOLINT
+  bool uses_password{false};
+  std::string name{};
+  std::string mac_address{};
+  std::string esphome_version{};
+  std::string compilation_time{};
+  std::string model{};
+  bool has_deep_sleep{false};
+  std::string project_name{};
+  std::string project_version{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -200,12 +216,12 @@ class SubscribeStatesRequest : public ProtoMessage {
 };
 class ListEntitiesBinarySensorResponse : public ProtoMessage {
  public:
-  std::string object_id{};              // NOLINT
-  uint32_t key{0};                      // NOLINT
-  std::string name{};                   // NOLINT
-  std::string unique_id{};              // NOLINT
-  std::string device_class{};           // NOLINT
-  bool is_status_binary_sensor{false};  // NOLINT
+  std::string object_id{};
+  uint32_t key{0};
+  std::string name{};
+  std::string unique_id{};
+  std::string device_class{};
+  bool is_status_binary_sensor{false};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -216,9 +232,9 @@ class ListEntitiesBinarySensorResponse : public ProtoMessage {
 };
 class BinarySensorStateResponse : public ProtoMessage {
  public:
-  uint32_t key{0};            // NOLINT
-  bool state{false};          // NOLINT
-  bool missing_state{false};  // NOLINT
+  uint32_t key{0};
+  bool state{false};
+  bool missing_state{false};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -228,14 +244,14 @@ class BinarySensorStateResponse : public ProtoMessage {
 };
 class ListEntitiesCoverResponse : public ProtoMessage {
  public:
-  std::string object_id{};        // NOLINT
-  uint32_t key{0};                // NOLINT
-  std::string name{};             // NOLINT
-  std::string unique_id{};        // NOLINT
-  bool assumed_state{false};      // NOLINT
-  bool supports_position{false};  // NOLINT
-  bool supports_tilt{false};      // NOLINT
-  std::string device_class{};     // NOLINT
+  std::string object_id{};
+  uint32_t key{0};
+  std::string name{};
+  std::string unique_id{};
+  bool assumed_state{false};
+  bool supports_position{false};
+  bool supports_tilt{false};
+  std::string device_class{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -246,11 +262,11 @@ class ListEntitiesCoverResponse : public ProtoMessage {
 };
 class CoverStateResponse : public ProtoMessage {
  public:
-  uint32_t key{0};                            // NOLINT
-  enums::LegacyCoverState legacy_state{};     // NOLINT
-  float position{0.0f};                       // NOLINT
-  float tilt{0.0f};                           // NOLINT
-  enums::CoverOperation current_operation{};  // NOLINT
+  uint32_t key{0};
+  enums::LegacyCoverState legacy_state{};
+  float position{0.0f};
+  float tilt{0.0f};
+  enums::CoverOperation current_operation{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -260,14 +276,14 @@ class CoverStateResponse : public ProtoMessage {
 };
 class CoverCommandRequest : public ProtoMessage {
  public:
-  uint32_t key{0};                             // NOLINT
-  bool has_legacy_command{false};              // NOLINT
-  enums::LegacyCoverCommand legacy_command{};  // NOLINT
-  bool has_position{false};                    // NOLINT
-  float position{0.0f};                        // NOLINT
-  bool has_tilt{false};                        // NOLINT
-  float tilt{0.0f};                            // NOLINT
-  bool stop{false};                            // NOLINT
+  uint32_t key{0};
+  bool has_legacy_command{false};
+  enums::LegacyCoverCommand legacy_command{};
+  bool has_position{false};
+  float position{0.0f};
+  bool has_tilt{false};
+  float tilt{0.0f};
+  bool stop{false};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -277,14 +293,14 @@ class CoverCommandRequest : public ProtoMessage {
 };
 class ListEntitiesFanResponse : public ProtoMessage {
  public:
-  std::string object_id{};           // NOLINT
-  uint32_t key{0};                   // NOLINT
-  std::string name{};                // NOLINT
-  std::string unique_id{};           // NOLINT
-  bool supports_oscillation{false};  // NOLINT
-  bool supports_speed{false};        // NOLINT
-  bool supports_direction{false};    // NOLINT
-  int32_t supported_speed_count{0};  // NOLINT
+  std::string object_id{};
+  uint32_t key{0};
+  std::string name{};
+  std::string unique_id{};
+  bool supports_oscillation{false};
+  bool supports_speed{false};
+  bool supports_direction{false};
+  int32_t supported_speed_count{0};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -295,12 +311,12 @@ class ListEntitiesFanResponse : public ProtoMessage {
 };
 class FanStateResponse : public ProtoMessage {
  public:
-  uint32_t key{0};                  // NOLINT
-  bool state{false};                // NOLINT
-  bool oscillating{false};          // NOLINT
-  enums::FanSpeed speed{};          // NOLINT
-  enums::FanDirection direction{};  // NOLINT
-  int32_t speed_level{0};           // NOLINT
+  uint32_t key{0};
+  bool state{false};
+  bool oscillating{false};
+  enums::FanSpeed speed{};
+  enums::FanDirection direction{};
+  int32_t speed_level{0};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -310,17 +326,17 @@ class FanStateResponse : public ProtoMessage {
 };
 class FanCommandRequest : public ProtoMessage {
  public:
-  uint32_t key{0};                  // NOLINT
-  bool has_state{false};            // NOLINT
-  bool state{false};                // NOLINT
-  bool has_speed{false};            // NOLINT
-  enums::FanSpeed speed{};          // NOLINT
-  bool has_oscillating{false};      // NOLINT
-  bool oscillating{false};          // NOLINT
-  bool has_direction{false};        // NOLINT
-  enums::FanDirection direction{};  // NOLINT
-  bool has_speed_level{false};      // NOLINT
-  int32_t speed_level{0};           // NOLINT
+  uint32_t key{0};
+  bool has_state{false};
+  bool state{false};
+  bool has_speed{false};
+  enums::FanSpeed speed{};
+  bool has_oscillating{false};
+  bool oscillating{false};
+  bool has_direction{false};
+  enums::FanDirection direction{};
+  bool has_speed_level{false};
+  int32_t speed_level{0};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -330,17 +346,17 @@ class FanCommandRequest : public ProtoMessage {
 };
 class ListEntitiesLightResponse : public ProtoMessage {
  public:
-  std::string object_id{};                 // NOLINT
-  uint32_t key{0};                         // NOLINT
-  std::string name{};                      // NOLINT
-  std::string unique_id{};                 // NOLINT
-  bool supports_brightness{false};         // NOLINT
-  bool supports_rgb{false};                // NOLINT
-  bool supports_white_value{false};        // NOLINT
-  bool supports_color_temperature{false};  // NOLINT
-  float min_mireds{0.0f};                  // NOLINT
-  float max_mireds{0.0f};                  // NOLINT
-  std::vector<std::string> effects{};      // NOLINT
+  std::string object_id{};
+  uint32_t key{0};
+  std::string name{};
+  std::string unique_id{};
+  bool supports_brightness{false};
+  bool supports_rgb{false};
+  bool supports_white_value{false};
+  bool supports_color_temperature{false};
+  float min_mireds{0.0f};
+  float max_mireds{0.0f};
+  std::vector<std::string> effects{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -351,15 +367,15 @@ class ListEntitiesLightResponse : public ProtoMessage {
 };
 class LightStateResponse : public ProtoMessage {
  public:
-  uint32_t key{0};                // NOLINT
-  bool state{false};              // NOLINT
-  float brightness{0.0f};         // NOLINT
-  float red{0.0f};                // NOLINT
-  float green{0.0f};              // NOLINT
-  float blue{0.0f};               // NOLINT
-  float white{0.0f};              // NOLINT
-  float color_temperature{0.0f};  // NOLINT
-  std::string effect{};           // NOLINT
+  uint32_t key{0};
+  bool state{false};
+  float brightness{0.0f};
+  float red{0.0f};
+  float green{0.0f};
+  float blue{0.0f};
+  float white{0.0f};
+  float color_temperature{0.0f};
+  std::string effect{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -370,25 +386,25 @@ class LightStateResponse : public ProtoMessage {
 };
 class LightCommandRequest : public ProtoMessage {
  public:
-  uint32_t key{0};                    // NOLINT
-  bool has_state{false};              // NOLINT
-  bool state{false};                  // NOLINT
-  bool has_brightness{false};         // NOLINT
-  float brightness{0.0f};             // NOLINT
-  bool has_rgb{false};                // NOLINT
-  float red{0.0f};                    // NOLINT
-  float green{0.0f};                  // NOLINT
-  float blue{0.0f};                   // NOLINT
-  bool has_white{false};              // NOLINT
-  float white{0.0f};                  // NOLINT
-  bool has_color_temperature{false};  // NOLINT
-  float color_temperature{0.0f};      // NOLINT
-  bool has_transition_length{false};  // NOLINT
-  uint32_t transition_length{0};      // NOLINT
-  bool has_flash_length{false};       // NOLINT
-  uint32_t flash_length{0};           // NOLINT
-  bool has_effect{false};             // NOLINT
-  std::string effect{};               // NOLINT
+  uint32_t key{0};
+  bool has_state{false};
+  bool state{false};
+  bool has_brightness{false};
+  float brightness{0.0f};
+  bool has_rgb{false};
+  float red{0.0f};
+  float green{0.0f};
+  float blue{0.0f};
+  bool has_white{false};
+  float white{0.0f};
+  bool has_color_temperature{false};
+  float color_temperature{0.0f};
+  bool has_transition_length{false};
+  uint32_t transition_length{0};
+  bool has_flash_length{false};
+  uint32_t flash_length{0};
+  bool has_effect{false};
+  std::string effect{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -399,15 +415,16 @@ class LightCommandRequest : public ProtoMessage {
 };
 class ListEntitiesSensorResponse : public ProtoMessage {
  public:
-  std::string object_id{};            // NOLINT
-  uint32_t key{0};                    // NOLINT
-  std::string name{};                 // NOLINT
-  std::string unique_id{};            // NOLINT
-  std::string icon{};                 // NOLINT
-  std::string unit_of_measurement{};  // NOLINT
-  int32_t accuracy_decimals{0};       // NOLINT
-  bool force_update{false};           // NOLINT
-  std::string device_class{};         // NOLINT
+  std::string object_id{};
+  uint32_t key{0};
+  std::string name{};
+  std::string unique_id{};
+  std::string icon{};
+  std::string unit_of_measurement{};
+  int32_t accuracy_decimals{0};
+  bool force_update{false};
+  std::string device_class{};
+  enums::SensorStateClass state_class{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -418,9 +435,9 @@ class ListEntitiesSensorResponse : public ProtoMessage {
 };
 class SensorStateResponse : public ProtoMessage {
  public:
-  uint32_t key{0};            // NOLINT
-  float state{0.0f};          // NOLINT
-  bool missing_state{false};  // NOLINT
+  uint32_t key{0};
+  float state{0.0f};
+  bool missing_state{false};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -430,12 +447,12 @@ class SensorStateResponse : public ProtoMessage {
 };
 class ListEntitiesSwitchResponse : public ProtoMessage {
  public:
-  std::string object_id{};    // NOLINT
-  uint32_t key{0};            // NOLINT
-  std::string name{};         // NOLINT
-  std::string unique_id{};    // NOLINT
-  std::string icon{};         // NOLINT
-  bool assumed_state{false};  // NOLINT
+  std::string object_id{};
+  uint32_t key{0};
+  std::string name{};
+  std::string unique_id{};
+  std::string icon{};
+  bool assumed_state{false};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -446,8 +463,8 @@ class ListEntitiesSwitchResponse : public ProtoMessage {
 };
 class SwitchStateResponse : public ProtoMessage {
  public:
-  uint32_t key{0};    // NOLINT
-  bool state{false};  // NOLINT
+  uint32_t key{0};
+  bool state{false};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -457,8 +474,8 @@ class SwitchStateResponse : public ProtoMessage {
 };
 class SwitchCommandRequest : public ProtoMessage {
  public:
-  uint32_t key{0};    // NOLINT
-  bool state{false};  // NOLINT
+  uint32_t key{0};
+  bool state{false};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -468,11 +485,11 @@ class SwitchCommandRequest : public ProtoMessage {
 };
 class ListEntitiesTextSensorResponse : public ProtoMessage {
  public:
-  std::string object_id{};  // NOLINT
-  uint32_t key{0};          // NOLINT
-  std::string name{};       // NOLINT
-  std::string unique_id{};  // NOLINT
-  std::string icon{};       // NOLINT
+  std::string object_id{};
+  uint32_t key{0};
+  std::string name{};
+  std::string unique_id{};
+  std::string icon{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -482,9 +499,9 @@ class ListEntitiesTextSensorResponse : public ProtoMessage {
 };
 class TextSensorStateResponse : public ProtoMessage {
  public:
-  uint32_t key{0};            // NOLINT
-  std::string state{};        // NOLINT
-  bool missing_state{false};  // NOLINT
+  uint32_t key{0};
+  std::string state{};
+  bool missing_state{false};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -495,8 +512,8 @@ class TextSensorStateResponse : public ProtoMessage {
 };
 class SubscribeLogsRequest : public ProtoMessage {
  public:
-  enums::LogLevel level{};  // NOLINT
-  bool dump_config{false};  // NOLINT
+  enums::LogLevel level{};
+  bool dump_config{false};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -505,10 +522,10 @@ class SubscribeLogsRequest : public ProtoMessage {
 };
 class SubscribeLogsResponse : public ProtoMessage {
  public:
-  enums::LogLevel level{};  // NOLINT
-  std::string tag{};        // NOLINT
-  std::string message{};    // NOLINT
-  bool send_failed{false};  // NOLINT
+  enums::LogLevel level{};
+  std::string tag{};
+  std::string message{};
+  bool send_failed{false};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -525,8 +542,8 @@ class SubscribeHomeassistantServicesRequest : public ProtoMessage {
 };
 class HomeassistantServiceMap : public ProtoMessage {
  public:
-  std::string key{};    // NOLINT
-  std::string value{};  // NOLINT
+  std::string key{};
+  std::string value{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -535,11 +552,11 @@ class HomeassistantServiceMap : public ProtoMessage {
 };
 class HomeassistantServiceResponse : public ProtoMessage {
  public:
-  std::string service{};                                 // NOLINT
-  std::vector<HomeassistantServiceMap> data{};           // NOLINT
-  std::vector<HomeassistantServiceMap> data_template{};  // NOLINT
-  std::vector<HomeassistantServiceMap> variables{};      // NOLINT
-  bool is_event{false};                                  // NOLINT
+  std::string service{};
+  std::vector<HomeassistantServiceMap> data{};
+  std::vector<HomeassistantServiceMap> data_template{};
+  std::vector<HomeassistantServiceMap> variables{};
+  bool is_event{false};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -556,7 +573,8 @@ class SubscribeHomeAssistantStatesRequest : public ProtoMessage {
 };
 class SubscribeHomeAssistantStateResponse : public ProtoMessage {
  public:
-  std::string entity_id{};  // NOLINT
+  std::string entity_id{};
+  std::string attribute{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -565,8 +583,9 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage {
 };
 class HomeAssistantStateResponse : public ProtoMessage {
  public:
-  std::string entity_id{};  // NOLINT
-  std::string state{};      // NOLINT
+  std::string entity_id{};
+  std::string state{};
+  std::string attribute{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -582,7 +601,7 @@ class GetTimeRequest : public ProtoMessage {
 };
 class GetTimeResponse : public ProtoMessage {
  public:
-  uint32_t epoch_seconds{0};  // NOLINT
+  uint32_t epoch_seconds{0};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -591,8 +610,8 @@ class GetTimeResponse : public ProtoMessage {
 };
 class ListEntitiesServicesArgument : public ProtoMessage {
  public:
-  std::string name{};            // NOLINT
-  enums::ServiceArgType type{};  // NOLINT
+  std::string name{};
+  enums::ServiceArgType type{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -602,9 +621,9 @@ class ListEntitiesServicesArgument : public ProtoMessage {
 };
 class ListEntitiesServicesResponse : public ProtoMessage {
  public:
-  std::string name{};                                // NOLINT
-  uint32_t key{0};                                   // NOLINT
-  std::vector<ListEntitiesServicesArgument> args{};  // NOLINT
+  std::string name{};
+  uint32_t key{0};
+  std::vector<ListEntitiesServicesArgument> args{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -614,15 +633,15 @@ class ListEntitiesServicesResponse : public ProtoMessage {
 };
 class ExecuteServiceArgument : public ProtoMessage {
  public:
-  bool bool_{false};                        // NOLINT
-  int32_t legacy_int{0};                    // NOLINT
-  float float_{0.0f};                       // NOLINT
-  std::string string_{};                    // NOLINT
-  int32_t int_{0};                          // NOLINT
-  std::vector<bool> bool_array{};           // NOLINT
-  std::vector<int32_t> int_array{};         // NOLINT
-  std::vector<float> float_array{};         // NOLINT
-  std::vector<std::string> string_array{};  // NOLINT
+  bool bool_{false};
+  int32_t legacy_int{0};
+  float float_{0.0f};
+  std::string string_{};
+  int32_t int_{0};
+  std::vector<bool> bool_array{};
+  std::vector<int32_t> int_array{};
+  std::vector<float> float_array{};
+  std::vector<std::string> string_array{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -633,8 +652,8 @@ class ExecuteServiceArgument : public ProtoMessage {
 };
 class ExecuteServiceRequest : public ProtoMessage {
  public:
-  uint32_t key{0};                             // NOLINT
-  std::vector<ExecuteServiceArgument> args{};  // NOLINT
+  uint32_t key{0};
+  std::vector<ExecuteServiceArgument> args{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -644,10 +663,10 @@ class ExecuteServiceRequest : public ProtoMessage {
 };
 class ListEntitiesCameraResponse : public ProtoMessage {
  public:
-  std::string object_id{};  // NOLINT
-  uint32_t key{0};          // NOLINT
-  std::string name{};       // NOLINT
-  std::string unique_id{};  // NOLINT
+  std::string object_id{};
+  uint32_t key{0};
+  std::string name{};
+  std::string unique_id{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -657,9 +676,9 @@ class ListEntitiesCameraResponse : public ProtoMessage {
 };
 class CameraImageResponse : public ProtoMessage {
  public:
-  uint32_t key{0};     // NOLINT
-  std::string data{};  // NOLINT
-  bool done{false};    // NOLINT
+  uint32_t key{0};
+  std::string data{};
+  bool done{false};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -670,8 +689,8 @@ class CameraImageResponse : public ProtoMessage {
 };
 class CameraImageRequest : public ProtoMessage {
  public:
-  bool single{false};  // NOLINT
-  bool stream{false};  // NOLINT
+  bool single{false};
+  bool stream{false};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -680,20 +699,23 @@ class CameraImageRequest : public ProtoMessage {
 };
 class ListEntitiesClimateResponse : public ProtoMessage {
  public:
-  std::string object_id{};                                       // NOLINT
-  uint32_t key{0};                                               // NOLINT
-  std::string name{};                                            // NOLINT
-  std::string unique_id{};                                       // NOLINT
-  bool supports_current_temperature{false};                      // NOLINT
-  bool supports_two_point_target_temperature{false};             // NOLINT
-  std::vector<enums::ClimateMode> supported_modes{};             // NOLINT
-  float visual_min_temperature{0.0f};                            // NOLINT
-  float visual_max_temperature{0.0f};                            // NOLINT
-  float visual_temperature_step{0.0f};                           // NOLINT
-  bool supports_away{false};                                     // NOLINT
-  bool supports_action{false};                                   // NOLINT
-  std::vector<enums::ClimateFanMode> supported_fan_modes{};      // NOLINT
-  std::vector<enums::ClimateSwingMode> supported_swing_modes{};  // NOLINT
+  std::string object_id{};
+  uint32_t key{0};
+  std::string name{};
+  std::string unique_id{};
+  bool supports_current_temperature{false};
+  bool supports_two_point_target_temperature{false};
+  std::vector<enums::ClimateMode> supported_modes{};
+  float visual_min_temperature{0.0f};
+  float visual_max_temperature{0.0f};
+  float visual_temperature_step{0.0f};
+  bool supports_away{false};
+  bool supports_action{false};
+  std::vector<enums::ClimateFanMode> supported_fan_modes{};
+  std::vector<enums::ClimateSwingMode> supported_swing_modes{};
+  std::vector<std::string> supported_custom_fan_modes{};
+  std::vector<enums::ClimatePreset> supported_presets{};
+  std::vector<std::string> supported_custom_presets{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -704,45 +726,56 @@ class ListEntitiesClimateResponse : public ProtoMessage {
 };
 class ClimateStateResponse : public ProtoMessage {
  public:
-  uint32_t key{0};                       // NOLINT
-  enums::ClimateMode mode{};             // NOLINT
-  float current_temperature{0.0f};       // NOLINT
-  float target_temperature{0.0f};        // NOLINT
-  float target_temperature_low{0.0f};    // NOLINT
-  float target_temperature_high{0.0f};   // NOLINT
-  bool away{false};                      // NOLINT
-  enums::ClimateAction action{};         // NOLINT
-  enums::ClimateFanMode fan_mode{};      // NOLINT
-  enums::ClimateSwingMode swing_mode{};  // NOLINT
+  uint32_t key{0};
+  enums::ClimateMode mode{};
+  float current_temperature{0.0f};
+  float target_temperature{0.0f};
+  float target_temperature_low{0.0f};
+  float target_temperature_high{0.0f};
+  bool away{false};
+  enums::ClimateAction action{};
+  enums::ClimateFanMode fan_mode{};
+  enums::ClimateSwingMode swing_mode{};
+  std::string custom_fan_mode{};
+  enums::ClimatePreset preset{};
+  std::string custom_preset{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
  protected:
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
 class ClimateCommandRequest : public ProtoMessage {
  public:
-  uint32_t key{0};                          // NOLINT
-  bool has_mode{false};                     // NOLINT
-  enums::ClimateMode mode{};                // NOLINT
-  bool has_target_temperature{false};       // NOLINT
-  float target_temperature{0.0f};           // NOLINT
-  bool has_target_temperature_low{false};   // NOLINT
-  float target_temperature_low{0.0f};       // NOLINT
-  bool has_target_temperature_high{false};  // NOLINT
-  float target_temperature_high{0.0f};      // NOLINT
-  bool has_away{false};                     // NOLINT
-  bool away{false};                         // NOLINT
-  bool has_fan_mode{false};                 // NOLINT
-  enums::ClimateFanMode fan_mode{};         // NOLINT
-  bool has_swing_mode{false};               // NOLINT
-  enums::ClimateSwingMode swing_mode{};     // NOLINT
+  uint32_t key{0};
+  bool has_mode{false};
+  enums::ClimateMode mode{};
+  bool has_target_temperature{false};
+  float target_temperature{0.0f};
+  bool has_target_temperature_low{false};
+  float target_temperature_low{0.0f};
+  bool has_target_temperature_high{false};
+  float target_temperature_high{0.0f};
+  bool has_away{false};
+  bool away{false};
+  bool has_fan_mode{false};
+  enums::ClimateFanMode fan_mode{};
+  bool has_swing_mode{false};
+  enums::ClimateSwingMode swing_mode{};
+  bool has_custom_fan_mode{false};
+  std::string custom_fan_mode{};
+  bool has_preset{false};
+  enums::ClimatePreset preset{};
+  bool has_custom_preset{false};
+  std::string custom_preset{};
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
  protected:
   bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
+  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
   bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 };
 
diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp
index 25ae9a98a3..dd77f2004f 100644
--- a/esphome/components/api/api_server.cpp
+++ b/esphome/components/api/api_server.cpp
@@ -208,9 +208,11 @@ void APIServer::send_homeassistant_service_call(const HomeassistantServiceRespon
   }
 }
 APIServer::APIServer() { global_api_server = this; }
-void APIServer::subscribe_home_assistant_state(std::string entity_id, std::function<void(std::string)> f) {
+void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
+                                               std::function<void(std::string)> f) {
   this->state_subs_.push_back(HomeAssistantStateSubscription{
       .entity_id = std::move(entity_id),
+      .attribute = std::move(attribute),
       .callback = std::move(f),
   });
 }
diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h
index db826c55c2..538a385efb 100644
--- a/esphome/components/api/api_server.h
+++ b/esphome/components/api/api_server.h
@@ -71,10 +71,12 @@ class APIServer : public Component, public Controller {
 
   struct HomeAssistantStateSubscription {
     std::string entity_id;
+    optional<std::string> attribute;
     std::function<void(std::string)> callback;
   };
 
-  void subscribe_home_assistant_state(std::string entity_id, std::function<void(std::string)> f);
+  void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
+                                      std::function<void(std::string)> f);
   const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
   const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
 
diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h
index aac91244b6..74343904eb 100644
--- a/esphome/components/api/custom_api_device.h
+++ b/esphome/components/api/custom_api_device.h
@@ -76,13 +76,13 @@ class CustomAPIDevice {
     global_api_server->register_user_service(service);
   }
 
-  /** Subscribe to the state of an entity from Home Assistant.
+  /** Subscribe to the state (or attribute state) of an entity from Home Assistant.
    *
    * Usage:
    *
    * ```cpp
    * void setup() override {
-   *   subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast");
+   *   subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "climate.kitchen", "current_temperature");
    * }
    *
    * void on_state_changed(std::string state) {
@@ -93,17 +93,19 @@ class CustomAPIDevice {
    * @tparam T The class type creating the service, automatically deduced from the function pointer.
    * @param callback The member function to call when the entity state changes.
    * @param entity_id The entity_id to track.
+   * @param attribute The entity state attribute to track.
    */
   template<typename T>
-  void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id) {
+  void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
+                                     const std::string &attribute = "") {
     auto f = std::bind(callback, (T *) this, std::placeholders::_1);
-    global_api_server->subscribe_home_assistant_state(entity_id, f);
+    global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
   }
 
-  /** Subscribe to the state of an entity from Home Assistant.
+  /** Subscribe to the state (or attribute state) of an entity from Home Assistant.
    *
    * Usage:
-   *
+   *å
    * ```cpp
    * void setup() override {
    *   subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast");
@@ -117,11 +119,13 @@ class CustomAPIDevice {
    * @tparam T The class type creating the service, automatically deduced from the function pointer.
    * @param callback The member function to call when the entity state changes.
    * @param entity_id The entity_id to track.
+   * @param attribute The entity state attribute to track.
    */
   template<typename T>
-  void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id) {
+  void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
+                                     const std::string &attribute = "") {
     auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
-    global_api_server->subscribe_home_assistant_state(entity_id, f);
+    global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
   }
 
   /** Call a Home Assistant service from ESPHome.
diff --git a/esphome/components/as3935/__init__.py b/esphome/components/as3935/__init__.py
index 337f0a9081..0951d01e68 100644
--- a/esphome/components/as3935/__init__.py
+++ b/esphome/components/as3935/__init__.py
@@ -11,7 +11,6 @@ from esphome.const import (
     CONF_DIV_RATIO,
     CONF_CAPACITANCE,
 )
-from esphome.core import coroutine
 
 AUTO_LOAD = ["sensor", "binary_sensor"]
 MULTI_CONF = True
@@ -40,11 +39,10 @@ AS3935_SCHEMA = cv.Schema(
 )
 
 
-@coroutine
-def setup_as3935(var, config):
-    yield cg.register_component(var, config)
+async def setup_as3935(var, config):
+    await cg.register_component(var, config)
 
-    irq_pin = yield cg.gpio_pin_expression(config[CONF_IRQ_PIN])
+    irq_pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN])
     cg.add(var.set_irq_pin(irq_pin))
     cg.add(var.set_indoor(config[CONF_INDOOR]))
     cg.add(var.set_noise_level(config[CONF_NOISE_LEVEL]))
diff --git a/esphome/components/as3935/binary_sensor.py b/esphome/components/as3935/binary_sensor.py
index 85ba052869..11b2ac812c 100644
--- a/esphome/components/as3935/binary_sensor.py
+++ b/esphome/components/as3935/binary_sensor.py
@@ -12,7 +12,7 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 )
 
 
-def to_code(config):
-    hub = yield cg.get_variable(config[CONF_AS3935_ID])
-    var = yield binary_sensor.new_binary_sensor(config)
+async def to_code(config):
+    hub = await cg.get_variable(config[CONF_AS3935_ID])
+    var = await binary_sensor.new_binary_sensor(config)
     cg.add(hub.set_thunder_alert_binary_sensor(var))
diff --git a/esphome/components/as3935/sensor.py b/esphome/components/as3935/sensor.py
index 64eeea7b04..a571121742 100644
--- a/esphome/components/as3935/sensor.py
+++ b/esphome/components/as3935/sensor.py
@@ -5,6 +5,7 @@ from esphome.const import (
     CONF_DISTANCE,
     CONF_LIGHTNING_ENERGY,
     DEVICE_CLASS_EMPTY,
+    STATE_CLASS_NONE,
     UNIT_KILOMETER,
     UNIT_EMPTY,
     ICON_SIGNAL_DISTANCE_VARIANT,
@@ -18,24 +19,28 @@ CONFIG_SCHEMA = cv.Schema(
     {
         cv.GenerateID(CONF_AS3935_ID): cv.use_id(AS3935),
         cv.Optional(CONF_DISTANCE): sensor.sensor_schema(
-            UNIT_KILOMETER, ICON_SIGNAL_DISTANCE_VARIANT, 1, DEVICE_CLASS_EMPTY
+            UNIT_KILOMETER,
+            ICON_SIGNAL_DISTANCE_VARIANT,
+            1,
+            DEVICE_CLASS_EMPTY,
+            STATE_CLASS_NONE,
         ),
         cv.Optional(CONF_LIGHTNING_ENERGY): sensor.sensor_schema(
-            UNIT_EMPTY, ICON_FLASH, 1, DEVICE_CLASS_EMPTY
+            UNIT_EMPTY, ICON_FLASH, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
         ),
     }
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
-    hub = yield cg.get_variable(config[CONF_AS3935_ID])
+async def to_code(config):
+    hub = await cg.get_variable(config[CONF_AS3935_ID])
 
     if CONF_DISTANCE in config:
         conf = config[CONF_DISTANCE]
-        distance_sensor = yield sensor.new_sensor(conf)
+        distance_sensor = await sensor.new_sensor(conf)
         cg.add(hub.set_distance_sensor(distance_sensor))
 
     if CONF_LIGHTNING_ENERGY in config:
         conf = config[CONF_LIGHTNING_ENERGY]
-        lightning_energy_sensor = yield sensor.new_sensor(conf)
+        lightning_energy_sensor = await sensor.new_sensor(conf)
         cg.add(hub.set_energy_sensor(lightning_energy_sensor))
diff --git a/esphome/components/as3935_i2c/__init__.py b/esphome/components/as3935_i2c/__init__.py
index 277ccd84d3..aa741b2ea6 100644
--- a/esphome/components/as3935_i2c/__init__.py
+++ b/esphome/components/as3935_i2c/__init__.py
@@ -20,7 +20,7 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield as3935.setup_as3935(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await as3935.setup_as3935(var, config)
+    await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/as3935_spi/__init__.py b/esphome/components/as3935_spi/__init__.py
index b921444b8b..849539f092 100644
--- a/esphome/components/as3935_spi/__init__.py
+++ b/esphome/components/as3935_spi/__init__.py
@@ -20,7 +20,7 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield as3935.setup_as3935(var, config)
-    yield spi.register_spi_device(var, config)
+    await as3935.setup_as3935(var, config)
+    await spi.register_spi_device(var, config)
diff --git a/esphome/components/as3935_spi/as3935_spi.cpp b/esphome/components/as3935_spi/as3935_spi.cpp
index 73752a8ee0..54b52b2fd0 100644
--- a/esphome/components/as3935_spi/as3935_spi.cpp
+++ b/esphome/components/as3935_spi/as3935_spi.cpp
@@ -33,7 +33,7 @@ void SPIAS3935Component::write_register(uint8_t reg, uint8_t mask, uint8_t bits,
 uint8_t SPIAS3935Component::read_register(uint8_t reg) {
   uint8_t value = 0;
   this->enable();
-  this->write_byte(reg |= SPI_READ_M);
+  this->write_byte(reg | SPI_READ_M);
   value = this->read_byte();
   // According to datsheet, the chip select must be written HIGH, LOW, HIGH
   // to correctly end the READ command.
diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py
index 8532f349e6..8938dc4671 100644
--- a/esphome/components/async_tcp/__init__.py
+++ b/esphome/components/async_tcp/__init__.py
@@ -6,7 +6,7 @@ CODEOWNERS = ["@OttoWinter"]
 
 
 @coroutine_with_priority(200.0)
-def to_code(config):
+async def to_code(config):
     if CORE.is_esp32:
         # https://github.com/esphome/AsyncTCP/blob/master/library.json
         cg.add_library("esphome/AsyncTCP-esphome", "1.2.2")
diff --git a/esphome/components/atc_mithermometer/sensor.py b/esphome/components/atc_mithermometer/sensor.py
index 51891b33cb..efa3f2b51a 100644
--- a/esphome/components/atc_mithermometer/sensor.py
+++ b/esphome/components/atc_mithermometer/sensor.py
@@ -13,6 +13,7 @@ from esphome.const import (
     DEVICE_CLASS_TEMPERATURE,
     DEVICE_CLASS_VOLTAGE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
     UNIT_VOLT,
@@ -33,16 +34,28 @@ CONFIG_SCHEMA = (
             cv.GenerateID(): cv.declare_id(ATCMiThermometer),
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_BATTERY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_BATTERY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(
-                UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE
+                UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT
             ),
         }
     )
@@ -51,22 +64,22 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity(sens))
     if CONF_BATTERY_LEVEL in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
         cg.add(var.set_battery_level(sens))
     if CONF_BATTERY_VOLTAGE in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_VOLTAGE])
+        sens = await sensor.new_sensor(config[CONF_BATTERY_VOLTAGE])
         cg.add(var.set_battery_voltage(sens))
diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py
index 4a9100d9d6..68ec199bff 100644
--- a/esphome/components/atm90e32/sensor.py
+++ b/esphome/components/atm90e32/sensor.py
@@ -3,6 +3,7 @@ import esphome.config_validation as cv
 from esphome.components import sensor, spi
 from esphome.const import (
     CONF_ID,
+    CONF_REACTIVE_POWER,
     CONF_VOLTAGE,
     CONF_CURRENT,
     CONF_POWER,
@@ -20,6 +21,7 @@ from esphome.const import (
     ICON_EMPTY,
     ICON_LIGHTBULB,
     ICON_CURRENT_AC,
+    STATE_CLASS_MEASUREMENT,
     UNIT_HERTZ,
     UNIT_VOLT,
     UNIT_AMPERE,
@@ -34,7 +36,6 @@ CONF_PHASE_A = "phase_a"
 CONF_PHASE_B = "phase_b"
 CONF_PHASE_C = "phase_c"
 
-CONF_REACTIVE_POWER = "reactive_power"
 CONF_LINE_FREQUENCY = "line_frequency"
 CONF_CHIP_TEMPERATURE = "chip_temperature"
 CONF_GAIN_PGA = "gain_pga"
@@ -63,25 +64,37 @@ ATM90E32Component = atm90e32_ns.class_(
 ATM90E32_PHASE_SCHEMA = cv.Schema(
     {
         cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
-            UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE
+            UNIT_VOLT,
+            ICON_EMPTY,
+            2,
+            DEVICE_CLASS_VOLTAGE,
+            STATE_CLASS_MEASUREMENT,
         ),
         cv.Optional(CONF_CURRENT): sensor.sensor_schema(
-            UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT
+            UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional(CONF_POWER): sensor.sensor_schema(
-            UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER
+            UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema(
-            UNIT_VOLT_AMPS_REACTIVE, ICON_LIGHTBULB, 2, DEVICE_CLASS_EMPTY
+            UNIT_VOLT_AMPS_REACTIVE,
+            ICON_LIGHTBULB,
+            2,
+            DEVICE_CLASS_EMPTY,
+            STATE_CLASS_MEASUREMENT,
         ),
         cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(
-            UNIT_EMPTY, ICON_EMPTY, 2, DEVICE_CLASS_POWER_FACTOR
+            UNIT_EMPTY,
+            ICON_EMPTY,
+            2,
+            DEVICE_CLASS_POWER_FACTOR,
+            STATE_CLASS_MEASUREMENT,
         ),
         cv.Optional(CONF_FORWARD_ACTIVE_ENERGY): sensor.sensor_schema(
-            UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY
+            UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema(
-            UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY
+            UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t,
         cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t,
@@ -96,10 +109,18 @@ CONFIG_SCHEMA = (
             cv.Optional(CONF_PHASE_B): ATM90E32_PHASE_SCHEMA,
             cv.Optional(CONF_PHASE_C): ATM90E32_PHASE_SCHEMA,
             cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(
-                UNIT_HERTZ, ICON_CURRENT_AC, 1, DEVICE_CLASS_EMPTY
+                UNIT_HERTZ,
+                ICON_CURRENT_AC,
+                1,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_CHIP_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True),
             cv.Optional(CONF_CURRENT_PHASES, default="3"): cv.enum(
@@ -113,10 +134,10 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield spi.register_spi_device(var, config)
+    await cg.register_component(var, config)
+    await spi.register_spi_device(var, config)
 
     for i, phase in enumerate([CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]):
         if phase not in config:
@@ -125,31 +146,31 @@ def to_code(config):
         cg.add(var.set_volt_gain(i, conf[CONF_GAIN_VOLTAGE]))
         cg.add(var.set_ct_gain(i, conf[CONF_GAIN_CT]))
         if CONF_VOLTAGE in conf:
-            sens = yield sensor.new_sensor(conf[CONF_VOLTAGE])
+            sens = await sensor.new_sensor(conf[CONF_VOLTAGE])
             cg.add(var.set_voltage_sensor(i, sens))
         if CONF_CURRENT in conf:
-            sens = yield sensor.new_sensor(conf[CONF_CURRENT])
+            sens = await sensor.new_sensor(conf[CONF_CURRENT])
             cg.add(var.set_current_sensor(i, sens))
         if CONF_POWER in conf:
-            sens = yield sensor.new_sensor(conf[CONF_POWER])
+            sens = await sensor.new_sensor(conf[CONF_POWER])
             cg.add(var.set_power_sensor(i, sens))
         if CONF_REACTIVE_POWER in conf:
-            sens = yield sensor.new_sensor(conf[CONF_REACTIVE_POWER])
+            sens = await sensor.new_sensor(conf[CONF_REACTIVE_POWER])
             cg.add(var.set_reactive_power_sensor(i, sens))
         if CONF_POWER_FACTOR in conf:
-            sens = yield sensor.new_sensor(conf[CONF_POWER_FACTOR])
+            sens = await sensor.new_sensor(conf[CONF_POWER_FACTOR])
             cg.add(var.set_power_factor_sensor(i, sens))
         if CONF_FORWARD_ACTIVE_ENERGY in conf:
-            sens = yield sensor.new_sensor(conf[CONF_FORWARD_ACTIVE_ENERGY])
+            sens = await sensor.new_sensor(conf[CONF_FORWARD_ACTIVE_ENERGY])
             cg.add(var.set_forward_active_energy_sensor(i, sens))
         if CONF_REVERSE_ACTIVE_ENERGY in conf:
-            sens = yield sensor.new_sensor(conf[CONF_REVERSE_ACTIVE_ENERGY])
+            sens = await sensor.new_sensor(conf[CONF_REVERSE_ACTIVE_ENERGY])
             cg.add(var.set_reverse_active_energy_sensor(i, sens))
     if CONF_FREQUENCY in config:
-        sens = yield sensor.new_sensor(config[CONF_FREQUENCY])
+        sens = await sensor.new_sensor(config[CONF_FREQUENCY])
         cg.add(var.set_freq_sensor(sens))
     if CONF_CHIP_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_CHIP_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_CHIP_TEMPERATURE])
         cg.add(var.set_chip_temperature_sensor(sens))
     cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
     cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES]))
diff --git a/esphome/components/b_parasite/sensor.py b/esphome/components/b_parasite/sensor.py
index d90ea84cd3..d93e41816b 100644
--- a/esphome/components/b_parasite/sensor.py
+++ b/esphome/components/b_parasite/sensor.py
@@ -12,6 +12,7 @@ from esphome.const import (
     DEVICE_CLASS_TEMPERATURE,
     DEVICE_CLASS_VOLTAGE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
     UNIT_VOLT,
@@ -32,16 +33,28 @@ CONFIG_SCHEMA = (
             cv.GenerateID(): cv.declare_id(BParasite),
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(
-                UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE
+                UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT
             ),
             cv.Optional(CONF_MOISTURE): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -50,10 +63,10 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
 
@@ -64,5 +77,5 @@ def to_code(config):
         (CONF_MOISTURE, var.set_soil_moisture),
     ]:
         if config_key in config:
-            sens = yield sensor.new_sensor(config[config_key])
+            sens = await sensor.new_sensor(config[config_key])
             cg.add(setter(sens))
diff --git a/esphome/components/bang_bang/climate.py b/esphome/components/bang_bang/climate.py
index e0d75a6a1e..5c935987de 100644
--- a/esphome/components/bang_bang/climate.py
+++ b/esphome/components/bang_bang/climate.py
@@ -39,12 +39,12 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield climate.register_climate(var, config)
+    await cg.register_component(var, config)
+    await climate.register_climate(var, config)
 
-    sens = yield cg.get_variable(config[CONF_SENSOR])
+    sens = await cg.get_variable(config[CONF_SENSOR])
     cg.add(var.set_sensor(sens))
 
     normal_config = BangBangClimateTargetTempConfig(
@@ -53,17 +53,17 @@ def to_code(config):
     )
     cg.add(var.set_normal_config(normal_config))
 
-    yield automation.build_automation(
+    await automation.build_automation(
         var.get_idle_trigger(), [], config[CONF_IDLE_ACTION]
     )
 
     if CONF_COOL_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_cool_trigger(), [], config[CONF_COOL_ACTION]
         )
         cg.add(var.set_supports_cool(True))
     if CONF_HEAT_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_heat_trigger(), [], config[CONF_HEAT_ACTION]
         )
         cg.add(var.set_supports_heat(True))
diff --git a/esphome/components/bh1750/sensor.py b/esphome/components/bh1750/sensor.py
index 0e37b2b865..e688241dcc 100644
--- a/esphome/components/bh1750/sensor.py
+++ b/esphome/components/bh1750/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_RESOLUTION,
     DEVICE_CLASS_ILLUMINANCE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_LUX,
     CONF_MEASUREMENT_DURATION,
 )
@@ -26,7 +27,9 @@ BH1750Sensor = bh1750_ns.class_(
 
 CONF_MEASUREMENT_TIME = "measurement_time"
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_LUX, ICON_EMPTY, 1, DEVICE_CLASS_ILLUMINANCE)
+    sensor.sensor_schema(
+        UNIT_LUX, ICON_EMPTY, 1, DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(BH1750Sensor),
@@ -46,11 +49,11 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
+    await i2c.register_i2c_device(var, config)
 
     cg.add(var.set_resolution(config[CONF_RESOLUTION]))
     cg.add(var.set_measurement_duration(config[CONF_MEASUREMENT_DURATION]))
diff --git a/esphome/components/binary/fan/__init__.py b/esphome/components/binary/fan/__init__.py
index 88dc506235..e6c8d9bfe9 100644
--- a/esphome/components/binary/fan/__init__.py
+++ b/esphome/components/binary/fan/__init__.py
@@ -21,19 +21,19 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
-    fan_ = yield fan.create_fan_state(config)
+    fan_ = await fan.create_fan_state(config)
     cg.add(var.set_fan(fan_))
-    output_ = yield cg.get_variable(config[CONF_OUTPUT])
+    output_ = await cg.get_variable(config[CONF_OUTPUT])
     cg.add(var.set_output(output_))
 
     if CONF_OSCILLATION_OUTPUT in config:
-        oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT])
+        oscillation_output = await cg.get_variable(config[CONF_OSCILLATION_OUTPUT])
         cg.add(var.set_oscillating(oscillation_output))
 
     if CONF_DIRECTION_OUTPUT in config:
-        direction_output = yield cg.get_variable(config[CONF_DIRECTION_OUTPUT])
+        direction_output = await cg.get_variable(config[CONF_DIRECTION_OUTPUT])
         cg.add(var.set_direction(direction_output))
diff --git a/esphome/components/binary/light/__init__.py b/esphome/components/binary/light/__init__.py
index 7e62f4705f..49227ccadc 100644
--- a/esphome/components/binary/light/__init__.py
+++ b/esphome/components/binary/light/__init__.py
@@ -14,9 +14,9 @@ CONFIG_SCHEMA = light.BINARY_LIGHT_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
-    yield light.register_light(var, config)
+    await light.register_light(var, config)
 
-    out = yield cg.get_variable(config[CONF_OUTPUT])
+    out = await cg.get_variable(config[CONF_OUTPUT])
     cg.add(var.set_output(out))
diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py
index 56e6b7dc97..68d4d3e324 100644
--- a/esphome/components/binary_sensor/__init__.py
+++ b/esphome/components/binary_sensor/__init__.py
@@ -4,6 +4,7 @@ from esphome import automation, core
 from esphome.automation import Condition, maybe_simple_id
 from esphome.components import mqtt
 from esphome.const import (
+    CONF_DELAY,
     CONF_DEVICE_CLASS,
     CONF_FILTERS,
     CONF_ID,
@@ -50,7 +51,7 @@ from esphome.const import (
     DEVICE_CLASS_VIBRATION,
     DEVICE_CLASS_WINDOW,
 )
-from esphome.core import CORE, coroutine, coroutine_with_priority
+from esphome.core import CORE, coroutine_with_priority
 from esphome.util import Registry
 
 CODEOWNERS = ["@esphome/core"]
@@ -120,6 +121,7 @@ DelayedOnOffFilter = binary_sensor_ns.class_("DelayedOnOffFilter", Filter, cg.Co
 DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter, cg.Component)
 DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Component)
 InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
+AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
 LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
 
 FILTER_REGISTRY = Registry()
@@ -127,43 +129,88 @@ validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
 
 
 @FILTER_REGISTRY.register("invert", InvertFilter, {})
-def invert_filter_to_code(config, filter_id):
-    yield cg.new_Pvariable(filter_id)
+async def invert_filter_to_code(config, filter_id):
+    return cg.new_Pvariable(filter_id)
 
 
 @FILTER_REGISTRY.register(
     "delayed_on_off", DelayedOnOffFilter, cv.positive_time_period_milliseconds
 )
-def delayed_on_off_filter_to_code(config, filter_id):
+async def delayed_on_off_filter_to_code(config, filter_id):
     var = cg.new_Pvariable(filter_id, config)
-    yield cg.register_component(var, {})
-    yield var
+    await cg.register_component(var, {})
+    return var
 
 
 @FILTER_REGISTRY.register(
     "delayed_on", DelayedOnFilter, cv.positive_time_period_milliseconds
 )
-def delayed_on_filter_to_code(config, filter_id):
+async def delayed_on_filter_to_code(config, filter_id):
     var = cg.new_Pvariable(filter_id, config)
-    yield cg.register_component(var, {})
-    yield var
+    await cg.register_component(var, {})
+    return var
 
 
 @FILTER_REGISTRY.register(
     "delayed_off", DelayedOffFilter, cv.positive_time_period_milliseconds
 )
-def delayed_off_filter_to_code(config, filter_id):
+async def delayed_off_filter_to_code(config, filter_id):
     var = cg.new_Pvariable(filter_id, config)
-    yield cg.register_component(var, {})
-    yield var
+    await cg.register_component(var, {})
+    return var
+
+
+CONF_TIME_OFF = "time_off"
+CONF_TIME_ON = "time_on"
+
+DEFAULT_DELAY = "1s"
+DEFAULT_TIME_OFF = "100ms"
+DEFAULT_TIME_ON = "900ms"
+
+
+@FILTER_REGISTRY.register(
+    "autorepeat",
+    AutorepeatFilter,
+    cv.All(
+        cv.ensure_list(
+            {
+                cv.Optional(
+                    CONF_DELAY, default=DEFAULT_DELAY
+                ): cv.positive_time_period_milliseconds,
+                cv.Optional(
+                    CONF_TIME_OFF, default=DEFAULT_TIME_OFF
+                ): cv.positive_time_period_milliseconds,
+                cv.Optional(
+                    CONF_TIME_ON, default=DEFAULT_TIME_ON
+                ): cv.positive_time_period_milliseconds,
+            }
+        ),
+    ),
+)
+async def autorepeat_filter_to_code(config, filter_id):
+    timings = []
+    if len(config) > 0:
+        for conf in config:
+            timings.append((conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON]))
+    else:
+        timings.append(
+            (
+                cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds,
+                cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds,
+                cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds,
+            )
+        )
+    var = cg.new_Pvariable(filter_id, timings)
+    await cg.register_component(var, {})
+    return var
 
 
 @FILTER_REGISTRY.register("lambda", LambdaFilter, cv.returning_lambda)
-def lambda_filter_to_code(config, filter_id):
-    lambda_ = yield cg.process_lambda(
+async def lambda_filter_to_code(config, filter_id):
+    lambda_ = await cg.process_lambda(
         config, [(bool, "x")], return_type=cg.optional.template(bool)
     )
-    yield cg.new_Pvariable(filter_id, lambda_)
+    return cg.new_Pvariable(filter_id, lambda_)
 
 
 MULTI_CLICK_TIMING_SCHEMA = cv.Schema(
@@ -334,8 +381,7 @@ BINARY_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend(
 )
 
 
-@coroutine
-def setup_binary_sensor_core_(var, config):
+async def setup_binary_sensor_core_(var, config):
     cg.add(var.set_name(config[CONF_NAME]))
     if CONF_INTERNAL in config:
         cg.add(var.set_internal(config[CONF_INTERNAL]))
@@ -344,28 +390,28 @@ def setup_binary_sensor_core_(var, config):
     if CONF_INVERTED in config:
         cg.add(var.set_inverted(config[CONF_INVERTED]))
     if CONF_FILTERS in config:
-        filters = yield cg.build_registry_list(FILTER_REGISTRY, config[CONF_FILTERS])
+        filters = await cg.build_registry_list(FILTER_REGISTRY, config[CONF_FILTERS])
         cg.add(var.add_filters(filters))
 
     for conf in config.get(CONF_ON_PRESS, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [], conf)
+        await automation.build_automation(trigger, [], conf)
 
     for conf in config.get(CONF_ON_RELEASE, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [], conf)
+        await automation.build_automation(trigger, [], conf)
 
     for conf in config.get(CONF_ON_CLICK, []):
         trigger = cg.new_Pvariable(
             conf[CONF_TRIGGER_ID], var, conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH]
         )
-        yield automation.build_automation(trigger, [], conf)
+        await automation.build_automation(trigger, [], conf)
 
     for conf in config.get(CONF_ON_DOUBLE_CLICK, []):
         trigger = cg.new_Pvariable(
             conf[CONF_TRIGGER_ID], var, conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH]
         )
-        yield automation.build_automation(trigger, [], conf)
+        await automation.build_automation(trigger, [], conf)
 
     for conf in config.get(CONF_ON_MULTI_CLICK, []):
         timings = []
@@ -381,31 +427,29 @@ def setup_binary_sensor_core_(var, config):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, timings)
         if CONF_INVALID_COOLDOWN in conf:
             cg.add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN]))
-        yield cg.register_component(trigger, conf)
-        yield automation.build_automation(trigger, [], conf)
+        await cg.register_component(trigger, conf)
+        await automation.build_automation(trigger, [], conf)
 
     for conf in config.get(CONF_ON_STATE, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [(bool, "x")], conf)
+        await automation.build_automation(trigger, [(bool, "x")], conf)
 
     if CONF_MQTT_ID in config:
         mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
-        yield mqtt.register_mqtt_component(mqtt_, config)
+        await mqtt.register_mqtt_component(mqtt_, config)
 
 
-@coroutine
-def register_binary_sensor(var, config):
+async def register_binary_sensor(var, config):
     if not CORE.has_id(config[CONF_ID]):
         var = cg.Pvariable(config[CONF_ID], var)
     cg.add(cg.App.register_binary_sensor(var))
-    yield setup_binary_sensor_core_(var, config)
+    await setup_binary_sensor_core_(var, config)
 
 
-@coroutine
-def new_binary_sensor(config):
+async def new_binary_sensor(config):
     var = cg.new_Pvariable(config[CONF_ID], config[CONF_NAME])
-    yield register_binary_sensor(var, config)
-    yield var
+    await register_binary_sensor(var, config)
+    return var
 
 
 BINARY_SENSOR_CONDITION_SCHEMA = maybe_simple_id(
@@ -422,20 +466,20 @@ BINARY_SENSOR_CONDITION_SCHEMA = maybe_simple_id(
 @automation.register_condition(
     "binary_sensor.is_on", BinarySensorCondition, BINARY_SENSOR_CONDITION_SCHEMA
 )
-def binary_sensor_is_on_to_code(config, condition_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(condition_id, template_arg, paren, True)
+async def binary_sensor_is_on_to_code(config, condition_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(condition_id, template_arg, paren, True)
 
 
 @automation.register_condition(
     "binary_sensor.is_off", BinarySensorCondition, BINARY_SENSOR_CONDITION_SCHEMA
 )
-def binary_sensor_is_off_to_code(config, condition_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(condition_id, template_arg, paren, False)
+async def binary_sensor_is_off_to_code(config, condition_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(condition_id, template_arg, paren, False)
 
 
 @coroutine_with_priority(100.0)
-def to_code(config):
+async def to_code(config):
     cg.add_define("USE_BINARY_SENSOR")
     cg.add_global(binary_sensor_ns.using)
diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp
index f4612d62e9..c6ca3e2f79 100644
--- a/esphome/components/binary_sensor/filter.cpp
+++ b/esphome/components/binary_sensor/filter.cpp
@@ -64,6 +64,50 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD
 
 optional<bool> InvertFilter::new_value(bool value, bool is_initial) { return !value; }
 
+AutorepeatFilter::AutorepeatFilter(const std::vector<AutorepeatFilterTiming> &timings) : timings_(timings) {}
+
+optional<bool> AutorepeatFilter::new_value(bool value, bool is_initial) {
+  if (value) {
+    // Ignore if already running
+    if (this->active_timing_ != 0)
+      return {};
+
+    this->next_timing_();
+    return true;
+  } else {
+    this->cancel_timeout("TIMING");
+    this->cancel_timeout("ON_OFF");
+    this->active_timing_ = 0;
+    return false;
+  }
+}
+
+void AutorepeatFilter::next_timing_() {
+  // Entering this method
+  // 1st time: starts waiting the first delay
+  // 2nd time: starts waiting the second delay and starts toggling with the first time_off / _on
+  // last time: no delay to start but have to bump the index to reflect the last
+  if (this->active_timing_ < this->timings_.size())
+    this->set_timeout("TIMING", this->timings_[this->active_timing_].delay, [this]() { this->next_timing_(); });
+
+  if (this->active_timing_ <= this->timings_.size()) {
+    this->active_timing_++;
+  }
+
+  if (this->active_timing_ == 2)
+    this->next_value_(false);
+
+  // Leaving this method: if the toggling is started, it has to use [active_timing_ - 2] for the intervals
+}
+
+void AutorepeatFilter::next_value_(bool val) {
+  const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
+  this->output(val, false);  // This is at least the second one so not initial
+  this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); });
+}
+
+float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
+
 LambdaFilter::LambdaFilter(const std::function<optional<bool>(bool)> &f) : f_(f) {}
 
 optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); }
diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h
index 0b54251cda..8528e74a9f 100644
--- a/esphome/components/binary_sensor/filter.h
+++ b/esphome/components/binary_sensor/filter.h
@@ -66,6 +66,33 @@ class InvertFilter : public Filter {
   optional<bool> new_value(bool value, bool is_initial) override;
 };
 
+struct AutorepeatFilterTiming {
+  AutorepeatFilterTiming(uint32_t delay, uint32_t off, uint32_t on) {
+    this->delay = delay;
+    this->time_off = off;
+    this->time_on = on;
+  }
+  uint32_t delay;
+  uint32_t time_off;
+  uint32_t time_on;
+};
+
+class AutorepeatFilter : public Filter, public Component {
+ public:
+  explicit AutorepeatFilter(const std::vector<AutorepeatFilterTiming> &timings);
+
+  optional<bool> new_value(bool value, bool is_initial) override;
+
+  float get_setup_priority() const override;
+
+ protected:
+  void next_timing_();
+  void next_value_(bool val);
+
+  std::vector<AutorepeatFilterTiming> timings_;
+  uint8_t active_timing_{0};
+};
+
 class LambdaFilter : public Filter {
  public:
   explicit LambdaFilter(const std::function<optional<bool>(bool)> &f);
diff --git a/esphome/components/binary_sensor_map/sensor.py b/esphome/components/binary_sensor_map/sensor.py
index 81bc85e570..131a050052 100644
--- a/esphome/components/binary_sensor_map/sensor.py
+++ b/esphome/components/binary_sensor_map/sensor.py
@@ -12,6 +12,7 @@ from esphome.const import (
     ICON_CHECK_CIRCLE_OUTLINE,
     CONF_BINARY_SENSOR,
     CONF_GROUP,
+    STATE_CLASS_NONE,
 )
 
 DEPENDENCIES = ["binary_sensor"]
@@ -34,7 +35,11 @@ entry = {
 CONFIG_SCHEMA = cv.typed_schema(
     {
         CONF_GROUP: sensor.sensor_schema(
-            UNIT_EMPTY, ICON_CHECK_CIRCLE_OUTLINE, 0, DEVICE_CLASS_EMPTY
+            UNIT_EMPTY,
+            ICON_CHECK_CIRCLE_OUTLINE,
+            0,
+            DEVICE_CLASS_EMPTY,
+            STATE_CLASS_NONE,
         ).extend(
             {
                 cv.GenerateID(): cv.declare_id(BinarySensorMap),
@@ -48,14 +53,14 @@ CONFIG_SCHEMA = cv.typed_schema(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
 
     constant = SENSOR_MAP_TYPES[config[CONF_TYPE]]
     cg.add(var.set_sensor_type(constant))
 
     for ch in config[CONF_CHANNELS]:
-        input_var = yield cg.get_variable(ch[CONF_BINARY_SENSOR])
+        input_var = await cg.get_variable(ch[CONF_BINARY_SENSOR])
         cg.add(var.add_channel(input_var, ch[CONF_VALUE]))
diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py
index d3b287574b..4bd5c25246 100644
--- a/esphome/components/ble_client/__init__.py
+++ b/esphome/components/ble_client/__init__.py
@@ -9,7 +9,6 @@ from esphome.const import (
     CONF_ON_DISCONNECT,
     CONF_TRIGGER_ID,
 )
-from esphome.core import coroutine
 from esphome import automation
 
 CODEOWNERS = ["@buxtronix"]
@@ -68,20 +67,19 @@ BLE_CLIENT_SCHEMA = cv.Schema(
 )
 
 
-@coroutine
-def register_ble_node(var, config):
-    parent = yield cg.get_variable(config[CONF_BLE_CLIENT_ID])
+async def register_ble_node(var, config):
+    parent = await cg.get_variable(config[CONF_BLE_CLIENT_ID])
     cg.add(parent.register_ble_node(var))
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_client(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_client(var, config)
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
     for conf in config.get(CONF_ON_CONNECT, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [], conf)
+        await automation.build_automation(trigger, [], conf)
     for conf in config.get(CONF_ON_DISCONNECT, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [], conf)
+        await automation.build_automation(trigger, [], conf)
diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp
index 3819a6e560..f12d3a21c9 100644
--- a/esphome/components/ble_client/ble_client.cpp
+++ b/esphome/components/ble_client/ble_client.cpp
@@ -94,7 +94,7 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es
                                     esp_ble_gattc_cb_param_t *param) {
   if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id)
     return;
-  if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && gattc_if != this->gattc_if)
+  if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && esp_gattc_if != this->gattc_if)
     return;
 
   bool all_established = this->all_nodes_established();
diff --git a/esphome/components/ble_client/sensor/__init__.py b/esphome/components/ble_client/sensor/__init__.py
index 27eca87a37..c6f05932ef 100644
--- a/esphome/components/ble_client/sensor/__init__.py
+++ b/esphome/components/ble_client/sensor/__init__.py
@@ -4,6 +4,8 @@ from esphome.components import sensor, ble_client, esp32_ble_tracker
 from esphome.const import (
     DEVICE_CLASS_EMPTY,
     CONF_ID,
+    CONF_LAMBDA,
+    STATE_CLASS_NONE,
     UNIT_EMPTY,
     ICON_EMPTY,
     CONF_TRIGGER_ID,
@@ -20,6 +22,9 @@ CONF_DESCRIPTOR_UUID = "descriptor_uuid"
 CONF_NOTIFY = "notify"
 CONF_ON_NOTIFY = "on_notify"
 
+adv_data_t = cg.std_vector.template(cg.uint8)
+adv_data_t_const_ref = adv_data_t.operator("ref").operator("const")
+
 BLESensor = ble_client_ns.class_(
     "BLESensor", sensor.Sensor, cg.PollingComponent, ble_client.BLEClientNode
 )
@@ -28,13 +33,16 @@ BLESensorNotifyTrigger = ble_client_ns.class_(
 )
 
 CONFIG_SCHEMA = cv.All(
-    sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY)
+    sensor.sensor_schema(
+        UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(BLESensor),
             cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
             cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid,
             cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid,
+            cv.Optional(CONF_LAMBDA): cv.returning_lambda,
             cv.Optional(CONF_NOTIFY, default=False): cv.boolean,
             cv.Optional(CONF_ON_NOTIFY): automation.validate_automation(
                 {
@@ -50,7 +58,7 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
         cg.add(
@@ -105,11 +113,17 @@ def to_code(config):
             uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_DESCRIPTOR_UUID])
             cg.add(var.set_descr_uuid128(uuid128))
 
-    yield cg.register_component(var, config)
-    yield ble_client.register_ble_node(var, config)
+    if CONF_LAMBDA in config:
+        lambda_ = await cg.process_lambda(
+            config[CONF_LAMBDA], [(adv_data_t_const_ref, "x")], return_type=cg.float_
+        )
+        cg.add(var.set_data_to_value(lambda_))
+
+    await cg.register_component(var, config)
+    await ble_client.register_ble_node(var, config)
     cg.add(var.set_enable_notify(config[CONF_NOTIFY]))
-    yield sensor.register_sensor(var, config)
+    await sensor.register_sensor(var, config)
     for conf in config.get(CONF_ON_NOTIFY, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield ble_client.register_ble_node(trigger, config)
-        yield automation.build_automation(trigger, [(float, "x")], conf)
+        await ble_client.register_ble_node(trigger, config)
+        await automation.build_automation(trigger, [(float, "x")], conf)
diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp
index ef1d6c120f..69a4b23313 100644
--- a/esphome/components/ble_client/sensor/ble_sensor.cpp
+++ b/esphome/components/ble_client/sensor/ble_sensor.cpp
@@ -84,7 +84,7 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
       }
       if (param->read.handle == this->handle) {
         this->status_clear_warning();
-        this->publish_state((float) param->read.value[0]);
+        this->publish_state(this->parse_data(param->read.value, param->read.value_len));
       }
       break;
     }
@@ -93,7 +93,7 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
         break;
       ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
                param->notify.handle, param->notify.value[0]);
-      this->publish_state((float) param->notify.value[0]);
+      this->publish_state(this->parse_data(param->notify.value, param->notify.value_len));
       break;
     }
     case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
@@ -105,6 +105,15 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
   }
 }
 
+float BLESensor::parse_data(uint8_t *value, uint16_t value_len) {
+  if (this->data_to_value_func_.has_value()) {
+    std::vector<uint8_t> data(value, value + value_len);
+    return (*this->data_to_value_func_)(data);
+  } else {
+    return value[0];
+  }
+}
+
 void BLESensor::update() {
   if (this->node_state != espbt::ClientState::Established) {
     ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->get_name().c_str());
diff --git a/esphome/components/ble_client/sensor/ble_sensor.h b/esphome/components/ble_client/sensor/ble_sensor.h
index e3a8a8ea25..25e996b6ee 100644
--- a/esphome/components/ble_client/sensor/ble_sensor.h
+++ b/esphome/components/ble_client/sensor/ble_sensor.h
@@ -13,6 +13,8 @@ namespace ble_client {
 
 namespace espbt = esphome::esp32_ble_tracker;
 
+using data_to_value_t = std::function<float(std::vector<uint8_t>)>;
+
 class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode {
  public:
   void loop() override;
@@ -30,11 +32,14 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie
   void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
   void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
   void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
+  void set_data_to_value(data_to_value_t &&lambda_) { this->data_to_value_func_ = lambda_; }
   void set_enable_notify(bool notify) { this->notify_ = notify; }
   uint16_t handle;
 
  protected:
   uint32_t hash_base() override;
+  float parse_data(uint8_t *value, uint16_t value_len);
+  optional<data_to_value_t> data_to_value_func_{};
   bool notify_;
   espbt::ESPBTUUID service_uuid_;
   espbt::ESPBTUUID char_uuid_;
diff --git a/esphome/components/ble_client/switch/__init__.py b/esphome/components/ble_client/switch/__init__.py
index acc8683407..e5b5ab281b 100644
--- a/esphome/components/ble_client/switch/__init__.py
+++ b/esphome/components/ble_client/switch/__init__.py
@@ -23,8 +23,8 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield switch.register_switch(var, config)
-    yield ble_client.register_ble_node(var, config)
+    await cg.register_component(var, config)
+    await switch.register_switch(var, config)
+    await ble_client.register_ble_node(var, config)
diff --git a/esphome/components/ble_presence/binary_sensor.py b/esphome/components/ble_presence/binary_sensor.py
index 4c6e7ee567..c58d29e6be 100644
--- a/esphome/components/ble_presence/binary_sensor.py
+++ b/esphome/components/ble_presence/binary_sensor.py
@@ -27,11 +27,11 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
-    yield binary_sensor.register_binary_sensor(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
+    await binary_sensor.register_binary_sensor(var, config)
 
     if CONF_MAC_ADDRESS in config:
         cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
diff --git a/esphome/components/ble_rssi/sensor.py b/esphome/components/ble_rssi/sensor.py
index f6ee209ae1..819b7c6fd7 100644
--- a/esphome/components/ble_rssi/sensor.py
+++ b/esphome/components/ble_rssi/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_MAC_ADDRESS,
     CONF_ID,
     DEVICE_CLASS_SIGNAL_STRENGTH,
+    STATE_CLASS_MEASUREMENT,
     UNIT_DECIBEL,
     ICON_EMPTY,
 )
@@ -18,7 +19,13 @@ BLERSSISensor = ble_rssi_ns.class_(
 )
 
 CONFIG_SCHEMA = cv.All(
-    sensor.sensor_schema(UNIT_DECIBEL, ICON_EMPTY, 0, DEVICE_CLASS_SIGNAL_STRENGTH)
+    sensor.sensor_schema(
+        UNIT_DECIBEL,
+        ICON_EMPTY,
+        0,
+        DEVICE_CLASS_SIGNAL_STRENGTH,
+        STATE_CLASS_MEASUREMENT,
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(BLERSSISensor),
@@ -32,11 +39,11 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
+    await sensor.register_sensor(var, config)
 
     if CONF_MAC_ADDRESS in config:
         cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
diff --git a/esphome/components/ble_scanner/text_sensor.py b/esphome/components/ble_scanner/text_sensor.py
index 96c7b4d887..0e140aa701 100644
--- a/esphome/components/ble_scanner/text_sensor.py
+++ b/esphome/components/ble_scanner/text_sensor.py
@@ -24,8 +24,8 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
-    yield text_sensor.register_text_sensor(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
+    await text_sensor.register_text_sensor(var, config)
diff --git a/esphome/components/bme280/sensor.py b/esphome/components/bme280/sensor.py
index 5b0e418a66..8c6cc7ae56 100644
--- a/esphome/components/bme280/sensor.py
+++ b/esphome/components/bme280/sensor.py
@@ -12,6 +12,7 @@ from esphome.const import (
     DEVICE_CLASS_PRESSURE,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_HECTOPASCAL,
     UNIT_PERCENT,
@@ -48,7 +49,11 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(BME280Component),
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ).extend(
                 {
                     cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
@@ -57,7 +62,11 @@ CONFIG_SCHEMA = (
                 }
             ),
             cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
-                UNIT_HECTOPASCAL, ICON_EMPTY, 1, DEVICE_CLASS_PRESSURE
+                UNIT_HECTOPASCAL,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_PRESSURE,
+                STATE_CLASS_MEASUREMENT,
             ).extend(
                 {
                     cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
@@ -66,7 +75,11 @@ CONFIG_SCHEMA = (
                 }
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ).extend(
                 {
                     cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
@@ -84,26 +97,26 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     if CONF_TEMPERATURE in config:
         conf = config[CONF_TEMPERATURE]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_temperature_sensor(sens))
         cg.add(var.set_temperature_oversampling(conf[CONF_OVERSAMPLING]))
 
     if CONF_PRESSURE in config:
         conf = config[CONF_PRESSURE]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_pressure_sensor(sens))
         cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING]))
 
     if CONF_HUMIDITY in config:
         conf = config[CONF_HUMIDITY]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_humidity_sensor(sens))
         cg.add(var.set_humidity_oversampling(conf[CONF_OVERSAMPLING]))
 
diff --git a/esphome/components/bme680/sensor.py b/esphome/components/bme680/sensor.py
index a82ecefe99..eaa158c9f8 100644
--- a/esphome/components/bme680/sensor.py
+++ b/esphome/components/bme680/sensor.py
@@ -16,6 +16,7 @@ from esphome.const import (
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_PRESSURE,
     DEVICE_CLASS_TEMPERATURE,
+    STATE_CLASS_MEASUREMENT,
     UNIT_OHM,
     ICON_GAS_CYLINDER,
     UNIT_CELSIUS,
@@ -58,7 +59,11 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(BME680Component),
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ).extend(
                 {
                     cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
@@ -67,7 +72,11 @@ CONFIG_SCHEMA = (
                 }
             ),
             cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
-                UNIT_HECTOPASCAL, ICON_EMPTY, 1, DEVICE_CLASS_PRESSURE
+                UNIT_HECTOPASCAL,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_PRESSURE,
+                STATE_CLASS_MEASUREMENT,
             ).extend(
                 {
                     cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
@@ -76,7 +85,11 @@ CONFIG_SCHEMA = (
                 }
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ).extend(
                 {
                     cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
@@ -85,7 +98,11 @@ CONFIG_SCHEMA = (
                 }
             ),
             cv.Optional(CONF_GAS_RESISTANCE): sensor.sensor_schema(
-                UNIT_OHM, ICON_GAS_CYLINDER, 1, DEVICE_CLASS_EMPTY
+                UNIT_OHM,
+                ICON_GAS_CYLINDER,
+                1,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
                 IIR_FILTER_OPTIONS, upper=True
@@ -114,32 +131,32 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     if CONF_TEMPERATURE in config:
         conf = config[CONF_TEMPERATURE]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_temperature_sensor(sens))
         cg.add(var.set_temperature_oversampling(conf[CONF_OVERSAMPLING]))
 
     if CONF_PRESSURE in config:
         conf = config[CONF_PRESSURE]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_pressure_sensor(sens))
         cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING]))
 
     if CONF_HUMIDITY in config:
         conf = config[CONF_HUMIDITY]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_humidity_sensor(sens))
         cg.add(var.set_humidity_oversampling(conf[CONF_OVERSAMPLING]))
 
     if CONF_GAS_RESISTANCE in config:
         conf = config[CONF_GAS_RESISTANCE]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_gas_resistance_sensor(sens))
 
     cg.add(var.set_iir_filter(IIR_FILTER_OPTIONS[config[CONF_IIR_FILTER]]))
diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py
index 8286029c3b..d258819aa4 100644
--- a/esphome/components/bme680_bsec/__init__.py
+++ b/esphome/components/bme680_bsec/__init__.py
@@ -48,10 +48,10 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(i2c.i2c_device_schema(0x76))
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET]))
     cg.add(var.set_iaq_mode(config[CONF_IAQ_MODE]))
@@ -60,5 +60,5 @@ def to_code(config):
         var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds)
     )
 
-    cg.add_define("USING_BSEC")
+    cg.add_define("USE_BSEC")
     cg.add_library("BSEC Software Library", "1.6.1480")
diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp
index a463ff78c4..756629383c 100644
--- a/esphome/components/bme680_bsec/bme680_bsec.cpp
+++ b/esphome/components/bme680_bsec/bme680_bsec.cpp
@@ -1,5 +1,3 @@
-
-
 #include "bme680_bsec.h"
 #include "esphome/core/log.h"
 #include "esphome/core/helpers.h"
@@ -7,7 +5,7 @@
 
 namespace esphome {
 namespace bme680_bsec {
-#ifdef USING_BSEC
+#ifdef USE_BSEC
 static const char *TAG = "bme680_bsec.sensor";
 
 static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"};
@@ -30,7 +28,6 @@ void BME680BSECComponent::setup() {
   this->bme680_.write = BME680BSECComponent::write_bytes_wrapper;
   this->bme680_.delay_ms = BME680BSECComponent::delay_ms;
   this->bme680_.amb_temp = 25;
-  this->bme680_.power_mode = BME680_FORCED_MODE;
 
   this->bme680_status_ = bme680_init(&this->bme680_);
   if (this->bme680_status_ != BME680_OK) {
@@ -43,14 +40,13 @@ void BME680BSECComponent::setup() {
 #include "config/generic_33v_300s_28d/bsec_iaq.txt"
     };
     this->set_config_(bsec_config);
-    this->update_subscription_(BSEC_SAMPLE_RATE_ULP);
   } else {
     const uint8_t bsec_config[] = {
 #include "config/generic_33v_3s_28d/bsec_iaq.txt"
     };
     this->set_config_(bsec_config);
-    this->update_subscription_(BSEC_SAMPLE_RATE_LP);
   }
+  this->update_subscription_();
   if (this->bsec_status_ != BSEC_OK) {
     this->mark_failed();
     return;
@@ -64,50 +60,57 @@ void BME680BSECComponent::set_config_(const uint8_t *config) {
   this->bsec_status_ = bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, work_buffer, sizeof(work_buffer));
 }
 
-void BME680BSECComponent::update_subscription_(float sample_rate) {
+float BME680BSECComponent::calc_sensor_sample_rate_(SampleRate sample_rate) {
+  if (sample_rate == SAMPLE_RATE_DEFAULT) {
+    sample_rate = this->sample_rate_;
+  }
+  return sample_rate == SAMPLE_RATE_ULP ? BSEC_SAMPLE_RATE_ULP : BSEC_SAMPLE_RATE_LP;
+}
+
+void BME680BSECComponent::update_subscription_() {
   bsec_sensor_configuration_t virtual_sensors[BSEC_NUMBER_OUTPUTS];
   int num_virtual_sensors = 0;
 
   if (this->iaq_sensor_) {
     virtual_sensors[num_virtual_sensors].sensor_id =
         this->iaq_mode_ == IAQ_MODE_STATIC ? BSEC_OUTPUT_STATIC_IAQ : BSEC_OUTPUT_IAQ;
-    virtual_sensors[num_virtual_sensors].sample_rate = sample_rate;
+    virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT);
     num_virtual_sensors++;
   }
 
   if (this->co2_equivalent_sensor_) {
     virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_CO2_EQUIVALENT;
-    virtual_sensors[num_virtual_sensors].sample_rate = sample_rate;
+    virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT);
     num_virtual_sensors++;
   }
 
   if (this->breath_voc_equivalent_sensor_) {
     virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_BREATH_VOC_EQUIVALENT;
-    virtual_sensors[num_virtual_sensors].sample_rate = sample_rate;
+    virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT);
     num_virtual_sensors++;
   }
 
   if (this->pressure_sensor_) {
     virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_PRESSURE;
-    virtual_sensors[num_virtual_sensors].sample_rate = sample_rate;
+    virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(this->pressure_sample_rate_);
     num_virtual_sensors++;
   }
 
   if (this->gas_resistance_sensor_) {
     virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_GAS;
-    virtual_sensors[num_virtual_sensors].sample_rate = sample_rate;
+    virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT);
     num_virtual_sensors++;
   }
 
   if (this->temperature_sensor_) {
     virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE;
-    virtual_sensors[num_virtual_sensors].sample_rate = sample_rate;
+    virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(this->temperature_sample_rate_);
     num_virtual_sensors++;
   }
 
   if (this->humidity_sensor_) {
     virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY;
-    virtual_sensors[num_virtual_sensors].sample_rate = sample_rate;
+    virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(this->humidity_sample_rate_);
     num_virtual_sensors++;
   }
 
@@ -134,12 +137,15 @@ void BME680BSECComponent::dump_config() {
 
   ESP_LOGCONFIG(TAG, "  Temperature Offset: %.2f", this->temperature_offset_);
   ESP_LOGCONFIG(TAG, "  IAQ Mode: %s", this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile");
-  ESP_LOGCONFIG(TAG, "  Sample Rate: %s", this->sample_rate_ == SAMPLE_RATE_ULP ? "ULP" : "LP");
+  ESP_LOGCONFIG(TAG, "  Sample Rate: %s", BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_));
   ESP_LOGCONFIG(TAG, "  State Save Interval: %ims", this->state_save_interval_ms_);
 
   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
+  ESP_LOGCONFIG(TAG, "    Sample Rate: %s", BME680_BSEC_SAMPLE_RATE_LOG(this->temperature_sample_rate_));
   LOG_SENSOR("  ", "Pressure", this->pressure_sensor_);
+  ESP_LOGCONFIG(TAG, "    Sample Rate: %s", BME680_BSEC_SAMPLE_RATE_LOG(this->pressure_sample_rate_));
   LOG_SENSOR("  ", "Humidity", this->humidity_sensor_);
+  ESP_LOGCONFIG(TAG, "    Sample Rate: %s", BME680_BSEC_SAMPLE_RATE_LOG(this->humidity_sample_rate_));
   LOG_SENSOR("  ", "Gas Resistance", this->gas_resistance_sensor_);
   LOG_SENSOR("  ", "IAQ", this->iaq_sensor_);
   LOG_SENSOR("  ", "Numeric IAQ Accuracy", this->iaq_accuracy_sensor_);
@@ -181,34 +187,55 @@ void BME680BSECComponent::run_() {
   }
   this->next_call_ns_ = bme680_settings.next_call;
 
-  this->bme680_.gas_sett.run_gas = bme680_settings.run_gas;
-  this->bme680_.tph_sett.os_hum = bme680_settings.humidity_oversampling;
-  this->bme680_.tph_sett.os_temp = bme680_settings.temperature_oversampling;
-  this->bme680_.tph_sett.os_pres = bme680_settings.pressure_oversampling;
-  this->bme680_.gas_sett.heatr_temp = bme680_settings.heater_temperature;
-  this->bme680_.gas_sett.heatr_dur = bme680_settings.heating_duration;
-  uint16_t desired_settings =
-      BME680_OST_SEL | BME680_OSP_SEL | BME680_OSH_SEL | BME680_FILTER_SEL | BME680_GAS_SENSOR_SEL;
-  this->bme680_status_ = bme680_set_sensor_settings(desired_settings, &this->bme680_);
-  if (this->bme680_status_ != BME680_OK) {
-    ESP_LOGW(TAG, "Failed to set sensor settings (BME680 Error Code %d)", this->bme680_status_);
-    return;
-  }
+  if (bme680_settings.trigger_measurement) {
+    this->bme680_.tph_sett.os_temp = bme680_settings.temperature_oversampling;
+    this->bme680_.tph_sett.os_pres = bme680_settings.pressure_oversampling;
+    this->bme680_.tph_sett.os_hum = bme680_settings.humidity_oversampling;
+    this->bme680_.gas_sett.run_gas = bme680_settings.run_gas;
+    this->bme680_.gas_sett.heatr_temp = bme680_settings.heater_temperature;
+    this->bme680_.gas_sett.heatr_dur = bme680_settings.heating_duration;
+    this->bme680_.power_mode = BME680_FORCED_MODE;
+    uint16_t desired_settings = BME680_OST_SEL | BME680_OSP_SEL | BME680_OSH_SEL | BME680_GAS_SENSOR_SEL;
+    this->bme680_status_ = bme680_set_sensor_settings(desired_settings, &this->bme680_);
+    if (this->bme680_status_ != BME680_OK) {
+      ESP_LOGW(TAG, "Failed to set sensor settings (BME680 Error Code %d)", this->bme680_status_);
+      return;
+    }
 
-  this->bme680_status_ = bme680_set_sensor_mode(&this->bme680_);
-  if (this->bme680_status_ != BME680_OK) {
-    ESP_LOGW(TAG, "Failed to set sensor mode (BME680 Error Code %d)", this->bme680_status_);
-    return;
-  }
+    this->bme680_status_ = bme680_set_sensor_mode(&this->bme680_);
+    if (this->bme680_status_ != BME680_OK) {
+      ESP_LOGW(TAG, "Failed to set sensor mode (BME680 Error Code %d)", this->bme680_status_);
+      return;
+    }
 
-  uint16_t meas_dur = 0;
-  bme680_get_profile_dur(&meas_dur, &this->bme680_);
-  ESP_LOGV(TAG, "Queueing read in %ums", meas_dur);
-  this->set_timeout("read", meas_dur, [this, bme680_settings]() { this->read_(bme680_settings); });
+    uint16_t meas_dur = 0;
+    bme680_get_profile_dur(&meas_dur, &this->bme680_);
+    ESP_LOGV(TAG, "Queueing read in %ums", meas_dur);
+    this->set_timeout("read", meas_dur,
+                      [this, curr_time_ns, bme680_settings]() { this->read_(curr_time_ns, bme680_settings); });
+  } else {
+    ESP_LOGV(TAG, "Measurement not required");
+    this->read_(curr_time_ns, bme680_settings);
+  }
 }
 
-void BME680BSECComponent::read_(bsec_bme_settings_t bme680_settings) {
+void BME680BSECComponent::read_(int64_t trigger_time_ns, bsec_bme_settings_t bme680_settings) {
   ESP_LOGV(TAG, "Reading data");
+
+  if (bme680_settings.trigger_measurement) {
+    while (this->bme680_.power_mode != BME680_SLEEP_MODE) {
+      this->bme680_status_ = bme680_get_sensor_mode(&this->bme680_);
+      if (this->bme680_status_ != BME680_OK) {
+        ESP_LOGW(TAG, "Failed to get sensor mode (BME680 Error Code %d)", this->bme680_status_);
+      }
+    }
+  }
+
+  if (!bme680_settings.process_data) {
+    ESP_LOGV(TAG, "Data processing not required");
+    return;
+  }
+
   struct bme680_field_data data;
   this->bme680_status_ = bme680_get_sensor_data(&data, &this->bme680_);
 
@@ -223,37 +250,40 @@ void BME680BSECComponent::read_(bsec_bme_settings_t bme680_settings) {
 
   bsec_input_t inputs[BSEC_MAX_PHYSICAL_SENSOR];  // Temperature, Pressure, Humidity & Gas Resistance
   uint8_t num_inputs = 0;
-  int64_t curr_time_ns = this->get_time_ns_();
 
   if (bme680_settings.process_data & BSEC_PROCESS_TEMPERATURE) {
     inputs[num_inputs].sensor_id = BSEC_INPUT_TEMPERATURE;
     inputs[num_inputs].signal = data.temperature / 100.0f;
-    inputs[num_inputs].time_stamp = curr_time_ns;
+    inputs[num_inputs].time_stamp = trigger_time_ns;
     num_inputs++;
 
     // Temperature offset from the real temperature due to external heat sources
     inputs[num_inputs].sensor_id = BSEC_INPUT_HEATSOURCE;
     inputs[num_inputs].signal = this->temperature_offset_;
-    inputs[num_inputs].time_stamp = curr_time_ns;
+    inputs[num_inputs].time_stamp = trigger_time_ns;
     num_inputs++;
   }
   if (bme680_settings.process_data & BSEC_PROCESS_HUMIDITY) {
     inputs[num_inputs].sensor_id = BSEC_INPUT_HUMIDITY;
     inputs[num_inputs].signal = data.humidity / 1000.0f;
-    inputs[num_inputs].time_stamp = curr_time_ns;
+    inputs[num_inputs].time_stamp = trigger_time_ns;
     num_inputs++;
   }
   if (bme680_settings.process_data & BSEC_PROCESS_PRESSURE) {
     inputs[num_inputs].sensor_id = BSEC_INPUT_PRESSURE;
     inputs[num_inputs].signal = data.pressure;
-    inputs[num_inputs].time_stamp = curr_time_ns;
+    inputs[num_inputs].time_stamp = trigger_time_ns;
     num_inputs++;
   }
   if (bme680_settings.process_data & BSEC_PROCESS_GAS) {
-    inputs[num_inputs].sensor_id = BSEC_INPUT_GASRESISTOR;
-    inputs[num_inputs].signal = data.gas_resistance;
-    inputs[num_inputs].time_stamp = curr_time_ns;
-    num_inputs++;
+    if (data.status & BME680_GASM_VALID_MSK) {
+      inputs[num_inputs].sensor_id = BSEC_INPUT_GASRESISTOR;
+      inputs[num_inputs].signal = data.gas_resistance;
+      inputs[num_inputs].time_stamp = trigger_time_ns;
+      num_inputs++;
+    } else {
+      ESP_LOGD(TAG, "BME680 did not report gas data");
+    }
   }
   if (num_inputs < 1) {
     ESP_LOGD(TAG, "No signal inputs available for BSEC");
diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h
index 4a71e1d23b..73994b7541 100644
--- a/esphome/components/bme680_bsec/bme680_bsec.h
+++ b/esphome/components/bme680_bsec/bme680_bsec.h
@@ -1,5 +1,3 @@
-
-
 #pragma once
 
 #include "esphome/core/component.h"
@@ -9,13 +7,13 @@
 #include "esphome/core/preferences.h"
 #include <map>
 
-#ifdef USING_BSEC
+#ifdef USE_BSEC
 #include <bsec.h>
 #endif
 
 namespace esphome {
 namespace bme680_bsec {
-#ifdef USING_BSEC
+#ifdef USE_BSEC
 
 enum IAQMode {
   IAQ_MODE_STATIC = 0,
@@ -25,32 +23,31 @@ enum IAQMode {
 enum SampleRate {
   SAMPLE_RATE_LP = 0,
   SAMPLE_RATE_ULP = 1,
+  SAMPLE_RATE_DEFAULT = 2,
 };
 
+#define BME680_BSEC_SAMPLE_RATE_LOG(r) (r == SAMPLE_RATE_DEFAULT ? "Default" : (r == SAMPLE_RATE_ULP ? "ULP" : "LP"))
+
 class BME680BSECComponent : public Component, public i2c::I2CDevice {
  public:
   void set_temperature_offset(float offset) { this->temperature_offset_ = offset; }
   void set_iaq_mode(IAQMode iaq_mode) { this->iaq_mode_ = iaq_mode; }
-  void set_sample_rate(SampleRate sample_rate) { this->sample_rate_ = sample_rate; }
   void set_state_save_interval(uint32_t interval) { this->state_save_interval_ms_ = interval; }
 
-  void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
-  void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
-  void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
-  void set_gas_resistance_sensor(sensor::Sensor *gas_resistance_sensor) {
-    gas_resistance_sensor_ = gas_resistance_sensor;
-  }
-  void set_iaq_sensor(sensor::Sensor *iaq_sensor) { iaq_sensor_ = iaq_sensor; }
-  void set_iaq_accuracy_text_sensor(text_sensor::TextSensor *iaq_accuracy_text_sensor) {
-    iaq_accuracy_text_sensor_ = iaq_accuracy_text_sensor;
-  }
-  void set_iaq_accuracy_sensor(sensor::Sensor *iaq_accuracy_sensor) { iaq_accuracy_sensor_ = iaq_accuracy_sensor; }
-  void set_co2_equivalent_sensor(sensor::Sensor *co2_equivalent_sensor) {
-    co2_equivalent_sensor_ = co2_equivalent_sensor;
-  }
-  void set_breath_voc_equivalent_sensor(sensor::Sensor *breath_voc_equivalent_sensor) {
-    breath_voc_equivalent_sensor_ = breath_voc_equivalent_sensor;
-  }
+  void set_sample_rate(SampleRate sample_rate) { this->sample_rate_ = sample_rate; }
+  void set_temperature_sample_rate(SampleRate sample_rate) { this->temperature_sample_rate_ = sample_rate; }
+  void set_pressure_sample_rate(SampleRate sample_rate) { this->pressure_sample_rate_ = sample_rate; }
+  void set_humidity_sample_rate(SampleRate sample_rate) { this->humidity_sample_rate_ = sample_rate; }
+
+  void set_temperature_sensor(sensor::Sensor *sensor) { this->temperature_sensor_ = sensor; }
+  void set_pressure_sensor(sensor::Sensor *sensor) { this->pressure_sensor_ = sensor; }
+  void set_humidity_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; }
+  void set_gas_resistance_sensor(sensor::Sensor *sensor) { this->gas_resistance_sensor_ = sensor; }
+  void set_iaq_sensor(sensor::Sensor *sensor) { this->iaq_sensor_ = sensor; }
+  void set_iaq_accuracy_text_sensor(text_sensor::TextSensor *sensor) { this->iaq_accuracy_text_sensor_ = sensor; }
+  void set_iaq_accuracy_sensor(sensor::Sensor *sensor) { this->iaq_accuracy_sensor_ = sensor; }
+  void set_co2_equivalent_sensor(sensor::Sensor *sensor) { this->co2_equivalent_sensor_ = sensor; }
+  void set_breath_voc_equivalent_sensor(sensor::Sensor *sensor) { this->breath_voc_equivalent_sensor_ = sensor; }
 
   static BME680BSECComponent *instance;
   static int8_t read_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len);
@@ -64,10 +61,11 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
 
  protected:
   void set_config_(const uint8_t *config);
-  void update_subscription_(float sample_rate);
+  float calc_sensor_sample_rate_(SampleRate sample_rate);
+  void update_subscription_();
 
   void run_();
-  void read_(bsec_bme_settings_t bme680_settings);
+  void read_(int64_t trigger_time_ns, bsec_bme_settings_t bme680_settings);
   void publish_(const bsec_output_t *outputs, uint8_t num_outputs);
   int64_t get_time_ns_();
 
@@ -91,7 +89,11 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
 
   float temperature_offset_{0};
   IAQMode iaq_mode_{IAQ_MODE_STATIC};
-  SampleRate sample_rate_{SAMPLE_RATE_LP};
+
+  SampleRate sample_rate_{SAMPLE_RATE_LP};  // Core/gas sample rate
+  SampleRate temperature_sample_rate_{SAMPLE_RATE_DEFAULT};
+  SampleRate pressure_sample_rate_{SAMPLE_RATE_DEFAULT};
+  SampleRate humidity_sample_rate_{SAMPLE_RATE_DEFAULT};
 
   sensor::Sensor *temperature_sensor_;
   sensor::Sensor *pressure_sensor_;
diff --git a/esphome/components/bme680_bsec/sensor.py b/esphome/components/bme680_bsec/sensor.py
index b27b267c11..4520bf3480 100644
--- a/esphome/components/bme680_bsec/sensor.py
+++ b/esphome/components/bme680_bsec/sensor.py
@@ -10,6 +10,7 @@ from esphome.const import (
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_PRESSURE,
     DEVICE_CLASS_TEMPERATURE,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_EMPTY,
     UNIT_HECTOPASCAL,
@@ -21,8 +22,12 @@ from esphome.const import (
     ICON_THERMOMETER,
     ICON_WATER_PERCENT,
 )
-from esphome.core import coroutine
-from . import BME680BSECComponent, CONF_BME680_BSEC_ID
+from . import (
+    BME680BSECComponent,
+    CONF_BME680_BSEC_ID,
+    CONF_SAMPLE_RATE,
+    SAMPLE_RATE_OPTIONS,
+)
 
 DEPENDENCIES = ["bme680_bsec"]
 
@@ -34,58 +39,84 @@ UNIT_IAQ = "IAQ"
 ICON_ACCURACY = "mdi:checkbox-marked-circle-outline"
 ICON_TEST_TUBE = "mdi:test-tube"
 
-TYPES = {
-    CONF_TEMPERATURE: "set_temperature_sensor",
-    CONF_PRESSURE: "set_pressure_sensor",
-    CONF_HUMIDITY: "set_humidity_sensor",
-    CONF_GAS_RESISTANCE: "set_gas_resistance_sensor",
-    CONF_IAQ: "set_iaq_sensor",
-    CONF_IAQ_ACCURACY: "set_iaq_accuracy_sensor",
-    CONF_CO2_EQUIVALENT: "set_co2_equivalent_sensor",
-    CONF_BREATH_VOC_EQUIVALENT: "set_breath_voc_equivalent_sensor",
-}
+TYPES = [
+    CONF_TEMPERATURE,
+    CONF_PRESSURE,
+    CONF_HUMIDITY,
+    CONF_GAS_RESISTANCE,
+    CONF_IAQ,
+    CONF_IAQ_ACCURACY,
+    CONF_CO2_EQUIVALENT,
+    CONF_BREATH_VOC_EQUIVALENT,
+]
 
 CONFIG_SCHEMA = cv.Schema(
     {
         cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent),
         cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-            UNIT_CELSIUS, ICON_THERMOMETER, 1, DEVICE_CLASS_TEMPERATURE
+            UNIT_CELSIUS,
+            ICON_THERMOMETER,
+            1,
+            DEVICE_CLASS_TEMPERATURE,
+            STATE_CLASS_MEASUREMENT,
+        ).extend(
+            {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)}
         ),
         cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
-            UNIT_HECTOPASCAL, ICON_GAUGE, 1, DEVICE_CLASS_PRESSURE
+            UNIT_HECTOPASCAL,
+            ICON_GAUGE,
+            1,
+            DEVICE_CLASS_PRESSURE,
+            STATE_CLASS_MEASUREMENT,
+        ).extend(
+            {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)}
         ),
         cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-            UNIT_PERCENT, ICON_WATER_PERCENT, 1, DEVICE_CLASS_HUMIDITY
+            UNIT_PERCENT,
+            ICON_WATER_PERCENT,
+            1,
+            DEVICE_CLASS_HUMIDITY,
+            STATE_CLASS_MEASUREMENT,
+        ).extend(
+            {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)}
         ),
         cv.Optional(CONF_GAS_RESISTANCE): sensor.sensor_schema(
-            UNIT_OHM, ICON_GAS_CYLINDER, 0, DEVICE_CLASS_EMPTY
+            UNIT_OHM, ICON_GAS_CYLINDER, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional(CONF_IAQ): sensor.sensor_schema(
-            UNIT_IAQ, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY
+            UNIT_IAQ, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional(CONF_IAQ_ACCURACY): sensor.sensor_schema(
-            UNIT_EMPTY, ICON_ACCURACY, 0, DEVICE_CLASS_EMPTY
+            UNIT_EMPTY, ICON_ACCURACY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema(
-            UNIT_PARTS_PER_MILLION, ICON_TEST_TUBE, 1, DEVICE_CLASS_EMPTY
+            UNIT_PARTS_PER_MILLION,
+            ICON_TEST_TUBE,
+            1,
+            DEVICE_CLASS_EMPTY,
+            STATE_CLASS_MEASUREMENT,
         ),
         cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema(
-            UNIT_PARTS_PER_MILLION, ICON_TEST_TUBE, 1, DEVICE_CLASS_EMPTY
+            UNIT_PARTS_PER_MILLION,
+            ICON_TEST_TUBE,
+            1,
+            DEVICE_CLASS_EMPTY,
+            STATE_CLASS_MEASUREMENT,
         ),
     }
 )
 
 
-@coroutine
-def setup_conf(config, key, hub, funcName):
+async def setup_conf(config, key, hub):
     if key in config:
         conf = config[key]
-        var = yield sensor.new_sensor(conf)
-        func = getattr(hub, funcName)
-        cg.add(func(var))
+        sens = await sensor.new_sensor(conf)
+        cg.add(getattr(hub, f"set_{key}_sensor")(sens))
+        if CONF_SAMPLE_RATE in conf:
+            cg.add(getattr(hub, f"set_{key}_sample_rate")(conf[CONF_SAMPLE_RATE]))
 
 
-def to_code(config):
-    hub = yield cg.get_variable(config[CONF_BME680_BSEC_ID])
-    for key, funcName in TYPES.items():
-        yield setup_conf(config, key, hub, funcName)
+async def to_code(config):
+    hub = await cg.get_variable(config[CONF_BME680_BSEC_ID])
+    for key in TYPES:
+        await setup_conf(config, key, hub)
diff --git a/esphome/components/bme680_bsec/text_sensor.py b/esphome/components/bme680_bsec/text_sensor.py
index 992e2989c9..96020544e7 100644
--- a/esphome/components/bme680_bsec/text_sensor.py
+++ b/esphome/components/bme680_bsec/text_sensor.py
@@ -2,7 +2,6 @@ import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import text_sensor
 from esphome.const import CONF_ID, CONF_ICON
-from esphome.core import coroutine
 from . import BME680BSECComponent, CONF_BME680_BSEC_ID
 
 DEPENDENCIES = ["bme680_bsec"]
@@ -10,7 +9,7 @@ DEPENDENCIES = ["bme680_bsec"]
 CONF_IAQ_ACCURACY = "iaq_accuracy"
 ICON_ACCURACY = "mdi:checkbox-marked-circle-outline"
 
-TYPES = {CONF_IAQ_ACCURACY: "set_iaq_accuracy_text_sensor"}
+TYPES = [CONF_IAQ_ACCURACY]
 
 CONFIG_SCHEMA = cv.Schema(
     {
@@ -25,17 +24,15 @@ CONFIG_SCHEMA = cv.Schema(
 )
 
 
-@coroutine
-def setup_conf(config, key, hub, funcName):
+async def setup_conf(config, key, hub):
     if key in config:
         conf = config[key]
-        var = cg.new_Pvariable(conf[CONF_ID])
-        yield text_sensor.register_text_sensor(var, conf)
-        func = getattr(hub, funcName)
-        cg.add(func(var))
+        sens = cg.new_Pvariable(conf[CONF_ID])
+        await text_sensor.register_text_sensor(sens, conf)
+        cg.add(getattr(hub, f"set_{key}_text_sensor")(sens))
 
 
-def to_code(config):
-    hub = yield cg.get_variable(config[CONF_BME680_BSEC_ID])
-    for key, funcName in TYPES.items():
-        yield setup_conf(config, key, hub, funcName)
+async def to_code(config):
+    hub = await cg.get_variable(config[CONF_BME680_BSEC_ID])
+    for key in TYPES:
+        await setup_conf(config, key, hub)
diff --git a/esphome/components/bmp085/sensor.py b/esphome/components/bmp085/sensor.py
index a070e4aa69..1b48f2e440 100644
--- a/esphome/components/bmp085/sensor.py
+++ b/esphome/components/bmp085/sensor.py
@@ -7,6 +7,7 @@ from esphome.const import (
     CONF_TEMPERATURE,
     DEVICE_CLASS_PRESSURE,
     DEVICE_CLASS_TEMPERATURE,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     ICON_EMPTY,
     UNIT_HECTOPASCAL,
@@ -24,10 +25,18 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(BMP085Component),
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
-                UNIT_HECTOPASCAL, ICON_EMPTY, 1, DEVICE_CLASS_PRESSURE
+                UNIT_HECTOPASCAL,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_PRESSURE,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -36,17 +45,17 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     if CONF_TEMPERATURE in config:
         conf = config[CONF_TEMPERATURE]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_temperature(sens))
 
     if CONF_PRESSURE in config:
         conf = config[CONF_PRESSURE]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_pressure(sens))
diff --git a/esphome/components/bmp280/sensor.py b/esphome/components/bmp280/sensor.py
index b12d7bff7f..48953d0259 100644
--- a/esphome/components/bmp280/sensor.py
+++ b/esphome/components/bmp280/sensor.py
@@ -7,6 +7,7 @@ from esphome.const import (
     CONF_TEMPERATURE,
     DEVICE_CLASS_PRESSURE,
     DEVICE_CLASS_TEMPERATURE,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     ICON_EMPTY,
     UNIT_HECTOPASCAL,
@@ -45,7 +46,11 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(BMP280Component),
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ).extend(
                 {
                     cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
@@ -54,7 +59,11 @@ CONFIG_SCHEMA = (
                 }
             ),
             cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
-                UNIT_HECTOPASCAL, ICON_EMPTY, 1, DEVICE_CLASS_PRESSURE
+                UNIT_HECTOPASCAL,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_PRESSURE,
+                STATE_CLASS_MEASUREMENT,
             ).extend(
                 {
                     cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
@@ -72,19 +81,19 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     if CONF_TEMPERATURE in config:
         conf = config[CONF_TEMPERATURE]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_temperature_sensor(sens))
         cg.add(var.set_temperature_oversampling(conf[CONF_OVERSAMPLING]))
 
     if CONF_PRESSURE in config:
         conf = config[CONF_PRESSURE]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_pressure_sensor(sens))
         cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING]))
diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py
index 1cbebb07da..3a3cece579 100644
--- a/esphome/components/canbus/__init__.py
+++ b/esphome/components/canbus/__init__.py
@@ -1,7 +1,7 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import automation
-from esphome.core import CORE, coroutine
+from esphome.core import CORE
 from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_DATA
 
 CODEOWNERS = ["@mvturnho", "@danielschramm"]
@@ -82,10 +82,9 @@ CANBUS_SCHEMA = cv.Schema(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-@coroutine
-def setup_canbus_core_(var, config):
+async def setup_canbus_core_(var, config):
     validate_id(config[CONF_CAN_ID], config[CONF_USE_EXTENDED_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
     cg.add(var.set_can_id([config[CONF_CAN_ID]]))
     cg.add(var.set_use_extended_id([config[CONF_USE_EXTENDED_ID]]))
     cg.add(var.set_bitrate(CAN_SPEEDS[config[CONF_BIT_RATE]]))
@@ -95,17 +94,16 @@ def setup_canbus_core_(var, config):
         ext_id = conf[CONF_USE_EXTENDED_ID]
         validate_id(can_id, ext_id)
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, can_id, ext_id)
-        yield cg.register_component(trigger, conf)
-        yield automation.build_automation(
+        await cg.register_component(trigger, conf)
+        await automation.build_automation(
             trigger, [(cg.std_vector.template(cg.uint8), "x")], conf
         )
 
 
-@coroutine
-def register_canbus(var, config):
+async def register_canbus(var, config):
     if not CORE.has_id(config[CONF_ID]):
         var = cg.new_Pvariable(config[CONF_ID], var)
-    yield setup_canbus_core_(var, config)
+    await setup_canbus_core_(var, config)
 
 
 # Actions
@@ -122,16 +120,16 @@ def register_canbus(var, config):
         key=CONF_DATA,
     ),
 )
-def canbus_action_to_code(config, action_id, template_arg, args):
+async def canbus_action_to_code(config, action_id, template_arg, args):
     validate_id(config[CONF_CAN_ID], config[CONF_USE_EXTENDED_ID])
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_CANBUS_ID])
+    await cg.register_parented(var, config[CONF_CANBUS_ID])
 
     if CONF_CAN_ID in config:
-        can_id = yield cg.templatable(config[CONF_CAN_ID], args, cg.uint32)
+        can_id = await cg.templatable(config[CONF_CAN_ID], args, cg.uint32)
         cg.add(var.set_can_id(can_id))
 
-    use_extended_id = yield cg.templatable(
+    use_extended_id = await cg.templatable(
         config[CONF_USE_EXTENDED_ID], args, cg.uint32
     )
     cg.add(var.set_use_extended_id(use_extended_id))
@@ -140,8 +138,8 @@ def canbus_action_to_code(config, action_id, template_arg, args):
     if isinstance(data, bytes):
         data = [int(x) for x in data]
     if cg.is_template(data):
-        templ = yield cg.templatable(data, args, cg.std_vector.template(cg.uint8))
+        templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
         cg.add(var.set_data_template(templ))
     else:
         cg.add(var.set_data_static(data))
-    yield var
+    return var
diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py
index e158db6746..102bfd370e 100644
--- a/esphome/components/captive_portal/__init__.py
+++ b/esphome/components/captive_portal/__init__.py
@@ -23,9 +23,9 @@ CONFIG_SCHEMA = cv.Schema(
 
 
 @coroutine_with_priority(64.0)
-def to_code(config):
-    paren = yield cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
+async def to_code(config):
+    paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
 
     var = cg.new_Pvariable(config[CONF_ID], paren)
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
     cg.add_define("USE_CAPTIVE_PORTAL")
diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp
index 83f85d354c..0e0e08fdea 100644
--- a/esphome/components/captive_portal/captive_portal.cpp
+++ b/esphome/components/captive_portal/captive_portal.cpp
@@ -64,32 +64,11 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
   ESP_LOGI(TAG, "Captive Portal Requested WiFi Settings Change:");
   ESP_LOGI(TAG, "  SSID='%s'", ssid.c_str());
   ESP_LOGI(TAG, "  Password=" LOG_SECRET("'%s'"), psk.c_str());
-  this->override_sta_(ssid, psk);
+  wifi::global_wifi_component->save_wifi_sta(ssid, psk);
   request->redirect("/?save=true");
 }
-void CaptivePortal::override_sta_(const std::string &ssid, const std::string &password) {
-  CaptivePortalSettings save{};
-  strcpy(save.ssid, ssid.c_str());
-  strcpy(save.password, password.c_str());
-  this->pref_.save(&save);
 
-  wifi::WiFiAP sta{};
-  sta.set_ssid(ssid);
-  sta.set_password(password);
-  wifi::global_wifi_component->set_sta(sta);
-}
-
-void CaptivePortal::setup() {
-  // Hash with compilation time
-  // This ensures the AP override is not applied for OTA
-  uint32_t hash = fnv1_hash(App.get_compilation_time());
-  this->pref_ = global_preferences.make_preference<CaptivePortalSettings>(hash, true);
-
-  CaptivePortalSettings save{};
-  if (this->pref_.load(&save)) {
-    this->override_sta_(save.ssid, save.password);
-  }
-}
+void CaptivePortal::setup() {}
 void CaptivePortal::start() {
   this->base_->init();
   if (!this->initialized_) {
diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h
index 3af47546cf..afd4ff9dc5 100644
--- a/esphome/components/captive_portal/captive_portal.h
+++ b/esphome/components/captive_portal/captive_portal.h
@@ -10,11 +10,6 @@ namespace esphome {
 
 namespace captive_portal {
 
-struct CaptivePortalSettings {
-  char ssid[33];
-  char password[65];
-} PACKED;  // NOLINT
-
 class CaptivePortal : public AsyncWebHandler, public Component {
  public:
   CaptivePortal(web_server_base::WebServerBase *base);
@@ -67,12 +62,9 @@ class CaptivePortal : public AsyncWebHandler, public Component {
   void handleRequest(AsyncWebServerRequest *req) override;
 
  protected:
-  void override_sta_(const std::string &ssid, const std::string &password);
-
   web_server_base::WebServerBase *base_;
   bool initialized_{false};
   bool active_{false};
-  ESPPreferenceObject pref_;
   DNSServer *dns_server_{nullptr};
 };
 
diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py
index 95b108a225..4c4f8802d4 100644
--- a/esphome/components/ccs811/sensor.py
+++ b/esphome/components/ccs811/sensor.py
@@ -5,6 +5,7 @@ from esphome.const import (
     CONF_ID,
     DEVICE_CLASS_EMPTY,
     ICON_RADIATOR,
+    STATE_CLASS_MEASUREMENT,
     UNIT_PARTS_PER_MILLION,
     UNIT_PARTS_PER_BILLION,
     CONF_TEMPERATURE,
@@ -28,10 +29,18 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(CCS811Component),
             cv.Required(CONF_ECO2): sensor.sensor_schema(
-                UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, 0, DEVICE_CLASS_EMPTY
+                UNIT_PARTS_PER_MILLION,
+                ICON_MOLECULE_CO2,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Required(CONF_TVOC): sensor.sensor_schema(
-                UNIT_PARTS_PER_BILLION, ICON_RADIATOR, 0, DEVICE_CLASS_EMPTY
+                UNIT_PARTS_PER_BILLION,
+                ICON_RADIATOR,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_BASELINE): cv.hex_uint16_t,
             cv.Optional(CONF_TEMPERATURE): cv.use_id(sensor.Sensor),
@@ -43,22 +52,22 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
-    sens = yield sensor.new_sensor(config[CONF_ECO2])
+    sens = await sensor.new_sensor(config[CONF_ECO2])
     cg.add(var.set_co2(sens))
-    sens = yield sensor.new_sensor(config[CONF_TVOC])
+    sens = await sensor.new_sensor(config[CONF_TVOC])
     cg.add(var.set_tvoc(sens))
 
     if CONF_BASELINE in config:
         cg.add(var.set_baseline(config[CONF_BASELINE]))
 
     if CONF_TEMPERATURE in config:
-        sens = yield cg.get_variable(config[CONF_TEMPERATURE])
+        sens = await cg.get_variable(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
     if CONF_HUMIDITY in config:
-        sens = yield cg.get_variable(config[CONF_HUMIDITY])
+        sens = await cg.get_variable(config[CONF_HUMIDITY])
         cg.add(var.set_humidity(sens))
diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py
index 7f74b62c61..5a4492216e 100644
--- a/esphome/components/climate/__init__.py
+++ b/esphome/components/climate/__init__.py
@@ -4,11 +4,14 @@ from esphome import automation
 from esphome.components import mqtt
 from esphome.const import (
     CONF_AWAY,
+    CONF_CUSTOM_FAN_MODE,
+    CONF_CUSTOM_PRESET,
     CONF_ID,
     CONF_INTERNAL,
     CONF_MAX_TEMPERATURE,
     CONF_MIN_TEMPERATURE,
     CONF_MODE,
+    CONF_PRESET,
     CONF_TARGET_TEMPERATURE,
     CONF_TARGET_TEMPERATURE_HIGH,
     CONF_TARGET_TEMPERATURE_LOW,
@@ -19,7 +22,7 @@ from esphome.const import (
     CONF_FAN_MODE,
     CONF_SWING_MODE,
 )
-from esphome.core import CORE, coroutine, coroutine_with_priority
+from esphome.core import CORE, coroutine_with_priority
 
 IS_PLATFORM_COMPONENT = True
 
@@ -33,11 +36,12 @@ ClimateTraits = climate_ns.class_("ClimateTraits")
 ClimateMode = climate_ns.enum("ClimateMode")
 CLIMATE_MODES = {
     "OFF": ClimateMode.CLIMATE_MODE_OFF,
-    "AUTO": ClimateMode.CLIMATE_MODE_AUTO,
+    "HEAT_COOL": ClimateMode.CLIMATE_HEAT_COOL,
     "COOL": ClimateMode.CLIMATE_MODE_COOL,
     "HEAT": ClimateMode.CLIMATE_MODE_HEAT,
     "DRY": ClimateMode.CLIMATE_MODE_DRY,
     "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY,
+    "AUTO": ClimateMode.CLIMATE_MODE_AUTO,
 }
 validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True)
 
@@ -56,6 +60,19 @@ CLIMATE_FAN_MODES = {
 
 validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True)
 
+ClimatePreset = climate_ns.enum("ClimatePreset")
+CLIMATE_PRESETS = {
+    "ECO": ClimatePreset.CLIMATE_PRESET_ECO,
+    "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
+    "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
+    "COMFORT": ClimatePreset.CLIMATE_PRESET_COMFORT,
+    "HOME": ClimatePreset.CLIMATE_PRESET_HOME,
+    "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
+    "ACTIVITY": ClimatePreset.CLIMATE_PRESET_ACTIVITY,
+}
+
+validate_climate_preset = cv.enum(CLIMATE_PRESETS, upper=True)
+
 ClimateSwingMode = climate_ns.enum("ClimateSwingMode")
 CLIMATE_SWING_MODES = {
     "OFF": ClimateSwingMode.CLIMATE_SWING_OFF,
@@ -85,8 +102,7 @@ CLIMATE_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend(
 )
 
 
-@coroutine
-def setup_climate_core_(var, config):
+async def setup_climate_core_(var, config):
     cg.add(var.set_name(config[CONF_NAME]))
     if CONF_INTERNAL in config:
         cg.add(var.set_internal(config[CONF_INTERNAL]))
@@ -100,15 +116,14 @@ def setup_climate_core_(var, config):
 
     if CONF_MQTT_ID in config:
         mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
-        yield mqtt.register_mqtt_component(mqtt_, config)
+        await mqtt.register_mqtt_component(mqtt_, config)
 
 
-@coroutine
-def register_climate(var, config):
+async def register_climate(var, config):
     if not CORE.has_id(config[CONF_ID]):
         var = cg.Pvariable(config[CONF_ID], var)
     cg.add(cg.App.register_climate(var))
-    yield setup_climate_core_(var, config)
+    await setup_climate_core_(var, config)
 
 
 CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
@@ -119,7 +134,12 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
         cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature),
         cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature),
         cv.Optional(CONF_AWAY): cv.templatable(cv.boolean),
-        cv.Optional(CONF_FAN_MODE): cv.templatable(validate_climate_fan_mode),
+        cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable(
+            validate_climate_fan_mode
+        ),
+        cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.string_strict,
+        cv.Exclusive(CONF_PRESET, "preset"): cv.templatable(validate_climate_preset),
+        cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.string_strict,
         cv.Optional(CONF_SWING_MODE): cv.templatable(validate_climate_swing_mode),
     }
 )
@@ -128,40 +148,49 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
 @automation.register_action(
     "climate.control", ControlAction, CLIMATE_CONTROL_ACTION_SCHEMA
 )
-def climate_control_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def climate_control_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
     if CONF_MODE in config:
-        template_ = yield cg.templatable(config[CONF_MODE], args, ClimateMode)
+        template_ = await cg.templatable(config[CONF_MODE], args, ClimateMode)
         cg.add(var.set_mode(template_))
     if CONF_TARGET_TEMPERATURE in config:
-        template_ = yield cg.templatable(config[CONF_TARGET_TEMPERATURE], args, float)
+        template_ = await cg.templatable(config[CONF_TARGET_TEMPERATURE], args, float)
         cg.add(var.set_target_temperature(template_))
     if CONF_TARGET_TEMPERATURE_LOW in config:
-        template_ = yield cg.templatable(
+        template_ = await cg.templatable(
             config[CONF_TARGET_TEMPERATURE_LOW], args, float
         )
         cg.add(var.set_target_temperature_low(template_))
     if CONF_TARGET_TEMPERATURE_HIGH in config:
-        template_ = yield cg.templatable(
+        template_ = await cg.templatable(
             config[CONF_TARGET_TEMPERATURE_HIGH], args, float
         )
         cg.add(var.set_target_temperature_high(template_))
     if CONF_AWAY in config:
-        template_ = yield cg.templatable(config[CONF_AWAY], args, bool)
+        template_ = await cg.templatable(config[CONF_AWAY], args, bool)
         cg.add(var.set_away(template_))
     if CONF_FAN_MODE in config:
-        template_ = yield cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode)
+        template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode)
         cg.add(var.set_fan_mode(template_))
+    if CONF_CUSTOM_FAN_MODE in config:
+        template_ = await cg.templatable(config[CONF_CUSTOM_FAN_MODE], args, str)
+        cg.add(var.set_custom_fan_mode(template_))
+    if CONF_PRESET in config:
+        template_ = await cg.templatable(config[CONF_PRESET], args, ClimatePreset)
+        cg.add(var.set_preset(template_))
+    if CONF_CUSTOM_PRESET in config:
+        template_ = await cg.templatable(config[CONF_CUSTOM_PRESET], args, str)
+        cg.add(var.set_custom_preset(template_))
     if CONF_SWING_MODE in config:
-        template_ = yield cg.templatable(
+        template_ = await cg.templatable(
             config[CONF_SWING_MODE], args, ClimateSwingMode
         )
         cg.add(var.set_swing_mode(template_))
-    yield var
+    return var
 
 
 @coroutine_with_priority(100.0)
-def to_code(config):
+async def to_code(config):
     cg.add_define("USE_CLIMATE")
     cg.add_global(climate_ns.using)
diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h
index 0cd52b1036..b0b71cb7d7 100644
--- a/esphome/components/climate/automation.h
+++ b/esphome/components/climate/automation.h
@@ -16,6 +16,9 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
   TEMPLATABLE_VALUE(float, target_temperature_high)
   TEMPLATABLE_VALUE(bool, away)
   TEMPLATABLE_VALUE(ClimateFanMode, fan_mode)
+  TEMPLATABLE_VALUE(std::string, custom_fan_mode)
+  TEMPLATABLE_VALUE(ClimatePreset, preset)
+  TEMPLATABLE_VALUE(std::string, custom_preset)
   TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode)
 
   void play(Ts... x) override {
@@ -26,6 +29,9 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
     call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...));
     call.set_away(this->away_.optional_value(x...));
     call.set_fan_mode(this->fan_mode_.optional_value(x...));
+    call.set_fan_mode(this->custom_fan_mode_.optional_value(x...));
+    call.set_preset(this->preset_.optional_value(x...));
+    call.set_preset(this->custom_preset_.optional_value(x...));
     call.set_swing_mode(this->swing_mode_.optional_value(x...));
     call.perform();
   }
diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp
index 443290ed6d..c047d96cdb 100644
--- a/esphome/components/climate/climate.cpp
+++ b/esphome/components/climate/climate.cpp
@@ -1,5 +1,4 @@
 #include "climate.h"
-#include "esphome/core/log.h"
 
 namespace esphome {
 namespace climate {
@@ -13,10 +12,24 @@ void ClimateCall::perform() {
     const char *mode_s = climate_mode_to_string(*this->mode_);
     ESP_LOGD(TAG, "  Mode: %s", mode_s);
   }
+  if (this->custom_fan_mode_.has_value()) {
+    this->fan_mode_.reset();
+    ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_.value().c_str());
+  }
   if (this->fan_mode_.has_value()) {
+    this->custom_fan_mode_.reset();
     const char *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_);
     ESP_LOGD(TAG, "  Fan: %s", fan_mode_s);
   }
+  if (this->custom_preset_.has_value()) {
+    this->preset_.reset();
+    ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_.value().c_str());
+  }
+  if (this->preset_.has_value()) {
+    this->custom_preset_.reset();
+    const char *preset_s = climate_preset_to_string(*this->preset_);
+    ESP_LOGD(TAG, "  Preset: %s", preset_s);
+  }
   if (this->swing_mode_.has_value()) {
     const char *swing_mode_s = climate_swing_mode_to_string(*this->swing_mode_);
     ESP_LOGD(TAG, "  Swing: %s", swing_mode_s);
@@ -44,13 +57,32 @@ void ClimateCall::validate_() {
       this->mode_.reset();
     }
   }
-  if (this->fan_mode_.has_value()) {
+  if (this->custom_fan_mode_.has_value()) {
+    auto custom_fan_mode = *this->custom_fan_mode_;
+    if (!traits.supports_custom_fan_mode(custom_fan_mode)) {
+      ESP_LOGW(TAG, "  Fan Mode %s is not supported by this device!", custom_fan_mode.c_str());
+      this->custom_fan_mode_.reset();
+    }
+  } else if (this->fan_mode_.has_value()) {
     auto fan_mode = *this->fan_mode_;
     if (!traits.supports_fan_mode(fan_mode)) {
       ESP_LOGW(TAG, "  Fan Mode %s is not supported by this device!", climate_fan_mode_to_string(fan_mode));
       this->fan_mode_.reset();
     }
   }
+  if (this->custom_preset_.has_value()) {
+    auto custom_preset = *this->custom_preset_;
+    if (!traits.supports_custom_preset(custom_preset)) {
+      ESP_LOGW(TAG, "  Preset %s is not supported by this device!", custom_preset.c_str());
+      this->custom_preset_.reset();
+    }
+  } else if (this->preset_.has_value()) {
+    auto preset = *this->preset_;
+    if (!traits.supports_preset(preset)) {
+      ESP_LOGW(TAG, "  Preset %s is not supported by this device!", climate_preset_to_string(preset));
+      this->preset_.reset();
+    }
+  }
   if (this->swing_mode_.has_value()) {
     auto swing_mode = *this->swing_mode_;
     if (!traits.supports_swing_mode(swing_mode)) {
@@ -117,6 +149,8 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) {
     this->set_mode(CLIMATE_MODE_FAN_ONLY);
   } else if (str_equals_case_insensitive(mode, "DRY")) {
     this->set_mode(CLIMATE_MODE_DRY);
+  } else if (str_equals_case_insensitive(mode, "HEAT_COOL")) {
+    this->set_mode(CLIMATE_MODE_HEAT_COOL);
   } else {
     ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str());
   }
@@ -124,6 +158,7 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) {
 }
 ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
   this->fan_mode_ = fan_mode;
+  this->custom_fan_mode_.reset();
   return *this;
 }
 ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
@@ -146,11 +181,59 @@ ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
   } else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) {
     this->set_fan_mode(CLIMATE_FAN_DIFFUSE);
   } else {
-    ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
+    auto custom_fan_modes = this->parent_->get_traits().get_supported_custom_fan_modes();
+    if (std::find(custom_fan_modes.begin(), custom_fan_modes.end(), fan_mode) != custom_fan_modes.end()) {
+      this->custom_fan_mode_ = fan_mode;
+      this->fan_mode_.reset();
+    } else {
+      ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
+    }
+  }
+  return *this;
+}
+ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
+  if (fan_mode.has_value()) {
+    this->set_fan_mode(fan_mode.value());
+  }
+  return *this;
+}
+ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
+  this->preset_ = preset;
+  this->custom_preset_.reset();
+  return *this;
+}
+ClimateCall &ClimateCall::set_preset(const std::string &preset) {
+  if (str_equals_case_insensitive(preset, "ECO")) {
+    this->set_preset(CLIMATE_PRESET_ECO);
+  } else if (str_equals_case_insensitive(preset, "AWAY")) {
+    this->set_preset(CLIMATE_PRESET_AWAY);
+  } else if (str_equals_case_insensitive(preset, "BOOST")) {
+    this->set_preset(CLIMATE_PRESET_BOOST);
+  } else if (str_equals_case_insensitive(preset, "COMFORT")) {
+    this->set_preset(CLIMATE_PRESET_COMFORT);
+  } else if (str_equals_case_insensitive(preset, "HOME")) {
+    this->set_preset(CLIMATE_PRESET_HOME);
+  } else if (str_equals_case_insensitive(preset, "SLEEP")) {
+    this->set_preset(CLIMATE_PRESET_SLEEP);
+  } else if (str_equals_case_insensitive(preset, "ACTIVITY")) {
+    this->set_preset(CLIMATE_PRESET_ACTIVITY);
+  } else {
+    auto custom_presets = this->parent_->get_traits().get_supported_custom_presets();
+    if (std::find(custom_presets.begin(), custom_presets.end(), preset) != custom_presets.end()) {
+      this->custom_preset_ = preset;
+      this->preset_.reset();
+    } else {
+      ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
+    }
+  }
+  return *this;
+}
+ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
+  if (preset.has_value()) {
+    this->set_preset(preset.value());
   }
   return *this;
 }
-
 ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) {
   this->swing_mode_ = swing_mode;
   return *this;
@@ -188,6 +271,9 @@ const optional<float> &ClimateCall::get_target_temperature_low() const { return
 const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; }
 const optional<bool> &ClimateCall::get_away() const { return this->away_; }
 const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
+const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
+const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
+const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
 const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
 ClimateCall &ClimateCall::set_away(bool away) {
   this->away_ = away;
@@ -215,6 +301,12 @@ ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
 }
 ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) {
   this->fan_mode_ = fan_mode;
+  this->custom_fan_mode_.reset();
+  return *this;
+}
+ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) {
+  this->preset_ = preset;
+  this->custom_preset_.reset();
   return *this;
 }
 ClimateCall &ClimateCall::set_swing_mode(optional<ClimateSwingMode> swing_mode) {
@@ -249,8 +341,31 @@ void Climate::save_state_() {
   if (traits.get_supports_away()) {
     state.away = this->away;
   }
-  if (traits.get_supports_fan_modes()) {
-    state.fan_mode = this->fan_mode;
+  if (traits.get_supports_fan_modes() && fan_mode.has_value()) {
+    state.uses_custom_fan_mode = false;
+    state.fan_mode = this->fan_mode.value();
+  }
+  if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) {
+    state.uses_custom_fan_mode = true;
+    auto &custom_fan_modes = traits.get_supported_custom_fan_modes();
+    auto it = std::find(custom_fan_modes.begin(), custom_fan_modes.end(), this->custom_fan_mode.value());
+    // only set custom fan mode if value exists, otherwise leave it as is
+    if (it != custom_fan_modes.cend()) {
+      state.custom_fan_mode = std::distance(custom_fan_modes.begin(), it);
+    }
+  }
+  if (traits.get_supports_presets() && preset.has_value()) {
+    state.uses_custom_preset = false;
+    state.preset = this->preset.value();
+  }
+  if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) {
+    state.uses_custom_preset = true;
+    auto custom_presets = traits.get_supported_custom_presets();
+    auto it = std::find(custom_presets.begin(), custom_presets.end(), this->custom_preset.value());
+    // only set custom preset if value exists, otherwise leave it as is
+    if (it != custom_presets.cend()) {
+      state.custom_preset = std::distance(custom_presets.begin(), it);
+    }
   }
   if (traits.get_supports_swing_modes()) {
     state.swing_mode = this->swing_mode;
@@ -266,8 +381,17 @@ void Climate::publish_state() {
   if (traits.get_supports_action()) {
     ESP_LOGD(TAG, "  Action: %s", climate_action_to_string(this->action));
   }
-  if (traits.get_supports_fan_modes()) {
-    ESP_LOGD(TAG, "  Fan Mode: %s", climate_fan_mode_to_string(this->fan_mode));
+  if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) {
+    ESP_LOGD(TAG, "  Fan Mode: %s", climate_fan_mode_to_string(this->fan_mode.value()));
+  }
+  if (!traits.get_supported_custom_fan_modes().empty() && this->custom_fan_mode.has_value()) {
+    ESP_LOGD(TAG, "  Custom Fan Mode: %s", this->custom_fan_mode.value().c_str());
+  }
+  if (traits.get_supports_presets() && this->preset.has_value()) {
+    ESP_LOGD(TAG, "  Preset: %s", climate_preset_to_string(this->preset.value()));
+  }
+  if (!traits.get_supported_custom_presets().empty() && this->custom_preset.has_value()) {
+    ESP_LOGD(TAG, "  Custom Preset: %s", this->custom_preset.value().c_str());
   }
   if (traits.get_supports_swing_modes()) {
     ESP_LOGD(TAG, "  Swing Mode: %s", climate_swing_mode_to_string(this->swing_mode));
@@ -332,9 +456,12 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
   if (traits.get_supports_away()) {
     call.set_away(this->away);
   }
-  if (traits.get_supports_fan_modes()) {
+  if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) {
     call.set_fan_mode(this->fan_mode);
   }
+  if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
+    call.set_preset(this->preset);
+  }
   if (traits.get_supports_swing_modes()) {
     call.set_swing_mode(this->swing_mode);
   }
@@ -352,9 +479,21 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
   if (traits.get_supports_away()) {
     climate->away = this->away;
   }
-  if (traits.get_supports_fan_modes()) {
+  if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) {
     climate->fan_mode = this->fan_mode;
   }
+  if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) {
+    climate->custom_fan_mode = traits.get_supported_custom_fan_modes()[this->custom_fan_mode];
+  }
+  if (traits.get_supports_presets() && !this->uses_custom_preset) {
+    climate->preset = this->preset;
+  }
+  if (!traits.get_supported_custom_presets().empty() && this->uses_custom_preset) {
+    climate->custom_preset = traits.get_supported_custom_presets()[this->custom_preset];
+  }
+  if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) {
+    climate->custom_preset = traits.get_supported_custom_presets()[this->preset];
+  }
   if (traits.get_supports_swing_modes()) {
     climate->swing_mode = this->swing_mode;
   }
diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h
index 786afe097a..cd69469692 100644
--- a/esphome/components/climate/climate.h
+++ b/esphome/components/climate/climate.h
@@ -3,6 +3,7 @@
 #include "esphome/core/component.h"
 #include "esphome/core/helpers.h"
 #include "esphome/core/preferences.h"
+#include "esphome/core/log.h"
 #include "climate_mode.h"
 #include "climate_traits.h"
 
@@ -70,12 +71,22 @@ class ClimateCall {
   ClimateCall &set_fan_mode(optional<ClimateFanMode> fan_mode);
   /// Set the fan mode of the climate device based on a string.
   ClimateCall &set_fan_mode(const std::string &fan_mode);
+  /// Set the fan mode of the climate device based on a string.
+  ClimateCall &set_fan_mode(optional<std::string> fan_mode);
   /// Set the swing mode of the climate device.
   ClimateCall &set_swing_mode(ClimateSwingMode swing_mode);
   /// Set the swing mode of the climate device.
   ClimateCall &set_swing_mode(optional<ClimateSwingMode> swing_mode);
   /// Set the swing mode of the climate device based on a string.
   ClimateCall &set_swing_mode(const std::string &swing_mode);
+  /// Set the preset of the climate device.
+  ClimateCall &set_preset(ClimatePreset preset);
+  /// Set the preset of the climate device.
+  ClimateCall &set_preset(optional<ClimatePreset> preset);
+  /// Set the preset of the climate device based on a string.
+  ClimateCall &set_preset(const std::string &preset);
+  /// Set the preset of the climate device based on a string.
+  ClimateCall &set_preset(optional<std::string> preset);
 
   void perform();
 
@@ -86,6 +97,9 @@ class ClimateCall {
   const optional<bool> &get_away() const;
   const optional<ClimateFanMode> &get_fan_mode() const;
   const optional<ClimateSwingMode> &get_swing_mode() const;
+  const optional<std::string> &get_custom_fan_mode() const;
+  const optional<ClimatePreset> &get_preset() const;
+  const optional<std::string> &get_custom_preset() const;
 
  protected:
   void validate_();
@@ -98,13 +112,25 @@ class ClimateCall {
   optional<bool> away_;
   optional<ClimateFanMode> fan_mode_;
   optional<ClimateSwingMode> swing_mode_;
+  optional<std::string> custom_fan_mode_;
+  optional<ClimatePreset> preset_;
+  optional<std::string> custom_preset_;
 };
 
 /// Struct used to save the state of the climate device in restore memory.
 struct ClimateDeviceRestoreState {
   ClimateMode mode;
   bool away;
-  ClimateFanMode fan_mode;
+  bool uses_custom_fan_mode{false};
+  union {
+    ClimateFanMode fan_mode;
+    uint8_t custom_fan_mode;
+  };
+  bool uses_custom_preset{false};
+  union {
+    ClimatePreset preset;
+    uint8_t custom_preset;
+  };
   ClimateSwingMode swing_mode;
   union {
     float target_temperature;
@@ -168,11 +194,20 @@ class Climate : public Nameable {
   bool away{false};
 
   /// The active fan mode of the climate device.
-  ClimateFanMode fan_mode;
+  optional<ClimateFanMode> fan_mode;
 
   /// The active swing mode of the climate device.
   ClimateSwingMode swing_mode;
 
+  /// The active custom fan mode of the climate device.
+  optional<std::string> custom_fan_mode;
+
+  /// The active preset of the climate device.
+  optional<ClimatePreset> preset;
+
+  /// The active custom preset mode of the climate device.
+  optional<std::string> custom_preset;
+
   /** Add a callback for the climate device state, each time the state of the climate device is updated
    * (using publish_state), this callback will be called.
    *
diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp
index ddcc4af4d9..4540208a3f 100644
--- a/esphome/components/climate/climate_mode.cpp
+++ b/esphome/components/climate/climate_mode.cpp
@@ -17,6 +17,8 @@ const char *climate_mode_to_string(ClimateMode mode) {
       return "FAN_ONLY";
     case CLIMATE_MODE_DRY:
       return "DRY";
+    case CLIMATE_MODE_HEAT_COOL:
+      return "HEAT_COOL";
     default:
       return "UNKNOWN";
   }
@@ -80,5 +82,26 @@ const char *climate_swing_mode_to_string(ClimateSwingMode swing_mode) {
   }
 }
 
+const char *climate_preset_to_string(ClimatePreset preset) {
+  switch (preset) {
+    case climate::CLIMATE_PRESET_ECO:
+      return "ECO";
+    case climate::CLIMATE_PRESET_AWAY:
+      return "AWAY";
+    case climate::CLIMATE_PRESET_BOOST:
+      return "BOOST";
+    case climate::CLIMATE_PRESET_COMFORT:
+      return "COMFORT";
+    case climate::CLIMATE_PRESET_HOME:
+      return "HOME";
+    case climate::CLIMATE_PRESET_SLEEP:
+      return "SLEEP";
+    case climate::CLIMATE_PRESET_ACTIVITY:
+      return "ACTIVITY";
+    default:
+      return "UNKNOWN";
+  }
+}
+
 }  // namespace climate
 }  // namespace esphome
diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h
index 8037ea2196..e129fca91d 100644
--- a/esphome/components/climate/climate_mode.h
+++ b/esphome/components/climate/climate_mode.h
@@ -10,7 +10,7 @@ enum ClimateMode : uint8_t {
   /// The climate device is off (not in auto, heat or cool mode)
   CLIMATE_MODE_OFF = 0,
   /// The climate device is set to automatically change the heating/cooling cycle
-  CLIMATE_MODE_AUTO = 1,
+  CLIMATE_MODE_HEAT_COOL = 1,
   /// The climate device is manually set to cool mode (not in auto mode!)
   CLIMATE_MODE_COOL = 2,
   /// The climate device is manually set to heat mode (not in auto mode!)
@@ -19,6 +19,8 @@ enum ClimateMode : uint8_t {
   CLIMATE_MODE_FAN_ONLY = 4,
   /// The climate device is manually set to dry mode
   CLIMATE_MODE_DRY = 5,
+  /// The climate device is manually set to heat-cool mode
+  CLIMATE_MODE_AUTO = 6
 };
 
 /// Enum for the current action of the climate device. Values match those of ClimateMode.
@@ -61,7 +63,7 @@ enum ClimateFanMode : uint8_t {
 
 /// Enum for all modes a climate swing can be in
 enum ClimateSwingMode : uint8_t {
-  /// The sing mode is set to Off
+  /// The swing mode is set to Off
   CLIMATE_SWING_OFF = 0,
   /// The fan mode is set to Both
   CLIMATE_SWING_BOTH = 1,
@@ -71,6 +73,24 @@ enum ClimateSwingMode : uint8_t {
   CLIMATE_SWING_HORIZONTAL = 3,
 };
 
+/// Enum for all modes a climate swing can be in
+enum ClimatePreset : uint8_t {
+  /// Preset is set to ECO
+  CLIMATE_PRESET_ECO = 0,
+  /// Preset is set to AWAY
+  CLIMATE_PRESET_AWAY = 1,
+  /// Preset is set to BOOST
+  CLIMATE_PRESET_BOOST = 2,
+  /// Preset is set to COMFORT
+  CLIMATE_PRESET_COMFORT = 3,
+  /// Preset is set to HOME
+  CLIMATE_PRESET_HOME = 4,
+  /// Preset is set to SLEEP
+  CLIMATE_PRESET_SLEEP = 5,
+  /// Preset is set to ACTIVITY
+  CLIMATE_PRESET_ACTIVITY = 6,
+};
+
 /// Convert the given ClimateMode to a human-readable string.
 const char *climate_mode_to_string(ClimateMode mode);
 
@@ -83,5 +103,8 @@ const char *climate_fan_mode_to_string(ClimateFanMode mode);
 /// Convert the given ClimateSwingMode to a human-readable string.
 const char *climate_swing_mode_to_string(ClimateSwingMode mode);
 
+/// Convert the given ClimateSwingMode to a human-readable string.
+const char *climate_preset_to_string(ClimatePreset preset);
+
 }  // namespace climate
 }  // namespace esphome
diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp
index 6e941bddf0..eda4722fcb 100644
--- a/esphome/components/climate/climate_traits.cpp
+++ b/esphome/components/climate/climate_traits.cpp
@@ -119,6 +119,71 @@ bool ClimateTraits::get_supports_fan_modes() const {
          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_;
 }
+void ClimateTraits::set_supported_custom_fan_modes(std::vector<std::string> &supported_custom_fan_modes) {
+  this->supported_custom_fan_modes_ = supported_custom_fan_modes;
+}
+const std::vector<std::string> ClimateTraits::get_supported_custom_fan_modes() const {
+  return this->supported_custom_fan_modes_;
+}
+bool ClimateTraits::supports_custom_fan_mode(std::string &custom_fan_mode) const {
+  return std::count(this->supported_custom_fan_modes_.begin(), this->supported_custom_fan_modes_.end(),
+                    custom_fan_mode);
+}
+bool ClimateTraits::supports_preset(ClimatePreset preset) const {
+  switch (preset) {
+    case climate::CLIMATE_PRESET_ECO:
+      return this->supports_preset_eco_;
+    case climate::CLIMATE_PRESET_AWAY:
+      return this->supports_preset_away_;
+    case climate::CLIMATE_PRESET_BOOST:
+      return this->supports_preset_boost_;
+    case climate::CLIMATE_PRESET_COMFORT:
+      return this->supports_preset_comfort_;
+    case climate::CLIMATE_PRESET_HOME:
+      return this->supports_preset_home_;
+    case climate::CLIMATE_PRESET_SLEEP:
+      return this->supports_preset_sleep_;
+    case climate::CLIMATE_PRESET_ACTIVITY:
+      return this->supports_preset_activity_;
+    default:
+      return false;
+  }
+}
+void ClimateTraits::set_supports_preset_eco(bool supports_preset_eco) {
+  this->supports_preset_eco_ = supports_preset_eco;
+}
+void ClimateTraits::set_supports_preset_away(bool supports_preset_away) {
+  this->supports_preset_away_ = supports_preset_away;
+}
+void ClimateTraits::set_supports_preset_boost(bool supports_preset_boost) {
+  this->supports_preset_boost_ = supports_preset_boost;
+}
+void ClimateTraits::set_supports_preset_comfort(bool supports_preset_comfort) {
+  this->supports_preset_comfort_ = supports_preset_comfort;
+}
+void ClimateTraits::set_supports_preset_home(bool supports_preset_home) {
+  this->supports_preset_home_ = supports_preset_home;
+}
+void ClimateTraits::set_supports_preset_sleep(bool supports_preset_sleep) {
+  this->supports_preset_sleep_ = supports_preset_sleep;
+}
+void ClimateTraits::set_supports_preset_activity(bool supports_preset_activity) {
+  this->supports_preset_activity_ = supports_preset_activity;
+}
+bool ClimateTraits::get_supports_presets() const {
+  return this->supports_preset_eco_ || this->supports_preset_away_ || this->supports_preset_boost_ ||
+         this->supports_preset_comfort_ || this->supports_preset_home_ || this->supports_preset_sleep_ ||
+         this->supports_preset_activity_;
+}
+void ClimateTraits::set_supported_custom_presets(std::vector<std::string> &supported_custom_presets) {
+  this->supported_custom_presets_ = supported_custom_presets;
+}
+const std::vector<std::string> ClimateTraits::get_supported_custom_presets() const {
+  return this->supported_custom_presets_;
+}
+bool ClimateTraits::supports_custom_preset(std::string &custom_preset) const {
+  return std::count(this->supported_custom_presets_.begin(), this->supported_custom_presets_.end(), custom_preset);
+}
 void ClimateTraits::set_supports_swing_mode_off(bool supports_swing_mode_off) {
   this->supports_swing_mode_off_ = supports_swing_mode_off;
 }
diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h
index 347a7bc1f2..f0a48ca308 100644
--- a/esphome/components/climate/climate_traits.h
+++ b/esphome/components/climate/climate_traits.h
@@ -1,5 +1,6 @@
 #pragma once
 
+#include "esphome/core/helpers.h"
 #include "climate_mode.h"
 
 namespace esphome {
@@ -65,6 +66,21 @@ class ClimateTraits {
   void set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse);
   bool supports_fan_mode(ClimateFanMode fan_mode) const;
   bool get_supports_fan_modes() const;
+  void set_supported_custom_fan_modes(std::vector<std::string> &supported_custom_fan_modes);
+  const std::vector<std::string> get_supported_custom_fan_modes() const;
+  bool supports_custom_fan_mode(std::string &custom_fan_mode) const;
+  bool supports_preset(ClimatePreset preset) const;
+  void set_supports_preset_eco(bool supports_preset_eco);
+  void set_supports_preset_away(bool supports_preset_away);
+  void set_supports_preset_boost(bool supports_preset_boost);
+  void set_supports_preset_comfort(bool supports_preset_comfort);
+  void set_supports_preset_home(bool supports_preset_home);
+  void set_supports_preset_sleep(bool supports_preset_sleep);
+  void set_supports_preset_activity(bool supports_preset_activity);
+  bool get_supports_presets() const;
+  void set_supported_custom_presets(std::vector<std::string> &supported_custom_presets);
+  const std::vector<std::string> get_supported_custom_presets() const;
+  bool supports_custom_preset(std::string &custom_preset) const;
   void set_supports_swing_mode_off(bool supports_swing_mode_off);
   void set_supports_swing_mode_both(bool supports_swing_mode_both);
   void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical);
@@ -103,6 +119,15 @@ class ClimateTraits {
   bool supports_swing_mode_both_{false};
   bool supports_swing_mode_vertical_{false};
   bool supports_swing_mode_horizontal_{false};
+  bool supports_preset_eco_{false};
+  bool supports_preset_away_{false};
+  bool supports_preset_boost_{false};
+  bool supports_preset_comfort_{false};
+  bool supports_preset_home_{false};
+  bool supports_preset_sleep_{false};
+  bool supports_preset_activity_{false};
+  std::vector<std::string> supported_custom_fan_modes_;
+  std::vector<std::string> supported_custom_presets_;
 
   float visual_min_temperature_{10};
   float visual_max_temperature_{30};
diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py
index 8dcd75c31f..1389ebfc6d 100644
--- a/esphome/components/climate_ir/__init__.py
+++ b/esphome/components/climate_ir/__init__.py
@@ -9,7 +9,6 @@ from esphome.components import (
 )
 from esphome.components.remote_base import CONF_RECEIVER_ID, CONF_TRANSMITTER_ID
 from esphome.const import CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, CONF_SENSOR
-from esphome.core import coroutine
 
 AUTO_LOAD = ["sensor", "remote_base"]
 CODEOWNERS = ["@glmnet"]
@@ -39,19 +38,18 @@ CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend(
 )
 
 
-@coroutine
-def register_climate_ir(var, config):
-    yield cg.register_component(var, config)
-    yield climate.register_climate(var, config)
+async def register_climate_ir(var, config):
+    await cg.register_component(var, config)
+    await climate.register_climate(var, config)
 
     cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
     cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
     if CONF_SENSOR in config:
-        sens = yield cg.get_variable(config[CONF_SENSOR])
+        sens = await cg.get_variable(config[CONF_SENSOR])
         cg.add(var.set_sensor(sens))
     if CONF_RECEIVER_ID in config:
-        receiver = yield cg.get_variable(config[CONF_RECEIVER_ID])
+        receiver = await cg.get_variable(config[CONF_RECEIVER_ID])
         cg.add(receiver.register_listener(var))
 
-    transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID])
+    transmitter = await cg.get_variable(config[CONF_TRANSMITTER_ID])
     cg.add(var.set_transmitter(transmitter))
diff --git a/esphome/components/climate_ir_lg/climate.py b/esphome/components/climate_ir_lg/climate.py
index 06e538d9c7..c58e40f7f4 100644
--- a/esphome/components/climate_ir_lg/climate.py
+++ b/esphome/components/climate_ir_lg/climate.py
@@ -36,9 +36,9 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield climate_ir.register_climate_ir(var, config)
+    await climate_ir.register_climate_ir(var, config)
 
     cg.add(var.set_header_high(config[CONF_HEADER_HIGH]))
     cg.add(var.set_header_low(config[CONF_HEADER_LOW]))
diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.cpp b/esphome/components/climate_ir_lg/climate_ir_lg.cpp
index ee73d30796..983d33c0b1 100644
--- a/esphome/components/climate_ir_lg/climate_ir_lg.cpp
+++ b/esphome/components/climate_ir_lg/climate_ir_lg.cpp
@@ -72,7 +72,7 @@ void LgIrClimate::transmit_state() {
       remote_state |= FAN_AUTO;
     } else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_DRY ||
                this->mode == climate::CLIMATE_MODE_HEAT) {
-      switch (this->fan_mode) {
+      switch (this->fan_mode.value()) {
         case climate::CLIMATE_FAN_HIGH:
           remote_state |= FAN_MAX;
           break;
diff --git a/esphome/components/color/__init__.py b/esphome/components/color/__init__.py
index 6712d078a4..47679fcc68 100644
--- a/esphome/components/color/__init__.py
+++ b/esphome/components/color/__init__.py
@@ -26,7 +26,7 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     r = 0
     if CONF_RED in config:
         r = int(config[CONF_RED] * 255)
diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py
index b7a584643b..2cfd1912e5 100644
--- a/esphome/components/coolix/climate.py
+++ b/esphome/components/coolix/climate.py
@@ -16,6 +16,6 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield climate_ir.register_climate_ir(var, config)
+    await climate_ir.register_climate_ir(var, config)
diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp
index 441f43b424..e50521a348 100644
--- a/esphome/components/coolix/coolix.cpp
+++ b/esphome/components/coolix/coolix.cpp
@@ -93,7 +93,7 @@ void CoolixClimate::transmit_state() {
         this->fan_mode = climate::CLIMATE_FAN_AUTO;
         remote_state |= COOLIX_FAN_MODE_AUTO_DRY;
       } else {
-        switch (this->fan_mode) {
+        switch (this->fan_mode.value()) {
           case climate::CLIMATE_FAN_HIGH:
             remote_state |= COOLIX_FAN_MAX;
             break;
diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py
index e731c18333..4a7266303d 100644
--- a/esphome/components/cover/__init__.py
+++ b/esphome/components/cover/__init__.py
@@ -14,7 +14,7 @@ from esphome.const import (
     CONF_MQTT_ID,
     CONF_NAME,
 )
-from esphome.core import CORE, coroutine, coroutine_with_priority
+from esphome.core import CORE, coroutine_with_priority
 
 IS_PLATFORM_COMPONENT = True
 
@@ -73,8 +73,7 @@ COVER_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend(
 )
 
 
-@coroutine
-def setup_cover_core_(var, config):
+async def setup_cover_core_(var, config):
     cg.add(var.set_name(config[CONF_NAME]))
     if CONF_INTERNAL in config:
         cg.add(var.set_internal(config[CONF_INTERNAL]))
@@ -83,15 +82,14 @@ def setup_cover_core_(var, config):
 
     if CONF_MQTT_ID in config:
         mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
-        yield mqtt.register_mqtt_component(mqtt_, config)
+        await mqtt.register_mqtt_component(mqtt_, config)
 
 
-@coroutine
-def register_cover(var, config):
+async def register_cover(var, config):
     if not CORE.has_id(config[CONF_ID]):
         var = cg.Pvariable(config[CONF_ID], var)
     cg.add(cg.App.register_cover(var))
-    yield setup_cover_core_(var, config)
+    await setup_cover_core_(var, config)
 
 
 COVER_ACTION_SCHEMA = maybe_simple_id(
@@ -102,21 +100,21 @@ COVER_ACTION_SCHEMA = maybe_simple_id(
 
 
 @automation.register_action("cover.open", OpenAction, COVER_ACTION_SCHEMA)
-def cover_open_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, paren)
+async def cover_open_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
 
 
 @automation.register_action("cover.close", CloseAction, COVER_ACTION_SCHEMA)
-def cover_close_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, paren)
+async def cover_close_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
 
 
 @automation.register_action("cover.stop", StopAction, COVER_ACTION_SCHEMA)
-def cover_stop_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, paren)
+async def cover_stop_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
 
 
 COVER_CONTROL_ACTION_SCHEMA = cv.Schema(
@@ -131,25 +129,25 @@ COVER_CONTROL_ACTION_SCHEMA = cv.Schema(
 
 
 @automation.register_action("cover.control", ControlAction, COVER_CONTROL_ACTION_SCHEMA)
-def cover_control_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def cover_control_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
     if CONF_STOP in config:
-        template_ = yield cg.templatable(config[CONF_STOP], args, bool)
+        template_ = await cg.templatable(config[CONF_STOP], args, bool)
         cg.add(var.set_stop(template_))
     if CONF_STATE in config:
-        template_ = yield cg.templatable(config[CONF_STATE], args, float)
+        template_ = await cg.templatable(config[CONF_STATE], args, float)
         cg.add(var.set_position(template_))
     if CONF_POSITION in config:
-        template_ = yield cg.templatable(config[CONF_POSITION], args, float)
+        template_ = await cg.templatable(config[CONF_POSITION], args, float)
         cg.add(var.set_position(template_))
     if CONF_TILT in config:
-        template_ = yield cg.templatable(config[CONF_TILT], args, float)
+        template_ = await cg.templatable(config[CONF_TILT], args, float)
         cg.add(var.set_tilt(template_))
-    yield var
+    return var
 
 
 @coroutine_with_priority(100.0)
-def to_code(config):
+async def to_code(config):
     cg.add_define("USE_COVER")
     cg.add_global(cover_ns.using)
diff --git a/esphome/components/cs5460a/__init__.py b/esphome/components/cs5460a/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/cs5460a/cs5460a.cpp b/esphome/components/cs5460a/cs5460a.cpp
new file mode 100644
index 0000000000..03cbd83513
--- /dev/null
+++ b/esphome/components/cs5460a/cs5460a.cpp
@@ -0,0 +1,342 @@
+#include "cs5460a.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace cs5460a {
+
+static const char *TAG = "cs5460a";
+
+void CS5460AComponent::write_register_(enum CS5460ARegister addr, uint32_t value) {
+  this->write_byte(CMD_WRITE | (addr << 1));
+  this->write_byte(value >> 16);
+  this->write_byte(value >> 8);
+  this->write_byte(value >> 0);
+}
+
+uint32_t CS5460AComponent::read_register_(uint8_t addr) {
+  uint32_t value;
+
+  this->write_byte(CMD_READ | (addr << 1));
+  value = (uint32_t) this->transfer_byte(CMD_SYNC0) << 16;
+  value |= (uint32_t) this->transfer_byte(CMD_SYNC0) << 8;
+  value |= this->transfer_byte(CMD_SYNC0) << 0;
+
+  return value;
+}
+
+bool CS5460AComponent::softreset_() {
+  uint32_t pc = ((uint8_t) phase_offset_ & 0x3f) | (phase_offset_ < 0 ? 0x40 : 0);
+  uint32_t config = (1 << 0) |                    /* K = 0b0001 */
+                    (current_hpf_ ? 1 << 5 : 0) | /* IHPF */
+                    (voltage_hpf_ ? 1 << 6 : 0) | /* VHPF */
+                    (pga_gain_ << 16) |           /* Gi */
+                    (pc << 17);                   /* PC */
+  int cnt = 0;
+
+  /* Serial resynchronization */
+  this->write_byte(CMD_SYNC1);
+  this->write_byte(CMD_SYNC1);
+  this->write_byte(CMD_SYNC1);
+  this->write_byte(CMD_SYNC0);
+
+  /* Reset */
+  this->write_register_(REG_CONFIG, 1 << 7);
+  delay(10);
+  while (cnt++ < 50 && (this->read_register_(REG_CONFIG) & 0x81) != 0x000001)
+    ;
+  if (cnt > 50)
+    return false;
+
+  this->write_register_(REG_CONFIG, config);
+  return true;
+}
+
+void CS5460AComponent::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up CS5460A...");
+
+  float current_full_scale = (pga_gain_ == CS5460A_PGA_GAIN_10X) ? 0.25 : 0.10;
+  float voltage_full_scale = 0.25;
+  current_multiplier_ = current_full_scale / (fabsf(current_gain_) * 0x1000000);
+  voltage_multiplier_ = voltage_full_scale / (voltage_gain_ * 0x1000000);
+
+  /*
+   * Calculate power from the Energy register because the Power register
+   * stores instantaneous power which varies a lot in each AC cycle,
+   * while the Energy value is accumulated over the "computation cycle"
+   * which should be an integer number of AC cycles.
+   */
+  power_multiplier_ =
+      (current_full_scale * voltage_full_scale * 4096) / (current_gain_ * voltage_gain_ * samples_ * 0x800000);
+
+  pulse_freq_ =
+      (current_full_scale * voltage_full_scale) / (fabsf(current_gain_) * voltage_gain_ * pulse_energy_wh_ * 3600);
+
+  hw_init_();
+}
+
+void CS5460AComponent::hw_init_() {
+  this->spi_setup();
+  this->enable();
+
+  if (!this->softreset_()) {
+    this->disable();
+    ESP_LOGE(TAG, "CS5460A reset failed!");
+    this->mark_failed();
+    return;
+  }
+
+  uint32_t status = this->read_register_(REG_STATUS);
+  ESP_LOGCONFIG(TAG, "  Version: %x", (status >> 6) & 7);
+
+  this->write_register_(REG_CYCLE_COUNT, samples_);
+  this->write_register_(REG_PULSE_RATE, lroundf(pulse_freq_ * 32.0f));
+
+  /* Use one of the power saving features (assuming external oscillator), reset other CONTROL bits,
+   * sometimes softreset_() is not enough */
+  this->write_register_(REG_CONTROL, 0x000004);
+
+  this->restart_();
+  this->disable();
+  ESP_LOGCONFIG(TAG, "  Init ok");
+}
+
+/* Doesn't reset the register values etc., just restarts the "computation cycle" */
+void CS5460AComponent::restart_() {
+  int cnt;
+
+  this->enable();
+  /* Stop running conversion, wake up if needed */
+  this->write_byte(CMD_POWER_UP);
+  /* Start continuous conversion */
+  this->write_byte(CMD_START_CONT);
+  this->disable();
+
+  this->started_();
+}
+
+void CS5460AComponent::started_() {
+  /*
+   * Try to guess when the next batch of results is going to be ready and
+   * schedule next STATUS check some time before that moment.  This assumes
+   * two things:
+   *   * a new "computation cycle" started just now.  If it started some
+   *     time ago we may be a late next time, but hopefully less late in each
+   *     iteration -- that's why we schedule the next check in some 0.8 of
+   *     the time we actually expect the next reading ready.
+   *   * MCLK rate is 4.096MHz and K == 1.  If there's a CS5460A module in
+   *     use with a different clock this will need to be parametrised.
+   */
+  expect_data_ts_ = millis() + samples_ * 1024 / 4096;
+
+  schedule_next_check_();
+}
+
+void CS5460AComponent::schedule_next_check_() {
+  int32_t time_left = expect_data_ts_ - millis();
+
+  /* First try at 0.8 of the actual expected time (if it's in the future) */
+  if (time_left > 0)
+    time_left -= time_left / 5;
+
+  if (time_left > -500) {
+    /* But not sooner than in 30ms from now */
+    if (time_left < 30)
+      time_left = 30;
+  } else {
+    /*
+     * If the measurement is more than 0.5s overdue start worrying.  The
+     * device may be stuck because of an overcurrent error or similar,
+     * from now on just retry every 1s.  After 15s try a reset, if it
+     * fails we give up and mark the component "failed".
+     */
+    if (time_left > -15000) {
+      time_left = 1000;
+      this->status_momentary_warning("warning", 1000);
+    } else {
+      ESP_LOGCONFIG(TAG, "Device officially stuck, resetting");
+      this->cancel_timeout("status-check");
+      this->hw_init_();
+      return;
+    }
+  }
+
+  this->set_timeout("status-check", time_left, [this]() {
+    if (!this->check_status_())
+      this->schedule_next_check_();
+  });
+}
+
+bool CS5460AComponent::check_status_() {
+  this->enable();
+  uint32_t status = this->read_register_(REG_STATUS);
+
+  if (!(status & 0xcbf83c)) {
+    this->disable();
+    return false;
+  }
+
+  uint32_t clear = 1 << 20;
+
+  /* TODO: Report if IC=0 but only once as it can't be cleared */
+
+  if (status & (1 << 2)) {
+    clear |= 1 << 2;
+    ESP_LOGE(TAG, "Low supply detected");
+    this->status_momentary_warning("warning", 500);
+  }
+
+  if (status & (1 << 3)) {
+    clear |= 1 << 3;
+    ESP_LOGE(TAG, "Modulator oscillation on current channel");
+    this->status_momentary_warning("warning", 500);
+  }
+
+  if (status & (1 << 4)) {
+    clear |= 1 << 4;
+    ESP_LOGE(TAG, "Modulator oscillation on voltage channel");
+    this->status_momentary_warning("warning", 500);
+  }
+
+  if (status & (1 << 5)) {
+    clear |= 1 << 5;
+    ESP_LOGE(TAG, "Watch-dog timeout");
+    this->status_momentary_warning("warning", 500);
+  }
+
+  if (status & (1 << 11)) {
+    clear |= 1 << 11;
+    ESP_LOGE(TAG, "EOUT Energy Accumulation Register out of range");
+    this->status_momentary_warning("warning", 500);
+  }
+
+  if (status & (1 << 12)) {
+    clear |= 1 << 12;
+    ESP_LOGE(TAG, "Energy out of range");
+    this->status_momentary_warning("warning", 500);
+  }
+
+  if (status & (1 << 13)) {
+    clear |= 1 << 13;
+    ESP_LOGE(TAG, "RMS voltage out of range");
+    this->status_momentary_warning("warning", 500);
+  }
+
+  if (status & (1 << 14)) {
+    clear |= 1 << 14;
+    ESP_LOGE(TAG, "RMS current out of range");
+    this->status_momentary_warning("warning", 500);
+  }
+
+  if (status & (1 << 15)) {
+    clear |= 1 << 15;
+    ESP_LOGE(TAG, "Power calculation out of range");
+    this->status_momentary_warning("warning", 500);
+  }
+
+  if (status & (1 << 16)) {
+    clear |= 1 << 16;
+    ESP_LOGE(TAG, "Voltage out of range");
+    this->status_momentary_warning("warning", 500);
+  }
+
+  if (status & (1 << 17)) {
+    clear |= 1 << 17;
+    ESP_LOGE(TAG, "Current out of range");
+    this->status_momentary_warning("warning", 500);
+  }
+
+  if (status & (1 << 19)) {
+    clear |= 1 << 19;
+    ESP_LOGE(TAG, "Divide overflowed");
+  }
+
+  if (status & (1 << 22)) {
+    bool dir = status & (1 << 21);
+    if (current_gain_ < 0)
+      dir = !dir;
+    ESP_LOGI(TAG, "Energy counter %s pulse", dir ? "negative" : "positive");
+    clear |= 1 << 22;
+  }
+
+  uint32_t raw_current = 0; /* Calm the validators */
+  uint32_t raw_voltage = 0;
+  uint32_t raw_energy = 0;
+
+  if (status & (1 << 23)) {
+    clear |= 1 << 23;
+
+    if (current_sensor_ != nullptr)
+      raw_current = this->read_register_(REG_IRMS);
+
+    if (voltage_sensor_ != nullptr)
+      raw_voltage = this->read_register_(REG_VRMS);
+  }
+
+  if (status & ((1 << 23) | (1 << 5))) {
+    /* Read to clear the WDT bit */
+    raw_energy = this->read_register_(REG_E);
+  }
+
+  this->write_register_(REG_STATUS, clear);
+  this->disable();
+
+  /*
+   * Schedule the next STATUS check assuming that DRDY was asserted very
+   * recently, then publish the new values.  Do this last for reentrancy in
+   * case the publish triggers a restart() or for whatever reason needs to
+   * cancel the timeout set in schedule_next_check_(), or needs to use SPI.
+   * If the current or power values haven't changed one bit it may be that
+   * the chip somehow forgot to update the registers -- seen happening very
+   * rarely.  In that case don't publish them because the user may have
+   * the input connected to a multiplexer and may have switched channels
+   * since the previous reading and we'd be publishing the stale value for
+   * the new channel.  If the value *was* updated it's very unlikely that
+   * it wouldn't have changed, especially power/energy which are affected
+   * by the noise on both the current and value channels (in case of energy,
+   * accumulated over many conversion cycles.)
+   */
+  if (status & (1 << 23)) {
+    this->started_();
+
+    if (current_sensor_ != nullptr && raw_current != prev_raw_current_) {
+      current_sensor_->publish_state(raw_current * current_multiplier_);
+      prev_raw_current_ = raw_current;
+    }
+
+    if (voltage_sensor_ != nullptr)
+      voltage_sensor_->publish_state(raw_voltage * voltage_multiplier_);
+
+    if (power_sensor_ != nullptr && raw_energy != prev_raw_energy_) {
+      int32_t raw = (int32_t)(raw_energy << 8) >> 8; /* Sign-extend */
+      power_sensor_->publish_state(raw * power_multiplier_);
+      prev_raw_energy_ = raw_energy;
+    }
+
+    return true;
+  }
+
+  return false;
+}
+
+void CS5460AComponent::dump_config() {
+  uint32_t state = this->get_component_state();
+
+  ESP_LOGCONFIG(TAG, "CS5460A:");
+  ESP_LOGCONFIG(TAG, "  Init status: %s",
+                state == COMPONENT_STATE_LOOP ? "OK" : (state == COMPONENT_STATE_FAILED ? "failed" : "other"));
+  LOG_PIN("  CS Pin: ", cs_);
+  ESP_LOGCONFIG(TAG, "  Samples / cycle: %u", samples_);
+  ESP_LOGCONFIG(TAG, "  Phase offset: %i", phase_offset_);
+  ESP_LOGCONFIG(TAG, "  PGA Gain: %s", pga_gain_ == CS5460A_PGA_GAIN_50X ? "50x" : "10x");
+  ESP_LOGCONFIG(TAG, "  Current gain: %.5f", current_gain_);
+  ESP_LOGCONFIG(TAG, "  Voltage gain: %.5f", voltage_gain_);
+  ESP_LOGCONFIG(TAG, "  Current HPF: %s", current_hpf_ ? "enabled" : "disabled");
+  ESP_LOGCONFIG(TAG, "  Voltage HPF: %s", voltage_hpf_ ? "enabled" : "disabled");
+  ESP_LOGCONFIG(TAG, "  Pulse energy: %.2f Wh", pulse_energy_wh_);
+  LOG_SENSOR("  ", "Voltage", voltage_sensor_);
+  LOG_SENSOR("  ", "Current", current_sensor_);
+  LOG_SENSOR("  ", "Power", power_sensor_);
+}
+
+}  // namespace cs5460a
+}  // namespace esphome
diff --git a/esphome/components/cs5460a/cs5460a.h b/esphome/components/cs5460a/cs5460a.h
new file mode 100644
index 0000000000..699049757c
--- /dev/null
+++ b/esphome/components/cs5460a/cs5460a.h
@@ -0,0 +1,123 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/automation.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/spi/spi.h"
+
+namespace esphome {
+namespace cs5460a {
+
+enum CS5460ACommand {
+  CMD_SYNC0 = 0xfe,
+  CMD_SYNC1 = 0xff,
+  CMD_START_SINGLE = 0xe0,
+  CMD_START_CONT = 0xe8,
+  CMD_POWER_UP = 0xa0,
+  CMD_POWER_STANDBY = 0x88,
+  CMD_POWER_SLEEP = 0x90,
+  CMD_CALIBRATION = 0xc0,
+  CMD_READ = 0x00,
+  CMD_WRITE = 0x40,
+};
+
+enum CS5460ARegister {
+  REG_CONFIG = 0x00,
+  REG_IDCOFF = 0x01,
+  REG_IGN = 0x02,
+  REG_VDCOFF = 0x03,
+  REG_VGN = 0x04,
+  REG_CYCLE_COUNT = 0x05,
+  REG_PULSE_RATE = 0x06,
+  REG_I = 0x07,
+  REG_V = 0x08,
+  REG_P = 0x09,
+  REG_E = 0x0a,
+  REG_IRMS = 0x0b,
+  REG_VRMS = 0x0c,
+  REG_TBC = 0x0d,
+  REG_POFF = 0x0e,
+  REG_STATUS = 0x0f,
+  REG_IACOFF = 0x10,
+  REG_VACOFF = 0x11,
+  REG_MASK = 0x1a,
+  REG_CONTROL = 0x1c,
+};
+
+/** Enum listing the current channel aplifiergain settings for the CS5460A.
+ */
+enum CS5460APGAGain {
+  CS5460A_PGA_GAIN_10X = 0b0,
+  CS5460A_PGA_GAIN_50X = 0b1,
+};
+
+class CS5460AComponent : public Component,
+                         public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
+                                               spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
+ public:
+  void set_samples(uint32_t samples) { samples_ = samples; }
+  void set_phase_offset(int8_t phase_offset) { phase_offset_ = phase_offset; }
+  void set_pga_gain(CS5460APGAGain pga_gain) { pga_gain_ = pga_gain; }
+  void set_gains(float current_gain, float voltage_gain) {
+    current_gain_ = current_gain;
+    voltage_gain_ = voltage_gain;
+  }
+  void set_hpf_enable(bool current_hpf, bool voltage_hpf) {
+    current_hpf_ = current_hpf;
+    voltage_hpf_ = voltage_hpf;
+  }
+  void set_pulse_energy_wh(float pulse_energy_wh) { pulse_energy_wh_ = pulse_energy_wh; }
+  void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
+  void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
+  void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; }
+
+  void restart() { restart_(); }
+
+  void setup() override;
+  void loop() override {}
+  float get_setup_priority() const override { return setup_priority::DATA; }
+  void dump_config() override;
+
+ protected:
+  uint32_t samples_;
+  int8_t phase_offset_;
+  CS5460APGAGain pga_gain_;
+  float current_gain_;
+  float voltage_gain_;
+  bool current_hpf_;
+  bool voltage_hpf_;
+  float pulse_energy_wh_;
+  sensor::Sensor *current_sensor_{nullptr};
+  sensor::Sensor *voltage_sensor_{nullptr};
+  sensor::Sensor *power_sensor_{nullptr};
+
+  void write_register_(enum CS5460ARegister addr, uint32_t value);
+  uint32_t read_register_(uint8_t addr);
+  bool softreset_();
+  void hw_init_();
+  void restart_();
+  void started_();
+  void schedule_next_check_();
+  bool check_status_();
+
+  float current_multiplier_;
+  float voltage_multiplier_;
+  float power_multiplier_;
+  float pulse_freq_;
+  uint32_t expect_data_ts_;
+  uint32_t prev_raw_current_{0};
+  uint32_t prev_raw_energy_{0};
+};
+
+template<typename... Ts> class CS5460ARestartAction : public Action<Ts...> {
+ public:
+  CS5460ARestartAction(CS5460AComponent *cs5460a) : cs5460a_(cs5460a) {}
+
+  void play(Ts... x) override { cs5460a_->restart(); }
+
+ protected:
+  CS5460AComponent *cs5460a_;
+};
+
+}  // namespace cs5460a
+}  // namespace esphome
diff --git a/esphome/components/cs5460a/sensor.py b/esphome/components/cs5460a/sensor.py
new file mode 100644
index 0000000000..efb1d1d426
--- /dev/null
+++ b/esphome/components/cs5460a/sensor.py
@@ -0,0 +1,136 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import spi, sensor
+from esphome.const import (
+    CONF_CURRENT,
+    CONF_ID,
+    CONF_POWER,
+    CONF_VOLTAGE,
+    UNIT_VOLT,
+    UNIT_AMPERE,
+    UNIT_WATT,
+    ICON_EMPTY,
+    DEVICE_CLASS_POWER,
+    DEVICE_CLASS_CURRENT,
+    DEVICE_CLASS_VOLTAGE,
+)
+from esphome import automation
+from esphome.automation import maybe_simple_id
+
+CODEOWNERS = ["@balrog-kun"]
+DEPENDENCIES = ["spi"]
+
+cs5460a_ns = cg.esphome_ns.namespace("cs5460a")
+CS5460APGAGain = cs5460a_ns.enum("CS5460APGAGain")
+PGA_GAIN_OPTIONS = {
+    "10X": CS5460APGAGain.CS5460A_PGA_GAIN_10X,
+    "50X": CS5460APGAGain.CS5460A_PGA_GAIN_50X,
+}
+
+CS5460AComponent = cs5460a_ns.class_("CS5460AComponent", spi.SPIDevice, cg.Component)
+CS5460ARestartAction = cs5460a_ns.class_("CS5460ARestartAction", automation.Action)
+
+CONF_SAMPLES = "samples"
+CONF_PHASE_OFFSET = "phase_offset"
+CONF_PGA_GAIN = "pga_gain"
+CONF_CURRENT_GAIN = "current_gain"
+CONF_VOLTAGE_GAIN = "voltage_gain"
+CONF_CURRENT_HPF = "current_hpf"
+CONF_VOLTAGE_HPF = "voltage_hpf"
+CONF_PULSE_ENERGY = "pulse_energy"
+
+
+def validate_config(config):
+    current_gain = abs(config[CONF_CURRENT_GAIN]) * (
+        1.0 if config[CONF_PGA_GAIN] == "10X" else 5.0
+    )
+    voltage_gain = config[CONF_VOLTAGE_GAIN]
+    pulse_energy = config[CONF_PULSE_ENERGY]
+
+    if current_gain == 0.0 or voltage_gain == 0.0:
+        raise cv.Invalid("The gains can't be zero")
+
+    max_energy = (0.25 * 0.25 / 3600 / (2 ** -4)) / (voltage_gain * current_gain)
+    min_energy = (0.25 * 0.25 / 3600 / (2 ** 18)) / (voltage_gain * current_gain)
+    mech_min_energy = (0.25 * 0.25 / 3600 / 7.8) / (voltage_gain * current_gain)
+    if pulse_energy < min_energy or pulse_energy > max_energy:
+        raise cv.Invalid(
+            "For given current&voltage gains, the pulse energy must be between "
+            f"{min_energy} Wh and {max_energy} Wh and in mechanical counter mode "
+            f"between {mech_min_energy} Wh and {max_energy} Wh"
+        )
+
+    return config
+
+
+validate_energy = cv.float_with_unit("energy", "(Wh|WH|wh)?", optional_unit=True)
+
+CONFIG_SCHEMA = cv.All(
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(CS5460AComponent),
+            cv.Optional(CONF_SAMPLES, default=4000): cv.int_range(min=1, max=0xFFFFFF),
+            cv.Optional(CONF_PHASE_OFFSET, default=0): cv.int_range(min=-64, max=63),
+            cv.Optional(CONF_PGA_GAIN, default="10X"): cv.enum(
+                PGA_GAIN_OPTIONS, upper=True
+            ),
+            cv.Optional(CONF_CURRENT_GAIN, default=0.001): cv.negative_one_to_one_float,
+            cv.Optional(CONF_VOLTAGE_GAIN, default=0.001): cv.zero_to_one_float,
+            cv.Optional(CONF_CURRENT_HPF, default=True): cv.boolean,
+            cv.Optional(CONF_VOLTAGE_HPF, default=True): cv.boolean,
+            cv.Optional(CONF_PULSE_ENERGY, default=10.0): validate_energy,
+            cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
+                UNIT_VOLT, ICON_EMPTY, 0, DEVICE_CLASS_VOLTAGE
+            ),
+            cv.Optional(CONF_CURRENT): sensor.sensor_schema(
+                UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT
+            ),
+            cv.Optional(CONF_POWER): sensor.sensor_schema(
+                UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER
+            ),
+        }
+    )
+    .extend(cv.COMPONENT_SCHEMA)
+    .extend(spi.spi_device_schema(cs_pin_required=False)),
+    validate_config,
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await spi.register_spi_device(var, config)
+
+    cg.add(var.set_samples(config[CONF_SAMPLES]))
+    cg.add(var.set_phase_offset(config[CONF_PHASE_OFFSET]))
+    cg.add(var.set_pga_gain(config[CONF_PGA_GAIN]))
+    cg.add(var.set_gains(config[CONF_CURRENT_GAIN], config[CONF_VOLTAGE_GAIN]))
+    cg.add(var.set_hpf_enable(config[CONF_CURRENT_HPF], config[CONF_VOLTAGE_HPF]))
+    cg.add(var.set_pulse_energy_wh(config[CONF_PULSE_ENERGY]))
+
+    if CONF_VOLTAGE in config:
+        conf = config[CONF_VOLTAGE]
+        sens = await sensor.new_sensor(conf)
+        cg.add(var.set_voltage_sensor(sens))
+    if CONF_CURRENT in config:
+        conf = config[CONF_CURRENT]
+        sens = await sensor.new_sensor(conf)
+        cg.add(var.set_current_sensor(sens))
+    if CONF_POWER in config:
+        conf = config[CONF_POWER]
+        sens = await sensor.new_sensor(conf)
+        cg.add(var.set_power_sensor(sens))
+
+
+@automation.register_action(
+    "cs5460a.restart",
+    CS5460ARestartAction,
+    maybe_simple_id(
+        {
+            cv.Required(CONF_ID): cv.use_id(CS5460AComponent),
+        }
+    ),
+)
+async def restart_action_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
diff --git a/esphome/components/cse7766/sensor.py b/esphome/components/cse7766/sensor.py
index 71e9458618..98cf4da96d 100644
--- a/esphome/components/cse7766/sensor.py
+++ b/esphome/components/cse7766/sensor.py
@@ -10,6 +10,7 @@ from esphome.const import (
     DEVICE_CLASS_POWER,
     DEVICE_CLASS_VOLTAGE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_VOLT,
     UNIT_AMPERE,
     UNIT_WATT,
@@ -27,13 +28,17 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(CSE7766Component),
             cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
-                UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE
+                UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT
             ),
             cv.Optional(CONF_CURRENT): sensor.sensor_schema(
-                UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT
+                UNIT_AMPERE,
+                ICON_EMPTY,
+                2,
+                DEVICE_CLASS_CURRENT,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_POWER): sensor.sensor_schema(
-                UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER
+                UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
             ),
         }
     )
@@ -42,20 +47,26 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield uart.register_uart_device(var, config)
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
 
     if CONF_VOLTAGE in config:
         conf = config[CONF_VOLTAGE]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_voltage_sensor(sens))
     if CONF_CURRENT in config:
         conf = config[CONF_CURRENT]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_current_sensor(sens))
     if CONF_POWER in config:
         conf = config[CONF_POWER]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_power_sensor(sens))
+
+
+def validate(config, item_config):
+    uart.validate_device(
+        "cse7766", config, item_config, baud_rate=4800, require_tx=False
+    )
diff --git a/esphome/components/ct_clamp/sensor.py b/esphome/components/ct_clamp/sensor.py
index e4dbd92387..e44d46e7f4 100644
--- a/esphome/components/ct_clamp/sensor.py
+++ b/esphome/components/ct_clamp/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_ID,
     DEVICE_CLASS_CURRENT,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_AMPERE,
 )
 
@@ -18,7 +19,9 @@ ct_clamp_ns = cg.esphome_ns.namespace("ct_clamp")
 CTClampSensor = ct_clamp_ns.class_("CTClampSensor", sensor.Sensor, cg.PollingComponent)
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT)
+    sensor.sensor_schema(
+        UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(CTClampSensor),
@@ -32,11 +35,11 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
 
-    sens = yield cg.get_variable(config[CONF_SENSOR])
+    sens = await cg.get_variable(config[CONF_SENSOR])
     cg.add(var.set_source(sens))
     cg.add(var.set_sample_duration(config[CONF_SAMPLE_DURATION]))
diff --git a/esphome/components/custom/binary_sensor/__init__.py b/esphome/components/custom/binary_sensor/__init__.py
index 402540c254..18d613d4c1 100644
--- a/esphome/components/custom/binary_sensor/__init__.py
+++ b/esphome/components/custom/binary_sensor/__init__.py
@@ -17,8 +17,8 @@ CONFIG_SCHEMA = cv.Schema(
 )
 
 
-def to_code(config):
-    template_ = yield cg.process_lambda(
+async def to_code(config):
+    template_ = await cg.process_lambda(
         config[CONF_LAMBDA],
         [],
         return_type=cg.std_vector.template(binary_sensor.BinarySensorPtr),
@@ -28,4 +28,4 @@ def to_code(config):
     custom = cg.variable(config[CONF_ID], rhs)
     for i, conf in enumerate(config[CONF_BINARY_SENSORS]):
         rhs = custom.Pget_binary_sensor(i)
-        yield binary_sensor.register_binary_sensor(rhs, conf)
+        await binary_sensor.register_binary_sensor(rhs, conf)
diff --git a/esphome/components/custom/climate/__init__.py b/esphome/components/custom/climate/__init__.py
index 75f9c3247e..a95456133a 100644
--- a/esphome/components/custom/climate/__init__.py
+++ b/esphome/components/custom/climate/__init__.py
@@ -16,8 +16,8 @@ CONFIG_SCHEMA = cv.Schema(
 )
 
 
-def to_code(config):
-    template_ = yield cg.process_lambda(
+async def to_code(config):
+    template_ = await cg.process_lambda(
         config[CONF_LAMBDA],
         [],
         return_type=cg.std_vector.template(climate.Climate.operator("ptr")),
@@ -27,4 +27,4 @@ def to_code(config):
     custom = cg.variable(config[CONF_ID], rhs)
     for i, conf in enumerate(config[CONF_CLIMATES]):
         rhs = custom.Pget_climate(i)
-        yield climate.register_climate(rhs, conf)
+        await climate.register_climate(rhs, conf)
diff --git a/esphome/components/custom/cover/__init__.py b/esphome/components/custom/cover/__init__.py
index 35f25b827d..37fd4cdbbc 100644
--- a/esphome/components/custom/cover/__init__.py
+++ b/esphome/components/custom/cover/__init__.py
@@ -16,8 +16,8 @@ CONFIG_SCHEMA = cv.Schema(
 )
 
 
-def to_code(config):
-    template_ = yield cg.process_lambda(
+async def to_code(config):
+    template_ = await cg.process_lambda(
         config[CONF_LAMBDA],
         [],
         return_type=cg.std_vector.template(cover.Cover.operator("ptr")),
@@ -27,4 +27,4 @@ def to_code(config):
     custom = cg.variable(config[CONF_ID], rhs)
     for i, conf in enumerate(config[CONF_COVERS]):
         rhs = custom.Pget_cover(i)
-        yield cover.register_cover(rhs, conf)
+        await cover.register_cover(rhs, conf)
diff --git a/esphome/components/custom/light/__init__.py b/esphome/components/custom/light/__init__.py
index f4bd8331f1..b6ebe13ab2 100644
--- a/esphome/components/custom/light/__init__.py
+++ b/esphome/components/custom/light/__init__.py
@@ -16,8 +16,8 @@ CONFIG_SCHEMA = cv.Schema(
 )
 
 
-def to_code(config):
-    template_ = yield cg.process_lambda(
+async def to_code(config):
+    template_ = await cg.process_lambda(
         config[CONF_LAMBDA],
         [],
         return_type=cg.std_vector.template(light.LightOutput.operator("ptr")),
@@ -27,4 +27,4 @@ def to_code(config):
     custom = cg.variable(config[CONF_ID], rhs)
     for i, conf in enumerate(config[CONF_LIGHTS]):
         rhs = custom.Pget_light(i)
-        yield light.register_light(rhs, conf)
+        await light.register_light(rhs, conf)
diff --git a/esphome/components/custom/output/__init__.py b/esphome/components/custom/output/__init__.py
index c803d89c32..97ef070fc3 100644
--- a/esphome/components/custom/output/__init__.py
+++ b/esphome/components/custom/output/__init__.py
@@ -42,7 +42,7 @@ CONFIG_SCHEMA = cv.typed_schema(
 )
 
 
-def to_code(config):
+async def to_code(config):
     type = config[CONF_TYPE]
     if type == "binary":
         ret_type = output.BinaryOutputPtr
@@ -50,7 +50,7 @@ def to_code(config):
     else:
         ret_type = output.FloatOutputPtr
         klass = CustomFloatOutputConstructor
-    template_ = yield cg.process_lambda(
+    template_ = await cg.process_lambda(
         config[CONF_LAMBDA], [], return_type=cg.std_vector.template(ret_type)
     )
 
@@ -58,4 +58,4 @@ def to_code(config):
     custom = cg.variable(config[CONF_ID], rhs)
     for i, conf in enumerate(config[CONF_OUTPUTS]):
         out = cg.Pvariable(conf[CONF_ID], custom.get_output(i))
-        yield output.register_output(out, conf)
+        await output.register_output(out, conf)
diff --git a/esphome/components/custom/sensor/__init__.py b/esphome/components/custom/sensor/__init__.py
index 2be14afc0d..bf9421e43e 100644
--- a/esphome/components/custom/sensor/__init__.py
+++ b/esphome/components/custom/sensor/__init__.py
@@ -15,8 +15,8 @@ CONFIG_SCHEMA = cv.Schema(
 )
 
 
-def to_code(config):
-    template_ = yield cg.process_lambda(
+async def to_code(config):
+    template_ = await cg.process_lambda(
         config[CONF_LAMBDA], [], return_type=cg.std_vector.template(sensor.SensorPtr)
     )
 
@@ -24,4 +24,4 @@ def to_code(config):
     var = cg.variable(config[CONF_ID], rhs)
     for i, conf in enumerate(config[CONF_SENSORS]):
         sens = cg.Pvariable(conf[CONF_ID], var.get_sensor(i))
-        yield sensor.register_sensor(sens, conf)
+        await sensor.register_sensor(sens, conf)
diff --git a/esphome/components/custom/switch/__init__.py b/esphome/components/custom/switch/__init__.py
index a470c3e440..e0b9d7751a 100644
--- a/esphome/components/custom/switch/__init__.py
+++ b/esphome/components/custom/switch/__init__.py
@@ -21,8 +21,8 @@ CONFIG_SCHEMA = cv.Schema(
 )
 
 
-def to_code(config):
-    template_ = yield cg.process_lambda(
+async def to_code(config):
+    template_ = await cg.process_lambda(
         config[CONF_LAMBDA], [], return_type=cg.std_vector.template(switch.SwitchPtr)
     )
 
@@ -30,4 +30,4 @@ def to_code(config):
     var = cg.variable(config[CONF_ID], rhs)
     for i, conf in enumerate(config[CONF_SWITCHES]):
         switch_ = cg.Pvariable(conf[CONF_ID], var.get_switch(i))
-        yield switch.register_switch(switch_, conf)
+        await switch.register_switch(switch_, conf)
diff --git a/esphome/components/custom/text_sensor/__init__.py b/esphome/components/custom/text_sensor/__init__.py
index f09d72d228..5b6d416436 100644
--- a/esphome/components/custom/text_sensor/__init__.py
+++ b/esphome/components/custom/text_sensor/__init__.py
@@ -21,8 +21,8 @@ CONFIG_SCHEMA = cv.Schema(
 )
 
 
-def to_code(config):
-    template_ = yield cg.process_lambda(
+async def to_code(config):
+    template_ = await cg.process_lambda(
         config[CONF_LAMBDA],
         [],
         return_type=cg.std_vector.template(text_sensor.TextSensorPtr),
@@ -33,4 +33,4 @@ def to_code(config):
 
     for i, conf in enumerate(config[CONF_TEXT_SENSORS]):
         text = cg.Pvariable(conf[CONF_ID], var.get_text_sensor(i))
-        yield text_sensor.register_text_sensor(text, conf)
+        await text_sensor.register_text_sensor(text, conf)
diff --git a/esphome/components/custom_component/__init__.py b/esphome/components/custom_component/__init__.py
index fc154a82c3..d41dd7ea59 100644
--- a/esphome/components/custom_component/__init__.py
+++ b/esphome/components/custom_component/__init__.py
@@ -19,8 +19,8 @@ CONFIG_SCHEMA = cv.Schema(
 )
 
 
-def to_code(config):
-    template_ = yield cg.process_lambda(
+async def to_code(config):
+    template_ = await cg.process_lambda(
         config[CONF_LAMBDA], [], return_type=cg.std_vector.template(cg.ComponentPtr)
     )
 
@@ -28,4 +28,4 @@ def to_code(config):
     var = cg.variable(config[CONF_ID], rhs)
     for i, conf in enumerate(config.get(CONF_COMPONENTS, [])):
         comp = cg.Pvariable(conf[CONF_ID], var.get_component(i))
-        yield cg.register_component(comp, conf)
+        await cg.register_component(comp, conf)
diff --git a/esphome/components/cwww/light.py b/esphome/components/cwww/light.py
index b099de435d..674c48d219 100644
--- a/esphome/components/cwww/light.py
+++ b/esphome/components/cwww/light.py
@@ -26,14 +26,14 @@ CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
-    yield light.register_light(var, config)
-    cwhite = yield cg.get_variable(config[CONF_COLD_WHITE])
+    await light.register_light(var, config)
+    cwhite = await cg.get_variable(config[CONF_COLD_WHITE])
     cg.add(var.set_cold_white(cwhite))
     cg.add(var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE]))
 
-    wwhite = yield cg.get_variable(config[CONF_WARM_WHITE])
+    wwhite = await cg.get_variable(config[CONF_WARM_WHITE])
     cg.add(var.set_warm_white(wwhite))
     cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE]))
     cg.add(var.set_constant_brightness(config[CONF_CONSTANT_BRIGHTNESS]))
diff --git a/esphome/components/daikin/climate.py b/esphome/components/daikin/climate.py
index 07bc079a9a..af60b17448 100644
--- a/esphome/components/daikin/climate.py
+++ b/esphome/components/daikin/climate.py
@@ -15,6 +15,6 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield climate_ir.register_climate_ir(var, config)
+    await climate_ir.register_climate_ir(var, config)
diff --git a/esphome/components/daikin/daikin.cpp b/esphome/components/daikin/daikin.cpp
index 0701344a8b..e0ffd46387 100644
--- a/esphome/components/daikin/daikin.cpp
+++ b/esphome/components/daikin/daikin.cpp
@@ -94,7 +94,7 @@ uint8_t DaikinClimate::operation_mode_() {
 
 uint16_t DaikinClimate::fan_speed_() {
   uint16_t fan_speed;
-  switch (this->fan_mode) {
+  switch (this->fan_mode.value()) {
     case climate::CLIMATE_FAN_LOW:
       fan_speed = DAIKIN_FAN_1 << 8;
       break;
diff --git a/esphome/components/dallas/__init__.py b/esphome/components/dallas/__init__.py
index 87049b8c64..762bfdc3c3 100644
--- a/esphome/components/dallas/__init__.py
+++ b/esphome/components/dallas/__init__.py
@@ -15,13 +15,13 @@ CONFIG_SCHEMA = cv.Schema(
     {
         cv.GenerateID(): cv.declare_id(DallasComponent),
         cv.GenerateID(CONF_ONE_WIRE_ID): cv.declare_id(ESPOneWire),
-        cv.Required(CONF_PIN): pins.gpio_input_pin_schema,
+        cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
     }
 ).extend(cv.polling_component_schema("60s"))
 
 
-def to_code(config):
-    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+async def to_code(config):
+    pin = await cg.gpio_pin_expression(config[CONF_PIN])
     one_wire = cg.new_Pvariable(config[CONF_ONE_WIRE_ID], pin)
     var = cg.new_Pvariable(config[CONF_ID], one_wire)
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
diff --git a/esphome/components/dallas/sensor.py b/esphome/components/dallas/sensor.py
index bb0a463a62..1c8db8fa2f 100644
--- a/esphome/components/dallas/sensor.py
+++ b/esphome/components/dallas/sensor.py
@@ -8,6 +8,7 @@ from esphome.const import (
     CONF_RESOLUTION,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     CONF_ID,
 )
@@ -16,7 +17,9 @@ from . import DallasComponent, dallas_ns
 DallasTemperatureSensor = dallas_ns.class_("DallasTemperatureSensor", sensor.Sensor)
 
 CONFIG_SCHEMA = cv.All(
-    sensor.sensor_schema(UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE).extend(
+    sensor.sensor_schema(
+        UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT
+    ).extend(
         {
             cv.GenerateID(): cv.declare_id(DallasTemperatureSensor),
             cv.GenerateID(CONF_DALLAS_ID): cv.use_id(DallasComponent),
@@ -29,12 +32,12 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
-    hub = yield cg.get_variable(config[CONF_DALLAS_ID])
+async def to_code(config):
+    hub = await cg.get_variable(config[CONF_DALLAS_ID])
     if CONF_ADDRESS in config:
         address = config[CONF_ADDRESS]
         rhs = hub.Pget_sensor_by_address(address, config.get(CONF_RESOLUTION))
     else:
         rhs = hub.Pget_sensor_by_index(config[CONF_INDEX], config.get(CONF_RESOLUTION))
     var = cg.Pvariable(config[CONF_ID], rhs)
-    yield sensor.register_sensor(var, config)
+    await sensor.register_sensor(var, config)
diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py
index e43eb8bfc1..32c4339530 100644
--- a/esphome/components/debug/__init__.py
+++ b/esphome/components/debug/__init__.py
@@ -14,6 +14,6 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py
index 793d6b2ebb..7011081774 100644
--- a/esphome/components/deep_sleep/__init__.py
+++ b/esphome/components/deep_sleep/__init__.py
@@ -78,14 +78,14 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
     if CONF_SLEEP_DURATION in config:
         cg.add(var.set_sleep_duration(config[CONF_SLEEP_DURATION]))
     if CONF_WAKEUP_PIN in config:
-        pin = yield cg.gpio_pin_expression(config[CONF_WAKEUP_PIN])
+        pin = await cg.gpio_pin_expression(config[CONF_WAKEUP_PIN])
         cg.add(var.set_wakeup_pin(pin))
     if CONF_WAKEUP_PIN_MODE in config:
         cg.add(var.set_wakeup_pin_mode(config[CONF_WAKEUP_PIN_MODE]))
@@ -108,7 +108,9 @@ def to_code(config):
 DEEP_SLEEP_ENTER_SCHEMA = automation.maybe_simple_id(
     {
         cv.GenerateID(): cv.use_id(DeepSleepComponent),
-        cv.Optional(CONF_SLEEP_DURATION): cv.positive_time_period_milliseconds,
+        cv.Optional(CONF_SLEEP_DURATION): cv.templatable(
+            cv.positive_time_period_milliseconds
+        ),
     }
 )
 
@@ -123,18 +125,18 @@ DEEP_SLEEP_PREVENT_SCHEMA = automation.maybe_simple_id(
 @automation.register_action(
     "deep_sleep.enter", EnterDeepSleepAction, DEEP_SLEEP_ENTER_SCHEMA
 )
-def deep_sleep_enter_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def deep_sleep_enter_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
     if CONF_SLEEP_DURATION in config:
-        template_ = yield cg.templatable(config[CONF_SLEEP_DURATION], args, cg.int32)
+        template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.int32)
         cg.add(var.set_sleep_duration(template_))
-    yield var
+    return var
 
 
 @automation.register_action(
     "deep_sleep.prevent", PreventDeepSleepAction, DEEP_SLEEP_PREVENT_SCHEMA
 )
-def deep_sleep_prevent_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, paren)
+async def deep_sleep_prevent_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py
index 33e74a220b..6af83888ab 100644
--- a/esphome/components/dfplayer/__init__.py
+++ b/esphome/components/dfplayer/__init__.py
@@ -43,6 +43,8 @@ PreviousAction = dfplayer_ns.class_("PreviousAction", automation.Action)
 PlayFileAction = dfplayer_ns.class_("PlayFileAction", automation.Action)
 PlayFolderAction = dfplayer_ns.class_("PlayFolderAction", automation.Action)
 SetVolumeAction = dfplayer_ns.class_("SetVolumeAction", automation.Action)
+VolumeUpAction = dfplayer_ns.class_("VolumeUpAction", automation.Action)
+VolumeDownAction = dfplayer_ns.class_("VolumeDownAction", automation.Action)
 SetEqAction = dfplayer_ns.class_("SetEqAction", automation.Action)
 SleepAction = dfplayer_ns.class_("SleepAction", automation.Action)
 ResetAction = dfplayer_ns.class_("ResetAction", automation.Action)
@@ -68,14 +70,20 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield uart.register_uart_device(var, config)
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
 
     for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [], conf)
+        await automation.build_automation(trigger, [], conf)
+
+
+def validate(config, item_config):
+    uart.validate_device(
+        "dfplayer", config, item_config, baud_rate=9600, require_rx=False
+    )
 
 
 @automation.register_action(
@@ -87,10 +95,10 @@ def to_code(config):
         }
     ),
 )
-def dfplayer_next_to_code(config, action_id, template_arg, args):
+async def dfplayer_next_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    yield var
+    await cg.register_parented(var, config[CONF_ID])
+    return var
 
 
 @automation.register_action(
@@ -102,10 +110,10 @@ def dfplayer_next_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def dfplayer_previous_to_code(config, action_id, template_arg, args):
+async def dfplayer_previous_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    yield var
+    await cg.register_parented(var, config[CONF_ID])
+    return var
 
 
 @automation.register_action(
@@ -120,15 +128,15 @@ def dfplayer_previous_to_code(config, action_id, template_arg, args):
         key=CONF_FILE,
     ),
 )
-def dfplayer_play_to_code(config, action_id, template_arg, args):
+async def dfplayer_play_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    template_ = yield cg.templatable(config[CONF_FILE], args, float)
+    await cg.register_parented(var, config[CONF_ID])
+    template_ = await cg.templatable(config[CONF_FILE], args, float)
     cg.add(var.set_file(template_))
     if CONF_LOOP in config:
-        template_ = yield cg.templatable(config[CONF_LOOP], args, float)
+        template_ = await cg.templatable(config[CONF_LOOP], args, float)
         cg.add(var.set_loop(template_))
-    yield var
+    return var
 
 
 @automation.register_action(
@@ -143,18 +151,18 @@ def dfplayer_play_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def dfplayer_play_folder_to_code(config, action_id, template_arg, args):
+async def dfplayer_play_folder_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    template_ = yield cg.templatable(config[CONF_FOLDER], args, float)
+    await cg.register_parented(var, config[CONF_ID])
+    template_ = await cg.templatable(config[CONF_FOLDER], args, float)
     cg.add(var.set_folder(template_))
     if CONF_FILE in config:
-        template_ = yield cg.templatable(config[CONF_FILE], args, float)
+        template_ = await cg.templatable(config[CONF_FILE], args, float)
         cg.add(var.set_file(template_))
     if CONF_LOOP in config:
-        template_ = yield cg.templatable(config[CONF_LOOP], args, float)
+        template_ = await cg.templatable(config[CONF_LOOP], args, float)
         cg.add(var.set_loop(template_))
-    yield var
+    return var
 
 
 @automation.register_action(
@@ -168,12 +176,12 @@ def dfplayer_play_folder_to_code(config, action_id, template_arg, args):
         key=CONF_DEVICE,
     ),
 )
-def dfplayer_set_device_to_code(config, action_id, template_arg, args):
+async def dfplayer_set_device_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    template_ = yield cg.templatable(config[CONF_DEVICE], args, Device)
+    await cg.register_parented(var, config[CONF_ID])
+    template_ = await cg.templatable(config[CONF_DEVICE], args, Device)
     cg.add(var.set_device(template_))
-    yield var
+    return var
 
 
 @automation.register_action(
@@ -187,12 +195,42 @@ def dfplayer_set_device_to_code(config, action_id, template_arg, args):
         key=CONF_VOLUME,
     ),
 )
-def dfplayer_set_volume_to_code(config, action_id, template_arg, args):
+async def dfplayer_set_volume_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    template_ = yield cg.templatable(config[CONF_VOLUME], args, float)
+    await cg.register_parented(var, config[CONF_ID])
+    template_ = await cg.templatable(config[CONF_VOLUME], args, float)
     cg.add(var.set_volume(template_))
-    yield var
+    return var
+
+
+@automation.register_action(
+    "dfplayer.volume_up",
+    VolumeUpAction,
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.use_id(DFPlayer),
+        }
+    ),
+)
+async def dfplayer_volume_up_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(
+    "dfplayer.volume_down",
+    VolumeDownAction,
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.use_id(DFPlayer),
+        }
+    ),
+)
+async def dfplayer_volume_down_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(
@@ -206,12 +244,12 @@ def dfplayer_set_volume_to_code(config, action_id, template_arg, args):
         key=CONF_EQ_PRESET,
     ),
 )
-def dfplayer_set_eq_to_code(config, action_id, template_arg, args):
+async def dfplayer_set_eq_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    template_ = yield cg.templatable(config[CONF_EQ_PRESET], args, EqPreset)
+    await cg.register_parented(var, config[CONF_ID])
+    template_ = await cg.templatable(config[CONF_EQ_PRESET], args, EqPreset)
     cg.add(var.set_eq(template_))
-    yield var
+    return var
 
 
 @automation.register_action(
@@ -223,10 +261,10 @@ def dfplayer_set_eq_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def dfplayer_sleep_to_code(config, action_id, template_arg, args):
+async def dfplayer_sleep_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    yield var
+    await cg.register_parented(var, config[CONF_ID])
+    return var
 
 
 @automation.register_action(
@@ -238,10 +276,10 @@ def dfplayer_sleep_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def dfplayer_reset_to_code(config, action_id, template_arg, args):
+async def dfplayer_reset_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    yield var
+    await cg.register_parented(var, config[CONF_ID])
+    return var
 
 
 @automation.register_action(
@@ -253,10 +291,10 @@ def dfplayer_reset_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def dfplayer_start_to_code(config, action_id, template_arg, args):
+async def dfplayer_start_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    yield var
+    await cg.register_parented(var, config[CONF_ID])
+    return var
 
 
 @automation.register_action(
@@ -268,10 +306,10 @@ def dfplayer_start_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def dfplayer_pause_to_code(config, action_id, template_arg, args):
+async def dfplayer_pause_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    yield var
+    await cg.register_parented(var, config[CONF_ID])
+    return var
 
 
 @automation.register_action(
@@ -283,10 +321,10 @@ def dfplayer_pause_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def dfplayer_stop_to_code(config, action_id, template_arg, args):
+async def dfplayer_stop_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    yield var
+    await cg.register_parented(var, config[CONF_ID])
+    return var
 
 
 @automation.register_action(
@@ -298,10 +336,10 @@ def dfplayer_stop_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def dfplayer_random_to_code(config, action_id, template_arg, args):
+async def dfplayer_random_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    yield var
+    await cg.register_parented(var, config[CONF_ID])
+    return var
 
 
 @automation.register_condition(
@@ -313,7 +351,7 @@ def dfplayer_random_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def dfplyaer_is_playing_to_code(config, condition_id, template_arg, args):
+async def dfplyaer_is_playing_to_code(config, condition_id, template_arg, args):
     var = cg.new_Pvariable(condition_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    yield var
+    await cg.register_parented(var, config[CONF_ID])
+    return var
diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h
index cb9686bb64..6742591926 100644
--- a/esphome/components/dfplayer/dfplayer.h
+++ b/esphome/components/dfplayer/dfplayer.h
@@ -180,6 +180,8 @@ DFPLAYER_SIMPLE_ACTION(StartAction, start)
 DFPLAYER_SIMPLE_ACTION(PauseAction, pause)
 DFPLAYER_SIMPLE_ACTION(StopAction, stop)
 DFPLAYER_SIMPLE_ACTION(RandomAction, random)
+DFPLAYER_SIMPLE_ACTION(VolumeUpAction, volume_up)
+DFPLAYER_SIMPLE_ACTION(VolumeDownAction, volume_down)
 
 template<typename... Ts> class DFPlayerIsPlayingCondition : public Condition<Ts...>, public Parented<DFPlayer> {
  public:
diff --git a/esphome/components/dht/sensor.py b/esphome/components/dht/sensor.py
index 4b2f6ee8d2..c33ddd2286 100644
--- a/esphome/components/dht/sensor.py
+++ b/esphome/components/dht/sensor.py
@@ -9,6 +9,7 @@ from esphome.const import (
     CONF_PIN,
     CONF_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
     DEVICE_CLASS_TEMPERATURE,
@@ -35,10 +36,14 @@ CONFIG_SCHEMA = cv.Schema(
         cv.GenerateID(): cv.declare_id(DHT),
         cv.Required(CONF_PIN): pins.gpio_input_pin_schema,
         cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-            UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+            UNIT_CELSIUS,
+            ICON_EMPTY,
+            1,
+            DEVICE_CLASS_TEMPERATURE,
+            STATE_CLASS_MEASUREMENT,
         ),
         cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-            UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_HUMIDITY
+            UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_HUMIDITY, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional(CONF_MODEL, default="auto detect"): cv.enum(
             DHT_MODELS, upper=True, space="_"
@@ -47,18 +52,18 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(cv.polling_component_schema("60s"))
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
-    pin = yield gpio_pin_expression(config[CONF_PIN])
+    pin = await gpio_pin_expression(config[CONF_PIN])
     cg.add(var.set_pin(pin))
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature_sensor(sens))
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity_sensor(sens))
 
     cg.add(var.set_dht_model(config[CONF_MODEL]))
diff --git a/esphome/components/dht12/sensor.py b/esphome/components/dht12/sensor.py
index f63768142c..14c01f5d34 100644
--- a/esphome/components/dht12/sensor.py
+++ b/esphome/components/dht12/sensor.py
@@ -5,6 +5,7 @@ from esphome.const import (
     CONF_HUMIDITY,
     CONF_ID,
     CONF_TEMPERATURE,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     ICON_EMPTY,
     UNIT_PERCENT,
@@ -22,10 +23,18 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(DHT12Component),
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -34,15 +43,15 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature_sensor(sens))
 
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity_sensor(sens))
diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py
index fe8bc9bd7c..2dff00da03 100644
--- a/esphome/components/display/__init__.py
+++ b/esphome/components/display/__init__.py
@@ -2,8 +2,17 @@ import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import core, automation
 from esphome.automation import maybe_simple_id
-from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_PAGE_ID, CONF_ROTATION
-from esphome.core import coroutine, coroutine_with_priority
+from esphome.const import (
+    CONF_ID,
+    CONF_LAMBDA,
+    CONF_PAGES,
+    CONF_PAGE_ID,
+    CONF_ROTATION,
+    CONF_FROM,
+    CONF_TO,
+    CONF_TRIGGER_ID,
+)
+from esphome.core import coroutine_with_priority
 
 IS_PLATFORM_COMPONENT = True
 
@@ -22,6 +31,9 @@ DisplayPageShowPrevAction = display_ns.class_(
 DisplayIsDisplayingPageCondition = display_ns.class_(
     "DisplayIsDisplayingPageCondition", automation.Condition
 )
+DisplayOnPageChangeTrigger = display_ns.class_("DisplayOnPageChangeTrigger")
+
+CONF_ON_PAGE_CHANGE = "on_page_change"
 
 DISPLAY_ROTATIONS = {
     0: display_ns.DISPLAY_ROTATION_0_DEGREES,
@@ -56,28 +68,46 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
             ),
             cv.Length(min=1),
         ),
+        cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
+                    DisplayOnPageChangeTrigger
+                ),
+                cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
+                cv.Optional(CONF_TO): cv.use_id(DisplayPage),
+            }
+        ),
     }
 )
 
 
-@coroutine
-def setup_display_core_(var, config):
+async def setup_display_core_(var, config):
     if CONF_ROTATION in config:
         cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]]))
     if CONF_PAGES in config:
         pages = []
         for conf in config[CONF_PAGES]:
-            lambda_ = yield cg.process_lambda(
+            lambda_ = await cg.process_lambda(
                 conf[CONF_LAMBDA], [(DisplayBufferRef, "it")], return_type=cg.void
             )
             page = cg.new_Pvariable(conf[CONF_ID], lambda_)
             pages.append(page)
         cg.add(var.set_pages(pages))
+    for conf in config.get(CONF_ON_PAGE_CHANGE, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        if CONF_FROM in conf:
+            page = await cg.get_variable(conf[CONF_FROM])
+            cg.add(trigger.set_from(page))
+        if CONF_TO in conf:
+            page = await cg.get_variable(conf[CONF_TO])
+            cg.add(trigger.set_to(page))
+        await automation.build_automation(
+            trigger, [(DisplayPagePtr, "from"), (DisplayPagePtr, "to")], conf
+        )
 
 
-@coroutine
-def register_display(var, config):
-    yield setup_display_core_(var, config)
+async def register_display(var, config):
+    await setup_display_core_(var, config)
 
 
 @automation.register_action(
@@ -89,15 +119,15 @@ def register_display(var, config):
         }
     ),
 )
-def display_page_show_to_code(config, action_id, template_arg, args):
+async def display_page_show_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
     if isinstance(config[CONF_ID], core.Lambda):
-        template_ = yield cg.templatable(config[CONF_ID], args, DisplayPagePtr)
+        template_ = await cg.templatable(config[CONF_ID], args, DisplayPagePtr)
         cg.add(var.set_page(template_))
     else:
-        paren = yield cg.get_variable(config[CONF_ID])
+        paren = await cg.get_variable(config[CONF_ID])
         cg.add(var.set_page(paren))
-    yield var
+    return var
 
 
 @automation.register_action(
@@ -109,9 +139,9 @@ def display_page_show_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def display_page_show_next_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, paren)
+async def display_page_show_next_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
 
 
 @automation.register_action(
@@ -123,9 +153,9 @@ def display_page_show_next_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def display_page_show_previous_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, paren)
+async def display_page_show_previous_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
 
 
 @automation.register_condition(
@@ -139,15 +169,15 @@ def display_page_show_previous_to_code(config, action_id, template_arg, args):
         key=CONF_PAGE_ID,
     ),
 )
-def display_is_displaying_page_to_code(config, condition_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    page = yield cg.get_variable(config[CONF_PAGE_ID])
+async def display_is_displaying_page_to_code(config, condition_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    page = await cg.get_variable(config[CONF_PAGE_ID])
     var = cg.new_Pvariable(condition_id, template_arg, paren)
     cg.add(var.set_page(page))
 
-    yield var
+    return var
 
 
 @coroutine_with_priority(100.0)
-def to_code(config):
+async def to_code(config):
     cg.add_global(display_ns.using)
diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp
index 994997e6e0..23f6faccb1 100644
--- a/esphome/components/display/display_buffer.cpp
+++ b/esphome/components/display/display_buffer.cpp
@@ -170,7 +170,7 @@ void DisplayBuffer::print(int x, int y, Font *font, Color color, TextAlign align
       // Unknown char, skip
       ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
       if (!font->get_glyphs().empty()) {
-        uint8_t glyph_width = font->get_glyphs()[0].width_;
+        uint8_t glyph_width = font->get_glyphs()[0].glyph_data_->width;
         for (int glyph_x = 0; glyph_x < glyph_width; glyph_x++)
           for (int glyph_y = 0; glyph_y < height; glyph_y++)
             this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color);
@@ -193,7 +193,7 @@ void DisplayBuffer::print(int x, int y, Font *font, Color color, TextAlign align
       }
     }
 
-    x_at += glyph.width_ + glyph.offset_x_;
+    x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
 
     i += match_length;
   }
@@ -315,7 +315,14 @@ void DisplayBuffer::set_pages(std::vector<DisplayPage *> pages) {
   pages[pages.size() - 1]->set_next(pages[0]);
   this->show_page(pages[0]);
 }
-void DisplayBuffer::show_page(DisplayPage *page) { this->page_ = page; }
+void DisplayBuffer::show_page(DisplayPage *page) {
+  this->previous_page_ = this->page_;
+  this->page_ = page;
+  if (this->previous_page_ != this->page_) {
+    for (auto *t : on_page_change_triggers_)
+      t->process(this->previous_page_, this->page_);
+  }
+}
 void DisplayBuffer::show_next_page() { this->page_->show_next(); }
 void DisplayBuffer::show_prev_page() { this->page_->show_prev(); }
 void DisplayBuffer::do_update_() {
@@ -326,6 +333,10 @@ void DisplayBuffer::do_update_() {
     (*this->writer_)(*this);
   }
 }
+void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) {
+  if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to))
+    this->trigger(from, to);
+}
 #ifdef USE_TIME
 void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format,
                              time::ESPTime time) {
@@ -345,35 +356,27 @@ void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, time:
 }
 #endif
 
-Glyph::Glyph(const char *a_char, const uint8_t *data_start, uint32_t offset, int offset_x, int offset_y, int width,
-             int height)
-    : char_(a_char),
-      data_(data_start + offset),
-      offset_x_(offset_x),
-      offset_y_(offset_y),
-      width_(width),
-      height_(height) {}
 bool Glyph::get_pixel(int x, int y) const {
-  const int x_data = x - this->offset_x_;
-  const int y_data = y - this->offset_y_;
-  if (x_data < 0 || x_data >= this->width_ || y_data < 0 || y_data >= this->height_)
+  const int x_data = x - this->glyph_data_->offset_x;
+  const int y_data = y - this->glyph_data_->offset_y;
+  if (x_data < 0 || x_data >= this->glyph_data_->width || y_data < 0 || y_data >= this->glyph_data_->height)
     return false;
-  const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
+  const uint32_t width_8 = ((this->glyph_data_->width + 7u) / 8u) * 8u;
   const uint32_t pos = x_data + y_data * width_8;
-  return pgm_read_byte(this->data_ + (pos / 8u)) & (0x80 >> (pos % 8u));
+  return pgm_read_byte(this->glyph_data_->data + (pos / 8u)) & (0x80 >> (pos % 8u));
 }
-const char *Glyph::get_char() const { return this->char_; }
+const char *Glyph::get_char() const { return this->glyph_data_->a_char; }
 bool Glyph::compare_to(const char *str) const {
   // 1 -> this->char_
   // 2 -> str
   for (uint32_t i = 0;; i++) {
-    if (this->char_[i] == '\0')
+    if (this->glyph_data_->a_char[i] == '\0')
       return true;
     if (str[i] == '\0')
       return false;
-    if (this->char_[i] > str[i])
+    if (this->glyph_data_->a_char[i] > str[i])
       return false;
-    if (this->char_[i] < str[i])
+    if (this->glyph_data_->a_char[i] < str[i])
       return true;
   }
   // this should not happen
@@ -381,19 +384,19 @@ bool Glyph::compare_to(const char *str) const {
 }
 int Glyph::match_length(const char *str) const {
   for (uint32_t i = 0;; i++) {
-    if (this->char_[i] == '\0')
+    if (this->glyph_data_->a_char[i] == '\0')
       return i;
-    if (str[i] != this->char_[i])
+    if (str[i] != this->glyph_data_->a_char[i])
       return 0;
   }
   // this should not happen
   return 0;
 }
 void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const {
-  *x1 = this->offset_x_;
-  *y1 = this->offset_y_;
-  *width = this->width_;
-  *height = this->height_;
+  *x1 = this->glyph_data_->offset_x;
+  *y1 = this->glyph_data_->offset_y;
+  *width = this->glyph_data_->width;
+  *height = this->glyph_data_->height;
 }
 int Font::match_next_glyph(const char *str, int *match_length) {
   int lo = 0;
@@ -423,17 +426,17 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in
     if (glyph_n < 0) {
       // Unknown char, skip
       if (!this->get_glyphs().empty())
-        x += this->get_glyphs()[0].width_;
+        x += this->get_glyphs()[0].glyph_data_->width;
       i++;
       continue;
     }
 
     const Glyph &glyph = this->glyphs_[glyph_n];
     if (!has_char)
-      min_x = glyph.offset_x_;
+      min_x = glyph.glyph_data_->offset_x;
     else
-      min_x = std::min(min_x, x + glyph.offset_x_);
-    x += glyph.width_ + glyph.offset_x_;
+      min_x = std::min(min_x, x + glyph.glyph_data_->offset_x);
+    x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
 
     i += match_length;
     has_char = true;
@@ -442,8 +445,10 @@ 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(std::vector<Glyph> &&glyphs, int baseline, int bottom)
-    : glyphs_(std::move(glyphs)), baseline_(baseline), bottom_(bottom) {}
+Font::Font(const GlyphData *data, int data_nr, int baseline, int bottom) : baseline_(baseline), bottom_(bottom) {
+  for (int i = 0; i < data_nr; ++i)
+    glyphs_.emplace_back(data + i);
+}
 
 bool Image::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 71a6189990..0763262d4c 100644
--- a/esphome/components/display/display_buffer.h
+++ b/esphome/components/display/display_buffer.h
@@ -81,6 +81,7 @@ class Font;
 class Image;
 class DisplayBuffer;
 class DisplayPage;
+class DisplayOnPageChangeTrigger;
 
 using display_writer_t = std::function<void(DisplayBuffer &)>;
 
@@ -298,6 +299,8 @@ class DisplayBuffer {
 
   const DisplayPage *get_active_page() const { return this->page_; }
 
+  void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); }
+
   /// Internal method to set the display rotation with.
   void set_rotation(DisplayRotation rotation);
 
@@ -318,6 +321,8 @@ class DisplayBuffer {
   DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES};
   optional<display_writer_t> writer_{};
   DisplayPage *page_{nullptr};
+  DisplayPage *previous_page_{nullptr};
+  std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;
 };
 
 class DisplayPage {
@@ -338,10 +343,18 @@ class DisplayPage {
   DisplayPage *next_{nullptr};
 };
 
+struct GlyphData {
+  const char *a_char;
+  const uint8_t *data;
+  int offset_x;
+  int offset_y;
+  int width;
+  int height;
+};
+
 class Glyph {
  public:
-  Glyph(const char *a_char, const uint8_t *data_start, uint32_t offset, int offset_x, int offset_y, int width,
-        int height);
+  Glyph(const GlyphData *data) : glyph_data_(data) {}
 
   bool get_pixel(int x, int y) const;
 
@@ -357,12 +370,7 @@ class Glyph {
   friend Font;
   friend DisplayBuffer;
 
-  const char *char_;
-  const uint8_t *data_;
-  int offset_x_;
-  int offset_y_;
-  int width_;
-  int height_;
+  const GlyphData *glyph_data_;
 };
 
 class Font {
@@ -373,7 +381,7 @@ 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(std::vector<Glyph> &&glyphs, int baseline, int bottom);
+  Font(const GlyphData *data, int data_nr, int baseline, int bottom);
 
   int match_next_glyph(const char *str, int *match_length);
 
@@ -462,5 +470,17 @@ template<typename... Ts> class DisplayIsDisplayingPageCondition : public Conditi
   DisplayPage *page_;
 };
 
+class DisplayOnPageChangeTrigger : public Trigger<DisplayPage *, DisplayPage *> {
+ public:
+  explicit DisplayOnPageChangeTrigger(DisplayBuffer *parent) { parent->add_on_page_change_trigger(this); }
+  void process(DisplayPage *from, DisplayPage *to);
+  void set_from(DisplayPage *p) { this->from_ = p; }
+  void set_to(DisplayPage *p) { this->to_ = p; }
+
+ protected:
+  DisplayPage *from_{nullptr};
+  DisplayPage *to_{nullptr};
+};
+
 }  // namespace display
 }  // namespace esphome
diff --git a/esphome/components/ds1307/time.py b/esphome/components/ds1307/time.py
index 9c1d75c029..ddc2939038 100644
--- a/esphome/components/ds1307/time.py
+++ b/esphome/components/ds1307/time.py
@@ -29,10 +29,10 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend(
         }
     ),
 )
-def ds1307_write_time_to_code(config, action_id, template_arg, args):
+async def ds1307_write_time_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    yield var
+    await cg.register_parented(var, config[CONF_ID])
+    return var
 
 
 @automation.register_action(
@@ -44,15 +44,15 @@ def ds1307_write_time_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def ds1307_read_time_to_code(config, action_id, template_arg, args):
+async def ds1307_read_time_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    yield var
+    await cg.register_parented(var, config[CONF_ID])
+    return var
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
 
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
-    yield time.register_time(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
+    await time.register_time(var, config)
diff --git a/esphome/components/duty_cycle/sensor.py b/esphome/components/duty_cycle/sensor.py
index 0dd91bade3..39f6ebc88f 100644
--- a/esphome/components/duty_cycle/sensor.py
+++ b/esphome/components/duty_cycle/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_ID,
     CONF_PIN,
     DEVICE_CLASS_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_PERCENT,
     ICON_PERCENT,
 )
@@ -16,7 +17,9 @@ DutyCycleSensor = duty_cycle_ns.class_(
 )
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_PERCENT, ICON_PERCENT, 1, DEVICE_CLASS_EMPTY)
+    sensor.sensor_schema(
+        UNIT_PERCENT, ICON_PERCENT, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(DutyCycleSensor),
@@ -29,10 +32,10 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
 
-    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+    pin = await cg.gpio_pin_expression(config[CONF_PIN])
     cg.add(var.set_pin(pin))
diff --git a/esphome/components/e131/__init__.py b/esphome/components/e131/__init__.py
index b33aef8cf3..5eb823064d 100644
--- a/esphome/components/e131/__init__.py
+++ b/esphome/components/e131/__init__.py
@@ -29,9 +29,9 @@ CONFIG_SCHEMA = cv.Schema(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
     cg.add(var.set_method(METHODS[config[CONF_METHOD]]))
 
 
@@ -45,11 +45,11 @@ def to_code(config):
         cv.Optional(CONF_CHANNELS, default="RGB"): cv.one_of(*CHANNELS, upper=True),
     },
 )
-def e131_light_effect_to_code(config, effect_id):
-    parent = yield cg.get_variable(config[CONF_E131_ID])
+async def e131_light_effect_to_code(config, effect_id):
+    parent = await cg.get_variable(config[CONF_E131_ID])
 
     effect = cg.new_Pvariable(effect_id, config[CONF_NAME])
     cg.add(effect.set_first_universe(config[CONF_UNIVERSE]))
     cg.add(effect.set_channels(CHANNELS[config[CONF_CHANNELS]]))
     cg.add(effect.set_e131(parent))
-    yield effect
+    return effect
diff --git a/esphome/components/endstop/cover.py b/esphome/components/endstop/cover.py
index b463d5d960..9f3cd395a5 100644
--- a/esphome/components/endstop/cover.py
+++ b/esphome/components/endstop/cover.py
@@ -32,26 +32,26 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield cover.register_cover(var, config)
+    await cg.register_component(var, config)
+    await cover.register_cover(var, config)
 
-    yield automation.build_automation(
+    await automation.build_automation(
         var.get_stop_trigger(), [], config[CONF_STOP_ACTION]
     )
 
-    bin = yield cg.get_variable(config[CONF_OPEN_ENDSTOP])
+    bin = await cg.get_variable(config[CONF_OPEN_ENDSTOP])
     cg.add(var.set_open_endstop(bin))
     cg.add(var.set_open_duration(config[CONF_OPEN_DURATION]))
-    yield automation.build_automation(
+    await automation.build_automation(
         var.get_open_trigger(), [], config[CONF_OPEN_ACTION]
     )
 
-    bin = yield cg.get_variable(config[CONF_CLOSE_ENDSTOP])
+    bin = await cg.get_variable(config[CONF_CLOSE_ENDSTOP])
     cg.add(var.set_close_endstop(bin))
     cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION]))
-    yield automation.build_automation(
+    await automation.build_automation(
         var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]
     )
 
diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py
new file mode 100644
index 0000000000..6437216f69
--- /dev/null
+++ b/esphome/components/esp32_ble/__init__.py
@@ -0,0 +1,44 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_ID, CONF_MODEL, ESP_PLATFORM_ESP32
+
+ESP_PLATFORMS = [ESP_PLATFORM_ESP32]
+CODEOWNERS = ["@jesserockz"]
+
+CONF_MANUFACTURER = "manufacturer"
+CONF_SERVER = "server"
+
+esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble")
+ESP32BLE = esp32_ble_ns.class_("ESP32BLE", cg.Component)
+BLEServer = esp32_ble_ns.class_("BLEServer", cg.Component)
+
+BLEServiceComponent = esp32_ble_ns.class_("BLEServiceComponent")
+
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(ESP32BLE),
+        cv.Optional(CONF_SERVER): cv.Schema(
+            {
+                cv.GenerateID(): cv.declare_id(BLEServer),
+                cv.Optional(CONF_MANUFACTURER, default="ESPHome"): cv.string,
+                cv.Optional(CONF_MODEL): cv.string,
+            }
+        ),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+
+    if CONF_SERVER in config:
+        conf = config[CONF_SERVER]
+        server = cg.new_Pvariable(conf[CONF_ID])
+        await cg.register_component(server, conf)
+        cg.add(server.set_manufacturer(conf[CONF_MANUFACTURER]))
+        if CONF_MODEL in conf:
+            cg.add(server.set_model(conf[CONF_MODEL]))
+        cg.add_define("USE_ESP32_BLE_SERVER")
+        cg.add(var.set_server(server))
diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp
new file mode 100644
index 0000000000..2200d3cef1
--- /dev/null
+++ b/esphome/components/esp32_ble/ble.cpp
@@ -0,0 +1,178 @@
+#include "ble.h"
+#include "esphome/core/application.h"
+#include "esphome/core/log.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+#include <nvs_flash.h>
+#include <freertos/FreeRTOSConfig.h>
+#include <esp_bt_main.h>
+#include <esp_bt.h>
+#include <freertos/task.h>
+#include <esp_gap_ble_api.h>
+
+namespace esphome {
+namespace esp32_ble {
+
+static const char *TAG = "esp32_ble";
+
+void ESP32BLE::setup() {
+  global_ble = this;
+  ESP_LOGCONFIG(TAG, "Setting up BLE...");
+
+  xTaskCreatePinnedToCore(ESP32BLE::ble_core_task_,
+                          "ble_task",  // name
+                          10000,       // stack size
+                          nullptr,     // input params
+                          1,           // priority
+                          nullptr,     // handle, not needed
+                          0            // core
+  );
+}
+
+void ESP32BLE::mark_failed() {
+  Component::mark_failed();
+  if (this->server_ != nullptr) {
+    this->server_->mark_failed();
+  }
+}
+
+bool ESP32BLE::can_proceed() { return this->ready_; }
+
+void ESP32BLE::ble_core_task_(void *params) {
+  if (!ble_setup_()) {
+    ESP_LOGE(TAG, "BLE could not be set up");
+    global_ble->mark_failed();
+    return;
+  }
+
+  global_ble->ready_ = true;
+  ESP_LOGD(TAG, "BLE Setup complete");
+
+  while (true) {
+    vTaskDelay(1000 / portTICK_PERIOD_MS);
+  }
+}
+
+bool ESP32BLE::ble_setup_() {
+  esp_err_t err = nvs_flash_init();
+  if (err != ESP_OK) {
+    ESP_LOGE(TAG, "nvs_flash_init failed: %d", err);
+    return false;
+  }
+
+  if (!btStart()) {
+    ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status());
+    return false;
+  }
+
+  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(ESP32BLE::gap_event_handler);
+  if (err != ESP_OK) {
+    ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err);
+    return false;
+  }
+
+  if (global_ble->has_server()) {
+    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);
+      return false;
+    }
+  }
+
+  if (global_ble->has_client()) {
+    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);
+      return false;
+    }
+  }
+
+  err = esp_ble_gap_set_device_name(App.get_name().c_str());
+  if (err != ESP_OK) {
+    ESP_LOGE(TAG, "esp_ble_gap_set_device_name failed: %d", err);
+    return false;
+  }
+
+  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 ESP32BLE::loop() {
+//   BLEEvent *ble_event = this->ble_events_.pop();
+//   while (ble_event != nullptr) {
+//     switch (ble_event->type_) {
+//       case ble_event->GATTS:
+//         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 ble_event->GAP:
+//         this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param);
+//         break;
+//       default:
+//         break;
+//     }
+//     delete ble_event;
+//     ble_event = this->ble_events_.pop();
+//   }
+// }
+
+void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
+  global_ble->real_gap_event_handler_(event, param);
+}
+
+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;
+  }
+}
+
+void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
+                                   esp_ble_gatts_cb_param_t *param) {
+  global_ble->real_gatts_event_handler_(event, gatts_if, param);
+}
+
+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);
+  this->server_->gatts_event_handler(event, gatts_if, param);
+}
+
+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);
+}
+
+float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; }
+
+void ESP32BLE::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE:"); }
+
+ESP32BLE *global_ble = nullptr;
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h
new file mode 100644
index 0000000000..8850f10d85
--- /dev/null
+++ b/esphome/components/esp32_ble/ble.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/helpers.h"
+#include "ble_server.h"
+#include "queue.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+#include <esp_gap_ble_api.h>
+#include <esp_gatts_api.h>
+#include <esp_gattc_api.h>
+
+namespace esphome {
+namespace esp32_ble {
+
+typedef struct {
+  void *peer_device;
+  bool connected;
+  uint16_t mtu;
+} conn_status_t;
+
+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 can_proceed() override;
+
+  bool has_server() { return this->server_ != nullptr; }
+  bool has_client() { return false; }
+
+  bool is_ready() { return this->ready_; }
+
+  void set_server(BLEServer *server) { this->server_ = server; }
+
+ 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);
+  static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
+
+  void real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_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);
+  void real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
+
+  static void ble_core_task_(void *params);
+  static bool ble_setup_();
+
+  bool ready_{false};
+
+  BLEServer *server_{nullptr};
+  Queue<BLEEvent> ble_events_;
+};
+
+extern ESP32BLE *global_ble;
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/ble_2901.cpp b/esphome/components/esp32_ble/ble_2901.cpp
new file mode 100644
index 0000000000..952d82d5f0
--- /dev/null
+++ b/esphome/components/esp32_ble/ble_2901.cpp
@@ -0,0 +1,18 @@
+#include "ble_2901.h"
+#include "ble_uuid.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace esp32_ble {
+
+BLE2901::BLE2901(const std::string &value) : BLE2901((uint8_t *) value.data(), value.length()) {}
+BLE2901::BLE2901(const uint8_t *data, size_t length) : BLEDescriptor(ESPBTUUID::from_uint16(0x2901)) {
+  this->set_value(data, length);
+  this->permissions_ = ESP_GATT_PERM_READ;
+}
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/ble_2901.h b/esphome/components/esp32_ble/ble_2901.h
new file mode 100644
index 0000000000..e606b6271a
--- /dev/null
+++ b/esphome/components/esp32_ble/ble_2901.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "ble_descriptor.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace esp32_ble {
+
+class BLE2901 : public BLEDescriptor {
+ public:
+  BLE2901(const std::string &value);
+  BLE2901(const uint8_t *data, size_t length);
+};
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/ble_2902.cpp b/esphome/components/esp32_ble/ble_2902.cpp
new file mode 100644
index 0000000000..164c8f40ff
--- /dev/null
+++ b/esphome/components/esp32_ble/ble_2902.cpp
@@ -0,0 +1,18 @@
+#include "ble_2902.h"
+#include "ble_uuid.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace esp32_ble {
+
+BLE2902::BLE2902() : BLEDescriptor(ESPBTUUID::from_uint16(0x2902)) {
+  this->value_.attr_len = 2;
+  uint8_t data[2] = {0, 0};
+  memcpy(this->value_.attr_value, data, 2);
+}
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/ble_2902.h b/esphome/components/esp32_ble/ble_2902.h
new file mode 100644
index 0000000000..356a0bb107
--- /dev/null
+++ b/esphome/components/esp32_ble/ble_2902.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "ble_descriptor.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace esp32_ble {
+
+class BLE2902 : public BLEDescriptor {
+ public:
+  BLE2902();
+};
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp
new file mode 100644
index 0000000000..6162bf3a40
--- /dev/null
+++ b/esphome/components/esp32_ble/ble_advertising.cpp
@@ -0,0 +1,97 @@
+#include "ble_advertising.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+#include "ble_uuid.h"
+
+namespace esphome {
+namespace esp32_ble {
+
+BLEAdvertising::BLEAdvertising() {
+  this->advertising_data_.set_scan_rsp = false;
+  this->advertising_data_.include_name = true;
+  this->advertising_data_.include_txpower = true;
+  this->advertising_data_.min_interval = 0x20;
+  this->advertising_data_.max_interval = 0x40;
+  this->advertising_data_.appearance = 0x00;
+  this->advertising_data_.manufacturer_len = 0;
+  this->advertising_data_.p_manufacturer_data = nullptr;
+  this->advertising_data_.service_data_len = 0;
+  this->advertising_data_.p_service_data = nullptr;
+  this->advertising_data_.service_uuid_len = 0;
+  this->advertising_data_.p_service_uuid = nullptr;
+  this->advertising_data_.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT);
+
+  this->advertising_params_.adv_int_min = 0x20;
+  this->advertising_params_.adv_int_max = 0x40;
+  this->advertising_params_.adv_type = ADV_TYPE_IND;
+  this->advertising_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC;
+  this->advertising_params_.channel_map = ADV_CHNL_ALL;
+  this->advertising_params_.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY;
+  this->advertising_params_.peer_addr_type = BLE_ADDR_TYPE_PUBLIC;
+}
+
+void BLEAdvertising::add_service_uuid(ESPBTUUID uuid) { this->advertising_uuids_.push_back(uuid); }
+
+void BLEAdvertising::start() {
+  int num_services = this->advertising_uuids_.size();
+  if (num_services == 0) {
+    this->advertising_data_.service_uuid_len = 0;
+  } else {
+    this->advertising_data_.service_uuid_len = 16 * num_services;
+    this->advertising_data_.p_service_uuid = new uint8_t[this->advertising_data_.service_uuid_len];
+    uint8_t *p = this->advertising_data_.p_service_uuid;
+    for (int i = 0; i < num_services; i++) {
+      ESPBTUUID uuid = this->advertising_uuids_[i];
+      memcpy(p, uuid.as_128bit().get_uuid().uuid.uuid128, 16);
+      p += 16;
+    }
+  }
+
+  esp_err_t err;
+
+  this->advertising_data_.set_scan_rsp = false;
+  this->advertising_data_.include_name = !this->scan_response_;
+  this->advertising_data_.include_txpower = !this->scan_response_;
+  err = esp_ble_gap_config_adv_data(&this->advertising_data_);
+  if (err != ESP_OK) {
+    ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Advertising): %d", err);
+    return;
+  }
+
+  memcpy(&this->scan_response_data_, &this->advertising_data_, sizeof(esp_ble_adv_data_t));
+  this->scan_response_data_.set_scan_rsp = true;
+  this->scan_response_data_.include_name = true;
+  this->scan_response_data_.include_txpower = true;
+  this->scan_response_data_.appearance = 0;
+  this->scan_response_data_.flag = 0;
+  err = esp_ble_gap_config_adv_data(&this->scan_response_data_);
+  if (err != ESP_OK) {
+    ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Scan response): %d", err);
+    return;
+  }
+
+  if (this->advertising_data_.service_uuid_len > 0) {
+    delete[] this->advertising_data_.p_service_uuid;
+    this->advertising_data_.p_service_uuid = nullptr;
+  }
+
+  err = esp_ble_gap_start_advertising(&this->advertising_params_);
+  if (err != ESP_OK) {
+    ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %d", err);
+    return;
+  }
+}
+
+void BLEAdvertising::stop() {
+  esp_err_t err = esp_ble_gap_stop_advertising();
+  if (err != ESP_OK) {
+    ESP_LOGE(TAG, "esp_ble_gap_stop_advertising failed: %d", err);
+    return;
+  }
+}
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h
new file mode 100644
index 0000000000..405edbf482
--- /dev/null
+++ b/esphome/components/esp32_ble/ble_advertising.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include <vector>
+
+#ifdef ARDUINO_ARCH_ESP32
+
+#include <esp_gap_ble_api.h>
+#include <esp_gatts_api.h>
+
+namespace esphome {
+namespace esp32_ble {
+
+class ESPBTUUID;
+
+class BLEAdvertising {
+ public:
+  BLEAdvertising();
+
+  void add_service_uuid(ESPBTUUID uuid);
+  void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
+  void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
+
+  void start();
+  void stop();
+
+ protected:
+  bool scan_response_;
+  esp_ble_adv_data_t advertising_data_;
+  esp_ble_adv_data_t scan_response_data_;
+  esp_ble_adv_params_t advertising_params_;
+  std::vector<ESPBTUUID> advertising_uuids_;
+};
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/ble_characteristic.cpp b/esphome/components/esp32_ble/ble_characteristic.cpp
new file mode 100644
index 0000000000..240b57eb38
--- /dev/null
+++ b/esphome/components/esp32_ble/ble_characteristic.cpp
@@ -0,0 +1,280 @@
+#include "ble_characteristic.h"
+#include "ble_server.h"
+#include "ble_service.h"
+
+#include "esphome/core/log.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace esp32_ble {
+
+static const char *TAG = "esp32_ble.characteristic";
+
+BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) : uuid_(uuid) {
+  this->set_value_lock_ = xSemaphoreCreateBinary();
+  this->create_lock_ = xSemaphoreCreateBinary();
+
+  xSemaphoreGive(this->set_value_lock_);
+  xSemaphoreGive(this->create_lock_);
+  this->properties_ = (esp_gatt_char_prop_t) 0;
+
+  this->set_broadcast_property((properties & PROPERTY_BROADCAST) != 0);
+  this->set_indicate_property((properties & PROPERTY_INDICATE) != 0);
+  this->set_notify_property((properties & PROPERTY_NOTIFY) != 0);
+  this->set_read_property((properties & PROPERTY_READ) != 0);
+  this->set_write_property((properties & PROPERTY_WRITE) != 0);
+  this->set_write_no_response_property((properties & PROPERTY_WRITE_NR) != 0);
+}
+
+void BLECharacteristic::set_value(std::vector<uint8_t> value) {
+  xSemaphoreTake(this->set_value_lock_, 0L);
+  this->value_ = value;
+  xSemaphoreGive(this->set_value_lock_);
+}
+void BLECharacteristic::set_value(const std::string &value) {
+  this->set_value(std::vector<uint8_t>(value.begin(), value.end()));
+}
+void BLECharacteristic::set_value(const uint8_t *data, size_t length) {
+  this->set_value(std::vector<uint8_t>(data, data + length));
+}
+void BLECharacteristic::set_value(uint8_t &data) {
+  uint8_t temp[1];
+  temp[0] = data;
+  this->set_value(temp, 1);
+}
+void BLECharacteristic::set_value(uint16_t &data) {
+  uint8_t temp[2];
+  temp[0] = data;
+  temp[1] = data >> 8;
+  this->set_value(temp, 2);
+}
+void BLECharacteristic::set_value(uint32_t &data) {
+  uint8_t temp[4];
+  temp[0] = data;
+  temp[1] = data >> 8;
+  temp[2] = data >> 16;
+  temp[3] = data >> 24;
+  this->set_value(temp, 4);
+}
+void BLECharacteristic::set_value(int &data) {
+  uint8_t temp[4];
+  temp[0] = data;
+  temp[1] = data >> 8;
+  temp[2] = data >> 16;
+  temp[3] = data >> 24;
+  this->set_value(temp, 4);
+}
+void BLECharacteristic::set_value(float &data) {
+  float temp = data;
+  this->set_value((uint8_t *) &temp, 4);
+}
+void BLECharacteristic::set_value(double &data) {
+  double temp = data;
+  this->set_value((uint8_t *) &temp, 8);
+}
+void BLECharacteristic::set_value(bool &data) {
+  uint8_t temp[1];
+  temp[0] = data;
+  this->set_value(temp, 1);
+}
+
+void BLECharacteristic::notify(bool notification) {
+  if (!notification) {
+    ESP_LOGW(TAG, "notification=false is not yet supported");
+    // TODO: Handle when notification=false
+  }
+  if (this->service_->get_server()->get_connected_client_count() == 0)
+    return;
+
+  for (auto &client : this->service_->get_server()->get_clients()) {
+    size_t length = this->value_.size();
+    esp_err_t err = esp_ble_gatts_send_indicate(this->service_->get_server()->get_gatts_if(), client.first,
+                                                this->handle_, length, this->value_.data(), false);
+    if (err != ESP_OK) {
+      ESP_LOGE(TAG, "esp_ble_gatts_send_indicate failed %d", err);
+      return;
+    }
+  }
+}
+
+void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) { this->descriptors_.push_back(descriptor); }
+
+bool BLECharacteristic::do_create(BLEService *service) {
+  this->service_ = service;
+  esp_attr_control_t control;
+  control.auto_rsp = ESP_GATT_RSP_BY_APP;
+
+  xSemaphoreTake(this->create_lock_, portMAX_DELAY);
+  ESP_LOGV(TAG, "Creating characteristic - %s", this->uuid_.to_string().c_str());
+
+  esp_bt_uuid_t uuid = this->uuid_.get_uuid();
+  esp_err_t err = esp_ble_gatts_add_char(service->get_handle(), &uuid, static_cast<esp_gatt_perm_t>(this->permissions_),
+                                         this->properties_, nullptr, &control);
+
+  if (err != ESP_OK) {
+    ESP_LOGE(TAG, "esp_ble_gatts_add_char failed: %d", err);
+    return false;
+  }
+
+  xSemaphoreWait(this->create_lock_, portMAX_DELAY);
+
+  for (auto *descriptor : this->descriptors_) {
+    descriptor->do_create(this);
+  }
+  return true;
+}
+
+void BLECharacteristic::set_broadcast_property(bool value) {
+  if (value)
+    this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_BROADCAST);
+  else
+    this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_BROADCAST);
+}
+void BLECharacteristic::set_indicate_property(bool value) {
+  if (value)
+    this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_INDICATE);
+  else
+    this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_INDICATE);
+}
+void BLECharacteristic::set_notify_property(bool value) {
+  if (value)
+    this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_NOTIFY);
+  else
+    this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_NOTIFY);
+}
+void BLECharacteristic::set_read_property(bool value) {
+  if (value)
+    this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_READ);
+  else
+    this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_READ);
+}
+void BLECharacteristic::set_write_property(bool value) {
+  if (value)
+    this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE);
+  else
+    this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE);
+}
+void BLECharacteristic::set_write_no_response_property(bool value) {
+  if (value)
+    this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE_NR);
+  else
+    this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE_NR);
+}
+
+void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
+                                            esp_ble_gatts_cb_param_t *param) {
+  switch (event) {
+    case ESP_GATTS_ADD_CHAR_EVT: {
+      if (this->uuid_ == ESPBTUUID::from_uuid(param->add_char.char_uuid)) {
+        this->handle_ = param->add_char.attr_handle;
+        xSemaphoreGive(this->create_lock_);
+      }
+      break;
+    }
+    case ESP_GATTS_READ_EVT: {
+      if (param->read.handle != this->handle_)
+        break;  // Not this characteristic
+
+      if (!param->read.need_rsp)
+        break;  // For some reason you can request a read but not want a response
+
+      uint16_t max_offset = 22;
+
+      esp_gatt_rsp_t response;
+      if (param->read.is_long) {
+        if (this->value_.size() - this->value_read_offset_ < max_offset) {
+          //  Last message in the chain
+          response.attr_value.len = this->value_.size() - this->value_read_offset_;
+          response.attr_value.offset = this->value_read_offset_;
+          memcpy(response.attr_value.value, this->value_.data() + response.attr_value.offset, response.attr_value.len);
+          this->value_read_offset_ = 0;
+        } else {
+          response.attr_value.len = max_offset;
+          response.attr_value.offset = this->value_read_offset_;
+          memcpy(response.attr_value.value, this->value_.data() + response.attr_value.offset, response.attr_value.len);
+          this->value_read_offset_ += max_offset;
+        }
+      } else {
+        response.attr_value.offset = 0;
+        if (this->value_.size() + 1 > max_offset) {
+          response.attr_value.len = max_offset;
+          this->value_read_offset_ = max_offset;
+        } else {
+          response.attr_value.len = this->value_.size();
+        }
+        memcpy(response.attr_value.value, this->value_.data(), response.attr_value.len);
+      }
+
+      response.attr_value.handle = this->handle_;
+      response.attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE;
+
+      esp_err_t err =
+          esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id, ESP_GATT_OK, &response);
+      if (err != ESP_OK) {
+        ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err);
+      }
+      break;
+    }
+    case ESP_GATTS_WRITE_EVT: {
+      if (this->handle_ != param->write.handle)
+        return;
+
+      if (param->write.is_prep) {
+        this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len);
+        this->write_event_ = true;
+      } else {
+        this->set_value(param->write.value, param->write.len);
+      }
+
+      if (param->write.need_rsp) {
+        esp_gatt_rsp_t response;
+
+        response.attr_value.len = param->write.len;
+        response.attr_value.handle = this->handle_;
+        response.attr_value.offset = param->write.offset;
+        response.attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE;
+        memcpy(response.attr_value.value, param->write.value, param->write.len);
+
+        esp_err_t err =
+            esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, &response);
+
+        if (err != ESP_OK) {
+          ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err);
+        }
+      }
+
+      if (!param->write.is_prep) {
+        this->on_write_(this->value_);
+      }
+
+      break;
+    }
+
+    case ESP_GATTS_EXEC_WRITE_EVT: {
+      if (!this->write_event_)
+        break;
+      this->write_event_ = false;
+      if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) {
+        this->on_write_(this->value_);
+      }
+      esp_err_t err =
+          esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr);
+      if (err != ESP_OK) {
+        ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err);
+      }
+      break;
+    }
+    default:
+      break;
+  }
+
+  for (auto *descriptor : this->descriptors_) {
+    descriptor->gatts_event_handler(event, gatts_if, param);
+  }
+}
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/ble_characteristic.h b/esphome/components/esp32_ble/ble_characteristic.h
new file mode 100644
index 0000000000..076f5c1a5f
--- /dev/null
+++ b/esphome/components/esp32_ble/ble_characteristic.h
@@ -0,0 +1,85 @@
+#pragma once
+
+#include <vector>
+
+#include "ble_uuid.h"
+#include "ble_descriptor.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+#include <esp_gap_ble_api.h>
+#include <esp_gatt_defs.h>
+#include <esp_gattc_api.h>
+#include <esp_gatts_api.h>
+#include <esp_bt_defs.h>
+
+namespace esphome {
+namespace esp32_ble {
+
+class BLEService;
+
+class BLECharacteristic {
+ public:
+  BLECharacteristic(const ESPBTUUID uuid, uint32_t properties);
+
+  void set_value(const uint8_t *data, size_t length);
+  void set_value(std::vector<uint8_t> value);
+  void set_value(const std::string &value);
+  void set_value(uint8_t &data);
+  void set_value(uint16_t &data);
+  void set_value(uint32_t &data);
+  void set_value(int &data);
+  void set_value(float &data);
+  void set_value(double &data);
+  void set_value(bool &data);
+
+  void set_broadcast_property(bool value);
+  void set_indicate_property(bool value);
+  void set_notify_property(bool value);
+  void set_read_property(bool value);
+  void set_write_property(bool value);
+  void set_write_no_response_property(bool value);
+
+  void notify(bool notification = true);
+
+  bool do_create(BLEService *service);
+  void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
+
+  void on_write(const std::function<void(const std::vector<uint8_t> &)> &func) { this->on_write_ = func; }
+
+  void add_descriptor(BLEDescriptor *descriptor);
+
+  BLEService *get_service() { return this->service_; }
+  ESPBTUUID get_uuid() { return this->uuid_; }
+  std::vector<uint8_t> &get_value() { return this->value_; }
+
+  static const uint32_t PROPERTY_READ = 1 << 0;
+  static const uint32_t PROPERTY_WRITE = 1 << 1;
+  static const uint32_t PROPERTY_NOTIFY = 1 << 2;
+  static const uint32_t PROPERTY_BROADCAST = 1 << 3;
+  static const uint32_t PROPERTY_INDICATE = 1 << 4;
+  static const uint32_t PROPERTY_WRITE_NR = 1 << 5;
+
+ protected:
+  bool write_event_{false};
+  BLEService *service_;
+  ESPBTUUID uuid_;
+  esp_gatt_char_prop_t properties_;
+  uint16_t handle_{0xFFFF};
+
+  uint16_t value_read_offset_{0};
+  std::vector<uint8_t> value_;
+  SemaphoreHandle_t set_value_lock_;
+  SemaphoreHandle_t create_lock_;
+
+  std::vector<BLEDescriptor *> descriptors_;
+
+  std::function<void(const std::vector<uint8_t> &)> on_write_;
+
+  esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE;
+};
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/ble_descriptor.cpp b/esphome/components/esp32_ble/ble_descriptor.cpp
new file mode 100644
index 0000000000..4c4e3968e8
--- /dev/null
+++ b/esphome/components/esp32_ble/ble_descriptor.cpp
@@ -0,0 +1,82 @@
+#include "ble_descriptor.h"
+#include "ble_characteristic.h"
+#include "ble_service.h"
+
+#include "esphome/core/log.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace esp32_ble {
+
+static const char *TAG = "esp32_ble.descriptor";
+
+BLEDescriptor::BLEDescriptor(ESPBTUUID uuid, uint16_t max_len) {
+  this->uuid_ = uuid;
+  this->value_.attr_len = 0;
+  this->value_.attr_max_len = max_len;
+  this->value_.attr_value = (uint8_t *) malloc(max_len);
+
+  this->create_lock_ = xSemaphoreCreateBinary();
+  xSemaphoreGive(this->create_lock_);
+}
+
+BLEDescriptor::~BLEDescriptor() { free(this->value_.attr_value); }
+
+bool BLEDescriptor::do_create(BLECharacteristic *characteristic) {
+  this->characteristic_ = characteristic;
+  esp_attr_control_t control;
+  control.auto_rsp = ESP_GATT_AUTO_RSP;
+
+  xSemaphoreTake(this->create_lock_, portMAX_DELAY);
+  ESP_LOGV(TAG, "Creating descriptor - %s", this->uuid_.to_string().c_str());
+  esp_bt_uuid_t uuid = this->uuid_.get_uuid();
+  esp_err_t err = esp_ble_gatts_add_char_descr(this->characteristic_->get_service()->get_handle(), &uuid,
+                                               this->permissions_, &this->value_, &control);
+  if (err != ESP_OK) {
+    ESP_LOGE(TAG, "esp_ble_gatts_add_char_descr failed: %d", err);
+    return false;
+  }
+  xSemaphoreWait(this->create_lock_, portMAX_DELAY);
+
+  return true;
+}
+
+void BLEDescriptor::set_value(const std::string &value) { this->set_value((uint8_t *) value.data(), value.length()); }
+void BLEDescriptor::set_value(const uint8_t *data, size_t length) {
+  if (length > this->value_.attr_max_len) {
+    ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len);
+    return;
+  }
+  this->value_.attr_len = length;
+  memcpy(this->value_.attr_value, data, length);
+}
+
+void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
+                                        esp_ble_gatts_cb_param_t *param) {
+  switch (event) {
+    case ESP_GATTS_ADD_CHAR_DESCR_EVT: {
+      if (this->characteristic_ != nullptr && this->uuid_ == ESPBTUUID::from_uuid(param->add_char_descr.descr_uuid) &&
+          this->characteristic_->get_service()->get_handle() == param->add_char_descr.service_handle &&
+          this->characteristic_ == this->characteristic_->get_service()->get_last_created_characteristic()) {
+        this->handle_ = param->add_char_descr.attr_handle;
+        xSemaphoreGive(this->create_lock_);
+      }
+      break;
+    }
+    case ESP_GATTS_WRITE_EVT: {
+      if (this->handle_ == param->write.handle) {
+        this->value_.attr_len = param->write.len;
+        memcpy(this->value_.attr_value, param->write.value, param->write.len);
+      }
+      break;
+    }
+    default:
+      break;
+  }
+}
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/ble_descriptor.h b/esphome/components/esp32_ble/ble_descriptor.h
new file mode 100644
index 0000000000..ffbb9645bd
--- /dev/null
+++ b/esphome/components/esp32_ble/ble_descriptor.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "ble_uuid.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+#include <esp_gatt_defs.h>
+#include <esp_gatts_api.h>
+
+namespace esphome {
+namespace esp32_ble {
+
+class BLECharacteristic;
+
+class BLEDescriptor {
+ public:
+  BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100);
+  virtual ~BLEDescriptor();
+  bool do_create(BLECharacteristic *characteristic);
+
+  void set_value(const std::string &value);
+  void set_value(const uint8_t *data, size_t length);
+
+  void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
+
+ protected:
+  BLECharacteristic *characteristic_{nullptr};
+  ESPBTUUID uuid_;
+  uint16_t handle_{0xFFFF};
+  SemaphoreHandle_t create_lock_;
+
+  esp_attr_value_t value_;
+
+  esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE;
+};
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/ble_server.cpp b/esphome/components/esp32_ble/ble_server.cpp
new file mode 100644
index 0000000000..a63312dd62
--- /dev/null
+++ b/esphome/components/esp32_ble/ble_server.cpp
@@ -0,0 +1,158 @@
+#include "ble_server.h"
+#include "ble.h"
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+#include "esphome/core/version.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+#include <nvs_flash.h>
+#include <freertos/FreeRTOSConfig.h>
+#include <esp_bt_main.h>
+#include <esp_bt.h>
+#include <freertos/task.h>
+#include <esp_gap_ble_api.h>
+
+namespace esphome {
+namespace esp32_ble {
+
+static const char *TAG = "esp32_ble.server";
+
+static const uint16_t DEVICE_INFORMATION_SERVICE_UUID = 0x180A;
+static const uint16_t MODEL_UUID = 0x2A24;
+static const uint16_t VERSION_UUID = 0x2A26;
+static const uint16_t MANUFACTURER_UUID = 0x2A29;
+
+void BLEServer::setup() {
+  if (this->is_failed()) {
+    ESP_LOGE(TAG, "BLE Server was marked failed by ESP32BLE");
+    return;
+  }
+
+  ESP_LOGD(TAG, "Setting up BLE Server...");
+
+  global_ble_server = this;
+  this->register_lock_ = xSemaphoreCreateBinary();
+  xSemaphoreGive(this->register_lock_);
+  this->advertising_ = new BLEAdvertising();
+
+  this->setup_server_();
+
+  for (auto *component : this->service_components_) {
+    component->setup_service();
+  }
+
+  ESP_LOGD(TAG, "BLE Server set up complete...");
+}
+
+void BLEServer::setup_server_() {
+  xSemaphoreTake(this->register_lock_, portMAX_DELAY);
+  esp_err_t err = esp_ble_gatts_app_register(0);
+  if (err != ESP_OK) {
+    ESP_LOGE(TAG, "esp_ble_gatts_app_register failed: %d", err);
+    this->mark_failed();
+    return;
+  }
+  xSemaphoreWait(this->register_lock_, portMAX_DELAY);
+
+  this->device_information_service = this->create_service(DEVICE_INFORMATION_SERVICE_UUID);
+
+  this->create_device_characteristics_();
+
+  this->advertising_->set_scan_response(true);
+  this->advertising_->set_min_preferred_interval(0x06);
+  this->advertising_->start();
+
+  this->device_information_service->start();
+}
+
+bool BLEServer::create_device_characteristics_() {
+  if (this->model_.has_value()) {
+    BLECharacteristic *model =
+        this->device_information_service->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ);
+    model->set_value(this->model_.value());
+  } else {
+#ifdef ARDUINO_BOARD
+    BLECharacteristic *model =
+        this->device_information_service->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ);
+    model->set_value(ARDUINO_BOARD);
+#endif
+  }
+
+  BLECharacteristic *version =
+      this->device_information_service->create_characteristic(VERSION_UUID, BLECharacteristic::PROPERTY_READ);
+  version->set_value("ESPHome " ESPHOME_VERSION);
+
+  BLECharacteristic *manufacturer =
+      this->device_information_service->create_characteristic(MANUFACTURER_UUID, BLECharacteristic::PROPERTY_READ);
+  manufacturer->set_value(this->manufacturer_);
+
+  return true;
+}
+
+BLEService *BLEServer::create_service(const uint8_t *uuid, bool advertise) {
+  return this->create_service(ESPBTUUID::from_raw(uuid), advertise);
+}
+BLEService *BLEServer::create_service(uint16_t uuid, bool advertise) {
+  return this->create_service(ESPBTUUID::from_uint16(uuid), advertise);
+}
+BLEService *BLEServer::create_service(const std::string &uuid, bool advertise) {
+  return this->create_service(ESPBTUUID::from_raw(uuid), advertise);
+}
+BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles, uint8_t inst_id) {
+  ESP_LOGV(TAG, "Creating service - %s", uuid.to_string().c_str());
+  BLEService *service = new BLEService(uuid, num_handles, inst_id);
+  this->services_.push_back(service);
+  if (advertise) {
+    this->advertising_->add_service_uuid(uuid);
+  }
+  service->do_create(this);
+  return service;
+}
+
+void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
+                                    esp_ble_gatts_cb_param_t *param) {
+  switch (event) {
+    case ESP_GATTS_CONNECT_EVT: {
+      ESP_LOGD(TAG, "BLE Client connected");
+      this->add_client_(param->connect.conn_id, (void *) this);
+      this->connected_clients_++;
+      for (auto *component : this->service_components_) {
+        component->on_client_connect();
+      }
+      break;
+    }
+    case ESP_GATTS_DISCONNECT_EVT: {
+      ESP_LOGD(TAG, "BLE Client disconnected");
+      if (this->remove_client_(param->disconnect.conn_id))
+        this->connected_clients_--;
+      this->advertising_->start();
+      for (auto *component : this->service_components_) {
+        component->on_client_disconnect();
+      }
+      break;
+    }
+    case ESP_GATTS_REG_EVT: {
+      this->gatts_if_ = gatts_if;
+      xSemaphoreGive(this->register_lock_);
+      break;
+    }
+    default:
+      break;
+  }
+
+  for (auto *service : this->services_) {
+    service->gatts_event_handler(event, gatts_if, param);
+  }
+}
+
+float BLEServer::get_setup_priority() const { return setup_priority::BLUETOOTH - 10; }
+
+void BLEServer::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE Server:"); }
+
+BLEServer *global_ble_server = nullptr;
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/ble_server.h b/esphome/components/esp32_ble/ble_server.h
new file mode 100644
index 0000000000..0713816ad3
--- /dev/null
+++ b/esphome/components/esp32_ble/ble_server.h
@@ -0,0 +1,84 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/preferences.h"
+#include "ble_service.h"
+#include "ble_characteristic.h"
+#include "ble_uuid.h"
+#include "ble_advertising.h"
+#include <map>
+
+#include "queue.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+#include <esp_gap_ble_api.h>
+#include <esp_gatts_api.h>
+
+namespace esphome {
+namespace esp32_ble {
+
+class BLEServiceComponent {
+ public:
+  virtual void setup_service();
+  virtual void on_client_connect(){};
+  virtual void on_client_disconnect(){};
+};
+
+class BLEServer : public Component {
+ public:
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override;
+
+  void teardown();
+
+  void set_manufacturer(const std::string &manufacturer) { this->manufacturer_ = manufacturer; }
+  void set_model(const std::string &model) { this->model_ = model; }
+
+  BLEService *create_service(const uint8_t *uuid, bool advertise = false);
+  BLEService *create_service(uint16_t uuid, bool advertise = false);
+  BLEService *create_service(const std::string &uuid, bool advertise = false);
+  BLEService *create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15, uint8_t inst_id = 0);
+
+  esp_gatt_if_t get_gatts_if() { return this->gatts_if_; }
+  uint32_t get_connected_client_count() { return this->connected_clients_; }
+  const std::map<uint16_t, void *> &get_clients() { return this->clients_; }
+  BLEAdvertising *get_advertising() { return this->advertising_; }
+
+  void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
+
+  void register_service_component(BLEServiceComponent *component) { this->service_components_.push_back(component); }
+
+ protected:
+  bool create_device_characteristics_();
+  void setup_server_();
+
+  void add_client_(uint16_t conn_id, void *client) {
+    this->clients_.insert(std::pair<uint16_t, void *>(conn_id, client));
+  }
+  bool remove_client_(uint16_t conn_id) { return this->clients_.erase(conn_id) > 0; }
+
+  std::string manufacturer_;
+  optional<std::string> model_;
+  esp_gatt_if_t gatts_if_{0};
+  BLEAdvertising *advertising_;
+
+  uint32_t connected_clients_{0};
+  std::map<uint16_t, void *> clients_;
+
+  std::vector<BLEService *> services_;
+  BLEService *device_information_service;
+
+  std::vector<BLEServiceComponent *> service_components_;
+
+  SemaphoreHandle_t register_lock_;
+};
+
+extern BLEServer *global_ble_server;
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/ble_service.cpp b/esphome/components/esp32_ble/ble_service.cpp
new file mode 100644
index 0000000000..af8fe00a1d
--- /dev/null
+++ b/esphome/components/esp32_ble/ble_service.cpp
@@ -0,0 +1,129 @@
+#include "ble_service.h"
+#include "ble_server.h"
+#include "esphome/core/log.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace esp32_ble {
+
+static const char *TAG = "esp32_ble.service";
+
+BLEService::BLEService(ESPBTUUID uuid, uint16_t num_handles, uint8_t inst_id)
+    : uuid_(uuid), num_handles_(num_handles), inst_id_(inst_id) {
+  this->create_lock_ = xSemaphoreCreateBinary();
+  this->start_lock_ = xSemaphoreCreateBinary();
+  this->stop_lock_ = xSemaphoreCreateBinary();
+
+  xSemaphoreGive(this->create_lock_);
+  xSemaphoreGive(this->start_lock_);
+  xSemaphoreGive(this->stop_lock_);
+}
+
+BLEService::~BLEService() {
+  for (auto &chr : this->characteristics_)
+    delete chr;
+}
+
+BLECharacteristic *BLEService::get_characteristic(ESPBTUUID uuid) {
+  for (auto *chr : this->characteristics_)
+    if (chr->get_uuid() == uuid)
+      return chr;
+  return nullptr;
+}
+
+BLECharacteristic *BLEService::get_characteristic(uint16_t uuid) {
+  return this->get_characteristic(ESPBTUUID::from_uint16(uuid));
+}
+BLECharacteristic *BLEService::create_characteristic(uint16_t uuid, esp_gatt_char_prop_t properties) {
+  return create_characteristic(ESPBTUUID::from_uint16(uuid), properties);
+}
+BLECharacteristic *BLEService::create_characteristic(const std::string &uuid, esp_gatt_char_prop_t properties) {
+  return create_characteristic(ESPBTUUID::from_raw(uuid), properties);
+}
+BLECharacteristic *BLEService::create_characteristic(ESPBTUUID uuid, esp_gatt_char_prop_t properties) {
+  BLECharacteristic *characteristic = new BLECharacteristic(uuid, properties);
+  this->characteristics_.push_back(characteristic);
+  return characteristic;
+}
+
+bool BLEService::do_create(BLEServer *server) {
+  this->server_ = server;
+
+  xSemaphoreTake(this->create_lock_, portMAX_DELAY);
+  esp_gatt_srvc_id_t srvc_id;
+  srvc_id.is_primary = true;
+  srvc_id.id.inst_id = this->inst_id_;
+  srvc_id.id.uuid = this->uuid_.get_uuid();
+
+  esp_err_t err = esp_ble_gatts_create_service(server->get_gatts_if(), &srvc_id, this->num_handles_);
+  if (err != ESP_OK) {
+    ESP_LOGE(TAG, "esp_ble_gatts_create_service failed: %d", err);
+    return false;
+  }
+  xSemaphoreWait(this->create_lock_, portMAX_DELAY);
+
+  return true;
+}
+
+void BLEService::start() {
+  for (auto *characteristic : this->characteristics_) {
+    this->last_created_characteristic_ = characteristic;
+    characteristic->do_create(this);
+  }
+
+  xSemaphoreTake(this->start_lock_, portMAX_DELAY);
+  esp_err_t err = esp_ble_gatts_start_service(this->handle_);
+  if (err != ESP_OK) {
+    ESP_LOGE(TAG, "esp_ble_gatts_start_service failed: %d", err);
+    return;
+  }
+  xSemaphoreWait(this->start_lock_, portMAX_DELAY);
+}
+
+void BLEService::stop() {
+  xSemaphoreTake(this->stop_lock_, portMAX_DELAY);
+  esp_err_t err = esp_ble_gatts_stop_service(this->handle_);
+  if (err != ESP_OK) {
+    ESP_LOGE(TAG, "esp_ble_gatts_stop_service failed: %d", err);
+    return;
+  }
+  xSemaphoreWait(this->stop_lock_, portMAX_DELAY);
+}
+
+void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
+                                     esp_ble_gatts_cb_param_t *param) {
+  switch (event) {
+    case ESP_GATTS_CREATE_EVT: {
+      if (this->uuid_ == ESPBTUUID::from_uuid(param->create.service_id.id.uuid) &&
+          this->inst_id_ == param->create.service_id.id.inst_id) {
+        this->handle_ = param->create.service_handle;
+        xSemaphoreGive(this->create_lock_);
+      }
+      break;
+    }
+    case ESP_GATTS_START_EVT: {
+      if (param->start.service_handle == this->handle_) {
+        xSemaphoreGive(this->start_lock_);
+      }
+      break;
+    }
+    case ESP_GATTS_STOP_EVT: {
+      if (param->start.service_handle == this->handle_) {
+        xSemaphoreGive(this->stop_lock_);
+      }
+      break;
+    }
+    default:
+      break;
+  }
+
+  for (auto *characteristic : this->characteristics_) {
+    characteristic->gatts_event_handler(event, gatts_if, param);
+  }
+}
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/ble_service.h b/esphome/components/esp32_ble/ble_service.h
new file mode 100644
index 0000000000..9a88d4e1e7
--- /dev/null
+++ b/esphome/components/esp32_ble/ble_service.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include "ble_uuid.h"
+#include "ble_characteristic.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+#include <esp_gap_ble_api.h>
+#include <esp_gatt_defs.h>
+#include <esp_gattc_api.h>
+#include <esp_gatts_api.h>
+#include <esp_bt_defs.h>
+
+namespace esphome {
+namespace esp32_ble {
+
+class BLEServer;
+
+class BLEService {
+ public:
+  BLEService(ESPBTUUID uuid, uint16_t num_handles, uint8_t inst_id);
+  ~BLEService();
+  BLECharacteristic *get_characteristic(ESPBTUUID uuid);
+  BLECharacteristic *get_characteristic(uint16_t uuid);
+
+  BLECharacteristic *create_characteristic(const std::string &uuid, esp_gatt_char_prop_t properties);
+  BLECharacteristic *create_characteristic(uint16_t uuid, esp_gatt_char_prop_t properties);
+  BLECharacteristic *create_characteristic(ESPBTUUID uuid, esp_gatt_char_prop_t properties);
+
+  ESPBTUUID get_uuid() { return this->uuid_; }
+  BLECharacteristic *get_last_created_characteristic() { return this->last_created_characteristic_; }
+  uint16_t get_handle() { return this->handle_; }
+
+  BLEServer *get_server() { return this->server_; }
+
+  bool do_create(BLEServer *server);
+  void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
+
+  void start();
+  void stop();
+
+ protected:
+  bool errored_{false};
+
+  std::vector<BLECharacteristic *> characteristics_;
+  BLECharacteristic *last_created_characteristic_{nullptr};
+  BLEServer *server_;
+  ESPBTUUID uuid_;
+  uint16_t num_handles_;
+  uint16_t handle_{0xFFFF};
+  uint8_t inst_id_;
+
+  SemaphoreHandle_t create_lock_;
+  SemaphoreHandle_t start_lock_;
+  SemaphoreHandle_t stop_lock_;
+};
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp
new file mode 100644
index 0000000000..cb0b99c62b
--- /dev/null
+++ b/esphome/components/esp32_ble/ble_uuid.cpp
@@ -0,0 +1,184 @@
+#include "ble_uuid.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace esp32_ble {
+
+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;
+  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];
+  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() { return this->uuid_; }
+std::string ESPBTUUID::to_string() {
+  char sbuf[64];
+  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;
+    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;
+    default:
+    case ESP_UUID_LEN_128:
+      char *bpos = sbuf;
+      for (int8_t i = 15; i >= 0; i--) {
+        sprintf(bpos, "%02X", this->uuid_.uuid.uuid128[i]);
+        bpos += 2;
+        if (i == 6 || i == 8 || i == 10 || i == 12)
+          sprintf(bpos++, "-");
+      }
+      sbuf[47] = '\0';
+      break;
+  }
+  return sbuf;
+}
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h
new file mode 100644
index 0000000000..0e3c9b0d0a
--- /dev/null
+++ b/esphome/components/esp32_ble/ble_uuid.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include "esphome/core/helpers.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+#include <string>
+#include <esp_bt_defs.h>
+
+namespace esphome {
+namespace esp32_ble {
+
+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();
+
+  std::string to_string();
+
+ protected:
+  esp_bt_uuid_t uuid_;
+};
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h
new file mode 100644
index 0000000000..f5c861a917
--- /dev/null
+++ b/esphome/components/esp32_ble/queue.h
@@ -0,0 +1,114 @@
+#pragma once
+#include "esphome/core/component.h"
+#include "esphome/core/helpers.h"
+
+#include <queue>
+#include <mutex>
+
+#ifdef ARDUINO_ARCH_ESP32
+
+#include <esp_gap_ble_api.h>
+#include <esp_gatts_api.h>
+#include <esp_gattc_api.h>
+
+/*
+ * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather
+ * than trying to deal wth 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.
+ */
+
+namespace esphome {
+namespace esp32_ble {
+
+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().
+// 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.
+    if (e == ESP_GATTC_NOTIFY_EVT) {
+      memcpy(this->event_.gattc.notify_data, p->notify.value, p->notify.value_len);
+      this->event_.gattc.gattc_param.notify.value = this->event_.gattc.notify_data;
+    }
+    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));
+    this->type_ = GATTS;
+  };
+
+  union {
+    struct gap_event {
+      esp_gap_ble_cb_event_t gap_event;
+      esp_ble_gap_cb_param_t gap_param;
+    } gap;
+
+    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 notify_data[64];
+    } gattc;
+
+    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_;
+  enum ble_event_t : uint8_t {
+    GAP,
+    GATTC,
+    GATTS,
+  } type_;
+};
+
+}  // namespace esp32_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py
index 46f5678856..3e00692d3a 100644
--- a/esphome/components/esp32_ble_beacon/__init__.py
+++ b/esphome/components/esp32_ble_beacon/__init__.py
@@ -22,12 +22,12 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     uuid = config[CONF_UUID].hex
     uuid_arr = [
         cg.RawExpression("0x{}".format(uuid[i : i + 2])) for i in range(0, len(uuid), 2)
     ]
     var = cg.new_Pvariable(config[CONF_ID], uuid_arr)
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
     cg.add(var.set_major(config[CONF_MAJOR]))
     cg.add(var.set_minor(config[CONF_MINOR]))
diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py
index 8726ab3e8d..5c70ddb27f 100644
--- a/esphome/components/esp32_ble_tracker/__init__.py
+++ b/esphome/components/esp32_ble_tracker/__init__.py
@@ -15,7 +15,6 @@ from esphome.const import (
     CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
     CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
 )
-from esphome.core import coroutine
 
 ESP_PLATFORMS = [ESP_PLATFORM_ESP32]
 AUTO_LOAD = ["xiaomi_ble", "ruuvi_ble"]
@@ -177,9 +176,9 @@ ESP_BLE_DEVICE_SCHEMA = cv.Schema(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
     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)))
@@ -189,7 +188,7 @@ def to_code(config):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
         if CONF_MAC_ADDRESS in conf:
             cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
-        yield automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf)
+        await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf)
     for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
         if len(conf[CONF_SERVICE_UUID]) == len(bt_uuid16_format):
@@ -201,7 +200,7 @@ def to_code(config):
             cg.add(trigger.set_service_uuid128(uuid128))
         if CONF_MAC_ADDRESS in conf:
             cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
-        yield automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
+        await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
     for conf in config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
         if len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid16_format):
@@ -213,18 +212,16 @@ def to_code(config):
             cg.add(trigger.set_manufacturer_uuid128(uuid128))
         if CONF_MAC_ADDRESS in conf:
             cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
-        yield automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
+        await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
 
 
-@coroutine
-def register_ble_device(var, config):
-    paren = yield cg.get_variable(config[CONF_ESP32_BLE_ID])
+async def register_ble_device(var, config):
+    paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
     cg.add(paren.register_listener(var))
-    yield var
+    return var
 
 
-@coroutine
-def register_client(var, config):
-    paren = yield cg.get_variable(config[CONF_ESP32_BLE_ID])
+async def register_client(var, config):
+    paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
     cg.add(paren.register_client(var))
-    yield var
+    return var
diff --git a/esphome/components/esp32_ble_tracker/queue.h b/esphome/components/esp32_ble_tracker/queue.h
index 6f36cf874d..17adb98034 100644
--- a/esphome/components/esp32_ble_tracker/queue.h
+++ b/esphome/components/esp32_ble_tracker/queue.h
@@ -66,10 +66,19 @@ class BLEEvent {
     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.
-    if (e == ESP_GATTC_NOTIFY_EVT) {
-      memcpy(this->event_.gattc.notify_data, p->notify.value, p->notify.value_len);
-      this->event_.gattc.gattc_param.notify.value = this->event_.gattc.notify_data;
+    // Need to also make a copy of relevant 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_ = 1;
   };
@@ -84,7 +93,7 @@ class BLEEvent {
       esp_gattc_cb_event_t gattc_event;
       esp_gatt_if_t gattc_if;
       esp_ble_gattc_cb_param_t gattc_param;
-      uint8_t notify_data[64];
+      uint8_t data[64];
     } gattc;
   } event_;
   uint8_t type_;  // 0=gap 1=gattc
diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py
index f7169257a4..abbb4b1b7e 100644
--- a/esphome/components/esp32_camera/__init__.py
+++ b/esphome/components/esp32_camera/__init__.py
@@ -25,8 +25,6 @@ ESP32CameraFrameSize = esp32_camera_ns.enum("ESP32CameraFrameSize")
 FRAME_SIZES = {
     "160X120": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120,
     "QQVGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120,
-    "128X160": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_128X160,
-    "QQVGA2": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_128X160,
     "176X144": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_176X144,
     "QCIF": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_176X144,
     "240X176": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_240X176,
@@ -124,9 +122,9 @@ SETTERS = {
 }
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID], config[CONF_NAME])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
     for key, setter in SETTERS.items():
         if key in config:
diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp
index 1d9faf7ea2..6bb57a8304 100644
--- a/esphome/components/esp32_camera/esp32_camera.cpp
+++ b/esphome/components/esp32_camera/esp32_camera.cpp
@@ -55,9 +55,6 @@ void ESP32Camera::dump_config() {
     case FRAMESIZE_QQVGA:
       ESP_LOGCONFIG(TAG, "  Resolution: 160x120 (QQVGA)");
       break;
-    case FRAMESIZE_QQVGA2:
-      ESP_LOGCONFIG(TAG, "  Resolution: 128x160 (QQVGA2)");
-      break;
     case FRAMESIZE_QCIF:
       ESP_LOGCONFIG(TAG, "  Resolution: 176x155 (QCIF)");
       break;
@@ -209,9 +206,6 @@ void ESP32Camera::set_frame_size(ESP32CameraFrameSize size) {
     case ESP32_CAMERA_SIZE_160X120:
       this->config_.frame_size = FRAMESIZE_QQVGA;
       break;
-    case ESP32_CAMERA_SIZE_128X160:
-      this->config_.frame_size = FRAMESIZE_QQVGA2;
-      break;
     case ESP32_CAMERA_SIZE_176X144:
       this->config_.frame_size = FRAMESIZE_QCIF;
       break;
diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h
index fd8597d0c1..03272d3b32 100644
--- a/esphome/components/esp32_camera/esp32_camera.h
+++ b/esphome/components/esp32_camera/esp32_camera.h
@@ -37,7 +37,6 @@ class CameraImageReader {
 
 enum ESP32CameraFrameSize {
   ESP32_CAMERA_SIZE_160X120,    // QQVGA
-  ESP32_CAMERA_SIZE_128X160,    // QQVGA2
   ESP32_CAMERA_SIZE_176X144,    // QCIF
   ESP32_CAMERA_SIZE_240X176,    // HQVGA
   ESP32_CAMERA_SIZE_320X240,    // QVGA
diff --git a/esphome/components/esp32_dac/output.py b/esphome/components/esp32_dac/output.py
index 8cfc7570e9..8534a1bae1 100644
--- a/esphome/components/esp32_dac/output.py
+++ b/esphome/components/esp32_dac/output.py
@@ -26,10 +26,10 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield output.register_output(var, config)
+    await cg.register_component(var, config)
+    await output.register_output(var, config)
 
-    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+    pin = await cg.gpio_pin_expression(config[CONF_PIN])
     cg.add(var.set_pin(pin))
diff --git a/esphome/components/esp32_hall/sensor.py b/esphome/components/esp32_hall/sensor.py
index 363b3f7548..b800b3436a 100644
--- a/esphome/components/esp32_hall/sensor.py
+++ b/esphome/components/esp32_hall/sensor.py
@@ -5,6 +5,7 @@ from esphome.const import (
     CONF_ID,
     DEVICE_CLASS_EMPTY,
     ESP_PLATFORM_ESP32,
+    STATE_CLASS_MEASUREMENT,
     UNIT_MICROTESLA,
     ICON_MAGNET,
 )
@@ -17,7 +18,9 @@ ESP32HallSensor = esp32_hall_ns.class_(
 )
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_MICROTESLA, ICON_MAGNET, 1, DEVICE_CLASS_EMPTY)
+    sensor.sensor_schema(
+        UNIT_MICROTESLA, ICON_MAGNET, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(ESP32HallSensor),
@@ -27,7 +30,7 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py
new file mode 100644
index 0000000000..8b91e9151d
--- /dev/null
+++ b/esphome/components/esp32_improv/__init__.py
@@ -0,0 +1,69 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import binary_sensor, output, esp32_ble
+from esphome.const import CONF_ID, ESP_PLATFORM_ESP32
+
+
+AUTO_LOAD = ["binary_sensor", "output", "improv"]
+CODEOWNERS = ["@jesserockz"]
+DEPENDENCIES = ["esp32_ble", "wifi"]
+ESP_PLATFORMS = [ESP_PLATFORM_ESP32]
+
+CONF_AUTHORIZED_DURATION = "authorized_duration"
+CONF_AUTHORIZER = "authorizer"
+CONF_BLE_SERVER_ID = "ble_server_id"
+CONF_IDENTIFY_DURATION = "identify_duration"
+CONF_STATUS_INDICATOR = "status_indicator"
+CONF_WIFI_TIMEOUT = "wifi_timeout"
+
+esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv")
+ESP32ImprovComponent = esp32_improv_ns.class_(
+    "ESP32ImprovComponent", cg.Component, esp32_ble.BLEServiceComponent
+)
+
+
+def validate_none_(value):
+    if value in ("none", "None"):
+        return None
+    if cv.boolean(value) is False:
+        return None
+    raise cv.Invalid("Must be none")
+
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
+        cv.GenerateID(CONF_BLE_SERVER_ID): cv.use_id(esp32_ble.BLEServer),
+        cv.Required(CONF_AUTHORIZER): cv.Any(
+            validate_none_, cv.use_id(binary_sensor.BinarySensor)
+        ),
+        cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput),
+        cv.Optional(
+            CONF_IDENTIFY_DURATION, default="10s"
+        ): cv.positive_time_period_milliseconds,
+        cv.Optional(
+            CONF_AUTHORIZED_DURATION, default="1min"
+        ): cv.positive_time_period_milliseconds,
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+
+    ble_server = await cg.get_variable(config[CONF_BLE_SERVER_ID])
+    cg.add(ble_server.register_service_component(var))
+
+    cg.add_define("USE_IMPROV")
+
+    cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION]))
+    cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION]))
+
+    if CONF_AUTHORIZER in config and config[CONF_AUTHORIZER] is not None:
+        activator = await cg.get_variable(config[CONF_AUTHORIZER])
+        cg.add(var.set_authorizer(activator))
+
+    if CONF_STATUS_INDICATOR in config:
+        status_indicator = await cg.get_variable(config[CONF_STATUS_INDICATOR])
+        cg.add(var.set_status_indicator(status_indicator))
diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp
new file mode 100644
index 0000000000..a407d819b7
--- /dev/null
+++ b/esphome/components/esp32_improv/esp32_improv_component.cpp
@@ -0,0 +1,281 @@
+#include "esp32_improv_component.h"
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+#include "esphome/components/esp32_ble/ble_2902.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace esp32_improv {
+
+static const char *TAG = "esp32_improv.component";
+
+ESP32ImprovComponent::ESP32ImprovComponent() { global_improv_component = this; }
+
+void ESP32ImprovComponent::setup_service() {
+  this->service_ = esp32_ble::global_ble_server->create_service(improv::SERVICE_UUID, true);
+
+  this->status_ = this->service_->create_characteristic(
+      improv::STATUS_UUID, esp32_ble::BLECharacteristic::PROPERTY_READ | esp32_ble::BLECharacteristic::PROPERTY_NOTIFY);
+  esp32_ble::BLEDescriptor *status_descriptor = new esp32_ble::BLE2902();
+  this->status_->add_descriptor(status_descriptor);
+
+  this->error_ = this->service_->create_characteristic(
+      improv::ERROR_UUID, esp32_ble::BLECharacteristic::PROPERTY_READ | esp32_ble::BLECharacteristic::PROPERTY_NOTIFY);
+  esp32_ble::BLEDescriptor *error_descriptor = new esp32_ble::BLE2902();
+  this->error_->add_descriptor(error_descriptor);
+
+  this->rpc_ =
+      this->service_->create_characteristic(improv::RPC_COMMAND_UUID, esp32_ble::BLECharacteristic::PROPERTY_WRITE);
+  this->rpc_->on_write([this](const std::vector<uint8_t> &data) {
+    if (data.size() > 0) {
+      this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end());
+    }
+  });
+  esp32_ble::BLEDescriptor *rpc_descriptor = new esp32_ble::BLE2902();
+  this->rpc_->add_descriptor(rpc_descriptor);
+
+  this->rpc_response_ =
+      this->service_->create_characteristic(improv::RPC_RESULT_UUID, esp32_ble::BLECharacteristic::PROPERTY_READ |
+                                                                         esp32_ble::BLECharacteristic::PROPERTY_NOTIFY);
+  esp32_ble::BLEDescriptor *rpc_response_descriptor = new esp32_ble::BLE2902();
+  this->rpc_response_->add_descriptor(rpc_response_descriptor);
+
+  this->capabilities_ =
+      this->service_->create_characteristic(improv::CAPABILITIES_UUID, esp32_ble::BLECharacteristic::PROPERTY_READ);
+  esp32_ble::BLEDescriptor *capabilities_descriptor = new esp32_ble::BLE2902();
+  this->capabilities_->add_descriptor(capabilities_descriptor);
+  uint8_t capabilities = 0x00;
+  if (this->status_indicator_ != nullptr)
+    capabilities |= improv::CAPABILITY_IDENTIFY;
+  this->capabilities_->set_value(capabilities);
+  this->setup_complete_ = true;
+}
+
+void ESP32ImprovComponent::loop() {
+  if (this->incoming_data_.size() > 0)
+    this->process_incoming_data_();
+  uint32_t now = millis();
+
+  switch (this->state_) {
+    case improv::STATE_STOPPED:
+      if (this->status_indicator_ != nullptr)
+        this->status_indicator_->turn_off();
+
+      if (this->should_start_ && this->setup_complete_) {
+        ESP_LOGD(TAG, "Starting Improv service...");
+
+        this->service_->start();
+        this->service_->get_server()->get_advertising()->start();
+
+        this->set_state_(improv::STATE_AWAITING_AUTHORIZATION);
+        this->set_error_(improv::ERROR_NONE);
+        this->should_start_ = false;
+        ESP_LOGD(TAG, "Service started!");
+      }
+      break;
+    case improv::STATE_AWAITING_AUTHORIZATION: {
+      if (this->authorizer_ == nullptr || this->authorizer_->state) {
+        this->set_state_(improv::STATE_AUTHORIZED);
+        this->authorized_start_ = now;
+      } else {
+        if (this->status_indicator_ != nullptr) {
+          if (!this->check_identify_())
+            this->status_indicator_->turn_on();
+        }
+      }
+      break;
+    }
+    case improv::STATE_AUTHORIZED: {
+      if (this->authorizer_ != nullptr) {
+        if (now - this->authorized_start_ > this->authorized_duration_) {
+          ESP_LOGD(TAG, "Authorization timeout");
+          this->set_state_(improv::STATE_AWAITING_AUTHORIZATION);
+          return;
+        }
+      }
+      if (this->status_indicator_ != nullptr) {
+        if (!this->check_identify_()) {
+          if ((now % 1000) < 500) {
+            this->status_indicator_->turn_on();
+          } else {
+            this->status_indicator_->turn_off();
+          }
+        }
+      }
+      break;
+    }
+    case improv::STATE_PROVISIONING: {
+      if (this->status_indicator_ != nullptr) {
+        if ((now % 200) < 100) {
+          this->status_indicator_->turn_on();
+        } else {
+          this->status_indicator_->turn_off();
+        }
+      }
+      if (wifi::global_wifi_component->is_connected()) {
+        wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
+                                                   this->connecting_sta_.get_password());
+        this->connecting_sta_ = {};
+        this->cancel_timeout("wifi-connect-timeout");
+        this->set_state_(improv::STATE_PROVISIONED);
+
+        std::string url = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome";
+        std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, {url});
+        this->send_response(data);
+        this->set_timeout("end-service", 1000, [this] {
+          this->service_->stop();
+          this->set_state_(improv::STATE_STOPPED);
+        });
+      }
+      break;
+    }
+    case improv::STATE_PROVISIONED: {
+      this->incoming_data_.clear();
+      if (this->status_indicator_ != nullptr)
+        this->status_indicator_->turn_off();
+      break;
+    }
+  }
+}
+
+bool ESP32ImprovComponent::check_identify_() {
+  uint32_t now = millis();
+
+  bool identify = this->identify_start_ != 0 && now - this->identify_start_ <= this->identify_duration_;
+
+  if (identify) {
+    uint32_t time = now % 1000;
+    if (time < 600 && time % 200 < 100) {
+      this->status_indicator_->turn_on();
+    } else {
+      this->status_indicator_->turn_off();
+    }
+  }
+  return identify;
+}
+
+void ESP32ImprovComponent::set_state_(improv::State state) {
+  ESP_LOGV(TAG, "Setting state: %d", state);
+  this->state_ = state;
+  if (this->status_->get_value().size() == 0 || this->status_->get_value()[0] != state) {
+    uint8_t data[1]{state};
+    this->status_->set_value(data, 1);
+    if (state != improv::STATE_STOPPED)
+      this->status_->notify();
+  }
+}
+
+void ESP32ImprovComponent::set_error_(improv::Error error) {
+  if (error != improv::ERROR_NONE)
+    ESP_LOGE(TAG, "Error: %d", error);
+  if (this->error_->get_value().size() == 0 || this->error_->get_value()[0] != error) {
+    uint8_t data[1]{error};
+    this->error_->set_value(data, 1);
+    if (this->state_ != improv::STATE_STOPPED)
+      this->error_->notify();
+  }
+}
+
+void ESP32ImprovComponent::send_response(std::vector<uint8_t> &response) {
+  this->rpc_response_->set_value(response);
+  if (this->state_ != improv::STATE_STOPPED)
+    this->rpc_response_->notify();
+}
+
+void ESP32ImprovComponent::start() {
+  if (this->state_ != improv::STATE_STOPPED)
+    return;
+
+  ESP_LOGD(TAG, "Setting Improv to start");
+  this->should_start_ = true;
+}
+
+void ESP32ImprovComponent::end() {
+  this->set_timeout("end-service", 1000, [this] {
+    this->service_->stop();
+    this->set_state_(improv::STATE_STOPPED);
+  });
+}
+
+float ESP32ImprovComponent::get_setup_priority() const {
+  // Before WiFi
+  return setup_priority::AFTER_BLUETOOTH;
+}
+
+void ESP32ImprovComponent::dump_config() {
+  ESP_LOGCONFIG(TAG, "ESP32 Improv:");
+  LOG_BINARY_SENSOR("  ", "Authorizer", this->authorizer_);
+  ESP_LOGCONFIG(TAG, "  Status Indicator: '%s'", YESNO(this->status_indicator_ != nullptr));
+}
+
+void ESP32ImprovComponent::process_incoming_data_() {
+  uint8_t length = this->incoming_data_[1];
+
+  ESP_LOGD(TAG, "Processing bytes - %s", hexencode(this->incoming_data_).c_str());
+  if (this->incoming_data_.size() - 3 == length) {
+    this->set_error_(improv::ERROR_NONE);
+    improv::ImprovCommand command = improv::parse_improv_data(this->incoming_data_);
+    switch (command.command) {
+      case improv::BAD_CHECKSUM:
+        ESP_LOGW(TAG, "Error decoding Improv payload");
+        this->set_error_(improv::ERROR_INVALID_RPC);
+        this->incoming_data_.clear();
+        break;
+      case improv::WIFI_SETTINGS: {
+        if (this->state_ != improv::STATE_AUTHORIZED) {
+          ESP_LOGW(TAG, "Settings received, but not authorized");
+          this->set_error_(improv::ERROR_NOT_AUTHORIZED);
+          this->incoming_data_.clear();
+          return;
+        }
+        wifi::WiFiAP sta{};
+        sta.set_ssid(command.ssid);
+        sta.set_password(command.password);
+        this->connecting_sta_ = sta;
+
+        wifi::global_wifi_component->set_sta(sta);
+        wifi::global_wifi_component->start_scanning();
+        this->set_state_(improv::STATE_PROVISIONING);
+        ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
+                 command.password.c_str());
+
+        auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this);
+        this->set_timeout("wifi-connect-timeout", 30000, f);
+        this->incoming_data_.clear();
+        break;
+      }
+      case improv::IDENTIFY:
+        this->incoming_data_.clear();
+        this->identify_start_ = millis();
+        break;
+      default:
+        ESP_LOGW(TAG, "Unknown Improv payload");
+        this->set_error_(improv::ERROR_UNKNOWN_RPC);
+        this->incoming_data_.clear();
+    }
+  } else if (this->incoming_data_.size() - 2 > length) {
+    ESP_LOGV(TAG, "Too much data came in, or malformed resetting buffer...");
+    this->incoming_data_.clear();
+  } else {
+    ESP_LOGV(TAG, "Waiting for split data packets...");
+  }
+}
+
+void ESP32ImprovComponent::on_wifi_connect_timeout_() {
+  this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
+  this->set_state_(improv::STATE_AUTHORIZED);
+  if (this->authorizer_ != nullptr)
+    this->authorized_start_ = millis();
+  ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network");
+  wifi::global_wifi_component->clear_sta();
+}
+
+void ESP32ImprovComponent::on_client_disconnect() { this->set_error_(improv::ERROR_NONE); };
+
+ESP32ImprovComponent *global_improv_component = nullptr;
+
+}  // namespace esp32_improv
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h
new file mode 100644
index 0000000000..498a443ec3
--- /dev/null
+++ b/esphome/components/esp32_improv/esp32_improv_component.h
@@ -0,0 +1,74 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/preferences.h"
+#include "esphome/components/binary_sensor/binary_sensor.h"
+#include "esphome/components/esp32_ble/ble_server.h"
+#include "esphome/components/esp32_ble/ble_characteristic.h"
+#include "esphome/components/output/binary_output.h"
+#include "esphome/components/wifi/wifi_component.h"
+#include "esphome/components/improv/improv.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace esp32_improv {
+
+class ESP32ImprovComponent : public Component, public esp32_ble::BLEServiceComponent {
+ public:
+  ESP32ImprovComponent();
+  void dump_config() override;
+  void loop() override;
+  void setup_service() override;
+  void on_client_disconnect() override;
+
+  float get_setup_priority() const override;
+  void start();
+  void end();
+  bool is_active() const { return this->state_ != improv::STATE_STOPPED; }
+
+  void set_authorizer(binary_sensor::BinarySensor *authorizer) { this->authorizer_ = authorizer; }
+  void set_status_indicator(output::BinaryOutput *status_indicator) { this->status_indicator_ = status_indicator; }
+  void set_identify_duration(uint32_t identify_duration) { this->identify_duration_ = identify_duration; }
+  void set_authorized_duration(uint32_t authorized_duration) { this->authorized_duration_ = authorized_duration; }
+
+ protected:
+  bool should_start_{false};
+  bool setup_complete_{false};
+
+  uint32_t identify_start_{0};
+  uint32_t identify_duration_;
+  uint32_t authorized_start_{0};
+  uint32_t authorized_duration_;
+
+  std::vector<uint8_t> incoming_data_;
+  wifi::WiFiAP connecting_sta_;
+
+  esp32_ble::BLEService *service_;
+  esp32_ble::BLECharacteristic *status_;
+  esp32_ble::BLECharacteristic *error_;
+  esp32_ble::BLECharacteristic *rpc_;
+  esp32_ble::BLECharacteristic *rpc_response_;
+  esp32_ble::BLECharacteristic *capabilities_;
+
+  binary_sensor::BinarySensor *authorizer_{nullptr};
+  output::BinaryOutput *status_indicator_{nullptr};
+
+  improv::State state_{improv::STATE_STOPPED};
+  improv::Error error_state_{improv::ERROR_NONE};
+
+  void set_state_(improv::State state);
+  void set_error_(improv::Error error);
+  void send_response(std::vector<uint8_t> &response);
+  void process_incoming_data_();
+  void on_wifi_connect_timeout_();
+  bool check_identify_();
+};
+
+extern ESP32ImprovComponent *global_improv_component;
+
+}  // namespace esp32_improv
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py
index 5c3b47af77..1564476ecf 100644
--- a/esphome/components/esp32_touch/__init__.py
+++ b/esphome/components/esp32_touch/__init__.py
@@ -77,9 +77,9 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     touch = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(touch, config)
+    await cg.register_component(touch, config)
 
     cg.add(touch.set_setup_mode(config[CONF_SETUP_MODE]))
     cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER]))
diff --git a/esphome/components/esp32_touch/binary_sensor.py b/esphome/components/esp32_touch/binary_sensor.py
index f8bc348132..300de23f08 100644
--- a/esphome/components/esp32_touch/binary_sensor.py
+++ b/esphome/components/esp32_touch/binary_sensor.py
@@ -51,13 +51,13 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 )
 
 
-def to_code(config):
-    hub = yield cg.get_variable(config[CONF_ESP32_TOUCH_ID])
+async def to_code(config):
+    hub = await cg.get_variable(config[CONF_ESP32_TOUCH_ID])
     var = cg.new_Pvariable(
         config[CONF_ID],
         config[CONF_NAME],
         TOUCH_PADS[config[CONF_PIN]],
         config[CONF_THRESHOLD],
     )
-    yield binary_sensor.register_binary_sensor(var, config)
+    await binary_sensor.register_binary_sensor(var, config)
     cg.add(hub.register_touch_pad(var))
diff --git a/esphome/components/esp8266_pwm/output.py b/esphome/components/esp8266_pwm/output.py
index ad7da4e001..770d400013 100644
--- a/esphome/components/esp8266_pwm/output.py
+++ b/esphome/components/esp8266_pwm/output.py
@@ -35,12 +35,12 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield output.register_output(var, config)
+    await cg.register_component(var, config)
+    await output.register_output(var, config)
 
-    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+    pin = await cg.gpio_pin_expression(config[CONF_PIN])
     cg.add(var.set_pin(pin))
 
     cg.add(var.set_frequency(config[CONF_FREQUENCY]))
@@ -56,9 +56,9 @@ def to_code(config):
         }
     ),
 )
-def esp8266_set_frequency_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def esp8266_set_frequency_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    template_ = yield cg.templatable(config[CONF_FREQUENCY], args, float)
+    template_ = await cg.templatable(config[CONF_FREQUENCY], args, float)
     cg.add(var.set_frequency(template_))
-    yield var
+    return var
diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py
index 82366eeac2..94c9ddd2e9 100644
--- a/esphome/components/ethernet/__init__.py
+++ b/esphome/components/ethernet/__init__.py
@@ -59,7 +59,7 @@ IPAddress = cg.global_ns.class_("IPAddress")
 ManualIP = ethernet_ns.struct("ManualIP")
 
 
-def validate(config):
+def _validate(config):
     if CONF_USE_ADDRESS not in config:
         if CONF_MANUAL_IP in config:
             use_address = str(config[CONF_MANUAL_IP][CONF_STATIC_IP])
@@ -90,7 +90,7 @@ CONFIG_SCHEMA = cv.All(
             ),
         }
     ).extend(cv.COMPONENT_SCHEMA),
-    validate,
+    _validate,
 )
 
 
@@ -106,9 +106,9 @@ def manual_ip(config):
 
 
 @coroutine_with_priority(60.0)
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
     cg.add(var.set_phy_addr(config[CONF_PHY_ADDR]))
     cg.add(var.set_mdc_pin(config[CONF_MDC_PIN]))
@@ -118,7 +118,7 @@ def to_code(config):
     cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
 
     if CONF_POWER_PIN in config:
-        pin = yield cg.gpio_pin_expression(config[CONF_POWER_PIN])
+        pin = await cg.gpio_pin_expression(config[CONF_POWER_PIN])
         cg.add(var.set_power_pin(pin))
 
     if CONF_MANUAL_IP in config:
diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp
index 005712420f..8c96cca951 100644
--- a/esphome/components/ethernet/ethernet_component.cpp
+++ b/esphome/components/ethernet/ethernet_component.cpp
@@ -224,10 +224,17 @@ void EthernetComponent::dump_connect_params_() {
   ESP_LOGCONFIG(TAG, "  Subnet: %s", IPAddress(ip.netmask.addr).toString().c_str());
   ESP_LOGCONFIG(TAG, "  Gateway: %s", IPAddress(ip.gw.addr).toString().c_str());
 
-  ip_addr_t dns_ip = dns_getserver(0);
-  ESP_LOGCONFIG(TAG, "  DNS1: %s", IPAddress(dns_ip.u_addr.ip4.addr).toString().c_str());
-  dns_ip = dns_getserver(1);
-  ESP_LOGCONFIG(TAG, "  DNS2: %s", IPAddress(dns_ip.u_addr.ip4.addr).toString().c_str());
+#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 4)
+  const ip_addr_t *dns_ip1 = dns_getserver(0);
+  const ip_addr_t *dns_ip2 = dns_getserver(1);
+#else
+  ip_addr_t tmp_ip1 = dns_getserver(0);
+  const ip_addr_t *dns_ip1 = &tmp_ip1;
+  ip_addr_t tmp_ip2 = dns_getserver(1);
+  const ip_addr_t *dns_ip2 = &tmp_ip2;
+#endif
+  ESP_LOGCONFIG(TAG, "  DNS1: %s", IPAddress(dns_ip1->u_addr.ip4.addr).toString().c_str());
+  ESP_LOGCONFIG(TAG, "  DNS2: %s", IPAddress(dns_ip2->u_addr.ip4.addr).toString().c_str());
   uint8_t mac[6];
   esp_eth_get_mac(mac);
   ESP_LOGCONFIG(TAG, "  MAC Address: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
diff --git a/esphome/components/exposure_notifications/__init__.py b/esphome/components/exposure_notifications/__init__.py
index 1bd8007785..9c28552267 100644
--- a/esphome/components/exposure_notifications/__init__.py
+++ b/esphome/components/exposure_notifications/__init__.py
@@ -32,8 +32,8 @@ CONFIG_SCHEMA = cv.Schema(
 )
 
 
-def to_code(config):
+async def to_code(config):
     for conf in config.get(CONF_ON_EXPOSURE_NOTIFICATION, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
-        yield automation.build_automation(trigger, [(ExposureNotification, "x")], conf)
-        yield esp32_ble_tracker.register_ble_device(trigger, conf)
+        await automation.build_automation(trigger, [(ExposureNotification, "x")], conf)
+        await esp32_ble_tracker.register_ble_device(trigger, conf)
diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py
index 272812adcf..1602ac3b07 100644
--- a/esphome/components/external_components/__init__.py
+++ b/esphome/components/external_components/__init__.py
@@ -98,7 +98,7 @@ CONFIG_SCHEMA = cv.ensure_list(
 )
 
 
-def to_code(config):
+async def to_code(config):
     pass
 
 
diff --git a/esphome/components/ezo/sensor.py b/esphome/components/ezo/sensor.py
index 12640f4038..09b36b7135 100644
--- a/esphome/components/ezo/sensor.py
+++ b/esphome/components/ezo/sensor.py
@@ -24,8 +24,8 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
+    await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py
index 10e38682e7..d1f43467ed 100644
--- a/esphome/components/fan/__init__.py
+++ b/esphome/components/fan/__init__.py
@@ -14,8 +14,11 @@ from esphome.const import (
     CONF_SPEED_COMMAND_TOPIC,
     CONF_SPEED_STATE_TOPIC,
     CONF_NAME,
+    CONF_ON_TURN_OFF,
+    CONF_ON_TURN_ON,
+    CONF_TRIGGER_ID,
 )
-from esphome.core import CORE, coroutine, coroutine_with_priority
+from esphome.core import CORE, coroutine_with_priority
 
 IS_PLATFORM_COMPONENT = True
 
@@ -28,6 +31,9 @@ TurnOnAction = fan_ns.class_("TurnOnAction", automation.Action)
 TurnOffAction = fan_ns.class_("TurnOffAction", automation.Action)
 ToggleAction = fan_ns.class_("ToggleAction", automation.Action)
 
+FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template())
+FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template())
+
 FAN_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend(
     {
         cv.GenerateID(): cv.declare_id(FanState),
@@ -44,19 +50,28 @@ FAN_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend(
         cv.Optional(CONF_SPEED_COMMAND_TOPIC): cv.All(
             cv.requires_component("mqtt"), cv.subscribe_topic
         ),
+        cv.Optional(CONF_ON_TURN_ON): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanTurnOnTrigger),
+            }
+        ),
+        cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanTurnOffTrigger),
+            }
+        ),
     }
 )
 
 
-@coroutine
-def setup_fan_core_(var, config):
+async def setup_fan_core_(var, config):
     cg.add(var.set_name(config[CONF_NAME]))
     if CONF_INTERNAL in config:
         cg.add(var.set_internal(config[CONF_INTERNAL]))
 
     if CONF_MQTT_ID in config:
         mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
-        yield mqtt.register_mqtt_component(mqtt_, config)
+        await mqtt.register_mqtt_component(mqtt_, config)
 
         if CONF_OSCILLATION_STATE_TOPIC in config:
             cg.add(
@@ -77,21 +92,26 @@ def setup_fan_core_(var, config):
                 mqtt_.set_custom_speed_command_topic(config[CONF_SPEED_COMMAND_TOPIC])
             )
 
+    for conf in config.get(CONF_ON_TURN_ON, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        await automation.build_automation(trigger, [], conf)
+    for conf in config.get(CONF_ON_TURN_OFF, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        await automation.build_automation(trigger, [], conf)
 
-@coroutine
-def register_fan(var, config):
+
+async def register_fan(var, config):
     if not CORE.has_id(config[CONF_ID]):
         var = cg.Pvariable(config[CONF_ID], var)
     cg.add(cg.App.register_fan(var))
-    yield cg.register_component(var, config)
-    yield setup_fan_core_(var, config)
+    await cg.register_component(var, config)
+    await setup_fan_core_(var, config)
 
 
-@coroutine
-def create_fan_state(config):
+async def create_fan_state(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield register_fan(var, config)
-    yield var
+    await register_fan(var, config)
+    return var
 
 
 FAN_ACTION_SCHEMA = maybe_simple_id(
@@ -102,15 +122,15 @@ FAN_ACTION_SCHEMA = maybe_simple_id(
 
 
 @automation.register_action("fan.toggle", ToggleAction, FAN_ACTION_SCHEMA)
-def fan_toggle_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, paren)
+async def fan_toggle_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
 
 
 @automation.register_action("fan.turn_off", TurnOffAction, FAN_ACTION_SCHEMA)
-def fan_turn_off_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, paren)
+async def fan_turn_off_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
 
 
 @automation.register_action(
@@ -124,19 +144,19 @@ def fan_turn_off_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def fan_turn_on_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def fan_turn_on_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
     if CONF_OSCILLATING in config:
-        template_ = yield cg.templatable(config[CONF_OSCILLATING], args, bool)
+        template_ = await cg.templatable(config[CONF_OSCILLATING], args, bool)
         cg.add(var.set_oscillating(template_))
     if CONF_SPEED in config:
-        template_ = yield cg.templatable(config[CONF_SPEED], args, int)
+        template_ = await cg.templatable(config[CONF_SPEED], args, int)
         cg.add(var.set_speed(template_))
-    yield var
+    return var
 
 
 @coroutine_with_priority(100.0)
-def to_code(config):
+async def to_code(config):
     cg.add_define("USE_FAN")
     cg.add_global(fan_ns.using)
diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h
index 25c92075ba..fbfc71c720 100644
--- a/esphome/components/fan/automation.h
+++ b/esphome/components/fan/automation.h
@@ -46,5 +46,41 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> {
   FanState *state_;
 };
 
+class FanTurnOnTrigger : public Trigger<> {
+ public:
+  FanTurnOnTrigger(FanState *state) {
+    state->add_on_state_callback([this, state]() {
+      auto is_on = state->state;
+      auto should_trigger = is_on && !this->last_on_;
+      this->last_on_ = is_on;
+      if (should_trigger) {
+        this->trigger();
+      }
+    });
+    this->last_on_ = state->state;
+  }
+
+ protected:
+  bool last_on_;
+};
+
+class FanTurnOffTrigger : public Trigger<> {
+ public:
+  FanTurnOffTrigger(FanState *state) {
+    state->add_on_state_callback([this, state]() {
+      auto is_on = state->state;
+      auto should_trigger = !is_on && this->last_on_;
+      this->last_on_ = is_on;
+      if (should_trigger) {
+        this->trigger();
+      }
+    });
+    this->last_on_ = state->state;
+  }
+
+ protected:
+  bool last_on_;
+};
+
 }  // namespace fan
 }  // namespace esphome
diff --git a/esphome/components/fastled_base/__init__.py b/esphome/components/fastled_base/__init__.py
index 4c720191b9..f2d0bb1f38 100644
--- a/esphome/components/fastled_base/__init__.py
+++ b/esphome/components/fastled_base/__init__.py
@@ -7,7 +7,6 @@ from esphome.const import (
     CONF_RGB_ORDER,
     CONF_MAX_REFRESH_RATE,
 )
-from esphome.core import coroutine
 
 CODEOWNERS = ["@OttoWinter"]
 fastled_base_ns = cg.esphome_ns.namespace("fastled_base")
@@ -34,17 +33,16 @@ BASE_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-@coroutine
-def new_fastled_light(config):
+async def new_fastled_light(config):
     var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
     if CONF_MAX_REFRESH_RATE in config:
         cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE]))
 
-    yield light.register_light(var, config)
+    await light.register_light(var, config)
     # https://github.com/FastLED/FastLED/blob/master/library.json
     # 3.3.3 has an issue on ESP32 with RMT and fastled_clockless:
     # https://github.com/esphome/issues/issues/1375
     cg.add_library("FastLED", "3.3.2")
-    yield var
+    return var
diff --git a/esphome/components/fastled_clockless/light.py b/esphome/components/fastled_clockless/light.py
index b8a36ff390..cfc62e930b 100644
--- a/esphome/components/fastled_clockless/light.py
+++ b/esphome/components/fastled_clockless/light.py
@@ -34,7 +34,7 @@ CHIPSETS = [
 ]
 
 
-def validate(value):
+def _validate(value):
     if value[CONF_CHIPSET] == "NEOPIXEL" and CONF_RGB_ORDER in value:
         raise cv.Invalid("NEOPIXEL doesn't support RGB order")
     return value
@@ -47,12 +47,12 @@ CONFIG_SCHEMA = cv.All(
             cv.Required(CONF_PIN): pins.output_pin,
         }
     ),
-    validate,
+    _validate,
 )
 
 
-def to_code(config):
-    var = yield fastled_base.new_fastled_light(config)
+async def to_code(config):
+    var = await fastled_base.new_fastled_light(config)
 
     rgb_order = None
     if CONF_RGB_ORDER in config:
diff --git a/esphome/components/fastled_spi/light.py b/esphome/components/fastled_spi/light.py
index 11e0a8159c..d6ba0e8358 100644
--- a/esphome/components/fastled_spi/light.py
+++ b/esphome/components/fastled_spi/light.py
@@ -34,8 +34,8 @@ CONFIG_SCHEMA = fastled_base.BASE_SCHEMA.extend(
 )
 
 
-def to_code(config):
-    var = yield fastled_base.new_fastled_light(config)
+async def to_code(config):
+    var = await fastled_base.new_fastled_light(config)
 
     rgb_order = cg.RawExpression(
         config[CONF_RGB_ORDER] if CONF_RGB_ORDER in config else "RGB"
diff --git a/esphome/components/fingerprint_grow/__init__.py b/esphome/components/fingerprint_grow/__init__.py
index 6fbaa4e6c9..757a633e09 100644
--- a/esphome/components/fingerprint_grow/__init__.py
+++ b/esphome/components/fingerprint_grow/__init__.py
@@ -132,45 +132,45 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
     if CONF_PASSWORD in config:
         password = config[CONF_PASSWORD]
         cg.add(var.set_password(password))
-    yield uart.register_uart_device(var, config)
+    await uart.register_uart_device(var, config)
 
     if CONF_NEW_PASSWORD in config:
         new_password = config[CONF_NEW_PASSWORD]
         cg.add(var.set_new_password(new_password))
 
     if CONF_SENSING_PIN in config:
-        sensing_pin = yield cg.gpio_pin_expression(config[CONF_SENSING_PIN])
+        sensing_pin = await cg.gpio_pin_expression(config[CONF_SENSING_PIN])
         cg.add(var.set_sensing_pin(sensing_pin))
 
     for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(
+        await automation.build_automation(
             trigger, [(cg.uint16, "finger_id"), (cg.uint16, "confidence")], conf
         )
 
     for conf in config.get(CONF_ON_FINGER_SCAN_UNMATCHED, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [], conf)
+        await automation.build_automation(trigger, [], conf)
 
     for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(
+        await automation.build_automation(
             trigger, [(cg.uint8, "scan_num"), (cg.uint16, "finger_id")], conf
         )
 
     for conf in config.get(CONF_ON_ENROLLMENT_DONE, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf)
+        await automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf)
 
     for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf)
+        await automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf)
 
 
 @automation.register_action(
@@ -185,16 +185,16 @@ def to_code(config):
         key=CONF_FINGER_ID,
     ),
 )
-def fingerprint_grow_enroll_to_code(config, action_id, template_arg, args):
+async def fingerprint_grow_enroll_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
+    await cg.register_parented(var, config[CONF_ID])
 
-    template_ = yield cg.templatable(config[CONF_FINGER_ID], args, cg.uint16)
+    template_ = await cg.templatable(config[CONF_FINGER_ID], args, cg.uint16)
     cg.add(var.set_finger_id(template_))
     if CONF_NUM_SCANS in config:
-        template_ = yield cg.templatable(config[CONF_NUM_SCANS], args, cg.uint8)
+        template_ = await cg.templatable(config[CONF_NUM_SCANS], args, cg.uint8)
         cg.add(var.set_num_scans(template_))
-    yield var
+    return var
 
 
 @automation.register_action(
@@ -206,10 +206,10 @@ def fingerprint_grow_enroll_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def fingerprint_grow_cancel_enroll_to_code(config, action_id, template_arg, args):
+async def fingerprint_grow_cancel_enroll_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    yield var
+    await cg.register_parented(var, config[CONF_ID])
+    return var
 
 
 @automation.register_action(
@@ -223,13 +223,13 @@ def fingerprint_grow_cancel_enroll_to_code(config, action_id, template_arg, args
         key=CONF_FINGER_ID,
     ),
 )
-def fingerprint_grow_delete_to_code(config, action_id, template_arg, args):
+async def fingerprint_grow_delete_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
+    await cg.register_parented(var, config[CONF_ID])
 
-    template_ = yield cg.templatable(config[CONF_FINGER_ID], args, cg.uint16)
+    template_ = await cg.templatable(config[CONF_FINGER_ID], args, cg.uint16)
     cg.add(var.set_finger_id(template_))
-    yield var
+    return var
 
 
 @automation.register_action(
@@ -241,10 +241,10 @@ def fingerprint_grow_delete_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def fingerprint_grow_delete_all_to_code(config, action_id, template_arg, args):
+async def fingerprint_grow_delete_all_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    yield var
+    await cg.register_parented(var, config[CONF_ID])
+    return var
 
 
 FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA = cv.maybe_simple_value(
@@ -261,13 +261,13 @@ FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA = cv.maybe_simple_value(
     LEDControlAction,
     FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA,
 )
-def fingerprint_grow_led_control_to_code(config, action_id, template_arg, args):
+async def fingerprint_grow_led_control_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
+    await cg.register_parented(var, config[CONF_ID])
 
-    template_ = yield cg.templatable(config[CONF_STATE], args, cg.bool_)
+    template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_)
     cg.add(var.set_state(template_))
-    yield var
+    return var
 
 
 @automation.register_action(
@@ -283,11 +283,13 @@ def fingerprint_grow_led_control_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def fingerprint_grow_aura_led_control_to_code(config, action_id, template_arg, args):
+async def fingerprint_grow_aura_led_control_to_code(
+    config, action_id, template_arg, args
+):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
+    await cg.register_parented(var, config[CONF_ID])
 
     for key in [CONF_STATE, CONF_SPEED, CONF_COLOR, CONF_COUNT]:
-        template_ = yield cg.templatable(config[key], args, cg.uint8)
+        template_ = await cg.templatable(config[key], args, cg.uint8)
         cg.add(getattr(var, f"set_{key}")(template_))
-    yield var
+    return var
diff --git a/esphome/components/fingerprint_grow/binary_sensor.py b/esphome/components/fingerprint_grow/binary_sensor.py
index 4f49841f15..f432ef92cc 100644
--- a/esphome/components/fingerprint_grow/binary_sensor.py
+++ b/esphome/components/fingerprint_grow/binary_sensor.py
@@ -14,7 +14,7 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 )
 
 
-def to_code(config):
-    hub = yield cg.get_variable(config[CONF_FINGERPRINT_GROW_ID])
-    var = yield binary_sensor.new_binary_sensor(config)
+async def to_code(config):
+    hub = await cg.get_variable(config[CONF_FINGERPRINT_GROW_ID])
+    var = await binary_sensor.new_binary_sensor(config)
     cg.add(hub.set_enrolling_binary_sensor(var))
diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp
index 77ddf8ec37..370bdc782b 100644
--- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp
+++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp
@@ -75,6 +75,7 @@ void FingerprintGrowComponent::enroll_fingerprint(uint16_t finger_id, uint8_t nu
 void FingerprintGrowComponent::finish_enrollment(uint8_t result) {
   if (result == OK) {
     this->enrollment_done_callback_.call(this->enrollment_slot_);
+    this->get_fingerprint_count_();
   } else {
     this->enrollment_failed_callback_.call(this->enrollment_slot_);
   }
diff --git a/esphome/components/fingerprint_grow/sensor.py b/esphome/components/fingerprint_grow/sensor.py
index c76c898727..f8a44eb0da 100644
--- a/esphome/components/fingerprint_grow/sensor.py
+++ b/esphome/components/fingerprint_grow/sensor.py
@@ -15,6 +15,7 @@ from esphome.const import (
     ICON_EMPTY,
     ICON_FINGERPRINT,
     ICON_SECURITY,
+    STATE_CLASS_NONE,
     UNIT_EMPTY,
 )
 from . import CONF_FINGERPRINT_GROW_ID, FingerprintGrowComponent
@@ -25,29 +26,29 @@ CONFIG_SCHEMA = cv.Schema(
     {
         cv.GenerateID(CONF_FINGERPRINT_GROW_ID): cv.use_id(FingerprintGrowComponent),
         cv.Optional(CONF_FINGERPRINT_COUNT): sensor.sensor_schema(
-            UNIT_EMPTY, ICON_FINGERPRINT, 0, DEVICE_CLASS_EMPTY
+            UNIT_EMPTY, ICON_FINGERPRINT, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
         ),
         cv.Optional(CONF_STATUS): sensor.sensor_schema(
-            UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY
+            UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
         ),
         cv.Optional(CONF_CAPACITY): sensor.sensor_schema(
-            UNIT_EMPTY, ICON_DATABASE, 0, DEVICE_CLASS_EMPTY
+            UNIT_EMPTY, ICON_DATABASE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
         ),
         cv.Optional(CONF_SECURITY_LEVEL): sensor.sensor_schema(
-            UNIT_EMPTY, ICON_SECURITY, 0, DEVICE_CLASS_EMPTY
+            UNIT_EMPTY, ICON_SECURITY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
         ),
         cv.Optional(CONF_LAST_FINGER_ID): sensor.sensor_schema(
-            UNIT_EMPTY, ICON_ACCOUNT, 0, DEVICE_CLASS_EMPTY
+            UNIT_EMPTY, ICON_ACCOUNT, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
         ),
         cv.Optional(CONF_LAST_CONFIDENCE): sensor.sensor_schema(
-            UNIT_EMPTY, ICON_ACCOUNT_CHECK, 0, DEVICE_CLASS_EMPTY
+            UNIT_EMPTY, ICON_ACCOUNT_CHECK, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
         ),
     }
 )
 
 
-def to_code(config):
-    hub = yield cg.get_variable(config[CONF_FINGERPRINT_GROW_ID])
+async def to_code(config):
+    hub = await cg.get_variable(config[CONF_FINGERPRINT_GROW_ID])
 
     for key in [
         CONF_FINGERPRINT_COUNT,
@@ -60,5 +61,5 @@ def to_code(config):
         if key not in config:
             continue
         conf = config[key]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(getattr(hub, f"set_{key}_sensor")(sens))
diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py
index c414d37c40..11bbedd80b 100644
--- a/esphome/components/font/__init__.py
+++ b/esphome/components/font/__init__.py
@@ -12,6 +12,7 @@ MULTI_CONF = True
 
 Font = display.display_ns.class_("Font")
 Glyph = display.display_ns.class_("Glyph")
+GlyphData = display.display_ns.struct("GlyphData")
 
 
 def validate_glyphs(value):
@@ -75,6 +76,7 @@ DEFAULT_GLYPHS = (
     ' !"%()+,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
 )
 CONF_RAW_DATA_ID = "raw_data_id"
+CONF_RAW_GLYPH_ID = "raw_glyph_id"
 
 FONT_SCHEMA = cv.Schema(
     {
@@ -83,13 +85,14 @@ FONT_SCHEMA = cv.Schema(
         cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs,
         cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1),
         cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
+        cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData),
     }
 )
 
 CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     from PIL import ImageFont
 
     path = CORE.relative_config_path(config[CONF_FILE])
@@ -120,8 +123,25 @@ def to_code(config):
     rhs = [HexInt(x) for x in data]
     prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
 
-    glyphs = []
+    glyph_initializer = []
     for glyph in config[CONF_GLYPHS]:
-        glyphs.append(Glyph(glyph, prog_arr, *glyph_args[glyph]))
+        glyph_initializer.append(
+            cg.StructInitializer(
+                GlyphData,
+                ("a_char", glyph),
+                (
+                    "data",
+                    cg.RawExpression(str(prog_arr) + " + " + str(glyph_args[glyph][0])),
+                ),
+                ("offset_x", glyph_args[glyph][1]),
+                ("offset_y", glyph_args[glyph][2]),
+                ("width", glyph_args[glyph][3]),
+                ("height", glyph_args[glyph][4]),
+            )
+        )
 
-    cg.new_Pvariable(config[CONF_ID], glyphs, ascent, ascent + descent)
+    glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer)
+
+    cg.new_Pvariable(
+        config[CONF_ID], glyphs, len(glyph_initializer), ascent, ascent + descent
+    )
diff --git a/esphome/components/fujitsu_general/climate.py b/esphome/components/fujitsu_general/climate.py
index d58049e3f2..427721f2db 100644
--- a/esphome/components/fujitsu_general/climate.py
+++ b/esphome/components/fujitsu_general/climate.py
@@ -17,6 +17,6 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield climate_ir.register_climate_ir(var, config)
+    await climate_ir.register_climate_ir(var, config)
diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp
index 2676609d9b..8671f38e8e 100644
--- a/esphome/components/fujitsu_general/fujitsu_general.cpp
+++ b/esphome/components/fujitsu_general/fujitsu_general.cpp
@@ -140,7 +140,7 @@ void FujitsuGeneralClimate::transmit_state() {
   }
 
   // Set fan
-  switch (this->fan_mode) {
+  switch (this->fan_mode.value()) {
     case climate::CLIMATE_FAN_HIGH:
       SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_HIGH);
       break;
diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py
index 9ec3bc17ce..9039d0d62e 100644
--- a/esphome/components/globals/__init__.py
+++ b/esphome/components/globals/__init__.py
@@ -29,7 +29,7 @@ CONFIG_SCHEMA = cv.Schema(
 
 # Run with low priority so that namespaces are registered first
 @coroutine_with_priority(-100.0)
-def to_code(config):
+async def to_code(config):
     type_ = cg.RawExpression(config[CONF_TYPE])
     template_args = cg.TemplateArguments(type_)
     res_type = GlobalsComponent.template(template_args)
@@ -40,7 +40,7 @@ def to_code(config):
 
     rhs = GlobalsComponent.new(template_args, initial_value)
     glob = cg.Pvariable(config[CONF_ID], rhs, res_type)
-    yield cg.register_component(glob, config)
+    await cg.register_component(glob, config)
 
     if config[CONF_RESTORE_VALUE]:
         value = config[CONF_ID].id
@@ -60,12 +60,12 @@ def to_code(config):
         }
     ),
 )
-def globals_set_to_code(config, action_id, template_arg, args):
-    full_id, paren = yield cg.get_variable_with_full_id(config[CONF_ID])
+async def globals_set_to_code(config, action_id, template_arg, args):
+    full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
     template_arg = cg.TemplateArguments(full_id.type, *template_arg)
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    templ = yield cg.templatable(
+    templ = await cg.templatable(
         config[CONF_VALUE], args, None, to_exp=cg.RawExpression
     )
     cg.add(var.set_value(templ))
-    yield var
+    return var
diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py
index 4a24efcdb0..4d91b81a44 100644
--- a/esphome/components/gpio/binary_sensor/__init__.py
+++ b/esphome/components/gpio/binary_sensor/__init__.py
@@ -17,10 +17,10 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield binary_sensor.register_binary_sensor(var, config)
+    await cg.register_component(var, config)
+    await binary_sensor.register_binary_sensor(var, config)
 
-    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+    pin = await cg.gpio_pin_expression(config[CONF_PIN])
     cg.add(var.set_pin(pin))
diff --git a/esphome/components/gpio/output/__init__.py b/esphome/components/gpio/output/__init__.py
index d98a490566..2fa9f4dc78 100644
--- a/esphome/components/gpio/output/__init__.py
+++ b/esphome/components/gpio/output/__init__.py
@@ -15,10 +15,10 @@ CONFIG_SCHEMA = output.BINARY_OUTPUT_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield output.register_output(var, config)
-    yield cg.register_component(var, config)
+    await output.register_output(var, config)
+    await cg.register_component(var, config)
 
-    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+    pin = await cg.gpio_pin_expression(config[CONF_PIN])
     cg.add(var.set_pin(pin))
diff --git a/esphome/components/gpio/switch/__init__.py b/esphome/components/gpio/switch/__init__.py
index ebd12d10ca..a03e16a2c1 100644
--- a/esphome/components/gpio/switch/__init__.py
+++ b/esphome/components/gpio/switch/__init__.py
@@ -13,6 +13,8 @@ RESTORE_MODES = {
     "RESTORE_DEFAULT_ON": GPIOSwitchRestoreMode.GPIO_SWITCH_RESTORE_DEFAULT_ON,
     "ALWAYS_OFF": GPIOSwitchRestoreMode.GPIO_SWITCH_ALWAYS_OFF,
     "ALWAYS_ON": GPIOSwitchRestoreMode.GPIO_SWITCH_ALWAYS_ON,
+    "RESTORE_INVERTED_DEFAULT_OFF": GPIOSwitchRestoreMode.GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_OFF,
+    "RESTORE_INVERTED_DEFAULT_ON": GPIOSwitchRestoreMode.GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_ON,
 }
 
 CONF_INTERLOCK_WAIT_TIME = "interlock_wait_time"
@@ -31,12 +33,12 @@ CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield switch.register_switch(var, config)
+    await cg.register_component(var, config)
+    await switch.register_switch(var, config)
 
-    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+    pin = await cg.gpio_pin_expression(config[CONF_PIN])
     cg.add(var.set_pin(pin))
 
     cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))
@@ -44,7 +46,7 @@ def to_code(config):
     if CONF_INTERLOCK in config:
         interlock = []
         for it in config[CONF_INTERLOCK]:
-            lock = yield cg.get_variable(it)
+            lock = await cg.get_variable(it)
             interlock.append(lock)
         cg.add(var.set_interlock(interlock))
         cg.add(var.set_interlock_wait_time(config[CONF_INTERLOCK_WAIT_TIME]))
diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp
index d87e5a61e6..2475d43e93 100644
--- a/esphome/components/gpio/switch/gpio_switch.cpp
+++ b/esphome/components/gpio/switch/gpio_switch.cpp
@@ -18,6 +18,12 @@ void GPIOSwitch::setup() {
     case GPIO_SWITCH_RESTORE_DEFAULT_ON:
       initial_state = this->get_initial_state().value_or(true);
       break;
+    case GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_OFF:
+      initial_state = !this->get_initial_state().value_or(true);
+      break;
+    case GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_ON:
+      initial_state = !this->get_initial_state().value_or(false);
+      break;
     case GPIO_SWITCH_ALWAYS_OFF:
       initial_state = false;
       break;
@@ -49,6 +55,12 @@ void GPIOSwitch::dump_config() {
     case GPIO_SWITCH_RESTORE_DEFAULT_ON:
       restore_mode = "Restore (Defaults to ON)";
       break;
+    case GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_ON:
+      restore_mode = "Restore inverted (Defaults to ON)";
+      break;
+    case GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_OFF:
+      restore_mode = "Restore inverted (Defaults to OFF)";
+      break;
     case GPIO_SWITCH_ALWAYS_OFF:
       restore_mode = "Always OFF";
       break;
diff --git a/esphome/components/gpio/switch/gpio_switch.h b/esphome/components/gpio/switch/gpio_switch.h
index dc0dd9bc95..c0036b20e9 100644
--- a/esphome/components/gpio/switch/gpio_switch.h
+++ b/esphome/components/gpio/switch/gpio_switch.h
@@ -11,6 +11,8 @@ enum GPIOSwitchRestoreMode {
   GPIO_SWITCH_RESTORE_DEFAULT_ON,
   GPIO_SWITCH_ALWAYS_OFF,
   GPIO_SWITCH_ALWAYS_ON,
+  GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_OFF,
+  GPIO_SWITCH_RESTORE_INVERTED_DEFAULT_ON,
 };
 
 class GPIOSwitch : public switch_::Switch, public Component {
diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py
index c09a49315c..de3eae1115 100644
--- a/esphome/components/gps/__init__.py
+++ b/esphome/components/gps/__init__.py
@@ -10,6 +10,8 @@ from esphome.const import (
     CONF_COURSE,
     CONF_ALTITUDE,
     CONF_SATELLITES,
+    STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_NONE,
     UNIT_DEGREES,
     UNIT_KILOMETER_PER_HOUR,
     UNIT_METER,
@@ -34,22 +36,26 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(GPS),
             cv.Optional(CONF_LATITUDE): sensor.sensor_schema(
-                UNIT_DEGREES, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY
+                UNIT_DEGREES, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
             ),
             cv.Optional(CONF_LONGITUDE): sensor.sensor_schema(
-                UNIT_DEGREES, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY
+                UNIT_DEGREES, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
             ),
             cv.Optional(CONF_SPEED): sensor.sensor_schema(
-                UNIT_KILOMETER_PER_HOUR, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY
+                UNIT_KILOMETER_PER_HOUR,
+                ICON_EMPTY,
+                6,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_NONE,
             ),
             cv.Optional(CONF_COURSE): sensor.sensor_schema(
-                UNIT_DEGREES, ICON_EMPTY, 2, DEVICE_CLASS_EMPTY
+                UNIT_DEGREES, ICON_EMPTY, 2, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
             ),
             cv.Optional(CONF_ALTITUDE): sensor.sensor_schema(
-                UNIT_METER, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY
+                UNIT_METER, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
             ),
             cv.Optional(CONF_SATELLITES): sensor.sensor_schema(
-                UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY
+                UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
             ),
         }
     )
@@ -58,34 +64,38 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield uart.register_uart_device(var, config)
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
 
     if CONF_LATITUDE in config:
-        sens = yield sensor.new_sensor(config[CONF_LATITUDE])
+        sens = await sensor.new_sensor(config[CONF_LATITUDE])
         cg.add(var.set_latitude_sensor(sens))
 
     if CONF_LONGITUDE in config:
-        sens = yield sensor.new_sensor(config[CONF_LONGITUDE])
+        sens = await sensor.new_sensor(config[CONF_LONGITUDE])
         cg.add(var.set_longitude_sensor(sens))
 
     if CONF_SPEED in config:
-        sens = yield sensor.new_sensor(config[CONF_SPEED])
+        sens = await sensor.new_sensor(config[CONF_SPEED])
         cg.add(var.set_speed_sensor(sens))
 
     if CONF_COURSE in config:
-        sens = yield sensor.new_sensor(config[CONF_COURSE])
+        sens = await sensor.new_sensor(config[CONF_COURSE])
         cg.add(var.set_course_sensor(sens))
 
     if CONF_ALTITUDE in config:
-        sens = yield sensor.new_sensor(config[CONF_ALTITUDE])
+        sens = await sensor.new_sensor(config[CONF_ALTITUDE])
         cg.add(var.set_altitude_sensor(sens))
 
     if CONF_SATELLITES in config:
-        sens = yield sensor.new_sensor(config[CONF_SATELLITES])
+        sens = await sensor.new_sensor(config[CONF_SATELLITES])
         cg.add(var.set_satellites_sensor(sens))
 
     # https://platformio.org/lib/show/1655/TinyGPSPlus
     cg.add_library("1655", "1.0.2")  # TinyGPSPlus, has name conflict
+
+
+def validate(config, item_config):
+    uart.validate_device("gps", config, item_config, require_tx=False)
diff --git a/esphome/components/gps/time/__init__.py b/esphome/components/gps/time/__init__.py
index 24ea263f6c..1dae22a2b2 100644
--- a/esphome/components/gps/time/__init__.py
+++ b/esphome/components/gps/time/__init__.py
@@ -18,10 +18,10 @@ CONFIG_SCHEMA = time_.TIME_SCHEMA.extend(
 ).extend(cv.polling_component_schema("5min"))
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield time_.register_time(var, config)
-    yield cg.register_component(var, config)
+    await time_.register_time(var, config)
+    await cg.register_component(var, config)
 
-    paren = yield cg.get_variable(config[CONF_GPS_ID])
+    paren = await cg.get_variable(config[CONF_GPS_ID])
     cg.add(paren.register_listener(var))
diff --git a/esphome/components/hbridge/light.py b/esphome/components/hbridge/light.py
index 6c695fbd4a..b4ae45977a 100644
--- a/esphome/components/hbridge/light.py
+++ b/esphome/components/hbridge/light.py
@@ -17,12 +17,12 @@ CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
-    yield cg.register_component(var, config)
-    yield light.register_light(var, config)
+    await cg.register_component(var, config)
+    await light.register_light(var, config)
 
-    hside = yield cg.get_variable(config[CONF_PIN_A])
+    hside = await cg.get_variable(config[CONF_PIN_A])
     cg.add(var.set_pina_pin(hside))
-    lside = yield cg.get_variable(config[CONF_PIN_B])
+    lside = await cg.get_variable(config[CONF_PIN_B])
     cg.add(var.set_pinb_pin(lside))
diff --git a/esphome/components/hdc1080/sensor.py b/esphome/components/hdc1080/sensor.py
index 15a92cccf2..26ec3ad0a9 100644
--- a/esphome/components/hdc1080/sensor.py
+++ b/esphome/components/hdc1080/sensor.py
@@ -8,6 +8,7 @@ from esphome.const import (
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
 )
@@ -24,10 +25,18 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(HDC1080Component),
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -36,15 +45,15 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
 
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity(sens))
diff --git a/esphome/components/hitachi_ac344/climate.py b/esphome/components/hitachi_ac344/climate.py
index 9ae0cd39de..94b34eb955 100644
--- a/esphome/components/hitachi_ac344/climate.py
+++ b/esphome/components/hitachi_ac344/climate.py
@@ -15,6 +15,6 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield climate_ir.register_climate_ir(var, config)
+    await climate_ir.register_climate_ir(var, config)
diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.cpp b/esphome/components/hitachi_ac344/hitachi_ac344.cpp
index 8d56c7f51c..b2798b608a 100644
--- a/esphome/components/hitachi_ac344/hitachi_ac344.cpp
+++ b/esphome/components/hitachi_ac344/hitachi_ac344.cpp
@@ -164,11 +164,13 @@ void HitachiClimate::transmit_state() {
     case climate::CLIMATE_MODE_OFF:
       set_power_(false);
       break;
+    default:
+      ESP_LOGW(TAG, "Unsupported mode: %s", climate_mode_to_string(this->mode));
   }
 
   set_temp_(static_cast<uint8_t>(this->target_temperature));
 
-  switch (this->fan_mode) {
+  switch (this->fan_mode.value()) {
     case climate::CLIMATE_FAN_LOW:
       set_fan_(HITACHI_AC344_FAN_LOW);
       break;
diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py
index 28aa24d7cd..6454a9fcc9 100644
--- a/esphome/components/hlw8012/sensor.py
+++ b/esphome/components/hlw8012/sensor.py
@@ -18,6 +18,8 @@ from esphome.const import (
     DEVICE_CLASS_POWER,
     DEVICE_CLASS_VOLTAGE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_NONE,
     UNIT_VOLT,
     UNIT_AMPERE,
     UNIT_WATT,
@@ -47,16 +49,16 @@ CONFIG_SCHEMA = cv.Schema(
             pins.internal_gpio_input_pullup_pin_schema, pins.validate_has_interrupt
         ),
         cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
-            UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE
+            UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional(CONF_CURRENT): sensor.sensor_schema(
-            UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT
+            UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional(CONF_POWER): sensor.sensor_schema(
-            UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER
+            UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional(CONF_ENERGY): sensor.sensor_schema(
-            UNIT_WATT_HOURS, ICON_EMPTY, 1, DEVICE_CLASS_ENERGY
+            UNIT_WATT_HOURS, ICON_EMPTY, 1, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE
         ),
         cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance,
         cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float,
@@ -70,28 +72,28 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(cv.polling_component_schema("60s"))
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
-    sel = yield cg.gpio_pin_expression(config[CONF_SEL_PIN])
+    sel = await cg.gpio_pin_expression(config[CONF_SEL_PIN])
     cg.add(var.set_sel_pin(sel))
-    cf = yield cg.gpio_pin_expression(config[CONF_CF_PIN])
+    cf = await cg.gpio_pin_expression(config[CONF_CF_PIN])
     cg.add(var.set_cf_pin(cf))
-    cf1 = yield cg.gpio_pin_expression(config[CONF_CF1_PIN])
+    cf1 = await cg.gpio_pin_expression(config[CONF_CF1_PIN])
     cg.add(var.set_cf1_pin(cf1))
 
     if CONF_VOLTAGE in config:
-        sens = yield sensor.new_sensor(config[CONF_VOLTAGE])
+        sens = await sensor.new_sensor(config[CONF_VOLTAGE])
         cg.add(var.set_voltage_sensor(sens))
     if CONF_CURRENT in config:
-        sens = yield sensor.new_sensor(config[CONF_CURRENT])
+        sens = await sensor.new_sensor(config[CONF_CURRENT])
         cg.add(var.set_current_sensor(sens))
     if CONF_POWER in config:
-        sens = yield sensor.new_sensor(config[CONF_POWER])
+        sens = await sensor.new_sensor(config[CONF_POWER])
         cg.add(var.set_power_sensor(sens))
     if CONF_ENERGY in config:
-        sens = yield sensor.new_sensor(config[CONF_ENERGY])
+        sens = await sensor.new_sensor(config[CONF_ENERGY])
         cg.add(var.set_energy_sensor(sens))
     cg.add(var.set_current_resistor(config[CONF_CURRENT_RESISTOR]))
     cg.add(var.set_voltage_divider(config[CONF_VOLTAGE_DIVIDER]))
diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py
index 14ca2e77ba..48a29ed5f8 100644
--- a/esphome/components/hm3301/sensor.py
+++ b/esphome/components/hm3301/sensor.py
@@ -7,6 +7,7 @@ from esphome.const import (
     CONF_PM_10_0,
     CONF_PM_1_0,
     DEVICE_CLASS_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_MICROGRAMS_PER_CUBIC_METER,
     ICON_CHEMICAL_WEAPON,
 )
@@ -29,7 +30,7 @@ AQI_CALCULATION_TYPE = {
 }
 
 
-def validate(config):
+def _validate(config):
     if CONF_AQI in config and CONF_PM_2_5 not in config:
         raise cv.Invalid("AQI sensor requires PM 2.5")
     if CONF_AQI in config and CONF_PM_10_0 not in config:
@@ -46,21 +47,28 @@ CONFIG_SCHEMA = cv.All(
                 ICON_CHEMICAL_WEAPON,
                 0,
                 DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
                 UNIT_MICROGRAMS_PER_CUBIC_METER,
                 ICON_CHEMICAL_WEAPON,
                 0,
                 DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
                 UNIT_MICROGRAMS_PER_CUBIC_METER,
                 ICON_CHEMICAL_WEAPON,
                 0,
                 DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_AQI): sensor.sensor_schema(
-                UNIT_INDEX, ICON_CHEMICAL_WEAPON, 0, DEVICE_CLASS_EMPTY
+                UNIT_INDEX,
+                ICON_CHEMICAL_WEAPON,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ).extend(
                 {
                     cv.Required(CONF_CALCULATION_TYPE): cv.enum(
@@ -72,29 +80,29 @@ CONFIG_SCHEMA = cv.All(
     )
     .extend(cv.polling_component_schema("60s"))
     .extend(i2c.i2c_device_schema(0x40)),
-    validate,
+    _validate,
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     if CONF_PM_1_0 in config:
-        sens = yield sensor.new_sensor(config[CONF_PM_1_0])
+        sens = await sensor.new_sensor(config[CONF_PM_1_0])
         cg.add(var.set_pm_1_0_sensor(sens))
 
     if CONF_PM_2_5 in config:
-        sens = yield sensor.new_sensor(config[CONF_PM_2_5])
+        sens = await sensor.new_sensor(config[CONF_PM_2_5])
         cg.add(var.set_pm_2_5_sensor(sens))
 
     if CONF_PM_10_0 in config:
-        sens = yield sensor.new_sensor(config[CONF_PM_10_0])
+        sens = await sensor.new_sensor(config[CONF_PM_10_0])
         cg.add(var.set_pm_10_0_sensor(sens))
 
     if CONF_AQI in config:
-        sens = yield sensor.new_sensor(config[CONF_AQI])
+        sens = await sensor.new_sensor(config[CONF_AQI])
         cg.add(var.set_aqi_sensor(sens))
         cg.add(var.set_aqi_calculation_type(config[CONF_AQI][CONF_CALCULATION_TYPE]))
 
diff --git a/esphome/components/hmc5883l/sensor.py b/esphome/components/hmc5883l/sensor.py
index d057caf030..65469003ed 100644
--- a/esphome/components/hmc5883l/sensor.py
+++ b/esphome/components/hmc5883l/sensor.py
@@ -8,6 +8,8 @@ from esphome.const import (
     CONF_RANGE,
     DEVICE_CLASS_EMPTY,
     ICON_MAGNET,
+    STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_NONE,
     UNIT_MICROTESLA,
     UNIT_DEGREES,
     ICON_SCREEN_ROTATION,
@@ -78,10 +80,10 @@ def validate_enum(enum_values, units=None, int=True):
 
 
 field_strength_schema = sensor.sensor_schema(
-    UNIT_MICROTESLA, ICON_MAGNET, 1, DEVICE_CLASS_EMPTY
+    UNIT_MICROTESLA, ICON_MAGNET, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
 )
 heading_schema = sensor.sensor_schema(
-    UNIT_DEGREES, ICON_SCREEN_ROTATION, 1, DEVICE_CLASS_EMPTY
+    UNIT_DEGREES, ICON_SCREEN_ROTATION, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
 )
 
 CONFIG_SCHEMA = (
@@ -115,23 +117,23 @@ def auto_data_rate(config):
     return HMC5883LDatarates[75]
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     cg.add(var.set_oversampling(config[CONF_OVERSAMPLING]))
     cg.add(var.set_datarate(auto_data_rate(config)))
     cg.add(var.set_range(config[CONF_RANGE]))
     if CONF_FIELD_STRENGTH_X in config:
-        sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_X])
+        sens = await sensor.new_sensor(config[CONF_FIELD_STRENGTH_X])
         cg.add(var.set_x_sensor(sens))
     if CONF_FIELD_STRENGTH_Y in config:
-        sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_Y])
+        sens = await sensor.new_sensor(config[CONF_FIELD_STRENGTH_Y])
         cg.add(var.set_y_sensor(sens))
     if CONF_FIELD_STRENGTH_Z in config:
-        sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_Z])
+        sens = await sensor.new_sensor(config[CONF_FIELD_STRENGTH_Z])
         cg.add(var.set_z_sensor(sens))
     if CONF_HEADING in config:
-        sens = yield sensor.new_sensor(config[CONF_HEADING])
+        sens = await sensor.new_sensor(config[CONF_HEADING])
         cg.add(var.set_heading_sensor(sens))
diff --git a/esphome/components/homeassistant/binary_sensor/__init__.py b/esphome/components/homeassistant/binary_sensor/__init__.py
index 850f239d6f..4972466aac 100644
--- a/esphome/components/homeassistant/binary_sensor/__init__.py
+++ b/esphome/components/homeassistant/binary_sensor/__init__.py
@@ -1,7 +1,7 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import binary_sensor
-from esphome.const import CONF_ENTITY_ID, CONF_ID
+from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_ID
 from .. import homeassistant_ns
 
 DEPENDENCIES = ["api"]
@@ -13,13 +13,16 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
     {
         cv.GenerateID(): cv.declare_id(HomeassistantBinarySensor),
         cv.Required(CONF_ENTITY_ID): cv.entity_id,
+        cv.Optional(CONF_ATTRIBUTE): cv.string,
     }
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield binary_sensor.register_binary_sensor(var, config)
+    await cg.register_component(var, config)
+    await binary_sensor.register_binary_sensor(var, config)
 
     cg.add(var.set_entity_id(config[CONF_ENTITY_ID]))
+    if CONF_ATTRIBUTE in config:
+        cg.add(var.set_attribute(config[CONF_ATTRIBUTE]))
diff --git a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp
index 203f6d8a24..cf2415d9c0 100644
--- a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp
+++ b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp
@@ -8,7 +8,7 @@ namespace homeassistant {
 static const char *TAG = "homeassistant.binary_sensor";
 
 void HomeassistantBinarySensor::setup() {
-  api::global_api_server->subscribe_home_assistant_state(this->entity_id_, [this](std::string state) {
+  api::global_api_server->subscribe_home_assistant_state(this->entity_id_, this->attribute_, [this](std::string state) {
     auto val = parse_on_off(state.c_str());
     switch (val) {
       case PARSE_NONE:
@@ -18,7 +18,12 @@ void HomeassistantBinarySensor::setup() {
       case PARSE_ON:
       case PARSE_OFF:
         bool new_state = val == PARSE_ON;
-        ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state));
+        if (this->attribute_.has_value()) {
+          ESP_LOGD(TAG, "'%s::%s': Got attribute state %s", this->entity_id_.c_str(), this->attribute_.value().c_str(),
+                   ONOFF(new_state));
+        } else {
+          ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state));
+        }
         if (this->initial_)
           this->publish_initial_state(new_state);
         else
@@ -31,6 +36,9 @@ void HomeassistantBinarySensor::setup() {
 void HomeassistantBinarySensor::dump_config() {
   LOG_BINARY_SENSOR("", "Homeassistant Binary Sensor", this);
   ESP_LOGCONFIG(TAG, "  Entity ID: '%s'", this->entity_id_.c_str());
+  if (this->attribute_.has_value()) {
+    ESP_LOGCONFIG(TAG, "  Attribute: '%s'", this->attribute_.value().c_str());
+  }
 }
 float HomeassistantBinarySensor::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
 
diff --git a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h
index e468fd00eb..7026496295 100644
--- a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h
+++ b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h
@@ -9,12 +9,14 @@ namespace homeassistant {
 class HomeassistantBinarySensor : public binary_sensor::BinarySensor, public Component {
  public:
   void set_entity_id(const std::string &entity_id) { entity_id_ = entity_id; }
+  void set_attribute(const std::string &attribute) { attribute_ = attribute; }
   void setup() override;
   void dump_config() override;
   float get_setup_priority() const override;
 
  protected:
   std::string entity_id_;
+  optional<std::string> attribute_;
   bool initial_{true};
 };
 
diff --git a/esphome/components/homeassistant/sensor/__init__.py b/esphome/components/homeassistant/sensor/__init__.py
index 9c19c7867f..0dadb78b73 100644
--- a/esphome/components/homeassistant/sensor/__init__.py
+++ b/esphome/components/homeassistant/sensor/__init__.py
@@ -2,9 +2,11 @@ import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import sensor
 from esphome.const import (
+    CONF_ATTRIBUTE,
     CONF_ENTITY_ID,
     CONF_ID,
     ICON_EMPTY,
+    STATE_CLASS_NONE,
     UNIT_EMPTY,
     DEVICE_CLASS_EMPTY,
 )
@@ -17,18 +19,21 @@ HomeassistantSensor = homeassistant_ns.class_(
 )
 
 CONFIG_SCHEMA = sensor.sensor_schema(
-    UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY
+    UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
 ).extend(
     {
         cv.GenerateID(): cv.declare_id(HomeassistantSensor),
         cv.Required(CONF_ENTITY_ID): cv.entity_id,
+        cv.Optional(CONF_ATTRIBUTE): cv.string,
     }
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
 
     cg.add(var.set_entity_id(config[CONF_ENTITY_ID]))
+    if CONF_ATTRIBUTE in config:
+        cg.add(var.set_attribute(config[CONF_ATTRIBUTE]))
diff --git a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp
index 6b1299f70e..5f7916403e 100644
--- a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp
+++ b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp
@@ -8,7 +8,7 @@ namespace homeassistant {
 static const char *TAG = "homeassistant.sensor";
 
 void HomeassistantSensor::setup() {
-  api::global_api_server->subscribe_home_assistant_state(this->entity_id_, [this](std::string state) {
+  api::global_api_server->subscribe_home_assistant_state(this->entity_id_, this->attribute_, [this](std::string state) {
     auto val = parse_float(state);
     if (!val.has_value()) {
       ESP_LOGW(TAG, "Can't convert '%s' to number!", state.c_str());
@@ -16,13 +16,21 @@ void HomeassistantSensor::setup() {
       return;
     }
 
-    ESP_LOGD(TAG, "'%s': Got state %.2f", this->entity_id_.c_str(), *val);
+    if (this->attribute_.has_value()) {
+      ESP_LOGD(TAG, "'%s::%s': Got attribute state %.2f", this->entity_id_.c_str(), this->attribute_.value().c_str(),
+               *val);
+    } else {
+      ESP_LOGD(TAG, "'%s': Got state %.2f", this->entity_id_.c_str(), *val);
+    }
     this->publish_state(*val);
   });
 }
 void HomeassistantSensor::dump_config() {
   LOG_SENSOR("", "Homeassistant Sensor", this);
   ESP_LOGCONFIG(TAG, "  Entity ID: '%s'", this->entity_id_.c_str());
+  if (this->attribute_.has_value()) {
+    ESP_LOGCONFIG(TAG, "  Attribute: '%s'", this->attribute_.value().c_str());
+  }
 }
 float HomeassistantSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
 
diff --git a/esphome/components/homeassistant/sensor/homeassistant_sensor.h b/esphome/components/homeassistant/sensor/homeassistant_sensor.h
index baca6594c1..53b288d7d4 100644
--- a/esphome/components/homeassistant/sensor/homeassistant_sensor.h
+++ b/esphome/components/homeassistant/sensor/homeassistant_sensor.h
@@ -9,12 +9,14 @@ namespace homeassistant {
 class HomeassistantSensor : public sensor::Sensor, public Component {
  public:
   void set_entity_id(const std::string &entity_id) { entity_id_ = entity_id; }
+  void set_attribute(const std::string &attribute) { attribute_ = attribute; }
   void setup() override;
   void dump_config() override;
   float get_setup_priority() const override;
 
  protected:
   std::string entity_id_;
+  optional<std::string> attribute_;
 };
 
 }  // namespace homeassistant
diff --git a/esphome/components/homeassistant/text_sensor/__init__.py b/esphome/components/homeassistant/text_sensor/__init__.py
index 478bd1c3d2..b63d45b9ce 100644
--- a/esphome/components/homeassistant/text_sensor/__init__.py
+++ b/esphome/components/homeassistant/text_sensor/__init__.py
@@ -1,7 +1,7 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import text_sensor
-from esphome.const import CONF_ENTITY_ID, CONF_ID
+from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_ID
 from .. import homeassistant_ns
 
 DEPENDENCIES = ["api"]
@@ -14,13 +14,16 @@ CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend(
     {
         cv.GenerateID(): cv.declare_id(HomeassistantTextSensor),
         cv.Required(CONF_ENTITY_ID): cv.entity_id,
+        cv.Optional(CONF_ATTRIBUTE): cv.string,
     }
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield text_sensor.register_text_sensor(var, config)
+    await cg.register_component(var, config)
+    await text_sensor.register_text_sensor(var, config)
 
     cg.add(var.set_entity_id(config[CONF_ENTITY_ID]))
+    if CONF_ATTRIBUTE in config:
+        cg.add(var.set_attribute(config[CONF_ATTRIBUTE]))
diff --git a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp
index 67fbf3cd5d..07b06cad3c 100644
--- a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp
+++ b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp
@@ -7,16 +7,24 @@ namespace homeassistant {
 
 static const char *TAG = "homeassistant.text_sensor";
 
-void HomeassistantTextSensor::dump_config() {
-  LOG_TEXT_SENSOR("", "Homeassistant Text Sensor", this);
-  ESP_LOGCONFIG(TAG, "  Entity ID: '%s'", this->entity_id_.c_str());
-}
 void HomeassistantTextSensor::setup() {
-  api::global_api_server->subscribe_home_assistant_state(this->entity_id_, [this](std::string state) {
-    ESP_LOGD(TAG, "'%s': Got state '%s'", this->entity_id_.c_str(), state.c_str());
+  api::global_api_server->subscribe_home_assistant_state(this->entity_id_, this->attribute_, [this](std::string state) {
+    if (this->attribute_.has_value()) {
+      ESP_LOGD(TAG, "'%s::%s': Got attribute state '%s'", this->entity_id_.c_str(), this->attribute_.value().c_str(),
+               state.c_str());
+    } else {
+      ESP_LOGD(TAG, "'%s': Got state '%s'", this->entity_id_.c_str(), state.c_str());
+    }
     this->publish_state(state);
   });
 }
-
+void HomeassistantTextSensor::dump_config() {
+  LOG_TEXT_SENSOR("", "Homeassistant Text Sensor", this);
+  ESP_LOGCONFIG(TAG, "  Entity ID: '%s'", this->entity_id_.c_str());
+  if (this->attribute_.has_value()) {
+    ESP_LOGCONFIG(TAG, "  Attribute: '%s'", this->attribute_.value().c_str());
+  }
+}
+float HomeassistantTextSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
 }  // namespace homeassistant
 }  // namespace esphome
diff --git a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h
index 02a74af1db..ce6b2c2c3f 100644
--- a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h
+++ b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h
@@ -9,11 +9,14 @@ namespace homeassistant {
 class HomeassistantTextSensor : public text_sensor::TextSensor, public Component {
  public:
   void set_entity_id(const std::string &entity_id) { entity_id_ = entity_id; }
-  void dump_config() override;
+  void set_attribute(const std::string &attribute) { attribute_ = attribute; }
   void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override;
 
  protected:
   std::string entity_id_;
+  optional<std::string> attribute_;
 };
 
 }  // namespace homeassistant
diff --git a/esphome/components/homeassistant/time/__init__.py b/esphome/components/homeassistant/time/__init__.py
index e995421302..0040988794 100644
--- a/esphome/components/homeassistant/time/__init__.py
+++ b/esphome/components/homeassistant/time/__init__.py
@@ -15,8 +15,8 @@ CONFIG_SCHEMA = time_.TIME_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield time_.register_time(var, config)
-    yield cg.register_component(var, config)
+    await time_.register_time(var, config)
+    await cg.register_component(var, config)
     cg.add_define("USE_HOMEASSISTANT_TIME")
diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py
index ef664a9d35..650602150a 100644
--- a/esphome/components/http_request/__init__.py
+++ b/esphome/components/http_request/__init__.py
@@ -107,11 +107,11 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     cg.add(var.set_timeout(config[CONF_TIMEOUT]))
     cg.add(var.set_useragent(config[CONF_USERAGENT]))
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
 
 HTTP_REQUEST_ACTION_SCHEMA = cv.Schema(
@@ -171,28 +171,28 @@ HTTP_REQUEST_SEND_ACTION_SCHEMA = HTTP_REQUEST_ACTION_SCHEMA.extend(
 @automation.register_action(
     "http_request.send", HttpRequestSendAction, HTTP_REQUEST_SEND_ACTION_SCHEMA
 )
-def http_request_action_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def http_request_action_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
 
-    template_ = yield cg.templatable(config[CONF_URL], args, cg.std_string)
+    template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
     cg.add(var.set_url(template_))
     cg.add(var.set_method(config[CONF_METHOD]))
     if CONF_BODY in config:
-        template_ = yield cg.templatable(config[CONF_BODY], args, cg.std_string)
+        template_ = await cg.templatable(config[CONF_BODY], args, cg.std_string)
         cg.add(var.set_body(template_))
     if CONF_JSON in config:
         json_ = config[CONF_JSON]
         if isinstance(json_, Lambda):
             args_ = args + [(cg.JsonObjectRef, "root")]
-            lambda_ = yield cg.process_lambda(json_, args_, return_type=cg.void)
+            lambda_ = await cg.process_lambda(json_, args_, return_type=cg.void)
             cg.add(var.set_json(lambda_))
         else:
             for key in json_:
-                template_ = yield cg.templatable(json_[key], args, cg.std_string)
+                template_ = await cg.templatable(json_[key], args, cg.std_string)
                 cg.add(var.add_json(key, template_))
     for key in config.get(CONF_HEADERS, []):
-        template_ = yield cg.templatable(
+        template_ = await cg.templatable(
             config[CONF_HEADERS][key], args, cg.const_char_ptr
         )
         cg.add(var.add_header(key, template_))
@@ -200,6 +200,6 @@ 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))
-        yield automation.build_automation(trigger, [(int, "status_code")], conf)
+        await automation.build_automation(trigger, [(int, "status_code")], conf)
 
-    yield var
+    return var
diff --git a/esphome/components/htu21d/sensor.py b/esphome/components/htu21d/sensor.py
index 258681a5aa..435c5bf1bb 100644
--- a/esphome/components/htu21d/sensor.py
+++ b/esphome/components/htu21d/sensor.py
@@ -8,6 +8,7 @@ from esphome.const import (
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
 )
@@ -24,10 +25,18 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(HTU21DComponent),
             cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Required(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -36,15 +45,15 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
 
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity(sens))
diff --git a/esphome/components/hx711/sensor.py b/esphome/components/hx711/sensor.py
index 191a7386e6..17a4e35d5f 100644
--- a/esphome/components/hx711/sensor.py
+++ b/esphome/components/hx711/sensor.py
@@ -8,6 +8,7 @@ from esphome.const import (
     CONF_ID,
     DEVICE_CLASS_EMPTY,
     ICON_SCALE,
+    STATE_CLASS_MEASUREMENT,
     UNIT_EMPTY,
 )
 
@@ -24,7 +25,9 @@ GAINS = {
 }
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_EMPTY, ICON_SCALE, 0, DEVICE_CLASS_EMPTY)
+    sensor.sensor_schema(
+        UNIT_EMPTY, ICON_SCALE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(HX711Sensor),
@@ -37,13 +40,13 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
 
-    dout_pin = yield cg.gpio_pin_expression(config[CONF_DOUT_PIN])
+    dout_pin = await cg.gpio_pin_expression(config[CONF_DOUT_PIN])
     cg.add(var.set_dout_pin(dout_pin))
-    sck_pin = yield cg.gpio_pin_expression(config[CONF_CLK_PIN])
+    sck_pin = await cg.gpio_pin_expression(config[CONF_CLK_PIN])
     cg.add(var.set_sck_pin(sck_pin))
     cg.add(var.set_gain(config[CONF_GAIN]))
diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py
index 59f90842e1..f43729066d 100644
--- a/esphome/components/i2c/__init__.py
+++ b/esphome/components/i2c/__init__.py
@@ -12,7 +12,7 @@ from esphome.const import (
     CONF_I2C_ID,
     CONF_MULTIPLEXER,
 )
-from esphome.core import coroutine, coroutine_with_priority
+from esphome.core import coroutine_with_priority
 
 CODEOWNERS = ["@esphome/core"]
 i2c_ns = cg.esphome_ns.namespace("i2c")
@@ -42,10 +42,10 @@ I2CMULTIPLEXER_SCHEMA = cv.Schema(
 
 
 @coroutine_with_priority(1.0)
-def to_code(config):
+async def to_code(config):
     cg.add_global(i2c_ns.using)
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
     cg.add(var.set_sda_pin(config[CONF_SDA]))
     cg.add(var.set_scl_pin(config[CONF_SCL]))
@@ -72,19 +72,18 @@ def i2c_device_schema(default_address):
     return cv.Schema(schema)
 
 
-@coroutine
-def register_i2c_device(var, config):
+async def register_i2c_device(var, config):
     """Register an i2c device with the given config.
 
     Sets the i2c bus to use and the i2c address.
 
     This is a coroutine, you need to await it with a 'yield' expression!
     """
-    parent = yield cg.get_variable(config[CONF_I2C_ID])
+    parent = await cg.get_variable(config[CONF_I2C_ID])
     cg.add(var.set_i2c_parent(parent))
     cg.add(var.set_i2c_address(config[CONF_ADDRESS]))
     if CONF_MULTIPLEXER in config:
-        multiplexer = yield cg.get_variable(config[CONF_MULTIPLEXER][CONF_ID])
+        multiplexer = await cg.get_variable(config[CONF_MULTIPLEXER][CONF_ID])
         cg.add(
             var.set_i2c_multiplexer(multiplexer, config[CONF_MULTIPLEXER][CONF_CHANNEL])
         )
diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp
index e9a7e72932..2111711264 100644
--- a/esphome/components/i2c/i2c.cpp
+++ b/esphome/components/i2c/i2c.cpp
@@ -193,12 +193,36 @@ void I2CDevice::check_multiplexer_() {
 }
 #endif
 
+void I2CDevice::raw_begin_transmission() {  // NOLINT
+#ifdef USE_I2C_MULTIPLEXER
+  this->check_multiplexer_();
+#endif
+  this->parent_->raw_begin_transmission(this->address_);
+}
+bool I2CDevice::raw_end_transmission(bool send_stop) {  // NOLINT
+#ifdef USE_I2C_MULTIPLEXER
+  this->check_multiplexer_();
+#endif
+  return this->parent_->raw_end_transmission(this->address_, send_stop);
+}
+void I2CDevice::raw_write(const uint8_t *data, uint8_t len) {  // NOLINT
+#ifdef USE_I2C_MULTIPLEXER
+  this->check_multiplexer_();
+#endif
+  this->parent_->raw_write(this->address_, data, len);
+}
 bool I2CDevice::read_bytes(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion) {  // NOLINT
 #ifdef USE_I2C_MULTIPLEXER
   this->check_multiplexer_();
 #endif
   return this->parent_->read_bytes(this->address_, a_register, data, len, conversion);
 }
+bool I2CDevice::read_bytes_raw(uint8_t *data, uint8_t len) {  // NOLINT
+#ifdef USE_I2C_MULTIPLEXER
+  this->check_multiplexer_();
+#endif
+  return this->parent_->read_bytes_raw(this->address_, data, len);
+}
 bool I2CDevice::read_byte(uint8_t a_register, uint8_t *data, uint32_t conversion) {  // NOLINT
 #ifdef USE_I2C_MULTIPLEXER
   this->check_multiplexer_();
@@ -211,6 +235,12 @@ bool I2CDevice::write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len
 #endif
   return this->parent_->write_bytes(this->address_, a_register, data, len);
 }
+bool I2CDevice::write_bytes_raw(const uint8_t *data, uint8_t len) {  // NOLINT
+#ifdef USE_I2C_MULTIPLEXER
+  this->check_multiplexer_();
+#endif
+  return this->parent_->write_bytes_raw(this->address_, data, len);
+}
 bool I2CDevice::write_byte(uint8_t a_register, uint8_t data) {  // NOLINT
 #ifdef USE_I2C_MULTIPLEXER
   this->check_multiplexer_();
diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h
index 56da64c218..da791ec633 100644
--- a/esphome/components/i2c/i2c.h
+++ b/esphome/components/i2c/i2c.h
@@ -178,15 +178,13 @@ class I2CDevice {
   I2CRegister reg(uint8_t a_register) { return {this, a_register}; }
 
   /// Begin a write transmission.
-  void raw_begin_transmission() { this->parent_->raw_begin_transmission(this->address_); };
+  void raw_begin_transmission();
 
   /// End a write transmission, return true if successful.
-  bool raw_end_transmission(bool send_stop = true) {
-    return this->parent_->raw_end_transmission(this->address_, send_stop);
-  };
+  bool raw_end_transmission(bool send_stop = true);
 
   /// Write len amount of bytes from data. begin_transmission_ must be called before this.
-  void raw_write(const uint8_t *data, uint8_t len) { this->parent_->raw_write(this->address_, data, len); };
+  void raw_write(const uint8_t *data, uint8_t len);
 
   /** Read len amount of bytes from a register into data. Optionally with a conversion time after
    * writing the register value to the bus.
@@ -198,7 +196,7 @@ class I2CDevice {
    * @return If the operation was successful.
    */
   bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion = 0);
-  bool read_bytes_raw(uint8_t *data, uint8_t len) { return this->parent_->read_bytes_raw(this->address_, data, len); }
+  bool read_bytes_raw(uint8_t *data, uint8_t len);
 
   template<size_t N> optional<std::array<uint8_t, N>> read_bytes(uint8_t a_register) {
     std::array<uint8_t, N> res;
@@ -246,9 +244,7 @@ class I2CDevice {
    * @return If the operation was successful.
    */
   bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len);
-  bool write_bytes_raw(const uint8_t *data, uint8_t len) {
-    return this->parent_->write_bytes_raw(this->address_, data, len);
-  }
+  bool write_bytes_raw(const uint8_t *data, uint8_t len);
 
   /** Write a vector of data to a register.
    *
diff --git a/esphome/components/ili9341/display.py b/esphome/components/ili9341/display.py
index 8c1a28ea5d..450a958c56 100644
--- a/esphome/components/ili9341/display.py
+++ b/esphome/components/ili9341/display.py
@@ -47,7 +47,7 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     if config[CONF_MODEL] == "M5STACK":
         lcd_type = ILI9341M5Stack
     if config[CONF_MODEL] == "TFT_2.4":
@@ -55,21 +55,21 @@ def to_code(config):
     rhs = lcd_type.new()
     var = cg.Pvariable(config[CONF_ID], rhs)
 
-    yield cg.register_component(var, config)
-    yield display.register_display(var, config)
-    yield spi.register_spi_device(var, config)
+    await cg.register_component(var, config)
+    await display.register_display(var, config)
+    await spi.register_spi_device(var, config)
     cg.add(var.set_model(config[CONF_MODEL]))
-    dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN])
+    dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
     cg.add(var.set_dc_pin(dc))
 
     if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
+        lambda_ = await cg.process_lambda(
             config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void
         )
         cg.add(var.set_writer(lambda_))
     if CONF_RESET_PIN in config:
-        reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN])
+        reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
         cg.add(var.set_reset_pin(reset))
     if CONF_LED_PIN in config:
-        led_pin = yield cg.gpio_pin_expression(config[CONF_LED_PIN])
+        led_pin = await cg.gpio_pin_expression(config[CONF_LED_PIN])
         cg.add(var.set_led_pin(led_pin))
diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py
index b629779690..b946a86bc4 100644
--- a/esphome/components/image/__init__.py
+++ b/esphome/components/image/__init__.py
@@ -39,7 +39,7 @@ IMAGE_SCHEMA = cv.Schema(
 CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     from PIL import Image
 
     path = CORE.relative_config_path(config[CONF_FILE])
diff --git a/esphome/components/improv/__init__.py b/esphome/components/improv/__init__.py
new file mode 100644
index 0000000000..b1de57df8f
--- /dev/null
+++ b/esphome/components/improv/__init__.py
@@ -0,0 +1 @@
+CODEOWNERS = ["@jesserockz"]
diff --git a/esphome/components/improv/improv.cpp b/esphome/components/improv/improv.cpp
new file mode 100644
index 0000000000..d1fee72866
--- /dev/null
+++ b/esphome/components/improv/improv.cpp
@@ -0,0 +1,89 @@
+#include "improv.h"
+
+namespace improv {
+
+ImprovCommand parse_improv_data(const std::vector<uint8_t> &data) {
+  return parse_improv_data(data.data(), data.size());
+}
+
+ImprovCommand parse_improv_data(const uint8_t *data, size_t length) {
+  Command command = (Command) data[0];
+  uint8_t data_length = data[1];
+
+  if (data_length != length - 3) {
+    return {.command = UNKNOWN};
+  }
+
+  uint8_t checksum = data[length - 1];
+
+  uint32_t calculated_checksum = 0;
+  for (uint8_t i = 0; i < length - 1; i++) {
+    calculated_checksum += data[i];
+  }
+
+  if ((uint8_t) calculated_checksum != checksum) {
+    return {.command = BAD_CHECKSUM};
+  }
+
+  if (command == WIFI_SETTINGS) {
+    uint8_t ssid_length = data[2];
+    uint8_t ssid_start = 3;
+    size_t ssid_end = ssid_start + ssid_length;
+
+    uint8_t pass_length = data[ssid_end];
+    size_t pass_start = ssid_end + 1;
+    size_t pass_end = pass_start + pass_length;
+
+    std::string ssid(data + ssid_start, data + ssid_end);
+    std::string password(data + pass_start, data + pass_end);
+    return {.command = command, .ssid = ssid, .password = password};
+  }
+
+  return {
+      .command = command,
+  };
+}
+
+std::vector<uint8_t> build_rpc_response(Command command, const std::vector<std::string> &datum) {
+  std::vector<uint8_t> out;
+  uint32_t length = 0;
+  out.push_back(command);
+  for (auto str : datum) {
+    uint8_t len = str.length();
+    length += len;
+    out.push_back(len);
+    out.insert(out.end(), str.begin(), str.end());
+  }
+  out.insert(out.begin() + 1, length);
+
+  uint32_t calculated_checksum = 0;
+
+  for (uint8_t byte : out) {
+    calculated_checksum += byte;
+  }
+  out.push_back(calculated_checksum);
+  return out;
+}
+
+std::vector<uint8_t> build_rpc_response(Command command, const std::vector<String> &datum) {
+  std::vector<uint8_t> out;
+  uint32_t length = 0;
+  out.push_back(command);
+  for (auto str : datum) {
+    uint8_t len = str.length();
+    length += len;
+    out.push_back(len);
+    out.insert(out.end(), str.begin(), str.end());
+  }
+  out.insert(out.begin() + 1, length);
+
+  uint32_t calculated_checksum = 0;
+
+  for (uint8_t byte : out) {
+    calculated_checksum += byte;
+  }
+  out.push_back(calculated_checksum);
+  return out;
+}
+
+}  // namespace improv
diff --git a/esphome/components/improv/improv.h b/esphome/components/improv/improv.h
new file mode 100644
index 0000000000..8a44812cec
--- /dev/null
+++ b/esphome/components/improv/improv.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include <stdint.h>
+#include <string>
+#include "WString.h"
+#include <vector>
+
+namespace improv {
+
+static const char *SERVICE_UUID = "00467768-6228-2272-4663-277478268000";
+static const char *STATUS_UUID = "00467768-6228-2272-4663-277478268001";
+static const char *ERROR_UUID = "00467768-6228-2272-4663-277478268002";
+static const char *RPC_COMMAND_UUID = "00467768-6228-2272-4663-277478268003";
+static const char *RPC_RESULT_UUID = "00467768-6228-2272-4663-277478268004";
+static const char *CAPABILITIES_UUID = "00467768-6228-2272-4663-277478268005";
+
+enum Error : uint8_t {
+  ERROR_NONE = 0x00,
+  ERROR_INVALID_RPC = 0x01,
+  ERROR_UNKNOWN_RPC = 0x02,
+  ERROR_UNABLE_TO_CONNECT = 0x03,
+  ERROR_NOT_AUTHORIZED = 0x04,
+  ERROR_UNKNOWN = 0xFF,
+};
+
+enum State : uint8_t {
+  STATE_STOPPED = 0x00,
+  STATE_AWAITING_AUTHORIZATION = 0x01,
+  STATE_AUTHORIZED = 0x02,
+  STATE_PROVISIONING = 0x03,
+  STATE_PROVISIONED = 0x04,
+};
+
+enum Command : uint8_t {
+  UNKNOWN = 0x00,
+  WIFI_SETTINGS = 0x01,
+  IDENTIFY = 0x02,
+  BAD_CHECKSUM = 0xFF,
+};
+
+static const uint8_t CAPABILITY_IDENTIFY = 0x01;
+
+struct ImprovCommand {
+  Command command;
+  std::string ssid;
+  std::string password;
+};
+
+ImprovCommand parse_improv_data(const std::vector<uint8_t> &data);
+ImprovCommand parse_improv_data(const uint8_t *data, size_t length);
+
+std::vector<uint8_t> build_rpc_response(Command command, const std::vector<std::string> &datum);
+std::vector<uint8_t> build_rpc_response(Command command, const std::vector<String> &datum);
+
+}  // namespace improv
diff --git a/esphome/components/ina219/sensor.py b/esphome/components/ina219/sensor.py
index d122754b88..ed88ace967 100644
--- a/esphome/components/ina219/sensor.py
+++ b/esphome/components/ina219/sensor.py
@@ -14,6 +14,7 @@ from esphome.const import (
     DEVICE_CLASS_POWER,
     DEVICE_CLASS_VOLTAGE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_VOLT,
     UNIT_AMPERE,
     UNIT_WATT,
@@ -31,16 +32,20 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(INA219Component),
             cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema(
-                UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE
+                UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT
             ),
             cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema(
-                UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE
+                UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT
             ),
             cv.Optional(CONF_CURRENT): sensor.sensor_schema(
-                UNIT_AMPERE, ICON_EMPTY, 3, DEVICE_CLASS_CURRENT
+                UNIT_AMPERE,
+                ICON_EMPTY,
+                3,
+                DEVICE_CLASS_CURRENT,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_POWER): sensor.sensor_schema(
-                UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER
+                UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
             ),
             cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All(
                 cv.resistance, cv.Range(min=0.0, max=32.0)
@@ -58,27 +63,27 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     cg.add(var.set_shunt_resistance_ohm(config[CONF_SHUNT_RESISTANCE]))
     cg.add(var.set_max_current_a(config[CONF_MAX_CURRENT]))
     cg.add(var.set_max_voltage_v(config[CONF_MAX_VOLTAGE]))
 
     if CONF_BUS_VOLTAGE in config:
-        sens = yield sensor.new_sensor(config[CONF_BUS_VOLTAGE])
+        sens = await sensor.new_sensor(config[CONF_BUS_VOLTAGE])
         cg.add(var.set_bus_voltage_sensor(sens))
 
     if CONF_SHUNT_VOLTAGE in config:
-        sens = yield sensor.new_sensor(config[CONF_SHUNT_VOLTAGE])
+        sens = await sensor.new_sensor(config[CONF_SHUNT_VOLTAGE])
         cg.add(var.set_shunt_voltage_sensor(sens))
 
     if CONF_CURRENT in config:
-        sens = yield sensor.new_sensor(config[CONF_CURRENT])
+        sens = await sensor.new_sensor(config[CONF_CURRENT])
         cg.add(var.set_current_sensor(sens))
 
     if CONF_POWER in config:
-        sens = yield sensor.new_sensor(config[CONF_POWER])
+        sens = await sensor.new_sensor(config[CONF_POWER])
         cg.add(var.set_power_sensor(sens))
diff --git a/esphome/components/ina226/sensor.py b/esphome/components/ina226/sensor.py
index 2b7346fbe2..e4ceda39c1 100644
--- a/esphome/components/ina226/sensor.py
+++ b/esphome/components/ina226/sensor.py
@@ -13,6 +13,7 @@ from esphome.const import (
     DEVICE_CLASS_CURRENT,
     DEVICE_CLASS_POWER,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_VOLT,
     UNIT_AMPERE,
     UNIT_WATT,
@@ -30,16 +31,20 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(INA226Component),
             cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema(
-                UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE
+                UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT
             ),
             cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema(
-                UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE
+                UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT
             ),
             cv.Optional(CONF_CURRENT): sensor.sensor_schema(
-                UNIT_AMPERE, ICON_EMPTY, 3, DEVICE_CLASS_CURRENT
+                UNIT_AMPERE,
+                ICON_EMPTY,
+                3,
+                DEVICE_CLASS_CURRENT,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_POWER): sensor.sensor_schema(
-                UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER
+                UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
             ),
             cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All(
                 cv.resistance, cv.Range(min=0.0)
@@ -54,27 +59,27 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     cg.add(var.set_shunt_resistance_ohm(config[CONF_SHUNT_RESISTANCE]))
 
     cg.add(var.set_max_current_a(config[CONF_MAX_CURRENT]))
 
     if CONF_BUS_VOLTAGE in config:
-        sens = yield sensor.new_sensor(config[CONF_BUS_VOLTAGE])
+        sens = await sensor.new_sensor(config[CONF_BUS_VOLTAGE])
         cg.add(var.set_bus_voltage_sensor(sens))
 
     if CONF_SHUNT_VOLTAGE in config:
-        sens = yield sensor.new_sensor(config[CONF_SHUNT_VOLTAGE])
+        sens = await sensor.new_sensor(config[CONF_SHUNT_VOLTAGE])
         cg.add(var.set_shunt_voltage_sensor(sens))
 
     if CONF_CURRENT in config:
-        sens = yield sensor.new_sensor(config[CONF_CURRENT])
+        sens = await sensor.new_sensor(config[CONF_CURRENT])
         cg.add(var.set_current_sensor(sens))
 
     if CONF_POWER in config:
-        sens = yield sensor.new_sensor(config[CONF_POWER])
+        sens = await sensor.new_sensor(config[CONF_POWER])
         cg.add(var.set_power_sensor(sens))
diff --git a/esphome/components/ina3221/sensor.py b/esphome/components/ina3221/sensor.py
index c055e149b7..8b861d972d 100644
--- a/esphome/components/ina3221/sensor.py
+++ b/esphome/components/ina3221/sensor.py
@@ -12,6 +12,7 @@ from esphome.const import (
     DEVICE_CLASS_CURRENT,
     DEVICE_CLASS_POWER,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_VOLT,
     UNIT_AMPERE,
     UNIT_WATT,
@@ -31,16 +32,16 @@ INA3221Component = ina3221_ns.class_(
 INA3221_CHANNEL_SCHEMA = cv.Schema(
     {
         cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema(
-            UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE
+            UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema(
-            UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE
+            UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional(CONF_CURRENT): sensor.sensor_schema(
-            UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT
+            UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional(CONF_POWER): sensor.sensor_schema(
-            UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER
+            UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All(
             cv.resistance, cv.Range(min=0.0, max=32.0)
@@ -62,10 +63,10 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     for i, channel in enumerate([CONF_CHANNEL_1, CONF_CHANNEL_2, CONF_CHANNEL_3]):
         if channel not in config:
@@ -74,14 +75,14 @@ def to_code(config):
         if CONF_SHUNT_RESISTANCE in conf:
             cg.add(var.set_shunt_resistance(i, conf[CONF_SHUNT_RESISTANCE]))
         if CONF_BUS_VOLTAGE in conf:
-            sens = yield sensor.new_sensor(conf[CONF_BUS_VOLTAGE])
+            sens = await sensor.new_sensor(conf[CONF_BUS_VOLTAGE])
             cg.add(var.set_bus_voltage_sensor(i, sens))
         if CONF_SHUNT_VOLTAGE in conf:
-            sens = yield sensor.new_sensor(conf[CONF_SHUNT_VOLTAGE])
+            sens = await sensor.new_sensor(conf[CONF_SHUNT_VOLTAGE])
             cg.add(var.set_shunt_voltage_sensor(i, sens))
         if CONF_CURRENT in conf:
-            sens = yield sensor.new_sensor(conf[CONF_CURRENT])
+            sens = await sensor.new_sensor(conf[CONF_CURRENT])
             cg.add(var.set_current_sensor(i, sens))
         if CONF_POWER in conf:
-            sens = yield sensor.new_sensor(conf[CONF_POWER])
+            sens = await sensor.new_sensor(conf[CONF_POWER])
             cg.add(var.set_power_sensor(i, sens))
diff --git a/esphome/components/inkbird_ibsth1_mini/sensor.py b/esphome/components/inkbird_ibsth1_mini/sensor.py
index c67acb8595..044e7fe67d 100644
--- a/esphome/components/inkbird_ibsth1_mini/sensor.py
+++ b/esphome/components/inkbird_ibsth1_mini/sensor.py
@@ -10,6 +10,7 @@ from esphome.const import (
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
     CONF_ID,
@@ -29,13 +30,25 @@ CONFIG_SCHEMA = (
             cv.GenerateID(): cv.declare_id(InkbirdUBSTH1_MINI),
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_BATTERY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_BATTERY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -44,19 +57,19 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity(sens))
     if CONF_BATTERY_LEVEL in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
         cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py
index 323936d2c4..8e00a69751 100644
--- a/esphome/components/inkplate6/display.py
+++ b/esphome/components/inkplate6/display.py
@@ -94,15 +94,15 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
 
-    yield cg.register_component(var, config)
-    yield display.register_display(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await display.register_display(var, config)
+    await i2c.register_i2c_device(var, config)
 
     if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
+        lambda_ = await cg.process_lambda(
             config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void
         )
         cg.add(var.set_writer(lambda_))
@@ -111,61 +111,61 @@ def to_code(config):
     cg.add(var.set_partial_updating(config[CONF_PARTIAL_UPDATING]))
     cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY]))
 
-    ckv = yield cg.gpio_pin_expression(config[CONF_CKV_PIN])
+    ckv = await cg.gpio_pin_expression(config[CONF_CKV_PIN])
     cg.add(var.set_ckv_pin(ckv))
 
-    gmod = yield cg.gpio_pin_expression(config[CONF_GMOD_PIN])
+    gmod = await cg.gpio_pin_expression(config[CONF_GMOD_PIN])
     cg.add(var.set_gmod_pin(gmod))
 
-    gpio0_enable = yield cg.gpio_pin_expression(config[CONF_GPIO0_ENABLE_PIN])
+    gpio0_enable = await cg.gpio_pin_expression(config[CONF_GPIO0_ENABLE_PIN])
     cg.add(var.set_gpio0_enable_pin(gpio0_enable))
 
-    oe = yield cg.gpio_pin_expression(config[CONF_OE_PIN])
+    oe = await cg.gpio_pin_expression(config[CONF_OE_PIN])
     cg.add(var.set_oe_pin(oe))
 
-    powerup = yield cg.gpio_pin_expression(config[CONF_POWERUP_PIN])
+    powerup = await cg.gpio_pin_expression(config[CONF_POWERUP_PIN])
     cg.add(var.set_powerup_pin(powerup))
 
-    sph = yield cg.gpio_pin_expression(config[CONF_SPH_PIN])
+    sph = await cg.gpio_pin_expression(config[CONF_SPH_PIN])
     cg.add(var.set_sph_pin(sph))
 
-    spv = yield cg.gpio_pin_expression(config[CONF_SPV_PIN])
+    spv = await cg.gpio_pin_expression(config[CONF_SPV_PIN])
     cg.add(var.set_spv_pin(spv))
 
-    vcom = yield cg.gpio_pin_expression(config[CONF_VCOM_PIN])
+    vcom = await cg.gpio_pin_expression(config[CONF_VCOM_PIN])
     cg.add(var.set_vcom_pin(vcom))
 
-    wakeup = yield cg.gpio_pin_expression(config[CONF_WAKEUP_PIN])
+    wakeup = await cg.gpio_pin_expression(config[CONF_WAKEUP_PIN])
     cg.add(var.set_wakeup_pin(wakeup))
 
-    cl = yield cg.gpio_pin_expression(config[CONF_CL_PIN])
+    cl = await cg.gpio_pin_expression(config[CONF_CL_PIN])
     cg.add(var.set_cl_pin(cl))
 
-    le = yield cg.gpio_pin_expression(config[CONF_LE_PIN])
+    le = await cg.gpio_pin_expression(config[CONF_LE_PIN])
     cg.add(var.set_le_pin(le))
 
-    display_data_0 = yield cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_0_PIN])
+    display_data_0 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_0_PIN])
     cg.add(var.set_display_data_0_pin(display_data_0))
 
-    display_data_1 = yield cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_1_PIN])
+    display_data_1 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_1_PIN])
     cg.add(var.set_display_data_1_pin(display_data_1))
 
-    display_data_2 = yield cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_2_PIN])
+    display_data_2 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_2_PIN])
     cg.add(var.set_display_data_2_pin(display_data_2))
 
-    display_data_3 = yield cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_3_PIN])
+    display_data_3 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_3_PIN])
     cg.add(var.set_display_data_3_pin(display_data_3))
 
-    display_data_4 = yield cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_4_PIN])
+    display_data_4 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_4_PIN])
     cg.add(var.set_display_data_4_pin(display_data_4))
 
-    display_data_5 = yield cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_5_PIN])
+    display_data_5 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_5_PIN])
     cg.add(var.set_display_data_5_pin(display_data_5))
 
-    display_data_6 = yield cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_6_PIN])
+    display_data_6 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_6_PIN])
     cg.add(var.set_display_data_6_pin(display_data_6))
 
-    display_data_7 = yield cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_7_PIN])
+    display_data_7 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_7_PIN])
     cg.add(var.set_display_data_7_pin(display_data_7))
 
     cg.add_build_flag("-DBOARD_HAS_PSRAM")
diff --git a/esphome/components/integration/sensor.py b/esphome/components/integration/sensor.py
index 81b588610e..3f32394ff6 100644
--- a/esphome/components/integration/sensor.py
+++ b/esphome/components/integration/sensor.py
@@ -41,13 +41,13 @@ CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
 
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
 
-    sens = yield cg.get_variable(config[CONF_SENSOR])
+    sens = await cg.get_variable(config[CONF_SENSOR])
     cg.add(var.set_sensor(sens))
     cg.add(var.set_time(config[CONF_TIME_UNIT]))
     cg.add(var.set_method(config[CONF_INTEGRATION_METHOD]))
@@ -63,6 +63,6 @@ def to_code(config):
         }
     ),
 )
-def sensor_integration_reset_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, paren)
+async def sensor_integration_reset_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
diff --git a/esphome/components/interval/__init__.py b/esphome/components/interval/__init__.py
index 7d04a9e9ab..4514f80ba3 100644
--- a/esphome/components/interval/__init__.py
+++ b/esphome/components/interval/__init__.py
@@ -19,10 +19,10 @@ CONFIG_SCHEMA = automation.validate_automation(
 )
 
 
-def to_code(config):
+async def to_code(config):
     for conf in config:
         var = cg.new_Pvariable(conf[CONF_ID])
-        yield cg.register_component(var, conf)
-        yield automation.build_automation(var, [], conf)
+        await cg.register_component(var, conf)
+        await automation.build_automation(var, [], conf)
 
         cg.add(var.set_update_interval(conf[CONF_INTERVAL]))
diff --git a/esphome/components/json/__init__.py b/esphome/components/json/__init__.py
index f73fcb53fb..7bdef6ea0e 100644
--- a/esphome/components/json/__init__.py
+++ b/esphome/components/json/__init__.py
@@ -6,7 +6,7 @@ json_ns = cg.esphome_ns.namespace("json")
 
 
 @coroutine_with_priority(1.0)
-def to_code(config):
+async def to_code(config):
     cg.add_library("ArduinoJson-esphomelib", "5.13.3")
     cg.add_define("USE_JSON")
     cg.add_global(json_ns.using)
diff --git a/esphome/components/lcd_base/__init__.py b/esphome/components/lcd_base/__init__.py
index c577d7f8be..0ed2036c55 100644
--- a/esphome/components/lcd_base/__init__.py
+++ b/esphome/components/lcd_base/__init__.py
@@ -2,7 +2,6 @@ import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import display
 from esphome.const import CONF_DIMENSIONS
-from esphome.core import coroutine
 
 lcd_base_ns = cg.esphome_ns.namespace("lcd_base")
 LCDDisplay = lcd_base_ns.class_("LCDDisplay", cg.PollingComponent)
@@ -24,8 +23,7 @@ LCD_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend(
 ).extend(cv.polling_component_schema("1s"))
 
 
-@coroutine
-def setup_lcd_display(var, config):
-    yield cg.register_component(var, config)
-    yield display.register_display(var, config)
+async def setup_lcd_display(var, config):
+    await cg.register_component(var, config)
+    await display.register_display(var, config)
     cg.add(var.set_dimensions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1]))
diff --git a/esphome/components/lcd_gpio/display.py b/esphome/components/lcd_gpio/display.py
index 28c6df2efd..6b0a2f69de 100644
--- a/esphome/components/lcd_gpio/display.py
+++ b/esphome/components/lcd_gpio/display.py
@@ -39,25 +39,25 @@ CONFIG_SCHEMA = lcd_base.LCD_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield lcd_base.setup_lcd_display(var, config)
+    await lcd_base.setup_lcd_display(var, config)
     pins_ = []
     for conf in config[CONF_DATA_PINS]:
-        pins_.append((yield cg.gpio_pin_expression(conf)))
+        pins_.append((await cg.gpio_pin_expression(conf)))
     cg.add(var.set_data_pins(*pins_))
-    enable = yield cg.gpio_pin_expression(config[CONF_ENABLE_PIN])
+    enable = await cg.gpio_pin_expression(config[CONF_ENABLE_PIN])
     cg.add(var.set_enable_pin(enable))
 
-    rs = yield cg.gpio_pin_expression(config[CONF_RS_PIN])
+    rs = await cg.gpio_pin_expression(config[CONF_RS_PIN])
     cg.add(var.set_rs_pin(rs))
 
     if CONF_RW_PIN in config:
-        rw = yield cg.gpio_pin_expression(config[CONF_RW_PIN])
+        rw = await cg.gpio_pin_expression(config[CONF_RW_PIN])
         cg.add(var.set_rw_pin(rw))
 
     if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
+        lambda_ = await cg.process_lambda(
             config[CONF_LAMBDA],
             [(GPIOLCDDisplay.operator("ref"), "it")],
             return_type=cg.void,
diff --git a/esphome/components/lcd_pcf8574/display.py b/esphome/components/lcd_pcf8574/display.py
index 6499a8369e..5d9dd7adba 100644
--- a/esphome/components/lcd_pcf8574/display.py
+++ b/esphome/components/lcd_pcf8574/display.py
@@ -18,13 +18,13 @@ CONFIG_SCHEMA = lcd_base.LCD_SCHEMA.extend(
 ).extend(i2c.i2c_device_schema(0x3F))
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield lcd_base.setup_lcd_display(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await lcd_base.setup_lcd_display(var, config)
+    await i2c.register_i2c_device(var, config)
 
     if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
+        lambda_ = await cg.process_lambda(
             config[CONF_LAMBDA],
             [(PCF8574LCDDisplay.operator("ref"), "it")],
             return_type=cg.void,
diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py
index df746fff4f..150c5fa410 100644
--- a/esphome/components/ledc/output.py
+++ b/esphome/components/ledc/output.py
@@ -58,11 +58,11 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
-    gpio = yield cg.gpio_pin_expression(config[CONF_PIN])
+async def to_code(config):
+    gpio = await cg.gpio_pin_expression(config[CONF_PIN])
     var = cg.new_Pvariable(config[CONF_ID], gpio)
-    yield cg.register_component(var, config)
-    yield output.register_output(var, config)
+    await cg.register_component(var, config)
+    await output.register_output(var, config)
     if CONF_CHANNEL in config:
         cg.add(var.set_channel(config[CONF_CHANNEL]))
     cg.add(var.set_frequency(config[CONF_FREQUENCY]))
@@ -78,9 +78,9 @@ def to_code(config):
         }
     ),
 )
-def ledc_set_frequency_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def ledc_set_frequency_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    template_ = yield cg.templatable(config[CONF_FREQUENCY], args, float)
+    template_ = await cg.templatable(config[CONF_FREQUENCY], args, float)
     cg.add(var.set_frequency(template_))
-    yield var
+    return var
diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py
index 034dbdaf34..276bf8073d 100644
--- a/esphome/components/light/__init__.py
+++ b/esphome/components/light/__init__.py
@@ -17,7 +17,7 @@ from esphome.const import (
     CONF_ON_TURN_ON,
     CONF_TRIGGER_ID,
 )
-from esphome.core import coroutine, coroutine_with_priority
+from esphome.core import coroutine_with_priority
 from .automation import light_control_to_code  # noqa
 from .effects import (
     validate_effects,
@@ -46,6 +46,8 @@ RESTORE_MODES = {
     "RESTORE_DEFAULT_ON": LightRestoreMode.LIGHT_RESTORE_DEFAULT_ON,
     "ALWAYS_OFF": LightRestoreMode.LIGHT_ALWAYS_OFF,
     "ALWAYS_ON": LightRestoreMode.LIGHT_ALWAYS_ON,
+    "RESTORE_INVERTED_DEFAULT_OFF": LightRestoreMode.LIGHT_RESTORE_INVERTED_DEFAULT_OFF,
+    "RESTORE_INVERTED_DEFAULT_ON": LightRestoreMode.LIGHT_RESTORE_INVERTED_DEFAULT_ON,
 }
 
 LIGHT_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend(
@@ -102,8 +104,7 @@ ADDRESSABLE_LIGHT_SCHEMA = RGB_LIGHT_SCHEMA.extend(
 )
 
 
-@coroutine
-def setup_light_core_(light_var, output_var, config):
+async def setup_light_core_(light_var, output_var, config):
     cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE]))
     if CONF_INTERNAL in config:
         cg.add(light_var.set_internal(config[CONF_INTERNAL]))
@@ -115,39 +116,38 @@ def setup_light_core_(light_var, output_var, config):
         )
     if CONF_GAMMA_CORRECT in config:
         cg.add(light_var.set_gamma_correct(config[CONF_GAMMA_CORRECT]))
-    effects = yield cg.build_registry_list(
+    effects = await cg.build_registry_list(
         EFFECTS_REGISTRY, config.get(CONF_EFFECTS, [])
     )
     cg.add(light_var.add_effects(effects))
 
     for conf in config.get(CONF_ON_TURN_ON, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var)
-        yield auto.build_automation(trigger, [], conf)
+        await auto.build_automation(trigger, [], conf)
     for conf in config.get(CONF_ON_TURN_OFF, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var)
-        yield auto.build_automation(trigger, [], conf)
+        await auto.build_automation(trigger, [], conf)
 
     if CONF_COLOR_CORRECT in config:
         cg.add(output_var.set_correction(*config[CONF_COLOR_CORRECT]))
 
     if CONF_POWER_SUPPLY in config:
-        var_ = yield cg.get_variable(config[CONF_POWER_SUPPLY])
+        var_ = await cg.get_variable(config[CONF_POWER_SUPPLY])
         cg.add(output_var.set_power_supply(var_))
 
     if CONF_MQTT_ID in config:
         mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], light_var)
-        yield mqtt.register_mqtt_component(mqtt_, config)
+        await mqtt.register_mqtt_component(mqtt_, config)
 
 
-@coroutine
-def register_light(output_var, config):
+async def register_light(output_var, config):
     light_var = cg.new_Pvariable(config[CONF_ID], config[CONF_NAME], output_var)
     cg.add(cg.App.register_light(light_var))
-    yield cg.register_component(light_var, config)
-    yield setup_light_core_(light_var, output_var, config)
+    await cg.register_component(light_var, config)
+    await setup_light_core_(light_var, output_var, config)
 
 
 @coroutine_with_priority(100.0)
-def to_code(config):
+async def to_code(config):
     cg.add_define("USE_LIGHT")
     cg.add_global(light_ns.using)
diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py
index 7f1c6d9cb9..3fb3126f14 100644
--- a/esphome/components/light/automation.py
+++ b/esphome/components/light/automation.py
@@ -40,15 +40,15 @@ from .types import (
         }
     ),
 )
-def light_toggle_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def light_toggle_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
     if CONF_TRANSITION_LENGTH in config:
-        template_ = yield cg.templatable(
+        template_ = await cg.templatable(
             config[CONF_TRANSITION_LENGTH], args, cg.uint32
         )
         cg.add(var.set_transition_length(template_))
-    yield var
+    return var
 
 
 LIGHT_CONTROL_ACTION_SCHEMA = cv.Schema(
@@ -97,42 +97,42 @@ LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id(
 @automation.register_action(
     "light.control", LightControlAction, LIGHT_CONTROL_ACTION_SCHEMA
 )
-def light_control_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def light_control_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
     if CONF_STATE in config:
-        template_ = yield cg.templatable(config[CONF_STATE], args, bool)
+        template_ = await cg.templatable(config[CONF_STATE], args, bool)
         cg.add(var.set_state(template_))
     if CONF_TRANSITION_LENGTH in config:
-        template_ = yield cg.templatable(
+        template_ = await cg.templatable(
             config[CONF_TRANSITION_LENGTH], args, cg.uint32
         )
         cg.add(var.set_transition_length(template_))
     if CONF_FLASH_LENGTH in config:
-        template_ = yield cg.templatable(config[CONF_FLASH_LENGTH], args, cg.uint32)
+        template_ = await cg.templatable(config[CONF_FLASH_LENGTH], args, cg.uint32)
         cg.add(var.set_flash_length(template_))
     if CONF_BRIGHTNESS in config:
-        template_ = yield cg.templatable(config[CONF_BRIGHTNESS], args, float)
+        template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, float)
         cg.add(var.set_brightness(template_))
     if CONF_RED in config:
-        template_ = yield cg.templatable(config[CONF_RED], args, float)
+        template_ = await cg.templatable(config[CONF_RED], args, float)
         cg.add(var.set_red(template_))
     if CONF_GREEN in config:
-        template_ = yield cg.templatable(config[CONF_GREEN], args, float)
+        template_ = await cg.templatable(config[CONF_GREEN], args, float)
         cg.add(var.set_green(template_))
     if CONF_BLUE in config:
-        template_ = yield cg.templatable(config[CONF_BLUE], args, float)
+        template_ = await cg.templatable(config[CONF_BLUE], args, float)
         cg.add(var.set_blue(template_))
     if CONF_WHITE in config:
-        template_ = yield cg.templatable(config[CONF_WHITE], args, float)
+        template_ = await cg.templatable(config[CONF_WHITE], args, float)
         cg.add(var.set_white(template_))
     if CONF_COLOR_TEMPERATURE in config:
-        template_ = yield cg.templatable(config[CONF_COLOR_TEMPERATURE], args, float)
+        template_ = await cg.templatable(config[CONF_COLOR_TEMPERATURE], args, float)
         cg.add(var.set_color_temperature(template_))
     if CONF_EFFECT in config:
-        template_ = yield cg.templatable(config[CONF_EFFECT], args, cg.std_string)
+        template_ = await cg.templatable(config[CONF_EFFECT], args, cg.std_string)
         cg.add(var.set_effect(template_))
-    yield var
+    return var
 
 
 CONF_RELATIVE_BRIGHTNESS = "relative_brightness"
@@ -152,15 +152,15 @@ LIGHT_DIM_RELATIVE_ACTION_SCHEMA = cv.Schema(
 @automation.register_action(
     "light.dim_relative", DimRelativeAction, LIGHT_DIM_RELATIVE_ACTION_SCHEMA
 )
-def light_dim_relative_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def light_dim_relative_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    templ = yield cg.templatable(config[CONF_RELATIVE_BRIGHTNESS], args, float)
+    templ = await cg.templatable(config[CONF_RELATIVE_BRIGHTNESS], args, float)
     cg.add(var.set_relative_brightness(templ))
     if CONF_TRANSITION_LENGTH in config:
-        templ = yield cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32)
+        templ = await cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32)
         cg.add(var.set_transition_length(templ))
-    yield var
+    return var
 
 
 LIGHT_ADDRESSABLE_SET_ACTION_SCHEMA = cv.Schema(
@@ -179,40 +179,40 @@ LIGHT_ADDRESSABLE_SET_ACTION_SCHEMA = cv.Schema(
 @automation.register_action(
     "light.addressable_set", AddressableSet, LIGHT_ADDRESSABLE_SET_ACTION_SCHEMA
 )
-def light_addressable_set_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def light_addressable_set_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
     if CONF_RANGE_FROM in config:
-        templ = yield cg.templatable(config[CONF_RANGE_FROM], args, cg.int32)
+        templ = await cg.templatable(config[CONF_RANGE_FROM], args, cg.int32)
         cg.add(var.set_range_from(templ))
     if CONF_RANGE_TO in config:
-        templ = yield cg.templatable(config[CONF_RANGE_TO], args, cg.int32)
+        templ = await cg.templatable(config[CONF_RANGE_TO], args, cg.int32)
         cg.add(var.set_range_to(templ))
 
     def rgbw_to_exp(x):
         return int(round(x * 255))
 
     if CONF_RED in config:
-        templ = yield cg.templatable(
+        templ = await cg.templatable(
             config[CONF_RED], args, cg.uint8, to_exp=rgbw_to_exp
         )
         cg.add(var.set_red(templ))
     if CONF_GREEN in config:
-        templ = yield cg.templatable(
+        templ = await cg.templatable(
             config[CONF_GREEN], args, cg.uint8, to_exp=rgbw_to_exp
         )
         cg.add(var.set_green(templ))
     if CONF_BLUE in config:
-        templ = yield cg.templatable(
+        templ = await cg.templatable(
             config[CONF_BLUE], args, cg.uint8, to_exp=rgbw_to_exp
         )
         cg.add(var.set_blue(templ))
     if CONF_WHITE in config:
-        templ = yield cg.templatable(
+        templ = await cg.templatable(
             config[CONF_WHITE], args, cg.uint8, to_exp=rgbw_to_exp
         )
         cg.add(var.set_white(templ))
-    yield var
+    return var
 
 
 @automation.register_condition(
@@ -233,6 +233,6 @@ def light_addressable_set_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def light_is_on_off_to_code(config, condition_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(condition_id, template_arg, paren)
+async def light_is_on_off_to_code(config, condition_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(condition_id, template_arg, paren)
diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py
index 08a78d90ed..c213de0ae6 100644
--- a/esphome/components/light/effects.py
+++ b/esphome/components/light/effects.py
@@ -1,3 +1,4 @@
+from esphome.jsonschema import jschema_extractor
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import automation
@@ -132,9 +133,9 @@ def register_addressable_effect(
         cv.Optional(CONF_UPDATE_INTERVAL, default="0ms"): cv.update_interval,
     },
 )
-def lambda_effect_to_code(config, effect_id):
-    lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [], return_type=cg.void)
-    yield cg.new_Pvariable(
+async def lambda_effect_to_code(config, effect_id):
+    lambda_ = await cg.process_lambda(config[CONF_LAMBDA], [], return_type=cg.void)
+    return cg.new_Pvariable(
         effect_id, config[CONF_NAME], lambda_, config[CONF_UPDATE_INTERVAL]
     )
 
@@ -147,10 +148,10 @@ def lambda_effect_to_code(config, effect_id):
         cv.Required(CONF_SEQUENCE): automation.validate_automation(single=True),
     },
 )
-def automation_effect_to_code(config, effect_id):
-    var = yield cg.new_Pvariable(effect_id, config[CONF_NAME])
-    yield automation.build_automation(var.get_trig(), [], config[CONF_SEQUENCE])
-    yield var
+async def automation_effect_to_code(config, effect_id):
+    var = cg.new_Pvariable(effect_id, config[CONF_NAME])
+    await automation.build_automation(var.get_trig(), [], config[CONF_SEQUENCE])
+    return var
 
 
 @register_monochromatic_effect(
@@ -166,11 +167,11 @@ def automation_effect_to_code(config, effect_id):
         ): cv.positive_time_period_milliseconds,
     },
 )
-def pulse_effect_to_code(config, effect_id):
+async def pulse_effect_to_code(config, effect_id):
     effect = cg.new_Pvariable(effect_id, config[CONF_NAME])
     cg.add(effect.set_transition_length(config[CONF_TRANSITION_LENGTH]))
     cg.add(effect.set_update_interval(config[CONF_UPDATE_INTERVAL]))
-    yield effect
+    return effect
 
 
 @register_monochromatic_effect(
@@ -186,11 +187,11 @@ def pulse_effect_to_code(config, effect_id):
         ): cv.positive_time_period_milliseconds,
     },
 )
-def random_effect_to_code(config, effect_id):
+async def random_effect_to_code(config, effect_id):
     effect = cg.new_Pvariable(effect_id, config[CONF_NAME])
     cg.add(effect.set_transition_length(config[CONF_TRANSITION_LENGTH]))
     cg.add(effect.set_update_interval(config[CONF_UPDATE_INTERVAL]))
-    yield effect
+    return effect
 
 
 @register_binary_effect(
@@ -232,7 +233,7 @@ def random_effect_to_code(config, effect_id):
         ),
     },
 )
-def strobe_effect_to_code(config, effect_id):
+async def strobe_effect_to_code(config, effect_id):
     var = cg.new_Pvariable(effect_id, config[CONF_NAME])
     colors = []
     for color in config.get(CONF_COLORS, []):
@@ -254,7 +255,7 @@ def strobe_effect_to_code(config, effect_id):
             )
         )
     cg.add(var.set_colors(colors))
-    yield var
+    return var
 
 
 @register_monochromatic_effect(
@@ -266,11 +267,11 @@ def strobe_effect_to_code(config, effect_id):
         cv.Optional(CONF_INTENSITY, default=0.015): cv.percentage,
     },
 )
-def flicker_effect_to_code(config, effect_id):
+async def flicker_effect_to_code(config, effect_id):
     var = cg.new_Pvariable(effect_id, config[CONF_NAME])
     cg.add(var.set_alpha(config[CONF_ALPHA]))
     cg.add(var.set_intensity(config[CONF_INTENSITY]))
-    yield var
+    return var
 
 
 @register_addressable_effect(
@@ -284,17 +285,17 @@ def flicker_effect_to_code(config, effect_id):
         ): cv.positive_time_period_milliseconds,
     },
 )
-def addressable_lambda_effect_to_code(config, effect_id):
+async def addressable_lambda_effect_to_code(config, effect_id):
     args = [
         (AddressableLightRef, "it"),
         (Color, "current_color"),
         (bool, "initial_run"),
     ]
-    lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], args, return_type=cg.void)
+    lambda_ = await cg.process_lambda(config[CONF_LAMBDA], args, return_type=cg.void)
     var = cg.new_Pvariable(
         effect_id, config[CONF_NAME], lambda_, config[CONF_UPDATE_INTERVAL]
     )
-    yield var
+    return var
 
 
 @register_addressable_effect(
@@ -306,11 +307,11 @@ def addressable_lambda_effect_to_code(config, effect_id):
         cv.Optional(CONF_WIDTH, default=50): cv.uint32_t,
     },
 )
-def addressable_rainbow_effect_to_code(config, effect_id):
+async def addressable_rainbow_effect_to_code(config, effect_id):
     var = cg.new_Pvariable(effect_id, config[CONF_NAME])
     cg.add(var.set_speed(config[CONF_SPEED]))
     cg.add(var.set_width(config[CONF_WIDTH]))
-    yield var
+    return var
 
 
 @register_addressable_effect(
@@ -336,7 +337,7 @@ def addressable_rainbow_effect_to_code(config, effect_id):
         cv.Optional(CONF_REVERSE, default=False): cv.boolean,
     },
 )
-def addressable_color_wipe_effect_to_code(config, effect_id):
+async def addressable_color_wipe_effect_to_code(config, effect_id):
     var = cg.new_Pvariable(effect_id, config[CONF_NAME])
     cg.add(var.set_add_led_interval(config[CONF_ADD_LED_INTERVAL]))
     cg.add(var.set_reverse(config[CONF_REVERSE]))
@@ -354,7 +355,7 @@ def addressable_color_wipe_effect_to_code(config, effect_id):
             )
         )
     cg.add(var.set_colors(colors))
-    yield var
+    return var
 
 
 @register_addressable_effect(
@@ -368,11 +369,11 @@ def addressable_color_wipe_effect_to_code(config, effect_id):
         cv.Optional(CONF_SCAN_WIDTH, default=1): cv.int_range(min=1),
     },
 )
-def addressable_scan_effect_to_code(config, effect_id):
+async def addressable_scan_effect_to_code(config, effect_id):
     var = cg.new_Pvariable(effect_id, config[CONF_NAME])
     cg.add(var.set_move_interval(config[CONF_MOVE_INTERVAL]))
     cg.add(var.set_scan_width(config[CONF_SCAN_WIDTH]))
-    yield var
+    return var
 
 
 @register_addressable_effect(
@@ -386,11 +387,11 @@ def addressable_scan_effect_to_code(config, effect_id):
         ): cv.positive_time_period_milliseconds,
     },
 )
-def addressable_twinkle_effect_to_code(config, effect_id):
+async def addressable_twinkle_effect_to_code(config, effect_id):
     var = cg.new_Pvariable(effect_id, config[CONF_NAME])
     cg.add(var.set_twinkle_probability(config[CONF_TWINKLE_PROBABILITY]))
     cg.add(var.set_progress_interval(config[CONF_PROGRESS_INTERVAL]))
-    yield var
+    return var
 
 
 @register_addressable_effect(
@@ -404,11 +405,11 @@ def addressable_twinkle_effect_to_code(config, effect_id):
         ): cv.positive_time_period_milliseconds,
     },
 )
-def addressable_random_twinkle_effect_to_code(config, effect_id):
+async def addressable_random_twinkle_effect_to_code(config, effect_id):
     var = cg.new_Pvariable(effect_id, config[CONF_NAME])
     cg.add(var.set_twinkle_probability(config[CONF_TWINKLE_PROBABILITY]))
     cg.add(var.set_progress_interval(config[CONF_PROGRESS_INTERVAL]))
-    yield var
+    return var
 
 
 @register_addressable_effect(
@@ -424,13 +425,13 @@ def addressable_random_twinkle_effect_to_code(config, effect_id):
         cv.Optional(CONF_FADE_OUT_RATE, default=120): cv.uint8_t,
     },
 )
-def addressable_fireworks_effect_to_code(config, effect_id):
+async def addressable_fireworks_effect_to_code(config, effect_id):
     var = cg.new_Pvariable(effect_id, config[CONF_NAME])
     cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))
     cg.add(var.set_spark_probability(config[CONF_SPARK_PROBABILITY]))
     cg.add(var.set_use_random_color(config[CONF_USE_RANDOM_COLOR]))
     cg.add(var.set_fade_out_rate(config[CONF_FADE_OUT_RATE]))
-    yield var
+    return var
 
 
 @register_addressable_effect(
@@ -444,15 +445,19 @@ def addressable_fireworks_effect_to_code(config, effect_id):
         cv.Optional(CONF_INTENSITY, default="5%"): cv.percentage,
     },
 )
-def addressable_flicker_effect_to_code(config, effect_id):
+async def addressable_flicker_effect_to_code(config, effect_id):
     var = cg.new_Pvariable(effect_id, config[CONF_NAME])
     cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))
     cg.add(var.set_intensity(config[CONF_INTENSITY]))
-    yield var
+    return var
 
 
 def validate_effects(allowed_effects):
+    @jschema_extractor("effects")
     def validator(value):
+        # pylint: disable=comparison-with-callable
+        if value == jschema_extractor:
+            return (allowed_effects, EFFECTS_REGISTRY)
         value = cv.validate_registry("effect", EFFECTS_REGISTRY)(value)
         errors = []
         names = set()
diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp
index dd78ced650..4e450f6ccb 100644
--- a/esphome/components/light/light_state.cpp
+++ b/esphome/components/light/light_state.cpp
@@ -106,10 +106,20 @@ void LightState::setup() {
   switch (this->restore_mode_) {
     case LIGHT_RESTORE_DEFAULT_OFF:
     case LIGHT_RESTORE_DEFAULT_ON:
+    case LIGHT_RESTORE_INVERTED_DEFAULT_OFF:
+    case LIGHT_RESTORE_INVERTED_DEFAULT_ON:
       this->rtc_ = global_preferences.make_preference<LightStateRTCState>(this->get_object_id_hash());
-      // Attempt to load from preferences, else fall back to default values from struct
+      // Attempt to load from preferences, else fall back to default values
       if (!this->rtc_.load(&recovered)) {
-        recovered.state = this->restore_mode_ == LIGHT_RESTORE_DEFAULT_ON;
+        recovered.state = false;
+        if (this->restore_mode_ == LIGHT_RESTORE_DEFAULT_ON ||
+            this->restore_mode_ == LIGHT_RESTORE_INVERTED_DEFAULT_ON) {
+          recovered.state = true;
+        }
+      } else if (this->restore_mode_ == LIGHT_RESTORE_INVERTED_DEFAULT_OFF ||
+                 this->restore_mode_ == LIGHT_RESTORE_INVERTED_DEFAULT_ON) {
+        // Inverted restore state
+        recovered.state = !recovered.state;
       }
       break;
     case LIGHT_ALWAYS_OFF:
diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h
index e761e576e3..aff63bf5db 100644
--- a/esphome/components/light/light_state.h
+++ b/esphome/components/light/light_state.h
@@ -165,6 +165,8 @@ enum LightRestoreMode {
   LIGHT_RESTORE_DEFAULT_ON,
   LIGHT_ALWAYS_OFF,
   LIGHT_ALWAYS_ON,
+  LIGHT_RESTORE_INVERTED_DEFAULT_OFF,
+  LIGHT_RESTORE_INVERTED_DEFAULT_ON,
 };
 
 /** This class represents the communication layer between the front-end MQTT layer and the
diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py
index 5e7383cca7..2b571b817b 100644
--- a/esphome/components/logger/__init__.py
+++ b/esphome/components/logger/__init__.py
@@ -127,7 +127,7 @@ CONFIG_SCHEMA = cv.All(
 
 
 @coroutine_with_priority(90.0)
-def to_code(config):
+async def to_code(config):
     baud_rate = config[CONF_BAUD_RATE]
     rhs = Logger.new(
         baud_rate,
@@ -177,13 +177,13 @@ def to_code(config):
         cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH")
 
     # Register at end for safe mode
-    yield cg.register_component(log, config)
+    await cg.register_component(log, config)
 
     for conf in config.get(CONF_ON_MESSAGE, []):
         trigger = cg.new_Pvariable(
             conf[CONF_TRIGGER_ID], log, LOG_LEVEL_SEVERITY.index(conf[CONF_LEVEL])
         )
-        yield automation.build_automation(
+        await automation.build_automation(
             trigger,
             [
                 (cg.int_, "level"),
@@ -209,14 +209,12 @@ def validate_printf(value):
     cfmt = """\
     (                                  # start of capture group 1
     %                                  # literal "%"
-    (?:                                # first option
     (?:[-+0 #]{0,5})                   # optional flags
     (?:\d+|\*)?                        # width
     (?:\.(?:\d+|\*))?                  # precision
     (?:h|l|ll|w|I|I32|I64)?            # size
     [cCdiouxXeEfgGaAnpsSZ]             # type
-    ) |                                # OR
-    %%)                                # literal "%%"
+    ) 
     """  # noqa
     matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.X)
     if len(matches) != len(value[CONF_ARGS]):
@@ -244,11 +242,11 @@ LOGGER_LOG_ACTION_SCHEMA = cv.All(
 
 
 @automation.register_action(CONF_LOGGER_LOG, LambdaAction, LOGGER_LOG_ACTION_SCHEMA)
-def logger_log_action_to_code(config, action_id, template_arg, args):
+async def logger_log_action_to_code(config, action_id, template_arg, args):
     esp_log = LOG_LEVEL_TO_ESP_LOG[config[CONF_LEVEL]]
     args_ = [cg.RawExpression(str(x)) for x in config[CONF_ARGS]]
 
     text = str(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_)))
 
-    lambda_ = yield cg.process_lambda(Lambda(text), args, return_type=cg.void)
-    yield cg.new_Pvariable(action_id, template_arg, lambda_)
+    lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void)
+    return cg.new_Pvariable(action_id, template_arg, lambda_)
diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp
index 8e7bd2ee49..9c65f494f0 100644
--- a/esphome/components/logger/logger.cpp
+++ b/esphome/components/logger/logger.cpp
@@ -136,7 +136,7 @@ void Logger::pre_setup() {
         break;
 #ifdef ARDUINO_ARCH_ESP32
       case UART_SELECTION_UART2:
-#if !CONFIG_IDF_TARGET_ESP32S2
+#if !CONFIG_IDF_TARGET_ESP32S2 && !CONFIG_IDF_TARGET_ESP32C3
         // FIXME: Validate in config that UART2 can't be set for ESP32-S2 (only has
         // UART0-UART1)
         this->hw_serial_ = &Serial2;
diff --git a/esphome/components/max31855/sensor.py b/esphome/components/max31855/sensor.py
index 5d3fa461b1..a8b5d25c61 100644
--- a/esphome/components/max31855/sensor.py
+++ b/esphome/components/max31855/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_REFERENCE_TEMPERATURE,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
 )
 
@@ -20,7 +21,11 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(MAX31855Sensor),
             cv.Optional(CONF_REFERENCE_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 2, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                2,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -29,11 +34,11 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield spi.register_spi_device(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await spi.register_spi_device(var, config)
+    await sensor.register_sensor(var, config)
     if CONF_REFERENCE_TEMPERATURE in config:
-        tc_ref = yield sensor.new_sensor(config[CONF_REFERENCE_TEMPERATURE])
+        tc_ref = await sensor.new_sensor(config[CONF_REFERENCE_TEMPERATURE])
         cg.add(var.set_reference_sensor(tc_ref))
diff --git a/esphome/components/max31856/sensor.py b/esphome/components/max31856/sensor.py
index afbb637609..9583c0bcf9 100644
--- a/esphome/components/max31856/sensor.py
+++ b/esphome/components/max31856/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_MAINS_FILTER,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
 )
 
@@ -21,7 +22,13 @@ FILTER = {
 }
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE)
+    sensor.sensor_schema(
+        UNIT_CELSIUS,
+        ICON_EMPTY,
+        1,
+        DEVICE_CLASS_TEMPERATURE,
+        STATE_CLASS_MEASUREMENT,
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(MAX31856Sensor),
@@ -35,9 +42,9 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield spi.register_spi_device(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await spi.register_spi_device(var, config)
+    await sensor.register_sensor(var, config)
     cg.add(var.set_filter(config[CONF_MAINS_FILTER]))
diff --git a/esphome/components/max31865/sensor.py b/esphome/components/max31865/sensor.py
index bebc42c95e..64495ebd7a 100644
--- a/esphome/components/max31865/sensor.py
+++ b/esphome/components/max31865/sensor.py
@@ -9,6 +9,7 @@ from esphome.const import (
     CONF_RTD_WIRES,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
 )
 
@@ -24,7 +25,9 @@ FILTER = {
 }
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_CELSIUS, ICON_EMPTY, 2, DEVICE_CLASS_TEMPERATURE)
+    sensor.sensor_schema(
+        UNIT_CELSIUS, ICON_EMPTY, 2, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(MAX31865Sensor),
@@ -45,11 +48,11 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield spi.register_spi_device(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await spi.register_spi_device(var, config)
+    await sensor.register_sensor(var, config)
     cg.add(var.set_reference_resistance(config[CONF_REFERENCE_RESISTANCE]))
     cg.add(var.set_nominal_resistance(config[CONF_RTD_NOMINAL_RESISTANCE]))
     cg.add(var.set_filter(config[CONF_MAINS_FILTER]))
diff --git a/esphome/components/max6675/sensor.py b/esphome/components/max6675/sensor.py
index 0713654f84..ad0e89c028 100644
--- a/esphome/components/max6675/sensor.py
+++ b/esphome/components/max6675/sensor.py
@@ -1,7 +1,13 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import sensor, spi
-from esphome.const import CONF_ID, DEVICE_CLASS_TEMPERATURE, ICON_EMPTY, UNIT_CELSIUS
+from esphome.const import (
+    CONF_ID,
+    DEVICE_CLASS_TEMPERATURE,
+    ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+)
 
 max6675_ns = cg.esphome_ns.namespace("max6675")
 MAX6675Sensor = max6675_ns.class_(
@@ -9,7 +15,9 @@ MAX6675Sensor = max6675_ns.class_(
 )
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE)
+    sensor.sensor_schema(
+        UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(MAX6675Sensor),
@@ -20,8 +28,8 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield spi.register_spi_device(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await spi.register_spi_device(var, config)
+    await sensor.register_sensor(var, config)
diff --git a/esphome/components/max7219/display.py b/esphome/components/max7219/display.py
index fba4d7ca76..391d033f24 100644
--- a/esphome/components/max7219/display.py
+++ b/esphome/components/max7219/display.py
@@ -27,18 +27,18 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield spi.register_spi_device(var, config)
-    yield display.register_display(var, config)
+    await cg.register_component(var, config)
+    await spi.register_spi_device(var, config)
+    await display.register_display(var, config)
 
     cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))
     cg.add(var.set_intensity(config[CONF_INTENSITY]))
     cg.add(var.set_reverse(config[CONF_REVERSE_ENABLE]))
 
     if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
+        lambda_ = await cg.process_lambda(
             config[CONF_LAMBDA], [(MAX7219ComponentRef, "it")], return_type=cg.void
         )
         cg.add(var.set_writer(lambda_))
diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp
index f58f203442..9c5c729487 100644
--- a/esphome/components/max7219/max7219.cpp
+++ b/esphome/components/max7219/max7219.cpp
@@ -41,7 +41,7 @@ const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = {
     0b01011111,            // '6', ord 0x36
     0b01110000,            // '7', ord 0x37
     0b01111111,            // '8', ord 0x38
-    0b01110011,            // '9', ord 0x39
+    0b01111011,            // '9', ord 0x39
     0b01001000,            // ':', ord 0x3A
     0b01011000,            // ';', ord 0x3B
     MAX7219_UNKNOWN_CHAR,  // '<', ord 0x3C
diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py
index 4863312b5a..e1ca128699 100644
--- a/esphome/components/max7219digit/display.py
+++ b/esphome/components/max7219digit/display.py
@@ -60,11 +60,11 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield spi.register_spi_device(var, config)
-    yield display.register_display(var, config)
+    await cg.register_component(var, config)
+    await spi.register_spi_device(var, config)
+    await display.register_display(var, config)
 
     cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))
     cg.add(var.set_intensity(config[CONF_INTENSITY]))
@@ -77,7 +77,7 @@ def to_code(config):
     cg.add(var.set_reverse(config[CONF_REVERSE_ENABLE]))
 
     if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
+        lambda_ = await cg.process_lambda(
             config[CONF_LAMBDA], [(MAX7219ComponentRef, "it")], return_type=cg.void
         )
         cg.add(var.set_writer(lambda_))
diff --git a/esphome/components/mcp23008/__init__.py b/esphome/components/mcp23008/__init__.py
index ae43e9ff0a..a534c9f87f 100644
--- a/esphome/components/mcp23008/__init__.py
+++ b/esphome/components/mcp23008/__init__.py
@@ -23,6 +23,6 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
-    var = yield mcp23xxx_base.register_mcp23xxx(config)
-    yield i2c.register_i2c_device(var, config)
+async def to_code(config):
+    var = await mcp23xxx_base.register_mcp23xxx(config)
+    await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/mcp23016/__init__.py b/esphome/components/mcp23016/__init__.py
index 2aa29c9cde..4d9657e794 100644
--- a/esphome/components/mcp23016/__init__.py
+++ b/esphome/components/mcp23016/__init__.py
@@ -28,10 +28,10 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
 
 CONF_MCP23016 = "mcp23016"
@@ -60,8 +60,8 @@ MCP23016_INPUT_PIN_SCHEMA = cv.Schema(
 @pins.PIN_SCHEMA_REGISTRY.register(
     CONF_MCP23016, (MCP23016_OUTPUT_PIN_SCHEMA, MCP23016_INPUT_PIN_SCHEMA)
 )
-def mcp23016_pin_to_code(config):
-    parent = yield cg.get_variable(config[CONF_MCP23016])
-    yield MCP23016GPIOPin.new(
+async def mcp23016_pin_to_code(config):
+    parent = await cg.get_variable(config[CONF_MCP23016])
+    return MCP23016GPIOPin.new(
         parent, config[CONF_NUMBER], config[CONF_MODE], config[CONF_INVERTED]
     )
diff --git a/esphome/components/mcp23017/__init__.py b/esphome/components/mcp23017/__init__.py
index a86133e232..42fc37dd1d 100644
--- a/esphome/components/mcp23017/__init__.py
+++ b/esphome/components/mcp23017/__init__.py
@@ -23,6 +23,6 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
-    var = yield mcp23xxx_base.register_mcp23xxx(config)
-    yield i2c.register_i2c_device(var, config)
+async def to_code(config):
+    var = await mcp23xxx_base.register_mcp23xxx(config)
+    await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/mcp23s08/__init__.py b/esphome/components/mcp23s08/__init__.py
index cfeb65de9b..4d3998def8 100644
--- a/esphome/components/mcp23s08/__init__.py
+++ b/esphome/components/mcp23s08/__init__.py
@@ -26,7 +26,7 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
-    var = yield mcp23xxx_base.register_mcp23xxx(config)
+async def to_code(config):
+    var = await mcp23xxx_base.register_mcp23xxx(config)
     cg.add(var.set_device_address(config[CONF_DEVICEADDRESS]))
-    yield spi.register_spi_device(var, config)
+    await spi.register_spi_device(var, config)
diff --git a/esphome/components/mcp23s17/__init__.py b/esphome/components/mcp23s17/__init__.py
index c38bd2617a..9e199f79c4 100644
--- a/esphome/components/mcp23s17/__init__.py
+++ b/esphome/components/mcp23s17/__init__.py
@@ -26,7 +26,7 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
-    var = yield mcp23xxx_base.register_mcp23xxx(config)
+async def to_code(config):
+    var = await mcp23xxx_base.register_mcp23xxx(config)
     cg.add(var.set_device_address(config[CONF_DEVICEADDRESS]))
-    yield spi.register_spi_device(var, config)
+    await spi.register_spi_device(var, config)
diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py
index f8ab224193..019b7c7e64 100644
--- a/esphome/components/mcp23xxx_base/__init__.py
+++ b/esphome/components/mcp23xxx_base/__init__.py
@@ -40,9 +40,9 @@ MCP23XXX_CONFIG_SCHEMA = cv.Schema(
 
 
 @coroutine
-def register_mcp23xxx(config):
+async def register_mcp23xxx(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
     cg.add(var.set_open_drain_ints(config[CONF_OPEN_DRAIN_INTERRUPT]))
     return var
 
@@ -79,9 +79,9 @@ MCP23XXX_INPUT_PIN_SCHEMA = cv.Schema(
 @pins.PIN_SCHEMA_REGISTRY.register(
     CONF_MCP23XXX, (MCP23XXX_OUTPUT_PIN_SCHEMA, MCP23XXX_INPUT_PIN_SCHEMA)
 )
-def mcp23xxx_pin_to_code(config):
-    parent = yield cg.get_variable(config[CONF_MCP23XXX])
-    yield MCP23XXXGPIOPin.new(
+async def mcp23xxx_pin_to_code(config):
+    parent = await cg.get_variable(config[CONF_MCP23XXX])
+    return MCP23XXXGPIOPin.new(
         parent,
         config[CONF_NUMBER],
         config[CONF_MODE],
diff --git a/esphome/components/mcp2515/canbus.py b/esphome/components/mcp2515/canbus.py
index 0fc679d17a..c410c1af69 100644
--- a/esphome/components/mcp2515/canbus.py
+++ b/esphome/components/mcp2515/canbus.py
@@ -35,10 +35,10 @@ CONFIG_SCHEMA = canbus.CANBUS_SCHEMA.extend(
 ).extend(spi.spi_device_schema(True))
 
 
-def to_code(config):
+async def to_code(config):
     rhs = mcp2515.new()
     var = cg.Pvariable(config[CONF_ID], rhs)
-    yield canbus.register_canbus(var, config)
+    await canbus.register_canbus(var, config)
     if CONF_CLOCK in config:
         canclock = CAN_CLOCK[config[CONF_CLOCK]]
         cg.add(var.set_mcp_clock(canclock))
@@ -46,4 +46,4 @@ def to_code(config):
         mode = MCP_MODE[config[CONF_MODE]]
         cg.add(var.set_mcp_mode(mode))
 
-    yield spi.register_spi_device(var, config)
+    await spi.register_spi_device(var, config)
diff --git a/esphome/components/mcp3008/__init__.py b/esphome/components/mcp3008/__init__.py
index 6a0410a75b..24a48664c1 100644
--- a/esphome/components/mcp3008/__init__.py
+++ b/esphome/components/mcp3008/__init__.py
@@ -19,7 +19,7 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(spi.spi_device_schema(cs_pin_required=True))
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield spi.register_spi_device(var, config)
+    await cg.register_component(var, config)
+    await spi.register_spi_device(var, config)
diff --git a/esphome/components/mcp3008/sensor.py b/esphome/components/mcp3008/sensor.py
index b146158552..4fc9b83afb 100644
--- a/esphome/components/mcp3008/sensor.py
+++ b/esphome/components/mcp3008/sensor.py
@@ -24,8 +24,8 @@ CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend(
 ).extend(cv.polling_component_schema("1s"))
 
 
-def to_code(config):
-    parent = yield cg.get_variable(config[CONF_MCP3008_ID])
+async def to_code(config):
+    parent = await cg.get_variable(config[CONF_MCP3008_ID])
     var = cg.new_Pvariable(
         config[CONF_ID],
         parent,
@@ -33,5 +33,5 @@ def to_code(config):
         config[CONF_NUMBER],
         config[CONF_REFERENCE_VOLTAGE],
     )
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
diff --git a/esphome/components/mcp4725/mcp4725.cpp b/esphome/components/mcp4725/mcp4725.cpp
index 85028d2f39..37ae7ac2b8 100644
--- a/esphome/components/mcp4725/mcp4725.cpp
+++ b/esphome/components/mcp4725/mcp4725.cpp
@@ -9,9 +9,9 @@ static const char *TAG = "mcp4725";
 void MCP4725::setup() {
   ESP_LOGCONFIG(TAG, "Setting up MCP4725 (0x%02X)...", this->address_);
 
-  this->parent_->raw_begin_transmission(this->address_);
+  this->raw_begin_transmission();
 
-  if (!this->parent_->raw_end_transmission(this->address_)) {
+  if (!this->raw_end_transmission()) {
     this->error_code_ = COMMUNICATION_FAILED;
     this->mark_failed();
 
diff --git a/esphome/components/mcp4725/output.py b/esphome/components/mcp4725/output.py
index a010ca9970..8f8b80d927 100644
--- a/esphome/components/mcp4725/output.py
+++ b/esphome/components/mcp4725/output.py
@@ -19,8 +19,8 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
-    yield output.register_output(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
+    await output.register_output(var, config)
diff --git a/esphome/components/mcp9808/sensor.py b/esphome/components/mcp9808/sensor.py
index 4973d41ff5..d417f45955 100644
--- a/esphome/components/mcp9808/sensor.py
+++ b/esphome/components/mcp9808/sensor.py
@@ -1,7 +1,13 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import i2c, sensor
-from esphome.const import CONF_ID, DEVICE_CLASS_TEMPERATURE, ICON_EMPTY, UNIT_CELSIUS
+from esphome.const import (
+    CONF_ID,
+    DEVICE_CLASS_TEMPERATURE,
+    ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+)
 
 CODEOWNERS = ["@k7hpn"]
 DEPENDENCIES = ["i2c"]
@@ -12,7 +18,9 @@ MCP9808Sensor = mcp9808_ns.class_(
 )
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE)
+    sensor.sensor_schema(
+        UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(MCP9808Sensor),
@@ -23,8 +31,8 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
+    await sensor.register_sensor(var, config)
diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py
index 6989814ada..ebcecb84e2 100644
--- a/esphome/components/mhz19/sensor.py
+++ b/esphome/components/mhz19/sensor.py
@@ -10,6 +10,7 @@ from esphome.const import (
     DEVICE_CLASS_EMPTY,
     DEVICE_CLASS_TEMPERATURE,
     ICON_MOLECULE_CO2,
+    STATE_CLASS_MEASUREMENT,
     UNIT_PARTS_PER_MILLION,
     UNIT_CELSIUS,
     ICON_EMPTY,
@@ -32,10 +33,18 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(MHZ19Component),
             cv.Required(CONF_CO2): sensor.sensor_schema(
-                UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, 0, DEVICE_CLASS_EMPTY
+                UNIT_PARTS_PER_MILLION,
+                ICON_MOLECULE_CO2,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 0, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.boolean,
         }
@@ -45,17 +54,17 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield uart.register_uart_device(var, config)
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
 
     if CONF_CO2 in config:
-        sens = yield sensor.new_sensor(config[CONF_CO2])
+        sens = await sensor.new_sensor(config[CONF_CO2])
         cg.add(var.set_co2_sensor(sens))
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature_sensor(sens))
 
     if CONF_AUTOMATIC_BASELINE_CALIBRATION in config:
@@ -78,6 +87,6 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id(
 @automation.register_action(
     "mhz19.abc_disable", MHZ19ABCDisableAction, CALIBRATION_ACTION_SCHEMA
 )
-def mhz19_calibration_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, paren)
+async def mhz19_calibration_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
diff --git a/esphome/components/midea_ac/climate.py b/esphome/components/midea_ac/climate.py
index 94aed91d4c..00aa979515 100644
--- a/esphome/components/midea_ac/climate.py
+++ b/esphome/components/midea_ac/climate.py
@@ -2,7 +2,13 @@ from esphome.components import climate, sensor
 import esphome.config_validation as cv
 import esphome.codegen as cg
 from esphome.const import (
+    CONF_CUSTOM_FAN_MODES,
+    CONF_CUSTOM_PRESETS,
     CONF_ID,
+    CONF_PRESET_BOOST,
+    CONF_PRESET_ECO,
+    CONF_PRESET_SLEEP,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
     UNIT_WATT,
@@ -17,7 +23,6 @@ from esphome.components.midea_dongle import CONF_MIDEA_DONGLE_ID, MideaDongle
 
 AUTO_LOAD = ["climate", "sensor", "midea_dongle"]
 CODEOWNERS = ["@dudanov"]
-
 CONF_BEEPER = "beeper"
 CONF_SWING_HORIZONTAL = "swing_horizontal"
 CONF_SWING_BOTH = "swing_both"
@@ -27,43 +32,80 @@ CONF_HUMIDITY_SETPOINT = "humidity_setpoint"
 midea_ac_ns = cg.esphome_ns.namespace("midea_ac")
 MideaAC = midea_ac_ns.class_("MideaAC", climate.Climate, cg.Component)
 
+CLIMATE_CUSTOM_FAN_MODES = {
+    "SILENT": "silent",
+    "TURBO": "turbo",
+}
+
+validate_climate_custom_fan_mode = cv.enum(CLIMATE_CUSTOM_FAN_MODES, upper=True)
+
+CLIMATE_CUSTOM_PRESETS = {
+    "FREEZE_PROTECTION": "freeze protection",
+}
+
+validate_climate_custom_preset = cv.enum(CLIMATE_CUSTOM_PRESETS, upper=True)
+
 CONFIG_SCHEMA = cv.All(
     climate.CLIMATE_SCHEMA.extend(
         {
             cv.GenerateID(): cv.declare_id(MideaAC),
             cv.GenerateID(CONF_MIDEA_DONGLE_ID): cv.use_id(MideaDongle),
             cv.Optional(CONF_BEEPER, default=False): cv.boolean,
+            cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list(
+                validate_climate_custom_fan_mode
+            ),
+            cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list(
+                validate_climate_custom_preset
+            ),
             cv.Optional(CONF_SWING_HORIZONTAL, default=False): cv.boolean,
             cv.Optional(CONF_SWING_BOTH, default=False): cv.boolean,
+            cv.Optional(CONF_PRESET_ECO, default=False): cv.boolean,
+            cv.Optional(CONF_PRESET_SLEEP, default=False): cv.boolean,
+            cv.Optional(CONF_PRESET_BOOST, default=False): cv.boolean,
             cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_THERMOMETER, 0, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_THERMOMETER,
+                0,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema(
-                UNIT_WATT, ICON_POWER, 0, DEVICE_CLASS_POWER
+                UNIT_WATT, ICON_POWER, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
             ),
             cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_WATER_PERCENT, 0, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_WATER_PERCENT,
+                0,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     ).extend(cv.COMPONENT_SCHEMA)
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield climate.register_climate(var, config)
-    paren = yield cg.get_variable(config[CONF_MIDEA_DONGLE_ID])
+    await cg.register_component(var, config)
+    await climate.register_climate(var, config)
+    paren = await cg.get_variable(config[CONF_MIDEA_DONGLE_ID])
     cg.add(var.set_midea_dongle_parent(paren))
     cg.add(var.set_beeper_feedback(config[CONF_BEEPER]))
+    if CONF_CUSTOM_FAN_MODES in config:
+        cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES]))
+    if CONF_CUSTOM_PRESETS in config:
+        cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS]))
     cg.add(var.set_swing_horizontal(config[CONF_SWING_HORIZONTAL]))
     cg.add(var.set_swing_both(config[CONF_SWING_BOTH]))
+    cg.add(var.set_preset_eco(config[CONF_PRESET_ECO]))
+    cg.add(var.set_preset_sleep(config[CONF_PRESET_SLEEP]))
+    cg.add(var.set_preset_boost(config[CONF_PRESET_BOOST]))
     if CONF_OUTDOOR_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
         cg.add(var.set_outdoor_temperature_sensor(sens))
     if CONF_POWER_USAGE in config:
-        sens = yield sensor.new_sensor(config[CONF_POWER_USAGE])
+        sens = await sensor.new_sensor(config[CONF_POWER_USAGE])
         cg.add(var.set_power_sensor(sens))
     if CONF_HUMIDITY_SETPOINT in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT])
         cg.add(var.set_humidity_setpoint_sensor(sens))
diff --git a/esphome/components/midea_ac/midea_climate.cpp b/esphome/components/midea_ac/midea_climate.cpp
index 8a74251696..f98cf74ac1 100644
--- a/esphome/components/midea_ac/midea_climate.cpp
+++ b/esphome/components/midea_ac/midea_climate.cpp
@@ -40,8 +40,24 @@ void MideaAC::on_frame(const midea_dongle::Frame &frame) {
   set_property(this->mode, p.get_mode(), need_publish);
   set_property(this->target_temperature, p.get_target_temp(), need_publish);
   set_property(this->current_temperature, p.get_indoor_temp(), need_publish);
-  set_property(this->fan_mode, p.get_fan_mode(), need_publish);
+  if (p.is_custom_fan_mode()) {
+    this->fan_mode.reset();
+    optional<std::string> mode = p.get_custom_fan_mode();
+    set_property(this->custom_fan_mode, mode, need_publish);
+  } else {
+    this->custom_fan_mode.reset();
+    optional<climate::ClimateFanMode> mode = p.get_fan_mode();
+    set_property(this->fan_mode, mode, need_publish);
+  }
   set_property(this->swing_mode, p.get_swing_mode(), need_publish);
+  if (p.is_custom_preset()) {
+    this->preset.reset();
+    optional<std::string> preset = p.get_custom_preset();
+    set_property(this->custom_preset, preset, need_publish);
+  } else {
+    this->custom_preset.reset();
+    set_property(this->preset, p.get_preset(), need_publish);
+  }
   if (need_publish)
     this->publish_state();
   set_sensor(this->outdoor_sensor_, p.get_outdoor_temp());
@@ -61,6 +77,48 @@ void MideaAC::on_update() {
   }
 }
 
+bool MideaAC::allow_preset(climate::ClimatePreset preset) const {
+  switch (preset) {
+    case climate::CLIMATE_PRESET_ECO:
+      if (this->mode == climate::CLIMATE_MODE_COOL) {
+        return true;
+      } else {
+        ESP_LOGD(TAG, "ECO preset is only available in COOL mode");
+      }
+      break;
+    case climate::CLIMATE_PRESET_SLEEP:
+      if (this->mode == climate::CLIMATE_MODE_FAN_ONLY || this->mode == climate::CLIMATE_MODE_DRY) {
+        ESP_LOGD(TAG, "SLEEP preset is not available in FAN_ONLY or DRY mode");
+      } else {
+        return true;
+      }
+      break;
+    case climate::CLIMATE_PRESET_BOOST:
+      if (this->mode == climate::CLIMATE_MODE_HEAT || this->mode == climate::CLIMATE_MODE_COOL) {
+        return true;
+      } else {
+        ESP_LOGD(TAG, "BOOST preset is only available in HEAT or COOL mode");
+      }
+      break;
+    case climate::CLIMATE_PRESET_HOME:
+      return true;
+    default:
+      break;
+  }
+  return false;
+}
+
+bool MideaAC::allow_custom_preset(const std::string &custom_preset) const {
+  if (custom_preset == MIDEA_FREEZE_PROTECTION_PRESET) {
+    if (this->mode == climate::CLIMATE_MODE_HEAT) {
+      return true;
+    } else {
+      ESP_LOGD(TAG, "%s is only available in HEAT mode", MIDEA_FREEZE_PROTECTION_PRESET.c_str());
+    }
+  }
+  return false;
+}
+
 void MideaAC::control(const climate::ClimateCall &call) {
   if (call.get_mode().has_value() && call.get_mode().value() != this->mode) {
     this->cmd_frame_.set_mode(call.get_mode().value());
@@ -70,14 +128,34 @@ void MideaAC::control(const climate::ClimateCall &call) {
     this->cmd_frame_.set_target_temp(call.get_target_temperature().value());
     this->ctrl_request_ = true;
   }
-  if (call.get_fan_mode().has_value() && call.get_fan_mode().value() != this->fan_mode) {
+  if (call.get_fan_mode().has_value() &&
+      (!this->fan_mode.has_value() || this->fan_mode.value() != call.get_fan_mode().value())) {
+    this->custom_fan_mode.reset();
     this->cmd_frame_.set_fan_mode(call.get_fan_mode().value());
     this->ctrl_request_ = true;
   }
+  if (call.get_custom_fan_mode().has_value() &&
+      (!this->custom_fan_mode.has_value() || this->custom_fan_mode.value() != call.get_custom_fan_mode().value())) {
+    this->fan_mode.reset();
+    this->cmd_frame_.set_custom_fan_mode(call.get_custom_fan_mode().value());
+    this->ctrl_request_ = true;
+  }
   if (call.get_swing_mode().has_value() && call.get_swing_mode().value() != this->swing_mode) {
     this->cmd_frame_.set_swing_mode(call.get_swing_mode().value());
     this->ctrl_request_ = true;
   }
+  if (call.get_preset().has_value() && this->allow_preset(call.get_preset().value()) &&
+      (!this->preset.has_value() || this->preset.value() != call.get_preset().value())) {
+    this->custom_preset.reset();
+    this->cmd_frame_.set_preset(call.get_preset().value());
+    this->ctrl_request_ = true;
+  }
+  if (call.get_custom_preset().has_value() && this->allow_custom_preset(call.get_custom_preset().value()) &&
+      (!this->custom_preset.has_value() || this->custom_preset.value() != call.get_custom_preset().value())) {
+    this->preset.reset();
+    this->cmd_frame_.set_custom_preset(call.get_custom_preset().value());
+    this->ctrl_request_ = true;
+  }
   if (this->ctrl_request_) {
     this->cmd_frame_.set_beeper_feedback(this->beeper_feedback_);
     this->cmd_frame_.finalize();
@@ -98,10 +176,16 @@ climate::ClimateTraits MideaAC::traits() {
   traits.set_supports_fan_mode_low(true);
   traits.set_supports_fan_mode_medium(true);
   traits.set_supports_fan_mode_high(true);
+  traits.set_supported_custom_fan_modes(this->traits_custom_fan_modes_);
   traits.set_supports_swing_mode_off(true);
   traits.set_supports_swing_mode_vertical(true);
   traits.set_supports_swing_mode_horizontal(this->traits_swing_horizontal_);
   traits.set_supports_swing_mode_both(this->traits_swing_both_);
+  traits.set_supports_preset_home(true);
+  traits.set_supports_preset_eco(this->traits_preset_eco_);
+  traits.set_supports_preset_sleep(this->traits_preset_sleep_);
+  traits.set_supports_preset_boost(this->traits_preset_boost_);
+  traits.set_supported_custom_presets(this->traits_custom_presets_);
   traits.set_supports_current_temperature(true);
   return traits;
 }
diff --git a/esphome/components/midea_ac/midea_climate.h b/esphome/components/midea_ac/midea_climate.h
index f08350b252..0a63312961 100644
--- a/esphome/components/midea_ac/midea_climate.h
+++ b/esphome/components/midea_ac/midea_climate.h
@@ -22,6 +22,15 @@ class MideaAC : public midea_dongle::MideaAppliance, public climate::Climate, pu
   void set_beeper_feedback(bool state) { this->beeper_feedback_ = state; }
   void set_swing_horizontal(bool state) { this->traits_swing_horizontal_ = state; }
   void set_swing_both(bool state) { this->traits_swing_both_ = state; }
+  void set_preset_eco(bool state) { this->traits_preset_eco_ = state; }
+  void set_preset_sleep(bool state) { this->traits_preset_sleep_ = state; }
+  void set_preset_boost(bool state) { this->traits_preset_boost_ = state; }
+  bool allow_preset(climate::ClimatePreset preset) const;
+  void set_custom_fan_modes(std::vector<std::string> custom_fan_modes) {
+    this->traits_custom_fan_modes_ = custom_fan_modes;
+  }
+  void set_custom_presets(std::vector<std::string> custom_presets) { this->traits_custom_presets_ = custom_presets; }
+  bool allow_custom_preset(const std::string &custom_preset) const;
 
  protected:
   /// Override control to change settings of the climate device.
@@ -41,6 +50,11 @@ class MideaAC : public midea_dongle::MideaAppliance, public climate::Climate, pu
   bool beeper_feedback_{false};
   bool traits_swing_horizontal_{false};
   bool traits_swing_both_{false};
+  bool traits_preset_eco_{false};
+  bool traits_preset_sleep_{false};
+  bool traits_preset_boost_{false};
+  std::vector<std::string> traits_custom_fan_modes_{{}};
+  std::vector<std::string> traits_custom_presets_{{}};
 };
 
 }  // namespace midea_ac
diff --git a/esphome/components/midea_ac/midea_frame.cpp b/esphome/components/midea_ac/midea_frame.cpp
index 2d9be1bdc5..e90155bad3 100644
--- a/esphome/components/midea_ac/midea_frame.cpp
+++ b/esphome/components/midea_ac/midea_frame.cpp
@@ -3,6 +3,11 @@
 namespace esphome {
 namespace midea_ac {
 
+static const char *TAG = "midea_ac";
+const std::string MIDEA_SILENT_FAN_MODE = "silent";
+const std::string MIDEA_TURBO_FAN_MODE = "turbo";
+const std::string MIDEA_FREEZE_PROTECTION_PRESET = "freeze protection";
+
 const uint8_t QueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x00,
                                     0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                                     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0x68};
@@ -80,6 +85,54 @@ void PropertiesFrame::set_mode(climate::ClimateMode mode) {
   this->pbuf_[12] |= m << 5;
 }
 
+optional<climate::ClimatePreset> PropertiesFrame::get_preset() const {
+  if (this->get_eco_mode()) {
+    return climate::CLIMATE_PRESET_ECO;
+  } else if (this->get_sleep_mode()) {
+    return climate::CLIMATE_PRESET_SLEEP;
+  } else if (this->get_turbo_mode()) {
+    return climate::CLIMATE_PRESET_BOOST;
+  } else {
+    return climate::CLIMATE_PRESET_HOME;
+  }
+}
+
+void PropertiesFrame::set_preset(climate::ClimatePreset preset) {
+  switch (preset) {
+    case climate::CLIMATE_PRESET_ECO:
+      this->set_eco_mode(true);
+      break;
+    case climate::CLIMATE_PRESET_SLEEP:
+      this->set_sleep_mode(true);
+      break;
+    case climate::CLIMATE_PRESET_BOOST:
+      this->set_turbo_mode(true);
+      break;
+    default:
+      break;
+  }
+}
+
+bool PropertiesFrame::is_custom_preset() const { return this->get_freeze_protection_mode(); }
+
+const std::string &PropertiesFrame::get_custom_preset() const { return midea_ac::MIDEA_FREEZE_PROTECTION_PRESET; };
+
+void PropertiesFrame::set_custom_preset(const std::string &preset) {
+  if (preset == MIDEA_FREEZE_PROTECTION_PRESET) {
+    this->set_freeze_protection_mode(true);
+  }
+}
+
+bool PropertiesFrame::is_custom_fan_mode() const {
+  switch (this->pbuf_[13]) {
+    case MIDEA_FAN_SILENT:
+    case MIDEA_FAN_TURBO:
+      return true;
+    default:
+      return false;
+  }
+}
+
 climate::ClimateFanMode PropertiesFrame::get_fan_mode() const {
   switch (this->pbuf_[13]) {
     case MIDEA_FAN_LOW:
@@ -112,6 +165,25 @@ void PropertiesFrame::set_fan_mode(climate::ClimateFanMode mode) {
   this->pbuf_[13] = m;
 }
 
+const std::string &PropertiesFrame::get_custom_fan_mode() const {
+  switch (this->pbuf_[13]) {
+    case MIDEA_FAN_SILENT:
+      return MIDEA_SILENT_FAN_MODE;
+    default:
+      return MIDEA_TURBO_FAN_MODE;
+  }
+}
+
+void PropertiesFrame::set_custom_fan_mode(const std::string &mode) {
+  uint8_t m;
+  if (mode == MIDEA_SILENT_FAN_MODE) {
+    m = MIDEA_FAN_SILENT;
+  } else {
+    m = MIDEA_FAN_TURBO;
+  }
+  this->pbuf_[13] = m;
+}
+
 climate::ClimateSwingMode PropertiesFrame::get_swing_mode() const {
   switch (this->pbuf_[17] & 0x0F) {
     case MIDEA_SWING_VERTICAL:
diff --git a/esphome/components/midea_ac/midea_frame.h b/esphome/components/midea_ac/midea_frame.h
index e07a5bf946..a84161b4af 100644
--- a/esphome/components/midea_ac/midea_frame.h
+++ b/esphome/components/midea_ac/midea_frame.h
@@ -5,6 +5,10 @@
 namespace esphome {
 namespace midea_ac {
 
+extern const std::string MIDEA_SILENT_FAN_MODE;
+extern const std::string MIDEA_TURBO_FAN_MODE;
+extern const std::string MIDEA_FREEZE_PROTECTION_PRESET;
+
 /// Enum for all modes a Midea device can be in.
 enum MideaMode : uint8_t {
   /// The Midea device is set to automatically change the heating/cooling cycle
@@ -23,12 +27,16 @@ enum MideaMode : uint8_t {
 enum MideaFanMode : uint8_t {
   /// The fan mode is set to Auto
   MIDEA_FAN_AUTO = 102,
+  /// The fan mode is set to Silent
+  MIDEA_FAN_SILENT = 20,
   /// The fan mode is set to Low
   MIDEA_FAN_LOW = 40,
   /// The fan mode is set to Medium
   MIDEA_FAN_MEDIUM = 60,
   /// The fan mode is set to High
   MIDEA_FAN_HIGH = 80,
+  /// The fan mode is set to Turbo
+  MIDEA_FAN_TURBO = 100,
 };
 
 /// Enum for all modes a Midea swing can be in
@@ -65,9 +73,13 @@ class PropertiesFrame : public midea_dongle::BaseFrame {
   void set_mode(climate::ClimateMode mode);
 
   /* FAN SPEED */
+  bool is_custom_fan_mode() const;
   climate::ClimateFanMode get_fan_mode() const;
   void set_fan_mode(climate::ClimateFanMode mode);
 
+  const std::string &get_custom_fan_mode() const;
+  void set_custom_fan_mode(const std::string &mode);
+
   /* SWING MODE */
   climate::ClimateSwingMode get_swing_mode() const;
   void set_swing_mode(climate::ClimateSwingMode mode);
@@ -82,16 +94,28 @@ class PropertiesFrame : public midea_dongle::BaseFrame {
   float get_humidity_setpoint() const;
 
   /* ECO MODE */
-  bool get_eco_mode() const { return this->pbuf_[19]; }
-  void set_eco_mode(bool state) { this->set_bytemask_(19, 0xFF, state); }
+  bool get_eco_mode() const { return this->pbuf_[19] & 0x10; }
+  void set_eco_mode(bool state) { this->set_bytemask_(19, 0x80, state); }
 
   /* SLEEP MODE */
   bool get_sleep_mode() const { return this->pbuf_[20] & 0x01; }
   void set_sleep_mode(bool state) { this->set_bytemask_(20, 0x01, state); }
 
   /* TURBO MODE */
-  bool get_turbo_mode() const { return this->pbuf_[20] & 0x02; }
-  void set_turbo_mode(bool state) { this->set_bytemask_(20, 0x02, state); }
+  bool get_turbo_mode() const { return this->pbuf_[18] & 0x20; }
+  void set_turbo_mode(bool state) { this->set_bytemask_(18, 0x20, state); }
+
+  /* FREEZE PROTECTION */
+  bool get_freeze_protection_mode() const { return this->pbuf_[31] & 0x80; }
+  void set_freeze_protection_mode(bool state) { this->set_bytemask_(31, 0x80, state); }
+
+  /* PRESET */
+  optional<climate::ClimatePreset> get_preset() const;
+  void set_preset(climate::ClimatePreset preset);
+
+  bool is_custom_preset() const;
+  const std::string &get_custom_preset() const;
+  void set_custom_preset(const std::string &preset);
 
   /* POWER USAGE */
   float get_power_usage() const;
diff --git a/esphome/components/midea_dongle/__init__.py b/esphome/components/midea_dongle/__init__.py
index 3efeb2661d..daa8ea6657 100644
--- a/esphome/components/midea_dongle/__init__.py
+++ b/esphome/components/midea_dongle/__init__.py
@@ -23,8 +23,8 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield uart.register_uart_device(var, config)
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
     cg.add(var.use_strength_icon(config[CONF_STRENGTH_ICON]))
diff --git a/esphome/components/mitsubishi/climate.py b/esphome/components/mitsubishi/climate.py
index c08baf7e54..0df17a5d16 100644
--- a/esphome/components/mitsubishi/climate.py
+++ b/esphome/components/mitsubishi/climate.py
@@ -3,6 +3,7 @@ import esphome.config_validation as cv
 from esphome.components import climate_ir
 from esphome.const import CONF_ID
 
+CODEOWNERS = ["@RubyBailey"]
 AUTO_LOAD = ["climate_ir"]
 
 mitsubishi_ns = cg.esphome_ns.namespace("mitsubishi")
@@ -15,6 +16,6 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield climate_ir.register_climate_ir(var, config)
+    await climate_ir.register_climate_ir(var, config)
diff --git a/esphome/components/mitsubishi/mitsubishi.cpp b/esphome/components/mitsubishi/mitsubishi.cpp
index b70aa6d394..dbc70af75c 100644
--- a/esphome/components/mitsubishi/mitsubishi.cpp
+++ b/esphome/components/mitsubishi/mitsubishi.cpp
@@ -23,8 +23,8 @@ const uint16_t MITSUBISHI_HEADER_SPACE = 1700;
 const uint16_t MITSUBISHI_MIN_GAP = 17500;
 
 void MitsubishiClimate::transmit_state() {
-  uint32_t remote_state[18] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x48, 0x00, 0x30,
-                               0x58, 0x61, 0x00, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00};
+  uint32_t remote_state[18] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x08, 0x00, 0x30,
+                               0x58, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
 
   switch (this->mode) {
     case climate::CLIMATE_MODE_COOL:
diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py
index e71f196fca..6b454cbaf0 100644
--- a/esphome/components/modbus/__init__.py
+++ b/esphome/components/modbus/__init__.py
@@ -1,8 +1,9 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
+from esphome.cpp_helpers import gpio_pin_expression
 from esphome.components import uart
-from esphome.const import CONF_ID, CONF_ADDRESS
-from esphome.core import coroutine
+from esphome.const import CONF_FLOW_CONTROL_PIN, CONF_ID, CONF_ADDRESS
+from esphome import pins
 
 DEPENDENCIES = ["uart"]
 
@@ -16,6 +17,7 @@ CONFIG_SCHEMA = (
     cv.Schema(
         {
             cv.GenerateID(): cv.declare_id(Modbus),
+            cv.Optional(CONF_FLOW_CONTROL_PIN): pins.gpio_output_pin_schema,
         }
     )
     .extend(cv.COMPONENT_SCHEMA)
@@ -23,12 +25,16 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     cg.add_global(modbus_ns.using)
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
-    yield uart.register_uart_device(var, config)
+    await uart.register_uart_device(var, config)
+
+    if CONF_FLOW_CONTROL_PIN in config:
+        pin = await gpio_pin_expression(config[CONF_FLOW_CONTROL_PIN])
+        cg.add(var.set_flow_control_pin(pin))
 
 
 def modbus_device_schema(default_address):
@@ -42,9 +48,8 @@ def modbus_device_schema(default_address):
     return cv.Schema(schema)
 
 
-@coroutine
-def register_modbus_device(var, config):
-    parent = yield cg.get_variable(config[CONF_MODBUS_ID])
+async def register_modbus_device(var, config):
+    parent = await cg.get_variable(config[CONF_MODBUS_ID])
     cg.add(var.set_parent(parent))
     cg.add(var.set_address(config[CONF_ADDRESS]))
     cg.add(parent.register_device(var))
diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp
index 74d0c40986..7820e14bf9 100644
--- a/esphome/components/modbus/modbus.cpp
+++ b/esphome/components/modbus/modbus.cpp
@@ -6,6 +6,11 @@ namespace modbus {
 
 static const char *TAG = "modbus";
 
+void Modbus::setup() {
+  if (this->flow_control_pin_ != nullptr) {
+    this->flow_control_pin_->setup();
+  }
+}
 void Modbus::loop() {
   const uint32_t now = millis();
   if (now - this->last_modbus_byte_ > 50) {
@@ -94,7 +99,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
 
 void Modbus::dump_config() {
   ESP_LOGCONFIG(TAG, "Modbus:");
-  this->check_uart_settings(9600, 2);
+  LOG_PIN("  Flow Control Pin: ", this->flow_control_pin_);
 }
 float Modbus::get_setup_priority() const {
   // After UART bus
@@ -112,7 +117,14 @@ void Modbus::send(uint8_t address, uint8_t function, uint16_t start_address, uin
   frame[6] = crc >> 0;
   frame[7] = crc >> 8;
 
+  if (this->flow_control_pin_ != nullptr)
+    this->flow_control_pin_->digital_write(true);
+
   this->write_array(frame, 8);
+  this->flush();
+
+  if (this->flow_control_pin_ != nullptr)
+    this->flow_control_pin_->digital_write(false);
 }
 
 }  // namespace modbus
diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h
index b75de147b1..876c46b688 100644
--- a/esphome/components/modbus/modbus.h
+++ b/esphome/components/modbus/modbus.h
@@ -12,6 +12,8 @@ class Modbus : public uart::UARTDevice, public Component {
  public:
   Modbus() = default;
 
+  void setup() override;
+
   void loop() override;
 
   void dump_config() override;
@@ -22,7 +24,11 @@ class Modbus : public uart::UARTDevice, public Component {
 
   void send(uint8_t address, uint8_t function, uint16_t start_address, uint16_t register_count);
 
+  void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; }
+
  protected:
+  GPIOPin *flow_control_pin_{nullptr};
+
   bool parse_modbus_byte_(uint8_t byte);
 
   std::vector<uint8_t> rx_buffer_;
@@ -30,6 +36,8 @@ class Modbus : public uart::UARTDevice, public Component {
   std::vector<ModbusDevice *> devices_;
 };
 
+uint16_t crc16(const uint8_t *data, uint8_t len);
+
 class ModbusDevice {
  public:
   void set_parent(Modbus *parent) { parent_ = parent; }
diff --git a/esphome/components/monochromatic/light.py b/esphome/components/monochromatic/light.py
index 32d9981a57..8f13f58f89 100644
--- a/esphome/components/monochromatic/light.py
+++ b/esphome/components/monochromatic/light.py
@@ -16,9 +16,9 @@ CONFIG_SCHEMA = light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
-    yield light.register_light(var, config)
+    await light.register_light(var, config)
 
-    out = yield cg.get_variable(config[CONF_OUTPUT])
+    out = await cg.get_variable(config[CONF_OUTPUT])
     cg.add(var.set_output(out))
diff --git a/esphome/components/mpr121/__init__.py b/esphome/components/mpr121/__init__.py
index eb3043c2b1..dabfb47ad6 100644
--- a/esphome/components/mpr121/__init__.py
+++ b/esphome/components/mpr121/__init__.py
@@ -35,11 +35,11 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     cg.add(var.set_touch_debounce(config[CONF_TOUCH_DEBOUNCE]))
     cg.add(var.set_release_debounce(config[CONF_RELEASE_DEBOUNCE]))
     cg.add(var.set_touch_threshold(config[CONF_TOUCH_THRESHOLD]))
     cg.add(var.set_release_threshold(config[CONF_RELEASE_THRESHOLD]))
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/mpr121/binary_sensor.py b/esphome/components/mpr121/binary_sensor.py
index 68e56075f5..20b80e063e 100644
--- a/esphome/components/mpr121/binary_sensor.py
+++ b/esphome/components/mpr121/binary_sensor.py
@@ -24,10 +24,10 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield binary_sensor.register_binary_sensor(var, config)
-    hub = yield cg.get_variable(config[CONF_MPR121_ID])
+    await binary_sensor.register_binary_sensor(var, config)
+    hub = await cg.get_variable(config[CONF_MPR121_ID])
     cg.add(var.set_channel(config[CONF_CHANNEL]))
 
     if CONF_TOUCH_THRESHOLD in config:
diff --git a/esphome/components/mpu6050/sensor.py b/esphome/components/mpu6050/sensor.py
index 11d491006e..05c26289b4 100644
--- a/esphome/components/mpu6050/sensor.py
+++ b/esphome/components/mpu6050/sensor.py
@@ -8,6 +8,7 @@ from esphome.const import (
     DEVICE_CLASS_TEMPERATURE,
     ICON_BRIEFCASE_DOWNLOAD,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_METER_PER_SECOND_SQUARED,
     ICON_SCREEN_ROTATION,
     UNIT_DEGREE_PER_SECOND,
@@ -29,13 +30,21 @@ MPU6050Component = mpu6050_ns.class_(
 )
 
 accel_schema = sensor.sensor_schema(
-    UNIT_METER_PER_SECOND_SQUARED, ICON_BRIEFCASE_DOWNLOAD, 2, DEVICE_CLASS_EMPTY
+    UNIT_METER_PER_SECOND_SQUARED,
+    ICON_BRIEFCASE_DOWNLOAD,
+    2,
+    DEVICE_CLASS_EMPTY,
+    STATE_CLASS_MEASUREMENT,
 )
 gyro_schema = sensor.sensor_schema(
-    UNIT_DEGREE_PER_SECOND, ICON_SCREEN_ROTATION, 2, DEVICE_CLASS_EMPTY
+    UNIT_DEGREE_PER_SECOND,
+    ICON_SCREEN_ROTATION,
+    2,
+    DEVICE_CLASS_EMPTY,
+    STATE_CLASS_MEASUREMENT,
 )
 temperature_schema = sensor.sensor_schema(
-    UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+    UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT
 )
 
 CONFIG_SCHEMA = (
@@ -56,21 +65,21 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     for d in ["x", "y", "z"]:
         accel_key = f"accel_{d}"
         if accel_key in config:
-            sens = yield sensor.new_sensor(config[accel_key])
+            sens = await sensor.new_sensor(config[accel_key])
             cg.add(getattr(var, f"set_accel_{d}_sensor")(sens))
         accel_key = f"gyro_{d}"
         if accel_key in config:
-            sens = yield sensor.new_sensor(config[accel_key])
+            sens = await sensor.new_sensor(config[accel_key])
             cg.add(getattr(var, f"set_gyro_{d}_sensor")(sens))
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature_sensor(sens))
diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py
index e90f90cd6a..906c570b17 100644
--- a/esphome/components/mqtt/__init__.py
+++ b/esphome/components/mqtt/__init__.py
@@ -37,7 +37,7 @@ from esphome.const import (
     CONF_USERNAME,
     CONF_WILL_MESSAGE,
 )
-from esphome.core import coroutine_with_priority, coroutine, CORE
+from esphome.core import coroutine_with_priority, CORE
 
 DEPENDENCIES = ["network"]
 AUTO_LOAD = ["json", "async_tcp"]
@@ -207,9 +207,9 @@ def exp_mqtt_message(config):
 
 
 @coroutine_with_priority(40.0)
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
     # https://github.com/OttoWinter/async-mqtt-client/blob/master/library.json
     cg.add_library("AsyncMqttClient-esphome", "0.8.4")
@@ -279,12 +279,12 @@ def to_code(config):
         cg.add(trig.set_qos(conf[CONF_QOS]))
         if CONF_PAYLOAD in conf:
             cg.add(trig.set_payload(conf[CONF_PAYLOAD]))
-        yield cg.register_component(trig, conf)
-        yield automation.build_automation(trig, [(cg.std_string, "x")], conf)
+        await cg.register_component(trig, conf)
+        await automation.build_automation(trig, [(cg.std_string, "x")], conf)
 
     for conf in config.get(CONF_ON_JSON_MESSAGE, []):
         trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf[CONF_TOPIC], conf[CONF_QOS])
-        yield automation.build_automation(trig, [(cg.JsonObjectConstRef, "x")], conf)
+        await automation.build_automation(trig, [(cg.JsonObjectConstRef, "x")], conf)
 
 
 MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema(
@@ -301,19 +301,19 @@ MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema(
 @automation.register_action(
     "mqtt.publish", MQTTPublishAction, MQTT_PUBLISH_ACTION_SCHEMA
 )
-def mqtt_publish_action_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def mqtt_publish_action_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    template_ = yield cg.templatable(config[CONF_TOPIC], args, cg.std_string)
+    template_ = await cg.templatable(config[CONF_TOPIC], args, cg.std_string)
     cg.add(var.set_topic(template_))
 
-    template_ = yield cg.templatable(config[CONF_PAYLOAD], args, cg.std_string)
+    template_ = await cg.templatable(config[CONF_PAYLOAD], args, cg.std_string)
     cg.add(var.set_payload(template_))
-    template_ = yield cg.templatable(config[CONF_QOS], args, cg.uint8)
+    template_ = await cg.templatable(config[CONF_QOS], args, cg.uint8)
     cg.add(var.set_qos(template_))
-    template_ = yield cg.templatable(config[CONF_RETAIN], args, bool)
+    template_ = await cg.templatable(config[CONF_RETAIN], args, bool)
     cg.add(var.set_retain(template_))
-    yield var
+    return var
 
 
 MQTT_PUBLISH_JSON_ACTION_SCHEMA = cv.Schema(
@@ -330,20 +330,20 @@ MQTT_PUBLISH_JSON_ACTION_SCHEMA = cv.Schema(
 @automation.register_action(
     "mqtt.publish_json", MQTTPublishJsonAction, MQTT_PUBLISH_JSON_ACTION_SCHEMA
 )
-def mqtt_publish_json_action_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def mqtt_publish_json_action_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    template_ = yield cg.templatable(config[CONF_TOPIC], args, cg.std_string)
+    template_ = await cg.templatable(config[CONF_TOPIC], args, cg.std_string)
     cg.add(var.set_topic(template_))
 
     args_ = args + [(cg.JsonObjectRef, "root")]
-    lambda_ = yield cg.process_lambda(config[CONF_PAYLOAD], args_, return_type=cg.void)
+    lambda_ = await cg.process_lambda(config[CONF_PAYLOAD], args_, return_type=cg.void)
     cg.add(var.set_payload(lambda_))
-    template_ = yield cg.templatable(config[CONF_QOS], args, cg.uint8)
+    template_ = await cg.templatable(config[CONF_QOS], args, cg.uint8)
     cg.add(var.set_qos(template_))
-    template_ = yield cg.templatable(config[CONF_RETAIN], args, bool)
+    template_ = await cg.templatable(config[CONF_RETAIN], args, bool)
     cg.add(var.set_retain(template_))
-    yield var
+    return var
 
 
 def get_default_topic_for(data, component_type, name, suffix):
@@ -356,9 +356,8 @@ def get_default_topic_for(data, component_type, name, suffix):
     )
 
 
-@coroutine
-def register_mqtt_component(var, config):
-    yield cg.register_component(var, {})
+async def register_mqtt_component(var, config):
+    await cg.register_component(var, {})
 
     if CONF_RETAIN in config:
         cg.add(var.set_retain(config[CONF_RETAIN]))
@@ -391,6 +390,6 @@ def register_mqtt_component(var, config):
         }
     ),
 )
-def mqtt_connected_to_code(config, condition_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(condition_id, template_arg, paren)
+async def mqtt_connected_to_code(config, condition_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(condition_id, template_arg, paren)
diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp
index b8743fc142..3330956b52 100644
--- a/esphome/components/mqtt/mqtt_client.cpp
+++ b/esphome/components/mqtt/mqtt_client.cpp
@@ -565,15 +565,16 @@ MQTTMessageTrigger::MQTTMessageTrigger(const std::string &topic) : topic_(topic)
 void MQTTMessageTrigger::set_qos(uint8_t qos) { this->qos_ = qos; }
 void MQTTMessageTrigger::set_payload(const std::string &payload) { this->payload_ = payload; }
 void MQTTMessageTrigger::setup() {
-  global_mqtt_client->subscribe(this->topic_,
-                                [this](const std::string &topic, const std::string &payload) {
-                                  if (this->payload_.has_value() && payload != *this->payload_) {
-                                    return;
-                                  }
+  global_mqtt_client->subscribe(
+      this->topic_,
+      [this](const std::string &topic, const std::string &payload) {
+        if (this->payload_.has_value() && payload != *this->payload_) {
+          return;
+        }
 
-                                  this->trigger(payload);
-                                },
-                                this->qos_);
+        this->trigger(payload);
+      },
+      this->qos_);
 }
 void MQTTMessageTrigger::dump_config() {
   ESP_LOGCONFIG(TAG, "MQTT Message Trigger:");
diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp
index 2a95ed2f64..47923dc924 100644
--- a/esphome/components/mqtt/mqtt_climate.cpp
+++ b/esphome/components/mqtt/mqtt_climate.cpp
@@ -35,6 +35,8 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC
     modes.add("fan_only");
   if (traits.supports_mode(CLIMATE_MODE_DRY))
     modes.add("dry");
+  if (traits.supports_mode(CLIMATE_MODE_HEAT_COOL))
+    modes.add("heat_cool");
 
   if (traits.get_supports_two_point_target_temperature()) {
     // temperature_low_command_topic
@@ -231,6 +233,9 @@ bool MQTTClimateComponent::publish_state_() {
     case CLIMATE_MODE_DRY:
       mode_s = "dry";
       break;
+    case CLIMATE_MODE_HEAT_COOL:
+      mode_s = "heat_cool";
+      break;
   }
   bool success = true;
   if (!this->publish(this->get_mode_state_topic(), mode_s))
@@ -287,7 +292,7 @@ bool MQTTClimateComponent::publish_state_() {
 
   if (traits.get_supports_fan_modes()) {
     const char *payload = "";
-    switch (this->device_->fan_mode) {
+    switch (this->device_->fan_mode.value()) {
       case CLIMATE_FAN_ON:
         payload = "on";
         break;
diff --git a/esphome/components/mqtt_subscribe/sensor/__init__.py b/esphome/components/mqtt_subscribe/sensor/__init__.py
index 00c2d324d3..d640b254de 100644
--- a/esphome/components/mqtt_subscribe/sensor/__init__.py
+++ b/esphome/components/mqtt_subscribe/sensor/__init__.py
@@ -5,6 +5,7 @@ from esphome.const import (
     CONF_ID,
     CONF_QOS,
     CONF_TOPIC,
+    STATE_CLASS_NONE,
     UNIT_EMPTY,
     ICON_EMPTY,
     DEVICE_CLASS_EMPTY,
@@ -19,7 +20,9 @@ MQTTSubscribeSensor = mqtt_subscribe_ns.class_(
 )
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY)
+    sensor.sensor_schema(
+        UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(MQTTSubscribeSensor),
@@ -32,12 +35,12 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
 
-    parent = yield cg.get_variable(config[CONF_MQTT_PARENT_ID])
+    parent = await cg.get_variable(config[CONF_MQTT_PARENT_ID])
     cg.add(var.set_parent(parent))
     cg.add(var.set_topic(config[CONF_TOPIC]))
     cg.add(var.set_qos(config[CONF_QOS]))
diff --git a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp
index 50977e98ca..f6ecb0df36 100644
--- a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp
+++ b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp
@@ -7,18 +7,19 @@ namespace mqtt_subscribe {
 static const char *TAG = "mqtt_subscribe.sensor";
 
 void MQTTSubscribeSensor::setup() {
-  mqtt::global_mqtt_client->subscribe(this->topic_,
-                                      [this](const std::string &topic, std::string payload) {
-                                        auto val = parse_float(payload);
-                                        if (!val.has_value()) {
-                                          ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str());
-                                          this->publish_state(NAN);
-                                          return;
-                                        }
+  mqtt::global_mqtt_client->subscribe(
+      this->topic_,
+      [this](const std::string &topic, std::string payload) {
+        auto val = parse_float(payload);
+        if (!val.has_value()) {
+          ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str());
+          this->publish_state(NAN);
+          return;
+        }
 
-                                        this->publish_state(*val);
-                                      },
-                                      this->qos_);
+        this->publish_state(*val);
+      },
+      this->qos_);
 }
 
 float MQTTSubscribeSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
diff --git a/esphome/components/mqtt_subscribe/text_sensor/__init__.py b/esphome/components/mqtt_subscribe/text_sensor/__init__.py
index b5f2c4307b..477e4dec45 100644
--- a/esphome/components/mqtt_subscribe/text_sensor/__init__.py
+++ b/esphome/components/mqtt_subscribe/text_sensor/__init__.py
@@ -21,12 +21,12 @@ CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield text_sensor.register_text_sensor(var, config)
+    await cg.register_component(var, config)
+    await text_sensor.register_text_sensor(var, config)
 
-    parent = yield cg.get_variable(config[CONF_MQTT_PARENT_ID])
+    parent = await cg.get_variable(config[CONF_MQTT_PARENT_ID])
     cg.add(var.set_parent(parent))
     cg.add(var.set_topic(config[CONF_TOPIC]))
     cg.add(var.set_qos(config[CONF_QOS]))
diff --git a/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.cpp b/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.cpp
index fdab5cf6d7..e7373c4b95 100644
--- a/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.cpp
+++ b/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.cpp
@@ -7,9 +7,9 @@ namespace mqtt_subscribe {
 static const char *TAG = "mqtt_subscribe.text_sensor";
 
 void MQTTSubscribeTextSensor::setup() {
-  this->parent_->subscribe(this->topic_,
-                           [this](const std::string &topic, std::string payload) { this->publish_state(payload); },
-                           this->qos_);
+  this->parent_->subscribe(
+      this->topic_, [this](const std::string &topic, std::string payload) { this->publish_state(payload); },
+      this->qos_);
 }
 float MQTTSubscribeTextSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
 void MQTTSubscribeTextSensor::set_qos(uint8_t qos) { this->qos_ = qos; }
diff --git a/esphome/components/ms5611/sensor.py b/esphome/components/ms5611/sensor.py
index d180008140..34198e04eb 100644
--- a/esphome/components/ms5611/sensor.py
+++ b/esphome/components/ms5611/sensor.py
@@ -8,6 +8,7 @@ from esphome.const import (
     DEVICE_CLASS_PRESSURE,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     ICON_GAUGE,
     UNIT_HECTOPASCAL,
@@ -25,10 +26,18 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(MS5611Component),
             cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Required(CONF_PRESSURE): sensor.sensor_schema(
-                UNIT_HECTOPASCAL, ICON_GAUGE, 1, DEVICE_CLASS_PRESSURE
+                UNIT_HECTOPASCAL,
+                ICON_GAUGE,
+                1,
+                DEVICE_CLASS_PRESSURE,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -37,15 +46,15 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature_sensor(sens))
 
     if CONF_PRESSURE in config:
-        sens = yield sensor.new_sensor(config[CONF_PRESSURE])
+        sens = await sensor.new_sensor(config[CONF_PRESSURE])
         cg.add(var.set_pressure_sensor(sens))
diff --git a/esphome/components/my9231/__init__.py b/esphome/components/my9231/__init__.py
index ed1edd7b2d..58419450cd 100644
--- a/esphome/components/my9231/__init__.py
+++ b/esphome/components/my9231/__init__.py
@@ -27,13 +27,13 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
-    di = yield cg.gpio_pin_expression(config[CONF_DATA_PIN])
+    di = await cg.gpio_pin_expression(config[CONF_DATA_PIN])
     cg.add(var.set_pin_di(di))
-    dcki = yield cg.gpio_pin_expression(config[CONF_CLOCK_PIN])
+    dcki = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN])
     cg.add(var.set_pin_dcki(dcki))
 
     cg.add(var.set_num_channels(config[CONF_NUM_CHANNELS]))
diff --git a/esphome/components/my9231/output.py b/esphome/components/my9231/output.py
index 30db84064d..a3c16fd49a 100644
--- a/esphome/components/my9231/output.py
+++ b/esphome/components/my9231/output.py
@@ -18,10 +18,10 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield output.register_output(var, config)
+    await output.register_output(var, config)
 
-    parent = yield cg.get_variable(config[CONF_MY9231_ID])
+    parent = await cg.get_variable(config[CONF_MY9231_ID])
     cg.add(var.set_parent(parent))
     cg.add(var.set_channel(config[CONF_CHANNEL]))
diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py
index 0cf8057b8e..59d784a614 100644
--- a/esphome/components/neopixelbus/light.py
+++ b/esphome/components/neopixelbus/light.py
@@ -150,7 +150,7 @@ def format_method(config):
     raise NotImplementedError
 
 
-def validate(config):
+def _validate(config):
     if CONF_PIN in config:
         if CONF_CLOCK_PIN in config or CONF_DATA_PIN in config:
             raise cv.Invalid("Cannot specify both 'pin' and 'clock_pin'+'data_pin'")
@@ -176,12 +176,12 @@ CONFIG_SCHEMA = cv.All(
             cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
         }
     ).extend(cv.COMPONENT_SCHEMA),
-    validate,
+    _validate,
     validate_method_pin,
 )
 
 
-def to_code(config):
+async def to_code(config):
     has_white = "W" in config[CONF_TYPE]
     template = cg.TemplateArguments(getattr(cg.global_ns, format_method(config)))
     if has_white:
@@ -190,8 +190,8 @@ def to_code(config):
         out_type = NeoPixelRGBLightOutput.template(template)
     rhs = out_type.new()
     var = cg.Pvariable(config[CONF_OUTPUT_ID], rhs, out_type)
-    yield light.register_light(var, config)
-    yield cg.register_component(var, config)
+    await light.register_light(var, config)
+    await cg.register_component(var, config)
 
     if CONF_PIN in config:
         cg.add(var.add_leds(config[CONF_NUM_LEDS], config[CONF_PIN]))
@@ -205,4 +205,4 @@ def to_code(config):
     cg.add(var.set_pixel_order(getattr(ESPNeoPixelOrder, config[CONF_TYPE])))
 
     # https://github.com/Makuna/NeoPixelBus/blob/master/library.json
-    cg.add_library("NeoPixelBus-esphome", "2.5.7")
+    cg.add_library("NeoPixelBus-esphome", "2.6.2")
diff --git a/esphome/components/nextion/binary_sensor.py b/esphome/components/nextion/binary_sensor.py
index e822b65eb5..ed4e8d832a 100644
--- a/esphome/components/nextion/binary_sensor.py
+++ b/esphome/components/nextion/binary_sensor.py
@@ -23,11 +23,11 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield binary_sensor.register_binary_sensor(var, config)
+    await binary_sensor.register_binary_sensor(var, config)
 
-    hub = yield cg.get_variable(config[CONF_NEXTION_ID])
+    hub = await cg.get_variable(config[CONF_NEXTION_ID])
     cg.add(hub.register_touch_component(var))
 
     cg.add(var.set_component_id(config[CONF_COMPONENT_ID]))
diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py
index 483395fe2f..7d7018a4c4 100644
--- a/esphome/components/nextion/display.py
+++ b/esphome/components/nextion/display.py
@@ -22,17 +22,17 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield uart.register_uart_device(var, config)
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
 
     if CONF_BRIGHTNESS in config:
         cg.add(var.set_brightness(config[CONF_BRIGHTNESS]))
     if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
+        lambda_ = await cg.process_lambda(
             config[CONF_LAMBDA], [(NextionRef, "it")], return_type=cg.void
         )
         cg.add(var.set_writer(lambda_))
 
-    yield display.register_display(var, config)
+    await display.register_display(var, config)
diff --git a/esphome/components/ntc/sensor.py b/esphome/components/ntc/sensor.py
index f3505eec53..e7b8c03586 100644
--- a/esphome/components/ntc/sensor.py
+++ b/esphome/components/ntc/sensor.py
@@ -13,6 +13,7 @@ from esphome.const import (
     CONF_VALUE,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
 )
 
@@ -118,7 +119,9 @@ def process_calibration(value):
 
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE)
+    sensor.sensor_schema(
+        UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(NTC),
@@ -130,12 +133,12 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
 
-    sens = yield cg.get_variable(config[CONF_SENSOR])
+    sens = await cg.get_variable(config[CONF_SENSOR])
     cg.add(var.set_sensor(sens))
     calib = config[CONF_CALIBRATION]
     cg.add(var.set_a(calib[CONF_A]))
diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py
index 25a278f5bf..7ee7ef47ca 100644
--- a/esphome/components/ota/__init__.py
+++ b/esphome/components/ota/__init__.py
@@ -32,12 +32,12 @@ CONFIG_SCHEMA = cv.Schema(
 
 
 @coroutine_with_priority(50.0)
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     cg.add(var.set_port(config[CONF_PORT]))
     cg.add(var.set_auth_password(config[CONF_PASSWORD]))
 
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
     if config[CONF_SAFE_MODE]:
         condition = var.should_enter_safe_mode(
diff --git a/esphome/components/output/__init__.py b/esphome/components/output/__init__.py
index 487bd8cba5..4471794033 100644
--- a/esphome/components/output/__init__.py
+++ b/esphome/components/output/__init__.py
@@ -11,7 +11,7 @@ from esphome.const import (
     CONF_MIN_POWER,
     CONF_POWER_SUPPLY,
 )
-from esphome.core import CORE, coroutine
+from esphome.core import CORE
 
 
 CODEOWNERS = ["@esphome/core"]
@@ -43,12 +43,11 @@ TurnOnAction = output_ns.class_("TurnOnAction", automation.Action)
 SetLevelAction = output_ns.class_("SetLevelAction", automation.Action)
 
 
-@coroutine
-def setup_output_platform_(obj, config):
+async def setup_output_platform_(obj, config):
     if CONF_INVERTED in config:
         cg.add(obj.set_inverted(config[CONF_INVERTED]))
     if CONF_POWER_SUPPLY in config:
-        power_supply_ = yield cg.get_variable(config[CONF_POWER_SUPPLY])
+        power_supply_ = await cg.get_variable(config[CONF_POWER_SUPPLY])
         cg.add(obj.set_power_supply(power_supply_))
     if CONF_MAX_POWER in config:
         cg.add(obj.set_max_power(config[CONF_MAX_POWER]))
@@ -56,11 +55,10 @@ def setup_output_platform_(obj, config):
         cg.add(obj.set_min_power(config[CONF_MIN_POWER]))
 
 
-@coroutine
-def register_output(var, config):
+async def register_output(var, config):
     if not CORE.has_id(config[CONF_ID]):
         var = cg.Pvariable(config[CONF_ID], var)
-    yield setup_output_platform_(var, config)
+    await setup_output_platform_(var, config)
 
 
 BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id(
@@ -71,17 +69,17 @@ BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id(
 
 
 @automation.register_action("output.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA)
-def output_turn_on_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, paren)
+async def output_turn_on_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
 
 
 @automation.register_action(
     "output.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA
 )
-def output_turn_off_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, paren)
+async def output_turn_off_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
 
 
 @automation.register_action(
@@ -94,13 +92,13 @@ def output_turn_off_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def output_set_level_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def output_set_level_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    template_ = yield cg.templatable(config[CONF_LEVEL], args, float)
+    template_ = await cg.templatable(config[CONF_LEVEL], args, float)
     cg.add(var.set_level(template_))
-    yield var
+    return var
 
 
-def to_code(config):
+async def to_code(config):
     cg.add_global(output_ns.using)
diff --git a/esphome/components/output/switch/__init__.py b/esphome/components/output/switch/__init__.py
index 14027de74c..11d073d28c 100644
--- a/esphome/components/output/switch/__init__.py
+++ b/esphome/components/output/switch/__init__.py
@@ -14,10 +14,10 @@ CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield switch.register_switch(var, config)
+    await cg.register_component(var, config)
+    await switch.register_switch(var, config)
 
-    output_ = yield cg.get_variable(config[CONF_OUTPUT])
+    output_ = await cg.get_variable(config[CONF_OUTPUT])
     cg.add(var.set_output(output_))
diff --git a/esphome/components/partition/light.py b/esphome/components/partition/light.py
index 202481b936..5ded6b906c 100644
--- a/esphome/components/partition/light.py
+++ b/esphome/components/partition/light.py
@@ -1,7 +1,14 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import light
-from esphome.const import CONF_FROM, CONF_ID, CONF_SEGMENTS, CONF_TO, CONF_OUTPUT_ID
+from esphome.const import (
+    CONF_FROM,
+    CONF_ID,
+    CONF_SEGMENTS,
+    CONF_TO,
+    CONF_OUTPUT_ID,
+    CONF_REVERSED,
+)
 
 partitions_ns = cg.esphome_ns.namespace("partition")
 AddressableSegment = partitions_ns.class_("AddressableSegment")
@@ -28,6 +35,7 @@ CONFIG_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend(
                     cv.Required(CONF_ID): cv.use_id(light.AddressableLightState),
                     cv.Required(CONF_FROM): cv.positive_int,
                     cv.Required(CONF_TO): cv.positive_int,
+                    cv.Optional(CONF_REVERSED, default=False): cv.boolean,
                 },
                 validate_from_to,
             ),
@@ -37,16 +45,19 @@ CONFIG_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     segments = []
     for conf in config[CONF_SEGMENTS]:
-        var = yield cg.get_variable(conf[CONF_ID])
+        var = await cg.get_variable(conf[CONF_ID])
         segments.append(
             AddressableSegment(
-                var, conf[CONF_FROM], conf[CONF_TO] - conf[CONF_FROM] + 1
+                var,
+                conf[CONF_FROM],
+                conf[CONF_TO] - conf[CONF_FROM] + 1,
+                conf[CONF_REVERSED],
             )
         )
 
     var = cg.new_Pvariable(config[CONF_OUTPUT_ID], segments)
-    yield cg.register_component(var, config)
-    yield light.register_light(var, config)
+    await cg.register_component(var, config)
+    await light.register_light(var, config)
diff --git a/esphome/components/partition/light_partition.h b/esphome/components/partition/light_partition.h
index 8085c43720..11589ee539 100644
--- a/esphome/components/partition/light_partition.h
+++ b/esphome/components/partition/light_partition.h
@@ -8,20 +8,25 @@ namespace partition {
 
 class AddressableSegment {
  public:
-  AddressableSegment(light::LightState *src, int32_t src_offset, int32_t size)
-      : src_(static_cast<light::AddressableLight *>(src->get_output())), src_offset_(src_offset), size_(size) {}
+  AddressableSegment(light::LightState *src, int32_t src_offset, int32_t size, bool reversed)
+      : src_(static_cast<light::AddressableLight *>(src->get_output())),
+        src_offset_(src_offset),
+        size_(size),
+        reversed_(reversed) {}
 
   light::AddressableLight *get_src() const { return this->src_; }
   int32_t get_src_offset() const { return this->src_offset_; }
   int32_t get_size() const { return this->size_; }
   int32_t get_dst_offset() const { return this->dst_offset_; }
   void set_dst_offset(int32_t dst_offset) { this->dst_offset_ = dst_offset; }
+  bool is_reversed() const { return this->reversed_; }
 
  protected:
   light::AddressableLight *src_;
   int32_t src_offset_;
   int32_t size_;
   int32_t dst_offset_;
+  bool reversed_;
 };
 
 class PartitionLightOutput : public light::AddressableLight {
@@ -72,7 +77,12 @@ class PartitionLightOutput : public light::AddressableLight {
     // offset within the segment
     int32_t seg_off = index - seg.get_dst_offset();
     // offset within the src
-    int32_t src_off = seg.get_src_offset() + seg_off;
+    int32_t src_off;
+    if (seg.is_reversed())
+      src_off = seg.get_src_offset() + seg.get_size() - seg_off - 1;
+    else
+      src_off = seg.get_src_offset() + seg_off;
+
     auto view = (*seg.get_src())[src_off];
     view.raw_set_color_correction(&this->correction_);
     return view;
diff --git a/esphome/components/pca9685/__init__.py b/esphome/components/pca9685/__init__.py
index d88012a1cd..1a5ccc3247 100644
--- a/esphome/components/pca9685/__init__.py
+++ b/esphome/components/pca9685/__init__.py
@@ -23,7 +23,7 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID], config[CONF_FREQUENCY])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/pca9685/output.py b/esphome/components/pca9685/output.py
index c3cb88eeaf..40f7b3cd74 100644
--- a/esphome/components/pca9685/output.py
+++ b/esphome/components/pca9685/output.py
@@ -18,8 +18,8 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
 )
 
 
-def to_code(config):
-    paren = yield cg.get_variable(config[CONF_PCA9685_ID])
+async def to_code(config):
+    paren = await cg.get_variable(config[CONF_PCA9685_ID])
     rhs = paren.create_channel(config[CONF_CHANNEL])
     var = cg.Pvariable(config[CONF_ID], rhs)
-    yield output.register_output(var, config)
+    await output.register_output(var, config)
diff --git a/esphome/components/pcd8544/display.py b/esphome/components/pcd8544/display.py
index 50cc1b02a8..d0184a58d3 100644
--- a/esphome/components/pcd8544/display.py
+++ b/esphome/components/pcd8544/display.py
@@ -36,22 +36,22 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
 
-    yield cg.register_component(var, config)
-    yield display.register_display(var, config)
-    yield spi.register_spi_device(var, config)
+    await cg.register_component(var, config)
+    await display.register_display(var, config)
+    await spi.register_spi_device(var, config)
 
-    dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN])
+    dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
     cg.add(var.set_dc_pin(dc))
-    reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN])
+    reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
     cg.add(var.set_reset_pin(reset))
 
     cg.add(var.set_contrast(config[CONF_CONTRAST]))
 
     if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
+        lambda_ = await cg.process_lambda(
             config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void
         )
         cg.add(var.set_writer(lambda_))
diff --git a/esphome/components/pcf8574/__init__.py b/esphome/components/pcf8574/__init__.py
index 8c028fabc1..e96c526cb0 100644
--- a/esphome/components/pcf8574/__init__.py
+++ b/esphome/components/pcf8574/__init__.py
@@ -31,10 +31,10 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
     cg.add(var.set_pcf8575(config[CONF_PCF8575]))
 
 
@@ -69,8 +69,8 @@ PCF8574_INPUT_PIN_SCHEMA = cv.Schema(
 @pins.PIN_SCHEMA_REGISTRY.register(
     "pcf8574", (PCF8574_OUTPUT_PIN_SCHEMA, PCF8574_INPUT_PIN_SCHEMA)
 )
-def pcf8574_pin_to_code(config):
-    parent = yield cg.get_variable(config[CONF_PCF8574])
-    yield PCF8574GPIOPin.new(
+async def pcf8574_pin_to_code(config):
+    parent = await cg.get_variable(config[CONF_PCF8574])
+    return PCF8574GPIOPin.new(
         parent, config[CONF_NUMBER], config[CONF_MODE], config[CONF_INVERTED]
     )
diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py
index c16f1726ae..ffc6392ec2 100644
--- a/esphome/components/pid/climate.py
+++ b/esphome/components/pid/climate.py
@@ -51,19 +51,19 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield climate.register_climate(var, config)
+    await cg.register_component(var, config)
+    await climate.register_climate(var, config)
 
-    sens = yield cg.get_variable(config[CONF_SENSOR])
+    sens = await cg.get_variable(config[CONF_SENSOR])
     cg.add(var.set_sensor(sens))
 
     if CONF_COOL_OUTPUT in config:
-        out = yield cg.get_variable(config[CONF_COOL_OUTPUT])
+        out = await cg.get_variable(config[CONF_COOL_OUTPUT])
         cg.add(var.set_cool_output(out))
     if CONF_HEAT_OUTPUT in config:
-        out = yield cg.get_variable(config[CONF_HEAT_OUTPUT])
+        out = await cg.get_variable(config[CONF_HEAT_OUTPUT])
         cg.add(var.set_heat_output(out))
     params = config[CONF_CONTROL_PARAMETERS]
     cg.add(var.set_kp(params[CONF_KP]))
@@ -86,9 +86,9 @@ def to_code(config):
         }
     ),
 )
-def pid_reset_integral_term(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, paren)
+async def pid_reset_integral_term(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
 
 
 @automation.register_action(
@@ -107,13 +107,13 @@ def pid_reset_integral_term(config, action_id, template_arg, args):
         }
     ),
 )
-def esp8266_set_frequency_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def esp8266_set_frequency_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
     cg.add(var.set_noiseband(config[CONF_NOISEBAND]))
     cg.add(var.set_positive_output(config[CONF_POSITIVE_OUTPUT]))
     cg.add(var.set_negative_output(config[CONF_NEGATIVE_OUTPUT]))
-    yield var
+    return var
 
 
 @automation.register_action(
@@ -128,16 +128,16 @@ def esp8266_set_frequency_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def set_control_parameters(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def set_control_parameters(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
 
-    kp_template_ = yield cg.templatable(config[CONF_KP], args, float)
+    kp_template_ = await cg.templatable(config[CONF_KP], args, float)
     cg.add(var.set_kp(kp_template_))
 
-    ki_template_ = yield cg.templatable(config[CONF_KI], args, float)
+    ki_template_ = await cg.templatable(config[CONF_KI], args, float)
     cg.add(var.set_ki(ki_template_))
 
-    kd_template_ = yield cg.templatable(config[CONF_KD], args, float)
+    kd_template_ = await cg.templatable(config[CONF_KD], args, float)
     cg.add(var.set_kd(kd_template_))
-    yield var
+    return var
diff --git a/esphome/components/pid/sensor/__init__.py b/esphome/components/pid/sensor/__init__.py
index d29fb6b662..61669d4716 100644
--- a/esphome/components/pid/sensor/__init__.py
+++ b/esphome/components/pid/sensor/__init__.py
@@ -4,6 +4,7 @@ from esphome.components import sensor
 from esphome.const import (
     CONF_ID,
     DEVICE_CLASS_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_PERCENT,
     ICON_GAUGE,
     CONF_TYPE,
@@ -28,7 +29,9 @@ PID_CLIMATE_SENSOR_TYPES = {
 
 CONF_CLIMATE_ID = "climate_id"
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_PERCENT, ICON_GAUGE, 1, DEVICE_CLASS_EMPTY)
+    sensor.sensor_schema(
+        UNIT_PERCENT, ICON_GAUGE, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(PIDClimateSensor),
@@ -40,11 +43,11 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
-    parent = yield cg.get_variable(config[CONF_CLIMATE_ID])
+async def to_code(config):
+    parent = await cg.get_variable(config[CONF_CLIMATE_ID])
     var = cg.new_Pvariable(config[CONF_ID])
-    yield sensor.register_sensor(var, config)
-    yield cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
 
     cg.add(var.set_parent(parent))
     cg.add(var.set_type(config[CONF_TYPE]))
diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py
index 05becfb71f..80f2b80e5e 100644
--- a/esphome/components/pmsx003/sensor.py
+++ b/esphome/components/pmsx003/sensor.py
@@ -15,6 +15,7 @@ from esphome.const import (
     DEVICE_CLASS_TEMPERATURE,
     ICON_CHEMICAL_WEAPON,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_MICROGRAMS_PER_CUBIC_METER,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -66,30 +67,42 @@ CONFIG_SCHEMA = (
                 ICON_CHEMICAL_WEAPON,
                 0,
                 DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
                 UNIT_MICROGRAMS_PER_CUBIC_METER,
                 ICON_CHEMICAL_WEAPON,
                 0,
                 DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
                 UNIT_MICROGRAMS_PER_CUBIC_METER,
                 ICON_CHEMICAL_WEAPON,
                 0,
                 DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema(
                 UNIT_MICROGRAMS_PER_CUBIC_METER,
                 ICON_CHEMICAL_WEAPON,
                 0,
                 DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -98,33 +111,33 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield uart.register_uart_device(var, config)
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
 
     cg.add(var.set_type(config[CONF_TYPE]))
 
     if CONF_PM_1_0 in config:
-        sens = yield sensor.new_sensor(config[CONF_PM_1_0])
+        sens = await sensor.new_sensor(config[CONF_PM_1_0])
         cg.add(var.set_pm_1_0_sensor(sens))
 
     if CONF_PM_2_5 in config:
-        sens = yield sensor.new_sensor(config[CONF_PM_2_5])
+        sens = await sensor.new_sensor(config[CONF_PM_2_5])
         cg.add(var.set_pm_2_5_sensor(sens))
 
     if CONF_PM_10_0 in config:
-        sens = yield sensor.new_sensor(config[CONF_PM_10_0])
+        sens = await sensor.new_sensor(config[CONF_PM_10_0])
         cg.add(var.set_pm_10_0_sensor(sens))
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature_sensor(sens))
 
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity_sensor(sens))
 
     if CONF_FORMALDEHYDE in config:
-        sens = yield sensor.new_sensor(config[CONF_FORMALDEHYDE])
+        sens = await sensor.new_sensor(config[CONF_FORMALDEHYDE])
         cg.add(var.set_formaldehyde_sensor(sens))
diff --git a/esphome/components/pn532/__init__.py b/esphome/components/pn532/__init__.py
index 5403ebe5cd..b902e8e3d0 100644
--- a/esphome/components/pn532/__init__.py
+++ b/esphome/components/pn532/__init__.py
@@ -2,8 +2,7 @@ import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import automation
 from esphome.components import nfc
-from esphome.const import CONF_ID, CONF_ON_TAG, CONF_TRIGGER_ID
-from esphome.core import coroutine
+from esphome.const import CONF_ID, CONF_ON_TAG_REMOVED, CONF_ON_TAG, CONF_TRIGGER_ID
 
 CODEOWNERS = ["@OttoWinter", "@jesserockz"]
 AUTO_LOAD = ["binary_sensor", "nfc"]
@@ -41,6 +40,11 @@ PN532_SCHEMA = cv.Schema(
                 ),
             }
         ),
+        cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PN532OnTagTrigger),
+            }
+        ),
     }
 ).extend(cv.polling_component_schema("1s"))
 
@@ -53,20 +57,26 @@ def CONFIG_SCHEMA(conf):
         )
 
 
-@coroutine
-def setup_pn532(var, config):
-    yield cg.register_component(var, config)
+async def setup_pn532(var, config):
+    await cg.register_component(var, config)
 
     for conf in config.get(CONF_ON_TAG, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
-        cg.add(var.register_trigger(trigger))
-        yield automation.build_automation(
+        cg.add(var.register_ontag_trigger(trigger))
+        await automation.build_automation(
+            trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf
+        )
+
+    for conf in config.get(CONF_ON_TAG_REMOVED, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
+        cg.add(var.register_ontagremoved_trigger(trigger))
+        await automation.build_automation(
             trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf
         )
 
     for conf in config.get(CONF_ON_FINISHED_WRITE, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [], conf)
+        await automation.build_automation(trigger, [], conf)
 
 
 @automation.register_condition(
@@ -78,7 +88,7 @@ def setup_pn532(var, config):
         }
     ),
 )
-def pn532_is_writing_to_code(config, condition_id, template_arg, args):
+async def pn532_is_writing_to_code(config, condition_id, template_arg, args):
     var = cg.new_Pvariable(condition_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    yield var
+    await cg.register_parented(var, config[CONF_ID])
+    return var
diff --git a/esphome/components/pn532/binary_sensor.py b/esphome/components/pn532/binary_sensor.py
index 5e6bf6c2b9..9a5816b322 100644
--- a/esphome/components/pn532/binary_sensor.py
+++ b/esphome/components/pn532/binary_sensor.py
@@ -40,11 +40,11 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield binary_sensor.register_binary_sensor(var, config)
+    await binary_sensor.register_binary_sensor(var, config)
 
-    hub = yield cg.get_variable(config[CONF_PN532_ID])
+    hub = await cg.get_variable(config[CONF_PN532_ID])
     cg.add(hub.register_tag(var))
     addr = [HexInt(int(x, 16)) for x in config[CONF_UID].split("-")]
     cg.add(var.set_uid(addr))
diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp
index 1cc2b19c2e..5e0ec458ab 100644
--- a/esphome/components/pn532/pn532.cpp
+++ b/esphome/components/pn532/pn532.cpp
@@ -103,6 +103,11 @@ void PN532::loop() {
 
   if (!success) {
     // Something failed
+    if (!this->current_uid_.empty()) {
+      auto tag = new nfc::NfcTag(this->current_uid_);
+      for (auto *trigger : this->triggers_ontagremoved_)
+        trigger->process(tag);
+    }
     this->current_uid_ = {};
     this->turn_off_rf_();
     return;
@@ -111,6 +116,11 @@ void PN532::loop() {
   uint8_t num_targets = read[0];
   if (num_targets != 1) {
     // no tags found or too many
+    if (!this->current_uid_.empty()) {
+      auto tag = new nfc::NfcTag(this->current_uid_);
+      for (auto *trigger : this->triggers_ontagremoved_)
+        trigger->process(tag);
+    }
     this->current_uid_ = {};
     this->turn_off_rf_();
     return;
@@ -142,7 +152,7 @@ void PN532::loop() {
 
   if (next_task_ == READ) {
     auto tag = this->read_tag_(nfcid);
-    for (auto *trigger : this->triggers_)
+    for (auto *trigger : this->triggers_ontag_)
       trigger->process(tag);
 
     if (report) {
diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h
index 95a2e0dd2a..c4854cf7f2 100644
--- a/esphome/components/pn532/pn532.h
+++ b/esphome/components/pn532/pn532.h
@@ -30,7 +30,8 @@ class PN532 : public PollingComponent {
   void loop() override;
 
   void register_tag(PN532BinarySensor *tag) { this->binary_sensors_.push_back(tag); }
-  void register_trigger(PN532OnTagTrigger *trig) { this->triggers_.push_back(trig); }
+  void register_ontag_trigger(PN532OnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); }
+  void register_ontagremoved_trigger(PN532OnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); }
 
   void add_on_finished_write_callback(std::function<void()> callback) {
     this->on_finished_write_callback_.add(std::move(callback));
@@ -78,7 +79,8 @@ class PN532 : public PollingComponent {
 
   bool requested_read_{false};
   std::vector<PN532BinarySensor *> binary_sensors_;
-  std::vector<PN532OnTagTrigger *> triggers_;
+  std::vector<PN532OnTagTrigger *> triggers_ontag_;
+  std::vector<PN532OnTagTrigger *> triggers_ontagremoved_;
   std::vector<uint8_t> current_uid_;
   nfc::NdefMessage *next_task_message_to_write_;
   enum NfcTask {
diff --git a/esphome/components/pn532_i2c/__init__.py b/esphome/components/pn532_i2c/__init__.py
index 80995bd018..36af2f8aa0 100644
--- a/esphome/components/pn532_i2c/__init__.py
+++ b/esphome/components/pn532_i2c/__init__.py
@@ -19,7 +19,7 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield pn532.setup_pn532(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await pn532.setup_pn532(var, config)
+    await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/pn532_spi/__init__.py b/esphome/components/pn532_spi/__init__.py
index 2abe436291..2683f34ad5 100644
--- a/esphome/components/pn532_spi/__init__.py
+++ b/esphome/components/pn532_spi/__init__.py
@@ -19,7 +19,7 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield pn532.setup_pn532(var, config)
-    yield spi.register_spi_device(var, config)
+    await pn532.setup_pn532(var, config)
+    await spi.register_spi_device(var, config)
diff --git a/esphome/components/power_supply/__init__.py b/esphome/components/power_supply/__init__.py
index efed5e0d81..f7dd8bca84 100644
--- a/esphome/components/power_supply/__init__.py
+++ b/esphome/components/power_supply/__init__.py
@@ -22,11 +22,11 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
-    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+    pin = await cg.gpio_pin_expression(config[CONF_PIN])
     cg.add(var.set_pin(pin))
     cg.add(var.set_enable_time(config[CONF_ENABLE_TIME]))
     cg.add(var.set_keep_on_time(config[CONF_KEEP_ON_TIME]))
diff --git a/esphome/components/prometheus/__init__.py b/esphome/components/prometheus/__init__.py
index a1b376c763..f5f166d085 100644
--- a/esphome/components/prometheus/__init__.py
+++ b/esphome/components/prometheus/__init__.py
@@ -19,10 +19,10 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
-    paren = yield cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
+async def to_code(config):
+    paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
 
     cg.add_define("USE_PROMETHEUS")
 
     var = cg.new_Pvariable(config[CONF_ID], paren)
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py
index 0dff2959e8..71227ec491 100644
--- a/esphome/components/pulse_counter/sensor.py
+++ b/esphome/components/pulse_counter/sensor.py
@@ -13,6 +13,8 @@ from esphome.const import (
     CONF_TOTAL,
     DEVICE_CLASS_EMPTY,
     ICON_PULSE,
+    STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_NONE,
     UNIT_PULSES_PER_MINUTE,
     UNIT_PULSES,
 )
@@ -64,7 +66,13 @@ def validate_count_mode(value):
 
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_PULSES_PER_MINUTE, ICON_PULSE, 2, DEVICE_CLASS_EMPTY)
+    sensor.sensor_schema(
+        UNIT_PULSES_PER_MINUTE,
+        ICON_PULSE,
+        2,
+        DEVICE_CLASS_EMPTY,
+        STATE_CLASS_MEASUREMENT,
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(PulseCounterSensor),
@@ -86,7 +94,7 @@ CONFIG_SCHEMA = (
             ),
             cv.Optional(CONF_INTERNAL_FILTER, default="13us"): validate_internal_filter,
             cv.Optional(CONF_TOTAL): sensor.sensor_schema(
-                UNIT_PULSES, ICON_PULSE, 0, DEVICE_CLASS_EMPTY
+                UNIT_PULSES, ICON_PULSE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
             ),
         }
     )
@@ -94,12 +102,12 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
 
-    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+    pin = await cg.gpio_pin_expression(config[CONF_PIN])
     cg.add(var.set_pin(pin))
     count = config[CONF_COUNT_MODE]
     cg.add(var.set_rising_edge_mode(count[CONF_RISING_EDGE]))
@@ -107,5 +115,5 @@ def to_code(config):
     cg.add(var.set_filter_us(config[CONF_INTERNAL_FILTER]))
 
     if CONF_TOTAL in config:
-        sens = yield sensor.new_sensor(config[CONF_TOTAL])
+        sens = await sensor.new_sensor(config[CONF_TOTAL])
         cg.add(var.set_total_sensor(sens))
diff --git a/esphome/components/pulse_meter/automation.h b/esphome/components/pulse_meter/automation.h
new file mode 100644
index 0000000000..3112ded680
--- /dev/null
+++ b/esphome/components/pulse_meter/automation.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/automation.h"
+#include "esphome/components/pulse_meter/pulse_meter_sensor.h"
+
+namespace esphome {
+
+namespace pulse_meter {
+
+template<typename... Ts> class SetTotalPulsesAction : public Action<Ts...> {
+ public:
+  SetTotalPulsesAction(PulseMeterSensor *pulse_meter) : pulse_meter_(pulse_meter) {}
+
+  TEMPLATABLE_VALUE(uint32_t, total_pulses)
+
+  void play(Ts... x) override { this->pulse_meter_->set_total_pulses(this->total_pulses_.value(x...)); }
+
+ protected:
+  PulseMeterSensor *pulse_meter_;
+};
+
+}  // namespace pulse_meter
+}  // namespace esphome
diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp
index 8cf341c8a1..539da3a2aa 100644
--- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp
+++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp
@@ -23,7 +23,6 @@ void PulseMeterSensor::loop() {
   const uint32_t time_since_valid_edge_us = now - this->last_valid_edge_us_;
   if ((this->last_valid_edge_us_ != 0) && (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_detected_edge_us_ = 0;
     this->last_valid_edge_us_ = 0;
     this->pulse_width_us_ = 0;
   }
@@ -48,6 +47,8 @@ void PulseMeterSensor::loop() {
   }
 }
 
+void PulseMeterSensor::set_total_pulses(uint32_t pulses) { this->total_pulses_ = pulses; }
+
 void PulseMeterSensor::dump_config() {
   LOG_SENSOR("", "Pulse Meter", this);
   LOG_PIN("  Pin: ", this->pin_);
@@ -66,18 +67,15 @@ void ICACHE_RAM_ATTR PulseMeterSensor::gpio_intr(PulseMeterSensor *sensor) {
     return;
   }
 
-  // Ignore the first detected pulse (we need at least two pulses to measure the width)
-  if (sensor->last_detected_edge_us_ != 0) {
-    // 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->last_valid_edge_us_ != 0) {
-        sensor->pulse_width_us_ = (now - sensor->last_valid_edge_us_);
-      }
-
-      sensor->total_pulses_++;
-      sensor->last_valid_edge_us_ = now;
+  // 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->last_valid_edge_us_ != 0) {
+      sensor->pulse_width_us_ = (now - sensor->last_valid_edge_us_);
     }
+
+    sensor->total_pulses_++;
+    sensor->last_valid_edge_us_ = now;
   }
 
   sensor->last_detected_edge_us_ = now;
diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.h b/esphome/components/pulse_meter/pulse_meter_sensor.h
index 7d3adbbbcb..d02cff6123 100644
--- a/esphome/components/pulse_meter/pulse_meter_sensor.h
+++ b/esphome/components/pulse_meter/pulse_meter_sensor.h
@@ -15,6 +15,8 @@ class PulseMeterSensor : public sensor::Sensor, public Component {
   void set_timeout_us(uint32_t timeout) { this->timeout_us_ = timeout; }
   void set_total_sensor(sensor::Sensor *sensor) { this->total_sensor_ = sensor; }
 
+  void set_total_pulses(uint32_t pulses);
+
   void setup() override;
   void loop() override;
   float get_setup_priority() const override { return setup_priority::DATA; }
diff --git a/esphome/components/pulse_meter/sensor.py b/esphome/components/pulse_meter/sensor.py
index 37827b735d..e732971c3a 100644
--- a/esphome/components/pulse_meter/sensor.py
+++ b/esphome/components/pulse_meter/sensor.py
@@ -1,6 +1,6 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
-from esphome import pins
+from esphome import automation, pins
 from esphome.components import sensor
 from esphome.const import (
     CONF_ID,
@@ -9,7 +9,10 @@ from esphome.const import (
     CONF_NUMBER,
     CONF_TIMEOUT,
     CONF_TOTAL,
+    CONF_VALUE,
     ICON_PULSE,
+    STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_NONE,
     UNIT_PULSES,
     UNIT_PULSES_PER_MINUTE,
     DEVICE_CLASS_EMPTY,
@@ -24,6 +27,8 @@ PulseMeterSensor = pulse_meter_ns.class_(
     "PulseMeterSensor", sensor.Sensor, cg.Component
 )
 
+SetTotalPulsesAction = pulse_meter_ns.class_("SetTotalPulsesAction", automation.Action)
+
 
 def validate_internal_filter(value):
     return cv.positive_time_period_microseconds(value)
@@ -46,7 +51,7 @@ def validate_pulse_meter_pin(value):
 
 
 CONFIG_SCHEMA = sensor.sensor_schema(
-    UNIT_PULSES_PER_MINUTE, ICON_PULSE, 2, DEVICE_CLASS_EMPTY
+    UNIT_PULSES_PER_MINUTE, ICON_PULSE, 2, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
 ).extend(
     {
         cv.GenerateID(): cv.declare_id(PulseMeterSensor),
@@ -54,22 +59,40 @@ CONFIG_SCHEMA = sensor.sensor_schema(
         cv.Optional(CONF_INTERNAL_FILTER, default="13us"): validate_internal_filter,
         cv.Optional(CONF_TIMEOUT, default="5min"): validate_timeout,
         cv.Optional(CONF_TOTAL): sensor.sensor_schema(
-            UNIT_PULSES, ICON_PULSE, 0, DEVICE_CLASS_EMPTY
+            UNIT_PULSES, ICON_PULSE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
         ),
     }
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
 
-    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+    pin = await cg.gpio_pin_expression(config[CONF_PIN])
     cg.add(var.set_pin(pin))
     cg.add(var.set_filter_us(config[CONF_INTERNAL_FILTER]))
     cg.add(var.set_timeout_us(config[CONF_TIMEOUT]))
 
     if CONF_TOTAL in config:
-        sens = yield sensor.new_sensor(config[CONF_TOTAL])
+        sens = await sensor.new_sensor(config[CONF_TOTAL])
         cg.add(var.set_total_sensor(sens))
+
+
+@automation.register_action(
+    "pulse_meter.set_total_pulses",
+    SetTotalPulsesAction,
+    cv.Schema(
+        {
+            cv.Required(CONF_ID): cv.use_id(PulseMeterSensor),
+            cv.Required(CONF_VALUE): cv.templatable(cv.uint32_t),
+        }
+    ),
+)
+async def set_total_action_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    var = cg.new_Pvariable(action_id, template_arg, paren)
+    template_ = await cg.templatable(config[CONF_VALUE], args, int)
+    cg.add(var.set_total_pulses(template_))
+    return var
diff --git a/esphome/components/pulse_width/sensor.py b/esphome/components/pulse_width/sensor.py
index 228c5f8dfe..6a6147c6aa 100644
--- a/esphome/components/pulse_width/sensor.py
+++ b/esphome/components/pulse_width/sensor.py
@@ -2,7 +2,14 @@ import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import pins
 from esphome.components import sensor
-from esphome.const import CONF_ID, CONF_PIN, DEVICE_CLASS_EMPTY, UNIT_SECOND, ICON_TIMER
+from esphome.const import (
+    CONF_ID,
+    CONF_PIN,
+    DEVICE_CLASS_EMPTY,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_SECOND,
+    ICON_TIMER,
+)
 
 pulse_width_ns = cg.esphome_ns.namespace("pulse_width")
 
@@ -11,7 +18,9 @@ PulseWidthSensor = pulse_width_ns.class_(
 )
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_SECOND, ICON_TIMER, 3, DEVICE_CLASS_EMPTY)
+    sensor.sensor_schema(
+        UNIT_SECOND, ICON_TIMER, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(PulseWidthSensor),
@@ -24,10 +33,10 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
 
-    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+    pin = await cg.gpio_pin_expression(config[CONF_PIN])
     cg.add(var.set_pin(pin))
diff --git a/esphome/components/pzem004t/sensor.py b/esphome/components/pzem004t/sensor.py
index 1228fc4ab6..e3859f090c 100644
--- a/esphome/components/pzem004t/sensor.py
+++ b/esphome/components/pzem004t/sensor.py
@@ -12,6 +12,8 @@ from esphome.const import (
     DEVICE_CLASS_POWER,
     DEVICE_CLASS_VOLTAGE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_NONE,
     UNIT_VOLT,
     UNIT_AMPERE,
     UNIT_WATT,
@@ -28,16 +30,24 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(PZEM004T),
             cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
-                UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE
+                UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT
             ),
             cv.Optional(CONF_CURRENT): sensor.sensor_schema(
-                UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT
+                UNIT_AMPERE,
+                ICON_EMPTY,
+                2,
+                DEVICE_CLASS_CURRENT,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_POWER): sensor.sensor_schema(
-                UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER
+                UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
             ),
             cv.Optional(CONF_ENERGY): sensor.sensor_schema(
-                UNIT_WATT_HOURS, ICON_EMPTY, 0, DEVICE_CLASS_ENERGY
+                UNIT_WATT_HOURS,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_ENERGY,
+                STATE_CLASS_NONE,
             ),
         }
     )
@@ -46,24 +56,24 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield uart.register_uart_device(var, config)
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
 
     if CONF_VOLTAGE in config:
         conf = config[CONF_VOLTAGE]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_voltage_sensor(sens))
     if CONF_CURRENT in config:
         conf = config[CONF_CURRENT]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_current_sensor(sens))
     if CONF_POWER in config:
         conf = config[CONF_POWER]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_power_sensor(sens))
     if CONF_ENERGY in config:
         conf = config[CONF_ENERGY]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_energy_sensor(sens))
diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py
index aa30549c25..778c5054a0 100644
--- a/esphome/components/pzemac/sensor.py
+++ b/esphome/components/pzemac/sensor.py
@@ -17,6 +17,8 @@ from esphome.const import (
     DEVICE_CLASS_ENERGY,
     ICON_EMPTY,
     ICON_CURRENT_AC,
+    STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_NONE,
     UNIT_HERTZ,
     UNIT_VOLT,
     UNIT_AMPERE,
@@ -35,22 +37,38 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(PZEMAC),
             cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
-                UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE
+                UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT
             ),
             cv.Optional(CONF_CURRENT): sensor.sensor_schema(
-                UNIT_AMPERE, ICON_EMPTY, 3, DEVICE_CLASS_CURRENT
+                UNIT_AMPERE,
+                ICON_EMPTY,
+                3,
+                DEVICE_CLASS_CURRENT,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_POWER): sensor.sensor_schema(
-                UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER
+                UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
             ),
             cv.Optional(CONF_ENERGY): sensor.sensor_schema(
-                UNIT_WATT_HOURS, ICON_EMPTY, 0, DEVICE_CLASS_ENERGY
+                UNIT_WATT_HOURS,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_ENERGY,
+                STATE_CLASS_NONE,
             ),
             cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(
-                UNIT_HERTZ, ICON_CURRENT_AC, 1, DEVICE_CLASS_EMPTY
+                UNIT_HERTZ,
+                ICON_CURRENT_AC,
+                1,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(
-                UNIT_EMPTY, ICON_EMPTY, 2, DEVICE_CLASS_POWER_FACTOR
+                UNIT_EMPTY,
+                ICON_EMPTY,
+                2,
+                DEVICE_CLASS_POWER_FACTOR,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -59,32 +77,32 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield modbus.register_modbus_device(var, config)
+    await cg.register_component(var, config)
+    await modbus.register_modbus_device(var, config)
 
     if CONF_VOLTAGE in config:
         conf = config[CONF_VOLTAGE]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_voltage_sensor(sens))
     if CONF_CURRENT in config:
         conf = config[CONF_CURRENT]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_current_sensor(sens))
     if CONF_POWER in config:
         conf = config[CONF_POWER]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_power_sensor(sens))
     if CONF_ENERGY in config:
         conf = config[CONF_ENERGY]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_energy_sensor(sens))
     if CONF_FREQUENCY in config:
         conf = config[CONF_FREQUENCY]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_frequency_sensor(sens))
     if CONF_POWER_FACTOR in config:
         conf = config[CONF_POWER_FACTOR]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_power_factor_sensor(sens))
diff --git a/esphome/components/pzemdc/sensor.py b/esphome/components/pzemdc/sensor.py
index 962c970359..58afea8e30 100644
--- a/esphome/components/pzemdc/sensor.py
+++ b/esphome/components/pzemdc/sensor.py
@@ -10,6 +10,7 @@ from esphome.const import (
     DEVICE_CLASS_POWER,
     DEVICE_CLASS_VOLTAGE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_VOLT,
     UNIT_AMPERE,
     UNIT_WATT,
@@ -25,13 +26,17 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(PZEMDC),
             cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
-                UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE
+                UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT
             ),
             cv.Optional(CONF_CURRENT): sensor.sensor_schema(
-                UNIT_AMPERE, ICON_EMPTY, 3, DEVICE_CLASS_CURRENT
+                UNIT_AMPERE,
+                ICON_EMPTY,
+                3,
+                DEVICE_CLASS_CURRENT,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_POWER): sensor.sensor_schema(
-                UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER
+                UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
             ),
         }
     )
@@ -40,20 +45,20 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield modbus.register_modbus_device(var, config)
+    await cg.register_component(var, config)
+    await modbus.register_modbus_device(var, config)
 
     if CONF_VOLTAGE in config:
         conf = config[CONF_VOLTAGE]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_voltage_sensor(sens))
     if CONF_CURRENT in config:
         conf = config[CONF_CURRENT]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_current_sensor(sens))
     if CONF_POWER in config:
         conf = config[CONF_POWER]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_power_sensor(sens))
diff --git a/esphome/components/qmc5883l/sensor.py b/esphome/components/qmc5883l/sensor.py
index 4359427628..d0fdf1b77a 100644
--- a/esphome/components/qmc5883l/sensor.py
+++ b/esphome/components/qmc5883l/sensor.py
@@ -8,6 +8,8 @@ from esphome.const import (
     CONF_RANGE,
     DEVICE_CLASS_EMPTY,
     ICON_MAGNET,
+    STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_NONE,
     UNIT_MICROTESLA,
     UNIT_DEGREES,
     ICON_SCREEN_ROTATION,
@@ -69,10 +71,10 @@ def validate_enum(enum_values, units=None, int=True):
 
 
 field_strength_schema = sensor.sensor_schema(
-    UNIT_MICROTESLA, ICON_MAGNET, 1, DEVICE_CLASS_EMPTY
+    UNIT_MICROTESLA, ICON_MAGNET, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
 )
 heading_schema = sensor.sensor_schema(
-    UNIT_DEGREES, ICON_SCREEN_ROTATION, 1, DEVICE_CLASS_EMPTY
+    UNIT_DEGREES, ICON_SCREEN_ROTATION, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
 )
 
 CONFIG_SCHEMA = (
@@ -106,23 +108,23 @@ def auto_data_rate(config):
     return QMC5883LDatarates[200]
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     cg.add(var.set_oversampling(config[CONF_OVERSAMPLING]))
     cg.add(var.set_datarate(auto_data_rate(config)))
     cg.add(var.set_range(config[CONF_RANGE]))
     if CONF_FIELD_STRENGTH_X in config:
-        sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_X])
+        sens = await sensor.new_sensor(config[CONF_FIELD_STRENGTH_X])
         cg.add(var.set_x_sensor(sens))
     if CONF_FIELD_STRENGTH_Y in config:
-        sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_Y])
+        sens = await sensor.new_sensor(config[CONF_FIELD_STRENGTH_Y])
         cg.add(var.set_y_sensor(sens))
     if CONF_FIELD_STRENGTH_Z in config:
-        sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_Z])
+        sens = await sensor.new_sensor(config[CONF_FIELD_STRENGTH_Z])
         cg.add(var.set_z_sensor(sens))
     if CONF_HEADING in config:
-        sens = yield sensor.new_sensor(config[CONF_HEADING])
+        sens = await sensor.new_sensor(config[CONF_HEADING])
         cg.add(var.set_heading_sensor(sens))
diff --git a/esphome/components/rc522/__init__.py b/esphome/components/rc522/__init__.py
index 7858213f06..d64cf3c085 100644
--- a/esphome/components/rc522/__init__.py
+++ b/esphome/components/rc522/__init__.py
@@ -3,7 +3,6 @@ import esphome.config_validation as cv
 from esphome import automation, pins
 from esphome.components import i2c
 from esphome.const import CONF_ON_TAG, CONF_TRIGGER_ID, CONF_RESET_PIN
-from esphome.core import coroutine
 
 CODEOWNERS = ["@glmnet"]
 AUTO_LOAD = ["binary_sensor"]
@@ -29,15 +28,14 @@ RC522_SCHEMA = cv.Schema(
 ).extend(cv.polling_component_schema("1s"))
 
 
-@coroutine
-def setup_rc522(var, config):
-    yield cg.register_component(var, config)
+async def setup_rc522(var, config):
+    await cg.register_component(var, config)
 
     if CONF_RESET_PIN in config:
-        reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN])
+        reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
         cg.add(var.set_reset_pin(reset))
 
     for conf in config.get(CONF_ON_TAG, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
         cg.add(var.register_trigger(trigger))
-        yield automation.build_automation(trigger, [(cg.std_string, "x")], conf)
+        await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
diff --git a/esphome/components/rc522/binary_sensor.py b/esphome/components/rc522/binary_sensor.py
index 89eef2f976..67d3068599 100644
--- a/esphome/components/rc522/binary_sensor.py
+++ b/esphome/components/rc522/binary_sensor.py
@@ -2,7 +2,7 @@ import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import binary_sensor
 from esphome.const import CONF_UID, CONF_ID
-from esphome.core import HexInt, coroutine
+from esphome.core import HexInt
 from . import rc522_ns, RC522, CONF_RC522_ID
 
 DEPENDENCIES = ["rc522"]
@@ -40,12 +40,11 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 )
 
 
-@coroutine
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield binary_sensor.register_binary_sensor(var, config)
+    await binary_sensor.register_binary_sensor(var, config)
 
-    hub = yield cg.get_variable(config[CONF_RC522_ID])
+    hub = await cg.get_variable(config[CONF_RC522_ID])
     cg.add(hub.register_tag(var))
     addr = [HexInt(int(x, 16)) for x in config[CONF_UID].split("-")]
     cg.add(var.set_uid(addr))
diff --git a/esphome/components/rc522_i2c/__init__.py b/esphome/components/rc522_i2c/__init__.py
index 081536b6b1..e42817352c 100644
--- a/esphome/components/rc522_i2c/__init__.py
+++ b/esphome/components/rc522_i2c/__init__.py
@@ -20,7 +20,7 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield rc522.setup_rc522(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await rc522.setup_rc522(var, config)
+    await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/rc522_spi/__init__.py b/esphome/components/rc522_spi/__init__.py
index 2e5630f46d..68b1e64145 100644
--- a/esphome/components/rc522_spi/__init__.py
+++ b/esphome/components/rc522_spi/__init__.py
@@ -20,7 +20,12 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield rc522.setup_rc522(var, config)
-    yield spi.register_spi_device(var, config)
+    await rc522.setup_rc522(var, config)
+    await spi.register_spi_device(var, config)
+
+
+def validate(config, item_config):
+    # validate given SPI hub is suitable for rc522_spi, it needs both miso and mosi
+    spi.validate_device("rc522_spi", config, item_config, True, True)
diff --git a/esphome/components/rc522_spi/binary_sensor.py b/esphome/components/rc522_spi/binary_sensor.py
index f5400a2331..8139f6d2ac 100644
--- a/esphome/components/rc522_spi/binary_sensor.py
+++ b/esphome/components/rc522_spi/binary_sensor.py
@@ -5,5 +5,5 @@ DEPENDENCIES = ["rc522"]
 CONFIG_SCHEMA = rc522_binary_sensor.CONFIG_SCHEMA
 
 
-def to_code(config):
-    yield rc522_binary_sensor.to_code(config)
+async def to_code(config):
+    await rc522_binary_sensor.to_code(config)
diff --git a/esphome/components/rdm6300/__init__.py b/esphome/components/rdm6300/__init__.py
index a416d95a12..37ebcb49a9 100644
--- a/esphome/components/rdm6300/__init__.py
+++ b/esphome/components/rdm6300/__init__.py
@@ -29,12 +29,12 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield uart.register_uart_device(var, config)
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
 
     for conf in config.get(CONF_ON_TAG, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
         cg.add(var.register_trigger(trigger))
-        yield automation.build_automation(trigger, [(cg.uint32, "x")], conf)
+        await automation.build_automation(trigger, [(cg.uint32, "x")], conf)
diff --git a/esphome/components/rdm6300/binary_sensor.py b/esphome/components/rdm6300/binary_sensor.py
index 02e0b6ceb6..c99a2bfc06 100644
--- a/esphome/components/rdm6300/binary_sensor.py
+++ b/esphome/components/rdm6300/binary_sensor.py
@@ -20,10 +20,10 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield binary_sensor.register_binary_sensor(var, config)
+    await binary_sensor.register_binary_sensor(var, config)
 
-    hub = yield cg.get_variable(config[CONF_RDM6300_ID])
+    hub = await cg.get_variable(config[CONF_RDM6300_ID])
     cg.add(hub.register_card(var))
     cg.add(var.set_id(config[CONF_UID]))
diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py
index 96579c05bb..99778c3088 100644
--- a/esphome/components/remote_base/__init__.py
+++ b/esphome/components/remote_base/__init__.py
@@ -63,9 +63,8 @@ def templatize(value):
     return cv.Schema(ret)
 
 
-@coroutine
-def register_listener(var, config):
-    receiver = yield cg.get_variable(config[CONF_RECEIVER_ID])
+async def register_listener(var, config):
+    receiver = await cg.get_variable(config[CONF_RECEIVER_ID])
     cg.add(receiver.register_listener(var))
 
 
@@ -83,13 +82,12 @@ def register_trigger(name, type, data_type):
     registerer = TRIGGER_REGISTRY.register(f"on_{name}", validator)
 
     def decorator(func):
-        @coroutine
-        def new_func(config):
+        async def new_func(config):
             var = cg.new_Pvariable(config[CONF_TRIGGER_ID])
-            yield register_listener(var, config)
-            yield coroutine(func)(var, config)
-            yield automation.build_automation(var, [(data_type, "x")], config)
-            yield var
+            await register_listener(var, config)
+            await coroutine(func)(var, config)
+            await automation.build_automation(var, [(data_type, "x")], config)
+            return var
 
         return registerer(new_func)
 
@@ -100,11 +98,10 @@ def register_dumper(name, type):
     registerer = DUMPER_REGISTRY.register(name, type, {})
 
     def decorator(func):
-        @coroutine
-        def new_func(config, dumper_id):
+        async def new_func(config, dumper_id):
             var = cg.new_Pvariable(dumper_id)
-            yield coroutine(func)(var, config)
-            yield var
+            await coroutine(func)(var, config)
+            return var
 
         return registerer(new_func)
 
@@ -139,19 +136,18 @@ def register_action(name, type_, schema):
     )
 
     def decorator(func):
-        @coroutine
-        def new_func(config, action_id, template_arg, args):
-            transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID])
+        async def new_func(config, action_id, template_arg, args):
+            transmitter = await cg.get_variable(config[CONF_TRANSMITTER_ID])
             var = cg.new_Pvariable(action_id, template_arg)
             cg.add(var.set_parent(transmitter))
             if CONF_REPEAT in config:
                 conf = config[CONF_REPEAT]
-                template_ = yield cg.templatable(conf[CONF_TIMES], args, cg.uint32)
+                template_ = await cg.templatable(conf[CONF_TIMES], args, cg.uint32)
                 cg.add(var.set_send_times(template_))
-                template_ = yield cg.templatable(conf[CONF_WAIT_TIME], args, cg.uint32)
+                template_ = await cg.templatable(conf[CONF_WAIT_TIME], args, cg.uint32)
                 cg.add(var.set_send_wait(template_))
-            yield coroutine(func)(var, config, args)
-            yield var
+            await coroutine(func)(var, config, args)
+            return var
 
         return registerer(new_func)
 
@@ -208,37 +204,34 @@ def validate_triggers(base_schema):
     return validator
 
 
-@coroutine
-def build_binary_sensor(full_config):
+async def build_binary_sensor(full_config):
     registry_entry, config = cg.extract_registry_entry_config(
         BINARY_SENSOR_REGISTRY, full_config
     )
     type_id = full_config[CONF_TYPE_ID]
     builder = registry_entry.coroutine_fun
     var = cg.new_Pvariable(type_id)
-    yield cg.register_component(var, full_config)
-    yield register_listener(var, full_config)
-    yield builder(var, config)
-    yield var
+    await cg.register_component(var, full_config)
+    await register_listener(var, full_config)
+    await builder(var, config)
+    return var
 
 
-@coroutine
-def build_triggers(full_config):
+async def build_triggers(full_config):
     for key in TRIGGER_REGISTRY:
         for config in full_config.get(key, []):
             func = TRIGGER_REGISTRY[key][0]
-            yield func(config)
+            await func(config)
 
 
-@coroutine
-def build_dumpers(config):
+async def build_dumpers(config):
     dumpers = []
     for conf in config:
-        dumper = yield cg.build_registry_entry(DUMPER_REGISTRY, conf)
-        receiver = yield cg.get_variable(conf[CONF_RECEIVER_ID])
+        dumper = await cg.build_registry_entry(DUMPER_REGISTRY, conf)
+        receiver = await cg.get_variable(conf[CONF_RECEIVER_ID])
         cg.add(receiver.register_dumper(dumper))
         dumpers.append(dumper)
-    yield dumpers
+    return dumpers
 
 
 # JVC
@@ -269,8 +262,8 @@ def jvc_dumper(var, config):
 
 
 @register_action("jvc", JVCAction, JVC_SCHEMA)
-def jvc_action(var, config, args):
-    template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint32)
+async def jvc_action(var, config, args):
+    template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32)
     cg.add(var.set_data(template_))
 
 
@@ -308,10 +301,10 @@ def lg_dumper(var, config):
 
 
 @register_action("lg", LGAction, LG_SCHEMA)
-def lg_action(var, config, args):
-    template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint32)
+async def lg_action(var, config, args):
+    template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32)
     cg.add(var.set_data(template_))
-    template_ = yield cg.templatable(config[CONF_NBITS], args, cg.uint8)
+    template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint8)
     cg.add(var.set_nbits(template_))
 
 
@@ -349,10 +342,10 @@ def nec_dumper(var, config):
 
 
 @register_action("nec", NECAction, NEC_SCHEMA)
-def nec_action(var, config, args):
-    template_ = yield cg.templatable(config[CONF_ADDRESS], args, cg.uint16)
+async def nec_action(var, config, args):
+    template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint16)
     cg.add(var.set_address(template_))
-    template_ = yield cg.templatable(config[CONF_COMMAND], args, cg.uint16)
+    template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint16)
     cg.add(var.set_command(template_))
 
 
@@ -396,10 +389,10 @@ def pioneer_dumper(var, config):
 
 
 @register_action("pioneer", PioneerAction, PIONEER_SCHEMA)
-def pioneer_action(var, config, args):
-    template_ = yield cg.templatable(config[CONF_RC_CODE_1], args, cg.uint16)
+async def pioneer_action(var, config, args):
+    template_ = await cg.templatable(config[CONF_RC_CODE_1], args, cg.uint16)
     cg.add(var.set_rc_code_1(template_))
-    template_ = yield cg.templatable(config[CONF_RC_CODE_2], args, cg.uint16)
+    template_ = await cg.templatable(config[CONF_RC_CODE_2], args, cg.uint16)
     cg.add(var.set_rc_code_2(template_))
 
 
@@ -439,10 +432,10 @@ def sony_dumper(var, config):
 
 
 @register_action("sony", SonyAction, SONY_SCHEMA)
-def sony_action(var, config, args):
-    template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint16)
+async def sony_action(var, config, args):
+    template_ = await cg.templatable(config[CONF_DATA], args, cg.uint16)
     cg.add(var.set_data(template_))
-    template_ = yield cg.templatable(config[CONF_NBITS], args, cg.uint32)
+    template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint32)
     cg.add(var.set_nbits(template_))
 
 
@@ -506,16 +499,16 @@ def raw_dumper(var, config):
         }
     ),
 )
-def raw_action(var, config, args):
+async def raw_action(var, config, args):
     code_ = config[CONF_CODE]
     if cg.is_template(code_):
-        template_ = yield cg.templatable(code_, args, cg.std_vector.template(cg.int32))
+        template_ = await cg.templatable(code_, args, cg.std_vector.template(cg.int32))
         cg.add(var.set_code_template(template_))
     else:
         code_ = config[CONF_CODE]
         arr = cg.progmem_array(config[CONF_CODE_STORAGE_ID], code_)
         cg.add(var.set_code_static(arr, len(code_)))
-    templ = yield cg.templatable(config[CONF_CARRIER_FREQUENCY], args, cg.uint32)
+    templ = await cg.templatable(config[CONF_CARRIER_FREQUENCY], args, cg.uint32)
     cg.add(var.set_carrier_frequency(templ))
 
 
@@ -553,10 +546,10 @@ def rc5_dumper(var, config):
 
 
 @register_action("rc5", RC5Action, RC5_SCHEMA)
-def rc5_action(var, config, args):
-    template_ = yield cg.templatable(config[CONF_ADDRESS], args, cg.uint8)
+async def rc5_action(var, config, args):
+    template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8)
     cg.add(var.set_address(template_))
-    template_ = yield cg.templatable(config[CONF_COMMAND], args, cg.uint8)
+    template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8)
     cg.add(var.set_command(template_))
 
 
@@ -729,12 +722,12 @@ def rc_switch_raw_binary_sensor(var, config):
     RCSwitchRawAction,
     RC_SWITCH_RAW_SCHEMA.extend(RC_SWITCH_TRANSMITTER),
 )
-def rc_switch_raw_action(var, config, args):
-    proto = yield cg.templatable(
+async def rc_switch_raw_action(var, config, args):
+    proto = await cg.templatable(
         config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol
     )
     cg.add(var.set_protocol(proto))
-    cg.add(var.set_code((yield cg.templatable(config[CONF_CODE], args, cg.std_string))))
+    cg.add(var.set_code((await cg.templatable(config[CONF_CODE], args, cg.std_string))))
 
 
 @register_binary_sensor(
@@ -750,18 +743,18 @@ def rc_switch_type_a_binary_sensor(var, config):
     RCSwitchTypeAAction,
     RC_SWITCH_TYPE_A_SCHEMA.extend(RC_SWITCH_TRANSMITTER),
 )
-def rc_switch_type_a_action(var, config, args):
-    proto = yield cg.templatable(
+async def rc_switch_type_a_action(var, config, args):
+    proto = await cg.templatable(
         config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol
     )
     cg.add(var.set_protocol(proto))
     cg.add(
-        var.set_group((yield cg.templatable(config[CONF_GROUP], args, cg.std_string)))
+        var.set_group((await cg.templatable(config[CONF_GROUP], args, cg.std_string)))
     )
     cg.add(
-        var.set_device((yield cg.templatable(config[CONF_DEVICE], args, cg.std_string)))
+        var.set_device((await cg.templatable(config[CONF_DEVICE], args, cg.std_string)))
     )
-    cg.add(var.set_state((yield cg.templatable(config[CONF_STATE], args, bool))))
+    cg.add(var.set_state((await cg.templatable(config[CONF_STATE], args, bool))))
 
 
 @register_binary_sensor(
@@ -779,18 +772,18 @@ def rc_switch_type_b_binary_sensor(var, config):
     RCSwitchTypeBAction,
     RC_SWITCH_TYPE_B_SCHEMA.extend(RC_SWITCH_TRANSMITTER),
 )
-def rc_switch_type_b_action(var, config, args):
-    proto = yield cg.templatable(
+async def rc_switch_type_b_action(var, config, args):
+    proto = await cg.templatable(
         config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol
     )
     cg.add(var.set_protocol(proto))
     cg.add(
-        var.set_address((yield cg.templatable(config[CONF_ADDRESS], args, cg.uint8)))
+        var.set_address((await cg.templatable(config[CONF_ADDRESS], args, cg.uint8)))
     )
     cg.add(
-        var.set_channel((yield cg.templatable(config[CONF_CHANNEL], args, cg.uint8)))
+        var.set_channel((await cg.templatable(config[CONF_CHANNEL], args, cg.uint8)))
     )
-    cg.add(var.set_state((yield cg.templatable(config[CONF_STATE], args, bool))))
+    cg.add(var.set_state((await cg.templatable(config[CONF_STATE], args, bool))))
 
 
 @register_binary_sensor(
@@ -813,17 +806,17 @@ def rc_switch_type_c_binary_sensor(var, config):
     RCSwitchTypeCAction,
     RC_SWITCH_TYPE_C_SCHEMA.extend(RC_SWITCH_TRANSMITTER),
 )
-def rc_switch_type_c_action(var, config, args):
-    proto = yield cg.templatable(
+async def rc_switch_type_c_action(var, config, args):
+    proto = await cg.templatable(
         config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol
     )
     cg.add(var.set_protocol(proto))
     cg.add(
-        var.set_family((yield cg.templatable(config[CONF_FAMILY], args, cg.std_string)))
+        var.set_family((await cg.templatable(config[CONF_FAMILY], args, cg.std_string)))
     )
-    cg.add(var.set_group((yield cg.templatable(config[CONF_GROUP], args, cg.uint8))))
-    cg.add(var.set_device((yield cg.templatable(config[CONF_DEVICE], args, cg.uint8))))
-    cg.add(var.set_state((yield cg.templatable(config[CONF_STATE], args, bool))))
+    cg.add(var.set_group((await cg.templatable(config[CONF_GROUP], args, cg.uint8))))
+    cg.add(var.set_device((await cg.templatable(config[CONF_DEVICE], args, cg.uint8))))
+    cg.add(var.set_state((await cg.templatable(config[CONF_STATE], args, bool))))
 
 
 @register_binary_sensor(
@@ -841,16 +834,16 @@ def rc_switch_type_d_binary_sensor(var, config):
     RCSwitchTypeDAction,
     RC_SWITCH_TYPE_D_SCHEMA.extend(RC_SWITCH_TRANSMITTER),
 )
-def rc_switch_type_d_action(var, config, args):
-    proto = yield cg.templatable(
+async def rc_switch_type_d_action(var, config, args):
+    proto = await cg.templatable(
         config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol
     )
     cg.add(var.set_protocol(proto))
     cg.add(
-        var.set_group((yield cg.templatable(config[CONF_GROUP], args, cg.std_string)))
+        var.set_group((await cg.templatable(config[CONF_GROUP], args, cg.std_string)))
     )
-    cg.add(var.set_device((yield cg.templatable(config[CONF_DEVICE], args, cg.uint8))))
-    cg.add(var.set_state((yield cg.templatable(config[CONF_STATE], args, bool))))
+    cg.add(var.set_device((await cg.templatable(config[CONF_DEVICE], args, cg.uint8))))
+    cg.add(var.set_state((await cg.templatable(config[CONF_STATE], args, bool))))
 
 
 @register_trigger("rc_switch", RCSwitchTrigger, RCSwitchData)
@@ -901,8 +894,8 @@ def samsung_dumper(var, config):
 
 
 @register_action("samsung", SamsungAction, SAMSUNG_SCHEMA)
-def samsung_action(var, config, args):
-    template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint32)
+async def samsung_action(var, config, args):
+    template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32)
     cg.add(var.set_data(template_))
 
 
@@ -946,10 +939,10 @@ def samsung36_dumper(var, config):
 
 
 @register_action("samsung36", Samsung36Action, SAMSUNG36_SCHEMA)
-def samsung36_action(var, config, args):
-    template_ = yield cg.templatable(config[CONF_ADDRESS], args, cg.uint16)
+async def samsung36_action(var, config, args):
+    template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint16)
     cg.add(var.set_address(template_))
-    template_ = yield cg.templatable(config[CONF_COMMAND], args, cg.uint32)
+    template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint32)
     cg.add(var.set_command(template_))
 
 
@@ -993,8 +986,8 @@ def panasonic_dumper(var, config):
 
 
 @register_action("panasonic", PanasonicAction, PANASONIC_SCHEMA)
-def panasonic_action(var, config, args):
-    template_ = yield cg.templatable(config[CONF_ADDRESS], args, cg.uint16)
+async def panasonic_action(var, config, args):
+    template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint16)
     cg.add(var.set_address(template_))
-    template_ = yield cg.templatable(config[CONF_COMMAND], args, cg.uint32)
+    template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint32)
     cg.add(var.set_command(template_))
diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py
index f29c59b3f8..fed52803cb 100644
--- a/esphome/components/remote_receiver/__init__.py
+++ b/esphome/components/remote_receiver/__init__.py
@@ -47,16 +47,16 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
 )
 
 
-def to_code(config):
-    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+async def to_code(config):
+    pin = await cg.gpio_pin_expression(config[CONF_PIN])
     if CORE.is_esp32:
         var = cg.new_Pvariable(config[CONF_ID], pin, config[CONF_MEMORY_BLOCKS])
     else:
         var = cg.new_Pvariable(config[CONF_ID], pin)
 
-    yield remote_base.build_dumpers(config[CONF_DUMP])
-    yield remote_base.build_triggers(config)
-    yield cg.register_component(var, config)
+    await remote_base.build_dumpers(config[CONF_DUMP])
+    await remote_base.build_triggers(config)
+    await cg.register_component(var, config)
 
     cg.add(var.set_tolerance(config[CONF_TOLERANCE]))
     cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
diff --git a/esphome/components/remote_receiver/binary_sensor.py b/esphome/components/remote_receiver/binary_sensor.py
index 522333584c..62c1d9cb27 100644
--- a/esphome/components/remote_receiver/binary_sensor.py
+++ b/esphome/components/remote_receiver/binary_sensor.py
@@ -7,7 +7,7 @@ DEPENDENCIES = ["remote_receiver"]
 CONFIG_SCHEMA = remote_base.validate_binary_sensor
 
 
-def to_code(config):
-    var = yield remote_base.build_binary_sensor(config)
+async def to_code(config):
+    var = await remote_base.build_binary_sensor(config)
     cg.add(var.set_name(config[CONF_NAME]))
-    yield binary_sensor.register_binary_sensor(var, config)
+    await binary_sensor.register_binary_sensor(var, config)
diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py
index 123af7db97..e09e4c7f55 100644
--- a/esphome/components/remote_transmitter/__init__.py
+++ b/esphome/components/remote_transmitter/__init__.py
@@ -22,9 +22,9 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
-    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+async def to_code(config):
+    pin = await cg.gpio_pin_expression(config[CONF_PIN])
     var = cg.new_Pvariable(config[CONF_ID], pin)
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
     cg.add(var.set_carrier_duty_percent(config[CONF_CARRIER_DUTY_PERCENT]))
diff --git a/esphome/components/resistance/sensor.py b/esphome/components/resistance/sensor.py
index 92a564f68e..ca1501195a 100644
--- a/esphome/components/resistance/sensor.py
+++ b/esphome/components/resistance/sensor.py
@@ -1,7 +1,14 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import sensor
-from esphome.const import CONF_SENSOR, DEVICE_CLASS_EMPTY, UNIT_OHM, ICON_FLASH, CONF_ID
+from esphome.const import (
+    CONF_SENSOR,
+    DEVICE_CLASS_EMPTY,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_OHM,
+    ICON_FLASH,
+    CONF_ID,
+)
 
 resistance_ns = cg.esphome_ns.namespace("resistance")
 ResistanceSensor = resistance_ns.class_("ResistanceSensor", cg.Component, sensor.Sensor)
@@ -17,7 +24,9 @@ CONFIGURATIONS = {
 }
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_OHM, ICON_FLASH, 1, DEVICE_CLASS_EMPTY)
+    sensor.sensor_schema(
+        UNIT_OHM, ICON_FLASH, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(ResistanceSensor),
@@ -31,12 +40,12 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
 
-    sens = yield cg.get_variable(config[CONF_SENSOR])
+    sens = await cg.get_variable(config[CONF_SENSOR])
     cg.add(var.set_sensor(sens))
     cg.add(var.set_configuration(config[CONF_CONFIGURATION]))
     cg.add(var.set_resistor(config[CONF_RESISTOR]))
diff --git a/esphome/components/restart/switch.py b/esphome/components/restart/switch.py
index fe170aee61..4f1904e273 100644
--- a/esphome/components/restart/switch.py
+++ b/esphome/components/restart/switch.py
@@ -17,7 +17,7 @@ CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield switch.register_switch(var, config)
+    await cg.register_component(var, config)
+    await switch.register_switch(var, config)
diff --git a/esphome/components/rf_bridge/__init__.py b/esphome/components/rf_bridge/__init__.py
index b24e19b4bf..228e7d882b 100644
--- a/esphome/components/rf_bridge/__init__.py
+++ b/esphome/components/rf_bridge/__init__.py
@@ -12,6 +12,7 @@ from esphome.const import (
     CONF_RAW,
     CONF_SYNC,
     CONF_TRIGGER_ID,
+    CONF_DURATION,
 )
 
 DEPENDENCIES = ["uart"]
@@ -49,6 +50,12 @@ RFBridgeStopAdvancedSniffingAction = rf_bridge_ns.class_(
     "RFBridgeStopAdvancedSniffingAction", automation.Action
 )
 
+RFBridgeStartBucketSniffingAction = rf_bridge_ns.class_(
+    "RFBridgeStartBucketSniffingAction", automation.Action
+)
+
+RFBridgeBeepAction = rf_bridge_ns.class_("RFBridgeBeepAction", automation.Action)
+
 RFBridgeSendRawAction = rf_bridge_ns.class_("RFBridgeSendRawAction", automation.Action)
 
 CONF_ON_CODE_RECEIVED = "on_code_received"
@@ -79,18 +86,18 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield uart.register_uart_device(var, config)
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
 
     for conf in config.get(CONF_ON_CODE_RECEIVED, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [(RFBridgeData, "data")], conf)
+        await automation.build_automation(trigger, [(RFBridgeData, "data")], conf)
 
     for conf in config.get(CONF_ON_ADVANCED_CODE_RECEIVED, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(
+        await automation.build_automation(
             trigger, [(RFBridgeAdvancedData, "data")], conf
         )
 
@@ -109,28 +116,28 @@ RFBRIDGE_SEND_CODE_SCHEMA = cv.Schema(
 @automation.register_action(
     "rf_bridge.send_code", RFBridgeSendCodeAction, RFBRIDGE_SEND_CODE_SCHEMA
 )
-def rf_bridge_send_code_to_code(config, action_id, template_args, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def rf_bridge_send_code_to_code(config, action_id, template_args, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_args, paren)
-    template_ = yield cg.templatable(config[CONF_SYNC], args, cg.uint16)
+    template_ = await cg.templatable(config[CONF_SYNC], args, cg.uint16)
     cg.add(var.set_sync(template_))
-    template_ = yield cg.templatable(config[CONF_LOW], args, cg.uint16)
+    template_ = await cg.templatable(config[CONF_LOW], args, cg.uint16)
     cg.add(var.set_low(template_))
-    template_ = yield cg.templatable(config[CONF_HIGH], args, cg.uint16)
+    template_ = await cg.templatable(config[CONF_HIGH], args, cg.uint16)
     cg.add(var.set_high(template_))
-    template_ = yield cg.templatable(config[CONF_CODE], args, cg.uint32)
+    template_ = await cg.templatable(config[CONF_CODE], args, cg.uint32)
     cg.add(var.set_code(template_))
-    yield var
+    return var
 
 
 RFBRIDGE_ID_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(RFBridgeComponent)})
 
 
 @automation.register_action("rf_bridge.learn", RFBridgeLearnAction, RFBRIDGE_ID_SCHEMA)
-def rf_bridge_learnx_to_code(config, action_id, template_args, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def rf_bridge_learnx_to_code(config, action_id, template_args, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_args, paren)
-    yield var
+    return var
 
 
 @automation.register_action(
@@ -138,10 +145,12 @@ def rf_bridge_learnx_to_code(config, action_id, template_args, args):
     RFBridgeStartAdvancedSniffingAction,
     RFBRIDGE_ID_SCHEMA,
 )
-def rf_bridge_start_advanced_sniffing_to_code(config, action_id, template_args, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def rf_bridge_start_advanced_sniffing_to_code(
+    config, action_id, template_args, args
+):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_args, paren)
-    yield var
+    return var
 
 
 @automation.register_action(
@@ -149,10 +158,25 @@ def rf_bridge_start_advanced_sniffing_to_code(config, action_id, template_args,
     RFBridgeStopAdvancedSniffingAction,
     RFBRIDGE_ID_SCHEMA,
 )
-def rf_bridge_stop_advanced_sniffing_to_code(config, action_id, template_args, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def rf_bridge_stop_advanced_sniffing_to_code(
+    config, action_id, template_args, args
+):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_args, paren)
-    yield var
+    return var
+
+
+@automation.register_action(
+    "rf_bridge.start_bucket_sniffing",
+    RFBridgeStartBucketSniffingAction,
+    RFBRIDGE_ID_SCHEMA,
+)
+async def rf_bridge_start_bucket_sniffing_to_code(
+    config, action_id, template_args, args
+):
+    paren = await cg.get_variable(config[CONF_ID])
+    var = cg.new_Pvariable(action_id, template_args, paren)
+    return var
 
 
 RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA = cv.Schema(
@@ -170,16 +194,16 @@ RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA = cv.Schema(
     RFBridgeSendAdvancedCodeAction,
     RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA,
 )
-def rf_bridge_send_advanced_code_to_code(config, action_id, template_args, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def rf_bridge_send_advanced_code_to_code(config, action_id, template_args, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_args, paren)
-    template_ = yield cg.templatable(config[CONF_LENGTH], args, cg.uint16)
+    template_ = await cg.templatable(config[CONF_LENGTH], args, cg.uint16)
     cg.add(var.set_length(template_))
-    template_ = yield cg.templatable(config[CONF_PROTOCOL], args, cg.uint16)
+    template_ = await cg.templatable(config[CONF_PROTOCOL], args, cg.uint16)
     cg.add(var.set_protocol(template_))
-    template_ = yield cg.templatable(config[CONF_CODE], args, cg.std_string)
+    template_ = await cg.templatable(config[CONF_CODE], args, cg.std_string)
     cg.add(var.set_code(template_))
-    yield var
+    return var
 
 
 RFBRIDGE_SEND_RAW_SCHEMA = cv.Schema(
@@ -193,9 +217,26 @@ RFBRIDGE_SEND_RAW_SCHEMA = cv.Schema(
 @automation.register_action(
     "rf_bridge.send_raw", RFBridgeSendRawAction, RFBRIDGE_SEND_RAW_SCHEMA
 )
-def rf_bridge_send_raw_to_code(config, action_id, template_args, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def rf_bridge_send_raw_to_code(config, action_id, template_args, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_args, paren)
-    template_ = yield cg.templatable(config[CONF_RAW], args, cg.std_string)
+    template_ = await cg.templatable(config[CONF_RAW], args, cg.std_string)
     cg.add(var.set_raw(template_))
-    yield var
+    return var
+
+
+RFBRIDGE_BEEP_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.use_id(RFBridgeComponent),
+        cv.Required(CONF_DURATION): cv.templatable(cv.uint16_t),
+    }
+)
+
+
+@automation.register_action("rf_bridge.beep", RFBridgeBeepAction, RFBRIDGE_BEEP_SCHEMA)
+async def rf_bridge_beep_to_code(config, action_id, template_args, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    var = cg.new_Pvariable(action_id, template_args, paren)
+    template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint16)
+    cg.add(var.set_duration(template_))
+    return var
diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp
index 32e72453e0..a63523917a 100644
--- a/esphome/components/rf_bridge/rf_bridge.cpp
+++ b/esphome/components/rf_bridge/rf_bridge.cpp
@@ -67,7 +67,7 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) {
 
       data.length = raw[2];
       data.protocol = raw[3];
-      char next_byte[2];
+      char next_byte[3];
       for (uint8_t i = 0; i < data.length - 1; i++) {
         sprintf(next_byte, "%02X", raw[4 + i]);
         data.code += next_byte;
@@ -85,7 +85,7 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) {
 
       uint8_t buckets = raw[2] << 1;
       std::string str;
-      char next_byte[2];
+      char next_byte[3];
 
       for (uint32_t i = 0; i <= at; i++) {
         sprintf(next_byte, "%02X", raw[i]);
@@ -201,6 +201,14 @@ void RFBridgeComponent::stop_advanced_sniffing() {
   this->flush();
 }
 
+void RFBridgeComponent::start_bucket_sniffing() {
+  ESP_LOGD(TAG, "Raw Bucket Sniffing on");
+  this->write(RF_CODE_START);
+  this->write(RF_CODE_RFIN_BUCKET);
+  this->write(RF_CODE_STOP);
+  this->flush();
+}
+
 void RFBridgeComponent::send_raw(std::string raw_code) {
   ESP_LOGD(TAG, "Sending Raw Code: %s", raw_code.c_str());
 
@@ -208,5 +216,16 @@ void RFBridgeComponent::send_raw(std::string raw_code) {
   this->flush();
 }
 
+void RFBridgeComponent::beep(uint16_t ms) {
+  ESP_LOGD(TAG, "Beeping for %hu ms", ms);
+
+  this->write(RF_CODE_START);
+  this->write(RF_CODE_BEEP);
+  this->write((ms >> 8) & 0xFF);
+  this->write(ms & 0xFF);
+  this->write(RF_CODE_STOP);
+  this->flush();
+}
+
 }  // namespace rf_bridge
 }  // namespace esphome
diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h
index b850140b75..573bb2df3f 100644
--- a/esphome/components/rf_bridge/rf_bridge.h
+++ b/esphome/components/rf_bridge/rf_bridge.h
@@ -24,6 +24,7 @@ static const uint8_t RF_CODE_LEARN_KO_NEW = 0xAA;
 static const uint8_t RF_CODE_LEARN_OK_NEW = 0xAB;
 static const uint8_t RF_CODE_RFOUT_BUCKET = 0xB0;
 static const uint8_t RF_CODE_RFIN_BUCKET = 0xB1;
+static const uint8_t RF_CODE_BEEP = 0xC0;
 static const uint8_t RF_CODE_STOP = 0x55;
 static const uint8_t RF_DEBOUNCE = 200;
 
@@ -55,7 +56,9 @@ class RFBridgeComponent : public uart::UARTDevice, public Component {
   void learn();
   void start_advanced_sniffing();
   void stop_advanced_sniffing();
+  void start_bucket_sniffing();
   void send_raw(std::string code);
+  void beep(uint16_t ms);
 
  protected:
   void ack_();
@@ -154,6 +157,16 @@ template<typename... Ts> class RFBridgeStopAdvancedSniffingAction : public Actio
   RFBridgeComponent *parent_;
 };
 
+template<typename... Ts> class RFBridgeStartBucketSniffingAction : public Action<Ts...> {
+ public:
+  RFBridgeStartBucketSniffingAction(RFBridgeComponent *parent) : parent_(parent) {}
+
+  void play(Ts... x) { this->parent_->start_bucket_sniffing(); }
+
+ protected:
+  RFBridgeComponent *parent_;
+};
+
 template<typename... Ts> class RFBridgeSendRawAction : public Action<Ts...> {
  public:
   RFBridgeSendRawAction(RFBridgeComponent *parent) : parent_(parent) {}
@@ -165,5 +178,16 @@ template<typename... Ts> class RFBridgeSendRawAction : public Action<Ts...> {
   RFBridgeComponent *parent_;
 };
 
+template<typename... Ts> class RFBridgeBeepAction : public Action<Ts...> {
+ public:
+  RFBridgeBeepAction(RFBridgeComponent *parent) : parent_(parent) {}
+  TEMPLATABLE_VALUE(uint16_t, duration)
+
+  void play(Ts... x) { this->parent_->beep(this->duration_.value(x...)); }
+
+ protected:
+  RFBridgeComponent *parent_;
+};
+
 }  // namespace rf_bridge
 }  // namespace esphome
diff --git a/esphome/components/rgb/light.py b/esphome/components/rgb/light.py
index 33e381d59e..3d07855b8e 100644
--- a/esphome/components/rgb/light.py
+++ b/esphome/components/rgb/light.py
@@ -16,13 +16,13 @@ CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
-    yield light.register_light(var, config)
+    await light.register_light(var, config)
 
-    red = yield cg.get_variable(config[CONF_RED])
+    red = await cg.get_variable(config[CONF_RED])
     cg.add(var.set_red(red))
-    green = yield cg.get_variable(config[CONF_GREEN])
+    green = await cg.get_variable(config[CONF_GREEN])
     cg.add(var.set_green(green))
-    blue = yield cg.get_variable(config[CONF_BLUE])
+    blue = await cg.get_variable(config[CONF_BLUE])
     cg.add(var.set_blue(blue))
diff --git a/esphome/components/rgbw/light.py b/esphome/components/rgbw/light.py
index a922a7de37..de26edf7d5 100644
--- a/esphome/components/rgbw/light.py
+++ b/esphome/components/rgbw/light.py
@@ -19,16 +19,16 @@ CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
-    yield light.register_light(var, config)
+    await light.register_light(var, config)
 
-    red = yield cg.get_variable(config[CONF_RED])
+    red = await cg.get_variable(config[CONF_RED])
     cg.add(var.set_red(red))
-    green = yield cg.get_variable(config[CONF_GREEN])
+    green = await cg.get_variable(config[CONF_GREEN])
     cg.add(var.set_green(green))
-    blue = yield cg.get_variable(config[CONF_BLUE])
+    blue = await cg.get_variable(config[CONF_BLUE])
     cg.add(var.set_blue(blue))
-    white = yield cg.get_variable(config[CONF_WHITE])
+    white = await cg.get_variable(config[CONF_WHITE])
     cg.add(var.set_white(white))
     cg.add(var.set_color_interlock(config[CONF_COLOR_INTERLOCK]))
diff --git a/esphome/components/rgbww/light.py b/esphome/components/rgbww/light.py
index d7d0d7fb15..d152fbc6db 100644
--- a/esphome/components/rgbww/light.py
+++ b/esphome/components/rgbww/light.py
@@ -34,22 +34,22 @@ CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
-    yield light.register_light(var, config)
+    await light.register_light(var, config)
 
-    red = yield cg.get_variable(config[CONF_RED])
+    red = await cg.get_variable(config[CONF_RED])
     cg.add(var.set_red(red))
-    green = yield cg.get_variable(config[CONF_GREEN])
+    green = await cg.get_variable(config[CONF_GREEN])
     cg.add(var.set_green(green))
-    blue = yield cg.get_variable(config[CONF_BLUE])
+    blue = await cg.get_variable(config[CONF_BLUE])
     cg.add(var.set_blue(blue))
 
-    cwhite = yield cg.get_variable(config[CONF_COLD_WHITE])
+    cwhite = await cg.get_variable(config[CONF_COLD_WHITE])
     cg.add(var.set_cold_white(cwhite))
     cg.add(var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE]))
 
-    wwhite = yield cg.get_variable(config[CONF_WARM_WHITE])
+    wwhite = await cg.get_variable(config[CONF_WARM_WHITE])
     cg.add(var.set_warm_white(wwhite))
     cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE]))
     cg.add(var.set_constant_brightness(config[CONF_CONSTANT_BRIGHTNESS]))
diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py
index bcc4a5c930..079f00d284 100644
--- a/esphome/components/rotary_encoder/sensor.py
+++ b/esphome/components/rotary_encoder/sensor.py
@@ -8,6 +8,7 @@ from esphome.const import (
     CONF_MIN_VALUE,
     CONF_MAX_VALUE,
     DEVICE_CLASS_EMPTY,
+    STATE_CLASS_NONE,
     UNIT_STEPS,
     ICON_ROTATE_RIGHT,
     CONF_VALUE,
@@ -56,7 +57,9 @@ def validate_min_max_value(config):
 
 
 CONFIG_SCHEMA = cv.All(
-    sensor.sensor_schema(UNIT_STEPS, ICON_ROTATE_RIGHT, 0, DEVICE_CLASS_EMPTY)
+    sensor.sensor_schema(
+        UNIT_STEPS, ICON_ROTATE_RIGHT, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(RotaryEncoderSensor),
@@ -91,17 +94,17 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
-    pin_a = yield cg.gpio_pin_expression(config[CONF_PIN_A])
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
+    pin_a = await cg.gpio_pin_expression(config[CONF_PIN_A])
     cg.add(var.set_pin_a(pin_a))
-    pin_b = yield cg.gpio_pin_expression(config[CONF_PIN_B])
+    pin_b = await cg.gpio_pin_expression(config[CONF_PIN_B])
     cg.add(var.set_pin_b(pin_b))
 
     if CONF_PIN_RESET in config:
-        pin_i = yield cg.gpio_pin_expression(config[CONF_PIN_RESET])
+        pin_i = await cg.gpio_pin_expression(config[CONF_PIN_RESET])
         cg.add(var.set_reset_pin(pin_i))
     cg.add(var.set_resolution(config[CONF_RESOLUTION]))
     if CONF_MIN_VALUE in config:
@@ -111,10 +114,10 @@ def to_code(config):
 
     for conf in config.get(CONF_ON_CLOCKWISE, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [], conf)
+        await automation.build_automation(trigger, [], conf)
     for conf in config.get(CONF_ON_ANTICLOCKWISE, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [], conf)
+        await automation.build_automation(trigger, [], conf)
 
 
 @automation.register_action(
@@ -127,9 +130,9 @@ def to_code(config):
         }
     ),
 )
-def sensor_template_publish_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def sensor_template_publish_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    template_ = yield cg.templatable(config[CONF_VALUE], args, int)
+    template_ = await cg.templatable(config[CONF_VALUE], args, int)
     cg.add(var.set_value(template_))
-    yield var
+    return var
diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py
index 97b7405c63..7f860fe3d7 100644
--- a/esphome/components/rtttl/__init__.py
+++ b/esphome/components/rtttl/__init__.py
@@ -1,8 +1,11 @@
+import logging
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import automation
 from esphome.components.output import FloatOutput
-from esphome.const import CONF_ID, CONF_OUTPUT, CONF_TRIGGER_ID
+from esphome.const import CONF_ID, CONF_OUTPUT, CONF_PLATFORM, CONF_TRIGGER_ID
+
+_LOGGER = logging.getLogger(__name__)
 
 CODEOWNERS = ["@glmnet"]
 CONF_RTTTL = "rtttl"
@@ -33,16 +36,43 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
-    var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+def validate(config, item_config):
+    # Not adding this to FloatOutput as this is the only component which needs `update_frequency`
 
-    out = yield cg.get_variable(config[CONF_OUTPUT])
+    parent_config = config.get_config_by_id(item_config[CONF_OUTPUT])
+    platform = parent_config[CONF_PLATFORM]
+
+    PWM_GOOD = ["esp8266_pwm", "ledc"]
+    PWM_BAD = [
+        "ac_dimmer ",
+        "esp32_dac",
+        "slow_pwm",
+        "mcp4725",
+        "pca9685",
+        "tlc59208f",
+        "my9231",
+        "sm16716",
+    ]
+
+    if platform in PWM_BAD:
+        raise ValueError(f"Component rtttl cannot use {platform} as output component")
+
+    if platform not in PWM_GOOD:
+        _LOGGER.warning(
+            "Component rtttl is not known to work with the selected output type. Make sure this output supports custom frequency output method."
+        )
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+
+    out = await cg.get_variable(config[CONF_OUTPUT])
     cg.add(var.set_output(out))
 
     for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [], conf)
+        await automation.build_automation(trigger, [], conf)
 
 
 @automation.register_action(
@@ -56,12 +86,12 @@ def to_code(config):
         key=CONF_RTTTL,
     ),
 )
-def rtttl_play_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def rtttl_play_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    template_ = yield cg.templatable(config[CONF_RTTTL], args, cg.std_string)
+    template_ = await cg.templatable(config[CONF_RTTTL], args, cg.std_string)
     cg.add(var.set_value(template_))
-    yield var
+    return var
 
 
 @automation.register_action(
@@ -73,10 +103,10 @@ def rtttl_play_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def rtttl_stop_to_code(config, action_id, template_arg, args):
+async def rtttl_stop_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    yield var
+    await cg.register_parented(var, config[CONF_ID])
+    return var
 
 
 @automation.register_condition(
@@ -88,7 +118,7 @@ def rtttl_stop_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def rtttl_is_playing_to_code(config, condition_id, template_arg, args):
+async def rtttl_is_playing_to_code(config, condition_id, template_arg, args):
     var = cg.new_Pvariable(condition_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    yield var
+    await cg.register_parented(var, config[CONF_ID])
+    return var
diff --git a/esphome/components/ruuvi_ble/__init__.py b/esphome/components/ruuvi_ble/__init__.py
index 74aec457bb..1e3fb4b003 100644
--- a/esphome/components/ruuvi_ble/__init__.py
+++ b/esphome/components/ruuvi_ble/__init__.py
@@ -17,6 +17,6 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
diff --git a/esphome/components/ruuvitag/sensor.py b/esphome/components/ruuvitag/sensor.py
index 2bde7b485c..12b8425d14 100644
--- a/esphome/components/ruuvitag/sensor.py
+++ b/esphome/components/ruuvitag/sensor.py
@@ -21,6 +21,8 @@ from esphome.const import (
     DEVICE_CLASS_TEMPERATURE,
     DEVICE_CLASS_VOLTAGE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_NONE,
     UNIT_CELSIUS,
     UNIT_PERCENT,
     UNIT_VOLT,
@@ -50,37 +52,69 @@ CONFIG_SCHEMA = (
             cv.GenerateID(): cv.declare_id(RuuviTag),
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 2, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                2,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 2, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                2,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
-                UNIT_HECTOPASCAL, ICON_EMPTY, 2, DEVICE_CLASS_PRESSURE
+                UNIT_HECTOPASCAL,
+                ICON_EMPTY,
+                2,
+                DEVICE_CLASS_PRESSURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_ACCELERATION): sensor.sensor_schema(
-                UNIT_G, ICON_ACCELERATION, 3, DEVICE_CLASS_EMPTY
+                UNIT_G,
+                ICON_ACCELERATION,
+                3,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_ACCELERATION_X): sensor.sensor_schema(
-                UNIT_G, ICON_ACCELERATION_X, 3, DEVICE_CLASS_EMPTY
+                UNIT_G,
+                ICON_ACCELERATION_X,
+                3,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_ACCELERATION_Y): sensor.sensor_schema(
-                UNIT_G, ICON_ACCELERATION_Y, 3, DEVICE_CLASS_EMPTY
+                UNIT_G,
+                ICON_ACCELERATION_Y,
+                3,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_ACCELERATION_Z): sensor.sensor_schema(
-                UNIT_G, ICON_ACCELERATION_Z, 3, DEVICE_CLASS_EMPTY
+                UNIT_G,
+                ICON_ACCELERATION_Z,
+                3,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(
-                UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE
+                UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT
             ),
             cv.Optional(CONF_TX_POWER): sensor.sensor_schema(
-                UNIT_DECIBEL_MILLIWATT, ICON_EMPTY, 0, DEVICE_CLASS_SIGNAL_STRENGTH
+                UNIT_DECIBEL_MILLIWATT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_SIGNAL_STRENGTH,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_MOVEMENT_COUNTER): sensor.sensor_schema(
-                UNIT_EMPTY, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY
+                UNIT_EMPTY, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
             ),
             cv.Optional(CONF_MEASUREMENT_SEQUENCE_NUMBER): sensor.sensor_schema(
-                UNIT_EMPTY, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY
+                UNIT_EMPTY, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
             ),
         }
     )
@@ -89,43 +123,43 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity(sens))
     if CONF_PRESSURE in config:
-        sens = yield sensor.new_sensor(config[CONF_PRESSURE])
+        sens = await sensor.new_sensor(config[CONF_PRESSURE])
         cg.add(var.set_pressure(sens))
     if CONF_ACCELERATION in config:
-        sens = yield sensor.new_sensor(config[CONF_ACCELERATION])
+        sens = await sensor.new_sensor(config[CONF_ACCELERATION])
         cg.add(var.set_acceleration(sens))
     if CONF_ACCELERATION_X in config:
-        sens = yield sensor.new_sensor(config[CONF_ACCELERATION_X])
+        sens = await sensor.new_sensor(config[CONF_ACCELERATION_X])
         cg.add(var.set_acceleration_x(sens))
     if CONF_ACCELERATION_Y in config:
-        sens = yield sensor.new_sensor(config[CONF_ACCELERATION_Y])
+        sens = await sensor.new_sensor(config[CONF_ACCELERATION_Y])
         cg.add(var.set_acceleration_y(sens))
     if CONF_ACCELERATION_Z in config:
-        sens = yield sensor.new_sensor(config[CONF_ACCELERATION_Z])
+        sens = await sensor.new_sensor(config[CONF_ACCELERATION_Z])
         cg.add(var.set_acceleration_z(sens))
     if CONF_BATTERY_VOLTAGE in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_VOLTAGE])
+        sens = await sensor.new_sensor(config[CONF_BATTERY_VOLTAGE])
         cg.add(var.set_battery_voltage(sens))
     if CONF_TX_POWER in config:
-        sens = yield sensor.new_sensor(config[CONF_TX_POWER])
+        sens = await sensor.new_sensor(config[CONF_TX_POWER])
         cg.add(var.set_tx_power(sens))
     if CONF_MOVEMENT_COUNTER in config:
-        sens = yield sensor.new_sensor(config[CONF_MOVEMENT_COUNTER])
+        sens = await sensor.new_sensor(config[CONF_MOVEMENT_COUNTER])
         cg.add(var.set_movement_counter(sens))
     if CONF_MEASUREMENT_SEQUENCE_NUMBER in config:
-        sens = yield sensor.new_sensor(config[CONF_MEASUREMENT_SEQUENCE_NUMBER])
+        sens = await sensor.new_sensor(config[CONF_MEASUREMENT_SEQUENCE_NUMBER])
         cg.add(var.set_measurement_sequence_number(sens))
diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp
index 443deda3cc..e658397e35 100644
--- a/esphome/components/scd30/scd30.cpp
+++ b/esphome/components/scd30/scd30.cpp
@@ -43,14 +43,6 @@ void SCD30Component::setup() {
   ESP_LOGD(TAG, "SCD30 Firmware v%0d.%02d", (uint16_t(raw_firmware_version[0]) >> 8),
            uint16_t(raw_firmware_version[0] & 0xFF));
 
-  /// Sensor initialization
-  if (!this->write_command_(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS, this->ambient_pressure_compensation_)) {
-    ESP_LOGE(TAG, "Sensor SCD30 error starting continuous measurements.");
-    this->error_code_ = MEASUREMENT_INIT_FAILED;
-    this->mark_failed();
-    return;
-  }
-
   if (this->temperature_offset_ != 0) {
     if (!this->write_command_(SCD30_CMD_TEMPERATURE_OFFSET, (uint16_t)(temperature_offset_ * 100.0))) {
       ESP_LOGE(TAG, "Sensor SCD30 error setting temperature offset.");
@@ -59,15 +51,31 @@ void SCD30Component::setup() {
       return;
     }
   }
+#ifdef ARDUINO_ARCH_ESP32
+  // According ESP32 clock stretching is typically 30ms and up to 150ms "due to
+  // internal calibration processes". The I2C peripheral only supports 13ms (at
+  // least when running at 80MHz).
+  // In practise it seems that clock stretching occures during this calibration
+  // calls. It also seems that delays in between calls makes them
+  // disappear/shorter. Hence work around with delays for ESP32.
+  //
+  // By experimentation a delay of 20ms as already sufficient. Let's go
+  // safe and use 30ms delays.
+  delay(30);
+#endif
+
   // The start measurement command disables the altitude compensation, if any, so we only set it if it's turned on
   if (this->altitude_compensation_ != 0xFFFF) {
     if (!this->write_command_(SCD30_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) {
-      ESP_LOGE(TAG, "Sensor SCD30 error starting continuous measurements.");
+      ESP_LOGE(TAG, "Sensor SCD30 error setting altitude compensation.");
       this->error_code_ = MEASUREMENT_INIT_FAILED;
       this->mark_failed();
       return;
     }
   }
+#ifdef ARDUINO_ARCH_ESP32
+  delay(30);
+#endif
 
   if (!this->write_command_(SCD30_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) {
     ESP_LOGE(TAG, "Sensor SCD30 error setting automatic self calibration.");
@@ -75,6 +83,17 @@ void SCD30Component::setup() {
     this->mark_failed();
     return;
   }
+#ifdef ARDUINO_ARCH_ESP32
+  delay(30);
+#endif
+
+  /// Sensor initialization
+  if (!this->write_command_(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS, this->ambient_pressure_compensation_)) {
+    ESP_LOGE(TAG, "Sensor SCD30 error starting continuous measurements.");
+    this->error_code_ = MEASUREMENT_INIT_FAILED;
+    this->mark_failed();
+    return;
+  }
 }
 
 void SCD30Component::dump_config() {
diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py
index aa3e5b65ba..7a08289474 100644
--- a/esphome/components/scd30/sensor.py
+++ b/esphome/components/scd30/sensor.py
@@ -1,4 +1,3 @@
-import re
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import i2c, sensor
@@ -11,6 +10,7 @@ from esphome.const import (
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_PARTS_PER_MILLION,
     ICON_MOLECULE_CO2,
     UNIT_CELSIUS,
@@ -28,26 +28,34 @@ CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation"
 CONF_TEMPERATURE_OFFSET = "temperature_offset"
 
 
-def remove_altitude_suffix(value):
-    return re.sub(r"\s*(?:m(?:\s+a\.s\.l)?)|(?:MAM?SL)$", "", value)
-
-
 CONFIG_SCHEMA = (
     cv.Schema(
         {
             cv.GenerateID(): cv.declare_id(SCD30Component),
             cv.Optional(CONF_CO2): sensor.sensor_schema(
-                UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, 0, DEVICE_CLASS_EMPTY
+                UNIT_PARTS_PER_MILLION,
+                ICON_MOLECULE_CO2,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_AUTOMATIC_SELF_CALIBRATION, default=True): cv.boolean,
             cv.Optional(CONF_ALTITUDE_COMPENSATION): cv.All(
-                remove_altitude_suffix,
+                cv.float_with_unit("altitude", "(m|m a.s.l.|MAMSL|MASL)"),
                 cv.int_range(min=0, max=0xFFFF, max_included=False),
             ),
             cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION, default=0): cv.pressure,
@@ -59,10 +67,10 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     cg.add(var.set_automatic_self_calibration(config[CONF_AUTOMATIC_SELF_CALIBRATION]))
     if CONF_ALTITUDE_COMPENSATION in config:
@@ -79,13 +87,13 @@ def to_code(config):
         cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET]))
 
     if CONF_CO2 in config:
-        sens = yield sensor.new_sensor(config[CONF_CO2])
+        sens = await sensor.new_sensor(config[CONF_CO2])
         cg.add(var.set_co2_sensor(sens))
 
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity_sensor(sens))
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature_sensor(sens))
diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py
index 45b01778da..43356c0036 100644
--- a/esphome/components/script/__init__.py
+++ b/esphome/components/script/__init__.py
@@ -61,7 +61,7 @@ CONFIG_SCHEMA = automation.validate_automation(
 )
 
 
-def to_code(config):
+async def to_code(config):
     # Register all variables first, so that scripts can use other scripts
     triggers = []
     for conf in config:
@@ -73,12 +73,12 @@ def to_code(config):
             cg.add(trigger.set_max_runs(conf[CONF_MAX_RUNS]))
 
         if conf[CONF_MODE] == CONF_QUEUED:
-            yield cg.register_component(trigger, conf)
+            await cg.register_component(trigger, conf)
 
         triggers.append((trigger, conf))
 
     for trigger, conf in triggers:
-        yield automation.build_automation(trigger, [], conf)
+        await automation.build_automation(trigger, [], conf)
 
 
 @automation.register_action(
@@ -90,9 +90,9 @@ def to_code(config):
         }
     ),
 )
-def script_execute_action_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, paren)
+async def script_execute_action_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
 
 
 @automation.register_action(
@@ -100,9 +100,9 @@ def script_execute_action_to_code(config, action_id, template_arg, args):
     ScriptStopAction,
     maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}),
 )
-def script_stop_action_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, paren)
+async def script_stop_action_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
 
 
 @automation.register_action(
@@ -110,11 +110,11 @@ def script_stop_action_to_code(config, action_id, template_arg, args):
     ScriptWaitAction,
     maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}),
 )
-def script_wait_action_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    var = yield cg.new_Pvariable(action_id, template_arg, paren)
-    yield cg.register_component(var, {})
-    yield var
+async def script_wait_action_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    var = cg.new_Pvariable(action_id, template_arg, paren)
+    await cg.register_component(var, {})
+    return var
 
 
 @automation.register_condition(
@@ -122,6 +122,6 @@ def script_wait_action_to_code(config, action_id, template_arg, args):
     IsRunningCondition,
     automation.maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}),
 )
-def script_is_running_to_code(config, condition_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(condition_id, template_arg, paren)
+async def script_is_running_to_code(config, condition_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(condition_id, template_arg, paren)
diff --git a/esphome/components/sdm_meter/__init__.py b/esphome/components/sdm_meter/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/sdm_meter/sdm_meter.cpp b/esphome/components/sdm_meter/sdm_meter.cpp
new file mode 100644
index 0000000000..de7c42421e
--- /dev/null
+++ b/esphome/components/sdm_meter/sdm_meter.cpp
@@ -0,0 +1,106 @@
+#include "sdm_meter.h"
+#include "sdm_meter_registers.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace sdm_meter {
+
+static const char *TAG = "sdm_meter";
+
+static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04;
+static const uint8_t MODBUS_REGISTER_COUNT = 80;  // 74 x 16-bit registers
+
+void SDMMeter::on_modbus_data(const std::vector<uint8_t> &data) {
+  if (data.size() < MODBUS_REGISTER_COUNT * 2) {
+    ESP_LOGW(TAG, "Invalid size for SDMMeter!");
+    return;
+  }
+
+  auto sdm_meter_get_float = [&](size_t i) -> float {
+    uint32_t temp = encode_uint32(data[i], data[i + 1], data[i + 2], data[i + 3]);
+    float f;
+    memcpy(&f, &temp, sizeof(f));
+    return f;
+  };
+
+  for (uint8_t i = 0; i < 3; i++) {
+    auto phase = this->phases_[i];
+    if (!phase.setup)
+      continue;
+
+    float voltage = sdm_meter_get_float(SDM_PHASE_1_VOLTAGE * 2 + (i * 4));
+    float current = sdm_meter_get_float(SDM_PHASE_1_CURRENT * 2 + (i * 4));
+    float active_power = sdm_meter_get_float(SDM_PHASE_1_ACTIVE_POWER * 2 + (i * 4));
+    float apparent_power = sdm_meter_get_float(SDM_PHASE_1_APPARENT_POWER * 2 + (i * 4));
+    float reactive_power = sdm_meter_get_float(SDM_PHASE_1_REACTIVE_POWER * 2 + (i * 4));
+    float power_factor = sdm_meter_get_float(SDM_PHASE_1_POWER_FACTOR * 2 + (i * 4));
+    float phase_angle = sdm_meter_get_float(SDM_PHASE_1_ANGLE * 2 + (i * 4));
+
+    ESP_LOGD(
+        TAG,
+        "SDMMeter Phase %c: V=%.3f V, I=%.3f A, Active P=%.3f W, Apparent P=%.3f VA, Reactive P=%.3f VAR, PF=%.3f, "
+        "PA=%.3f °",
+        i + 'A', voltage, current, active_power, apparent_power, reactive_power, power_factor, phase_angle);
+    if (phase.voltage_sensor_ != nullptr)
+      phase.voltage_sensor_->publish_state(voltage);
+    if (phase.current_sensor_ != nullptr)
+      phase.current_sensor_->publish_state(current);
+    if (phase.active_power_sensor_ != nullptr)
+      phase.active_power_sensor_->publish_state(active_power);
+    if (phase.apparent_power_sensor_ != nullptr)
+      phase.apparent_power_sensor_->publish_state(apparent_power);
+    if (phase.reactive_power_sensor_ != nullptr)
+      phase.reactive_power_sensor_->publish_state(reactive_power);
+    if (phase.power_factor_sensor_ != nullptr)
+      phase.power_factor_sensor_->publish_state(power_factor);
+    if (phase.phase_angle_sensor_ != nullptr)
+      phase.phase_angle_sensor_->publish_state(phase_angle);
+  }
+
+  float frequency = sdm_meter_get_float(SDM_FREQUENCY * 2);
+  float import_active_energy = sdm_meter_get_float(SDM_IMPORT_ACTIVE_ENERGY * 2);
+  float export_active_energy = sdm_meter_get_float(SDM_EXPORT_ACTIVE_ENERGY * 2);
+  float import_reactive_energy = sdm_meter_get_float(SDM_IMPORT_REACTIVE_ENERGY * 2);
+  float export_reactive_energy = sdm_meter_get_float(SDM_EXPORT_REACTIVE_ENERGY * 2);
+
+  ESP_LOGD(TAG, "SDMMeter: F=%.3f Hz, Im.A.E=%.3f Wh, Ex.A.E=%.3f Wh, Im.R.E=%.3f VARh, Ex.R.E=%.3f VARh", frequency,
+           import_active_energy, export_active_energy, import_reactive_energy, export_reactive_energy);
+
+  if (this->frequency_sensor_ != nullptr)
+    this->frequency_sensor_->publish_state(frequency);
+  if (this->import_active_energy_sensor_ != nullptr)
+    this->import_active_energy_sensor_->publish_state(import_active_energy);
+  if (this->export_active_energy_sensor_ != nullptr)
+    this->export_active_energy_sensor_->publish_state(export_active_energy);
+  if (this->import_reactive_energy_sensor_ != nullptr)
+    this->import_reactive_energy_sensor_->publish_state(import_reactive_energy);
+  if (this->export_reactive_energy_sensor_ != nullptr)
+    this->export_reactive_energy_sensor_->publish_state(export_reactive_energy);
+}
+
+void SDMMeter::update() { this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT); }
+void SDMMeter::dump_config() {
+  ESP_LOGCONFIG(TAG, "SDM Meter:");
+  ESP_LOGCONFIG(TAG, "  Address: 0x%02X", this->address_);
+  for (uint8_t i = 0; i < 3; i++) {
+    auto phase = this->phases_[i];
+    if (!phase.setup)
+      continue;
+    ESP_LOGCONFIG(TAG, "  Phase %c", i + 'A');
+    LOG_SENSOR("    ", "Voltage", phase.voltage_sensor_);
+    LOG_SENSOR("    ", "Current", phase.current_sensor_);
+    LOG_SENSOR("    ", "Active Power", phase.active_power_sensor_);
+    LOG_SENSOR("    ", "Apparent Power", phase.apparent_power_sensor_);
+    LOG_SENSOR("    ", "Reactive Power", phase.reactive_power_sensor_);
+    LOG_SENSOR("    ", "Power Factor", phase.power_factor_sensor_);
+    LOG_SENSOR("    ", "Phase Angle", phase.phase_angle_sensor_);
+  }
+  LOG_SENSOR("  ", "Frequency", this->frequency_sensor_);
+  LOG_SENSOR("  ", "Import Active Energy", this->import_active_energy_sensor_);
+  LOG_SENSOR("  ", "Export Active Energy", this->export_active_energy_sensor_);
+  LOG_SENSOR("  ", "Import Reactive Energy", this->import_reactive_energy_sensor_);
+  LOG_SENSOR("  ", "Export Reactive Energy", this->export_reactive_energy_sensor_);
+}
+
+}  // namespace sdm_meter
+}  // namespace esphome
diff --git a/esphome/components/sdm_meter/sdm_meter.h b/esphome/components/sdm_meter/sdm_meter.h
new file mode 100644
index 0000000000..07ebe65bb7
--- /dev/null
+++ b/esphome/components/sdm_meter/sdm_meter.h
@@ -0,0 +1,79 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/modbus/modbus.h"
+
+namespace esphome {
+namespace sdm_meter {
+
+class SDMMeter : public PollingComponent, public modbus::ModbusDevice {
+ public:
+  void set_voltage_sensor(uint8_t phase, sensor::Sensor *voltage_sensor) {
+    this->phases_[phase].setup = true;
+    this->phases_[phase].voltage_sensor_ = voltage_sensor;
+  }
+  void set_current_sensor(uint8_t phase, sensor::Sensor *current_sensor) {
+    this->phases_[phase].setup = true;
+    this->phases_[phase].current_sensor_ = current_sensor;
+  }
+  void set_active_power_sensor(uint8_t phase, sensor::Sensor *active_power_sensor) {
+    this->phases_[phase].setup = true;
+    this->phases_[phase].active_power_sensor_ = active_power_sensor;
+  }
+  void set_apparent_power_sensor(uint8_t phase, sensor::Sensor *apparent_power_sensor) {
+    this->phases_[phase].setup = true;
+    this->phases_[phase].apparent_power_sensor_ = apparent_power_sensor;
+  }
+  void set_reactive_power_sensor(uint8_t phase, sensor::Sensor *reactive_power_sensor) {
+    this->phases_[phase].setup = true;
+    this->phases_[phase].reactive_power_sensor_ = reactive_power_sensor;
+  }
+  void set_power_factor_sensor(uint8_t phase, sensor::Sensor *power_factor_sensor) {
+    this->phases_[phase].setup = true;
+    this->phases_[phase].power_factor_sensor_ = power_factor_sensor;
+  }
+  void set_phase_angle_sensor(uint8_t phase, sensor::Sensor *phase_angle_sensor) {
+    this->phases_[phase].setup = true;
+    this->phases_[phase].phase_angle_sensor_ = phase_angle_sensor;
+  }
+  void set_frequency_sensor(sensor::Sensor *frequency_sensor) { this->frequency_sensor_ = frequency_sensor; }
+  void set_import_active_energy_sensor(sensor::Sensor *import_active_energy_sensor) {
+    this->import_active_energy_sensor_ = import_active_energy_sensor;
+  }
+  void set_export_active_energy_sensor(sensor::Sensor *export_active_energy_sensor) {
+    this->export_active_energy_sensor_ = export_active_energy_sensor;
+  }
+  void set_import_reactive_energy_sensor(sensor::Sensor *import_reactive_energy_sensor) {
+    this->import_reactive_energy_sensor_ = import_reactive_energy_sensor;
+  }
+  void set_export_reactive_energy_sensor(sensor::Sensor *export_reactive_energy_sensor) {
+    this->export_reactive_energy_sensor_ = export_reactive_energy_sensor;
+  }
+
+  void update() override;
+
+  void on_modbus_data(const std::vector<uint8_t> &data) override;
+
+  void dump_config() override;
+
+ protected:
+  struct SDMPhase {
+    bool setup{false};
+    sensor::Sensor *voltage_sensor_{nullptr};
+    sensor::Sensor *current_sensor_{nullptr};
+    sensor::Sensor *active_power_sensor_{nullptr};
+    sensor::Sensor *apparent_power_sensor_{nullptr};
+    sensor::Sensor *reactive_power_sensor_{nullptr};
+    sensor::Sensor *power_factor_sensor_{nullptr};
+    sensor::Sensor *phase_angle_sensor_{nullptr};
+  } phases_[3];
+  sensor::Sensor *frequency_sensor_{nullptr};
+  sensor::Sensor *import_active_energy_sensor_{nullptr};
+  sensor::Sensor *export_active_energy_sensor_{nullptr};
+  sensor::Sensor *import_reactive_energy_sensor_{nullptr};
+  sensor::Sensor *export_reactive_energy_sensor_{nullptr};
+};
+
+}  // namespace sdm_meter
+}  // namespace esphome
diff --git a/esphome/components/sdm_meter/sdm_meter_registers.h b/esphome/components/sdm_meter/sdm_meter_registers.h
new file mode 100644
index 0000000000..dd981d6f00
--- /dev/null
+++ b/esphome/components/sdm_meter/sdm_meter_registers.h
@@ -0,0 +1,114 @@
+#pragma once
+
+namespace esphome {
+namespace sdm_meter {
+
+/* PHASE STATUS REGISTERS */
+static const uint16_t SDM_PHASE_1_VOLTAGE = 0x0000;
+static const uint16_t SDM_PHASE_2_VOLTAGE = 0x0002;
+static const uint16_t SDM_PHASE_3_VOLTAGE = 0x0004;
+static const uint16_t SDM_PHASE_1_CURRENT = 0x0006;
+static const uint16_t SDM_PHASE_2_CURRENT = 0x0008;
+static const uint16_t SDM_PHASE_3_CURRENT = 0x000A;
+static const uint16_t SDM_PHASE_1_ACTIVE_POWER = 0x000C;
+static const uint16_t SDM_PHASE_2_ACTIVE_POWER = 0x000E;
+static const uint16_t SDM_PHASE_3_ACTIVE_POWER = 0x0010;
+static const uint16_t SDM_PHASE_1_APPARENT_POWER = 0x0012;
+static const uint16_t SDM_PHASE_2_APPARENT_POWER = 0x0014;
+static const uint16_t SDM_PHASE_3_APPARENT_POWER = 0x0016;
+static const uint16_t SDM_PHASE_1_REACTIVE_POWER = 0x0018;
+static const uint16_t SDM_PHASE_2_REACTIVE_POWER = 0x001A;
+static const uint16_t SDM_PHASE_3_REACTIVE_POWER = 0x001C;
+static const uint16_t SDM_PHASE_1_POWER_FACTOR = 0x001E;
+static const uint16_t SDM_PHASE_2_POWER_FACTOR = 0x0020;
+static const uint16_t SDM_PHASE_3_POWER_FACTOR = 0x0022;
+static const uint16_t SDM_PHASE_1_ANGLE = 0x0024;
+static const uint16_t SDM_PHASE_2_ANGLE = 0x0026;
+static const uint16_t SDM_PHASE_3_ANGLE = 0x0028;
+
+static const uint16_t SDM_AVERAGE_L_TO_N_VOLTS = 0x002A;
+static const uint16_t SDM_AVERAGE_LINE_CURRENT = 0x002E;
+static const uint16_t SDM_SUM_LINE_CURRENT = 0x0030;
+static const uint16_t SDM_TOTAL_SYSTEM_POWER = 0x0034;
+static const uint16_t SDM_TOTAL_SYSTEM_APPARENT_POWER = 0x0038;
+static const uint16_t SDM_TOTAL_SYSTEM_REACTIVE_POWER = 0x003C;
+static const uint16_t SDM_TOTAL_SYSTEM_POWER_FACTOR = 0x003E;
+static const uint16_t SDM_TOTAL_SYSTEM_PHASE_ANGLE = 0x0042;
+
+static const uint16_t SDM_FREQUENCY = 0x0046;
+
+static const uint16_t SDM_IMPORT_ACTIVE_ENERGY = 0x0048;
+static const uint16_t SDM_EXPORT_ACTIVE_ENERGY = 0x004A;
+static const uint16_t SDM_IMPORT_REACTIVE_ENERGY = 0x004C;
+static const uint16_t SDM_EXPORT_REACTIVE_ENERGY = 0x004E;
+
+static const uint16_t SDM_VAH_SINCE_LAST_RESET = 0x0050;
+static const uint16_t SDM_AH_SINCE_LAST_RESET = 0x0052;
+static const uint16_t SDM_TOTAL_SYSTEM_POWER_DEMAND = 0x0054;
+static const uint16_t SDM_MAXIMUM_TOTAL_SYSTEM_POWER_DEMAND = 0x0056;
+static const uint16_t SDM_CURRENT_SYSTEM_POSITIVE_POWER_DEMAND = 0x0058;
+static const uint16_t SDM_MAXIMUM_SYSTEM_POSITIVE_POWER_DEMAND = 0x005A;
+static const uint16_t SDM_CURRENT_SYSTEM_REVERSE_POWER_DEMAND = 0x005C;
+static const uint16_t SDM_MAXIMUM_SYSTEM_REVERSE_POWER_DEMAND = 0x005E;
+static const uint16_t SDM_TOTAL_SYSTEM_VA_DEMAND = 0x0064;
+static const uint16_t SDM_MAXIMUM_TOTAL_SYSTEM_VA_DEMAND = 0x0066;
+static const uint16_t SDM_NEUTRAL_CURRENT_DEMAND = 0x0068;
+static const uint16_t SDM_MAXIMUM_NEUTRAL_CURRENT = 0x006A;
+static const uint16_t SDM_LINE_1_TO_LINE_2_VOLTS = 0x00C8;
+static const uint16_t SDM_LINE_2_TO_LINE_3_VOLTS = 0x00CA;
+static const uint16_t SDM_LINE_3_TO_LINE_1_VOLTS = 0x00CC;
+static const uint16_t SDM_AVERAGE_LINE_TO_LINE_VOLTS = 0x00CE;
+static const uint16_t SDM_NEUTRAL_CURRENT = 0x00E0;
+
+static const uint16_t SDM_PHASE_1_LN_VOLTS_THD = 0x00EA;
+static const uint16_t SDM_PHASE_2_LN_VOLTS_THD = 0x00EC;
+static const uint16_t SDM_PHASE_3_LN_VOLTS_THD = 0x00EE;
+static const uint16_t SDM_PHASE_1_CURRENT_THD = 0x00F0;
+static const uint16_t SDM_PHASE_2_CURRENT_THD = 0x00F2;
+static const uint16_t SDM_PHASE_3_CURRENT_THD = 0x00F4;
+
+static const uint16_t SDM_AVERAGE_LINE_TO_NEUTRAL_VOLTS_THD = 0x00F8;
+static const uint16_t SDM_AVERAGE_LINE_CURRENT_THD = 0x00FA;
+static const uint16_t SDM_TOTAL_SYSTEM_POWER_FACTOR_INV = 0x00FE;
+static const uint16_t SDM_PHASE_1_CURRENT_DEMAND = 0x0102;
+static const uint16_t SDM_PHASE_2_CURRENT_DEMAND = 0x0104;
+static const uint16_t SDM_PHASE_3_CURRENT_DEMAND = 0x0106;
+static const uint16_t SDM_MAXIMUM_PHASE_1_CURRENT_DEMAND = 0x0108;
+static const uint16_t SDM_MAXIMUM_PHASE_2_CURRENT_DEMAND = 0x010A;
+static const uint16_t SDM_MAXIMUM_PHASE_3_CURRENT_DEMAND = 0x010C;
+static const uint16_t SDM_LINE_1_TO_LINE_2_VOLTS_THD = 0x014E;
+static const uint16_t SDM_LINE_2_TO_LINE_3_VOLTS_THD = 0x0150;
+static const uint16_t SDM_LINE_3_TO_LINE_1_VOLTS_THD = 0x0152;
+static const uint16_t SDM_AVERAGE_LINE_TO_LINE_VOLTS_THD = 0x0154;
+
+static const uint16_t SDM_TOTAL_ACTIVE_ENERGY = 0x0156;
+static const uint16_t SDM_TOTAL_REACTIVE_ENERGY = 0x0158;
+
+static const uint16_t SDM_L1_IMPORT_ACTIVE_ENERGY = 0x015A;
+static const uint16_t SDM_L2_IMPORT_ACTIVE_ENERGY = 0x015C;
+static const uint16_t SDM_L3_IMPORT_ACTIVE_ENERGY = 0x015E;
+static const uint16_t SDM_L1_EXPORT_ACTIVE_ENERGY = 0x0160;
+static const uint16_t SDM_L2_EXPORT_ACTIVE_ENERGY = 0x0162;
+static const uint16_t SDM_L3_EXPORT_ACTIVE_ENERGY = 0x0164;
+static const uint16_t SDM_L1_TOTAL_ACTIVE_ENERGY = 0x0166;
+static const uint16_t SDM_L2_TOTAL_ACTIVE_ENERGY = 0x0168;
+static const uint16_t SDM_L3_TOTAL_ACTIVE_ENERGY = 0x016a;
+static const uint16_t SDM_L1_IMPORT_REACTIVE_ENERGY = 0x016C;
+static const uint16_t SDM_L2_IMPORT_REACTIVE_ENERGY = 0x016E;
+static const uint16_t SDM_L3_IMPORT_REACTIVE_ENERGY = 0x0170;
+static const uint16_t SDM_L1_EXPORT_REACTIVE_ENERGY = 0x0172;
+static const uint16_t SDM_L2_EXPORT_REACTIVE_ENERGY = 0x0174;
+static const uint16_t SDM_L3_EXPORT_REACTIVE_ENERGY = 0x0176;
+static const uint16_t SDM_L1_TOTAL_REACTIVE_ENERGY = 0x0178;
+static const uint16_t SDM_L2_TOTAL_REACTIVE_ENERGY = 0x017A;
+static const uint16_t SDM_L3_TOTAL_REACTIVE_ENERGY = 0x017C;
+
+static const uint16_t SDM_CURRENT_RESETTABLE_TOTAL_ACTIVE_ENERGY = 0x0180;
+static const uint16_t SDM_CURRENT_RESETTABLE_TOTAL_REACTIVE_ENERGY = 0x0182;
+static const uint16_t SDM_CURRENT_RESETTABLE_IMPORT_ENERGY = 0x0184;
+static const uint16_t SDM_CURRENT_RESETTABLE_EXPORT_ENERGY = 0x0186;
+static const uint16_t SDM_IMPORT_POWER = 0x0500;
+static const uint16_t SDM_EXPORT_POWER = 0x0502;
+
+}  // namespace sdm_meter
+}  // namespace esphome
diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py
new file mode 100644
index 0000000000..39ef280fef
--- /dev/null
+++ b/esphome/components/sdm_meter/sensor.py
@@ -0,0 +1,150 @@
+from esphome.components.atm90e32.sensor import CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor, modbus
+from esphome.const import (
+    CONF_ACTIVE_POWER,
+    CONF_APPARENT_POWER,
+    CONF_CURRENT,
+    CONF_EXPORT_ACTIVE_ENERGY,
+    CONF_EXPORT_REACTIVE_ENERGY,
+    CONF_FREQUENCY,
+    CONF_ID,
+    CONF_IMPORT_ACTIVE_ENERGY,
+    CONF_IMPORT_REACTIVE_ENERGY,
+    CONF_PHASE_ANGLE,
+    CONF_POWER_FACTOR,
+    CONF_REACTIVE_POWER,
+    CONF_VOLTAGE,
+    DEVICE_CLASS_CURRENT,
+    DEVICE_CLASS_EMPTY,
+    DEVICE_CLASS_ENERGY,
+    DEVICE_CLASS_POWER,
+    DEVICE_CLASS_POWER_FACTOR,
+    DEVICE_CLASS_VOLTAGE,
+    ICON_CURRENT_AC,
+    ICON_EMPTY,
+    ICON_FLASH,
+    STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_NONE,
+    UNIT_AMPERE,
+    UNIT_DEGREES,
+    UNIT_EMPTY,
+    UNIT_HERTZ,
+    UNIT_VOLT,
+    UNIT_VOLT_AMPS,
+    UNIT_VOLT_AMPS_REACTIVE,
+    UNIT_VOLT_AMPS_REACTIVE_HOURS,
+    UNIT_WATT,
+    UNIT_WATT_HOURS,
+)
+
+AUTO_LOAD = ["modbus"]
+CODEOWNERS = ["@polyfaces", "@jesserockz"]
+
+sdm_meter_ns = cg.esphome_ns.namespace("sdm_meter")
+SDMMeter = sdm_meter_ns.class_("SDMMeter", cg.PollingComponent, modbus.ModbusDevice)
+
+PHASE_SENSORS = {
+    CONF_VOLTAGE: sensor.sensor_schema(UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE),
+    CONF_CURRENT: sensor.sensor_schema(
+        UNIT_AMPERE, ICON_EMPTY, 3, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT
+    ),
+    CONF_ACTIVE_POWER: sensor.sensor_schema(
+        UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+    ),
+    CONF_APPARENT_POWER: sensor.sensor_schema(
+        UNIT_VOLT_AMPS, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+    ),
+    CONF_REACTIVE_POWER: sensor.sensor_schema(
+        UNIT_VOLT_AMPS_REACTIVE,
+        ICON_EMPTY,
+        2,
+        DEVICE_CLASS_POWER,
+        STATE_CLASS_MEASUREMENT,
+    ),
+    CONF_POWER_FACTOR: sensor.sensor_schema(
+        UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_POWER_FACTOR, STATE_CLASS_MEASUREMENT
+    ),
+    CONF_PHASE_ANGLE: sensor.sensor_schema(UNIT_DEGREES, ICON_FLASH, 3),
+}
+
+PHASE_SCHEMA = cv.Schema(
+    {cv.Optional(sensor): schema for sensor, schema in PHASE_SENSORS.items()}
+)
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(SDMMeter),
+            cv.Optional(CONF_PHASE_A): PHASE_SCHEMA,
+            cv.Optional(CONF_PHASE_B): PHASE_SCHEMA,
+            cv.Optional(CONF_PHASE_C): PHASE_SCHEMA,
+            cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(
+                UNIT_HERTZ,
+                ICON_CURRENT_AC,
+                3,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_IMPORT_ACTIVE_ENERGY): sensor.sensor_schema(
+                UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE
+            ),
+            cv.Optional(CONF_EXPORT_ACTIVE_ENERGY): sensor.sensor_schema(
+                UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE
+            ),
+            cv.Optional(CONF_IMPORT_REACTIVE_ENERGY): sensor.sensor_schema(
+                UNIT_VOLT_AMPS_REACTIVE_HOURS,
+                ICON_EMPTY,
+                2,
+                DEVICE_CLASS_ENERGY,
+                STATE_CLASS_NONE,
+            ),
+            cv.Optional(CONF_EXPORT_REACTIVE_ENERGY): sensor.sensor_schema(
+                UNIT_VOLT_AMPS_REACTIVE_HOURS,
+                ICON_EMPTY,
+                2,
+                DEVICE_CLASS_ENERGY,
+                STATE_CLASS_NONE,
+            ),
+        }
+    )
+    .extend(cv.polling_component_schema("10s"))
+    .extend(modbus.modbus_device_schema(0x01))
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await modbus.register_modbus_device(var, config)
+
+    if CONF_FREQUENCY in config:
+        sens = await sensor.new_sensor(config[CONF_FREQUENCY])
+        cg.add(var.set_frequency_sensor(sens))
+
+    if CONF_IMPORT_ACTIVE_ENERGY in config:
+        sens = await sensor.new_sensor(config[CONF_IMPORT_ACTIVE_ENERGY])
+        cg.add(var.set_import_active_energy_sensor(sens))
+
+    if CONF_EXPORT_ACTIVE_ENERGY in config:
+        sens = await sensor.new_sensor(config[CONF_EXPORT_ACTIVE_ENERGY])
+        cg.add(var.set_export_active_energy_sensor(sens))
+
+    if CONF_IMPORT_REACTIVE_ENERGY in config:
+        sens = await sensor.new_sensor(config[CONF_IMPORT_REACTIVE_ENERGY])
+        cg.add(var.set_import_reactive_energy_sensor(sens))
+
+    if CONF_EXPORT_REACTIVE_ENERGY in config:
+        sens = await sensor.new_sensor(config[CONF_EXPORT_REACTIVE_ENERGY])
+        cg.add(var.set_export_reactive_energy_sensor(sens))
+
+    for i, phase in enumerate([CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]):
+        if phase not in config:
+            continue
+
+        phase_config = config[phase]
+        for sensor_type in PHASE_SENSORS:
+            if sensor_type in phase_config:
+                sens = await sensor.new_sensor(phase_config[sensor_type])
+                cg.add(getattr(var, f"set_{sensor_type}_sensor")(i, sens))
diff --git a/esphome/components/sds011/sds011.cpp b/esphome/components/sds011/sds011.cpp
index 1a5be0adc3..ddcbdbb712 100644
--- a/esphome/components/sds011/sds011.cpp
+++ b/esphome/components/sds011/sds011.cpp
@@ -50,6 +50,21 @@ void SDS011Component::setup() {
   this->sds011_write_command_(command_data);
 }
 
+void SDS011Component::set_working_state(bool working_state) {
+  if (this->rx_mode_only_) {
+    // In RX-only mode we do not setup the sensor, it is assumed to be setup
+    // already
+    return;
+  }
+  uint8_t command_data[SDS011_DATA_REQUEST_LENGTH] = {0};
+  command_data[0] = SDS011_COMMAND_SLEEP;
+  command_data[1] = SDS011_SET_MODE;
+  command_data[2] = working_state ? SDS011_MODE_WORK : SDS011_MODE_SLEEP;
+  command_data[13] = 0xff;
+  command_data[14] = 0xff;
+  this->sds011_write_command_(command_data);
+}
+
 void SDS011Component::dump_config() {
   ESP_LOGCONFIG(TAG, "SDS011:");
   ESP_LOGCONFIG(TAG, "  Update Interval: %u min", this->update_interval_min_);
diff --git a/esphome/components/sds011/sds011.h b/esphome/components/sds011/sds011.h
index 83f89df237..19e0cd3efe 100644
--- a/esphome/components/sds011/sds011.h
+++ b/esphome/components/sds011/sds011.h
@@ -25,6 +25,7 @@ class SDS011Component : public Component, public uart::UARTDevice {
   void set_update_interval(uint32_t val) { /* ignore */
   }
   void set_update_interval_min(uint8_t update_interval_min);
+  void set_working_state(bool working_state);
 
  protected:
   void sds011_write_command_(const uint8_t *command);
diff --git a/esphome/components/sds011/sensor.py b/esphome/components/sds011/sensor.py
index fcee6fe7d2..af482839a9 100644
--- a/esphome/components/sds011/sensor.py
+++ b/esphome/components/sds011/sensor.py
@@ -8,6 +8,7 @@ from esphome.const import (
     CONF_RX_ONLY,
     CONF_UPDATE_INTERVAL,
     DEVICE_CLASS_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_MICROGRAMS_PER_CUBIC_METER,
     ICON_CHEMICAL_WEAPON,
 )
@@ -42,12 +43,14 @@ CONFIG_SCHEMA = cv.All(
                 ICON_CHEMICAL_WEAPON,
                 1,
                 DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
                 UNIT_MICROGRAMS_PER_CUBIC_METER,
                 ICON_CHEMICAL_WEAPON,
                 1,
                 DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_RX_ONLY, default=False): cv.boolean,
             cv.Optional(CONF_UPDATE_INTERVAL): cv.positive_time_period_minutes,
@@ -59,19 +62,19 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield uart.register_uart_device(var, config)
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
 
     if CONF_UPDATE_INTERVAL in config:
         cg.add(var.set_update_interval_min(config[CONF_UPDATE_INTERVAL]))
     cg.add(var.set_rx_mode_only(config[CONF_RX_ONLY]))
 
     if CONF_PM_2_5 in config:
-        sens = yield sensor.new_sensor(config[CONF_PM_2_5])
+        sens = await sensor.new_sensor(config[CONF_PM_2_5])
         cg.add(var.set_pm_2_5_sensor(sens))
 
     if CONF_PM_10_0 in config:
-        sens = yield sensor.new_sensor(config[CONF_PM_10_0])
+        sens = await sensor.new_sensor(config[CONF_PM_10_0])
         cg.add(var.set_pm_10_0_sensor(sens))
diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py
index 6f48651563..2d40e12a09 100644
--- a/esphome/components/senseair/sensor.py
+++ b/esphome/components/senseair/sensor.py
@@ -8,6 +8,7 @@ from esphome.const import (
     CONF_ID,
     DEVICE_CLASS_EMPTY,
     ICON_MOLECULE_CO2,
+    STATE_CLASS_MEASUREMENT,
     UNIT_PARTS_PER_MILLION,
 )
 
@@ -38,7 +39,11 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(SenseAirComponent),
             cv.Required(CONF_CO2): sensor.sensor_schema(
-                UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, 0, DEVICE_CLASS_EMPTY
+                UNIT_PARTS_PER_MILLION,
+                ICON_MOLECULE_CO2,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -47,13 +52,13 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield uart.register_uart_device(var, config)
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
 
     if CONF_CO2 in config:
-        sens = yield sensor.new_sensor(config[CONF_CO2])
+        sens = await sensor.new_sensor(config[CONF_CO2])
         cg.add(var.set_co2_sensor(sens))
 
 
@@ -83,6 +88,6 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id(
 @automation.register_action(
     "senseair.abc_get_period", SenseAirABCGetPeriodAction, CALIBRATION_ACTION_SCHEMA
 )
-def senseair_action_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, paren)
+async def senseair_action_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py
index c5df0ca97c..89bde9476a 100644
--- a/esphome/components/sensor/__init__.py
+++ b/esphome/components/sensor/__init__.py
@@ -22,6 +22,7 @@ from esphome.const import (
     CONF_ON_VALUE_RANGE,
     CONF_SEND_EVERY,
     CONF_SEND_FIRST_AT,
+    CONF_STATE_CLASS,
     CONF_TO,
     CONF_TRIGGER_ID,
     CONF_UNIT_OF_MEASUREMENT,
@@ -33,6 +34,8 @@ from esphome.const import (
     ICON_EMPTY,
     DEVICE_CLASS_EMPTY,
     DEVICE_CLASS_BATTERY,
+    DEVICE_CLASS_CARBON_MONOXIDE,
+    DEVICE_CLASS_CARBON_DIOXIDE,
     DEVICE_CLASS_CURRENT,
     DEVICE_CLASS_ENERGY,
     DEVICE_CLASS_HUMIDITY,
@@ -44,27 +47,38 @@ from esphome.const import (
     DEVICE_CLASS_PRESSURE,
     DEVICE_CLASS_TIMESTAMP,
     DEVICE_CLASS_VOLTAGE,
+    STATE_CLASS_NONE,
 )
-from esphome.core import CORE, coroutine, coroutine_with_priority
+from esphome.core import CORE, coroutine_with_priority
 from esphome.util import Registry
 
 CODEOWNERS = ["@esphome/core"]
 DEVICE_CLASSES = [
     DEVICE_CLASS_EMPTY,
     DEVICE_CLASS_BATTERY,
+    DEVICE_CLASS_CARBON_MONOXIDE,
+    DEVICE_CLASS_CARBON_DIOXIDE,
     DEVICE_CLASS_CURRENT,
     DEVICE_CLASS_ENERGY,
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_ILLUMINANCE,
     DEVICE_CLASS_SIGNAL_STRENGTH,
     DEVICE_CLASS_TEMPERATURE,
+    DEVICE_CLASS_TIMESTAMP,
     DEVICE_CLASS_POWER,
     DEVICE_CLASS_POWER_FACTOR,
     DEVICE_CLASS_PRESSURE,
-    DEVICE_CLASS_TIMESTAMP,
     DEVICE_CLASS_VOLTAGE,
 ]
 
+sensor_ns = cg.esphome_ns.namespace("sensor")
+StateClasses = sensor_ns.enum("StateClass")
+STATE_CLASSES = {
+    "": StateClasses.STATE_CLASS_NONE,
+    "measurement": StateClasses.STATE_CLASS_MEASUREMENT,
+}
+validate_state_class = cv.enum(STATE_CLASSES, lower=True, space="_")
+
 IS_PLATFORM_COMPONENT = True
 
 
@@ -153,6 +167,7 @@ SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend(
         cv.Optional(CONF_ICON): icon,
         cv.Optional(CONF_ACCURACY_DECIMALS): accuracy_decimals,
         cv.Optional(CONF_DEVICE_CLASS): device_class,
+        cv.Optional(CONF_STATE_CLASS): validate_state_class,
         cv.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean,
         cv.Optional(CONF_EXPIRE_AFTER): cv.All(
             cv.requires_component("mqtt"),
@@ -186,6 +201,7 @@ def sensor_schema(
     icon_: str,
     accuracy_decimals_: int,
     device_class_: Optional[str] = DEVICE_CLASS_EMPTY,
+    state_class_: Optional[str] = STATE_CLASS_NONE,
 ) -> cv.Schema:
     schema = SENSOR_SCHEMA
     if unit_of_measurement_ != UNIT_EMPTY:
@@ -210,22 +226,26 @@ def sensor_schema(
         schema = schema.extend(
             {cv.Optional(CONF_DEVICE_CLASS, default=device_class_): device_class}
         )
+    if state_class_ != STATE_CLASS_NONE:
+        schema = schema.extend(
+            {cv.Optional(CONF_STATE_CLASS, default=state_class_): validate_state_class}
+        )
     return schema
 
 
 @FILTER_REGISTRY.register("offset", OffsetFilter, cv.float_)
-def offset_filter_to_code(config, filter_id):
-    yield cg.new_Pvariable(filter_id, config)
+async def offset_filter_to_code(config, filter_id):
+    return cg.new_Pvariable(filter_id, config)
 
 
 @FILTER_REGISTRY.register("multiply", MultiplyFilter, cv.float_)
-def multiply_filter_to_code(config, filter_id):
-    yield cg.new_Pvariable(filter_id, config)
+async def multiply_filter_to_code(config, filter_id):
+    return cg.new_Pvariable(filter_id, config)
 
 
 @FILTER_REGISTRY.register("filter_out", FilterOutValueFilter, cv.float_)
-def filter_out_filter_to_code(config, filter_id):
-    yield cg.new_Pvariable(filter_id, config)
+async def filter_out_filter_to_code(config, filter_id):
+    return cg.new_Pvariable(filter_id, config)
 
 
 MEDIAN_SCHEMA = cv.All(
@@ -241,8 +261,8 @@ MEDIAN_SCHEMA = cv.All(
 
 
 @FILTER_REGISTRY.register("median", MedianFilter, MEDIAN_SCHEMA)
-def median_filter_to_code(config, filter_id):
-    yield cg.new_Pvariable(
+async def median_filter_to_code(config, filter_id):
+    return cg.new_Pvariable(
         filter_id,
         config[CONF_WINDOW_SIZE],
         config[CONF_SEND_EVERY],
@@ -263,8 +283,8 @@ MIN_SCHEMA = cv.All(
 
 
 @FILTER_REGISTRY.register("min", MinFilter, MIN_SCHEMA)
-def min_filter_to_code(config, filter_id):
-    yield cg.new_Pvariable(
+async def min_filter_to_code(config, filter_id):
+    return cg.new_Pvariable(
         filter_id,
         config[CONF_WINDOW_SIZE],
         config[CONF_SEND_EVERY],
@@ -285,8 +305,8 @@ MAX_SCHEMA = cv.All(
 
 
 @FILTER_REGISTRY.register("max", MaxFilter, MAX_SCHEMA)
-def max_filter_to_code(config, filter_id):
-    yield cg.new_Pvariable(
+async def max_filter_to_code(config, filter_id):
+    return cg.new_Pvariable(
         filter_id,
         config[CONF_WINDOW_SIZE],
         config[CONF_SEND_EVERY],
@@ -311,8 +331,8 @@ SLIDING_AVERAGE_SCHEMA = cv.All(
     SlidingWindowMovingAverageFilter,
     SLIDING_AVERAGE_SCHEMA,
 )
-def sliding_window_moving_average_filter_to_code(config, filter_id):
-    yield cg.new_Pvariable(
+async def sliding_window_moving_average_filter_to_code(config, filter_id):
+    return cg.new_Pvariable(
         filter_id,
         config[CONF_WINDOW_SIZE],
         config[CONF_SEND_EVERY],
@@ -330,52 +350,52 @@ def sliding_window_moving_average_filter_to_code(config, filter_id):
         }
     ),
 )
-def exponential_moving_average_filter_to_code(config, filter_id):
-    yield cg.new_Pvariable(filter_id, config[CONF_ALPHA], config[CONF_SEND_EVERY])
+async def exponential_moving_average_filter_to_code(config, filter_id):
+    return cg.new_Pvariable(filter_id, config[CONF_ALPHA], config[CONF_SEND_EVERY])
 
 
 @FILTER_REGISTRY.register("lambda", LambdaFilter, cv.returning_lambda)
-def lambda_filter_to_code(config, filter_id):
-    lambda_ = yield cg.process_lambda(
+async def lambda_filter_to_code(config, filter_id):
+    lambda_ = await cg.process_lambda(
         config, [(float, "x")], return_type=cg.optional.template(float)
     )
-    yield cg.new_Pvariable(filter_id, lambda_)
+    return cg.new_Pvariable(filter_id, lambda_)
 
 
 @FILTER_REGISTRY.register("delta", DeltaFilter, cv.float_)
-def delta_filter_to_code(config, filter_id):
-    yield cg.new_Pvariable(filter_id, config)
+async def delta_filter_to_code(config, filter_id):
+    return cg.new_Pvariable(filter_id, config)
 
 
 @FILTER_REGISTRY.register("or", OrFilter, validate_filters)
-def or_filter_to_code(config, filter_id):
-    filters = yield build_filters(config)
-    yield cg.new_Pvariable(filter_id, filters)
+async def or_filter_to_code(config, filter_id):
+    filters = await build_filters(config)
+    return cg.new_Pvariable(filter_id, filters)
 
 
 @FILTER_REGISTRY.register(
     "throttle", ThrottleFilter, cv.positive_time_period_milliseconds
 )
-def throttle_filter_to_code(config, filter_id):
-    yield cg.new_Pvariable(filter_id, config)
+async def throttle_filter_to_code(config, filter_id):
+    return cg.new_Pvariable(filter_id, config)
 
 
 @FILTER_REGISTRY.register(
     "heartbeat", HeartbeatFilter, cv.positive_time_period_milliseconds
 )
-def heartbeat_filter_to_code(config, filter_id):
+async def heartbeat_filter_to_code(config, filter_id):
     var = cg.new_Pvariable(filter_id, config)
-    yield cg.register_component(var, {})
-    yield var
+    await cg.register_component(var, {})
+    return var
 
 
 @FILTER_REGISTRY.register(
     "debounce", DebounceFilter, cv.positive_time_period_milliseconds
 )
-def debounce_filter_to_code(config, filter_id):
+async def debounce_filter_to_code(config, filter_id):
     var = cg.new_Pvariable(filter_id, config)
-    yield cg.register_component(var, {})
-    yield var
+    await cg.register_component(var, {})
+    return var
 
 
 def validate_not_all_from_same(config):
@@ -394,11 +414,11 @@ def validate_not_all_from_same(config):
         cv.ensure_list(validate_datapoint), cv.Length(min=2), validate_not_all_from_same
     ),
 )
-def calibrate_linear_filter_to_code(config, filter_id):
+async def calibrate_linear_filter_to_code(config, filter_id):
     x = [conf[CONF_FROM] for conf in config]
     y = [conf[CONF_TO] for conf in config]
     k, b = fit_linear(x, y)
-    yield cg.new_Pvariable(filter_id, k, b)
+    return cg.new_Pvariable(filter_id, k, b)
 
 
 CONF_DATAPOINTS = "datapoints"
@@ -430,7 +450,7 @@ def validate_calibrate_polynomial(config):
         validate_calibrate_polynomial,
     ),
 )
-def calibrate_polynomial_filter_to_code(config, filter_id):
+async def calibrate_polynomial_filter_to_code(config, filter_id):
     x = [conf[CONF_FROM] for conf in config[CONF_DATAPOINTS]]
     y = [conf[CONF_TO] for conf in config[CONF_DATAPOINTS]]
     degree = config[CONF_DEGREE]
@@ -438,21 +458,21 @@ def calibrate_polynomial_filter_to_code(config, filter_id):
     # Column vector
     b = [[v] for v in y]
     res = [v[0] for v in _lstsq(a, b)]
-    yield cg.new_Pvariable(filter_id, res)
+    return cg.new_Pvariable(filter_id, res)
 
 
-@coroutine
-def build_filters(config):
-    yield cg.build_registry_list(FILTER_REGISTRY, config)
+async def build_filters(config):
+    return await cg.build_registry_list(FILTER_REGISTRY, config)
 
 
-@coroutine
-def setup_sensor_core_(var, config):
+async def setup_sensor_core_(var, config):
     cg.add(var.set_name(config[CONF_NAME]))
     if CONF_INTERNAL in config:
         cg.add(var.set_internal(config[CONF_INTERNAL]))
     if CONF_DEVICE_CLASS in config:
         cg.add(var.set_device_class(config[CONF_DEVICE_CLASS]))
+    if CONF_STATE_CLASS in config:
+        cg.add(var.set_state_class(config[CONF_STATE_CLASS]))
     if CONF_UNIT_OF_MEASUREMENT in config:
         cg.add(var.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT]))
     if CONF_ICON in config:
@@ -461,29 +481,29 @@ def setup_sensor_core_(var, config):
         cg.add(var.set_accuracy_decimals(config[CONF_ACCURACY_DECIMALS]))
     cg.add(var.set_force_update(config[CONF_FORCE_UPDATE]))
     if config.get(CONF_FILTERS):  # must exist and not be empty
-        filters = yield build_filters(config[CONF_FILTERS])
+        filters = await build_filters(config[CONF_FILTERS])
         cg.add(var.set_filters(filters))
 
     for conf in config.get(CONF_ON_VALUE, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [(float, "x")], conf)
+        await automation.build_automation(trigger, [(float, "x")], conf)
     for conf in config.get(CONF_ON_RAW_VALUE, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [(float, "x")], conf)
+        await automation.build_automation(trigger, [(float, "x")], conf)
     for conf in config.get(CONF_ON_VALUE_RANGE, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield cg.register_component(trigger, conf)
+        await cg.register_component(trigger, conf)
         if CONF_ABOVE in conf:
-            template_ = yield cg.templatable(conf[CONF_ABOVE], [(float, "x")], float)
+            template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float)
             cg.add(trigger.set_min(template_))
         if CONF_BELOW in conf:
-            template_ = yield cg.templatable(conf[CONF_BELOW], [(float, "x")], float)
+            template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float)
             cg.add(trigger.set_max(template_))
-        yield automation.build_automation(trigger, [(float, "x")], conf)
+        await automation.build_automation(trigger, [(float, "x")], conf)
 
     if CONF_MQTT_ID in config:
         mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
-        yield mqtt.register_mqtt_component(mqtt_, config)
+        await mqtt.register_mqtt_component(mqtt_, config)
 
         if CONF_EXPIRE_AFTER in config:
             if config[CONF_EXPIRE_AFTER] is None:
@@ -492,19 +512,17 @@ def setup_sensor_core_(var, config):
                 cg.add(mqtt_.set_expire_after(config[CONF_EXPIRE_AFTER]))
 
 
-@coroutine
-def register_sensor(var, config):
+async def register_sensor(var, config):
     if not CORE.has_id(config[CONF_ID]):
         var = cg.Pvariable(config[CONF_ID], var)
     cg.add(cg.App.register_sensor(var))
-    yield setup_sensor_core_(var, config)
+    await setup_sensor_core_(var, config)
 
 
-@coroutine
-def new_sensor(config):
+async def new_sensor(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield register_sensor(var, config)
-    yield var
+    await register_sensor(var, config)
+    return var
 
 
 SENSOR_IN_RANGE_CONDITION_SCHEMA = cv.All(
@@ -520,8 +538,8 @@ SENSOR_IN_RANGE_CONDITION_SCHEMA = cv.All(
 @automation.register_condition(
     "sensor.in_range", SensorInRangeCondition, SENSOR_IN_RANGE_CONDITION_SCHEMA
 )
-def sensor_in_range_to_code(config, condition_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def sensor_in_range_to_code(config, condition_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(condition_id, template_arg, paren)
 
     if CONF_ABOVE in config:
@@ -529,7 +547,7 @@ def sensor_in_range_to_code(config, condition_id, template_arg, args):
     if CONF_BELOW in config:
         cg.add(var.set_max(config[CONF_BELOW]))
 
-    yield var
+    return var
 
 
 def _mean(xs):
@@ -618,6 +636,6 @@ def _lstsq(a, b):
 
 
 @coroutine_with_priority(40.0)
-def to_code(config):
+async def to_code(config):
     cg.add_define("USE_SENSOR")
     cg.add_global(sensor_ns.using)
diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp
index 069a5c5923..d7be618c2a 100644
--- a/esphome/components/sensor/sensor.cpp
+++ b/esphome/components/sensor/sensor.cpp
@@ -6,6 +6,16 @@ namespace sensor {
 
 static const char *TAG = "sensor";
 
+const char *state_class_to_string(StateClass state_class) {
+  switch (state_class) {
+    case STATE_CLASS_MEASUREMENT:
+      return "measurement";
+    case STATE_CLASS_NONE:
+    default:
+      return "";
+  }
+}
+
 void Sensor::publish_state(float state) {
   this->raw_state = state;
   this->raw_callback_.call(state);
@@ -47,6 +57,14 @@ std::string Sensor::get_device_class() {
   return this->device_class();
 }
 std::string Sensor::device_class() { return ""; }
+void Sensor::set_state_class(StateClass state_class) { this->state_class = state_class; }
+void Sensor::set_state_class(const std::string &state_class) {
+  if (str_equals_case_insensitive(state_class, "measurement")) {
+    this->state_class = STATE_CLASS_MEASUREMENT;
+  } else {
+    ESP_LOGW(TAG, "'%s' - Unrecognized state class %s", this->get_name().c_str(), state_class.c_str());
+  }
+}
 std::string Sensor::get_unit_of_measurement() {
   if (this->unit_of_measurement_.has_value())
     return *this->unit_of_measurement_;
diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h
index 6bb6c876ab..fe528603c4 100644
--- a/esphome/components/sensor/sensor.h
+++ b/esphome/components/sensor/sensor.h
@@ -13,6 +13,7 @@ namespace sensor {
     if (!obj->get_device_class().empty()) { \
       ESP_LOGCONFIG(TAG, "%s  Device Class: '%s'", prefix, obj->get_device_class().c_str()); \
     } \
+    ESP_LOGCONFIG(TAG, "%s  State Class: '%s'", prefix, state_class_to_string(obj->state_class)); \
     ESP_LOGCONFIG(TAG, "%s  Unit of Measurement: '%s'", prefix, obj->get_unit_of_measurement().c_str()); \
     ESP_LOGCONFIG(TAG, "%s  Accuracy Decimals: %d", prefix, obj->get_accuracy_decimals()); \
     if (!obj->get_icon().empty()) { \
@@ -26,6 +27,16 @@ namespace sensor {
     } \
   }
 
+/**
+ * Sensor state classes
+ */
+enum StateClass : uint8_t {
+  STATE_CLASS_NONE = 0,
+  STATE_CLASS_MEASUREMENT = 1,
+};
+
+const char *state_class_to_string(StateClass state_class);
+
 /** Base-class for all sensors.
  *
  * A sensor has unit of measurement and can use publish_state to send out a new value with the specified accuracy.
@@ -139,6 +150,13 @@ class Sensor : public Nameable {
   /// Return whether this sensor has gotten a full state (that passed through all filters) yet.
   bool has_state() const;
 
+  // The state class of this sensor state
+  StateClass state_class{STATE_CLASS_NONE};
+
+  /// Manually set the Home Assistant state class (see sensor::state_class)
+  void set_state_class(StateClass state_class);
+  void set_state_class(const std::string &state_class);
+
   /** Override this to set the Home Assistant device class for this sensor.
    *
    * Return "" to disable this feature.
diff --git a/esphome/components/servo/__init__.py b/esphome/components/servo/__init__.py
index 489c295255..7147828a07 100644
--- a/esphome/components/servo/__init__.py
+++ b/esphome/components/servo/__init__.py
@@ -39,11 +39,11 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
-    out = yield cg.get_variable(config[CONF_OUTPUT])
+    out = await cg.get_variable(config[CONF_OUTPUT])
     cg.add(var.set_output(out))
     cg.add(var.set_min_level(config[CONF_MIN_LEVEL]))
     cg.add(var.set_idle_level(config[CONF_IDLE_LEVEL]))
@@ -63,12 +63,12 @@ def to_code(config):
         }
     ),
 )
-def servo_write_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def servo_write_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    template_ = yield cg.templatable(config[CONF_LEVEL], args, float)
+    template_ = await cg.templatable(config[CONF_LEVEL], args, float)
     cg.add(var.set_value(template_))
-    yield var
+    return var
 
 
 @automation.register_action(
@@ -80,6 +80,6 @@ def servo_write_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def servo_detach_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, paren)
+async def servo_detach_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py
index 31b40840e3..f393627eda 100644
--- a/esphome/components/sgp30/sensor.py
+++ b/esphome/components/sgp30/sensor.py
@@ -5,6 +5,7 @@ from esphome.const import (
     CONF_ID,
     DEVICE_CLASS_EMPTY,
     ICON_RADIATOR,
+    STATE_CLASS_MEASUREMENT,
     UNIT_PARTS_PER_MILLION,
     UNIT_PARTS_PER_BILLION,
     ICON_MOLECULE_CO2,
@@ -30,10 +31,18 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(SGP30Component),
             cv.Required(CONF_ECO2): sensor.sensor_schema(
-                UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, 0, DEVICE_CLASS_EMPTY
+                UNIT_PARTS_PER_MILLION,
+                ICON_MOLECULE_CO2,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Required(CONF_TVOC): sensor.sensor_schema(
-                UNIT_PARTS_PER_BILLION, ICON_RADIATOR, 0, DEVICE_CLASS_EMPTY
+                UNIT_PARTS_PER_BILLION,
+                ICON_RADIATOR,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_BASELINE): cv.Schema(
                 {
@@ -54,17 +63,17 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     if CONF_ECO2 in config:
-        sens = yield sensor.new_sensor(config[CONF_ECO2])
+        sens = await sensor.new_sensor(config[CONF_ECO2])
         cg.add(var.set_eco2_sensor(sens))
 
     if CONF_TVOC in config:
-        sens = yield sensor.new_sensor(config[CONF_TVOC])
+        sens = await sensor.new_sensor(config[CONF_TVOC])
         cg.add(var.set_tvoc_sensor(sens))
 
     if CONF_BASELINE in config:
@@ -74,7 +83,7 @@ def to_code(config):
 
     if CONF_COMPENSATION in config:
         compensation_config = config[CONF_COMPENSATION]
-        sens = yield cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE])
+        sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE])
         cg.add(var.set_humidity_sensor(sens))
-        sens = yield cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE])
+        sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE])
         cg.add(var.set_temperature_sensor(sens))
diff --git a/esphome/components/sgp40/sensor.py b/esphome/components/sgp40/sensor.py
index 40bc07389b..36e039d2b5 100644
--- a/esphome/components/sgp40/sensor.py
+++ b/esphome/components/sgp40/sensor.py
@@ -1,7 +1,13 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import i2c, sensor
-from esphome.const import CONF_ID, DEVICE_CLASS_EMPTY, ICON_RADIATOR, UNIT_EMPTY
+from esphome.const import (
+    CONF_ID,
+    DEVICE_CLASS_EMPTY,
+    ICON_RADIATOR,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_EMPTY,
+)
 
 DEPENDENCIES = ["i2c"]
 
@@ -19,7 +25,9 @@ CONF_STORE_BASELINE = "store_baseline"
 CONF_VOC_BASELINE = "voc_baseline"
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_EMPTY, ICON_RADIATOR, 0, DEVICE_CLASS_EMPTY)
+    sensor.sensor_schema(
+        UNIT_EMPTY, ICON_RADIATOR, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(SGP40Component),
@@ -38,17 +46,17 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
+    await sensor.register_sensor(var, config)
 
     if CONF_COMPENSATION in config:
         compensation_config = config[CONF_COMPENSATION]
-        sens = yield cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE])
+        sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE])
         cg.add(var.set_humidity_sensor(sens))
-        sens = yield cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE])
+        sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE])
         cg.add(var.set_temperature_sensor(sens))
 
     cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE]))
diff --git a/esphome/components/sgp40/sgp40.cpp b/esphome/components/sgp40/sgp40.cpp
index 3e9f2b96cf..cfd9766aa9 100644
--- a/esphome/components/sgp40/sgp40.cpp
+++ b/esphome/components/sgp40/sgp40.cpp
@@ -220,7 +220,8 @@ void SGP40Component::update() {
 
   uint32_t voc_index = this->measure_voc_index_();
 
-  if (this->samples_read_++ < this->samples_to_stabalize_) {
+  if (this->samples_read_ < this->samples_to_stabalize_) {
+    this->samples_read_++;
     ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_,
              this->samples_to_stabalize_, voc_index);
     return;
diff --git a/esphome/components/sht3xd/sensor.py b/esphome/components/sht3xd/sensor.py
index 0b30d6780b..2a4bb6594e 100644
--- a/esphome/components/sht3xd/sensor.py
+++ b/esphome/components/sht3xd/sensor.py
@@ -8,6 +8,7 @@ from esphome.const import (
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
 )
@@ -24,10 +25,18 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(SHT3XDComponent),
             cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Required(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -36,15 +45,15 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature_sensor(sens))
 
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity_sensor(sens))
diff --git a/esphome/components/sht4x/sensor.py b/esphome/components/sht4x/sensor.py
index bd3e5c108c..a746ecde07 100644
--- a/esphome/components/sht4x/sensor.py
+++ b/esphome/components/sht4x/sensor.py
@@ -5,6 +5,7 @@ from esphome.const import (
     CONF_ID,
     CONF_TEMPERATURE,
     CONF_HUMIDITY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
     ICON_THERMOMETER,
@@ -50,10 +51,18 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(SHT4XComponent),
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_THERMOMETER, 2, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_THERMOMETER,
+                2,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_WATER_PERCENT, 2, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_WATER_PERCENT,
+                2,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PRECISION, default="High"): cv.enum(PRECISION_OPTIONS),
             cv.Optional(CONF_HEATER_POWER, default="High"): cv.enum(
@@ -75,10 +84,10 @@ TYPES = {
 }
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     cg.add(var.set_precision_value(config[CONF_PRECISION]))
     cg.add(var.set_heater_power_value(config[CONF_HEATER_POWER]))
@@ -88,5 +97,5 @@ def to_code(config):
     for key, funcName in TYPES.items():
 
         if key in config:
-            sens = yield sensor.new_sensor(config[key])
+            sens = await sensor.new_sensor(config[key])
             cg.add(getattr(var, funcName)(sens))
diff --git a/esphome/components/shtcx/sensor.py b/esphome/components/shtcx/sensor.py
index 4b1c6c7afe..af9379218c 100644
--- a/esphome/components/shtcx/sensor.py
+++ b/esphome/components/shtcx/sensor.py
@@ -8,6 +8,7 @@ from esphome.const import (
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
 )
@@ -24,10 +25,18 @@ CONFIG_SCHEMA = (
         {
             cv.GenerateID(): cv.declare_id(SHTCXComponent),
             cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Required(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -36,15 +45,15 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature_sensor(sens))
 
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity_sensor(sens))
diff --git a/esphome/components/shutdown/switch.py b/esphome/components/shutdown/switch.py
index e35029afd7..30c2bc2b74 100644
--- a/esphome/components/shutdown/switch.py
+++ b/esphome/components/shutdown/switch.py
@@ -17,7 +17,7 @@ CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield switch.register_switch(var, config)
+    await cg.register_component(var, config)
+    await switch.register_switch(var, config)
diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py
index 3b06ca6d89..40c011a769 100644
--- a/esphome/components/sim800l/__init__.py
+++ b/esphome/components/sim800l/__init__.py
@@ -42,18 +42,22 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield uart.register_uart_device(var, config)
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
 
     for conf in config.get(CONF_ON_SMS_RECEIVED, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(
+        await automation.build_automation(
             trigger, [(cg.std_string, "message"), (cg.std_string, "sender")], conf
         )
 
 
+def validate(config, item_config):
+    uart.validate_device("sim800l", config, item_config, baud_rate=9600)
+
+
 SIM800L_SEND_SMS_SCHEMA = cv.Schema(
     {
         cv.GenerateID(): cv.use_id(Sim800LComponent),
@@ -66,14 +70,14 @@ SIM800L_SEND_SMS_SCHEMA = cv.Schema(
 @automation.register_action(
     "sim800l.send_sms", Sim800LSendSmsAction, SIM800L_SEND_SMS_SCHEMA
 )
-def sim800l_send_sms_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def sim800l_send_sms_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    template_ = yield cg.templatable(config[CONF_RECIPIENT], args, cg.std_string)
+    template_ = await cg.templatable(config[CONF_RECIPIENT], args, cg.std_string)
     cg.add(var.set_recipient(template_))
-    template_ = yield cg.templatable(config[CONF_MESSAGE], args, cg.std_string)
+    template_ = await cg.templatable(config[CONF_MESSAGE], args, cg.std_string)
     cg.add(var.set_message(template_))
-    yield var
+    return var
 
 
 SIM800L_DIAL_SCHEMA = cv.Schema(
@@ -85,9 +89,9 @@ SIM800L_DIAL_SCHEMA = cv.Schema(
 
 
 @automation.register_action("sim800l.dial", Sim800LDialAction, SIM800L_DIAL_SCHEMA)
-def sim800l_dial_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def sim800l_dial_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    template_ = yield cg.templatable(config[CONF_RECIPIENT], args, cg.std_string)
+    template_ = await cg.templatable(config[CONF_RECIPIENT], args, cg.std_string)
     cg.add(var.set_recipient(template_))
-    yield var
+    return var
diff --git a/esphome/components/slow_pwm/output.py b/esphome/components/slow_pwm/output.py
index f7b26a953a..4f44582eba 100644
--- a/esphome/components/slow_pwm/output.py
+++ b/esphome/components/slow_pwm/output.py
@@ -19,11 +19,11 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield output.register_output(var, config)
+    await cg.register_component(var, config)
+    await output.register_output(var, config)
 
-    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+    pin = await cg.gpio_pin_expression(config[CONF_PIN])
     cg.add(var.set_pin(pin))
     cg.add(var.set_period(config[CONF_PERIOD]))
diff --git a/esphome/components/sm16716/__init__.py b/esphome/components/sm16716/__init__.py
index 8030f78f41..ec8ed722f3 100644
--- a/esphome/components/sm16716/__init__.py
+++ b/esphome/components/sm16716/__init__.py
@@ -25,13 +25,13 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
-    data = yield cg.gpio_pin_expression(config[CONF_DATA_PIN])
+    data = await cg.gpio_pin_expression(config[CONF_DATA_PIN])
     cg.add(var.set_data_pin(data))
-    clock = yield cg.gpio_pin_expression(config[CONF_CLOCK_PIN])
+    clock = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN])
     cg.add(var.set_clock_pin(clock))
 
     cg.add(var.set_num_channels(config[CONF_NUM_CHANNELS]))
diff --git a/esphome/components/sm16716/output.py b/esphome/components/sm16716/output.py
index 033bd86e9c..e9b92f68f8 100644
--- a/esphome/components/sm16716/output.py
+++ b/esphome/components/sm16716/output.py
@@ -18,10 +18,10 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield output.register_output(var, config)
+    await output.register_output(var, config)
 
-    parent = yield cg.get_variable(config[CONF_SM16716_ID])
+    parent = await cg.get_variable(config[CONF_SM16716_ID])
     cg.add(var.set_parent(parent))
     cg.add(var.set_channel(config[CONF_CHANNEL]))
diff --git a/esphome/components/sm2135/__init__.py b/esphome/components/sm2135/__init__.py
index 019a1c68c1..68a9094518 100644
--- a/esphome/components/sm2135/__init__.py
+++ b/esphome/components/sm2135/__init__.py
@@ -23,11 +23,11 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
-    data = yield cg.gpio_pin_expression(config[CONF_DATA_PIN])
+    data = await cg.gpio_pin_expression(config[CONF_DATA_PIN])
     cg.add(var.set_data_pin(data))
-    clock = yield cg.gpio_pin_expression(config[CONF_CLOCK_PIN])
+    clock = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN])
     cg.add(var.set_clock_pin(clock))
diff --git a/esphome/components/sm2135/output.py b/esphome/components/sm2135/output.py
index 86f6db1bb4..5cd969c6a5 100644
--- a/esphome/components/sm2135/output.py
+++ b/esphome/components/sm2135/output.py
@@ -19,10 +19,10 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield output.register_output(var, config)
+    await output.register_output(var, config)
 
-    parent = yield cg.get_variable(config[CONF_SM2135_ID])
+    parent = await cg.get_variable(config[CONF_SM2135_ID])
     cg.add(var.set_parent(parent))
     cg.add(var.set_channel(config[CONF_CHANNEL]))
diff --git a/esphome/components/sm300d2/sensor.py b/esphome/components/sm300d2/sensor.py
index b1df1fb1b8..3d522b3bd5 100644
--- a/esphome/components/sm300d2/sensor.py
+++ b/esphome/components/sm300d2/sensor.py
@@ -13,6 +13,7 @@ from esphome.const import (
     DEVICE_CLASS_EMPTY,
     DEVICE_CLASS_TEMPERATURE,
     DEVICE_CLASS_HUMIDITY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_PARTS_PER_MILLION,
     UNIT_MICROGRAMS_PER_CUBIC_METER,
     UNIT_CELSIUS,
@@ -34,28 +35,53 @@ CONFIG_SCHEMA = cv.All(
         {
             cv.GenerateID(): cv.declare_id(SM300D2Sensor),
             cv.Optional(CONF_CO2): sensor.sensor_schema(
-                UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, 0, DEVICE_CLASS_EMPTY
+                UNIT_PARTS_PER_MILLION,
+                ICON_MOLECULE_CO2,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema(
-                UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_FLASK, 0, DEVICE_CLASS_EMPTY
+                UNIT_MICROGRAMS_PER_CUBIC_METER,
+                ICON_FLASK,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_TVOC): sensor.sensor_schema(
                 UNIT_MICROGRAMS_PER_CUBIC_METER,
                 ICON_CHEMICAL_WEAPON,
                 0,
                 DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
-                UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_GRAIN, 0, DEVICE_CLASS_EMPTY
+                UNIT_MICROGRAMS_PER_CUBIC_METER,
+                ICON_GRAIN,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
-                UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_GRAIN, 0, DEVICE_CLASS_EMPTY
+                UNIT_MICROGRAMS_PER_CUBIC_METER,
+                ICON_GRAIN,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 0, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -64,29 +90,29 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield uart.register_uart_device(var, config)
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
 
     if CONF_CO2 in config:
-        sens = yield sensor.new_sensor(config[CONF_CO2])
+        sens = await sensor.new_sensor(config[CONF_CO2])
         cg.add(var.set_co2_sensor(sens))
     if CONF_FORMALDEHYDE in config:
-        sens = yield sensor.new_sensor(config[CONF_FORMALDEHYDE])
+        sens = await sensor.new_sensor(config[CONF_FORMALDEHYDE])
         cg.add(var.set_formaldehyde_sensor(sens))
     if CONF_TVOC in config:
-        sens = yield sensor.new_sensor(config[CONF_TVOC])
+        sens = await sensor.new_sensor(config[CONF_TVOC])
         cg.add(var.set_tvoc_sensor(sens))
     if CONF_PM_2_5 in config:
-        sens = yield sensor.new_sensor(config[CONF_PM_2_5])
+        sens = await sensor.new_sensor(config[CONF_PM_2_5])
         cg.add(var.set_pm_2_5_sensor(sens))
     if CONF_PM_10_0 in config:
-        sens = yield sensor.new_sensor(config[CONF_PM_10_0])
+        sens = await sensor.new_sensor(config[CONF_PM_10_0])
         cg.add(var.set_pm_10_0_sensor(sens))
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature_sensor(sens))
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity_sensor(sens))
diff --git a/esphome/components/sn74hc595/__init__.py b/esphome/components/sn74hc595/__init__.py
index 152ac02106..4437878970 100644
--- a/esphome/components/sn74hc595/__init__.py
+++ b/esphome/components/sn74hc595/__init__.py
@@ -33,17 +33,17 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    data_pin = yield cg.gpio_pin_expression(config[CONF_DATA_PIN])
+    await cg.register_component(var, config)
+    data_pin = await cg.gpio_pin_expression(config[CONF_DATA_PIN])
     cg.add(var.set_data_pin(data_pin))
-    clock_pin = yield cg.gpio_pin_expression(config[CONF_CLOCK_PIN])
+    clock_pin = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN])
     cg.add(var.set_clock_pin(clock_pin))
-    latch_pin = yield cg.gpio_pin_expression(config[CONF_LATCH_PIN])
+    latch_pin = await cg.gpio_pin_expression(config[CONF_LATCH_PIN])
     cg.add(var.set_latch_pin(latch_pin))
     if CONF_OE_PIN in config:
-        oe_pin = yield cg.gpio_pin_expression(config[CONF_OE_PIN])
+        oe_pin = await cg.gpio_pin_expression(config[CONF_OE_PIN])
         cg.add(var.set_oe_pin(oe_pin))
     cg.add(var.set_sr_count(config[CONF_SR_COUNT]))
 
@@ -61,6 +61,6 @@ SN74HC595_INPUT_PIN_SCHEMA = cv.Schema({})
 @pins.PIN_SCHEMA_REGISTRY.register(
     CONF_SN74HC595, (SN74HC595_OUTPUT_PIN_SCHEMA, SN74HC595_INPUT_PIN_SCHEMA)
 )
-def sn74hc595_pin_to_code(config):
-    parent = yield cg.get_variable(config[CONF_SN74HC595])
-    yield SN74HC595GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_INVERTED])
+async def sn74hc595_pin_to_code(config):
+    parent = await cg.get_variable(config[CONF_SN74HC595])
+    return SN74HC595GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_INVERTED])
diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py
index a142ad72d7..5475dc0a1f 100644
--- a/esphome/components/sntp/time.py
+++ b/esphome/components/sntp/time.py
@@ -22,15 +22,15 @@ CONFIG_SCHEMA = time_.TIME_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
 
     servers = config[CONF_SERVERS]
     servers += [""] * (3 - len(servers))
     cg.add(var.set_servers(*servers))
 
-    yield cg.register_component(var, config)
-    yield time_.register_time(var, config)
+    await cg.register_component(var, config)
+    await time_.register_time(var, config)
 
     if CORE.is_esp8266 and len(servers) > 1:
         # We need LwIP features enabled to get 3 SNTP servers (not just one)
diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py
index fdb0a9af09..0ee31c76a0 100644
--- a/esphome/components/speed/fan/__init__.py
+++ b/esphome/components/speed/fan/__init__.py
@@ -27,18 +27,18 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
-    output_ = yield cg.get_variable(config[CONF_OUTPUT])
-    state = yield fan.create_fan_state(config)
+async def to_code(config):
+    output_ = await cg.get_variable(config[CONF_OUTPUT])
+    state = await fan.create_fan_state(config)
     var = cg.new_Pvariable(
         config[CONF_OUTPUT_ID], state, output_, config[CONF_SPEED_COUNT]
     )
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
     if CONF_OSCILLATION_OUTPUT in config:
-        oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT])
+        oscillation_output = await cg.get_variable(config[CONF_OSCILLATION_OUTPUT])
         cg.add(var.set_oscillating(oscillation_output))
 
     if CONF_DIRECTION_OUTPUT in config:
-        direction_output = yield cg.get_variable(config[CONF_DIRECTION_OUTPUT])
+        direction_output = await cg.get_variable(config[CONF_DIRECTION_OUTPUT])
         cg.add(var.set_direction(direction_output))
diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py
index 5a25ac254b..e6e073c4a4 100644
--- a/esphome/components/spi/__init__.py
+++ b/esphome/components/spi/__init__.py
@@ -9,7 +9,7 @@ from esphome.const import (
     CONF_SPI_ID,
     CONF_CS_PIN,
 )
-from esphome.core import coroutine, coroutine_with_priority
+from esphome.core import coroutine_with_priority
 
 CODEOWNERS = ["@esphome/core"]
 spi_ns = cg.esphome_ns.namespace("spi")
@@ -31,18 +31,18 @@ CONFIG_SCHEMA = cv.All(
 
 
 @coroutine_with_priority(1.0)
-def to_code(config):
+async def to_code(config):
     cg.add_global(spi_ns.using)
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
-    clk = yield cg.gpio_pin_expression(config[CONF_CLK_PIN])
+    clk = await cg.gpio_pin_expression(config[CONF_CLK_PIN])
     cg.add(var.set_clk(clk))
     if CONF_MISO_PIN in config:
-        miso = yield cg.gpio_pin_expression(config[CONF_MISO_PIN])
+        miso = await cg.gpio_pin_expression(config[CONF_MISO_PIN])
         cg.add(var.set_miso(miso))
     if CONF_MOSI_PIN in config:
-        mosi = yield cg.gpio_pin_expression(config[CONF_MOSI_PIN])
+        mosi = await cg.gpio_pin_expression(config[CONF_MOSI_PIN])
         cg.add(var.set_mosi(mosi))
 
 
@@ -61,10 +61,17 @@ def spi_device_schema(cs_pin_required=True):
     return cv.Schema(schema)
 
 
-@coroutine
-def register_spi_device(var, config):
-    parent = yield cg.get_variable(config[CONF_SPI_ID])
+async def register_spi_device(var, config):
+    parent = await cg.get_variable(config[CONF_SPI_ID])
     cg.add(var.set_spi_parent(parent))
     if CONF_CS_PIN in config:
-        pin = yield cg.gpio_pin_expression(config[CONF_CS_PIN])
+        pin = await cg.gpio_pin_expression(config[CONF_CS_PIN])
         cg.add(var.set_cs_pin(pin))
+
+
+def validate_device(name, config, item_config, require_mosi, require_miso):
+    spi_config = config.get_config_by_id(item_config[CONF_SPI_ID])
+    if require_mosi and CONF_MISO_PIN not in spi_config:
+        raise ValueError(f"Component {name} requires parent spi to declare miso_pin")
+    if require_miso and CONF_MOSI_PIN not in spi_config:
+        raise ValueError(f"Component {name} requires parent spi to declare mosi_pin")
diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py
index 5bc586ea8b..219f68c5c8 100644
--- a/esphome/components/sps30/sensor.py
+++ b/esphome/components/sps30/sensor.py
@@ -14,6 +14,7 @@ from esphome.const import (
     CONF_PMC_10_0,
     CONF_PM_SIZE,
     DEVICE_CLASS_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_MICROGRAMS_PER_CUBIC_METER,
     UNIT_COUNTS_PER_CUBIC_METER,
     UNIT_MICROMETER,
@@ -36,42 +37,70 @@ CONFIG_SCHEMA = (
                 ICON_CHEMICAL_WEAPON,
                 2,
                 DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
                 UNIT_MICROGRAMS_PER_CUBIC_METER,
                 ICON_CHEMICAL_WEAPON,
                 2,
                 DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_4_0): sensor.sensor_schema(
                 UNIT_MICROGRAMS_PER_CUBIC_METER,
                 ICON_CHEMICAL_WEAPON,
                 2,
                 DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
                 UNIT_MICROGRAMS_PER_CUBIC_METER,
                 ICON_CHEMICAL_WEAPON,
                 2,
                 DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PMC_0_5): sensor.sensor_schema(
-                UNIT_COUNTS_PER_CUBIC_METER, ICON_COUNTER, 2, DEVICE_CLASS_EMPTY
+                UNIT_COUNTS_PER_CUBIC_METER,
+                ICON_COUNTER,
+                2,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PMC_1_0): sensor.sensor_schema(
-                UNIT_COUNTS_PER_CUBIC_METER, ICON_COUNTER, 2, DEVICE_CLASS_EMPTY
+                UNIT_COUNTS_PER_CUBIC_METER,
+                ICON_COUNTER,
+                2,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PMC_2_5): sensor.sensor_schema(
-                UNIT_COUNTS_PER_CUBIC_METER, ICON_COUNTER, 2, DEVICE_CLASS_EMPTY
+                UNIT_COUNTS_PER_CUBIC_METER,
+                ICON_COUNTER,
+                2,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PMC_4_0): sensor.sensor_schema(
-                UNIT_COUNTS_PER_CUBIC_METER, ICON_COUNTER, 2, DEVICE_CLASS_EMPTY
+                UNIT_COUNTS_PER_CUBIC_METER,
+                ICON_COUNTER,
+                2,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PMC_10_0): sensor.sensor_schema(
-                UNIT_COUNTS_PER_CUBIC_METER, ICON_COUNTER, 2, DEVICE_CLASS_EMPTY
+                UNIT_COUNTS_PER_CUBIC_METER,
+                ICON_COUNTER,
+                2,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_SIZE): sensor.sensor_schema(
-                UNIT_MICROMETER, ICON_RULER, 0, DEVICE_CLASS_EMPTY
+                UNIT_MICROMETER,
+                ICON_RULER,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -80,47 +109,47 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     if CONF_PM_1_0 in config:
-        sens = yield sensor.new_sensor(config[CONF_PM_1_0])
+        sens = await sensor.new_sensor(config[CONF_PM_1_0])
         cg.add(var.set_pm_1_0_sensor(sens))
 
     if CONF_PM_2_5 in config:
-        sens = yield sensor.new_sensor(config[CONF_PM_2_5])
+        sens = await sensor.new_sensor(config[CONF_PM_2_5])
         cg.add(var.set_pm_2_5_sensor(sens))
 
     if CONF_PM_4_0 in config:
-        sens = yield sensor.new_sensor(config[CONF_PM_4_0])
+        sens = await sensor.new_sensor(config[CONF_PM_4_0])
         cg.add(var.set_pm_4_0_sensor(sens))
 
     if CONF_PM_10_0 in config:
-        sens = yield sensor.new_sensor(config[CONF_PM_10_0])
+        sens = await sensor.new_sensor(config[CONF_PM_10_0])
         cg.add(var.set_pm_10_0_sensor(sens))
 
     if CONF_PMC_0_5 in config:
-        sens = yield sensor.new_sensor(config[CONF_PMC_0_5])
+        sens = await sensor.new_sensor(config[CONF_PMC_0_5])
         cg.add(var.set_pmc_0_5_sensor(sens))
 
     if CONF_PMC_1_0 in config:
-        sens = yield sensor.new_sensor(config[CONF_PMC_1_0])
+        sens = await sensor.new_sensor(config[CONF_PMC_1_0])
         cg.add(var.set_pmc_1_0_sensor(sens))
 
     if CONF_PMC_2_5 in config:
-        sens = yield sensor.new_sensor(config[CONF_PMC_2_5])
+        sens = await sensor.new_sensor(config[CONF_PMC_2_5])
         cg.add(var.set_pmc_2_5_sensor(sens))
 
     if CONF_PMC_4_0 in config:
-        sens = yield sensor.new_sensor(config[CONF_PMC_4_0])
+        sens = await sensor.new_sensor(config[CONF_PMC_4_0])
         cg.add(var.set_pmc_4_0_sensor(sens))
 
     if CONF_PMC_10_0 in config:
-        sens = yield sensor.new_sensor(config[CONF_PMC_10_0])
+        sens = await sensor.new_sensor(config[CONF_PMC_10_0])
         cg.add(var.set_pmc_10_0_sensor(sens))
 
     if CONF_PM_SIZE in config:
-        sens = yield sensor.new_sensor(config[CONF_PM_SIZE])
+        sens = await sensor.new_sensor(config[CONF_PM_SIZE])
         cg.add(var.set_pm_size_sensor(sens))
diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py
index af7c1a019a..9652d01efa 100644
--- a/esphome/components/ssd1306_base/__init__.py
+++ b/esphome/components/ssd1306_base/__init__.py
@@ -9,7 +9,6 @@ from esphome.const import (
     CONF_RESET_PIN,
     CONF_BRIGHTNESS,
 )
-from esphome.core import coroutine
 
 ssd1306_base_ns = cg.esphome_ns.namespace("ssd1306_base")
 SSD1306 = ssd1306_base_ns.class_("SSD1306", cg.PollingComponent, display.DisplayBuffer)
@@ -38,21 +37,20 @@ SSD1306_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
 ).extend(cv.polling_component_schema("1s"))
 
 
-@coroutine
-def setup_ssd1036(var, config):
-    yield cg.register_component(var, config)
-    yield display.register_display(var, config)
+async def setup_ssd1036(var, config):
+    await cg.register_component(var, config)
+    await display.register_display(var, config)
 
     cg.add(var.set_model(config[CONF_MODEL]))
     if CONF_RESET_PIN in config:
-        reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN])
+        reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
         cg.add(var.set_reset_pin(reset))
     if CONF_BRIGHTNESS in config:
         cg.add(var.init_brightness(config[CONF_BRIGHTNESS]))
     if CONF_EXTERNAL_VCC in config:
         cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC]))
     if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
+        lambda_ = await cg.process_lambda(
             config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void
         )
         cg.add(var.set_writer(lambda_))
diff --git a/esphome/components/ssd1306_i2c/display.py b/esphome/components/ssd1306_i2c/display.py
index c51ef1cfba..4b51a90431 100644
--- a/esphome/components/ssd1306_i2c/display.py
+++ b/esphome/components/ssd1306_i2c/display.py
@@ -21,7 +21,7 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield ssd1306_base.setup_ssd1036(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await ssd1306_base.setup_ssd1036(var, config)
+    await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp
index 34a963e532..8c535094ac 100644
--- a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp
+++ b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp
@@ -10,8 +10,8 @@ void I2CSSD1306::setup() {
   ESP_LOGCONFIG(TAG, "Setting up I2C SSD1306...");
   this->init_reset_();
 
-  this->parent_->raw_begin_transmission(this->address_);
-  if (!this->parent_->raw_end_transmission(this->address_)) {
+  this->raw_begin_transmission();
+  if (!this->raw_end_transmission()) {
     this->error_code_ = COMMUNICATION_FAILED;
     this->mark_failed();
     return;
diff --git a/esphome/components/ssd1306_spi/display.py b/esphome/components/ssd1306_spi/display.py
index b1558ac413..f7dd1553ba 100644
--- a/esphome/components/ssd1306_spi/display.py
+++ b/esphome/components/ssd1306_spi/display.py
@@ -23,10 +23,10 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield ssd1306_base.setup_ssd1036(var, config)
-    yield spi.register_spi_device(var, config)
+    await ssd1306_base.setup_ssd1036(var, config)
+    await spi.register_spi_device(var, config)
 
-    dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN])
+    dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
     cg.add(var.set_dc_pin(dc))
diff --git a/esphome/components/ssd1322_base/__init__.py b/esphome/components/ssd1322_base/__init__.py
index 0792fd0f89..434caf4e35 100644
--- a/esphome/components/ssd1322_base/__init__.py
+++ b/esphome/components/ssd1322_base/__init__.py
@@ -9,7 +9,6 @@ from esphome.const import (
     CONF_MODEL,
     CONF_RESET_PIN,
 )
-from esphome.core import coroutine
 
 CODEOWNERS = ["@kbx81"]
 
@@ -33,21 +32,20 @@ SSD1322_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
 ).extend(cv.polling_component_schema("1s"))
 
 
-@coroutine
-def setup_ssd1322(var, config):
-    yield cg.register_component(var, config)
-    yield display.register_display(var, config)
+async def setup_ssd1322(var, config):
+    await cg.register_component(var, config)
+    await display.register_display(var, config)
 
     cg.add(var.set_model(config[CONF_MODEL]))
     if CONF_RESET_PIN in config:
-        reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN])
+        reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
         cg.add(var.set_reset_pin(reset))
     if CONF_BRIGHTNESS in config:
         cg.add(var.init_brightness(config[CONF_BRIGHTNESS]))
     if CONF_EXTERNAL_VCC in config:
         cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC]))
     if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
+        lambda_ = await cg.process_lambda(
             config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void
         )
         cg.add(var.set_writer(lambda_))
diff --git a/esphome/components/ssd1322_spi/display.py b/esphome/components/ssd1322_spi/display.py
index fa094acc5f..88b3a53355 100644
--- a/esphome/components/ssd1322_spi/display.py
+++ b/esphome/components/ssd1322_spi/display.py
@@ -25,10 +25,10 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield ssd1322_base.setup_ssd1322(var, config)
-    yield spi.register_spi_device(var, config)
+    await ssd1322_base.setup_ssd1322(var, config)
+    await spi.register_spi_device(var, config)
 
-    dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN])
+    dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
     cg.add(var.set_dc_pin(dc))
diff --git a/esphome/components/ssd1325_base/__init__.py b/esphome/components/ssd1325_base/__init__.py
index 1e6535c7ec..68be287d2a 100644
--- a/esphome/components/ssd1325_base/__init__.py
+++ b/esphome/components/ssd1325_base/__init__.py
@@ -9,7 +9,6 @@ from esphome.const import (
     CONF_MODEL,
     CONF_RESET_PIN,
 )
-from esphome.core import coroutine
 
 CODEOWNERS = ["@kbx81"]
 
@@ -37,21 +36,20 @@ SSD1325_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
 ).extend(cv.polling_component_schema("1s"))
 
 
-@coroutine
-def setup_ssd1325(var, config):
-    yield cg.register_component(var, config)
-    yield display.register_display(var, config)
+async def setup_ssd1325(var, config):
+    await cg.register_component(var, config)
+    await display.register_display(var, config)
 
     cg.add(var.set_model(config[CONF_MODEL]))
     if CONF_RESET_PIN in config:
-        reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN])
+        reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
         cg.add(var.set_reset_pin(reset))
     if CONF_BRIGHTNESS in config:
         cg.add(var.init_brightness(config[CONF_BRIGHTNESS]))
     if CONF_EXTERNAL_VCC in config:
         cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC]))
     if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
+        lambda_ = await cg.process_lambda(
             config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void
         )
         cg.add(var.set_writer(lambda_))
diff --git a/esphome/components/ssd1325_spi/display.py b/esphome/components/ssd1325_spi/display.py
index 6ce133bec1..a86dc751d5 100644
--- a/esphome/components/ssd1325_spi/display.py
+++ b/esphome/components/ssd1325_spi/display.py
@@ -25,10 +25,10 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield ssd1325_base.setup_ssd1325(var, config)
-    yield spi.register_spi_device(var, config)
+    await ssd1325_base.setup_ssd1325(var, config)
+    await spi.register_spi_device(var, config)
 
-    dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN])
+    dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
     cg.add(var.set_dc_pin(dc))
diff --git a/esphome/components/ssd1327_base/__init__.py b/esphome/components/ssd1327_base/__init__.py
index 74446d6f09..eada66a6e3 100644
--- a/esphome/components/ssd1327_base/__init__.py
+++ b/esphome/components/ssd1327_base/__init__.py
@@ -3,7 +3,6 @@ import esphome.config_validation as cv
 from esphome import pins
 from esphome.components import display
 from esphome.const import CONF_BRIGHTNESS, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN
-from esphome.core import coroutine
 
 CODEOWNERS = ["@kbx81"]
 
@@ -26,19 +25,18 @@ SSD1327_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
 ).extend(cv.polling_component_schema("1s"))
 
 
-@coroutine
-def setup_ssd1327(var, config):
-    yield cg.register_component(var, config)
-    yield display.register_display(var, config)
+async def setup_ssd1327(var, config):
+    await cg.register_component(var, config)
+    await display.register_display(var, config)
 
     cg.add(var.set_model(config[CONF_MODEL]))
     if CONF_RESET_PIN in config:
-        reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN])
+        reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
         cg.add(var.set_reset_pin(reset))
     if CONF_BRIGHTNESS in config:
         cg.add(var.init_brightness(config[CONF_BRIGHTNESS]))
     if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
+        lambda_ = await cg.process_lambda(
             config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void
         )
         cg.add(var.set_writer(lambda_))
diff --git a/esphome/components/ssd1327_i2c/display.py b/esphome/components/ssd1327_i2c/display.py
index f13ed003ad..7b581cc92c 100644
--- a/esphome/components/ssd1327_i2c/display.py
+++ b/esphome/components/ssd1327_i2c/display.py
@@ -23,7 +23,7 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield ssd1327_base.setup_ssd1327(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await ssd1327_base.setup_ssd1327(var, config)
+    await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp b/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp
index f256c9df77..16cada0ef2 100644
--- a/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp
+++ b/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp
@@ -10,8 +10,8 @@ void I2CSSD1327::setup() {
   ESP_LOGCONFIG(TAG, "Setting up I2C SSD1327...");
   this->init_reset_();
 
-  this->parent_->raw_begin_transmission(this->address_);
-  if (!this->parent_->raw_end_transmission(this->address_)) {
+  this->raw_begin_transmission();
+  if (!this->raw_end_transmission()) {
     this->error_code_ = COMMUNICATION_FAILED;
     this->mark_failed();
     return;
diff --git a/esphome/components/ssd1327_spi/display.py b/esphome/components/ssd1327_spi/display.py
index fe119836a3..138e85eecd 100644
--- a/esphome/components/ssd1327_spi/display.py
+++ b/esphome/components/ssd1327_spi/display.py
@@ -25,10 +25,10 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield ssd1327_base.setup_ssd1327(var, config)
-    yield spi.register_spi_device(var, config)
+    await ssd1327_base.setup_ssd1327(var, config)
+    await spi.register_spi_device(var, config)
 
-    dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN])
+    dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
     cg.add(var.set_dc_pin(dc))
diff --git a/esphome/components/ssd1331_base/__init__.py b/esphome/components/ssd1331_base/__init__.py
index e151d3fe17..067f55a252 100644
--- a/esphome/components/ssd1331_base/__init__.py
+++ b/esphome/components/ssd1331_base/__init__.py
@@ -3,7 +3,6 @@ import esphome.config_validation as cv
 from esphome import pins
 from esphome.components import display
 from esphome.const import CONF_BRIGHTNESS, CONF_LAMBDA, CONF_RESET_PIN
-from esphome.core import coroutine
 
 CODEOWNERS = ["@kbx81"]
 
@@ -18,18 +17,17 @@ SSD1331_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
 ).extend(cv.polling_component_schema("1s"))
 
 
-@coroutine
-def setup_ssd1331(var, config):
-    yield cg.register_component(var, config)
-    yield display.register_display(var, config)
+async def setup_ssd1331(var, config):
+    await cg.register_component(var, config)
+    await display.register_display(var, config)
 
     if CONF_RESET_PIN in config:
-        reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN])
+        reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
         cg.add(var.set_reset_pin(reset))
     if CONF_BRIGHTNESS in config:
         cg.add(var.init_brightness(config[CONF_BRIGHTNESS]))
     if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
+        lambda_ = await cg.process_lambda(
             config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void
         )
         cg.add(var.set_writer(lambda_))
diff --git a/esphome/components/ssd1331_spi/display.py b/esphome/components/ssd1331_spi/display.py
index c4e9dad8bc..c32ac60578 100644
--- a/esphome/components/ssd1331_spi/display.py
+++ b/esphome/components/ssd1331_spi/display.py
@@ -25,10 +25,10 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield ssd1331_base.setup_ssd1331(var, config)
-    yield spi.register_spi_device(var, config)
+    await ssd1331_base.setup_ssd1331(var, config)
+    await spi.register_spi_device(var, config)
 
-    dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN])
+    dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
     cg.add(var.set_dc_pin(dc))
diff --git a/esphome/components/ssd1351_base/__init__.py b/esphome/components/ssd1351_base/__init__.py
index 5e2c14cb6f..555d6c5e2e 100644
--- a/esphome/components/ssd1351_base/__init__.py
+++ b/esphome/components/ssd1351_base/__init__.py
@@ -3,7 +3,6 @@ import esphome.config_validation as cv
 from esphome import pins
 from esphome.components import display
 from esphome.const import CONF_BRIGHTNESS, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN
-from esphome.core import coroutine
 
 CODEOWNERS = ["@kbx81"]
 
@@ -27,19 +26,18 @@ SSD1351_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
 ).extend(cv.polling_component_schema("1s"))
 
 
-@coroutine
-def setup_ssd1351(var, config):
-    yield cg.register_component(var, config)
-    yield display.register_display(var, config)
+async def setup_ssd1351(var, config):
+    await cg.register_component(var, config)
+    await display.register_display(var, config)
 
     cg.add(var.set_model(config[CONF_MODEL]))
     if CONF_RESET_PIN in config:
-        reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN])
+        reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
         cg.add(var.set_reset_pin(reset))
     if CONF_BRIGHTNESS in config:
         cg.add(var.init_brightness(config[CONF_BRIGHTNESS]))
     if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
+        lambda_ = await cg.process_lambda(
             config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void
         )
         cg.add(var.set_writer(lambda_))
diff --git a/esphome/components/ssd1351_spi/display.py b/esphome/components/ssd1351_spi/display.py
index 66e4fad02c..3f3409226c 100644
--- a/esphome/components/ssd1351_spi/display.py
+++ b/esphome/components/ssd1351_spi/display.py
@@ -25,10 +25,10 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield ssd1351_base.setup_ssd1351(var, config)
-    yield spi.register_spi_device(var, config)
+    await ssd1351_base.setup_ssd1351(var, config)
+    await spi.register_spi_device(var, config)
 
-    dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN])
+    dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
     cg.add(var.set_dc_pin(dc))
diff --git a/esphome/components/st7735/display.py b/esphome/components/st7735/display.py
index f1e7d37d51..c1ede4e0ce 100644
--- a/esphome/components/st7735/display.py
+++ b/esphome/components/st7735/display.py
@@ -1,100 +1,98 @@
-import esphome.codegen as cg
-import esphome.config_validation as cv
-from esphome import pins
-from esphome.components import spi
-from esphome.components import display
-from esphome.core import coroutine
-from esphome.const import (
-    CONF_DC_PIN,
-    CONF_ID,
-    CONF_LAMBDA,
-    CONF_MODEL,
-    CONF_RESET_PIN,
-    CONF_PAGES,
-)
-from . import st7735_ns
-
-CODEOWNERS = ["@SenexCrenshaw"]
-
-DEPENDENCIES = ["spi"]
-
-CONF_DEVICE_WIDTH = "device_width"
-CONF_DEVICE_HEIGHT = "device_height"
-CONF_ROW_START = "row_start"
-CONF_COL_START = "col_start"
-CONF_EIGHT_BIT_COLOR = "eight_bit_color"
-CONF_USE_BGR = "use_bgr"
-
-SPIST7735 = st7735_ns.class_(
-    "ST7735", cg.PollingComponent, display.DisplayBuffer, spi.SPIDevice
-)
-ST7735Model = st7735_ns.enum("ST7735Model")
-
-MODELS = {
-    "INITR_GREENTAB": ST7735Model.ST7735_INITR_GREENTAB,
-    "INITR_REDTAB": ST7735Model.ST7735_INITR_REDTAB,
-    "INITR_BLACKTAB": ST7735Model.ST7735_INITR_BLACKTAB,
-    "INITR_MINI160X80": ST7735Model.ST7735_INITR_MINI_160X80,
-    "INITR_18BLACKTAB": ST7735Model.ST7735_INITR_18BLACKTAB,
-    "INITR_18REDTAB": ST7735Model.ST7735_INITR_18REDTAB,
-}
-ST7735_MODEL = cv.enum(MODELS, upper=True, space="_")
-
-
-ST7735_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
-    {
-        cv.Required(CONF_MODEL): ST7735_MODEL,
-        cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
-    }
-).extend(cv.polling_component_schema("1s"))
-
-CONFIG_SCHEMA = cv.All(
-    ST7735_SCHEMA.extend(
-        {
-            cv.GenerateID(): cv.declare_id(SPIST7735),
-            cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
-            cv.Required(CONF_DEVICE_WIDTH): cv.int_,
-            cv.Required(CONF_DEVICE_HEIGHT): cv.int_,
-            cv.Required(CONF_COL_START): cv.int_,
-            cv.Required(CONF_ROW_START): cv.int_,
-            cv.Optional(CONF_EIGHT_BIT_COLOR, default=False): cv.boolean,
-            cv.Optional(CONF_USE_BGR, default=False): cv.boolean,
-        }
-    )
-    .extend(cv.COMPONENT_SCHEMA)
-    .extend(spi.spi_device_schema()),
-    cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
-)
-
-
-@coroutine
-def setup_st7735(var, config):
-    yield cg.register_component(var, config)
-    yield display.register_display(var, config)
-
-    if CONF_RESET_PIN in config:
-        reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN])
-        cg.add(var.set_reset_pin(reset))
-    if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
-            config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void
-        )
-        cg.add(var.set_writer(lambda_))
-
-
-def to_code(config):
-    var = cg.new_Pvariable(
-        config[CONF_ID],
-        config[CONF_MODEL],
-        config[CONF_DEVICE_WIDTH],
-        config[CONF_DEVICE_HEIGHT],
-        config[CONF_COL_START],
-        config[CONF_ROW_START],
-        config[CONF_EIGHT_BIT_COLOR],
-        config[CONF_USE_BGR],
-    )
-    yield setup_st7735(var, config)
-    yield spi.register_spi_device(var, config)
-
-    dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN])
-    cg.add(var.set_dc_pin(dc))
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import pins
+from esphome.components import spi
+from esphome.components import display
+from esphome.const import (
+    CONF_DC_PIN,
+    CONF_ID,
+    CONF_LAMBDA,
+    CONF_MODEL,
+    CONF_RESET_PIN,
+    CONF_PAGES,
+)
+from . import st7735_ns
+
+CODEOWNERS = ["@SenexCrenshaw"]
+
+DEPENDENCIES = ["spi"]
+
+CONF_DEVICE_WIDTH = "device_width"
+CONF_DEVICE_HEIGHT = "device_height"
+CONF_ROW_START = "row_start"
+CONF_COL_START = "col_start"
+CONF_EIGHT_BIT_COLOR = "eight_bit_color"
+CONF_USE_BGR = "use_bgr"
+
+SPIST7735 = st7735_ns.class_(
+    "ST7735", cg.PollingComponent, display.DisplayBuffer, spi.SPIDevice
+)
+ST7735Model = st7735_ns.enum("ST7735Model")
+
+MODELS = {
+    "INITR_GREENTAB": ST7735Model.ST7735_INITR_GREENTAB,
+    "INITR_REDTAB": ST7735Model.ST7735_INITR_REDTAB,
+    "INITR_BLACKTAB": ST7735Model.ST7735_INITR_BLACKTAB,
+    "INITR_MINI160X80": ST7735Model.ST7735_INITR_MINI_160X80,
+    "INITR_18BLACKTAB": ST7735Model.ST7735_INITR_18BLACKTAB,
+    "INITR_18REDTAB": ST7735Model.ST7735_INITR_18REDTAB,
+}
+ST7735_MODEL = cv.enum(MODELS, upper=True, space="_")
+
+
+ST7735_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
+    {
+        cv.Required(CONF_MODEL): ST7735_MODEL,
+        cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
+    }
+).extend(cv.polling_component_schema("1s"))
+
+CONFIG_SCHEMA = cv.All(
+    ST7735_SCHEMA.extend(
+        {
+            cv.GenerateID(): cv.declare_id(SPIST7735),
+            cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
+            cv.Required(CONF_DEVICE_WIDTH): cv.int_,
+            cv.Required(CONF_DEVICE_HEIGHT): cv.int_,
+            cv.Required(CONF_COL_START): cv.int_,
+            cv.Required(CONF_ROW_START): cv.int_,
+            cv.Optional(CONF_EIGHT_BIT_COLOR, default=False): cv.boolean,
+            cv.Optional(CONF_USE_BGR, default=False): cv.boolean,
+        }
+    )
+    .extend(cv.COMPONENT_SCHEMA)
+    .extend(spi.spi_device_schema()),
+    cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
+)
+
+
+async def setup_st7735(var, config):
+    await cg.register_component(var, config)
+    await display.register_display(var, config)
+
+    if CONF_RESET_PIN in config:
+        reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
+        cg.add(var.set_reset_pin(reset))
+    if CONF_LAMBDA in config:
+        lambda_ = await cg.process_lambda(
+            config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void
+        )
+        cg.add(var.set_writer(lambda_))
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(
+        config[CONF_ID],
+        config[CONF_MODEL],
+        config[CONF_DEVICE_WIDTH],
+        config[CONF_DEVICE_HEIGHT],
+        config[CONF_COL_START],
+        config[CONF_ROW_START],
+        config[CONF_EIGHT_BIT_COLOR],
+        config[CONF_USE_BGR],
+    )
+    await setup_st7735(var, config)
+    await spi.register_spi_device(var, config)
+
+    dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
+    cg.add(var.set_dc_pin(dc))
diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py
index 9b3d550374..a053d00ea2 100644
--- a/esphome/components/st7789v/display.py
+++ b/esphome/components/st7789v/display.py
@@ -38,24 +38,24 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield spi.register_spi_device(var, config)
+    await cg.register_component(var, config)
+    await spi.register_spi_device(var, config)
 
-    dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN])
+    dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
     cg.add(var.set_dc_pin(dc))
 
-    reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN])
+    reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
     cg.add(var.set_reset_pin(reset))
 
-    bl = yield cg.gpio_pin_expression(config[CONF_BACKLIGHT_PIN])
+    bl = await cg.gpio_pin_expression(config[CONF_BACKLIGHT_PIN])
     cg.add(var.set_backlight_pin(bl))
 
     if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
+        lambda_ = await cg.process_lambda(
             config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void
         )
         cg.add(var.set_writer(lambda_))
 
-    yield display.register_display(var, config)
+    await display.register_display(var, config)
diff --git a/esphome/components/status/binary_sensor.py b/esphome/components/status/binary_sensor.py
index 3a52b0b617..e462bc5385 100644
--- a/esphome/components/status/binary_sensor.py
+++ b/esphome/components/status/binary_sensor.py
@@ -18,7 +18,7 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield binary_sensor.register_binary_sensor(var, config)
+    await cg.register_component(var, config)
+    await binary_sensor.register_binary_sensor(var, config)
diff --git a/esphome/components/status_led/__init__.py b/esphome/components/status_led/__init__.py
index 76fbc01f39..92869ee630 100644
--- a/esphome/components/status_led/__init__.py
+++ b/esphome/components/status_led/__init__.py
@@ -16,10 +16,10 @@ CONFIG_SCHEMA = cv.Schema(
 
 
 @coroutine_with_priority(80.0)
-def to_code(config):
-    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+async def to_code(config):
+    pin = await cg.gpio_pin_expression(config[CONF_PIN])
     rhs = StatusLED.new(pin)
     var = cg.Pvariable(config[CONF_ID], rhs)
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
     cg.add(var.pre_setup())
     cg.add_define("USE_STATUS_LED")
diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py
index 53ddc82e09..41b1db7df2 100644
--- a/esphome/components/stepper/__init__.py
+++ b/esphome/components/stepper/__init__.py
@@ -10,7 +10,7 @@ from esphome.const import (
     CONF_TARGET,
     CONF_SPEED,
 )
-from esphome.core import CORE, coroutine, coroutine_with_priority
+from esphome.core import CORE, coroutine_with_priority
 
 IS_PLATFORM_COMPONENT = True
 
@@ -74,8 +74,7 @@ STEPPER_SCHEMA = cv.Schema(
 )
 
 
-@coroutine
-def setup_stepper_core_(stepper_var, config):
+async def setup_stepper_core_(stepper_var, config):
     if CONF_ACCELERATION in config:
         cg.add(stepper_var.set_acceleration(config[CONF_ACCELERATION]))
     if CONF_DECELERATION in config:
@@ -84,11 +83,10 @@ def setup_stepper_core_(stepper_var, config):
         cg.add(stepper_var.set_max_speed(config[CONF_MAX_SPEED]))
 
 
-@coroutine
-def register_stepper(var, config):
+async def register_stepper(var, config):
     if not CORE.has_id(config[CONF_ID]):
         var = cg.Pvariable(config[CONF_ID], var)
-    yield setup_stepper_core_(var, config)
+    await setup_stepper_core_(var, config)
 
 
 @automation.register_action(
@@ -101,12 +99,12 @@ def register_stepper(var, config):
         }
     ),
 )
-def stepper_set_target_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def stepper_set_target_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    template_ = yield cg.templatable(config[CONF_TARGET], args, cg.int32)
+    template_ = await cg.templatable(config[CONF_TARGET], args, cg.int32)
     cg.add(var.set_target(template_))
-    yield var
+    return var
 
 
 @automation.register_action(
@@ -119,12 +117,12 @@ def stepper_set_target_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def stepper_report_position_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def stepper_report_position_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    template_ = yield cg.templatable(config[CONF_POSITION], args, cg.int32)
+    template_ = await cg.templatable(config[CONF_POSITION], args, cg.int32)
     cg.add(var.set_position(template_))
-    yield var
+    return var
 
 
 @automation.register_action(
@@ -137,14 +135,14 @@ def stepper_report_position_to_code(config, action_id, template_arg, args):
         }
     ),
 )
-def stepper_set_speed_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def stepper_set_speed_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    template_ = yield cg.templatable(config[CONF_SPEED], args, cg.int32)
+    template_ = await cg.templatable(config[CONF_SPEED], args, cg.int32)
     cg.add(var.set_speed(template_))
-    yield var
+    return var
 
 
 @coroutine_with_priority(100.0)
-def to_code(config):
+async def to_code(config):
     cg.add_global(stepper_ns.using)
diff --git a/esphome/components/sts3x/sensor.py b/esphome/components/sts3x/sensor.py
index 5e44c3ca3c..9de077c20a 100644
--- a/esphome/components/sts3x/sensor.py
+++ b/esphome/components/sts3x/sensor.py
@@ -1,7 +1,13 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import i2c, sensor
-from esphome.const import CONF_ID, DEVICE_CLASS_TEMPERATURE, ICON_EMPTY, UNIT_CELSIUS
+from esphome.const import (
+    CONF_ID,
+    DEVICE_CLASS_TEMPERATURE,
+    ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+)
 
 DEPENDENCIES = ["i2c"]
 
@@ -12,7 +18,9 @@ STS3XComponent = sts3x_ns.class_(
 )
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE)
+    sensor.sensor_schema(
+        UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(STS3XComponent),
@@ -23,8 +31,8 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
+    await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py
index 61ad4a698a..e7a5e24ee6 100644
--- a/esphome/components/substitutions/__init__.py
+++ b/esphome/components/substitutions/__init__.py
@@ -38,7 +38,7 @@ CONFIG_SCHEMA = cv.Schema(
 )
 
 
-def to_code(config):
+async def to_code(config):
     pass
 
 
diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py
index 8cc911529b..d00a00661a 100644
--- a/esphome/components/sun/__init__.py
+++ b/esphome/components/sun/__init__.py
@@ -115,28 +115,28 @@ CONFIG_SCHEMA = cv.Schema(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    time_ = yield cg.get_variable(config[CONF_TIME_ID])
+    time_ = await cg.get_variable(config[CONF_TIME_ID])
     cg.add(var.set_time(time_))
     cg.add(var.set_latitude(config[CONF_LATITUDE]))
     cg.add(var.set_longitude(config[CONF_LONGITUDE]))
 
     for conf in config.get(CONF_ON_SUNRISE, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
-        yield cg.register_component(trigger, conf)
-        yield cg.register_parented(trigger, var)
+        await cg.register_component(trigger, conf)
+        await cg.register_parented(trigger, var)
         cg.add(trigger.set_sunrise(True))
         cg.add(trigger.set_elevation(conf[CONF_ELEVATION]))
-        yield automation.build_automation(trigger, [], conf)
+        await automation.build_automation(trigger, [], conf)
 
     for conf in config.get(CONF_ON_SUNSET, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
-        yield cg.register_component(trigger, conf)
-        yield cg.register_parented(trigger, var)
+        await cg.register_component(trigger, conf)
+        await cg.register_parented(trigger, var)
         cg.add(trigger.set_sunrise(False))
         cg.add(trigger.set_elevation(conf[CONF_ELEVATION]))
-        yield automation.build_automation(trigger, [], conf)
+        await automation.build_automation(trigger, [], conf)
 
 
 @automation.register_condition(
@@ -151,13 +151,13 @@ def to_code(config):
         }
     ),
 )
-def sun_above_horizon_to_code(config, condition_id, template_arg, args):
+async def sun_above_horizon_to_code(config, condition_id, template_arg, args):
     var = cg.new_Pvariable(condition_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    templ = yield cg.templatable(config[CONF_ELEVATION], args, cg.double)
+    await cg.register_parented(var, config[CONF_ID])
+    templ = await cg.templatable(config[CONF_ELEVATION], args, cg.double)
     cg.add(var.set_elevation(templ))
     cg.add(var.set_above(True))
-    yield var
+    return var
 
 
 @automation.register_condition(
@@ -172,10 +172,10 @@ def sun_above_horizon_to_code(config, condition_id, template_arg, args):
         }
     ),
 )
-def sun_below_horizon_to_code(config, condition_id, template_arg, args):
+async def sun_below_horizon_to_code(config, condition_id, template_arg, args):
     var = cg.new_Pvariable(condition_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    templ = yield cg.templatable(config[CONF_ELEVATION], args, cg.double)
+    await cg.register_parented(var, config[CONF_ID])
+    templ = await cg.templatable(config[CONF_ELEVATION], args, cg.double)
     cg.add(var.set_elevation(templ))
     cg.add(var.set_above(False))
-    yield var
+    return var
diff --git a/esphome/components/sun/sensor/__init__.py b/esphome/components/sun/sensor/__init__.py
index 02e1ef28eb..644490ffc6 100644
--- a/esphome/components/sun/sensor/__init__.py
+++ b/esphome/components/sun/sensor/__init__.py
@@ -3,6 +3,7 @@ import esphome.config_validation as cv
 from esphome.components import sensor
 from esphome.const import (
     DEVICE_CLASS_EMPTY,
+    STATE_CLASS_NONE,
     UNIT_DEGREES,
     ICON_WEATHER_SUNSET,
     CONF_ID,
@@ -20,7 +21,9 @@ TYPES = {
 }
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_DEGREES, ICON_WEATHER_SUNSET, 1, DEVICE_CLASS_EMPTY)
+    sensor.sensor_schema(
+        UNIT_DEGREES, ICON_WEATHER_SUNSET, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(SunSensor),
@@ -32,11 +35,11 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
 
     cg.add(var.set_type(config[CONF_TYPE]))
-    paren = yield cg.get_variable(config[CONF_SUN_ID])
+    paren = await cg.get_variable(config[CONF_SUN_ID])
     cg.add(var.set_parent(paren))
diff --git a/esphome/components/sun/text_sensor/__init__.py b/esphome/components/sun/text_sensor/__init__.py
index b527da699b..ac1ce223d1 100644
--- a/esphome/components/sun/text_sensor/__init__.py
+++ b/esphome/components/sun/text_sensor/__init__.py
@@ -46,12 +46,12 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield text_sensor.register_text_sensor(var, config)
+    await cg.register_component(var, config)
+    await text_sensor.register_text_sensor(var, config)
 
-    paren = yield cg.get_variable(config[CONF_SUN_ID])
+    paren = await cg.get_variable(config[CONF_SUN_ID])
     cg.add(var.set_parent(paren))
     cg.add(var.set_sunrise(SUN_TYPES[config[CONF_TYPE]]))
     cg.add(var.set_elevation(config[CONF_ELEVATION]))
diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py
index cc47b059cb..647041c19c 100644
--- a/esphome/components/switch/__init__.py
+++ b/esphome/components/switch/__init__.py
@@ -14,7 +14,7 @@ from esphome.const import (
     CONF_MQTT_ID,
     CONF_NAME,
 )
-from esphome.core import CORE, coroutine, coroutine_with_priority
+from esphome.core import CORE, coroutine_with_priority
 
 CODEOWNERS = ["@esphome/core"]
 IS_PLATFORM_COMPONENT = True
@@ -57,8 +57,7 @@ SWITCH_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend(
 )
 
 
-@coroutine
-def setup_switch_core_(var, config):
+async def setup_switch_core_(var, config):
     cg.add(var.set_name(config[CONF_NAME]))
     if CONF_INTERNAL in config:
         cg.add(var.set_internal(config[CONF_INTERNAL]))
@@ -68,22 +67,21 @@ def setup_switch_core_(var, config):
         cg.add(var.set_inverted(config[CONF_INVERTED]))
     for conf in config.get(CONF_ON_TURN_ON, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [], conf)
+        await automation.build_automation(trigger, [], conf)
     for conf in config.get(CONF_ON_TURN_OFF, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [], conf)
+        await automation.build_automation(trigger, [], conf)
 
     if CONF_MQTT_ID in config:
         mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
-        yield mqtt.register_mqtt_component(mqtt_, config)
+        await mqtt.register_mqtt_component(mqtt_, config)
 
 
-@coroutine
-def register_switch(var, config):
+async def register_switch(var, config):
     if not CORE.has_id(config[CONF_ID]):
         var = cg.Pvariable(config[CONF_ID], var)
     cg.add(cg.App.register_switch(var))
-    yield setup_switch_core_(var, config)
+    await setup_switch_core_(var, config)
 
 
 SWITCH_ACTION_SCHEMA = maybe_simple_id(
@@ -96,24 +94,24 @@ SWITCH_ACTION_SCHEMA = maybe_simple_id(
 @automation.register_action("switch.toggle", ToggleAction, SWITCH_ACTION_SCHEMA)
 @automation.register_action("switch.turn_off", TurnOffAction, SWITCH_ACTION_SCHEMA)
 @automation.register_action("switch.turn_on", TurnOnAction, SWITCH_ACTION_SCHEMA)
-def switch_toggle_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(action_id, template_arg, paren)
+async def switch_toggle_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(action_id, template_arg, paren)
 
 
 @automation.register_condition("switch.is_on", SwitchCondition, SWITCH_ACTION_SCHEMA)
-def switch_is_on_to_code(config, condition_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(condition_id, template_arg, paren, True)
+async def switch_is_on_to_code(config, condition_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(condition_id, template_arg, paren, True)
 
 
 @automation.register_condition("switch.is_off", SwitchCondition, SWITCH_ACTION_SCHEMA)
-def switch_is_off_to_code(config, condition_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(condition_id, template_arg, paren, False)
+async def switch_is_off_to_code(config, condition_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(condition_id, template_arg, paren, False)
 
 
 @coroutine_with_priority(100.0)
-def to_code(config):
+async def to_code(config):
     cg.add_global(switch_ns.using)
     cg.add_define("USE_SWITCH")
diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py
index 06ee364e1b..8e1239924a 100644
--- a/esphome/components/sx1509/__init__.py
+++ b/esphome/components/sx1509/__init__.py
@@ -47,10 +47,10 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
     if CONF_KEYPAD in config:
         keypad = config[CONF_KEYPAD]
         cg.add(var.set_rows_cols(keypad[CONF_KEY_ROWS], keypad[CONF_KEY_COLUMNS]))
@@ -90,8 +90,8 @@ SX1509_INPUT_PIN_SCHEMA = cv.Schema(
 @pins.PIN_SCHEMA_REGISTRY.register(
     CONF_SX1509, (SX1509_OUTPUT_PIN_SCHEMA, SX1509_INPUT_PIN_SCHEMA)
 )
-def sx1509_pin_to_code(config):
-    parent = yield cg.get_variable(config[CONF_SX1509])
-    yield SX1509GPIOPin.new(
+async def sx1509_pin_to_code(config):
+    parent = await cg.get_variable(config[CONF_SX1509])
+    return SX1509GPIOPin.new(
         parent, config[CONF_NUMBER], config[CONF_MODE], config[CONF_INVERTED]
     )
diff --git a/esphome/components/sx1509/binary_sensor/__init__.py b/esphome/components/sx1509/binary_sensor/__init__.py
index e2344cd4af..1560af8e99 100644
--- a/esphome/components/sx1509/binary_sensor/__init__.py
+++ b/esphome/components/sx1509/binary_sensor/__init__.py
@@ -21,10 +21,10 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield binary_sensor.register_binary_sensor(var, config)
-    hub = yield cg.get_variable(config[CONF_SX1509_ID])
+    await binary_sensor.register_binary_sensor(var, config)
+    hub = await cg.get_variable(config[CONF_SX1509_ID])
     cg.add(var.set_row_col(config[CONF_ROW], config[CONF_COL]))
 
     cg.add(hub.register_keypad_binary_sensor(var))
diff --git a/esphome/components/sx1509/output/__init__.py b/esphome/components/sx1509/output/__init__.py
index 878534e829..7afea0fbf8 100644
--- a/esphome/components/sx1509/output/__init__.py
+++ b/esphome/components/sx1509/output/__init__.py
@@ -19,10 +19,10 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
-    parent = yield cg.get_variable(config[CONF_SX1509_ID])
+async def to_code(config):
+    parent = await cg.get_variable(config[CONF_SX1509_ID])
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield output.register_output(var, config)
+    await cg.register_component(var, config)
+    await output.register_output(var, config)
     cg.add(var.set_pin(config[CONF_PIN]))
     cg.add(var.set_parent(parent))
diff --git a/esphome/components/tca9548a/__init__.py b/esphome/components/tca9548a/__init__.py
index aedd751086..62cbace56a 100644
--- a/esphome/components/tca9548a/__init__.py
+++ b/esphome/components/tca9548a/__init__.py
@@ -22,9 +22,9 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(i2c.i2c_device_schema(0x70))
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     cg.add_define("USE_I2C_MULTIPLEXER")
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
     cg.add(var.set_scan(config[CONF_SCAN]))
diff --git a/esphome/components/tcl112/climate.py b/esphome/components/tcl112/climate.py
index 587bdcfbe9..9facd6b8db 100644
--- a/esphome/components/tcl112/climate.py
+++ b/esphome/components/tcl112/climate.py
@@ -16,6 +16,6 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield climate_ir.register_climate_ir(var, config)
+    await climate_ir.register_climate_ir(var, config)
diff --git a/esphome/components/tcl112/tcl112.cpp b/esphome/components/tcl112/tcl112.cpp
index 3e7eb7ec9a..91cec27094 100644
--- a/esphome/components/tcl112/tcl112.cpp
+++ b/esphome/components/tcl112/tcl112.cpp
@@ -88,7 +88,7 @@ void Tcl112Climate::transmit_state() {
 
   // Set fan
   uint8_t selected_fan;
-  switch (this->fan_mode) {
+  switch (this->fan_mode.value()) {
     case climate::CLIMATE_FAN_HIGH:
       selected_fan = TCL112_FAN_HIGH;
       break;
diff --git a/esphome/components/tcs34725/sensor.py b/esphome/components/tcs34725/sensor.py
index 84e3c290bf..d0fa0c1732 100644
--- a/esphome/components/tcs34725/sensor.py
+++ b/esphome/components/tcs34725/sensor.py
@@ -11,6 +11,7 @@ from esphome.const import (
     DEVICE_CLASS_ILLUMINANCE,
     ICON_EMPTY,
     ICON_LIGHTBULB,
+    STATE_CLASS_MEASUREMENT,
     UNIT_PERCENT,
     ICON_THERMOMETER,
     UNIT_KELVIN,
@@ -48,13 +49,13 @@ TCS34725_GAINS = {
 }
 
 color_channel_schema = sensor.sensor_schema(
-    UNIT_PERCENT, ICON_LIGHTBULB, 1, DEVICE_CLASS_EMPTY
+    UNIT_PERCENT, ICON_LIGHTBULB, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
 )
 color_temperature_schema = sensor.sensor_schema(
-    UNIT_KELVIN, ICON_THERMOMETER, 1, DEVICE_CLASS_EMPTY
+    UNIT_KELVIN, ICON_THERMOMETER, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
 )
 illuminance_schema = sensor.sensor_schema(
-    UNIT_LUX, ICON_EMPTY, 1, DEVICE_CLASS_ILLUMINANCE
+    UNIT_LUX, ICON_EMPTY, 1, DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT
 )
 
 CONFIG_SCHEMA = (
@@ -78,29 +79,29 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
 
     cg.add(var.set_integration_time(config[CONF_INTEGRATION_TIME]))
     cg.add(var.set_gain(config[CONF_GAIN]))
 
     if CONF_RED_CHANNEL in config:
-        sens = yield sensor.new_sensor(config[CONF_RED_CHANNEL])
+        sens = await sensor.new_sensor(config[CONF_RED_CHANNEL])
         cg.add(var.set_red_sensor(sens))
     if CONF_GREEN_CHANNEL in config:
-        sens = yield sensor.new_sensor(config[CONF_GREEN_CHANNEL])
+        sens = await sensor.new_sensor(config[CONF_GREEN_CHANNEL])
         cg.add(var.set_green_sensor(sens))
     if CONF_BLUE_CHANNEL in config:
-        sens = yield sensor.new_sensor(config[CONF_BLUE_CHANNEL])
+        sens = await sensor.new_sensor(config[CONF_BLUE_CHANNEL])
         cg.add(var.set_blue_sensor(sens))
     if CONF_CLEAR_CHANNEL in config:
-        sens = yield sensor.new_sensor(config[CONF_CLEAR_CHANNEL])
+        sens = await sensor.new_sensor(config[CONF_CLEAR_CHANNEL])
         cg.add(var.set_clear_sensor(sens))
     if CONF_ILLUMINANCE in config:
-        sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE])
+        sens = await sensor.new_sensor(config[CONF_ILLUMINANCE])
         cg.add(var.set_illuminance_sensor(sens))
     if CONF_COLOR_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_COLOR_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_COLOR_TEMPERATURE])
         cg.add(var.set_color_temperature_sensor(sens))
diff --git a/esphome/components/teleinfo/__init__.py b/esphome/components/teleinfo/__init__.py
index 2e279e892e..d7bf8999ef 100644
--- a/esphome/components/teleinfo/__init__.py
+++ b/esphome/components/teleinfo/__init__.py
@@ -1 +1,28 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import uart
+from esphome.const import CONF_ID
+
 CODEOWNERS = ["@0hax"]
+
+teleinfo_ns = cg.esphome_ns.namespace("teleinfo")
+TeleInfo = teleinfo_ns.class_("TeleInfo", cg.PollingComponent, uart.UARTDevice)
+CONF_TELEINFO_ID = "teleinfo_id"
+
+CONF_HISTORICAL_MODE = "historical_mode"
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(TeleInfo),
+            cv.Optional(CONF_HISTORICAL_MODE, default=False): cv.boolean,
+        }
+    )
+    .extend(cv.polling_component_schema("60s"))
+    .extend(uart.UART_DEVICE_SCHEMA)
+)
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID], config[CONF_HISTORICAL_MODE])
+    yield cg.register_component(var, config)
+    yield uart.register_uart_device(var, config)
diff --git a/esphome/components/teleinfo/sensor.py b/esphome/components/teleinfo/sensor.py
deleted file mode 100644
index 9f6c2c8e89..0000000000
--- a/esphome/components/teleinfo/sensor.py
+++ /dev/null
@@ -1,50 +0,0 @@
-import esphome.codegen as cg
-import esphome.config_validation as cv
-from esphome.components import sensor, uart
-from esphome.const import (
-    CONF_ID,
-    CONF_SENSOR,
-    DEVICE_CLASS_POWER,
-    ICON_EMPTY,
-    UNIT_WATT_HOURS,
-)
-
-DEPENDENCIES = ["uart"]
-
-teleinfo_ns = cg.esphome_ns.namespace("teleinfo")
-TeleInfo = teleinfo_ns.class_("TeleInfo", cg.PollingComponent, uart.UARTDevice)
-
-CONF_TAG_NAME = "tag_name"
-TELEINFO_TAG_SCHEMA = cv.Schema(
-    {
-        cv.Required(CONF_TAG_NAME): cv.string,
-        cv.Required(CONF_SENSOR): sensor.sensor_schema(
-            UNIT_WATT_HOURS, ICON_EMPTY, 0, DEVICE_CLASS_POWER
-        ),
-    }
-)
-
-CONF_TAGS = "tags"
-CONF_HISTORICAL_MODE = "historical_mode"
-CONFIG_SCHEMA = (
-    cv.Schema(
-        {
-            cv.GenerateID(): cv.declare_id(TeleInfo),
-            cv.Optional(CONF_HISTORICAL_MODE, default=False): cv.boolean,
-            cv.Optional(CONF_TAGS): cv.ensure_list(TELEINFO_TAG_SCHEMA),
-        }
-    )
-    .extend(cv.polling_component_schema("60s"))
-    .extend(uart.UART_DEVICE_SCHEMA)
-)
-
-
-def to_code(config):
-    var = cg.new_Pvariable(config[CONF_ID], config[CONF_HISTORICAL_MODE])
-    yield cg.register_component(var, config)
-    yield uart.register_uart_device(var, config)
-
-    if CONF_TAGS in config:
-        for tag in config[CONF_TAGS]:
-            sens = yield sensor.new_sensor(tag[CONF_SENSOR])
-            cg.add(var.register_teleinfo_sensor(tag[CONF_TAG_NAME], sens))
diff --git a/esphome/components/teleinfo/sensor/__init__.py b/esphome/components/teleinfo/sensor/__init__.py
new file mode 100644
index 0000000000..ffdb1509be
--- /dev/null
+++ b/esphome/components/teleinfo/sensor/__init__.py
@@ -0,0 +1,26 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor
+from esphome.const import CONF_ID, ICON_FLASH, UNIT_WATT_HOURS
+
+from .. import teleinfo_ns, TeleInfo, CONF_TELEINFO_ID
+
+CONF_TAG_NAME = "tag_name"
+
+TeleInfoSensor = teleinfo_ns.class_("TeleInfoSensor", sensor.Sensor, cg.Component)
+
+CONFIG_SCHEMA = sensor.sensor_schema(UNIT_WATT_HOURS, ICON_FLASH, 0).extend(
+    {
+        cv.GenerateID(): cv.declare_id(TeleInfoSensor),
+        cv.GenerateID(CONF_TELEINFO_ID): cv.use_id(TeleInfo),
+        cv.Required(CONF_TAG_NAME): cv.string,
+    }
+)
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG_NAME])
+    yield cg.register_component(var, config)
+    yield sensor.register_sensor(var, config)
+    teleinfo = yield cg.get_variable(config[CONF_TELEINFO_ID])
+    cg.add(teleinfo.register_teleinfo_listener(var))
diff --git a/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp b/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp
new file mode 100644
index 0000000000..c00bf8f441
--- /dev/null
+++ b/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp
@@ -0,0 +1,14 @@
+#include "esphome/core/log.h"
+#include "teleinfo_sensor.h"
+namespace esphome {
+namespace teleinfo {
+
+static const char *TAG = "teleinfo_sensor";
+TeleInfoSensor::TeleInfoSensor(const char *tag) { this->tag = std::string(tag); }
+void TeleInfoSensor::publish_val(std::string val) {
+  auto newval = parse_float(val);
+  publish_state(*newval);
+}
+void TeleInfoSensor::dump_config() { LOG_SENSOR("  ", tag.c_str(), this); }
+}  // namespace teleinfo
+}  // namespace esphome
diff --git a/esphome/components/teleinfo/sensor/teleinfo_sensor.h b/esphome/components/teleinfo/sensor/teleinfo_sensor.h
new file mode 100644
index 0000000000..de46cbfd1a
--- /dev/null
+++ b/esphome/components/teleinfo/sensor/teleinfo_sensor.h
@@ -0,0 +1,16 @@
+#pragma once
+#include "esphome/components/teleinfo/teleinfo.h"
+#include "esphome/components/sensor/sensor.h"
+
+namespace esphome {
+namespace teleinfo {
+
+class TeleInfoSensor : public TeleInfoListener, public sensor::Sensor, public Component {
+ public:
+  TeleInfoSensor(const char *tag);
+  void publish_val(std::string val) override;
+  void dump_config() override;
+};
+
+}  // namespace teleinfo
+}  // namespace esphome
diff --git a/esphome/components/teleinfo/teleinfo.cpp b/esphome/components/teleinfo/teleinfo.cpp
index 7c0a83d103..bd233c9595 100644
--- a/esphome/components/teleinfo/teleinfo.cpp
+++ b/esphome/components/teleinfo/teleinfo.cpp
@@ -149,16 +149,14 @@ void TeleInfo::loop() {
   }
 }
 void TeleInfo::publish_value_(std::string tag, std::string val) {
-  /* It will return 0 if tag is not a float. */
-  auto newval = parse_float(val);
-  for (auto element : teleinfo_sensors_)
-    if (tag == element->tag)
-      element->sensor->publish_state(*newval);
+  for (auto element : teleinfo_listeners_) {
+    if (tag != element->tag)
+      continue;
+    element->publish_val(val);
+  }
 }
 void TeleInfo::dump_config() {
   ESP_LOGCONFIG(TAG, "TeleInfo:");
-  for (auto element : teleinfo_sensors_)
-    LOG_SENSOR("  ", element->tag, element->sensor);
   this->check_uart_settings(baud_rate_, 1, uart::UART_CONFIG_PARITY_EVEN, 7);
 }
 TeleInfo::TeleInfo(bool historical_mode) {
@@ -175,10 +173,7 @@ TeleInfo::TeleInfo(bool historical_mode) {
     baud_rate_ = 9600;
   }
 }
-void TeleInfo::register_teleinfo_sensor(const char *tag, sensor::Sensor *sensor) {
-  const TeleinfoSensorElement *teleinfo_sensor = new TeleinfoSensorElement{tag, sensor};
-  teleinfo_sensors_.push_back(teleinfo_sensor);
-}
+void TeleInfo::register_teleinfo_listener(TeleInfoListener *listener) { teleinfo_listeners_.push_back(listener); }
 
 }  // namespace teleinfo
 }  // namespace esphome
diff --git a/esphome/components/teleinfo/teleinfo.h b/esphome/components/teleinfo/teleinfo.h
index de9cf646c4..3932c4758e 100644
--- a/esphome/components/teleinfo/teleinfo.h
+++ b/esphome/components/teleinfo/teleinfo.h
@@ -1,7 +1,6 @@
 #pragma once
 
 #include "esphome/core/component.h"
-#include "esphome/components/sensor/sensor.h"
 #include "esphome/components/uart/uart.h"
 
 namespace esphome {
@@ -14,20 +13,20 @@ static const uint8_t MAX_TAG_SIZE = 64;
 static const uint16_t MAX_VAL_SIZE = 256;
 static const uint16_t MAX_BUF_SIZE = 1024;
 
-struct TeleinfoSensorElement {
-  const char *tag;
-  sensor::Sensor *sensor;
+class TeleInfoListener {
+ public:
+  std::string tag;
+  virtual void publish_val(std::string val){};
 };
-
 class TeleInfo : public PollingComponent, public uart::UARTDevice {
  public:
   TeleInfo(bool historical_mode);
-  void register_teleinfo_sensor(const char *tag, sensor::Sensor *sensors);
+  void register_teleinfo_listener(TeleInfoListener *listener);
   void loop() override;
   void setup() override;
   void update() override;
   void dump_config() override;
-  std::vector<const TeleinfoSensorElement *> teleinfo_sensors_{};
+  std::vector<TeleInfoListener *> teleinfo_listeners_{};
 
  protected:
   uint32_t baud_rate_;
diff --git a/esphome/components/teleinfo/text_sensor/__init__.py b/esphome/components/teleinfo/text_sensor/__init__.py
new file mode 100644
index 0000000000..b1ade4df41
--- /dev/null
+++ b/esphome/components/teleinfo/text_sensor/__init__.py
@@ -0,0 +1,28 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import text_sensor
+from esphome.const import CONF_ID
+
+from .. import teleinfo_ns, TeleInfo, CONF_TELEINFO_ID
+
+CONF_TAG_NAME = "tag_name"
+
+TeleInfoTextSensor = teleinfo_ns.class_(
+    "TeleInfoTextSensor", text_sensor.TextSensor, cg.Component
+)
+
+CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend(
+    {
+        cv.GenerateID(): cv.declare_id(TeleInfoTextSensor),
+        cv.GenerateID(CONF_TELEINFO_ID): cv.use_id(TeleInfo),
+        cv.Required(CONF_TAG_NAME): cv.string,
+    }
+)
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG_NAME])
+    yield cg.register_component(var, config)
+    yield text_sensor.register_text_sensor(var, config)
+    teleinfo = yield cg.get_variable(config[CONF_TELEINFO_ID])
+    cg.add(teleinfo.register_teleinfo_listener(var))
diff --git a/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.cpp b/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.cpp
new file mode 100644
index 0000000000..ac48aeabbd
--- /dev/null
+++ b/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.cpp
@@ -0,0 +1,11 @@
+#include "esphome/core/log.h"
+#include "teleinfo_text_sensor.h"
+namespace esphome {
+namespace teleinfo {
+
+static const char *TAG = "teleinfo_text_sensor";
+TeleInfoTextSensor::TeleInfoTextSensor(const char *tag) { this->tag = std::string(tag); }
+void TeleInfoTextSensor::publish_val(std::string val) { publish_state(val); }
+void TeleInfoTextSensor::dump_config() { LOG_TEXT_SENSOR("  ", tag.c_str(), this); }
+}  // namespace teleinfo
+}  // namespace esphome
diff --git a/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.h b/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.h
new file mode 100644
index 0000000000..be355151ce
--- /dev/null
+++ b/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.h
@@ -0,0 +1,13 @@
+#pragma once
+#include "esphome/components/teleinfo/teleinfo.h"
+#include "esphome/components/text_sensor/text_sensor.h"
+namespace esphome {
+namespace teleinfo {
+class TeleInfoTextSensor : public TeleInfoListener, public text_sensor::TextSensor, public Component {
+ public:
+  TeleInfoTextSensor(const char *tag);
+  void publish_val(std::string val) override;
+  void dump_config() override;
+};
+}  // namespace teleinfo
+}  // namespace esphome
diff --git a/esphome/components/template/binary_sensor/__init__.py b/esphome/components/template/binary_sensor/__init__.py
index d23e6423a1..8f551e3215 100644
--- a/esphome/components/template/binary_sensor/__init__.py
+++ b/esphome/components/template/binary_sensor/__init__.py
@@ -17,13 +17,13 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield binary_sensor.register_binary_sensor(var, config)
+    await cg.register_component(var, config)
+    await binary_sensor.register_binary_sensor(var, config)
 
     if CONF_LAMBDA in config:
-        template_ = yield cg.process_lambda(
+        template_ = await cg.process_lambda(
             config[CONF_LAMBDA], [], return_type=cg.optional.template(bool)
         )
         cg.add(var.set_template(template_))
@@ -39,9 +39,9 @@ def to_code(config):
         }
     ),
 )
-def binary_sensor_template_publish_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def binary_sensor_template_publish_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    template_ = yield cg.templatable(config[CONF_STATE], args, bool)
+    template_ = await cg.templatable(config[CONF_STATE], args, bool)
     cg.add(var.set_state(template_))
-    yield var
+    return var
diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py
index 14050cb7c6..a628da70d2 100644
--- a/esphome/components/template/cover/__init__.py
+++ b/esphome/components/template/cover/__init__.py
@@ -52,39 +52,39 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield cover.register_cover(var, config)
+    await cg.register_component(var, config)
+    await cover.register_cover(var, config)
     if CONF_LAMBDA in config:
-        template_ = yield cg.process_lambda(
+        template_ = await cg.process_lambda(
             config[CONF_LAMBDA], [], return_type=cg.optional.template(float)
         )
         cg.add(var.set_state_lambda(template_))
     if CONF_OPEN_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_open_trigger(), [], config[CONF_OPEN_ACTION]
         )
     if CONF_CLOSE_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]
         )
     if CONF_STOP_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_stop_trigger(), [], config[CONF_STOP_ACTION]
         )
     if CONF_TILT_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_tilt_trigger(), [(float, "tilt")], config[CONF_TILT_ACTION]
         )
         cg.add(var.set_has_tilt(True))
     if CONF_TILT_LAMBDA in config:
-        tilt_template_ = yield cg.process_lambda(
+        tilt_template_ = await cg.process_lambda(
             config[CONF_TILT_LAMBDA], [], return_type=cg.optional.template(float)
         )
         cg.add(var.set_tilt_lambda(tilt_template_))
     if CONF_POSITION_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_position_trigger(), [(float, "pos")], config[CONF_POSITION_ACTION]
         )
         cg.add(var.set_has_position(True))
@@ -111,21 +111,21 @@ def to_code(config):
         }
     ),
 )
-def cover_template_publish_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def cover_template_publish_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
     if CONF_STATE in config:
-        template_ = yield cg.templatable(config[CONF_STATE], args, float)
+        template_ = await cg.templatable(config[CONF_STATE], args, float)
         cg.add(var.set_position(template_))
     if CONF_POSITION in config:
-        template_ = yield cg.templatable(config[CONF_POSITION], args, float)
+        template_ = await cg.templatable(config[CONF_POSITION], args, float)
         cg.add(var.set_position(template_))
     if CONF_TILT in config:
-        template_ = yield cg.templatable(config[CONF_TILT], args, float)
+        template_ = await cg.templatable(config[CONF_TILT], args, float)
         cg.add(var.set_tilt(template_))
     if CONF_CURRENT_OPERATION in config:
-        template_ = yield cg.templatable(
+        template_ = await cg.templatable(
             config[CONF_CURRENT_OPERATION], args, cover.CoverOperation
         )
         cg.add(var.set_current_operation(template_))
-    yield var
+    return var
diff --git a/esphome/components/template/output/__init__.py b/esphome/components/template/output/__init__.py
index 61286772d2..b42a4be166 100644
--- a/esphome/components/template/output/__init__.py
+++ b/esphome/components/template/output/__init__.py
@@ -34,14 +34,14 @@ CONFIG_SCHEMA = cv.typed_schema(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     if config[CONF_TYPE] == CONF_BINARY:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_trigger(), [(bool, "state")], config[CONF_WRITE_ACTION]
         )
     else:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_trigger(), [(float, "state")], config[CONF_WRITE_ACTION]
         )
-    yield output.register_output(var, config)
+    await output.register_output(var, config)
diff --git a/esphome/components/template/sensor/__init__.py b/esphome/components/template/sensor/__init__.py
index 21ef5db32d..47027583bf 100644
--- a/esphome/components/template/sensor/__init__.py
+++ b/esphome/components/template/sensor/__init__.py
@@ -7,8 +7,9 @@ from esphome.const import (
     CONF_LAMBDA,
     CONF_STATE,
     DEVICE_CLASS_EMPTY,
-    UNIT_EMPTY,
     ICON_EMPTY,
+    STATE_CLASS_NONE,
+    UNIT_EMPTY,
 )
 from .. import template_ns
 
@@ -17,7 +18,13 @@ TemplateSensor = template_ns.class_(
 )
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY)
+    sensor.sensor_schema(
+        UNIT_EMPTY,
+        ICON_EMPTY,
+        1,
+        DEVICE_CLASS_EMPTY,
+        STATE_CLASS_NONE,
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(TemplateSensor),
@@ -28,13 +35,13 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
 
     if CONF_LAMBDA in config:
-        template_ = yield cg.process_lambda(
+        template_ = await cg.process_lambda(
             config[CONF_LAMBDA], [], return_type=cg.optional.template(float)
         )
         cg.add(var.set_template(template_))
@@ -50,9 +57,9 @@ def to_code(config):
         }
     ),
 )
-def sensor_template_publish_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def sensor_template_publish_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    template_ = yield cg.templatable(config[CONF_STATE], args, float)
+    template_ = await cg.templatable(config[CONF_STATE], args, float)
     cg.add(var.set_state(template_))
-    yield var
+    return var
diff --git a/esphome/components/template/switch/__init__.py b/esphome/components/template/switch/__init__.py
index 7698e4f2a9..b00710dfb7 100644
--- a/esphome/components/template/switch/__init__.py
+++ b/esphome/components/template/switch/__init__.py
@@ -29,22 +29,22 @@ CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield switch.register_switch(var, config)
+    await cg.register_component(var, config)
+    await switch.register_switch(var, config)
 
     if CONF_LAMBDA in config:
-        template_ = yield cg.process_lambda(
+        template_ = await cg.process_lambda(
             config[CONF_LAMBDA], [], return_type=cg.optional.template(bool)
         )
         cg.add(var.set_state_lambda(template_))
     if CONF_TURN_OFF_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_turn_off_trigger(), [], config[CONF_TURN_OFF_ACTION]
         )
     if CONF_TURN_ON_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_turn_on_trigger(), [], config[CONF_TURN_ON_ACTION]
         )
     cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
@@ -62,9 +62,9 @@ def to_code(config):
         }
     ),
 )
-def switch_template_publish_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def switch_template_publish_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    template_ = yield cg.templatable(config[CONF_STATE], args, bool)
+    template_ = await cg.templatable(config[CONF_STATE], args, bool)
     cg.add(var.set_state(template_))
-    yield var
+    return var
diff --git a/esphome/components/template/text_sensor/__init__.py b/esphome/components/template/text_sensor/__init__.py
index cae00e3932..2e098a77c2 100644
--- a/esphome/components/template/text_sensor/__init__.py
+++ b/esphome/components/template/text_sensor/__init__.py
@@ -18,13 +18,13 @@ CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend(
 ).extend(cv.polling_component_schema("60s"))
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield text_sensor.register_text_sensor(var, config)
+    await cg.register_component(var, config)
+    await text_sensor.register_text_sensor(var, config)
 
     if CONF_LAMBDA in config:
-        template_ = yield cg.process_lambda(
+        template_ = await cg.process_lambda(
             config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.std_string)
         )
         cg.add(var.set_template(template_))
@@ -40,9 +40,9 @@ def to_code(config):
         }
     ),
 )
-def text_sensor_template_publish_to_code(config, action_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def text_sensor_template_publish_to_code(config, action_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(action_id, template_arg, paren)
-    template_ = yield cg.templatable(config[CONF_STATE], args, cg.std_string)
+    template_ = await cg.templatable(config[CONF_STATE], args, cg.std_string)
     cg.add(var.set_state(template_))
-    yield var
+    return var
diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py
index ff73889d61..84fedc8d94 100644
--- a/esphome/components/text_sensor/__init__.py
+++ b/esphome/components/text_sensor/__init__.py
@@ -12,7 +12,7 @@ from esphome.const import (
     CONF_NAME,
     CONF_STATE,
 )
-from esphome.core import CORE, coroutine, coroutine_with_priority
+from esphome.core import CORE, coroutine_with_priority
 
 IS_PLATFORM_COMPONENT = True
 
@@ -46,8 +46,7 @@ TEXT_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend(
 )
 
 
-@coroutine
-def setup_text_sensor_core_(var, config):
+async def setup_text_sensor_core_(var, config):
     cg.add(var.set_name(config[CONF_NAME]))
     if CONF_INTERNAL in config:
         cg.add(var.set_internal(config[CONF_INTERNAL]))
@@ -56,23 +55,22 @@ def setup_text_sensor_core_(var, config):
 
     for conf in config.get(CONF_ON_VALUE, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
-        yield automation.build_automation(trigger, [(cg.std_string, "x")], conf)
+        await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
 
     if CONF_MQTT_ID in config:
         mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
-        yield mqtt.register_mqtt_component(mqtt_, config)
+        await mqtt.register_mqtt_component(mqtt_, config)
 
 
-@coroutine
-def register_text_sensor(var, config):
+async def register_text_sensor(var, config):
     if not CORE.has_id(config[CONF_ID]):
         var = cg.Pvariable(config[CONF_ID], var)
     cg.add(cg.App.register_text_sensor(var))
-    yield setup_text_sensor_core_(var, config)
+    await setup_text_sensor_core_(var, config)
 
 
 @coroutine_with_priority(100.0)
-def to_code(config):
+async def to_code(config):
     cg.add_define("USE_TEXT_SENSOR")
     cg.add_global(text_sensor_ns.using)
 
@@ -87,9 +85,9 @@ def to_code(config):
         }
     ),
 )
-def text_sensor_state_to_code(config, condition_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
+async def text_sensor_state_to_code(config, condition_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
     var = cg.new_Pvariable(condition_id, template_arg, paren)
-    templ = yield cg.templatable(config[CONF_STATE], args, cg.std_string)
+    templ = await cg.templatable(config[CONF_STATE], args, cg.std_string)
     cg.add(var.set_state(templ))
-    yield var
+    return var
diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py
index 04584583af..4a371ec165 100644
--- a/esphome/components/thermostat/climate.py
+++ b/esphome/components/thermostat/climate.py
@@ -222,17 +222,17 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield climate.register_climate(var, config)
+    await cg.register_component(var, config)
+    await climate.register_climate(var, config)
 
     auto_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config
     two_points_available = CONF_HEAT_ACTION in config and (
         CONF_COOL_ACTION in config or CONF_FAN_ONLY_ACTION in config
     )
 
-    sens = yield cg.get_variable(config[CONF_SENSOR])
+    sens = await cg.get_variable(config[CONF_SENSOR])
     cg.add(var.set_sensor(sens))
     cg.add(var.set_hysteresis(config[CONF_HYSTERESIS]))
 
@@ -254,7 +254,7 @@ def to_code(config):
         )
     cg.add(var.set_normal_config(normal_config))
 
-    yield automation.build_automation(
+    await automation.build_automation(
         var.get_idle_action_trigger(), [], config[CONF_IDLE_ACTION]
     )
 
@@ -264,117 +264,117 @@ def to_code(config):
         cg.add(var.set_supports_auto(False))
 
     if CONF_COOL_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_cool_action_trigger(), [], config[CONF_COOL_ACTION]
         )
         cg.add(var.set_supports_cool(True))
     if CONF_DRY_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_dry_action_trigger(), [], config[CONF_DRY_ACTION]
         )
         cg.add(var.set_supports_dry(True))
     if CONF_FAN_ONLY_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_fan_only_action_trigger(), [], config[CONF_FAN_ONLY_ACTION]
         )
         cg.add(var.set_supports_fan_only(True))
     if CONF_HEAT_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_heat_action_trigger(), [], config[CONF_HEAT_ACTION]
         )
         cg.add(var.set_supports_heat(True))
     if CONF_AUTO_MODE in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_auto_mode_trigger(), [], config[CONF_AUTO_MODE]
         )
     if CONF_COOL_MODE in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_cool_mode_trigger(), [], config[CONF_COOL_MODE]
         )
         cg.add(var.set_supports_cool(True))
     if CONF_DRY_MODE in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_dry_mode_trigger(), [], config[CONF_DRY_MODE]
         )
         cg.add(var.set_supports_dry(True))
     if CONF_FAN_ONLY_MODE in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_fan_only_mode_trigger(), [], config[CONF_FAN_ONLY_MODE]
         )
         cg.add(var.set_supports_fan_only(True))
     if CONF_HEAT_MODE in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_heat_mode_trigger(), [], config[CONF_HEAT_MODE]
         )
         cg.add(var.set_supports_heat(True))
     if CONF_OFF_MODE in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_off_mode_trigger(), [], config[CONF_OFF_MODE]
         )
     if CONF_FAN_MODE_ON_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_fan_mode_on_trigger(), [], config[CONF_FAN_MODE_ON_ACTION]
         )
         cg.add(var.set_supports_fan_mode_on(True))
     if CONF_FAN_MODE_OFF_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_fan_mode_off_trigger(), [], config[CONF_FAN_MODE_OFF_ACTION]
         )
         cg.add(var.set_supports_fan_mode_off(True))
     if CONF_FAN_MODE_AUTO_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_fan_mode_auto_trigger(), [], config[CONF_FAN_MODE_AUTO_ACTION]
         )
         cg.add(var.set_supports_fan_mode_auto(True))
     if CONF_FAN_MODE_LOW_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_fan_mode_low_trigger(), [], config[CONF_FAN_MODE_LOW_ACTION]
         )
         cg.add(var.set_supports_fan_mode_low(True))
     if CONF_FAN_MODE_MEDIUM_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_fan_mode_medium_trigger(), [], config[CONF_FAN_MODE_MEDIUM_ACTION]
         )
         cg.add(var.set_supports_fan_mode_medium(True))
     if CONF_FAN_MODE_HIGH_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_fan_mode_high_trigger(), [], config[CONF_FAN_MODE_HIGH_ACTION]
         )
         cg.add(var.set_supports_fan_mode_high(True))
     if CONF_FAN_MODE_MIDDLE_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_fan_mode_middle_trigger(), [], config[CONF_FAN_MODE_MIDDLE_ACTION]
         )
         cg.add(var.set_supports_fan_mode_middle(True))
     if CONF_FAN_MODE_FOCUS_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_fan_mode_focus_trigger(), [], config[CONF_FAN_MODE_FOCUS_ACTION]
         )
         cg.add(var.set_supports_fan_mode_focus(True))
     if CONF_FAN_MODE_DIFFUSE_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_fan_mode_diffuse_trigger(), [], config[CONF_FAN_MODE_DIFFUSE_ACTION]
         )
         cg.add(var.set_supports_fan_mode_diffuse(True))
     if CONF_SWING_BOTH_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_swing_mode_both_trigger(), [], config[CONF_SWING_BOTH_ACTION]
         )
         cg.add(var.set_supports_swing_mode_both(True))
     if CONF_SWING_HORIZONTAL_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_swing_mode_horizontal_trigger(),
             [],
             config[CONF_SWING_HORIZONTAL_ACTION],
         )
         cg.add(var.set_supports_swing_mode_horizontal(True))
     if CONF_SWING_OFF_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_swing_mode_off_trigger(), [], config[CONF_SWING_OFF_ACTION]
         )
         cg.add(var.set_supports_swing_mode_off(True))
     if CONF_SWING_VERTICAL_ACTION in config:
-        yield automation.build_automation(
+        await automation.build_automation(
             var.get_swing_mode_vertical_trigger(),
             [],
             config[CONF_SWING_VERTICAL_ACTION],
diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp
index 64a7c1b05d..3bab0e85fd 100644
--- a/esphome/components/thermostat/thermostat_climate.cpp
+++ b/esphome/components/thermostat/thermostat_climate.cpp
@@ -33,7 +33,7 @@ float ThermostatClimate::hysteresis() { return this->hysteresis_; }
 void ThermostatClimate::refresh() {
   this->switch_to_mode_(this->mode);
   this->switch_to_action_(compute_action_());
-  this->switch_to_fan_mode_(this->fan_mode);
+  this->switch_to_fan_mode_(this->fan_mode.value());
   this->switch_to_swing_mode_(this->swing_mode);
   this->publish_state();
 }
diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py
index 8fbc2dcaf6..4872c89f88 100644
--- a/esphome/components/time/__init__.py
+++ b/esphome/components/time/__init__.py
@@ -28,7 +28,7 @@ from esphome.const import (
     CONF_HOUR,
     CONF_MINUTE,
 )
-from esphome.core import coroutine, coroutine_with_priority
+from esphome.core import coroutine_with_priority
 from esphome.automation import Condition
 
 _LOGGER = logging.getLogger(__name__)
@@ -380,8 +380,7 @@ TIME_SCHEMA = cv.Schema(
 ).extend(cv.polling_component_schema("15min"))
 
 
-@coroutine
-def setup_time_core_(time_var, config):
+async def setup_time_core_(time_var, config):
     cg.add(time_var.set_timezone(config[CONF_TIMEZONE]))
 
     for conf in config.get(CONF_ON_TIME, []):
@@ -400,23 +399,22 @@ def setup_time_core_(time_var, config):
         days_of_week = conf.get(CONF_DAYS_OF_WEEK, list(range(1, 8)))
         cg.add(trigger.add_days_of_week(days_of_week))
 
-        yield cg.register_component(trigger, conf)
-        yield automation.build_automation(trigger, [], conf)
+        await cg.register_component(trigger, conf)
+        await automation.build_automation(trigger, [], conf)
 
     for conf in config.get(CONF_ON_TIME_SYNC, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var)
 
-        yield cg.register_component(trigger, conf)
-        yield automation.build_automation(trigger, [], conf)
+        await cg.register_component(trigger, conf)
+        await automation.build_automation(trigger, [], conf)
 
 
-@coroutine
-def register_time(time_var, config):
-    yield setup_time_core_(time_var, config)
+async def register_time(time_var, config):
+    await setup_time_core_(time_var, config)
 
 
 @coroutine_with_priority(100.0)
-def to_code(config):
+async def to_code(config):
     cg.add_define("USE_TIME")
     cg.add_global(time_ns.using)
 
@@ -430,6 +428,6 @@ def to_code(config):
         }
     ),
 )
-def time_has_time_to_code(config, condition_id, template_arg, args):
-    paren = yield cg.get_variable(config[CONF_ID])
-    yield cg.new_Pvariable(condition_id, template_arg, paren)
+async def time_has_time_to_code(config, condition_id, template_arg, args):
+    paren = await cg.get_variable(config[CONF_ID])
+    return cg.new_Pvariable(condition_id, template_arg, paren)
diff --git a/esphome/components/time_based/cover.py b/esphome/components/time_based/cover.py
index 9246f78884..9625781c96 100644
--- a/esphome/components/time_based/cover.py
+++ b/esphome/components/time_based/cover.py
@@ -31,22 +31,22 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield cover.register_cover(var, config)
+    await cg.register_component(var, config)
+    await cover.register_cover(var, config)
 
-    yield automation.build_automation(
+    await automation.build_automation(
         var.get_stop_trigger(), [], config[CONF_STOP_ACTION]
     )
 
     cg.add(var.set_open_duration(config[CONF_OPEN_DURATION]))
-    yield automation.build_automation(
+    await automation.build_automation(
         var.get_open_trigger(), [], config[CONF_OPEN_ACTION]
     )
 
     cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION]))
-    yield automation.build_automation(
+    await automation.build_automation(
         var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]
     )
 
diff --git a/esphome/components/tlc59208f/__init__.py b/esphome/components/tlc59208f/__init__.py
index ff5b75954b..419fa397b7 100644
--- a/esphome/components/tlc59208f/__init__.py
+++ b/esphome/components/tlc59208f/__init__.py
@@ -20,7 +20,7 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/tlc59208f/output.py b/esphome/components/tlc59208f/output.py
index 94cf529a75..f7cc89b252 100644
--- a/esphome/components/tlc59208f/output.py
+++ b/esphome/components/tlc59208f/output.py
@@ -18,8 +18,8 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
 )
 
 
-def to_code(config):
-    paren = yield cg.get_variable(config[CONF_TLC59208F_ID])
+async def to_code(config):
+    paren = await cg.get_variable(config[CONF_TLC59208F_ID])
     rhs = paren.create_channel(config[CONF_CHANNEL])
     var = cg.Pvariable(config[CONF_ID], rhs)
-    yield output.register_output(var, config)
+    await output.register_output(var, config)
diff --git a/esphome/components/tm1637/display.py b/esphome/components/tm1637/display.py
index 06a9716e59..7999029f5a 100644
--- a/esphome/components/tm1637/display.py
+++ b/esphome/components/tm1637/display.py
@@ -28,20 +28,20 @@ CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend(
 ).extend(cv.polling_component_schema("1s"))
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield display.register_display(var, config)
+    await cg.register_component(var, config)
+    await display.register_display(var, config)
 
-    clk = yield cg.gpio_pin_expression(config[CONF_CLK_PIN])
+    clk = await cg.gpio_pin_expression(config[CONF_CLK_PIN])
     cg.add(var.set_clk_pin(clk))
-    dio = yield cg.gpio_pin_expression(config[CONF_DIO_PIN])
+    dio = await cg.gpio_pin_expression(config[CONF_DIO_PIN])
     cg.add(var.set_dio_pin(dio))
 
     cg.add(var.set_intensity(config[CONF_INTENSITY]))
 
     if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
+        lambda_ = await cg.process_lambda(
             config[CONF_LAMBDA], [(TM1637DisplayRef, "it")], return_type=cg.void
         )
         cg.add(var.set_writer(lambda_))
diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp
index 833e3caecd..df904ad9ab 100644
--- a/esphome/components/tm1637/tm1637.cpp
+++ b/esphome/components/tm1637/tm1637.cpp
@@ -46,7 +46,7 @@ const uint8_t TM1637_ASCII_TO_RAW[] PROGMEM = {
     0b01011111,           // '6', ord 0x36
     0b01110000,           // '7', ord 0x37
     0b01111111,           // '8', ord 0x38
-    0b01110011,           // '9', ord 0x39
+    0b01111011,           // '9', ord 0x39
     0b01001000,           // ':', ord 0x3A
     0b01011000,           // ';', ord 0x3B
     TM1637_UNKNOWN_CHAR,  // '<', ord 0x3C
@@ -253,7 +253,7 @@ uint8_t TM1637Display::print(uint8_t start_pos, const char* str) {
         pos--;
       this->buffer_[pos] |= 0b10000000;
     } else {
-      if (pos >= 4) {
+      if (pos >= 6) {
         ESP_LOGE(TAG, "String is too long for the display!");
         break;
       }
diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h
index 91e8ba66c0..003344eae9 100644
--- a/esphome/components/tm1637/tm1637.h
+++ b/esphome/components/tm1637/tm1637.h
@@ -63,7 +63,7 @@ class TM1637Display : public PollingComponent {
   GPIOPin *clk_pin_;
   uint8_t intensity_;
   optional<tm1637_writer_t> writer_{};
-  uint8_t buffer_[4] = {0};
+  uint8_t buffer_[6] = {0};
 };
 
 }  // namespace tm1637
diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py
index 97c1e472e9..f67a9f4512 100644
--- a/esphome/components/tm1651/__init__.py
+++ b/esphome/components/tm1651/__init__.py
@@ -40,13 +40,13 @@ validate_level = cv.All(cv.int_range(min=0, max=7))
 validate_brightness = cv.enum(TM1651_BRIGHTNESS_OPTIONS, int=True)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
-    clk_pin = yield cg.gpio_pin_expression(config[CONF_CLK_PIN])
+    clk_pin = await cg.gpio_pin_expression(config[CONF_CLK_PIN])
     cg.add(var.set_clk_pin(clk_pin))
-    dio_pin = yield cg.gpio_pin_expression(config[CONF_DIO_PIN])
+    dio_pin = await cg.gpio_pin_expression(config[CONF_DIO_PIN])
     cg.add(var.set_dio_pin(dio_pin))
 
     # https://platformio.org/lib/show/6865/TM1651
@@ -61,19 +61,19 @@ BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id(
 
 
 @automation.register_action("tm1651.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA)
-def output_turn_on_to_code(config, action_id, template_arg, args):
+async def output_turn_on_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    yield var
+    await cg.register_parented(var, config[CONF_ID])
+    return var
 
 
 @automation.register_action(
     "tm1651.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA
 )
-def output_turn_off_to_code(config, action_id, template_arg, args):
+async def output_turn_off_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    yield var
+    await cg.register_parented(var, config[CONF_ID])
+    return var
 
 
 @automation.register_action(
@@ -87,12 +87,12 @@ def output_turn_off_to_code(config, action_id, template_arg, args):
         key=CONF_LEVEL_PERCENT,
     ),
 )
-def tm1651_set_level_percent_to_code(config, action_id, template_arg, args):
+async def tm1651_set_level_percent_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    template_ = yield cg.templatable(config[CONF_LEVEL_PERCENT], args, cg.uint8)
+    await cg.register_parented(var, config[CONF_ID])
+    template_ = await cg.templatable(config[CONF_LEVEL_PERCENT], args, cg.uint8)
     cg.add(var.set_level_percent(template_))
-    yield var
+    return var
 
 
 @automation.register_action(
@@ -106,12 +106,12 @@ def tm1651_set_level_percent_to_code(config, action_id, template_arg, args):
         key=CONF_LEVEL,
     ),
 )
-def tm1651_set_level_to_code(config, action_id, template_arg, args):
+async def tm1651_set_level_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    template_ = yield cg.templatable(config[CONF_LEVEL], args, cg.uint8)
+    await cg.register_parented(var, config[CONF_ID])
+    template_ = await cg.templatable(config[CONF_LEVEL], args, cg.uint8)
     cg.add(var.set_level(template_))
-    yield var
+    return var
 
 
 @automation.register_action(
@@ -125,9 +125,9 @@ def tm1651_set_level_to_code(config, action_id, template_arg, args):
         key=CONF_BRIGHTNESS,
     ),
 )
-def tm1651_set_brightness_to_code(config, action_id, template_arg, args):
+async def tm1651_set_brightness_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
-    template_ = yield cg.templatable(config[CONF_BRIGHTNESS], args, cg.uint8)
+    await cg.register_parented(var, config[CONF_ID])
+    template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, cg.uint8)
     cg.add(var.set_brightness(template_))
-    yield var
+    return var
diff --git a/esphome/components/tmp102/sensor.py b/esphome/components/tmp102/sensor.py
index 4c04271d96..b54d5646ba 100644
--- a/esphome/components/tmp102/sensor.py
+++ b/esphome/components/tmp102/sensor.py
@@ -10,7 +10,13 @@ https://www.sparkfun.com/datasheets/Sensors/Temperature/tmp102.pdf
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import i2c, sensor
-from esphome.const import CONF_ID, DEVICE_CLASS_TEMPERATURE, ICON_EMPTY, UNIT_CELSIUS
+from esphome.const import (
+    CONF_ID,
+    DEVICE_CLASS_TEMPERATURE,
+    ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+)
 
 CODEOWNERS = ["@timsavage"]
 DEPENDENCIES = ["i2c"]
@@ -21,7 +27,9 @@ TMP102Component = tmp102_ns.class_(
 )
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE)
+    sensor.sensor_schema(
+        UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(TMP102Component),
@@ -32,8 +40,8 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
+    await sensor.register_sensor(var, config)
diff --git a/esphome/components/tmp117/sensor.py b/esphome/components/tmp117/sensor.py
index 33c13e3f3b..a5fc027b20 100644
--- a/esphome/components/tmp117/sensor.py
+++ b/esphome/components/tmp117/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_UPDATE_INTERVAL,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
 )
 
@@ -17,7 +18,9 @@ TMP117Component = tmp117_ns.class_(
 )
 
 CONFIG_SCHEMA = cv.All(
-    sensor.sensor_schema(UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE)
+    sensor.sensor_schema(
+        UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(TMP117Component),
@@ -70,11 +73,11 @@ def determine_config_register(polling_period):
     return 0x0000
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
+    await sensor.register_sensor(var, config)
 
     update_period = config[CONF_UPDATE_INTERVAL].total_seconds
     cg.add(var.set_config(determine_config_register(update_period)))
diff --git a/esphome/components/tof10120/sensor.py b/esphome/components/tof10120/sensor.py
index 91a15960b4..2110cbfcf8 100644
--- a/esphome/components/tof10120/sensor.py
+++ b/esphome/components/tof10120/sensor.py
@@ -1,7 +1,13 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import i2c, sensor
-from esphome.const import CONF_ID, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL
+from esphome.const import (
+    CONF_ID,
+    DEVICE_CLASS_EMPTY,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_METER,
+    ICON_ARROW_EXPAND_VERTICAL,
+)
 
 CODEOWNERS = ["@wstrzalka"]
 DEPENDENCIES = ["i2c"]
@@ -12,15 +18,21 @@ TOF10120Sensor = tof10120_ns.class_(
 )
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, 3)
+    sensor.sensor_schema(
+        UNIT_METER,
+        ICON_ARROW_EXPAND_VERTICAL,
+        3,
+        DEVICE_CLASS_EMPTY,
+        STATE_CLASS_MEASUREMENT,
+    )
     .extend({cv.GenerateID(): cv.declare_id(TOF10120Sensor)})
     .extend(cv.polling_component_schema("60s"))
     .extend(i2c.i2c_device_schema(0x52))
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
+    await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/toshiba/climate.py b/esphome/components/toshiba/climate.py
index 3021697f21..95c9f1f127 100644
--- a/esphome/components/toshiba/climate.py
+++ b/esphome/components/toshiba/climate.py
@@ -15,6 +15,6 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield climate_ir.register_climate_ir(var, config)
+    await climate_ir.register_climate_ir(var, config)
diff --git a/esphome/components/total_daily_energy/sensor.py b/esphome/components/total_daily_energy/sensor.py
index 150cab77b4..b70a0cece9 100644
--- a/esphome/components/total_daily_energy/sensor.py
+++ b/esphome/components/total_daily_energy/sensor.py
@@ -20,13 +20,13 @@ CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
 
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
 
-    sens = yield cg.get_variable(config[CONF_POWER_ID])
+    sens = await cg.get_variable(config[CONF_POWER_ID])
     cg.add(var.set_parent(sens))
-    time_ = yield cg.get_variable(config[CONF_TIME_ID])
+    time_ = await cg.get_variable(config[CONF_TIME_ID])
     cg.add(var.set_time(time_))
diff --git a/esphome/components/tsl2561/sensor.py b/esphome/components/tsl2561/sensor.py
index d0219fc078..c05079f668 100644
--- a/esphome/components/tsl2561/sensor.py
+++ b/esphome/components/tsl2561/sensor.py
@@ -7,6 +7,7 @@ from esphome.const import (
     CONF_INTEGRATION_TIME,
     DEVICE_CLASS_ILLUMINANCE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_LUX,
 )
 
@@ -39,7 +40,9 @@ TSL2561Sensor = tsl2561_ns.class_(
 )
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_LUX, ICON_EMPTY, 1, DEVICE_CLASS_ILLUMINANCE)
+    sensor.sensor_schema(
+        UNIT_LUX, ICON_EMPTY, 1, DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(TSL2561Sensor),
@@ -55,11 +58,11 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
+    await sensor.register_sensor(var, config)
 
     cg.add(var.set_integration_time(config[CONF_INTEGRATION_TIME]))
     cg.add(var.set_gain(config[CONF_GAIN]))
diff --git a/esphome/components/ttp229_bsf/__init__.py b/esphome/components/ttp229_bsf/__init__.py
index 8707c9efe3..f1f86c929e 100644
--- a/esphome/components/ttp229_bsf/__init__.py
+++ b/esphome/components/ttp229_bsf/__init__.py
@@ -21,11 +21,11 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
-    sdo = yield cg.gpio_pin_expression(config[CONF_SDO_PIN])
+    sdo = await cg.gpio_pin_expression(config[CONF_SDO_PIN])
     cg.add(var.set_sdo_pin(sdo))
-    scl = yield cg.gpio_pin_expression(config[CONF_SCL_PIN])
+    scl = await cg.gpio_pin_expression(config[CONF_SCL_PIN])
     cg.add(var.set_scl_pin(scl))
diff --git a/esphome/components/ttp229_bsf/binary_sensor.py b/esphome/components/ttp229_bsf/binary_sensor.py
index 7351e73d80..75540fe0e8 100644
--- a/esphome/components/ttp229_bsf/binary_sensor.py
+++ b/esphome/components/ttp229_bsf/binary_sensor.py
@@ -16,10 +16,10 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield binary_sensor.register_binary_sensor(var, config)
+    await binary_sensor.register_binary_sensor(var, config)
 
     cg.add(var.set_channel(config[CONF_CHANNEL]))
-    hub = yield cg.get_variable(config[CONF_TTP229_ID])
+    hub = await cg.get_variable(config[CONF_TTP229_ID])
     cg.add(hub.register_channel(var))
diff --git a/esphome/components/ttp229_lsf/__init__.py b/esphome/components/ttp229_lsf/__init__.py
index 1d7efea205..cba41a7938 100644
--- a/esphome/components/ttp229_lsf/__init__.py
+++ b/esphome/components/ttp229_lsf/__init__.py
@@ -25,7 +25,7 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await cg.register_component(var, config)
+    await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/ttp229_lsf/binary_sensor.py b/esphome/components/ttp229_lsf/binary_sensor.py
index 09a8a1e207..b52a9e8575 100644
--- a/esphome/components/ttp229_lsf/binary_sensor.py
+++ b/esphome/components/ttp229_lsf/binary_sensor.py
@@ -16,10 +16,10 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield binary_sensor.register_binary_sensor(var, config)
+    await binary_sensor.register_binary_sensor(var, config)
 
     cg.add(var.set_channel(config[CONF_CHANNEL]))
-    hub = yield cg.get_variable(config[CONF_TTP229_ID])
+    hub = await cg.get_variable(config[CONF_TTP229_ID])
     cg.add(hub.register_channel(var))
diff --git a/esphome/components/tuya/__init__.py b/esphome/components/tuya/__init__.py
index 58dad13257..436759979a 100644
--- a/esphome/components/tuya/__init__.py
+++ b/esphome/components/tuya/__init__.py
@@ -27,12 +27,12 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield uart.register_uart_device(var, config)
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
     if CONF_TIME_ID in config:
-        time_ = yield cg.get_variable(config[CONF_TIME_ID])
+        time_ = await cg.get_variable(config[CONF_TIME_ID])
         cg.add(var.set_time_id(time_))
     if CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS in config:
         for dp in config[CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS]:
diff --git a/esphome/components/tuya/binary_sensor/__init__.py b/esphome/components/tuya/binary_sensor/__init__.py
index 45f918ff24..65f13ea422 100644
--- a/esphome/components/tuya/binary_sensor/__init__.py
+++ b/esphome/components/tuya/binary_sensor/__init__.py
@@ -22,12 +22,12 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield binary_sensor.register_binary_sensor(var, config)
+    await cg.register_component(var, config)
+    await binary_sensor.register_binary_sensor(var, config)
 
-    paren = yield cg.get_variable(config[CONF_TUYA_ID])
+    paren = await cg.get_variable(config[CONF_TUYA_ID])
     cg.add(var.set_tuya_parent(paren))
 
     cg.add(var.set_sensor_id(config[CONF_SENSOR_DATAPOINT]))
diff --git a/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp
index ad3d18efae..6b2954a6a0 100644
--- a/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp
+++ b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp
@@ -8,8 +8,8 @@ static const char *TAG = "tuya.binary_sensor";
 
 void TuyaBinarySensor::setup() {
   this->parent_->register_listener(this->sensor_id_, [this](TuyaDatapoint datapoint) {
+    ESP_LOGV(TAG, "MCU reported binary sensor %u is: %s", datapoint.id, ONOFF(datapoint.value_bool));
     this->publish_state(datapoint.value_bool);
-    ESP_LOGD(TAG, "MCU reported binary sensor is: %s", ONOFF(datapoint.value_bool));
   });
 }
 
diff --git a/esphome/components/tuya/climate/__init__.py b/esphome/components/tuya/climate/__init__.py
index 8ac42f7c8f..06c80964ee 100644
--- a/esphome/components/tuya/climate/__init__.py
+++ b/esphome/components/tuya/climate/__init__.py
@@ -1,12 +1,20 @@
 from esphome.components import climate
 import esphome.config_validation as cv
 import esphome.codegen as cg
-from esphome.const import CONF_ID, CONF_SWITCH_DATAPOINT
+from esphome.const import (
+    CONF_ID,
+    CONF_SWITCH_DATAPOINT,
+    CONF_SUPPORTS_COOL,
+    CONF_SUPPORTS_HEAT,
+)
 from .. import tuya_ns, CONF_TUYA_ID, Tuya
 
 DEPENDENCIES = ["tuya"]
 CODEOWNERS = ["@jesserockz"]
 
+CONF_ACTIVE_STATE_DATAPOINT = "active_state_datapoint"
+CONF_ACTIVE_STATE_HEATING_VALUE = "active_state_heating_value"
+CONF_ACTIVE_STATE_COOLING_VALUE = "active_state_cooling_value"
 CONF_TARGET_TEMPERATURE_DATAPOINT = "target_temperature_datapoint"
 CONF_CURRENT_TEMPERATURE_DATAPOINT = "current_temperature_datapoint"
 CONF_TEMPERATURE_MULTIPLIER = "temperature_multiplier"
@@ -59,12 +67,30 @@ def validate_temperature_multipliers(value):
     return value
 
 
+def validate_active_state_values(value):
+    if CONF_ACTIVE_STATE_DATAPOINT not in value:
+        return value
+    if value[CONF_SUPPORTS_COOL] and CONF_ACTIVE_STATE_COOLING_VALUE not in value:
+        raise cv.Invalid(
+            (
+                f"{CONF_ACTIVE_STATE_COOLING_VALUE} required if using "
+                f"{CONF_ACTIVE_STATE_DATAPOINT} and device supports cooling"
+            )
+        )
+    return value
+
+
 CONFIG_SCHEMA = cv.All(
     climate.CLIMATE_SCHEMA.extend(
         {
             cv.GenerateID(): cv.declare_id(TuyaClimate),
             cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya),
+            cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
+            cv.Optional(CONF_SUPPORTS_COOL, default=False): cv.boolean,
             cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
+            cv.Optional(CONF_ACTIVE_STATE_DATAPOINT): cv.uint8_t,
+            cv.Optional(CONF_ACTIVE_STATE_HEATING_VALUE, default=1): cv.uint8_t,
+            cv.Optional(CONF_ACTIVE_STATE_COOLING_VALUE): cv.uint8_t,
             cv.Optional(CONF_TARGET_TEMPERATURE_DATAPOINT): cv.uint8_t,
             cv.Optional(CONF_CURRENT_TEMPERATURE_DATAPOINT): cv.uint8_t,
             cv.Optional(CONF_TEMPERATURE_MULTIPLIER): cv.positive_float,
@@ -74,19 +100,32 @@ CONFIG_SCHEMA = cv.All(
     ).extend(cv.COMPONENT_SCHEMA),
     cv.has_at_least_one_key(CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT),
     validate_temperature_multipliers,
+    validate_active_state_values,
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield climate.register_climate(var, config)
+    await cg.register_component(var, config)
+    await climate.register_climate(var, config)
 
-    paren = yield cg.get_variable(config[CONF_TUYA_ID])
+    paren = await cg.get_variable(config[CONF_TUYA_ID])
     cg.add(var.set_tuya_parent(paren))
 
+    cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
+    cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
     if CONF_SWITCH_DATAPOINT in config:
         cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT]))
+    if CONF_ACTIVE_STATE_DATAPOINT in config:
+        cg.add(var.set_active_state_id(config[CONF_ACTIVE_STATE_DATAPOINT]))
+    if CONF_ACTIVE_STATE_HEATING_VALUE in config:
+        cg.add(
+            var.set_active_state_heating_value(config[CONF_ACTIVE_STATE_HEATING_VALUE])
+        )
+    if CONF_ACTIVE_STATE_COOLING_VALUE in config:
+        cg.add(
+            var.set_active_state_cooling_value(config[CONF_ACTIVE_STATE_COOLING_VALUE])
+        )
     if CONF_TARGET_TEMPERATURE_DATAPOINT in config:
         cg.add(var.set_target_temperature_id(config[CONF_TARGET_TEMPERATURE_DATAPOINT]))
     if CONF_CURRENT_TEMPERATURE_DATAPOINT in config:
diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp
index d1f6829e72..962d18a391 100644
--- a/esphome/components/tuya/climate/tuya_climate.cpp
+++ b/esphome/components/tuya/climate/tuya_climate.cpp
@@ -9,65 +9,67 @@ static const char *TAG = "tuya.climate";
 void TuyaClimate::setup() {
   if (this->switch_id_.has_value()) {
     this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) {
+      ESP_LOGV(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool));
+      this->mode = climate::CLIMATE_MODE_OFF;
       if (datapoint.value_bool) {
-        this->mode = climate::CLIMATE_MODE_HEAT;
-      } else {
-        this->mode = climate::CLIMATE_MODE_OFF;
+        if (this->supports_heat_ && this->supports_cool_) {
+          this->mode = climate::CLIMATE_MODE_AUTO;
+        } else if (this->supports_heat_) {
+          this->mode = climate::CLIMATE_MODE_HEAT;
+        } else if (this->supports_cool_) {
+          this->mode = climate::CLIMATE_MODE_COOL;
+        }
       }
       this->compute_state_();
       this->publish_state();
-      ESP_LOGD(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool));
+    });
+  }
+  if (this->active_state_id_.has_value()) {
+    this->parent_->register_listener(*this->active_state_id_, [this](TuyaDatapoint datapoint) {
+      ESP_LOGV(TAG, "MCU reported active state is: %u", datapoint.value_enum);
+      this->active_state_ = datapoint.value_enum;
+      this->compute_state_();
+      this->publish_state();
     });
   }
   if (this->target_temperature_id_.has_value()) {
     this->parent_->register_listener(*this->target_temperature_id_, [this](TuyaDatapoint datapoint) {
       this->target_temperature = datapoint.value_int * this->target_temperature_multiplier_;
+      ESP_LOGV(TAG, "MCU reported target temperature is: %.1f", this->target_temperature);
       this->compute_state_();
       this->publish_state();
-      ESP_LOGD(TAG, "MCU reported target temperature is: %.1f", this->target_temperature);
     });
   }
   if (this->current_temperature_id_.has_value()) {
     this->parent_->register_listener(*this->current_temperature_id_, [this](TuyaDatapoint datapoint) {
       this->current_temperature = datapoint.value_int * this->current_temperature_multiplier_;
+      ESP_LOGV(TAG, "MCU reported current temperature is: %.1f", this->current_temperature);
       this->compute_state_();
       this->publish_state();
-      ESP_LOGD(TAG, "MCU reported current temperature is: %.1f", this->current_temperature);
     });
   }
 }
 
 void TuyaClimate::control(const climate::ClimateCall &call) {
   if (call.get_mode().has_value()) {
-    this->mode = *call.get_mode();
-
-    TuyaDatapoint datapoint{};
-    datapoint.id = *this->switch_id_;
-    datapoint.type = TuyaDatapointType::BOOLEAN;
-    datapoint.value_bool = this->mode != climate::CLIMATE_MODE_OFF;
-    this->parent_->set_datapoint_value(datapoint);
-    ESP_LOGD(TAG, "Setting switch: %s", ONOFF(datapoint.value_bool));
+    const bool switch_state = *call.get_mode() != climate::CLIMATE_MODE_OFF;
+    ESP_LOGV(TAG, "Setting switch: %s", ONOFF(switch_state));
+    this->parent_->set_datapoint_value(*this->switch_id_, switch_state);
   }
 
   if (call.get_target_temperature().has_value()) {
-    this->target_temperature = *call.get_target_temperature();
-
-    TuyaDatapoint datapoint{};
-    datapoint.id = *this->target_temperature_id_;
-    datapoint.type = TuyaDatapointType::INTEGER;
-    datapoint.value_int = (int) (this->target_temperature / this->target_temperature_multiplier_);
-    this->parent_->set_datapoint_value(datapoint);
-    ESP_LOGD(TAG, "Setting target temperature: %.1f", this->target_temperature);
+    const float target_temperature = *call.get_target_temperature();
+    ESP_LOGV(TAG, "Setting target temperature: %.1f", target_temperature);
+    this->parent_->set_datapoint_value(*this->target_temperature_id_,
+                                       (int) (target_temperature / this->target_temperature_multiplier_));
   }
-
-  this->compute_state_();
-  this->publish_state();
 }
 
 climate::ClimateTraits TuyaClimate::traits() {
   auto traits = climate::ClimateTraits();
   traits.set_supports_current_temperature(this->current_temperature_id_.has_value());
-  traits.set_supports_heat_mode(true);
+  traits.set_supports_heat_mode(this->supports_heat_);
+  traits.set_supports_cool_mode(this->supports_cool_);
   traits.set_supports_action(true);
   return traits;
 }
@@ -76,6 +78,8 @@ void TuyaClimate::dump_config() {
   LOG_CLIMATE("", "Tuya Climate", this);
   if (this->switch_id_.has_value())
     ESP_LOGCONFIG(TAG, "  Switch has datapoint ID %u", *this->switch_id_);
+  if (this->active_state_id_.has_value())
+    ESP_LOGCONFIG(TAG, "  Active state has datapoint ID %u", *this->active_state_id_);
   if (this->target_temperature_id_.has_value())
     ESP_LOGCONFIG(TAG, "  Target Temperature has datapoint ID %u", *this->target_temperature_id_);
   if (this->current_temperature_id_.has_value())
@@ -94,30 +98,27 @@ void TuyaClimate::compute_state_() {
     return;
   }
 
-  const bool too_cold = this->current_temperature < this->target_temperature - 1;
-  const bool too_hot = this->current_temperature > this->target_temperature + 1;
-  const bool on_target = this->current_temperature == this->target_temperature;
-
-  climate::ClimateAction target_action;
-  if (too_cold) {
-    // too cold -> show as heating if possible, else idle
-    if (this->traits().supports_mode(climate::CLIMATE_MODE_HEAT)) {
+  climate::ClimateAction target_action = climate::CLIMATE_ACTION_IDLE;
+  if (this->active_state_id_.has_value()) {
+    if (this->supports_heat_ && this->active_state_heating_value_.has_value() &&
+        this->active_state_ == this->active_state_heating_value_) {
       target_action = climate::CLIMATE_ACTION_HEATING;
-    } else {
-      target_action = climate::CLIMATE_ACTION_IDLE;
-    }
-  } else if (too_hot) {
-    // too hot -> show as cooling if possible, else idle
-    if (this->traits().supports_mode(climate::CLIMATE_MODE_COOL)) {
+    } else if (this->supports_cool_ && this->active_state_cooling_value_.has_value() &&
+               this->active_state_ == this->active_state_cooling_value_) {
       target_action = climate::CLIMATE_ACTION_COOLING;
-    } else {
-      target_action = climate::CLIMATE_ACTION_IDLE;
     }
-  } else if (on_target) {
-    target_action = climate::CLIMATE_ACTION_IDLE;
   } else {
-    target_action = this->action;
+    // Fallback to active state calc based on temp and hysteresis
+    const float temp_diff = this->target_temperature - this->current_temperature;
+    if (std::abs(temp_diff) > this->hysteresis_) {
+      if (this->supports_heat_ && temp_diff > 0) {
+        target_action = climate::CLIMATE_ACTION_HEATING;
+      } else if (this->supports_cool_ && temp_diff < 0) {
+        target_action = climate::CLIMATE_ACTION_COOLING;
+      }
+    }
   }
+
   this->switch_to_action_(target_action);
 }
 
diff --git a/esphome/components/tuya/climate/tuya_climate.h b/esphome/components/tuya/climate/tuya_climate.h
index e9c366e898..f015bc337c 100644
--- a/esphome/components/tuya/climate/tuya_climate.h
+++ b/esphome/components/tuya/climate/tuya_climate.h
@@ -11,7 +11,12 @@ class TuyaClimate : public climate::Climate, public Component {
  public:
   void setup() override;
   void dump_config() override;
+  void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
+  void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
   void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; }
+  void set_active_state_id(uint8_t state_id) { this->active_state_id_ = state_id; }
+  void set_active_state_heating_value(uint8_t value) { this->active_state_heating_value_ = value; }
+  void set_active_state_cooling_value(uint8_t value) { this->active_state_cooling_value_ = value; }
   void set_target_temperature_id(uint8_t target_temperature_id) {
     this->target_temperature_id_ = target_temperature_id;
   }
@@ -40,11 +45,18 @@ class TuyaClimate : public climate::Climate, public Component {
   void switch_to_action_(climate::ClimateAction action);
 
   Tuya *parent_;
+  bool supports_heat_;
+  bool supports_cool_;
   optional<uint8_t> switch_id_{};
+  optional<uint8_t> active_state_id_{};
+  optional<uint8_t> active_state_heating_value_{};
+  optional<uint8_t> active_state_cooling_value_{};
   optional<uint8_t> target_temperature_id_{};
   optional<uint8_t> current_temperature_id_{};
   float current_temperature_multiplier_{1.0f};
   float target_temperature_multiplier_{1.0f};
+  float hysteresis_{1.0f};
+  uint8_t active_state_;
 };
 
 }  // namespace tuya
diff --git a/esphome/components/tuya/fan/__init__.py b/esphome/components/tuya/fan/__init__.py
index 5d3345a5c4..6d660e6d29 100644
--- a/esphome/components/tuya/fan/__init__.py
+++ b/esphome/components/tuya/fan/__init__.py
@@ -28,14 +28,14 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
-    parent = yield cg.get_variable(config[CONF_TUYA_ID])
-    state = yield fan.create_fan_state(config)
+async def to_code(config):
+    parent = await cg.get_variable(config[CONF_TUYA_ID])
+    state = await fan.create_fan_state(config)
 
     var = cg.new_Pvariable(
         config[CONF_OUTPUT_ID], parent, state, config[CONF_SPEED_COUNT]
     )
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
     if CONF_SPEED_DATAPOINT in config:
         cg.add(var.set_speed_id(config[CONF_SPEED_DATAPOINT]))
diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp
index 62f4a78db7..b2326034a1 100644
--- a/esphome/components/tuya/fan/tuya_fan.cpp
+++ b/esphome/components/tuya/fan/tuya_fan.cpp
@@ -14,29 +14,29 @@ void TuyaFan::setup() {
 
   if (this->speed_id_.has_value()) {
     this->parent_->register_listener(*this->speed_id_, [this](TuyaDatapoint datapoint) {
+      ESP_LOGV(TAG, "MCU reported speed of: %d", datapoint.value_enum);
       auto call = this->fan_->make_call();
       if (datapoint.value_enum < this->speed_count_)
         call.set_speed(datapoint.value_enum + 1);
       else
         ESP_LOGCONFIG(TAG, "Speed has invalid value %d", datapoint.value_enum);
-      ESP_LOGD(TAG, "MCU reported speed of: %d", datapoint.value_enum);
       call.perform();
     });
   }
   if (this->switch_id_.has_value()) {
     this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) {
+      ESP_LOGV(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool));
       auto call = this->fan_->make_call();
       call.set_state(datapoint.value_bool);
       call.perform();
-      ESP_LOGD(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool));
     });
   }
   if (this->oscillation_id_.has_value()) {
     this->parent_->register_listener(*this->oscillation_id_, [this](TuyaDatapoint datapoint) {
+      ESP_LOGV(TAG, "MCU reported oscillation is: %s", ONOFF(datapoint.value_bool));
       auto call = this->fan_->make_call();
       call.set_oscillating(datapoint.value_bool);
       call.perform();
-      ESP_LOGD(TAG, "MCU reported oscillation is: %s", ONOFF(datapoint.value_bool));
     });
   }
   if (this->direction_id_.has_value()) {
@@ -66,37 +66,21 @@ void TuyaFan::dump_config() {
 
 void TuyaFan::write_state() {
   if (this->switch_id_.has_value()) {
-    TuyaDatapoint datapoint{};
-    datapoint.id = *this->switch_id_;
-    datapoint.type = TuyaDatapointType::BOOLEAN;
-    datapoint.value_bool = this->fan_->state;
-    this->parent_->set_datapoint_value(datapoint);
-    ESP_LOGD(TAG, "Setting switch: %s", ONOFF(this->fan_->state));
+    ESP_LOGV(TAG, "Setting switch: %s", ONOFF(this->fan_->state));
+    this->parent_->set_datapoint_value(*this->switch_id_, this->fan_->state);
   }
   if (this->oscillation_id_.has_value()) {
-    TuyaDatapoint datapoint{};
-    datapoint.id = *this->oscillation_id_;
-    datapoint.type = TuyaDatapointType::BOOLEAN;
-    datapoint.value_bool = this->fan_->oscillating;
-    this->parent_->set_datapoint_value(datapoint);
-    ESP_LOGD(TAG, "Setting oscillating: %s", ONOFF(this->fan_->oscillating));
+    ESP_LOGV(TAG, "Setting oscillating: %s", ONOFF(this->fan_->oscillating));
+    this->parent_->set_datapoint_value(*this->oscillation_id_, this->fan_->oscillating);
   }
   if (this->direction_id_.has_value()) {
-    TuyaDatapoint datapoint{};
-    datapoint.id = *this->direction_id_;
-    datapoint.type = TuyaDatapointType::BOOLEAN;
     bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE;
-    datapoint.value_bool = enable;
-    this->parent_->set_datapoint_value(datapoint);
-    ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable));
+    ESP_LOGV(TAG, "Setting reverse direction: %s", ONOFF(enable));
+    this->parent_->set_datapoint_value(*this->direction_id_, enable);
   }
   if (this->speed_id_.has_value()) {
-    TuyaDatapoint datapoint{};
-    datapoint.id = *this->speed_id_;
-    datapoint.type = TuyaDatapointType::ENUM;
-    datapoint.value_enum = this->fan_->speed - 1;
-    ESP_LOGD(TAG, "Setting speed: %d", datapoint.value_enum);
-    this->parent_->set_datapoint_value(datapoint);
+    ESP_LOGV(TAG, "Setting speed: %d", this->fan_->speed);
+    this->parent_->set_datapoint_value(*this->speed_id_, this->fan_->speed);
   }
 }
 
diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py
index f8026e47e8..979082d636 100644
--- a/esphome/components/tuya/light/__init__.py
+++ b/esphome/components/tuya/light/__init__.py
@@ -8,6 +8,8 @@ from esphome.const import (
     CONF_GAMMA_CORRECT,
     CONF_DEFAULT_TRANSITION_LENGTH,
     CONF_SWITCH_DATAPOINT,
+    CONF_COLD_WHITE_COLOR_TEMPERATURE,
+    CONF_WARM_WHITE_COLOR_TEMPERATURE,
 )
 from .. import tuya_ns, CONF_TUYA_ID, Tuya
 
@@ -15,6 +17,8 @@ DEPENDENCIES = ["tuya"]
 
 CONF_DIMMER_DATAPOINT = "dimmer_datapoint"
 CONF_MIN_VALUE_DATAPOINT = "min_value_datapoint"
+CONF_COLOR_TEMPERATURE_DATAPOINT = "color_temperature_datapoint"
+CONF_COLOR_TEMPERATURE_MAX_VALUE = "color_temperature_max_value"
 
 TuyaLight = tuya_ns.class_("TuyaLight", light.LightOutput, cg.Component)
 
@@ -26,8 +30,18 @@ CONFIG_SCHEMA = cv.All(
             cv.Optional(CONF_DIMMER_DATAPOINT): cv.uint8_t,
             cv.Optional(CONF_MIN_VALUE_DATAPOINT): cv.uint8_t,
             cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
+            cv.Inclusive(
+                CONF_COLOR_TEMPERATURE_DATAPOINT, "color_temperature"
+            ): cv.uint8_t,
             cv.Optional(CONF_MIN_VALUE): cv.int_,
             cv.Optional(CONF_MAX_VALUE): cv.int_,
+            cv.Optional(CONF_COLOR_TEMPERATURE_MAX_VALUE): cv.int_,
+            cv.Inclusive(
+                CONF_COLD_WHITE_COLOR_TEMPERATURE, "color_temperature"
+            ): cv.color_temperature,
+            cv.Inclusive(
+                CONF_WARM_WHITE_COLOR_TEMPERATURE, "color_temperature"
+            ): cv.color_temperature,
             # Change the default gamma_correct and default transition length settings.
             # The Tuya MCU handles transitions and gamma correction on its own.
             cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float,
@@ -40,10 +54,10 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
-    yield cg.register_component(var, config)
-    yield light.register_light(var, config)
+    await cg.register_component(var, config)
+    await light.register_light(var, config)
 
     if CONF_DIMMER_DATAPOINT in config:
         cg.add(var.set_dimmer_id(config[CONF_DIMMER_DATAPOINT]))
@@ -51,9 +65,23 @@ def to_code(config):
         cg.add(var.set_min_value_datapoint_id(config[CONF_MIN_VALUE_DATAPOINT]))
     if CONF_SWITCH_DATAPOINT in config:
         cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT]))
+    if CONF_COLOR_TEMPERATURE_DATAPOINT in config:
+        cg.add(var.set_color_temperature_id(config[CONF_COLOR_TEMPERATURE_DATAPOINT]))
+        cg.add(
+            var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE])
+        )
+        cg.add(
+            var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])
+        )
     if CONF_MIN_VALUE in config:
         cg.add(var.set_min_value(config[CONF_MIN_VALUE]))
     if CONF_MAX_VALUE in config:
         cg.add(var.set_max_value(config[CONF_MAX_VALUE]))
-    paren = yield cg.get_variable(config[CONF_TUYA_ID])
+    if CONF_COLOR_TEMPERATURE_MAX_VALUE in config:
+        cg.add(
+            var.set_color_temperature_max_value(
+                config[CONF_COLOR_TEMPERATURE_MAX_VALUE]
+            )
+        )
+    paren = await cg.get_variable(config[CONF_TUYA_ID])
     cg.add(var.set_tuya_parent(paren))
diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp
index e7b44882a1..4befad401e 100644
--- a/esphome/components/tuya/light/tuya_light.cpp
+++ b/esphome/components/tuya/light/tuya_light.cpp
@@ -7,6 +7,15 @@ namespace tuya {
 static const char *TAG = "tuya.light";
 
 void TuyaLight::setup() {
+  if (this->color_temperature_id_.has_value()) {
+    this->parent_->register_listener(*this->color_temperature_id_, [this](TuyaDatapoint datapoint) {
+      auto call = this->state_->make_call();
+      call.set_color_temperature(this->cold_white_temperature_ +
+                                 (this->warm_white_temperature_ - this->cold_white_temperature_) *
+                                     (float(datapoint.value_uint) / float(this->color_temperature_max_value_)));
+      call.perform();
+    });
+  }
   if (this->dimmer_id_.has_value()) {
     this->parent_->register_listener(*this->dimmer_id_, [this](TuyaDatapoint datapoint) {
       auto call = this->state_->make_call();
@@ -22,11 +31,7 @@ void TuyaLight::setup() {
     });
   }
   if (min_value_datapoint_id_.has_value()) {
-    TuyaDatapoint datapoint{};
-    datapoint.id = *this->min_value_datapoint_id_;
-    datapoint.type = TuyaDatapointType::INTEGER;
-    datapoint.value_int = this->min_value_;
-    parent_->set_datapoint_value(datapoint);
+    parent_->set_datapoint_value(*this->min_value_datapoint_id_, this->min_value_);
   }
 }
 
@@ -41,6 +46,11 @@ void TuyaLight::dump_config() {
 light::LightTraits TuyaLight::get_traits() {
   auto traits = light::LightTraits();
   traits.set_supports_brightness(this->dimmer_id_.has_value());
+  traits.set_supports_color_temperature(this->color_temperature_id_.has_value());
+  if (this->color_temperature_id_.has_value()) {
+    traits.set_min_mireds(this->cold_white_temperature_);
+    traits.set_max_mireds(this->warm_white_temperature_);
+  }
   return traits;
 }
 
@@ -53,38 +63,29 @@ void TuyaLight::write_state(light::LightState *state) {
   if (brightness == 0.0f) {
     // turning off, first try via switch (if exists), then dimmer
     if (switch_id_.has_value()) {
-      TuyaDatapoint datapoint{};
-      datapoint.id = *this->switch_id_;
-      datapoint.type = TuyaDatapointType::BOOLEAN;
-      datapoint.value_bool = false;
-
-      parent_->set_datapoint_value(datapoint);
+      parent_->set_datapoint_value(*this->switch_id_, false);
     } else if (dimmer_id_.has_value()) {
-      TuyaDatapoint datapoint{};
-      datapoint.id = *this->dimmer_id_;
-      datapoint.type = TuyaDatapointType::INTEGER;
-      datapoint.value_int = 0;
-      parent_->set_datapoint_value(datapoint);
+      parent_->set_datapoint_value(*this->dimmer_id_, 0);
     }
     return;
   }
 
+  if (this->color_temperature_id_.has_value()) {
+    uint32_t color_temp_int =
+        static_cast<uint32_t>(this->color_temperature_max_value_ *
+                              (state->current_values.get_color_temperature() - this->cold_white_temperature_) /
+                              (this->warm_white_temperature_ - this->cold_white_temperature_));
+    parent_->set_datapoint_value(*this->color_temperature_id_, color_temp_int);
+  }
+
   auto brightness_int = static_cast<uint32_t>(brightness * this->max_value_);
   brightness_int = std::max(brightness_int, this->min_value_);
 
   if (this->dimmer_id_.has_value()) {
-    TuyaDatapoint datapoint{};
-    datapoint.id = *this->dimmer_id_;
-    datapoint.type = TuyaDatapointType::INTEGER;
-    datapoint.value_int = brightness_int;
-    parent_->set_datapoint_value(datapoint);
+    parent_->set_datapoint_value(*this->dimmer_id_, brightness_int);
   }
   if (this->switch_id_.has_value()) {
-    TuyaDatapoint datapoint{};
-    datapoint.id = *this->switch_id_;
-    datapoint.type = TuyaDatapointType::BOOLEAN;
-    datapoint.value_bool = true;
-    parent_->set_datapoint_value(datapoint);
+    parent_->set_datapoint_value(*this->switch_id_, true);
   }
 }
 
diff --git a/esphome/components/tuya/light/tuya_light.h b/esphome/components/tuya/light/tuya_light.h
index 896c0cc7ef..72422bc9e7 100644
--- a/esphome/components/tuya/light/tuya_light.h
+++ b/esphome/components/tuya/light/tuya_light.h
@@ -16,9 +16,19 @@ class TuyaLight : public Component, public light::LightOutput {
     this->min_value_datapoint_id_ = min_value_datapoint_id;
   }
   void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; }
+  void set_color_temperature_id(uint8_t color_temperature_id) { this->color_temperature_id_ = color_temperature_id; }
   void set_tuya_parent(Tuya *parent) { this->parent_ = parent; }
   void set_min_value(uint32_t min_value) { min_value_ = min_value; }
   void set_max_value(uint32_t max_value) { max_value_ = max_value; }
+  void set_color_temperature_max_value(uint32_t color_temperature_max_value) {
+    this->color_temperature_max_value_ = color_temperature_max_value;
+  }
+  void set_cold_white_temperature(float cold_white_temperature) {
+    this->cold_white_temperature_ = cold_white_temperature;
+  }
+  void set_warm_white_temperature(float warm_white_temperature) {
+    this->warm_white_temperature_ = warm_white_temperature;
+  }
   light::LightTraits get_traits() override;
   void setup_state(light::LightState *state) override;
   void write_state(light::LightState *state) override;
@@ -31,8 +41,12 @@ class TuyaLight : public Component, public light::LightOutput {
   optional<uint8_t> dimmer_id_{};
   optional<uint8_t> min_value_datapoint_id_{};
   optional<uint8_t> switch_id_{};
+  optional<uint8_t> color_temperature_id_{};
   uint32_t min_value_ = 0;
   uint32_t max_value_ = 255;
+  uint32_t color_temperature_max_value_ = 255;
+  float cold_white_temperature_;
+  float warm_white_temperature_;
   light::LightState *state_{nullptr};
 };
 
diff --git a/esphome/components/tuya/sensor/__init__.py b/esphome/components/tuya/sensor/__init__.py
index 0a02fb77a1..d87a2e7ce4 100644
--- a/esphome/components/tuya/sensor/__init__.py
+++ b/esphome/components/tuya/sensor/__init__.py
@@ -20,12 +20,12 @@ CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
 
-    paren = yield cg.get_variable(config[CONF_TUYA_ID])
+    paren = await cg.get_variable(config[CONF_TUYA_ID])
     cg.add(var.set_tuya_parent(paren))
 
     cg.add(var.set_sensor_id(config[CONF_SENSOR_DATAPOINT]))
diff --git a/esphome/components/tuya/sensor/tuya_sensor.cpp b/esphome/components/tuya/sensor/tuya_sensor.cpp
index b8e2aa97b7..483552950f 100644
--- a/esphome/components/tuya/sensor/tuya_sensor.cpp
+++ b/esphome/components/tuya/sensor/tuya_sensor.cpp
@@ -9,17 +9,17 @@ static const char *TAG = "tuya.sensor";
 void TuyaSensor::setup() {
   this->parent_->register_listener(this->sensor_id_, [this](TuyaDatapoint datapoint) {
     if (datapoint.type == TuyaDatapointType::BOOLEAN) {
+      ESP_LOGV(TAG, "MCU reported sensor %u is: %s", datapoint.id, ONOFF(datapoint.value_bool));
       this->publish_state(datapoint.value_bool);
-      ESP_LOGD(TAG, "MCU reported sensor is: %s", ONOFF(datapoint.value_bool));
     } else if (datapoint.type == TuyaDatapointType::INTEGER) {
+      ESP_LOGV(TAG, "MCU reported sensor %u is: %d", datapoint.id, datapoint.value_int);
       this->publish_state(datapoint.value_int);
-      ESP_LOGD(TAG, "MCU reported sensor is: %d", datapoint.value_int);
     } else if (datapoint.type == TuyaDatapointType::ENUM) {
+      ESP_LOGV(TAG, "MCU reported sensor %u is: %u", datapoint.id, datapoint.value_enum);
       this->publish_state(datapoint.value_enum);
-      ESP_LOGD(TAG, "MCU reported sensor is: %d", datapoint.value_enum);
     } else if (datapoint.type == TuyaDatapointType::BITMASK) {
+      ESP_LOGV(TAG, "MCU reported sensor %u is: %x", datapoint.id, datapoint.value_bitmask);
       this->publish_state(datapoint.value_bitmask);
-      ESP_LOGD(TAG, "MCU reported sensor is: %x", datapoint.value_bitmask);
     }
   });
 }
diff --git a/esphome/components/tuya/switch/__init__.py b/esphome/components/tuya/switch/__init__.py
index 4c4ccbf814..4df6bba713 100644
--- a/esphome/components/tuya/switch/__init__.py
+++ b/esphome/components/tuya/switch/__init__.py
@@ -18,12 +18,12 @@ CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield switch.register_switch(var, config)
+    await cg.register_component(var, config)
+    await switch.register_switch(var, config)
 
-    paren = yield cg.get_variable(config[CONF_TUYA_ID])
+    paren = await cg.get_variable(config[CONF_TUYA_ID])
     cg.add(var.set_tuya_parent(paren))
 
     cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT]))
diff --git a/esphome/components/tuya/switch/tuya_switch.cpp b/esphome/components/tuya/switch/tuya_switch.cpp
index 8f7c7f170d..c735b32341 100644
--- a/esphome/components/tuya/switch/tuya_switch.cpp
+++ b/esphome/components/tuya/switch/tuya_switch.cpp
@@ -8,19 +8,14 @@ static const char *TAG = "tuya.switch";
 
 void TuyaSwitch::setup() {
   this->parent_->register_listener(this->switch_id_, [this](TuyaDatapoint datapoint) {
+    ESP_LOGV(TAG, "MCU reported switch %u is: %s", this->switch_id_, ONOFF(datapoint.value_bool));
     this->publish_state(datapoint.value_bool);
-    ESP_LOGD(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool));
   });
 }
 
 void TuyaSwitch::write_state(bool state) {
-  TuyaDatapoint datapoint{};
-  datapoint.id = this->switch_id_;
-  datapoint.type = TuyaDatapointType::BOOLEAN;
-  datapoint.value_bool = state;
-  this->parent_->set_datapoint_value(datapoint);
-  ESP_LOGD(TAG, "Setting switch: %s", ONOFF(state));
-
+  ESP_LOGV(TAG, "Setting switch %u: %s", this->switch_id_, ONOFF(state));
+  this->parent_->set_datapoint_value(this->switch_id_, state);
   this->publish_state(state);
 }
 
diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp
index f4a72e8109..3f09db068d 100644
--- a/esphome/components/tuya/tuya.cpp
+++ b/esphome/components/tuya/tuya.cpp
@@ -1,5 +1,6 @@
 #include "tuya.h"
 #include "esphome/core/log.h"
+#include "esphome/core/util.h"
 #include "esphome/core/helpers.h"
 
 namespace esphome {
@@ -9,7 +10,7 @@ static const char *TAG = "tuya";
 static const int COMMAND_DELAY = 50;
 
 void Tuya::setup() {
-  this->set_interval("heartbeat", 1000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); });
+  this->set_interval("heartbeat", 10000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); });
 }
 
 void Tuya::loop() {
@@ -30,18 +31,20 @@ void Tuya::dump_config() {
     return;
   }
   for (auto &info : this->datapoints_) {
-    if (info.type == TuyaDatapointType::BOOLEAN)
-      ESP_LOGCONFIG(TAG, "  Datapoint %d: switch (value: %s)", info.id, ONOFF(info.value_bool));
+    if (info.type == TuyaDatapointType::RAW)
+      ESP_LOGCONFIG(TAG, "  Datapoint %u: raw (value: %s)", info.id, hexencode(info.value_raw).c_str());
+    else if (info.type == TuyaDatapointType::BOOLEAN)
+      ESP_LOGCONFIG(TAG, "  Datapoint %u: switch (value: %s)", info.id, ONOFF(info.value_bool));
     else if (info.type == TuyaDatapointType::INTEGER)
-      ESP_LOGCONFIG(TAG, "  Datapoint %d: int value (value: %d)", info.id, info.value_int);
+      ESP_LOGCONFIG(TAG, "  Datapoint %u: int value (value: %d)", info.id, info.value_int);
     else if (info.type == TuyaDatapointType::STRING)
-      ESP_LOGCONFIG(TAG, "  Datapoint %d: string value (value: %s)", info.id, info.value_string.c_str());
+      ESP_LOGCONFIG(TAG, "  Datapoint %u: string value (value: %s)", info.id, info.value_string.c_str());
     else if (info.type == TuyaDatapointType::ENUM)
-      ESP_LOGCONFIG(TAG, "  Datapoint %d: enum (value: %d)", info.id, info.value_enum);
+      ESP_LOGCONFIG(TAG, "  Datapoint %u: enum (value: %d)", info.id, info.value_enum);
     else if (info.type == TuyaDatapointType::BITMASK)
-      ESP_LOGCONFIG(TAG, "  Datapoint %d: bitmask (value: %x)", info.id, info.value_bitmask);
+      ESP_LOGCONFIG(TAG, "  Datapoint %u: bitmask (value: %x)", info.id, info.value_bitmask);
     else
-      ESP_LOGCONFIG(TAG, "  Datapoint %d: unknown", info.id);
+      ESP_LOGCONFIG(TAG, "  Datapoint %u: unknown", info.id);
   }
   if ((this->gpio_status_ != -1) || (this->gpio_reset_ != -1)) {
     ESP_LOGCONFIG(TAG, "  GPIO Configuration: status: pin %d, reset: pin %d (not supported)", this->gpio_status_,
@@ -98,8 +101,8 @@ bool Tuya::validate_message_() {
 
   // valid message
   const uint8_t *message_data = data + 6;
-  ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version,  // NOLINT
-           hexencode(message_data, length).c_str(), this->init_state_);
+  ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version,
+           hexencode(message_data, length).c_str(), static_cast<uint8_t>(this->init_state_));
   this->handle_command_(command, version, message_data, length);
 
   // return false to reset rx buffer
@@ -117,6 +120,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
   switch ((TuyaCommandType) command) {
     case TuyaCommandType::HEARTBEAT:
       ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]);
+      this->protocol_version_ = version;
       if (buffer[0] == 0) {
         ESP_LOGI(TAG, "MCU restarted");
         this->init_state_ = TuyaInitState::INIT_HEARTBEAT;
@@ -148,8 +152,8 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
     }
     case TuyaCommandType::CONF_QUERY: {
       if (len >= 2) {
-        gpio_status_ = buffer[0];
-        gpio_reset_ = buffer[1];
+        this->gpio_status_ = buffer[0];
+        this->gpio_reset_ = buffer[1];
       }
       if (this->init_state_ == TuyaInitState::INIT_CONF) {
         // If mcu returned status gpio, then we can ommit sending wifi state
@@ -158,10 +162,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
           this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY);
         } else {
           this->init_state_ = TuyaInitState::INIT_WIFI;
-          // If we were following the spec to the letter we would send
-          // state updates until connected to both WiFi and API/MQTT.
-          // Instead we just claim to be connected immediately and move on.
-          this->send_command_(TuyaCommand{.cmd = TuyaCommandType::WIFI_STATE, .payload = std::vector<uint8_t>{0x04}});
+          this->set_interval("wifi", 1000, [this] { this->send_wifi_status_(); });
         }
       }
       break;
@@ -173,10 +174,10 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
       }
       break;
     case TuyaCommandType::WIFI_RESET:
-      ESP_LOGE(TAG, "TUYA_CMD_WIFI_RESET is not handled");
+      ESP_LOGE(TAG, "WIFI_RESET is not handled");
       break;
     case TuyaCommandType::WIFI_SELECT:
-      ESP_LOGE(TAG, "TUYA_CMD_WIFI_SELECT is not handled");
+      ESP_LOGE(TAG, "WIFI_SELECT is not handled");
       break;
     case TuyaCommandType::DATAPOINT_DELIVER:
       break;
@@ -189,48 +190,24 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
       break;
     case TuyaCommandType::DATAPOINT_QUERY:
       break;
-    case TuyaCommandType::WIFI_TEST: {
+    case TuyaCommandType::WIFI_TEST:
       this->send_command_(TuyaCommand{.cmd = TuyaCommandType::WIFI_TEST, .payload = std::vector<uint8_t>{0x00, 0x00}});
       break;
-    }
-    case TuyaCommandType::LOCAL_TIME_QUERY: {
+    case TuyaCommandType::LOCAL_TIME_QUERY:
 #ifdef USE_TIME
       if (this->time_id_.has_value()) {
+        this->send_local_time_();
         auto time_id = *this->time_id_;
-        auto now = time_id->now();
-
-        if (now.is_valid()) {
-          uint8_t year = now.year - 2000;
-          uint8_t month = now.month;
-          uint8_t day_of_month = now.day_of_month;
-          uint8_t hour = now.hour;
-          uint8_t minute = now.minute;
-          uint8_t second = now.second;
-          // Tuya days starts from Monday, esphome uses Sunday as day 1
-          uint8_t day_of_week = now.day_of_week - 1;
-          if (day_of_week == 0) {
-            day_of_week = 7;
-          }
-          this->send_command_(TuyaCommand{
-              .cmd = TuyaCommandType::LOCAL_TIME_QUERY,
-              .payload = std::vector<uint8_t>{0x01, year, month, day_of_month, hour, minute, second, day_of_week}});
-        } else {
-          ESP_LOGW(TAG, "TUYA_CMD_LOCAL_TIME_QUERY is not handled because time is not valid");
-          // By spec we need to notify MCU that the time was not obtained
-          this->send_command_(
-              TuyaCommand{.cmd = TuyaCommandType::LOCAL_TIME_QUERY,
-                          .payload = std::vector<uint8_t>{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}});
-        }
+        time_id->add_on_time_sync_callback([this] { this->send_local_time_(); });
       } else {
-        ESP_LOGW(TAG, "TUYA_CMD_LOCAL_TIME_QUERY is not handled because time is not configured");
+        ESP_LOGW(TAG, "LOCAL_TIME_QUERY is not handled because time is not configured");
       }
 #else
       ESP_LOGE(TAG, "LOCAL_TIME_QUERY is not handled");
 #endif
       break;
-    }
     default:
-      ESP_LOGE(TAG, "invalid command (%02x) received", command);
+      ESP_LOGE(TAG, "Invalid command (0x%02X) received", command);
   }
 }
 
@@ -243,8 +220,8 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) {
   datapoint.type = (TuyaDatapointType) buffer[1];
   datapoint.value_uint = 0;
 
-  // drop update if datapoint is in ignore_mcu_datapoint_update list
-  for (auto i : this->ignore_mcu_update_on_datapoints_) {
+  // Drop update if datapoint is in ignore_mcu_datapoint_update list
+  for (uint8_t i : this->ignore_mcu_update_on_datapoints_) {
     if (datapoint.id == i) {
       ESP_LOGV(TAG, "Datapoint %u found in ignore_mcu_update_on_datapoints list, dropping MCU update", datapoint.id);
       return;
@@ -255,38 +232,65 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) {
   const uint8_t *data = buffer + 4;
   size_t data_len = len - 4;
   if (data_size != data_len) {
-    ESP_LOGW(TAG, "invalid datapoint update");
+    ESP_LOGW(TAG, "Datapoint %u is not expected size", datapoint.id);
     return;
   }
+  datapoint.len = data_len;
 
   switch (datapoint.type) {
+    case TuyaDatapointType::RAW:
+      datapoint.value_raw = std::vector<uint8_t>(data, data + data_len);
+      ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, hexencode(datapoint.value_raw).c_str());
+      break;
     case TuyaDatapointType::BOOLEAN:
-      if (data_len != 1)
+      if (data_len != 1) {
+        ESP_LOGW(TAG, "Datapoint %u has bad boolean len %zu", datapoint.id, data_len);
         return;
+      }
       datapoint.value_bool = data[0];
+      ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, ONOFF(datapoint.value_bool));
       break;
     case TuyaDatapointType::INTEGER:
-      if (data_len != 4)
+      if (data_len != 4) {
+        ESP_LOGW(TAG, "Datapoint %u has bad integer len %zu", datapoint.id, data_len);
         return;
+      }
       datapoint.value_uint = encode_uint32(data[0], data[1], data[2], data[3]);
+      ESP_LOGD(TAG, "Datapoint %u update to %d", datapoint.id, datapoint.value_int);
       break;
     case TuyaDatapointType::STRING:
       datapoint.value_string = std::string(reinterpret_cast<const char *>(data), data_len);
+      ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, datapoint.value_string.c_str());
       break;
     case TuyaDatapointType::ENUM:
-      if (data_len != 1)
+      if (data_len != 1) {
+        ESP_LOGW(TAG, "Datapoint %u has bad enum len %zu", datapoint.id, data_len);
         return;
+      }
       datapoint.value_enum = data[0];
+      ESP_LOGD(TAG, "Datapoint %u update to %d", datapoint.id, datapoint.value_enum);
       break;
     case TuyaDatapointType::BITMASK:
-      if (data_len != 2)
-        return;
-      datapoint.value_bitmask = (uint16_t(data[0]) << 8) | (uint16_t(data[1]) << 0);
+      switch (data_len) {
+        case 1:
+          datapoint.value_bitmask = encode_uint32(0, 0, 0, data[0]);
+          break;
+        case 2:
+          datapoint.value_bitmask = encode_uint32(0, 0, data[0], data[1]);
+          break;
+        case 4:
+          datapoint.value_bitmask = encode_uint32(data[0], data[1], data[2], data[3]);
+          break;
+        default:
+          ESP_LOGW(TAG, "Datapoint %u has bad bitmask len %zu", datapoint.id, data_len);
+          return;
+      }
+      ESP_LOGD(TAG, "Datapoint %u update to %#08X", datapoint.id, datapoint.value_bitmask);
       break;
     default:
+      ESP_LOGW(TAG, "Datapoint %u has unknown type %#02hhX", datapoint.id, datapoint.type);
       return;
   }
-  ESP_LOGV(TAG, "Datapoint %u update to %u", datapoint.id, datapoint.value_uint);
 
   // Update internal datapoints
   bool found = false;
@@ -313,8 +317,8 @@ void Tuya::send_raw_command_(TuyaCommand command) {
 
   this->last_command_timestamp_ = millis();
 
-  ESP_LOGV(TAG, "Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command.cmd, version,  // NOLINT
-           hexencode(command.payload).c_str(), this->init_state_);
+  ESP_LOGV(TAG, "Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", static_cast<uint8_t>(command.cmd),
+           version, hexencode(command.payload).c_str(), static_cast<uint8_t>(this->init_state_));
 
   this->write_array({0x55, 0xAA, version, (uint8_t) command.cmd, len_hi, len_lo});
   if (!command.payload.empty())
@@ -344,53 +348,113 @@ void Tuya::send_empty_command_(TuyaCommandType command) {
   send_command_(TuyaCommand{.cmd = command, .payload = std::vector<uint8_t>{0x04}});
 }
 
-void Tuya::set_datapoint_value(TuyaDatapoint datapoint) {
-  std::vector<uint8_t> buffer;
-  ESP_LOGV(TAG, "Datapoint %u set to %u", datapoint.id, datapoint.value_uint);
-  for (auto &other : this->datapoints_) {
-    if (other.id == datapoint.id) {
-      // String value is stored outside the union; must be checked separately.
-      if (datapoint.type == TuyaDatapointType::STRING) {
-        if (other.value_string == datapoint.value_string) {
-          ESP_LOGV(TAG, "Not sending unchanged value");
-          return;
-        }
-      } else if (other.value_uint == datapoint.value_uint) {
-        ESP_LOGV(TAG, "Not sending unchanged value");
-        return;
+void Tuya::send_wifi_status_() {
+  uint8_t status = 0x02;
+  if (network_is_connected()) {
+    status = 0x03;
+
+    // Protocol version 3 also supports specifying when connected to "the cloud"
+    if (this->protocol_version_ >= 0x03) {
+      if (remote_is_connected()) {
+        status = 0x04;
       }
     }
   }
-  buffer.push_back(datapoint.id);
-  buffer.push_back(static_cast<uint8_t>(datapoint.type));
 
-  std::vector<uint8_t> data;
-  switch (datapoint.type) {
-    case TuyaDatapointType::BOOLEAN:
-      data.push_back(datapoint.value_bool);
-      break;
-    case TuyaDatapointType::INTEGER:
-      data.push_back(datapoint.value_uint >> 24);
-      data.push_back(datapoint.value_uint >> 16);
-      data.push_back(datapoint.value_uint >> 8);
-      data.push_back(datapoint.value_uint >> 0);
-      break;
-    case TuyaDatapointType::STRING:
-      for (char const &c : datapoint.value_string) {
-        data.push_back(c);
-      }
-      break;
-    case TuyaDatapointType::ENUM:
-      data.push_back(datapoint.value_enum);
-      break;
-    case TuyaDatapointType::BITMASK:
-      data.push_back(datapoint.value_bitmask >> 8);
-      data.push_back(datapoint.value_bitmask >> 0);
-      break;
-    default:
-      return;
+  if (status == this->wifi_status_) {
+    return;
   }
 
+  ESP_LOGD(TAG, "Sending WiFi Status");
+  this->wifi_status_ = status;
+  this->send_command_(TuyaCommand{.cmd = TuyaCommandType::WIFI_STATE, .payload = std::vector<uint8_t>{status}});
+}
+
+#ifdef USE_TIME
+void Tuya::send_local_time_() {
+  std::vector<uint8_t> payload;
+  auto time_id = *this->time_id_;
+  time::ESPTime now = time_id->now();
+  if (now.is_valid()) {
+    uint8_t year = now.year - 2000;
+    uint8_t month = now.month;
+    uint8_t day_of_month = now.day_of_month;
+    uint8_t hour = now.hour;
+    uint8_t minute = now.minute;
+    uint8_t second = now.second;
+    // Tuya days starts from Monday, esphome uses Sunday as day 1
+    uint8_t day_of_week = now.day_of_week - 1;
+    if (day_of_week == 0) {
+      day_of_week = 7;
+    }
+    ESP_LOGD(TAG, "Sending local time");
+    payload = std::vector<uint8_t>{0x01, year, month, day_of_month, hour, minute, second, day_of_week};
+  } else {
+    // By spec we need to notify MCU that the time was not obtained if this is a response to a query
+    ESP_LOGW(TAG, "Sending missing local time");
+    payload = std::vector<uint8_t>{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+  }
+  this->send_command_(TuyaCommand{.cmd = TuyaCommandType::LOCAL_TIME_QUERY, .payload = payload});
+}
+#endif
+
+void Tuya::set_datapoint_value(uint8_t datapoint_id, uint32_t value) {
+  ESP_LOGD(TAG, "Setting datapoint %u to %u", datapoint_id, value);
+  optional<TuyaDatapoint> datapoint = this->get_datapoint_(datapoint_id);
+  if (!datapoint.has_value()) {
+    ESP_LOGE(TAG, "Attempt to set unknown datapoint %u", datapoint_id);
+    return;
+  }
+  if (datapoint->value_uint == value) {
+    ESP_LOGV(TAG, "Not sending unchanged value");
+    return;
+  }
+
+  std::vector<uint8_t> data;
+  switch (datapoint->len) {
+    case 4:
+      data.push_back(value >> 24);
+      data.push_back(value >> 16);
+    case 2:
+      data.push_back(value >> 8);
+    case 1:
+      data.push_back(value >> 0);
+      break;
+    default:
+      ESP_LOGE(TAG, "Unexpected datapoint length %zu", datapoint->len);
+      return;
+  }
+  this->send_datapoint_command_(datapoint->id, datapoint->type, data);
+}
+
+void Tuya::set_datapoint_value(uint8_t datapoint_id, std::string value) {
+  ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, value.c_str());
+  optional<TuyaDatapoint> datapoint = this->get_datapoint_(datapoint_id);
+  if (!datapoint.has_value()) {
+    ESP_LOGE(TAG, "Attempt to set unknown datapoint %u", datapoint_id);
+  }
+  if (datapoint->value_string == value) {
+    ESP_LOGV(TAG, "Not sending unchanged value");
+    return;
+  }
+  std::vector<uint8_t> data;
+  for (char const &c : value) {
+    data.push_back(c);
+  }
+  this->send_datapoint_command_(datapoint->id, datapoint->type, data);
+}
+
+optional<TuyaDatapoint> Tuya::get_datapoint_(uint8_t datapoint_id) {
+  for (auto &datapoint : this->datapoints_)
+    if (datapoint.id == datapoint_id)
+      return datapoint;
+  return {};
+}
+
+void Tuya::send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, std::vector<uint8_t> data) {
+  std::vector<uint8_t> buffer;
+  buffer.push_back(datapoint_id);
+  buffer.push_back(static_cast<uint8_t>(datapoint_type));
   buffer.push_back(data.size() >> 8);
   buffer.push_back(data.size() >> 0);
   buffer.insert(buffer.end(), data.begin(), data.end());
diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h
index a2b4040eb3..42848b4914 100644
--- a/esphome/components/tuya/tuya.h
+++ b/esphome/components/tuya/tuya.h
@@ -23,14 +23,16 @@ enum class TuyaDatapointType : uint8_t {
 struct TuyaDatapoint {
   uint8_t id;
   TuyaDatapointType type;
+  size_t len;
   union {
     bool value_bool;
     int value_int;
     uint32_t value_uint;
     uint8_t value_enum;
-    uint16_t value_bitmask;
+    uint32_t value_bitmask;
   };
   std::string value_string;
+  std::vector<uint8_t> value_raw;
 };
 
 struct TuyaDatapointListener {
@@ -73,7 +75,8 @@ class Tuya : public Component, public uart::UARTDevice {
   void loop() override;
   void dump_config() override;
   void register_listener(uint8_t datapoint_id, const std::function<void(TuyaDatapoint)> &func);
-  void set_datapoint_value(TuyaDatapoint datapoint);
+  void set_datapoint_value(uint8_t datapoint_id, uint32_t value);
+  void set_datapoint_value(uint8_t datapoint_id, std::string value);
 #ifdef USE_TIME
   void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
 #endif
@@ -84,6 +87,7 @@ class Tuya : public Component, public uart::UARTDevice {
  protected:
   void handle_char_(uint8_t c);
   void handle_datapoint_(const uint8_t *buffer, size_t len);
+  optional<TuyaDatapoint> get_datapoint_(uint8_t datapoint_id);
   bool validate_message_();
 
   void handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len);
@@ -91,11 +95,15 @@ class Tuya : public Component, public uart::UARTDevice {
   void process_command_queue_();
   void send_command_(TuyaCommand command);
   void send_empty_command_(TuyaCommandType command);
+  void send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, std::vector<uint8_t> data);
+  void send_wifi_status_();
 
 #ifdef USE_TIME
+  void send_local_time_();
   optional<time::RealTimeClock *> time_id_{};
 #endif
   TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT;
+  uint8_t protocol_version_ = -1;
   int gpio_status_ = -1;
   int gpio_reset_ = -1;
   uint32_t last_command_timestamp_ = 0;
@@ -105,6 +113,7 @@ class Tuya : public Component, public uart::UARTDevice {
   std::vector<uint8_t> rx_message_;
   std::vector<uint8_t> ignore_mcu_update_on_datapoints_{};
   std::vector<TuyaCommand> command_queue_;
+  uint8_t wifi_status_ = -1;
 };
 
 }  // namespace tuya
diff --git a/esphome/components/tx20/sensor.py b/esphome/components/tx20/sensor.py
index 434257470b..57c3165d16 100644
--- a/esphome/components/tx20/sensor.py
+++ b/esphome/components/tx20/sensor.py
@@ -8,6 +8,8 @@ from esphome.const import (
     CONF_PIN,
     CONF_WIND_DIRECTION_DEGREES,
     DEVICE_CLASS_EMPTY,
+    STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_NONE,
     UNIT_KILOMETER_PER_HOUR,
     ICON_WEATHER_WINDY,
     ICON_SIGN_DIRECTION,
@@ -21,10 +23,14 @@ CONFIG_SCHEMA = cv.Schema(
     {
         cv.GenerateID(): cv.declare_id(Tx20Component),
         cv.Optional(CONF_WIND_SPEED): sensor.sensor_schema(
-            UNIT_KILOMETER_PER_HOUR, ICON_WEATHER_WINDY, 1, DEVICE_CLASS_EMPTY
+            UNIT_KILOMETER_PER_HOUR,
+            ICON_WEATHER_WINDY,
+            1,
+            DEVICE_CLASS_EMPTY,
+            STATE_CLASS_MEASUREMENT,
         ),
         cv.Optional(CONF_WIND_DIRECTION_DEGREES): sensor.sensor_schema(
-            UNIT_DEGREES, ICON_SIGN_DIRECTION, 1, DEVICE_CLASS_EMPTY
+            UNIT_DEGREES, ICON_SIGN_DIRECTION, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
         ),
         cv.Required(CONF_PIN): cv.All(
             pins.internal_gpio_input_pin_schema, pins.validate_has_interrupt
@@ -33,19 +39,19 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
     if CONF_WIND_SPEED in config:
         conf = config[CONF_WIND_SPEED]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_wind_speed_sensor(sens))
 
     if CONF_WIND_DIRECTION_DEGREES in config:
         conf = config[CONF_WIND_DIRECTION_DEGREES]
-        sens = yield sensor.new_sensor(conf)
+        sens = await sensor.new_sensor(conf)
         cg.add(var.set_wind_direction_degrees_sensor(sens))
 
-    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+    pin = await cg.gpio_pin_expression(config[CONF_PIN])
     cg.add(var.set_pin(pin))
diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py
index a02ea58def..aaed333e34 100644
--- a/esphome/components/uart/__init__.py
+++ b/esphome/components/uart/__init__.py
@@ -11,7 +11,7 @@ from esphome.const import (
     CONF_RX_BUFFER_SIZE,
     CONF_INVERT,
 )
-from esphome.core import CORE, coroutine
+from esphome.core import CORE
 
 CODEOWNERS = ["@esphome/core"]
 uart_ns = cg.esphome_ns.namespace("uart")
@@ -73,10 +73,10 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     cg.add_global(uart_ns.using)
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
     cg.add(var.set_baud_rate(config[CONF_BAUD_RATE]))
 
@@ -92,6 +92,42 @@ def to_code(config):
     cg.add(var.set_parity(config[CONF_PARITY]))
 
 
+def validate_device(
+    name, config, item_config, baud_rate=None, require_tx=True, require_rx=True
+):
+    if not hasattr(config, "uart_devices"):
+        config.uart_devices = {}
+    devices = config.uart_devices
+
+    uart_config = config.get_config_by_id(item_config[CONF_UART_ID])
+
+    uart_id = uart_config[CONF_ID]
+    device = devices.setdefault(uart_id, {})
+
+    if require_tx:
+        if CONF_TX_PIN not in uart_config:
+            raise ValueError(f"Component {name} requires parent uart to declare tx_pin")
+        if CONF_TX_PIN in device:
+            raise ValueError(
+                f"Component {name} cannot use the same uart.{CONF_TX_PIN} as component {device[CONF_TX_PIN]} is already using it"
+            )
+        device[CONF_TX_PIN] = name
+
+    if require_rx:
+        if CONF_RX_PIN not in uart_config:
+            raise ValueError(f"Component {name} requires parent uart to declare rx_pin")
+        if CONF_RX_PIN in device:
+            raise ValueError(
+                f"Component {name} cannot use the same uart.{CONF_RX_PIN} as component {device[CONF_RX_PIN]} is already using it"
+            )
+        device[CONF_RX_PIN] = name
+
+    if baud_rate and uart_config[CONF_BAUD_RATE] != baud_rate:
+        raise ValueError(
+            f"Component {name} requires parent uart baud rate be {baud_rate}"
+        )
+
+
 # A schema to use for all UART devices, all UART integrations must extend this!
 UART_DEVICE_SCHEMA = cv.Schema(
     {
@@ -100,13 +136,12 @@ UART_DEVICE_SCHEMA = cv.Schema(
 )
 
 
-@coroutine
-def register_uart_device(var, config):
+async def register_uart_device(var, config):
     """Register a UART device, setting up all the internal values.
 
     This is a coroutine, you need to await it with a 'yield' expression!
     """
-    parent = yield cg.get_variable(config[CONF_UART_ID])
+    parent = await cg.get_variable(config[CONF_UART_ID])
     cg.add(var.set_uart_parent(parent))
 
 
@@ -121,16 +156,16 @@ def register_uart_device(var, config):
         key=CONF_DATA,
     ),
 )
-def uart_write_to_code(config, action_id, template_arg, args):
+async def uart_write_to_code(config, action_id, template_arg, args):
     var = cg.new_Pvariable(action_id, template_arg)
-    yield cg.register_parented(var, config[CONF_ID])
+    await cg.register_parented(var, config[CONF_ID])
     data = config[CONF_DATA]
     if isinstance(data, bytes):
         data = list(data)
 
     if cg.is_template(data):
-        templ = yield cg.templatable(data, args, cg.std_vector.template(cg.uint8))
+        templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
         cg.add(var.set_data_template(templ))
     else:
         cg.add(var.set_data_static(data))
-    yield var
+    return var
diff --git a/esphome/components/uart/switch/__init__.py b/esphome/components/uart/switch/__init__.py
index e84035aa3e..9e7f95bd2a 100644
--- a/esphome/components/uart/switch/__init__.py
+++ b/esphome/components/uart/switch/__init__.py
@@ -26,11 +26,11 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield switch.register_switch(var, config)
-    yield uart.register_uart_device(var, config)
+    await cg.register_component(var, config)
+    await switch.register_switch(var, config)
+    await uart.register_uart_device(var, config)
 
     data = config[CONF_DATA]
     if isinstance(data, bytes):
diff --git a/esphome/components/uln2003/stepper.py b/esphome/components/uln2003/stepper.py
index 4d2e5ab518..88252ead73 100644
--- a/esphome/components/uln2003/stepper.py
+++ b/esphome/components/uln2003/stepper.py
@@ -38,18 +38,18 @@ CONFIG_SCHEMA = stepper.STEPPER_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield stepper.register_stepper(var, config)
+    await cg.register_component(var, config)
+    await stepper.register_stepper(var, config)
 
-    pin_a = yield cg.gpio_pin_expression(config[CONF_PIN_A])
+    pin_a = await cg.gpio_pin_expression(config[CONF_PIN_A])
     cg.add(var.set_pin_a(pin_a))
-    pin_b = yield cg.gpio_pin_expression(config[CONF_PIN_B])
+    pin_b = await cg.gpio_pin_expression(config[CONF_PIN_B])
     cg.add(var.set_pin_b(pin_b))
-    pin_c = yield cg.gpio_pin_expression(config[CONF_PIN_C])
+    pin_c = await cg.gpio_pin_expression(config[CONF_PIN_C])
     cg.add(var.set_pin_c(pin_c))
-    pin_d = yield cg.gpio_pin_expression(config[CONF_PIN_D])
+    pin_d = await cg.gpio_pin_expression(config[CONF_PIN_D])
     cg.add(var.set_pin_d(pin_d))
 
     cg.add(var.set_sleep_when_done(config[CONF_SLEEP_WHEN_DONE]))
diff --git a/esphome/components/ultrasonic/sensor.py b/esphome/components/ultrasonic/sensor.py
index d5d8dec6f4..77b08b3324 100644
--- a/esphome/components/ultrasonic/sensor.py
+++ b/esphome/components/ultrasonic/sensor.py
@@ -8,6 +8,7 @@ from esphome.const import (
     CONF_TRIGGER_PIN,
     CONF_TIMEOUT,
     DEVICE_CLASS_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_METER,
     ICON_ARROW_EXPAND_VERTICAL,
 )
@@ -20,7 +21,13 @@ UltrasonicSensorComponent = ultrasonic_ns.class_(
 )
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, 2, DEVICE_CLASS_EMPTY)
+    sensor.sensor_schema(
+        UNIT_METER,
+        ICON_ARROW_EXPAND_VERTICAL,
+        2,
+        DEVICE_CLASS_EMPTY,
+        STATE_CLASS_MEASUREMENT,
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(UltrasonicSensorComponent),
@@ -43,14 +50,14 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
 
-    trigger = yield cg.gpio_pin_expression(config[CONF_TRIGGER_PIN])
+    trigger = await cg.gpio_pin_expression(config[CONF_TRIGGER_PIN])
     cg.add(var.set_trigger_pin(trigger))
-    echo = yield cg.gpio_pin_expression(config[CONF_ECHO_PIN])
+    echo = await cg.gpio_pin_expression(config[CONF_ECHO_PIN])
     cg.add(var.set_echo_pin(echo))
 
     cg.add(var.set_timeout_us(config[CONF_TIMEOUT] / (0.000343 / 2)))
diff --git a/esphome/components/uptime/sensor.py b/esphome/components/uptime/sensor.py
index c2e35ddfef..eaaee5a2d5 100644
--- a/esphome/components/uptime/sensor.py
+++ b/esphome/components/uptime/sensor.py
@@ -1,13 +1,21 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import sensor
-from esphome.const import CONF_ID, DEVICE_CLASS_EMPTY, UNIT_SECOND, ICON_TIMER
+from esphome.const import (
+    CONF_ID,
+    DEVICE_CLASS_EMPTY,
+    STATE_CLASS_NONE,
+    UNIT_SECOND,
+    ICON_TIMER,
+)
 
 uptime_ns = cg.esphome_ns.namespace("uptime")
 UptimeSensor = uptime_ns.class_("UptimeSensor", sensor.Sensor, cg.PollingComponent)
 
 CONFIG_SCHEMA = (
-    sensor.sensor_schema(UNIT_SECOND, ICON_TIMER, 0, DEVICE_CLASS_EMPTY)
+    sensor.sensor_schema(
+        UNIT_SECOND, ICON_TIMER, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(UptimeSensor),
@@ -17,7 +25,7 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
diff --git a/esphome/components/version/text_sensor.py b/esphome/components/version/text_sensor.py
index 711800136c..e67f881d32 100644
--- a/esphome/components/version/text_sensor.py
+++ b/esphome/components/version/text_sensor.py
@@ -17,8 +17,8 @@ CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend(
 ).extend(cv.COMPONENT_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield text_sensor.register_text_sensor(var, config)
-    yield cg.register_component(var, config)
+    await text_sensor.register_text_sensor(var, config)
+    await cg.register_component(var, config)
     cg.add(var.set_hide_timestamp(config[CONF_HIDE_TIMESTAMP]))
diff --git a/esphome/components/vl53l0x/sensor.py b/esphome/components/vl53l0x/sensor.py
index 309d4cf8b3..8a9667a1bd 100644
--- a/esphome/components/vl53l0x/sensor.py
+++ b/esphome/components/vl53l0x/sensor.py
@@ -4,6 +4,7 @@ from esphome.components import i2c, sensor
 from esphome.const import (
     CONF_ID,
     DEVICE_CLASS_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_METER,
     ICON_ARROW_EXPAND_VERTICAL,
     CONF_ADDRESS,
@@ -40,7 +41,13 @@ def check_timeout(value):
 
 
 CONFIG_SCHEMA = cv.All(
-    sensor.sensor_schema(UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, 2, DEVICE_CLASS_EMPTY)
+    sensor.sensor_schema(
+        UNIT_METER,
+        ICON_ARROW_EXPAND_VERTICAL,
+        2,
+        DEVICE_CLASS_EMPTY,
+        STATE_CLASS_MEASUREMENT,
+    )
     .extend(
         {
             cv.GenerateID(): cv.declare_id(VL53L0XSensor),
@@ -58,16 +65,16 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
     cg.add(var.set_signal_rate_limit(config[CONF_SIGNAL_RATE_LIMIT]))
     cg.add(var.set_long_range(config[CONF_LONG_RANGE]))
     cg.add(var.set_timeout_us(config[CONF_TIMEOUT]))
 
     if CONF_ENABLE_PIN in config:
-        enable = yield cg.gpio_pin_expression(config[CONF_ENABLE_PIN])
+        enable = await cg.gpio_pin_expression(config[CONF_ENABLE_PIN])
         cg.add(var.set_enable_pin(enable))
 
-    yield sensor.register_sensor(var, config)
-    yield i2c.register_i2c_device(var, config)
+    await sensor.register_sensor(var, config)
+    await i2c.register_i2c_device(var, config)
diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py
index 16f234563c..42509f39f1 100644
--- a/esphome/components/waveshare_epaper/display.py
+++ b/esphome/components/waveshare_epaper/display.py
@@ -90,7 +90,7 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     model_type, model = MODELS[config[CONF_MODEL]]
     if model_type == "a":
         rhs = WaveshareEPaperTypeA.new(model)
@@ -101,23 +101,23 @@ def to_code(config):
     else:
         raise NotImplementedError()
 
-    yield cg.register_component(var, config)
-    yield display.register_display(var, config)
-    yield spi.register_spi_device(var, config)
+    await cg.register_component(var, config)
+    await display.register_display(var, config)
+    await spi.register_spi_device(var, config)
 
-    dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN])
+    dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
     cg.add(var.set_dc_pin(dc))
 
     if CONF_LAMBDA in config:
-        lambda_ = yield cg.process_lambda(
+        lambda_ = await cg.process_lambda(
             config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void
         )
         cg.add(var.set_writer(lambda_))
     if CONF_RESET_PIN in config:
-        reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN])
+        reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
         cg.add(var.set_reset_pin(reset))
     if CONF_BUSY_PIN in config:
-        reset = yield cg.gpio_pin_expression(config[CONF_BUSY_PIN])
+        reset = await cg.gpio_pin_expression(config[CONF_BUSY_PIN])
         cg.add(var.set_busy_pin(reset))
     if CONF_FULL_UPDATE_EVERY in config:
         cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY]))
diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py
index d04f2077f4..a181f83c64 100644
--- a/esphome/components/web_server/__init__.py
+++ b/esphome/components/web_server/__init__.py
@@ -46,11 +46,11 @@ CONFIG_SCHEMA = cv.Schema(
 
 
 @coroutine_with_priority(40.0)
-def to_code(config):
-    paren = yield cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
+async def to_code(config):
+    paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
 
     var = cg.new_Pvariable(config[CONF_ID], paren)
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
     cg.add(paren.set_port(config[CONF_PORT]))
     cg.add_define("WEBSERVER_PORT", config[CONF_PORT])
diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py
index 09f5dacd7c..37f3c989e4 100644
--- a/esphome/components/web_server_base/__init__.py
+++ b/esphome/components/web_server_base/__init__.py
@@ -19,9 +19,9 @@ CONFIG_SCHEMA = cv.Schema(
 
 
 @coroutine_with_priority(65.0)
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
     if CORE.is_esp32:
         cg.add_library("FS", None)
diff --git a/esphome/components/whirlpool/climate.py b/esphome/components/whirlpool/climate.py
index d6d9f3e111..c5b953c46f 100644
--- a/esphome/components/whirlpool/climate.py
+++ b/esphome/components/whirlpool/climate.py
@@ -23,7 +23,7 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield climate_ir.register_climate_ir(var, config)
+    await climate_ir.register_climate_ir(var, config)
     cg.add(var.set_model(config[CONF_MODEL]))
diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp
index eba08d5bbe..e7c93246f2 100644
--- a/esphome/components/whirlpool/whirlpool.cpp
+++ b/esphome/components/whirlpool/whirlpool.cpp
@@ -81,7 +81,7 @@ void WhirlpoolClimate::transmit_state() {
   remote_state[3] |= (uint8_t)(temp - this->temperature_min_()) << 4;
 
   // Fan speed
-  switch (this->fan_mode) {
+  switch (this->fan_mode.value()) {
     case climate::CLIMATE_FAN_HIGH:
       remote_state[2] |= WHIRLPOOL_FAN_HIGH;
       break;
diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py
index 421797eb28..c45e179bc4 100644
--- a/esphome/components/wifi/__init__.py
+++ b/esphome/components/wifi/__init__.py
@@ -137,7 +137,7 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend(
 )
 
 
-def validate(config):
+def _validate(config):
     if CONF_PASSWORD in config and CONF_SSID not in config:
         raise cv.Invalid("Cannot have WiFi password without SSID!")
 
@@ -207,7 +207,7 @@ CONFIG_SCHEMA = cv.All(
             ),
         }
     ),
-    validate,
+    _validate,
 )
 
 
@@ -277,7 +277,7 @@ def wifi_network(config, static_ip):
 
 
 @coroutine_with_priority(60.0)
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
 
@@ -305,9 +305,9 @@ def to_code(config):
         add_mdns_library()
 
     # Register at end for OTA safe mode
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
 
 @automation.register_condition("wifi.connected", WiFiConnectedCondition, cv.Schema({}))
-def wifi_connected_to_code(config, condition_id, template_arg, args):
-    yield cg.new_Pvariable(condition_id, template_arg)
+async def wifi_connected_to_code(config, condition_id, template_arg, args):
+    return cg.new_Pvariable(condition_id, template_arg)
diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp
index 0a6607852d..da1f6f85a7 100644
--- a/esphome/components/wifi/wifi_component.cpp
+++ b/esphome/components/wifi/wifi_component.cpp
@@ -22,6 +22,10 @@
 #include "esphome/components/captive_portal/captive_portal.h"
 #endif
 
+#ifdef USE_IMPROV
+#include "esphome/components/esp32_improv/esp32_improv_component.h"
+#endif
+
 namespace esphome {
 namespace wifi {
 
@@ -34,6 +38,19 @@ void WiFiComponent::setup() {
   this->last_connected_ = millis();
   this->wifi_pre_setup_();
 
+  uint32_t hash = fnv1_hash(App.get_compilation_time());
+  this->pref_ = global_preferences.make_preference<wifi::SavedWifiSettings>(hash, true);
+
+  SavedWifiSettings save{};
+  if (this->pref_.load(&save)) {
+    ESP_LOGD(TAG, "Loaded saved wifi settings: %s", save.ssid);
+
+    WiFiAP sta{};
+    sta.set_ssid(save.ssid);
+    sta.set_password(save.password);
+    this->set_sta(sta);
+  }
+
   if (this->has_sta()) {
     this->wifi_sta_pre_setup_();
     if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) {
@@ -60,7 +77,10 @@ void WiFiComponent::setup() {
       captive_portal::global_captive_portal->start();
 #endif
   }
-
+#ifdef USE_IMPROV
+  if (esp32_improv::global_improv_component != nullptr)
+    esp32_improv::global_improv_component->start();
+#endif
   this->wifi_apply_hostname_();
 #if defined(ARDUINO_ARCH_ESP32) && defined(USE_MDNS)
   network_setup_mdns();
@@ -122,6 +142,14 @@ void WiFiComponent::loop() {
       }
     }
 
+#ifdef USE_IMPROV
+    if (esp32_improv::global_improv_component != nullptr) {
+      if (!this->is_connected()) {
+        esp32_improv::global_improv_component->start();
+      }
+    }
+#endif
+
     if (!this->has_ap() && this->reboot_timeout_ != 0) {
       if (now - this->last_connected_ > this->reboot_timeout_) {
         ESP_LOGE(TAG, "Can't connect to WiFi, rebooting...");
@@ -186,9 +214,21 @@ float WiFiComponent::get_loop_priority() const {
 void WiFiComponent::set_ap(const WiFiAP &ap) { this->ap_ = ap; }
 void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); }
 void WiFiComponent::set_sta(const WiFiAP &ap) {
-  this->sta_.clear();
+  this->clear_sta();
   this->add_sta(ap);
 }
+void WiFiComponent::clear_sta() { this->sta_.clear(); }
+void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) {
+  SavedWifiSettings save{};
+  strcpy(save.ssid, ssid.c_str());
+  strcpy(save.password, password.c_str());
+  this->pref_.save(&save);
+
+  WiFiAP sta{};
+  sta.set_ssid(ssid);
+  sta.set_password(password);
+  this->set_sta(sta);
+}
 
 void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
   ESP_LOGI(TAG, "WiFi Connecting to '%s'...", ap.get_ssid().c_str());
@@ -466,6 +506,12 @@ void WiFiComponent::check_connecting_finished() {
       ESP_LOGD(TAG, "Disabling AP...");
       this->wifi_mode_({}, false);
     }
+#ifdef USE_IMPROV
+    if (this->is_esp32_improv_active_()) {
+      esp32_improv::global_improv_component->end();
+    }
+#endif
+
 #if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS)
     network_setup_mdns(this->wifi_sta_ip_(), 0);
 #endif
@@ -517,7 +563,8 @@ void WiFiComponent::retry_connect() {
   }
 
   delay(10);
-  if (!this->is_captive_portal_active_() && (this->num_retried_ > 5 || this->error_from_callback_)) {
+  if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() &&
+      (this->num_retried_ > 5 || this->error_from_callback_)) {
     // If retry failed for more than 5 times, let's restart STA
     ESP_LOGW(TAG, "Restarting WiFi adapter...");
     this->wifi_mode_(false, {});
@@ -563,6 +610,13 @@ bool WiFiComponent::is_captive_portal_active_() {
   return false;
 #endif
 }
+bool WiFiComponent::is_esp32_improv_active_() {
+#ifdef USE_IMPROV
+  return esp32_improv::global_improv_component != nullptr && esp32_improv::global_improv_component->is_active();
+#else
+  return false;
+#endif
+}
 
 void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
 void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; }
diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h
index ee8fd208b2..753455ddb1 100644
--- a/esphome/components/wifi/wifi_component.h
+++ b/esphome/components/wifi/wifi_component.h
@@ -27,6 +27,11 @@ extern "C" {
 namespace esphome {
 namespace wifi {
 
+struct SavedWifiSettings {
+  char ssid[33];
+  char password[65];
+} PACKED;  // NOLINT
+
 enum WiFiComponentState {
   /** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */
   WIFI_COMPONENT_STATE_OFF = 0,
@@ -156,6 +161,7 @@ class WiFiComponent : public Component {
 
   void set_sta(const WiFiAP &ap);
   void add_sta(const WiFiAP &ap);
+  void clear_sta();
 
   /** Setup an Access Point that should be created if no connection to a station can be made.
    *
@@ -185,6 +191,7 @@ class WiFiComponent : public Component {
   void set_power_save_mode(WiFiPowerSaveMode power_save);
   void set_output_power(float output_power) { output_power_ = output_power; }
 
+  void save_wifi_sta(const std::string &ssid, const std::string &password);
   // ========== INTERNAL METHODS ==========
   // (In most use cases you won't need these)
   /// Setup WiFi interface.
@@ -253,6 +260,7 @@ class WiFiComponent : public Component {
   bool wifi_disconnect_();
 
   bool is_captive_portal_active_();
+  bool is_esp32_improv_active_();
 
 #ifdef ARDUINO_ARCH_ESP8266
   static void wifi_event_callback(System_Event_t *event);
@@ -288,6 +296,8 @@ class WiFiComponent : public Component {
   bool scan_done_{false};
   bool ap_setup_{false};
   optional<float> output_power_;
+  ESPPreferenceObject pref_;
+  bool has_saved_wifi_settings_{false};
 };
 
 extern WiFiComponent *global_wifi_component;
diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py
index 07af63524c..50ec3eb272 100644
--- a/esphome/components/wifi_info/text_sensor.py
+++ b/esphome/components/wifi_info/text_sensor.py
@@ -8,7 +8,6 @@ from esphome.const import (
     CONF_SSID,
     CONF_MAC_ADDRESS,
 )
-from esphome.core import coroutine
 
 DEPENDENCIES = ["wifi"]
 
@@ -50,17 +49,16 @@ CONFIG_SCHEMA = cv.Schema(
 )
 
 
-@coroutine
-def setup_conf(config, key):
+async def setup_conf(config, key):
     if key in config:
         conf = config[key]
         var = cg.new_Pvariable(conf[CONF_ID])
-        yield cg.register_component(var, conf)
-        yield text_sensor.register_text_sensor(var, conf)
+        await cg.register_component(var, conf)
+        await text_sensor.register_text_sensor(var, conf)
 
 
-def to_code(config):
-    yield setup_conf(config, CONF_IP_ADDRESS)
-    yield setup_conf(config, CONF_SSID)
-    yield setup_conf(config, CONF_BSSID)
-    yield setup_conf(config, CONF_MAC_ADDRESS)
+async def to_code(config):
+    await setup_conf(config, CONF_IP_ADDRESS)
+    await setup_conf(config, CONF_SSID)
+    await setup_conf(config, CONF_BSSID)
+    await setup_conf(config, CONF_MAC_ADDRESS)
diff --git a/esphome/components/wifi_signal/sensor.py b/esphome/components/wifi_signal/sensor.py
index 891d95d33e..f1807966a2 100644
--- a/esphome/components/wifi_signal/sensor.py
+++ b/esphome/components/wifi_signal/sensor.py
@@ -5,6 +5,7 @@ from esphome.const import (
     CONF_ID,
     DEVICE_CLASS_SIGNAL_STRENGTH,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_DECIBEL_MILLIWATT,
 )
 
@@ -16,7 +17,11 @@ WiFiSignalSensor = wifi_signal_ns.class_(
 
 CONFIG_SCHEMA = (
     sensor.sensor_schema(
-        UNIT_DECIBEL_MILLIWATT, ICON_EMPTY, 0, DEVICE_CLASS_SIGNAL_STRENGTH
+        UNIT_DECIBEL_MILLIWATT,
+        ICON_EMPTY,
+        0,
+        DEVICE_CLASS_SIGNAL_STRENGTH,
+        STATE_CLASS_MEASUREMENT,
     )
     .extend(
         {
@@ -27,7 +32,7 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield sensor.register_sensor(var, config)
+    await cg.register_component(var, config)
+    await sensor.register_sensor(var, config)
diff --git a/esphome/components/wled/__init__.py b/esphome/components/wled/__init__.py
index 31ec318281..c9e23bb7eb 100644
--- a/esphome/components/wled/__init__.py
+++ b/esphome/components/wled/__init__.py
@@ -18,8 +18,8 @@ CONFIG_SCHEMA = cv.Schema({})
         cv.Optional(CONF_PORT, default=21324): cv.port,
     },
 )
-def wled_light_effect_to_code(config, effect_id):
+async def wled_light_effect_to_code(config, effect_id):
     effect = cg.new_Pvariable(effect_id, config[CONF_NAME])
     cg.add(effect.set_port(config[CONF_PORT]))
 
-    yield effect
+    return effect
diff --git a/esphome/components/xiaomi_ble/__init__.py b/esphome/components/xiaomi_ble/__init__.py
index 3d11ea8125..046adc6248 100644
--- a/esphome/components/xiaomi_ble/__init__.py
+++ b/esphome/components/xiaomi_ble/__init__.py
@@ -17,6 +17,6 @@ CONFIG_SCHEMA = cv.Schema(
 ).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp
index 29a34be81c..5806ceb72b 100644
--- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp
+++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp
@@ -189,6 +189,9 @@ optional<XiaomiParseResult> parse_xiaomi_header(const esp32_ble_tracker::Service
   } else if ((raw[2] == 0x76) && (raw[3] == 0x05)) {  // Cleargrass (Qingping) alarm clock, segment LCD
     result.type = XiaomiParseResult::TYPE_CGD1;
     result.name = "CGD1";
+  } else if ((raw[2] == 0x6F) && (raw[3] == 0x06)) {  // Cleargrass (Qingping) Temp & RH Lite
+    result.type = XiaomiParseResult::TYPE_CGDK2;
+    result.name = "CGDK2";
   } else if ((raw[2] == 0x5b) && (raw[3] == 0x05)) {  // small square body, segment LCD, encrypted
     result.type = XiaomiParseResult::TYPE_LYWSD03MMC;
     result.name = "LYWSD03MMC";
diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h
index 3973dac80f..f431eca11e 100644
--- a/esphome/components/xiaomi_ble/xiaomi_ble.h
+++ b/esphome/components/xiaomi_ble/xiaomi_ble.h
@@ -18,6 +18,7 @@ struct XiaomiParseResult {
     TYPE_CGG1,
     TYPE_LYWSD03MMC,
     TYPE_CGD1,
+    TYPE_CGDK2,
     TYPE_JQJCY01YM,
     TYPE_MUE4094RT,
     TYPE_WX08ZM,
diff --git a/esphome/components/xiaomi_cgd1/sensor.py b/esphome/components/xiaomi_cgd1/sensor.py
index 25d1f93674..e7f18a6be9 100644
--- a/esphome/components/xiaomi_cgd1/sensor.py
+++ b/esphome/components/xiaomi_cgd1/sensor.py
@@ -11,6 +11,7 @@ from esphome.const import (
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
     CONF_BINDKEY,
@@ -31,13 +32,25 @@ CONFIG_SCHEMA = (
             cv.Required(CONF_BINDKEY): cv.bind_key,
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_BATTERY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_BATTERY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -46,20 +59,20 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
     cg.add(var.set_bindkey(config[CONF_BINDKEY]))
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity(sens))
     if CONF_BATTERY_LEVEL in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
         cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xiaomi_cgdk2/__init__.py b/esphome/components/xiaomi_cgdk2/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_cgdk2/sensor.py b/esphome/components/xiaomi_cgdk2/sensor.py
new file mode 100644
index 0000000000..6b2c144911
--- /dev/null
+++ b/esphome/components/xiaomi_cgdk2/sensor.py
@@ -0,0 +1,78 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor, esp32_ble_tracker
+from esphome.const import (
+    CONF_BATTERY_LEVEL,
+    CONF_HUMIDITY,
+    CONF_MAC_ADDRESS,
+    CONF_TEMPERATURE,
+    DEVICE_CLASS_BATTERY,
+    DEVICE_CLASS_HUMIDITY,
+    DEVICE_CLASS_TEMPERATURE,
+    ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+    UNIT_PERCENT,
+    CONF_ID,
+    CONF_BINDKEY,
+)
+
+DEPENDENCIES = ["esp32_ble_tracker"]
+AUTO_LOAD = ["xiaomi_ble"]
+
+xiaomi_cgd1_ns = cg.esphome_ns.namespace("xiaomi_cgdk2")
+XiaomiCGD1 = xiaomi_cgd1_ns.class_(
+    "XiaomiCGDK2", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
+)
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(XiaomiCGD1),
+            cv.Required(CONF_BINDKEY): cv.bind_key,
+            cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_BATTERY,
+                STATE_CLASS_MEASUREMENT,
+            ),
+        }
+    )
+    .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
+    .extend(cv.COMPONENT_SCHEMA)
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
+
+    cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
+    cg.add(var.set_bindkey(config[CONF_BINDKEY]))
+
+    if CONF_TEMPERATURE in config:
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
+        cg.add(var.set_temperature(sens))
+    if CONF_HUMIDITY in config:
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
+        cg.add(var.set_humidity(sens))
+    if CONF_BATTERY_LEVEL in config:
+        sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp
new file mode 100644
index 0000000000..cd18c12afc
--- /dev/null
+++ b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp
@@ -0,0 +1,77 @@
+#include "xiaomi_cgdk2.h"
+#include "esphome/core/log.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace xiaomi_cgdk2 {
+
+static const char *TAG = "xiaomi_cgdk2";
+
+void XiaomiCGDK2::dump_config() {
+  ESP_LOGCONFIG(TAG, "Xiaomi CGDK2");
+  ESP_LOGCONFIG(TAG, "  Bindkey: %s", hexencode(this->bindkey_, 16).c_str());
+  LOG_SENSOR("  ", "Temperature", this->temperature_);
+  LOG_SENSOR("  ", "Humidity", this->humidity_);
+  LOG_SENSOR("  ", "Battery Level", this->battery_level_);
+}
+
+bool XiaomiCGDK2::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
+  if (device.address_uint64() != this->address_) {
+    ESP_LOGVV(TAG, "parse_device(): unknown MAC address.");
+    return false;
+  }
+  ESP_LOGV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str());
+
+  bool success = false;
+  for (auto &service_data : device.get_service_datas()) {
+    auto res = xiaomi_ble::parse_xiaomi_header(service_data);
+    if (!res.has_value()) {
+      continue;
+    }
+    if (res->is_duplicate) {
+      continue;
+    }
+    if (res->has_encryption &&
+        (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast<std::vector<uint8_t> &>(service_data.data), this->bindkey_,
+                                              this->address_)))) {
+      continue;
+    }
+    if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) {
+      continue;
+    }
+    if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) {
+      continue;
+    }
+    if (res->temperature.has_value() && this->temperature_ != nullptr)
+      this->temperature_->publish_state(*res->temperature);
+    if (res->humidity.has_value() && this->humidity_ != nullptr)
+      this->humidity_->publish_state(*res->humidity);
+    if (res->battery_level.has_value() && this->battery_level_ != nullptr)
+      this->battery_level_->publish_state(*res->battery_level);
+    success = true;
+  }
+
+  if (!success) {
+    return false;
+  }
+
+  return true;
+}
+
+void XiaomiCGDK2::set_bindkey(const std::string &bindkey) {
+  memset(bindkey_, 0, 16);
+  if (bindkey.size() != 32) {
+    return;
+  }
+  char temp[3] = {0};
+  for (int i = 0; i < 16; i++) {
+    strncpy(temp, &(bindkey.c_str()[i * 2]), 2);
+    bindkey_[i] = std::strtoul(temp, NULL, 16);
+  }
+}
+
+}  // namespace xiaomi_cgdk2
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h
new file mode 100644
index 0000000000..70f2ae9e2e
--- /dev/null
+++ b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+#include "esphome/components/xiaomi_ble/xiaomi_ble.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace xiaomi_cgdk2 {
+
+class XiaomiCGDK2 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
+ public:
+  void set_address(uint64_t address) { address_ = address; };
+  void set_bindkey(const std::string &bindkey);
+
+  bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
+  void dump_config() override;
+  float get_setup_priority() const override { return setup_priority::DATA; }
+  void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
+  void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
+  void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; }
+
+ protected:
+  uint64_t address_;
+  uint8_t bindkey_[16];
+  sensor::Sensor *temperature_{nullptr};
+  sensor::Sensor *humidity_{nullptr};
+  sensor::Sensor *battery_level_{nullptr};
+};
+
+}  // namespace xiaomi_cgdk2
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_cgg1/sensor.py b/esphome/components/xiaomi_cgg1/sensor.py
index cedf04d8a6..f26a7ae54e 100644
--- a/esphome/components/xiaomi_cgg1/sensor.py
+++ b/esphome/components/xiaomi_cgg1/sensor.py
@@ -12,6 +12,7 @@ from esphome.const import (
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
 )
@@ -31,13 +32,25 @@ CONFIG_SCHEMA = (
             cv.Optional(CONF_BINDKEY): cv.bind_key,
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_BATTERY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_BATTERY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -46,21 +59,21 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
     if CONF_BINDKEY in config:
         cg.add(var.set_bindkey(config[CONF_BINDKEY]))
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity(sens))
     if CONF_BATTERY_LEVEL in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
         cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xiaomi_gcls002/sensor.py b/esphome/components/xiaomi_gcls002/sensor.py
index b838371155..a5c702aa9d 100644
--- a/esphome/components/xiaomi_gcls002/sensor.py
+++ b/esphome/components/xiaomi_gcls002/sensor.py
@@ -9,6 +9,7 @@ from esphome.const import (
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
     ICON_WATER_PERCENT,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
     CONF_ID,
@@ -34,16 +35,32 @@ CONFIG_SCHEMA = (
             cv.GenerateID(): cv.declare_id(XiaomiGCLS002),
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_MOISTURE): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_WATER_PERCENT, 0, DEVICE_CLASS_EMPTY
+                UNIT_PERCENT,
+                ICON_WATER_PERCENT,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
-                UNIT_LUX, ICON_EMPTY, 0, DEVICE_CLASS_ILLUMINANCE
+                UNIT_LUX,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_ILLUMINANCE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_CONDUCTIVITY): sensor.sensor_schema(
-                UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0, DEVICE_CLASS_EMPTY
+                UNIT_MICROSIEMENS_PER_CENTIMETER,
+                ICON_FLOWER,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -52,22 +69,22 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
     if CONF_MOISTURE in config:
-        sens = yield sensor.new_sensor(config[CONF_MOISTURE])
+        sens = await sensor.new_sensor(config[CONF_MOISTURE])
         cg.add(var.set_moisture(sens))
     if CONF_ILLUMINANCE in config:
-        sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE])
+        sens = await sensor.new_sensor(config[CONF_ILLUMINANCE])
         cg.add(var.set_illuminance(sens))
     if CONF_CONDUCTIVITY in config:
-        sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY])
+        sens = await sensor.new_sensor(config[CONF_CONDUCTIVITY])
         cg.add(var.set_conductivity(sens))
diff --git a/esphome/components/xiaomi_hhccjcy01/sensor.py b/esphome/components/xiaomi_hhccjcy01/sensor.py
index f657ec9373..03289a6219 100644
--- a/esphome/components/xiaomi_hhccjcy01/sensor.py
+++ b/esphome/components/xiaomi_hhccjcy01/sensor.py
@@ -9,6 +9,7 @@ from esphome.const import (
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
     ICON_WATER_PERCENT,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
     CONF_ID,
@@ -36,19 +37,39 @@ CONFIG_SCHEMA = (
             cv.GenerateID(): cv.declare_id(XiaomiHHCCJCY01),
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_MOISTURE): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_WATER_PERCENT, 0, DEVICE_CLASS_EMPTY
+                UNIT_PERCENT,
+                ICON_WATER_PERCENT,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
-                UNIT_LUX, ICON_EMPTY, 0, DEVICE_CLASS_ILLUMINANCE
+                UNIT_LUX,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_ILLUMINANCE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_CONDUCTIVITY): sensor.sensor_schema(
-                UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0, DEVICE_CLASS_EMPTY
+                UNIT_MICROSIEMENS_PER_CENTIMETER,
+                ICON_FLOWER,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_BATTERY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_BATTERY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -57,25 +78,25 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
     if CONF_MOISTURE in config:
-        sens = yield sensor.new_sensor(config[CONF_MOISTURE])
+        sens = await sensor.new_sensor(config[CONF_MOISTURE])
         cg.add(var.set_moisture(sens))
     if CONF_ILLUMINANCE in config:
-        sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE])
+        sens = await sensor.new_sensor(config[CONF_ILLUMINANCE])
         cg.add(var.set_illuminance(sens))
     if CONF_CONDUCTIVITY in config:
-        sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY])
+        sens = await sensor.new_sensor(config[CONF_CONDUCTIVITY])
         cg.add(var.set_conductivity(sens))
     if CONF_BATTERY_LEVEL in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
         cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xiaomi_hhccpot002/sensor.py b/esphome/components/xiaomi_hhccpot002/sensor.py
index 820cda173d..8393de5e5a 100644
--- a/esphome/components/xiaomi_hhccpot002/sensor.py
+++ b/esphome/components/xiaomi_hhccpot002/sensor.py
@@ -4,6 +4,7 @@ from esphome.components import sensor, esp32_ble_tracker
 from esphome.const import (
     CONF_MAC_ADDRESS,
     DEVICE_CLASS_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_PERCENT,
     ICON_WATER_PERCENT,
     CONF_ID,
@@ -27,10 +28,18 @@ CONFIG_SCHEMA = (
             cv.GenerateID(): cv.declare_id(XiaomiHHCCPOT002),
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(CONF_MOISTURE): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_WATER_PERCENT, 0, DEVICE_CLASS_EMPTY
+                UNIT_PERCENT,
+                ICON_WATER_PERCENT,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_CONDUCTIVITY): sensor.sensor_schema(
-                UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0, DEVICE_CLASS_EMPTY
+                UNIT_MICROSIEMENS_PER_CENTIMETER,
+                ICON_FLOWER,
+                0,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -39,16 +48,16 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
 
     if CONF_MOISTURE in config:
-        sens = yield sensor.new_sensor(config[CONF_MOISTURE])
+        sens = await sensor.new_sensor(config[CONF_MOISTURE])
         cg.add(var.set_moisture(sens))
     if CONF_CONDUCTIVITY in config:
-        sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY])
+        sens = await sensor.new_sensor(config[CONF_CONDUCTIVITY])
         cg.add(var.set_conductivity(sens))
diff --git a/esphome/components/xiaomi_jqjcy01ym/sensor.py b/esphome/components/xiaomi_jqjcy01ym/sensor.py
index ce5e8e2b37..70036eb5d9 100644
--- a/esphome/components/xiaomi_jqjcy01ym/sensor.py
+++ b/esphome/components/xiaomi_jqjcy01ym/sensor.py
@@ -11,6 +11,7 @@ from esphome.const import (
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     UNIT_PERCENT,
     CONF_HUMIDITY,
@@ -33,19 +34,32 @@ CONFIG_SCHEMA = (
             cv.GenerateID(): cv.declare_id(XiaomiJQJCY01YM),
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema(
                 UNIT_MILLIGRAMS_PER_CUBIC_METER,
                 ICON_FLASK_OUTLINE,
                 2,
                 DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_BATTERY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_BATTERY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -54,22 +68,22 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity(sens))
     if CONF_FORMALDEHYDE in config:
-        sens = yield sensor.new_sensor(config[CONF_FORMALDEHYDE])
+        sens = await sensor.new_sensor(config[CONF_FORMALDEHYDE])
         cg.add(var.set_formaldehyde(sens))
     if CONF_BATTERY_LEVEL in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
         cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xiaomi_lywsd02/sensor.py b/esphome/components/xiaomi_lywsd02/sensor.py
index c17eb17a5f..ca55f28176 100644
--- a/esphome/components/xiaomi_lywsd02/sensor.py
+++ b/esphome/components/xiaomi_lywsd02/sensor.py
@@ -7,6 +7,7 @@ from esphome.const import (
     CONF_MAC_ADDRESS,
     CONF_TEMPERATURE,
     DEVICE_CLASS_TEMPERATURE,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     ICON_EMPTY,
     UNIT_PERCENT,
@@ -29,13 +30,25 @@ CONFIG_SCHEMA = (
             cv.GenerateID(): cv.declare_id(XiaomiLYWSD02),
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_BATTERY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_BATTERY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -44,19 +57,19 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity(sens))
     if CONF_BATTERY_LEVEL in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
         cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xiaomi_lywsd03mmc/sensor.py b/esphome/components/xiaomi_lywsd03mmc/sensor.py
index b9de3f0bcc..05b3798955 100644
--- a/esphome/components/xiaomi_lywsd03mmc/sensor.py
+++ b/esphome/components/xiaomi_lywsd03mmc/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_HUMIDITY,
     CONF_MAC_ADDRESS,
     CONF_TEMPERATURE,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     ICON_EMPTY,
     UNIT_PERCENT,
@@ -33,13 +34,25 @@ CONFIG_SCHEMA = (
             cv.Required(CONF_BINDKEY): cv.bind_key,
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_BATTERY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_BATTERY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -48,20 +61,20 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
     cg.add(var.set_bindkey(config[CONF_BINDKEY]))
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity(sens))
     if CONF_BATTERY_LEVEL in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
         cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xiaomi_lywsdcgq/sensor.py b/esphome/components/xiaomi_lywsdcgq/sensor.py
index a4a03a3fb0..82bb4c83fb 100644
--- a/esphome/components/xiaomi_lywsdcgq/sensor.py
+++ b/esphome/components/xiaomi_lywsdcgq/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_HUMIDITY,
     CONF_MAC_ADDRESS,
     CONF_TEMPERATURE,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     ICON_EMPTY,
     UNIT_PERCENT,
@@ -29,13 +30,25 @@ CONFIG_SCHEMA = (
             cv.GenerateID(): cv.declare_id(XiaomiLYWSDCGQ),
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_BATTERY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_BATTERY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -44,19 +57,19 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity(sens))
     if CONF_BATTERY_LEVEL in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
         cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xiaomi_mhoc401/sensor.py b/esphome/components/xiaomi_mhoc401/sensor.py
index ee0e06b3a8..5180bdbb89 100644
--- a/esphome/components/xiaomi_mhoc401/sensor.py
+++ b/esphome/components/xiaomi_mhoc401/sensor.py
@@ -6,6 +6,7 @@ from esphome.const import (
     CONF_HUMIDITY,
     CONF_MAC_ADDRESS,
     CONF_TEMPERATURE,
+    STATE_CLASS_MEASUREMENT,
     UNIT_CELSIUS,
     ICON_EMPTY,
     UNIT_PERCENT,
@@ -32,13 +33,25 @@ CONFIG_SCHEMA = (
             cv.Required(CONF_BINDKEY): cv.bind_key,
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-                UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+                UNIT_CELSIUS,
+                ICON_EMPTY,
+                1,
+                DEVICE_CLASS_TEMPERATURE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_HUMIDITY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_HUMIDITY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_BATTERY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_BATTERY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -47,20 +60,20 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
     cg.add(var.set_bindkey(config[CONF_BINDKEY]))
 
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature(sens))
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity(sens))
     if CONF_BATTERY_LEVEL in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
         cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xiaomi_miscale/sensor.py b/esphome/components/xiaomi_miscale/sensor.py
index cd225e4853..9fe76c0645 100644
--- a/esphome/components/xiaomi_miscale/sensor.py
+++ b/esphome/components/xiaomi_miscale/sensor.py
@@ -5,6 +5,7 @@ from esphome.const import (
     CONF_MAC_ADDRESS,
     CONF_ID,
     CONF_WEIGHT,
+    STATE_CLASS_MEASUREMENT,
     UNIT_KILOGRAM,
     ICON_SCALE_BATHROOM,
     DEVICE_CLASS_EMPTY,
@@ -23,7 +24,11 @@ CONFIG_SCHEMA = (
             cv.GenerateID(): cv.declare_id(XiaomiMiscale),
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(CONF_WEIGHT): sensor.sensor_schema(
-                UNIT_KILOGRAM, ICON_SCALE_BATHROOM, 2, DEVICE_CLASS_EMPTY
+                UNIT_KILOGRAM,
+                ICON_SCALE_BATHROOM,
+                2,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -32,13 +37,13 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
 
     if CONF_WEIGHT in config:
-        sens = yield sensor.new_sensor(config[CONF_WEIGHT])
+        sens = await sensor.new_sensor(config[CONF_WEIGHT])
         cg.add(var.set_weight(sens))
diff --git a/esphome/components/xiaomi_miscale2/sensor.py b/esphome/components/xiaomi_miscale2/sensor.py
index fa124e8860..9944098407 100644
--- a/esphome/components/xiaomi_miscale2/sensor.py
+++ b/esphome/components/xiaomi_miscale2/sensor.py
@@ -5,6 +5,7 @@ from esphome.const import (
     CONF_MAC_ADDRESS,
     CONF_ID,
     CONF_WEIGHT,
+    STATE_CLASS_MEASUREMENT,
     UNIT_KILOGRAM,
     ICON_SCALE_BATHROOM,
     UNIT_OHM,
@@ -26,10 +27,14 @@ CONFIG_SCHEMA = (
             cv.GenerateID(): cv.declare_id(XiaomiMiscale2),
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(CONF_WEIGHT): sensor.sensor_schema(
-                UNIT_KILOGRAM, ICON_SCALE_BATHROOM, 2, DEVICE_CLASS_EMPTY
+                UNIT_KILOGRAM,
+                ICON_SCALE_BATHROOM,
+                2,
+                DEVICE_CLASS_EMPTY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_IMPEDANCE): sensor.sensor_schema(
-                UNIT_OHM, ICON_OMEGA, 0, DEVICE_CLASS_EMPTY
+                UNIT_OHM, ICON_OMEGA, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
             ),
         }
     )
@@ -38,16 +43,16 @@ CONFIG_SCHEMA = (
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
 
     if CONF_WEIGHT in config:
-        sens = yield sensor.new_sensor(config[CONF_WEIGHT])
+        sens = await sensor.new_sensor(config[CONF_WEIGHT])
         cg.add(var.set_weight(sens))
     if CONF_IMPEDANCE in config:
-        sens = yield sensor.new_sensor(config[CONF_IMPEDANCE])
+        sens = await sensor.new_sensor(config[CONF_IMPEDANCE])
         cg.add(var.set_impedance(sens))
diff --git a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py
index 1b0ad03f1a..90b971c08a 100644
--- a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py
+++ b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py
@@ -12,6 +12,8 @@ from esphome.const import (
     DEVICE_CLASS_EMPTY,
     DEVICE_CLASS_ILLUMINANCE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_NONE,
     UNIT_PERCENT,
     CONF_IDLE_TIME,
     CONF_ILLUMINANCE,
@@ -41,13 +43,21 @@ CONFIG_SCHEMA = cv.All(
                 CONF_DEVICE_CLASS, default="motion"
             ): binary_sensor.device_class,
             cv.Optional(CONF_IDLE_TIME): sensor.sensor_schema(
-                UNIT_MINUTE, ICON_TIMELAPSE, 0, DEVICE_CLASS_EMPTY
+                UNIT_MINUTE, ICON_TIMELAPSE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
             ),
             cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_BATTERY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_BATTERY,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
-                UNIT_LUX, ICON_EMPTY, 0, DEVICE_CLASS_ILLUMINANCE
+                UNIT_LUX,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_ILLUMINANCE,
+                STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_LIGHT): binary_sensor.BINARY_SENSOR_SCHEMA.extend(
                 {
@@ -63,24 +73,24 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
-    yield binary_sensor.register_binary_sensor(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
+    await binary_sensor.register_binary_sensor(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
     cg.add(var.set_bindkey(config[CONF_BINDKEY]))
 
     if CONF_IDLE_TIME in config:
-        sens = yield sensor.new_sensor(config[CONF_IDLE_TIME])
+        sens = await sensor.new_sensor(config[CONF_IDLE_TIME])
         cg.add(var.set_idle_time(sens))
     if CONF_BATTERY_LEVEL in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
         cg.add(var.set_battery_level(sens))
     if CONF_ILLUMINANCE in config:
-        sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE])
+        sens = await sensor.new_sensor(config[CONF_ILLUMINANCE])
         cg.add(var.set_illuminance(sens))
     if CONF_LIGHT in config:
-        sens = yield binary_sensor.new_binary_sensor(config[CONF_LIGHT])
+        sens = await binary_sensor.new_binary_sensor(config[CONF_LIGHT])
         cg.add(var.set_light(sens))
diff --git a/esphome/components/xiaomi_mue4094rt/binary_sensor.py b/esphome/components/xiaomi_mue4094rt/binary_sensor.py
index 353e9aa3a6..5d19263c9c 100644
--- a/esphome/components/xiaomi_mue4094rt/binary_sensor.py
+++ b/esphome/components/xiaomi_mue4094rt/binary_sensor.py
@@ -33,11 +33,11 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
-    yield binary_sensor.register_binary_sensor(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
+    await binary_sensor.register_binary_sensor(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
     cg.add(var.set_time(config[CONF_TIMEOUT]))
diff --git a/esphome/components/xiaomi_wx08zm/binary_sensor.py b/esphome/components/xiaomi_wx08zm/binary_sensor.py
index c13085b5eb..90d4702da4 100644
--- a/esphome/components/xiaomi_wx08zm/binary_sensor.py
+++ b/esphome/components/xiaomi_wx08zm/binary_sensor.py
@@ -8,6 +8,7 @@ from esphome.const import (
     DEVICE_CLASS_BATTERY,
     DEVICE_CLASS_EMPTY,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_PERCENT,
     ICON_BUG,
     CONF_ID,
@@ -31,10 +32,14 @@ CONFIG_SCHEMA = cv.All(
             cv.GenerateID(): cv.declare_id(XiaomiWX08ZM),
             cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(CONF_TABLET): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_BUG, 0, DEVICE_CLASS_EMPTY
+                UNIT_PERCENT, ICON_BUG, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
             ),
             cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
-                UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_BATTERY
+                UNIT_PERCENT,
+                ICON_EMPTY,
+                0,
+                DEVICE_CLASS_BATTERY,
+                STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -43,17 +48,17 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield esp32_ble_tracker.register_ble_device(var, config)
-    yield binary_sensor.register_binary_sensor(var, config)
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
+    await binary_sensor.register_binary_sensor(var, config)
 
     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
 
     if CONF_TABLET in config:
-        sens = yield sensor.new_sensor(config[CONF_TABLET])
+        sens = await sensor.new_sensor(config[CONF_TABLET])
         cg.add(var.set_tablet(sens))
     if CONF_BATTERY_LEVEL in config:
-        sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL])
+        sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL])
         cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xpt2046/__init__.py b/esphome/components/xpt2046/__init__.py
new file mode 100644
index 0000000000..3de89a6425
--- /dev/null
+++ b/esphome/components/xpt2046/__init__.py
@@ -0,0 +1,129 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import automation
+from esphome import pins
+from esphome.components import spi
+from esphome.const import CONF_ID, CONF_ON_STATE, CONF_THRESHOLD, CONF_TRIGGER_ID
+
+CODEOWNERS = ["@numo68"]
+AUTO_LOAD = ["binary_sensor"]
+DEPENDENCIES = ["spi"]
+MULTI_CONF = True
+
+CONF_REPORT_INTERVAL = "report_interval"
+CONF_CALIBRATION_X_MIN = "calibration_x_min"
+CONF_CALIBRATION_X_MAX = "calibration_x_max"
+CONF_CALIBRATION_Y_MIN = "calibration_y_min"
+CONF_CALIBRATION_Y_MAX = "calibration_y_max"
+CONF_DIMENSION_X = "dimension_x"
+CONF_DIMENSION_Y = "dimension_y"
+CONF_SWAP_X_Y = "swap_x_y"
+CONF_IRQ_PIN = "irq_pin"
+
+xpt2046_ns = cg.esphome_ns.namespace("xpt2046")
+CONF_XPT2046_ID = "xpt2046_id"
+
+XPT2046Component = xpt2046_ns.class_(
+    "XPT2046Component", cg.PollingComponent, spi.SPIDevice
+)
+
+XPT2046OnStateTrigger = xpt2046_ns.class_(
+    "XPT2046OnStateTrigger", automation.Trigger.template(cg.int_, cg.int_, cg.bool_)
+)
+
+
+def validate_xpt2046(config):
+    if (
+        abs(
+            cv.int_(config[CONF_CALIBRATION_X_MAX])
+            - cv.int_(config[CONF_CALIBRATION_X_MIN])
+        )
+        < 1000
+    ):
+        raise cv.Invalid("Calibration X values difference < 1000")
+
+    if (
+        abs(
+            cv.int_(config[CONF_CALIBRATION_Y_MAX])
+            - cv.int_(config[CONF_CALIBRATION_Y_MIN])
+        )
+        < 1000
+    ):
+        raise cv.Invalid("Calibration Y values difference < 1000")
+
+    return config
+
+
+def report_interval(value):
+    if value == "never":
+        return 4294967295  # uint32_t max
+    return cv.positive_time_period_milliseconds(value)
+
+
+CONFIG_SCHEMA = cv.All(
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(XPT2046Component),
+            cv.Optional(CONF_IRQ_PIN): pins.gpio_input_pin_schema,
+            cv.Optional(CONF_CALIBRATION_X_MIN, default=0): cv.int_range(
+                min=0, max=4095
+            ),
+            cv.Optional(CONF_CALIBRATION_X_MAX, default=4095): cv.int_range(
+                min=0, max=4095
+            ),
+            cv.Optional(CONF_CALIBRATION_Y_MIN, default=0): cv.int_range(
+                min=0, max=4095
+            ),
+            cv.Optional(CONF_CALIBRATION_Y_MAX, default=4095): cv.int_range(
+                min=0, max=4095
+            ),
+            cv.Optional(CONF_DIMENSION_X, default=100): cv.positive_not_null_int,
+            cv.Optional(CONF_DIMENSION_Y, default=100): cv.positive_not_null_int,
+            cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095),
+            cv.Optional(CONF_REPORT_INTERVAL, default="never"): report_interval,
+            cv.Optional(CONF_SWAP_X_Y, default=False): cv.boolean,
+            cv.Optional(CONF_ON_STATE): automation.validate_automation(
+                {
+                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
+                        XPT2046OnStateTrigger
+                    ),
+                }
+            ),
+        }
+    )
+    .extend(cv.polling_component_schema("50ms"))
+    .extend(spi.spi_device_schema()),
+    validate_xpt2046,
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await spi.register_spi_device(var, config)
+
+    cg.add(var.set_threshold(config[CONF_THRESHOLD]))
+    cg.add(var.set_report_interval(config[CONF_REPORT_INTERVAL]))
+    cg.add(var.set_dimensions(config[CONF_DIMENSION_X], config[CONF_DIMENSION_Y]))
+    cg.add(
+        var.set_calibration(
+            config[CONF_CALIBRATION_X_MIN],
+            config[CONF_CALIBRATION_X_MAX],
+            config[CONF_CALIBRATION_Y_MIN],
+            config[CONF_CALIBRATION_Y_MAX],
+        )
+    )
+
+    if CONF_SWAP_X_Y in config:
+        cg.add(var.set_swap_x_y(config[CONF_SWAP_X_Y]))
+
+    if CONF_IRQ_PIN in config:
+        pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN])
+        cg.add(var.set_irq_pin(pin))
+
+    for conf in config.get(CONF_ON_STATE, []):
+        await automation.build_automation(
+            var.get_on_state_trigger(),
+            [(cg.int_, "x"), (cg.int_, "y"), (cg.bool_, "touched")],
+            conf,
+        )
diff --git a/esphome/components/xpt2046/binary_sensor.py b/esphome/components/xpt2046/binary_sensor.py
new file mode 100644
index 0000000000..6959d6c342
--- /dev/null
+++ b/esphome/components/xpt2046/binary_sensor.py
@@ -0,0 +1,57 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import binary_sensor
+from esphome.const import CONF_ID
+from . import (
+    xpt2046_ns,
+    XPT2046Component,
+    CONF_XPT2046_ID,
+)
+
+CONF_X_MIN = "x_min"
+CONF_X_MAX = "x_max"
+CONF_Y_MIN = "y_min"
+CONF_Y_MAX = "y_max"
+
+DEPENDENCIES = ["xpt2046"]
+XPT2046Button = xpt2046_ns.class_("XPT2046Button", binary_sensor.BinarySensor)
+
+
+def validate_xpt2046_button(config):
+    if cv.int_(config[CONF_X_MAX]) < cv.int_(config[CONF_X_MIN]) or cv.int_(
+        config[CONF_Y_MAX]
+    ) < cv.int_(config[CONF_Y_MIN]):
+        raise cv.Invalid("x_max is less than x_min or y_max is less than y_min")
+
+    return config
+
+
+CONFIG_SCHEMA = cv.All(
+    binary_sensor.BINARY_SENSOR_SCHEMA.extend(
+        {
+            cv.GenerateID(): cv.declare_id(XPT2046Button),
+            cv.GenerateID(CONF_XPT2046_ID): cv.use_id(XPT2046Component),
+            cv.Required(CONF_X_MIN): cv.int_range(min=0, max=4095),
+            cv.Required(CONF_X_MAX): cv.int_range(min=0, max=4095),
+            cv.Required(CONF_Y_MIN): cv.int_range(min=0, max=4095),
+            cv.Required(CONF_Y_MAX): cv.int_range(min=0, max=4095),
+        }
+    ),
+    validate_xpt2046_button,
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await binary_sensor.register_binary_sensor(var, config)
+    hub = await cg.get_variable(config[CONF_XPT2046_ID])
+    cg.add(
+        var.set_area(
+            config[CONF_X_MIN],
+            config[CONF_X_MAX],
+            config[CONF_Y_MIN],
+            config[CONF_Y_MAX],
+        )
+    )
+
+    cg.add(hub.register_button(var))
diff --git a/esphome/components/xpt2046/xpt2046.cpp b/esphome/components/xpt2046/xpt2046.cpp
new file mode 100644
index 0000000000..3e22e82859
--- /dev/null
+++ b/esphome/components/xpt2046/xpt2046.cpp
@@ -0,0 +1,217 @@
+#include "xpt2046.h"
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+
+#include <algorithm>
+
+namespace esphome {
+namespace xpt2046 {
+
+static const char *TAG = "xpt2046";
+
+void XPT2046Component::setup() {
+  if (this->irq_pin_ != nullptr) {
+    // The pin reports a touch with a falling edge. Unfortunately the pin goes also changes state
+    // while the channels are read and wiring it as an interrupt is not straightforward and would
+    // need careful masking. A GPIO poll is cheap so we'll just use that.
+    this->irq_pin_->setup();  // INPUT
+  }
+  spi_setup();
+  read_adc_(0xD0);  // ADC powerdown, enable PENIRQ pin
+}
+
+void XPT2046Component::loop() {
+  if (this->irq_pin_ != nullptr) {
+    // Force immediate update if a falling edge (= touched is seen) Ignore if still active
+    // (that would mean that we missed the release because of a too long update interval)
+    bool val = this->irq_pin_->digital_read();
+    if (!val && this->last_irq_ && !this->touched) {
+      ESP_LOGD(TAG, "Falling penirq edge, forcing update");
+      update();
+    }
+    this->last_irq_ = val;
+  }
+}
+
+void XPT2046Component::update() {
+  int16_t data[6];
+  bool touch = false;
+  unsigned long now = millis();
+
+  this->z_raw = 0;
+
+  // In case the penirq pin is present only do the SPI transaction if it reports a touch (is low).
+  // The touch has to be also confirmed with checking the pressure over threshold
+  if (this->irq_pin_ == nullptr || !this->irq_pin_->digital_read()) {
+    enable();
+
+    int16_t z1 = read_adc_(0xB1 /* Z1 */);
+    int16_t z2 = read_adc_(0xC1 /* Z2 */);
+
+    this->z_raw = z1 + 4095 - z2;
+
+    touch = (this->z_raw >= this->threshold_);
+    if (touch) {
+      read_adc_(0x91 /* Y */);  // dummy Y measure, 1st is always noisy
+      data[0] = read_adc_(0xD1 /* X */);
+      data[1] = read_adc_(0x91 /* Y */);  // make 3 x-y measurements
+      data[2] = read_adc_(0xD1 /* X */);
+      data[3] = read_adc_(0x91 /* Y */);
+      data[4] = read_adc_(0xD1 /* X */);
+    }
+
+    data[5] = read_adc_(0x90 /* Y */);  // Last Y touch power down
+
+    disable();
+  }
+
+  if (touch) {
+    this->x_raw = best_two_avg(data[0], data[2], data[4]);
+    this->y_raw = best_two_avg(data[1], data[3], data[5]);
+  } else {
+    this->x_raw = this->y_raw = 0;
+  }
+
+  ESP_LOGV(TAG, "Update [x, y] = [%d, %d], z = %d%s", this->x_raw, this->y_raw, this->z_raw, (touch ? " touched" : ""));
+
+  if (touch) {
+    // Normalize raw data according to calibration min and max
+
+    int16_t x_val = normalize(this->x_raw, this->x_raw_min_, this->x_raw_max_);
+    int16_t y_val = normalize(this->y_raw, this->y_raw_min_, this->y_raw_max_);
+
+    if (this->swap_x_y_) {
+      std::swap(x_val, y_val);
+    }
+
+    if (this->invert_x_) {
+      x_val = 0x7fff - x_val;
+    }
+
+    if (this->invert_y_) {
+      y_val = 0x7fff - y_val;
+    }
+
+    x_val = (int16_t)((int) x_val * this->x_dim_ / 0x7fff);
+    y_val = (int16_t)((int) y_val * this->y_dim_ / 0x7fff);
+
+    if (!this->touched || (now - this->last_pos_ms_) >= this->report_millis_) {
+      ESP_LOGD(TAG, "Raw [x, y] = [%d, %d], transformed = [%d, %d]", this->x_raw, this->y_raw, x_val, y_val);
+
+      this->x = x_val;
+      this->y = y_val;
+      this->touched = true;
+      this->last_pos_ms_ = now;
+
+      this->on_state_trigger_->process(this->x, this->y, true);
+      for (auto *button : this->buttons_)
+        button->touch(this->x, this->y);
+    }
+  } else {
+    if (this->touched) {
+      ESP_LOGD(TAG, "Released [%d, %d]", this->x, this->y);
+
+      this->touched = false;
+
+      this->on_state_trigger_->process(this->x, this->y, false);
+      for (auto *button : this->buttons_)
+        button->release();
+    }
+  }
+}
+
+void XPT2046Component::set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) {
+  this->x_raw_min_ = std::min(x_min, x_max);
+  this->x_raw_max_ = std::max(x_min, x_max);
+  this->y_raw_min_ = std::min(y_min, y_max);
+  this->y_raw_max_ = std::max(y_min, y_max);
+  this->invert_x_ = (x_min > x_max);
+  this->invert_y_ = (y_min > y_max);
+}
+
+void XPT2046Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "XPT2046:");
+
+  LOG_PIN("  IRQ Pin: ", this->irq_pin_);
+  ESP_LOGCONFIG(TAG, "  X min: %d", this->x_raw_min_);
+  ESP_LOGCONFIG(TAG, "  X max: %d", this->x_raw_max_);
+  ESP_LOGCONFIG(TAG, "  Y min: %d", this->y_raw_min_);
+  ESP_LOGCONFIG(TAG, "  Y max: %d", this->y_raw_max_);
+  ESP_LOGCONFIG(TAG, "  X dim: %d", this->x_dim_);
+  ESP_LOGCONFIG(TAG, "  Y dim: %d", this->y_dim_);
+  if (this->swap_x_y_) {
+    ESP_LOGCONFIG(TAG, "  Swap X/Y");
+  }
+  ESP_LOGCONFIG(TAG, "  threshold: %d", this->threshold_);
+  ESP_LOGCONFIG(TAG, "  Report interval: %u", this->report_millis_);
+
+  LOG_UPDATE_INTERVAL(this);
+}
+
+float XPT2046Component::get_setup_priority() const { return setup_priority::DATA; }
+
+int16_t XPT2046Component::best_two_avg(int16_t x, int16_t y, int16_t z) {
+  int16_t da, db, dc;
+  int16_t reta = 0;
+
+  da = (x > y) ? x - y : y - x;
+  db = (x > z) ? x - z : z - x;
+  dc = (z > y) ? z - y : y - z;
+
+  if (da <= db && da <= dc) {
+    reta = (x + y) >> 1;
+  } else if (db <= da && db <= dc) {
+    reta = (x + z) >> 1;
+  } else {
+    reta = (y + z) >> 1;
+  }
+
+  return reta;
+}
+
+int16_t XPT2046Component::normalize(int16_t val, int16_t min_val, int16_t max_val) {
+  int16_t ret;
+
+  if (val <= min_val) {
+    ret = 0;
+  } else if (val >= max_val) {
+    ret = 0x7fff;
+  } else {
+    ret = (int16_t)((int) 0x7fff * (val - min_val) / (max_val - min_val));
+  }
+
+  return ret;
+}
+
+int16_t XPT2046Component::read_adc_(uint8_t ctrl) {
+  uint8_t data[2];
+
+  write_byte(ctrl);
+  data[0] = read_byte();
+  data[1] = read_byte();
+
+  return ((data[0] << 8) | data[1]) >> 3;
+}
+
+void XPT2046OnStateTrigger::process(int x, int y, bool touched) { this->trigger(x, y, touched); }
+
+void XPT2046Button::touch(int16_t x, int16_t y) {
+  bool touched = (x >= this->x_min_ && x <= this->x_max_ && y >= this->y_min_ && y <= this->y_max_);
+
+  if (touched) {
+    this->publish_state(true);
+    this->state_ = true;
+  } else {
+    release();
+  }
+}
+
+void XPT2046Button::release() {
+  if (this->state_) {
+    this->publish_state(false);
+    this->state_ = false;
+  }
+}
+
+}  // namespace xpt2046
+}  // namespace esphome
diff --git a/esphome/components/xpt2046/xpt2046.h b/esphome/components/xpt2046/xpt2046.h
new file mode 100644
index 0000000000..7fd80c3228
--- /dev/null
+++ b/esphome/components/xpt2046/xpt2046.h
@@ -0,0 +1,124 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/automation.h"
+#include "esphome/components/spi/spi.h"
+#include "esphome/components/binary_sensor/binary_sensor.h"
+
+namespace esphome {
+namespace xpt2046 {
+
+class XPT2046OnStateTrigger : public Trigger<int, int, bool> {
+ public:
+  void process(int x, int y, bool touched);
+};
+
+class XPT2046Button : public binary_sensor::BinarySensor {
+ public:
+  /// Set the touch screen area where the button will detect the touch.
+  void set_area(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) {
+    this->x_min_ = x_min;
+    this->x_max_ = x_max;
+    this->y_min_ = y_min;
+    this->y_max_ = y_max;
+  }
+
+  void touch(int16_t x, int16_t y);
+  void release();
+
+ protected:
+  int16_t x_min_, x_max_, y_min_, y_max_;
+  bool state_{false};
+};
+
+class XPT2046Component : public PollingComponent,
+                         public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
+                                               spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_2MHZ> {
+ public:
+  /// Set the logical touch screen dimensions.
+  void set_dimensions(int16_t x, int16_t y) {
+    this->x_dim_ = x;
+    this->y_dim_ = y;
+  }
+  /// Set the coordinates for the touch screen edges.
+  void set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max);
+  /// If true the x and y axes will be swapped
+  void set_swap_x_y(bool val) { this->swap_x_y_ = val; }
+
+  /// Set the interval to report the touch point perodically.
+  void set_report_interval(uint32_t interval) { this->report_millis_ = interval; }
+  /// Set the threshold for the touch detection.
+  void set_threshold(int16_t threshold) { this->threshold_ = threshold; }
+  /// Set the pin used to detect the touch.
+  void set_irq_pin(GPIOPin *pin) { this->irq_pin_ = pin; }
+  /// Get an access to the on_state automation trigger
+  XPT2046OnStateTrigger *get_on_state_trigger() const { return this->on_state_trigger_; }
+  /// Register a virtual button to the component.
+  void register_button(XPT2046Button *button) { this->buttons_.push_back(button); }
+
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override;
+
+  /** Detect the touch if the irq pin is specified.
+   *
+   * If the touch is detected and the component does not already know about it
+   * the update() is called immediately. If the irq pin is not specified
+   * the loop() is a no-op.
+   */
+  void loop() override;
+
+  /** Read and process the values from the hardware.
+   *
+   * Read the raw x, y and touch pressure values from the chip, detect the touch,
+   * and if touched, transform to the user x and y coordinates. If the state has
+   * changed or if the value should be reported again due to the
+   * report interval, run the action and inform the virtual buttons.
+   */
+  void update() override;
+
+  /**@{*/
+  /** Coordinates of the touch position.
+   *
+   * The values are set immediately before the on_state action with touched == true
+   * is triggered. The action with touched == false sends the coordinates of the last
+   * reported touch.
+   */
+  int16_t x{0}, y{0};
+  /**@}*/
+
+  /// True if the component currently detects the touch
+  bool touched{false};
+
+  /**@{*/
+  /** Raw sensor values of the coordinates and the pressure.
+   *
+   * The values are set each time the update() method is called.
+   */
+  int16_t x_raw{0}, y_raw{0}, z_raw{0};
+  /**@}*/
+
+ protected:
+  static int16_t best_two_avg(int16_t x, int16_t y, int16_t z);
+  static int16_t normalize(int16_t val, int16_t min_val, int16_t max_val);
+
+  int16_t read_adc_(uint8_t ctrl);
+
+  int16_t threshold_;
+  int16_t x_raw_min_, x_raw_max_, y_raw_min_, y_raw_max_;
+  int16_t x_dim_, y_dim_;
+  bool invert_x_, invert_y_;
+  bool swap_x_y_;
+
+  uint32_t report_millis_;
+  unsigned long last_pos_ms_{0};
+
+  GPIOPin *irq_pin_{nullptr};
+  bool last_irq_{true};
+
+  XPT2046OnStateTrigger *on_state_trigger_{new XPT2046OnStateTrigger()};
+  std::vector<XPT2046Button *> buttons_{};
+};
+
+}  // namespace xpt2046
+}  // namespace esphome
diff --git a/esphome/components/yashima/climate.py b/esphome/components/yashima/climate.py
index 2965d4cdb8..8cafd468ac 100644
--- a/esphome/components/yashima/climate.py
+++ b/esphome/components/yashima/climate.py
@@ -24,16 +24,16 @@ CONFIG_SCHEMA = cv.All(
 )
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
-    yield climate.register_climate(var, config)
+    await cg.register_component(var, config)
+    await climate.register_climate(var, config)
 
     cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
     cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
     if CONF_SENSOR in config:
-        sens = yield cg.get_variable(config[CONF_SENSOR])
+        sens = await cg.get_variable(config[CONF_SENSOR])
         cg.add(var.set_sensor(sens))
 
-    transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID])
+    transmitter = await cg.get_variable(config[CONF_TRANSMITTER_ID])
     cg.add(var.set_transmitter(transmitter))
diff --git a/esphome/components/zyaura/sensor.py b/esphome/components/zyaura/sensor.py
index e9035ce106..5f9a5e3add 100644
--- a/esphome/components/zyaura/sensor.py
+++ b/esphome/components/zyaura/sensor.py
@@ -13,6 +13,7 @@ from esphome.const import (
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
     ICON_EMPTY,
+    STATE_CLASS_MEASUREMENT,
     UNIT_PARTS_PER_MILLION,
     UNIT_CELSIUS,
     UNIT_PERCENT,
@@ -33,33 +34,41 @@ CONFIG_SCHEMA = cv.Schema(
             pins.internal_gpio_input_pin_schema, pins.validate_has_interrupt
         ),
         cv.Optional(CONF_CO2): sensor.sensor_schema(
-            UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, 0, DEVICE_CLASS_EMPTY
+            UNIT_PARTS_PER_MILLION,
+            ICON_MOLECULE_CO2,
+            0,
+            DEVICE_CLASS_EMPTY,
+            STATE_CLASS_MEASUREMENT,
         ),
         cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
-            UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE
+            UNIT_CELSIUS,
+            ICON_EMPTY,
+            1,
+            DEVICE_CLASS_TEMPERATURE,
+            STATE_CLASS_MEASUREMENT,
         ),
         cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
-            UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY
+            UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY, STATE_CLASS_MEASUREMENT
         ),
     }
 ).extend(cv.polling_component_schema("60s"))
 
 
-def to_code(config):
+async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield cg.register_component(var, config)
+    await cg.register_component(var, config)
 
-    pin_clock = yield gpio_pin_expression(config[CONF_CLOCK_PIN])
+    pin_clock = await gpio_pin_expression(config[CONF_CLOCK_PIN])
     cg.add(var.set_pin_clock(pin_clock))
-    pin_data = yield gpio_pin_expression(config[CONF_DATA_PIN])
+    pin_data = await gpio_pin_expression(config[CONF_DATA_PIN])
     cg.add(var.set_pin_data(pin_data))
 
     if CONF_CO2 in config:
-        sens = yield sensor.new_sensor(config[CONF_CO2])
+        sens = await sensor.new_sensor(config[CONF_CO2])
         cg.add(var.set_co2_sensor(sens))
     if CONF_TEMPERATURE in config:
-        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature_sensor(sens))
     if CONF_HUMIDITY in config:
-        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
         cg.add(var.set_humidity_sensor(sens))
diff --git a/esphome/config.py b/esphome/config.py
index a1fc07a21f..fcd2fac90f 100644
--- a/esphome/config.py
+++ b/esphome/config.py
@@ -63,6 +63,8 @@ class Config(OrderedDict):
         # The values will be the paths to all "domain", for example (['logger'], 'logger')
         # or (['sensor', 'ultrasonic'], 'sensor.ultrasonic')
         self.output_paths = []  # type: List[Tuple[ConfigPath, str]]
+        # A list of components ids with the config path
+        self.declare_ids = []  # type: List[Tuple[core.ID, ConfigPath]]
 
     def add_error(self, error):
         # type: (vol.Invalid) -> None
@@ -161,6 +163,12 @@ class Config(OrderedDict):
             part.append(item_index)
         return part
 
+    def get_config_by_id(self, id):
+        for declared_id, path in self.declare_ids:
+            if declared_id.id == str(id):
+                return self.get_nested_item(path[:-1])
+        return None
+
 
 def iter_ids(config, path=None):
     path = path or []
@@ -181,7 +189,7 @@ def do_id_pass(result):  # type: (Config) -> None
     from esphome.cpp_generator import MockObjClass
     from esphome.cpp_types import Component
 
-    declare_ids = []  # type: List[Tuple[core.ID, ConfigPath]]
+    declare_ids = result.declare_ids  # type: List[Tuple[core.ID, ConfigPath]]
     searching_ids = []  # type: List[Tuple[core.ID, ConfigPath]]
     for id, path in iter_ids(result):
         if id.is_declaration:
@@ -546,6 +554,19 @@ def validate_config(config, command_line_substitutions):
         # Only parse IDs if no validation error. Otherwise
         # user gets confusing messages
         do_id_pass(result)
+
+    # 7. Final validation
+    if not result.errors:
+        # Inter - components validation
+        for path, conf, comp in validate_queue:
+            if comp.config_schema is None:
+                continue
+            if callable(comp.validate):
+                try:
+                    comp.validate(result, result.get_nested_item(path))
+                except ValueError as err:
+                    result.add_str_error(err, path)
+
     return result
 
 
diff --git a/esphome/config_validation.py b/esphome/config_validation.py
index 24c86e6713..2cdb6b0b76 100644
--- a/esphome/config_validation.py
+++ b/esphome/config_validation.py
@@ -46,7 +46,13 @@ from esphome.core import (
     TimePeriodMinutes,
 )
 from esphome.helpers import list_starts_with, add_class_to_obj
-from esphome.jsonschema import jschema_composite, jschema_registry, jschema_typed
+from esphome.jsonschema import (
+    jschema_composite,
+    jschema_extractor,
+    jschema_registry,
+    jschema_typed,
+)
+
 from esphome.voluptuous_schema import _Schema
 from esphome.yaml_util import make_data_base
 
@@ -1121,7 +1127,12 @@ def one_of(*values, **kwargs):
     if kwargs:
         raise ValueError
 
+    @jschema_extractor("one_of")
     def validator(value):
+        # pylint: disable=comparison-with-callable
+        if value == jschema_extractor:
+            return values
+
         if string_:
             value = string(value)
             value = value.replace(" ", space)
@@ -1161,7 +1172,12 @@ def enum(mapping, **kwargs):
     assert isinstance(mapping, dict)
     one_of_validator = one_of(*mapping, **kwargs)
 
+    @jschema_extractor("enum")
     def validator(value):
+        # pylint: disable=comparison-with-callable
+        if value == jschema_extractor:
+            return mapping
+
         value = one_of_validator(value)
         value = add_class_to_obj(value, core.EnumValue)
         value.enum_value = mapping[value]
@@ -1347,15 +1363,17 @@ def extract_keys(schema):
 def typed_schema(schemas, **kwargs):
     """Create a schema that has a key to distinguish between schemas"""
     key = kwargs.pop("key", CONF_TYPE)
+    default_schema_option = kwargs.pop("default_type", None)
     key_validator = one_of(*schemas, **kwargs)
 
     def validator(value):
         if not isinstance(value, dict):
             raise Invalid("Value must be dict")
-        if key not in value:
-            raise Invalid("type not specified!")
         value = value.copy()
-        key_v = key_validator(value.pop(key))
+        schema_option = value.pop(key, default_schema_option)
+        if schema_option is None:
+            raise Invalid(key + " not specified!")
+        key_v = key_validator(schema_option)
         value = schemas[key_v](value)
         value[key] = key_v
         return value
diff --git a/esphome/const.py b/esphome/const.py
index fdecf0ecb7..159a74adc2 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,8 +1,8 @@
 """Constants used by esphome."""
 
 MAJOR_VERSION = 1
-MINOR_VERSION = 18
-PATCH_VERSION = "0b4"
+MINOR_VERSION = 19
+PATCH_VERSION = "0b1"
 __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
 __version__ = f"{__short_version__}.{PATCH_VERSION}"
 
@@ -55,17 +55,20 @@ CONF_ACCELERATION_Z = "acceleration_z"
 CONF_ACCURACY = "accuracy"
 CONF_ACCURACY_DECIMALS = "accuracy_decimals"
 CONF_ACTION_ID = "action_id"
+CONF_ACTIVE_POWER = "active_power"
 CONF_ADDRESS = "address"
 CONF_ADDRESSABLE_LIGHT_ID = "addressable_light_id"
 CONF_ALPHA = "alpha"
 CONF_ALTITUDE = "altitude"
 CONF_AND = "and"
 CONF_AP = "ap"
+CONF_APPARENT_POWER = "apparent_power"
 CONF_ARDUINO_VERSION = "arduino_version"
 CONF_ARGS = "args"
 CONF_ASSUMED_STATE = "assumed_state"
 CONF_AT = "at"
 CONF_ATTENUATION = "attenuation"
+CONF_ATTRIBUTE = "attribute"
 CONF_AUTH = "auth"
 CONF_AUTO_MODE = "auto_mode"
 CONF_AUTOMATION_ID = "automation_id"
@@ -142,6 +145,10 @@ CONF_CSS_URL = "css_url"
 CONF_CURRENT = "current"
 CONF_CURRENT_OPERATION = "current_operation"
 CONF_CURRENT_RESISTOR = "current_resistor"
+CONF_CUSTOM_FAN_MODE = "custom_fan_mode"
+CONF_CUSTOM_FAN_MODES = "custom_fan_modes"
+CONF_CUSTOM_PRESET = "custom_preset"
+CONF_CUSTOM_PRESETS = "custom_presets"
 CONF_DALLAS_ID = "dallas_id"
 CONF_DATA = "data"
 CONF_DATA_PIN = "data_pin"
@@ -193,6 +200,8 @@ CONF_ESPHOME = "esphome"
 CONF_ETHERNET = "ethernet"
 CONF_EVENT = "event"
 CONF_EXPIRE_AFTER = "expire_after"
+CONF_EXPORT_ACTIVE_ENERGY = "export_active_energy"
+CONF_EXPORT_REACTIVE_ENERGY = "export_reactive_energy"
 CONF_EXTERNAL_COMPONENTS = "external_components"
 CONF_EXTERNAL_VCC = "external_vcc"
 CONF_FALLING_EDGE = "falling_edge"
@@ -217,6 +226,7 @@ CONF_FILTERS = "filters"
 CONF_FINGER_ID = "finger_id"
 CONF_FINGERPRINT_COUNT = "fingerprint_count"
 CONF_FLASH_LENGTH = "flash_length"
+CONF_FLOW_CONTROL_PIN = "flow_control_pin"
 CONF_FOR = "for"
 CONF_FORCE_UPDATE = "force_update"
 CONF_FORMALDEHYDE = "formaldehyde"
@@ -260,6 +270,8 @@ CONF_IF = "if"
 CONF_IIR_FILTER = "iir_filter"
 CONF_ILLUMINANCE = "illuminance"
 CONF_IMPEDANCE = "impedance"
+CONF_IMPORT_ACTIVE_ENERGY = "import_active_energy"
+CONF_IMPORT_REACTIVE_ENERGY = "import_reactive_energy"
 CONF_INCLUDES = "includes"
 CONF_INDEX = "index"
 CONF_INDOOR = "indoor"
@@ -379,6 +391,7 @@ CONF_ON_RELEASE = "on_release"
 CONF_ON_SHUTDOWN = "on_shutdown"
 CONF_ON_STATE = "on_state"
 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_TURN_OFF = "on_turn_off"
@@ -411,6 +424,7 @@ CONF_PAYLOAD = "payload"
 CONF_PAYLOAD_AVAILABLE = "payload_available"
 CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available"
 CONF_PERIOD = "period"
+CONF_PHASE_ANGLE = "phase_angle"
 CONF_PHASE_BALANCER = "phase_balancer"
 CONF_PIN = "pin"
 CONF_PIN_A = "pin_a"
@@ -439,8 +453,13 @@ CONF_POWER_FACTOR = "power_factor"
 CONF_POWER_ON_VALUE = "power_on_value"
 CONF_POWER_SAVE_MODE = "power_save_mode"
 CONF_POWER_SUPPLY = "power_supply"
+CONF_PRESET = "preset"
+CONF_PRESET_BOOST = "preset_boost"
+CONF_PRESET_ECO = "preset_eco"
+CONF_PRESET_SLEEP = "preset_sleep"
 CONF_PRESSURE = "pressure"
 CONF_PRIORITY = "priority"
+CONF_PROJECT = "project"
 CONF_PROTOCOL = "protocol"
 CONF_PULL_MODE = "pull_mode"
 CONF_PULSE_LENGTH = "pulse_length"
@@ -453,6 +472,7 @@ CONF_RATE = "rate"
 CONF_RAW = "raw"
 CONF_RC_CODE_1 = "rc_code_1"
 CONF_RC_CODE_2 = "rc_code_2"
+CONF_REACTIVE_POWER = "reactive_power"
 CONF_REBOOT_TIMEOUT = "reboot_timeout"
 CONF_RECEIVE_TIMEOUT = "receive_timeout"
 CONF_RED = "red"
@@ -469,6 +489,7 @@ CONF_RESTORE_STATE = "restore_state"
 CONF_RESTORE_VALUE = "restore_value"
 CONF_RETAIN = "retain"
 CONF_REVERSE_ACTIVE_ENERGY = "reverse_active_energy"
+CONF_REVERSED = "reversed"
 CONF_RGB_ORDER = "rgb_order"
 CONF_RGBW = "rgbw"
 CONF_RISING_EDGE = "rising_edge"
@@ -526,6 +547,7 @@ CONF_SPIKE_REJECTION = "spike_rejection"
 CONF_SSID = "ssid"
 CONF_SSL_FINGERPRINTS = "ssl_fingerprints"
 CONF_STATE = "state"
+CONF_STATE_CLASS = "state_class"
 CONF_STATE_TOPIC = "state_topic"
 CONF_STATIC_IP = "static_ip"
 CONF_STATUS = "status"
@@ -595,6 +617,7 @@ CONF_UUID = "uuid"
 CONF_VALUE = "value"
 CONF_VARIABLES = "variables"
 CONF_VARIANT = "variant"
+CONF_VERSION = "version"
 CONF_VISUAL = "visual"
 CONF_VOLTAGE = "voltage"
 CONF_VOLTAGE_ATTENUATION = "voltage_attenuation"
@@ -679,6 +702,7 @@ ICON_WIFI = "mdi:wifi"
 UNIT_AMPERE = "A"
 UNIT_CELSIUS = "°C"
 UNIT_COUNTS_PER_CUBIC_METER = "#/m³"
+UNIT_CUBIC_METER = "m³"
 UNIT_DECIBEL = "dB"
 UNIT_DECIBEL_MILLIWATT = "dBm"
 UNIT_DEGREE_PER_SECOND = "°/s"
@@ -711,6 +735,7 @@ UNIT_STEPS = "steps"
 UNIT_VOLT = "V"
 UNIT_VOLT_AMPS = "VA"
 UNIT_VOLT_AMPS_REACTIVE = "VAR"
+UNIT_VOLT_AMPS_REACTIVE_HOURS = "VARh"
 UNIT_WATT = "W"
 UNIT_WATT_HOURS = "Wh"
 
@@ -742,6 +767,8 @@ DEVICE_CLASS_EMPTY = ""
 DEVICE_CLASS_BATTERY = "battery"
 DEVICE_CLASS_POWER = "power"
 # device classes of sensor component
+DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide"
+DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide"
 DEVICE_CLASS_CURRENT = "current"
 DEVICE_CLASS_ENERGY = "energy"
 DEVICE_CLASS_HUMIDITY = "humidity"
@@ -752,3 +779,9 @@ DEVICE_CLASS_POWER_FACTOR = "power_factor"
 DEVICE_CLASS_PRESSURE = "pressure"
 DEVICE_CLASS_TIMESTAMP = "timestamp"
 DEVICE_CLASS_VOLTAGE = "voltage"
+
+# state classes
+STATE_CLASS_NONE = ""
+
+# The state represents a measurement in present time
+STATE_CLASS_MEASUREMENT = "measurement"
diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py
index 47048478ef..1841dfd8be 100644
--- a/esphome/core/__init__.py
+++ b/esphome/core/__init__.py
@@ -1,24 +1,23 @@
-import functools
-import heapq
-import inspect
 import logging
-
 import math
 import os
 import re
-
-# pylint: disable=unused-import, wrong-import-order
-from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING  # noqa
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
 
 from esphome.const import (
     CONF_ARDUINO_VERSION,
-    SOURCE_FILE_EXTENSIONS,
     CONF_COMMENT,
     CONF_ESPHOME,
     CONF_USE_ADDRESS,
     CONF_ETHERNET,
     CONF_WIFI,
+    SOURCE_FILE_EXTENSIONS,
 )
+from esphome.coroutine import FakeAwaitable as _FakeAwaitable
+from esphome.coroutine import FakeEventLoop as _FakeEventLoop
+
+# pylint: disable=unused-import
+from esphome.coroutine import coroutine, coroutine_with_priority  # noqa
 from esphome.helpers import ensure_unique_string, is_hassio
 from esphome.util import OrderedDict
 
@@ -431,64 +430,6 @@ class Library:
         return NotImplemented
 
 
-def coroutine(func):
-    return coroutine_with_priority(0.0)(func)
-
-
-def coroutine_with_priority(priority):
-    def decorator(func):
-        if getattr(func, "_esphome_coroutine", False):
-            # If func is already a coroutine, do not re-wrap it (performance)
-            return func
-
-        @functools.wraps(func)
-        def _wrapper_generator(*args, **kwargs):
-            instance_id = kwargs.pop("__esphome_coroutine_instance__")
-            if not inspect.isgeneratorfunction(func):
-                # If func is not a generator, return result immediately
-                yield func(*args, **kwargs)
-                # pylint: disable=protected-access
-                CORE._remove_coroutine(instance_id)
-                return
-            gen = func(*args, **kwargs)
-            var = None
-            try:
-                while True:
-                    var = gen.send(var)
-                    if inspect.isgenerator(var):
-                        # Yielded generator, equivalent to 'yield from'
-                        x = None
-                        for x in var:
-                            yield None
-                        # Last yield value is the result
-                        var = x
-                    else:
-                        yield var
-            except StopIteration:
-                # Stopping iteration
-                yield var
-            # pylint: disable=protected-access
-            CORE._remove_coroutine(instance_id)
-
-        @functools.wraps(func)
-        def wrapper(*args, **kwargs):
-            import random
-
-            instance_id = random.randint(0, 2 ** 32)
-            kwargs["__esphome_coroutine_instance__"] = instance_id
-            gen = _wrapper_generator(*args, **kwargs)
-            # pylint: disable=protected-access
-            CORE._add_active_coroutine(instance_id, gen)
-            return gen
-
-        # pylint: disable=protected-access
-        wrapper._esphome_coroutine = True
-        wrapper.priority = priority
-        return wrapper
-
-    return decorator
-
-
 def find_source_files(file):
     files = set()
     directory = os.path.abspath(os.path.dirname(file))
@@ -527,7 +468,7 @@ class EsphomeCore:
         # The pending tasks in the task queue (mostly for C++ generation)
         # This is a priority queue (with heapq)
         # Each item is a tuple of form: (-priority, unique number, task)
-        self.pending_tasks = []
+        self.event_loop = _FakeEventLoop()
         # Task counter for pending tasks
         self.task_counter = 0
         # The variable cache, for each ID this holds a MockObj of the variable obj
@@ -542,9 +483,6 @@ class EsphomeCore:
         self.build_flags: Set[str] = set()
         # A set of defines to set for the compile process in esphome/core/defines.h
         self.defines: Set["Define"] = set()
-        # A dictionary of started coroutines, used to warn when a coroutine was not
-        # awaited.
-        self.active_coroutines: Dict[int, Any] = {}
         # A set of strings of names of loaded integrations, used to find namespace ID conflicts
         self.loaded_integrations = set()
         # A set of component IDs to track what Component subclasses are declared
@@ -561,7 +499,7 @@ class EsphomeCore:
         self.board = None
         self.raw_config = None
         self.config = None
-        self.pending_tasks = []
+        self.event_loop = _FakeEventLoop()
         self.task_counter = 0
         self.variables = {}
         self.main_statements = []
@@ -569,7 +507,6 @@ class EsphomeCore:
         self.libraries = []
         self.build_flags = set()
         self.defines = set()
-        self.active_coroutines = {}
         self.loaded_integrations = set()
         self.component_ids = set()
 
@@ -596,12 +533,6 @@ class EsphomeCore:
 
         return None
 
-    def _add_active_coroutine(self, instance_id, obj):
-        self.active_coroutines[instance_id] = obj
-
-    def _remove_coroutine(self, instance_id):
-        self.active_coroutines.pop(instance_id)
-
     @property
     def arduino_version(self) -> str:
         if self.config is None:
@@ -657,50 +588,13 @@ class EsphomeCore:
         return self.esp_platform == "ESP32"
 
     def add_job(self, func, *args, **kwargs):
-        coro = coroutine(func)
-        task = coro(*args, **kwargs)
-        item = (-coro.priority, self.task_counter, task)
-        self.task_counter += 1
-        heapq.heappush(self.pending_tasks, item)
-        return task
+        self.event_loop.add_job(func, *args, **kwargs)
 
     def flush_tasks(self):
-        i = 0
-        while self.pending_tasks:
-            i += 1
-            if i > 1000000:
-                raise EsphomeError("Circular dependency detected!")
-
-            inv_priority, num, task = heapq.heappop(self.pending_tasks)
-            priority = -inv_priority
-            _LOGGER.debug("Running %s (num %s)", task, num)
-            try:
-                next(task)
-                # Decrease priority over time, so that if this task is blocked
-                # due to a dependency others will clear the dependency
-                # This could be improved with a less naive approach
-                priority -= 1
-                item = (-priority, num, task)
-                heapq.heappush(self.pending_tasks, item)
-            except StopIteration:
-                _LOGGER.debug(" -> finished")
-
-        # Print not-awaited coroutines
-        for obj in self.active_coroutines.values():
-            _LOGGER.warning(
-                "Coroutine '%s' %s was never awaited with 'yield'.", obj.__name__, obj
-            )
-            _LOGGER.warning("Please file a bug report with your configuration.")
-        if self.active_coroutines:
-            raise EsphomeError()
-        if self.component_ids:
-            comps = ", ".join(f"'{x}'" for x in self.component_ids)
-            _LOGGER.warning(
-                "Components %s were never registered. Please create a bug report", comps
-            )
-            _LOGGER.warning("with your configuration.")
-            raise EsphomeError()
-        self.active_coroutines.clear()
+        try:
+            self.event_loop.flush_tasks()
+        except RuntimeError as e:
+            raise EsphomeError(str(e)) from e
 
     def add(self, expression):
         from esphome.cpp_generator import Expression, Statement, statement
@@ -779,25 +673,35 @@ class EsphomeCore:
         _LOGGER.debug("Adding define: %s", define)
         return define
 
-    def get_variable(self, id):
+    def _get_variable_generator(self, id):
+        while True:
+            try:
+                return self.variables[id]
+            except KeyError:
+                _LOGGER.debug("Waiting for variable %s (%r)", id, id)
+                yield
+
+    async def get_variable(self, id) -> "MockObj":
         if not isinstance(id, ID):
             raise ValueError(f"ID {id!r} must be of type ID!")
-        while True:
-            if id in self.variables:
-                yield self.variables[id]
-                return
-            _LOGGER.debug("Waiting for variable %s (%r)", id, id)
-            yield None
+        # Fast path, check if already registered without awaiting
+        if id in self.variables:
+            return self.variables[id]
+        return await _FakeAwaitable(self._get_variable_generator(id))
 
-    def get_variable_with_full_id(self, id):
+    def _get_variable_with_full_id_generator(self, id):
         while True:
             if id in self.variables:
                 for k, v in self.variables.items():
                     if k == id:
-                        yield (k, v)
-                        return
+                        return (k, v)
             _LOGGER.debug("Waiting for variable %s", id)
-            yield None, None
+            yield
+
+    async def get_variable_with_full_id(self, id: ID) -> Tuple[ID, "MockObj"]:
+        if not isinstance(id, ID):
+            raise ValueError(f"ID {id!r} must be of type ID!")
+        return await _FakeAwaitable(self._get_variable_with_full_id_generator(id))
 
     def register_variable(self, id, obj):
         if id in self.variables:
diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp
index 5e23c6250b..99726b6248 100644
--- a/esphome/core/application.cpp
+++ b/esphome/core/application.cpp
@@ -103,6 +103,9 @@ void Application::loop() {
   if (this->dump_config_at_ >= 0 && this->dump_config_at_ < this->components_.size()) {
     if (this->dump_config_at_ == 0) {
       ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", this->compilation_time_.c_str());
+#ifdef ESPHOME_PROJECT_NAME
+      ESP_LOGI(TAG, "Project " ESPHOME_PROJECT_NAME " version " ESPHOME_PROJECT_VERSION);
+#endif
     }
 
     this->components_[this->dump_config_at_]->dump_config();
diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp
index d2e9607e28..c571e5e23f 100644
--- a/esphome/core/component.cpp
+++ b/esphome/core/component.cpp
@@ -15,6 +15,8 @@ const float IO = 900.0f;
 const float HARDWARE = 800.0f;
 const float DATA = 600.0f;
 const float PROCESSOR = 400.0;
+const float BLUETOOTH = 350.0f;
+const float AFTER_BLUETOOTH = 300.0f;
 const float WIFI = 250.0f;
 const float AFTER_WIFI = 200.0f;
 const float AFTER_CONNECTION = 100.0f;
diff --git a/esphome/core/component.h b/esphome/core/component.h
index e3f9a51f25..ad3619386b 100644
--- a/esphome/core/component.h
+++ b/esphome/core/component.h
@@ -26,6 +26,8 @@ extern const float DATA;
 extern const float HARDWARE_LATE;
 /// For components that use data from sensors like displays
 extern const float PROCESSOR;
+extern const float BLUETOOTH;
+extern const float AFTER_BLUETOOTH;
 extern const float WIFI;
 /// For components that should be initialized after WiFi is connected.
 extern const float AFTER_WIFI;
diff --git a/esphome/core/config.py b/esphome/core/config.py
index 1dbe2ec33a..fd4b7088cc 100644
--- a/esphome/core/config.py
+++ b/esphome/core/config.py
@@ -21,10 +21,12 @@ from esphome.const import (
     CONF_PLATFORM,
     CONF_PLATFORMIO_OPTIONS,
     CONF_PRIORITY,
+    CONF_PROJECT,
     CONF_TRIGGER_ID,
     CONF_ESP8266_RESTORE_FROM_FLASH,
     ARDUINO_VERSION_ESP8266,
     ARDUINO_VERSION_ESP32,
+    CONF_VERSION,
     ESP_PLATFORMS,
 )
 from esphome.core import CORE, coroutine_with_priority
@@ -69,6 +71,13 @@ validate_platform = cv.one_of(*ESP_PLATFORMS, upper=True)
 
 PLATFORMIO_ESP8266_LUT = {
     **ARDUINO_VERSION_ESP8266,
+    # Keep this in mind when updating the recommended version:
+    #  * New framework historically have had some regressions, especially for WiFi, BLE and the
+    #    bootloader system. The new version needs to be thoroughly validated before changing the
+    #    recommended version as otherwise a bunch of devices could be bricked
+    #  * The docker images need to be updated to ship the new recommended version, in order not
+    #    to DDoS platformio servers.
+    #    Update this file: https://github.com/esphome/esphome-docker-base/blob/master/platformio.ini
     "RECOMMENDED": ARDUINO_VERSION_ESP8266["2.7.4"],
     "LATEST": "espressif8266",
     "DEV": ARDUINO_VERSION_ESP8266["dev"],
@@ -76,7 +85,8 @@ PLATFORMIO_ESP8266_LUT = {
 
 PLATFORMIO_ESP32_LUT = {
     **ARDUINO_VERSION_ESP32,
-    "RECOMMENDED": ARDUINO_VERSION_ESP32["1.0.4"],
+    # See PLATFORMIO_ESP8266_LUT for considerations when changing the recommended version
+    "RECOMMENDED": ARDUINO_VERSION_ESP32["1.0.6"],
     "LATEST": "espressif32",
     "DEV": ARDUINO_VERSION_ESP32["dev"],
 }
@@ -136,6 +146,15 @@ def valid_include(value):
     return value
 
 
+def valid_project_name(value: str):
+    if value.count(".") != 1:
+        raise cv.Invalid("project name needs to have a namespace")
+
+    value = value.replace(" ", "_")
+
+    return value
+
+
 CONFIG_SCHEMA = cv.Schema(
     {
         cv.Required(CONF_NAME): cv.valid_name,
@@ -176,6 +195,12 @@ CONFIG_SCHEMA = cv.Schema(
         cv.Optional(CONF_INCLUDES, default=[]): cv.ensure_list(valid_include),
         cv.Optional(CONF_LIBRARIES, default=[]): cv.ensure_list(cv.string_strict),
         cv.Optional(CONF_NAME_ADD_MAC_SUFFIX, default=False): cv.boolean,
+        cv.Optional(CONF_PROJECT): cv.Schema(
+            {
+                cv.Required(CONF_NAME): cv.All(cv.string_strict, valid_project_name),
+                cv.Required(CONF_VERSION): cv.string_strict,
+            }
+        ),
         cv.Optional("esphome_core_version"): cv.invalid(
             "The esphome_core_version option has been "
             "removed in 1.13 - the esphome core source "
@@ -233,7 +258,7 @@ def include_file(path, basename):
 
 
 @coroutine_with_priority(-1000.0)
-def add_includes(includes):
+async def add_includes(includes):
     # Add includes at the very end, so that the included files can access global variables
     for include in includes:
         path = CORE.relative_config_path(include)
@@ -249,7 +274,7 @@ def add_includes(includes):
 
 
 @coroutine_with_priority(-1000.0)
-def _esp8266_add_lwip_type():
+async def _esp8266_add_lwip_type():
     # If any component has already set this, do not change it
     if any(
         flag.startswith("-DPIO_FRAMEWORK_ARDUINO_LWIP2_") for flag in CORE.build_flags
@@ -271,25 +296,25 @@ def _esp8266_add_lwip_type():
 
 
 @coroutine_with_priority(30.0)
-def _add_automations(config):
+async def _add_automations(config):
     for conf in config.get(CONF_ON_BOOT, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf.get(CONF_PRIORITY))
-        yield cg.register_component(trigger, conf)
-        yield automation.build_automation(trigger, [], conf)
+        await cg.register_component(trigger, conf)
+        await automation.build_automation(trigger, [], conf)
 
     for conf in config.get(CONF_ON_SHUTDOWN, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
-        yield cg.register_component(trigger, conf)
-        yield automation.build_automation(trigger, [], conf)
+        await cg.register_component(trigger, conf)
+        await automation.build_automation(trigger, [], conf)
 
     for conf in config.get(CONF_ON_LOOP, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
-        yield cg.register_component(trigger, conf)
-        yield automation.build_automation(trigger, [], conf)
+        await cg.register_component(trigger, conf)
+        await automation.build_automation(trigger, [], conf)
 
 
 @coroutine_with_priority(100.0)
-def to_code(config):
+async def to_code(config):
     cg.add_global(cg.global_ns.namespace("esphome").using)
     cg.add(
         cg.App.pre_setup(
@@ -323,3 +348,8 @@ def to_code(config):
 
     if config[CONF_INCLUDES]:
         CORE.add_job(add_includes, config[CONF_INCLUDES])
+
+    cg.add_define("ESPHOME_BOARD", CORE.board)
+    if CONF_PROJECT in config:
+        cg.add_define("ESPHOME_PROJECT_NAME", config[CONF_PROJECT][CONF_NAME])
+        cg.add_define("ESPHOME_PROJECT_VERSION", config[CONF_PROJECT][CONF_VERSION])
diff --git a/esphome/core/defines.h b/esphome/core/defines.h
index fe10a42baa..24efd0fd14 100644
--- a/esphome/core/defines.h
+++ b/esphome/core/defines.h
@@ -19,7 +19,10 @@
 #define USE_JSON
 #ifdef ARDUINO_ARCH_ESP32
 #define USE_ESP32_CAMERA
+#define USE_IMPROV
 #endif
 #define USE_TIME
 #define USE_DEEP_SLEEP
 #define USE_CAPTIVE_PORTAL
+#define ESPHOME_BOARD "dummy_board"
+#define USE_MDNS
diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h
index 5f9ab1fdd1..d49415ca6b 100644
--- a/esphome/core/helpers.h
+++ b/esphome/core/helpers.h
@@ -21,6 +21,10 @@
 #define ALWAYS_INLINE __attribute__((always_inline))
 #define PACKED __attribute__((packed))
 
+#define xSemaphoreWait(semaphore, wait_time) \
+  xSemaphoreTake(semaphore, wait_time); \
+  xSemaphoreGive(semaphore);
+
 namespace esphome {
 
 /// The characters that are allowed in a hostname.
@@ -89,7 +93,7 @@ float clamp(float val, float min, float max);
 float lerp(float completion, float start, float end);
 
 /// std::make_unique
-template<typename T, typename... Args> std::unique_ptr<T> make_unique(Args &&... args) {
+template<typename T, typename... Args> std::unique_ptr<T> make_unique(Args &&...args) {
   return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
 }
 
@@ -318,8 +322,6 @@ template<typename T> class Parented {
 
 uint32_t fnv1_hash(const std::string &str);
 
-}  // namespace esphome
-
 template<typename T> T *new_buffer(size_t length) {
   T *buffer;
 #ifdef ARDUINO_ARCH_ESP32
@@ -333,4 +335,6 @@ template<typename T> T *new_buffer(size_t length) {
 #endif
 
   return buffer;
+}
+
 }  // namespace esphome
diff --git a/esphome/core/preferences.cpp b/esphome/core/preferences.cpp
index 8b41cbc7b5..2d58fc9efb 100644
--- a/esphome/core/preferences.cpp
+++ b/esphome/core/preferences.cpp
@@ -252,9 +252,9 @@ bool ESPPreferenceObject::load_internal_() {
 
   char key[32];
   sprintf(key, "%u", this->offset_);
-  uint32_t len = (this->length_words_ + 1) * 4;
+  size_t len = (this->length_words_ + 1) * 4;
 
-  uint32_t actual_len;
+  size_t actual_len;
   esp_err_t err = nvs_get_blob(global_preferences.nvs_handle_, key, nullptr, &actual_len);
   if (err) {
     ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key, esp_err_to_name(err));
diff --git a/esphome/core/util.cpp b/esphome/core/util.cpp
index 4e15d142be..19b8ab89e9 100644
--- a/esphome/core/util.cpp
+++ b/esphome/core/util.cpp
@@ -16,6 +16,10 @@
 #include "esphome/components/ethernet/ethernet_component.h"
 #endif
 
+#ifdef USE_MQTT
+#include "esphome/components/mqtt/mqtt_client.h"
+#endif
+
 #ifdef USE_MDNS
 #ifdef ARDUINO_ARCH_ESP32
 #include <ESPmDNS.h>
@@ -41,6 +45,26 @@ bool network_is_connected() {
   return false;
 }
 
+bool api_is_connected() {
+#ifdef USE_API
+  if (api::global_api_server != nullptr) {
+    return api::global_api_server->is_connected();
+  }
+#endif
+  return false;
+}
+
+bool mqtt_is_connected() {
+#ifdef USE_MQTT
+  if (mqtt::global_mqtt_client != nullptr) {
+    return mqtt::global_mqtt_client->is_connected();
+  }
+#endif
+  return false;
+}
+
+bool remote_is_connected() { return api_is_connected() || mqtt_is_connected(); }
+
 #if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS)
 bool mdns_setup;
 #endif
@@ -70,6 +94,17 @@ void network_setup_mdns(IPAddress address, int interface) {
       MDNS.addServiceTxt("esphomelib", "tcp", "version", ESPHOME_VERSION);
       MDNS.addServiceTxt("esphomelib", "tcp", "address", network_get_address().c_str());
       MDNS.addServiceTxt("esphomelib", "tcp", "mac", get_mac_address().c_str());
+#ifdef ARDUINO_ARCH_ESP8266
+      MDNS.addServiceTxt("esphomelib", "tcp", "platform", "ESP8266");
+#endif
+#ifdef ARDUINO_ARCH_ESP32
+      MDNS.addServiceTxt("esphomelib", "tcp", "platform", "ESP32");
+#endif
+      MDNS.addServiceTxt("esphomelib", "tcp", "board", ESPHOME_BOARD);
+#ifdef ESPHOME_PROJECT_NAME
+      MDNS.addServiceTxt("esphomelib", "tcp", "project_name", ESPHOME_PROJECT_NAME);
+      MDNS.addServiceTxt("esphomelib", "tcp", "project_version", ESPHOME_PROJECT_VERSION);
+#endif
     } else {
 #endif
       // Publish "http" service if not using native API nor the webserver component
diff --git a/esphome/core/util.h b/esphome/core/util.h
index 8e30211be6..180c7aaefa 100644
--- a/esphome/core/util.h
+++ b/esphome/core/util.h
@@ -5,11 +5,25 @@
 
 namespace esphome {
 
+/// Macro for IDF version comparision
+#ifndef ESP_IDF_VERSION_VAL
+#define ESP_IDF_VERSION_VAL(major, minor, patch) ((major << 16) | (minor << 8) | (patch))
+#endif
+
 /// Return whether the node is connected to the network (through wifi, eth, ...)
 bool network_is_connected();
 /// Get the active network hostname
 std::string network_get_address();
 
+/// Return whether the node has at least one client connected to the native API
+bool api_is_connected();
+
+/// Return whether the node has an active connection to an MQTT broker
+bool mqtt_is_connected();
+
+/// Return whether the node has any form of "remote" connection via the API or to an MQTT broker
+bool remote_is_connected();
+
 /// Manually set up the network stack (outside of the App.setup() loop, for example in OTA safe mode)
 #ifdef ARDUINO_ARCH_ESP8266
 void network_setup_mdns(IPAddress address, int interface);
diff --git a/esphome/coroutine.py b/esphome/coroutine.py
new file mode 100644
index 0000000000..58f79c6b36
--- /dev/null
+++ b/esphome/coroutine.py
@@ -0,0 +1,252 @@
+"""
+ESPHome's coroutine system.
+
+The Problem: When running the code generationg, components can depend on variables being registered.
+For example, an i2c-based sensor would need the i2c bus component to first be declared before the
+codegen can emit code using that variable (or otherwise the C++ won't compile).
+
+ESPHome's codegen system solves this by using coroutine-like methods. When a component depends on
+a variable, it waits for it to be registered using `await cg.get_variable()`. If the variable
+hasn't been registered yet, control will be yielded back to another component until the variable
+is registered. This leads to a topological sort, solving the dependency problem.
+
+Importantly, ESPHome only uses the coroutine *syntax*, no actual asyncio event loop is running in
+the background. This is so that we can ensure the order of execution is constant for the same
+YAML configuration, thus main.cpp only has to be recompiled if the configuration actually changes.
+
+There are two syntaxes for ESPHome coroutines ("old style" vs "new style" coroutines).
+
+"new style" - This is very much like coroutines you might be used to:
+
+```py
+async def my_coroutine(config):
+    var = await cg.get_variable(config[CONF_ID])
+    await some_other_coroutine(xyz)
+    return var
+```
+
+new style coroutines are `async def` methods that use `await` to await the result of another coroutine,
+and can return values using a `return` statement.
+
+"old style" - This was a hack for when ESPHome still had to run on python 2, but is still compatible
+
+```py
+@coroutine
+def my_coroutine(config):
+    var = yield cg.get_variable(config[CONF_ID])
+    yield some_other_coroutine(xyz)
+    yield var
+```
+
+Here everything is combined in `yield` expressions. You await other coroutines using `yield` and
+the last `yield` expression defines what is returned.
+"""
+
+import collections
+import functools
+import heapq
+import inspect
+import logging
+import types
+from typing import Any, Awaitable, Callable, Generator, Iterator, List, Tuple
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def coroutine(func: Callable[..., Any]) -> Callable[..., Awaitable[Any]]:
+    """Decorator to apply to methods to convert them to ESPHome coroutines."""
+    if getattr(func, "_esphome_coroutine", False):
+        # If func is already a coroutine, do not re-wrap it (performance)
+        return func
+    if inspect.isasyncgenfunction(func):
+        # Trade-off: In ESPHome, there's not really a use-case for async generators.
+        # and during the transition to new-style syntax it will happen that a `yield`
+        # is not replaced properly, so don't accept async generators.
+        raise ValueError(
+            f"Async generator functions are not allowed. "
+            f"Please check whether you've replaced all yields with awaits/returns. "
+            f"See {func} in {func.__module__}"
+        )
+    if inspect.iscoroutinefunction(func):
+        # A new-style async-def coroutine function, no conversion needed.
+        return func
+
+    if inspect.isgeneratorfunction(func):
+
+        @functools.wraps(func)
+        def coro(*args, **kwargs):
+            gen = func(*args, **kwargs)
+            ret = yield from _flatten_generator(gen)
+            return ret
+
+    else:
+        # A "normal" function with no `yield` statements, convert to generator
+        # that includes a yield just so it's also a generator function
+        @functools.wraps(func)
+        def coro(*args, **kwargs):
+            res = func(*args, **kwargs)
+            yield
+            return res
+
+    # Add coroutine internal python flag so that it can be awaited from new-style coroutines.
+    coro = types.coroutine(coro)
+    # pylint: disable=protected-access
+    coro._esphome_coroutine = True
+    return coro
+
+
+def coroutine_with_priority(priority: float):
+    """Decorator to apply to functions to convert them to ESPHome coroutines.
+
+    :param priority: priority with which to schedule the coroutine, higher priorities run first.
+    """
+
+    def decorator(func):
+        coro = coroutine(func)
+        coro.priority = priority
+        return coro
+
+    return decorator
+
+
+def _flatten_generator(gen: Generator[Any, Any, Any]):
+    to_send = None
+    while True:
+        try:
+            # Run until next yield expression
+            val = gen.send(to_send)
+        except StopIteration as e:
+            # return statement or end of function
+
+            # From py3.3, return with a value is allowed in generators,
+            # and return value is transported in the value field of the exception.
+            # If we find a value in the exception, use that as the return value,
+            # otherwise use the value from the last yield statement ("old style")
+            ret = to_send if e.value is None else e.value
+            return ret
+
+        if isinstance(val, collections.abc.Awaitable):
+            # yielded object that is awaitable (like `yield some_new_style_method()`)
+            # yield from __await__() like actual coroutines would.
+            to_send = yield from val.__await__()
+        elif inspect.isgenerator(val):
+            # Old style, like `yield cg.get_variable()`
+            to_send = yield from _flatten_generator(val)
+        else:
+            # Could be the last expression from this generator, record this as the return value
+            to_send = val
+            # perform a yield so that expressions like `while some_condition(): yield None`
+            # do not run without yielding control back to the top
+            yield
+
+
+class FakeAwaitable:
+    """Convert a generator to an awaitable object.
+
+    Needed for internals of `cg.get_variable`. There we can't use @coroutine because
+    native coroutines await from types.coroutine() directly without yielding back control to the top
+    (likely as a performance enhancement).
+
+    If we instead wrap the generator in this FakeAwaitable, control is yielded back to the top
+    (reason unknown).
+    """
+
+    def __init__(self, gen: Generator[Any, Any, Any]) -> None:
+        self._gen = gen
+
+    def __await__(self):
+        ret = yield from self._gen
+        return ret
+
+
+@functools.total_ordering
+class _Task:
+    def __init__(
+        self,
+        priority: float,
+        id_number: int,
+        iterator: Iterator[None],
+        original_function: Any,
+    ):
+        self.priority = priority
+        self.id_number = id_number
+        self.iterator = iterator
+        self.original_function = original_function
+
+    def with_priority(self, priority: float) -> "_Task":
+        return _Task(priority, self.id_number, self.iterator, self.original_function)
+
+    @property
+    def _cmp_tuple(self) -> Tuple[float, int]:
+        return (-self.priority, self.id_number)
+
+    def __eq__(self, other):
+        return self._cmp_tuple == other._cmp_tuple
+
+    def __ne__(self, other):
+        return not (self == other)
+
+    def __lt__(self, other):
+        return self._cmp_tuple < other._cmp_tuple
+
+
+class FakeEventLoop:
+    """Emulate an asyncio EventLoop to run some registered coroutine jobs in sequence."""
+
+    def __init__(self):
+        self._pending_tasks: List[_Task] = []
+        self._task_counter = 0
+
+    def add_job(self, func, *args, **kwargs):
+        """Add a job to the task queue,
+
+        Optionally retrieves priority from the function object, and schedules according to that.
+        """
+        if inspect.iscoroutine(func):
+            raise ValueError("Can only add coroutine functions, not coroutine objects")
+        if inspect.iscoroutinefunction(func):
+            coro = func
+            gen = coro(*args, **kwargs).__await__()
+        else:
+            coro = coroutine(func)
+            gen = coro(*args, **kwargs)
+        prio = getattr(coro, "priority", 0.0)
+        task = _Task(prio, self._task_counter, gen, func)
+        self._task_counter += 1
+        heapq.heappush(self._pending_tasks, task)
+
+    def flush_tasks(self):
+        """Run until all tasks have been completed.
+
+        :raises RuntimeError: if a deadlock is detected.
+        """
+        i = 0
+        while self._pending_tasks:
+            i += 1
+            if i > 1000000:
+                # Detect deadlock/circular dependency by measuring how many times tasks have been
+                # executed. On the big tests/test1.yaml we only get to a fraction of this, so
+                # this shouldn't be a problem.
+                raise RuntimeError(
+                    "Circular dependency detected! "
+                    "Please run with -v option to see what functions failed to "
+                    "complete."
+                )
+
+            task: _Task = heapq.heappop(self._pending_tasks)
+            _LOGGER.debug(
+                "Running %s in %s (num %s)",
+                task.original_function.__qualname__,
+                task.original_function.__module__,
+                task.id_number,
+            )
+
+            try:
+                next(task.iterator)
+                # Decrease priority over time, so that if this task is blocked
+                # due to a dependency others will clear the dependency
+                # This could be improved with a less naive approach
+                new_task = task.with_priority(task.priority - 1)
+                heapq.heappush(self._pending_tasks, new_task)
+            except StopIteration:
+                _LOGGER.debug(" -> finished")
diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py
index 999b252dde..802e9a9d38 100644
--- a/esphome/cpp_generator.py
+++ b/esphome/cpp_generator.py
@@ -354,7 +354,7 @@ def safe_exp(obj: SafeExpType) -> Expression:
     if inspect.isgenerator(obj):
         raise ValueError(
             "Object {} is a coroutine. Did you forget to await the expression with "
-            "'yield'?".format(obj)
+            "'await'?".format(obj)
         )
     raise ValueError("Object is not an expression", obj)
 
@@ -411,6 +411,16 @@ class ProgmemAssignmentExpression(AssignmentExpression):
         return f"static const {self.type} {self.name}[] PROGMEM = {self.rhs}"
 
 
+class StaticConstAssignmentExpression(AssignmentExpression):
+    __slots__ = ()
+
+    def __init__(self, type_, name, rhs, obj):
+        super().__init__(type_, "", name, rhs, obj)
+
+    def __str__(self):
+        return f"static const {self.type} {self.name}[] = {self.rhs}"
+
+
 def progmem_array(id_, rhs) -> "MockObj":
     rhs = safe_exp(rhs)
     obj = MockObj(id_, ".")
@@ -420,6 +430,15 @@ def progmem_array(id_, rhs) -> "MockObj":
     return obj
 
 
+def static_const_array(id_, rhs) -> "MockObj":
+    rhs = safe_exp(rhs)
+    obj = MockObj(id_, ".")
+    assignment = StaticConstAssignmentExpression(id_.type, id_, rhs, obj)
+    CORE.add(assignment)
+    CORE.register_variable(id_, obj)
+    return obj
+
+
 def statement(expression: Union[Expression, Statement]) -> Statement:
     """Convert expression into a statement unless is already a statement."""
     if isinstance(expression, Statement):
@@ -549,38 +568,33 @@ def add_define(name: str, value: SafeExpType = None):
         CORE.add_define(Define(name, safe_exp(value)))
 
 
-@coroutine
-def get_variable(id_: ID) -> Generator["MockObj", None, None]:
+async def get_variable(id_: ID) -> "MockObj":
     """
     Wait for the given ID to be defined in the code generation and
     return it as a MockObj.
 
-    This is a coroutine, you need to await it with a 'yield' expression!
+    This is a coroutine, you need to await it with a 'await' expression!
 
     :param id_: The ID to retrieve
     :return: The variable as a MockObj.
     """
-    var = yield CORE.get_variable(id_)
-    yield var
+    return await CORE.get_variable(id_)
 
 
-@coroutine
-def get_variable_with_full_id(id_: ID) -> Generator[Tuple[ID, "MockObj"], None, None]:
+async def get_variable_with_full_id(id_: ID) -> Tuple[ID, "MockObj"]:
     """
     Wait for the given ID to be defined in the code generation and
     return it as a MockObj.
 
-    This is a coroutine, you need to await it with a 'yield' expression!
+    This is a coroutine, you need to await it with a 'await' expression!
 
     :param id_: The ID to retrieve
     :return: The variable as a MockObj.
     """
-    full_id, var = yield CORE.get_variable_with_full_id(id_)
-    yield full_id, var
+    return await CORE.get_variable_with_full_id(id_)
 
 
-@coroutine
-def process_lambda(
+async def process_lambda(
     value: Lambda,
     parameters: List[Tuple[SafeExpType, str]],
     capture: str = "=",
@@ -589,7 +603,7 @@ def process_lambda(
     """Process the given lambda value into a LambdaExpression.
 
     This is a coroutine because lambdas can depend on other IDs,
-    you need to await it with 'yield'!
+    you need to await it with 'await'!
 
     :param value: The lambda to process.
     :param parameters: The parameters to pass to the Lambda, list of tuples
@@ -600,11 +614,10 @@ def process_lambda(
     from esphome.components.globals import GlobalsComponent
 
     if value is None:
-        yield
         return
     parts = value.parts[:]
     for i, id in enumerate(value.requires_ids):
-        full_id, var = yield CORE.get_variable_with_full_id(id)
+        full_id, var = await get_variable_with_full_id(id)
         if (
             full_id is not None
             and isinstance(full_id.type, MockObjClass)
@@ -624,7 +637,7 @@ def process_lambda(
         location.line += value.content_offset
     else:
         location = None
-    yield LambdaExpression(parts, parameters, capture, return_type, location)
+    return LambdaExpression(parts, parameters, capture, return_type, location)
 
 
 def is_template(value):
@@ -632,8 +645,7 @@ def is_template(value):
     return isinstance(value, Lambda)
 
 
-@coroutine
-def templatable(
+async def templatable(
     value: Any,
     args: List[Tuple[SafeExpType, str]],
     output_type: Optional[SafeExpType],
@@ -651,15 +663,12 @@ def templatable(
     :return: The potentially templated value.
     """
     if is_template(value):
-        lambda_ = yield process_lambda(value, args, return_type=output_type)
-        yield lambda_
-    else:
-        if to_exp is None:
-            yield value
-        elif isinstance(to_exp, dict):
-            yield to_exp[value]
-        else:
-            yield to_exp(value)
+        return await process_lambda(value, args, return_type=output_type)
+    if to_exp is None:
+        return value
+    if isinstance(to_exp, dict):
+        return to_exp[value]
+    return to_exp(value)
 
 
 class MockObj(Expression):
@@ -675,6 +684,9 @@ class MockObj(Expression):
         self.op = op
 
     def __getattr__(self, attr: str) -> "MockObj":
+        # prevent python dunder methods being replaced by mock objects
+        if attr.startswith("__"):
+            raise AttributeError()
         next_op = "."
         if attr.startswith("P") and self.op not in ["::", ""]:
             attr = attr[1:]
diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py
index e83f989d7a..1c52f38e50 100644
--- a/esphome/cpp_helpers.py
+++ b/esphome/cpp_helpers.py
@@ -14,11 +14,10 @@ from esphome.cpp_types import App, GPIOPin
 from esphome.util import Registry, RegistryEntry
 
 
-@coroutine
-def gpio_pin_expression(conf):
+async def gpio_pin_expression(conf):
     """Generate an expression for the given pin option.
 
-    This is a coroutine, you must await it with a 'yield' expression!
+    This is a coroutine, you must await it with a 'await' expression!
     """
     if conf is None:
         return
@@ -26,20 +25,18 @@ def gpio_pin_expression(conf):
 
     for key, (func, _) in pins.PIN_SCHEMA_REGISTRY.items():
         if key in conf:
-            yield coroutine(func)(conf)
-            return
+            return await coroutine(func)(conf)
 
     number = conf[CONF_NUMBER]
     mode = conf[CONF_MODE]
     inverted = conf.get(CONF_INVERTED)
-    yield GPIOPin.new(number, RawExpression(mode), inverted)
+    return GPIOPin.new(number, RawExpression(mode), inverted)
 
 
-@coroutine
-def register_component(var, config):
+async def register_component(var, config):
     """Register the given obj as a component.
 
-    This is a coroutine, you must await it with a 'yield' expression!
+    This is a coroutine, you must await it with a 'await' expression!
 
     :param var: The variable representing the component.
     :param config: The configuration for the component.
@@ -57,13 +54,12 @@ def register_component(var, config):
     if CONF_UPDATE_INTERVAL in config:
         add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))
     add(App.register_component(var))
-    yield var
+    return var
 
 
-@coroutine
-def register_parented(var, value):
+async def register_parented(var, value):
     if isinstance(value, ID):
-        paren = yield get_variable(value)
+        paren = await get_variable(value)
     else:
         paren = value
     add(var.set_parent(paren))
@@ -75,18 +71,16 @@ def extract_registry_entry_config(registry, full_config):
     return registry[key], config
 
 
-@coroutine
-def build_registry_entry(registry, full_config):
+async def build_registry_entry(registry, full_config):
     registry_entry, config = extract_registry_entry_config(registry, full_config)
     type_id = full_config[CONF_TYPE_ID]
     builder = registry_entry.coroutine_fun
-    yield builder(config, type_id)
+    return await builder(config, type_id)
 
 
-@coroutine
-def build_registry_list(registry, config):
+async def build_registry_list(registry, config):
     actions = []
     for conf in config:
-        action = yield build_registry_entry(registry, conf)
+        action = await build_registry_entry(registry, conf)
         actions.append(action)
-    yield actions
+    return actions
diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py
index 1575d9f9b1..4b40237768 100644
--- a/esphome/dashboard/dashboard.py
+++ b/esphome/dashboard/dashboard.py
@@ -62,7 +62,7 @@ class DashboardSettings:
             self.using_password = bool(password)
         if self.using_password:
             self.password_hash = password_hash(password)
-        self.config_dir = args.configuration[0]
+        self.config_dir = args.configuration
 
     @property
     def relative_url(self):
@@ -274,9 +274,9 @@ class EsphomeLogsHandler(EsphomeCommandWebSocket):
         return [
             "esphome",
             "--dashboard",
-            config_file,
             "logs",
-            "--serial-port",
+            config_file,
+            "--device",
             json_message["port"],
         ]
 
@@ -287,9 +287,9 @@ class EsphomeUploadHandler(EsphomeCommandWebSocket):
         return [
             "esphome",
             "--dashboard",
-            config_file,
             "run",
-            "--upload-port",
+            config_file,
+            "--device",
             json_message["port"],
         ]
 
@@ -297,40 +297,40 @@ class EsphomeUploadHandler(EsphomeCommandWebSocket):
 class EsphomeCompileHandler(EsphomeCommandWebSocket):
     def build_command(self, json_message):
         config_file = settings.rel_path(json_message["configuration"])
-        return ["esphome", "--dashboard", config_file, "compile"]
+        return ["esphome", "--dashboard", "compile", config_file]
 
 
 class EsphomeValidateHandler(EsphomeCommandWebSocket):
     def build_command(self, json_message):
         config_file = settings.rel_path(json_message["configuration"])
-        return ["esphome", "--dashboard", config_file, "config"]
+        return ["esphome", "--dashboard", "config", config_file]
 
 
 class EsphomeCleanMqttHandler(EsphomeCommandWebSocket):
     def build_command(self, json_message):
         config_file = settings.rel_path(json_message["configuration"])
-        return ["esphome", "--dashboard", config_file, "clean-mqtt"]
+        return ["esphome", "--dashboard", "clean-mqtt", config_file]
 
 
 class EsphomeCleanHandler(EsphomeCommandWebSocket):
     def build_command(self, json_message):
         config_file = settings.rel_path(json_message["configuration"])
-        return ["esphome", "--dashboard", config_file, "clean"]
+        return ["esphome", "--dashboard", "clean", config_file]
 
 
 class EsphomeVscodeHandler(EsphomeCommandWebSocket):
     def build_command(self, json_message):
-        return ["esphome", "--dashboard", "-q", "dummy", "vscode"]
+        return ["esphome", "--dashboard", "-q", "vscode", "dummy"]
 
 
 class EsphomeAceEditorHandler(EsphomeCommandWebSocket):
     def build_command(self, json_message):
-        return ["esphome", "--dashboard", "-q", settings.config_dir, "vscode", "--ace"]
+        return ["esphome", "--dashboard", "-q", "vscode", settings.config_dir, "--ace"]
 
 
 class EsphomeUpdateAllHandler(EsphomeCommandWebSocket):
     def build_command(self, json_message):
-        return ["esphome", "--dashboard", settings.config_dir, "update-all"]
+        return ["esphome", "--dashboard", "update-all", settings.config_dir]
 
 
 class SerialPortRequestHandler(BaseHandler):
@@ -515,45 +515,45 @@ class MDNSStatusThread(threading.Thread):
 
 class PingStatusThread(threading.Thread):
     def run(self):
-        pool = multiprocessing.Pool(processes=8)
-        while not STOP_EVENT.is_set():
-            # Only do pings if somebody has the dashboard open
+        with multiprocessing.Pool(processes=8) as pool:
+            while not STOP_EVENT.is_set():
+                # Only do pings if somebody has the dashboard open
 
-            def callback(ret):
-                PING_RESULT[ret[0]] = ret[1]
+                def callback(ret):
+                    PING_RESULT[ret[0]] = ret[1]
 
-            entries = _list_dashboard_entries()
-            queue = collections.deque()
-            for entry in entries:
-                if entry.address is None:
-                    PING_RESULT[entry.filename] = None
-                    continue
+                entries = _list_dashboard_entries()
+                queue = collections.deque()
+                for entry in entries:
+                    if entry.address is None:
+                        PING_RESULT[entry.filename] = None
+                        continue
 
-                result = pool.apply_async(
-                    _ping_func, (entry.filename, entry.address), callback=callback
-                )
-                queue.append(result)
+                    result = pool.apply_async(
+                        _ping_func, (entry.filename, entry.address), callback=callback
+                    )
+                    queue.append(result)
 
-            while queue:
-                item = queue[0]
-                if item.ready():
-                    queue.popleft()
-                    continue
+                while queue:
+                    item = queue[0]
+                    if item.ready():
+                        queue.popleft()
+                        continue
 
-                try:
-                    item.get(0.1)
-                except OSError:
-                    # ping not installed
-                    pass
-                except multiprocessing.TimeoutError:
-                    pass
+                    try:
+                        item.get(0.1)
+                    except OSError:
+                        # ping not installed
+                        pass
+                    except multiprocessing.TimeoutError:
+                        pass
 
-                if STOP_EVENT.is_set():
-                    pool.terminate()
-                    return
+                    if STOP_EVENT.is_set():
+                        pool.terminate()
+                        return
 
-            PING_REQUEST.wait()
-            PING_REQUEST.clear()
+                PING_REQUEST.wait()
+                PING_REQUEST.clear()
 
 
 class PingRequestHandler(BaseHandler):
diff --git a/esphome/espota2.py b/esphome/espota2.py
index 785cb155df..351f6feda9 100644
--- a/esphome/espota2.py
+++ b/esphome/espota2.py
@@ -296,15 +296,14 @@ def run_ota_impl_(remote_host, remote_port, password, filename):
         _LOGGER.error("Connecting to %s:%s failed: %s", remote_host, remote_port, err)
         return 1
 
-    file_handle = open(filename, "rb")
-    try:
-        perform_ota(sock, password, file_handle, filename)
-    except OTAError as err:
-        _LOGGER.error(str(err))
-        return 1
-    finally:
-        sock.close()
-        file_handle.close()
+    with open(filename, "rb") as file_handle:
+        try:
+            perform_ota(sock, password, file_handle, filename)
+        except OTAError as err:
+            _LOGGER.error(str(err))
+            return 1
+        finally:
+            sock.close()
 
     return 0
 
diff --git a/esphome/helpers.py b/esphome/helpers.py
index d9730f96a7..ad7b8272b2 100644
--- a/esphome/helpers.py
+++ b/esphome/helpers.py
@@ -60,10 +60,10 @@ def cpp_string_escape(string, encoding="utf-8"):
 def run_system_command(*args):
     import subprocess
 
-    p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-    stdout, stderr = p.communicate()
-    rc = p.returncode
-    return rc, stdout, stderr
+    with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p:
+        stdout, stderr = p.communicate()
+        rc = p.returncode
+        return rc, stdout, stderr
 
 
 def mkdir_p(path):
diff --git a/esphome/loader.py b/esphome/loader.py
index c418008453..d9d407d787 100644
--- a/esphome/loader.py
+++ b/esphome/loader.py
@@ -80,6 +80,10 @@ class ComponentManifest:
     def codeowners(self) -> List[str]:
         return getattr(self.module, "CODEOWNERS", [])
 
+    @property
+    def validate(self):
+        return getattr(self.module, "validate", None)
+
     @property
     def source_files(self) -> Dict[Path, SourceFile]:
         ret = {}
diff --git a/esphome/wizard.py b/esphome/wizard.py
index 3550e39392..7d875b7dd2 100644
--- a/esphome/wizard.py
+++ b/esphome/wizard.py
@@ -295,7 +295,7 @@ def wizard(path):
     safe_print(
         "First, what's the "
         + color(Fore.GREEN, "SSID")
-        + f" (the name) of the WiFi network {name} I should connect to?"
+        + f" (the name) of the WiFi network {name} should connect to?"
     )
     sleep(1.5)
     safe_print('For example "{}".'.format(color(Fore.BOLD_WHITE, "Abraham Linksys")))
diff --git a/platformio.ini b/platformio.ini
index 7a63ea67a9..ba7724ad24 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -10,43 +10,49 @@ include_dir = include
 
 [common]
 lib_deps =
-    esphome/AsyncTCP-esphome@1.2.2
     AsyncMqttClient-esphome@0.8.4
     ArduinoJson-esphomelib@5.13.3
     ESPAsyncWebServer-esphome@1.2.7
-    fastled/FastLED@3.3.2
-    NeoPixelBus-esphome@2.5.7
-    ESPAsyncTCP-esphome@1.2.3
+    FastLED@3.3.2
+    NeoPixelBus-esphome@2.6.2
     1655@1.0.2  ; TinyGPSPlus (has name conflict)
     6865@1.0.0  ; TM1651 Battery Display
     6306@1.0.3  ; HM3301
 build_flags =
-    -Wno-reorder
-    -DUSE_WEB_SERVER
-    -DUSE_FAST_LED_LIGHT
-    -DUSE_NEO_PIXEL_BUS_LIGHT
+    -fno-exceptions
+    -Wno-sign-compare
+    -Wno-unused-but-set-variable
+    -Wno-unused-variable
     -DCLANG_TIDY
     -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE
-; Don't use FlashStringHelper for debug builds because CLion freaks out for all
-; log messages
-src_filter = +<esphome>
+src_filter =
+    +<esphome>
+    +<tests/dummy_main.cpp>
 
 [env:livingroom8266]
-; use Arduino framework v2.3.0 for clang-tidy (latest 2.5.2 breaks static code analysis, see #760)
-platform = espressif8266@1.8.0
-board = nodemcuv2
+; use Arduino framework v2.4.2 for clang-tidy (latest 2.5.2 breaks static code analysis, see #760)
+platform = platformio/espressif8266@1.8.0
 framework = arduino
+board = nodemcuv2
 lib_deps =
     ${common.lib_deps}
     ESP8266WiFi
-    Hash
+    ESPAsyncTCP-esphome@1.2.3
+    Update
 build_flags = ${common.build_flags}
-src_filter = ${common.src_filter} +<tests/livingroom8266.cpp>
+src_filter = ${common.src_filter}
 
 [env:livingroom32]
-platform = espressif32@1.12.4
-board = nodemcu-32s
+platform = platformio/espressif32@3.2.0
 framework = arduino
-lib_deps = ${common.lib_deps}
-build_flags = ${common.build_flags} -DUSE_ETHERNET
-src_filter = ${common.src_filter} +<tests/livingroom32.cpp>
+board = nodemcu-32s
+lib_deps =
+    ${common.lib_deps}
+    esphome/AsyncTCP-esphome@1.2.2
+    Update
+build_flags =
+    ${common.build_flags}
+    -DUSE_ETHERNET
+src_filter =
+    ${common.src_filter}
+    -<esphome/components/esp8266_pwm>
diff --git a/requirements.txt b/requirements.txt
index f749358001..5915c2963f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,7 +3,7 @@ PyYAML==5.4.1
 paho-mqtt==1.5.1
 colorama==0.4.4
 tornado==6.1
-protobuf==3.15.8
+protobuf==3.17.0
 tzlocal==2.1
 pytz==2021.1
 pyserial==3.5
diff --git a/requirements_optional.txt b/requirements_optional.txt
new file mode 100644
index 0000000000..2c73430109
--- /dev/null
+++ b/requirements_optional.txt
@@ -0,0 +1,2 @@
+pillow>4.0.0
+cryptography>=2.0.0,<4
diff --git a/requirements_test.txt b/requirements_test.txt
index fdd1e91b20..15593a8e12 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -1,14 +1,13 @@
-pylint==2.7.2
-flake8==3.9.0
-black==20.8b1
-pillow>4.0.0
-cryptography>=2.0.0,<4
+pylint==2.8.2
+flake8==3.9.2
+black==21.5b1
 pexpect==4.8.0
 pre-commit
 
 # Unit tests
-pytest==6.2.3
+pytest==6.2.4
 pytest-cov==2.11.1
 pytest-mock==3.5.1
+pytest-asyncio==0.14.0
 asyncmock==0.4.2
 hypothesis==5.21.0
diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py
index f86145df2f..97cc95e556 100644
--- a/script/api_protobuf/api_protobuf.py
+++ b/script/api_protobuf/api_protobuf.py
@@ -40,7 +40,16 @@ d = descriptor.FileDescriptorSet.FromString(content)
 
 
 def indent_list(text, padding="  "):
-    return [padding + line for line in text.splitlines()]
+    lines = []
+    for line in text.splitlines():
+        if line == "":
+            p = ""
+        elif line.startswith("#ifdef") or line.startswith("#endif"):
+            p = ""
+        else:
+            p = padding
+        lines.append(p + line)
+    return lines
 
 
 def indent(text, padding="  "):
@@ -103,7 +112,7 @@ class TypeInfo:
 
     @property
     def class_member(self) -> str:
-        return f"{self.cpp_type} {self.field_name}{{{self.default_value}}};  // NOLINT"
+        return f"{self.cpp_type} {self.field_name}{{{self.default_value}}};"
 
     @property
     def decode_varint_content(self) -> str:
@@ -432,7 +441,7 @@ class SInt64Type(TypeInfo):
     decode_varint = "value.as_sint64()"
     encode_func = "encode_sin64"
 
-    def dump(self):
+    def dump(self, name):
         o = f'sprintf(buffer, "%ll", {name});\n'
         o += f"out.append(buffer);"
         return o
@@ -514,10 +523,10 @@ class RepeatedTypeInfo(TypeInfo):
 
     @property
     def encode_content(self):
-        return f"""\
-        for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{
-          buffer.{self._ti.encode_func}({self.number}, it, true);
-        }}"""
+        o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n"
+        o += f"  buffer.{self._ti.encode_func}({self.number}, it, true);\n"
+        o += f"}}"
+        return o
 
     @property
     def dump_content(self):
@@ -536,12 +545,13 @@ def build_enum_type(desc):
         out += f"  {v.name} = {v.number},\n"
     out += "};\n"
 
-    cpp = f"template<>\n"
-    cpp += f"const char *proto_enum_to_string<enums::{name}>(enums::{name} value) {{\n"
+    cpp = f"template<> const char *proto_enum_to_string<enums::{name}>(enums::{name} value) {{\n"
     cpp += f"  switch (value) {{\n"
     for v in desc.value:
-        cpp += f'    case enums::{v.name}: return "{v.name}";\n'
-    cpp += f'    default: return "UNKNOWN";\n'
+        cpp += f"    case enums::{v.name}:\n"
+        cpp += f'      return "{v.name}";\n'
+    cpp += f"    default:\n"
+    cpp += f'      return "UNKNOWN";\n'
     cpp += f"  }}\n"
     cpp += f"}}\n"
 
@@ -620,21 +630,35 @@ def build_message_type(desc):
         prot = "bool decode_64bit(uint32_t field_id, Proto64bit value) override;"
         protected_content.insert(0, prot)
 
-    o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{\n"
-    o += indent("\n".join(encode)) + "\n"
+    o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{"
+    if encode:
+        if len(encode) == 1 and len(encode[0]) + len(o) + 3 < 120:
+            o += f" {encode[0]} "
+        else:
+            o += "\n"
+            o += indent("\n".join(encode)) + "\n"
     o += "}\n"
     cpp += o
     prot = "void encode(ProtoWriteBuffer buffer) const override;"
     public_content.append(prot)
 
-    o = f"void {desc.name}::dump_to(std::string &out) const {{\n"
+    o = f"void {desc.name}::dump_to(std::string &out) const {{"
     if dump:
-        o += f"  char buffer[64];\n"
-        o += f'  out.append("{desc.name} {{\\n");\n'
-        o += indent("\n".join(dump)) + "\n"
-        o += f'  out.append("}}");\n'
+        if len(dump) == 1 and len(dump[0]) + len(o) + 3 < 120:
+            o += f" {dump[0]} "
+        else:
+            o += "\n"
+            o += f"  char buffer[64];\n"
+            o += f'  out.append("{desc.name} {{\\n");\n'
+            o += indent("\n".join(dump)) + "\n"
+            o += f'  out.append("}}");\n'
     else:
-        o += f'  out.append("{desc.name} {{}}");\n'
+        o2 = f'out.append("{desc.name} {{}}");'
+        if len(o) + len(o2) + 3 < 120:
+            o += f" {o2} "
+        else:
+            o += "\n"
+            o += f"  {o2}\n"
     o += "}\n"
     cpp += o
     prot = "void dump_to(std::string &out) const override;"
@@ -643,8 +667,11 @@ def build_message_type(desc):
     out = f"class {desc.name} : public ProtoMessage {{\n"
     out += " public:\n"
     out += indent("\n".join(public_content)) + "\n"
+    out += "\n"
     out += " protected:\n"
-    out += indent("\n".join(protected_content)) + "\n"
+    out += indent("\n".join(protected_content))
+    if len(protected_content) > 0:
+        out += "\n"
     out += "};\n"
     return out, cpp
 
@@ -814,13 +841,13 @@ cases.sort()
 hpp += " protected:\n"
 hpp += f"  bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n"
 out = f"bool {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n"
-out += f"  switch(msg_type) {{\n"
+out += f"  switch (msg_type) {{\n"
 for i, case in cases:
     c = f"case {i}: {{\n"
     c += indent(case) + "\n"
     c += f"}}"
     out += indent(c, "    ") + "\n"
-out += "    default: \n"
+out += "    default:\n"
 out += "      return false;\n"
 out += "  }\n"
 out += "  return true;\n"
diff --git a/script/build_jsonschema.py b/script/build_jsonschema.py
index 6d19e25e29..89d621fd5a 100644
--- a/script/build_jsonschema.py
+++ b/script/build_jsonschema.py
@@ -1,5 +1,6 @@
 #!/usr/bin/env python3
 
+from esphome.cpp_generator import MockObj
 import json
 import argparse
 import os
@@ -54,7 +55,7 @@ def is_ref(jschema):
 
 
 def unref(jschema):
-    return definitions[jschema[JSC_REF][len("#/definitions/") :]]
+    return definitions.get(jschema[JSC_REF][len("#/definitions/") :])
 
 
 def add_definition_array_or_single_object(ref):
@@ -104,8 +105,11 @@ def add_registry(registry_name, registry):
     for name in registry.keys():
         schema = get_jschema(str(name), registry[name].schema, create_return_ref=False)
         if not schema:
-            schema = {"type": "string"}
+            schema = {"type": "null"}
         o_schema = {"type": "object", JSC_PROPERTIES: {name: schema}}
+        o_schema = create_ref(
+            registry_name + "-" + name, str(registry[name].schema) + "x", o_schema
+        )
         validators.append(o_schema)
     definitions[registry_name] = {JSC_ANYOF: validators}
 
@@ -134,7 +138,7 @@ def add_module_schemas(name, module):
 
 
 def get_dirs():
-    from esphome.config import CORE_COMPONENTS_PATH
+    from esphome.loader import CORE_COMPONENTS_PATH
 
     dir_names = [
         d
@@ -146,7 +150,7 @@ def get_dirs():
 
 
 def get_logger_tags():
-    from esphome.config import CORE_COMPONENTS_PATH
+    from esphome.loader import CORE_COMPONENTS_PATH
     import glob
 
     pattern = re.compile(r'^static const char(\*\s|\s\*)TAG = "(\w.*)";', re.MULTILINE)
@@ -241,7 +245,7 @@ def add_components():
 
             elif c.config_schema is not None:
                 # adds root components which are not platforms, e.g. api: logger:
-                if c.is_multi_conf:
+                if c.multi_conf:
                     schema = get_jschema(domain, c.config_schema)
                     schema = add_definition_array_or_single_object(schema)
                 else:
@@ -322,7 +326,6 @@ def get_entry(parent_key, vschema):
     elif str(vschema) in ejs.list_schemas:
         ref = get_jschema(parent_key, ejs.list_schemas[str(vschema)][0])
         entry = {JSC_ANYOF: [ref, {"type": "array", "items": ref}]}
-
     elif str(vschema) in ejs.typed_schemas:
         schema_types = [{"type": "object", "properties": {"type": {"type": "string"}}}]
         entry = {"allOf": schema_types}
@@ -342,6 +345,22 @@ def get_entry(parent_key, vschema):
             entry = get_automation_schema(parent_key, inner_vschema)
         elif type == "maybe":
             entry = get_jschema(parent_key, inner_vschema)
+        elif type == "one_of":
+            entry = {"enum": list(inner_vschema)}
+        elif type == "enum":
+            entry = {"enum": list(inner_vschema.keys())}
+        elif type == "effects":
+            # Like list schema but subset from list.
+            subset_list = inner_vschema[0]
+            # get_jschema('strobex', registry['strobe'].schema)
+            registry_schemas = []
+            for name in subset_list:
+                registry_schemas.append(get_ref("light.EFFECTS_REGISTRY-" + name))
+
+            entry = {
+                JSC_ANYOF: [{"type": "array", "items": {JSC_ANYOF: registry_schemas}}]
+            }
+
         else:
             raise ValueError("Unknown extracted schema type")
     elif str(vschema).startswith("<function invalid."):
@@ -374,7 +393,10 @@ def default_schema():
 
 def is_default_schema(jschema):
     if is_ref(jschema):
-        return is_default_schema(unref(jschema))
+        jschema = unref(jschema)
+        if not jschema:
+            return False
+        return is_default_schema(jschema)
     return "type" in jschema and jschema["type"] == default_schema()["type"]
 
 
@@ -512,6 +534,10 @@ def convert_schema(path, vschema, un_extend=True):
 
     # When schema contains all, all also has a schema which points
     # back to the containing schema
+
+    if isinstance(vschema, MockObj):
+        return output
+
     while hasattr(vschema, "schema") and not hasattr(vschema, "validators"):
         vschema = vschema.schema
 
@@ -531,7 +557,6 @@ def convert_schema(path, vschema, un_extend=True):
                         output = val_schema
                     else:
                         output = {**output, **val_schema}
-
         return output
 
     if not vschema:
@@ -679,6 +704,7 @@ def dump_schema():
         pins.output_pin,
         pins.input_pin,
         pins.input_pullup_pin,
+        cv.float_with_unit,
         cv.subscribe_topic,
         cv.publish_topic,
         cv.mqtt_payload,
@@ -698,9 +724,13 @@ def dump_schema():
 
     for v in [pins.gpio_input_pin_schema, pins.gpio_input_pullup_pin_schema]:
         schema_registry[v] = get_ref("PIN.GPIO_FULL_INPUT_PIN_SCHEMA")
+    for v in [pins.internal_gpio_input_pin_schema, pins.input_pin]:
+        schema_registry[v] = get_ref("PIN.INPUT_INTERNAL")
 
     for v in [pins.gpio_output_pin_schema, pins.internal_gpio_output_pin_schema]:
         schema_registry[v] = get_ref("PIN.GPIO_FULL_OUTPUT_PIN_SCHEMA")
+    for v in [pins.internal_gpio_output_pin_schema, pins.output_pin]:
+        schema_registry[v] = get_ref("PIN.OUTPUT_INTERNAL")
 
     add_module_schemas("CONFIG", cv)
     get_jschema("POLLING_COMPONENT", cv.polling_component_schema("60s"))
diff --git a/script/clang-format b/script/clang-format
index e9c3692bb8..bb2b722e1c 100755
--- a/script/clang-format
+++ b/script/clang-format
@@ -31,7 +31,7 @@ def run_format(args, queue, lock):
     """Takes filenames out of queue and runs clang-tidy on them."""
     while True:
         path = queue.get()
-        invocation = ['clang-format-7']
+        invocation = ['clang-format-11']
         if args.inplace:
             invocation.append('-i')
         invocation.append(path)
@@ -69,12 +69,12 @@ def main():
     args = parser.parse_args()
 
     try:
-        get_output('clang-format-7', '-version')
+        get_output('clang-format-11', '-version')
     except:
         print("""
         Oops. It looks like clang-format is not installed. 
         
-        Please check you can run "clang-format-7 -version" in your terminal and install
+        Please check you can run "clang-format-11 -version" in your terminal and install
         clang-format (v7) if necessary.
         
         Note you can also upload your code as a pull request on GitHub and see the CI check
diff --git a/script/clang-tidy b/script/clang-tidy
index 0bd1ef51fa..0bf17f9076 100755
--- a/script/clang-tidy
+++ b/script/clang-tidy
@@ -30,7 +30,7 @@ else:
 def run_tidy(args, tmpdir, queue, lock, failed_files):
     while True:
         path = queue.get()
-        invocation = ['clang-tidy-7', '-header-filter=^{}/.*'.format(re.escape(basepath))]
+        invocation = ['clang-tidy-11', '-header-filter=^{}/.*'.format(re.escape(basepath))]
         if tmpdir is not None:
             invocation.append('-export-fixes')
             # Get a temporary file. We immediately close the handle so clang-tidy can
@@ -90,13 +90,13 @@ def main():
     args = parser.parse_args()
 
     try:
-        get_output('clang-tidy-7', '-version')
+        get_output('clang-tidy-11', '-version')
     except:
         print("""
-        Oops. It looks like clang-tidy is not installed.
+        Oops. It looks like clang-tidy-11 is not installed.
 
-        Please check you can run "clang-tidy-7 -version" in your terminal and install
-        clang-tidy (v7) if necessary.
+        Please check you can run "clang-tidy-11 -version" in your terminal and install
+        clang-tidy (v11) if necessary.
 
         Note you can also upload your code as a pull request on GitHub and see the CI check
         output to apply clang-tidy.
@@ -163,7 +163,7 @@ def main():
     if args.fix and failed_files:
         print('Applying fixes ...')
         try:
-            subprocess.call(['clang-apply-replacements-7', tmpdir])
+            subprocess.call(['clang-apply-replacements-11', tmpdir])
         except:
             print('Error applying fixes.\n', file=sys.stderr)
             raise
diff --git a/script/setup b/script/setup
index 199b46891d..6d095af46c 100755
--- a/script/setup
+++ b/script/setup
@@ -4,7 +4,7 @@
 set -e
 
 cd "$(dirname "$0")/.."
-pip3 install -r requirements.txt -r requirements_test.txt
+pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
 pip3 install -e .
 
 pre-commit install
diff --git a/script/test b/script/test
index a6d99c8f62..9f5dca65fa 100755
--- a/script/test
+++ b/script/test
@@ -6,7 +6,8 @@ cd "$(dirname "$0")/.."
 
 set -x
 
-esphome tests/test1.yaml compile
-esphome tests/test2.yaml compile
-esphome tests/test3.yaml compile
-esphome tests/test4.yaml compile
+esphome compile tests/test1.yaml
+esphome compile tests/test2.yaml
+esphome compile tests/test3.yaml
+esphome compile tests/test4.yaml
+esphome compile tests/test5.yaml
diff --git a/tests/README.md b/tests/README.md
index c8962dedc1..546025526f 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -17,9 +17,10 @@ be tested on the same device.
 
 Current test_.yaml file contents.
 
-| Test name | Platform | Network |
-|-|-|-|
-| test1.yaml | ESP32 | wifi |
-| test2.yaml | ESP32 | ethernet |
-| test3.yaml | ESP8266 | wifi |
-| test4.yaml | ESP32 | ethernet |
+| Test name | Platform | Network | BLE |
+|-|-|-|-|
+| test1.yaml | ESP32 | wifi | None
+| test2.yaml | ESP32 | ethernet | esp32_ble_tracker
+| test3.yaml | ESP8266 | wifi | N/A
+| test4.yaml | ESP32 | ethernet | None
+| test5.yaml | ESP32 | wifi | ble_server
diff --git a/tests/custom.h b/tests/custom.h
index 278e300785..f1a35b9b3c 100644
--- a/tests/custom.h
+++ b/tests/custom.h
@@ -49,6 +49,7 @@ class CustomNativeAPI : public Component, public CustomAPIDevice {
     register_service(&CustomNativeAPI::on_start_dryer, "start_dryer", {"value"});
     register_service(&CustomNativeAPI::on_many_values, "many_values", {"bool", "int", "float", "str1", "str2"});
     subscribe_homeassistant_state(&CustomNativeAPI::on_light_state, "light.my_light");
+    subscribe_homeassistant_state(&CustomNativeAPI::on_brightness_state, "light.my_light", "brightness");
   }
 
   void on_hello_world() { ESP_LOGD("custom_api", "Hello World from native API service!"); }
@@ -69,4 +70,5 @@ class CustomNativeAPI : public Component, public CustomAPIDevice {
     call_homeassistant_service("homeassistant.restart");
   }
   void on_light_state(std::string state) { ESP_LOGD("custom_api", "Got state %s", state.c_str()); }
+  void on_brightness_state(std::string state) { ESP_LOGD("custom_api", "Got attribute state %s", state.c_str()); }
 };
diff --git a/tests/livingroom8266.cpp b/tests/dummy_main.cpp
similarity index 59%
rename from tests/livingroom8266.cpp
rename to tests/dummy_main.cpp
index ad017ce73b..c3b192d15f 100644
--- a/tests/livingroom8266.cpp
+++ b/tests/dummy_main.cpp
@@ -1,3 +1,8 @@
+// Dummy main.cpp file for the PlatformIO project in the git repository.
+// Primarily used to get IDE integration working (so the contents here don't
+// 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>
@@ -7,7 +12,7 @@
 using namespace esphome;
 
 void setup() {
-  App.pre_setup("livingroom", __DATE__ " " __TIME__);
+  App.pre_setup("livingroom", __DATE__ ", " __TIME__, false);
   auto *log = new logger::Logger(115200, 512, logger::UART_SELECTION_UART0);
   log->pre_setup();
   App.register_component(log);
@@ -19,10 +24,12 @@ void setup() {
   ap.set_password("password1");
   wifi->add_sta(ap);
 
-  auto *ota = new ota::OTAComponent(8266);
-  ota->start_safe_mode();
+  auto *ota = new ota::OTAComponent();
+  ota->set_port(8266);
 
-  auto *gpio = new gpio::GPIOSwitch("GPIO Switch", new GPIOPin(8, OUTPUT));
+  auto *gpio = new gpio::GPIOSwitch();
+  gpio->set_name("GPIO Switch");
+  gpio->set_pin(new GPIOPin(8, OUTPUT, false));
   App.register_component(gpio);
   App.register_switch(gpio);
 
diff --git a/tests/livingroom32.cpp b/tests/livingroom32.cpp
deleted file mode 100644
index 7005ec95e0..0000000000
--- a/tests/livingroom32.cpp
+++ /dev/null
@@ -1,26 +0,0 @@
-#include <esphome.h>
-
-using namespace esphome;
-
-void setup() {
-  App.set_name("livingroom32");
-  App.init_log();
-
-  App.init_wifi("YOUR_SSID", "YOUR_PASSWORD");
-  App.init_mqtt("MQTT_HOST", "USERNAME", "PASSWORD");
-  App.init_ota()->start_safe_mode();
-
-  // LEDC is only available on ESP32! for the ESP8266, take a look at App.make_esp8266_pwm_output().
-  auto *red = App.make_ledc_output(32);  // on pin 32
-  auto *green = App.make_ledc_output(33);
-  auto *blue = App.make_ledc_output(34);
-  App.make_rgb_light("Livingroom Light", red, green, blue);
-
-  App.make_dht_sensor("Livingroom Temperature", "Livingroom Humidity", 12);
-  App.make_status_binary_sensor("Livingroom Node Status");
-  App.make_restart_switch("Livingroom Restart");
-
-  App.setup();
-}
-
-void loop() { App.loop(); }
diff --git a/tests/test1.yaml b/tests/test1.yaml
index 8fedf4eb20..29df5857d3 100644
--- a/tests/test1.yaml
+++ b/tests/test1.yaml
@@ -247,10 +247,10 @@ ble_client:
     id: ble_blah
     on_connect:
       then:
-       - switch.turn_on: ble1_status
+        - switch.turn_on: ble1_status
     on_disconnect:
       then:
-       - switch.turn_on: ble1_status
+        - switch.turn_on: ble1_status
 mcp23s08:
   - id: 'mcp23s08_hub'
     cs_pin: GPIO12
@@ -261,16 +261,18 @@ mcp23s17:
     cs_pin: GPIO12
     deviceaddress: 1
 
-
 sensor:
   - platform: ble_client
     ble_client_id: ble_foo
-    name: "Green iTag btn"
+    name: 'Green iTag btn'
     service_uuid: 'ffe0'
     characteristic_uuid: 'ffe1'
     descriptor_uuid: 'ffe2'
     notify: true
     update_interval: never
+    lambda: |-
+      ESP_LOGD("main", "Length of data is %i", x.size());
+      return x[0];
     on_notify:
       then:
         - lambda: |-
@@ -659,9 +661,14 @@ sensor:
     update_interval: 15s
   - platform: pulse_meter
     name: 'Pulse Meter'
+    id: pulse_meter_sensor
     pin: GPIO12
     internal_filter: 100ms
     timeout: 2 min
+    on_value:
+      - pulse_meter.set_total_pulses:
+          id: pulse_meter_sensor
+          value: 12345
     total:
       name: 'Pulse Meter Total'
   - platform: rotary_encoder
@@ -786,6 +793,7 @@ sensor:
     update_interval: 15s
   - platform: template
     name: 'Template Sensor'
+    state_class: measurement
     id: template_sensor
     lambda: |-
       if (id(ultrasonic_sensor1).state > 1) {
@@ -883,25 +891,11 @@ sensor:
       name: 'AQI'
       calculation_type: 'CAQI'
   - platform: teleinfo
-    uart_id: uart0
-    tags:
-      - tag_name: 'HCHC'
-        sensor:
-          name: 'hchc'
-          unit_of_measurement: 'Wh'
-          icon: mdi:flash
-      - tag_name: 'HCHP'
-        sensor:
-          name: 'hchp'
-          unit_of_measurement: 'Wh'
-          icon: mdi:flash
-      - tag_name: 'PAPP'
-        sensor:
-          name: 'papp'
-          unit_of_measurement: 'VA'
-          icon: mdi:flash
-    update_interval: 60s
-    historical_mode: true
+    tag_name: "HCHC"
+    name: "hchc"
+    unit_of_measurement: "Wh"
+    icon: mdi:flash
+    teleinfo_id: myteleinfo
   - platform: mcp9808
     name: 'MCP9808 Temperature'
     update_interval: 15s
@@ -909,6 +903,28 @@ sensor:
     id: ph_ezo
     address: 99
     unit_of_measurement: 'pH'
+  - platform: cs5460a
+    id: cs5460a1
+    current:
+      name: "Socket current"
+    voltage:
+      name: "Mains voltage"
+    power:
+      name: "Socket power"
+      on_value:
+        then:
+          cs5460a.restart: cs5460a1
+    samples: 1600
+    pga_gain: 10X
+    current_gain: 0.01
+    voltage_gain: 0.000573
+    current_hpf: on
+    voltage_hpf: on
+    phase_offset: 20
+    pulse_energy: 0.01 kWh
+    cs_pin:
+      mcp23xxx: mcp23017_hub
+      number: 14
 
 esp32_touch:
   setup_mode: False
@@ -1509,23 +1525,6 @@ climate:
     name: Toshiba Climate
   - platform: hitachi_ac344
     name: Hitachi Climate
-  - platform: midea_ac
-    visual:
-      min_temperature: 18 °C
-      max_temperature: 25 °C
-      temperature_step: 0.1 °C
-    name: 'Electrolux EACS'
-    beeper: true
-    outdoor_temperature:
-      name: 'Temp'
-    power_usage:
-      name: 'Power'
-    humidity_setpoint:
-      name: 'Hum'
-
-midea_dongle:
-  uart_id: uart0
-  strength_icon: true
 
 switch:
   - platform: gpio
@@ -1869,6 +1868,12 @@ display:
       - id: page2
         lambda: |-
           // Nothing
+    on_page_change:
+      from: page1
+      to: page2
+      then:
+        lambda: |-
+          ESP_LOGD("display", "1 -> 2");
   - platform: ssd1306_spi
     model: 'SSD1306 128x64'
     cs_pin: GPIO23
@@ -1942,6 +1947,7 @@ display:
     row_start: 0
     lambda: |-
       it.rectangle(0, 0, it.get_width(), it.get_height());
+
 tm1651:
   id: tm1651_battery
   clk_pin: GPIO23
@@ -1964,6 +1970,12 @@ pn532_spi:
     - mqtt.publish:
         topic: the/topic
         payload: !lambda 'return x;'
+  on_tag_removed:
+    - lambda: |-
+        ESP_LOGD("main", "Removed tag %s", x.c_str());
+    - mqtt.publish:
+        topic: the/topic
+        payload: !lambda 'return x;'
 
 pn532_i2c:
 
@@ -2043,7 +2055,6 @@ tca9548a:
     multiplexer:
       id: multiplex0
       channel: 0
-    
 
 pcf8574:
   - id: 'pcf8574_hub'
@@ -2125,6 +2136,10 @@ text_sensor:
   - platform: version
     name: 'ESPHome Version No Timestamp'
     hide_timestamp: True
+  - platform: teleinfo
+    tag_name: "OPTARIF"
+    name: "optarif"
+    teleinfo_id: myteleinfo
 
 sn74hc595:
   - id: 'sn74hc595_hub'
@@ -2155,3 +2170,9 @@ canbus:
                 lambda: 'return x[0] == 0x11;'
               then:
                 light.toggle: ${roomname}_lights
+
+teleinfo:
+  id: myteleinfo
+  uart_id: uart0
+  update_interval: 60s
+  historical_mode: true
diff --git a/tests/test2.yaml b/tests/test2.yaml
index 34724ee955..faa76300cc 100644
--- a/tests/test2.yaml
+++ b/tests/test2.yaml
@@ -63,6 +63,10 @@ sensor:
   - platform: homeassistant
     entity_id: sensor.hello_world
     id: ha_hello_world
+  - platform: homeassistant
+    entity_id: climate.living_room
+    attribute: temperature
+    id: ha_hello_world_temperature
   - platform: ble_rssi
     mac_address: AC:37:43:77:5F:4C
     name: 'BLE Google Home Mini RSSI value'
@@ -246,6 +250,10 @@ binary_sensor:
   - platform: homeassistant
     entity_id: binary_sensor.hello_world
     id: ha_hello_world_binary
+  - platform: homeassistant
+    entity_id: binary_sensor.hello
+    attribute: world
+    id: ha_hello_world_binary_attribute
   - platform: ble_presence
     mac_address: AC:37:43:77:5F:4C
     name: 'ESP32 BLE Tracker Google Home Mini'
@@ -346,6 +354,8 @@ text_sensor:
       - homeassistant.tag_scanned: 1234-abcd
       - deep_sleep.enter:
           sleep_duration: 30min
+      - deep_sleep.enter:
+          sleep_duration: !lambda "return 30 * 60 * 1000;"
   - platform: template
     name: 'Template Text Sensor'
     lambda: |-
@@ -353,6 +363,10 @@ text_sensor:
   - platform: homeassistant
     entity_id: sensor.hello_world2
     id: ha_hello_world2
+  - platform: homeassistant
+    entity_id: sensor.hello_world3
+    id: ha_hello_world3
+    attribute: some_attribute
   - platform: ble_scanner
     name: Scanner
 
diff --git a/tests/test3.yaml b/tests/test3.yaml
index 8104c70fbf..af5398b604 100644
--- a/tests/test3.yaml
+++ b/tests/test3.yaml
@@ -1,6 +1,6 @@
 esphome:
-  name: $devicename
-  comment: $devicecomment
+  name: $device_name
+  comment: $device_comment
   platform: ESP8266
   board: d1_mini
   build_path: build/test3
@@ -13,8 +13,8 @@ esphome:
     - custom.h
 
 substitutions:
-  devicename: test3
-  devicecomment: test3 device
+  device_name: test3
+  device_comment: test3 device
   min_sub: '0.03'
   max_sub: '12.0%'
 
@@ -141,6 +141,14 @@ api:
       then:
         - dfplayer.random
 
+    - service: dfplayer_volume_up
+      then:
+        - dfplayer.volume_up
+
+    - service: dfplayer_volume_down
+      then:
+        - dfplayer.volume_down
+
     - service: battery_level_percent
       variables:
         level_percent: int
@@ -213,9 +221,33 @@ spi:
   miso_pin: GPIO14
 
 uart:
-  - tx_pin: GPIO1
+  - id: uart1
+    tx_pin: GPIO1
     rx_pin: GPIO3
     baud_rate: 115200
+  - id: uart2
+    tx_pin: GPIO4
+    rx_pin: GPIO5
+    baud_rate: 9600
+  - id: uart3
+    tx_pin: GPIO4
+    rx_pin: GPIO5
+    baud_rate: 4800
+  - id: uart4
+    tx_pin: GPIO4
+    rx_pin: GPIO5
+    baud_rate: 9600
+  - id: uart5
+    tx_pin: GPIO4
+    rx_pin: GPIO5
+    baud_rate: 9600
+  - id: uart6
+    tx_pin: GPIO4
+    rx_pin: GPIO5
+    baud_rate: 9600
+
+modbus:
+  uart_id: uart1
 
 ota:
   safe_mode: True
@@ -369,6 +401,7 @@ sensor:
     active_power_b:
       name: ADE7953 Active Power B
   - platform: pzem004t
+    uart_id: uart3
     voltage:
       name: 'PZEM00T Voltage'
     current:
@@ -408,6 +441,7 @@ sensor:
       name: 'AQI'
       calculation_type: 'AQI'
   - platform: pmsx003
+    uart_id: uart2
     type: PMSX003
     pm_1_0:
       name: 'PM 1.0 Concentration'
@@ -415,25 +449,8 @@ sensor:
       name: 'PM 2.5 Concentration'
     pm_10_0:
       name: 'PM 10.0 Concentration'
-  - platform: pmsx003
-    type: PMS5003T
-    pm_2_5:
-      name: 'PM 2.5 Concentration'
-    temperature:
-      name: 'PMS Temperature'
-    humidity:
-      name: 'PMS Humidity'
-  - platform: pmsx003
-    type: PMS5003ST
-    pm_2_5:
-      name: 'PM 2.5 Concentration'
-    temperature:
-      name: 'PMS Temperature'
-    humidity:
-      name: 'PMS Humidity'
-    formaldehyde:
-      name: 'PMS Formaldehyde Concentration'
   - platform: cse7766
+    uart_id: uart3
     voltage:
       name: 'CSE7766 Voltage'
     current:
@@ -443,7 +460,7 @@ sensor:
   - platform: ezo
     id: ph_ezo
     address: 99
-    unit_of_measurement: 'pH'  
+    unit_of_measurement: 'pH'
   - platform: tof10120
     name: "Distance sensor"
     update_interval: 5s
@@ -460,6 +477,62 @@ sensor:
       name: "Fingerprint Last Finger ID"
     last_confidence:
       name: "Fingerprint Last Confidence"
+  - platform: sdm_meter
+    phase_a:
+      current:
+        name: 'Phase A Current'
+      voltage:
+        name: 'Phase A Voltage'
+      active_power:
+        name: 'Phase A Power'
+      power_factor:
+        name: 'Phase A Power Factor'
+      apparent_power:
+        name: 'Phase A Apparent Power'
+      reactive_power:
+        name: 'Phase A Reactive Power'
+      phase_angle:
+        name: 'Phase A Phase Angle'
+    phase_b:
+      current:
+        name: 'Phase B Current'
+      voltage:
+        name: 'Phase B Voltage'
+      active_power:
+        name: 'Phase B Power'
+      power_factor:
+        name: 'Phase B Power Factor'
+      apparent_power:
+        name: 'Phase B Apparent Power'
+      reactive_power:
+        name: 'Phase B Reactive Power'
+      phase_angle:
+        name: 'Phase B Phase Angle'
+    phase_c:
+      current:
+        name: 'Phase C Current'
+      voltage:
+        name: 'Phase C Voltage'
+      active_power:
+        name: 'Phase C Power'
+      power_factor:
+        name: 'Phase C Power Factor'
+      apparent_power:
+        name: 'Phase C Apparent Power'
+      reactive_power:
+        name: 'Phase C Reactive Power'
+      phase_angle:
+        name: 'Phase C Phase Angle'
+    frequency:
+      name: 'Frequency'
+    import_active_energy:
+      name: 'Import Active Energy'
+    export_active_energy:
+      name: 'Export Active Energy'
+    import_reactive_energy:
+      name: 'Import Reactive Energy'
+    export_reactive_energy:
+      name: 'Export Reactive Energy'
 
 time:
   - platform: homeassistant
@@ -585,6 +658,17 @@ script:
   - id: my_script
     then:
       - lambda: 'ESP_LOGD("main", "Hello World!");'
+  - id: climate_custom
+    then:
+      - climate.control:
+          id: midea_ac_unit
+          custom_preset: FREEZE_PROTECTION
+          custom_fan_mode: SILENT
+  - id: climate_preset
+    then:
+      - climate.control:
+          id: midea_ac_unit
+          preset: SLEEP
 
 sm2135:
   data_pin: GPIO12
@@ -746,6 +830,32 @@ climate:
       kp: 0.0
       ki: 0.0
       kd: 0.0
+  - platform: midea_ac
+    id: midea_ac_unit
+    visual:
+      min_temperature: 18 °C
+      max_temperature: 25 °C
+      temperature_step: 0.1 °C
+    name: "Electrolux EACS"
+    beeper: true
+    custom_fan_modes:
+      - SILENT
+      - TURBO
+    preset_eco: true
+    preset_sleep: true
+    preset_boost: true
+    custom_presets:
+      - FREEZE_PROTECTION
+    outdoor_temperature:
+      name: "Temp"
+    power_usage:
+      name: "Power"
+    humidity_setpoint:
+      name: "Hum"
+
+midea_dongle:
+  uart_id: uart1
+  strength_icon: true
 
 cover:
   - platform: endstop
@@ -867,6 +977,7 @@ light:
     effects:
       - wled:
       - adalight:
+          uart_id: uart3
       - e131:
           universe: 1
   - platform: hbridge
@@ -888,6 +999,7 @@ ttp229_bsf:
   scl_pin: D1
 
 sim800l:
+  uart_id: uart4
   on_sms_received:
     - lambda: |-
         std::string str;
@@ -900,6 +1012,7 @@ sim800l:
         recipient: '+1234'
 
 dfplayer:
+  uart_id: uart5
   on_finished_playback:
     then:
       if:
@@ -913,6 +1026,7 @@ tm1651:
   dio_pin: D5
 
 rf_bridge:
+  uart_id: uart5
   on_code_received:
     - lambda: |-
         uint32_t test;
@@ -1006,3 +1120,4 @@ fingerprint_grow:
         event: esphome.${devicename}_fingerprint_grow_enrollment_failed
         data:
           finger_id: !lambda 'return finger_id;'
+  uart_id: uart6
diff --git a/tests/test4.yaml b/tests/test4.yaml
index e85cdfbc19..7868fd4968 100644
--- a/tests/test4.yaml
+++ b/tests/test4.yaml
@@ -86,6 +86,29 @@ binary_sensor:
   - platform: tuya
     id: tuya_binary_sensor
     sensor_datapoint: 1
+  - platform: template
+    id: ar1
+    lambda: 'return {};'
+    filters:
+      - autorepeat:
+        - delay: 2s
+          time_off: 100ms
+          time_on: 900ms
+        - delay: 4s
+          time_off: 100ms
+          time_on: 400ms
+    on_state:
+      then:
+        - lambda: 'ESP_LOGI("ar1:", "%d", x);'
+  - platform: xpt2046
+    xpt2046_id: touchscreen
+    id: touch_key0
+    x_min: 80
+    x_max: 160
+    y_min: 106
+    y_max: 212
+    on_state:
+      - lambda: 'ESP_LOGI("main", "key0: %s", (x ? "touch" : "release"));'
 
 climate:
   - platform: tuya
@@ -110,6 +133,17 @@ light:
     rgb_order: GRB
     default_transition_length: 0s
     color_correct: [50%, 50%, 50%]
+  - platform: tuya
+    id: tuya_light
+    switch_datapoint: 1
+    dimmer_datapoint: 2
+    min_value_datapoint: 3
+    color_temperature_datapoint: 4
+    min_value: 1
+    max_value: 100
+    cold_white_color_temperature: 153 mireds
+    warm_white_color_temperature: 500 mireds
+    gamma_correct: 1
 
 display:
   - platform: addressable_light
@@ -132,6 +166,7 @@ display:
       it.rectangle(3, 3, it.get_width()-6, it.get_height()-6, red);
     rotation: 0°
     update_interval: 16ms
+    
   - platform: waveshare_epaper
     cs_pin: GPIO23
     dc_pin: GPIO23
@@ -160,9 +195,51 @@ display:
     lambda: |-
       it.rectangle(0, 0, it.get_width(), it.get_height());
 
+esp32_camera:
+  name: ESP-32 Camera
+  data_pins: [GPIO17, GPIO35, GPIO34, GPIO5, GPIO39, GPIO18, GPIO36, GPIO19]
+  vsync_pin: GPIO22
+  href_pin: GPIO26
+  pixel_clock_pin: GPIO21
+  external_clock:
+    pin: GPIO27
+    frequency: 20MHz
+  i2c_pins:
+    sda: GPIO25
+    scl: GPIO23
+  reset_pin: GPIO15
+  power_down_pin: GPIO1
+  resolution: 640x480
+  jpeg_quality: 10
+
 external_components:
   - source: github://esphome/esphome@dev
     refresh: 1d
     components: ["bh1750"]
   - source: ../esphome/components
     components: ["sntp"]
+xpt2046:
+  id: touchscreen
+  cs_pin: 17
+  irq_pin: 16
+  update_interval: 50ms
+  report_interval: 1s
+  threshold: 400
+  dimension_x: 240
+  dimension_y: 320
+  calibration_x_min: 3860
+  calibration_x_max: 280
+  calibration_y_min: 340
+  calibration_y_max: 3860
+  swap_x_y: False
+  on_state:
+    - lambda: |-
+        ESP_LOGI("main", "args x=%d, y=%d, touched=%s", x, y, (touched ? "touch" : "release"));
+        ESP_LOGI("main", "member x=%d, y=%d, touched=%d, x_raw=%d, y_raw=%d, z_raw=%d",
+            id(touchscreen).x,
+            id(touchscreen).y,
+            (int) id(touchscreen).touched,
+            id(touchscreen).x_raw,
+            id(touchscreen).y_raw,
+            id(touchscreen).z_raw
+            );
diff --git a/tests/test5.yaml b/tests/test5.yaml
new file mode 100644
index 0000000000..d4e19d985b
--- /dev/null
+++ b/tests/test5.yaml
@@ -0,0 +1,39 @@
+esphome:
+  name: test5
+  platform: ESP32
+  board: nodemcu-32s
+  build_path: build/test5
+  project:
+    name: esphome.test5_project
+    version: "1.0.0"
+
+wifi:
+  networks:
+    - ssid: 'MySSID'
+      password: 'password1'
+
+api:
+
+ota:
+
+logger:
+
+binary_sensor:
+  - platform: gpio
+    pin: GPIO0
+    id: io0_button
+
+output:
+  - platform: gpio
+    pin: GPIO2
+    id: built_in_led
+
+esp32_ble:
+  server:
+    manufacturer: "ESPHome"
+    model: "Test5"
+
+esp32_improv:
+  authorizer: io0_button
+  authorized_duration: 1min
+  status_indicator: built_in_led
diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py
index 37a4920224..4e60880033 100644
--- a/tests/unit_tests/test_core.py
+++ b/tests/unit_tests/test_core.py
@@ -490,11 +490,15 @@ class TestEsphomeCore:
 
     def test_reset(self, target):
         """Call reset on target and compare to new instance"""
-        other = core.EsphomeCore()
+        other = core.EsphomeCore().__dict__
 
         target.reset()
+        t = target.__dict__
+        # ignore event loop
+        del other["event_loop"]
+        del t["event_loop"]
 
-        assert target.__dict__ == other.__dict__
+        assert t == other
 
     def test_address__none(self, target):
         target.config = {}
diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py
index c6f37f6b5d..3e317589a9 100644
--- a/tests/unit_tests/test_cpp_helpers.py
+++ b/tests/unit_tests/test_cpp_helpers.py
@@ -6,25 +6,24 @@ from esphome import const
 from esphome.cpp_generator import MockObj
 
 
-def test_gpio_pin_expression__conf_is_none(monkeypatch):
-    target = ch.gpio_pin_expression(None)
-
-    actual = next(target)
+@pytest.mark.asyncio
+async def test_gpio_pin_expression__conf_is_none(monkeypatch):
+    actual = await ch.gpio_pin_expression(None)
 
     assert actual is None
 
 
-def test_gpio_pin_expression__new_pin(monkeypatch):
-    target = ch.gpio_pin_expression(
+@pytest.mark.asyncio
+async def test_gpio_pin_expression__new_pin(monkeypatch):
+    actual = await ch.gpio_pin_expression(
         {const.CONF_NUMBER: 42, const.CONF_MODE: "input", const.CONF_INVERTED: False}
     )
 
-    actual = next(target)
-
     assert isinstance(actual, MockObj)
 
 
-def test_register_component(monkeypatch):
+@pytest.mark.asyncio
+async def test_register_component(monkeypatch):
     var = Mock(base="foo.bar")
 
     app_mock = Mock(register_component=Mock(return_value=var))
@@ -36,9 +35,7 @@ def test_register_component(monkeypatch):
     add_mock = Mock()
     monkeypatch.setattr(ch, "add", add_mock)
 
-    target = ch.register_component(var, {})
-
-    actual = next(target)
+    actual = await ch.register_component(var, {})
 
     assert actual is var
     add_mock.assert_called_once()
@@ -46,18 +43,19 @@ def test_register_component(monkeypatch):
     assert core_mock.component_ids == []
 
 
-def test_register_component__no_component_id(monkeypatch):
+@pytest.mark.asyncio
+async def test_register_component__no_component_id(monkeypatch):
     var = Mock(base="foo.eek")
 
     core_mock = Mock(component_ids=["foo.bar"])
     monkeypatch.setattr(ch, "CORE", core_mock)
 
     with pytest.raises(ValueError, match="Component ID foo.eek was not declared to"):
-        target = ch.register_component(var, {})
-        next(target)
+        await ch.register_component(var, {})
 
 
-def test_register_component__with_setup_priority(monkeypatch):
+@pytest.mark.asyncio
+async def test_register_component__with_setup_priority(monkeypatch):
     var = Mock(base="foo.bar")
 
     app_mock = Mock(register_component=Mock(return_value=var))
@@ -69,7 +67,7 @@ def test_register_component__with_setup_priority(monkeypatch):
     add_mock = Mock()
     monkeypatch.setattr(ch, "add", add_mock)
 
-    target = ch.register_component(
+    actual = await ch.register_component(
         var,
         {
             const.CONF_SETUP_PRIORITY: "123",
@@ -77,8 +75,6 @@ def test_register_component__with_setup_priority(monkeypatch):
         },
     )
 
-    actual = next(target)
-
     assert actual is var
     add_mock.assert_called()
     assert add_mock.call_count == 3