diff --git a/CODEOWNERS b/CODEOWNERS
index 1298d4d43d..8aa96d14af 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -14,6 +14,8 @@ esphome/core/* @esphome/core
 esphome/components/ac_dimmer/* @glmnet
 esphome/components/adc/* @esphome/core
 esphome/components/addressable_light/* @justfalter
+esphome/components/airthings_ble/* @jeromelaban
+esphome/components/airthings_wave_plus/* @jeromelaban
 esphome/components/am43/* @buxtronix
 esphome/components/am43/cover/* @buxtronix
 esphome/components/animation/* @syndlex
@@ -29,6 +31,7 @@ esphome/components/ble_client/* @buxtronix
 esphome/components/bme680_bsec/* @trvrnrth
 esphome/components/canbus/* @danielschramm @mvturnho
 esphome/components/captive_portal/* @OttoWinter
+esphome/components/ccs811/* @habbie
 esphome/components/climate/* @esphome/core
 esphome/components/climate_ir/* @glmnet
 esphome/components/color_temperature/* @jesserockz
@@ -52,6 +55,8 @@ esphome/components/globals/* @esphome/core
 esphome/components/gpio/* @esphome/core
 esphome/components/gps/* @coogle
 esphome/components/havells_solar/* @sourabhjaiswal
+esphome/components/hbridge/fan/* @WeekendWarrior
+esphome/components/hbridge/light/* @DotNetDann
 esphome/components/hitachi_ac424/* @sourabhjaiswal
 esphome/components/homeassistant/* @OttoWinter
 esphome/components/hrxl_maxsonar_wr/* @netmikey
@@ -75,8 +80,7 @@ esphome/components/mcp23x17_base/* @jesserockz
 esphome/components/mcp23xxx_base/* @jesserockz
 esphome/components/mcp2515/* @danielschramm @mvturnho
 esphome/components/mcp9808/* @k7hpn
-esphome/components/midea_ac/* @dudanov
-esphome/components/midea_dongle/* @dudanov
+esphome/components/midea/* @dudanov
 esphome/components/mitsubishi/* @RubyBailey
 esphome/components/network/* @esphome/core
 esphome/components/nextion/* @senexcrenshaw
@@ -90,6 +94,7 @@ esphome/components/ota/* @esphome/core
 esphome/components/output/* @esphome/core
 esphome/components/pid/* @OttoWinter
 esphome/components/pipsolar/* @andreashergert1984
+esphome/components/pm1006/* @habbie
 esphome/components/pmsa003i/* @sjtrny
 esphome/components/pn532/* @OttoWinter @jesserockz
 esphome/components/pn532_i2c/* @OttoWinter @jesserockz
@@ -115,6 +120,7 @@ esphome/components/sht4x/* @sjtrny
 esphome/components/shutdown/* @esphome/core
 esphome/components/sim800l/* @glmnet
 esphome/components/sm2135/* @BoukeHaarsma23
+esphome/components/socket/* @esphome/core
 esphome/components/spi/* @esphome/core
 esphome/components/ssd1322_base/* @kbx81
 esphome/components/ssd1322_spi/* @kbx81
@@ -129,6 +135,7 @@ esphome/components/ssd1351_base/* @kbx81
 esphome/components/ssd1351_spi/* @kbx81
 esphome/components/st7735/* @SenexCrenshaw
 esphome/components/st7789v/* @kbx81
+esphome/components/st7920/* @marsjan155
 esphome/components/substitutions/* @esphome/core
 esphome/components/sun/* @OttoWinter
 esphome/components/switch/* @esphome/core
diff --git a/docker/build.py b/docker/build.py
index 54a279f845..c926b3653b 100755
--- a/docker/build.py
+++ b/docker/build.py
@@ -24,7 +24,7 @@ TYPE_LINT = 'lint'
 TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT]
 
 
-BASE_VERSION = "3.6.0"
+BASE_VERSION = "4.2.0"
 
 
 parser = argparse.ArgumentParser()
diff --git a/esphome/__main__.py b/esphome/__main__.py
index 8d6f2b8f89..121fa7cc9e 100644
--- a/esphome/__main__.py
+++ b/esphome/__main__.py
@@ -256,7 +256,7 @@ def show_logs(config, args, port):
         run_miniterm(config, port)
         return 0
     if get_port_type(port) == "NETWORK" and "api" in config:
-        from esphome.api.client import run_logs
+        from esphome.components.api.client import run_logs
 
         return run_logs(config, port)
     if get_port_type(port) == "MQTT" and "mqtt" in config:
@@ -483,75 +483,9 @@ def parse_args(argv):
         metavar=("key", "value"),
     )
 
-    # 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",
-            "vscode",
-            "update-all",
-        ],
-    )
-
-    # 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
-
-    deprecated_argv_suggestion = None
-
-    if ["dashboard", "config"] == argv[1:3] or ["version"] == argv[1:2]:
-        # this is most likely meant in new-style arg format. do not try compat parsing
-        pass
-    else:
-        try:
-            result, unparsed = compat_parser.parse_known_args(argv[1:])
-            last_option = len(argv) - len(unparsed) - 1 - len(result.configuration)
-            unparsed = [
-                "--device" if arg in ("--upload-port", "--serial-port") else arg
-                for arg in unparsed
-            ]
-            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.
-            pass
-
-    # 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.")
@@ -701,7 +635,83 @@ def parse_args(argv):
         "configuration", help="Your YAML configuration file directories.", nargs="+"
     )
 
-    return parser.parse_args(argv[1:])
+    # 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, if parsing using the current format fails, construct an ad-hoc parser
+    # 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 retry with the normal parser (and raise
+    # a deprecation warning).
+    arguments = argv[1:]
+
+    # On Python 3.9+ we can simply set exit_on_error=False in the constructor
+    def _raise(x):
+        raise argparse.ArgumentError(None, x)
+
+    # First, try new-style parsing, but don't exit in case of failure
+    try:
+        # duplicate parser so that we can use the original one to raise errors later on
+        current_parser = argparse.ArgumentParser(add_help=False, parents=[parser])
+        current_parser.set_defaults(deprecated_argv_suggestion=None)
+        current_parser.error = _raise
+        return current_parser.parse_args(arguments)
+    except argparse.ArgumentError:
+        pass
+
+    # Second, try compat parsing and rearrange the command-line if it succeeds
+    # 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", action="store_true")
+    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",
+            "vscode",
+            "update-all",
+        ],
+    )
+
+    try:
+        compat_parser.error = _raise
+        result, unparsed = compat_parser.parse_known_args(argv[1:])
+        last_option = len(arguments) - len(unparsed) - 1 - len(result.configuration)
+        unparsed = [
+            "--device" if arg in ("--upload-port", "--serial-port") else arg
+            for arg in unparsed
+        ]
+        arguments = (
+            arguments[0:last_option]
+            + [result.command]
+            + result.configuration
+            + unparsed
+        )
+        deprecated_argv_suggestion = arguments
+    except argparse.ArgumentError:
+        # old-style parsing failed, don't suggest any argument
+        deprecated_argv_suggestion = None
+
+    # Finally, run the new-style parser again with the possibly swapped arguments,
+    # and let it error out if the command is unparsable.
+    parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion)
+    return parser.parse_args(arguments)
 
 
 def run_esphome(argv):
@@ -715,7 +725,7 @@ def run_esphome(argv):
             "and will be removed in the future. "
         )
         _LOGGER.warning("Please instead use:")
-        _LOGGER.warning("   esphome %s", " ".join(args.deprecated_argv_suggestion[1:]))
+        _LOGGER.warning("   esphome %s", " ".join(args.deprecated_argv_suggestion))
 
     if sys.version_info < (3, 7, 0):
         _LOGGER.error(
diff --git a/esphome/api/api_pb2.py b/esphome/api/api_pb2.py
deleted file mode 100644
index 6262b752c6..0000000000
--- a/esphome/api/api_pb2.py
+++ /dev/null
@@ -1,3997 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by the protocol buffer compiler.  DO NOT EDIT!
-# source: api.proto
-
-import sys
-
-_b = sys.version_info[0] < 3 and (lambda x: x) or (lambda x: x.encode("latin1"))
-from google.protobuf.internal import enum_type_wrapper
-from google.protobuf import descriptor as _descriptor
-from google.protobuf import message as _message
-from google.protobuf import reflection as _reflection
-from google.protobuf import symbol_database as _symbol_database
-
-# @@protoc_insertion_point(imports)
-
-_sym_db = _symbol_database.Default()
-
-
-DESCRIPTOR = _descriptor.FileDescriptor(
-    name="api.proto",
-    package="",
-    syntax="proto3",
-    serialized_options=None,
-    serialized_pb=_b(
-        '\n\tapi.proto"#\n\x0cHelloRequest\x12\x13\n\x0b\x63lient_info\x18\x01 \x01(\t"Z\n\rHelloResponse\x12\x19\n\x11\x61pi_version_major\x18\x01 \x01(\r\x12\x19\n\x11\x61pi_version_minor\x18\x02 \x01(\r\x12\x13\n\x0bserver_info\x18\x03 \x01(\t""\n\x0e\x43onnectRequest\x12\x10\n\x08password\x18\x01 \x01(\t"+\n\x0f\x43onnectResponse\x12\x18\n\x10invalid_password\x18\x01 \x01(\x08"\x13\n\x11\x44isconnectRequest"\x14\n\x12\x44isconnectResponse"\r\n\x0bPingRequest"\x0e\n\x0cPingResponse"\x13\n\x11\x44\x65viceInfoRequest"\xad\x01\n\x12\x44\x65viceInfoResponse\x12\x15\n\ruses_password\x18\x01 \x01(\x08\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x13\n\x0bmac_address\x18\x03 \x01(\t\x12\x1c\n\x14\x65sphome_core_version\x18\x04 \x01(\t\x12\x18\n\x10\x63ompilation_time\x18\x05 \x01(\t\x12\r\n\x05model\x18\x06 \x01(\t\x12\x16\n\x0ehas_deep_sleep\x18\x07 \x01(\x08"\x15\n\x13ListEntitiesRequest"\x9a\x01\n ListEntitiesBinarySensorResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x14\n\x0c\x64\x65vice_class\x18\x05 \x01(\t\x12\x1f\n\x17is_status_binary_sensor\x18\x06 \x01(\x08"s\n\x19ListEntitiesCoverResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x15\n\ris_optimistic\x18\x05 \x01(\x08"\x90\x01\n\x17ListEntitiesFanResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x1c\n\x14supports_oscillation\x18\x05 \x01(\x08\x12\x16\n\x0esupports_speed\x18\x06 \x01(\x08"\x8a\x02\n\x19ListEntitiesLightResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x1b\n\x13supports_brightness\x18\x05 \x01(\x08\x12\x14\n\x0csupports_rgb\x18\x06 \x01(\x08\x12\x1c\n\x14supports_white_value\x18\x07 \x01(\x08\x12"\n\x1asupports_color_temperature\x18\x08 \x01(\x08\x12\x12\n\nmin_mireds\x18\t \x01(\x02\x12\x12\n\nmax_mireds\x18\n \x01(\x02\x12\x0f\n\x07\x65\x66\x66\x65\x63ts\x18\x0b \x03(\t"\xa3\x01\n\x1aListEntitiesSensorResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x0c\n\x04icon\x18\x05 \x01(\t\x12\x1b\n\x13unit_of_measurement\x18\x06 \x01(\t\x12\x19\n\x11\x61\x63\x63uracy_decimals\x18\x07 \x01(\x05"\x7f\n\x1aListEntitiesSwitchResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x0c\n\x04icon\x18\x05 \x01(\t\x12\x12\n\noptimistic\x18\x06 \x01(\x08"o\n\x1eListEntitiesTextSensorResponse\x12\x11\n\tobject_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\x07\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x11\n\tunique_id\x18\x04 \x01(\t\x12\x0c\n\x04icon\x18\x05 \x01(\t"\x1a\n\x18ListEntitiesDoneResponse"\x18\n\x16SubscribeStatesRequest"7\n\x19\x42inarySensorStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\x08"t\n\x12\x43overStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12-\n\x05state\x18\x02 \x01(\x0e\x32\x1e.CoverStateResponse.CoverState""\n\nCoverState\x12\x08\n\x04OPEN\x10\x00\x12\n\n\x06\x43LOSED\x10\x01"]\n\x10\x46\x61nStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\x08\x12\x13\n\x0boscillating\x18\x03 \x01(\x08\x12\x18\n\x05speed\x18\x04 \x01(\x0e\x32\t.FanSpeed"\xa8\x01\n\x12LightStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\x08\x12\x12\n\nbrightness\x18\x03 \x01(\x02\x12\x0b\n\x03red\x18\x04 \x01(\x02\x12\r\n\x05green\x18\x05 \x01(\x02\x12\x0c\n\x04\x62lue\x18\x06 \x01(\x02\x12\r\n\x05white\x18\x07 \x01(\x02\x12\x19\n\x11\x63olor_temperature\x18\x08 \x01(\x02\x12\x0e\n\x06\x65\x66\x66\x65\x63t\x18\t \x01(\t"1\n\x13SensorStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\x02"1\n\x13SwitchStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\x08"5\n\x17TextSensorStateResponse\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\t"\x98\x01\n\x13\x43overCommandRequest\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\x11\n\thas_state\x18\x02 \x01(\x08\x12\x32\n\x07\x63ommand\x18\x03 \x01(\x0e\x32!.CoverCommandRequest.CoverCommand"-\n\x0c\x43overCommand\x12\x08\n\x04OPEN\x10\x00\x12\t\n\x05\x43LOSE\x10\x01\x12\x08\n\x04STOP\x10\x02"\x9d\x01\n\x11\x46\x61nCommandRequest\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\x11\n\thas_state\x18\x02 \x01(\x08\x12\r\n\x05state\x18\x03 \x01(\x08\x12\x11\n\thas_speed\x18\x04 \x01(\x08\x12\x18\n\x05speed\x18\x05 \x01(\x0e\x32\t.FanSpeed\x12\x17\n\x0fhas_oscillating\x18\x06 \x01(\x08\x12\x13\n\x0boscillating\x18\x07 \x01(\x08"\x95\x03\n\x13LightCommandRequest\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\x11\n\thas_state\x18\x02 \x01(\x08\x12\r\n\x05state\x18\x03 \x01(\x08\x12\x16\n\x0ehas_brightness\x18\x04 \x01(\x08\x12\x12\n\nbrightness\x18\x05 \x01(\x02\x12\x0f\n\x07has_rgb\x18\x06 \x01(\x08\x12\x0b\n\x03red\x18\x07 \x01(\x02\x12\r\n\x05green\x18\x08 \x01(\x02\x12\x0c\n\x04\x62lue\x18\t \x01(\x02\x12\x11\n\thas_white\x18\n \x01(\x08\x12\r\n\x05white\x18\x0b \x01(\x02\x12\x1d\n\x15has_color_temperature\x18\x0c \x01(\x08\x12\x19\n\x11\x63olor_temperature\x18\r \x01(\x02\x12\x1d\n\x15has_transition_length\x18\x0e \x01(\x08\x12\x19\n\x11transition_length\x18\x0f \x01(\r\x12\x18\n\x10has_flash_length\x18\x10 \x01(\x08\x12\x14\n\x0c\x66lash_length\x18\x11 \x01(\r\x12\x12\n\nhas_effect\x18\x12 \x01(\x08\x12\x0e\n\x06\x65\x66\x66\x65\x63t\x18\x13 \x01(\t"2\n\x14SwitchCommandRequest\x12\x0b\n\x03key\x18\x01 \x01(\x07\x12\r\n\x05state\x18\x02 \x01(\x08"E\n\x14SubscribeLogsRequest\x12\x18\n\x05level\x18\x01 \x01(\x0e\x32\t.LogLevel\x12\x13\n\x0b\x64ump_config\x18\x02 \x01(\x08"d\n\x15SubscribeLogsResponse\x12\x18\n\x05level\x18\x01 \x01(\x0e\x32\t.LogLevel\x12\x0b\n\x03tag\x18\x02 \x01(\t\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\x13\n\x0bsend_failed\x18\x04 \x01(\x08"\x1e\n\x1cSubscribeServiceCallsRequest"\xdf\x02\n\x13ServiceCallResponse\x12\x0f\n\x07service\x18\x01 \x01(\t\x12,\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32\x1e.ServiceCallResponse.DataEntry\x12=\n\rdata_template\x18\x03 \x03(\x0b\x32&.ServiceCallResponse.DataTemplateEntry\x12\x36\n\tvariables\x18\x04 \x03(\x0b\x32#.ServiceCallResponse.VariablesEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x33\n\x11\x44\x61taTemplateEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x30\n\x0eVariablesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01"%\n#SubscribeHomeAssistantStatesRequest"8\n#SubscribeHomeAssistantStateResponse\x12\x11\n\tentity_id\x18\x01 \x01(\t">\n\x1aHomeAssistantStateResponse\x12\x11\n\tentity_id\x18\x01 \x01(\t\x12\r\n\x05state\x18\x02 \x01(\t"\x10\n\x0eGetTimeRequest"(\n\x0fGetTimeResponse\x12\x15\n\repoch_seconds\x18\x01 \x01(\x07*)\n\x08\x46\x61nSpeed\x12\x07\n\x03LOW\x10\x00\x12\n\n\x06MEDIUM\x10\x01\x12\x08\n\x04HIGH\x10\x02*]\n\x08LogLevel\x12\x08\n\x04NONE\x10\x00\x12\t\n\x05\x45RROR\x10\x01\x12\x08\n\x04WARN\x10\x02\x12\x08\n\x04INFO\x10\x03\x12\t\n\x05\x44\x45\x42UG\x10\x04\x12\x0b\n\x07VERBOSE\x10\x05\x12\x10\n\x0cVERY_VERBOSE\x10\x06\x62\x06proto3'
-    ),
-)
-
-_FANSPEED = _descriptor.EnumDescriptor(
-    name="FanSpeed",
-    full_name="FanSpeed",
-    filename=None,
-    file=DESCRIPTOR,
-    values=[
-        _descriptor.EnumValueDescriptor(
-            name="LOW", index=0, number=0, serialized_options=None, type=None
-        ),
-        _descriptor.EnumValueDescriptor(
-            name="MEDIUM", index=1, number=1, serialized_options=None, type=None
-        ),
-        _descriptor.EnumValueDescriptor(
-            name="HIGH", index=2, number=2, serialized_options=None, type=None
-        ),
-    ],
-    containing_type=None,
-    serialized_options=None,
-    serialized_start=3822,
-    serialized_end=3863,
-)
-_sym_db.RegisterEnumDescriptor(_FANSPEED)
-
-FanSpeed = enum_type_wrapper.EnumTypeWrapper(_FANSPEED)
-_LOGLEVEL = _descriptor.EnumDescriptor(
-    name="LogLevel",
-    full_name="LogLevel",
-    filename=None,
-    file=DESCRIPTOR,
-    values=[
-        _descriptor.EnumValueDescriptor(
-            name="NONE", index=0, number=0, serialized_options=None, type=None
-        ),
-        _descriptor.EnumValueDescriptor(
-            name="ERROR", index=1, number=1, serialized_options=None, type=None
-        ),
-        _descriptor.EnumValueDescriptor(
-            name="WARN", index=2, number=2, serialized_options=None, type=None
-        ),
-        _descriptor.EnumValueDescriptor(
-            name="INFO", index=3, number=3, serialized_options=None, type=None
-        ),
-        _descriptor.EnumValueDescriptor(
-            name="DEBUG", index=4, number=4, serialized_options=None, type=None
-        ),
-        _descriptor.EnumValueDescriptor(
-            name="VERBOSE", index=5, number=5, serialized_options=None, type=None
-        ),
-        _descriptor.EnumValueDescriptor(
-            name="VERY_VERBOSE", index=6, number=6, serialized_options=None, type=None
-        ),
-    ],
-    containing_type=None,
-    serialized_options=None,
-    serialized_start=3865,
-    serialized_end=3958,
-)
-_sym_db.RegisterEnumDescriptor(_LOGLEVEL)
-
-LogLevel = enum_type_wrapper.EnumTypeWrapper(_LOGLEVEL)
-LOW = 0
-MEDIUM = 1
-HIGH = 2
-NONE = 0
-ERROR = 1
-WARN = 2
-INFO = 3
-DEBUG = 4
-VERBOSE = 5
-VERY_VERBOSE = 6
-
-
-_COVERSTATERESPONSE_COVERSTATE = _descriptor.EnumDescriptor(
-    name="CoverState",
-    full_name="CoverStateResponse.CoverState",
-    filename=None,
-    file=DESCRIPTOR,
-    values=[
-        _descriptor.EnumValueDescriptor(
-            name="OPEN", index=0, number=0, serialized_options=None, type=None
-        ),
-        _descriptor.EnumValueDescriptor(
-            name="CLOSED", index=1, number=1, serialized_options=None, type=None
-        ),
-    ],
-    containing_type=None,
-    serialized_options=None,
-    serialized_start=1808,
-    serialized_end=1842,
-)
-_sym_db.RegisterEnumDescriptor(_COVERSTATERESPONSE_COVERSTATE)
-
-_COVERCOMMANDREQUEST_COVERCOMMAND = _descriptor.EnumDescriptor(
-    name="CoverCommand",
-    full_name="CoverCommandRequest.CoverCommand",
-    filename=None,
-    file=DESCRIPTOR,
-    values=[
-        _descriptor.EnumValueDescriptor(
-            name="OPEN", index=0, number=0, serialized_options=None, type=None
-        ),
-        _descriptor.EnumValueDescriptor(
-            name="CLOSE", index=1, number=1, serialized_options=None, type=None
-        ),
-        _descriptor.EnumValueDescriptor(
-            name="STOP", index=2, number=2, serialized_options=None, type=None
-        ),
-    ],
-    containing_type=None,
-    serialized_options=None,
-    serialized_start=2375,
-    serialized_end=2420,
-)
-_sym_db.RegisterEnumDescriptor(_COVERCOMMANDREQUEST_COVERCOMMAND)
-
-
-_HELLOREQUEST = _descriptor.Descriptor(
-    name="HelloRequest",
-    full_name="HelloRequest",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="client_info",
-            full_name="HelloRequest.client_info",
-            index=0,
-            number=1,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=13,
-    serialized_end=48,
-)
-
-
-_HELLORESPONSE = _descriptor.Descriptor(
-    name="HelloResponse",
-    full_name="HelloResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="api_version_major",
-            full_name="HelloResponse.api_version_major",
-            index=0,
-            number=1,
-            type=13,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="api_version_minor",
-            full_name="HelloResponse.api_version_minor",
-            index=1,
-            number=2,
-            type=13,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="server_info",
-            full_name="HelloResponse.server_info",
-            index=2,
-            number=3,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=50,
-    serialized_end=140,
-)
-
-
-_CONNECTREQUEST = _descriptor.Descriptor(
-    name="ConnectRequest",
-    full_name="ConnectRequest",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="password",
-            full_name="ConnectRequest.password",
-            index=0,
-            number=1,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=142,
-    serialized_end=176,
-)
-
-
-_CONNECTRESPONSE = _descriptor.Descriptor(
-    name="ConnectResponse",
-    full_name="ConnectResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="invalid_password",
-            full_name="ConnectResponse.invalid_password",
-            index=0,
-            number=1,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=178,
-    serialized_end=221,
-)
-
-
-_DISCONNECTREQUEST = _descriptor.Descriptor(
-    name="DisconnectRequest",
-    full_name="DisconnectRequest",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=223,
-    serialized_end=242,
-)
-
-
-_DISCONNECTRESPONSE = _descriptor.Descriptor(
-    name="DisconnectResponse",
-    full_name="DisconnectResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=244,
-    serialized_end=264,
-)
-
-
-_PINGREQUEST = _descriptor.Descriptor(
-    name="PingRequest",
-    full_name="PingRequest",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=266,
-    serialized_end=279,
-)
-
-
-_PINGRESPONSE = _descriptor.Descriptor(
-    name="PingResponse",
-    full_name="PingResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=281,
-    serialized_end=295,
-)
-
-
-_DEVICEINFOREQUEST = _descriptor.Descriptor(
-    name="DeviceInfoRequest",
-    full_name="DeviceInfoRequest",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=297,
-    serialized_end=316,
-)
-
-
-_DEVICEINFORESPONSE = _descriptor.Descriptor(
-    name="DeviceInfoResponse",
-    full_name="DeviceInfoResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="uses_password",
-            full_name="DeviceInfoResponse.uses_password",
-            index=0,
-            number=1,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="name",
-            full_name="DeviceInfoResponse.name",
-            index=1,
-            number=2,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="mac_address",
-            full_name="DeviceInfoResponse.mac_address",
-            index=2,
-            number=3,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="esphome_core_version",
-            full_name="DeviceInfoResponse.esphome_core_version",
-            index=3,
-            number=4,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="compilation_time",
-            full_name="DeviceInfoResponse.compilation_time",
-            index=4,
-            number=5,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="model",
-            full_name="DeviceInfoResponse.model",
-            index=5,
-            number=6,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="has_deep_sleep",
-            full_name="DeviceInfoResponse.has_deep_sleep",
-            index=6,
-            number=7,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=319,
-    serialized_end=492,
-)
-
-
-_LISTENTITIESREQUEST = _descriptor.Descriptor(
-    name="ListEntitiesRequest",
-    full_name="ListEntitiesRequest",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=494,
-    serialized_end=515,
-)
-
-
-_LISTENTITIESBINARYSENSORRESPONSE = _descriptor.Descriptor(
-    name="ListEntitiesBinarySensorResponse",
-    full_name="ListEntitiesBinarySensorResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="object_id",
-            full_name="ListEntitiesBinarySensorResponse.object_id",
-            index=0,
-            number=1,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="ListEntitiesBinarySensorResponse.key",
-            index=1,
-            number=2,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="name",
-            full_name="ListEntitiesBinarySensorResponse.name",
-            index=2,
-            number=3,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="unique_id",
-            full_name="ListEntitiesBinarySensorResponse.unique_id",
-            index=3,
-            number=4,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="device_class",
-            full_name="ListEntitiesBinarySensorResponse.device_class",
-            index=4,
-            number=5,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="is_status_binary_sensor",
-            full_name="ListEntitiesBinarySensorResponse.is_status_binary_sensor",
-            index=5,
-            number=6,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=518,
-    serialized_end=672,
-)
-
-
-_LISTENTITIESCOVERRESPONSE = _descriptor.Descriptor(
-    name="ListEntitiesCoverResponse",
-    full_name="ListEntitiesCoverResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="object_id",
-            full_name="ListEntitiesCoverResponse.object_id",
-            index=0,
-            number=1,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="ListEntitiesCoverResponse.key",
-            index=1,
-            number=2,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="name",
-            full_name="ListEntitiesCoverResponse.name",
-            index=2,
-            number=3,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="unique_id",
-            full_name="ListEntitiesCoverResponse.unique_id",
-            index=3,
-            number=4,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="is_optimistic",
-            full_name="ListEntitiesCoverResponse.is_optimistic",
-            index=4,
-            number=5,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=674,
-    serialized_end=789,
-)
-
-
-_LISTENTITIESFANRESPONSE = _descriptor.Descriptor(
-    name="ListEntitiesFanResponse",
-    full_name="ListEntitiesFanResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="object_id",
-            full_name="ListEntitiesFanResponse.object_id",
-            index=0,
-            number=1,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="ListEntitiesFanResponse.key",
-            index=1,
-            number=2,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="name",
-            full_name="ListEntitiesFanResponse.name",
-            index=2,
-            number=3,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="unique_id",
-            full_name="ListEntitiesFanResponse.unique_id",
-            index=3,
-            number=4,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="supports_oscillation",
-            full_name="ListEntitiesFanResponse.supports_oscillation",
-            index=4,
-            number=5,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="supports_speed",
-            full_name="ListEntitiesFanResponse.supports_speed",
-            index=5,
-            number=6,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=792,
-    serialized_end=936,
-)
-
-
-_LISTENTITIESLIGHTRESPONSE = _descriptor.Descriptor(
-    name="ListEntitiesLightResponse",
-    full_name="ListEntitiesLightResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="object_id",
-            full_name="ListEntitiesLightResponse.object_id",
-            index=0,
-            number=1,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="ListEntitiesLightResponse.key",
-            index=1,
-            number=2,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="name",
-            full_name="ListEntitiesLightResponse.name",
-            index=2,
-            number=3,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="unique_id",
-            full_name="ListEntitiesLightResponse.unique_id",
-            index=3,
-            number=4,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="supports_brightness",
-            full_name="ListEntitiesLightResponse.supports_brightness",
-            index=4,
-            number=5,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="supports_rgb",
-            full_name="ListEntitiesLightResponse.supports_rgb",
-            index=5,
-            number=6,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="supports_white_value",
-            full_name="ListEntitiesLightResponse.supports_white_value",
-            index=6,
-            number=7,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="supports_color_temperature",
-            full_name="ListEntitiesLightResponse.supports_color_temperature",
-            index=7,
-            number=8,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="min_mireds",
-            full_name="ListEntitiesLightResponse.min_mireds",
-            index=8,
-            number=9,
-            type=2,
-            cpp_type=6,
-            label=1,
-            has_default_value=False,
-            default_value=float(0),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="max_mireds",
-            full_name="ListEntitiesLightResponse.max_mireds",
-            index=9,
-            number=10,
-            type=2,
-            cpp_type=6,
-            label=1,
-            has_default_value=False,
-            default_value=float(0),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="effects",
-            full_name="ListEntitiesLightResponse.effects",
-            index=10,
-            number=11,
-            type=9,
-            cpp_type=9,
-            label=3,
-            has_default_value=False,
-            default_value=[],
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=939,
-    serialized_end=1205,
-)
-
-
-_LISTENTITIESSENSORRESPONSE = _descriptor.Descriptor(
-    name="ListEntitiesSensorResponse",
-    full_name="ListEntitiesSensorResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="object_id",
-            full_name="ListEntitiesSensorResponse.object_id",
-            index=0,
-            number=1,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="ListEntitiesSensorResponse.key",
-            index=1,
-            number=2,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="name",
-            full_name="ListEntitiesSensorResponse.name",
-            index=2,
-            number=3,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="unique_id",
-            full_name="ListEntitiesSensorResponse.unique_id",
-            index=3,
-            number=4,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="icon",
-            full_name="ListEntitiesSensorResponse.icon",
-            index=4,
-            number=5,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="unit_of_measurement",
-            full_name="ListEntitiesSensorResponse.unit_of_measurement",
-            index=5,
-            number=6,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="accuracy_decimals",
-            full_name="ListEntitiesSensorResponse.accuracy_decimals",
-            index=6,
-            number=7,
-            type=5,
-            cpp_type=1,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=1208,
-    serialized_end=1371,
-)
-
-
-_LISTENTITIESSWITCHRESPONSE = _descriptor.Descriptor(
-    name="ListEntitiesSwitchResponse",
-    full_name="ListEntitiesSwitchResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="object_id",
-            full_name="ListEntitiesSwitchResponse.object_id",
-            index=0,
-            number=1,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="ListEntitiesSwitchResponse.key",
-            index=1,
-            number=2,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="name",
-            full_name="ListEntitiesSwitchResponse.name",
-            index=2,
-            number=3,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="unique_id",
-            full_name="ListEntitiesSwitchResponse.unique_id",
-            index=3,
-            number=4,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="icon",
-            full_name="ListEntitiesSwitchResponse.icon",
-            index=4,
-            number=5,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="optimistic",
-            full_name="ListEntitiesSwitchResponse.optimistic",
-            index=5,
-            number=6,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=1373,
-    serialized_end=1500,
-)
-
-
-_LISTENTITIESTEXTSENSORRESPONSE = _descriptor.Descriptor(
-    name="ListEntitiesTextSensorResponse",
-    full_name="ListEntitiesTextSensorResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="object_id",
-            full_name="ListEntitiesTextSensorResponse.object_id",
-            index=0,
-            number=1,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="ListEntitiesTextSensorResponse.key",
-            index=1,
-            number=2,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="name",
-            full_name="ListEntitiesTextSensorResponse.name",
-            index=2,
-            number=3,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="unique_id",
-            full_name="ListEntitiesTextSensorResponse.unique_id",
-            index=3,
-            number=4,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="icon",
-            full_name="ListEntitiesTextSensorResponse.icon",
-            index=4,
-            number=5,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=1502,
-    serialized_end=1613,
-)
-
-
-_LISTENTITIESDONERESPONSE = _descriptor.Descriptor(
-    name="ListEntitiesDoneResponse",
-    full_name="ListEntitiesDoneResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=1615,
-    serialized_end=1641,
-)
-
-
-_SUBSCRIBESTATESREQUEST = _descriptor.Descriptor(
-    name="SubscribeStatesRequest",
-    full_name="SubscribeStatesRequest",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=1643,
-    serialized_end=1667,
-)
-
-
-_BINARYSENSORSTATERESPONSE = _descriptor.Descriptor(
-    name="BinarySensorStateResponse",
-    full_name="BinarySensorStateResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="BinarySensorStateResponse.key",
-            index=0,
-            number=1,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="state",
-            full_name="BinarySensorStateResponse.state",
-            index=1,
-            number=2,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=1669,
-    serialized_end=1724,
-)
-
-
-_COVERSTATERESPONSE = _descriptor.Descriptor(
-    name="CoverStateResponse",
-    full_name="CoverStateResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="CoverStateResponse.key",
-            index=0,
-            number=1,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="state",
-            full_name="CoverStateResponse.state",
-            index=1,
-            number=2,
-            type=14,
-            cpp_type=8,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[
-        _COVERSTATERESPONSE_COVERSTATE,
-    ],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=1726,
-    serialized_end=1842,
-)
-
-
-_FANSTATERESPONSE = _descriptor.Descriptor(
-    name="FanStateResponse",
-    full_name="FanStateResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="FanStateResponse.key",
-            index=0,
-            number=1,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="state",
-            full_name="FanStateResponse.state",
-            index=1,
-            number=2,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="oscillating",
-            full_name="FanStateResponse.oscillating",
-            index=2,
-            number=3,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="speed",
-            full_name="FanStateResponse.speed",
-            index=3,
-            number=4,
-            type=14,
-            cpp_type=8,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=1844,
-    serialized_end=1937,
-)
-
-
-_LIGHTSTATERESPONSE = _descriptor.Descriptor(
-    name="LightStateResponse",
-    full_name="LightStateResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="LightStateResponse.key",
-            index=0,
-            number=1,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="state",
-            full_name="LightStateResponse.state",
-            index=1,
-            number=2,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="brightness",
-            full_name="LightStateResponse.brightness",
-            index=2,
-            number=3,
-            type=2,
-            cpp_type=6,
-            label=1,
-            has_default_value=False,
-            default_value=float(0),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="red",
-            full_name="LightStateResponse.red",
-            index=3,
-            number=4,
-            type=2,
-            cpp_type=6,
-            label=1,
-            has_default_value=False,
-            default_value=float(0),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="green",
-            full_name="LightStateResponse.green",
-            index=4,
-            number=5,
-            type=2,
-            cpp_type=6,
-            label=1,
-            has_default_value=False,
-            default_value=float(0),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="blue",
-            full_name="LightStateResponse.blue",
-            index=5,
-            number=6,
-            type=2,
-            cpp_type=6,
-            label=1,
-            has_default_value=False,
-            default_value=float(0),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="white",
-            full_name="LightStateResponse.white",
-            index=6,
-            number=7,
-            type=2,
-            cpp_type=6,
-            label=1,
-            has_default_value=False,
-            default_value=float(0),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="color_temperature",
-            full_name="LightStateResponse.color_temperature",
-            index=7,
-            number=8,
-            type=2,
-            cpp_type=6,
-            label=1,
-            has_default_value=False,
-            default_value=float(0),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="effect",
-            full_name="LightStateResponse.effect",
-            index=8,
-            number=9,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=1940,
-    serialized_end=2108,
-)
-
-
-_SENSORSTATERESPONSE = _descriptor.Descriptor(
-    name="SensorStateResponse",
-    full_name="SensorStateResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="SensorStateResponse.key",
-            index=0,
-            number=1,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="state",
-            full_name="SensorStateResponse.state",
-            index=1,
-            number=2,
-            type=2,
-            cpp_type=6,
-            label=1,
-            has_default_value=False,
-            default_value=float(0),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=2110,
-    serialized_end=2159,
-)
-
-
-_SWITCHSTATERESPONSE = _descriptor.Descriptor(
-    name="SwitchStateResponse",
-    full_name="SwitchStateResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="SwitchStateResponse.key",
-            index=0,
-            number=1,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="state",
-            full_name="SwitchStateResponse.state",
-            index=1,
-            number=2,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=2161,
-    serialized_end=2210,
-)
-
-
-_TEXTSENSORSTATERESPONSE = _descriptor.Descriptor(
-    name="TextSensorStateResponse",
-    full_name="TextSensorStateResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="TextSensorStateResponse.key",
-            index=0,
-            number=1,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="state",
-            full_name="TextSensorStateResponse.state",
-            index=1,
-            number=2,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=2212,
-    serialized_end=2265,
-)
-
-
-_COVERCOMMANDREQUEST = _descriptor.Descriptor(
-    name="CoverCommandRequest",
-    full_name="CoverCommandRequest",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="CoverCommandRequest.key",
-            index=0,
-            number=1,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="has_state",
-            full_name="CoverCommandRequest.has_state",
-            index=1,
-            number=2,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="command",
-            full_name="CoverCommandRequest.command",
-            index=2,
-            number=3,
-            type=14,
-            cpp_type=8,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[
-        _COVERCOMMANDREQUEST_COVERCOMMAND,
-    ],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=2268,
-    serialized_end=2420,
-)
-
-
-_FANCOMMANDREQUEST = _descriptor.Descriptor(
-    name="FanCommandRequest",
-    full_name="FanCommandRequest",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="FanCommandRequest.key",
-            index=0,
-            number=1,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="has_state",
-            full_name="FanCommandRequest.has_state",
-            index=1,
-            number=2,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="state",
-            full_name="FanCommandRequest.state",
-            index=2,
-            number=3,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="has_speed",
-            full_name="FanCommandRequest.has_speed",
-            index=3,
-            number=4,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="speed",
-            full_name="FanCommandRequest.speed",
-            index=4,
-            number=5,
-            type=14,
-            cpp_type=8,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="has_oscillating",
-            full_name="FanCommandRequest.has_oscillating",
-            index=5,
-            number=6,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="oscillating",
-            full_name="FanCommandRequest.oscillating",
-            index=6,
-            number=7,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=2423,
-    serialized_end=2580,
-)
-
-
-_LIGHTCOMMANDREQUEST = _descriptor.Descriptor(
-    name="LightCommandRequest",
-    full_name="LightCommandRequest",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="LightCommandRequest.key",
-            index=0,
-            number=1,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="has_state",
-            full_name="LightCommandRequest.has_state",
-            index=1,
-            number=2,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="state",
-            full_name="LightCommandRequest.state",
-            index=2,
-            number=3,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="has_brightness",
-            full_name="LightCommandRequest.has_brightness",
-            index=3,
-            number=4,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="brightness",
-            full_name="LightCommandRequest.brightness",
-            index=4,
-            number=5,
-            type=2,
-            cpp_type=6,
-            label=1,
-            has_default_value=False,
-            default_value=float(0),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="has_rgb",
-            full_name="LightCommandRequest.has_rgb",
-            index=5,
-            number=6,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="red",
-            full_name="LightCommandRequest.red",
-            index=6,
-            number=7,
-            type=2,
-            cpp_type=6,
-            label=1,
-            has_default_value=False,
-            default_value=float(0),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="green",
-            full_name="LightCommandRequest.green",
-            index=7,
-            number=8,
-            type=2,
-            cpp_type=6,
-            label=1,
-            has_default_value=False,
-            default_value=float(0),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="blue",
-            full_name="LightCommandRequest.blue",
-            index=8,
-            number=9,
-            type=2,
-            cpp_type=6,
-            label=1,
-            has_default_value=False,
-            default_value=float(0),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="has_white",
-            full_name="LightCommandRequest.has_white",
-            index=9,
-            number=10,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="white",
-            full_name="LightCommandRequest.white",
-            index=10,
-            number=11,
-            type=2,
-            cpp_type=6,
-            label=1,
-            has_default_value=False,
-            default_value=float(0),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="has_color_temperature",
-            full_name="LightCommandRequest.has_color_temperature",
-            index=11,
-            number=12,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="color_temperature",
-            full_name="LightCommandRequest.color_temperature",
-            index=12,
-            number=13,
-            type=2,
-            cpp_type=6,
-            label=1,
-            has_default_value=False,
-            default_value=float(0),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="has_transition_length",
-            full_name="LightCommandRequest.has_transition_length",
-            index=13,
-            number=14,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="transition_length",
-            full_name="LightCommandRequest.transition_length",
-            index=14,
-            number=15,
-            type=13,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="has_flash_length",
-            full_name="LightCommandRequest.has_flash_length",
-            index=15,
-            number=16,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="flash_length",
-            full_name="LightCommandRequest.flash_length",
-            index=16,
-            number=17,
-            type=13,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="has_effect",
-            full_name="LightCommandRequest.has_effect",
-            index=17,
-            number=18,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="effect",
-            full_name="LightCommandRequest.effect",
-            index=18,
-            number=19,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=2583,
-    serialized_end=2988,
-)
-
-
-_SWITCHCOMMANDREQUEST = _descriptor.Descriptor(
-    name="SwitchCommandRequest",
-    full_name="SwitchCommandRequest",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="SwitchCommandRequest.key",
-            index=0,
-            number=1,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="state",
-            full_name="SwitchCommandRequest.state",
-            index=1,
-            number=2,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=2990,
-    serialized_end=3040,
-)
-
-
-_SUBSCRIBELOGSREQUEST = _descriptor.Descriptor(
-    name="SubscribeLogsRequest",
-    full_name="SubscribeLogsRequest",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="level",
-            full_name="SubscribeLogsRequest.level",
-            index=0,
-            number=1,
-            type=14,
-            cpp_type=8,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="dump_config",
-            full_name="SubscribeLogsRequest.dump_config",
-            index=1,
-            number=2,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=3042,
-    serialized_end=3111,
-)
-
-
-_SUBSCRIBELOGSRESPONSE = _descriptor.Descriptor(
-    name="SubscribeLogsResponse",
-    full_name="SubscribeLogsResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="level",
-            full_name="SubscribeLogsResponse.level",
-            index=0,
-            number=1,
-            type=14,
-            cpp_type=8,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="tag",
-            full_name="SubscribeLogsResponse.tag",
-            index=1,
-            number=2,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="message",
-            full_name="SubscribeLogsResponse.message",
-            index=2,
-            number=3,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="send_failed",
-            full_name="SubscribeLogsResponse.send_failed",
-            index=3,
-            number=4,
-            type=8,
-            cpp_type=7,
-            label=1,
-            has_default_value=False,
-            default_value=False,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=3113,
-    serialized_end=3213,
-)
-
-
-_SUBSCRIBESERVICECALLSREQUEST = _descriptor.Descriptor(
-    name="SubscribeServiceCallsRequest",
-    full_name="SubscribeServiceCallsRequest",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=3215,
-    serialized_end=3245,
-)
-
-
-_SERVICECALLRESPONSE_DATAENTRY = _descriptor.Descriptor(
-    name="DataEntry",
-    full_name="ServiceCallResponse.DataEntry",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="ServiceCallResponse.DataEntry.key",
-            index=0,
-            number=1,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="value",
-            full_name="ServiceCallResponse.DataEntry.value",
-            index=1,
-            number=2,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=_b("8\001"),
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=3453,
-    serialized_end=3496,
-)
-
-_SERVICECALLRESPONSE_DATATEMPLATEENTRY = _descriptor.Descriptor(
-    name="DataTemplateEntry",
-    full_name="ServiceCallResponse.DataTemplateEntry",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="ServiceCallResponse.DataTemplateEntry.key",
-            index=0,
-            number=1,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="value",
-            full_name="ServiceCallResponse.DataTemplateEntry.value",
-            index=1,
-            number=2,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=_b("8\001"),
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=3498,
-    serialized_end=3549,
-)
-
-_SERVICECALLRESPONSE_VARIABLESENTRY = _descriptor.Descriptor(
-    name="VariablesEntry",
-    full_name="ServiceCallResponse.VariablesEntry",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="key",
-            full_name="ServiceCallResponse.VariablesEntry.key",
-            index=0,
-            number=1,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="value",
-            full_name="ServiceCallResponse.VariablesEntry.value",
-            index=1,
-            number=2,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=_b("8\001"),
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=3551,
-    serialized_end=3599,
-)
-
-_SERVICECALLRESPONSE = _descriptor.Descriptor(
-    name="ServiceCallResponse",
-    full_name="ServiceCallResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="service",
-            full_name="ServiceCallResponse.service",
-            index=0,
-            number=1,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="data",
-            full_name="ServiceCallResponse.data",
-            index=1,
-            number=2,
-            type=11,
-            cpp_type=10,
-            label=3,
-            has_default_value=False,
-            default_value=[],
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="data_template",
-            full_name="ServiceCallResponse.data_template",
-            index=2,
-            number=3,
-            type=11,
-            cpp_type=10,
-            label=3,
-            has_default_value=False,
-            default_value=[],
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="variables",
-            full_name="ServiceCallResponse.variables",
-            index=3,
-            number=4,
-            type=11,
-            cpp_type=10,
-            label=3,
-            has_default_value=False,
-            default_value=[],
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[
-        _SERVICECALLRESPONSE_DATAENTRY,
-        _SERVICECALLRESPONSE_DATATEMPLATEENTRY,
-        _SERVICECALLRESPONSE_VARIABLESENTRY,
-    ],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=3248,
-    serialized_end=3599,
-)
-
-
-_SUBSCRIBEHOMEASSISTANTSTATESREQUEST = _descriptor.Descriptor(
-    name="SubscribeHomeAssistantStatesRequest",
-    full_name="SubscribeHomeAssistantStatesRequest",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=3601,
-    serialized_end=3638,
-)
-
-
-_SUBSCRIBEHOMEASSISTANTSTATERESPONSE = _descriptor.Descriptor(
-    name="SubscribeHomeAssistantStateResponse",
-    full_name="SubscribeHomeAssistantStateResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="entity_id",
-            full_name="SubscribeHomeAssistantStateResponse.entity_id",
-            index=0,
-            number=1,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=3640,
-    serialized_end=3696,
-)
-
-
-_HOMEASSISTANTSTATERESPONSE = _descriptor.Descriptor(
-    name="HomeAssistantStateResponse",
-    full_name="HomeAssistantStateResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="entity_id",
-            full_name="HomeAssistantStateResponse.entity_id",
-            index=0,
-            number=1,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-        _descriptor.FieldDescriptor(
-            name="state",
-            full_name="HomeAssistantStateResponse.state",
-            index=1,
-            number=2,
-            type=9,
-            cpp_type=9,
-            label=1,
-            has_default_value=False,
-            default_value=_b("").decode("utf-8"),
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=3698,
-    serialized_end=3760,
-)
-
-
-_GETTIMEREQUEST = _descriptor.Descriptor(
-    name="GetTimeRequest",
-    full_name="GetTimeRequest",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=3762,
-    serialized_end=3778,
-)
-
-
-_GETTIMERESPONSE = _descriptor.Descriptor(
-    name="GetTimeResponse",
-    full_name="GetTimeResponse",
-    filename=None,
-    file=DESCRIPTOR,
-    containing_type=None,
-    fields=[
-        _descriptor.FieldDescriptor(
-            name="epoch_seconds",
-            full_name="GetTimeResponse.epoch_seconds",
-            index=0,
-            number=1,
-            type=7,
-            cpp_type=3,
-            label=1,
-            has_default_value=False,
-            default_value=0,
-            message_type=None,
-            enum_type=None,
-            containing_type=None,
-            is_extension=False,
-            extension_scope=None,
-            serialized_options=None,
-            file=DESCRIPTOR,
-        ),
-    ],
-    extensions=[],
-    nested_types=[],
-    enum_types=[],
-    serialized_options=None,
-    is_extendable=False,
-    syntax="proto3",
-    extension_ranges=[],
-    oneofs=[],
-    serialized_start=3780,
-    serialized_end=3820,
-)
-
-_COVERSTATERESPONSE.fields_by_name["state"].enum_type = _COVERSTATERESPONSE_COVERSTATE
-_COVERSTATERESPONSE_COVERSTATE.containing_type = _COVERSTATERESPONSE
-_FANSTATERESPONSE.fields_by_name["speed"].enum_type = _FANSPEED
-_COVERCOMMANDREQUEST.fields_by_name[
-    "command"
-].enum_type = _COVERCOMMANDREQUEST_COVERCOMMAND
-_COVERCOMMANDREQUEST_COVERCOMMAND.containing_type = _COVERCOMMANDREQUEST
-_FANCOMMANDREQUEST.fields_by_name["speed"].enum_type = _FANSPEED
-_SUBSCRIBELOGSREQUEST.fields_by_name["level"].enum_type = _LOGLEVEL
-_SUBSCRIBELOGSRESPONSE.fields_by_name["level"].enum_type = _LOGLEVEL
-_SERVICECALLRESPONSE_DATAENTRY.containing_type = _SERVICECALLRESPONSE
-_SERVICECALLRESPONSE_DATATEMPLATEENTRY.containing_type = _SERVICECALLRESPONSE
-_SERVICECALLRESPONSE_VARIABLESENTRY.containing_type = _SERVICECALLRESPONSE
-_SERVICECALLRESPONSE.fields_by_name[
-    "data"
-].message_type = _SERVICECALLRESPONSE_DATAENTRY
-_SERVICECALLRESPONSE.fields_by_name[
-    "data_template"
-].message_type = _SERVICECALLRESPONSE_DATATEMPLATEENTRY
-_SERVICECALLRESPONSE.fields_by_name[
-    "variables"
-].message_type = _SERVICECALLRESPONSE_VARIABLESENTRY
-DESCRIPTOR.message_types_by_name["HelloRequest"] = _HELLOREQUEST
-DESCRIPTOR.message_types_by_name["HelloResponse"] = _HELLORESPONSE
-DESCRIPTOR.message_types_by_name["ConnectRequest"] = _CONNECTREQUEST
-DESCRIPTOR.message_types_by_name["ConnectResponse"] = _CONNECTRESPONSE
-DESCRIPTOR.message_types_by_name["DisconnectRequest"] = _DISCONNECTREQUEST
-DESCRIPTOR.message_types_by_name["DisconnectResponse"] = _DISCONNECTRESPONSE
-DESCRIPTOR.message_types_by_name["PingRequest"] = _PINGREQUEST
-DESCRIPTOR.message_types_by_name["PingResponse"] = _PINGRESPONSE
-DESCRIPTOR.message_types_by_name["DeviceInfoRequest"] = _DEVICEINFOREQUEST
-DESCRIPTOR.message_types_by_name["DeviceInfoResponse"] = _DEVICEINFORESPONSE
-DESCRIPTOR.message_types_by_name["ListEntitiesRequest"] = _LISTENTITIESREQUEST
-DESCRIPTOR.message_types_by_name[
-    "ListEntitiesBinarySensorResponse"
-] = _LISTENTITIESBINARYSENSORRESPONSE
-DESCRIPTOR.message_types_by_name[
-    "ListEntitiesCoverResponse"
-] = _LISTENTITIESCOVERRESPONSE
-DESCRIPTOR.message_types_by_name["ListEntitiesFanResponse"] = _LISTENTITIESFANRESPONSE
-DESCRIPTOR.message_types_by_name[
-    "ListEntitiesLightResponse"
-] = _LISTENTITIESLIGHTRESPONSE
-DESCRIPTOR.message_types_by_name[
-    "ListEntitiesSensorResponse"
-] = _LISTENTITIESSENSORRESPONSE
-DESCRIPTOR.message_types_by_name[
-    "ListEntitiesSwitchResponse"
-] = _LISTENTITIESSWITCHRESPONSE
-DESCRIPTOR.message_types_by_name[
-    "ListEntitiesTextSensorResponse"
-] = _LISTENTITIESTEXTSENSORRESPONSE
-DESCRIPTOR.message_types_by_name["ListEntitiesDoneResponse"] = _LISTENTITIESDONERESPONSE
-DESCRIPTOR.message_types_by_name["SubscribeStatesRequest"] = _SUBSCRIBESTATESREQUEST
-DESCRIPTOR.message_types_by_name[
-    "BinarySensorStateResponse"
-] = _BINARYSENSORSTATERESPONSE
-DESCRIPTOR.message_types_by_name["CoverStateResponse"] = _COVERSTATERESPONSE
-DESCRIPTOR.message_types_by_name["FanStateResponse"] = _FANSTATERESPONSE
-DESCRIPTOR.message_types_by_name["LightStateResponse"] = _LIGHTSTATERESPONSE
-DESCRIPTOR.message_types_by_name["SensorStateResponse"] = _SENSORSTATERESPONSE
-DESCRIPTOR.message_types_by_name["SwitchStateResponse"] = _SWITCHSTATERESPONSE
-DESCRIPTOR.message_types_by_name["TextSensorStateResponse"] = _TEXTSENSORSTATERESPONSE
-DESCRIPTOR.message_types_by_name["CoverCommandRequest"] = _COVERCOMMANDREQUEST
-DESCRIPTOR.message_types_by_name["FanCommandRequest"] = _FANCOMMANDREQUEST
-DESCRIPTOR.message_types_by_name["LightCommandRequest"] = _LIGHTCOMMANDREQUEST
-DESCRIPTOR.message_types_by_name["SwitchCommandRequest"] = _SWITCHCOMMANDREQUEST
-DESCRIPTOR.message_types_by_name["SubscribeLogsRequest"] = _SUBSCRIBELOGSREQUEST
-DESCRIPTOR.message_types_by_name["SubscribeLogsResponse"] = _SUBSCRIBELOGSRESPONSE
-DESCRIPTOR.message_types_by_name[
-    "SubscribeServiceCallsRequest"
-] = _SUBSCRIBESERVICECALLSREQUEST
-DESCRIPTOR.message_types_by_name["ServiceCallResponse"] = _SERVICECALLRESPONSE
-DESCRIPTOR.message_types_by_name[
-    "SubscribeHomeAssistantStatesRequest"
-] = _SUBSCRIBEHOMEASSISTANTSTATESREQUEST
-DESCRIPTOR.message_types_by_name[
-    "SubscribeHomeAssistantStateResponse"
-] = _SUBSCRIBEHOMEASSISTANTSTATERESPONSE
-DESCRIPTOR.message_types_by_name[
-    "HomeAssistantStateResponse"
-] = _HOMEASSISTANTSTATERESPONSE
-DESCRIPTOR.message_types_by_name["GetTimeRequest"] = _GETTIMEREQUEST
-DESCRIPTOR.message_types_by_name["GetTimeResponse"] = _GETTIMERESPONSE
-DESCRIPTOR.enum_types_by_name["FanSpeed"] = _FANSPEED
-DESCRIPTOR.enum_types_by_name["LogLevel"] = _LOGLEVEL
-_sym_db.RegisterFileDescriptor(DESCRIPTOR)
-
-HelloRequest = _reflection.GeneratedProtocolMessageType(
-    "HelloRequest",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_HELLOREQUEST,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:HelloRequest)
-    ),
-)
-_sym_db.RegisterMessage(HelloRequest)
-
-HelloResponse = _reflection.GeneratedProtocolMessageType(
-    "HelloResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_HELLORESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:HelloResponse)
-    ),
-)
-_sym_db.RegisterMessage(HelloResponse)
-
-ConnectRequest = _reflection.GeneratedProtocolMessageType(
-    "ConnectRequest",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_CONNECTREQUEST,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:ConnectRequest)
-    ),
-)
-_sym_db.RegisterMessage(ConnectRequest)
-
-ConnectResponse = _reflection.GeneratedProtocolMessageType(
-    "ConnectResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_CONNECTRESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:ConnectResponse)
-    ),
-)
-_sym_db.RegisterMessage(ConnectResponse)
-
-DisconnectRequest = _reflection.GeneratedProtocolMessageType(
-    "DisconnectRequest",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_DISCONNECTREQUEST,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:DisconnectRequest)
-    ),
-)
-_sym_db.RegisterMessage(DisconnectRequest)
-
-DisconnectResponse = _reflection.GeneratedProtocolMessageType(
-    "DisconnectResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_DISCONNECTRESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:DisconnectResponse)
-    ),
-)
-_sym_db.RegisterMessage(DisconnectResponse)
-
-PingRequest = _reflection.GeneratedProtocolMessageType(
-    "PingRequest",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_PINGREQUEST,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:PingRequest)
-    ),
-)
-_sym_db.RegisterMessage(PingRequest)
-
-PingResponse = _reflection.GeneratedProtocolMessageType(
-    "PingResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_PINGRESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:PingResponse)
-    ),
-)
-_sym_db.RegisterMessage(PingResponse)
-
-DeviceInfoRequest = _reflection.GeneratedProtocolMessageType(
-    "DeviceInfoRequest",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_DEVICEINFOREQUEST,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:DeviceInfoRequest)
-    ),
-)
-_sym_db.RegisterMessage(DeviceInfoRequest)
-
-DeviceInfoResponse = _reflection.GeneratedProtocolMessageType(
-    "DeviceInfoResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_DEVICEINFORESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:DeviceInfoResponse)
-    ),
-)
-_sym_db.RegisterMessage(DeviceInfoResponse)
-
-ListEntitiesRequest = _reflection.GeneratedProtocolMessageType(
-    "ListEntitiesRequest",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_LISTENTITIESREQUEST,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:ListEntitiesRequest)
-    ),
-)
-_sym_db.RegisterMessage(ListEntitiesRequest)
-
-ListEntitiesBinarySensorResponse = _reflection.GeneratedProtocolMessageType(
-    "ListEntitiesBinarySensorResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_LISTENTITIESBINARYSENSORRESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:ListEntitiesBinarySensorResponse)
-    ),
-)
-_sym_db.RegisterMessage(ListEntitiesBinarySensorResponse)
-
-ListEntitiesCoverResponse = _reflection.GeneratedProtocolMessageType(
-    "ListEntitiesCoverResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_LISTENTITIESCOVERRESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:ListEntitiesCoverResponse)
-    ),
-)
-_sym_db.RegisterMessage(ListEntitiesCoverResponse)
-
-ListEntitiesFanResponse = _reflection.GeneratedProtocolMessageType(
-    "ListEntitiesFanResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_LISTENTITIESFANRESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:ListEntitiesFanResponse)
-    ),
-)
-_sym_db.RegisterMessage(ListEntitiesFanResponse)
-
-ListEntitiesLightResponse = _reflection.GeneratedProtocolMessageType(
-    "ListEntitiesLightResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_LISTENTITIESLIGHTRESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:ListEntitiesLightResponse)
-    ),
-)
-_sym_db.RegisterMessage(ListEntitiesLightResponse)
-
-ListEntitiesSensorResponse = _reflection.GeneratedProtocolMessageType(
-    "ListEntitiesSensorResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_LISTENTITIESSENSORRESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:ListEntitiesSensorResponse)
-    ),
-)
-_sym_db.RegisterMessage(ListEntitiesSensorResponse)
-
-ListEntitiesSwitchResponse = _reflection.GeneratedProtocolMessageType(
-    "ListEntitiesSwitchResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_LISTENTITIESSWITCHRESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:ListEntitiesSwitchResponse)
-    ),
-)
-_sym_db.RegisterMessage(ListEntitiesSwitchResponse)
-
-ListEntitiesTextSensorResponse = _reflection.GeneratedProtocolMessageType(
-    "ListEntitiesTextSensorResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_LISTENTITIESTEXTSENSORRESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:ListEntitiesTextSensorResponse)
-    ),
-)
-_sym_db.RegisterMessage(ListEntitiesTextSensorResponse)
-
-ListEntitiesDoneResponse = _reflection.GeneratedProtocolMessageType(
-    "ListEntitiesDoneResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_LISTENTITIESDONERESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:ListEntitiesDoneResponse)
-    ),
-)
-_sym_db.RegisterMessage(ListEntitiesDoneResponse)
-
-SubscribeStatesRequest = _reflection.GeneratedProtocolMessageType(
-    "SubscribeStatesRequest",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_SUBSCRIBESTATESREQUEST,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:SubscribeStatesRequest)
-    ),
-)
-_sym_db.RegisterMessage(SubscribeStatesRequest)
-
-BinarySensorStateResponse = _reflection.GeneratedProtocolMessageType(
-    "BinarySensorStateResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_BINARYSENSORSTATERESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:BinarySensorStateResponse)
-    ),
-)
-_sym_db.RegisterMessage(BinarySensorStateResponse)
-
-CoverStateResponse = _reflection.GeneratedProtocolMessageType(
-    "CoverStateResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_COVERSTATERESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:CoverStateResponse)
-    ),
-)
-_sym_db.RegisterMessage(CoverStateResponse)
-
-FanStateResponse = _reflection.GeneratedProtocolMessageType(
-    "FanStateResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_FANSTATERESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:FanStateResponse)
-    ),
-)
-_sym_db.RegisterMessage(FanStateResponse)
-
-LightStateResponse = _reflection.GeneratedProtocolMessageType(
-    "LightStateResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_LIGHTSTATERESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:LightStateResponse)
-    ),
-)
-_sym_db.RegisterMessage(LightStateResponse)
-
-SensorStateResponse = _reflection.GeneratedProtocolMessageType(
-    "SensorStateResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_SENSORSTATERESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:SensorStateResponse)
-    ),
-)
-_sym_db.RegisterMessage(SensorStateResponse)
-
-SwitchStateResponse = _reflection.GeneratedProtocolMessageType(
-    "SwitchStateResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_SWITCHSTATERESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:SwitchStateResponse)
-    ),
-)
-_sym_db.RegisterMessage(SwitchStateResponse)
-
-TextSensorStateResponse = _reflection.GeneratedProtocolMessageType(
-    "TextSensorStateResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_TEXTSENSORSTATERESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:TextSensorStateResponse)
-    ),
-)
-_sym_db.RegisterMessage(TextSensorStateResponse)
-
-CoverCommandRequest = _reflection.GeneratedProtocolMessageType(
-    "CoverCommandRequest",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_COVERCOMMANDREQUEST,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:CoverCommandRequest)
-    ),
-)
-_sym_db.RegisterMessage(CoverCommandRequest)
-
-FanCommandRequest = _reflection.GeneratedProtocolMessageType(
-    "FanCommandRequest",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_FANCOMMANDREQUEST,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:FanCommandRequest)
-    ),
-)
-_sym_db.RegisterMessage(FanCommandRequest)
-
-LightCommandRequest = _reflection.GeneratedProtocolMessageType(
-    "LightCommandRequest",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_LIGHTCOMMANDREQUEST,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:LightCommandRequest)
-    ),
-)
-_sym_db.RegisterMessage(LightCommandRequest)
-
-SwitchCommandRequest = _reflection.GeneratedProtocolMessageType(
-    "SwitchCommandRequest",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_SWITCHCOMMANDREQUEST,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:SwitchCommandRequest)
-    ),
-)
-_sym_db.RegisterMessage(SwitchCommandRequest)
-
-SubscribeLogsRequest = _reflection.GeneratedProtocolMessageType(
-    "SubscribeLogsRequest",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_SUBSCRIBELOGSREQUEST,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:SubscribeLogsRequest)
-    ),
-)
-_sym_db.RegisterMessage(SubscribeLogsRequest)
-
-SubscribeLogsResponse = _reflection.GeneratedProtocolMessageType(
-    "SubscribeLogsResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_SUBSCRIBELOGSRESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:SubscribeLogsResponse)
-    ),
-)
-_sym_db.RegisterMessage(SubscribeLogsResponse)
-
-SubscribeServiceCallsRequest = _reflection.GeneratedProtocolMessageType(
-    "SubscribeServiceCallsRequest",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_SUBSCRIBESERVICECALLSREQUEST,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:SubscribeServiceCallsRequest)
-    ),
-)
-_sym_db.RegisterMessage(SubscribeServiceCallsRequest)
-
-ServiceCallResponse = _reflection.GeneratedProtocolMessageType(
-    "ServiceCallResponse",
-    (_message.Message,),
-    dict(
-        DataEntry=_reflection.GeneratedProtocolMessageType(
-            "DataEntry",
-            (_message.Message,),
-            dict(
-                DESCRIPTOR=_SERVICECALLRESPONSE_DATAENTRY,
-                __module__="api_pb2"
-                # @@protoc_insertion_point(class_scope:ServiceCallResponse.DataEntry)
-            ),
-        ),
-        DataTemplateEntry=_reflection.GeneratedProtocolMessageType(
-            "DataTemplateEntry",
-            (_message.Message,),
-            dict(
-                DESCRIPTOR=_SERVICECALLRESPONSE_DATATEMPLATEENTRY,
-                __module__="api_pb2"
-                # @@protoc_insertion_point(class_scope:ServiceCallResponse.DataTemplateEntry)
-            ),
-        ),
-        VariablesEntry=_reflection.GeneratedProtocolMessageType(
-            "VariablesEntry",
-            (_message.Message,),
-            dict(
-                DESCRIPTOR=_SERVICECALLRESPONSE_VARIABLESENTRY,
-                __module__="api_pb2"
-                # @@protoc_insertion_point(class_scope:ServiceCallResponse.VariablesEntry)
-            ),
-        ),
-        DESCRIPTOR=_SERVICECALLRESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:ServiceCallResponse)
-    ),
-)
-_sym_db.RegisterMessage(ServiceCallResponse)
-_sym_db.RegisterMessage(ServiceCallResponse.DataEntry)
-_sym_db.RegisterMessage(ServiceCallResponse.DataTemplateEntry)
-_sym_db.RegisterMessage(ServiceCallResponse.VariablesEntry)
-
-SubscribeHomeAssistantStatesRequest = _reflection.GeneratedProtocolMessageType(
-    "SubscribeHomeAssistantStatesRequest",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_SUBSCRIBEHOMEASSISTANTSTATESREQUEST,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:SubscribeHomeAssistantStatesRequest)
-    ),
-)
-_sym_db.RegisterMessage(SubscribeHomeAssistantStatesRequest)
-
-SubscribeHomeAssistantStateResponse = _reflection.GeneratedProtocolMessageType(
-    "SubscribeHomeAssistantStateResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_SUBSCRIBEHOMEASSISTANTSTATERESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:SubscribeHomeAssistantStateResponse)
-    ),
-)
-_sym_db.RegisterMessage(SubscribeHomeAssistantStateResponse)
-
-HomeAssistantStateResponse = _reflection.GeneratedProtocolMessageType(
-    "HomeAssistantStateResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_HOMEASSISTANTSTATERESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:HomeAssistantStateResponse)
-    ),
-)
-_sym_db.RegisterMessage(HomeAssistantStateResponse)
-
-GetTimeRequest = _reflection.GeneratedProtocolMessageType(
-    "GetTimeRequest",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_GETTIMEREQUEST,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:GetTimeRequest)
-    ),
-)
-_sym_db.RegisterMessage(GetTimeRequest)
-
-GetTimeResponse = _reflection.GeneratedProtocolMessageType(
-    "GetTimeResponse",
-    (_message.Message,),
-    dict(
-        DESCRIPTOR=_GETTIMERESPONSE,
-        __module__="api_pb2"
-        # @@protoc_insertion_point(class_scope:GetTimeResponse)
-    ),
-)
-_sym_db.RegisterMessage(GetTimeResponse)
-
-
-_SERVICECALLRESPONSE_DATAENTRY._options = None
-_SERVICECALLRESPONSE_DATATEMPLATEENTRY._options = None
-_SERVICECALLRESPONSE_VARIABLESENTRY._options = None
-# @@protoc_insertion_point(module_scope)
diff --git a/esphome/api/client.py b/esphome/api/client.py
deleted file mode 100644
index dd11f79922..0000000000
--- a/esphome/api/client.py
+++ /dev/null
@@ -1,518 +0,0 @@
-from datetime import datetime
-import functools
-import logging
-import socket
-import threading
-import time
-
-# pylint: disable=unused-import
-from typing import Optional  # noqa
-from google.protobuf import message  # noqa
-
-from esphome import const
-import esphome.api.api_pb2 as pb
-from esphome.const import CONF_PASSWORD, CONF_PORT
-from esphome.core import EsphomeError
-from esphome.helpers import resolve_ip_address, indent
-from esphome.log import color, Fore
-from esphome.util import safe_print
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class APIConnectionError(EsphomeError):
-    pass
-
-
-MESSAGE_TYPE_TO_PROTO = {
-    1: pb.HelloRequest,
-    2: pb.HelloResponse,
-    3: pb.ConnectRequest,
-    4: pb.ConnectResponse,
-    5: pb.DisconnectRequest,
-    6: pb.DisconnectResponse,
-    7: pb.PingRequest,
-    8: pb.PingResponse,
-    9: pb.DeviceInfoRequest,
-    10: pb.DeviceInfoResponse,
-    11: pb.ListEntitiesRequest,
-    12: pb.ListEntitiesBinarySensorResponse,
-    13: pb.ListEntitiesCoverResponse,
-    14: pb.ListEntitiesFanResponse,
-    15: pb.ListEntitiesLightResponse,
-    16: pb.ListEntitiesSensorResponse,
-    17: pb.ListEntitiesSwitchResponse,
-    18: pb.ListEntitiesTextSensorResponse,
-    19: pb.ListEntitiesDoneResponse,
-    20: pb.SubscribeStatesRequest,
-    21: pb.BinarySensorStateResponse,
-    22: pb.CoverStateResponse,
-    23: pb.FanStateResponse,
-    24: pb.LightStateResponse,
-    25: pb.SensorStateResponse,
-    26: pb.SwitchStateResponse,
-    27: pb.TextSensorStateResponse,
-    28: pb.SubscribeLogsRequest,
-    29: pb.SubscribeLogsResponse,
-    30: pb.CoverCommandRequest,
-    31: pb.FanCommandRequest,
-    32: pb.LightCommandRequest,
-    33: pb.SwitchCommandRequest,
-    34: pb.SubscribeServiceCallsRequest,
-    35: pb.ServiceCallResponse,
-    36: pb.GetTimeRequest,
-    37: pb.GetTimeResponse,
-}
-
-
-def _varuint_to_bytes(value):
-    if value <= 0x7F:
-        return bytes([value])
-
-    ret = bytes()
-    while value:
-        temp = value & 0x7F
-        value >>= 7
-        if value:
-            ret += bytes([temp | 0x80])
-        else:
-            ret += bytes([temp])
-
-    return ret
-
-
-def _bytes_to_varuint(value):
-    result = 0
-    bitpos = 0
-    for val in value:
-        result |= (val & 0x7F) << bitpos
-        bitpos += 7
-        if (val & 0x80) == 0:
-            return result
-    return None
-
-
-# pylint: disable=too-many-instance-attributes,not-callable
-class APIClient(threading.Thread):
-    def __init__(self, address, port, password):
-        threading.Thread.__init__(self)
-        self._address = address  # type: str
-        self._port = port  # type: int
-        self._password = password  # type: Optional[str]
-        self._socket = None  # type: Optional[socket.socket]
-        self._socket_open_event = threading.Event()
-        self._socket_write_lock = threading.Lock()
-        self._connected = False
-        self._authenticated = False
-        self._message_handlers = []
-        self._keepalive = 5
-        self._ping_timer = None
-
-        self.on_disconnect = None
-        self.on_connect = None
-        self.on_login = None
-        self.auto_reconnect = False
-        self._running_event = threading.Event()
-        self._stop_event = threading.Event()
-
-    @property
-    def stopped(self):
-        return self._stop_event.is_set()
-
-    def _refresh_ping(self):
-        if self._ping_timer is not None:
-            self._ping_timer.cancel()
-            self._ping_timer = None
-
-        def func():
-            self._ping_timer = None
-
-            if self._connected:
-                try:
-                    self.ping()
-                except APIConnectionError as err:
-                    self._fatal_error(err)
-                else:
-                    self._refresh_ping()
-
-        self._ping_timer = threading.Timer(self._keepalive, func)
-        self._ping_timer.start()
-
-    def _cancel_ping(self):
-        if self._ping_timer is not None:
-            self._ping_timer.cancel()
-            self._ping_timer = None
-
-    def _close_socket(self):
-        self._cancel_ping()
-        if self._socket is not None:
-            self._socket.close()
-            self._socket = None
-        self._socket_open_event.clear()
-        self._connected = False
-        self._authenticated = False
-        self._message_handlers = []
-
-    def stop(self, force=False):
-        if self.stopped:
-            raise ValueError
-
-        if self._connected and not force:
-            try:
-                self.disconnect()
-            except APIConnectionError:
-                pass
-        self._close_socket()
-
-        self._stop_event.set()
-        if not force:
-            self.join()
-
-    def connect(self):
-        if not self._running_event.wait(0.1):
-            raise APIConnectionError("You need to call start() first!")
-
-        if self._connected:
-            self.disconnect(on_disconnect=False)
-
-        try:
-            ip = resolve_ip_address(self._address)
-        except EsphomeError as err:
-            _LOGGER.warning(
-                "Error resolving IP address of %s. Is it connected to WiFi?",
-                self._address,
-            )
-            _LOGGER.warning(
-                "(If this error persists, please set a static IP address: "
-                "https://esphome.io/components/wifi.html#manual-ips)"
-            )
-            raise APIConnectionError(err) from err
-
-        _LOGGER.info("Connecting to %s:%s (%s)", self._address, self._port, ip)
-        self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        self._socket.settimeout(10.0)
-        self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
-        try:
-            self._socket.connect((ip, self._port))
-        except OSError as err:
-            err = APIConnectionError(f"Error connecting to {ip}: {err}")
-            self._fatal_error(err)
-            raise err
-        self._socket.settimeout(0.1)
-
-        self._socket_open_event.set()
-
-        hello = pb.HelloRequest()
-        hello.client_info = f"ESPHome v{const.__version__}"
-        try:
-            resp = self._send_message_await_response(hello, pb.HelloResponse)
-        except APIConnectionError as err:
-            self._fatal_error(err)
-            raise err
-        _LOGGER.debug(
-            "Successfully connected to %s ('%s' API=%s.%s)",
-            self._address,
-            resp.server_info,
-            resp.api_version_major,
-            resp.api_version_minor,
-        )
-        self._connected = True
-        self._refresh_ping()
-        if self.on_connect is not None:
-            self.on_connect()
-
-    def _check_connected(self):
-        if not self._connected:
-            err = APIConnectionError("Must be connected!")
-            self._fatal_error(err)
-            raise err
-
-    def login(self):
-        self._check_connected()
-        if self._authenticated:
-            raise APIConnectionError("Already logged in!")
-
-        connect = pb.ConnectRequest()
-        if self._password is not None:
-            connect.password = self._password
-        resp = self._send_message_await_response(connect, pb.ConnectResponse)
-        if resp.invalid_password:
-            raise APIConnectionError("Invalid password!")
-
-        self._authenticated = True
-        if self.on_login is not None:
-            self.on_login()
-
-    def _fatal_error(self, err):
-        was_connected = self._connected
-
-        self._close_socket()
-
-        if was_connected and self.on_disconnect is not None:
-            self.on_disconnect(err)
-
-    def _write(self, data):  # type: (bytes) -> None
-        if self._socket is None:
-            raise APIConnectionError("Socket closed")
-
-        # _LOGGER.debug("Write: %s", format_bytes(data))
-        with self._socket_write_lock:
-            try:
-                self._socket.sendall(data)
-            except OSError as err:
-                err = APIConnectionError(f"Error while writing data: {err}")
-                self._fatal_error(err)
-                raise err
-
-    def _send_message(self, msg):
-        # type: (message.Message) -> None
-        for message_type, klass in MESSAGE_TYPE_TO_PROTO.items():
-            if isinstance(msg, klass):
-                break
-        else:
-            raise ValueError
-
-        encoded = msg.SerializeToString()
-        _LOGGER.debug("Sending %s:\n%s", type(msg), indent(str(msg)))
-        req = bytes([0])
-        req += _varuint_to_bytes(len(encoded))
-        req += _varuint_to_bytes(message_type)
-        req += encoded
-        self._write(req)
-
-    def _send_message_await_response_complex(
-        self, send_msg, do_append, do_stop, timeout=5
-    ):
-        event = threading.Event()
-        responses = []
-
-        def on_message(resp):
-            if do_append(resp):
-                responses.append(resp)
-            if do_stop(resp):
-                event.set()
-
-        self._message_handlers.append(on_message)
-        self._send_message(send_msg)
-        ret = event.wait(timeout)
-        try:
-            self._message_handlers.remove(on_message)
-        except ValueError:
-            pass
-        if not ret:
-            raise APIConnectionError("Timeout while waiting for message response!")
-        return responses
-
-    def _send_message_await_response(self, send_msg, response_type, timeout=5):
-        def is_response(msg):
-            return isinstance(msg, response_type)
-
-        return self._send_message_await_response_complex(
-            send_msg, is_response, is_response, timeout
-        )[0]
-
-    def device_info(self):
-        self._check_connected()
-        return self._send_message_await_response(
-            pb.DeviceInfoRequest(), pb.DeviceInfoResponse
-        )
-
-    def ping(self):
-        self._check_connected()
-        return self._send_message_await_response(pb.PingRequest(), pb.PingResponse)
-
-    def disconnect(self, on_disconnect=True):
-        self._check_connected()
-
-        try:
-            self._send_message_await_response(
-                pb.DisconnectRequest(), pb.DisconnectResponse
-            )
-        except APIConnectionError:
-            pass
-        self._close_socket()
-
-        if self.on_disconnect is not None and on_disconnect:
-            self.on_disconnect(None)
-
-    def _check_authenticated(self):
-        if not self._authenticated:
-            raise APIConnectionError("Must login first!")
-
-    def subscribe_logs(self, on_log, log_level=7, dump_config=False):
-        self._check_authenticated()
-
-        def on_msg(msg):
-            if isinstance(msg, pb.SubscribeLogsResponse):
-                on_log(msg)
-
-        self._message_handlers.append(on_msg)
-        req = pb.SubscribeLogsRequest(dump_config=dump_config)
-        req.level = log_level
-        self._send_message(req)
-
-    def _recv(self, amount):
-        ret = bytes()
-        if amount == 0:
-            return ret
-
-        while len(ret) < amount:
-            if self.stopped:
-                raise APIConnectionError("Stopped!")
-            if not self._socket_open_event.is_set():
-                raise APIConnectionError("No socket!")
-            try:
-                val = self._socket.recv(amount - len(ret))
-            except AttributeError as err:
-                raise APIConnectionError("Socket was closed") from err
-            except socket.timeout:
-                continue
-            except OSError as err:
-                raise APIConnectionError(f"Error while receiving data: {err}") from err
-            ret += val
-        return ret
-
-    def _recv_varint(self):
-        raw = bytes()
-        while not raw or raw[-1] & 0x80:
-            raw += self._recv(1)
-        return _bytes_to_varuint(raw)
-
-    def _run_once(self):
-        if not self._socket_open_event.wait(0.1):
-            return
-
-        # Preamble
-        if self._recv(1)[0] != 0x00:
-            raise APIConnectionError("Invalid preamble")
-
-        length = self._recv_varint()
-        msg_type = self._recv_varint()
-
-        raw_msg = self._recv(length)
-        if msg_type not in MESSAGE_TYPE_TO_PROTO:
-            _LOGGER.debug("Skipping message type %s", msg_type)
-            return
-
-        msg = MESSAGE_TYPE_TO_PROTO[msg_type]()
-        msg.ParseFromString(raw_msg)
-        _LOGGER.debug("Got message: %s:\n%s", type(msg), indent(str(msg)))
-        for msg_handler in self._message_handlers[:]:
-            msg_handler(msg)
-        self._handle_internal_messages(msg)
-
-    def run(self):
-        self._running_event.set()
-        while not self.stopped:
-            try:
-                self._run_once()
-            except APIConnectionError as err:
-                if self.stopped:
-                    break
-                if self._connected:
-                    _LOGGER.error("Error while reading incoming messages: %s", err)
-                    self._fatal_error(err)
-        self._running_event.clear()
-
-    def _handle_internal_messages(self, msg):
-        if isinstance(msg, pb.DisconnectRequest):
-            self._send_message(pb.DisconnectResponse())
-            if self._socket is not None:
-                self._socket.close()
-                self._socket = None
-            self._connected = False
-            if self.on_disconnect is not None:
-                self.on_disconnect(None)
-        elif isinstance(msg, pb.PingRequest):
-            self._send_message(pb.PingResponse())
-        elif isinstance(msg, pb.GetTimeRequest):
-            resp = pb.GetTimeResponse()
-            resp.epoch_seconds = int(time.time())
-            self._send_message(resp)
-
-
-def run_logs(config, address):
-    conf = config["api"]
-    port = conf[CONF_PORT]
-    password = conf[CONF_PASSWORD]
-    _LOGGER.info("Starting log output from %s using esphome API", address)
-
-    cli = APIClient(address, port, password)
-    stopping = False
-    retry_timer = []
-
-    has_connects = []
-
-    def try_connect(err, tries=0):
-        if stopping:
-            return
-
-        if err:
-            _LOGGER.warning("Disconnected from API: %s", err)
-
-        while retry_timer:
-            retry_timer.pop(0).cancel()
-
-        error = None
-        try:
-            cli.connect()
-            cli.login()
-        except APIConnectionError as err2:  # noqa
-            error = err2
-
-        if error is None:
-            _LOGGER.info("Successfully connected to %s", address)
-            return
-
-        wait_time = int(min(1.5 ** min(tries, 100), 30))
-        if not has_connects:
-            _LOGGER.warning(
-                "Initial connection failed. The ESP might not be connected "
-                "to WiFi yet (%s). Re-Trying in %s seconds",
-                error,
-                wait_time,
-            )
-        else:
-            _LOGGER.warning(
-                "Couldn't connect to API (%s). Trying to reconnect in %s seconds",
-                error,
-                wait_time,
-            )
-        timer = threading.Timer(
-            wait_time, functools.partial(try_connect, None, tries + 1)
-        )
-        timer.start()
-        retry_timer.append(timer)
-
-    def on_log(msg):
-        time_ = datetime.now().time().strftime("[%H:%M:%S]")
-        text = msg.message
-        if msg.send_failed:
-            text = color(
-                Fore.WHITE,
-                "(Message skipped because it was too big to fit in "
-                "TCP buffer - This is only cosmetic)",
-            )
-        safe_print(time_ + text)
-
-    def on_login():
-        try:
-            cli.subscribe_logs(on_log, dump_config=not has_connects)
-            has_connects.append(True)
-        except APIConnectionError:
-            cli.disconnect()
-
-    cli.on_disconnect = try_connect
-    cli.on_login = on_login
-    cli.start()
-
-    try:
-        try_connect(None)
-        while True:
-            time.sleep(1)
-    except KeyboardInterrupt:
-        stopping = True
-        cli.stop(True)
-        while retry_timer:
-            retry_timer.pop(0).cancel()
-    return 0
diff --git a/esphome/boards.py b/esphome/boards.py
index 220d440a37..ba6fe889ea 100644
--- a/esphome/boards.py
+++ b/esphome/boards.py
@@ -55,6 +55,7 @@ ESP8266_BOARD_PINS = {
     "espectro": {"LED": 15, "BUTTON": 2},
     "espino": {"LED": 2, "LED_RED": 2, "LED_GREEN": 4, "LED_BLUE": 5, "BUTTON": 0},
     "espinotee": {"LED": 16},
+    "espmxdevkit": {},
     "espresso_lite_v1": {"LED": 16},
     "espresso_lite_v2": {"LED": 2},
     "gen4iod": {},
@@ -105,6 +106,10 @@ ESP8266_BOARD_PINS = {
     },
     "phoenix_v1": {"LED": 16},
     "phoenix_v2": {"LED": 2},
+    "sonoff_basic": {},
+    "sonoff_s20": {},
+    "sonoff_sv": {},
+    "sonoff_th": {},
     "sparkfunBlynk": "thing",
     "thing": {"LED": 5, "SDA": 2, "SCL": 14},
     "thingdev": "thing",
@@ -166,6 +171,7 @@ ESP8266_FLASH_SIZES = {
     "espectro": FLASH_SIZE_4_MB,
     "espino": FLASH_SIZE_4_MB,
     "espinotee": FLASH_SIZE_4_MB,
+    "espmxdevkit": FLASH_SIZE_1_MB,
     "espresso_lite_v1": FLASH_SIZE_4_MB,
     "espresso_lite_v2": FLASH_SIZE_4_MB,
     "gen4iod": FLASH_SIZE_512_KB,
@@ -178,6 +184,10 @@ ESP8266_FLASH_SIZES = {
     "oak": FLASH_SIZE_4_MB,
     "phoenix_v1": FLASH_SIZE_4_MB,
     "phoenix_v2": FLASH_SIZE_4_MB,
+    "sonoff_basic": FLASH_SIZE_1_MB,
+    "sonoff_s20": FLASH_SIZE_1_MB,
+    "sonoff_sv": FLASH_SIZE_1_MB,
+    "sonoff_th": FLASH_SIZE_1_MB,
     "sparkfunBlynk": FLASH_SIZE_4_MB,
     "thing": FLASH_SIZE_512_KB,
     "thingdev": FLASH_SIZE_512_KB,
@@ -291,6 +301,7 @@ ESP32_BOARD_PINS = {
         "SW2": 2,
         "SW3": 0,
     },
+    "az-delivery-devkit-v4": {},
     "bpi-bit": {
         "BUTTON_A": 35,
         "BUTTON_B": 27,
@@ -320,6 +331,8 @@ ESP32_BOARD_PINS = {
         "RGB_LED": 4,
         "TEMPERATURE_SENSOR": 34,
     },
+    "briki_abc_esp32": {},
+    "briki_mbc-wb_esp32": {},
     "d-duino-32": {
         "D1": 5,
         "D10": 1,
@@ -380,11 +393,58 @@ ESP32_BOARD_PINS = {
     "esp32cam": {},
     "esp32dev": {},
     "esp32doit-devkit-v1": {"LED": 2},
+    "esp32doit-espduino": {"TX0": 1, "RX0": 3, "CMD": 11, "CLK": 6, "SD0": 7, "SD1": 8},
     "esp32thing": {"BUTTON": 0, "LED": 5, "SS": 2},
+    "esp32thing_plus": {
+        "SDA": 23,
+        "SCL": 22,
+        "SS": 33,
+        "MOSI": 18,
+        "MISO": 19,
+        "SCK": 5,
+        "A0": 26,
+        "A1": 25,
+        "A2": 34,
+        "A3": 39,
+        "A4": 36,
+        "A5": 4,
+        "A6": 14,
+        "A7": 32,
+        "A8": 15,
+        "A9": 33,
+        "A10": 27,
+        "A11": 12,
+        "A12": 13,
+    },
     "esp32vn-iot-uno": {},
     "espea32": {"BUTTON": 0, "LED": 5},
     "espectro32": {"LED": 15, "SD_SS": 33},
     "espino32": {"BUTTON": 0, "LED": 16},
+    "etboard": {
+        "LED_BUILTIN": 5,
+        "TX": 34,
+        "RX": 35,
+        "SS": 29,
+        "MOSI": 37,
+        "MISO": 31,
+        "SCK": 30,
+        "A0": 36,
+        "A1": 39,
+        "A2": 32,
+        "A3": 33,
+        "A4": 34,
+        "A5": 35,
+        "A6": 25,
+        "A7": 26,
+        "D2": 27,
+        "D3": 14,
+        "D4": 12,
+        "D5": 13,
+        "D6": 15,
+        "D7": 16,
+        "D8": 17,
+        "D9": 4,
+    },
     "featheresp32": {
         "A0": 26,
         "A1": 25,
@@ -434,6 +494,18 @@ ESP32_BOARD_PINS = {
         "SW4": 21,
     },
     "frogboard": {},
+    "healtypi4": {
+        "KEY_BUILTIN": 17,
+        "ADS1292_DRDY_PIN": 26,
+        "ADS1292_CS_PIN": 13,
+        "ADS1292_START_PIN": 14,
+        "ADS1292_PWDN_PIN": 27,
+        "AFE4490_CS_PIN": 21,
+        "AFE4490_DRDY_PIN": 39,
+        "AFE4490_PWDN_PIN": 4,
+        "PUSH_BUTTON": 17,
+        "SLIDE_SWITCH": 16,
+    },
     "heltec_wifi_kit_32": {
         "A1": 37,
         "A2": 38,
@@ -444,6 +516,7 @@ ESP32_BOARD_PINS = {
         "SDA_OLED": 4,
         "Vext": 21,
     },
+    "heltec_wifi_kit_32_v2": "heltec_wifi_kit_32",
     "heltec_wifi_lora_32": {
         "BUTTON": 0,
         "DIO0": 26,
@@ -489,8 +562,68 @@ ESP32_BOARD_PINS = {
         "SS": 18,
         "Vext": 21,
     },
+    "heltec_wireless_stick_lite": {
+        "LED_BUILTIN": 25,
+        "KEY_BUILTIN": 0,
+        "SS": 18,
+        "MOSI": 27,
+        "MISO": 19,
+        "SCK": 5,
+        "Vext": 21,
+        "LED": 25,
+        "RST_LoRa": 14,
+        "DIO0": 26,
+        "DIO1": 35,
+        "DIO2": 34,
+    },
+    "honeylemon": {
+        "LED_BUILTIN": 2,
+        "BUILTIN_KEY": 0,
+    },
     "hornbill32dev": {"BUTTON": 0, "LED": 13},
     "hornbill32minima": {"SS": 2},
+    "imbrios-logsens-v1p1": {
+        "LED_BUILTIN": 33,
+        "UART2_TX": 17,
+        "UART2_RX": 16,
+        "UART2_RTS": 4,
+        "CAN_TX": 17,
+        "CAN_RX": 16,
+        "CAN_TXDE": 4,
+        "SS": 15,
+        "MOSI": 13,
+        "MISO": 12,
+        "SCK": 14,
+        "SPI_SS1": 23,
+        "BUZZER_CTRL": 19,
+        "SD_CARD_DETECT": 35,
+        "SW2_BUILDIN": 0,
+        "SW3_BUILDIN": 36,
+        "SW4_BUILDIN": 34,
+        "LED1_BUILDIN": 32,
+        "LED2_BUILDIN": 33,
+    },
+    "inex_openkb": {
+        "LED_BUILTIN": 16,
+        "LDR_PIN": 36,
+        "SW1": 16,
+        "SW2": 14,
+        "BT_LED": 17,
+        "WIFI_LED": 2,
+        "NTP_LED": 15,
+        "IOT_LED": 12,
+        "BUZZER": 13,
+        "INPUT1": 32,
+        "INPUT2": 33,
+        "INPUT3": 34,
+        "INPUT4": 35,
+        "OUTPUT1": 26,
+        "OUTPUT2": 27,
+        "SDA0": 21,
+        "SCL0": 22,
+        "SDA1": 4,
+        "SCL1": 5,
+    },
     "intorobot": {
         "A1": 39,
         "A2": 35,
@@ -528,6 +661,40 @@ ESP32_BOARD_PINS = {
     "iotaap_magnolia": {},
     "iotbusio": {},
     "iotbusproteus": {},
+    "kits-edu": {},
+    "labplus_mpython": {
+        "SDA": 23,
+        "SCL": 22,
+        "P0": 33,
+        "P1": 32,
+        "P2": 35,
+        "P3": 34,
+        "P4": 39,
+        "P5": 0,
+        "P6": 16,
+        "P7": 17,
+        "P8": 26,
+        "P9": 25,
+        "P10": 36,
+        "P11": 2,
+        "P13": 18,
+        "P14": 19,
+        "P15": 21,
+        "P16": 5,
+        "P19": 22,
+        "P20": 23,
+        "P": 27,
+        "Y": 14,
+        "T": 12,
+        "H": 13,
+        "O": 15,
+        "N": 4,
+        "BTN_A": 0,
+        "BTN_B": 2,
+        "SOUND": 36,
+        "LIGHT": 39,
+        "BUZZER": 16,
+    },
     "lolin32": {"LED": 5},
     "lolin32_lite": {"LED": 22},
     "lolin_d32": {"LED": 5, "_VBAT": 35},
@@ -554,6 +721,16 @@ ESP32_BOARD_PINS = {
         "SDA": 12,
         "SS": 18,
     },
+    "m5stack-atom": {
+        "SDA": 26,
+        "SCL": 32,
+        "ADC1": 35,
+        "ADC2": 36,
+        "SS": 19,
+        "MOSI": 33,
+        "MISO": 23,
+        "SCK": 22,
+    },
     "m5stack-core-esp32": {
         "ADC1": 35,
         "ADC2": 36,
@@ -580,6 +757,26 @@ ESP32_BOARD_PINS = {
         "RXD2": 16,
         "TXD2": 17,
     },
+    "m5stack-core2": {
+        "SDA": 32,
+        "SCL": 33,
+        "SS": 5,
+        "MOSI": 23,
+        "MISO": 38,
+        "SCK": 18,
+        "ADC1": 35,
+        "ADC2": 36,
+    },
+    "m5stack-coreink": {
+        "SDA": 32,
+        "SCL": 33,
+        "SS": 9,
+        "MOSI": 23,
+        "MISO": 34,
+        "SCK": 18,
+        "ADC1": 35,
+        "ADC2": 36,
+    },
     "m5stack-fire": {
         "ADC1": 35,
         "ADC2": 36,
@@ -630,6 +827,17 @@ ESP32_BOARD_PINS = {
         "RXD2": 16,
         "TXD2": 17,
     },
+    "m5stack-timer-cam": {
+        "LED_BUILTIN": 2,
+        "SDA": 4,
+        "SCL": 13,
+        "SS": 5,
+        "MOSI": 23,
+        "MISO": 19,
+        "SCK": 18,
+        "ADC1": 35,
+        "ADC2": 36,
+    },
     "m5stick-c": {
         "ADC1": 35,
         "ADC2": 36,
@@ -664,6 +872,17 @@ ESP32_BOARD_PINS = {
         "RIGHT_PUTTON": 34,
         "YELLOW_LED": 18,
     },
+    "mgbot-iotik32a": {
+        "LED_BUILTIN": 4,
+        "TX2": 17,
+        "RX2": 16,
+    },
+    "mgbot-iotik32b": {
+        "LED_BUILTIN": 18,
+        "IR": 27,
+        "TX2": 17,
+        "RX2": 16,
+    },
     "mhetesp32devkit": {"LED": 2},
     "mhetesp32minikit": {"LED": 2},
     "microduino-core-esp32": {
@@ -740,6 +959,7 @@ ESP32_BOARD_PINS = {
     },
     "node32s": {},
     "nodemcu-32s": {"BUTTON": 0, "LED": 2},
+    "nscreen-32": {},
     "odroid_esp32": {"ADC1": 35, "ADC2": 36, "LED": 2, "SCL": 4, "SDA": 15, "SS": 22},
     "onehorse32dev": {"A1": 37, "A2": 38, "BUTTON": 0, "LED": 5},
     "oroca_edubot": {
@@ -766,6 +986,10 @@ ESP32_BOARD_PINS = {
         "VBAT": 35,
     },
     "pico32": {},
+    "piranha_esp32": {
+        "LED_BUILTIN": 2,
+        "KEY_BUILTIN": 0,
+    },
     "pocket_32": {"LED": 16},
     "pycom_gpy": {
         "A1": 37,
@@ -778,7 +1002,14 @@ ESP32_BOARD_PINS = {
         "SDA": 12,
         "SS": 17,
     },
+    "qchip": "heltec_wifi_kit_32",
     "quantum": {},
+    "s_odi_ultra": {
+        "LED_BUILTIN": 2,
+        "LED_BUILTINB": 4,
+    },
+    "sensesiot_weizen": {},
+    "sg-o_airMon": {},
     "sparkfun_lora_gateway_1-channel": {"MISO": 12, "MOSI": 13, "SCK": 14, "SS": 16},
     "tinypico": {},
     "ttgo-lora32-v1": {
@@ -790,6 +1021,26 @@ ESP32_BOARD_PINS = {
         "SCK": 5,
         "SS": 18,
     },
+    "ttgo-lora32-v2": {
+        "LED_BUILTIN": 22,
+        "KEY_BUILTIN": 0,
+        "SS": 18,
+        "MOSI": 27,
+        "MISO": 19,
+        "SCK": 5,
+        "A1": 37,
+        "A2": 38,
+    },
+    "ttgo-lora32-v21": {
+        "LED_BUILTIN": 25,
+        "KEY_BUILTIN": 0,
+        "SS": 18,
+        "MOSI": 27,
+        "MISO": 19,
+        "SCK": 5,
+        "A1": 37,
+        "A2": 38,
+    },
     "ttgo-t-beam": {"BUTTON": 39, "LED": 14, "MOSI": 27, "SCK": 5, "SS": 18},
     "ttgo-t-watch": {"BUTTON": 36, "MISO": 2, "MOSI": 15, "SCK": 14, "SS": 13},
     "ttgo-t1": {"LED": 22, "MISO": 2, "MOSI": 15, "SCK": 14, "SCL": 23, "SS": 13},
@@ -855,6 +1106,32 @@ ESP32_BOARD_PINS = {
         "T5": 5,
         "T6": 4,
     },
+    "wifiduino32": {
+        "LED_BUILTIN": 2,
+        "KEY_BUILTIN": 0,
+        "SDA": 5,
+        "SCL": 16,
+        "A0": 27,
+        "A1": 14,
+        "A2": 12,
+        "A3": 35,
+        "A4": 13,
+        "A5": 4,
+        "D0": 3,
+        "D1": 1,
+        "D2": 17,
+        "D3": 15,
+        "D4": 32,
+        "D5": 33,
+        "D6": 25,
+        "D7": 26,
+        "D8": 23,
+        "D9": 22,
+        "D10": 21,
+        "D11": 19,
+        "D12": 18,
+        "D13": 2,
+    },
     "xinabox_cw02": {"LED": 27},
 }
 
diff --git a/esphome/components/adalight/adalight_light_effect.cpp b/esphome/components/adalight/adalight_light_effect.cpp
index 5f60fbe0b2..d9c2892d21 100644
--- a/esphome/components/adalight/adalight_light_effect.cpp
+++ b/esphome/components/adalight/adalight_light_effect.cpp
@@ -44,6 +44,7 @@ void AdalightLightEffect::blank_all_leds_(light::AddressableLight &it) {
   for (int led = it.size(); led-- > 0;) {
     it[led].set(Color::BLACK);
   }
+  it.schedule_show();
 }
 
 void AdalightLightEffect::apply(light::AddressableLight &it, const Color &current_color) {
@@ -133,6 +134,7 @@ AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableL
     it[led].set(Color(led_data[0], led_data[1], led_data[2], white));
   }
 
+  it.schedule_show();
   return CONSUMED;
 }
 
diff --git a/esphome/components/airthings_ble/__init__.py b/esphome/components/airthings_ble/__init__.py
new file mode 100644
index 0000000000..ca94069703
--- /dev/null
+++ b/esphome/components/airthings_ble/__init__.py
@@ -0,0 +1,23 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import esp32_ble_tracker
+from esphome.const import CONF_ID
+
+DEPENDENCIES = ["esp32_ble_tracker"]
+CODEOWNERS = ["@jeromelaban"]
+
+airthings_ble_ns = cg.esphome_ns.namespace("airthings_ble")
+AirthingsListener = airthings_ble_ns.class_(
+    "AirthingsListener", esp32_ble_tracker.ESPBTDeviceListener
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(AirthingsListener),
+    }
+).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield esp32_ble_tracker.register_ble_device(var, config)
diff --git a/esphome/components/airthings_ble/airthings_listener.cpp b/esphome/components/airthings_ble/airthings_listener.cpp
new file mode 100644
index 0000000000..921e42c498
--- /dev/null
+++ b/esphome/components/airthings_ble/airthings_listener.cpp
@@ -0,0 +1,33 @@
+#include "airthings_listener.h"
+#include "esphome/core/log.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace airthings_ble {
+
+static const char *TAG = "airthings_ble";
+
+bool AirthingsListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
+  for (auto &it : device.get_manufacturer_datas()) {
+    if (it.uuid == esp32_ble_tracker::ESPBTUUID::from_uint32(0x0334)) {
+      if (it.data.size() < 4)
+        continue;
+
+      uint32_t sn = it.data[0];
+      sn |= ((uint32_t) it.data[1] << 8);
+      sn |= ((uint32_t) it.data[2] << 16);
+      sn |= ((uint32_t) it.data[3] << 24);
+
+      ESP_LOGD(TAG, "Found AirThings device Serial:%u (MAC: %s)", sn, device.address_str().c_str());
+      return true;
+    }
+  }
+
+  return false;
+}
+
+}  // namespace airthings_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/airthings_ble/airthings_listener.h b/esphome/components/airthings_ble/airthings_listener.h
new file mode 100644
index 0000000000..cd240ac1ba
--- /dev/null
+++ b/esphome/components/airthings_ble/airthings_listener.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#ifdef ARDUINO_ARCH_ESP32
+
+#include "esphome/core/component.h"
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+#include <BLEDevice.h>
+
+namespace esphome {
+namespace airthings_ble {
+
+class AirthingsListener : public esp32_ble_tracker::ESPBTDeviceListener {
+ public:
+  bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
+};
+
+}  // namespace airthings_ble
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/airthings_wave_plus/__init__.py b/esphome/components/airthings_wave_plus/__init__.py
new file mode 100644
index 0000000000..1aff461edd
--- /dev/null
+++ b/esphome/components/airthings_wave_plus/__init__.py
@@ -0,0 +1 @@
+CODEOWNERS = ["@jeromelaban"]
diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp
new file mode 100644
index 0000000000..6b2e807e0b
--- /dev/null
+++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp
@@ -0,0 +1,142 @@
+#include "airthings_wave_plus.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace airthings_wave_plus {
+
+void AirthingsWavePlus::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
+                                            esp_ble_gattc_cb_param_t *param) {
+  switch (event) {
+    case ESP_GATTC_OPEN_EVT: {
+      if (param->open.status == ESP_GATT_OK) {
+        ESP_LOGI(TAG, "Connected successfully!");
+      }
+      break;
+    }
+
+    case ESP_GATTC_DISCONNECT_EVT: {
+      ESP_LOGW(TAG, "Disconnected!");
+      break;
+    }
+
+    case ESP_GATTC_SEARCH_CMPL_EVT: {
+      this->handle = 0;
+      auto chr = this->parent()->get_characteristic(service_uuid, sensors_data_characteristic_uuid);
+      if (chr == nullptr) {
+        ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid.to_string().c_str(),
+                 sensors_data_characteristic_uuid.to_string().c_str());
+        break;
+      }
+      this->handle = chr->handle;
+      this->node_state = espbt::ClientState::Established;
+
+      request_read_values_();
+      break;
+    }
+
+    case ESP_GATTC_READ_CHAR_EVT: {
+      if (param->read.conn_id != this->parent()->conn_id)
+        break;
+      if (param->read.status != ESP_GATT_OK) {
+        ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
+        break;
+      }
+      if (param->read.handle == this->handle) {
+        read_sensors_(param->read.value, param->read.value_len);
+      }
+      break;
+    }
+
+    default:
+      break;
+  }
+}
+
+void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) {
+  auto value = (WavePlusReadings *) raw_value;
+
+  if (sizeof(WavePlusReadings) <= value_len) {
+    ESP_LOGD(TAG, "version = %d", value->version);
+
+    if (value->version == 1) {
+      ESP_LOGD(TAG, "ambient light = %d", value->ambientLight);
+
+      this->humidity_sensor_->publish_state(value->humidity / 2.0f);
+      if (is_valid_radon_value_(value->radon)) {
+        this->radon_sensor_->publish_state(value->radon);
+      }
+      if (is_valid_radon_value_(value->radon_lt)) {
+        this->radon_long_term_sensor_->publish_state(value->radon_lt);
+      }
+      this->temperature_sensor_->publish_state(value->temperature / 100.0f);
+      this->pressure_sensor_->publish_state(value->pressure / 50.0f);
+      if (is_valid_co2_value_(value->co2)) {
+        this->co2_sensor_->publish_state(value->co2);
+      }
+      if (is_valid_voc_value_(value->voc)) {
+        this->tvoc_sensor_->publish_state(value->voc);
+      }
+
+      // This instance must not stay connected
+      // so other clients can connect to it (e.g. the
+      // mobile app).
+      parent()->set_enabled(false);
+    } else {
+      ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version);
+    }
+  }
+}
+
+bool AirthingsWavePlus::is_valid_radon_value_(short radon) { return 0 <= radon && radon <= 16383; }
+
+bool AirthingsWavePlus::is_valid_voc_value_(short voc) { return 0 <= voc && voc <= 16383; }
+
+bool AirthingsWavePlus::is_valid_co2_value_(short co2) { return 0 <= co2 && co2 <= 16383; }
+
+void AirthingsWavePlus::loop() {}
+
+void AirthingsWavePlus::update() {
+  if (this->node_state != espbt::ClientState::Established) {
+    if (!parent()->enabled) {
+      ESP_LOGW(TAG, "Reconnecting to device");
+      parent()->set_enabled(true);
+      parent()->connect();
+    } else {
+      ESP_LOGW(TAG, "Connection in progress");
+    }
+  }
+}
+
+void AirthingsWavePlus::request_read_values_() {
+  auto status =
+      esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle, ESP_GATT_AUTH_REQ_NONE);
+  if (status) {
+    ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status);
+  }
+}
+
+void AirthingsWavePlus::dump_config() {
+  LOG_SENSOR("  ", "Humidity", this->humidity_sensor_);
+  LOG_SENSOR("  ", "Radon", this->radon_sensor_);
+  LOG_SENSOR("  ", "Radon Long Term", this->radon_long_term_sensor_);
+  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
+  LOG_SENSOR("  ", "Pressure", this->pressure_sensor_);
+  LOG_SENSOR("  ", "CO2", this->co2_sensor_);
+  LOG_SENSOR("  ", "TVOC", this->tvoc_sensor_);
+}
+
+AirthingsWavePlus::AirthingsWavePlus() : PollingComponent(10000) {
+  auto service_bt = *BLEUUID::fromString(std::string("b42e1c08-ade7-11e4-89d3-123b93f75cba")).getNative();
+  auto characteristic_bt = *BLEUUID::fromString(std::string("b42e2a68-ade7-11e4-89d3-123b93f75cba")).getNative();
+
+  service_uuid = espbt::ESPBTUUID::from_uuid(service_bt);
+  sensors_data_characteristic_uuid = espbt::ESPBTUUID::from_uuid(characteristic_bt);
+}
+
+void AirthingsWavePlus::setup() {}
+
+}  // namespace airthings_wave_plus
+}  // namespace esphome
+
+#endif  // ARDUINO_ARCH_ESP32
diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.h b/esphome/components/airthings_wave_plus/airthings_wave_plus.h
new file mode 100644
index 0000000000..18d7fe60d2
--- /dev/null
+++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.h
@@ -0,0 +1,79 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/ble_client/ble_client.h"
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/core/log.h"
+#include <algorithm>
+#include <iterator>
+
+#ifdef ARDUINO_ARCH_ESP32
+#include <esp_gattc_api.h>
+#include <BLEDevice.h>
+
+using namespace esphome::ble_client;
+
+namespace esphome {
+namespace airthings_wave_plus {
+
+static const char *TAG = "airthings_wave_plus";
+
+class AirthingsWavePlus : public PollingComponent, public BLEClientNode {
+ public:
+  AirthingsWavePlus();
+
+  void setup() override;
+  void dump_config() override;
+  void update() override;
+  void loop() override;
+
+  void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
+                           esp_ble_gattc_cb_param_t *param) override;
+
+  void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
+  void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; }
+  void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; }
+  void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
+  void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
+  void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; }
+  void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
+
+ protected:
+  bool is_valid_radon_value_(short radon);
+  bool is_valid_voc_value_(short voc);
+  bool is_valid_co2_value_(short co2);
+
+  void read_sensors_(uint8_t *value, uint16_t value_len);
+  void request_read_values_();
+
+  sensor::Sensor *temperature_sensor_{nullptr};
+  sensor::Sensor *radon_sensor_{nullptr};
+  sensor::Sensor *radon_long_term_sensor_{nullptr};
+  sensor::Sensor *humidity_sensor_{nullptr};
+  sensor::Sensor *pressure_sensor_{nullptr};
+  sensor::Sensor *co2_sensor_{nullptr};
+  sensor::Sensor *tvoc_sensor_{nullptr};
+
+  uint16_t handle;
+  espbt::ESPBTUUID service_uuid;
+  espbt::ESPBTUUID sensors_data_characteristic_uuid;
+
+  struct WavePlusReadings {
+    uint8_t version;
+    uint8_t humidity;
+    uint8_t ambientLight;
+    uint8_t unused01;
+    uint16_t radon;
+    uint16_t radon_lt;
+    uint16_t temperature;
+    uint16_t pressure;
+    uint16_t co2;
+    uint16_t voc;
+  };
+};
+
+}  // namespace airthings_wave_plus
+}  // namespace esphome
+
+#endif  // ARDUINO_ARCH_ESP32
diff --git a/esphome/components/airthings_wave_plus/sensor.py b/esphome/components/airthings_wave_plus/sensor.py
new file mode 100644
index 0000000000..4109fca700
--- /dev/null
+++ b/esphome/components/airthings_wave_plus/sensor.py
@@ -0,0 +1,116 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor, ble_client
+
+from esphome.const import (
+    DEVICE_CLASS_CARBON_DIOXIDE,
+    DEVICE_CLASS_HUMIDITY,
+    DEVICE_CLASS_TEMPERATURE,
+    DEVICE_CLASS_PRESSURE,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_PERCENT,
+    UNIT_CELSIUS,
+    UNIT_HECTOPASCAL,
+    ICON_RADIOACTIVE,
+    CONF_ID,
+    CONF_RADON,
+    CONF_RADON_LONG_TERM,
+    CONF_HUMIDITY,
+    CONF_TVOC,
+    CONF_CO2,
+    CONF_PRESSURE,
+    CONF_TEMPERATURE,
+    UNIT_BECQUEREL_PER_CUBIC_METER,
+    UNIT_PARTS_PER_MILLION,
+    UNIT_PARTS_PER_BILLION,
+    ICON_RADIATOR,
+)
+
+DEPENDENCIES = ["ble_client"]
+
+airthings_wave_plus_ns = cg.esphome_ns.namespace("airthings_wave_plus")
+AirthingsWavePlus = airthings_wave_plus_ns.class_(
+    "AirthingsWavePlus", cg.PollingComponent, ble_client.BLEClientNode
+)
+
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(AirthingsWavePlus),
+            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                device_class=DEVICE_CLASS_HUMIDITY,
+                state_class=STATE_CLASS_MEASUREMENT,
+                accuracy_decimals=0,
+            ),
+            cv.Optional(CONF_RADON): sensor.sensor_schema(
+                unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
+                icon=ICON_RADIOACTIVE,
+                accuracy_decimals=0,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema(
+                unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
+                icon=ICON_RADIOACTIVE,
+                accuracy_decimals=0,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_CELSIUS,
+                accuracy_decimals=2,
+                device_class=DEVICE_CLASS_TEMPERATURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_HECTOPASCAL,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_PRESSURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_CO2): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PARTS_PER_MILLION,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_CARBON_DIOXIDE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_TVOC): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PARTS_PER_BILLION,
+                icon=ICON_RADIATOR,
+                accuracy_decimals=0,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+        }
+    )
+    .extend(cv.polling_component_schema("5mins"))
+    .extend(ble_client.BLE_CLIENT_SCHEMA)
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+
+    await ble_client.register_ble_node(var, config)
+
+    if CONF_HUMIDITY in config:
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
+        cg.add(var.set_humidity(sens))
+    if CONF_RADON in config:
+        sens = await sensor.new_sensor(config[CONF_RADON])
+        cg.add(var.set_radon(sens))
+    if CONF_RADON_LONG_TERM in config:
+        sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM])
+        cg.add(var.set_radon_long_term(sens))
+    if CONF_TEMPERATURE in config:
+        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
+        cg.add(var.set_temperature(sens))
+    if CONF_PRESSURE in config:
+        sens = await sensor.new_sensor(config[CONF_PRESSURE])
+        cg.add(var.set_pressure(sens))
+    if CONF_CO2 in config:
+        sens = await sensor.new_sensor(config[CONF_CO2])
+        cg.add(var.set_co2(sens))
+    if CONF_TVOC in config:
+        sens = await sensor.new_sensor(config[CONF_TVOC])
+        cg.add(var.set_tvoc(sens))
diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py
index 559f8f649c..3705f0d7ca 100644
--- a/esphome/components/api/__init__.py
+++ b/esphome/components/api/__init__.py
@@ -1,3 +1,5 @@
+import base64
+
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome import automation
@@ -6,6 +8,7 @@ from esphome.const import (
     CONF_DATA,
     CONF_DATA_TEMPLATE,
     CONF_ID,
+    CONF_KEY,
     CONF_PASSWORD,
     CONF_PORT,
     CONF_REBOOT_TIMEOUT,
@@ -19,7 +22,7 @@ from esphome.const import (
 from esphome.core import coroutine_with_priority
 
 DEPENDENCIES = ["network"]
-AUTO_LOAD = ["async_tcp"]
+AUTO_LOAD = ["socket"]
 CODEOWNERS = ["@OttoWinter"]
 
 api_ns = cg.esphome_ns.namespace("api")
@@ -41,6 +44,22 @@ SERVICE_ARG_NATIVE_TYPES = {
     "float[]": cg.std_vector.template(float),
     "string[]": cg.std_vector.template(cg.std_string),
 }
+CONF_ENCRYPTION = "encryption"
+
+
+def validate_encryption_key(value):
+    value = cv.string_strict(value)
+    try:
+        decoded = base64.b64decode(value, validate=True)
+    except ValueError as err:
+        raise cv.Invalid("Invalid key format, please check it's using base64") from err
+
+    if len(decoded) != 32:
+        raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
+
+    # Return original data for roundtrip conversion
+    return value
+
 
 CONFIG_SCHEMA = cv.Schema(
     {
@@ -63,6 +82,11 @@ CONFIG_SCHEMA = cv.Schema(
                 ),
             }
         ),
+        cv.Optional(CONF_ENCRYPTION): cv.Schema(
+            {
+                cv.Required(CONF_KEY): validate_encryption_key,
+            }
+        ),
     }
 ).extend(cv.COMPONENT_SCHEMA)
 
@@ -92,6 +116,15 @@ async def to_code(config):
         cg.add(var.register_user_service(trigger))
         await automation.build_automation(trigger, func_args, conf)
 
+    if CONF_ENCRYPTION in config:
+        conf = config[CONF_ENCRYPTION]
+        decoded = base64.b64decode(conf[CONF_KEY])
+        cg.add(var.set_noise_psk(list(decoded)))
+        cg.add_define("USE_API_NOISE")
+        cg.add_library("esphome/noise-c", "0.1.1")
+    else:
+        cg.add_define("USE_API_PLAINTEXT")
+
     cg.add_define("USE_API")
     cg.add_global(api_ns.using)
 
diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto
index e3ef2d7c9e..7648ffeaa2 100644
--- a/esphome/components/api/api.proto
+++ b/esphome/components/api/api.proto
@@ -473,7 +473,8 @@ message ListEntitiesSensorResponse {
   bool force_update = 8;
   string device_class = 9;
   SensorStateClass state_class = 10;
-  SensorLastResetType last_reset_type = 11;
+  // Last reset type removed in 2021.9.0
+  SensorLastResetType legacy_last_reset_type = 11;
   bool disabled_by_default = 12;
 }
 message SensorStateResponse {
diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp
index 4a31f15e77..786fc28d68 100644
--- a/esphome/components/api/api_connection.cpp
+++ b/esphome/components/api/api_connection.cpp
@@ -2,6 +2,7 @@
 #include "esphome/core/log.h"
 #include "esphome/core/util.h"
 #include "esphome/core/version.h"
+#include <cerrno>
 
 #ifdef USE_DEEP_SLEEP
 #include "esphome/components/deep_sleep/deep_sleep_component.h"
@@ -18,145 +19,146 @@ namespace api {
 
 static const char *const TAG = "api.connection";
 
-APIConnection::APIConnection(AsyncClient *client, APIServer *parent)
-    : client_(client), parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) {
-  this->client_->onError([](void *s, AsyncClient *c, int8_t error) { ((APIConnection *) s)->on_error_(error); }, this);
-  this->client_->onDisconnect([](void *s, AsyncClient *c) { ((APIConnection *) s)->on_disconnect_(); }, this);
-  this->client_->onTimeout([](void *s, AsyncClient *c, uint32_t time) { ((APIConnection *) s)->on_timeout_(time); },
-                           this);
-  this->client_->onData([](void *s, AsyncClient *c, void *buf,
-                           size_t len) { ((APIConnection *) s)->on_data_(reinterpret_cast<uint8_t *>(buf), len); },
-                        this);
+APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
+    : parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) {
+  this->proto_write_buffer_.reserve(64);
 
-  this->send_buffer_.reserve(64);
-  this->recv_buffer_.reserve(32);
-  this->client_info_ = this->client_->remoteIP().toString().c_str();
+#if defined(USE_API_PLAINTEXT)
+  helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
+#elif defined(USE_API_NOISE)
+  helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())};
+#else
+#error "No frame helper defined"
+#endif
+}
+void APIConnection::start() {
   this->last_traffic_ = millis();
-}
-APIConnection::~APIConnection() { delete this->client_; }
-void APIConnection::on_error_(int8_t error) { this->remove_ = true; }
-void APIConnection::on_disconnect_() { this->remove_ = true; }
-void APIConnection::on_timeout_(uint32_t time) { this->on_fatal_error(); }
-void APIConnection::on_data_(uint8_t *buf, size_t len) {
-  if (len == 0 || buf == nullptr)
+
+  APIError err = helper_->init();
+  if (err != APIError::OK) {
+    on_fatal_error();
+    ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
     return;
-  this->recv_buffer_.insert(this->recv_buffer_.end(), buf, buf + len);
-}
-void APIConnection::parse_recv_buffer_() {
-  if (this->recv_buffer_.empty() || this->remove_)
-    return;
-
-  while (!this->recv_buffer_.empty()) {
-    if (this->recv_buffer_[0] != 0x00) {
-      ESP_LOGW(TAG, "Invalid preamble from %s", this->client_info_.c_str());
-      this->on_fatal_error();
-      return;
-    }
-    uint32_t i = 1;
-    const uint32_t size = this->recv_buffer_.size();
-    uint32_t consumed;
-    auto msg_size_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed);
-    if (!msg_size_varint.has_value())
-      // not enough data there yet
-      return;
-    i += consumed;
-    uint32_t msg_size = msg_size_varint->as_uint32();
-
-    auto msg_type_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed);
-    if (!msg_type_varint.has_value())
-      // not enough data there yet
-      return;
-    i += consumed;
-    uint32_t msg_type = msg_type_varint->as_uint32();
-
-    if (size - i < msg_size)
-      // message body not fully received
-      return;
-
-    uint8_t *msg = &this->recv_buffer_[i];
-    this->read_message(msg_size, msg_type, msg);
-    if (this->remove_)
-      return;
-    // pop front
-    uint32_t total = i + msg_size;
-    this->recv_buffer_.erase(this->recv_buffer_.begin(), this->recv_buffer_.begin() + total);
-    this->last_traffic_ = millis();
   }
-}
-
-void APIConnection::disconnect_client() {
-  this->client_->close();
-  this->remove_ = true;
+  client_info_ = helper_->getpeername();
+  helper_->set_log_info(client_info_);
 }
 
 void APIConnection::loop() {
   if (this->remove_)
     return;
 
-  if (this->next_close_) {
-    this->disconnect_client();
-    return;
-  }
-
   if (!network_is_connected()) {
     // when network is disconnected force disconnect immediately
     // don't wait for timeout
     this->on_fatal_error();
+    ESP_LOGW(TAG, "%s: Network unavailable, disconnecting", client_info_.c_str());
     return;
   }
-  if (this->client_->disconnected()) {
-    // failsafe for disconnect logic
-    this->on_disconnect_();
+  if (this->next_close_) {
+    // requested a disconnect
+    this->helper_->close();
+    this->remove_ = true;
     return;
   }
-  this->parse_recv_buffer_();
+
+  APIError err = helper_->loop();
+  if (err != APIError::OK) {
+    on_fatal_error();
+    ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
+    return;
+  }
+  ReadPacketBuffer buffer;
+  err = helper_->read_packet(&buffer);
+  if (err == APIError::WOULD_BLOCK) {
+    // pass
+  } else if (err != APIError::OK) {
+    on_fatal_error();
+    if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) {
+      ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str());
+    } else {
+      ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
+    }
+    return;
+  } else {
+    this->last_traffic_ = millis();
+    // read a packet
+    this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
+    if (this->remove_)
+      return;
+  }
 
   this->list_entities_iterator_.advance();
   this->initial_state_iterator_.advance();
 
   const uint32_t keepalive = 60000;
+  const uint32_t now = millis();
   if (this->sent_ping_) {
     // Disconnect if not responded within 2.5*keepalive
-    if (millis() - this->last_traffic_ > (keepalive * 5) / 2) {
-      ESP_LOGW(TAG, "'%s' didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str());
-      this->disconnect_client();
+    if (now - this->last_traffic_ > (keepalive * 5) / 2) {
+      on_fatal_error();
+      ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str());
     }
-  } else if (millis() - this->last_traffic_ > keepalive) {
+  } else if (now - this->last_traffic_ > keepalive) {
     this->sent_ping_ = true;
     this->send_ping_request(PingRequest());
   }
 
 #ifdef USE_ESP32_CAMERA
-  if (this->image_reader_.available()) {
-    uint32_t space = this->client_->space();
-    // reserve 15 bytes for metadata, and at least 64 bytes of data
-    if (space >= 15 + 64) {
-      uint32_t to_send = std::min(space - 15, this->image_reader_.available());
-      auto buffer = this->create_buffer();
-      // fixed32 key = 1;
-      buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash());
-      // bytes data = 2;
-      buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send);
-      // bool done = 3;
-      bool done = this->image_reader_.available() == to_send;
-      buffer.encode_bool(3, done);
-      bool success = this->send_buffer(buffer, 44);
+  if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) {
+    uint32_t to_send = std::min((size_t) 1024, this->image_reader_.available());
+    auto buffer = this->create_buffer();
+    // fixed32 key = 1;
+    buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash());
+    // bytes data = 2;
+    buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send);
+    // bool done = 3;
+    bool done = this->image_reader_.available() == to_send;
+    buffer.encode_bool(3, done);
+    bool success = this->send_buffer(buffer, 44);
 
-      if (success) {
-        this->image_reader_.consume_data(to_send);
-      }
-      if (success && done) {
-        this->image_reader_.return_image();
-      }
+    if (success) {
+      this->image_reader_.consume_data(to_send);
+    }
+    if (success && done) {
+      this->image_reader_.return_image();
     }
   }
 #endif
+
+  if (state_subs_at_ != -1) {
+    const auto &subs = this->parent_->get_state_subs();
+    if (state_subs_at_ >= subs.size()) {
+      state_subs_at_ = -1;
+    } else {
+      auto &it = subs[state_subs_at_];
+      SubscribeHomeAssistantStateResponse resp;
+      resp.entity_id = it.entity_id;
+      resp.attribute = it.attribute.value();
+      if (this->send_subscribe_home_assistant_state_response(resp)) {
+        state_subs_at_++;
+      }
+    }
+  }
 }
 
 std::string get_default_unique_id(const std::string &component_type, Nameable *nameable) {
   return App.get_name() + component_type + nameable->get_object_id();
 }
 
+DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) {
+  // remote initiated disconnect_client
+  // don't close yet, we still need to send the disconnect response
+  // close will happen on next loop
+  ESP_LOGD(TAG, "%s requested disconnected", client_info_.c_str());
+  this->next_close_ = true;
+  DisconnectResponse resp;
+  return resp;
+}
+void APIConnection::on_disconnect_response(const DisconnectResponse &value) {
+  // pass
+}
+
 #ifdef USE_BINARY_SENSOR
 bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state) {
   if (!this->state_subscription_)
@@ -241,6 +243,9 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) {
 #endif
 
 #ifdef USE_FAN
+// Shut-up about usage of deprecated speed_level_to_enum/speed_enum_to_level functions for a bit.
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 bool APIConnection::send_fan_state(fan::FanState *fan) {
   if (!this->state_subscription_)
     return false;
@@ -295,6 +300,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
     call.set_direction(static_cast<fan::FanDirection>(msg.direction));
   call.perform();
 }
+#pragma GCC diagnostic pop
 #endif
 
 #ifdef USE_LIGHT
@@ -417,7 +423,6 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) {
   msg.force_update = sensor->get_force_update();
   msg.device_class = sensor->get_device_class();
   msg.state_class = static_cast<enums::SensorStateClass>(sensor->state_class);
-  msg.last_reset_type = static_cast<enums::SensorLastResetType>(sensor->last_reset_type);
   msg.disabled_by_default = sensor->is_disabled_by_default();
 
   return this->send_list_entities_sensor_response(msg);
@@ -709,8 +714,8 @@ bool APIConnection::send_log_message(int level, const char *tag, const char *lin
 }
 
 HelloResponse APIConnection::hello(const HelloRequest &msg) {
-  this->client_info_ = msg.client_info + " (" + this->client_->remoteIP().toString().c_str();
-  this->client_info_ += ")";
+  this->client_info_ = msg.client_info + " (" + this->helper_->getpeername() + ")";
+  this->helper_->set_log_info(client_info_);
   ESP_LOGV(TAG, "Hello from client: '%s'", this->client_info_.c_str());
 
   HelloResponse resp;
@@ -727,7 +732,7 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
   // bool invalid_password = 1;
   resp.invalid_password = !correct;
   if (correct) {
-    ESP_LOGD(TAG, "Client '%s' connected successfully!", this->client_info_.c_str());
+    ESP_LOGD(TAG, "%s: Connected successfully", this->client_info_.c_str());
     this->connection_state_ = ConnectionState::AUTHENTICATED;
 
 #ifdef USE_HOMEASSISTANT_TIME
@@ -745,9 +750,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
   resp.mac_address = get_mac_address_pretty();
   resp.esphome_version = ESPHOME_VERSION;
   resp.compilation_time = App.get_compilation_time();
-#ifdef ARDUINO_BOARD
-  resp.model = ARDUINO_BOARD;
-#endif
+  resp.model = ESPHOME_BOARD;
 #ifdef USE_DEEP_SLEEP
   resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;
 #endif
@@ -775,57 +778,39 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
   }
 }
 void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) {
-  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;
-    }
-  }
+  state_subs_at_ = 0;
 }
 bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) {
   if (this->remove_)
     return false;
+  if (!this->helper_->can_write_without_blocking())
+    return false;
 
-  std::vector<uint8_t> header;
-  header.push_back(0x00);
-  ProtoVarInt(buffer.get_buffer()->size()).encode(header);
-  ProtoVarInt(message_type).encode(header);
-
-  size_t needed_space = buffer.get_buffer()->size() + header.size();
-
-  if (needed_space > this->client_->space()) {
-    delay(0);
-    if (needed_space > this->client_->space()) {
-      // SubscribeLogsResponse
-      if (message_type != 29) {
-        ESP_LOGV(TAG, "Cannot send message because of TCP buffer space");
-      }
-      delay(0);
-      return false;
+  APIError err = this->helper_->write_packet(message_type, buffer.get_buffer()->data(), buffer.get_buffer()->size());
+  if (err == APIError::WOULD_BLOCK)
+    return false;
+  if (err != APIError::OK) {
+    on_fatal_error();
+    if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) {
+      ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str());
+    } else {
+      ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
     }
+    return false;
   }
-
-  this->client_->add(reinterpret_cast<char *>(header.data()), header.size(),
-                     ASYNC_WRITE_FLAG_COPY | ASYNC_WRITE_FLAG_MORE);
-  this->client_->add(reinterpret_cast<char *>(buffer.get_buffer()->data()), buffer.get_buffer()->size(),
-                     ASYNC_WRITE_FLAG_COPY);
-  bool ret = this->client_->send();
-  return ret;
+  this->last_traffic_ = millis();
+  return true;
 }
 void APIConnection::on_unauthenticated_access() {
-  ESP_LOGD(TAG, "'%s' tried to access without authentication.", this->client_info_.c_str());
   this->on_fatal_error();
+  ESP_LOGD(TAG, "%s: tried to access without authentication.", this->client_info_.c_str());
 }
 void APIConnection::on_no_setup_connection() {
-  ESP_LOGD(TAG, "'%s' tried to access without full connection.", this->client_info_.c_str());
   this->on_fatal_error();
+  ESP_LOGD(TAG, "%s: tried to access without full connection.", this->client_info_.c_str());
 }
 void APIConnection::on_fatal_error() {
-  ESP_LOGV(TAG, "Error: Disconnecting %s", this->client_info_.c_str());
-  this->client_->close();
+  this->helper_->close();
   this->remove_ = true;
 }
 
diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h
index bc9839a423..a1f1769a19 100644
--- a/esphome/components/api/api_connection.h
+++ b/esphome/components/api/api_connection.h
@@ -5,16 +5,17 @@
 #include "api_pb2.h"
 #include "api_pb2_service.h"
 #include "api_server.h"
+#include "api_frame_helper.h"
 
 namespace esphome {
 namespace api {
 
 class APIConnection : public APIServerConnection {
  public:
-  APIConnection(AsyncClient *client, APIServer *parent);
-  virtual ~APIConnection();
+  APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent);
+  virtual ~APIConnection() = default;
 
-  void disconnect_client();
+  void start();
   void loop();
 
   bool send_list_info_done() {
@@ -86,10 +87,7 @@ class APIConnection : public APIServerConnection {
   }
 #endif
 
-  void on_disconnect_response(const DisconnectResponse &value) override {
-    // we initiated disconnect_client
-    this->next_close_ = true;
-  }
+  void on_disconnect_response(const DisconnectResponse &value) override;
   void on_ping_response(const PingResponse &value) override {
     // we initiated ping
     this->sent_ping_ = false;
@@ -100,12 +98,7 @@ class APIConnection : public APIServerConnection {
 #endif
   HelloResponse hello(const HelloRequest &msg) override;
   ConnectResponse connect(const ConnectRequest &msg) override;
-  DisconnectResponse disconnect(const DisconnectRequest &msg) override {
-    // remote initiated disconnect_client
-    this->next_close_ = true;
-    DisconnectResponse resp;
-    return resp;
-  }
+  DisconnectResponse disconnect(const DisconnectRequest &msg) override;
   PingResponse ping(const PingRequest &msg) override { return {}; }
   DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override;
   void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); }
@@ -135,19 +128,16 @@ class APIConnection : public APIServerConnection {
   void on_unauthenticated_access() override;
   void on_no_setup_connection() override;
   ProtoWriteBuffer create_buffer() override {
-    this->send_buffer_.clear();
-    return {&this->send_buffer_};
+    // FIXME: ensure no recursive writes can happen
+    this->proto_write_buffer_.clear();
+    return {&this->proto_write_buffer_};
   }
   bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override;
 
  protected:
   friend APIServer;
 
-  void on_error_(int8_t error);
-  void on_disconnect_();
-  void on_timeout_(uint32_t time);
-  void on_data_(uint8_t *buf, size_t len);
-  void parse_recv_buffer_();
+  bool send_(const void *buf, size_t len, bool force);
 
   enum class ConnectionState {
     WAITING_FOR_HELLO,
@@ -157,8 +147,10 @@ class APIConnection : public APIServerConnection {
 
   bool remove_{false};
 
-  std::vector<uint8_t> send_buffer_;
-  std::vector<uint8_t> recv_buffer_;
+  // Buffer used to encode proto messages
+  // Re-use to prevent allocations
+  std::vector<uint8_t> proto_write_buffer_;
+  std::unique_ptr<APIFrameHelper> helper_;
 
   std::string client_info_;
 #ifdef USE_ESP32_CAMERA
@@ -170,12 +162,11 @@ class APIConnection : public APIServerConnection {
   uint32_t last_traffic_;
   bool sent_ping_{false};
   bool service_call_subscription_{false};
-  bool current_nodelay_{false};
-  bool next_close_{false};
-  AsyncClient *client_;
+  bool next_close_ = false;
   APIServer *parent_;
   InitialStateIterator initial_state_iterator_;
   ListEntitiesIterator list_entities_iterator_;
+  int state_subs_at_ = -1;
 };
 
 }  // namespace api
diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp
new file mode 100644
index 0000000000..e68831e594
--- /dev/null
+++ b/esphome/components/api/api_frame_helper.cpp
@@ -0,0 +1,961 @@
+#include "api_frame_helper.h"
+
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+#include "proto.h"
+
+namespace esphome {
+namespace api {
+
+static const char *const TAG = "api.socket";
+
+/// Is the given return value (from read/write syscalls) a wouldblock error?
+bool is_would_block(ssize_t ret) {
+  if (ret == -1) {
+    return errno == EWOULDBLOCK || errno == EAGAIN;
+  }
+  return ret == 0;
+}
+
+const char *api_error_to_str(APIError err) {
+  // not using switch to ensure compiler doesn't try to build a big table out of it
+  if (err == APIError::OK) {
+    return "OK";
+  } else if (err == APIError::WOULD_BLOCK) {
+    return "WOULD_BLOCK";
+  } else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) {
+    return "BAD_HANDSHAKE_PACKET_LEN";
+  } else if (err == APIError::BAD_INDICATOR) {
+    return "BAD_INDICATOR";
+  } else if (err == APIError::BAD_DATA_PACKET) {
+    return "BAD_DATA_PACKET";
+  } else if (err == APIError::TCP_NODELAY_FAILED) {
+    return "TCP_NODELAY_FAILED";
+  } else if (err == APIError::TCP_NONBLOCKING_FAILED) {
+    return "TCP_NONBLOCKING_FAILED";
+  } else if (err == APIError::CLOSE_FAILED) {
+    return "CLOSE_FAILED";
+  } else if (err == APIError::SHUTDOWN_FAILED) {
+    return "SHUTDOWN_FAILED";
+  } else if (err == APIError::BAD_STATE) {
+    return "BAD_STATE";
+  } else if (err == APIError::BAD_ARG) {
+    return "BAD_ARG";
+  } else if (err == APIError::SOCKET_READ_FAILED) {
+    return "SOCKET_READ_FAILED";
+  } else if (err == APIError::SOCKET_WRITE_FAILED) {
+    return "SOCKET_WRITE_FAILED";
+  } else if (err == APIError::HANDSHAKESTATE_READ_FAILED) {
+    return "HANDSHAKESTATE_READ_FAILED";
+  } else if (err == APIError::HANDSHAKESTATE_WRITE_FAILED) {
+    return "HANDSHAKESTATE_WRITE_FAILED";
+  } else if (err == APIError::HANDSHAKESTATE_BAD_STATE) {
+    return "HANDSHAKESTATE_BAD_STATE";
+  } else if (err == APIError::CIPHERSTATE_DECRYPT_FAILED) {
+    return "CIPHERSTATE_DECRYPT_FAILED";
+  } else if (err == APIError::CIPHERSTATE_ENCRYPT_FAILED) {
+    return "CIPHERSTATE_ENCRYPT_FAILED";
+  } else if (err == APIError::OUT_OF_MEMORY) {
+    return "OUT_OF_MEMORY";
+  } else if (err == APIError::HANDSHAKESTATE_SETUP_FAILED) {
+    return "HANDSHAKESTATE_SETUP_FAILED";
+  } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) {
+    return "HANDSHAKESTATE_SPLIT_FAILED";
+  } else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) {
+    return "BAD_HANDSHAKE_ERROR_BYTE";
+  }
+  return "UNKNOWN";
+}
+
+#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__)
+// uncomment to log raw packets
+//#define HELPER_LOG_PACKETS
+
+#ifdef USE_API_NOISE
+static const char *const PROLOGUE_INIT = "NoiseAPIInit";
+
+/// Convert a noise error code to a readable error
+std::string noise_err_to_str(int err) {
+  if (err == NOISE_ERROR_NO_MEMORY)
+    return "NO_MEMORY";
+  if (err == NOISE_ERROR_UNKNOWN_ID)
+    return "UNKNOWN_ID";
+  if (err == NOISE_ERROR_UNKNOWN_NAME)
+    return "UNKNOWN_NAME";
+  if (err == NOISE_ERROR_MAC_FAILURE)
+    return "MAC_FAILURE";
+  if (err == NOISE_ERROR_NOT_APPLICABLE)
+    return "NOT_APPLICABLE";
+  if (err == NOISE_ERROR_SYSTEM)
+    return "SYSTEM";
+  if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
+    return "REMOTE_KEY_REQUIRED";
+  if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
+    return "LOCAL_KEY_REQUIRED";
+  if (err == NOISE_ERROR_PSK_REQUIRED)
+    return "PSK_REQUIRED";
+  if (err == NOISE_ERROR_INVALID_LENGTH)
+    return "INVALID_LENGTH";
+  if (err == NOISE_ERROR_INVALID_PARAM)
+    return "INVALID_PARAM";
+  if (err == NOISE_ERROR_INVALID_STATE)
+    return "INVALID_STATE";
+  if (err == NOISE_ERROR_INVALID_NONCE)
+    return "INVALID_NONCE";
+  if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
+    return "INVALID_PRIVATE_KEY";
+  if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
+    return "INVALID_PUBLIC_KEY";
+  if (err == NOISE_ERROR_INVALID_FORMAT)
+    return "INVALID_FORMAT";
+  if (err == NOISE_ERROR_INVALID_SIGNATURE)
+    return "INVALID_SIGNATURE";
+  return to_string(err);
+}
+
+/// Initialize the frame helper, returns OK if successful.
+APIError APINoiseFrameHelper::init() {
+  if (state_ != State::INITIALIZE || socket_ == nullptr) {
+    HELPER_LOG("Bad state for init %d", (int) state_);
+    return APIError::BAD_STATE;
+  }
+  int err = socket_->setblocking(false);
+  if (err != 0) {
+    state_ = State::FAILED;
+    HELPER_LOG("Setting nonblocking failed with errno %d", errno);
+    return APIError::TCP_NONBLOCKING_FAILED;
+  }
+  int enable = 1;
+  err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
+  if (err != 0) {
+    state_ = State::FAILED;
+    HELPER_LOG("Setting nodelay failed with errno %d", errno);
+    return APIError::TCP_NODELAY_FAILED;
+  }
+
+  // init prologue
+  prologue_.insert(prologue_.end(), PROLOGUE_INIT, PROLOGUE_INIT + strlen(PROLOGUE_INIT));
+
+  state_ = State::CLIENT_HELLO;
+  return APIError::OK;
+}
+/// Run through handshake messages (if in that phase)
+APIError APINoiseFrameHelper::loop() {
+  APIError err = state_action_();
+  if (err == APIError::WOULD_BLOCK)
+    return APIError::OK;
+  if (err != APIError::OK)
+    return err;
+  if (!tx_buf_.empty()) {
+    err = try_send_tx_buf_();
+    if (err != APIError::OK) {
+      return err;
+    }
+  }
+  return APIError::OK;
+}
+
+/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
+ *
+ * @param frame: The struct to hold the frame information in.
+ *   msg_start: points to the start of the payload - this pointer is only valid until the next
+ *     try_receive_raw_ call
+ *
+ * @return 0 if a full packet is in rx_buf_
+ * @return -1 if error, check errno.
+ *
+ * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
+ * errno ENOMEM: Not enough memory for reading packet.
+ * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
+ * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
+ */
+APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
+  int err;
+  APIError aerr;
+
+  if (frame == nullptr) {
+    HELPER_LOG("Bad argument for try_read_frame_");
+    return APIError::BAD_ARG;
+  }
+
+  // read header
+  if (rx_header_buf_len_ < 3) {
+    // no header information yet
+    size_t to_read = 3 - rx_header_buf_len_;
+    ssize_t received = socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
+    if (is_would_block(received)) {
+      return APIError::WOULD_BLOCK;
+    } else if (received == -1) {
+      state_ = State::FAILED;
+      HELPER_LOG("Socket read failed with errno %d", errno);
+      return APIError::SOCKET_READ_FAILED;
+    }
+    rx_header_buf_len_ += received;
+    if (received != to_read) {
+      // not a full read
+      return APIError::WOULD_BLOCK;
+    }
+
+    // header reading done
+  }
+
+  // read body
+  uint8_t indicator = rx_header_buf_[0];
+  if (indicator != 0x01) {
+    state_ = State::FAILED;
+    HELPER_LOG("Bad indicator byte %u", indicator);
+    return APIError::BAD_INDICATOR;
+  }
+
+  uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
+
+  if (state_ != State::DATA && msg_size > 128) {
+    // for handshake message only permit up to 128 bytes
+    state_ = State::FAILED;
+    HELPER_LOG("Bad packet len for handshake: %d", msg_size);
+    return APIError::BAD_HANDSHAKE_PACKET_LEN;
+  }
+
+  // reserve space for body
+  if (rx_buf_.size() != msg_size) {
+    rx_buf_.resize(msg_size);
+  }
+
+  if (rx_buf_len_ < msg_size) {
+    // more data to read
+    size_t to_read = msg_size - rx_buf_len_;
+    ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read);
+    if (is_would_block(received)) {
+      return APIError::WOULD_BLOCK;
+    } else if (received == -1) {
+      state_ = State::FAILED;
+      HELPER_LOG("Socket read failed with errno %d", errno);
+      return APIError::SOCKET_READ_FAILED;
+    }
+    rx_buf_len_ += received;
+    if (received != to_read) {
+      // not all read
+      return APIError::WOULD_BLOCK;
+    }
+  }
+
+  // uncomment for even more debugging
+#ifdef HELPER_LOG_PACKETS
+  ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str());
+#endif
+  frame->msg = std::move(rx_buf_);
+  // consume msg
+  rx_buf_ = {};
+  rx_buf_len_ = 0;
+  rx_header_buf_len_ = 0;
+  return APIError::OK;
+}
+
+/** To be called from read/write methods.
+ *
+ * This method runs through the internal handshake methods, if in that state.
+ *
+ * If the handshake is still active when this method returns and a read/write can't take place at
+ * the moment, returns WOULD_BLOCK.
+ * If an error occured, returns that error. Only returns OK if the transport is ready for data
+ * traffic.
+ */
+APIError APINoiseFrameHelper::state_action_() {
+  int err;
+  APIError aerr;
+  if (state_ == State::INITIALIZE) {
+    HELPER_LOG("Bad state for method: %d", (int) state_);
+    return APIError::BAD_STATE;
+  }
+  if (state_ == State::CLIENT_HELLO) {
+    // waiting for client hello
+    ParsedFrame frame;
+    aerr = try_read_frame_(&frame);
+    if (aerr == APIError::BAD_INDICATOR) {
+      send_explicit_handshake_reject_("Bad indicator byte");
+      return aerr;
+    }
+    if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
+      send_explicit_handshake_reject_("Bad handshake packet len");
+      return aerr;
+    }
+    if (aerr != APIError::OK)
+      return aerr;
+    // ignore contents, may be used in future for flags
+    prologue_.push_back((uint8_t)(frame.msg.size() >> 8));
+    prologue_.push_back((uint8_t) frame.msg.size());
+    prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end());
+
+    state_ = State::SERVER_HELLO;
+  }
+  if (state_ == State::SERVER_HELLO) {
+    // send server hello
+    uint8_t msg[1];
+    msg[0] = 0x01;  // chosen proto
+    aerr = write_frame_(msg, 1);
+    if (aerr != APIError::OK)
+      return aerr;
+
+    // start handshake
+    aerr = init_handshake_();
+    if (aerr != APIError::OK)
+      return aerr;
+
+    state_ = State::HANDSHAKE;
+  }
+  if (state_ == State::HANDSHAKE) {
+    int action = noise_handshakestate_get_action(handshake_);
+    if (action == NOISE_ACTION_READ_MESSAGE) {
+      // waiting for handshake msg
+      ParsedFrame frame;
+      aerr = try_read_frame_(&frame);
+      if (aerr == APIError::BAD_INDICATOR) {
+        send_explicit_handshake_reject_("Bad indicator byte");
+        return aerr;
+      }
+      if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
+        send_explicit_handshake_reject_("Bad handshake packet len");
+        return aerr;
+      }
+      if (aerr != APIError::OK)
+        return aerr;
+
+      if (frame.msg.empty()) {
+        send_explicit_handshake_reject_("Empty handshake message");
+        return APIError::BAD_HANDSHAKE_ERROR_BYTE;
+      } else if (frame.msg[0] != 0x00) {
+        HELPER_LOG("Bad handshake error byte: %u", frame.msg[0]);
+        send_explicit_handshake_reject_("Bad handshake error byte");
+        return APIError::BAD_HANDSHAKE_ERROR_BYTE;
+      }
+
+      NoiseBuffer mbuf;
+      noise_buffer_init(mbuf);
+      noise_buffer_set_input(mbuf, frame.msg.data() + 1, frame.msg.size() - 1);
+      err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
+      if (err != 0) {
+        state_ = State::FAILED;
+        HELPER_LOG("noise_handshakestate_read_message failed: %s", noise_err_to_str(err).c_str());
+        if (err == NOISE_ERROR_MAC_FAILURE) {
+          send_explicit_handshake_reject_("Handshake MAC failure");
+        } else {
+          send_explicit_handshake_reject_("Handshake error");
+        }
+        return APIError::HANDSHAKESTATE_READ_FAILED;
+      }
+
+      aerr = check_handshake_finished_();
+      if (aerr != APIError::OK)
+        return aerr;
+    } else if (action == NOISE_ACTION_WRITE_MESSAGE) {
+      uint8_t buffer[65];
+      NoiseBuffer mbuf;
+      noise_buffer_init(mbuf);
+      noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
+
+      err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr);
+      if (err != 0) {
+        state_ = State::FAILED;
+        HELPER_LOG("noise_handshakestate_write_message failed: %s", noise_err_to_str(err).c_str());
+        return APIError::HANDSHAKESTATE_WRITE_FAILED;
+      }
+      buffer[0] = 0x00;  // success
+
+      aerr = write_frame_(buffer, mbuf.size + 1);
+      if (aerr != APIError::OK)
+        return aerr;
+      aerr = check_handshake_finished_();
+      if (aerr != APIError::OK)
+        return aerr;
+    } else {
+      // bad state for action
+      state_ = State::FAILED;
+      HELPER_LOG("Bad action for handshake: %d", action);
+      return APIError::HANDSHAKESTATE_BAD_STATE;
+    }
+  }
+  if (state_ == State::CLOSED || state_ == State::FAILED) {
+    return APIError::BAD_STATE;
+  }
+  return APIError::OK;
+}
+void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) {
+  std::vector<uint8_t> data;
+  data.resize(reason.length() + 1);
+  data[0] = 0x01;  // failure
+  for (size_t i = 0; i < reason.length(); i++) {
+    data[i + 1] = (uint8_t) reason[i];
+  }
+  // temporarily remove failed state
+  auto orig_state = state_;
+  state_ = State::EXPLICIT_REJECT;
+  write_frame_(data.data(), data.size());
+  state_ = orig_state;
+}
+
+APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
+  int err;
+  APIError aerr;
+  aerr = state_action_();
+  if (aerr != APIError::OK) {
+    return aerr;
+  }
+
+  if (state_ != State::DATA) {
+    return APIError::WOULD_BLOCK;
+  }
+
+  ParsedFrame frame;
+  aerr = try_read_frame_(&frame);
+  if (aerr != APIError::OK)
+    return aerr;
+
+  NoiseBuffer mbuf;
+  noise_buffer_init(mbuf);
+  noise_buffer_set_inout(mbuf, frame.msg.data(), frame.msg.size(), frame.msg.size());
+  err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
+  if (err != 0) {
+    state_ = State::FAILED;
+    HELPER_LOG("noise_cipherstate_decrypt failed: %s", noise_err_to_str(err).c_str());
+    return APIError::CIPHERSTATE_DECRYPT_FAILED;
+  }
+
+  size_t msg_size = mbuf.size;
+  uint8_t *msg_data = frame.msg.data();
+  if (msg_size < 4) {
+    state_ = State::FAILED;
+    HELPER_LOG("Bad data packet: size %d too short", msg_size);
+    return APIError::BAD_DATA_PACKET;
+  }
+
+  // uint16_t type;
+  // uint16_t data_len;
+  // uint8_t *data;
+  // uint8_t *padding;  zero or more bytes to fill up the rest of the packet
+  uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
+  uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
+  if (data_len > msg_size - 4) {
+    state_ = State::FAILED;
+    HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
+    return APIError::BAD_DATA_PACKET;
+  }
+
+  buffer->container = std::move(frame.msg);
+  buffer->data_offset = 4;
+  buffer->data_len = data_len;
+  buffer->type = type;
+  return APIError::OK;
+}
+bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
+APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
+  int err;
+  APIError aerr;
+  aerr = state_action_();
+  if (aerr != APIError::OK) {
+    return aerr;
+  }
+
+  if (state_ != State::DATA) {
+    return APIError::WOULD_BLOCK;
+  }
+
+  size_t padding = 0;
+  size_t msg_len = 4 + payload_len + padding;
+  size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_);
+  auto tmpbuf = std::unique_ptr<uint8_t[]>{new (std::nothrow) uint8_t[frame_len]};
+  if (tmpbuf == nullptr) {
+    HELPER_LOG("Could not allocate for writing packet");
+    return APIError::OUT_OF_MEMORY;
+  }
+
+  tmpbuf[0] = 0x01;  // indicator
+  // tmpbuf[1], tmpbuf[2] to be set later
+  const uint8_t msg_offset = 3;
+  const uint8_t payload_offset = msg_offset + 4;
+  tmpbuf[msg_offset + 0] = (uint8_t)(type >> 8);  // type
+  tmpbuf[msg_offset + 1] = (uint8_t) type;
+  tmpbuf[msg_offset + 2] = (uint8_t)(payload_len >> 8);  // data_len
+  tmpbuf[msg_offset + 3] = (uint8_t) payload_len;
+  // copy data
+  std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]);
+  // fill padding with zeros
+  std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0);
+
+  NoiseBuffer mbuf;
+  noise_buffer_init(mbuf);
+  noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset);
+  err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
+  if (err != 0) {
+    state_ = State::FAILED;
+    HELPER_LOG("noise_cipherstate_encrypt failed: %s", noise_err_to_str(err).c_str());
+    return APIError::CIPHERSTATE_ENCRYPT_FAILED;
+  }
+
+  size_t total_len = 3 + mbuf.size;
+  tmpbuf[1] = (uint8_t)(mbuf.size >> 8);
+  tmpbuf[2] = (uint8_t) mbuf.size;
+  // write raw to not have two packets sent if NAGLE disabled
+  aerr = write_raw_(&tmpbuf[0], total_len);
+  if (aerr != APIError::OK) {
+    return aerr;
+  }
+  return APIError::OK;
+}
+APIError APINoiseFrameHelper::try_send_tx_buf_() {
+  // try send from tx_buf
+  while (state_ != State::CLOSED && !tx_buf_.empty()) {
+    ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
+    if (sent == -1) {
+      if (errno == EWOULDBLOCK || errno == EAGAIN)
+        break;
+      state_ = State::FAILED;
+      HELPER_LOG("Socket write failed with errno %d", errno);
+      return APIError::SOCKET_WRITE_FAILED;
+    } else if (sent == 0) {
+      break;
+    }
+    // TODO: inefficient if multiple packets in txbuf
+    // replace with deque of buffers
+    tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
+  }
+
+  return APIError::OK;
+}
+/** Write the data to the socket, or buffer it a write would block
+ *
+ * @param data The data to write
+ * @param len The length of data
+ */
+APIError APINoiseFrameHelper::write_raw_(const uint8_t *data, size_t len) {
+  if (len == 0)
+    return APIError::OK;
+  int err;
+  APIError aerr;
+
+  // uncomment for even more debugging
+#ifdef HELPER_LOG_PACKETS
+  ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str());
+#endif
+
+  if (!tx_buf_.empty()) {
+    // try to empty tx_buf_ first
+    aerr = try_send_tx_buf_();
+    if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
+      return aerr;
+  }
+
+  if (!tx_buf_.empty()) {
+    // tx buf not empty, can't write now because then stream would be inconsistent
+    tx_buf_.insert(tx_buf_.end(), data, data + len);
+    return APIError::OK;
+  }
+
+  ssize_t sent = socket_->write(data, len);
+  if (is_would_block(sent)) {
+    // operation would block, add buffer to tx_buf
+    tx_buf_.insert(tx_buf_.end(), data, data + len);
+    return APIError::OK;
+  } else if (sent == -1) {
+    // an error occured
+    state_ = State::FAILED;
+    HELPER_LOG("Socket write failed with errno %d", errno);
+    return APIError::SOCKET_WRITE_FAILED;
+  } else if (sent != len) {
+    // partially sent, add end to tx_buf
+    tx_buf_.insert(tx_buf_.end(), data + sent, data + len);
+    return APIError::OK;
+  }
+  // fully sent
+  return APIError::OK;
+}
+APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
+  APIError aerr;
+
+  uint8_t header[3];
+  header[0] = 0x01;  // indicator
+  header[1] = (uint8_t)(len >> 8);
+  header[2] = (uint8_t) len;
+
+  aerr = write_raw_(header, 3);
+  if (aerr != APIError::OK)
+    return aerr;
+  aerr = write_raw_(data, len);
+  return aerr;
+}
+
+/** Initiate the data structures for the handshake.
+ *
+ * @return 0 on success, -1 on error (check errno)
+ */
+APIError APINoiseFrameHelper::init_handshake_() {
+  int err;
+  memset(&nid_, 0, sizeof(nid_));
+  // const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
+  // err = noise_protocol_name_to_id(&nid_, proto, strlen(proto));
+  nid_.pattern_id = NOISE_PATTERN_NN;
+  nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY;
+  nid_.dh_id = NOISE_DH_CURVE25519;
+  nid_.prefix_id = NOISE_PREFIX_STANDARD;
+  nid_.hybrid_id = NOISE_DH_NONE;
+  nid_.hash_id = NOISE_HASH_SHA256;
+  nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0;
+
+  err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER);
+  if (err != 0) {
+    state_ = State::FAILED;
+    HELPER_LOG("noise_handshakestate_new_by_id failed: %s", noise_err_to_str(err).c_str());
+    return APIError::HANDSHAKESTATE_SETUP_FAILED;
+  }
+
+  const auto &psk = ctx_->get_psk();
+  err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
+  if (err != 0) {
+    state_ = State::FAILED;
+    HELPER_LOG("noise_handshakestate_set_pre_shared_key failed: %s", noise_err_to_str(err).c_str());
+    return APIError::HANDSHAKESTATE_SETUP_FAILED;
+  }
+
+  err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size());
+  if (err != 0) {
+    state_ = State::FAILED;
+    HELPER_LOG("noise_handshakestate_set_prologue failed: %s", noise_err_to_str(err).c_str());
+    return APIError::HANDSHAKESTATE_SETUP_FAILED;
+  }
+  // set_prologue copies it into handshakestate, so we can get rid of it now
+  prologue_ = {};
+
+  err = noise_handshakestate_start(handshake_);
+  if (err != 0) {
+    state_ = State::FAILED;
+    HELPER_LOG("noise_handshakestate_start failed: %s", noise_err_to_str(err).c_str());
+    return APIError::HANDSHAKESTATE_SETUP_FAILED;
+  }
+  return APIError::OK;
+}
+
+APIError APINoiseFrameHelper::check_handshake_finished_() {
+  assert(state_ == State::HANDSHAKE);
+
+  int action = noise_handshakestate_get_action(handshake_);
+  if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)
+    return APIError::OK;
+  if (action != NOISE_ACTION_SPLIT) {
+    state_ = State::FAILED;
+    HELPER_LOG("Bad action for handshake: %d", action);
+    return APIError::HANDSHAKESTATE_BAD_STATE;
+  }
+  int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_);
+  if (err != 0) {
+    state_ = State::FAILED;
+    HELPER_LOG("noise_handshakestate_split failed: %s", noise_err_to_str(err).c_str());
+    return APIError::HANDSHAKESTATE_SPLIT_FAILED;
+  }
+
+  HELPER_LOG("Handshake complete!");
+  noise_handshakestate_free(handshake_);
+  handshake_ = nullptr;
+  state_ = State::DATA;
+  return APIError::OK;
+}
+
+APINoiseFrameHelper::~APINoiseFrameHelper() {
+  if (handshake_ != nullptr) {
+    noise_handshakestate_free(handshake_);
+    handshake_ = nullptr;
+  }
+  if (send_cipher_ != nullptr) {
+    noise_cipherstate_free(send_cipher_);
+    send_cipher_ = nullptr;
+  }
+  if (recv_cipher_ != nullptr) {
+    noise_cipherstate_free(recv_cipher_);
+    recv_cipher_ = nullptr;
+  }
+}
+
+APIError APINoiseFrameHelper::close() {
+  state_ = State::CLOSED;
+  int err = socket_->close();
+  if (err == -1)
+    return APIError::CLOSE_FAILED;
+  return APIError::OK;
+}
+APIError APINoiseFrameHelper::shutdown(int how) {
+  int err = socket_->shutdown(how);
+  if (err == -1)
+    return APIError::SHUTDOWN_FAILED;
+  if (how == SHUT_RDWR) {
+    state_ = State::CLOSED;
+  }
+  return APIError::OK;
+}
+extern "C" {
+// declare how noise generates random bytes (here with a good HWRNG based on the RF system)
+void noise_rand_bytes(void *output, size_t len) { esphome::fill_random(reinterpret_cast<uint8_t *>(output), len); }
+}
+#endif  // USE_API_NOISE
+
+#ifdef USE_API_PLAINTEXT
+
+/// Initialize the frame helper, returns OK if successful.
+APIError APIPlaintextFrameHelper::init() {
+  if (state_ != State::INITIALIZE || socket_ == nullptr) {
+    HELPER_LOG("Bad state for init %d", (int) state_);
+    return APIError::BAD_STATE;
+  }
+  int err = socket_->setblocking(false);
+  if (err != 0) {
+    state_ = State::FAILED;
+    HELPER_LOG("Setting nonblocking failed with errno %d", errno);
+    return APIError::TCP_NONBLOCKING_FAILED;
+  }
+  int enable = 1;
+  err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
+  if (err != 0) {
+    state_ = State::FAILED;
+    HELPER_LOG("Setting nodelay failed with errno %d", errno);
+    return APIError::TCP_NODELAY_FAILED;
+  }
+
+  state_ = State::DATA;
+  return APIError::OK;
+}
+/// Not used for plaintext
+APIError APIPlaintextFrameHelper::loop() {
+  if (state_ != State::DATA) {
+    return APIError::BAD_STATE;
+  }
+  // try send pending TX data
+  if (!tx_buf_.empty()) {
+    APIError err = try_send_tx_buf_();
+    if (err != APIError::OK) {
+      return err;
+    }
+  }
+  return APIError::OK;
+}
+
+/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
+ *
+ * @param frame: The struct to hold the frame information in.
+ *   msg: store the parsed frame in that struct
+ *
+ * @return See APIError
+ *
+ * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
+ */
+APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
+  int err;
+  APIError aerr;
+
+  if (frame == nullptr) {
+    HELPER_LOG("Bad argument for try_read_frame_");
+    return APIError::BAD_ARG;
+  }
+
+  // read header
+  while (!rx_header_parsed_) {
+    uint8_t data;
+    ssize_t received = socket_->read(&data, 1);
+    if (is_would_block(received)) {
+      return APIError::WOULD_BLOCK;
+    } else if (received == -1) {
+      state_ = State::FAILED;
+      HELPER_LOG("Socket read failed with errno %d", errno);
+      return APIError::SOCKET_READ_FAILED;
+    }
+    rx_header_buf_.push_back(data);
+
+    // try parse header
+    if (rx_header_buf_[0] != 0x00) {
+      state_ = State::FAILED;
+      HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
+      return APIError::BAD_INDICATOR;
+    }
+
+    size_t i = 1;
+    uint32_t consumed = 0;
+    auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
+    if (!msg_size_varint.has_value()) {
+      // not enough data there yet
+      continue;
+    }
+
+    i += consumed;
+    rx_header_parsed_len_ = msg_size_varint->as_uint32();
+
+    auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
+    if (!msg_type_varint.has_value()) {
+      // not enough data there yet
+      continue;
+    }
+    rx_header_parsed_type_ = msg_type_varint->as_uint32();
+    rx_header_parsed_ = true;
+  }
+  // header reading done
+
+  // reserve space for body
+  if (rx_buf_.size() != rx_header_parsed_len_) {
+    rx_buf_.resize(rx_header_parsed_len_);
+  }
+
+  if (rx_buf_len_ < rx_header_parsed_len_) {
+    // more data to read
+    size_t to_read = rx_header_parsed_len_ - rx_buf_len_;
+    ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read);
+    if (is_would_block(received)) {
+      return APIError::WOULD_BLOCK;
+    } else if (received == -1) {
+      state_ = State::FAILED;
+      HELPER_LOG("Socket read failed with errno %d", errno);
+      return APIError::SOCKET_READ_FAILED;
+    }
+    rx_buf_len_ += received;
+    if (received != to_read) {
+      // not all read
+      return APIError::WOULD_BLOCK;
+    }
+  }
+
+  // uncomment for even more debugging
+#ifdef HELPER_LOG_PACKETS
+  ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str());
+#endif
+  frame->msg = std::move(rx_buf_);
+  // consume msg
+  rx_buf_ = {};
+  rx_buf_len_ = 0;
+  rx_header_buf_.clear();
+  rx_header_parsed_ = false;
+  return APIError::OK;
+}
+
+APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
+  int err;
+  APIError aerr;
+
+  if (state_ != State::DATA) {
+    return APIError::WOULD_BLOCK;
+  }
+
+  ParsedFrame frame;
+  aerr = try_read_frame_(&frame);
+  if (aerr != APIError::OK)
+    return aerr;
+
+  buffer->container = std::move(frame.msg);
+  buffer->data_offset = 0;
+  buffer->data_len = rx_header_parsed_len_;
+  buffer->type = rx_header_parsed_type_;
+  return APIError::OK;
+}
+bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
+APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
+  int err;
+  APIError aerr;
+
+  if (state_ != State::DATA) {
+    return APIError::BAD_STATE;
+  }
+
+  std::vector<uint8_t> header;
+  header.push_back(0x00);
+  ProtoVarInt(payload_len).encode(header);
+  ProtoVarInt(type).encode(header);
+
+  aerr = write_raw_(&header[0], header.size());
+  if (aerr != APIError::OK) {
+    return aerr;
+  }
+  aerr = write_raw_(payload, payload_len);
+  if (aerr != APIError::OK) {
+    return aerr;
+  }
+  return APIError::OK;
+}
+APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
+  // try send from tx_buf
+  while (state_ != State::CLOSED && !tx_buf_.empty()) {
+    ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
+    if (is_would_block(sent)) {
+      break;
+    } else if (sent == -1) {
+      state_ = State::FAILED;
+      HELPER_LOG("Socket write failed with errno %d", errno);
+      return APIError::SOCKET_WRITE_FAILED;
+    }
+    // TODO: inefficient if multiple packets in txbuf
+    // replace with deque of buffers
+    tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
+  }
+
+  return APIError::OK;
+}
+/** Write the data to the socket, or buffer it a write would block
+ *
+ * @param data The data to write
+ * @param len The length of data
+ */
+APIError APIPlaintextFrameHelper::write_raw_(const uint8_t *data, size_t len) {
+  if (len == 0)
+    return APIError::OK;
+  int err;
+  APIError aerr;
+
+  // uncomment for even more debugging
+#ifdef HELPER_LOG_PACKETS
+  ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str());
+#endif
+
+  if (!tx_buf_.empty()) {
+    // try to empty tx_buf_ first
+    aerr = try_send_tx_buf_();
+    if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
+      return aerr;
+  }
+
+  if (!tx_buf_.empty()) {
+    // tx buf not empty, can't write now because then stream would be inconsistent
+    tx_buf_.insert(tx_buf_.end(), data, data + len);
+    return APIError::OK;
+  }
+
+  ssize_t sent = socket_->write(data, len);
+  if (is_would_block(sent)) {
+    // operation would block, add buffer to tx_buf
+    tx_buf_.insert(tx_buf_.end(), data, data + len);
+    return APIError::OK;
+  } else if (sent == -1) {
+    // an error occured
+    state_ = State::FAILED;
+    HELPER_LOG("Socket write failed with errno %d", errno);
+    return APIError::SOCKET_WRITE_FAILED;
+  } else if (sent != len) {
+    // partially sent, add end to tx_buf
+    tx_buf_.insert(tx_buf_.end(), data + sent, data + len);
+    return APIError::OK;
+  }
+  // fully sent
+  return APIError::OK;
+}
+
+APIError APIPlaintextFrameHelper::close() {
+  state_ = State::CLOSED;
+  int err = socket_->close();
+  if (err == -1)
+    return APIError::CLOSE_FAILED;
+  return APIError::OK;
+}
+APIError APIPlaintextFrameHelper::shutdown(int how) {
+  int err = socket_->shutdown(how);
+  if (err == -1)
+    return APIError::SHUTDOWN_FAILED;
+  if (how == SHUT_RDWR) {
+    state_ = State::CLOSED;
+  }
+  return APIError::OK;
+}
+#endif  // USE_API_PLAINTEXT
+
+}  // namespace api
+}  // namespace esphome
diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h
new file mode 100644
index 0000000000..a9a653cf4f
--- /dev/null
+++ b/esphome/components/api/api_frame_helper.h
@@ -0,0 +1,182 @@
+#pragma once
+#include <cstdint>
+#include <vector>
+#include <deque>
+
+#include "esphome/core/defines.h"
+
+#ifdef USE_API_NOISE
+#include "noise/protocol.h"
+#endif
+
+#include "esphome/components/socket/socket.h"
+#include "api_noise_context.h"
+
+namespace esphome {
+namespace api {
+
+struct ReadPacketBuffer {
+  std::vector<uint8_t> container;
+  uint16_t type;
+  size_t data_offset;
+  size_t data_len;
+};
+
+struct PacketBuffer {
+  const std::vector<uint8_t> container;
+  uint16_t type;
+  uint8_t data_offset;
+  uint8_t data_len;
+};
+
+enum class APIError : int {
+  OK = 0,
+  WOULD_BLOCK = 1001,
+  BAD_HANDSHAKE_PACKET_LEN = 1002,
+  BAD_INDICATOR = 1003,
+  BAD_DATA_PACKET = 1004,
+  TCP_NODELAY_FAILED = 1005,
+  TCP_NONBLOCKING_FAILED = 1006,
+  CLOSE_FAILED = 1007,
+  SHUTDOWN_FAILED = 1008,
+  BAD_STATE = 1009,
+  BAD_ARG = 1010,
+  SOCKET_READ_FAILED = 1011,
+  SOCKET_WRITE_FAILED = 1012,
+  HANDSHAKESTATE_READ_FAILED = 1013,
+  HANDSHAKESTATE_WRITE_FAILED = 1014,
+  HANDSHAKESTATE_BAD_STATE = 1015,
+  CIPHERSTATE_DECRYPT_FAILED = 1016,
+  CIPHERSTATE_ENCRYPT_FAILED = 1017,
+  OUT_OF_MEMORY = 1018,
+  HANDSHAKESTATE_SETUP_FAILED = 1019,
+  HANDSHAKESTATE_SPLIT_FAILED = 1020,
+  BAD_HANDSHAKE_ERROR_BYTE = 1021,
+};
+
+const char *api_error_to_str(APIError err);
+
+class APIFrameHelper {
+ public:
+  virtual APIError init() = 0;
+  virtual APIError loop() = 0;
+  virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
+  virtual bool can_write_without_blocking() = 0;
+  virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0;
+  virtual std::string getpeername() = 0;
+  virtual APIError close() = 0;
+  virtual APIError shutdown(int how) = 0;
+  // Give this helper a name for logging
+  virtual void set_log_info(std::string info) = 0;
+};
+
+#ifdef USE_API_NOISE
+class APINoiseFrameHelper : public APIFrameHelper {
+ public:
+  APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
+      : socket_(std::move(socket)), ctx_(ctx) {}
+  ~APINoiseFrameHelper();
+  APIError init() override;
+  APIError loop() override;
+  APIError read_packet(ReadPacketBuffer *buffer) override;
+  bool can_write_without_blocking() override;
+  APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
+  std::string getpeername() override { return socket_->getpeername(); }
+  APIError close() override;
+  APIError shutdown(int how) override;
+  // Give this helper a name for logging
+  void set_log_info(std::string info) override { info_ = std::move(info); }
+
+ protected:
+  struct ParsedFrame {
+    std::vector<uint8_t> msg;
+  };
+
+  APIError state_action_();
+  APIError try_read_frame_(ParsedFrame *frame);
+  APIError try_send_tx_buf_();
+  APIError write_frame_(const uint8_t *data, size_t len);
+  APIError write_raw_(const uint8_t *data, size_t len);
+  APIError init_handshake_();
+  APIError check_handshake_finished_();
+  void send_explicit_handshake_reject_(const std::string &reason);
+
+  std::unique_ptr<socket::Socket> socket_;
+
+  std::string info_;
+  uint8_t rx_header_buf_[3];
+  size_t rx_header_buf_len_ = 0;
+  std::vector<uint8_t> rx_buf_;
+  size_t rx_buf_len_ = 0;
+
+  std::vector<uint8_t> tx_buf_;
+  std::vector<uint8_t> prologue_;
+
+  std::shared_ptr<APINoiseContext> ctx_;
+  NoiseHandshakeState *handshake_ = nullptr;
+  NoiseCipherState *send_cipher_ = nullptr;
+  NoiseCipherState *recv_cipher_ = nullptr;
+  NoiseProtocolId nid_;
+
+  enum class State {
+    INITIALIZE = 1,
+    CLIENT_HELLO = 2,
+    SERVER_HELLO = 3,
+    HANDSHAKE = 4,
+    DATA = 5,
+    CLOSED = 6,
+    FAILED = 7,
+    EXPLICIT_REJECT = 8,
+  } state_ = State::INITIALIZE;
+};
+#endif  // USE_API_NOISE
+
+#ifdef USE_API_PLAINTEXT
+class APIPlaintextFrameHelper : public APIFrameHelper {
+ public:
+  APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {}
+  ~APIPlaintextFrameHelper() = default;
+  APIError init() override;
+  APIError loop() override;
+  APIError read_packet(ReadPacketBuffer *buffer) override;
+  bool can_write_without_blocking() override;
+  APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
+  std::string getpeername() override { return socket_->getpeername(); }
+  APIError close() override;
+  APIError shutdown(int how) override;
+  // Give this helper a name for logging
+  void set_log_info(std::string info) override { info_ = std::move(info); }
+
+ protected:
+  struct ParsedFrame {
+    std::vector<uint8_t> msg;
+  };
+
+  APIError try_read_frame_(ParsedFrame *frame);
+  APIError try_send_tx_buf_();
+  APIError write_raw_(const uint8_t *data, size_t len);
+
+  std::unique_ptr<socket::Socket> socket_;
+
+  std::string info_;
+  std::vector<uint8_t> rx_header_buf_;
+  bool rx_header_parsed_ = false;
+  uint32_t rx_header_parsed_type_ = 0;
+  uint32_t rx_header_parsed_len_ = 0;
+
+  std::vector<uint8_t> rx_buf_;
+  size_t rx_buf_len_ = 0;
+
+  std::vector<uint8_t> tx_buf_;
+
+  enum class State {
+    INITIALIZE = 1,
+    DATA = 2,
+    CLOSED = 3,
+    FAILED = 4,
+  } state_ = State::INITIALIZE;
+};
+#endif
+
+}  // namespace api
+}  // namespace esphome
diff --git a/esphome/components/api/api_noise_context.h b/esphome/components/api/api_noise_context.h
new file mode 100644
index 0000000000..fba6b65a26
--- /dev/null
+++ b/esphome/components/api/api_noise_context.h
@@ -0,0 +1,23 @@
+#pragma once
+#include <cstdint>
+#include <array>
+#include "esphome/core/defines.h"
+
+namespace esphome {
+namespace api {
+
+#ifdef USE_API_NOISE
+using psk_t = std::array<uint8_t, 32>;
+
+class APINoiseContext {
+ public:
+  void set_psk(psk_t psk) { psk_ = std::move(psk); }
+  const psk_t &get_psk() const { return psk_; }
+
+ protected:
+  psk_t psk_;
+};
+#endif  // USE_API_NOISE
+
+}  // namespace api
+}  // namespace esphome
diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp
index f5860bee64..d6b85d257c 100644
--- a/esphome/components/api/api_pb2.cpp
+++ b/esphome/components/api/api_pb2.cpp
@@ -1817,7 +1817,7 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va
       return true;
     }
     case 11: {
-      this->last_reset_type = value.as_enum<enums::SensorLastResetType>();
+      this->legacy_last_reset_type = value.as_enum<enums::SensorLastResetType>();
       return true;
     }
     case 12: {
@@ -1879,7 +1879,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_bool(8, this->force_update);
   buffer.encode_string(9, this->device_class);
   buffer.encode_enum<enums::SensorStateClass>(10, this->state_class);
-  buffer.encode_enum<enums::SensorLastResetType>(11, this->last_reset_type);
+  buffer.encode_enum<enums::SensorLastResetType>(11, this->legacy_last_reset_type);
   buffer.encode_bool(12, this->disabled_by_default);
 }
 #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1928,8 +1928,8 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const {
   out.append(proto_enum_to_string<enums::SensorStateClass>(this->state_class));
   out.append("\n");
 
-  out.append("  last_reset_type: ");
-  out.append(proto_enum_to_string<enums::SensorLastResetType>(this->last_reset_type));
+  out.append("  legacy_last_reset_type: ");
+  out.append(proto_enum_to_string<enums::SensorLastResetType>(this->legacy_last_reset_type));
   out.append("\n");
 
   out.append("  disabled_by_default: ");
diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h
index 93bfcd9b55..1371ab5248 100644
--- a/esphome/components/api/api_pb2.h
+++ b/esphome/components/api/api_pb2.h
@@ -510,7 +510,7 @@ class ListEntitiesSensorResponse : public ProtoMessage {
   bool force_update{false};
   std::string device_class{};
   enums::SensorStateClass state_class{};
-  enums::SensorLastResetType last_reset_type{};
+  enums::SensorLastResetType legacy_last_reset_type{};
   bool disabled_by_default{false};
   void encode(ProtoWriteBuffer buffer) const override;
 #ifdef HAS_PROTO_MESSAGE_DUMP
diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp
index d48c0a4fd8..33843f384b 100644
--- a/esphome/components/api/api_server.cpp
+++ b/esphome/components/api/api_server.cpp
@@ -1,10 +1,11 @@
 #include "api_server.h"
 #include "api_connection.h"
-#include "esphome/core/log.h"
 #include "esphome/core/application.h"
-#include "esphome/core/util.h"
 #include "esphome/core/defines.h"
+#include "esphome/core/log.h"
+#include "esphome/core/util.h"
 #include "esphome/core/version.h"
+#include <cerrno>
 
 #ifdef USE_LOGGER
 #include "esphome/components/logger/logger.h"
@@ -21,20 +22,45 @@ static const char *const TAG = "api";
 void APIServer::setup() {
   ESP_LOGCONFIG(TAG, "Setting up Home Assistant API server...");
   this->setup_controller();
-  this->server_ = AsyncServer(this->port_);
-  this->server_.setNoDelay(false);
-  this->server_.begin();
-  this->server_.onClient(
-      [](void *s, AsyncClient *client) {
-        if (client == nullptr)
-          return;
+  socket_ = socket::socket(AF_INET, SOCK_STREAM, 0);
+  if (socket_ == nullptr) {
+    ESP_LOGW(TAG, "Could not create socket.");
+    this->mark_failed();
+    return;
+  }
+  int enable = 1;
+  int err = socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
+  if (err != 0) {
+    ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
+    // we can still continue
+  }
+  err = socket_->setblocking(false);
+  if (err != 0) {
+    ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
+    this->mark_failed();
+    return;
+  }
+
+  struct sockaddr_in server;
+  memset(&server, 0, sizeof(server));
+  server.sin_family = AF_INET;
+  server.sin_addr.s_addr = ESPHOME_INADDR_ANY;
+  server.sin_port = htons(this->port_);
+
+  err = socket_->bind((struct sockaddr *) &server, sizeof(server));
+  if (err != 0) {
+    ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
+    this->mark_failed();
+    return;
+  }
+
+  err = socket_->listen(4);
+  if (err != 0) {
+    ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
+    this->mark_failed();
+    return;
+  }
 
-        // can't print here because in lwIP thread
-        // ESP_LOGD(TAG, "New client connected from %s", client->remoteIP().toString().c_str());
-        auto *a_this = (APIServer *) s;
-        a_this->clients_.push_back(new APIConnection(client, a_this));
-      },
-      this);
 #ifdef USE_LOGGER
   if (logger::global_logger != nullptr) {
     logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) {
@@ -59,12 +85,26 @@ void APIServer::setup() {
 #endif
 }
 void APIServer::loop() {
+  // Accept new clients
+  while (true) {
+    struct sockaddr_storage source_addr;
+    socklen_t addr_len = sizeof(source_addr);
+    auto sock = socket_->accept((struct sockaddr *) &source_addr, &addr_len);
+    if (!sock)
+      break;
+    ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str());
+
+    auto *conn = new APIConnection(std::move(sock), this);
+    clients_.push_back(conn);
+    conn->start();
+  }
+
   // Partition clients into remove and active
   auto new_end =
       std::partition(this->clients_.begin(), this->clients_.end(), [](APIConnection *conn) { return !conn->remove_; });
   // print disconnection messages
   for (auto it = new_end; it != this->clients_.end(); ++it) {
-    ESP_LOGD(TAG, "Disconnecting %s", (*it)->client_info_.c_str());
+    ESP_LOGV(TAG, "Removing connection to %s", (*it)->client_info_.c_str());
   }
   // only then delete the pointers, otherwise log routine
   // would access freed memory
diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h
index 96b3192e9e..e3fa6b18c9 100644
--- a/esphome/components/api/api_server.h
+++ b/esphome/components/api/api_server.h
@@ -4,19 +4,14 @@
 #include "esphome/core/controller.h"
 #include "esphome/core/defines.h"
 #include "esphome/core/log.h"
+#include "esphome/components/socket/socket.h"
 #include "api_pb2.h"
 #include "api_pb2_service.h"
 #include "util.h"
 #include "list_entities.h"
 #include "subscribe_state.h"
 #include "user_services.h"
-
-#ifdef ARDUINO_ARCH_ESP32
-#include <AsyncTCP.h>
-#endif
-#ifdef ARDUINO_ARCH_ESP8266
-#include <ESPAsyncTCP.h>
-#endif
+#include "api_noise_context.h"
 
 namespace esphome {
 namespace api {
@@ -35,6 +30,12 @@ class APIServer : public Component, public Controller {
   void set_port(uint16_t port);
   void set_password(const std::string &password);
   void set_reboot_timeout(uint32_t reboot_timeout);
+
+#ifdef USE_API_NOISE
+  void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(std::move(psk)); }
+  std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
+#endif  // USE_API_NOISE
+
   void handle_disconnect(APIConnection *conn);
 #ifdef USE_BINARY_SENSOR
   void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override;
@@ -86,7 +87,7 @@ class APIServer : public Component, public Controller {
   const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
 
  protected:
-  AsyncServer server_{0};
+  std::unique_ptr<socket::Socket> socket_ = nullptr;
   uint16_t port_{6053};
   uint32_t reboot_timeout_{300000};
   uint32_t last_connected_{0};
@@ -94,6 +95,10 @@ class APIServer : public Component, public Controller {
   std::string password_;
   std::vector<HomeAssistantStateSubscription> state_subs_;
   std::vector<UserServiceDescriptor *> user_services_;
+
+#ifdef USE_API_NOISE
+  std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
+#endif  // USE_API_NOISE
 };
 
 extern APIServer *global_api_server;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py
new file mode 100644
index 0000000000..4a3944d33e
--- /dev/null
+++ b/esphome/components/api/client.py
@@ -0,0 +1,73 @@
+import asyncio
+import logging
+from datetime import datetime
+from typing import Optional
+
+from aioesphomeapi import APIClient, ReconnectLogic, APIConnectionError, LogLevel
+import zeroconf
+
+from esphome.const import CONF_KEY, CONF_PORT, CONF_PASSWORD, __version__
+from esphome.util import safe_print
+from . import CONF_ENCRYPTION
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_run_logs(config, address):
+    conf = config["api"]
+    port: int = int(conf[CONF_PORT])
+    password: str = conf[CONF_PASSWORD]
+    noise_psk: Optional[str] = None
+    if CONF_ENCRYPTION in conf:
+        noise_psk = conf[CONF_ENCRYPTION][CONF_KEY]
+    _LOGGER.info("Starting log output from %s using esphome API", address)
+    zc = zeroconf.Zeroconf()
+    cli = APIClient(
+        asyncio.get_event_loop(),
+        address,
+        port,
+        password,
+        client_info=f"ESPHome Logs {__version__}",
+        noise_psk=noise_psk,
+    )
+    first_connect = True
+
+    def on_log(msg):
+        time_ = datetime.now().time().strftime("[%H:%M:%S]")
+        text = msg.message.decode("utf8", "backslashreplace")
+        safe_print(time_ + text)
+
+    async def on_connect():
+        nonlocal first_connect
+        try:
+            await cli.subscribe_logs(
+                on_log,
+                log_level=LogLevel.LOG_LEVEL_VERY_VERBOSE,
+                dump_config=first_connect,
+            )
+            first_connect = False
+        except APIConnectionError:
+            cli.disconnect()
+
+    async def on_disconnect():
+        _LOGGER.warning("Disconnected from API")
+
+    zc = zeroconf.Zeroconf()
+    reconnect = ReconnectLogic(
+        client=cli,
+        on_connect=on_connect,
+        on_disconnect=on_disconnect,
+        zeroconf_instance=zc,
+    )
+    await reconnect.start()
+
+    try:
+        while True:
+            await asyncio.sleep(60)
+    except KeyboardInterrupt:
+        await reconnect.stop()
+        zc.close()
+
+
+def run_logs(config, address):
+    asyncio.run(async_run_logs(config, address))
diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py
index 28b49604ff..05e5250d89 100644
--- a/esphome/components/atm90e32/sensor.py
+++ b/esphome/components/atm90e32/sensor.py
@@ -19,8 +19,8 @@ from esphome.const import (
     DEVICE_CLASS_VOLTAGE,
     ICON_LIGHTBULB,
     ICON_CURRENT_AC,
-    LAST_RESET_TYPE_AUTO,
     STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_TOTAL_INCREASING,
     UNIT_HERTZ,
     UNIT_VOLT,
     UNIT_AMPERE,
@@ -94,15 +94,13 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
             unit_of_measurement=UNIT_WATT_HOURS,
             accuracy_decimals=2,
             device_class=DEVICE_CLASS_ENERGY,
-            state_class=STATE_CLASS_MEASUREMENT,
-            last_reset_type=LAST_RESET_TYPE_AUTO,
+            state_class=STATE_CLASS_TOTAL_INCREASING,
         ),
         cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema(
             unit_of_measurement=UNIT_WATT_HOURS,
             accuracy_decimals=2,
             device_class=DEVICE_CLASS_ENERGY,
-            state_class=STATE_CLASS_MEASUREMENT,
-            last_reset_type=LAST_RESET_TYPE_AUTO,
+            state_class=STATE_CLASS_TOTAL_INCREASING,
         ),
         cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t,
         cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t,
diff --git a/esphome/components/binary/fan/binary_fan.cpp b/esphome/components/binary/fan/binary_fan.cpp
index eaf41829bb..2201fe576e 100644
--- a/esphome/components/binary/fan/binary_fan.cpp
+++ b/esphome/components/binary/fan/binary_fan.cpp
@@ -55,7 +55,10 @@ void BinaryFan::loop() {
     ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable));
   }
 }
-float BinaryFan::get_setup_priority() const { return setup_priority::DATA; }
+
+// We need a higher priority than the FanState component to make sure that the traits are set
+// when that component sets itself up.
+float BinaryFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; }
 
 }  // namespace binary
 }  // namespace esphome
diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py
index 3c2169a922..9cd2a045ff 100644
--- a/esphome/components/binary_sensor/__init__.py
+++ b/esphome/components/binary_sensor/__init__.py
@@ -48,6 +48,7 @@ from esphome.const import (
     DEVICE_CLASS_SAFETY,
     DEVICE_CLASS_SMOKE,
     DEVICE_CLASS_SOUND,
+    DEVICE_CLASS_UPDATE,
     DEVICE_CLASS_VIBRATION,
     DEVICE_CLASS_WINDOW,
 )
@@ -79,6 +80,7 @@ DEVICE_CLASSES = [
     DEVICE_CLASS_SAFETY,
     DEVICE_CLASS_SMOKE,
     DEVICE_CLASS_SOUND,
+    DEVICE_CLASS_UPDATE,
     DEVICE_CLASS_VIBRATION,
     DEVICE_CLASS_WINDOW,
 ]
diff --git a/esphome/components/ble_presence/binary_sensor.py b/esphome/components/ble_presence/binary_sensor.py
index c58d29e6be..2a242c3aca 100644
--- a/esphome/components/ble_presence/binary_sensor.py
+++ b/esphome/components/ble_presence/binary_sensor.py
@@ -1,7 +1,14 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import binary_sensor, esp32_ble_tracker
-from esphome.const import CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_ID
+from esphome.const import (
+    CONF_MAC_ADDRESS,
+    CONF_SERVICE_UUID,
+    CONF_IBEACON_MAJOR,
+    CONF_IBEACON_MINOR,
+    CONF_IBEACON_UUID,
+    CONF_ID,
+)
 
 DEPENDENCIES = ["esp32_ble_tracker"]
 
@@ -13,17 +20,30 @@ BLEPresenceDevice = ble_presence_ns.class_(
     esp32_ble_tracker.ESPBTDeviceListener,
 )
 
+
+def _validate(config):
+    if CONF_IBEACON_MAJOR in config and CONF_IBEACON_UUID not in config:
+        raise cv.Invalid("iBeacon major identifier requires iBeacon UUID")
+    if CONF_IBEACON_MINOR in config and CONF_IBEACON_UUID not in config:
+        raise cv.Invalid("iBeacon minor identifier requires iBeacon UUID")
+    return config
+
+
 CONFIG_SCHEMA = cv.All(
     binary_sensor.BINARY_SENSOR_SCHEMA.extend(
         {
             cv.GenerateID(): cv.declare_id(BLEPresenceDevice),
             cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
             cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
+            cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t,
+            cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t,
+            cv.Optional(CONF_IBEACON_UUID): cv.uuid,
         }
     )
     .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
     .extend(cv.COMPONENT_SCHEMA),
-    cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID),
+    cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_IBEACON_UUID),
+    _validate,
 )
 
 
@@ -50,5 +70,15 @@ async def to_code(config):
                 )
             )
         elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format):
-            uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID])
+            uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID])
             cg.add(var.set_service_uuid128(uuid128))
+
+    if CONF_IBEACON_UUID in config:
+        ibeacon_uuid = esp32_ble_tracker.as_hex_array(str(config[CONF_IBEACON_UUID]))
+        cg.add(var.set_ibeacon_uuid(ibeacon_uuid))
+
+        if CONF_IBEACON_MAJOR in config:
+            cg.add(var.set_ibeacon_major(config[CONF_IBEACON_MAJOR]))
+
+        if CONF_IBEACON_MINOR in config:
+            cg.add(var.set_ibeacon_minor(config[CONF_IBEACON_MINOR]))
diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h
index bce6a9cf98..dfc36d68cb 100644
--- a/esphome/components/ble_presence/ble_presence_device.h
+++ b/esphome/components/ble_presence/ble_presence_device.h
@@ -14,41 +14,78 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff,
                           public Component {
  public:
   void set_address(uint64_t address) {
-    this->by_address_ = true;
+    this->match_by_ = MATCH_BY_MAC_ADDRESS;
     this->address_ = address;
   }
   void set_service_uuid16(uint16_t uuid) {
-    this->by_address_ = false;
+    this->match_by_ = MATCH_BY_SERVICE_UUID;
     this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint16(uuid);
   }
   void set_service_uuid32(uint32_t uuid) {
-    this->by_address_ = false;
+    this->match_by_ = MATCH_BY_SERVICE_UUID;
     this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint32(uuid);
   }
   void set_service_uuid128(uint8_t *uuid) {
-    this->by_address_ = false;
+    this->match_by_ = MATCH_BY_SERVICE_UUID;
     this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid);
   }
+  void set_ibeacon_uuid(uint8_t *uuid) {
+    this->match_by_ = MATCH_BY_IBEACON_UUID;
+    this->ibeacon_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid);
+  }
+  void set_ibeacon_major(uint16_t major) {
+    this->check_ibeacon_major_ = true;
+    this->ibeacon_major_ = major;
+  }
+  void set_ibeacon_minor(uint16_t minor) {
+    this->check_ibeacon_minor_ = true;
+    this->ibeacon_minor_ = minor;
+  }
   void on_scan_end() override {
     if (!this->found_)
       this->publish_state(false);
     this->found_ = false;
   }
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override {
-    if (this->by_address_) {
-      if (device.address_uint64() == this->address_) {
-        this->publish_state(true);
-        this->found_ = true;
-        return true;
-      }
-    } else {
-      for (auto uuid : device.get_service_uuids()) {
-        if (this->uuid_ == uuid) {
-          this->publish_state(device.get_rssi());
+    switch (this->match_by_) {
+      case MATCH_BY_MAC_ADDRESS:
+        if (device.address_uint64() == this->address_) {
+          this->publish_state(true);
           this->found_ = true;
           return true;
         }
-      }
+        break;
+      case MATCH_BY_SERVICE_UUID:
+        for (auto uuid : device.get_service_uuids()) {
+          if (this->uuid_ == uuid) {
+            this->publish_state(device.get_rssi());
+            this->found_ = true;
+            return true;
+          }
+        }
+        break;
+      case MATCH_BY_IBEACON_UUID:
+        if (!device.get_ibeacon().has_value()) {
+          return false;
+        }
+
+        auto ibeacon = device.get_ibeacon().value();
+
+        if (this->ibeacon_uuid_ != ibeacon.get_uuid()) {
+          return false;
+        }
+
+        if (this->check_ibeacon_major_ && this->ibeacon_major_ != ibeacon.get_major()) {
+          return false;
+        }
+
+        if (this->check_ibeacon_minor_ && this->ibeacon_minor_ != ibeacon.get_minor()) {
+          return false;
+        }
+
+        this->publish_state(device.get_rssi());
+        this->found_ = true;
+        return true;
     }
     return false;
   }
@@ -56,10 +93,20 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff,
   float get_setup_priority() const override { return setup_priority::DATA; }
 
  protected:
+  enum MATCH_TYPE { MATCH_BY_MAC_ADDRESS, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID };
+  MATCH_TYPE match_by_;
+
   bool found_{false};
-  bool by_address_{false};
+
   uint64_t address_;
+
   esp32_ble_tracker::ESPBTUUID uuid_;
+
+  esp32_ble_tracker::ESPBTUUID ibeacon_uuid_;
+  uint16_t ibeacon_major_;
+  bool check_ibeacon_major_;
+  uint16_t ibeacon_minor_;
+  bool check_ibeacon_minor_;
 };
 
 }  // namespace ble_presence
diff --git a/esphome/components/ble_rssi/sensor.py b/esphome/components/ble_rssi/sensor.py
index bca73328f9..0c4308b11a 100644
--- a/esphome/components/ble_rssi/sensor.py
+++ b/esphome/components/ble_rssi/sensor.py
@@ -60,5 +60,5 @@ async def to_code(config):
                 )
             )
         elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format):
-            uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID])
+            uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID])
             cg.add(var.set_service_uuid128(uuid128))
diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp
index 8f53180296..e2cb7491a6 100644
--- a/esphome/components/bme680_bsec/bme680_bsec.cpp
+++ b/esphome/components/bme680_bsec/bme680_bsec.cpp
@@ -10,7 +10,7 @@ static const char *const TAG = "bme680_bsec.sensor";
 
 static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"};
 
-BME680BSECComponent *BME680BSECComponent::instance;
+BME680BSECComponent *BME680BSECComponent::instance;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
 void BME680BSECComponent::setup() {
   ESP_LOGCONFIG(TAG, "Setting up BME680 via BSEC...");
@@ -359,7 +359,7 @@ void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float va
   sensor->publish_state(value);
 }
 
-void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value) {
+void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value) {
   if (!sensor || (sensor->has_state() && sensor->state == value)) {
     return;
   }
diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h
index 73994b7541..365aec725e 100644
--- a/esphome/components/bme680_bsec/bme680_bsec.h
+++ b/esphome/components/bme680_bsec/bme680_bsec.h
@@ -70,7 +70,7 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
   int64_t get_time_ns_();
 
   void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false);
-  void publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value);
+  void publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value);
 
   void load_state_();
   void save_state_(uint8_t accuracy);
diff --git a/esphome/components/ccs811/ccs811.cpp b/esphome/components/ccs811/ccs811.cpp
index dec070a9b2..08df6f7774 100644
--- a/esphome/components/ccs811/ccs811.cpp
+++ b/esphome/components/ccs811/ccs811.cpp
@@ -16,7 +16,7 @@ static const char *const TAG = "ccs811";
     return; \
   }
 
-#define CHECKED_IO(f) CHECK_TRUE(f, COMMUNICAITON_FAILED)
+#define CHECKED_IO(f) CHECK_TRUE(f, COMMUNICATION_FAILED)
 
 void CCS811Component::setup() {
   // page 9 programming guide - hwid is always 0x81
@@ -38,12 +38,14 @@ void CCS811Component::setup() {
   // set MEAS_MODE (page 5)
   uint8_t meas_mode = 0;
   uint32_t interval = this->get_update_interval();
-  if (interval <= 1000)
-    meas_mode = 1 << 4;
-  else if (interval <= 10000)
-    meas_mode = 2 << 4;
+  if (interval >= 60 * 1000)
+    meas_mode = 3 << 4;  // sensor takes a reading every 60 seconds
+  else if (interval >= 10 * 1000)
+    meas_mode = 2 << 4;  // sensor takes a reading every 10 seconds
+  else if (interval >= 1 * 1000)
+    meas_mode = 1 << 4;  // sensor takes a reading every second
   else
-    meas_mode = 3 << 4;
+    meas_mode = 4 << 4;  // sensor takes a reading every 250ms
 
   CHECKED_IO(this->write_byte(0x01, meas_mode))
 
@@ -51,6 +53,36 @@ void CCS811Component::setup() {
     // baseline available, write to sensor
     this->write_bytes(0x11, decode_uint16(*this->baseline_));
   }
+
+  auto hardware_version_data = this->read_bytes<1>(0x21);
+  auto bootloader_version_data = this->read_bytes<2>(0x23);
+  auto application_version_data = this->read_bytes<2>(0x24);
+
+  uint8_t hardware_version = 0;
+  uint16_t bootloader_version = 0;
+  uint16_t application_version = 0;
+
+  if (hardware_version_data.has_value()) {
+    hardware_version = (*hardware_version_data)[0];
+  }
+
+  if (bootloader_version_data.has_value()) {
+    bootloader_version = encode_uint16((*bootloader_version_data)[0], (*bootloader_version_data)[1]);
+  }
+
+  if (application_version_data.has_value()) {
+    application_version = encode_uint16((*application_version_data)[0], (*application_version_data)[1]);
+  }
+
+  ESP_LOGD(TAG, "hardware_version=0x%x bootloader_version=0x%x application_version=0x%x\n", hardware_version,
+           bootloader_version, application_version);
+  if (this->version_ != nullptr) {
+    char version[20];  // "15.15.15 (0xffff)" is 17 chars, plus NUL, plus wiggle room
+    sprintf(version, "%d.%d.%d (0x%02x)", (application_version >> 12 & 15), (application_version >> 8 & 15),
+            (application_version >> 4 & 15), application_version);
+    ESP_LOGD(TAG, "publishing version state: %s", version);
+    this->version_->publish_state(version);
+  }
 }
 void CCS811Component::update() {
   if (!this->status_has_data_())
@@ -117,6 +149,7 @@ void CCS811Component::dump_config() {
   LOG_UPDATE_INTERVAL(this)
   LOG_SENSOR("  ", "CO2 Sensor", this->co2_)
   LOG_SENSOR("  ", "TVOC Sensor", this->tvoc_)
+  LOG_TEXT_SENSOR("  ", "Firmware Version Sensor", this->version_)
   if (this->baseline_) {
     ESP_LOGCONFIG(TAG, "  Baseline: %04X", *this->baseline_);
   } else {
@@ -124,7 +157,7 @@ void CCS811Component::dump_config() {
   }
   if (this->is_failed()) {
     switch (this->error_code_) {
-      case COMMUNICAITON_FAILED:
+      case COMMUNICATION_FAILED:
         ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
         break;
       case INVALID_ID:
diff --git a/esphome/components/ccs811/ccs811.h b/esphome/components/ccs811/ccs811.h
index cea919c9a5..8a0d60d002 100644
--- a/esphome/components/ccs811/ccs811.h
+++ b/esphome/components/ccs811/ccs811.h
@@ -3,6 +3,7 @@
 #include "esphome/core/component.h"
 #include "esphome/core/preferences.h"
 #include "esphome/components/sensor/sensor.h"
+#include "esphome/components/text_sensor/text_sensor.h"
 #include "esphome/components/i2c/i2c.h"
 
 namespace esphome {
@@ -12,6 +13,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice {
  public:
   void set_co2(sensor::Sensor *co2) { co2_ = co2; }
   void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; }
+  void set_version(text_sensor::TextSensor *version) { version_ = version; }
   void set_baseline(uint16_t baseline) { baseline_ = baseline; }
   void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
   void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
@@ -34,7 +36,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice {
 
   enum ErrorCode {
     UNKNOWN,
-    COMMUNICAITON_FAILED,
+    COMMUNICATION_FAILED,
     INVALID_ID,
     SENSOR_REPORTED_ERROR,
     APP_INVALID,
@@ -43,6 +45,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice {
 
   sensor::Sensor *co2_{nullptr};
   sensor::Sensor *tvoc_{nullptr};
+  text_sensor::TextSensor *version_{nullptr};
   optional<uint16_t> baseline_{};
   /// Input sensor for humidity reading.
   sensor::Sensor *humidity_{nullptr};
diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py
index 4c09a14c3e..bb8200273d 100644
--- a/esphome/components/ccs811/sensor.py
+++ b/esphome/components/ccs811/sensor.py
@@ -1,9 +1,13 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
-from esphome.components import i2c, sensor
+from esphome.components import i2c, sensor, text_sensor
 from esphome.const import (
+    CONF_ICON,
     CONF_ID,
     ICON_RADIATOR,
+    ICON_RESTART,
+    DEVICE_CLASS_CARBON_DIOXIDE,
+    DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
     STATE_CLASS_MEASUREMENT,
     UNIT_PARTS_PER_MILLION,
     UNIT_PARTS_PER_BILLION,
@@ -12,9 +16,12 @@ from esphome.const import (
     CONF_TEMPERATURE,
     CONF_TVOC,
     CONF_HUMIDITY,
+    CONF_VERSION,
     ICON_MOLECULE_CO2,
 )
 
+AUTO_LOAD = ["text_sensor"]
+CODEOWNERS = ["@habbie"]
 DEPENDENCIES = ["i2c"]
 
 ccs811_ns = cg.esphome_ns.namespace("ccs811")
@@ -30,14 +37,22 @@ CONFIG_SCHEMA = (
                 unit_of_measurement=UNIT_PARTS_PER_MILLION,
                 icon=ICON_MOLECULE_CO2,
                 accuracy_decimals=0,
+                device_class=DEVICE_CLASS_CARBON_DIOXIDE,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Required(CONF_TVOC): sensor.sensor_schema(
                 unit_of_measurement=UNIT_PARTS_PER_BILLION,
                 icon=ICON_RADIATOR,
                 accuracy_decimals=0,
+                device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
+            cv.Optional(CONF_VERSION): text_sensor.TEXT_SENSOR_SCHEMA.extend(
+                {
+                    cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
+                    cv.Optional(CONF_ICON, default=ICON_RESTART): cv.icon,
+                }
+            ),
             cv.Optional(CONF_BASELINE): cv.hex_uint16_t,
             cv.Optional(CONF_TEMPERATURE): cv.use_id(sensor.Sensor),
             cv.Optional(CONF_HUMIDITY): cv.use_id(sensor.Sensor),
@@ -58,6 +73,11 @@ async def to_code(config):
     sens = await sensor.new_sensor(config[CONF_TVOC])
     cg.add(var.set_tvoc(sens))
 
+    if CONF_VERSION in config:
+        sens = cg.new_Pvariable(config[CONF_VERSION][CONF_ID])
+        await text_sensor.register_text_sensor(sens, config[CONF_VERSION])
+        cg.add(var.set_version(sens))
+
     if CONF_BASELINE in config:
         cg.add(var.set_baseline(config[CONF_BASELINE]))
 
diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py
index f6a9fa2927..c2f07ce423 100644
--- a/esphome/components/climate/__init__.py
+++ b/esphome/components/climate/__init__.py
@@ -63,6 +63,7 @@ validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True)
 
 ClimatePreset = climate_ns.enum("ClimatePreset")
 CLIMATE_PRESETS = {
+    "NONE": ClimatePreset.CLIMATE_PRESET_NONE,
     "ECO": ClimatePreset.CLIMATE_PRESET_ECO,
     "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
     "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp
index 8da2206f37..4861e7b8cb 100644
--- a/esphome/components/climate/climate.cpp
+++ b/esphome/components/climate/climate.cpp
@@ -494,5 +494,74 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
   climate->publish_state();
 }
 
+template<typename T1, typename T2> bool set_alternative(optional<T1> &dst, optional<T2> &alt, const T1 &src) {
+  bool is_changed = alt.has_value();
+  alt.reset();
+  if (is_changed || dst != src) {
+    dst = src;
+    is_changed = true;
+  }
+  return is_changed;
+}
+
+bool Climate::set_fan_mode_(ClimateFanMode mode) {
+  return set_alternative(this->fan_mode, this->custom_fan_mode, mode);
+}
+
+bool Climate::set_custom_fan_mode_(const std::string &mode) {
+  return set_alternative(this->custom_fan_mode, this->fan_mode, mode);
+}
+
+bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); }
+
+bool Climate::set_custom_preset_(const std::string &preset) {
+  return set_alternative(this->custom_preset, this->preset, preset);
+}
+
+void Climate::dump_traits_(const char *tag) {
+  auto traits = this->get_traits();
+  ESP_LOGCONFIG(tag, "ClimateTraits:");
+  ESP_LOGCONFIG(tag, "  [x] Visual settings:");
+  ESP_LOGCONFIG(tag, "      - Min: %.1f", traits.get_visual_min_temperature());
+  ESP_LOGCONFIG(tag, "      - Max: %.1f", traits.get_visual_max_temperature());
+  ESP_LOGCONFIG(tag, "      - Step: %.1f", traits.get_visual_temperature_step());
+  if (traits.get_supports_current_temperature())
+    ESP_LOGCONFIG(tag, "  [x] Supports current temperature");
+  if (traits.get_supports_two_point_target_temperature())
+    ESP_LOGCONFIG(tag, "  [x] Supports two-point target temperature");
+  if (traits.get_supports_action())
+    ESP_LOGCONFIG(tag, "  [x] Supports action");
+  if (!traits.get_supported_modes().empty()) {
+    ESP_LOGCONFIG(tag, "  [x] Supported modes:");
+    for (ClimateMode m : traits.get_supported_modes())
+      ESP_LOGCONFIG(tag, "      - %s", climate_mode_to_string(m));
+  }
+  if (!traits.get_supported_fan_modes().empty()) {
+    ESP_LOGCONFIG(tag, "  [x] Supported fan modes:");
+    for (ClimateFanMode m : traits.get_supported_fan_modes())
+      ESP_LOGCONFIG(tag, "      - %s", climate_fan_mode_to_string(m));
+  }
+  if (!traits.get_supported_custom_fan_modes().empty()) {
+    ESP_LOGCONFIG(tag, "  [x] Supported custom fan modes:");
+    for (const std::string &s : traits.get_supported_custom_fan_modes())
+      ESP_LOGCONFIG(tag, "      - %s", s.c_str());
+  }
+  if (!traits.get_supported_presets().empty()) {
+    ESP_LOGCONFIG(tag, "  [x] Supported presets:");
+    for (ClimatePreset p : traits.get_supported_presets())
+      ESP_LOGCONFIG(tag, "      - %s", climate_preset_to_string(p));
+  }
+  if (!traits.get_supported_custom_presets().empty()) {
+    ESP_LOGCONFIG(tag, "  [x] Supported custom presets:");
+    for (const std::string &s : traits.get_supported_custom_presets())
+      ESP_LOGCONFIG(tag, "      - %s", s.c_str());
+  }
+  if (!traits.get_supported_swing_modes().empty()) {
+    ESP_LOGCONFIG(tag, "  [x] Supported swing modes:");
+    for (ClimateSwingMode m : traits.get_supported_swing_modes())
+      ESP_LOGCONFIG(tag, "      - %s", climate_swing_mode_to_string(m));
+  }
+}
+
 }  // namespace climate
 }  // namespace esphome
diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h
index b208e5946a..46d0fb1d77 100644
--- a/esphome/components/climate/climate.h
+++ b/esphome/components/climate/climate.h
@@ -245,6 +245,18 @@ class Climate : public Nameable {
  protected:
   friend ClimateCall;
 
+  /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed.
+  bool set_fan_mode_(ClimateFanMode mode);
+
+  /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed.
+  bool set_custom_fan_mode_(const std::string &mode);
+
+  /// Set preset. Reset custom preset. Return true if preset has been changed.
+  bool set_preset_(ClimatePreset preset);
+
+  /// Set custom preset. Reset primary preset. Return true if preset has been changed.
+  bool set_custom_preset_(const std::string &preset);
+
   /** Get the default traits of this climate device.
    *
    * Traits are static data that encode the capabilities and static data for a climate device such as supported
@@ -270,6 +282,7 @@ class Climate : public Nameable {
   void save_state_();
 
   uint32_t hash_base() override;
+  void dump_traits_(const char *tag);
 
   CallbackManager<void()> state_callback_{};
   ESPPreferenceObject rtc_;
diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h
index 48493b500c..903ce085d8 100644
--- a/esphome/components/climate/climate_traits.h
+++ b/esphome/components/climate/climate_traits.h
@@ -72,6 +72,7 @@ class ClimateTraits {
 
   void set_supported_fan_modes(std::set<ClimateFanMode> modes) { supported_fan_modes_ = std::move(modes); }
   void add_supported_fan_mode(ClimateFanMode mode) { supported_fan_modes_.insert(mode); }
+  void add_supported_custom_fan_mode(const std::string &mode) { supported_custom_fan_modes_.insert(mode); }
   ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
   void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); }
   ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
@@ -104,6 +105,7 @@ class ClimateTraits {
 
   void set_supported_presets(std::set<ClimatePreset> presets) { supported_presets_ = std::move(presets); }
   void add_supported_preset(ClimatePreset preset) { supported_presets_.insert(preset); }
+  void add_supported_custom_preset(const std::string &preset) { supported_custom_presets_.insert(preset); }
   bool supports_preset(ClimatePreset preset) const { return supported_presets_.count(preset); }
   bool get_supports_presets() const { return !supported_presets_.empty(); }
   const std::set<climate::ClimatePreset> &get_supported_presets() const { return supported_presets_; }
diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h
index 8f30750fbd..77a53f11c5 100644
--- a/esphome/components/cover/cover.h
+++ b/esphome/components/cover/cover.h
@@ -125,16 +125,19 @@ class Cover : public Nameable {
    *
    * This is a legacy method and may be removed later, please use `.make_call()` instead.
    */
+  ESPDEPRECATED("open() is deprecated, use make_call().set_command_open() instead.", "2021.9")
   void open();
   /** Close the cover.
    *
    * This is a legacy method and may be removed later, please use `.make_call()` instead.
    */
+  ESPDEPRECATED("close() is deprecated, use make_call().set_command_close() instead.", "2021.9")
   void close();
   /** Stop the cover.
    *
    * This is a legacy method and may be removed later, please use `.make_call()` instead.
    */
+  ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop() instead.", "2021.9")
   void stop();
 
   void add_on_state_callback(std::function<void()> &&f);
diff --git a/esphome/components/demo/__init__.py b/esphome/components/demo/__init__.py
index b3ea47d869..fae8a2b07d 100644
--- a/esphome/components/demo/__init__.py
+++ b/esphome/components/demo/__init__.py
@@ -19,7 +19,6 @@ from esphome.const import (
     CONF_ICON,
     CONF_ID,
     CONF_INVERTED,
-    CONF_LAST_RESET_TYPE,
     CONF_MAX_VALUE,
     CONF_MIN_VALUE,
     CONF_NAME,
@@ -40,8 +39,8 @@ from esphome.const import (
     ICON_BLUR,
     ICON_EMPTY,
     ICON_THERMOMETER,
-    LAST_RESET_TYPE_AUTO,
     STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_TOTAL_INCREASING,
     UNIT_CELSIUS,
     UNIT_EMPTY,
     UNIT_PERCENT,
@@ -336,8 +335,7 @@ CONFIG_SCHEMA = cv.Schema(
                     CONF_UNIT_OF_MEASUREMENT: UNIT_WATT_HOURS,
                     CONF_ACCURACY_DECIMALS: 0,
                     CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
-                    CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
-                    CONF_LAST_RESET_TYPE: LAST_RESET_TYPE_AUTO,
+                    CONF_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING,
                 },
             ],
         ): [
diff --git a/esphome/components/demo/demo_sensor.h b/esphome/components/demo/demo_sensor.h
index 117468793b..344aaf26f8 100644
--- a/esphome/components/demo/demo_sensor.h
+++ b/esphome/components/demo/demo_sensor.h
@@ -11,8 +11,8 @@ class DemoSensor : public sensor::Sensor, public PollingComponent {
  public:
   void update() override {
     float val = random_float();
-    bool is_auto = this->last_reset_type == sensor::LAST_RESET_TYPE_AUTO;
-    if (is_auto) {
+    bool increasing = this->state_class == sensor::STATE_CLASS_TOTAL_INCREASING;
+    if (increasing) {
       float base = isnan(this->state) ? 0.0f : this->state;
       this->publish_state(base + val * 10);
     } else {
diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py
index 84b263c2d5..9d531293e9 100644
--- a/esphome/components/dsmr/sensor.py
+++ b/esphome/components/dsmr/sensor.py
@@ -9,13 +9,17 @@ from esphome.const import (
     DEVICE_CLASS_POWER,
     DEVICE_CLASS_VOLTAGE,
     ICON_EMPTY,
-    LAST_RESET_TYPE_NEVER,
     STATE_CLASS_MEASUREMENT,
     STATE_CLASS_NONE,
+    STATE_CLASS_TOTAL_INCREASING,
     UNIT_AMPERE,
+    UNIT_CUBIC_METER,
     UNIT_EMPTY,
+    UNIT_KILOWATT,
+    UNIT_KILOWATT_HOURS,
+    UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
+    UNIT_KILOVOLT_AMPS_REACTIVE,
     UNIT_VOLT,
-    UNIT_WATT,
 )
 from . import Dsmr, CONF_DSMR_ID
 
@@ -26,70 +30,80 @@ CONFIG_SCHEMA = cv.Schema(
     {
         cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr),
         cv.Optional("energy_delivered_lux"): sensor.sensor_schema(
-            "kWh",
+            UNIT_KILOWATT_HOURS,
             ICON_EMPTY,
             3,
             DEVICE_CLASS_ENERGY,
-            STATE_CLASS_MEASUREMENT,
-            LAST_RESET_TYPE_NEVER,
+            STATE_CLASS_TOTAL_INCREASING,
         ),
         cv.Optional("energy_delivered_tariff1"): sensor.sensor_schema(
-            "kWh",
+            UNIT_KILOWATT_HOURS,
             ICON_EMPTY,
             3,
             DEVICE_CLASS_ENERGY,
-            STATE_CLASS_MEASUREMENT,
-            LAST_RESET_TYPE_NEVER,
+            STATE_CLASS_TOTAL_INCREASING,
         ),
         cv.Optional("energy_delivered_tariff2"): sensor.sensor_schema(
-            "kWh",
+            UNIT_KILOWATT_HOURS,
             ICON_EMPTY,
             3,
             DEVICE_CLASS_ENERGY,
-            STATE_CLASS_MEASUREMENT,
-            LAST_RESET_TYPE_NEVER,
+            STATE_CLASS_TOTAL_INCREASING,
         ),
         cv.Optional("energy_returned_lux"): sensor.sensor_schema(
-            "kWh",
+            UNIT_KILOWATT_HOURS,
             ICON_EMPTY,
             3,
             DEVICE_CLASS_ENERGY,
-            STATE_CLASS_MEASUREMENT,
-            LAST_RESET_TYPE_NEVER,
+            STATE_CLASS_TOTAL_INCREASING,
         ),
         cv.Optional("energy_returned_tariff1"): sensor.sensor_schema(
-            "kWh",
+            UNIT_KILOWATT_HOURS,
             ICON_EMPTY,
             3,
             DEVICE_CLASS_ENERGY,
-            STATE_CLASS_MEASUREMENT,
-            LAST_RESET_TYPE_NEVER,
+            STATE_CLASS_TOTAL_INCREASING,
         ),
         cv.Optional("energy_returned_tariff2"): sensor.sensor_schema(
-            "kWh",
+            UNIT_KILOWATT_HOURS,
             ICON_EMPTY,
             3,
             DEVICE_CLASS_ENERGY,
-            STATE_CLASS_MEASUREMENT,
-            LAST_RESET_TYPE_NEVER,
+            STATE_CLASS_TOTAL_INCREASING,
         ),
         cv.Optional("total_imported_energy"): sensor.sensor_schema(
-            "kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE
+            UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
+            ICON_EMPTY,
+            3,
+            DEVICE_CLASS_ENERGY,
+            STATE_CLASS_NONE,
         ),
         cv.Optional("total_exported_energy"): sensor.sensor_schema(
-            "kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE
+            UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
+            ICON_EMPTY,
+            3,
+            DEVICE_CLASS_ENERGY,
+            STATE_CLASS_NONE,
         ),
         cv.Optional("power_delivered"): sensor.sensor_schema(
-            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+            UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional("power_returned"): sensor.sensor_schema(
-            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+            UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional("reactive_power_delivered"): sensor.sensor_schema(
-            "kvar", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE
+            UNIT_KILOVOLT_AMPS_REACTIVE,
+            ICON_EMPTY,
+            3,
+            DEVICE_CLASS_POWER,
+            STATE_CLASS_MEASUREMENT,
         ),
         cv.Optional("reactive_power_returned"): sensor.sensor_schema(
-            "kvar", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_MEASUREMENT
+            UNIT_KILOVOLT_AMPS_REACTIVE,
+            ICON_EMPTY,
+            3,
+            DEVICE_CLASS_POWER,
+            STATE_CLASS_MEASUREMENT,
         ),
         cv.Optional("electricity_threshold"): sensor.sensor_schema(
             UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
@@ -107,13 +121,13 @@ CONFIG_SCHEMA = cv.Schema(
             UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
         ),
         cv.Optional("electricity_sags_l2"): sensor.sensor_schema(
-            UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
+            UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
         ),
         cv.Optional("electricity_sags_l3"): sensor.sensor_schema(
             UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
         ),
         cv.Optional("electricity_swells_l1"): sensor.sensor_schema(
-            UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
+            UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
         ),
         cv.Optional("electricity_swells_l2"): sensor.sensor_schema(
             UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
@@ -131,40 +145,64 @@ CONFIG_SCHEMA = cv.Schema(
             UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional("power_delivered_l1"): sensor.sensor_schema(
-            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+            UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional("power_delivered_l2"): sensor.sensor_schema(
-            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+            UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional("power_delivered_l3"): sensor.sensor_schema(
-            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+            UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional("power_returned_l1"): sensor.sensor_schema(
-            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+            UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional("power_returned_l2"): sensor.sensor_schema(
-            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+            UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional("power_returned_l3"): sensor.sensor_schema(
-            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+            UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
         ),
         cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema(
-            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+            UNIT_KILOVOLT_AMPS_REACTIVE,
+            ICON_EMPTY,
+            3,
+            DEVICE_CLASS_POWER,
+            STATE_CLASS_MEASUREMENT,
         ),
         cv.Optional("reactive_power_delivered_l2"): sensor.sensor_schema(
-            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+            UNIT_KILOVOLT_AMPS_REACTIVE,
+            ICON_EMPTY,
+            3,
+            DEVICE_CLASS_POWER,
+            STATE_CLASS_MEASUREMENT,
         ),
         cv.Optional("reactive_power_delivered_l3"): sensor.sensor_schema(
-            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+            UNIT_KILOVOLT_AMPS_REACTIVE,
+            ICON_EMPTY,
+            3,
+            DEVICE_CLASS_POWER,
+            STATE_CLASS_MEASUREMENT,
         ),
         cv.Optional("reactive_power_returned_l1"): sensor.sensor_schema(
-            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+            UNIT_KILOVOLT_AMPS_REACTIVE,
+            ICON_EMPTY,
+            3,
+            DEVICE_CLASS_POWER,
+            STATE_CLASS_MEASUREMENT,
         ),
         cv.Optional("reactive_power_returned_l2"): sensor.sensor_schema(
-            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+            UNIT_KILOVOLT_AMPS_REACTIVE,
+            ICON_EMPTY,
+            3,
+            DEVICE_CLASS_POWER,
+            STATE_CLASS_MEASUREMENT,
         ),
         cv.Optional("reactive_power_returned_l3"): sensor.sensor_schema(
-            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+            UNIT_KILOVOLT_AMPS_REACTIVE,
+            ICON_EMPTY,
+            3,
+            DEVICE_CLASS_POWER,
+            STATE_CLASS_MEASUREMENT,
         ),
         cv.Optional("voltage_l1"): sensor.sensor_schema(
             UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE
@@ -176,20 +214,18 @@ CONFIG_SCHEMA = cv.Schema(
             UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE
         ),
         cv.Optional("gas_delivered"): sensor.sensor_schema(
-            "m³",
+            UNIT_CUBIC_METER,
             ICON_EMPTY,
             3,
             DEVICE_CLASS_GAS,
-            STATE_CLASS_MEASUREMENT,
-            LAST_RESET_TYPE_NEVER,
+            STATE_CLASS_TOTAL_INCREASING,
         ),
         cv.Optional("gas_delivered_be"): sensor.sensor_schema(
-            "m³",
+            UNIT_CUBIC_METER,
             ICON_EMPTY,
             3,
             DEVICE_CLASS_GAS,
-            STATE_CLASS_MEASUREMENT,
-            LAST_RESET_TYPE_NEVER,
+            STATE_CLASS_TOTAL_INCREASING,
         ),
     }
 ).extend(cv.COMPONENT_SCHEMA)
diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp
index f280b5bc94..f0f165b25f 100644
--- a/esphome/components/e131/e131_addressable_light_effect.cpp
+++ b/esphome/components/e131/e131_addressable_light_effect.cpp
@@ -84,6 +84,7 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
       break;
   }
 
+  it->schedule_show();
   return true;
 }
 
diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp
index db83eb6bee..f1eebf3c8a 100644
--- a/esphome/components/esp32_ble_server/ble_server.cpp
+++ b/esphome/components/esp32_ble_server/ble_server.cpp
@@ -82,11 +82,9 @@ bool BLEServer::create_device_characteristics_() {
         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
+    model->set_value(ESPHOME_BOARD);
   }
 
   BLECharacteristic *version =
diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py
index 18f1c46ff2..fec0f6dcfb 100644
--- a/esphome/components/esp32_ble_tracker/__init__.py
+++ b/esphome/components/esp32_ble_tracker/__init__.py
@@ -108,6 +108,16 @@ def as_hex(value):
 
 
 def as_hex_array(value):
+    value = value.replace("-", "")
+    cpp_array = [
+        f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)]
+    ]
+    return cg.RawExpression(
+        "(uint8_t*)(const uint8_t[16]){{{}}}".format(",".join(cpp_array))
+    )
+
+
+def as_reversed_hex_array(value):
     value = value.replace("-", "")
     cpp_array = [
         f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)]
@@ -193,7 +203,7 @@ async def to_code(config):
         elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid32_format):
             cg.add(trigger.set_service_uuid32(as_hex(conf[CONF_SERVICE_UUID])))
         elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid128_format):
-            uuid128 = as_hex_array(conf[CONF_SERVICE_UUID])
+            uuid128 = as_reversed_hex_array(conf[CONF_SERVICE_UUID])
             cg.add(trigger.set_service_uuid128(uuid128))
         if CONF_MAC_ADDRESS in conf:
             cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
@@ -205,7 +215,7 @@ async def to_code(config):
         elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid32_format):
             cg.add(trigger.set_manufacturer_uuid32(as_hex(conf[CONF_MANUFACTURER_ID])))
         elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid128_format):
-            uuid128 = as_hex_array(conf[CONF_MANUFACTURER_ID])
+            uuid128 = as_reversed_hex_array(conf[CONF_MANUFACTURER_ID])
             cg.add(trigger.set_manufacturer_uuid128(uuid128))
         if CONF_MAC_ADDRESS in conf:
             cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
index b3db651655..e1cd3975e8 100644
--- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
+++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
@@ -434,6 +434,14 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e
   }
   for (auto &data : this->manufacturer_datas_) {
     ESP_LOGVV(TAG, "  Manufacturer data: %s", hexencode(data.data).c_str());
+    if (this->get_ibeacon().has_value()) {
+      auto ibeacon = this->get_ibeacon().value();
+      ESP_LOGVV(TAG, "    iBeacon data:");
+      ESP_LOGVV(TAG, "      UUID: %s", ibeacon.get_uuid().to_string().c_str());
+      ESP_LOGVV(TAG, "      Major: %u", ibeacon.get_major());
+      ESP_LOGVV(TAG, "      Minor: %u", ibeacon.get_minor());
+      ESP_LOGVV(TAG, "      TXPower: %d", ibeacon.get_signal_power());
+    }
   }
   for (auto &data : this->service_datas_) {
     ESP_LOGVV(TAG, "  Service data:");
diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.cpp b/esphome/components/esp8266_pwm/esp8266_pwm.cpp
index 37a9f3efbf..d725e90edc 100644
--- a/esphome/components/esp8266_pwm/esp8266_pwm.cpp
+++ b/esphome/components/esp8266_pwm/esp8266_pwm.cpp
@@ -1,9 +1,10 @@
 #include "esp8266_pwm.h"
+#include "esphome/core/macros.h"
 #include "esphome/core/log.h"
 #include "esphome/core/helpers.h"
 
-#ifdef ARDUINO_ESP8266_RELEASE_2_3_0
-#error ESP8266 PWM requires at least arduino_core_version 2.4.0
+#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0)
+#error ESP8266 PWM requires at least arduino_version 2.4.0
 #endif
 
 #include <core_esp8266_waveform.h>
diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py
index 8f6e2bece4..110a8d95ed 100644
--- a/esphome/components/external_components/__init__.py
+++ b/esphome/components/external_components/__init__.py
@@ -1,13 +1,12 @@
 import re
 import logging
 from pathlib import Path
-import subprocess
-import hashlib
-import datetime
 
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_COMPONENTS,
+    CONF_REF,
+    CONF_REFRESH,
     CONF_SOURCE,
     CONF_URL,
     CONF_TYPE,
@@ -15,7 +14,7 @@ from esphome.const import (
     CONF_PATH,
 )
 from esphome.core import CORE
-from esphome import loader
+from esphome import git, loader
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -23,19 +22,11 @@ DOMAIN = CONF_EXTERNAL_COMPONENTS
 
 TYPE_GIT = "git"
 TYPE_LOCAL = "local"
-CONF_REFRESH = "refresh"
-CONF_REF = "ref"
-
-
-def validate_git_ref(value):
-    if re.match(r"[a-zA-Z0-9\-_.\./]+", value) is None:
-        raise cv.Invalid("Not a valid git ref")
-    return value
 
 
 GIT_SCHEMA = {
     cv.Required(CONF_URL): cv.url,
-    cv.Optional(CONF_REF): validate_git_ref,
+    cv.Optional(CONF_REF): cv.git_ref,
 }
 LOCAL_SCHEMA = {
     cv.Required(CONF_PATH): cv.directory,
@@ -68,14 +59,6 @@ def validate_source_shorthand(value):
     return SOURCE_SCHEMA(conf)
 
 
-def validate_refresh(value: str):
-    if value.lower() == "always":
-        return validate_refresh("0s")
-    if value.lower() == "never":
-        return validate_refresh("1000y")
-    return cv.positive_time_period_seconds(value)
-
-
 SOURCE_SCHEMA = cv.Any(
     validate_source_shorthand,
     cv.typed_schema(
@@ -90,7 +73,7 @@ SOURCE_SCHEMA = cv.Any(
 CONFIG_SCHEMA = cv.ensure_list(
     {
         cv.Required(CONF_SOURCE): SOURCE_SCHEMA,
-        cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, validate_refresh),
+        cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, cv.source_refresh),
         cv.Optional(CONF_COMPONENTS, default="all"): cv.Any(
             "all", cv.ensure_list(cv.string)
         ),
@@ -102,65 +85,13 @@ async def to_code(config):
     pass
 
 
-def _compute_destination_path(key: str) -> Path:
-    base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN
-    h = hashlib.new("sha256")
-    h.update(key.encode())
-    return base_dir / h.hexdigest()[:8]
-
-
-def _run_git_command(cmd, cwd=None):
-    try:
-        ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False)
-    except FileNotFoundError as err:
-        raise cv.Invalid(
-            "git is not installed but required for external_components.\n"
-            "Please see https://git-scm.com/book/en/v2/Getting-Started-Installing-Git for installing git"
-        ) from err
-
-    if ret.returncode != 0 and ret.stderr:
-        err_str = ret.stderr.decode("utf-8")
-        lines = [x.strip() for x in err_str.splitlines()]
-        if lines[-1].startswith("fatal:"):
-            raise cv.Invalid(lines[-1][len("fatal: ") :])
-        raise cv.Invalid(err_str)
-
-
 def _process_git_config(config: dict, refresh) -> str:
-    key = f"{config[CONF_URL]}@{config.get(CONF_REF)}"
-    repo_dir = _compute_destination_path(key)
-    if not repo_dir.is_dir():
-        _LOGGER.info("Cloning %s", key)
-        _LOGGER.debug("Location: %s", repo_dir)
-        cmd = ["git", "clone", "--depth=1"]
-        if CONF_REF in config:
-            cmd += ["--branch", config[CONF_REF]]
-        cmd += ["--", config[CONF_URL], str(repo_dir)]
-        _run_git_command(cmd)
-
-    else:
-        # Check refresh needed
-        file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD")
-        # On first clone, FETCH_HEAD does not exists
-        if not file_timestamp.exists():
-            file_timestamp = Path(repo_dir / ".git" / "HEAD")
-        age = datetime.datetime.now() - datetime.datetime.fromtimestamp(
-            file_timestamp.stat().st_mtime
-        )
-        if age.total_seconds() > refresh.total_seconds:
-            _LOGGER.info("Updating %s", key)
-            _LOGGER.debug("Location: %s", repo_dir)
-            # Stash local changes (if any)
-            _run_git_command(
-                ["git", "stash", "push", "--include-untracked"], str(repo_dir)
-            )
-            # Fetch remote ref
-            cmd = ["git", "fetch", "--", "origin"]
-            if CONF_REF in config:
-                cmd.append(config[CONF_REF])
-            _run_git_command(cmd, str(repo_dir))
-            # Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch)
-            _run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir))
+    repo_dir = git.clone_or_update(
+        url=config[CONF_URL],
+        ref=config.get(CONF_REF),
+        refresh=refresh,
+        domain=DOMAIN,
+    )
 
     if (repo_dir / "esphome" / "components").is_dir():
         components_dir = repo_dir / "esphome" / "components"
diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py
index 9db2e9ed12..46ff0c2d53 100644
--- a/esphome/components/fan/__init__.py
+++ b/esphome/components/fan/__init__.py
@@ -15,9 +15,11 @@ from esphome.const import (
     CONF_SPEED_COMMAND_TOPIC,
     CONF_SPEED_STATE_TOPIC,
     CONF_NAME,
+    CONF_ON_SPEED_SET,
     CONF_ON_TURN_OFF,
     CONF_ON_TURN_ON,
     CONF_TRIGGER_ID,
+    CONF_DIRECTION,
 )
 from esphome.core import CORE, coroutine_with_priority
 
@@ -27,6 +29,12 @@ fan_ns = cg.esphome_ns.namespace("fan")
 FanState = fan_ns.class_("FanState", cg.Nameable, cg.Component)
 MakeFan = cg.Application.struct("MakeFan")
 
+FanDirection = fan_ns.enum("FanDirection")
+FAN_DIRECTION_ENUM = {
+    "FORWARD": FanDirection.FAN_DIRECTION_FORWARD,
+    "REVERSE": FanDirection.FAN_DIRECTION_REVERSE,
+}
+
 # Actions
 TurnOnAction = fan_ns.class_("TurnOnAction", automation.Action)
 TurnOffAction = fan_ns.class_("TurnOffAction", automation.Action)
@@ -34,6 +42,10 @@ ToggleAction = fan_ns.class_("ToggleAction", automation.Action)
 
 FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template())
 FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template())
+FanSpeedSetTrigger = fan_ns.class_("FanSpeedSetTrigger", automation.Trigger.template())
+
+FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template())
+FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template())
 
 FAN_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
     {
@@ -61,6 +73,11 @@ FAN_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanTurnOffTrigger),
             }
         ),
+        cv.Optional(CONF_ON_SPEED_SET): automation.validate_automation(
+            {
+                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger),
+            }
+        ),
     }
 )
 
@@ -100,6 +117,9 @@ async def setup_fan_core_(var, config):
     for conf in config.get(CONF_ON_TURN_OFF, []):
         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
         await automation.build_automation(trigger, [], conf)
+    for conf in config.get(CONF_ON_SPEED_SET, []):
+        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+        await automation.build_automation(trigger, [], conf)
 
 
 async def register_fan(var, config):
@@ -143,6 +163,9 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args):
             cv.Required(CONF_ID): cv.use_id(FanState),
             cv.Optional(CONF_OSCILLATING): cv.templatable(cv.boolean),
             cv.Optional(CONF_SPEED): cv.templatable(cv.int_range(1)),
+            cv.Optional(CONF_DIRECTION): cv.templatable(
+                cv.enum(FAN_DIRECTION_ENUM, upper=True)
+            ),
         }
     ),
 )
@@ -155,9 +178,35 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args):
     if CONF_SPEED in config:
         template_ = await cg.templatable(config[CONF_SPEED], args, int)
         cg.add(var.set_speed(template_))
+    if CONF_DIRECTION in config:
+        template_ = await cg.templatable(config[CONF_DIRECTION], args, FanDirection)
+        cg.add(var.set_direction(template_))
     return var
 
 
+@automation.register_condition(
+    "fan.is_on",
+    FanIsOnCondition,
+    automation.maybe_simple_id(
+        {
+            cv.Required(CONF_ID): cv.use_id(FanState),
+        }
+    ),
+)
+@automation.register_condition(
+    "fan.is_off",
+    FanIsOffCondition,
+    automation.maybe_simple_id(
+        {
+            cv.Required(CONF_ID): cv.use_id(FanState),
+        }
+    ),
+)
+async def fan_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)
+
+
 @coroutine_with_priority(100.0)
 async def to_code(config):
     cg.add_define("USE_FAN")
diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h
index fbfc71c720..7ff7c720df 100644
--- a/esphome/components/fan/automation.h
+++ b/esphome/components/fan/automation.h
@@ -13,6 +13,7 @@ template<typename... Ts> class TurnOnAction : public Action<Ts...> {
 
   TEMPLATABLE_VALUE(bool, oscillating)
   TEMPLATABLE_VALUE(int, speed)
+  TEMPLATABLE_VALUE(FanDirection, direction)
 
   void play(Ts... x) override {
     auto call = this->state_->turn_on();
@@ -22,6 +23,9 @@ template<typename... Ts> class TurnOnAction : public Action<Ts...> {
     if (this->speed_.has_value()) {
       call.set_speed(this->speed_.value(x...));
     }
+    if (this->direction_.has_value()) {
+      call.set_direction(this->direction_.value(x...));
+    }
     call.perform();
   }
 
@@ -46,6 +50,23 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> {
   FanState *state_;
 };
 
+template<typename... Ts> class FanIsOnCondition : public Condition<Ts...> {
+ public:
+  explicit FanIsOnCondition(FanState *state) : state_(state) {}
+  bool check(Ts... x) override { return this->state_->state; }
+
+ protected:
+  FanState *state_;
+};
+template<typename... Ts> class FanIsOffCondition : public Condition<Ts...> {
+ public:
+  explicit FanIsOffCondition(FanState *state) : state_(state) {}
+  bool check(Ts... x) override { return !this->state_->state; }
+
+ protected:
+  FanState *state_;
+};
+
 class FanTurnOnTrigger : public Trigger<> {
  public:
   FanTurnOnTrigger(FanState *state) {
@@ -82,5 +103,23 @@ class FanTurnOffTrigger : public Trigger<> {
   bool last_on_;
 };
 
+class FanSpeedSetTrigger : public Trigger<> {
+ public:
+  FanSpeedSetTrigger(FanState *state) {
+    state->add_on_state_callback([this, state]() {
+      auto speed = state->speed;
+      auto should_trigger = speed != !this->last_speed_;
+      this->last_speed_ = speed;
+      if (should_trigger) {
+        this->trigger();
+      }
+    });
+    this->last_speed_ = state->speed;
+  }
+
+ protected:
+  int last_speed_;
+};
+
 }  // namespace fan
 }  // namespace esphome
diff --git a/esphome/components/fan/fan_helpers.cpp b/esphome/components/fan/fan_helpers.cpp
index 09be20991b..34883617e6 100644
--- a/esphome/components/fan/fan_helpers.cpp
+++ b/esphome/components/fan/fan_helpers.cpp
@@ -4,6 +4,9 @@
 namespace esphome {
 namespace fan {
 
+// This whole file is deprecated, don't warn about usage of deprecated types in here.
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+
 FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) {
   const auto speed_ratio = static_cast<float>(speed_level) / (supported_speed_levels + 1);
   const auto legacy_level = clamp<int>(static_cast<int>(ceilf(speed_ratio * 3)), 1, 3);
diff --git a/esphome/components/fan/fan_helpers.h b/esphome/components/fan/fan_helpers.h
index 138aa5bca3..009505601e 100644
--- a/esphome/components/fan/fan_helpers.h
+++ b/esphome/components/fan/fan_helpers.h
@@ -4,8 +4,16 @@
 namespace esphome {
 namespace fan {
 
+// Shut-up about usage of deprecated FanSpeed for a bit.
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+
+ESPDEPRECATED("FanSpeed and speed_level_to_enum() are deprecated.", "2021.9")
 FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels);
+ESPDEPRECATED("FanSpeed and speed_enum_to_level() are deprecated.", "2021.9")
 int speed_enum_to_level(FanSpeed speed, int supported_speed_levels);
 
+#pragma GCC diagnostic pop
+
 }  // namespace fan
 }  // namespace esphome
diff --git a/esphome/components/fan/fan_state.cpp b/esphome/components/fan/fan_state.cpp
index 9b4ae53937..a57115beb4 100644
--- a/esphome/components/fan/fan_state.cpp
+++ b/esphome/components/fan/fan_state.cpp
@@ -39,7 +39,7 @@ void FanState::setup() {
   call.set_direction(recovered.direction);
   call.perform();
 }
-float FanState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
+float FanState::get_setup_priority() const { return setup_priority::DATA - 1.0f; }
 uint32_t FanState::hash_base() { return 418001110UL; }
 
 void FanStateCall::perform() const {
@@ -67,6 +67,8 @@ void FanStateCall::perform() const {
   this->state_->state_callback_.call();
 }
 
+// This whole method is deprecated, don't warn about usage of deprecated methods inside of it.
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 FanStateCall &FanStateCall::set_speed(const char *legacy_speed) {
   const auto supported_speed_count = this->state_->get_traits().supported_speed_count();
   if (strcasecmp(legacy_speed, "low") == 0) {
diff --git a/esphome/components/fan/fan_state.h b/esphome/components/fan/fan_state.h
index a0dda4083a..af00275df0 100644
--- a/esphome/components/fan/fan_state.h
+++ b/esphome/components/fan/fan_state.h
@@ -10,7 +10,7 @@ namespace esphome {
 namespace fan {
 
 /// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon
-enum FanSpeed {
+enum ESPDEPRECATED("FanSpeed is deprecated.", "2021.9") FanSpeed {
   FAN_SPEED_LOW = 0,     ///< The fan is running on low speed.
   FAN_SPEED_MEDIUM = 1,  ///< The fan is running on medium speed.
   FAN_SPEED_HIGH = 2     ///< The fan is running on high/full speed.
@@ -45,6 +45,7 @@ class FanStateCall {
     this->speed_ = speed;
     return *this;
   }
+  ESPDEPRECATED("set_speed() with string argument is deprecated, use integer argument instead.", "2021.9")
   FanStateCall &set_speed(const char *legacy_speed);
   FanStateCall &set_direction(FanDirection direction) {
     this->direction_ = direction;
diff --git a/esphome/components/fastled_base/fastled_light.cpp b/esphome/components/fastled_base/fastled_light.cpp
index 4d791f5709..edfeb401f1 100644
--- a/esphome/components/fastled_base/fastled_light.cpp
+++ b/esphome/components/fastled_base/fastled_light.cpp
@@ -20,13 +20,12 @@ void FastLEDLightOutput::dump_config() {
   ESP_LOGCONFIG(TAG, "  Num LEDs: %u", this->num_leds_);
   ESP_LOGCONFIG(TAG, "  Max refresh rate: %u", *this->max_refresh_rate_);
 }
-void FastLEDLightOutput::loop() {
-  if (!this->should_show_())
-    return;
-
-  uint32_t now = micros();
+void FastLEDLightOutput::write_state(light::LightState *state) {
   // protect from refreshing too often
+  uint32_t now = micros();
   if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) {
+    // try again next loop iteration, so that this change won't get lost
+    this->schedule_show();
     return;
   }
   this->last_refresh_ = now;
diff --git a/esphome/components/fastled_base/fastled_light.h b/esphome/components/fastled_base/fastled_light.h
index ac6acc95a5..ee85735dea 100644
--- a/esphome/components/fastled_base/fastled_light.h
+++ b/esphome/components/fastled_base/fastled_light.h
@@ -213,7 +213,7 @@ class FastLEDLightOutput : public light::AddressableLight {
   }
   void setup() override;
   void dump_config() override;
-  void loop() override;
+  void write_state(light::LightState *state) override;
   float get_setup_priority() const override { return setup_priority::HARDWARE; }
 
   void clear_effect_data() override {
diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp
index 8d789bbcfc..9e58f672c7 100644
--- a/esphome/components/fujitsu_general/fujitsu_general.cpp
+++ b/esphome/components/fujitsu_general/fujitsu_general.cpp
@@ -297,12 +297,6 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) {
     }
   }
 
-  // Validate footer
-  if (!data.expect_mark(FUJITSU_GENERAL_BIT_MARK)) {
-    ESP_LOGV(TAG, "Footer fail");
-    return false;
-  }
-
   for (uint8_t byte = 0; byte < recv_message_length; ++byte) {
     ESP_LOGVV(TAG, "%02X", recv_message[byte]);
   }
diff --git a/esphome/components/havells_solar/sensor.py b/esphome/components/havells_solar/sensor.py
index 1d685b9b2e..3ec12d5b83 100644
--- a/esphome/components/havells_solar/sensor.py
+++ b/esphome/components/havells_solar/sensor.py
@@ -13,9 +13,8 @@ from esphome.const import (
     DEVICE_CLASS_POWER,
     DEVICE_CLASS_VOLTAGE,
     ICON_CURRENT_AC,
-    LAST_RESET_TYPE_AUTO,
     STATE_CLASS_MEASUREMENT,
-    STATE_CLASS_NONE,
+    STATE_CLASS_TOTAL_INCREASING,
     UNIT_AMPERE,
     UNIT_DEGREES,
     UNIT_HERTZ,
@@ -143,25 +142,23 @@ CONFIG_SCHEMA = (
                 unit_of_measurement=UNIT_KILOWATT_HOURS,
                 accuracy_decimals=2,
                 device_class=DEVICE_CLASS_ENERGY,
-                state_class=STATE_CLASS_MEASUREMENT,
-                last_reset_type=LAST_RESET_TYPE_AUTO,
+                state_class=STATE_CLASS_TOTAL_INCREASING,
             ),
             cv.Optional(CONF_TOTAL_ENERGY_PRODUCTION): sensor.sensor_schema(
                 unit_of_measurement=UNIT_KILOWATT_HOURS,
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_ENERGY,
-                state_class=STATE_CLASS_MEASUREMENT,
-                last_reset_type=LAST_RESET_TYPE_AUTO,
+                state_class=STATE_CLASS_TOTAL_INCREASING,
             ),
             cv.Optional(CONF_TOTAL_GENERATION_TIME): sensor.sensor_schema(
                 unit_of_measurement=UNIT_HOURS,
                 accuracy_decimals=0,
-                state_class=STATE_CLASS_NONE,
+                state_class=STATE_CLASS_TOTAL_INCREASING,
             ),
             cv.Optional(CONF_TODAY_GENERATION_TIME): sensor.sensor_schema(
                 unit_of_measurement=UNIT_MINUTE,
                 accuracy_decimals=0,
-                state_class=STATE_CLASS_NONE,
+                state_class=STATE_CLASS_TOTAL_INCREASING,
             ),
             cv.Optional(CONF_INVERTER_MODULE_TEMP): sensor.sensor_schema(
                 unit_of_measurement=UNIT_DEGREES,
diff --git a/esphome/components/hbridge/__init__.py b/esphome/components/hbridge/__init__.py
index e69de29bb2..7eae863ff5 100644
--- a/esphome/components/hbridge/__init__.py
+++ b/esphome/components/hbridge/__init__.py
@@ -0,0 +1,3 @@
+import esphome.codegen as cg
+
+hbridge_ns = cg.esphome_ns.namespace("hbridge")
diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py
new file mode 100644
index 0000000000..b169978acd
--- /dev/null
+++ b/esphome/components/hbridge/fan/__init__.py
@@ -0,0 +1,70 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import automation
+from esphome.automation import maybe_simple_id
+from esphome.components import fan, output
+from esphome.const import (
+    CONF_ID,
+    CONF_DECAY_MODE,
+    CONF_SPEED_COUNT,
+    CONF_PIN_A,
+    CONF_PIN_B,
+    CONF_ENABLE_PIN,
+)
+from .. import hbridge_ns
+
+
+CODEOWNERS = ["@WeekendWarrior"]
+
+
+HBridgeFan = hbridge_ns.class_("HBridgeFan", fan.FanState)
+
+DecayMode = hbridge_ns.enum("DecayMode")
+DECAY_MODE_OPTIONS = {
+    "SLOW": DecayMode.DECAY_MODE_SLOW,
+    "FAST": DecayMode.DECAY_MODE_FAST,
+}
+
+# Actions
+BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action)
+
+
+CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
+    {
+        cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan),
+        cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput),
+        cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput),
+        cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
+            DECAY_MODE_OPTIONS, upper=True
+        ),
+        cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
+        cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+@automation.register_action(
+    "fan.hbridge.brake",
+    BrakeAction,
+    maybe_simple_id({cv.Required(CONF_ID): cv.use_id(HBridgeFan)}),
+)
+async def fan_hbridge_brake_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)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(
+        config[CONF_ID],
+        config[CONF_SPEED_COUNT],
+        config[CONF_DECAY_MODE],
+    )
+    await fan.register_fan(var, config)
+    pin_a_ = await cg.get_variable(config[CONF_PIN_A])
+    cg.add(var.set_pin_a(pin_a_))
+    pin_b_ = await cg.get_variable(config[CONF_PIN_B])
+    cg.add(var.set_pin_b(pin_b_))
+
+    if CONF_ENABLE_PIN in config:
+        enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN])
+        cg.add(var.set_enable_pin(enable_pin))
diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp
new file mode 100644
index 0000000000..a4e5429ff4
--- /dev/null
+++ b/esphome/components/hbridge/fan/hbridge_fan.cpp
@@ -0,0 +1,85 @@
+#include "hbridge_fan.h"
+#include "esphome/components/fan/fan_helpers.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace hbridge {
+
+static const char *const TAG = "fan.hbridge";
+
+void HBridgeFan::set_hbridge_levels_(float a_level, float b_level) {
+  this->pin_a_->set_level(a_level);
+  this->pin_b_->set_level(b_level);
+  ESP_LOGD(TAG, "Setting speed: a: %.2f, b: %.2f", a_level, b_level);
+}
+
+// constant IN1/IN2, PWM on EN => power control, fast current decay
+// constant IN1/EN, PWM on IN2 => power control, slow current decay
+void HBridgeFan::set_hbridge_levels_(float a_level, float b_level, float enable) {
+  this->pin_a_->set_level(a_level);
+  this->pin_b_->set_level(b_level);
+  this->enable_->set_level(enable);
+  ESP_LOGD(TAG, "Setting speed: a: %.2f, b: %.2f, enable: %.2f", a_level, b_level, enable);
+}
+
+fan::FanStateCall HBridgeFan::brake() {
+  ESP_LOGD(TAG, "Braking");
+  (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f, 1.0f) : this->set_hbridge_levels_(1.0f, 1.0f, 1.0f);
+  return this->make_call().set_state(false);
+}
+
+void HBridgeFan::dump_config() {
+  ESP_LOGCONFIG(TAG, "Fan '%s':", this->get_name().c_str());
+  if (this->get_traits().supports_oscillation()) {
+    ESP_LOGCONFIG(TAG, "  Oscillation: YES");
+  }
+  if (this->get_traits().supports_direction()) {
+    ESP_LOGCONFIG(TAG, "  Direction: YES");
+  }
+  if (this->decay_mode_ == DECAY_MODE_SLOW) {
+    ESP_LOGCONFIG(TAG, "  Decay Mode: Slow");
+  } else {
+    ESP_LOGCONFIG(TAG, "  Decay Mode: Fast");
+  }
+}
+void HBridgeFan::setup() {
+  auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_);
+  this->set_traits(traits);
+  this->add_on_state_callback([this]() { this->next_update_ = true; });
+}
+void HBridgeFan::loop() {
+  if (!this->next_update_) {
+    return;
+  }
+  this->next_update_ = false;
+
+  float speed = 0.0f;
+  if (this->state) {
+    speed = static_cast<float>(this->speed) / static_cast<float>(this->speed_count_);
+  }
+  if (speed == 0.0f) {  // off means idle
+    (this->enable_ == nullptr) ? this->set_hbridge_levels_(speed, speed)
+                               : this->set_hbridge_levels_(speed, speed, speed);
+    return;
+  }
+  if (this->direction == fan::FAN_DIRECTION_FORWARD) {
+    if (this->decay_mode_ == DECAY_MODE_SLOW) {
+      (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f - speed, 1.0f)
+                                 : this->set_hbridge_levels_(1.0f - speed, 1.0f, 1.0f);
+    } else {  // DECAY_MODE_FAST
+      (this->enable_ == nullptr) ? this->set_hbridge_levels_(0.0f, speed)
+                                 : this->set_hbridge_levels_(0.0f, 1.0f, speed);
+    }
+  } else {  // fan::FAN_DIRECTION_REVERSE
+    if (this->decay_mode_ == DECAY_MODE_SLOW) {
+      (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f, 1.0f - speed)
+                                 : this->set_hbridge_levels_(1.0f, 1.0f - speed, 1.0f);
+    } else {  // DECAY_MODE_FAST
+      (this->enable_ == nullptr) ? this->set_hbridge_levels_(speed, 0.0f)
+                                 : this->set_hbridge_levels_(1.0f, 0.0f, speed);
+    }
+  }
+}
+
+}  // namespace hbridge
+}  // namespace esphome
diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h
new file mode 100644
index 0000000000..984318c8d6
--- /dev/null
+++ b/esphome/components/hbridge/fan/hbridge_fan.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include "esphome/core/automation.h"
+#include "esphome/components/output/binary_output.h"
+#include "esphome/components/output/float_output.h"
+#include "esphome/components/fan/fan_state.h"
+
+namespace esphome {
+namespace hbridge {
+
+enum DecayMode {
+  DECAY_MODE_SLOW = 0,
+  DECAY_MODE_FAST = 1,
+};
+
+class HBridgeFan : public fan::FanState {
+ public:
+  HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
+
+  void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
+  void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
+  void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; }
+
+  void setup() override;
+  void loop() override;
+  void dump_config() override;
+  float get_setup_priority() const override { return setup_priority::HARDWARE; }
+
+  fan::FanStateCall brake();
+
+  int get_speed_count() { return this->speed_count_; }
+  // update Hbridge without a triggered FanState change, eg. for acceleration/deceleration ramping
+  void internal_update() { this->next_update_ = true; }
+
+ protected:
+  output::FloatOutput *pin_a_;
+  output::FloatOutput *pin_b_;
+  output::FloatOutput *enable_{nullptr};
+  output::BinaryOutput *oscillating_{nullptr};
+  bool next_update_{true};
+  int speed_count_{};
+  DecayMode decay_mode_{DECAY_MODE_SLOW};
+
+  void set_hbridge_levels_(float a_level, float b_level);
+  void set_hbridge_levels_(float a_level, float b_level, float enable);
+};
+
+template<typename... Ts> class BrakeAction : public Action<Ts...> {
+ public:
+  explicit BrakeAction(HBridgeFan *parent) : parent_(parent) {}
+
+  void play(Ts... x) override { this->parent_->brake(); }
+
+  HBridgeFan *parent_;
+};
+
+}  // namespace hbridge
+}  // namespace esphome
diff --git a/esphome/components/hbridge/light.py b/esphome/components/hbridge/light/__init__.py
similarity index 94%
rename from esphome/components/hbridge/light.py
rename to esphome/components/hbridge/light/__init__.py
index b4ae45977a..fe5c3e9845 100644
--- a/esphome/components/hbridge/light.py
+++ b/esphome/components/hbridge/light/__init__.py
@@ -2,8 +2,10 @@ import esphome.codegen as cg
 import esphome.config_validation as cv
 from esphome.components import light, output
 from esphome.const import CONF_OUTPUT_ID, CONF_PIN_A, CONF_PIN_B
+from .. import hbridge_ns
+
+CODEOWNERS = ["@DotNetDann"]
 
-hbridge_ns = cg.esphome_ns.namespace("hbridge")
 HBridgeLightOutput = hbridge_ns.class_(
     "HBridgeLightOutput", cg.PollingComponent, light.LightOutput
 )
diff --git a/esphome/components/hbridge/hbridge_light_output.h b/esphome/components/hbridge/light/hbridge_light_output.h
similarity index 100%
rename from esphome/components/hbridge/hbridge_light_output.h
rename to esphome/components/hbridge/light/hbridge_light_output.h
diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py
index 75590f8572..11e9c8e4d4 100644
--- a/esphome/components/hlw8012/sensor.py
+++ b/esphome/components/hlw8012/sensor.py
@@ -18,8 +18,8 @@ from esphome.const import (
     DEVICE_CLASS_ENERGY,
     DEVICE_CLASS_POWER,
     DEVICE_CLASS_VOLTAGE,
-    LAST_RESET_TYPE_AUTO,
     STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_TOTAL_INCREASING,
     UNIT_VOLT,
     UNIT_AMPERE,
     UNIT_WATT,
@@ -78,8 +78,7 @@ CONFIG_SCHEMA = cv.Schema(
             unit_of_measurement=UNIT_WATT_HOURS,
             accuracy_decimals=1,
             device_class=DEVICE_CLASS_ENERGY,
-            state_class=STATE_CLASS_MEASUREMENT,
-            last_reset_type=LAST_RESET_TYPE_AUTO,
+            state_class=STATE_CLASS_TOTAL_INCREASING,
         ),
         cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance,
         cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float,
diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py
index fe1c6008d4..7cd81fec1d 100644
--- a/esphome/components/hm3301/sensor.py
+++ b/esphome/components/hm3301/sensor.py
@@ -6,6 +6,10 @@ from esphome.const import (
     CONF_PM_2_5,
     CONF_PM_10_0,
     CONF_PM_1_0,
+    DEVICE_CLASS_AQI,
+    DEVICE_CLASS_PM1,
+    DEVICE_CLASS_PM10,
+    DEVICE_CLASS_PM25,
     STATE_CLASS_MEASUREMENT,
     UNIT_MICROGRAMS_PER_CUBIC_METER,
     ICON_CHEMICAL_WEAPON,
@@ -45,24 +49,28 @@ CONFIG_SCHEMA = cv.All(
                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
                 icon=ICON_CHEMICAL_WEAPON,
                 accuracy_decimals=0,
+                device_class=DEVICE_CLASS_PM1,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
                 icon=ICON_CHEMICAL_WEAPON,
                 accuracy_decimals=0,
+                device_class=DEVICE_CLASS_PM25,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
                 icon=ICON_CHEMICAL_WEAPON,
                 accuracy_decimals=0,
+                device_class=DEVICE_CLASS_PM10,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_AQI): sensor.sensor_schema(
                 unit_of_measurement=UNIT_INDEX,
                 icon=ICON_CHEMICAL_WEAPON,
                 accuracy_decimals=0,
+                device_class=DEVICE_CLASS_AQI,
                 state_class=STATE_CLASS_MEASUREMENT,
             ).extend(
                 {
diff --git a/esphome/components/ili9341/display.py b/esphome/components/ili9341/display.py
index 450a958c56..157e8212bd 100644
--- a/esphome/components/ili9341/display.py
+++ b/esphome/components/ili9341/display.py
@@ -42,7 +42,7 @@ CONFIG_SCHEMA = cv.All(
         }
     )
     .extend(cv.polling_component_schema("1s"))
-    .extend(spi.spi_device_schema()),
+    .extend(spi.spi_device_schema(False)),
     cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
 )
 
diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py
index 52e8984545..69cb87e539 100644
--- a/esphome/components/light/__init__.py
+++ b/esphome/components/light/__init__.py
@@ -7,6 +7,7 @@ from esphome.const import (
     CONF_DEFAULT_TRANSITION_LENGTH,
     CONF_DISABLED_BY_DEFAULT,
     CONF_EFFECTS,
+    CONF_FLASH_TRANSITION_LENGTH,
     CONF_GAMMA_CORRECT,
     CONF_ID,
     CONF_INTERNAL,
@@ -85,6 +86,9 @@ BRIGHTNESS_ONLY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend(
         cv.Optional(
             CONF_DEFAULT_TRANSITION_LENGTH, default="1s"
         ): cv.positive_time_period_milliseconds,
+        cv.Optional(
+            CONF_FLASH_TRANSITION_LENGTH, default="0s"
+        ): cv.positive_time_period_milliseconds,
         cv.Optional(CONF_EFFECTS): validate_effects(MONOCHROMATIC_EFFECTS),
     }
 )
@@ -132,6 +136,10 @@ async def setup_light_core_(light_var, output_var, config):
                 config[CONF_DEFAULT_TRANSITION_LENGTH]
             )
         )
+    if CONF_FLASH_TRANSITION_LENGTH in config:
+        cg.add(
+            light_var.set_flash_transition_length(config[CONF_FLASH_TRANSITION_LENGTH])
+        )
     if CONF_GAMMA_CORRECT in config:
         cg.add(light_var.set_gamma_correct(config[CONF_GAMMA_CORRECT]))
     effects = await cg.build_registry_list(
diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp
index 80a1e14ffd..a8fa2cd7ac 100644
--- a/esphome/components/light/addressable_light.cpp
+++ b/esphome/components/light/addressable_light.cpp
@@ -12,14 +12,13 @@ void AddressableLight::call_setup() {
 #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
   this->set_interval(5000, [this]() {
     const char *name = this->state_parent_ == nullptr ? "" : this->state_parent_->get_name().c_str();
-    ESP_LOGVV(TAG, "Addressable Light '%s' (effect_active=%s next_show=%s)", name, YESNO(this->effect_active_),
-              YESNO(this->next_show_));
+    ESP_LOGVV(TAG, "Addressable Light '%s' (effect_active=%s)", name, YESNO(this->effect_active_));
     for (int i = 0; i < this->size(); i++) {
       auto color = this->get(i);
       ESP_LOGVV(TAG, "  [%2d] Color: R=%3u G=%3u B=%3u W=%3u", i, color.get_red_raw(), color.get_green_raw(),
                 color.get_blue_raw(), color.get_white_raw());
     }
-    ESP_LOGVV(TAG, "");
+    ESP_LOGVV(TAG, " ");
   });
 #endif
 }
@@ -36,7 +35,7 @@ Color esp_color_from_light_color_values(LightColorValues val) {
   return Color(r, g, b, w);
 }
 
-void AddressableLight::write_state(LightState *state) {
+void AddressableLight::update_state(LightState *state) {
   auto val = state->current_values;
   auto max_brightness = to_uint8_scale(val.get_brightness() * val.get_state());
   this->correction_.set_local_brightness(max_brightness);
diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h
index ab1efdf160..bba2158457 100644
--- a/esphome/components/light/addressable_light.h
+++ b/esphome/components/light/addressable_light.h
@@ -51,9 +51,9 @@ class AddressableLight : public LightOutput, public Component {
       amnt = this->size();
     this->range(amnt, this->size()) = this->range(0, -amnt);
   }
+  // Indicates whether an effect that directly updates the output buffer is active to prevent overwriting
   bool is_effect_active() const { return this->effect_active_; }
   void set_effect_active(bool effect_active) { this->effect_active_ = effect_active; }
-  void write_state(LightState *state) override;
   std::unique_ptr<LightTransformer> create_default_transition() override;
   void set_correction(float red, float green, float blue, float white = 1.0f) {
     this->correction_.set_max_brightness(
@@ -63,7 +63,8 @@ class AddressableLight : public LightOutput, public Component {
     this->correction_.calculate_gamma_table(state->get_gamma_correct());
     this->state_parent_ = state;
   }
-  void schedule_show() { this->next_show_ = true; }
+  void update_state(LightState *state) override;
+  void schedule_show() { this->state_parent_->next_write_ = true; }
 
 #ifdef USE_POWER_SUPPLY
   void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_.set_parent(power_supply); }
@@ -74,9 +75,7 @@ class AddressableLight : public LightOutput, public Component {
  protected:
   friend class AddressableLightTransformer;
 
-  bool should_show_() const { return this->effect_active_ || this->next_show_; }
   void mark_shown_() {
-    this->next_show_ = false;
 #ifdef USE_POWER_SUPPLY
     for (auto c : *this) {
       if (c.get().is_on()) {
@@ -90,7 +89,6 @@ class AddressableLight : public LightOutput, public Component {
   virtual ESPColorView get_view_internal(int32_t index) const = 0;
 
   bool effect_active_{false};
-  bool next_show_{true};
   ESPColorCorrection correction_{};
 #ifdef USE_POWER_SUPPLY
   power_supply::PowerSupplyRequester power_;
diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h
index 3a2ba66845..1cb29dfa4e 100644
--- a/esphome/components/light/addressable_light_effect.h
+++ b/esphome/components/light/addressable_light_effect.h
@@ -63,6 +63,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect {
       this->last_run_ = now;
       this->f_(it, current_color, this->initial_run_);
       this->initial_run_ = false;
+      it.schedule_show();
     }
   }
 
@@ -87,6 +88,7 @@ class AddressableRainbowLightEffect : public AddressableLightEffect {
       var = hsv;
       hue += add;
     }
+    it.schedule_show();
   }
   void set_speed(uint32_t speed) { this->speed_ = speed; }
   void set_width(uint16_t width) { this->width_ = width; }
@@ -134,6 +136,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect {
         new_color.b = c.b;
       }
     }
+    it.schedule_show();
   }
 
  protected:
@@ -151,25 +154,27 @@ class AddressableScanEffect : public AddressableLightEffect {
   void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; }
   void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; }
   void apply(AddressableLight &it, const Color &current_color) override {
-    it.all() = Color::BLACK;
+    const uint32_t now = millis();
+    if (now - this->last_move_ < this->move_interval_)
+      return;
 
+    if (direction_) {
+      this->at_led_++;
+      if (this->at_led_ == it.size() - this->scan_width_)
+        this->direction_ = false;
+    } else {
+      this->at_led_--;
+      if (this->at_led_ == 0)
+        this->direction_ = true;
+    }
+    this->last_move_ = now;
+
+    it.all() = Color::BLACK;
     for (auto i = 0; i < this->scan_width_; i++) {
       it[this->at_led_ + i] = current_color;
     }
 
-    const uint32_t now = millis();
-    if (now - this->last_move_ > this->move_interval_) {
-      if (direction_) {
-        this->at_led_++;
-        if (this->at_led_ == it.size() - this->scan_width_)
-          this->direction_ = false;
-      } else {
-        this->at_led_--;
-        if (this->at_led_ == 0)
-          this->direction_ = true;
-      }
-      this->last_move_ = now;
-    }
+    it.schedule_show();
   }
 
  protected:
@@ -210,6 +215,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect {
         continue;
       addressable[pos].set_effect_data(1);
     }
+    addressable.schedule_show();
   }
   void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; }
   void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; }
@@ -257,6 +263,7 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect {
       const uint8_t color = random_uint32() & 0b111;
       it[pos].set_effect_data(0b1000 | color);
     }
+    it.schedule_show();
   }
   void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; }
   void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; }
@@ -301,6 +308,7 @@ class AddressableFireworksEffect : public AddressableLightEffect {
         it[pos] = current_color;
       }
     }
+    it.schedule_show();
   }
   void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
   void set_spark_probability(float spark_probability) { this->spark_probability_ = spark_probability; }
@@ -335,6 +343,7 @@ class AddressableFlickerEffect : public AddressableLightEffect {
       // slowly fade back to "real" value
       var = (var.get() * inv_intensity) + (current_color * intensity);
     }
+    it.schedule_show();
   }
   void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
   void set_intensity(float intensity) { this->intensity_ = to_uint8_scale(intensity); }
diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h
index f66b90f665..5ab9f66ce4 100644
--- a/esphome/components/light/base_light_effects.h
+++ b/esphome/components/light/base_light_effects.h
@@ -156,7 +156,7 @@ class StrobeLightEffect : public LightEffect {
 
     if (!color.is_on()) {
       // Don't turn the light off, otherwise the light effect will be stopped
-      call.set_brightness_if_supported(0.0f);
+      call.set_brightness(0.0f);
       call.set_state(true);
     }
     call.set_publish(false);
@@ -196,7 +196,6 @@ class FlickerLightEffect : public LightEffect {
     out.set_warm_white(remote.get_warm_white() * beta + current.get_warm_white() * alpha +
                        (random_cubic_float() * this->intensity_));
 
-    auto traits = this->state_->get_traits();
     auto call = this->state_->make_call();
     call.set_publish(false);
     call.set_save(false);
diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h
index 0f5b7b4b93..77c377d39e 100644
--- a/esphome/components/light/color_mode.h
+++ b/esphome/components/light/color_mode.h
@@ -52,7 +52,7 @@ enum class ColorMode : uint8_t {
   /// Only on/off control.
   ON_OFF = (uint8_t) ColorCapability::ON_OFF,
   /// Dimmable light.
-  BRIGHTNESS = (uint8_t) ColorCapability::BRIGHTNESS,
+  BRIGHTNESS = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS),
   /// White output only (use only if the light also has another color mode such as RGB).
   WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::WHITE),
   /// Controllable color temperature output.
diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp
index 6945d37ded..d979b13368 100644
--- a/esphome/components/light/light_call.cpp
+++ b/esphome/components/light/light_call.cpp
@@ -8,26 +8,23 @@ namespace light {
 static const char *const TAG = "light";
 
 static const char *color_mode_to_human(ColorMode color_mode) {
-  switch (color_mode) {
-    case ColorMode::UNKNOWN:
-      return "Unknown";
-    case ColorMode::WHITE:
-      return "White";
-    case ColorMode::COLOR_TEMPERATURE:
-      return "Color temperature";
-    case ColorMode::COLD_WARM_WHITE:
-      return "Cold/warm white";
-    case ColorMode::RGB:
-      return "RGB";
-    case ColorMode::RGB_WHITE:
-      return "RGBW";
-    case ColorMode::RGB_COLD_WARM_WHITE:
-      return "RGB + cold/warm white";
-    case ColorMode::RGB_COLOR_TEMPERATURE:
-      return "RGB + color temperature";
-    default:
-      return "";
-  }
+  if (color_mode == ColorMode::UNKNOWN)
+    return "Unknown";
+  if (color_mode == ColorMode::WHITE)
+    return "White";
+  if (color_mode == ColorMode::COLOR_TEMPERATURE)
+    return "Color temperature";
+  if (color_mode == ColorMode::COLD_WARM_WHITE)
+    return "Cold/warm white";
+  if (color_mode == ColorMode::RGB)
+    return "RGB";
+  if (color_mode == ColorMode::RGB_WHITE)
+    return "RGBW";
+  if (color_mode == ColorMode::RGB_COLD_WARM_WHITE)
+    return "RGB + cold/warm white";
+  if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE)
+    return "RGB + color temperature";
+  return "";
 }
 
 void LightCall::perform() {
diff --git a/esphome/components/light/light_output.h b/esphome/components/light/light_output.h
index 7568ea6831..73ba0371cd 100644
--- a/esphome/components/light/light_output.h
+++ b/esphome/components/light/light_output.h
@@ -19,6 +19,13 @@ class LightOutput {
 
   virtual void setup_state(LightState *state) {}
 
+  /// Called on every update of the current values of the associated LightState,
+  /// can optionally be used to do processing of this change.
+  virtual void update_state(LightState *state) {}
+
+  /// Called from loop() every time the light state has changed, and should
+  /// should write the new state to hardware. Every call to write_state() is
+  /// preceded by (at least) one call to update_state().
   virtual void write_state(LightState *state) = 0;
 };
 
diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp
index 030cf4b7a2..945d3910d5 100644
--- a/esphome/components/light/light_state.cpp
+++ b/esphome/components/light/light_state.cpp
@@ -114,9 +114,11 @@ void LightState::loop() {
   // Apply transformer (if any)
   if (this->transformer_ != nullptr) {
     auto values = this->transformer_->apply();
-    this->next_write_ = values.has_value();  // don't write if transformer doesn't want us to
-    if (values.has_value())
+    if (values.has_value()) {
       this->current_values = *values;
+      this->output_->update_state(this);
+      this->next_write_ = true;
+    }
 
     if (this->transformer_->is_finished()) {
       this->transformer_->stop();
@@ -127,18 +129,15 @@ void LightState::loop() {
 
   // Write state to the light
   if (this->next_write_) {
-    this->output_->write_state(this);
     this->next_write_ = false;
+    this->output_->write_state(this);
   }
 }
 
 float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
 uint32_t LightState::hash_base() { return 1114400283; }
 
-void LightState::publish_state() {
-  this->remote_values_callback_.call();
-  this->next_write_ = true;
-}
+void LightState::publish_state() { this->remote_values_callback_.call(); }
 
 LightOutput *LightState::get_output() const { return this->output_; }
 std::string LightState::get_effect_name() {
@@ -158,6 +157,11 @@ void LightState::add_new_target_state_reached_callback(std::function<void()> &&s
 void LightState::set_default_transition_length(uint32_t default_transition_length) {
   this->default_transition_length_ = default_transition_length;
 }
+uint32_t LightState::get_default_transition_length() const { return this->default_transition_length_; }
+void LightState::set_flash_transition_length(uint32_t flash_transition_length) {
+  this->flash_transition_length_ = flash_transition_length;
+}
+uint32_t LightState::get_flash_transition_length() const { return this->flash_transition_length_; }
 void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; }
 void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
 bool LightState::supports_effects() { return !this->effects_.empty(); }
@@ -235,7 +239,7 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length) {
   // If starting a flash if one is already happening, set end values to end values of current flash
   // Hacky but works
   if (this->transformer_ != nullptr)
-    end_colors = this->transformer_->get_target_values();
+    end_colors = this->transformer_->get_start_values();
 
   this->transformer_ = make_unique<LightFlashTransformer>(*this);
   this->transformer_->setup(end_colors, target, length);
@@ -248,6 +252,7 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot
   if (set_remote_values) {
     this->remote_values = target;
   }
+  this->output_->update_state(this);
   this->next_write_ = true;
 }
 
diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h
index dfea9a15f4..dd42aa76db 100644
--- a/esphome/components/light/light_state.h
+++ b/esphome/components/light/light_state.h
@@ -99,6 +99,11 @@ class LightState : public Nameable, public Component {
 
   /// Set the default transition length, i.e. the transition length when no transition is provided.
   void set_default_transition_length(uint32_t default_transition_length);
+  uint32_t get_default_transition_length() const;
+
+  /// Set the flash transition length
+  void set_flash_transition_length(uint32_t flash_transition_length);
+  uint32_t get_flash_transition_length() const;
 
   /// Set the gamma correction factor
   void set_gamma_correct(float gamma_correct);
@@ -188,6 +193,8 @@ class LightState : public Nameable, public Component {
 
   /// Default transition length for all transitions in ms.
   uint32_t default_transition_length_{};
+  /// Transition length to use for flash transitions.
+  uint32_t flash_transition_length_{};
   /// Gamma correction factor for the light.
   float gamma_correct_{};
   /// Restore mode of the light.
diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h
index fd0bfd20f3..d501d53f72 100644
--- a/esphome/components/light/transformers.h
+++ b/esphome/components/light/transformers.h
@@ -58,7 +58,43 @@ class LightFlashTransformer : public LightTransformer {
  public:
   LightFlashTransformer(LightState &state) : state_(state) {}
 
-  optional<LightColorValues> apply() override { return this->get_target_values(); }
+  void start() override {
+    this->transition_length_ = this->state_.get_flash_transition_length();
+    if (this->transition_length_ * 2 > this->length_)
+      this->transition_length_ = this->length_ / 2;
+
+    // do not create transition if length is 0
+    if (this->transition_length_ == 0)
+      return;
+
+    // first transition to original target
+    this->transformer_ = this->state_.get_output()->create_default_transition();
+    this->transformer_->setup(this->state_.current_values, this->target_values_, this->transition_length_);
+  }
+
+  optional<LightColorValues> apply() override {
+    // transition transformer does not handle 0 length as progress returns nan
+    if (this->transition_length_ == 0)
+      return this->target_values_;
+
+    if (this->transformer_ != nullptr) {
+      if (!this->transformer_->is_finished()) {
+        return this->transformer_->apply();
+      } else {
+        this->transformer_->stop();
+        this->transformer_ = nullptr;
+      }
+    }
+
+    if (millis() > this->start_time_ + this->length_ - this->transition_length_) {
+      // second transition back to start value
+      this->transformer_ = this->state_.get_output()->create_default_transition();
+      this->transformer_->setup(this->state_.current_values, this->get_start_values(), this->transition_length_);
+    }
+
+    // once transition is complete, don't change states until next transition
+    return optional<LightColorValues>();
+  }
 
   // Restore the original values after the flash.
   void stop() override {
@@ -69,6 +105,8 @@ class LightFlashTransformer : public LightTransformer {
 
  protected:
   LightState &state_;
+  uint32_t transition_length_;
+  std::unique_ptr<LightTransformer> transformer_{nullptr};
 };
 
 }  // namespace light
diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp
index ce82a51b94..9d79037087 100644
--- a/esphome/components/logger/logger.cpp
+++ b/esphome/components/logger/logger.cpp
@@ -43,21 +43,24 @@ void Logger::write_header_(int level, const char *tag, int line) {
 }
 
 void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) {  // NOLINT
-  if (level > this->level_for(tag))
+  if (level > this->level_for(tag) || recursion_guard_)
     return;
 
+  recursion_guard_ = true;
   this->reset_buffer_();
   this->write_header_(level, tag, line);
   this->vprintf_to_buffer_(format, args);
   this->write_footer_();
   this->log_message_(level, tag);
+  recursion_guard_ = false;
 }
 #ifdef USE_STORE_LOG_STR_IN_FLASH
 void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format,
                           va_list args) {  // NOLINT
-  if (level > this->level_for(tag))
+  if (level > this->level_for(tag) || recursion_guard_)
     return;
 
+  recursion_guard_ = true;
   this->reset_buffer_();
   // copy format string
   const char *format_pgm_p = (PGM_P) format;
@@ -78,6 +81,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr
   this->vprintf_to_buffer_(this->tx_buffer_, args);
   this->write_footer_();
   this->log_message_(level, tag, offset);
+  recursion_guard_ = false;
 }
 #endif
 
diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h
index 1724875229..365261cb91 100644
--- a/esphome/components/logger/logger.h
+++ b/esphome/components/logger/logger.h
@@ -113,6 +113,8 @@ class Logger : public Component {
   };
   std::vector<LogLevelOverride> log_levels_;
   CallbackManager<void(int, const char *, const char *)> log_callback_{};
+  /// Prevents recursive log calls, if true a log message is already being processed.
+  bool recursion_guard_ = false;
 };
 
 extern Logger *global_logger;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py
index 019b7c7e64..c22d377b3c 100644
--- a/esphome/components/mcp23xxx_base/__init__.py
+++ b/esphome/components/mcp23xxx_base/__init__.py
@@ -91,7 +91,7 @@ async def mcp23xxx_pin_to_code(config):
 
 
 # BEGIN Removed pin schemas below to show error in configuration
-# TODO remove in 1.19.0
+# TODO remove in 2022.5.0
 
 for id in ["mcp23008", "mcp23s08", "mcp23017", "mcp23s17"]:
     PIN_SCHEMA = cv.Schema(
@@ -110,6 +110,7 @@ for id in ["mcp23008", "mcp23s08", "mcp23017", "mcp23s17"]:
         }
     )
 
+    # pylint: disable=cell-var-from-loop
     @pins.PIN_SCHEMA_REGISTRY.register(id, (PIN_SCHEMA, PIN_SCHEMA))
     def pin_to_code(config):
         pass
diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py
index 1a111f7891..0081f42952 100644
--- a/esphome/components/mhz19/sensor.py
+++ b/esphome/components/mhz19/sensor.py
@@ -8,6 +8,7 @@ from esphome.const import (
     CONF_ID,
     CONF_TEMPERATURE,
     DEVICE_CLASS_TEMPERATURE,
+    DEVICE_CLASS_CARBON_DIOXIDE,
     ICON_MOLECULE_CO2,
     STATE_CLASS_MEASUREMENT,
     UNIT_PARTS_PER_MILLION,
@@ -34,6 +35,7 @@ CONFIG_SCHEMA = (
                 unit_of_measurement=UNIT_PARTS_PER_MILLION,
                 icon=ICON_MOLECULE_CO2,
                 accuracy_decimals=0,
+                device_class=DEVICE_CLASS_CARBON_DIOXIDE,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
diff --git a/esphome/api/__init__.py b/esphome/components/midea/__init__.py
similarity index 100%
rename from esphome/api/__init__.py
rename to esphome/components/midea/__init__.py
diff --git a/esphome/components/midea/adapter.cpp b/esphome/components/midea/adapter.cpp
new file mode 100644
index 0000000000..bd5b289095
--- /dev/null
+++ b/esphome/components/midea/adapter.cpp
@@ -0,0 +1,173 @@
+#include "esphome/core/log.h"
+#include "adapter.h"
+
+namespace esphome {
+namespace midea {
+
+const char *const Constants::TAG = "midea";
+const std::string Constants::FREEZE_PROTECTION = "freeze protection";
+const std::string Constants::SILENT = "silent";
+const std::string Constants::TURBO = "turbo";
+
+ClimateMode Converters::to_climate_mode(MideaMode mode) {
+  switch (mode) {
+    case MideaMode::MODE_AUTO:
+      return ClimateMode::CLIMATE_MODE_HEAT_COOL;
+    case MideaMode::MODE_COOL:
+      return ClimateMode::CLIMATE_MODE_COOL;
+    case MideaMode::MODE_DRY:
+      return ClimateMode::CLIMATE_MODE_DRY;
+    case MideaMode::MODE_FAN_ONLY:
+      return ClimateMode::CLIMATE_MODE_FAN_ONLY;
+    case MideaMode::MODE_HEAT:
+      return ClimateMode::CLIMATE_MODE_HEAT;
+    default:
+      return ClimateMode::CLIMATE_MODE_OFF;
+  }
+}
+
+MideaMode Converters::to_midea_mode(ClimateMode mode) {
+  switch (mode) {
+    case ClimateMode::CLIMATE_MODE_HEAT_COOL:
+      return MideaMode::MODE_AUTO;
+    case ClimateMode::CLIMATE_MODE_COOL:
+      return MideaMode::MODE_COOL;
+    case ClimateMode::CLIMATE_MODE_DRY:
+      return MideaMode::MODE_DRY;
+    case ClimateMode::CLIMATE_MODE_FAN_ONLY:
+      return MideaMode::MODE_FAN_ONLY;
+    case ClimateMode::CLIMATE_MODE_HEAT:
+      return MideaMode::MODE_HEAT;
+    default:
+      return MideaMode::MODE_OFF;
+  }
+}
+
+ClimateSwingMode Converters::to_climate_swing_mode(MideaSwingMode mode) {
+  switch (mode) {
+    case MideaSwingMode::SWING_VERTICAL:
+      return ClimateSwingMode::CLIMATE_SWING_VERTICAL;
+    case MideaSwingMode::SWING_HORIZONTAL:
+      return ClimateSwingMode::CLIMATE_SWING_HORIZONTAL;
+    case MideaSwingMode::SWING_BOTH:
+      return ClimateSwingMode::CLIMATE_SWING_BOTH;
+    default:
+      return ClimateSwingMode::CLIMATE_SWING_OFF;
+  }
+}
+
+MideaSwingMode Converters::to_midea_swing_mode(ClimateSwingMode mode) {
+  switch (mode) {
+    case ClimateSwingMode::CLIMATE_SWING_VERTICAL:
+      return MideaSwingMode::SWING_VERTICAL;
+    case ClimateSwingMode::CLIMATE_SWING_HORIZONTAL:
+      return MideaSwingMode::SWING_HORIZONTAL;
+    case ClimateSwingMode::CLIMATE_SWING_BOTH:
+      return MideaSwingMode::SWING_BOTH;
+    default:
+      return MideaSwingMode::SWING_OFF;
+  }
+}
+
+MideaFanMode Converters::to_midea_fan_mode(ClimateFanMode mode) {
+  switch (mode) {
+    case ClimateFanMode::CLIMATE_FAN_LOW:
+      return MideaFanMode::FAN_LOW;
+    case ClimateFanMode::CLIMATE_FAN_MEDIUM:
+      return MideaFanMode::FAN_MEDIUM;
+    case ClimateFanMode::CLIMATE_FAN_HIGH:
+      return MideaFanMode::FAN_HIGH;
+    default:
+      return MideaFanMode::FAN_AUTO;
+  }
+}
+
+ClimateFanMode Converters::to_climate_fan_mode(MideaFanMode mode) {
+  switch (mode) {
+    case MideaFanMode::FAN_LOW:
+      return ClimateFanMode::CLIMATE_FAN_LOW;
+    case MideaFanMode::FAN_MEDIUM:
+      return ClimateFanMode::CLIMATE_FAN_MEDIUM;
+    case MideaFanMode::FAN_HIGH:
+      return ClimateFanMode::CLIMATE_FAN_HIGH;
+    default:
+      return ClimateFanMode::CLIMATE_FAN_AUTO;
+  }
+}
+
+bool Converters::is_custom_midea_fan_mode(MideaFanMode mode) {
+  switch (mode) {
+    case MideaFanMode::FAN_SILENT:
+    case MideaFanMode::FAN_TURBO:
+      return true;
+    default:
+      return false;
+  }
+}
+
+const std::string &Converters::to_custom_climate_fan_mode(MideaFanMode mode) {
+  switch (mode) {
+    case MideaFanMode::FAN_SILENT:
+      return Constants::SILENT;
+    default:
+      return Constants::TURBO;
+  }
+}
+
+MideaFanMode Converters::to_midea_fan_mode(const std::string &mode) {
+  if (mode == Constants::SILENT)
+    return MideaFanMode::FAN_SILENT;
+  return MideaFanMode::FAN_TURBO;
+}
+
+MideaPreset Converters::to_midea_preset(ClimatePreset preset) {
+  switch (preset) {
+    case ClimatePreset::CLIMATE_PRESET_SLEEP:
+      return MideaPreset::PRESET_SLEEP;
+    case ClimatePreset::CLIMATE_PRESET_ECO:
+      return MideaPreset::PRESET_ECO;
+    case ClimatePreset::CLIMATE_PRESET_BOOST:
+      return MideaPreset::PRESET_TURBO;
+    default:
+      return MideaPreset::PRESET_NONE;
+  }
+}
+
+ClimatePreset Converters::to_climate_preset(MideaPreset preset) {
+  switch (preset) {
+    case MideaPreset::PRESET_SLEEP:
+      return ClimatePreset::CLIMATE_PRESET_SLEEP;
+    case MideaPreset::PRESET_ECO:
+      return ClimatePreset::CLIMATE_PRESET_ECO;
+    case MideaPreset::PRESET_TURBO:
+      return ClimatePreset::CLIMATE_PRESET_BOOST;
+    default:
+      return ClimatePreset::CLIMATE_PRESET_NONE;
+  }
+}
+
+bool Converters::is_custom_midea_preset(MideaPreset preset) { return preset == MideaPreset::PRESET_FREEZE_PROTECTION; }
+
+const std::string &Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; }
+
+MideaPreset Converters::to_midea_preset(const std::string &preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; }
+
+void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities) {
+  if (capabilities.supportAutoMode())
+    traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT_COOL);
+  if (capabilities.supportCoolMode())
+    traits.add_supported_mode(ClimateMode::CLIMATE_MODE_COOL);
+  if (capabilities.supportHeatMode())
+    traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT);
+  if (capabilities.supportDryMode())
+    traits.add_supported_mode(ClimateMode::CLIMATE_MODE_DRY);
+  if (capabilities.supportTurboPreset())
+    traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_BOOST);
+  if (capabilities.supportEcoPreset())
+    traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO);
+  if (capabilities.supportFrostProtectionPreset())
+    traits.add_supported_custom_preset(Constants::FREEZE_PROTECTION);
+}
+
+}  // namespace midea
+}  // namespace esphome
diff --git a/esphome/components/midea/adapter.h b/esphome/components/midea/adapter.h
new file mode 100644
index 0000000000..8d8d57e8f9
--- /dev/null
+++ b/esphome/components/midea/adapter.h
@@ -0,0 +1,42 @@
+#pragma once
+#include <Appliance/AirConditioner/AirConditioner.h>
+#include "esphome/components/climate/climate_traits.h"
+#include "appliance_base.h"
+
+namespace esphome {
+namespace midea {
+
+using MideaMode = dudanov::midea::ac::Mode;
+using MideaSwingMode = dudanov::midea::ac::SwingMode;
+using MideaFanMode = dudanov::midea::ac::FanMode;
+using MideaPreset = dudanov::midea::ac::Preset;
+
+class Constants {
+ public:
+  static const char *const TAG;
+  static const std::string FREEZE_PROTECTION;
+  static const std::string SILENT;
+  static const std::string TURBO;
+};
+
+class Converters {
+ public:
+  static MideaMode to_midea_mode(ClimateMode mode);
+  static ClimateMode to_climate_mode(MideaMode mode);
+  static MideaSwingMode to_midea_swing_mode(ClimateSwingMode mode);
+  static ClimateSwingMode to_climate_swing_mode(MideaSwingMode mode);
+  static MideaPreset to_midea_preset(ClimatePreset preset);
+  static MideaPreset to_midea_preset(const std::string &preset);
+  static bool is_custom_midea_preset(MideaPreset preset);
+  static ClimatePreset to_climate_preset(MideaPreset preset);
+  static const std::string &to_custom_climate_preset(MideaPreset preset);
+  static MideaFanMode to_midea_fan_mode(ClimateFanMode fan_mode);
+  static MideaFanMode to_midea_fan_mode(const std::string &fan_mode);
+  static bool is_custom_midea_fan_mode(MideaFanMode fan_mode);
+  static ClimateFanMode to_climate_fan_mode(MideaFanMode fan_mode);
+  static const std::string &to_custom_climate_fan_mode(MideaFanMode fan_mode);
+  static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities);
+};
+
+}  // namespace midea
+}  // namespace esphome
diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp
new file mode 100644
index 0000000000..a71f1dbdfb
--- /dev/null
+++ b/esphome/components/midea/air_conditioner.cpp
@@ -0,0 +1,152 @@
+#include "esphome/core/log.h"
+#include "air_conditioner.h"
+#include "adapter.h"
+#ifdef USE_REMOTE_TRANSMITTER
+#include "midea_ir.h"
+#endif
+
+namespace esphome {
+namespace midea {
+
+static void set_sensor(Sensor *sensor, float value) {
+  if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value))
+    sensor->publish_state(value);
+}
+
+template<typename T> void update_property(T &property, const T &value, bool &flag) {
+  if (property != value) {
+    property = value;
+    flag = true;
+  }
+}
+
+void AirConditioner::on_status_change() {
+  bool need_publish = false;
+  update_property(this->target_temperature, this->base_.getTargetTemp(), need_publish);
+  update_property(this->current_temperature, this->base_.getIndoorTemp(), need_publish);
+  auto mode = Converters::to_climate_mode(this->base_.getMode());
+  update_property(this->mode, mode, need_publish);
+  auto swing_mode = Converters::to_climate_swing_mode(this->base_.getSwingMode());
+  update_property(this->swing_mode, swing_mode, need_publish);
+  // Preset
+  auto preset = this->base_.getPreset();
+  if (Converters::is_custom_midea_preset(preset)) {
+    if (this->set_custom_preset_(Converters::to_custom_climate_preset(preset)))
+      need_publish = true;
+  } else if (this->set_preset_(Converters::to_climate_preset(preset))) {
+    need_publish = true;
+  }
+  // Fan mode
+  auto fan_mode = this->base_.getFanMode();
+  if (Converters::is_custom_midea_fan_mode(fan_mode)) {
+    if (this->set_custom_fan_mode_(Converters::to_custom_climate_fan_mode(fan_mode)))
+      need_publish = true;
+  } else if (this->set_fan_mode_(Converters::to_climate_fan_mode(fan_mode))) {
+    need_publish = true;
+  }
+  if (need_publish)
+    this->publish_state();
+  set_sensor(this->outdoor_sensor_, this->base_.getOutdoorTemp());
+  set_sensor(this->power_sensor_, this->base_.getPowerUsage());
+  set_sensor(this->humidity_sensor_, this->base_.getIndoorHum());
+}
+
+void AirConditioner::control(const ClimateCall &call) {
+  dudanov::midea::ac::Control ctrl{};
+  if (call.get_target_temperature().has_value())
+    ctrl.targetTemp = call.get_target_temperature().value();
+  if (call.get_swing_mode().has_value())
+    ctrl.swingMode = Converters::to_midea_swing_mode(call.get_swing_mode().value());
+  if (call.get_mode().has_value())
+    ctrl.mode = Converters::to_midea_mode(call.get_mode().value());
+  if (call.get_preset().has_value())
+    ctrl.preset = Converters::to_midea_preset(call.get_preset().value());
+  else if (call.get_custom_preset().has_value())
+    ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().value());
+  if (call.get_fan_mode().has_value())
+    ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value());
+  else if (call.get_custom_fan_mode().has_value())
+    ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().value());
+  this->base_.control(ctrl);
+}
+
+ClimateTraits AirConditioner::traits() {
+  auto traits = ClimateTraits();
+  traits.set_supports_current_temperature(true);
+  traits.set_visual_min_temperature(17);
+  traits.set_visual_max_temperature(30);
+  traits.set_visual_temperature_step(0.5);
+  traits.set_supported_modes(this->supported_modes_);
+  traits.set_supported_swing_modes(this->supported_swing_modes_);
+  traits.set_supported_presets(this->supported_presets_);
+  traits.set_supported_custom_presets(this->supported_custom_presets_);
+  traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_);
+  /* + MINIMAL SET OF CAPABILITIES */
+  traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF);
+  traits.add_supported_mode(ClimateMode::CLIMATE_MODE_FAN_ONLY);
+  traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO);
+  traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW);
+  traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM);
+  traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH);
+  traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF);
+  traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_VERTICAL);
+  traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE);
+  traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_SLEEP);
+  if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK)
+    Converters::to_climate_traits(traits, this->base_.getCapabilities());
+  return traits;
+}
+
+void AirConditioner::dump_config() {
+  ESP_LOGCONFIG(Constants::TAG, "MideaDongle:");
+  ESP_LOGCONFIG(Constants::TAG, "  [x] Period: %dms", this->base_.getPeriod());
+  ESP_LOGCONFIG(Constants::TAG, "  [x] Response timeout: %dms", this->base_.getTimeout());
+  ESP_LOGCONFIG(Constants::TAG, "  [x] Request attempts: %d", this->base_.getNumAttempts());
+#ifdef USE_REMOTE_TRANSMITTER
+  ESP_LOGCONFIG(Constants::TAG, "  [x] Using RemoteTransmitter");
+#endif
+  if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) {
+    this->base_.getCapabilities().dump();
+  } else if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_ERROR) {
+    ESP_LOGW(Constants::TAG,
+             "Failed to get 0xB5 capabilities report. Suggest to disable it in config and manually set your "
+             "appliance options.");
+  }
+  this->dump_traits_(Constants::TAG);
+}
+
+/* ACTIONS */
+
+void AirConditioner::do_follow_me(float temperature, bool beeper) {
+#ifdef USE_REMOTE_TRANSMITTER
+  IrFollowMeData data(static_cast<uint8_t>(lroundf(temperature)), beeper);
+  this->transmit_ir(data);
+#else
+  ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
+#endif
+}
+
+void AirConditioner::do_swing_step() {
+#ifdef USE_REMOTE_TRANSMITTER
+  IrSpecialData data(0x01);
+  this->transmit_ir(data);
+#else
+  ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
+#endif
+}
+
+void AirConditioner::do_display_toggle() {
+  if (this->base_.getCapabilities().supportLightControl()) {
+    this->base_.displayToggle();
+  } else {
+#ifdef USE_REMOTE_TRANSMITTER
+    IrSpecialData data(0x08);
+    this->transmit_ir(data);
+#else
+    ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
+#endif
+  }
+}
+
+}  // namespace midea
+}  // namespace esphome
diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h
new file mode 100644
index 0000000000..895b6412f3
--- /dev/null
+++ b/esphome/components/midea/air_conditioner.h
@@ -0,0 +1,41 @@
+#pragma once
+#include <Appliance/AirConditioner/AirConditioner.h>
+#include "appliance_base.h"
+#include "esphome/components/sensor/sensor.h"
+
+namespace esphome {
+namespace midea {
+
+using sensor::Sensor;
+using climate::ClimateCall;
+
+class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner> {
+ public:
+  void dump_config() override;
+  void set_outdoor_temperature_sensor(Sensor *sensor) { this->outdoor_sensor_ = sensor; }
+  void set_humidity_setpoint_sensor(Sensor *sensor) { this->humidity_sensor_ = sensor; }
+  void set_power_sensor(Sensor *sensor) { this->power_sensor_ = sensor; }
+  void on_status_change() override;
+
+  /* ############### */
+  /* ### ACTIONS ### */
+  /* ############### */
+
+  void do_follow_me(float temperature, bool beeper = false);
+  void do_display_toggle();
+  void do_swing_step();
+  void do_beeper_on() { this->set_beeper_feedback(true); }
+  void do_beeper_off() { this->set_beeper_feedback(false); }
+  void do_power_on() { this->base_.setPowerState(true); }
+  void do_power_off() { this->base_.setPowerState(false); }
+
+ protected:
+  void control(const ClimateCall &call) override;
+  ClimateTraits traits() override;
+  Sensor *outdoor_sensor_{nullptr};
+  Sensor *humidity_sensor_{nullptr};
+  Sensor *power_sensor_{nullptr};
+};
+
+}  // namespace midea
+}  // namespace esphome
diff --git a/esphome/components/midea/appliance_base.h b/esphome/components/midea/appliance_base.h
new file mode 100644
index 0000000000..aa616ced36
--- /dev/null
+++ b/esphome/components/midea/appliance_base.h
@@ -0,0 +1,76 @@
+#pragma once
+#include "esphome/core/component.h"
+#include "esphome/core/log.h"
+#include "esphome/components/uart/uart.h"
+#include "esphome/components/climate/climate.h"
+#ifdef USE_REMOTE_TRANSMITTER
+#include "esphome/components/remote_base/midea_protocol.h"
+#include "esphome/components/remote_transmitter/remote_transmitter.h"
+#endif
+#include <Appliance/ApplianceBase.h>
+#include <Helpers/Logger.h>
+
+namespace esphome {
+namespace midea {
+
+using climate::ClimatePreset;
+using climate::ClimateTraits;
+using climate::ClimateMode;
+using climate::ClimateSwingMode;
+using climate::ClimateFanMode;
+
+template<typename T> class ApplianceBase : public Component, public uart::UARTDevice, public climate::Climate {
+  static_assert(std::is_base_of<dudanov::midea::ApplianceBase, T>::value,
+                "T must derive from dudanov::midea::ApplianceBase class");
+
+ public:
+  ApplianceBase() {
+    this->base_.setStream(this);
+    this->base_.addOnStateCallback(std::bind(&ApplianceBase::on_status_change, this));
+    dudanov::midea::ApplianceBase::setLogger([](int level, const char *tag, int line, String format, va_list args) {
+      esp_log_vprintf_(level, tag, line, format.c_str(), args);
+    });
+  }
+  bool can_proceed() override {
+    return this->base_.getAutoconfStatus() != dudanov::midea::AutoconfStatus::AUTOCONF_PROGRESS;
+  }
+  float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; }
+  void setup() override { this->base_.setup(); }
+  void loop() override { this->base_.loop(); }
+  void set_period(uint32_t ms) { this->base_.setPeriod(ms); }
+  void set_response_timeout(uint32_t ms) { this->base_.setTimeout(ms); }
+  void set_request_attempts(uint32_t attempts) { this->base_.setNumAttempts(attempts); }
+  void set_beeper_feedback(bool state) { this->base_.setBeeper(state); }
+  void set_autoconf(bool value) { this->base_.setAutoconf(value); }
+  void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
+  void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
+  void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); }
+  void set_custom_presets(std::set<std::string> presets) { this->supported_custom_presets_ = std::move(presets); }
+  void set_custom_fan_modes(std::set<std::string> modes) { this->supported_custom_fan_modes_ = std::move(modes); }
+  virtual void on_status_change() = 0;
+#ifdef USE_REMOTE_TRANSMITTER
+  void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
+    this->transmitter_ = transmitter;
+  }
+  void transmit_ir(remote_base::MideaData &data) {
+    data.finalize();
+    auto transmit = this->transmitter_->transmit();
+    remote_base::MideaProtocol().encode(transmit.get_data(), data);
+    transmit.perform();
+  }
+#endif
+
+ protected:
+  T base_;
+  std::set<ClimateMode> supported_modes_{};
+  std::set<ClimateSwingMode> supported_swing_modes_{};
+  std::set<ClimatePreset> supported_presets_{};
+  std::set<std::string> supported_custom_presets_{};
+  std::set<std::string> supported_custom_fan_modes_{};
+#ifdef USE_REMOTE_TRANSMITTER
+  remote_transmitter::RemoteTransmitterComponent *transmitter_{nullptr};
+#endif
+};
+
+}  // namespace midea
+}  // namespace esphome
diff --git a/esphome/components/midea/automations.h b/esphome/components/midea/automations.h
new file mode 100644
index 0000000000..1f026c0c15
--- /dev/null
+++ b/esphome/components/midea/automations.h
@@ -0,0 +1,56 @@
+#pragma once
+#include "esphome/core/automation.h"
+#include "air_conditioner.h"
+
+namespace esphome {
+namespace midea {
+
+template<typename... Ts> class MideaActionBase : public Action<Ts...> {
+ public:
+  void set_parent(AirConditioner *parent) { this->parent_ = parent; }
+
+ protected:
+  AirConditioner *parent_;
+};
+
+template<typename... Ts> class FollowMeAction : public MideaActionBase<Ts...> {
+  TEMPLATABLE_VALUE(float, temperature)
+  TEMPLATABLE_VALUE(bool, beeper)
+
+  void play(Ts... x) override {
+    this->parent_->do_follow_me(this->temperature_.value(x...), this->beeper_.value(x...));
+  }
+};
+
+template<typename... Ts> class SwingStepAction : public MideaActionBase<Ts...> {
+ public:
+  void play(Ts... x) override { this->parent_->do_swing_step(); }
+};
+
+template<typename... Ts> class DisplayToggleAction : public MideaActionBase<Ts...> {
+ public:
+  void play(Ts... x) override { this->parent_->do_display_toggle(); }
+};
+
+template<typename... Ts> class BeeperOnAction : public MideaActionBase<Ts...> {
+ public:
+  void play(Ts... x) override { this->parent_->do_beeper_on(); }
+};
+
+template<typename... Ts> class BeeperOffAction : public MideaActionBase<Ts...> {
+ public:
+  void play(Ts... x) override { this->parent_->do_beeper_off(); }
+};
+
+template<typename... Ts> class PowerOnAction : public MideaActionBase<Ts...> {
+ public:
+  void play(Ts... x) override { this->parent_->do_power_on(); }
+};
+
+template<typename... Ts> class PowerOffAction : public MideaActionBase<Ts...> {
+ public:
+  void play(Ts... x) override { this->parent_->do_power_off(); }
+};
+
+}  // namespace midea
+}  // namespace esphome
diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py
new file mode 100644
index 0000000000..137fcdd607
--- /dev/null
+++ b/esphome/components/midea/climate.py
@@ -0,0 +1,284 @@
+from esphome.core import coroutine
+from esphome import automation
+from esphome.components import climate, sensor, uart, remote_transmitter
+from esphome.components.remote_base import CONF_TRANSMITTER_ID
+import esphome.config_validation as cv
+import esphome.codegen as cg
+from esphome.const import (
+    CONF_AUTOCONF,
+    CONF_BEEPER,
+    CONF_CUSTOM_FAN_MODES,
+    CONF_CUSTOM_PRESETS,
+    CONF_ID,
+    CONF_NUM_ATTEMPTS,
+    CONF_PERIOD,
+    CONF_SUPPORTED_MODES,
+    CONF_SUPPORTED_PRESETS,
+    CONF_SUPPORTED_SWING_MODES,
+    CONF_TIMEOUT,
+    CONF_TEMPERATURE,
+    DEVICE_CLASS_POWER,
+    DEVICE_CLASS_TEMPERATURE,
+    DEVICE_CLASS_HUMIDITY,
+    ICON_POWER,
+    ICON_THERMOMETER,
+    ICON_WATER_PERCENT,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+    UNIT_PERCENT,
+    UNIT_WATT,
+)
+from esphome.components.climate import (
+    ClimateMode,
+    ClimatePreset,
+    ClimateSwingMode,
+)
+
+CODEOWNERS = ["@dudanov"]
+DEPENDENCIES = ["climate", "uart", "wifi"]
+AUTO_LOAD = ["sensor"]
+CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
+CONF_POWER_USAGE = "power_usage"
+CONF_HUMIDITY_SETPOINT = "humidity_setpoint"
+midea_ns = cg.esphome_ns.namespace("midea")
+AirConditioner = midea_ns.class_("AirConditioner", climate.Climate, cg.Component)
+Capabilities = midea_ns.namespace("Constants")
+
+
+def templatize(value):
+    if isinstance(value, cv.Schema):
+        value = value.schema
+    ret = {}
+    for key, val in value.items():
+        ret[key] = cv.templatable(val)
+    return cv.Schema(ret)
+
+
+def register_action(name, type_, schema):
+    validator = templatize(schema).extend(MIDEA_ACTION_BASE_SCHEMA)
+    registerer = automation.register_action(f"midea_ac.{name}", type_, validator)
+
+    def decorator(func):
+        async def new_func(config, action_id, template_arg, args):
+            ac_ = await cg.get_variable(config[CONF_ID])
+            var = cg.new_Pvariable(action_id, template_arg)
+            cg.add(var.set_parent(ac_))
+            await coroutine(func)(var, config, args)
+            return var
+
+        return registerer(new_func)
+
+    return decorator
+
+
+ALLOWED_CLIMATE_MODES = {
+    "HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL,
+    "COOL": ClimateMode.CLIMATE_MODE_COOL,
+    "HEAT": ClimateMode.CLIMATE_MODE_HEAT,
+    "DRY": ClimateMode.CLIMATE_MODE_DRY,
+    "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY,
+}
+
+ALLOWED_CLIMATE_PRESETS = {
+    "ECO": ClimatePreset.CLIMATE_PRESET_ECO,
+    "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
+    "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
+}
+
+ALLOWED_CLIMATE_SWING_MODES = {
+    "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
+    "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
+    "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
+}
+
+CUSTOM_FAN_MODES = {
+    "SILENT": Capabilities.SILENT,
+    "TURBO": Capabilities.TURBO,
+}
+
+CUSTOM_PRESETS = {
+    "FREEZE_PROTECTION": Capabilities.FREEZE_PROTECTION,
+}
+
+validate_modes = cv.enum(ALLOWED_CLIMATE_MODES, upper=True)
+validate_presets = cv.enum(ALLOWED_CLIMATE_PRESETS, upper=True)
+validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True)
+validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True)
+validate_custom_presets = cv.enum(CUSTOM_PRESETS, upper=True)
+
+CONFIG_SCHEMA = cv.All(
+    climate.CLIMATE_SCHEMA.extend(
+        {
+            cv.GenerateID(): cv.declare_id(AirConditioner),
+            cv.Optional(CONF_PERIOD, default="1s"): cv.time_period,
+            cv.Optional(CONF_TIMEOUT, default="2s"): cv.time_period,
+            cv.Optional(CONF_NUM_ATTEMPTS, default=3): cv.int_range(min=1, max=5),
+            cv.Optional(CONF_TRANSMITTER_ID): cv.use_id(
+                remote_transmitter.RemoteTransmitterComponent
+            ),
+            cv.Optional(CONF_BEEPER, default=False): cv.boolean,
+            cv.Optional(CONF_AUTOCONF, default=True): cv.boolean,
+            cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(validate_modes),
+            cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list(
+                validate_swing_modes
+            ),
+            cv.Optional(CONF_SUPPORTED_PRESETS): cv.ensure_list(validate_presets),
+            cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list(validate_custom_presets),
+            cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list(
+                validate_custom_fan_modes
+            ),
+            cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_CELSIUS,
+                icon=ICON_THERMOMETER,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_TEMPERATURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_WATT,
+                icon=ICON_POWER,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_POWER,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                icon=ICON_WATER_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_HUMIDITY,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+        }
+    )
+    .extend(uart.UART_DEVICE_SCHEMA)
+    .extend(cv.COMPONENT_SCHEMA)
+)
+
+# Actions
+FollowMeAction = midea_ns.class_("FollowMeAction", automation.Action)
+DisplayToggleAction = midea_ns.class_("DisplayToggleAction", automation.Action)
+SwingStepAction = midea_ns.class_("SwingStepAction", automation.Action)
+BeeperOnAction = midea_ns.class_("BeeperOnAction", automation.Action)
+BeeperOffAction = midea_ns.class_("BeeperOffAction", automation.Action)
+PowerOnAction = midea_ns.class_("PowerOnAction", automation.Action)
+PowerOffAction = midea_ns.class_("PowerOffAction", automation.Action)
+
+MIDEA_ACTION_BASE_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(CONF_ID): cv.use_id(AirConditioner),
+    }
+)
+
+# FollowMe action
+MIDEA_FOLLOW_ME_MIN = 0
+MIDEA_FOLLOW_ME_MAX = 37
+MIDEA_FOLLOW_ME_SCHEMA = cv.Schema(
+    {
+        cv.Required(CONF_TEMPERATURE): cv.templatable(cv.temperature),
+        cv.Optional(CONF_BEEPER, default=False): cv.templatable(cv.boolean),
+    }
+)
+
+
+@register_action("follow_me", FollowMeAction, MIDEA_FOLLOW_ME_SCHEMA)
+async def follow_me_to_code(var, config, args):
+    template_ = await cg.templatable(config[CONF_BEEPER], args, cg.bool_)
+    cg.add(var.set_beeper(template_))
+    template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.float_)
+    cg.add(var.set_temperature(template_))
+
+
+# Toggle Display action
+@register_action(
+    "display_toggle",
+    DisplayToggleAction,
+    cv.Schema({}),
+)
+async def display_toggle_to_code(var, config, args):
+    pass
+
+
+# Swing Step action
+@register_action(
+    "swing_step",
+    SwingStepAction,
+    cv.Schema({}),
+)
+async def swing_step_to_code(var, config, args):
+    pass
+
+
+# Beeper On action
+@register_action(
+    "beeper_on",
+    BeeperOnAction,
+    cv.Schema({}),
+)
+async def beeper_on_to_code(var, config, args):
+    pass
+
+
+# Beeper Off action
+@register_action(
+    "beeper_off",
+    BeeperOffAction,
+    cv.Schema({}),
+)
+async def beeper_off_to_code(var, config, args):
+    pass
+
+
+# Power On action
+@register_action(
+    "power_on",
+    PowerOnAction,
+    cv.Schema({}),
+)
+async def power_on_to_code(var, config, args):
+    pass
+
+
+# Power Off action
+@register_action(
+    "power_off",
+    PowerOffAction,
+    cv.Schema({}),
+)
+async def power_off_to_code(var, config, args):
+    pass
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await uart.register_uart_device(var, config)
+    await climate.register_climate(var, config)
+    cg.add(var.set_period(config[CONF_PERIOD].total_milliseconds))
+    cg.add(var.set_response_timeout(config[CONF_TIMEOUT].total_milliseconds))
+    cg.add(var.set_request_attempts(config[CONF_NUM_ATTEMPTS]))
+    if CONF_TRANSMITTER_ID in config:
+        cg.add_define("USE_REMOTE_TRANSMITTER")
+        transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID])
+        cg.add(var.set_transmitter(transmitter_))
+    cg.add(var.set_beeper_feedback(config[CONF_BEEPER]))
+    cg.add(var.set_autoconf(config[CONF_AUTOCONF]))
+    if CONF_SUPPORTED_MODES in config:
+        cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
+    if CONF_SUPPORTED_SWING_MODES in config:
+        cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES]))
+    if CONF_SUPPORTED_PRESETS in config:
+        cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS]))
+    if CONF_CUSTOM_PRESETS in config:
+        cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS]))
+    if CONF_CUSTOM_FAN_MODES in config:
+        cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES]))
+    if CONF_OUTDOOR_TEMPERATURE in config:
+        sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
+        cg.add(var.set_outdoor_temperature_sensor(sens))
+    if CONF_POWER_USAGE in config:
+        sens = await sensor.new_sensor(config[CONF_POWER_USAGE])
+        cg.add(var.set_power_sensor(sens))
+    if CONF_HUMIDITY_SETPOINT in config:
+        sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT])
+        cg.add(var.set_humidity_setpoint_sensor(sens))
+    cg.add_library("dudanov/MideaUART", "1.1.5")
diff --git a/esphome/components/midea/midea_ir.h b/esphome/components/midea/midea_ir.h
new file mode 100644
index 0000000000..2459d844a1
--- /dev/null
+++ b/esphome/components/midea/midea_ir.h
@@ -0,0 +1,42 @@
+#pragma once
+#ifdef USE_REMOTE_TRANSMITTER
+#include "esphome/components/remote_base/midea_protocol.h"
+
+namespace esphome {
+namespace midea {
+
+using IrData = remote_base::MideaData;
+
+class IrFollowMeData : public IrData {
+ public:
+  // Default constructor (temp: 30C, beeper: off)
+  IrFollowMeData() : IrData({MIDEA_TYPE_FOLLOW_ME, 0x82, 0x48, 0x7F, 0x1F}) {}
+  // Copy from Base
+  IrFollowMeData(const IrData &data) : IrData(data) {}
+  // Direct from temperature and beeper values
+  IrFollowMeData(uint8_t temp, bool beeper = false) : IrFollowMeData() {
+    this->set_temp(temp);
+    this->set_beeper(beeper);
+  }
+
+  /* TEMPERATURE */
+  uint8_t temp() const { return this->data_[4] - 1; }
+  void set_temp(uint8_t val) { this->data_[4] = std::min(MAX_TEMP, val) + 1; }
+
+  /* BEEPER */
+  bool beeper() const { return this->data_[3] & 128; }
+  void set_beeper(bool val) { this->set_value_(3, 1, 7, val); }
+
+ protected:
+  static const uint8_t MAX_TEMP = 37;
+};
+
+class IrSpecialData : public IrData {
+ public:
+  IrSpecialData(uint8_t code) : IrData({MIDEA_TYPE_SPECIAL, code, 0xFF, 0xFF, 0xFF}) {}
+};
+
+}  // namespace midea
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/midea_ac/climate.py b/esphome/components/midea_ac/climate.py
index 741741fd03..f336f84787 100644
--- a/esphome/components/midea_ac/climate.py
+++ b/esphome/components/midea_ac/climate.py
@@ -1,115 +1,3 @@
-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,
-    ICON_THERMOMETER,
-    ICON_POWER,
-    DEVICE_CLASS_POWER,
-    DEVICE_CLASS_TEMPERATURE,
-    ICON_WATER_PERCENT,
-    DEVICE_CLASS_HUMIDITY,
-)
-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"
-CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
-CONF_POWER_USAGE = "power_usage"
-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_of_measurement=UNIT_CELSIUS,
-                icon=ICON_THERMOMETER,
-                accuracy_decimals=0,
-                device_class=DEVICE_CLASS_TEMPERATURE,
-                state_class=STATE_CLASS_MEASUREMENT,
-            ),
-            cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema(
-                unit_of_measurement=UNIT_WATT,
-                icon=ICON_POWER,
-                accuracy_decimals=0,
-                device_class=DEVICE_CLASS_POWER,
-                state_class=STATE_CLASS_MEASUREMENT,
-            ),
-            cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema(
-                unit_of_measurement=UNIT_PERCENT,
-                icon=ICON_WATER_PERCENT,
-                accuracy_decimals=0,
-                device_class=DEVICE_CLASS_HUMIDITY,
-                state_class=STATE_CLASS_MEASUREMENT,
-            ),
-        }
-    ).extend(cv.COMPONENT_SCHEMA)
-)
-
-
-async def to_code(config):
-    var = cg.new_Pvariable(config[CONF_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 = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
-        cg.add(var.set_outdoor_temperature_sensor(sens))
-    if CONF_POWER_USAGE in config:
-        sens = await sensor.new_sensor(config[CONF_POWER_USAGE])
-        cg.add(var.set_power_sensor(sens))
-    if CONF_HUMIDITY_SETPOINT in config:
-        sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT])
-        cg.add(var.set_humidity_setpoint_sensor(sens))
+CONFIG_SCHEMA = cv.invalid("This platform has been renamed to midea in 2021.9")
diff --git a/esphome/components/midea_ac/midea_climate.cpp b/esphome/components/midea_ac/midea_climate.cpp
deleted file mode 100644
index 72f7d23404..0000000000
--- a/esphome/components/midea_ac/midea_climate.cpp
+++ /dev/null
@@ -1,208 +0,0 @@
-#include "esphome/core/log.h"
-#include "midea_climate.h"
-
-namespace esphome {
-namespace midea_ac {
-
-static const char *const TAG = "midea_ac";
-
-static void set_sensor(sensor::Sensor *sensor, float value) {
-  if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value))
-    sensor->publish_state(value);
-}
-
-template<typename T> void set_property(T &property, T value, bool &flag) {
-  if (property != value) {
-    property = value;
-    flag = true;
-  }
-}
-
-void MideaAC::on_frame(const midea_dongle::Frame &frame) {
-  const auto p = frame.as<PropertiesFrame>();
-  if (p.has_power_info()) {
-    set_sensor(this->power_sensor_, p.get_power_usage());
-    return;
-  } else if (!p.has_properties()) {
-    ESP_LOGW(TAG, "RX: frame has unknown type");
-    return;
-  }
-  if (p.get_type() == midea_dongle::MideaMessageType::DEVICE_CONTROL) {
-    ESP_LOGD(TAG, "RX: control frame");
-    this->ctrl_request_ = false;
-  } else {
-    ESP_LOGD(TAG, "RX: query frame");
-  }
-  if (this->ctrl_request_)
-    return;
-  this->cmd_frame_.set_properties(p);  // copy properties from response
-  bool need_publish = false;
-  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);
-  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());
-  set_sensor(this->humidity_sensor_, p.get_humidity_setpoint());
-}
-
-void MideaAC::on_update() {
-  if (this->ctrl_request_) {
-    ESP_LOGD(TAG, "TX: control");
-    this->parent_->write_frame(this->cmd_frame_);
-  } else {
-    ESP_LOGD(TAG, "TX: query");
-    if (this->power_sensor_ == nullptr || this->request_num_++ % 32)
-      this->parent_->write_frame(this->query_frame_);
-    else
-      this->parent_->write_frame(this->power_frame_);
-  }
-}
-
-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_NONE:
-      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());
-    this->ctrl_request_ = true;
-  }
-  if (call.get_target_temperature().has_value() && call.get_target_temperature().value() != this->target_temperature) {
-    this->cmd_frame_.set_target_temp(call.get_target_temperature().value());
-    this->ctrl_request_ = true;
-  }
-  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();
-  }
-}
-
-climate::ClimateTraits MideaAC::traits() {
-  auto traits = climate::ClimateTraits();
-  traits.set_visual_min_temperature(17);
-  traits.set_visual_max_temperature(30);
-  traits.set_visual_temperature_step(0.5);
-  traits.set_supported_modes({
-      climate::CLIMATE_MODE_OFF,
-      climate::CLIMATE_MODE_HEAT_COOL,
-      climate::CLIMATE_MODE_COOL,
-      climate::CLIMATE_MODE_DRY,
-      climate::CLIMATE_MODE_HEAT,
-      climate::CLIMATE_MODE_FAN_ONLY,
-  });
-  traits.set_supported_fan_modes({
-      climate::CLIMATE_FAN_AUTO,
-      climate::CLIMATE_FAN_LOW,
-      climate::CLIMATE_FAN_MEDIUM,
-      climate::CLIMATE_FAN_HIGH,
-  });
-  traits.set_supported_custom_fan_modes(this->traits_custom_fan_modes_);
-  traits.set_supported_swing_modes({
-      climate::CLIMATE_SWING_OFF,
-      climate::CLIMATE_SWING_VERTICAL,
-  });
-  if (traits_swing_horizontal_)
-    traits.add_supported_swing_mode(climate::CLIMATE_SWING_HORIZONTAL);
-  if (traits_swing_both_)
-    traits.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH);
-  traits.set_supported_presets({
-      climate::CLIMATE_PRESET_NONE,
-  });
-  if (traits_preset_eco_)
-    traits.add_supported_preset(climate::CLIMATE_PRESET_ECO);
-  if (traits_preset_sleep_)
-    traits.add_supported_preset(climate::CLIMATE_PRESET_SLEEP);
-  if (traits_preset_boost_)
-    traits.add_supported_preset(climate::CLIMATE_PRESET_BOOST);
-  traits.set_supported_custom_presets(this->traits_custom_presets_);
-  traits.set_supports_current_temperature(true);
-  return traits;
-}
-
-}  // namespace midea_ac
-}  // namespace esphome
diff --git a/esphome/components/midea_ac/midea_climate.h b/esphome/components/midea_ac/midea_climate.h
deleted file mode 100644
index 62bd4c339e..0000000000
--- a/esphome/components/midea_ac/midea_climate.h
+++ /dev/null
@@ -1,68 +0,0 @@
-#pragma once
-
-#include <utility>
-
-#include "esphome/core/component.h"
-#include "esphome/components/sensor/sensor.h"
-#include "esphome/components/midea_dongle/midea_dongle.h"
-#include "esphome/components/climate/climate.h"
-#include "esphome/components/midea_dongle/midea_dongle.h"
-#include "esphome/components/sensor/sensor.h"
-#include "esphome/core/component.h"
-#include "midea_frame.h"
-
-namespace esphome {
-namespace midea_ac {
-
-class MideaAC : public midea_dongle::MideaAppliance, public climate::Climate, public Component {
- public:
-  float get_setup_priority() const override { return setup_priority::LATE; }
-  void on_frame(const midea_dongle::Frame &frame) override;
-  void on_update() override;
-  void setup() override { this->parent_->set_appliance(this); }
-  void set_midea_dongle_parent(midea_dongle::MideaDongle *parent) { this->parent_ = parent; }
-  void set_outdoor_temperature_sensor(sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; }
-  void set_humidity_setpoint_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; }
-  void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; }
-  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::set<std::string> custom_fan_modes) {
-    this->traits_custom_fan_modes_ = std::move(custom_fan_modes);
-  }
-  void set_custom_presets(std::set<std::string> custom_presets) {
-    this->traits_custom_presets_ = std::move(custom_presets);
-  }
-  bool allow_custom_preset(const std::string &custom_preset) const;
-
- protected:
-  /// Override control to change settings of the climate device.
-  void control(const climate::ClimateCall &call) override;
-  /// Return the traits of this controller.
-  climate::ClimateTraits traits() override;
-
-  const QueryFrame query_frame_;
-  const PowerQueryFrame power_frame_;
-  CommandFrame cmd_frame_;
-  midea_dongle::MideaDongle *parent_{nullptr};
-  sensor::Sensor *outdoor_sensor_{nullptr};
-  sensor::Sensor *humidity_sensor_{nullptr};
-  sensor::Sensor *power_sensor_{nullptr};
-  uint8_t request_num_{0};
-  bool ctrl_request_{false};
-  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::set<std::string> traits_custom_fan_modes_{{}};
-  std::set<std::string> traits_custom_presets_{{}};
-};
-
-}  // namespace midea_ac
-}  // namespace esphome
diff --git a/esphome/components/midea_ac/midea_frame.cpp b/esphome/components/midea_ac/midea_frame.cpp
deleted file mode 100644
index c0a5ce4b55..0000000000
--- a/esphome/components/midea_ac/midea_frame.cpp
+++ /dev/null
@@ -1,238 +0,0 @@
-#include "midea_frame.h"
-
-namespace esphome {
-namespace midea_ac {
-
-static const char *const 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, 0x21, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x41, 0x81,
-                                    0x00, 0xFF, 0x03, 0xFF, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x37, 0x31};
-
-const uint8_t PowerQueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x21,
-                                         0x01, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                                         0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x17, 0x6A};
-
-const uint8_t CommandFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x40, 0x00,
-                                      0x00, 0x00, 0x7F, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                                      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
-
-float PropertiesFrame::get_target_temp() const {
-  float temp = static_cast<float>((this->pbuf_[12] & 0x0F) + 16);
-  if (this->pbuf_[12] & 0x10)
-    temp += 0.5;
-  return temp;
-}
-
-void PropertiesFrame::set_target_temp(float temp) {
-  uint8_t tmp = static_cast<uint8_t>(temp * 16.0) + 4;
-  tmp = ((tmp & 8) << 1) | (tmp >> 4);
-  this->pbuf_[12] &= ~0x1F;
-  this->pbuf_[12] |= tmp;
-}
-
-static float i16tof(int16_t in) { return static_cast<float>(in - 50) / 2.0; }
-float PropertiesFrame::get_indoor_temp() const { return i16tof(this->pbuf_[21]); }
-float PropertiesFrame::get_outdoor_temp() const { return i16tof(this->pbuf_[22]); }
-float PropertiesFrame::get_humidity_setpoint() const { return static_cast<float>(this->pbuf_[29] & 0x7F); }
-
-climate::ClimateMode PropertiesFrame::get_mode() const {
-  if (!this->get_power_())
-    return climate::CLIMATE_MODE_OFF;
-  switch (this->pbuf_[12] >> 5) {
-    case MIDEA_MODE_AUTO:
-      return climate::CLIMATE_MODE_HEAT_COOL;
-    case MIDEA_MODE_COOL:
-      return climate::CLIMATE_MODE_COOL;
-    case MIDEA_MODE_DRY:
-      return climate::CLIMATE_MODE_DRY;
-    case MIDEA_MODE_HEAT:
-      return climate::CLIMATE_MODE_HEAT;
-    case MIDEA_MODE_FAN_ONLY:
-      return climate::CLIMATE_MODE_FAN_ONLY;
-    default:
-      return climate::CLIMATE_MODE_OFF;
-  }
-}
-
-void PropertiesFrame::set_mode(climate::ClimateMode mode) {
-  uint8_t m;
-  switch (mode) {
-    case climate::CLIMATE_MODE_HEAT_COOL:
-      m = MIDEA_MODE_AUTO;
-      break;
-    case climate::CLIMATE_MODE_COOL:
-      m = MIDEA_MODE_COOL;
-      break;
-    case climate::CLIMATE_MODE_DRY:
-      m = MIDEA_MODE_DRY;
-      break;
-    case climate::CLIMATE_MODE_HEAT:
-      m = MIDEA_MODE_HEAT;
-      break;
-    case climate::CLIMATE_MODE_FAN_ONLY:
-      m = MIDEA_MODE_FAN_ONLY;
-      break;
-    default:
-      this->set_power_(false);
-      return;
-  }
-  this->set_power_(true);
-  this->pbuf_[12] &= ~0xE0;
-  this->pbuf_[12] |= m << 5;
-}
-
-optional<climate::ClimatePreset> PropertiesFrame::get_preset() const {
-  if (this->get_eco_mode())
-    return climate::CLIMATE_PRESET_ECO;
-  if (this->get_sleep_mode())
-    return climate::CLIMATE_PRESET_SLEEP;
-  if (this->get_turbo_mode())
-    return climate::CLIMATE_PRESET_BOOST;
-  return climate::CLIMATE_PRESET_NONE;
-}
-
-void PropertiesFrame::set_preset(climate::ClimatePreset preset) {
-  this->clear_presets();
-  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;
-  }
-}
-
-void PropertiesFrame::clear_presets() {
-  this->set_eco_mode(false);
-  this->set_sleep_mode(false);
-  this->set_turbo_mode(false);
-  this->set_freeze_protection_mode(false);
-}
-
-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) {
-  this->clear_presets();
-  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:
-      return climate::CLIMATE_FAN_LOW;
-    case MIDEA_FAN_MEDIUM:
-      return climate::CLIMATE_FAN_MEDIUM;
-    case MIDEA_FAN_HIGH:
-      return climate::CLIMATE_FAN_HIGH;
-    default:
-      return climate::CLIMATE_FAN_AUTO;
-  }
-}
-
-void PropertiesFrame::set_fan_mode(climate::ClimateFanMode mode) {
-  uint8_t m;
-  switch (mode) {
-    case climate::CLIMATE_FAN_LOW:
-      m = MIDEA_FAN_LOW;
-      break;
-    case climate::CLIMATE_FAN_MEDIUM:
-      m = MIDEA_FAN_MEDIUM;
-      break;
-    case climate::CLIMATE_FAN_HIGH:
-      m = MIDEA_FAN_HIGH;
-      break;
-    default:
-      m = MIDEA_FAN_AUTO;
-      break;
-  }
-  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:
-      return climate::CLIMATE_SWING_VERTICAL;
-    case MIDEA_SWING_HORIZONTAL:
-      return climate::CLIMATE_SWING_HORIZONTAL;
-    case MIDEA_SWING_BOTH:
-      return climate::CLIMATE_SWING_BOTH;
-    default:
-      return climate::CLIMATE_SWING_OFF;
-  }
-}
-
-void PropertiesFrame::set_swing_mode(climate::ClimateSwingMode mode) {
-  uint8_t m;
-  switch (mode) {
-    case climate::CLIMATE_SWING_VERTICAL:
-      m = MIDEA_SWING_VERTICAL;
-      break;
-    case climate::CLIMATE_SWING_HORIZONTAL:
-      m = MIDEA_SWING_HORIZONTAL;
-      break;
-    case climate::CLIMATE_SWING_BOTH:
-      m = MIDEA_SWING_BOTH;
-      break;
-    default:
-      m = MIDEA_SWING_OFF;
-      break;
-  }
-  this->pbuf_[17] = 0x30 | m;
-}
-
-float PropertiesFrame::get_power_usage() const {
-  uint32_t power = 0;
-  const uint8_t *ptr = this->pbuf_ + 28;
-  for (uint32_t weight = 1;; weight *= 10, ptr--) {
-    power += (*ptr % 16) * weight;
-    weight *= 10;
-    power += (*ptr / 16) * weight;
-    if (weight == 100000)
-      return static_cast<float>(power) * 0.1;
-  }
-}
-
-}  // namespace midea_ac
-}  // namespace esphome
diff --git a/esphome/components/midea_ac/midea_frame.h b/esphome/components/midea_ac/midea_frame.h
deleted file mode 100644
index e1d6fed49d..0000000000
--- a/esphome/components/midea_ac/midea_frame.h
+++ /dev/null
@@ -1,165 +0,0 @@
-#pragma once
-#include "esphome/components/climate/climate.h"
-#include "esphome/components/midea_dongle/midea_frame.h"
-
-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
-  MIDEA_MODE_AUTO = 1,
-  /// The Midea device is manually set to cool mode (not in auto mode!)
-  MIDEA_MODE_COOL = 2,
-  /// The Midea device is manually set to dry mode
-  MIDEA_MODE_DRY = 3,
-  /// The Midea device is manually set to heat mode (not in auto mode!)
-  MIDEA_MODE_HEAT = 4,
-  /// The Midea device is manually set to fan only mode
-  MIDEA_MODE_FAN_ONLY = 5,
-};
-
-/// Enum for all modes a Midea fan can be in
-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
-enum MideaSwingMode : uint8_t {
-  /// The sing mode is set to Off
-  MIDEA_SWING_OFF = 0b0000,
-  /// The fan mode is set to Both
-  MIDEA_SWING_BOTH = 0b1111,
-  /// The fan mode is set to Vertical
-  MIDEA_SWING_VERTICAL = 0b1100,
-  /// The fan mode is set to Horizontal
-  MIDEA_SWING_HORIZONTAL = 0b0011,
-};
-
-class PropertiesFrame : public midea_dongle::BaseFrame {
- public:
-  PropertiesFrame() = delete;
-  PropertiesFrame(uint8_t *data) : BaseFrame(data) {}
-  PropertiesFrame(const Frame &frame) : BaseFrame(frame) {}
-
-  bool has_properties() const {
-    return this->has_response_type(0xC0) && (this->has_type(0x03) || this->has_type(0x02));
-  }
-
-  bool has_power_info() const { return this->has_response_type(0xC1); }
-
-  /* TARGET TEMPERATURE */
-
-  float get_target_temp() const;
-  void set_target_temp(float temp);
-
-  /* MODE */
-  climate::ClimateMode get_mode() const;
-  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);
-
-  /* INDOOR TEMPERATURE */
-  float get_indoor_temp() const;
-
-  /* OUTDOOR TEMPERATURE */
-  float get_outdoor_temp() const;
-
-  /* HUMIDITY SETPOINT */
-  float get_humidity_setpoint() const;
-
-  /* ECO MODE */
-  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_[18] & 0x20 || this->pbuf_[20] & 0x02; }
-  void set_turbo_mode(bool state) {
-    this->set_bytemask_(18, 0x20, state);
-    this->set_bytemask_(20, 0x02, 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);
-  void clear_presets();
-
-  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;
-
-  /// Set properties from another frame
-  void set_properties(const PropertiesFrame &p) { memcpy(this->pbuf_ + 11, p.data() + 11, 10); }
-
- protected:
-  /* POWER */
-  bool get_power_() const { return this->pbuf_[11] & 0x01; }
-  void set_power_(bool state) { this->set_bytemask_(11, 0x01, state); }
-};
-
-// Query state frame (read-only)
-class QueryFrame : public midea_dongle::StaticFrame<midea_dongle::Frame> {
- public:
-  QueryFrame() : StaticFrame(FPSTR(this->INIT)) {}
-
- private:
-  static const uint8_t PROGMEM INIT[];
-};
-
-// Power query state frame (read-only)
-class PowerQueryFrame : public midea_dongle::StaticFrame<midea_dongle::Frame> {
- public:
-  PowerQueryFrame() : StaticFrame(FPSTR(this->INIT)) {}
-
- private:
-  static const uint8_t PROGMEM INIT[];
-};
-
-// Command frame
-class CommandFrame : public midea_dongle::StaticFrame<PropertiesFrame> {
- public:
-  CommandFrame() : StaticFrame(FPSTR(this->INIT)) {}
-  void set_beeper_feedback(bool state) { this->set_bytemask_(11, 0x40, state); }
-
- private:
-  static const uint8_t PROGMEM INIT[];
-};
-
-}  // namespace midea_ac
-}  // namespace esphome
diff --git a/esphome/components/midea_dongle/__init__.py b/esphome/components/midea_dongle/__init__.py
deleted file mode 100644
index daa8ea6657..0000000000
--- a/esphome/components/midea_dongle/__init__.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import esphome.codegen as cg
-import esphome.config_validation as cv
-from esphome.components import uart
-from esphome.const import CONF_ID
-
-DEPENDENCIES = ["wifi", "uart"]
-CODEOWNERS = ["@dudanov"]
-
-midea_dongle_ns = cg.esphome_ns.namespace("midea_dongle")
-MideaDongle = midea_dongle_ns.class_("MideaDongle", cg.Component, uart.UARTDevice)
-
-CONF_MIDEA_DONGLE_ID = "midea_dongle_id"
-CONF_STRENGTH_ICON = "strength_icon"
-CONFIG_SCHEMA = (
-    cv.Schema(
-        {
-            cv.GenerateID(): cv.declare_id(MideaDongle),
-            cv.Optional(CONF_STRENGTH_ICON, default=False): cv.boolean,
-        }
-    )
-    .extend(cv.COMPONENT_SCHEMA)
-    .extend(uart.UART_DEVICE_SCHEMA)
-)
-
-
-async def to_code(config):
-    var = cg.new_Pvariable(config[CONF_ID])
-    await cg.register_component(var, config)
-    await uart.register_uart_device(var, config)
-    cg.add(var.use_strength_icon(config[CONF_STRENGTH_ICON]))
diff --git a/esphome/components/midea_dongle/midea_dongle.cpp b/esphome/components/midea_dongle/midea_dongle.cpp
deleted file mode 100644
index 7e3683a964..0000000000
--- a/esphome/components/midea_dongle/midea_dongle.cpp
+++ /dev/null
@@ -1,98 +0,0 @@
-#include "midea_dongle.h"
-#include "esphome/core/log.h"
-#include "esphome/core/helpers.h"
-
-namespace esphome {
-namespace midea_dongle {
-
-static const char *const TAG = "midea_dongle";
-
-void MideaDongle::loop() {
-  while (this->available()) {
-    const uint8_t rx = this->read();
-    if (this->idx_ <= OFFSET_LENGTH) {
-      if (this->idx_ == OFFSET_LENGTH) {
-        if (rx <= OFFSET_BODY || rx >= sizeof(this->buf_)) {
-          this->reset_();
-          continue;
-        }
-        this->cnt_ = rx;
-      } else if (rx != SYNC_BYTE) {
-        continue;
-      }
-    }
-    this->buf_[this->idx_++] = rx;
-    if (--this->cnt_)
-      continue;
-    this->reset_();
-    const BaseFrame frame(this->buf_);
-    ESP_LOGD(TAG, "RX: %s", frame.to_string().c_str());
-    if (!frame.is_valid()) {
-      ESP_LOGW(TAG, "RX: frame check failed!");
-      continue;
-    }
-    if (frame.get_type() == QUERY_NETWORK) {
-      this->notify_.set_type(QUERY_NETWORK);
-      this->need_notify_ = true;
-      continue;
-    }
-    if (this->appliance_ != nullptr)
-      this->appliance_->on_frame(frame);
-  }
-}
-
-void MideaDongle::update() {
-  const bool is_conn = WiFi.isConnected();
-  uint8_t wifi_strength = 0;
-  if (!this->rssi_timer_) {
-    if (is_conn)
-      wifi_strength = 4;
-  } else if (is_conn) {
-    if (--this->rssi_timer_) {
-      wifi_strength = this->notify_.get_signal_strength();
-    } else {
-      this->rssi_timer_ = 60;
-      const int32_t dbm = WiFi.RSSI();
-      if (dbm > -63)
-        wifi_strength = 4;
-      else if (dbm > -75)
-        wifi_strength = 3;
-      else if (dbm > -88)
-        wifi_strength = 2;
-      else if (dbm > -100)
-        wifi_strength = 1;
-    }
-  } else {
-    this->rssi_timer_ = 1;
-  }
-  if (this->notify_.is_connected() != is_conn) {
-    this->notify_.set_connected(is_conn);
-    this->need_notify_ = true;
-  }
-  if (this->notify_.get_signal_strength() != wifi_strength) {
-    this->notify_.set_signal_strength(wifi_strength);
-    this->need_notify_ = true;
-  }
-  if (!--this->notify_timer_) {
-    this->notify_.set_type(NETWORK_NOTIFY);
-    this->need_notify_ = true;
-  }
-  if (this->need_notify_) {
-    ESP_LOGD(TAG, "TX: notify WiFi STA %s, signal strength %d", is_conn ? "connected" : "not connected", wifi_strength);
-    this->need_notify_ = false;
-    this->notify_timer_ = 600;
-    this->notify_.finalize();
-    this->write_frame(this->notify_);
-    return;
-  }
-  if (this->appliance_ != nullptr)
-    this->appliance_->on_update();
-}
-
-void MideaDongle::write_frame(const Frame &frame) {
-  this->write_array(frame.data(), frame.size());
-  ESP_LOGD(TAG, "TX: %s", frame.to_string().c_str());
-}
-
-}  // namespace midea_dongle
-}  // namespace esphome
diff --git a/esphome/components/midea_dongle/midea_dongle.h b/esphome/components/midea_dongle/midea_dongle.h
deleted file mode 100644
index a7dfb9cf25..0000000000
--- a/esphome/components/midea_dongle/midea_dongle.h
+++ /dev/null
@@ -1,56 +0,0 @@
-#pragma once
-#include "esphome/core/component.h"
-#include "esphome/components/wifi/wifi_component.h"
-#include "esphome/components/uart/uart.h"
-#include "midea_frame.h"
-
-namespace esphome {
-namespace midea_dongle {
-
-enum MideaApplianceType : uint8_t { DEHUMIDIFIER = 0xA1, AIR_CONDITIONER = 0xAC, BROADCAST = 0xFF };
-enum MideaMessageType : uint8_t {
-  DEVICE_CONTROL = 0x02,
-  DEVICE_QUERY = 0x03,
-  NETWORK_NOTIFY = 0x0D,
-  QUERY_NETWORK = 0x63,
-};
-
-struct MideaAppliance {
-  /// Calling on update event
-  virtual void on_update() = 0;
-  /// Calling on frame receive event
-  virtual void on_frame(const Frame &frame) = 0;
-};
-
-class MideaDongle : public PollingComponent, public uart::UARTDevice {
- public:
-  MideaDongle() : PollingComponent(1000) {}
-  float get_setup_priority() const override { return setup_priority::LATE; }
-  void update() override;
-  void loop() override;
-  void set_appliance(MideaAppliance *app) { this->appliance_ = app; }
-  void use_strength_icon(bool state) { this->rssi_timer_ = state; }
-  void write_frame(const Frame &frame);
-
- protected:
-  MideaAppliance *appliance_{nullptr};
-  NotifyFrame notify_;
-  unsigned notify_timer_{1};
-  // Buffer
-  uint8_t buf_[36];
-  // Index
-  uint8_t idx_{0};
-  // Reverse receive counter
-  uint8_t cnt_{2};
-  uint8_t rssi_timer_{0};
-  bool need_notify_{false};
-
-  // Reset receiver state
-  void reset_() {
-    this->idx_ = 0;
-    this->cnt_ = 2;
-  }
-};
-
-}  // namespace midea_dongle
-}  // namespace esphome
diff --git a/esphome/components/midea_dongle/midea_frame.cpp b/esphome/components/midea_dongle/midea_frame.cpp
deleted file mode 100644
index acb3feee5f..0000000000
--- a/esphome/components/midea_dongle/midea_frame.cpp
+++ /dev/null
@@ -1,95 +0,0 @@
-#include "midea_frame.h"
-
-namespace esphome {
-namespace midea_dongle {
-
-const uint8_t BaseFrame::CRC_TABLE[] = {
-    0x00, 0x5E, 0xBC, 0xE2, 0x61, 0x3F, 0xDD, 0x83, 0xC2, 0x9C, 0x7E, 0x20, 0xA3, 0xFD, 0x1F, 0x41, 0x9D, 0xC3, 0x21,
-    0x7F, 0xFC, 0xA2, 0x40, 0x1E, 0x5F, 0x01, 0xE3, 0xBD, 0x3E, 0x60, 0x82, 0xDC, 0x23, 0x7D, 0x9F, 0xC1, 0x42, 0x1C,
-    0xFE, 0xA0, 0xE1, 0xBF, 0x5D, 0x03, 0x80, 0xDE, 0x3C, 0x62, 0xBE, 0xE0, 0x02, 0x5C, 0xDF, 0x81, 0x63, 0x3D, 0x7C,
-    0x22, 0xC0, 0x9E, 0x1D, 0x43, 0xA1, 0xFF, 0x46, 0x18, 0xFA, 0xA4, 0x27, 0x79, 0x9B, 0xC5, 0x84, 0xDA, 0x38, 0x66,
-    0xE5, 0xBB, 0x59, 0x07, 0xDB, 0x85, 0x67, 0x39, 0xBA, 0xE4, 0x06, 0x58, 0x19, 0x47, 0xA5, 0xFB, 0x78, 0x26, 0xC4,
-    0x9A, 0x65, 0x3B, 0xD9, 0x87, 0x04, 0x5A, 0xB8, 0xE6, 0xA7, 0xF9, 0x1B, 0x45, 0xC6, 0x98, 0x7A, 0x24, 0xF8, 0xA6,
-    0x44, 0x1A, 0x99, 0xC7, 0x25, 0x7B, 0x3A, 0x64, 0x86, 0xD8, 0x5B, 0x05, 0xE7, 0xB9, 0x8C, 0xD2, 0x30, 0x6E, 0xED,
-    0xB3, 0x51, 0x0F, 0x4E, 0x10, 0xF2, 0xAC, 0x2F, 0x71, 0x93, 0xCD, 0x11, 0x4F, 0xAD, 0xF3, 0x70, 0x2E, 0xCC, 0x92,
-    0xD3, 0x8D, 0x6F, 0x31, 0xB2, 0xEC, 0x0E, 0x50, 0xAF, 0xF1, 0x13, 0x4D, 0xCE, 0x90, 0x72, 0x2C, 0x6D, 0x33, 0xD1,
-    0x8F, 0x0C, 0x52, 0xB0, 0xEE, 0x32, 0x6C, 0x8E, 0xD0, 0x53, 0x0D, 0xEF, 0xB1, 0xF0, 0xAE, 0x4C, 0x12, 0x91, 0xCF,
-    0x2D, 0x73, 0xCA, 0x94, 0x76, 0x28, 0xAB, 0xF5, 0x17, 0x49, 0x08, 0x56, 0xB4, 0xEA, 0x69, 0x37, 0xD5, 0x8B, 0x57,
-    0x09, 0xEB, 0xB5, 0x36, 0x68, 0x8A, 0xD4, 0x95, 0xCB, 0x29, 0x77, 0xF4, 0xAA, 0x48, 0x16, 0xE9, 0xB7, 0x55, 0x0B,
-    0x88, 0xD6, 0x34, 0x6A, 0x2B, 0x75, 0x97, 0xC9, 0x4A, 0x14, 0xF6, 0xA8, 0x74, 0x2A, 0xC8, 0x96, 0x15, 0x4B, 0xA9,
-    0xF7, 0xB6, 0xE8, 0x0A, 0x54, 0xD7, 0x89, 0x6B, 0x35};
-
-const uint8_t NotifyFrame::INIT[] = {0xAA, 0x1F, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x0D, 0x01,
-                                     0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00,
-                                     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
-
-bool BaseFrame::is_valid() const { return /*this->has_valid_crc_() &&*/ this->has_valid_cs_(); }
-
-void BaseFrame::finalize() {
-  this->update_crc_();
-  this->update_cs_();
-}
-
-void BaseFrame::update_crc_() {
-  uint8_t crc = 0;
-  uint8_t *ptr = this->pbuf_ + OFFSET_BODY;
-  uint8_t len = this->length_() - OFFSET_BODY;
-  while (--len)
-    crc = pgm_read_byte(BaseFrame::CRC_TABLE + (crc ^ *ptr++));
-  *ptr = crc;
-}
-
-void BaseFrame::update_cs_() {
-  uint8_t cs = 0;
-  uint8_t *ptr = this->pbuf_ + OFFSET_LENGTH;
-  uint8_t len = this->length_();
-  while (--len)
-    cs -= *ptr++;
-  *ptr = cs;
-}
-
-bool BaseFrame::has_valid_crc_() const {
-  uint8_t crc = 0;
-  uint8_t len = this->length_() - OFFSET_BODY;
-  const uint8_t *ptr = this->pbuf_ + OFFSET_BODY;
-  for (; len; ptr++, len--)
-    crc = pgm_read_byte(BaseFrame::CRC_TABLE + (crc ^ *ptr));
-  return !crc;
-}
-
-bool BaseFrame::has_valid_cs_() const {
-  uint8_t cs = 0;
-  uint8_t len = this->length_();
-  const uint8_t *ptr = this->pbuf_ + OFFSET_LENGTH;
-  for (; len; ptr++, len--)
-    cs -= *ptr;
-  return !cs;
-}
-
-void BaseFrame::set_bytemask_(uint8_t idx, uint8_t mask, bool state) {
-  uint8_t *dst = this->pbuf_ + idx;
-  if (state)
-    *dst |= mask;
-  else
-    *dst &= ~mask;
-}
-
-static char u4hex(uint8_t num) { return num + ((num < 10) ? '0' : ('A' - 10)); }
-
-String Frame::to_string() const {
-  String ret;
-  char buf[4];
-  buf[2] = ' ';
-  buf[3] = '\0';
-  ret.reserve(3 * 36);
-  const uint8_t *it = this->data();
-  for (size_t i = 0; i < this->size(); i++, it++) {
-    buf[0] = u4hex(*it >> 4);
-    buf[1] = u4hex(*it & 15);
-    ret.concat(buf);
-  }
-  return ret;
-}
-
-}  // namespace midea_dongle
-}  // namespace esphome
diff --git a/esphome/components/midea_dongle/midea_frame.h b/esphome/components/midea_dongle/midea_frame.h
deleted file mode 100644
index ce89cc636e..0000000000
--- a/esphome/components/midea_dongle/midea_frame.h
+++ /dev/null
@@ -1,104 +0,0 @@
-#pragma once
-#include "esphome/core/component.h"
-
-namespace esphome {
-namespace midea_dongle {
-
-static const uint8_t OFFSET_START = 0;
-static const uint8_t OFFSET_LENGTH = 1;
-static const uint8_t OFFSET_APPTYPE = 2;
-static const uint8_t OFFSET_BODY = 10;
-static const uint8_t SYNC_BYTE = 0xAA;
-
-class Frame {
- public:
-  Frame() = delete;
-  Frame(uint8_t *data) : pbuf_(data) {}
-  Frame(const Frame &frame) : pbuf_(frame.data()) {}
-
-  // Frame buffer
-  uint8_t *data() const { return this->pbuf_; }
-  // Frame size
-  uint8_t size() const { return this->length_() + OFFSET_LENGTH; }
-  uint8_t app_type() const { return this->pbuf_[OFFSET_APPTYPE]; }
-
-  template<typename T> typename std::enable_if<std::is_base_of<Frame, T>::value, T>::type as() const {
-    return T(*this);
-  }
-  String to_string() const;
-
- protected:
-  uint8_t *pbuf_;
-  uint8_t length_() const { return this->pbuf_[OFFSET_LENGTH]; }
-};
-
-class BaseFrame : public Frame {
- public:
-  BaseFrame() = delete;
-  BaseFrame(uint8_t *data) : Frame(data) {}
-  BaseFrame(const Frame &frame) : Frame(frame) {}
-
-  // Check for valid
-  bool is_valid() const;
-  // Prepare for sending to device
-  void finalize();
-  uint8_t get_type() const { return this->pbuf_[9]; }
-  void set_type(uint8_t value) { this->pbuf_[9] = value; }
-  bool has_response_type(uint8_t type) const { return this->resp_type_() == type; }
-  bool has_type(uint8_t type) const { return this->get_type() == type; }
-
- protected:
-  static const uint8_t PROGMEM CRC_TABLE[256];
-  void set_bytemask_(uint8_t idx, uint8_t mask, bool state);
-  uint8_t resp_type_() const { return this->pbuf_[OFFSET_BODY]; }
-  bool has_valid_crc_() const;
-  bool has_valid_cs_() const;
-  void update_crc_();
-  void update_cs_();
-};
-
-template<typename T = Frame, size_t buf_size = 36> class StaticFrame : public T {
- public:
-  // Default constructor
-  StaticFrame() : T(this->buf_) {}
-  // Copy constructor
-  StaticFrame(const Frame &src) : T(this->buf_) {
-    if (src.length_() < sizeof(this->buf_)) {
-      memcpy(this->buf_, src.data(), src.length_() + OFFSET_LENGTH);
-    }
-  }
-  // Constructor for RAM data
-  StaticFrame(const uint8_t *src) : T(this->buf_) {
-    const uint8_t len = src[OFFSET_LENGTH];
-    if (len < sizeof(this->buf_)) {
-      memcpy(this->buf_, src, len + OFFSET_LENGTH);
-    }
-  }
-  // Constructor for PROGMEM data
-  StaticFrame(const __FlashStringHelper *pgm) : T(this->buf_) {
-    const uint8_t *src = reinterpret_cast<decltype(src)>(pgm);
-    const uint8_t len = pgm_read_byte(src + OFFSET_LENGTH);
-    if (len < sizeof(this->buf_)) {
-      memcpy_P(this->buf_, src, len + OFFSET_LENGTH);
-    }
-  }
-
- protected:
-  uint8_t buf_[buf_size];
-};
-
-// Device network notification frame
-class NotifyFrame : public midea_dongle::StaticFrame<BaseFrame, 32> {
- public:
-  NotifyFrame() : StaticFrame(FPSTR(NotifyFrame::INIT)) {}
-  void set_signal_strength(uint8_t value) { this->pbuf_[12] = value; }
-  uint8_t get_signal_strength() const { return this->pbuf_[12]; }
-  void set_connected(bool state) { this->pbuf_[18] = state ? 0 : 1; }
-  bool is_connected() const { return !this->pbuf_[18]; }
-
- private:
-  static const uint8_t PROGMEM INIT[];
-};
-
-}  // namespace midea_dongle
-}  // namespace esphome
diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp
index 13acdcacd8..6ddc080b53 100644
--- a/esphome/components/mqtt/mqtt_component.cpp
+++ b/esphome/components/mqtt/mqtt_component.cpp
@@ -102,9 +102,7 @@ bool MQTTComponent::send_discovery_() {
         device_info["identifiers"] = get_mac_address();
         device_info["name"] = node_name;
         device_info["sw_version"] = "esphome v" ESPHOME_VERSION " " + App.get_compilation_time();
-#ifdef ARDUINO_BOARD
-        device_info["model"] = ARDUINO_BOARD;
-#endif
+        device_info["model"] = ESPHOME_BOARD;
         device_info["manufacturer"] = "espressif";
       },
       0, discovery_info.retain);
diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp
index 4171dae04c..b8eecf0ff3 100644
--- a/esphome/components/mqtt/mqtt_fan.cpp
+++ b/esphome/components/mqtt/mqtt_fan.cpp
@@ -65,7 +65,9 @@ void MQTTFanComponent::setup() {
 
   if (this->state_->get_traits().supports_speed()) {
     this->subscribe(this->get_speed_command_topic(), [this](const std::string &topic, const std::string &payload) {
-      this->state_->make_call().set_speed(payload.c_str()).perform();
+      this->state_->make_call()
+          .set_speed(payload.c_str())  // NOLINT(clang-diagnostic-deprecated-declarations)
+          .perform();
     });
   }
 
@@ -98,17 +100,18 @@ bool MQTTFanComponent::publish_state() {
   auto traits = this->state_->get_traits();
   if (traits.supports_speed()) {
     const char *payload;
+    // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations)
     switch (fan::speed_level_to_enum(this->state_->speed, traits.supported_speed_count())) {
-      case FAN_SPEED_LOW: {
+      case FAN_SPEED_LOW: {  // NOLINT(clang-diagnostic-deprecated-declarations)
         payload = "low";
         break;
       }
-      case FAN_SPEED_MEDIUM: {
+      case FAN_SPEED_MEDIUM: {  // NOLINT(clang-diagnostic-deprecated-declarations)
         payload = "medium";
         break;
       }
       default:
-      case FAN_SPEED_HIGH: {
+      case FAN_SPEED_HIGH: {  // NOLINT(clang-diagnostic-deprecated-declarations)
         payload = "high";
         break;
       }
diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp
index b702e6e425..f53be9c010 100644
--- a/esphome/components/mqtt/mqtt_light.cpp
+++ b/esphome/components/mqtt/mqtt_light.cpp
@@ -38,18 +38,23 @@ void MQTTJSONLightComponent::send_discovery(JsonObject &root, mqtt::SendDiscover
 
   root["color_mode"] = true;
   JsonArray &color_modes = root.createNestedArray("supported_color_modes");
-  if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE))
+  if (traits.supports_color_mode(ColorMode::ON_OFF))
+    color_modes.add("onoff");
+  if (traits.supports_color_mode(ColorMode::BRIGHTNESS))
+    color_modes.add("brightness");
+  if (traits.supports_color_mode(ColorMode::WHITE))
+    color_modes.add("white");
+  if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) ||
+      traits.supports_color_mode(ColorMode::COLD_WARM_WHITE))
     color_modes.add("color_temp");
   if (traits.supports_color_mode(ColorMode::RGB))
     color_modes.add("rgb");
-  if (traits.supports_color_mode(ColorMode::RGB_WHITE))
+  if (traits.supports_color_mode(ColorMode::RGB_WHITE) ||
+      // HA doesn't support RGBCT, and there's no CWWW->CT emulation in ESPHome yet, so ignore CT control for now
+      traits.supports_color_mode(ColorMode::RGB_COLOR_TEMPERATURE))
     color_modes.add("rgbw");
   if (traits.supports_color_mode(ColorMode::RGB_COLD_WARM_WHITE))
     color_modes.add("rgbww");
-  if (traits.supports_color_mode(ColorMode::BRIGHTNESS))
-    color_modes.add("brightness");
-  if (traits.supports_color_mode(ColorMode::ON_OFF))
-    color_modes.add("onoff");
 
   // legacy API
   if (traits.supports_color_capability(ColorCapability::BRIGHTNESS))
diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp
index ce7e89c584..d440e30fc4 100644
--- a/esphome/components/mqtt/mqtt_sensor.cpp
+++ b/esphome/components/mqtt/mqtt_sensor.cpp
@@ -61,8 +61,8 @@ void MQTTSensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryCo
   if (this->sensor_->get_force_update())
     root["force_update"] = true;
 
-  if (this->sensor_->state_class == sensor::STATE_CLASS_MEASUREMENT)
-    root["state_class"] = "measurement";
+  if (this->sensor_->state_class != STATE_CLASS_NONE)
+    root["state_class"] = state_class_to_string(this->sensor_->state_class);
 
   config.command_topic = false;
 }
diff --git a/esphome/components/neopixelbus/neopixelbus_light.h b/esphome/components/neopixelbus/neopixelbus_light.h
index c7f7badc5a..6fa3fb3cd9 100644
--- a/esphome/components/neopixelbus/neopixelbus_light.h
+++ b/esphome/components/neopixelbus/neopixelbus_light.h
@@ -1,13 +1,14 @@
 #pragma once
 
+#include "esphome/core/macros.h"
 #include "esphome/core/component.h"
 #include "esphome/core/helpers.h"
 #include "esphome/core/color.h"
 #include "esphome/components/light/light_output.h"
 #include "esphome/components/light/addressable_light.h"
 
-#ifdef ARDUINO_ESP8266_RELEASE_2_3_0
-#error The NeoPixelBus library requires at least arduino_core_version 2.4.x
+#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0)
+#error The NeoPixelBus library requires at least arduino_version 2.4.x
 #endif
 
 #include "NeoPixelBus.h"
@@ -82,10 +83,7 @@ class NeoPixelBusLightOutputBase : public light::AddressableLight {
     this->controller_->Begin();
   }
 
-  void loop() override {
-    if (!this->should_show_())
-      return;
-
+  void write_state(light::LightState *state) override {
     this->mark_shown_();
     this->controller_->Dirty();
 
diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp
index 71f8101704..ac72befb9e 100644
--- a/esphome/components/ota/ota_component.cpp
+++ b/esphome/components/ota/ota_component.cpp
@@ -178,28 +178,29 @@ void OTAComponent::handle_() {
 #endif
 
   if (!Update.begin(ota_size, U_FLASH)) {
+    uint8_t error = Update.getError();
     StreamString ss;
     Update.printError(ss);
 #ifdef ARDUINO_ARCH_ESP8266
-    if (ss.indexOf("Invalid bootstrapping") != -1) {
+    if (error == UPDATE_ERROR_BOOTSTRAP) {
       error_code = OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING;
       goto error;
     }
-    if (ss.indexOf("new Flash config wrong") != -1 || ss.indexOf("new Flash config wsong") != -1) {
+    if (error == UPDATE_ERROR_NEW_FLASH_CONFIG) {
       error_code = OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG;
       goto error;
     }
-    if (ss.indexOf("Flash config wrong real") != -1 || ss.indexOf("Flash config wsong real") != -1) {
+    if (error == UPDATE_ERROR_FLASH_CONFIG) {
       error_code = OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG;
       goto error;
     }
-    if (ss.indexOf("Not Enough Space") != -1) {
+    if (error == UPDATE_ERROR_SPACE) {
       error_code = OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE;
       goto error;
     }
 #endif
 #ifdef ARDUINO_ARCH_ESP32
-    if (ss.indexOf("Bad Size Given") != -1) {
+    if (error == UPDATE_ERROR_SIZE) {
       error_code = OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE;
       goto error;
     }
diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py
index 8c5c9a0144..330ffc2bf2 100644
--- a/esphome/components/packages/__init__.py
+++ b/esphome/components/packages/__init__.py
@@ -1,6 +1,19 @@
+import re
+from pathlib import Path
+from esphome.core import EsphomeError
+
+from esphome import git, yaml_util
+from esphome.const import (
+    CONF_FILE,
+    CONF_FILES,
+    CONF_PACKAGES,
+    CONF_REF,
+    CONF_REFRESH,
+    CONF_URL,
+)
 import esphome.config_validation as cv
 
-from esphome.const import CONF_PACKAGES
+DOMAIN = CONF_PACKAGES
 
 
 def _merge_package(full_old, full_new):
@@ -23,11 +36,119 @@ def _merge_package(full_old, full_new):
     return merge(full_old, full_new)
 
 
+def validate_git_package(config: dict):
+    new_config = config
+    for key, conf in config.items():
+        if CONF_URL in conf:
+            try:
+                conf = BASE_SCHEMA(conf)
+                if CONF_FILE in conf:
+                    new_config[key][CONF_FILES] = [conf[CONF_FILE]]
+                    del new_config[key][CONF_FILE]
+            except cv.MultipleInvalid as e:
+                with cv.prepend_path([key]):
+                    raise e
+            except cv.Invalid as e:
+                raise cv.Invalid(
+                    "Extra keys not allowed in git based package",
+                    path=[key] + e.path,
+                ) from e
+    return new_config
+
+
+def validate_yaml_filename(value):
+    value = cv.string(value)
+
+    if not (value.endswith(".yaml") or value.endswith(".yml")):
+        raise cv.Invalid("Only YAML (.yaml / .yml) files are supported.")
+
+    return value
+
+
+def validate_source_shorthand(value):
+    if not isinstance(value, str):
+        raise cv.Invalid("Shorthand only for strings")
+
+    m = re.match(
+        r"github://([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)/([a-zA-Z0-9\-_.\./]+)(?:@([a-zA-Z0-9\-_.\./]+))?",
+        value,
+    )
+    if m is None:
+        raise cv.Invalid(
+            "Source is not a file system path or in expected github://username/name/[sub-folder/]file-path.yml[@branch-or-tag] format!"
+        )
+
+    conf = {
+        CONF_URL: f"https://github.com/{m.group(1)}/{m.group(2)}.git",
+        CONF_FILE: m.group(3),
+    }
+    if m.group(4):
+        conf[CONF_REF] = m.group(4)
+
+    # print(conf)
+    return BASE_SCHEMA(conf)
+
+
+BASE_SCHEMA = cv.All(
+    cv.Schema(
+        {
+            cv.Required(CONF_URL): cv.url,
+            cv.Exclusive(CONF_FILE, "files"): validate_yaml_filename,
+            cv.Exclusive(CONF_FILES, "files"): cv.All(
+                cv.ensure_list(validate_yaml_filename),
+                cv.Length(min=1),
+            ),
+            cv.Optional(CONF_REF): cv.git_ref,
+            cv.Optional(CONF_REFRESH, default="1d"): cv.All(
+                cv.string, cv.source_refresh
+            ),
+        }
+    ),
+    cv.has_at_least_one_key(CONF_FILE, CONF_FILES),
+)
+
+
+CONFIG_SCHEMA = cv.All(
+    cv.Schema(
+        {
+            str: cv.Any(validate_source_shorthand, BASE_SCHEMA, dict),
+        }
+    ),
+    validate_git_package,
+)
+
+
+def _process_base_package(config: dict) -> dict:
+    repo_dir = git.clone_or_update(
+        url=config[CONF_URL],
+        ref=config.get(CONF_REF),
+        refresh=config[CONF_REFRESH],
+        domain=DOMAIN,
+    )
+    files: str = config[CONF_FILES]
+
+    packages = {}
+    for file in files:
+        yaml_file: Path = repo_dir / file
+
+        if not yaml_file.is_file():
+            raise cv.Invalid(f"{file} does not exist in repository", path=[CONF_FILES])
+
+        try:
+            packages[file] = yaml_util.load_yaml(yaml_file)
+        except EsphomeError as e:
+            raise cv.Invalid(
+                f"{file} is not a valid YAML file. Please check the file contents."
+            ) from e
+    return {"packages": packages}
+
+
 def do_packages_pass(config: dict):
     if CONF_PACKAGES not in config:
         return config
     packages = config[CONF_PACKAGES]
     with cv.prepend_path(CONF_PACKAGES):
+        packages = CONFIG_SCHEMA(packages)
         if not isinstance(packages, dict):
             raise cv.Invalid(
                 "Packages must be a key to value mapping, got {} instead"
@@ -37,6 +158,8 @@ def do_packages_pass(config: dict):
         for package_name, package_config in packages.items():
             with cv.prepend_path(package_name):
                 recursive_package = package_config
+                if CONF_URL in package_config:
+                    package_config = _process_base_package(package_config)
                 if isinstance(package_config, dict):
                     recursive_package = do_packages_pass(package_config)
                 config = _merge_package(recursive_package, config)
diff --git a/esphome/components/partition/light_partition.h b/esphome/components/partition/light_partition.h
index 687fe562d1..f74001cf75 100644
--- a/esphome/components/partition/light_partition.h
+++ b/esphome/components/partition/light_partition.h
@@ -50,13 +50,11 @@ class PartitionLightOutput : public light::AddressableLight {
     }
   }
   light::LightTraits get_traits() override { return this->segments_[0].get_src()->get_traits(); }
-  void loop() override {
-    if (this->should_show_()) {
-      for (auto seg : this->segments_) {
-        seg.get_src()->schedule_show();
-      }
-      this->mark_shown_();
+  void write_state(light::LightState *state) override {
+    for (auto seg : this->segments_) {
+      seg.get_src()->schedule_show();
     }
+    this->mark_shown_();
   }
 
  protected:
diff --git a/esphome/components/pm1006/pm1006.cpp b/esphome/components/pm1006/pm1006.cpp
index 9bedb3cfc0..1a89307eee 100644
--- a/esphome/components/pm1006/pm1006.cpp
+++ b/esphome/components/pm1006/pm1006.cpp
@@ -7,6 +7,7 @@ namespace pm1006 {
 static const char *const TAG = "pm1006";
 
 static const uint8_t PM1006_RESPONSE_HEADER[] = {0x16, 0x11, 0x0B};
+static const uint8_t PM1006_REQUEST[] = {0x11, 0x02, 0x0B, 0x01, 0xE1};
 
 void PM1006Component::setup() {
   // because this implementation is currently rx-only, there is nothing to setup
@@ -15,9 +16,15 @@ void PM1006Component::setup() {
 void PM1006Component::dump_config() {
   ESP_LOGCONFIG(TAG, "PM1006:");
   LOG_SENSOR("  ", "PM2.5", this->pm_2_5_sensor_);
+  LOG_UPDATE_INTERVAL(this);
   this->check_uart_settings(9600);
 }
 
+void PM1006Component::update() {
+  ESP_LOGV(TAG, "sending measurement request");
+  this->write_array(PM1006_REQUEST, sizeof(PM1006_REQUEST));
+}
+
 void PM1006Component::loop() {
   while (this->available() != 0) {
     this->read_byte(&this->data_[this->data_index_]);
diff --git a/esphome/components/pm1006/pm1006.h b/esphome/components/pm1006/pm1006.h
index 66f4cf0311..238ac67006 100644
--- a/esphome/components/pm1006/pm1006.h
+++ b/esphome/components/pm1006/pm1006.h
@@ -7,7 +7,7 @@
 namespace esphome {
 namespace pm1006 {
 
-class PM1006Component : public Component, public uart::UARTDevice {
+class PM1006Component : public PollingComponent, public uart::UARTDevice {
  public:
   PM1006Component() = default;
 
@@ -15,6 +15,7 @@ class PM1006Component : public Component, public uart::UARTDevice {
   void setup() override;
   void dump_config() override;
   void loop() override;
+  void update() override;
 
   float get_setup_priority() const override;
 
diff --git a/esphome/components/pm1006/sensor.py b/esphome/components/pm1006/sensor.py
index 8ea0e303f3..1e648be199 100644
--- a/esphome/components/pm1006/sensor.py
+++ b/esphome/components/pm1006/sensor.py
@@ -4,11 +4,15 @@ from esphome.components import sensor, uart
 from esphome.const import (
     CONF_ID,
     CONF_PM_2_5,
+    CONF_UPDATE_INTERVAL,
+    DEVICE_CLASS_PM25,
     STATE_CLASS_MEASUREMENT,
     UNIT_MICROGRAMS_PER_CUBIC_METER,
     ICON_BLUR,
 )
+from esphome.core import TimePeriodMilliseconds
 
+CODEOWNERS = ["@habbie"]
 DEPENDENCIES = ["uart"]
 
 pm1006_ns = cg.esphome_ns.namespace("pm1006")
@@ -23,15 +27,34 @@ CONFIG_SCHEMA = cv.All(
                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
                 icon=ICON_BLUR,
                 accuracy_decimals=0,
+                device_class=DEVICE_CLASS_PM25,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
         }
     )
     .extend(cv.COMPONENT_SCHEMA)
-    .extend(uart.UART_DEVICE_SCHEMA),
+    .extend(uart.UART_DEVICE_SCHEMA)
+    .extend(cv.polling_component_schema("never")),
 )
 
 
+def validate_interval_uart(config):
+    require_tx = False
+
+    interval = config.get(CONF_UPDATE_INTERVAL)
+
+    if isinstance(interval, TimePeriodMilliseconds):
+        # 'never' is encoded as a very large int, not as a TimePeriodMilliseconds objects
+        require_tx = True
+
+    uart.final_validate_device_schema(
+        "pm1006", baud_rate=9600, require_rx=True, require_tx=require_tx
+    )(config)
+
+
+FINAL_VALIDATE_SCHEMA = validate_interval_uart
+
+
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     await cg.register_component(var, config)
diff --git a/esphome/components/pmsa003i/sensor.py b/esphome/components/pmsa003i/sensor.py
index ac26270cfc..ceca791cd6 100644
--- a/esphome/components/pmsa003i/sensor.py
+++ b/esphome/components/pmsa003i/sensor.py
@@ -13,7 +13,10 @@ from esphome.const import (
     UNIT_MICROGRAMS_PER_CUBIC_METER,
     ICON_CHEMICAL_WEAPON,
     ICON_COUNTER,
-    DEVICE_CLASS_EMPTY,
+    DEVICE_CLASS_PM1,
+    DEVICE_CLASS_PM10,
+    DEVICE_CLASS_PM25,
+    STATE_CLASS_MEASUREMENT,
 )
 
 CODEOWNERS = ["@sjtrny"]
@@ -36,40 +39,61 @@ CONFIG_SCHEMA = (
             cv.GenerateID(): cv.declare_id(PMSA003IComponent),
             cv.Optional(CONF_STANDARD_UNITS, default=True): cv.boolean,
             cv.Optional(CONF_PM_1_0): sensor.sensor_schema(
-                UNIT_MICROGRAMS_PER_CUBIC_METER,
-                ICON_CHEMICAL_WEAPON,
-                2,
-                DEVICE_CLASS_EMPTY,
+                unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
+                icon=ICON_CHEMICAL_WEAPON,
+                accuracy_decimals=2,
+                device_class=DEVICE_CLASS_PM1,
+                state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
-                UNIT_MICROGRAMS_PER_CUBIC_METER,
-                ICON_CHEMICAL_WEAPON,
-                2,
-                DEVICE_CLASS_EMPTY,
+                unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
+                icon=ICON_CHEMICAL_WEAPON,
+                accuracy_decimals=2,
+                device_class=DEVICE_CLASS_PM25,
+                state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
-                UNIT_MICROGRAMS_PER_CUBIC_METER,
-                ICON_CHEMICAL_WEAPON,
-                2,
-                DEVICE_CLASS_EMPTY,
+                unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
+                icon=ICON_CHEMICAL_WEAPON,
+                accuracy_decimals=2,
+                device_class=DEVICE_CLASS_PM10,
+                state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PMC_0_3): sensor.sensor_schema(
-                UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY
+                unit_of_measurement=UNIT_COUNTS_PER_100ML,
+                icon=ICON_COUNTER,
+                accuracy_decimals=0,
+                state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PMC_0_5): sensor.sensor_schema(
-                UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY
+                unit_of_measurement=UNIT_COUNTS_PER_100ML,
+                icon=ICON_COUNTER,
+                accuracy_decimals=0,
+                state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PMC_1_0): sensor.sensor_schema(
-                UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY
+                unit_of_measurement=UNIT_COUNTS_PER_100ML,
+                icon=ICON_COUNTER,
+                accuracy_decimals=0,
+                state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PMC_2_5): sensor.sensor_schema(
-                UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY
+                unit_of_measurement=UNIT_COUNTS_PER_100ML,
+                icon=ICON_COUNTER,
+                accuracy_decimals=0,
+                state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PMC_5_0): sensor.sensor_schema(
-                UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY
+                unit_of_measurement=UNIT_COUNTS_PER_100ML,
+                icon=ICON_COUNTER,
+                accuracy_decimals=0,
+                state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PMC_10_0): sensor.sensor_schema(
-                UNIT_COUNTS_PER_100ML, ICON_COUNTER, 0, DEVICE_CLASS_EMPTY
+                unit_of_measurement=UNIT_COUNTS_PER_100ML,
+                icon=ICON_COUNTER,
+                accuracy_decimals=0,
+                state_class=STATE_CLASS_MEASUREMENT,
             ),
         }
     )
@@ -90,15 +114,15 @@ 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_standard_units(config[CONF_STANDARD_UNITS]))
 
     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/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py
index c3dd7d5a97..8b9e5c9af2 100644
--- a/esphome/components/pmsx003/sensor.py
+++ b/esphome/components/pmsx003/sensor.py
@@ -19,6 +19,9 @@ from esphome.const import (
     CONF_PM_10_0UM,
     CONF_TEMPERATURE,
     CONF_TYPE,
+    DEVICE_CLASS_PM1,
+    DEVICE_CLASS_PM10,
+    DEVICE_CLASS_PM25,
     DEVICE_CLASS_EMPTY,
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
@@ -75,19 +78,19 @@ CONFIG_SCHEMA = (
                 UNIT_MICROGRAMS_PER_CUBIC_METER,
                 ICON_CHEMICAL_WEAPON,
                 0,
-                DEVICE_CLASS_EMPTY,
+                DEVICE_CLASS_PM1,
             ),
             cv.Optional(CONF_PM_2_5_STD): sensor.sensor_schema(
                 UNIT_MICROGRAMS_PER_CUBIC_METER,
                 ICON_CHEMICAL_WEAPON,
                 0,
-                DEVICE_CLASS_EMPTY,
+                DEVICE_CLASS_PM25,
             ),
             cv.Optional(CONF_PM_10_0_STD): sensor.sensor_schema(
                 UNIT_MICROGRAMS_PER_CUBIC_METER,
                 ICON_CHEMICAL_WEAPON,
                 0,
-                DEVICE_CLASS_EMPTY,
+                DEVICE_CLASS_PM10,
             ),
             cv.Optional(CONF_PM_1_0): sensor.sensor_schema(
                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp
index fc84f30078..fcab9872c4 100644
--- a/esphome/components/pn532/pn532.cpp
+++ b/esphome/components/pn532/pn532.cpp
@@ -49,7 +49,7 @@ void PN532::setup() {
   }
 
   // Set up SAM (secure access module)
-  uint8_t sam_timeout = std::min(255u, this->update_interval_ / 50);
+  uint8_t sam_timeout = std::min<uint8_t>(255u, this->update_interval_ / 50);
   if (!this->write_command_({
           PN532_COMMAND_SAMCONFIGURATION,
           0x01,         // normal mode
diff --git a/esphome/components/power_supply/power_supply.cpp b/esphome/components/power_supply/power_supply.cpp
index f50adac6f9..a492919202 100644
--- a/esphome/components/power_supply/power_supply.cpp
+++ b/esphome/components/power_supply/power_supply.cpp
@@ -42,7 +42,7 @@ void PowerSupply::request_high_power() {
 void PowerSupply::unrequest_high_power() {
   this->active_requests_--;
   if (this->active_requests_ < 0) {
-    // we're just going to use 0 as our now counter.
+    // we're just going to use 0 as our new counter.
     this->active_requests_ = 0;
   }
 
diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py
index 767728fc80..c7b89d41b0 100644
--- a/esphome/components/pulse_counter/sensor.py
+++ b/esphome/components/pulse_counter/sensor.py
@@ -13,7 +13,7 @@ from esphome.const import (
     CONF_TOTAL,
     ICON_PULSE,
     STATE_CLASS_MEASUREMENT,
-    STATE_CLASS_NONE,
+    STATE_CLASS_TOTAL_INCREASING,
     UNIT_PULSES_PER_MINUTE,
     UNIT_PULSES,
 )
@@ -95,7 +95,7 @@ CONFIG_SCHEMA = (
                 unit_of_measurement=UNIT_PULSES,
                 icon=ICON_PULSE,
                 accuracy_decimals=0,
-                state_class=STATE_CLASS_NONE,
+                state_class=STATE_CLASS_TOTAL_INCREASING,
             ),
         }
     )
diff --git a/esphome/components/pulse_meter/sensor.py b/esphome/components/pulse_meter/sensor.py
index 18da842bad..454cb3a69d 100644
--- a/esphome/components/pulse_meter/sensor.py
+++ b/esphome/components/pulse_meter/sensor.py
@@ -11,8 +11,8 @@ from esphome.const import (
     CONF_TOTAL,
     CONF_VALUE,
     ICON_PULSE,
-    LAST_RESET_TYPE_AUTO,
     STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_TOTAL_INCREASING,
     UNIT_PULSES,
     UNIT_PULSES_PER_MINUTE,
 )
@@ -64,8 +64,7 @@ CONFIG_SCHEMA = sensor.sensor_schema(
             unit_of_measurement=UNIT_PULSES,
             icon=ICON_PULSE,
             accuracy_decimals=0,
-            state_class=STATE_CLASS_MEASUREMENT,
-            last_reset_type=LAST_RESET_TYPE_AUTO,
+            state_class=STATE_CLASS_TOTAL_INCREASING,
         ),
     }
 )
diff --git a/esphome/components/pzem004t/sensor.py b/esphome/components/pzem004t/sensor.py
index 23502e849a..70dec82c3f 100644
--- a/esphome/components/pzem004t/sensor.py
+++ b/esphome/components/pzem004t/sensor.py
@@ -11,8 +11,8 @@ from esphome.const import (
     DEVICE_CLASS_ENERGY,
     DEVICE_CLASS_POWER,
     DEVICE_CLASS_VOLTAGE,
-    LAST_RESET_TYPE_AUTO,
     STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_TOTAL_INCREASING,
     UNIT_VOLT,
     UNIT_AMPERE,
     UNIT_WATT,
@@ -50,8 +50,7 @@ CONFIG_SCHEMA = (
                 unit_of_measurement=UNIT_WATT_HOURS,
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_ENERGY,
-                state_class=STATE_CLASS_MEASUREMENT,
-                last_reset_type=LAST_RESET_TYPE_AUTO,
+                state_class=STATE_CLASS_TOTAL_INCREASING,
             ),
         }
     )
diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py
index 1616bf0ace..b6697e3d19 100644
--- a/esphome/components/pzemac/sensor.py
+++ b/esphome/components/pzemac/sensor.py
@@ -15,8 +15,8 @@ from esphome.const import (
     DEVICE_CLASS_POWER,
     DEVICE_CLASS_ENERGY,
     ICON_CURRENT_AC,
-    LAST_RESET_TYPE_AUTO,
     STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_TOTAL_INCREASING,
     UNIT_HERTZ,
     UNIT_VOLT,
     UNIT_AMPERE,
@@ -55,8 +55,7 @@ CONFIG_SCHEMA = (
                 unit_of_measurement=UNIT_WATT_HOURS,
                 accuracy_decimals=0,
                 device_class=DEVICE_CLASS_ENERGY,
-                state_class=STATE_CLASS_MEASUREMENT,
-                last_reset_type=LAST_RESET_TYPE_AUTO,
+                state_class=STATE_CLASS_TOTAL_INCREASING,
             ),
             cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(
                 unit_of_measurement=UNIT_HERTZ,
diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py
index c9f1c611a8..d76dc6bc34 100644
--- a/esphome/components/remote_base/__init__.py
+++ b/esphome/components/remote_base/__init__.py
@@ -1085,3 +1085,45 @@ async def panasonic_action(var, config, args):
     cg.add(var.set_address(template_))
     template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint32)
     cg.add(var.set_command(template_))
+
+
+# Midea
+MideaData, MideaBinarySensor, MideaTrigger, MideaAction, MideaDumper = declare_protocol(
+    "Midea"
+)
+MideaAction = ns.class_("MideaAction", RemoteTransmitterActionBase)
+MIDEA_SCHEMA = cv.Schema(
+    {
+        cv.Required(CONF_CODE): cv.All(
+            [cv.Any(cv.hex_uint8_t, cv.uint8_t)],
+            cv.Length(min=5, max=5),
+        ),
+        cv.GenerateID(CONF_CODE_STORAGE_ID): cv.declare_id(cg.uint8),
+    }
+)
+
+
+@register_binary_sensor("midea", MideaBinarySensor, MIDEA_SCHEMA)
+def midea_binary_sensor(var, config):
+    arr_ = cg.progmem_array(config[CONF_CODE_STORAGE_ID], config[CONF_CODE])
+    cg.add(var.set_code(arr_))
+
+
+@register_trigger("midea", MideaTrigger, MideaData)
+def midea_trigger(var, config):
+    pass
+
+
+@register_dumper("midea", MideaDumper)
+def midea_dumper(var, config):
+    pass
+
+
+@register_action(
+    "midea",
+    MideaAction,
+    MIDEA_SCHEMA,
+)
+async def midea_action(var, config, args):
+    arr_ = cg.progmem_array(config[CONF_CODE_STORAGE_ID], config[CONF_CODE])
+    cg.add(var.set_code(arr_))
diff --git a/esphome/components/remote_base/midea_protocol.cpp b/esphome/components/remote_base/midea_protocol.cpp
new file mode 100644
index 0000000000..baf64f246f
--- /dev/null
+++ b/esphome/components/remote_base/midea_protocol.cpp
@@ -0,0 +1,99 @@
+#include "midea_protocol.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace remote_base {
+
+static const char *const TAG = "remote.midea";
+
+uint8_t MideaData::calc_cs_() const {
+  uint8_t cs = 0;
+  for (const uint8_t *it = this->data(); it != this->data() + OFFSET_CS; ++it)
+    cs -= reverse_bits_8(*it);
+  return reverse_bits_8(cs);
+}
+
+bool MideaData::check_compliment(const MideaData &rhs) const {
+  const uint8_t *it0 = rhs.data();
+  for (const uint8_t *it1 = this->data(); it1 != this->data() + this->size(); ++it0, ++it1) {
+    if (*it0 != ~(*it1))
+      return false;
+  }
+  return true;
+}
+
+void MideaProtocol::data(RemoteTransmitData *dst, const MideaData &src, bool compliment) {
+  for (const uint8_t *it = src.data(); it != src.data() + src.size(); ++it) {
+    const uint8_t data = compliment ? ~(*it) : *it;
+    for (uint8_t mask = 128; mask; mask >>= 1) {
+      if (data & mask)
+        one(dst);
+      else
+        zero(dst);
+    }
+  }
+}
+
+void MideaProtocol::encode(RemoteTransmitData *dst, const MideaData &data) {
+  dst->set_carrier_frequency(38000);
+  dst->reserve(2 + 48 * 2 + 2 + 2 + 48 * 2 + 2);
+  MideaProtocol::header(dst);
+  MideaProtocol::data(dst, data);
+  MideaProtocol::footer(dst);
+  MideaProtocol::header(dst);
+  MideaProtocol::data(dst, data, true);
+  MideaProtocol::footer(dst);
+}
+
+bool MideaProtocol::expect_one(RemoteReceiveData &src) {
+  if (!src.peek_item(BIT_HIGH_US, BIT_ONE_LOW_US))
+    return false;
+  src.advance(2);
+  return true;
+}
+
+bool MideaProtocol::expect_zero(RemoteReceiveData &src) {
+  if (!src.peek_item(BIT_HIGH_US, BIT_ZERO_LOW_US))
+    return false;
+  src.advance(2);
+  return true;
+}
+
+bool MideaProtocol::expect_header(RemoteReceiveData &src) {
+  if (!src.peek_item(HEADER_HIGH_US, HEADER_LOW_US))
+    return false;
+  src.advance(2);
+  return true;
+}
+
+bool MideaProtocol::expect_footer(RemoteReceiveData &src) {
+  if (!src.peek_item(BIT_HIGH_US, MIN_GAP_US))
+    return false;
+  src.advance(2);
+  return true;
+}
+
+bool MideaProtocol::expect_data(RemoteReceiveData &src, MideaData &out) {
+  for (uint8_t *dst = out.data(); dst != out.data() + out.size(); ++dst) {
+    for (uint8_t mask = 128; mask; mask >>= 1) {
+      if (MideaProtocol::expect_one(src))
+        *dst |= mask;
+      else if (!MideaProtocol::expect_zero(src))
+        return false;
+    }
+  }
+  return true;
+}
+
+optional<MideaData> MideaProtocol::decode(RemoteReceiveData src) {
+  MideaData out, inv;
+  if (MideaProtocol::expect_header(src) && MideaProtocol::expect_data(src, out) && MideaProtocol::expect_footer(src) &&
+      out.is_valid() && MideaProtocol::expect_data(src, inv) && out.check_compliment(inv))
+    return out;
+  return {};
+}
+
+void MideaProtocol::dump(const MideaData &data) { ESP_LOGD(TAG, "Received Midea: %s", data.to_string().c_str()); }
+
+}  // namespace remote_base
+}  // namespace esphome
diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h
new file mode 100644
index 0000000000..9b0d156617
--- /dev/null
+++ b/esphome/components/remote_base/midea_protocol.h
@@ -0,0 +1,105 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/helpers.h"
+#include "remote_base.h"
+
+namespace esphome {
+namespace remote_base {
+
+class MideaData {
+ public:
+  // Make zero-filled
+  MideaData() { memset(this->data_, 0, sizeof(this->data_)); }
+  // Make from initializer_list
+  MideaData(std::initializer_list<uint8_t> data) { std::copy(data.begin(), data.end(), this->data()); }
+  // Make from vector
+  MideaData(const std::vector<uint8_t> &data) {
+    memcpy(this->data_, data.data(), std::min<size_t>(data.size(), sizeof(this->data_)));
+  }
+  // Make 40-bit copy from PROGMEM array
+  MideaData(const uint8_t *data) { memcpy_P(this->data_, data, OFFSET_CS); }
+  // Default copy constructor
+  MideaData(const MideaData &) = default;
+
+  uint8_t *data() { return this->data_; }
+  const uint8_t *data() const { return this->data_; }
+  uint8_t size() const { return sizeof(this->data_); }
+  bool is_valid() const { return this->data_[OFFSET_CS] == this->calc_cs_(); }
+  void finalize() { this->data_[OFFSET_CS] = this->calc_cs_(); }
+  bool check_compliment(const MideaData &rhs) const;
+  std::string to_string() const { return hexencode(*this); }
+  // compare only 40-bits
+  bool operator==(const MideaData &rhs) const { return !memcmp(this->data_, rhs.data_, OFFSET_CS); }
+  enum MideaDataType : uint8_t {
+    MIDEA_TYPE_COMMAND = 0xA1,
+    MIDEA_TYPE_SPECIAL = 0xA2,
+    MIDEA_TYPE_FOLLOW_ME = 0xA4,
+  };
+  MideaDataType type() const { return static_cast<MideaDataType>(this->data_[0]); }
+  template<typename T> T to() const { return T(*this); }
+
+ protected:
+  void set_value_(uint8_t offset, uint8_t val_mask, uint8_t shift, uint8_t val) {
+    data_[offset] &= ~(val_mask << shift);
+    data_[offset] |= (val << shift);
+  }
+  static const uint8_t OFFSET_CS = 5;
+  // 48-bits data
+  uint8_t data_[6];
+  // Calculate checksum
+  uint8_t calc_cs_() const;
+};
+
+class MideaProtocol : public RemoteProtocol<MideaData> {
+ public:
+  void encode(RemoteTransmitData *dst, const MideaData &data) override;
+  optional<MideaData> decode(RemoteReceiveData src) override;
+  void dump(const MideaData &data) override;
+
+ protected:
+  static const int32_t TICK_US = 560;
+  static const int32_t HEADER_HIGH_US = 8 * TICK_US;
+  static const int32_t HEADER_LOW_US = 8 * TICK_US;
+  static const int32_t BIT_HIGH_US = 1 * TICK_US;
+  static const int32_t BIT_ONE_LOW_US = 3 * TICK_US;
+  static const int32_t BIT_ZERO_LOW_US = 1 * TICK_US;
+  static const int32_t MIN_GAP_US = 10 * TICK_US;
+  static void one(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); }
+  static void zero(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); }
+  static void header(RemoteTransmitData *dst) { dst->item(HEADER_HIGH_US, HEADER_LOW_US); }
+  static void footer(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, MIN_GAP_US); }
+  static void data(RemoteTransmitData *dst, const MideaData &src, bool compliment = false);
+  static bool expect_one(RemoteReceiveData &src);
+  static bool expect_zero(RemoteReceiveData &src);
+  static bool expect_header(RemoteReceiveData &src);
+  static bool expect_footer(RemoteReceiveData &src);
+  static bool expect_data(RemoteReceiveData &src, MideaData &out);
+};
+
+class MideaBinarySensor : public RemoteReceiverBinarySensorBase {
+ public:
+  bool matches(RemoteReceiveData src) override {
+    auto data = MideaProtocol().decode(src);
+    return data.has_value() && data.value() == this->data_;
+  }
+  void set_code(const uint8_t *code) { this->data_ = code; }
+
+ protected:
+  MideaData data_;
+};
+
+using MideaTrigger = RemoteReceiverTrigger<MideaProtocol, MideaData>;
+using MideaDumper = RemoteReceiverDumper<MideaProtocol, MideaData>;
+
+template<typename... Ts> class MideaAction : public RemoteTransmitterActionBase<Ts...> {
+  TEMPLATABLE_VALUE(const uint8_t *, code)
+  void encode(RemoteTransmitData *dst, Ts... x) override {
+    MideaData data = this->code_.value(x...);
+    data.finalize();
+    MideaProtocol().encode(dst, data);
+  }
+};
+
+}  // namespace remote_base
+}  // namespace esphome
diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py
index 13cc94786c..8a0d9674a7 100644
--- a/esphome/components/sdm_meter/sensor.py
+++ b/esphome/components/sdm_meter/sensor.py
@@ -23,17 +23,17 @@ from esphome.const import (
     DEVICE_CLASS_VOLTAGE,
     ICON_CURRENT_AC,
     ICON_FLASH,
-    LAST_RESET_TYPE_AUTO,
     STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_TOTAL_INCREASING,
     UNIT_AMPERE,
     UNIT_DEGREES,
     UNIT_HERTZ,
+    UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
+    UNIT_KILOWATT_HOURS,
     UNIT_VOLT,
     UNIT_VOLT_AMPS,
     UNIT_VOLT_AMPS_REACTIVE,
-    UNIT_VOLT_AMPS_REACTIVE_HOURS,
     UNIT_WATT,
-    UNIT_WATT_HOURS,
 )
 
 AUTO_LOAD = ["modbus"]
@@ -47,6 +47,7 @@ PHASE_SENSORS = {
         unit_of_measurement=UNIT_VOLT,
         accuracy_decimals=2,
         device_class=DEVICE_CLASS_VOLTAGE,
+        state_class=STATE_CLASS_MEASUREMENT,
     ),
     CONF_CURRENT: sensor.sensor_schema(
         unit_of_measurement=UNIT_AMPERE,
@@ -100,32 +101,28 @@ CONFIG_SCHEMA = (
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_IMPORT_ACTIVE_ENERGY): sensor.sensor_schema(
-                unit_of_measurement=UNIT_WATT_HOURS,
+                unit_of_measurement=UNIT_KILOWATT_HOURS,
                 accuracy_decimals=2,
                 device_class=DEVICE_CLASS_ENERGY,
-                state_class=STATE_CLASS_MEASUREMENT,
-                last_reset_type=LAST_RESET_TYPE_AUTO,
+                state_class=STATE_CLASS_TOTAL_INCREASING,
             ),
             cv.Optional(CONF_EXPORT_ACTIVE_ENERGY): sensor.sensor_schema(
-                unit_of_measurement=UNIT_WATT_HOURS,
+                unit_of_measurement=UNIT_KILOWATT_HOURS,
                 accuracy_decimals=2,
                 device_class=DEVICE_CLASS_ENERGY,
-                state_class=STATE_CLASS_MEASUREMENT,
-                last_reset_type=LAST_RESET_TYPE_AUTO,
+                state_class=STATE_CLASS_TOTAL_INCREASING,
             ),
             cv.Optional(CONF_IMPORT_REACTIVE_ENERGY): sensor.sensor_schema(
-                unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE_HOURS,
+                unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
                 accuracy_decimals=2,
                 device_class=DEVICE_CLASS_ENERGY,
-                state_class=STATE_CLASS_MEASUREMENT,
-                last_reset_type=LAST_RESET_TYPE_AUTO,
+                state_class=STATE_CLASS_TOTAL_INCREASING,
             ),
             cv.Optional(CONF_EXPORT_REACTIVE_ENERGY): sensor.sensor_schema(
-                unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE_HOURS,
+                unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
                 accuracy_decimals=2,
                 device_class=DEVICE_CLASS_ENERGY,
-                state_class=STATE_CLASS_MEASUREMENT,
-                last_reset_type=LAST_RESET_TYPE_AUTO,
+                state_class=STATE_CLASS_TOTAL_INCREASING,
             ),
         }
     )
diff --git a/esphome/components/sds011/sensor.py b/esphome/components/sds011/sensor.py
index 0997b47ef6..456d47ee91 100644
--- a/esphome/components/sds011/sensor.py
+++ b/esphome/components/sds011/sensor.py
@@ -7,6 +7,8 @@ from esphome.const import (
     CONF_PM_2_5,
     CONF_RX_ONLY,
     CONF_UPDATE_INTERVAL,
+    DEVICE_CLASS_PM25,
+    DEVICE_CLASS_PM10,
     STATE_CLASS_MEASUREMENT,
     UNIT_MICROGRAMS_PER_CUBIC_METER,
     ICON_CHEMICAL_WEAPON,
@@ -41,12 +43,14 @@ CONFIG_SCHEMA = cv.All(
                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
                 icon=ICON_CHEMICAL_WEAPON,
                 accuracy_decimals=1,
+                device_class=DEVICE_CLASS_PM25,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
                 icon=ICON_CHEMICAL_WEAPON,
                 accuracy_decimals=1,
+                device_class=DEVICE_CLASS_PM10,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_RX_ONLY, default=False): cv.boolean,
diff --git a/esphome/components/selec_meter/sensor.py b/esphome/components/selec_meter/sensor.py
index 2d05d00380..168d3a3db2 100644
--- a/esphome/components/selec_meter/sensor.py
+++ b/esphome/components/selec_meter/sensor.py
@@ -20,8 +20,8 @@ from esphome.const import (
     DEVICE_CLASS_POWER_FACTOR,
     DEVICE_CLASS_VOLTAGE,
     ICON_CURRENT_AC,
-    LAST_RESET_TYPE_AUTO,
     STATE_CLASS_MEASUREMENT,
+    STATE_CLASS_TOTAL_INCREASING,
     UNIT_AMPERE,
     UNIT_HERTZ,
     UNIT_VOLT,
@@ -54,50 +54,43 @@ SENSORS = {
         unit_of_measurement=UNIT_KILOWATT_HOURS,
         accuracy_decimals=2,
         device_class=DEVICE_CLASS_ENERGY,
-        state_class=STATE_CLASS_MEASUREMENT,
-        last_reset_type=LAST_RESET_TYPE_AUTO,
+        state_class=STATE_CLASS_TOTAL_INCREASING,
     ),
     CONF_IMPORT_ACTIVE_ENERGY: sensor.sensor_schema(
         unit_of_measurement=UNIT_KILOWATT_HOURS,
         accuracy_decimals=2,
         device_class=DEVICE_CLASS_ENERGY,
-        state_class=STATE_CLASS_MEASUREMENT,
-        last_reset_type=LAST_RESET_TYPE_AUTO,
+        state_class=STATE_CLASS_TOTAL_INCREASING,
     ),
     CONF_EXPORT_ACTIVE_ENERGY: sensor.sensor_schema(
         unit_of_measurement=UNIT_KILOWATT_HOURS,
         accuracy_decimals=2,
         device_class=DEVICE_CLASS_ENERGY,
-        state_class=STATE_CLASS_MEASUREMENT,
-        last_reset_type=LAST_RESET_TYPE_AUTO,
+        state_class=STATE_CLASS_TOTAL_INCREASING,
     ),
     CONF_TOTAL_REACTIVE_ENERGY: sensor.sensor_schema(
         unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
         accuracy_decimals=2,
         device_class=DEVICE_CLASS_ENERGY,
-        state_class=STATE_CLASS_MEASUREMENT,
-        last_reset_type=LAST_RESET_TYPE_AUTO,
+        state_class=STATE_CLASS_TOTAL_INCREASING,
     ),
     CONF_IMPORT_REACTIVE_ENERGY: sensor.sensor_schema(
         unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
         accuracy_decimals=2,
         device_class=DEVICE_CLASS_ENERGY,
-        state_class=STATE_CLASS_MEASUREMENT,
-        last_reset_type=LAST_RESET_TYPE_AUTO,
+        state_class=STATE_CLASS_TOTAL_INCREASING,
     ),
     CONF_EXPORT_REACTIVE_ENERGY: sensor.sensor_schema(
         unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
         accuracy_decimals=2,
         device_class=DEVICE_CLASS_ENERGY,
-        state_class=STATE_CLASS_MEASUREMENT,
-        last_reset_type=LAST_RESET_TYPE_AUTO,
+        state_class=STATE_CLASS_TOTAL_INCREASING,
     ),
     CONF_APPARENT_ENERGY: sensor.sensor_schema(
         unit_of_measurement=UNIT_KILOVOLT_AMPS_HOURS,
         accuracy_decimals=2,
         device_class=DEVICE_CLASS_ENERGY,
-        state_class=STATE_CLASS_MEASUREMENT,
-        last_reset_type=LAST_RESET_TYPE_AUTO,
+        state_class=STATE_CLASS_TOTAL_INCREASING,
     ),
     CONF_ACTIVE_POWER: sensor.sensor_schema(
         unit_of_measurement=UNIT_WATT,
diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py
index 739a8ada50..d423793873 100644
--- a/esphome/components/senseair/sensor.py
+++ b/esphome/components/senseair/sensor.py
@@ -7,6 +7,7 @@ from esphome.const import (
     CONF_CO2,
     CONF_ID,
     ICON_MOLECULE_CO2,
+    DEVICE_CLASS_CARBON_DIOXIDE,
     STATE_CLASS_MEASUREMENT,
     UNIT_PARTS_PER_MILLION,
 )
@@ -41,6 +42,7 @@ CONFIG_SCHEMA = (
                 unit_of_measurement=UNIT_PARTS_PER_MILLION,
                 icon=ICON_MOLECULE_CO2,
                 accuracy_decimals=0,
+                device_class=DEVICE_CLASS_CARBON_DIOXIDE,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
         }
diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py
index 1bb4e25a17..fd278be51e 100644
--- a/esphome/components/sensor/__init__.py
+++ b/esphome/components/sensor/__init__.py
@@ -17,7 +17,6 @@ from esphome.const import (
     CONF_ICON,
     CONF_ID,
     CONF_INTERNAL,
-    CONF_LAST_RESET_TYPE,
     CONF_ON_RAW_VALUE,
     CONF_ON_VALUE,
     CONF_ON_VALUE_RANGE,
@@ -31,25 +30,32 @@ from esphome.const import (
     CONF_NAME,
     CONF_MQTT_ID,
     CONF_FORCE_UPDATE,
-    LAST_RESET_TYPE_AUTO,
-    LAST_RESET_TYPE_NEVER,
-    LAST_RESET_TYPE_NONE,
     DEVICE_CLASS_EMPTY,
+    DEVICE_CLASS_AQI,
     DEVICE_CLASS_BATTERY,
-    DEVICE_CLASS_CARBON_MONOXIDE,
     DEVICE_CLASS_CARBON_DIOXIDE,
+    DEVICE_CLASS_CARBON_MONOXIDE,
     DEVICE_CLASS_CURRENT,
     DEVICE_CLASS_ENERGY,
     DEVICE_CLASS_GAS,
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_ILLUMINANCE,
     DEVICE_CLASS_MONETARY,
-    DEVICE_CLASS_SIGNAL_STRENGTH,
-    DEVICE_CLASS_TEMPERATURE,
+    DEVICE_CLASS_NITROGEN_DIOXIDE,
+    DEVICE_CLASS_NITROGEN_MONOXIDE,
+    DEVICE_CLASS_NITROUS_OXIDE,
+    DEVICE_CLASS_OZONE,
+    DEVICE_CLASS_PM1,
+    DEVICE_CLASS_PM10,
+    DEVICE_CLASS_PM25,
     DEVICE_CLASS_POWER,
     DEVICE_CLASS_POWER_FACTOR,
     DEVICE_CLASS_PRESSURE,
+    DEVICE_CLASS_SIGNAL_STRENGTH,
+    DEVICE_CLASS_SULPHUR_DIOXIDE,
+    DEVICE_CLASS_TEMPERATURE,
     DEVICE_CLASS_TIMESTAMP,
+    DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
     DEVICE_CLASS_VOLTAGE,
 )
 from esphome.core import CORE, coroutine_with_priority
@@ -58,21 +64,31 @@ from esphome.util import Registry
 CODEOWNERS = ["@esphome/core"]
 DEVICE_CLASSES = [
     DEVICE_CLASS_EMPTY,
+    DEVICE_CLASS_AQI,
     DEVICE_CLASS_BATTERY,
-    DEVICE_CLASS_CARBON_MONOXIDE,
     DEVICE_CLASS_CARBON_DIOXIDE,
+    DEVICE_CLASS_CARBON_MONOXIDE,
     DEVICE_CLASS_CURRENT,
     DEVICE_CLASS_ENERGY,
     DEVICE_CLASS_GAS,
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_ILLUMINANCE,
     DEVICE_CLASS_MONETARY,
-    DEVICE_CLASS_SIGNAL_STRENGTH,
-    DEVICE_CLASS_TEMPERATURE,
-    DEVICE_CLASS_TIMESTAMP,
+    DEVICE_CLASS_NITROGEN_DIOXIDE,
+    DEVICE_CLASS_NITROGEN_MONOXIDE,
+    DEVICE_CLASS_NITROUS_OXIDE,
+    DEVICE_CLASS_OZONE,
+    DEVICE_CLASS_PM1,
+    DEVICE_CLASS_PM10,
+    DEVICE_CLASS_PM25,
     DEVICE_CLASS_POWER,
     DEVICE_CLASS_POWER_FACTOR,
     DEVICE_CLASS_PRESSURE,
+    DEVICE_CLASS_SIGNAL_STRENGTH,
+    DEVICE_CLASS_SULPHUR_DIOXIDE,
+    DEVICE_CLASS_TEMPERATURE,
+    DEVICE_CLASS_TIMESTAMP,
+    DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
     DEVICE_CLASS_VOLTAGE,
 ]
 
@@ -85,15 +101,6 @@ STATE_CLASSES = {
 }
 validate_state_class = cv.enum(STATE_CLASSES, lower=True, space="_")
 
-LastResetTypes = sensor_ns.enum("LastResetType")
-LAST_RESET_TYPES = {
-    LAST_RESET_TYPE_NONE: LastResetTypes.LAST_RESET_TYPE_NONE,
-    LAST_RESET_TYPE_NEVER: LastResetTypes.LAST_RESET_TYPE_NEVER,
-    LAST_RESET_TYPE_AUTO: LastResetTypes.LAST_RESET_TYPE_AUTO,
-}
-validate_last_reset_type = cv.enum(LAST_RESET_TYPES, lower=True, space="_")
-
-
 IS_PLATFORM_COMPONENT = True
 
 
@@ -183,7 +190,9 @@ SENSOR_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend(
         cv.Optional(CONF_ACCURACY_DECIMALS): validate_accuracy_decimals,
         cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
         cv.Optional(CONF_STATE_CLASS): validate_state_class,
-        cv.Optional(CONF_LAST_RESET_TYPE): validate_last_reset_type,
+        cv.Optional("last_reset_type"): cv.invalid(
+            "last_reset_type has been removed since 2021.9.0. state_class: total_increasing should be used for total values."
+        ),
         cv.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean,
         cv.Optional(CONF_EXPIRE_AFTER): cv.All(
             cv.requires_component("mqtt"),
@@ -220,7 +229,6 @@ def sensor_schema(
     accuracy_decimals: int = _UNDEF,
     device_class: str = _UNDEF,
     state_class: str = _UNDEF,
-    last_reset_type: str = _UNDEF,
 ) -> cv.Schema:
     schema = SENSOR_SCHEMA
     if unit_of_measurement is not _UNDEF:
@@ -253,14 +261,6 @@ def sensor_schema(
         schema = schema.extend(
             {cv.Optional(CONF_STATE_CLASS, default=state_class): validate_state_class}
         )
-    if last_reset_type is not _UNDEF:
-        schema = schema.extend(
-            {
-                cv.Optional(
-                    CONF_LAST_RESET_TYPE, default=last_reset_type
-                ): validate_last_reset_type
-            }
-        )
     return schema
 
 
@@ -511,8 +511,6 @@ async def setup_sensor_core_(var, config):
         cg.add(var.set_icon(config[CONF_ICON]))
     if CONF_ACCURACY_DECIMALS in config:
         cg.add(var.set_accuracy_decimals(config[CONF_ACCURACY_DECIMALS]))
-    if CONF_LAST_RESET_TYPE in config:
-        cg.add(var.set_last_reset_type(config[CONF_LAST_RESET_TYPE]))
     cg.add(var.set_force_update(config[CONF_FORCE_UPDATE]))
     if config.get(CONF_FILTERS):  # must exist and not be empty
         filters = await build_filters(config[CONF_FILTERS])
diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp
index 1a5c76db51..1dbc1c901a 100644
--- a/esphome/components/sensor/sensor.cpp
+++ b/esphome/components/sensor/sensor.cpp
@@ -18,18 +18,6 @@ const char *state_class_to_string(StateClass state_class) {
   }
 }
 
-const char *last_reset_type_to_string(LastResetType last_reset_type) {
-  switch (last_reset_type) {
-    case LAST_RESET_TYPE_NEVER:
-      return "never";
-    case LAST_RESET_TYPE_AUTO:
-      return "auto";
-    case LAST_RESET_TYPE_NONE:
-    default:
-      return "";
-  }
-}
-
 void Sensor::publish_state(float state) {
   this->raw_state = state;
   this->raw_callback_.call(state);
@@ -80,7 +68,6 @@ void Sensor::set_state_class(const std::string &state_class) {
     ESP_LOGW(TAG, "'%s' - Unrecognized state class %s", this->get_name().c_str(), state_class.c_str());
   }
 }
-void Sensor::set_last_reset_type(LastResetType last_reset_type) { this->last_reset_type = last_reset_type; }
 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 f0d7ba4887..34b8b26a54 100644
--- a/esphome/components/sensor/sensor.h
+++ b/esphome/components/sensor/sensor.h
@@ -14,10 +14,6 @@ namespace sensor {
       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)); \
-    if ((obj)->state_class == sensor::STATE_CLASS_MEASUREMENT && \
-        (obj)->last_reset_type != sensor::LAST_RESET_TYPE_NONE) { \
-      ESP_LOGCONFIG(TAG, "%s  Last Reset Type: '%s'", prefix, last_reset_type_to_string((obj)->last_reset_type)); \
-    } \
     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()) { \
@@ -42,20 +38,6 @@ enum StateClass : uint8_t {
 
 const char *state_class_to_string(StateClass state_class);
 
-/**
- * Sensor last reset types
- */
-enum LastResetType : uint8_t {
-  /// This sensor does not support resetting. ie, it is not accumulative
-  LAST_RESET_TYPE_NONE = 0,
-  /// This sensor is expected to never reset its value
-  LAST_RESET_TYPE_NEVER = 1,
-  /// This sensor may reset and Home Assistant will watch for this
-  LAST_RESET_TYPE_AUTO = 2,
-};
-
-const char *last_reset_type_to_string(LastResetType last_reset_type);
-
 /** 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.
@@ -174,12 +156,6 @@ class Sensor : public Nameable {
    */
   virtual std::string device_class();
 
-  // The Last reset type of this sensor
-  LastResetType last_reset_type{LAST_RESET_TYPE_NONE};
-
-  /// Manually set the Home Assistant last reset type for this sensor.
-  void set_last_reset_type(LastResetType last_reset_type);
-
   /** A unique ID for this sensor, empty for no unique id. See unique ID requirements:
    * https://developers.home-assistant.io/docs/en/entity_registry_index.html#unique-id-requirements
    *
diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py
index 3e33af3b4a..2596e0065d 100644
--- a/esphome/components/sgp30/sensor.py
+++ b/esphome/components/sgp30/sensor.py
@@ -7,6 +7,8 @@ from esphome.const import (
     CONF_ECO2,
     CONF_TVOC,
     ICON_RADIATOR,
+    DEVICE_CLASS_CARBON_DIOXIDE,
+    DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
     STATE_CLASS_MEASUREMENT,
     UNIT_PARTS_PER_MILLION,
     UNIT_PARTS_PER_BILLION,
@@ -34,12 +36,14 @@ CONFIG_SCHEMA = (
                 unit_of_measurement=UNIT_PARTS_PER_MILLION,
                 icon=ICON_MOLECULE_CO2,
                 accuracy_decimals=0,
+                device_class=DEVICE_CLASS_CARBON_DIOXIDE,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Required(CONF_TVOC): sensor.sensor_schema(
                 unit_of_measurement=UNIT_PARTS_PER_BILLION,
                 icon=ICON_RADIATOR,
                 accuracy_decimals=0,
+                device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_ECO2_BASELINE): sensor.sensor_schema(
diff --git a/esphome/components/sgp40/sensor.py b/esphome/components/sgp40/sensor.py
index 0f562048ac..7b96f867af 100644
--- a/esphome/components/sgp40/sensor.py
+++ b/esphome/components/sgp40/sensor.py
@@ -4,6 +4,7 @@ from esphome.components import i2c, sensor
 from esphome.const import (
     CONF_ID,
     ICON_RADIATOR,
+    DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
     STATE_CLASS_MEASUREMENT,
 )
 
@@ -26,6 +27,7 @@ CONFIG_SCHEMA = (
     sensor.sensor_schema(
         icon=ICON_RADIATOR,
         accuracy_decimals=0,
+        device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
         state_class=STATE_CLASS_MEASUREMENT,
     )
     .extend(
diff --git a/esphome/components/sgp40/sgp40.cpp b/esphome/components/sgp40/sgp40.cpp
index 8d93b3e1b1..a911c107b9 100644
--- a/esphome/components/sgp40/sgp40.cpp
+++ b/esphome/components/sgp40/sgp40.cpp
@@ -78,27 +78,28 @@ void SGP40Component::setup() {
 }
 
 void SGP40Component::self_test_() {
-  ESP_LOGD(TAG, "selfTest started");
+  ESP_LOGD(TAG, "Self-test started");
   if (!this->write_command_(SGP40_CMD_SELF_TEST)) {
     this->error_code_ = COMMUNICATION_FAILED;
-    ESP_LOGD(TAG, "selfTest communicatin failed");
+    ESP_LOGD(TAG, "Self-test communication failed");
     this->mark_failed();
   }
 
   this->set_timeout(250, [this]() {
     uint16_t reply[1];
     if (!this->read_data_(reply, 1)) {
-      ESP_LOGD(TAG, "selfTest read_data_ failed");
+      ESP_LOGD(TAG, "Self-test read_data_ failed");
       this->mark_failed();
       return;
     }
 
     if (reply[0] == 0xD400) {
-      ESP_LOGD(TAG, "selfTest completed");
+      this->self_test_complete_ = true;
+      ESP_LOGD(TAG, "Self-test completed");
       return;
     }
 
-    ESP_LOGD(TAG, "selfTest failed");
+    ESP_LOGD(TAG, "Self-test failed");
     this->mark_failed();
   });
 }
@@ -154,6 +155,12 @@ int32_t SGP40Component::measure_voc_index_() {
  */
 uint16_t SGP40Component::measure_raw_() {
   float humidity = NAN;
+
+  if (!this->self_test_complete_) {
+    ESP_LOGD(TAG, "Self-test not yet complete");
+    return UINT16_MAX;
+  }
+
   if (this->humidity_sensor_ != nullptr) {
     humidity = this->humidity_sensor_->state;
   }
diff --git a/esphome/components/sgp40/sgp40.h b/esphome/components/sgp40/sgp40.h
index b9ea365169..62936102e7 100644
--- a/esphome/components/sgp40/sgp40.h
+++ b/esphome/components/sgp40/sgp40.h
@@ -68,6 +68,7 @@ class SGP40Component : public PollingComponent, public sensor::Sensor, public i2
   int32_t seconds_since_last_store_;
   SGP40Baselines baselines_storage_;
   VocAlgorithmParams voc_algorithm_params_;
+  bool self_test_complete_;
   bool store_baseline_;
   int32_t state0_;
   int32_t state1_;
diff --git a/esphome/components/sm300d2/sensor.py b/esphome/components/sm300d2/sensor.py
index 73cada0eb3..8452ee81f2 100644
--- a/esphome/components/sm300d2/sensor.py
+++ b/esphome/components/sm300d2/sensor.py
@@ -10,6 +10,10 @@ from esphome.const import (
     CONF_PM_10_0,
     CONF_TEMPERATURE,
     CONF_HUMIDITY,
+    DEVICE_CLASS_CARBON_DIOXIDE,
+    DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
+    DEVICE_CLASS_PM25,
+    DEVICE_CLASS_PM10,
     DEVICE_CLASS_TEMPERATURE,
     DEVICE_CLASS_HUMIDITY,
     STATE_CLASS_MEASUREMENT,
@@ -36,6 +40,7 @@ CONFIG_SCHEMA = cv.All(
                 unit_of_measurement=UNIT_PARTS_PER_MILLION,
                 icon=ICON_MOLECULE_CO2,
                 accuracy_decimals=0,
+                device_class=DEVICE_CLASS_CARBON_DIOXIDE,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema(
@@ -48,18 +53,21 @@ CONFIG_SCHEMA = cv.All(
                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
                 icon=ICON_CHEMICAL_WEAPON,
                 accuracy_decimals=0,
+                device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
                 icon=ICON_GRAIN,
                 accuracy_decimals=0,
+                device_class=DEVICE_CLASS_PM25,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
                 icon=ICON_GRAIN,
                 accuracy_decimals=0,
+                device_class=DEVICE_CLASS_PM10,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
diff --git a/esphome/components/sm300d2/sm300d2.cpp b/esphome/components/sm300d2/sm300d2.cpp
index 34d80349f9..b1787581ae 100644
--- a/esphome/components/sm300d2/sm300d2.cpp
+++ b/esphome/components/sm300d2/sm300d2.cpp
@@ -27,7 +27,8 @@ void SM300D2Sensor::update() {
   }
 
   uint16_t calculated_checksum = this->sm300d2_checksum_(response);
-  if (calculated_checksum != response[SM300D2_RESPONSE_LENGTH - 1]) {
+  if ((calculated_checksum != response[SM300D2_RESPONSE_LENGTH - 1]) &&
+      (calculated_checksum - 0x80 != response[SM300D2_RESPONSE_LENGTH - 1])) {
     ESP_LOGW(TAG, "SM300D2 Checksum doesn't match: 0x%02X!=0x%02X", response[SM300D2_RESPONSE_LENGTH - 1],
              calculated_checksum);
     this->status_set_warning();
diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp
index ff176b1d4e..895c775b19 100644
--- a/esphome/components/sntp/sntp_component.cpp
+++ b/esphome/components/sntp/sntp_component.cpp
@@ -56,9 +56,8 @@ void SNTPComponent::loop() {
   if (!time.is_valid())
     return;
 
-  char buf[128];
-  time.strftime(buf, sizeof(buf), "%c");
-  ESP_LOGD(TAG, "Synchronized time: %s", buf);
+  ESP_LOGD(TAG, "Synchronized time: %d-%d-%d %d:%d:%d", time.year, time.month, time.day_of_month, time.hour,
+           time.minute, time.second);
   this->time_sync_callback_.call();
   this->has_time_ = true;
 }
diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py
index 5475dc0a1f..b1362f5421 100644
--- a/esphome/components/sntp/time.py
+++ b/esphome/components/sntp/time.py
@@ -16,7 +16,7 @@ CONFIG_SCHEMA = time_.TIME_SCHEMA.extend(
     {
         cv.GenerateID(): cv.declare_id(SNTPComponent),
         cv.Optional(CONF_SERVERS, default=DEFAULT_SERVERS): cv.All(
-            cv.ensure_list(cv.domain), cv.Length(min=1, max=3)
+            cv.ensure_list(cv.Any(cv.domain, cv.hostname)), cv.Length(min=1, max=3)
         ),
     }
 ).extend(cv.COMPONENT_SCHEMA)
diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py
new file mode 100644
index 0000000000..8e9502be6d
--- /dev/null
+++ b/esphome/components/socket/__init__.py
@@ -0,0 +1,28 @@
+import esphome.config_validation as cv
+import esphome.codegen as cg
+
+CODEOWNERS = ["@esphome/core"]
+
+CONF_IMPLEMENTATION = "implementation"
+IMPLEMENTATION_LWIP_TCP = "lwip_tcp"
+IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets"
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.SplitDefault(
+            CONF_IMPLEMENTATION,
+            esp8266=IMPLEMENTATION_LWIP_TCP,
+            esp32=IMPLEMENTATION_BSD_SOCKETS,
+        ): cv.one_of(
+            IMPLEMENTATION_LWIP_TCP, IMPLEMENTATION_BSD_SOCKETS, lower=True, space="_"
+        ),
+    }
+)
+
+
+async def to_code(config):
+    impl = config[CONF_IMPLEMENTATION]
+    if impl == IMPLEMENTATION_LWIP_TCP:
+        cg.add_define("USE_SOCKET_IMPL_LWIP_TCP")
+    elif impl == IMPLEMENTATION_BSD_SOCKETS:
+        cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS")
diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp
new file mode 100644
index 0000000000..a0cdb6ec42
--- /dev/null
+++ b/esphome/components/socket/bsd_sockets_impl.cpp
@@ -0,0 +1,105 @@
+#include "socket.h"
+#include "esphome/core/defines.h"
+
+#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
+
+#include <string.h>
+
+namespace esphome {
+namespace socket {
+
+std::string format_sockaddr(const struct sockaddr_storage &storage) {
+  if (storage.ss_family == AF_INET) {
+    const struct sockaddr_in *addr = reinterpret_cast<const struct sockaddr_in *>(&storage);
+    char buf[INET_ADDRSTRLEN];
+    const char *ret = inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf));
+    if (ret == NULL)
+      return {};
+    return std::string{buf};
+  } else if (storage.ss_family == AF_INET6) {
+    const struct sockaddr_in6 *addr = reinterpret_cast<const struct sockaddr_in6 *>(&storage);
+    char buf[INET6_ADDRSTRLEN];
+    const char *ret = inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf));
+    if (ret == NULL)
+      return {};
+    return std::string{buf};
+  }
+  return {};
+}
+
+class BSDSocketImpl : public Socket {
+ public:
+  BSDSocketImpl(int fd) : Socket(), fd_(fd) {}
+  ~BSDSocketImpl() override {
+    if (!closed_) {
+      close();
+    }
+  }
+  std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
+    int fd = ::accept(fd_, addr, addrlen);
+    if (fd == -1)
+      return {};
+    return std::unique_ptr<BSDSocketImpl>{new BSDSocketImpl(fd)};
+  }
+  int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(fd_, addr, addrlen); }
+  int close() override {
+    int ret = ::close(fd_);
+    closed_ = true;
+    return ret;
+  }
+  int shutdown(int how) override { return ::shutdown(fd_, how); }
+
+  int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return ::getpeername(fd_, addr, addrlen); }
+  std::string getpeername() override {
+    struct sockaddr_storage storage;
+    socklen_t len = sizeof(storage);
+    int err = this->getpeername((struct sockaddr *) &storage, &len);
+    if (err != 0)
+      return {};
+    return format_sockaddr(storage);
+  }
+  int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { return ::getsockname(fd_, addr, addrlen); }
+  std::string getsockname() override {
+    struct sockaddr_storage storage;
+    socklen_t len = sizeof(storage);
+    int err = this->getsockname((struct sockaddr *) &storage, &len);
+    if (err != 0)
+      return {};
+    return format_sockaddr(storage);
+  }
+  int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override {
+    return ::getsockopt(fd_, level, optname, optval, optlen);
+  }
+  int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override {
+    return ::setsockopt(fd_, level, optname, optval, optlen);
+  }
+  int listen(int backlog) override { return ::listen(fd_, backlog); }
+  ssize_t read(void *buf, size_t len) override { return ::read(fd_, buf, len); }
+  ssize_t write(const void *buf, size_t len) override { return ::write(fd_, buf, len); }
+  int setblocking(bool blocking) override {
+    int fl = ::fcntl(fd_, F_GETFL, 0);
+    if (blocking) {
+      fl &= ~O_NONBLOCK;
+    } else {
+      fl |= O_NONBLOCK;
+    }
+    ::fcntl(fd_, F_SETFL, fl);
+    return 0;
+  }
+
+ protected:
+  int fd_;
+  bool closed_ = false;
+};
+
+std::unique_ptr<Socket> socket(int domain, int type, int protocol) {
+  int ret = ::socket(domain, type, protocol);
+  if (ret == -1)
+    return nullptr;
+  return std::unique_ptr<Socket>{new BSDSocketImpl(ret)};
+}
+
+}  // namespace socket
+}  // namespace esphome
+
+#endif  // USE_SOCKET_IMPL_BSD_SOCKETS
diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h
new file mode 100644
index 0000000000..da710b760e
--- /dev/null
+++ b/esphome/components/socket/headers.h
@@ -0,0 +1,127 @@
+#pragma once
+#include "esphome/core/defines.h"
+
+// Helper file to include all socket-related system headers (or use our own
+// definitions where system ones don't exist)
+
+#ifdef USE_SOCKET_IMPL_LWIP_TCP
+
+#define LWIP_INTERNAL
+#include <sys/types.h>
+#include "lwip/inet.h"
+#include <stdint.h>
+#include <errno.h>
+
+/* Address families.  */
+#define AF_UNSPEC 0
+#define AF_INET 2
+#define AF_INET6 10
+#define PF_INET AF_INET
+#define PF_INET6 AF_INET6
+#define PF_UNSPEC AF_UNSPEC
+#define IPPROTO_IP 0
+#define IPPROTO_TCP 6
+#define IPPROTO_IPV6 41
+#define IPPROTO_ICMPV6 58
+
+#define TCP_NODELAY 0x01
+
+#define F_GETFL 3
+#define F_SETFL 4
+#define O_NONBLOCK 1
+
+#define SHUT_RD 0
+#define SHUT_WR 1
+#define SHUT_RDWR 2
+
+/* Socket protocol types (TCP/UDP/RAW) */
+#define SOCK_STREAM 1
+#define SOCK_DGRAM 2
+#define SOCK_RAW 3
+
+#define SO_REUSEADDR 0x0004 /* Allow local address reuse */
+#define SO_KEEPALIVE 0x0008 /* keep connections alive */
+#define SO_BROADCAST 0x0020 /* permit to send and to receive broadcast messages (see IP_SOF_BROADCAST option) */
+
+#define SOL_SOCKET 0xfff /* options for socket level */
+
+typedef uint8_t sa_family_t;
+typedef uint16_t in_port_t;
+
+struct sockaddr_in {
+  uint8_t sin_len;
+  sa_family_t sin_family;
+  in_port_t sin_port;
+  struct in_addr sin_addr;
+#define SIN_ZERO_LEN 8
+  char sin_zero[SIN_ZERO_LEN];
+};
+
+struct sockaddr_in6 {
+  uint8_t sin6_len;          /* length of this structure    */
+  sa_family_t sin6_family;   /* AF_INET6                    */
+  in_port_t sin6_port;       /* Transport layer port #      */
+  uint32_t sin6_flowinfo;    /* IPv6 flow information       */
+  struct in6_addr sin6_addr; /* IPv6 address                */
+  uint32_t sin6_scope_id;    /* Set of interfaces for scope */
+};
+
+struct sockaddr {
+  uint8_t sa_len;
+  sa_family_t sa_family;
+  char sa_data[14];
+};
+
+struct sockaddr_storage {
+  uint8_t s2_len;
+  sa_family_t ss_family;
+  char s2_data1[2];
+  uint32_t s2_data2[3];
+  uint32_t s2_data3[3];
+};
+typedef uint32_t socklen_t;
+
+#ifdef ARDUINO_ARCH_ESP8266
+// arduino-esp8266 declares a global vars called INADDR_NONE/ANY which are invalid with the define
+#ifdef INADDR_ANY
+#undef INADDR_ANY
+#endif
+#ifdef INADDR_NONE
+#undef INADDR_NONE
+#endif
+
+#define ESPHOME_INADDR_ANY ((uint32_t) 0x00000000UL)
+#define ESPHOME_INADDR_NONE ((uint32_t) 0xFFFFFFFFUL)
+#else  // !ARDUINO_ARCH_ESP8266
+#define ESPHOME_INADDR_ANY INADDR_ANY
+#define ESPHOME_INADDR_NONE INADDR_NONE
+#endif
+
+#endif  // USE_SOCKET_IMPL_LWIP_TCP
+
+#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdint.h>
+
+#ifdef ARDUINO_ARCH_ESP32
+// arduino-esp32 declares a global var called INADDR_NONE which is replaced
+// by the define
+#ifdef INADDR_NONE
+#undef INADDR_NONE
+#endif
+// not defined for ESP32
+typedef uint32_t socklen_t;
+
+#define ESPHOME_INADDR_ANY ((uint32_t) 0x00000000UL)
+#define ESPHOME_INADDR_NONE ((uint32_t) 0xFFFFFFFFUL)
+#else  // !ARDUINO_ARCH_ESP32
+#define ESPHOME_INADDR_ANY INADDR_ANY
+#define ESPHOME_INADDR_NONE INADDR_NONE
+#endif
+
+#endif  // USE_SOCKET_IMPL_BSD_SOCKETS
diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp
new file mode 100644
index 0000000000..39741ea7ec
--- /dev/null
+++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp
@@ -0,0 +1,515 @@
+#include "socket.h"
+#include "esphome/core/defines.h"
+
+#ifdef USE_SOCKET_IMPL_LWIP_TCP
+
+#include "lwip/ip.h"
+#include "lwip/netif.h"
+#include "lwip/opt.h"
+#include "lwip/tcp.h"
+#include <cerrno>
+#include <cstring>
+#include <queue>
+
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace socket {
+
+static const char *const TAG = "socket.lwip";
+
+// set to 1 to enable verbose lwip logging
+#if 0
+#define LWIP_LOG(msg, ...) ESP_LOGVV(TAG, "socket %p: " msg, this, ##__VA_ARGS__)
+#else
+#define LWIP_LOG(msg, ...)
+#endif
+
+class LWIPRawImpl : public Socket {
+ public:
+  LWIPRawImpl(struct tcp_pcb *pcb) : pcb_(pcb) {}
+  ~LWIPRawImpl() override {
+    if (pcb_ != nullptr) {
+      LWIP_LOG("tcp_abort(%p)", pcb_);
+      tcp_abort(pcb_);
+      pcb_ = nullptr;
+    }
+  }
+
+  void init() {
+    LWIP_LOG("init(%p)", pcb_);
+    tcp_arg(pcb_, this);
+    tcp_accept(pcb_, LWIPRawImpl::s_accept_fn);
+    tcp_recv(pcb_, LWIPRawImpl::s_recv_fn);
+    tcp_err(pcb_, LWIPRawImpl::s_err_fn);
+  }
+
+  std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
+    if (pcb_ == nullptr) {
+      errno = EBADF;
+      return nullptr;
+    }
+    if (accepted_sockets_.empty()) {
+      errno = EWOULDBLOCK;
+      return nullptr;
+    }
+    std::unique_ptr<LWIPRawImpl> sock = std::move(accepted_sockets_.front());
+    accepted_sockets_.pop();
+    if (addr != nullptr) {
+      sock->getpeername(addr, addrlen);
+    }
+    LWIP_LOG("accept(%p)", sock.get());
+    return std::unique_ptr<Socket>(std::move(sock));
+  }
+  int bind(const struct sockaddr *name, socklen_t addrlen) override {
+    if (pcb_ == nullptr) {
+      errno = EBADF;
+      return -1;
+    }
+    if (name == nullptr) {
+      errno = EINVAL;
+      return 0;
+    }
+    ip_addr_t ip;
+    in_port_t port;
+    auto family = name->sa_family;
+#if LWIP_IPV6
+    if (family == AF_INET) {
+      if (addrlen < sizeof(sockaddr_in6)) {
+        errno = EINVAL;
+        return -1;
+      }
+      auto *addr4 = reinterpret_cast<const sockaddr_in *>(name);
+      port = ntohs(addr4->sin_port);
+      ip.type = IPADDR_TYPE_V4;
+      ip.u_addr.ip4.addr = addr4->sin_addr.s_addr;
+
+    } else if (family == AF_INET6) {
+      if (addrlen < sizeof(sockaddr_in)) {
+        errno = EINVAL;
+        return -1;
+      }
+      auto *addr6 = reinterpret_cast<const sockaddr_in6 *>(name);
+      port = ntohs(addr6->sin6_port);
+      ip.type = IPADDR_TYPE_V6;
+      memcpy(&ip.u_addr.ip6.addr, &addr6->sin6_addr.un.u8_addr, 16);
+    } else {
+      errno = EINVAL;
+      return -1;
+    }
+#else
+    if (family != AF_INET) {
+      errno = EINVAL;
+      return -1;
+    }
+    auto *addr4 = reinterpret_cast<const sockaddr_in *>(name);
+    port = ntohs(addr4->sin_port);
+    ip.addr = addr4->sin_addr.s_addr;
+#endif
+    LWIP_LOG("tcp_bind(%p ip=%u port=%u)", pcb_, ip.addr, port);
+    err_t err = tcp_bind(pcb_, &ip, port);
+    if (err == ERR_USE) {
+      LWIP_LOG("  -> err ERR_USE");
+      errno = EADDRINUSE;
+      return -1;
+    }
+    if (err == ERR_VAL) {
+      LWIP_LOG("  -> err ERR_VAL");
+      errno = EINVAL;
+      return -1;
+    }
+    if (err != ERR_OK) {
+      LWIP_LOG("  -> err %d", err);
+      errno = EIO;
+      return -1;
+    }
+    return 0;
+  }
+  int close() override {
+    if (pcb_ == nullptr) {
+      errno = ECONNRESET;
+      return -1;
+    }
+    LWIP_LOG("tcp_close(%p)", pcb_);
+    err_t err = tcp_close(pcb_);
+    if (err != ERR_OK) {
+      LWIP_LOG("  -> err %d", err);
+      tcp_abort(pcb_);
+      pcb_ = nullptr;
+      errno = err == ERR_MEM ? ENOMEM : EIO;
+      return -1;
+    }
+    pcb_ = nullptr;
+    return 0;
+  }
+  int shutdown(int how) override {
+    if (pcb_ == nullptr) {
+      errno = ECONNRESET;
+      return -1;
+    }
+    bool shut_rx = false, shut_tx = false;
+    if (how == SHUT_RD) {
+      shut_rx = true;
+    } else if (how == SHUT_WR) {
+      shut_tx = true;
+    } else if (how == SHUT_RDWR) {
+      shut_rx = shut_tx = true;
+    } else {
+      errno = EINVAL;
+      return -1;
+    }
+    LWIP_LOG("tcp_shutdown(%p shut_rx=%d shut_tx=%d)", pcb_, shut_rx ? 1 : 0, shut_tx ? 1 : 0);
+    err_t err = tcp_shutdown(pcb_, shut_rx, shut_tx);
+    if (err != ERR_OK) {
+      LWIP_LOG("  -> err %d", err);
+      errno = err == ERR_MEM ? ENOMEM : EIO;
+      return -1;
+    }
+    return 0;
+  }
+
+  int getpeername(struct sockaddr *name, socklen_t *addrlen) override {
+    if (pcb_ == nullptr) {
+      errno = ECONNRESET;
+      return -1;
+    }
+    if (name == nullptr || addrlen == nullptr) {
+      errno = EINVAL;
+      return -1;
+    }
+    if (*addrlen < sizeof(struct sockaddr_in)) {
+      errno = EINVAL;
+      return -1;
+    }
+    struct sockaddr_in *addr = reinterpret_cast<struct sockaddr_in *>(name);
+    addr->sin_family = AF_INET;
+    *addrlen = addr->sin_len = sizeof(struct sockaddr_in);
+    addr->sin_port = pcb_->remote_port;
+    addr->sin_addr.s_addr = pcb_->remote_ip.addr;
+    return 0;
+  }
+  std::string getpeername() override {
+    if (pcb_ == nullptr) {
+      errno = ECONNRESET;
+      return "";
+    }
+    char buffer[24];
+    uint32_t ip4 = pcb_->remote_ip.addr;
+    snprintf(buffer, sizeof(buffer), "%d.%d.%d.%d", (ip4 >> 0) & 0xFF, (ip4 >> 8) & 0xFF, (ip4 >> 16) & 0xFF,
+             (ip4 >> 24) & 0xFF);
+    return std::string(buffer);
+  }
+  int getsockname(struct sockaddr *name, socklen_t *addrlen) override {
+    if (pcb_ == nullptr) {
+      errno = ECONNRESET;
+      return -1;
+    }
+    if (name == nullptr || addrlen == nullptr) {
+      errno = EINVAL;
+      return -1;
+    }
+    if (*addrlen < sizeof(struct sockaddr_in)) {
+      errno = EINVAL;
+      return -1;
+    }
+    struct sockaddr_in *addr = reinterpret_cast<struct sockaddr_in *>(name);
+    addr->sin_family = AF_INET;
+    *addrlen = addr->sin_len = sizeof(struct sockaddr_in);
+    addr->sin_port = pcb_->local_port;
+    addr->sin_addr.s_addr = pcb_->local_ip.addr;
+    return 0;
+  }
+  std::string getsockname() override {
+    if (pcb_ == nullptr) {
+      errno = ECONNRESET;
+      return "";
+    }
+    char buffer[24];
+    uint32_t ip4 = pcb_->local_ip.addr;
+    snprintf(buffer, sizeof(buffer), "%d.%d.%d.%d", (ip4 >> 0) & 0xFF, (ip4 >> 8) & 0xFF, (ip4 >> 16) & 0xFF,
+             (ip4 >> 24) & 0xFF);
+    return std::string(buffer);
+  }
+  int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override {
+    if (pcb_ == nullptr) {
+      errno = ECONNRESET;
+      return -1;
+    }
+    if (optlen == nullptr || optval == nullptr) {
+      errno = EINVAL;
+      return -1;
+    }
+    if (level == SOL_SOCKET && optname == SO_REUSEADDR) {
+      if (*optlen < 4) {
+        errno = EINVAL;
+        return -1;
+      }
+
+      // lwip doesn't seem to have this feature. Don't send an error
+      // to prevent warnings
+      *reinterpret_cast<int *>(optval) = 1;
+      *optlen = 4;
+      return 0;
+    }
+    if (level == IPPROTO_TCP && optname == TCP_NODELAY) {
+      if (*optlen < 4) {
+        errno = EINVAL;
+        return -1;
+      }
+      *reinterpret_cast<int *>(optval) = tcp_nagle_disabled(pcb_);
+      *optlen = 4;
+      return 0;
+    }
+
+    errno = EINVAL;
+    return -1;
+  }
+  int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override {
+    if (pcb_ == nullptr) {
+      errno = ECONNRESET;
+      return -1;
+    }
+    if (level == SOL_SOCKET && optname == SO_REUSEADDR) {
+      if (optlen != 4) {
+        errno = EINVAL;
+        return -1;
+      }
+
+      // lwip doesn't seem to have this feature. Don't send an error
+      // to prevent warnings
+      return 0;
+    }
+    if (level == IPPROTO_TCP && optname == TCP_NODELAY) {
+      if (optlen != 4) {
+        errno = EINVAL;
+        return -1;
+      }
+      int val = *reinterpret_cast<const int *>(optval);
+      if (val != 0) {
+        tcp_nagle_disable(pcb_);
+      } else {
+        tcp_nagle_enable(pcb_);
+      }
+      return 0;
+    }
+
+    errno = EINVAL;
+    return -1;
+  }
+  int listen(int backlog) override {
+    if (pcb_ == nullptr) {
+      errno = EBADF;
+      return -1;
+    }
+    LWIP_LOG("tcp_listen_with_backlog(%p backlog=%d)", pcb_, backlog);
+    struct tcp_pcb *listen_pcb = tcp_listen_with_backlog(pcb_, backlog);
+    if (listen_pcb == nullptr) {
+      tcp_abort(pcb_);
+      pcb_ = nullptr;
+      errno = EOPNOTSUPP;
+      return -1;
+    }
+    // tcp_listen reallocates the pcb, replace ours
+    pcb_ = listen_pcb;
+    // set callbacks on new pcb
+    LWIP_LOG("tcp_arg(%p)", pcb_);
+    tcp_arg(pcb_, this);
+    tcp_accept(pcb_, LWIPRawImpl::s_accept_fn);
+    return 0;
+  }
+  ssize_t read(void *buf, size_t len) override {
+    if (pcb_ == nullptr) {
+      errno = ECONNRESET;
+      return -1;
+    }
+    if (rx_closed_ && rx_buf_ == nullptr) {
+      errno = ECONNRESET;
+      return -1;
+    }
+    if (len == 0) {
+      return 0;
+    }
+    if (rx_buf_ == nullptr) {
+      errno = EWOULDBLOCK;
+      return -1;
+    }
+
+    size_t read = 0;
+    uint8_t *buf8 = reinterpret_cast<uint8_t *>(buf);
+    while (len && rx_buf_ != nullptr) {
+      size_t pb_len = rx_buf_->len;
+      size_t pb_left = pb_len - rx_buf_offset_;
+      if (pb_left == 0)
+        break;
+      size_t copysize = std::min(len, pb_left);
+      memcpy(buf8, reinterpret_cast<uint8_t *>(rx_buf_->payload) + rx_buf_offset_, copysize);
+
+      if (pb_left == copysize) {
+        // full pb copied, free it
+        if (rx_buf_->next == nullptr) {
+          // last buffer in chain
+          pbuf_free(rx_buf_);
+          rx_buf_ = nullptr;
+          rx_buf_offset_ = 0;
+        } else {
+          auto *old_buf = rx_buf_;
+          rx_buf_ = rx_buf_->next;
+          pbuf_ref(rx_buf_);
+          pbuf_free(old_buf);
+          rx_buf_offset_ = 0;
+        }
+      } else {
+        rx_buf_offset_ += copysize;
+      }
+      LWIP_LOG("tcp_recved(%p %u)", pcb_, copysize);
+      tcp_recved(pcb_, copysize);
+
+      buf8 += copysize;
+      len -= copysize;
+      read += copysize;
+    }
+
+    return read;
+  }
+  ssize_t write(const void *buf, size_t len) override {
+    if (pcb_ == nullptr) {
+      errno = ECONNRESET;
+      return -1;
+    }
+    if (len == 0)
+      return 0;
+    if (buf == nullptr) {
+      errno = EINVAL;
+      return 0;
+    }
+    auto space = tcp_sndbuf(pcb_);
+    if (space == 0) {
+      errno = EWOULDBLOCK;
+      return -1;
+    }
+    size_t to_send = std::min((size_t) space, len);
+    LWIP_LOG("tcp_write(%p buf=%p %u)", pcb_, buf, to_send);
+    err_t err = tcp_write(pcb_, buf, to_send, TCP_WRITE_FLAG_COPY);
+    if (err == ERR_MEM) {
+      LWIP_LOG("  -> err ERR_MEM");
+      errno = EWOULDBLOCK;
+      return -1;
+    }
+    if (err != ERR_OK) {
+      LWIP_LOG("  -> err %d", err);
+      errno = ECONNRESET;
+      return -1;
+    }
+    if (tcp_nagle_disabled(pcb_)) {
+      LWIP_LOG("tcp_output(%p)", pcb_);
+      err = tcp_output(pcb_);
+      if (err == ERR_ABRT) {
+        LWIP_LOG("  -> err ERR_ABRT");
+        // sometimes lwip returns ERR_ABRT for no apparent reason
+        // the connection works fine afterwards, and back with ESPAsyncTCP we
+        // indirectly also ignored this error
+        // FIXME: figure out where this is returned and what it means in this context
+        return to_send;
+      }
+      if (err != ERR_OK) {
+        LWIP_LOG("  -> err %d", err);
+        errno = ECONNRESET;
+        return -1;
+      }
+    }
+    return to_send;
+  }
+  int setblocking(bool blocking) override {
+    if (pcb_ == nullptr) {
+      errno = ECONNRESET;
+      return -1;
+    }
+    if (blocking) {
+      // blocking operation not supported
+      errno = EINVAL;
+      return -1;
+    }
+    return 0;
+  }
+
+  err_t accept_fn(struct tcp_pcb *newpcb, err_t err) {
+    LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err);
+    if (err != ERR_OK || newpcb == nullptr) {
+      // "An error code if there has been an error accepting. Only return ERR_ABRT if you have
+      // called tcp_abort from within the callback function!"
+      // https://www.nongnu.org/lwip/2_1_x/tcp_8h.html#a00517abce6856d6c82f0efebdafb734d
+      // nothing to do here, we just don't push it to the queue
+      return ERR_OK;
+    }
+    auto *sock = new LWIPRawImpl(newpcb);
+    sock->init();
+    accepted_sockets_.emplace(sock);
+    return ERR_OK;
+  }
+  void err_fn(err_t err) {
+    LWIP_LOG("err(err=%d)", err);
+    // "If a connection is aborted because of an error, the application is alerted of this event by
+    // the err callback."
+    // pcb is already freed when this callback is called
+    // ERR_RST: connection was reset by remote host
+    // ERR_ABRT: aborted through tcp_abort or TCP timer
+    pcb_ = nullptr;
+  }
+  err_t recv_fn(struct pbuf *pb, err_t err) {
+    LWIP_LOG("recv(pb=%p err=%d)", pb, err);
+    if (err != 0) {
+      // "An error code if there has been an error receiving Only return ERR_ABRT if you have
+      // called tcp_abort from within the callback function!"
+      rx_closed_ = true;
+      return ERR_OK;
+    }
+    if (pb == nullptr) {
+      rx_closed_ = true;
+      return ERR_OK;
+    }
+    if (rx_buf_ == nullptr) {
+      // no need to copy because lwIP gave control of it to us
+      rx_buf_ = pb;
+      rx_buf_offset_ = 0;
+    } else {
+      pbuf_cat(rx_buf_, pb);
+    }
+    return ERR_OK;
+  }
+
+  static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) {
+    LWIPRawImpl *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
+    return arg_this->accept_fn(newpcb, err);
+  }
+
+  static void s_err_fn(void *arg, err_t err) {
+    LWIPRawImpl *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
+    arg_this->err_fn(err);
+  }
+
+  static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err) {
+    LWIPRawImpl *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
+    return arg_this->recv_fn(pb, err);
+  }
+
+ protected:
+  struct tcp_pcb *pcb_;
+  std::queue<std::unique_ptr<LWIPRawImpl>> accepted_sockets_;
+  bool rx_closed_ = false;
+  pbuf *rx_buf_ = nullptr;
+  size_t rx_buf_offset_ = 0;
+};
+
+std::unique_ptr<Socket> socket(int domain, int type, int protocol) {
+  auto *pcb = tcp_new();
+  if (pcb == nullptr)
+    return nullptr;
+  auto *sock = new LWIPRawImpl(pcb);
+  sock->init();
+  return std::unique_ptr<Socket>{sock};
+}
+
+}  // namespace socket
+}  // namespace esphome
+
+#endif  // USE_SOCKET_IMPL_LWIP_TCP
diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h
new file mode 100644
index 0000000000..7a5ce79161
--- /dev/null
+++ b/esphome/components/socket/socket.h
@@ -0,0 +1,42 @@
+#pragma once
+#include <string>
+#include <memory>
+
+#include "headers.h"
+#include "esphome/core/optional.h"
+
+namespace esphome {
+namespace socket {
+
+class Socket {
+ public:
+  Socket() = default;
+  virtual ~Socket() = default;
+  Socket(const Socket &) = delete;
+  Socket &operator=(const Socket &) = delete;
+
+  virtual std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) = 0;
+  virtual int bind(const struct sockaddr *addr, socklen_t addrlen) = 0;
+  virtual int close() = 0;
+  // not supported yet:
+  // virtual int connect(const std::string &address) = 0;
+  // virtual int connect(const struct sockaddr *addr, socklen_t addrlen) = 0;
+  virtual int shutdown(int how) = 0;
+
+  virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0;
+  virtual std::string getpeername() = 0;
+  virtual int getsockname(struct sockaddr *addr, socklen_t *addrlen) = 0;
+  virtual std::string getsockname() = 0;
+  virtual int getsockopt(int level, int optname, void *optval, socklen_t *optlen) = 0;
+  virtual int setsockopt(int level, int optname, const void *optval, socklen_t optlen) = 0;
+  virtual int listen(int backlog) = 0;
+  virtual ssize_t read(void *buf, size_t len) = 0;
+  virtual ssize_t write(const void *buf, size_t len) = 0;
+  virtual int setblocking(bool blocking) = 0;
+  virtual int loop() { return 0; };
+};
+
+std::unique_ptr<Socket> socket(int domain, int type, int protocol);
+
+}  // namespace socket
+}  // namespace esphome
diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp
index 8c6ec54d4c..cb10db4ed4 100644
--- a/esphome/components/speed/fan/speed_fan.cpp
+++ b/esphome/components/speed/fan/speed_fan.cpp
@@ -56,7 +56,10 @@ void SpeedFan::loop() {
     ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable));
   }
 }
-float SpeedFan::get_setup_priority() const { return setup_priority::DATA; }
+
+// We need a higher priority than the FanState component to make sure that the traits are set
+// when that component sets itself up.
+float SpeedFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; }
 
 }  // namespace speed
 }  // namespace esphome
diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py
index 959b427861..27264cf942 100644
--- a/esphome/components/sps30/sensor.py
+++ b/esphome/components/sps30/sensor.py
@@ -13,6 +13,9 @@ from esphome.const import (
     CONF_PMC_4_0,
     CONF_PMC_10_0,
     CONF_PM_SIZE,
+    DEVICE_CLASS_PM1,
+    DEVICE_CLASS_PM10,
+    DEVICE_CLASS_PM25,
     STATE_CLASS_MEASUREMENT,
     UNIT_MICROGRAMS_PER_CUBIC_METER,
     UNIT_COUNTS_PER_CUBIC_METER,
@@ -35,12 +38,14 @@ CONFIG_SCHEMA = (
                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
                 icon=ICON_CHEMICAL_WEAPON,
                 accuracy_decimals=2,
+                device_class=DEVICE_CLASS_PM1,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
                 icon=ICON_CHEMICAL_WEAPON,
                 accuracy_decimals=2,
+                device_class=DEVICE_CLASS_PM25,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PM_4_0): sensor.sensor_schema(
@@ -53,6 +58,7 @@ CONFIG_SCHEMA = (
                 unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
                 icon=ICON_CHEMICAL_WEAPON,
                 accuracy_decimals=2,
+                device_class=DEVICE_CLASS_PM10,
                 state_class=STATE_CLASS_MEASUREMENT,
             ),
             cv.Optional(CONF_PMC_0_5): sensor.sensor_schema(
diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py
index a053d00ea2..7b38b1d2c5 100644
--- a/esphome/components/st7789v/display.py
+++ b/esphome/components/st7789v/display.py
@@ -29,7 +29,7 @@ CONFIG_SCHEMA = (
             cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema,
             cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
             cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema,
-            cv.Required(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema,
+            cv.Optional(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema,
             cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage,
         }
     )
@@ -49,8 +49,9 @@ async def to_code(config):
     reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
     cg.add(var.set_reset_pin(reset))
 
-    bl = await cg.gpio_pin_expression(config[CONF_BACKLIGHT_PIN])
-    cg.add(var.set_backlight_pin(bl))
+    if CONF_BACKLIGHT_PIN in config:
+        bl = await cg.gpio_pin_expression(config[CONF_BACKLIGHT_PIN])
+        cg.add(var.set_backlight_pin(bl))
 
     if CONF_LAMBDA in config:
         lambda_ = await cg.process_lambda(
diff --git a/esphome/components/st7920/__init__.py b/esphome/components/st7920/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/st7920/display.py b/esphome/components/st7920/display.py
new file mode 100644
index 0000000000..9b544fa644
--- /dev/null
+++ b/esphome/components/st7920/display.py
@@ -0,0 +1,42 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import display, spi
+from esphome.const import CONF_ID, CONF_LAMBDA, CONF_WIDTH, CONF_HEIGHT
+
+AUTO_LOAD = ["display"]
+CODEOWNERS = ["@marsjan155"]
+DEPENDENCIES = ["spi"]
+
+st7920_ns = cg.esphome_ns.namespace("st7920")
+ST7920 = st7920_ns.class_(
+    "ST7920", cg.PollingComponent, display.DisplayBuffer, spi.SPIDevice
+)
+ST7920Ref = ST7920.operator("ref")
+
+CONFIG_SCHEMA = (
+    display.FULL_DISPLAY_SCHEMA.extend(
+        {
+            cv.GenerateID(): cv.declare_id(ST7920),
+            cv.Required(CONF_WIDTH): cv.int_,
+            cv.Required(CONF_HEIGHT): cv.int_,
+        }
+    )
+    .extend(cv.polling_component_schema("60s"))
+    .extend(spi.spi_device_schema())
+)
+
+
+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)
+
+    if CONF_LAMBDA in config:
+        lambda_ = await cg.process_lambda(
+            config[CONF_LAMBDA], [(ST7920Ref, "it")], return_type=cg.void
+        )
+        cg.add(var.set_writer(lambda_))
+    cg.add(var.set_width(config[CONF_WIDTH]))
+    cg.add(var.set_height(config[CONF_HEIGHT]))
+
+    await display.register_display(var, config)
diff --git a/esphome/components/st7920/st7920.cpp b/esphome/components/st7920/st7920.cpp
new file mode 100644
index 0000000000..d985b0a426
--- /dev/null
+++ b/esphome/components/st7920/st7920.cpp
@@ -0,0 +1,146 @@
+#include "st7920.h"
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+#include "esphome/components/display/display_buffer.h"
+
+namespace esphome {
+namespace st7920 {
+
+static const char *const TAG = "st7920";
+
+// ST7920 COMMANDS
+static const uint8_t LCD_DATA = 0xFA;
+static const uint8_t LCD_COMMAND = 0xF8;
+static const uint8_t LCD_CLS = 0x01;
+static const uint8_t LCD_HOME = 0x02;
+static const uint8_t LCD_ADDRINC = 0x06;
+static const uint8_t LCD_DISPLAYON = 0x0C;
+static const uint8_t LCD_DISPLAYOFF = 0x08;
+static const uint8_t LCD_CURSORON = 0x0E;
+static const uint8_t LCD_CURSORBLINK = 0x0F;
+static const uint8_t LCD_BASIC = 0x30;
+static const uint8_t LCD_GFXMODE = 0x36;
+static const uint8_t LCD_EXTEND = 0x34;
+static const uint8_t LCD_TXTMODE = 0x34;
+static const uint8_t LCD_STANDBY = 0x01;
+static const uint8_t LCD_SCROLL = 0x03;
+static const uint8_t LCD_SCROLLADDR = 0x40;
+static const uint8_t LCD_ADDR = 0x80;
+static const uint8_t LCD_LINE0 = 0x80;
+static const uint8_t LCD_LINE1 = 0x90;
+static const uint8_t LCD_LINE2 = 0x88;
+static const uint8_t LCD_LINE3 = 0x98;
+
+void ST7920::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up ST7920...");
+  this->dump_config();
+  this->spi_setup();
+  this->init_internal_(this->get_buffer_length_());
+  display_init_();
+}
+
+void ST7920::command_(uint8_t value) {
+  this->enable();
+  this->send_(LCD_COMMAND, value);
+  this->disable();
+}
+
+void ST7920::data_(uint8_t value) {
+  this->enable();
+  this->send_(LCD_DATA, value);
+  this->disable();
+}
+
+void ST7920::send_(uint8_t type, uint8_t value) {
+  this->write_byte(type);
+  this->write_byte(value & 0xF0);
+  this->write_byte(value << 4);
+}
+
+void ST7920::goto_xy_(uint16_t x, uint16_t y) {
+  if (y >= 32 && y < 64) {
+    y -= 32;
+    x += 8;
+  } else if (y >= 64 && y < 64 + 32) {
+    y -= 32;
+    x += 0;
+  } else if (y >= 64 + 32 && y < 64 + 64) {
+    y -= 64;
+    x += 8;
+  }
+  this->command_(LCD_ADDR | y);  // 6-bit (0..63)
+  this->command_(LCD_ADDR | x);  // 4-bit (0..15)
+}
+
+void HOT ST7920::write_display_data() {
+  uint8_t i, j, b;
+  for (j = 0; j < this->get_height_internal() / 2; j++) {
+    this->goto_xy_(0, j);
+    this->enable();
+    for (i = 0; i < 16; i++) {  // 16 bytes from line #0+
+      b = this->buffer_[i + j * 16];
+      this->send_(LCD_DATA, b);
+    }
+    for (i = 0; i < 16; i++) {  // 16 bytes from line #32+
+      b = this->buffer_[i + (j + 32) * 16];
+      this->send_(LCD_DATA, b);
+    }
+    this->disable();
+    App.feed_wdt();
+  }
+}
+
+void ST7920::fill(Color color) { memset(this->buffer_, color.is_on() ? 0xFF : 0x00, this->get_buffer_length_()); }
+
+void ST7920::dump_config() {
+  LOG_DISPLAY("", "ST7920", this);
+  LOG_PIN("  CS Pin: ", this->cs_);
+  ESP_LOGCONFIG(TAG, "  Height: %d", this->height_);
+  ESP_LOGCONFIG(TAG, "  Width: %d", this->width_);
+}
+
+float ST7920::get_setup_priority() const { return setup_priority::PROCESSOR; }
+
+void ST7920::update() {
+  this->clear();
+  if (this->writer_local_.has_value())  // call lambda function if available
+    (*this->writer_local_)(*this);
+  this->write_display_data();
+}
+
+int ST7920::get_width_internal() { return this->width_; }
+
+int ST7920::get_height_internal() { return this->height_; }
+
+size_t ST7920::get_buffer_length_() {
+  return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u;
+}
+
+void HOT ST7920::draw_absolute_pixel_internal(int x, int y, Color color) {
+  if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) {
+    ESP_LOGW(TAG, "Position out of area: %dx%d", x, y);
+    return;
+  }
+  int width = this->get_width_internal() / 8u;
+  if (color.is_on()) {
+    this->buffer_[y * width + x / 8] |= (0x80 >> (x & 7));
+  } else {
+    this->buffer_[y * width + x / 8] &= ~(0x80 >> (x & 7));
+  }
+}
+
+void ST7920::display_init_() {
+  ESP_LOGD(TAG, "Initializing display...");
+  this->command_(LCD_BASIC);      // 8bit mode
+  this->command_(LCD_BASIC);      // 8bit mode
+  this->command_(LCD_CLS);        // clear screen
+  delay(12);                      // >10 ms delay
+  this->command_(LCD_ADDRINC);    // cursor increment right no shift
+  this->command_(LCD_DISPLAYON);  // D=1, C=0, B=0
+  this->command_(LCD_EXTEND);     // LCD_EXTEND);
+  this->command_(LCD_GFXMODE);    // LCD_GFXMODE);
+  this->write_display_data();
+}
+
+}  // namespace st7920
+}  // namespace esphome
diff --git a/esphome/components/st7920/st7920.h b/esphome/components/st7920/st7920.h
new file mode 100644
index 0000000000..d0258d922c
--- /dev/null
+++ b/esphome/components/st7920/st7920.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/display/display_buffer.h"
+#include "esphome/components/spi/spi.h"
+
+namespace esphome {
+namespace st7920 {
+
+class ST7920;
+
+using st7920_writer_t = std::function<void(ST7920 &)>;
+
+class ST7920 : public PollingComponent,
+               public display::DisplayBuffer,
+               public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
+                                     spi::DATA_RATE_1MHZ> {
+ public:
+  void set_writer(st7920_writer_t &&writer) { this->writer_local_ = writer; }
+  void set_height(uint16_t height) { this->height_ = height; }
+  void set_width(uint16_t width) { this->width_ = width; }
+
+  // ========== INTERNAL METHODS ==========
+  // (In most use cases you won't need these)
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override;
+  void update() override;
+  void fill(Color color) override;
+  void write_display_data();
+
+ protected:
+  void draw_absolute_pixel_internal(int x, int y, Color color) override;
+  int get_height_internal() override;
+  int get_width_internal() override;
+  size_t get_buffer_length_();
+  void display_init_();
+  void command_(uint8_t value);
+  void data_(uint8_t value);
+  void send_(uint8_t type, uint8_t value);
+  void goto_xy_(uint16_t x, uint16_t y);
+  void start_transaction_();
+  void end_transaction_();
+
+  int16_t width_ = 128, height_ = 64;
+  optional<st7920_writer_t> writer_local_{};
+};
+
+}  // namespace st7920
+}  // namespace esphome
diff --git a/esphome/components/t6615/t6615.cpp b/esphome/components/t6615/t6615.cpp
index 09ff61827c..c139c56ce4 100644
--- a/esphome/components/t6615/t6615.cpp
+++ b/esphome/components/t6615/t6615.cpp
@@ -6,7 +6,7 @@ namespace t6615 {
 
 static const char *const TAG = "t6615";
 
-static const uint8_t T6615_RESPONSE_BUFFER_LENGTH = 32;
+static const uint32_t T6615_TIMEOUT = 1000;
 static const uint8_t T6615_MAGIC = 0xFF;
 static const uint8_t T6615_ADDR_HOST = 0xFA;
 static const uint8_t T6615_ADDR_SENSOR = 0xFE;
@@ -19,31 +19,49 @@ static const uint8_t T6615_COMMAND_ENABLE_ABC[] = {0xB7, 0x01};
 static const uint8_t T6615_COMMAND_DISABLE_ABC[] = {0xB7, 0x02};
 static const uint8_t T6615_COMMAND_SET_ELEVATION[] = {0x03, 0x0F};
 
-void T6615Component::loop() {
-  if (!this->available())
-    return;
+void T6615Component::send_ppm_command_() {
+  this->command_time_ = millis();
+  this->command_ = T6615Command::GET_PPM;
+  this->write_byte(T6615_MAGIC);
+  this->write_byte(T6615_ADDR_SENSOR);
+  this->write_byte(sizeof(T6615_COMMAND_GET_PPM));
+  this->write_array(T6615_COMMAND_GET_PPM, sizeof(T6615_COMMAND_GET_PPM));
+}
 
-  // Read header
-  uint8_t header[3];
-  this->read_array(header, 3);
-  if (header[0] != T6615_MAGIC || header[1] != T6615_ADDR_HOST) {
-    ESP_LOGW(TAG, "Reading data from T6615 failed!");
-    while (this->available())
-      this->read();  // Clear the incoming buffer
-    this->status_set_warning();
+void T6615Component::loop() {
+  if (this->available() < 5) {
+    if (this->command_ == T6615Command::GET_PPM && millis() - this->command_time_ > T6615_TIMEOUT) {
+      /* command got eaten, clear the buffer and fire another */
+      while (this->available())
+        this->read();
+      this->send_ppm_command_();
+    }
     return;
   }
 
-  // Read body
-  uint8_t length = header[2];
-  uint8_t response[T6615_RESPONSE_BUFFER_LENGTH];
-  this->read_array(response, length);
+  uint8_t response_buffer[6];
+
+  /* by the time we get here, we know we have at least five bytes in the buffer */
+  this->read_array(response_buffer, 5);
+
+  // Read header
+  if (response_buffer[0] != T6615_MAGIC || response_buffer[1] != T6615_ADDR_HOST) {
+    ESP_LOGW(TAG, "Got bad data from T6615! Magic was %02X and address was %02X", response_buffer[0],
+             response_buffer[1]);
+    /* make sure the buffer is empty */
+    while (this->available())
+      this->read();
+    /* try again to read the sensor */
+    this->send_ppm_command_();
+    this->status_set_warning();
+    return;
+  }
 
   this->status_clear_warning();
 
   switch (this->command_) {
     case T6615Command::GET_PPM: {
-      const uint16_t ppm = encode_uint16(response[0], response[1]);
+      const uint16_t ppm = encode_uint16(response_buffer[3], response_buffer[4]);
       ESP_LOGD(TAG, "T6615 Received CO₂=%uppm", ppm);
       this->co2_sensor_->publish_state(ppm);
       break;
@@ -51,23 +69,19 @@ void T6615Component::loop() {
     default:
       break;
   }
-
+  this->command_time_ = 0;
   this->command_ = T6615Command::NONE;
 }
 
 void T6615Component::update() { this->query_ppm_(); }
 
 void T6615Component::query_ppm_() {
-  if (this->co2_sensor_ == nullptr || this->command_ != T6615Command::NONE) {
+  if (this->co2_sensor_ == nullptr ||
+      (this->command_ != T6615Command::NONE && millis() - this->command_time_ < T6615_TIMEOUT)) {
     return;
   }
 
-  this->command_ = T6615Command::GET_PPM;
-
-  this->write_byte(T6615_MAGIC);
-  this->write_byte(T6615_ADDR_SENSOR);
-  this->write_byte(sizeof(T6615_COMMAND_GET_PPM));
-  this->write_array(T6615_COMMAND_GET_PPM, sizeof(T6615_COMMAND_GET_PPM));
+  this->send_ppm_command_();
 }
 
 float T6615Component::get_setup_priority() const { return setup_priority::DATA; }
diff --git a/esphome/components/t6615/t6615.h b/esphome/components/t6615/t6615.h
index a7da3b4cf6..a075685023 100644
--- a/esphome/components/t6615/t6615.h
+++ b/esphome/components/t6615/t6615.h
@@ -32,8 +32,10 @@ class T6615Component : public PollingComponent, public uart::UARTDevice {
 
  protected:
   void query_ppm_();
+  void send_ppm_command_();
 
   T6615Command command_ = T6615Command::NONE;
+  unsigned long command_time_ = 0;
 
   sensor::Sensor *co2_sensor_{nullptr};
 };
diff --git a/esphome/components/template/number/__init__.py b/esphome/components/template/number/__init__.py
index 22bbaacc15..887f6b15ad 100644
--- a/esphome/components/template/number/__init__.py
+++ b/esphome/components/template/number/__init__.py
@@ -29,12 +29,16 @@ def validate_min_max(config):
 
 def validate(config):
     if CONF_LAMBDA in config:
-        if CONF_OPTIMISTIC in config:
+        if config[CONF_OPTIMISTIC]:
             raise cv.Invalid("optimistic cannot be used with lambda")
         if CONF_INITIAL_VALUE in config:
             raise cv.Invalid("initial_value cannot be used with lambda")
         if CONF_RESTORE_VALUE in config:
             raise cv.Invalid("restore_value cannot be used with lambda")
+    if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config:
+        raise cv.Invalid(
+            "Either optimistic mode must be enabled, or set_action must be set, to handle the number being set."
+        )
     return config
 
 
@@ -46,7 +50,7 @@ CONFIG_SCHEMA = cv.All(
             cv.Required(CONF_MIN_VALUE): cv.float_,
             cv.Required(CONF_STEP): cv.positive_float,
             cv.Optional(CONF_LAMBDA): cv.returning_lambda,
-            cv.Optional(CONF_OPTIMISTIC): cv.boolean,
+            cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
             cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True),
             cv.Optional(CONF_INITIAL_VALUE): cv.float_,
             cv.Optional(CONF_RESTORE_VALUE): cv.boolean,
@@ -75,8 +79,7 @@ async def to_code(config):
         cg.add(var.set_template(template_))
 
     else:
-        if CONF_OPTIMISTIC in config:
-            cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
+        cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
         if CONF_INITIAL_VALUE in config:
             cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE]))
         if CONF_RESTORE_VALUE in config:
diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py
index 3a707628a8..4eba77119d 100644
--- a/esphome/components/template/select/__init__.py
+++ b/esphome/components/template/select/__init__.py
@@ -19,14 +19,26 @@ TemplateSelect = template_ns.class_(
 CONF_SET_ACTION = "set_action"
 
 
-def validate_initial_value_in_options(config):
-    if CONF_INITIAL_OPTION in config:
+def validate(config):
+    if CONF_LAMBDA in config:
+        if config[CONF_OPTIMISTIC]:
+            raise cv.Invalid("optimistic cannot be used with lambda")
+        if CONF_INITIAL_OPTION in config:
+            raise cv.Invalid("initial_value cannot be used with lambda")
+        if CONF_RESTORE_VALUE in config:
+            raise cv.Invalid("restore_value cannot be used with lambda")
+    elif CONF_INITIAL_OPTION in config:
         if config[CONF_INITIAL_OPTION] not in config[CONF_OPTIONS]:
             raise cv.Invalid(
                 f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]"
             )
     else:
         config[CONF_INITIAL_OPTION] = config[CONF_OPTIONS][0]
+
+    if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config:
+        raise cv.Invalid(
+            "Either optimistic mode must be enabled, or set_action must be set, to handle the option being set."
+        )
     return config
 
 
@@ -38,13 +50,13 @@ CONFIG_SCHEMA = cv.All(
                 cv.ensure_list(cv.string_strict), cv.Length(min=1)
             ),
             cv.Optional(CONF_LAMBDA): cv.returning_lambda,
-            cv.Optional(CONF_OPTIMISTIC): cv.boolean,
+            cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
             cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True),
             cv.Optional(CONF_INITIAL_OPTION): cv.string_strict,
             cv.Optional(CONF_RESTORE_VALUE): cv.boolean,
         }
     ).extend(cv.polling_component_schema("60s")),
-    validate_initial_value_in_options,
+    validate,
 )
 
 
@@ -60,9 +72,7 @@ async def to_code(config):
         cg.add(var.set_template(template_))
 
     else:
-        if CONF_OPTIMISTIC in config:
-            cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
-
+        cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
         cg.add(var.set_initial_option(config[CONF_INITIAL_OPTION]))
 
         if CONF_RESTORE_VALUE in config:
diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp
index 782c0ee6f9..8695880856 100644
--- a/esphome/components/template/select/template_select.cpp
+++ b/esphome/components/template/select/template_select.cpp
@@ -11,7 +11,7 @@ void TemplateSelect::setup() {
     return;
 
   std::string value;
-  ESP_LOGD(TAG, "Setting up Template Number");
+  ESP_LOGD(TAG, "Setting up Template Select");
   if (!this->restore_value_) {
     value = this->initial_option_;
     ESP_LOGD(TAG, "State from initial: %s", value.c_str());
diff --git a/esphome/components/template/sensor/template_sensor.cpp b/esphome/components/template/sensor/template_sensor.cpp
index 9324cb5dea..63cbd70db0 100644
--- a/esphome/components/template/sensor/template_sensor.cpp
+++ b/esphome/components/template/sensor/template_sensor.cpp
@@ -7,12 +7,13 @@ namespace template_ {
 static const char *const TAG = "template.sensor";
 
 void TemplateSensor::update() {
-  if (!this->f_.has_value())
-    return;
-
-  auto val = (*this->f_)();
-  if (val.has_value()) {
-    this->publish_state(*val);
+  if (this->f_.has_value()) {
+    auto val = (*this->f_)();
+    if (val.has_value()) {
+      this->publish_state(*val);
+    }
+  } else if (!isnan(this->get_raw_state())) {
+    this->publish_state(this->get_raw_state());
   }
 }
 float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; }
diff --git a/esphome/components/template/switch/__init__.py b/esphome/components/template/switch/__init__.py
index b00710dfb7..6095a7c561 100644
--- a/esphome/components/template/switch/__init__.py
+++ b/esphome/components/template/switch/__init__.py
@@ -16,17 +16,38 @@ from .. import template_ns
 
 TemplateSwitch = template_ns.class_("TemplateSwitch", switch.Switch, cg.Component)
 
-CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend(
-    {
-        cv.GenerateID(): cv.declare_id(TemplateSwitch),
-        cv.Optional(CONF_LAMBDA): cv.returning_lambda,
-        cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
-        cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean,
-        cv.Optional(CONF_TURN_OFF_ACTION): automation.validate_automation(single=True),
-        cv.Optional(CONF_TURN_ON_ACTION): automation.validate_automation(single=True),
-        cv.Optional(CONF_RESTORE_STATE, default=False): cv.boolean,
-    }
-).extend(cv.COMPONENT_SCHEMA)
+
+def validate(config):
+    if (
+        not config[CONF_OPTIMISTIC]
+        and CONF_TURN_ON_ACTION not in config
+        and CONF_TURN_OFF_ACTION not in config
+    ):
+        raise cv.Invalid(
+            "Either optimistic mode must be enabled, or turn_on_action or turn_off_action must be set, "
+            "to handle the switch being set."
+        )
+    return config
+
+
+CONFIG_SCHEMA = cv.All(
+    switch.SWITCH_SCHEMA.extend(
+        {
+            cv.GenerateID(): cv.declare_id(TemplateSwitch),
+            cv.Optional(CONF_LAMBDA): cv.returning_lambda,
+            cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
+            cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean,
+            cv.Optional(CONF_TURN_OFF_ACTION): automation.validate_automation(
+                single=True
+            ),
+            cv.Optional(CONF_TURN_ON_ACTION): automation.validate_automation(
+                single=True
+            ),
+            cv.Optional(CONF_RESTORE_STATE, default=False): cv.boolean,
+        }
+    ).extend(cv.COMPONENT_SCHEMA),
+    validate,
+)
 
 
 async def to_code(config):
diff --git a/esphome/components/template/text_sensor/template_text_sensor.cpp b/esphome/components/template/text_sensor/template_text_sensor.cpp
index 885ad47bbf..83bebb5bcf 100644
--- a/esphome/components/template/text_sensor/template_text_sensor.cpp
+++ b/esphome/components/template/text_sensor/template_text_sensor.cpp
@@ -7,12 +7,13 @@ namespace template_ {
 static const char *const TAG = "template.text_sensor";
 
 void TemplateTextSensor::update() {
-  if (!this->f_.has_value())
-    return;
-
-  auto val = (*this->f_)();
-  if (val.has_value()) {
-    this->publish_state(*val);
+  if (this->f_.has_value()) {
+    auto val = (*this->f_)();
+    if (val.has_value()) {
+      this->publish_state(*val);
+    }
+  } else if (this->has_state()) {
+    this->publish_state(this->state);
   }
 }
 float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; }
diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp
index c2a93b5191..27a2d84da6 100644
--- a/esphome/components/time/real_time_clock.cpp
+++ b/esphome/components/time/real_time_clock.cpp
@@ -35,9 +35,8 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
   }
 
   auto time = this->now();
-  char buf[128];
-  time.strftime(buf, sizeof(buf), "%c");
-  ESP_LOGD(TAG, "Synchronized time: %s", buf);
+  ESP_LOGD(TAG, "Synchronized time: %d-%d-%d %d:%d:%d", time.year, time.month, time.day_of_month, time.hour,
+           time.minute, time.second);
 
   this->time_sync_callback_.call();
 }
diff --git a/esphome/components/total_daily_energy/sensor.py b/esphome/components/total_daily_energy/sensor.py
index 6e75feae48..46eaac98eb 100644
--- a/esphome/components/total_daily_energy/sensor.py
+++ b/esphome/components/total_daily_energy/sensor.py
@@ -5,8 +5,8 @@ from esphome.const import (
     CONF_ID,
     CONF_TIME_ID,
     DEVICE_CLASS_ENERGY,
-    LAST_RESET_TYPE_AUTO,
-    STATE_CLASS_MEASUREMENT,
+    CONF_METHOD,
+    STATE_CLASS_TOTAL_INCREASING,
 )
 
 DEPENDENCIES = ["time"]
@@ -14,6 +14,12 @@ DEPENDENCIES = ["time"]
 CONF_POWER_ID = "power_id"
 CONF_MIN_SAVE_INTERVAL = "min_save_interval"
 total_daily_energy_ns = cg.esphome_ns.namespace("total_daily_energy")
+TotalDailyEnergyMethod = total_daily_energy_ns.enum("TotalDailyEnergyMethod")
+TOTAL_DAILY_ENERGY_METHODS = {
+    "trapezoid": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID,
+    "left": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_LEFT,
+    "right": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_RIGHT,
+}
 TotalDailyEnergy = total_daily_energy_ns.class_(
     "TotalDailyEnergy", sensor.Sensor, cg.Component
 )
@@ -21,8 +27,7 @@ TotalDailyEnergy = total_daily_energy_ns.class_(
 CONFIG_SCHEMA = (
     sensor.sensor_schema(
         device_class=DEVICE_CLASS_ENERGY,
-        state_class=STATE_CLASS_MEASUREMENT,
-        last_reset_type=LAST_RESET_TYPE_AUTO,
+        state_class=STATE_CLASS_TOTAL_INCREASING,
     )
     .extend(
         {
@@ -32,6 +37,9 @@ CONFIG_SCHEMA = (
             cv.Optional(
                 CONF_MIN_SAVE_INTERVAL, default="0s"
             ): cv.positive_time_period_milliseconds,
+            cv.Optional(CONF_METHOD, default="right"): cv.enum(
+                TOTAL_DAILY_ENERGY_METHODS, lower=True
+            ),
         }
     )
     .extend(cv.COMPONENT_SCHEMA)
@@ -49,3 +57,4 @@ async def to_code(config):
     time_ = await cg.get_variable(config[CONF_TIME_ID])
     cg.add(var.set_time(time_))
     cg.add(var.set_min_save_interval(config[CONF_MIN_SAVE_INTERVAL]))
+    cg.add(var.set_method(config[CONF_METHOD]))
diff --git a/esphome/components/total_daily_energy/total_daily_energy.cpp b/esphome/components/total_daily_energy/total_daily_energy.cpp
index 1e60442ae7..83333acab7 100644
--- a/esphome/components/total_daily_energy/total_daily_energy.cpp
+++ b/esphome/components/total_daily_energy/total_daily_energy.cpp
@@ -20,7 +20,9 @@ void TotalDailyEnergy::setup() {
 
   this->parent_->add_on_state_callback([this](float state) { this->process_new_state_(state); });
 }
+
 void TotalDailyEnergy::dump_config() { LOG_SENSOR("", "Total Daily Energy", this); }
+
 void TotalDailyEnergy::loop() {
   auto t = this->time_->now();
   if (!t.is_valid())
@@ -37,6 +39,7 @@ void TotalDailyEnergy::loop() {
     this->publish_state_and_save(0);
   }
 }
+
 void TotalDailyEnergy::publish_state_and_save(float state) {
   this->total_energy_ = state;
   this->publish_state(state);
@@ -47,13 +50,29 @@ void TotalDailyEnergy::publish_state_and_save(float state) {
   this->last_save_ = now;
   this->pref_.save(&state);
 }
+
 void TotalDailyEnergy::process_new_state_(float state) {
   if (isnan(state))
     return;
   const uint32_t now = millis();
+  const float old_state = this->last_power_state_;
+  const float new_state = state;
   float delta_hours = (now - this->last_update_) / 1000.0f / 60.0f / 60.0f;
+  float delta_energy = 0.0f;
+  switch (this->method_) {
+    case TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID:
+      delta_energy = delta_hours * (old_state + new_state) / 2.0;
+      break;
+    case TOTAL_DAILY_ENERGY_METHOD_LEFT:
+      delta_energy = delta_hours * old_state;
+      break;
+    case TOTAL_DAILY_ENERGY_METHOD_RIGHT:
+      delta_energy = delta_hours * new_state;
+      break;
+  }
+  this->last_power_state_ = new_state;
   this->last_update_ = now;
-  this->publish_state_and_save(this->total_energy_ + state * delta_hours);
+  this->publish_state_and_save(this->total_energy_ + delta_energy);
 }
 
 }  // namespace total_daily_energy
diff --git a/esphome/components/total_daily_energy/total_daily_energy.h b/esphome/components/total_daily_energy/total_daily_energy.h
index 123446c534..fd71b8decc 100644
--- a/esphome/components/total_daily_energy/total_daily_energy.h
+++ b/esphome/components/total_daily_energy/total_daily_energy.h
@@ -8,11 +8,18 @@
 namespace esphome {
 namespace total_daily_energy {
 
+enum TotalDailyEnergyMethod {
+  TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID = 0,
+  TOTAL_DAILY_ENERGY_METHOD_LEFT,
+  TOTAL_DAILY_ENERGY_METHOD_RIGHT,
+};
+
 class TotalDailyEnergy : public sensor::Sensor, public Component {
  public:
   void set_min_save_interval(uint32_t min_interval) { this->min_save_interval_ = min_interval; }
   void set_time(time::RealTimeClock *time) { time_ = time; }
   void set_parent(Sensor *parent) { parent_ = parent; }
+  void set_method(TotalDailyEnergyMethod method) { method_ = method; }
   void setup() override;
   void dump_config() override;
   float get_setup_priority() const override { return setup_priority::DATA; }
@@ -29,11 +36,13 @@ class TotalDailyEnergy : public sensor::Sensor, public Component {
   ESPPreferenceObject pref_;
   time::RealTimeClock *time_;
   Sensor *parent_;
+  TotalDailyEnergyMethod method_;
   uint16_t last_day_of_year_{};
   uint32_t last_update_{0};
   uint32_t last_save_{0};
   uint32_t min_save_interval_{0};
   float total_energy_{0.0f};
+  float last_power_state_{0.0f};
 };
 
 }  // namespace total_daily_energy
diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp
index e9f8ce8e96..f060b18eba 100644
--- a/esphome/components/tuya/fan/tuya_fan.cpp
+++ b/esphome/components/tuya/fan/tuya_fan.cpp
@@ -84,5 +84,9 @@ void TuyaFan::write_state() {
   }
 }
 
+// We need a higher priority than the FanState component to make sure that the traits are set
+// when that component sets itself up.
+float TuyaFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; }
+
 }  // namespace tuya
 }  // namespace esphome
diff --git a/esphome/components/tuya/fan/tuya_fan.h b/esphome/components/tuya/fan/tuya_fan.h
index a24e7a218e..e96770d8c3 100644
--- a/esphome/components/tuya/fan/tuya_fan.h
+++ b/esphome/components/tuya/fan/tuya_fan.h
@@ -11,6 +11,7 @@ class TuyaFan : public Component {
  public:
   TuyaFan(Tuya *parent, fan::FanState *fan, int speed_count) : parent_(parent), fan_(fan), speed_count_(speed_count) {}
   void setup() override;
+  float get_setup_priority() const override;
   void dump_config() override;
   void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; }
   void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; }
diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h
index 1dc1e18412..8ac00658f4 100644
--- a/esphome/components/uart/uart.h
+++ b/esphome/components/uart/uart.h
@@ -118,6 +118,11 @@ class UARTComponent : public Component, public Stream {
   uint8_t stop_bits_;
   uint8_t data_bits_;
   UARTParityOptions parity_;
+
+ private:
+#ifdef ARDUINO_ARCH_ESP8266
+  static bool serial0InUse;
+#endif
 };
 
 #ifdef ARDUINO_ARCH_ESP32
diff --git a/esphome/components/uart/uart_esp32.cpp b/esphome/components/uart/uart_esp32.cpp
index 16d683e4a6..c672a34c36 100644
--- a/esphome/components/uart/uart_esp32.cpp
+++ b/esphome/components/uart/uart_esp32.cpp
@@ -73,7 +73,11 @@ void UARTComponent::setup() {
   // Use Arduino HardwareSerial UARTs if all used pins match the ones
   // preconfigured by the platform. For example if RX disabled but TX pin
   // is 1 we still want to use Serial.
+#ifdef CONFIG_IDF_TARGET_ESP32C3
+  if (this->tx_pin_.value_or(21) == 21 && this->rx_pin_.value_or(20) == 20) {
+#else
   if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) {
+#endif
     this->hw_serial_ = &Serial;
   } else {
     this->hw_serial_ = new HardwareSerial(next_uart_num++);
diff --git a/esphome/components/uart/uart_esp8266.cpp b/esphome/components/uart/uart_esp8266.cpp
index c45f48644c..5cb625f2ff 100644
--- a/esphome/components/uart/uart_esp8266.cpp
+++ b/esphome/components/uart/uart_esp8266.cpp
@@ -4,11 +4,17 @@
 #include "esphome/core/helpers.h"
 #include "esphome/core/application.h"
 #include "esphome/core/defines.h"
-#
+
+#ifdef USE_LOGGER
+#include "esphome/components/logger/logger.h"
+#endif
+
 namespace esphome {
 namespace uart {
 
 static const char *const TAG = "uart_esp8266";
+bool UARTComponent::serial0InUse = false;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+
 uint32_t UARTComponent::get_config() {
   uint32_t config = 0;
 
@@ -49,15 +55,31 @@ void UARTComponent::setup() {
   // is 1 we still want to use Serial.
   SerialConfig config = static_cast<SerialConfig>(get_config());
 
-  if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) {
+  if (!UARTComponent::serial0InUse && this->tx_pin_.value_or(1) == 1 &&
+      this->rx_pin_.value_or(3) == 3
+#ifdef USE_LOGGER
+      // we will use UART0 if logger isn't using it in swapped mode
+      && (logger::global_logger->get_hw_serial() == nullptr ||
+          logger::global_logger->get_uart() != logger::UART_SELECTION_UART0_SWAP)
+#endif
+  ) {
     this->hw_serial_ = &Serial;
     this->hw_serial_->begin(this->baud_rate_, config);
     this->hw_serial_->setRxBufferSize(this->rx_buffer_size_);
-  } else if (this->tx_pin_.value_or(15) == 15 && this->rx_pin_.value_or(13) == 13) {
+    UARTComponent::serial0InUse = true;
+  } else if (!UARTComponent::serial0InUse && this->tx_pin_.value_or(15) == 15 &&
+             this->rx_pin_.value_or(13) == 13
+#ifdef USE_LOGGER
+             // we will use UART0 swapped if logger isn't using it in regular mode
+             && (logger::global_logger->get_hw_serial() == nullptr ||
+                 logger::global_logger->get_uart() != logger::UART_SELECTION_UART0)
+#endif
+  ) {
     this->hw_serial_ = &Serial;
     this->hw_serial_->begin(this->baud_rate_, config);
     this->hw_serial_->setRxBufferSize(this->rx_buffer_size_);
     this->hw_serial_->swap();
+    UARTComponent::serial0InUse = true;
   } else if (this->tx_pin_.value_or(2) == 2 && this->rx_pin_.value_or(8) == 8) {
     this->hw_serial_ = &Serial1;
     this->hw_serial_->begin(this->baud_rate_, config);
diff --git a/esphome/components/uptime/sensor.py b/esphome/components/uptime/sensor.py
index 6ea3cca189..7989f3befc 100644
--- a/esphome/components/uptime/sensor.py
+++ b/esphome/components/uptime/sensor.py
@@ -3,7 +3,7 @@ import esphome.config_validation as cv
 from esphome.components import sensor
 from esphome.const import (
     CONF_ID,
-    STATE_CLASS_NONE,
+    STATE_CLASS_TOTAL_INCREASING,
     UNIT_SECOND,
     ICON_TIMER,
 )
@@ -16,7 +16,7 @@ CONFIG_SCHEMA = (
         unit_of_measurement=UNIT_SECOND,
         icon=ICON_TIMER,
         accuracy_decimals=0,
-        state_class=STATE_CLASS_NONE,
+        state_class=STATE_CLASS_TOTAL_INCREASING,
     )
     .extend(
         {
diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py
index a181f83c64..7f17767657 100644
--- a/esphome/components/web_server/__init__.py
+++ b/esphome/components/web_server/__init__.py
@@ -13,7 +13,7 @@ from esphome.const import (
     CONF_USERNAME,
     CONF_PASSWORD,
 )
-from esphome.core import coroutine_with_priority
+from esphome.core import CORE, coroutine_with_priority
 
 AUTO_LOAD = ["json", "web_server_base"]
 
@@ -61,9 +61,11 @@ async def to_code(config):
         cg.add(var.set_password(config[CONF_AUTH][CONF_PASSWORD]))
     if CONF_CSS_INCLUDE in config:
         cg.add_define("WEBSERVER_CSS_INCLUDE")
-        with open(config[CONF_CSS_INCLUDE], "r") as myfile:
+        path = CORE.relative_config_path(config[CONF_CSS_INCLUDE])
+        with open(file=path, mode="r", encoding="utf-8") as myfile:
             cg.add(var.set_css_include(myfile.read()))
     if CONF_JS_INCLUDE in config:
         cg.add_define("WEBSERVER_JS_INCLUDE")
-        with open(config[CONF_JS_INCLUDE], "r") as myfile:
+        path = CORE.relative_config_path(config[CONF_JS_INCLUDE])
+        with open(file=path, mode="r", encoding="utf-8") as myfile:
             cg.add(var.set_js_include(myfile.read()))
diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp
index 9dad61bb5b..dc97bcd5c2 100644
--- a/esphome/components/web_server/web_server.cpp
+++ b/esphome/components/web_server/web_server.cpp
@@ -397,14 +397,15 @@ std::string WebServer::fan_json(fan::FanState *obj) {
     const auto traits = obj->get_traits();
     if (traits.supports_speed()) {
       root["speed_level"] = obj->speed;
+      // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations)
       switch (fan::speed_level_to_enum(obj->speed, traits.supported_speed_count())) {
-        case fan::FAN_SPEED_LOW:
+        case fan::FAN_SPEED_LOW:  // NOLINT(clang-diagnostic-deprecated-declarations)
           root["speed"] = "low";
           break;
-        case fan::FAN_SPEED_MEDIUM:
+        case fan::FAN_SPEED_MEDIUM:  // NOLINT(clang-diagnostic-deprecated-declarations)
           root["speed"] = "medium";
           break;
-        case fan::FAN_SPEED_HIGH:
+        case fan::FAN_SPEED_HIGH:  // NOLINT(clang-diagnostic-deprecated-declarations)
           root["speed"] = "high";
           break;
       }
@@ -430,7 +431,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
       auto call = obj->turn_on();
       if (request->hasParam("speed")) {
         String speed = request->getParam("speed")->value();
-        call.set_speed(speed.c_str());
+        call.set_speed(speed.c_str());  // NOLINT(clang-diagnostic-deprecated-declarations)
       }
       if (request->hasParam("speed_level")) {
         String speed_level = request->getParam("speed_level")->value();
diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py
index d066570cc8..c2943d0645 100644
--- a/esphome/components/wifi/__init__.py
+++ b/esphome/components/wifi/__init__.py
@@ -305,7 +305,7 @@ def wifi_network(config, static_ip):
         cg.add(ap.set_password(config[CONF_PASSWORD]))
     if CONF_EAP in config:
         cg.add(ap.set_eap(eap_auth(config[CONF_EAP])))
-        cg.add_define("ESPHOME_WIFI_WPA2_EAP")
+        cg.add_define("USE_WIFI_WPA2_EAP")
     if CONF_BSSID in config:
         cg.add(ap.set_bssid([HexInt(i) for i in config[CONF_BSSID].parts]))
     if CONF_HIDDEN in config:
diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp
index e99cd0e1b1..282900260d 100644
--- a/esphome/components/wifi/wifi_component.cpp
+++ b/esphome/components/wifi/wifi_component.cpp
@@ -244,6 +244,8 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa
   sta.set_ssid(ssid);
   sta.set_password(password);
   this->set_sta(sta);
+
+  this->start_scanning();
 }
 
 void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
@@ -258,7 +260,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
     ESP_LOGV(TAG, "  BSSID: Not Set");
   }
 
-#ifdef ESPHOME_WIFI_WPA2_EAP
+#ifdef USE_WIFI_WPA2_EAP
   if (ap.get_eap().has_value()) {
     ESP_LOGV(TAG, "  WPA2 Enterprise authentication configured:");
     EAPAuth eap_config = ap.get_eap().value();
@@ -274,7 +276,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
   } else {
 #endif
     ESP_LOGV(TAG, "  Password: " LOG_SECRET("'%s'"), ap.get_password().c_str());
-#ifdef ESPHOME_WIFI_WPA2_EAP
+#ifdef USE_WIFI_WPA2_EAP
   }
 #endif
   if (ap.get_channel().has_value()) {
@@ -478,7 +480,7 @@ void WiFiComponent::check_scanning_finished() {
     // copy manual IP (if set)
     connect_params.set_manual_ip(config.get_manual_ip());
 
-#ifdef ESPHOME_WIFI_WPA2_EAP
+#ifdef USE_WIFI_WPA2_EAP
     // copy EAP parameters (if set)
     connect_params.set_eap(config.get_eap());
 #endif
@@ -638,8 +640,8 @@ void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
 void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; }
 void WiFiAP::set_bssid(optional<bssid_t> bssid) { this->bssid_ = bssid; }
 void WiFiAP::set_password(const std::string &password) { this->password_ = password; }
-#ifdef ESPHOME_WIFI_WPA2_EAP
-void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = eap_auth; }
+#ifdef USE_WIFI_WPA2_EAP
+void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
 #endif
 void WiFiAP::set_channel(optional<uint8_t> channel) { this->channel_ = channel; }
 void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = std::move(manual_ip); }
@@ -647,7 +649,7 @@ void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
 const std::string &WiFiAP::get_ssid() const { return this->ssid_; }
 const optional<bssid_t> &WiFiAP::get_bssid() const { return this->bssid_; }
 const std::string &WiFiAP::get_password() const { return this->password_; }
-#ifdef ESPHOME_WIFI_WPA2_EAP
+#ifdef USE_WIFI_WPA2_EAP
 const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
 #endif
 const optional<uint8_t> &WiFiAP::get_channel() const { return this->channel_; }
@@ -679,7 +681,7 @@ bool WiFiScanResult::matches(const WiFiAP &config) {
   if (config.get_bssid().has_value() && *config.get_bssid() != this->bssid_)
     return false;
 
-#ifdef ESPHOME_WIFI_WPA2_EAP
+#ifdef USE_WIFI_WPA2_EAP
   // BSSID requires auth but no PSK or EAP credentials given
   if (this->with_auth_ && (config.get_password().empty() && !config.get_eap().has_value()))
     return false;
diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h
index f698e09d93..3a4213c93c 100644
--- a/esphome/components/wifi/wifi_component.h
+++ b/esphome/components/wifi/wifi_component.h
@@ -1,5 +1,6 @@
 #pragma once
 
+#include "esphome/core/macros.h"
 #include "esphome/core/component.h"
 #include "esphome/core/defines.h"
 #include "esphome/core/automation.h"
@@ -17,7 +18,7 @@
 #include <ESP8266WiFiType.h>
 #include <ESP8266WiFi.h>
 
-#ifdef ARDUINO_ESP8266_RELEASE_2_3_0
+#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0)
 extern "C" {
 #include <user_interface.h>
 };
@@ -62,7 +63,7 @@ struct ManualIP {
   IPAddress dns2;  ///< The second DNS server. 0.0.0.0 for default.
 };
 
-#ifdef ESPHOME_WIFI_WPA2_EAP
+#ifdef USE_WIFI_WPA2_EAP
 struct EAPAuth {
   std::string identity;  // required for all auth types
   std::string username;
@@ -72,7 +73,7 @@ struct EAPAuth {
   const char *client_cert;
   const char *client_key;
 };
-#endif  // ESPHOME_WIFI_WPA2_EAP
+#endif  // USE_WIFI_WPA2_EAP
 
 using bssid_t = std::array<uint8_t, 6>;
 
@@ -82,9 +83,9 @@ class WiFiAP {
   void set_bssid(bssid_t bssid);
   void set_bssid(optional<bssid_t> bssid);
   void set_password(const std::string &password);
-#ifdef ESPHOME_WIFI_WPA2_EAP
+#ifdef USE_WIFI_WPA2_EAP
   void set_eap(optional<EAPAuth> eap_auth);
-#endif  // ESPHOME_WIFI_WPA2_EAP
+#endif  // USE_WIFI_WPA2_EAP
   void set_channel(optional<uint8_t> channel);
   void set_priority(float priority) { priority_ = priority; }
   void set_manual_ip(optional<ManualIP> manual_ip);
@@ -92,9 +93,9 @@ class WiFiAP {
   const std::string &get_ssid() const;
   const optional<bssid_t> &get_bssid() const;
   const std::string &get_password() const;
-#ifdef ESPHOME_WIFI_WPA2_EAP
+#ifdef USE_WIFI_WPA2_EAP
   const optional<EAPAuth> &get_eap() const;
-#endif  // ESPHOME_WIFI_WPA2_EAP
+#endif  // USE_WIFI_WPA2_EAP
   const optional<uint8_t> &get_channel() const;
   float get_priority() const { return priority_; }
   const optional<ManualIP> &get_manual_ip() const;
@@ -104,9 +105,9 @@ class WiFiAP {
   std::string ssid_;
   optional<bssid_t> bssid_;
   std::string password_;
-#ifdef ESPHOME_WIFI_WPA2_EAP
+#ifdef USE_WIFI_WPA2_EAP
   optional<EAPAuth> eap_;
-#endif  // ESPHOME_WIFI_WPA2_EAP
+#endif  // USE_WIFI_WPA2_EAP
   optional<uint8_t> channel_;
   float priority_{0};
   optional<ManualIP> manual_ip_;
diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp
index 1bccf08a7f..b56030db56 100644
--- a/esphome/components/wifi/wifi_component_esp32.cpp
+++ b/esphome/components/wifi/wifi_component_esp32.cpp
@@ -6,11 +6,12 @@
 
 #include <utility>
 #include <algorithm>
-#ifdef ESPHOME_WIFI_WPA2_EAP
+#ifdef USE_WIFI_WPA2_EAP
 #include <esp_wpa2.h>
 #endif
 #include "lwip/err.h"
 #include "lwip/dns.h"
+#include "lwip/apps/sntp.h"
 
 #include "esphome/core/helpers.h"
 #include "esphome/core/log.h"
@@ -92,6 +93,11 @@ bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
   tcpip_adapter_dhcp_status_t dhcp_status;
   tcpip_adapter_dhcpc_get_status(TCPIP_ADAPTER_IF_STA, &dhcp_status);
   if (!manual_ip.has_value()) {
+    // lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
+    // the built-in SNTP client has a memory leak in certain situations. Disable this feature.
+    // https://github.com/esphome/issues/issues/2299
+    sntp_servermode_dhcp(false);
+
     // Use DHCP client
     if (dhcp_status != TCPIP_ADAPTER_DHCP_STARTED) {
       esp_err_t err = tcpip_adapter_dhcpc_start(TCPIP_ADAPTER_IF_STA);
@@ -163,7 +169,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
     conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK;
   }
 
-#ifdef ESPHOME_WIFI_WPA2_EAP
+#ifdef USE_WIFI_WPA2_EAP
   if (ap.get_eap().has_value()) {
     conf.sta.threshold.authmode = WIFI_AUTH_WPA2_ENTERPRISE;
   }
@@ -220,7 +226,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
   }
 
   // setup enterprise authentication if required
-#ifdef ESPHOME_WIFI_WPA2_EAP
+#ifdef USE_WIFI_WPA2_EAP
   if (ap.get_eap().has_value()) {
     // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
     EAPAuth eap = ap.get_eap().value();
@@ -264,7 +270,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
       ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", err);
     }
   }
-#endif  // ESPHOME_WIFI_WPA2_EAP
+#endif  // USE_WIFI_WPA2_EAP
 
   this->wifi_apply_hostname_();
 
diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp
index 2f6c32aec6..de529ee3aa 100644
--- a/esphome/components/wifi/wifi_component_esp8266.cpp
+++ b/esphome/components/wifi/wifi_component_esp8266.cpp
@@ -1,4 +1,5 @@
 #include "wifi_component.h"
+#include "esphome/core/macros.h"
 
 #ifdef ARDUINO_ARCH_ESP8266
 
@@ -6,23 +7,20 @@
 
 #include <utility>
 #include <algorithm>
-#ifdef ESPHOME_WIFI_WPA2_EAP
+#ifdef USE_WIFI_WPA2_EAP
 #include <wpa2_enterprise.h>
 #endif
 
-#ifdef WIFI_IS_OFF_AT_BOOT  // Identifies ESP8266 Arduino 3.0.0
-#define ARDUINO_ESP8266_RELEASE_3
-#endif
-
 extern "C" {
 #include "lwip/err.h"
 #include "lwip/dns.h"
 #include "lwip/dhcp.h"
 #include "lwip/init.h"  // LWIP_VERSION_
+#include "lwip/apps/sntp.h"
 #if LWIP_IPV6
 #include "lwip/netif.h"  // struct netif
 #endif
-#ifdef ARDUINO_ESP8266_RELEASE_3
+#if ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0)
 #include "LwipDhcpServer.h"
 #define wifi_softap_set_dhcps_lease(lease) dhcpSoftAP.set_dhcps_lease(lease)
 #define wifi_softap_set_dhcps_lease_time(time) dhcpSoftAP.set_dhcps_lease_time(time)
@@ -115,6 +113,11 @@ bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
 
   enum dhcp_status dhcp_status = wifi_station_dhcpc_status();
   if (!manual_ip.has_value()) {
+    // lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
+    // the built-in SNTP client has a memory leak in certain situations. Disable this feature.
+    // https://github.com/esphome/issues/issues/2299
+    sntp_servermode_dhcp(false);
+
     // Use DHCP client
     if (dhcp_status != DHCP_STARTED) {
       bool ret = wifi_station_dhcpc_start();
@@ -229,7 +232,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
     conf.bssid_set = 0;
   }
 
-#ifndef ARDUINO_ESP8266_RELEASE_2_3_0
+#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
   if (ap.get_password().empty()) {
     conf.threshold.authmode = AUTH_OPEN;
   } else {
@@ -253,7 +256,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
   }
 
   // setup enterprise authentication if required
-#ifdef ESPHOME_WIFI_WPA2_EAP
+#ifdef USE_WIFI_WPA2_EAP
   if (ap.get_eap().has_value()) {
     // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
     EAPAuth eap = ap.get_eap().value();
@@ -296,7 +299,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
       ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", ret);
     }
   }
-#endif  // ESPHOME_WIFI_WPA2_EAP
+#endif  // USE_WIFI_WPA2_EAP
 
   this->wifi_apply_hostname_();
 
@@ -369,65 +372,75 @@ const char *get_op_mode_str(uint8_t mode) {
       return "UNKNOWN";
   }
 }
+// Note that this method returns PROGMEM strings, so use LOG_STR_ARG() to access them.
 const char *get_disconnect_reason_str(uint8_t reason) {
+  /* If this were one big switch statement, GCC would generate a lookup table for it. However, the values of the
+   * REASON_* constants aren't continuous, and GCC will fill in the gap with the default value -- wasting 4 bytes of RAM
+   * per entry. As there's ~175 default entries, this wastes 700 bytes of RAM.
+   */
+  if (reason <= REASON_CIPHER_SUITE_REJECTED) {  // This must be the last constant with a value <200
+    switch (reason) {
+      case REASON_AUTH_EXPIRE:
+        return LOG_STR("Auth Expired");
+      case REASON_AUTH_LEAVE:
+        return LOG_STR("Auth Leave");
+      case REASON_ASSOC_EXPIRE:
+        return LOG_STR("Association Expired");
+      case REASON_ASSOC_TOOMANY:
+        return LOG_STR("Too Many Associations");
+      case REASON_NOT_AUTHED:
+        return LOG_STR("Not Authenticated");
+      case REASON_NOT_ASSOCED:
+        return LOG_STR("Not Associated");
+      case REASON_ASSOC_LEAVE:
+        return LOG_STR("Association Leave");
+      case REASON_ASSOC_NOT_AUTHED:
+        return LOG_STR("Association not Authenticated");
+      case REASON_DISASSOC_PWRCAP_BAD:
+        return LOG_STR("Disassociate Power Cap Bad");
+      case REASON_DISASSOC_SUPCHAN_BAD:
+        return LOG_STR("Disassociate Supported Channel Bad");
+      case REASON_IE_INVALID:
+        return LOG_STR("IE Invalid");
+      case REASON_MIC_FAILURE:
+        return LOG_STR("Mic Failure");
+      case REASON_4WAY_HANDSHAKE_TIMEOUT:
+        return LOG_STR("4-Way Handshake Timeout");
+      case REASON_GROUP_KEY_UPDATE_TIMEOUT:
+        return LOG_STR("Group Key Update Timeout");
+      case REASON_IE_IN_4WAY_DIFFERS:
+        return LOG_STR("IE In 4-Way Handshake Differs");
+      case REASON_GROUP_CIPHER_INVALID:
+        return LOG_STR("Group Cipher Invalid");
+      case REASON_PAIRWISE_CIPHER_INVALID:
+        return LOG_STR("Pairwise Cipher Invalid");
+      case REASON_AKMP_INVALID:
+        return LOG_STR("AKMP Invalid");
+      case REASON_UNSUPP_RSN_IE_VERSION:
+        return LOG_STR("Unsupported RSN IE version");
+      case REASON_INVALID_RSN_IE_CAP:
+        return LOG_STR("Invalid RSN IE Cap");
+      case REASON_802_1X_AUTH_FAILED:
+        return LOG_STR("802.1x Authentication Failed");
+      case REASON_CIPHER_SUITE_REJECTED:
+        return LOG_STR("Cipher Suite Rejected");
+    }
+  }
+
   switch (reason) {
-    case REASON_AUTH_EXPIRE:
-      return "Auth Expired";
-    case REASON_AUTH_LEAVE:
-      return "Auth Leave";
-    case REASON_ASSOC_EXPIRE:
-      return "Association Expired";
-    case REASON_ASSOC_TOOMANY:
-      return "Too Many Associations";
-    case REASON_NOT_AUTHED:
-      return "Not Authenticated";
-    case REASON_NOT_ASSOCED:
-      return "Not Associated";
-    case REASON_ASSOC_LEAVE:
-      return "Association Leave";
-    case REASON_ASSOC_NOT_AUTHED:
-      return "Association not Authenticated";
-    case REASON_DISASSOC_PWRCAP_BAD:
-      return "Disassociate Power Cap Bad";
-    case REASON_DISASSOC_SUPCHAN_BAD:
-      return "Disassociate Supported Channel Bad";
-    case REASON_IE_INVALID:
-      return "IE Invalid";
-    case REASON_MIC_FAILURE:
-      return "Mic Failure";
-    case REASON_4WAY_HANDSHAKE_TIMEOUT:
-      return "4-Way Handshake Timeout";
-    case REASON_GROUP_KEY_UPDATE_TIMEOUT:
-      return "Group Key Update Timeout";
-    case REASON_IE_IN_4WAY_DIFFERS:
-      return "IE In 4-Way Handshake Differs";
-    case REASON_GROUP_CIPHER_INVALID:
-      return "Group Cipher Invalid";
-    case REASON_PAIRWISE_CIPHER_INVALID:
-      return "Pairwise Cipher Invalid";
-    case REASON_AKMP_INVALID:
-      return "AKMP Invalid";
-    case REASON_UNSUPP_RSN_IE_VERSION:
-      return "Unsupported RSN IE version";
-    case REASON_INVALID_RSN_IE_CAP:
-      return "Invalid RSN IE Cap";
-    case REASON_802_1X_AUTH_FAILED:
-      return "802.1x Authentication Failed";
-    case REASON_CIPHER_SUITE_REJECTED:
-      return "Cipher Suite Rejected";
     case REASON_BEACON_TIMEOUT:
-      return "Beacon Timeout";
+      return LOG_STR("Beacon Timeout");
     case REASON_NO_AP_FOUND:
-      return "AP Not Found";
+      return LOG_STR("AP Not Found");
     case REASON_AUTH_FAIL:
-      return "Authentication Failed";
+      return LOG_STR("Authentication Failed");
     case REASON_ASSOC_FAIL:
-      return "Association Failed";
+      return LOG_STR("Association Failed");
     case REASON_HANDSHAKE_TIMEOUT:
-      return "Handshake Failed";
+      return LOG_STR("Handshake Failed");
     case REASON_UNSPECIFIED:
     default:
-      return "Unspecified";
+      return LOG_STR("Unspecified");
   }
 }
 
@@ -451,7 +464,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
         ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
       } else {
         ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
-                 format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
+                 format_mac_addr(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason)));
       }
       break;
     }
@@ -495,7 +508,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
       ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
       break;
     }
-#ifndef ARDUINO_ESP8266_RELEASE_2_3_0
+#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
     case EVENT_OPMODE_CHANGED: {
       auto it = event->event_info.opmode_changed;
       ESP_LOGV(TAG, "Event: Changed Mode old=%s new=%s", get_op_mode_str(it.old_opmode),
@@ -580,7 +593,7 @@ bool WiFiComponent::wifi_scan_start_() {
   config.bssid = nullptr;
   config.channel = 0;
   config.show_hidden = 1;
-#ifndef ARDUINO_ESP8266_RELEASE_2_3_0
+#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
   config.scan_type = WIFI_SCAN_TYPE_ACTIVE;
   if (FIRST_SCAN) {
     config.scan_time.active.min = 100;
@@ -659,7 +672,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
     return false;
   }
 
-#ifdef ARDUINO_ESP8266_RELEASE_3
+#if ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0)
   dhcpSoftAP.begin(&info);
 #endif
 
diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp
index 690d2f3b00..915d1c6cc2 100644
--- a/esphome/components/wled/wled_light_effect.cpp
+++ b/esphome/components/wled/wled_light_effect.cpp
@@ -42,6 +42,7 @@ void WLEDLightEffect::blank_all_leds_(light::AddressableLight &it) {
   for (int led = it.size(); led-- > 0;) {
     it[led].set(Color::BLACK);
   }
+  it.schedule_show();
 }
 
 void WLEDLightEffect::apply(light::AddressableLight &it, const Color &current_color) {
@@ -134,6 +135,7 @@ bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *p
     blank_at_ = millis() + DEFAULT_BLANK_TIME;
   }
 
+  it.schedule_show();
   return true;
 }
 
diff --git a/esphome/components/zyaura/sensor.py b/esphome/components/zyaura/sensor.py
index f2273afa9e..74f1f9ec61 100644
--- a/esphome/components/zyaura/sensor.py
+++ b/esphome/components/zyaura/sensor.py
@@ -9,6 +9,7 @@ from esphome.const import (
     CONF_CO2,
     CONF_TEMPERATURE,
     CONF_HUMIDITY,
+    DEVICE_CLASS_CARBON_DIOXIDE,
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
     STATE_CLASS_MEASUREMENT,
@@ -35,6 +36,7 @@ CONFIG_SCHEMA = cv.Schema(
             unit_of_measurement=UNIT_PARTS_PER_MILLION,
             icon=ICON_MOLECULE_CO2,
             accuracy_decimals=0,
+            device_class=DEVICE_CLASS_CARBON_DIOXIDE,
             state_class=STATE_CLASS_MEASUREMENT,
         ),
         cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
diff --git a/esphome/config.py b/esphome/config.py
index 93413a009c..de261f7eba 100644
--- a/esphome/config.py
+++ b/esphome/config.py
@@ -333,6 +333,8 @@ def validate_config(config, command_line_substitutions):
             result.add_error(err)
             return result
 
+    CORE.raw_config = config
+
     # 1. Load substitutions
     if CONF_SUBSTITUTIONS in config:
         from esphome.components import substitutions
@@ -348,6 +350,8 @@ def validate_config(config, command_line_substitutions):
             result.add_error(err)
             return result
 
+    CORE.raw_config = config
+
     # 1.1. Check for REPLACEME special value
     try:
         recursive_check_replaceme(config)
diff --git a/esphome/config_validation.py b/esphome/config_validation.py
index 3aebca81b8..fb659c41ea 100644
--- a/esphome/config_validation.py
+++ b/esphome/config_validation.py
@@ -33,7 +33,6 @@ from esphome.const import (
     CONF_UPDATE_INTERVAL,
     CONF_TYPE_ID,
     CONF_TYPE,
-    CONF_PACKAGES,
 )
 from esphome.core import (
     CORE,
@@ -836,10 +835,11 @@ pressure = float_with_unit("pressure", "(bar|Bar)", optional_unit=True)
 
 
 def temperature(value):
+    err = None
     try:
         return _temperature_c(value)
-    except Invalid as orig_err:  # noqa
-        pass
+    except Invalid as orig_err:
+        err = orig_err
 
     try:
         kelvin = _temperature_k(value)
@@ -853,7 +853,7 @@ def temperature(value):
     except Invalid:
         pass
 
-    raise orig_err  # noqa
+    raise err
 
 
 _color_temperature_mireds = float_with_unit("Color Temperature", r"(mireds|Mireds)")
@@ -1454,11 +1454,7 @@ class OnlyWith(Optional):
     @property
     def default(self):
         # pylint: disable=unsupported-membership-test
-        if self._component in CORE.raw_config or (
-            CONF_PACKAGES in CORE.raw_config
-            and self._component
-            in {list(x.keys())[0] for x in CORE.raw_config[CONF_PACKAGES].values()}
-        ):
+        if self._component in CORE.raw_config:
             return self._default
         return vol.UNDEFINED
 
@@ -1628,3 +1624,17 @@ def url(value):
     if not parsed.scheme or not parsed.netloc:
         raise Invalid("Expected a URL scheme and host")
     return parsed.geturl()
+
+
+def git_ref(value):
+    if re.match(r"[a-zA-Z0-9\-_.\./]+", value) is None:
+        raise Invalid("Not a valid git ref")
+    return value
+
+
+def source_refresh(value: str):
+    if value.lower() == "always":
+        return source_refresh("0s")
+    if value.lower() == "never":
+        return source_refresh("1000y")
+    return positive_time_period_seconds(value)
diff --git a/esphome/const.py b/esphome/const.py
index ce3b95b731..5a5351f1b0 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,6 +1,6 @@
 """Constants used by esphome."""
 
-__version__ = "2021.8.2"
+__version__ = "2021.9.0"
 
 ESP_PLATFORM_ESP32 = "ESP32"
 ESP_PLATFORM_ESP8266 = "ESP8266"
@@ -69,6 +69,7 @@ CONF_ATTENUATION = "attenuation"
 CONF_ATTRIBUTE = "attribute"
 CONF_AUTH = "auth"
 CONF_AUTO_MODE = "auto_mode"
+CONF_AUTOCONF = "autoconf"
 CONF_AUTOMATION_ID = "automation_id"
 CONF_AVAILABILITY = "availability"
 CONF_AWAY = "away"
@@ -78,6 +79,7 @@ CONF_BASELINE = "baseline"
 CONF_BATTERY_LEVEL = "battery_level"
 CONF_BATTERY_VOLTAGE = "battery_voltage"
 CONF_BAUD_RATE = "baud_rate"
+CONF_BEEPER = "beeper"
 CONF_BELOW = "below"
 CONF_BINARY = "binary"
 CONF_BINARY_SENSOR = "binary_sensor"
@@ -165,6 +167,7 @@ CONF_DAYS_OF_WEEK = "days_of_week"
 CONF_DC_PIN = "dc_pin"
 CONF_DEASSERT_RTS_DTR = "deassert_rts_dtr"
 CONF_DEBOUNCE = "debounce"
+CONF_DECAY_MODE = "decay_mode"
 CONF_DECELERATION = "deceleration"
 CONF_DEFAULT_MODE = "default_mode"
 CONF_DEFAULT_TARGET_TEMPERATURE_HIGH = "default_target_temperature_high"
@@ -234,12 +237,14 @@ CONF_FAN_WITH_COOLING = "fan_with_cooling"
 CONF_FAN_WITH_HEATING = "fan_with_heating"
 CONF_FAST_CONNECT = "fast_connect"
 CONF_FILE = "file"
+CONF_FILES = "files"
 CONF_FILTER = "filter"
 CONF_FILTER_OUT = "filter_out"
 CONF_FILTERS = "filters"
 CONF_FINGER_ID = "finger_id"
 CONF_FINGERPRINT_COUNT = "fingerprint_count"
 CONF_FLASH_LENGTH = "flash_length"
+CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length"
 CONF_FLOW_CONTROL_PIN = "flow_control_pin"
 CONF_FOR = "for"
 CONF_FORCE_UPDATE = "force_update"
@@ -277,6 +282,9 @@ CONF_HUMIDITY = "humidity"
 CONF_HYSTERESIS = "hysteresis"
 CONF_I2C = "i2c"
 CONF_I2C_ID = "i2c_id"
+CONF_IBEACON_MAJOR = "ibeacon_major"
+CONF_IBEACON_MINOR = "ibeacon_minor"
+CONF_IBEACON_UUID = "ibeacon_uuid"
 CONF_ICON = "icon"
 CONF_ID = "id"
 CONF_IDENTITY = "identity"
@@ -317,7 +325,6 @@ CONF_KEY = "key"
 CONF_LAMBDA = "lambda"
 CONF_LAST_CONFIDENCE = "last_confidence"
 CONF_LAST_FINGER_ID = "last_finger_id"
-CONF_LAST_RESET_TYPE = "last_reset_type"
 CONF_LATITUDE = "latitude"
 CONF_LENGTH = "length"
 CONF_LEVEL = "level"
@@ -420,6 +427,7 @@ CONF_ON_PRESS = "on_press"
 CONF_ON_RAW_VALUE = "on_raw_value"
 CONF_ON_RELEASE = "on_release"
 CONF_ON_SHUTDOWN = "on_shutdown"
+CONF_ON_SPEED_SET = "on_speed_set"
 CONF_ON_STATE = "on_state"
 CONF_ON_TAG = "on_tag"
 CONF_ON_TAG_REMOVED = "on_tag_removed"
@@ -506,6 +514,8 @@ CONF_PROTOCOL = "protocol"
 CONF_PULL_MODE = "pull_mode"
 CONF_PULSE_LENGTH = "pulse_length"
 CONF_QOS = "qos"
+CONF_RADON = "radon"
+CONF_RADON_LONG_TERM = "radon_long_term"
 CONF_RANDOM = "random"
 CONF_RANGE = "range"
 CONF_RANGE_FROM = "range_from"
@@ -518,8 +528,10 @@ CONF_REACTIVE_POWER = "reactive_power"
 CONF_REBOOT_TIMEOUT = "reboot_timeout"
 CONF_RECEIVE_TIMEOUT = "receive_timeout"
 CONF_RED = "red"
+CONF_REF = "ref"
 CONF_REFERENCE_RESISTANCE = "reference_resistance"
 CONF_REFERENCE_TEMPERATURE = "reference_temperature"
+CONF_REFRESH = "refresh"
 CONF_REPEAT = "repeat"
 CONF_REPOSITORY = "repository"
 CONF_RESET_PIN = "reset_pin"
@@ -605,6 +617,10 @@ CONF_SUPPLEMENTAL_COOLING_ACTION = "supplemental_cooling_action"
 CONF_SUPPLEMENTAL_COOLING_DELTA = "supplemental_cooling_delta"
 CONF_SUPPLEMENTAL_HEATING_ACTION = "supplemental_heating_action"
 CONF_SUPPLEMENTAL_HEATING_DELTA = "supplemental_heating_delta"
+CONF_SUPPORTED_FAN_MODES = "supported_fan_modes"
+CONF_SUPPORTED_MODES = "supported_modes"
+CONF_SUPPORTED_PRESETS = "supported_presets"
+CONF_SUPPORTED_SWING_MODES = "supported_swing_modes"
 CONF_SUPPORTS_COOL = "supports_cool"
 CONF_SUPPORTS_HEAT = "supports_heat"
 CONF_SWING_BOTH_ACTION = "swing_both_action"
@@ -731,6 +747,7 @@ ICON_PERCENT = "mdi:percent"
 ICON_POWER = "mdi:power"
 ICON_PULSE = "mdi:pulse"
 ICON_RADIATOR = "mdi:radiator"
+ICON_RADIOACTIVE = "mdi:radioactive"
 ICON_RESTART = "mdi:restart"
 ICON_ROTATE_RIGHT = "mdi:rotate-right"
 ICON_RULER = "mdi:ruler"
@@ -752,6 +769,7 @@ ICON_WEATHER_WINDY = "mdi:weather-windy"
 ICON_WIFI = "mdi:wifi"
 
 UNIT_AMPERE = "A"
+UNIT_BECQUEREL_PER_CUBIC_METER = "Bq/m³"
 UNIT_CELSIUS = "°C"
 UNIT_COUNT_DECILITRE = "/dL"
 UNIT_COUNTS_PER_CUBIC_METER = "#/m³"
@@ -768,6 +786,10 @@ UNIT_KELVIN = "K"
 UNIT_KILOGRAM = "kg"
 UNIT_KILOMETER = "km"
 UNIT_KILOMETER_PER_HOUR = "km/h"
+UNIT_KILOVOLT_AMPS_REACTIVE = "kVAr"
+UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVArh"
+UNIT_KILOWATT = "kW"
+UNIT_KILOWATT_HOURS = "kWh"
 UNIT_LUX = "lx"
 UNIT_METER = "m"
 UNIT_METER_PER_SECOND_SQUARED = "m/s²"
@@ -798,7 +820,6 @@ DEVICE_CLASS_COLD = "cold"
 DEVICE_CLASS_CONNECTIVITY = "connectivity"
 DEVICE_CLASS_DOOR = "door"
 DEVICE_CLASS_GARAGE_DOOR = "garage_door"
-DEVICE_CLASS_GAS = "gas"
 DEVICE_CLASS_HEAT = "heat"
 DEVICE_CLASS_LIGHT = "light"
 DEVICE_CLASS_LOCK = "lock"
@@ -813,25 +834,37 @@ DEVICE_CLASS_PROBLEM = "problem"
 DEVICE_CLASS_SAFETY = "safety"
 DEVICE_CLASS_SMOKE = "smoke"
 DEVICE_CLASS_SOUND = "sound"
+DEVICE_CLASS_UPDATE = "update"
 DEVICE_CLASS_VIBRATION = "vibration"
 DEVICE_CLASS_WINDOW = "window"
 # device classes of both binary_sensor and sensor component
 DEVICE_CLASS_EMPTY = ""
 DEVICE_CLASS_BATTERY = "battery"
+DEVICE_CLASS_GAS = "gas"
 DEVICE_CLASS_POWER = "power"
 # device classes of sensor component
-DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide"
+DEVICE_CLASS_AQI = "aqi"
 DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide"
+DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide"
 DEVICE_CLASS_CURRENT = "current"
 DEVICE_CLASS_ENERGY = "energy"
 DEVICE_CLASS_HUMIDITY = "humidity"
 DEVICE_CLASS_ILLUMINANCE = "illuminance"
 DEVICE_CLASS_MONETARY = "monetary"
-DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength"
-DEVICE_CLASS_TEMPERATURE = "temperature"
+DEVICE_CLASS_NITROGEN_DIOXIDE = "nitrogen_dioxide"
+DEVICE_CLASS_NITROGEN_MONOXIDE = "nitrogen_monoxide"
+DEVICE_CLASS_NITROUS_OXIDE = "nitrous_oxide"
+DEVICE_CLASS_OZONE = "ozone"
+DEVICE_CLASS_PM1 = "pm1"
+DEVICE_CLASS_PM10 = "pm10"
+DEVICE_CLASS_PM25 = "pm25"
 DEVICE_CLASS_POWER_FACTOR = "power_factor"
 DEVICE_CLASS_PRESSURE = "pressure"
+DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength"
+DEVICE_CLASS_SULPHUR_DIOXIDE = "sulphur_dioxide"
+DEVICE_CLASS_TEMPERATURE = "temperature"
 DEVICE_CLASS_TIMESTAMP = "timestamp"
+DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds"
 DEVICE_CLASS_VOLTAGE = "voltage"
 
 # state classes
@@ -842,10 +875,3 @@ STATE_CLASS_MEASUREMENT = "measurement"
 
 # The state represents a total that only increases, a decrease is considered a reset.
 STATE_CLASS_TOTAL_INCREASING = "total_increasing"
-
-# This sensor does not support resetting. ie, it is not accumulative
-LAST_RESET_TYPE_NONE = ""
-# This sensor is expected to never reset its value
-LAST_RESET_TYPE_NEVER = "never"
-# This sensor may reset and Home Assistant will watch for this
-LAST_RESET_TYPE_AUTO = "auto"
diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp
index 1a3158e4ce..fac17a8271 100644
--- a/esphome/core/application.cpp
+++ b/esphome/core/application.cpp
@@ -19,7 +19,7 @@ void Application::register_component_(Component *comp) {
 
   for (auto *c : this->components_) {
     if (comp == c) {
-      ESP_LOGW(TAG, "Component already registered! (%p)", c);
+      ESP_LOGW(TAG, "Component %s already registered! (%p)", c->get_component_source(), c);
       return;
     }
   }
@@ -66,23 +66,19 @@ void Application::setup() {
 }
 void Application::loop() {
   uint32_t new_app_state = 0;
-  const uint32_t start = millis();
 
   this->scheduler.call();
   for (Component *component : this->looping_components_) {
-    component->call();
+    {
+      WarnIfComponentBlockingGuard guard{component};
+      component->call();
+    }
     new_app_state |= component->get_component_state();
     this->app_state_ |= new_app_state;
     this->feed_wdt();
   }
   this->app_state_ = new_app_state;
 
-  const uint32_t end = millis();
-  if (end - start > 200) {
-    ESP_LOGV(TAG, "A component took a long time in a loop() cycle (%.2f s).", (end - start) / 1e3f);
-    ESP_LOGV(TAG, "Components should block for at most 20-30ms in loop().");
-  }
-
   const uint32_t now = millis();
 
   if (HighFrequencyLoopRequester::is_high_frequency()) {
diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp
index f6b15b1977..cd3081998b 100644
--- a/esphome/core/component.cpp
+++ b/esphome/core/component.cpp
@@ -20,6 +20,7 @@ const float PROCESSOR = 400.0;
 const float BLUETOOTH = 350.0f;
 const float AFTER_BLUETOOTH = 300.0f;
 const float WIFI = 250.0f;
+const float BEFORE_CONNECTION = 220.0f;
 const float AFTER_WIFI = 200.0f;
 const float AFTER_CONNECTION = 100.0f;
 const float LATE = -100.0f;
@@ -92,8 +93,13 @@ void Component::call() {
       break;
   }
 }
+const char *Component::get_component_source() const {
+  if (this->component_source_ == nullptr)
+    return "<unknown>";
+  return this->component_source_;
+}
 void Component::mark_failed() {
-  ESP_LOGE(TAG, "Component was marked as failed.");
+  ESP_LOGE(TAG, "Component %s was marked as failed.", this->get_component_source());
   this->component_state_ &= ~COMPONENT_STATE_MASK;
   this->component_state_ |= COMPONENT_STATE_FAILED;
   this->status_set_error();
@@ -190,4 +196,18 @@ uint32_t Nameable::get_object_id_hash() { return this->object_id_hash_; }
 bool Nameable::is_disabled_by_default() const { return this->disabled_by_default_; }
 void Nameable::set_disabled_by_default(bool disabled_by_default) { this->disabled_by_default_ = disabled_by_default; }
 
+WarnIfComponentBlockingGuard::WarnIfComponentBlockingGuard(Component *component) {
+  component_ = component;
+  started_ = millis();
+}
+WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {
+  uint32_t now = millis();
+  if (now - started_ > 50) {
+    const char *src = component_ == nullptr ? "<null>" : component_->get_component_source();
+    ESP_LOGV(TAG, "Component %s took a long time for an operation (%.2f s).", src, (now - started_) / 1e3f);
+    ESP_LOGV(TAG, "Components should block for at most 20-30ms.");
+    ;
+  }
+}
+
 }  // namespace esphome
diff --git a/esphome/core/component.h b/esphome/core/component.h
index a4a945ef2a..ea87ebcdfe 100644
--- a/esphome/core/component.h
+++ b/esphome/core/component.h
@@ -29,6 +29,8 @@ 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 and before API is connected.
+extern const float BEFORE_CONNECTION;
 /// For components that should be initialized after WiFi is connected.
 extern const float AFTER_WIFI;
 /// For components that should be initialized after a data connection (API/MQTT) is connected.
@@ -38,8 +40,12 @@ extern const float LATE;
 
 }  // namespace setup_priority
 
+static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL;
+
 #define LOG_UPDATE_INTERVAL(this) \
-  if (this->get_update_interval() < 100) { \
+  if (this->get_update_interval() == SCHEDULER_DONT_RUN) { \
+    ESP_LOGCONFIG(TAG, "  Update Interval: never"); \
+  } else if (this->get_update_interval() < 100) { \
     ESP_LOGCONFIG(TAG, "  Update Interval: %.3fs", this->get_update_interval() / 1000.0f); \
   } else { \
     ESP_LOGCONFIG(TAG, "  Update Interval: %.1fs", this->get_update_interval() / 1000.0f); \
@@ -130,6 +136,17 @@ class Component {
 
   bool has_overridden_loop() const;
 
+  /** Set where this component was loaded from for some debug messages.
+   *
+   * This is set by the ESPHome core, and should not be called manually.
+   */
+  void set_component_source(const char *source) { component_source_ = source; }
+  /** Get the integration where this component was declared as a string.
+   *
+   * Returns "<unknown>" if source not set
+   */
+  const char *get_component_source() const;
+
  protected:
   virtual void call_loop();
   virtual void call_setup();
@@ -201,6 +218,7 @@ class Component {
 
   uint32_t component_state_{0x0000};  ///< State of this component.
   float setup_priority_override_{NAN};
+  const char *component_source_ = nullptr;
 };
 
 /** This class simplifies creating components that periodically check a state.
@@ -276,4 +294,14 @@ class Nameable {
   bool disabled_by_default_{false};
 };
 
+class WarnIfComponentBlockingGuard {
+ public:
+  WarnIfComponentBlockingGuard(Component *component);
+  ~WarnIfComponentBlockingGuard();
+
+ protected:
+  uint32_t started_;
+  Component *component_;
+};
+
 }  // namespace esphome
diff --git a/esphome/core/defines.h b/esphome/core/defines.h
index 5c176d1b33..3cca6445b5 100644
--- a/esphome/core/defines.h
+++ b/esphome/core/defines.h
@@ -1,31 +1,61 @@
 #pragma once
-// This file is auto-generated! Do not edit!
 
+// This file is not used by the runtime, instead, a version is generated during
+// compilation with only the relevant feature flags for the current build.
+//
+// This file is only used by static analyzers and IDEs.
+
+// Informative flags
+#define ESPHOME_BOARD "dummy_board"
+#define ESPHOME_PROJECT_NAME "dummy project"
+#define ESPHOME_PROJECT_VERSION "v2"
+
+// Feature flags
+#define USE_ADC_SENSOR_VCC
 #define USE_API
-#define USE_LOGGER
 #define USE_BINARY_SENSOR
-#define USE_SENSOR
-#define USE_SWITCH
-#define USE_WIFI
-#define USE_STATUS_LED
-#define USE_TEXT_SENSOR
-#define USE_FAN
-#define USE_COVER
-#define USE_LIGHT
+#define USE_CAPTIVE_PORTAL
 #define USE_CLIMATE
-#define USE_NUMBER
-#define USE_SELECT
-#define USE_MQTT
-#define USE_POWER_SUPPLY
+#define USE_COVER
+#define USE_DEEP_SLEEP
+#define USE_ESP8266_PREFERENCES_FLASH
+#define USE_FAN
 #define USE_HOMEASSISTANT_TIME
+#define USE_I2C_MULTIPLEXER
 #define USE_JSON
+#define USE_LIGHT
+#define USE_LOGGER
+#define USE_MDNS
+#define USE_MQTT
+#define USE_NUMBER
+#define USE_OTA_STATE_CALLBACK
+#define USE_POWER_SUPPLY
+#define USE_PROMETHEUS
+#define USE_SELECT
+#define USE_SENSOR
+#define USE_STATUS_LED
+#define USE_SWITCH
+#define USE_TEXT_SENSOR
+#define USE_TFT_UPLOAD
+#define USE_TIME
+#define USE_WIFI
+#define USE_WIFI_WPA2_EAP
+
 #ifdef ARDUINO_ARCH_ESP32
-#define USE_ESP32_CAMERA
 #define USE_ESP32_BLE_SERVER
+#define USE_ESP32_CAMERA
+#define USE_ETHERNET
 #define USE_IMPROV
 #endif
-#define USE_TIME
-#define USE_DEEP_SLEEP
-#define USE_CAPTIVE_PORTAL
-#define ESPHOME_BOARD "dummy_board"
-#define USE_MDNS
+
+#ifdef ARDUINO_ARCH_ESP8266
+#define USE_SOCKET_IMPL_LWIP_TCP
+#else
+#define USE_SOCKET_IMPL_BSD_SOCKETS
+#endif
+
+#define USE_API_PLAINTEXT
+#define USE_API_NOISE
+
+// Disabled feature flags
+//#define USE_BSEC  // Requires a library with proprietary license.
diff --git a/esphome/core/esphal.cpp b/esphome/core/esphal.cpp
index 6b8350991e..7851ba01b6 100644
--- a/esphome/core/esphal.cpp
+++ b/esphome/core/esphal.cpp
@@ -1,4 +1,5 @@
 #include "esphome/core/esphal.h"
+#include "esphome/core/macros.h"
 #include "esphome/core/helpers.h"
 #include "esphome/core/defines.h"
 #include "esphome/core/log.h"
@@ -298,7 +299,7 @@ void force_link_symbols() {
 
 }  // namespace esphome
 
-#ifdef ARDUINO_ESP8266_RELEASE_2_3_0
+#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0)
 // Fix 2.3.0 std missing memchr
 extern "C" {
 void *memchr(const void *s, int c, size_t n) {
diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp
index 9e9c775899..c5ff0102c3 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -55,6 +55,15 @@ double random_double() { return random_uint32() / double(UINT32_MAX); }
 
 float random_float() { return float(random_double()); }
 
+void fill_random(uint8_t *data, size_t len) {
+#ifdef ARDUINO_ARCH_ESP32
+  esp_fill_random(data, len);
+#else
+  int err = os_get_random(data, len);
+  assert(err == 0);
+#endif
+}
+
 static uint32_t fast_random_seed = 0;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
 void fast_random_set_seed(uint32_t seed) { fast_random_seed = seed; }
diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h
index 5868918cd6..60bc7a9ad3 100644
--- a/esphome/core/helpers.h
+++ b/esphome/core/helpers.h
@@ -109,6 +109,8 @@ double random_double();
 /// Returns a random float between 0 and 1. Essentially just casts random_double() to a float.
 float random_float();
 
+void fill_random(uint8_t *data, size_t len);
+
 void fast_random_set_seed(uint32_t seed);
 uint32_t fast_random_32();
 uint16_t fast_random_16();
diff --git a/esphome/core/log.h b/esphome/core/log.h
index 0eec28101f..fbaaf14408 100644
--- a/esphome/core/log.h
+++ b/esphome/core/log.h
@@ -7,6 +7,7 @@
 #include "WString.h"
 #endif
 
+#include "esphome/core/macros.h"
 // avoid esp-idf redefining our macros
 #include "esphome/core/esphal.h"
 
@@ -162,4 +163,28 @@ int esp_idf_log_vprintf_(const char *format, va_list args);  // NOLINT
 #define ONOFF(b) ((b) ? "ON" : "OFF")
 #define TRUEFALSE(b) ((b) ? "TRUE" : "FALSE")
 
+#ifdef USE_STORE_LOG_STR_IN_FLASH
+#define LOG_STR(s) PSTR(s)
+
+// From Arduino 2.5 onwards, we can pass a PSTR() to printf(). For previous versions, emulate support
+// by copying the message to a local buffer first. String length is limited to 63 characters.
+// https://github.com/esp8266/Arduino/commit/6280e98b0360f85fdac2b8f10707fffb4f6e6e31
+#include <core_version.h>
+#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 5, 0)
+#define LOG_STR_ARG(s) \
+  ({ \
+    char __buf[64]; \
+    __buf[63] = '\0'; \
+    strncpy_P(__buf, s, 63); \
+    __buf; \
+  })
+#else
+#define LOG_STR_ARG(s) (s)
+#endif
+
+#else
+#define LOG_STR(s) (s)
+#define LOG_STR_ARG(s) (s)
+#endif
+
 }  // namespace esphome
diff --git a/esphome/core/macros.h b/esphome/core/macros.h
new file mode 100644
index 0000000000..59b52bf7a1
--- /dev/null
+++ b/esphome/core/macros.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#define VERSION_CODE(major, minor, patch) ((major) << 16 | (minor) << 8 | (patch))
+
+#if defined(ARDUINO_ARCH_ESP8266)
+
+#include <core_version.h>
+#if defined(ARDUINO_ESP8266_MAJOR) && defined(ARDUINO_ESP8266_MINOR) && defined(ARDUINO_ESP8266_REVISION)  // v3.0.1+
+#define ARDUINO_VERSION_CODE VERSION_CODE(ARDUINO_ESP8266_MAJOR, ARDUINO_ESP8266_MINOR, ARDUINO_ESP8266_REVISION)
+#elif ARDUINO_ESP8266_GIT_VER == 0xefb0341a  // version defines were screwed up in v3.0.0
+#define ARDUINO_VERSION_CODE VERSION_CODE(3, 0, 0)
+#elif defined(ARDUINO_ESP8266_RELEASE_2_7_4)
+#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 4)
+#elif defined(ARDUINO_ESP8266_RELEASE_2_7_3)
+#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 3)
+#elif defined(ARDUINO_ESP8266_RELEASE_2_7_2)
+#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 2)
+#elif defined(ARDUINO_ESP8266_RELEASE_2_7_1)
+#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 1)
+#elif defined(ARDUINO_ESP8266_RELEASE_2_7_0)
+#define ARDUINO_VERSION_CODE VERSION_CODE(2, 7, 0)
+#elif defined(ARDUINO_ESP8266_RELEASE_2_6_3)
+#define ARDUINO_VERSION_CODE VERSION_CODE(2, 6, 3)
+#elif defined(ARDUINO_ESP8266_RELEASE_2_6_2)
+#define ARDUINO_VERSION_CODE VERSION_CODE(2, 6, 2)
+#elif defined(ARDUINO_ESP8266_RELEASE_2_6_1)
+#define ARDUINO_VERSION_CODE VERSION_CODE(2, 6, 1)
+#elif defined(ARDUINO_ESP8266_RELEASE_2_5_2)
+#define ARDUINO_VERSION_CODE VERSION_CODE(2, 5, 2)
+#elif defined(ARDUINO_ESP8266_RELEASE_2_5_1)
+#define ARDUINO_VERSION_CODE VERSION_CODE(2, 5, 1)
+#elif defined(ARDUINO_ESP8266_RELEASE_2_5_0)
+#define ARDUINO_VERSION_CODE VERSION_CODE(2, 5, 0)
+#elif defined(ARDUINO_ESP8266_RELEASE_2_4_2)
+#define ARDUINO_VERSION_CODE VERSION_CODE(2, 4, 2)
+#elif defined(ARDUINO_ESP8266_RELEASE_2_4_1)
+#define ARDUINO_VERSION_CODE VERSION_CODE(2, 4, 1)
+#elif defined(ARDUINO_ESP8266_RELEASE_2_4_0)
+#define ARDUINO_VERSION_CODE VERSION_CODE(2, 4, 0)
+#elif defined(ARDUINO_ESP8266_RELEASE_2_3_0)
+#define ARDUINO_VERSION_CODE VERSION_CODE(2, 3, 0)
+#else
+#warning "Could not determine Arduino framework version, update esphome/core/macros.h!"
+#endif
+
+#elif defined(ARDUINO_ARCH_ESP32)
+
+#if defined(IDF_VER)  // identifies v2, needed since v1 doesn't have the esp_arduino_version.h header
+#include <esp_arduino_version.h>
+#define ARDUINO_VERSION_CODE \
+  VERSION_CODE(ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATH)
+#else
+#define ARDUINO_VERSION_CODE VERSION_CODE(1, 0, 0)  // there are no defines identifying minor/patch version
+#endif
+
+#endif
diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp
index 410c68052f..5718e3b396 100644
--- a/esphome/core/scheduler.cpp
+++ b/esphome/core/scheduler.cpp
@@ -7,7 +7,6 @@ namespace esphome {
 
 static const char *const TAG = "scheduler";
 
-static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL;
 static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10;
 
 // Uncomment to debug scheduler
@@ -155,7 +154,10 @@ void ICACHE_RAM_ATTR HOT Scheduler::call() {
       // Warning: During f(), a lot of stuff can happen, including:
       //  - timeouts/intervals get added, potentially invalidating vector pointers
       //  - timeouts/intervals get cancelled
-      item->f();
+      {
+        WarnIfComponentBlockingGuard guard{item->component};
+        item->f();
+      }
     }
 
     {
diff --git a/esphome/core/version.h b/esphome/core/version.h
index 0942c3e52f..b64f581b25 100644
--- a/esphome/core/version.h
+++ b/esphome/core/version.h
@@ -1,3 +1,9 @@
 #pragma once
-// This file is auto-generated! Do not edit!
+
+// This file is not used by the runtime, instead, a version is generated during
+// compilation with only the version for the current build. This is kept in its
+// own file so that not all files have to be recompiled for each new release.
+//
+// This file is only used by static analyzers and IDEs.
+
 #define ESPHOME_VERSION "dev"
diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py
index 1d66eabf6c..7912e4ae06 100644
--- a/esphome/cpp_helpers.py
+++ b/esphome/cpp_helpers.py
@@ -1,3 +1,5 @@
+import logging
+
 from esphome.const import (
     CONF_INVERTED,
     CONF_MODE,
@@ -15,6 +17,9 @@ from esphome.cpp_types import App, GPIOPin
 from esphome.util import Registry, RegistryEntry
 
 
+_LOGGER = logging.getLogger(__name__)
+
+
 async def gpio_pin_expression(conf):
     """Generate an expression for the given pin option.
 
@@ -42,6 +47,8 @@ async def register_component(var, config):
     :param var: The variable representing the component.
     :param config: The configuration for the component.
     """
+    import inspect
+
     id_ = str(var.base)
     if id_ not in CORE.component_ids:
         raise ValueError(
@@ -54,6 +61,32 @@ async def register_component(var, config):
         add(var.set_setup_priority(config[CONF_SETUP_PRIORITY]))
     if CONF_UPDATE_INTERVAL in config:
         add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))
+
+    # Set component source by inspecting the stack and getting the callee module
+    # https://stackoverflow.com/a/1095621
+    name = None
+    try:
+        for frm in inspect.stack()[1:]:
+            mod = inspect.getmodule(frm[0])
+            if mod is None:
+                continue
+            name = mod.__name__
+            if name.startswith("esphome.components."):
+                name = name[len("esphome.components.") :]
+                break
+            if name == "esphome.automation":
+                name = "automation"
+                # continue looking further up in stack in case we find a better one
+            if name == "esphome.coroutine":
+                # Only works for async-await coroutine syntax
+                break
+    except (KeyError, AttributeError, IndexError) as e:
+        _LOGGER.warning(
+            "Error while finding name of component, please report this", exc_info=e
+        )
+    if name is not None:
+        add(var.set_component_source(name))
+
     add(App.register_component(var))
     return var
 
diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py
index ea55ea3b18..97f9d60693 100644
--- a/esphome/dashboard/dashboard.py
+++ b/esphome/dashboard/dashboard.py
@@ -41,7 +41,7 @@ from .util import password_hash
 # pylint: disable=unused-import, wrong-import-order
 from typing import Optional  # noqa
 
-from esphome.zeroconf import DashboardStatus, Zeroconf
+from esphome.zeroconf import DashboardStatus, EsphomeZeroconf
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -431,7 +431,7 @@ class DashboardEntry:
     @property
     def name(self):
         if self.storage is None:
-            return self.filename[: -len(".yaml")]
+            return self.filename.replace(".yml", "").replace(".yaml", "")
         return self.storage.name
 
     @property
@@ -501,7 +501,7 @@ def _ping_func(filename, address):
 
 class MDNSStatusThread(threading.Thread):
     def run(self):
-        zc = Zeroconf()
+        zc = EsphomeZeroconf()
 
         def on_update(dat):
             for key, b in dat.items():
@@ -600,7 +600,7 @@ class EditRequestHandler(BaseHandler):
         content = ""
         if os.path.isfile(filename):
             # pylint: disable=no-value-for-parameter
-            with open(filename, "r") as f:
+            with open(file=filename, mode="r", encoding="utf-8") as f:
                 content = f.read()
         self.write(content)
 
@@ -608,7 +608,7 @@ class EditRequestHandler(BaseHandler):
     @bind_config
     def post(self, configuration=None):
         # pylint: disable=no-value-for-parameter
-        with open(settings.rel_path(configuration), "wb") as f:
+        with open(file=settings.rel_path(configuration), mode="wb") as f:
             f.write(self.request.body)
         self.set_status(200)
 
diff --git a/esphome/git.py b/esphome/git.py
new file mode 100644
index 0000000000..12c6b41648
--- /dev/null
+++ b/esphome/git.py
@@ -0,0 +1,74 @@
+from pathlib import Path
+import subprocess
+import hashlib
+import logging
+
+from datetime import datetime
+
+from esphome.core import CORE, TimePeriodSeconds
+import esphome.config_validation as cv
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def run_git_command(cmd, cwd=None):
+    try:
+        ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False)
+    except FileNotFoundError as err:
+        raise cv.Invalid(
+            "git is not installed but required for external_components.\n"
+            "Please see https://git-scm.com/book/en/v2/Getting-Started-Installing-Git for installing git"
+        ) from err
+
+    if ret.returncode != 0 and ret.stderr:
+        err_str = ret.stderr.decode("utf-8")
+        lines = [x.strip() for x in err_str.splitlines()]
+        if lines[-1].startswith("fatal:"):
+            raise cv.Invalid(lines[-1][len("fatal: ") :])
+        raise cv.Invalid(err_str)
+
+
+def _compute_destination_path(key: str, domain: str) -> Path:
+    base_dir = Path(CORE.config_dir) / ".esphome" / domain
+    h = hashlib.new("sha256")
+    h.update(key.encode())
+    return base_dir / h.hexdigest()[:8]
+
+
+def clone_or_update(
+    *, url: str, ref: str = None, refresh: TimePeriodSeconds, domain: str
+) -> Path:
+    key = f"{url}@{ref}"
+    repo_dir = _compute_destination_path(key, domain)
+    if not repo_dir.is_dir():
+        _LOGGER.info("Cloning %s", key)
+        _LOGGER.debug("Location: %s", repo_dir)
+        cmd = ["git", "clone", "--depth=1"]
+        if ref is not None:
+            cmd += ["--branch", ref]
+        cmd += ["--", url, str(repo_dir)]
+        run_git_command(cmd)
+
+    else:
+        # Check refresh needed
+        file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD")
+        # On first clone, FETCH_HEAD does not exists
+        if not file_timestamp.exists():
+            file_timestamp = Path(repo_dir / ".git" / "HEAD")
+        age = datetime.now() - datetime.fromtimestamp(file_timestamp.stat().st_mtime)
+        if age.total_seconds() > refresh.total_seconds:
+            _LOGGER.info("Updating %s", key)
+            _LOGGER.debug("Location: %s", repo_dir)
+            # Stash local changes (if any)
+            run_git_command(
+                ["git", "stash", "push", "--include-untracked"], str(repo_dir)
+            )
+            # Fetch remote ref
+            cmd = ["git", "fetch", "--", "origin"]
+            if ref is not None:
+                cmd.append(ref)
+            run_git_command(cmd, str(repo_dir))
+            # Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch)
+            run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir))
+
+    return repo_dir
diff --git a/esphome/helpers.py b/esphome/helpers.py
index ad7b8272b2..a1cb4367c5 100644
--- a/esphome/helpers.py
+++ b/esphome/helpers.py
@@ -97,10 +97,10 @@ def is_ip_address(host):
 
 def _resolve_with_zeroconf(host):
     from esphome.core import EsphomeError
-    from esphome.zeroconf import Zeroconf
+    from esphome.zeroconf import EsphomeZeroconf
 
     try:
-        zc = Zeroconf()
+        zc = EsphomeZeroconf()
     except Exception as err:
         raise EsphomeError(
             "Cannot start mDNS sockets, is this a docker container without "
@@ -276,11 +276,11 @@ def file_compare(path1: os.PathLike, path2: os.PathLike) -> bool:
 # A dict of types that need to be converted to heaptypes before a class can be added
 # to the object
 _TYPE_OVERLOADS = {
-    int: type("EInt", (int,), dict()),
-    float: type("EFloat", (float,), dict()),
-    str: type("EStr", (str,), dict()),
-    dict: type("EDict", (str,), dict()),
-    list: type("EList", (list,), dict()),
+    int: type("EInt", (int,), {}),
+    float: type("EFloat", (float,), {}),
+    str: type("EStr", (str,), {}),
+    dict: type("EDict", (str,), {}),
+    list: type("EList", (list,), {}),
 }
 
 # cache created classes here
diff --git a/esphome/util.py b/esphome/util.py
index 56bc97ca71..527e370ad8 100644
--- a/esphome/util.py
+++ b/esphome/util.py
@@ -260,8 +260,8 @@ def filter_yaml_files(files):
         f
         for f in files
         if (
-            os.path.splitext(f)[1] == ".yaml"
-            and os.path.basename(f) != "secrets.yaml"
+            os.path.splitext(f)[1] in (".yaml", ".yml")
+            and os.path.basename(f) not in ("secrets.yaml", "secrets.yml")
             and not os.path.basename(f).startswith(".")
         )
     ]
diff --git a/esphome/writer.py b/esphome/writer.py
index 641ae9b3cc..09ed284173 100644
--- a/esphome/writer.py
+++ b/esphome/writer.py
@@ -481,5 +481,5 @@ GITIGNORE_CONTENT = """# Gitignore settings for ESPHome
 def write_gitignore():
     path = CORE.relative_config_path(".gitignore")
     if not os.path.isfile(path):
-        with open(path, "w") as f:
+        with open(file=path, mode="w", encoding="utf-8") as f:
             f.write(GITIGNORE_CONTENT)
diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py
index a44c7c9114..e6853531f2 100644
--- a/esphome/zeroconf.py
+++ b/esphome/zeroconf.py
@@ -1,520 +1,28 @@
-# Custom zeroconf implementation based on python-zeroconf
-# (https://github.com/jstasiak/python-zeroconf) that supports Python 2
-
-import errno
-import logging
-import select
 import socket
-import struct
-import sys
 import threading
 import time
+from typing import Dict, Optional
 
-import ifaddr
-
-log = logging.getLogger(__name__)
-
-# Some timing constants
-
-_LISTENER_TIME = 200
-
-# Some DNS constants
-
-_MDNS_ADDR = "224.0.0.251"
-_MDNS_PORT = 5353
-
-_MAX_MSG_ABSOLUTE = 8966
-
-_FLAGS_QR_MASK = 0x8000  # query response mask
-_FLAGS_QR_QUERY = 0x0000  # query
-_FLAGS_QR_RESPONSE = 0x8000  # response
-
-_FLAGS_AA = 0x0400  # Authoritative answer
-_FLAGS_TC = 0x0200  # Truncated
-_FLAGS_RD = 0x0100  # Recursion desired
-_FLAGS_RA = 0x8000  # Recursion available
-
-_FLAGS_Z = 0x0040  # Zero
-_FLAGS_AD = 0x0020  # Authentic data
-_FLAGS_CD = 0x0010  # Checking disabled
+from zeroconf import (
+    DNSAddress,
+    DNSOutgoing,
+    DNSRecord,
+    DNSQuestion,
+    RecordUpdateListener,
+    Zeroconf,
+)
 
 _CLASS_IN = 1
-_CLASS_CS = 2
-_CLASS_CH = 3
-_CLASS_HS = 4
-_CLASS_NONE = 254
-_CLASS_ANY = 255
-_CLASS_MASK = 0x7FFF
-_CLASS_UNIQUE = 0x8000
-
+_FLAGS_QR_QUERY = 0x0000  # query
 _TYPE_A = 1
-_TYPE_NS = 2
-_TYPE_MD = 3
-_TYPE_MF = 4
-_TYPE_CNAME = 5
-_TYPE_SOA = 6
-_TYPE_MB = 7
-_TYPE_MG = 8
-_TYPE_MR = 9
-_TYPE_NULL = 10
-_TYPE_WKS = 11
-_TYPE_PTR = 12
-_TYPE_HINFO = 13
-_TYPE_MINFO = 14
-_TYPE_MX = 15
-_TYPE_TXT = 16
-_TYPE_AAAA = 28
-_TYPE_SRV = 33
-_TYPE_ANY = 255
-
-# Mapping constants to names
-int2byte = struct.Struct(">B").pack
-
-
-# Exceptions
-class Error(Exception):
-    pass
-
-
-class IncomingDecodeError(Error):
-    pass
-
-
-# pylint: disable=no-init
-class QuietLogger:
-    _seen_logs = {}
-
-    @classmethod
-    def log_exception_warning(cls, logger_data=None):
-        exc_info = sys.exc_info()
-        exc_str = str(exc_info[1])
-        if exc_str not in cls._seen_logs:
-            # log at warning level the first time this is seen
-            cls._seen_logs[exc_str] = exc_info
-            logger = log.warning
-        else:
-            logger = log.debug
-        if logger_data is not None:
-            logger(*logger_data)
-        logger("Exception occurred:", exc_info=True)
-
-    @classmethod
-    def log_warning_once(cls, *args):
-        msg_str = args[0]
-        if msg_str not in cls._seen_logs:
-            cls._seen_logs[msg_str] = 0
-            logger = log.warning
-        else:
-            logger = log.debug
-        cls._seen_logs[msg_str] += 1
-        logger(*args)
-
-
-class DNSEntry:
-    """A DNS entry"""
-
-    def __init__(self, name, type_, class_):
-        self.key = name.lower()
-        self.name = name
-        self.type = type_
-        self.class_ = class_ & _CLASS_MASK
-        self.unique = (class_ & _CLASS_UNIQUE) != 0
-
-
-class DNSQuestion(DNSEntry):
-    """A DNS question entry"""
-
-    def __init__(self, name, type_, class_):
-        DNSEntry.__init__(self, name, type_, class_)
-
-    def answered_by(self, rec):
-        """Returns true if the question is answered by the record"""
-        return (
-            self.class_ == rec.class_
-            and (self.type == rec.type or self.type == _TYPE_ANY)
-            and self.name == rec.name
-        )
-
-
-class DNSRecord(DNSEntry):
-    """A DNS record - like a DNS entry, but has a TTL"""
-
-    def __init__(self, name, type_, class_, ttl):
-        DNSEntry.__init__(self, name, type_, class_)
-        self.ttl = 15
-        self.created = time.time()
-
-    def write(self, out):
-        """Abstract method"""
-        raise NotImplementedError
-
-    def is_expired(self, now):
-        return self.created + self.ttl <= now
-
-    def is_removable(self, now):
-        return self.created + self.ttl * 2 <= now
-
-
-class DNSAddress(DNSRecord):
-    """A DNS address record"""
-
-    def __init__(self, name, type_, class_, ttl, address):
-        DNSRecord.__init__(self, name, type_, class_, ttl)
-        self.address = address
-
-    def write(self, out):
-        """Used in constructing an outgoing packet"""
-        out.write_string(self.address)
-
-
-class DNSText(DNSRecord):
-    """A DNS text record"""
-
-    def __init__(self, name, type_, class_, ttl, text):
-        assert isinstance(text, (bytes, type(None)))
-        DNSRecord.__init__(self, name, type_, class_, ttl)
-        self.text = text
-
-    def write(self, out):
-        """Used in constructing an outgoing packet"""
-        out.write_string(self.text)
-
-
-class DNSIncoming(QuietLogger):
-    """Object representation of an incoming DNS packet"""
-
-    def __init__(self, data):
-        """Constructor from string holding bytes of packet"""
-        self.offset = 0
-        self.data = data
-        self.questions = []
-        self.answers = []
-        self.id = 0
-        self.flags = 0  # type: int
-        self.num_questions = 0
-        self.num_answers = 0
-        self.num_authorities = 0
-        self.num_additionals = 0
-        self.valid = False
-
-        try:
-            self.read_header()
-            self.read_questions()
-            self.read_others()
-            self.valid = True
-
-        except (IndexError, struct.error, IncomingDecodeError):
-            self.log_exception_warning(
-                ("Choked at offset %d while unpacking %r", self.offset, data)
-            )
-
-    def unpack(self, format_):
-        length = struct.calcsize(format_)
-        info = struct.unpack(format_, self.data[self.offset : self.offset + length])
-        self.offset += length
-        return info
-
-    def read_header(self):
-        """Reads header portion of packet"""
-        (
-            self.id,
-            self.flags,
-            self.num_questions,
-            self.num_answers,
-            self.num_authorities,
-            self.num_additionals,
-        ) = self.unpack(b"!6H")
-
-    def read_questions(self):
-        """Reads questions section of packet"""
-        for _ in range(self.num_questions):
-            name = self.read_name()
-            type_, class_ = self.unpack(b"!HH")
-
-            question = DNSQuestion(name, type_, class_)
-            self.questions.append(question)
-
-    def read_character_string(self):
-        """Reads a character string from the packet"""
-        length = self.data[self.offset]
-        self.offset += 1
-        return self.read_string(length)
-
-    def read_string(self, length):
-        """Reads a string of a given length from the packet"""
-        info = self.data[self.offset : self.offset + length]
-        self.offset += length
-        return info
-
-    def read_unsigned_short(self):
-        """Reads an unsigned short from the packet"""
-        return self.unpack(b"!H")[0]
-
-    def read_others(self):
-        """Reads the answers, authorities and additionals section of the
-        packet"""
-        n = self.num_answers + self.num_authorities + self.num_additionals
-        for _ in range(n):
-            domain = self.read_name()
-            type_, class_, ttl, length = self.unpack(b"!HHiH")
-
-            rec = None
-            if type_ == _TYPE_A:
-                rec = DNSAddress(domain, type_, class_, ttl, self.read_string(4))
-            elif type_ == _TYPE_TXT:
-                rec = DNSText(domain, type_, class_, ttl, self.read_string(length))
-            elif type_ == _TYPE_AAAA:
-                rec = DNSAddress(domain, type_, class_, ttl, self.read_string(16))
-            else:
-                # Try to ignore types we don't know about
-                # Skip the payload for the resource record so the next
-                # records can be parsed correctly
-                self.offset += length
-
-            if rec is not None:
-                self.answers.append(rec)
-
-    def is_query(self):
-        """Returns true if this is a query"""
-        return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY
-
-    def is_response(self):
-        """Returns true if this is a response"""
-        return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE
-
-    def read_utf(self, offset, length):
-        """Reads a UTF-8 string of a given length from the packet"""
-        return str(self.data[offset : offset + length], "utf-8", "replace")
-
-    def read_name(self):
-        """Reads a domain name from the packet"""
-        result = ""
-        off = self.offset
-        next_ = -1
-        first = off
-
-        while True:
-            length = self.data[off]
-            off += 1
-            if length == 0:
-                break
-            t = length & 0xC0
-            if t == 0x00:
-                result = "".join((result, self.read_utf(off, length) + "."))
-                off += length
-            elif t == 0xC0:
-                if next_ < 0:
-                    next_ = off + 1
-                off = ((length & 0x3F) << 8) | self.data[off]
-                if off >= first:
-                    raise IncomingDecodeError(f"Bad domain name (circular) at {off}")
-                first = off
-            else:
-                raise IncomingDecodeError(f"Bad domain name at {off}")
-
-        if next_ >= 0:
-            self.offset = next_
-        else:
-            self.offset = off
-
-        return result
-
-
-class DNSOutgoing:
-    """Object representation of an outgoing packet"""
-
-    def __init__(self, flags):
-        self.finished = False
-        self.id = 0
-        self.flags = flags
-        self.names = {}
-        self.data = []
-        self.size = 12
-        self.state = False
-
-        self.questions = []
-        self.answers = []
-
-    def add_question(self, record):
-        """Adds a question"""
-        self.questions.append(record)
-
-    def pack(self, format_, value):
-        self.data.append(struct.pack(format_, value))
-        self.size += struct.calcsize(format_)
-
-    def write_byte(self, value):
-        """Writes a single byte to the packet"""
-        self.pack(b"!c", int2byte(value))
-
-    def insert_short(self, index, value):
-        """Inserts an unsigned short in a certain position in the packet"""
-        self.data.insert(index, struct.pack(b"!H", value))
-        self.size += 2
-
-    def write_short(self, value):
-        """Writes an unsigned short to the packet"""
-        self.pack(b"!H", value)
-
-    def write_int(self, value):
-        """Writes an unsigned integer to the packet"""
-        self.pack(b"!I", int(value))
-
-    def write_string(self, value):
-        """Writes a string to the packet"""
-        assert isinstance(value, bytes)
-        self.data.append(value)
-        self.size += len(value)
-
-    def write_utf(self, s):
-        """Writes a UTF-8 string of a given length to the packet"""
-        utfstr = s.encode("utf-8")
-        length = len(utfstr)
-        self.write_byte(length)
-        self.write_string(utfstr)
-
-    def write_character_string(self, value):
-        assert isinstance(value, bytes)
-        length = len(value)
-        self.write_byte(length)
-        self.write_string(value)
-
-    def write_name(self, name):
-        # split name into each label
-        parts = name.split(".")
-        if not parts[-1]:
-            parts.pop()
-
-        # construct each suffix
-        name_suffices = [".".join(parts[i:]) for i in range(len(parts))]
-
-        # look for an existing name or suffix
-        for count, sub_name in enumerate(name_suffices):
-            if sub_name in self.names:
-                break
-        else:
-            count = len(name_suffices)
-
-        # note the new names we are saving into the packet
-        name_length = len(name.encode("utf-8"))
-        for suffix in name_suffices[:count]:
-            self.names[suffix] = (
-                self.size + name_length - len(suffix.encode("utf-8")) - 1
-            )
-
-        # write the new names out.
-        for part in parts[:count]:
-            self.write_utf(part)
-
-        # if we wrote part of the name, create a pointer to the rest
-        if count != len(name_suffices):
-            # Found substring in packet, create pointer
-            index = self.names[name_suffices[count]]
-            self.write_byte((index >> 8) | 0xC0)
-            self.write_byte(index & 0xFF)
-        else:
-            # this is the end of a name
-            self.write_byte(0)
-
-    def write_question(self, question):
-        self.write_name(question.name)
-        self.write_short(question.type)
-        self.write_short(question.class_)
-
-    def packet(self):
-        if not self.state:
-            for question in self.questions:
-                self.write_question(question)
-            self.state = True
-
-            self.insert_short(0, 0)  # num additionals
-            self.insert_short(0, 0)  # num authorities
-            self.insert_short(0, 0)  # num answers
-            self.insert_short(0, len(self.questions))
-            self.insert_short(0, self.flags)  # _FLAGS_QR_QUERY
-            self.insert_short(0, 0)
-        return b"".join(self.data)
-
-
-class Engine(threading.Thread):
-    def __init__(self, zc):
-        threading.Thread.__init__(self, name="zeroconf-Engine")
-        self.daemon = True
-        self.zc = zc
-        self.readers = {}
-        self.timeout = 5
-        self.condition = threading.Condition()
-        self.start()
-
-    def run(self):
-        while not self.zc.done:
-            # pylint: disable=len-as-condition
-            with self.condition:
-                rs = self.readers.keys()
-                if len(rs) == 0:
-                    # No sockets to manage, but we wait for the timeout
-                    # or addition of a socket
-                    self.condition.wait(self.timeout)
-
-            if len(rs) != 0:
-                try:
-                    rr, _, _ = select.select(rs, [], [], self.timeout)
-                    if not self.zc.done:
-                        for socket_ in rr:
-                            reader = self.readers.get(socket_)
-                            if reader:
-                                reader.handle_read(socket_)
-
-                except OSError as e:
-                    # If the socket was closed by another thread, during
-                    # shutdown, ignore it and exit
-                    if e.args[0] != socket.EBADF or not self.zc.done:
-                        raise
-
-    def add_reader(self, reader, socket_):
-        with self.condition:
-            self.readers[socket_] = reader
-            self.condition.notify()
-
-    def del_reader(self, socket_):
-        with self.condition:
-            del self.readers[socket_]
-            self.condition.notify()
-
-
-class Listener(QuietLogger):
-    def __init__(self, zc):
-        self.zc = zc
-        self.data = None
-
-    def handle_read(self, socket_):
-        try:
-            data, (addr, port) = socket_.recvfrom(_MAX_MSG_ABSOLUTE)
-        except Exception:  # pylint: disable=broad-except
-            self.log_exception_warning()
-            return
-
-        log.debug("Received from %r:%r: %r ", addr, port, data)
-
-        self.data = data
-        msg = DNSIncoming(data)
-        if not msg.valid or msg.is_query():
-            pass
-        else:
-            self.zc.handle_response(msg)
-
-
-class RecordUpdateListener:
-    def update_record(self, zc, now, record):
-        raise NotImplementedError()
 
 
 class HostResolver(RecordUpdateListener):
-    def __init__(self, name):
+    def __init__(self, name: str):
         self.name = name
-        self.address = None
+        self.address: Optional[bytes] = None
 
-    def update_record(self, zc, now, record):
+    def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None:
         if record is None:
             return
         if record.type == _TYPE_A:
@@ -522,14 +30,14 @@ class HostResolver(RecordUpdateListener):
             if record.name == self.name:
                 self.address = record.address
 
-    def request(self, zc, timeout):
+    def request(self, zc: Zeroconf, timeout: float) -> bool:
         now = time.time()
         delay = 0.2
         next_ = now + delay
         last = now + timeout
 
         try:
-            zc.add_listener(self)
+            zc.add_listener(self, None)
             while self.address is None:
                 if last <= now:
                     # Timeout
@@ -541,7 +49,7 @@ class HostResolver(RecordUpdateListener):
                     next_ = now + delay
                     delay *= 2
 
-                zc.wait(min(next_, last) - now)
+                time.sleep(min(next_, last) - now)
                 now = time.time()
         finally:
             zc.remove_listener(self)
@@ -550,56 +58,52 @@ class HostResolver(RecordUpdateListener):
 
 
 class DashboardStatus(RecordUpdateListener, threading.Thread):
-    def __init__(self, zc, on_update):
+    PING_AFTER = 15 * 1000  # Send new mDNS request after 15 seconds
+    OFFLINE_AFTER = PING_AFTER * 2  # Offline if no mDNS response after 30 seconds
+
+    def __init__(self, zc: Zeroconf, on_update) -> None:
         threading.Thread.__init__(self)
         self.zc = zc
-        self.query_hosts = set()
-        self.key_to_host = {}
-        self.cache = {}
+        self.query_hosts: set[str] = set()
+        self.key_to_host: Dict[str, str] = {}
         self.stop_event = threading.Event()
         self.query_event = threading.Event()
         self.on_update = on_update
 
-    def update_record(self, zc, now, record):
-        if record is None:
-            return
-        if record.type in (_TYPE_A, _TYPE_AAAA, _TYPE_TXT):
-            assert isinstance(record, DNSEntry)
-            if record.name in self.query_hosts:
-                self.cache.setdefault(record.name, []).insert(0, record)
-            self.purge_cache()
+    def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None:
+        pass
 
-    def purge_cache(self):
-        new_cache = {}
-        for host, records in self.cache.items():
-            if host not in self.query_hosts:
-                continue
-            new_records = [rec for rec in records if not rec.is_removable(time.time())]
-            if new_records:
-                new_cache[host] = new_records
-        self.cache = new_cache
-        self.on_update({key: self.host_status(key) for key in self.key_to_host})
-
-    def request_query(self, hosts):
+    def request_query(self, hosts: Dict[str, str]) -> None:
         self.query_hosts = set(hosts.values())
         self.key_to_host = hosts
         self.query_event.set()
 
-    def stop(self):
+    def stop(self) -> None:
         self.stop_event.set()
         self.query_event.set()
 
-    def host_status(self, key):
-        return self.key_to_host.get(key) in self.cache
+    def host_status(self, key: str) -> bool:
+        entries = self.zc.cache.entries_with_name(key)
+        if not entries:
+            return False
+        now = time.time() * 1000
 
-    def run(self):
-        self.zc.add_listener(self)
+        return any(
+            (entry.created + DashboardStatus.OFFLINE_AFTER) >= now for entry in entries
+        )
+
+    def run(self) -> None:
+        self.zc.add_listener(self, None)
         while not self.stop_event.is_set():
-            self.purge_cache()
+            self.on_update(
+                {key: self.host_status(host) for key, host in self.key_to_host.items()}
+            )
+            now = time.time() * 1000
             for host in self.query_hosts:
-                if all(
-                    record.is_expired(time.time())
-                    for record in self.cache.get(host, [])
+                entries = self.zc.cache.entries_with_name(host)
+                if not entries or all(
+                    (entry.created + DashboardStatus.PING_AFTER) <= now
+                    for entry in entries
                 ):
                     out = DNSOutgoing(_FLAGS_QR_QUERY)
                     out.add_question(DNSQuestion(host, _TYPE_A, _CLASS_IN))
@@ -609,186 +113,9 @@ class DashboardStatus(RecordUpdateListener, threading.Thread):
         self.zc.remove_listener(self)
 
 
-def get_all_addresses():
-    return list(
-        {
-            addr.ip
-            for iface in ifaddr.get_adapters()
-            for addr in iface.ips
-            if addr.is_IPv4
-            and addr.network_prefix != 32  # Host only netmask 255.255.255.255
-        }
-    )
-
-
-def new_socket():
-    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-
-    # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
-    # multicast UDP sockets (p 731, "TCP/IP Illustrated,
-    # Volume 2"), but some BSD-derived systems require
-    # SO_REUSEPORT to be specified explicitly.  Also, not all
-    # versions of Python have SO_REUSEPORT available.
-    # Catch OSError and socket.error for kernel versions <3.9 because lacking
-    # SO_REUSEPORT support.
-    try:
-        reuseport = socket.SO_REUSEPORT
-    except AttributeError:
-        pass
-    else:
-        try:
-            s.setsockopt(socket.SOL_SOCKET, reuseport, 1)
-        except OSError as err:
-            # OSError on python 3, socket.error on python 2
-            if err.errno != errno.ENOPROTOOPT:
-                raise
-
-    # OpenBSD needs the ttl and loop values for the IP_MULTICAST_TTL and
-    # IP_MULTICAST_LOOP socket options as an unsigned char.
-    ttl = struct.pack(b"B", 255)
-    s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
-    loop = struct.pack(b"B", 1)
-    s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, loop)
-
-    s.bind(("", _MDNS_PORT))
-    return s
-
-
-class Zeroconf(QuietLogger):
-    def __init__(self):
-        # hook for threads
-        self._GLOBAL_DONE = False
-
-        self._listen_socket = new_socket()
-        interfaces = get_all_addresses()
-
-        self._respond_sockets = []
-
-        for i in interfaces:
-            try:
-                _value = socket.inet_aton(_MDNS_ADDR) + socket.inet_aton(i)
-                self._listen_socket.setsockopt(
-                    socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, _value
-                )
-            except OSError as e:
-                _errno = e.args[0]
-                if _errno == errno.EADDRINUSE:
-                    log.info(
-                        "Address in use when adding %s to multicast group, "
-                        "it is expected to happen on some systems",
-                        i,
-                    )
-                elif _errno == errno.EADDRNOTAVAIL:
-                    log.info(
-                        "Address not available when adding %s to multicast "
-                        "group, it is expected to happen on some systems",
-                        i,
-                    )
-                    continue
-                elif _errno == errno.EINVAL:
-                    log.info(
-                        "Interface of %s does not support multicast, "
-                        "it is expected in WSL",
-                        i,
-                    )
-                    continue
-
-                else:
-                    raise
-
-            respond_socket = new_socket()
-            respond_socket.setsockopt(
-                socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(i)
-            )
-
-            self._respond_sockets.append(respond_socket)
-
-        self.listeners = []
-
-        self.condition = threading.Condition()
-
-        self.engine = Engine(self)
-        self.listener = Listener(self)
-        self.engine.add_reader(self.listener, self._listen_socket)
-
-    @property
-    def done(self):
-        return self._GLOBAL_DONE
-
-    def wait(self, timeout):
-        """Calling thread waits for a given number of milliseconds or
-        until notified."""
-        with self.condition:
-            self.condition.wait(timeout)
-
-    def notify_all(self):
-        """Notifies all waiting threads"""
-        with self.condition:
-            self.condition.notify_all()
-
-    def resolve_host(self, host, timeout=3.0):
+class EsphomeZeroconf(Zeroconf):
+    def resolve_host(self, host: str, timeout=3.0):
         info = HostResolver(host)
         if info.request(self, timeout):
             return socket.inet_ntoa(info.address)
         return None
-
-    def add_listener(self, listener):
-        self.listeners.append(listener)
-        self.notify_all()
-
-    def remove_listener(self, listener):
-        """Removes a listener."""
-        try:
-            self.listeners.remove(listener)
-            self.notify_all()
-        except Exception as e:  # pylint: disable=broad-except
-            log.exception("Unknown error, possibly benign: %r", e)
-
-    def update_record(self, now, rec):
-        """Used to notify listeners of new information that has updated
-        a record."""
-        for listener in self.listeners:
-            listener.update_record(self, now, rec)
-        self.notify_all()
-
-    def handle_response(self, msg):
-        """Deal with incoming response packets.  All answers
-        are held in the cache, and listeners are notified."""
-        now = time.time()
-        for record in msg.answers:
-            self.update_record(now, record)
-
-    def send(self, out):
-        """Sends an outgoing packet."""
-        packet = out.packet()
-        log.debug("Sending %r (%d bytes) as %r...", out, len(packet), packet)
-        for s in self._respond_sockets:
-            if self._GLOBAL_DONE:
-                return
-            try:
-                bytes_sent = s.sendto(packet, 0, (_MDNS_ADDR, _MDNS_PORT))
-            except Exception:  # pylint: disable=broad-except
-                # on send errors, log the exception and keep going
-                self.log_exception_warning()
-            else:
-                if bytes_sent != len(packet):
-                    self.log_warning_once(
-                        "!!! sent %d out of %d bytes to %r"
-                        % (bytes_sent, len(packet), s)
-                    )
-
-    def close(self):
-        """Ends the background threads, and prevent this instance from
-        servicing further queries."""
-        if not self._GLOBAL_DONE:
-            self._GLOBAL_DONE = True
-            # shutdown recv socket and thread
-            self.engine.del_reader(self._listen_socket)
-            self._listen_socket.close()
-            self.engine.join()
-
-            # shutdown the rest
-            self.notify_all()
-            for s in self._respond_sockets:
-                s.close()
diff --git a/platformio.ini b/platformio.ini
index c280c54a21..f4dea3fcb9 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -36,6 +36,8 @@ lib_deps =
     6306@1.0.3  ; HM3301
     glmnet/Dsmr@0.3        ; used by dsmr
     rweather/Crypto@0.2.0  ; used by dsmr
+    esphome/noise-c@0.1.1  ; used by api
+    dudanov/MideaUART@1.1.0  ; used by midea
 
 build_flags =
     -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE
diff --git a/requirements.txt b/requirements.txt
index b4d557f06e..18752e16a3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,12 +3,11 @@ PyYAML==5.4.1
 paho-mqtt==1.5.1
 colorama==0.4.4
 tornado==6.1
-protobuf==3.17.3
 tzlocal==2.1
 pytz==2021.1
 pyserial==3.5
-ifaddr==0.1.7
-platformio==5.1.1
+platformio==5.2.0
 esptool==3.1
 click==7.1.2
-esphome-dashboard==20210728.0
+esphome-dashboard==20210908.0
+aioesphomeapi==9.0.0
diff --git a/requirements_test.txt b/requirements_test.txt
index 684582bd4c..59085b33e2 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -1,11 +1,11 @@
-pylint==2.9.6
+pylint==2.10.2
 flake8==3.9.2
-black==21.7b0
+black==21.8b0
 pexpect==4.8.0
 pre-commit
 
 # Unit tests
-pytest==6.2.4
+pytest==6.2.5
 pytest-cov==2.12.1
 pytest-mock==3.6.1
 pytest-asyncio==0.15.1
diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py
index 6983090fd9..7ccdc5a24e 100755
--- a/script/api_protobuf/api_protobuf.py
+++ b/script/api_protobuf/api_protobuf.py
@@ -778,9 +778,9 @@ def build_service_message_type(mt):
         hout += f"bool {func}(const {mt.name} &msg);\n"
         cout += f"bool {class_name}::{func}(const {mt.name} &msg) {{\n"
         if log:
-            cout += f'#ifdef HAS_PROTO_MESSAGE_DUMP\n'
+            cout += f"#ifdef HAS_PROTO_MESSAGE_DUMP\n"
             cout += f'  ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n'
-            cout += f'#endif\n'
+            cout += f"#endif\n"
         # cout += f'  this->set_nodelay({str(nodelay).lower()});\n'
         cout += f"  return this->send_message_<{mt.name}>(msg, {id_});\n"
         cout += f"}}\n"
@@ -794,9 +794,9 @@ def build_service_message_type(mt):
         case += f"{mt.name} msg;\n"
         case += f"msg.decode(msg_data, msg_size);\n"
         if log:
-            case += f'#ifdef HAS_PROTO_MESSAGE_DUMP\n'
+            case += f"#ifdef HAS_PROTO_MESSAGE_DUMP\n"
             case += f'ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n'
-            case += f'#endif\n'
+            case += f"#endif\n"
         case += f"this->{func}(msg);\n"
         if ifdef is not None:
             case += f"#endif\n"
diff --git a/script/build_jsonschema.py b/script/build_jsonschema.py
index 1ab0ffa015..7a3257411c 100644
--- a/script/build_jsonschema.py
+++ b/script/build_jsonschema.py
@@ -25,6 +25,11 @@ JSC_DESCRIPTION = "description"
 JSC_ONEOF = "oneOf"
 JSC_PROPERTIES = "properties"
 JSC_REF = "$ref"
+
+# this should be required, but YAML Language server completion does not work properly if required are specified.
+# still needed for other features / checks
+JSC_REQUIRED = "required_"
+
 SIMPLE_AUTOMATION = "simple_automation"
 
 schema_names = {}
@@ -295,9 +300,17 @@ def get_automation_schema(name, vschema):
     #   * an object with automation's schema and a then key
     #        with again a single action or an array of actions
 
+    if len(extra_jschema[JSC_PROPERTIES]) == 0:
+        return get_ref(SIMPLE_AUTOMATION)
+
     extra_jschema[JSC_PROPERTIES]["then"] = add_definition_array_or_single_object(
         get_ref(JSC_ACTION)
     )
+    # if there is a required element in extra_jschema then this automation does not support
+    # directly a list of actions
+    if JSC_REQUIRED in extra_jschema:
+        return create_ref(name, extra_vschema, extra_jschema)
+
     jschema = add_definition_array_or_single_object(get_ref(JSC_ACTION))
     jschema[JSC_ANYOF].append(extra_jschema)
 
@@ -370,9 +383,14 @@ def get_entry(parent_key, vschema):
         # everything else just accept string and let ESPHome validate
         try:
             from esphome.core import ID
+            from esphome.automation import Trigger, Automation
 
             v = vschema(None)
             if isinstance(v, ID):
+                if v.type.base != "script::Script" and (
+                    v.type.inherits_from(Trigger) or v.type == Automation
+                ):
+                    return None
                 entry = {"type": "string", "id_type": v.type.base}
             elif isinstance(v, str):
                 entry = {"type": "string"}
@@ -494,9 +512,11 @@ def convert_schema(path, vschema, un_extend=True):
     output = {}
 
     if str(vschema) in ejs.hidden_schemas:
-        # this can get another think twist. When adding this I've already figured out
-        # interval and script in other way
-        if path not in ["interval", "script"]:
+        if ejs.hidden_schemas[str(vschema)] == "automation":
+            vschema = vschema(ejs.jschema_extractor)
+            jschema = get_jschema(path, vschema, True)
+            return add_definition_array_or_single_object(jschema)
+        else:
             vschema = vschema(ejs.jschema_extractor)
 
     if un_extend:
@@ -515,9 +535,8 @@ def convert_schema(path, vschema, un_extend=True):
                 return rhs
 
             # merge
-
             if JSC_ALLOF in lhs and JSC_ALLOF in rhs:
-                output = lhs[JSC_ALLOF]
+                output = lhs
                 for k in rhs[JSC_ALLOF]:
                     merge(output[JSC_ALLOF], k)
             elif JSC_ALLOF in lhs:
@@ -574,6 +593,7 @@ def convert_schema(path, vschema, un_extend=True):
         return output
 
     props = output[JSC_PROPERTIES] = {}
+    required = []
 
     output["type"] = ["object", "null"]
     if DUMP_COMMENTS:
@@ -616,13 +636,21 @@ def convert_schema(path, vschema, un_extend=True):
             if prop:  # Deprecated (cv.Invalid) properties not added
                 props[str(k)] = prop
                 # TODO: see required, sometimes completions doesn't show up because of this...
-                # if isinstance(k, cv.Required):
-                #     required.append(str(k))
+                if isinstance(k, cv.Required):
+                    required.append(str(k))
                 try:
                     if str(k.default) != "...":
-                        prop["default"] = k.default()
+                        default_value = k.default()
+                        # Yaml validator fails if `"default": null` ends up in the json schema
+                        if default_value is not None:
+                            if prop["type"] == "string":
+                                default_value = str(default_value)
+                            prop["default"] = default_value
                 except:
                     pass
+
+    if len(required) > 0:
+        output[JSC_REQUIRED] = required
     return output
 
 
@@ -648,6 +676,7 @@ def add_pin_registry():
         internal = definitions[schema_name]
         definitions[schema_name]["additionalItems"] = False
         definitions[f"PIN.{mode}_INTERNAL"] = internal
+        internal[JSC_PROPERTIES]["number"] = {"type": ["number", "string"]}
         schemas = [get_ref(f"PIN.{mode}_INTERNAL")]
         schemas[0]["required"] = ["number"]
         # accept string and object, for internal shorthand pin IO:
diff --git a/script/ci-custom.py b/script/ci-custom.py
index 5dad3e2445..cdc450a96b 100755
--- a/script/ci-custom.py
+++ b/script/ci-custom.py
@@ -261,7 +261,7 @@ def highlight(s):
 @lint_re_check(
     r"^#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)" + CPP_RE_EOL,
     include=cpp_include,
-    exclude=["esphome/core/log.h"],
+    exclude=["esphome/core/log.h", "esphome/components/socket/headers.h"],
 )
 def lint_no_defines(fname, match):
     s = highlight(
@@ -493,7 +493,10 @@ def lint_relative_py_import(fname):
         "esphome/components/*.h",
         "esphome/components/*.cpp",
         "esphome/components/*.tcc",
-    ]
+    ],
+    exclude=[
+        "esphome/components/socket/headers.h",
+    ],
 )
 def lint_namespace(fname, content):
     expected_name = re.match(
diff --git a/tests/test1.yaml b/tests/test1.yaml
index bcf5f932a8..cd4179f394 100644
--- a/tests/test1.yaml
+++ b/tests/test1.yaml
@@ -129,6 +129,8 @@ mqtt:
             - mqtt.connected:
             - light.is_on: kitchen
             - light.is_off: kitchen
+            - fan.is_on: fan_speed
+            - fan.is_off: fan_speed
           then:
             - lambda: |-
                 int data = x["my_data"];
@@ -1606,29 +1608,84 @@ climate:
     name: Toshiba Climate
   - platform: hitachi_ac344
     name: Hitachi Climate
-  - platform: midea_ac
+  - platform: midea
+    id: midea_unit
+    uart_id: uart0
+    name: Midea Climate
+    transmitter_id:
+    period: 1s
+    num_attempts: 5
+    timeout: 2s
+    beeper: false
+    autoconf: true
     visual:
-      min_temperature: 18 °C
-      max_temperature: 25 °C
-      temperature_step: 0.1 °C
-    name: 'Electrolux EACS'
-    beeper: true
+      min_temperature: 17 °C
+      max_temperature: 30 °C
+      temperature_step: 0.5 °C
+    supported_modes:
+      - FAN_ONLY
+      - HEAT_COOL
+      - COOL
+      - HEAT
+      - DRY
+    custom_fan_modes:
+      - SILENT
+      - TURBO
+    supported_presets:
+      - ECO
+      - BOOST
+      - SLEEP
+    custom_presets:
+      - FREEZE_PROTECTION
+    supported_swing_modes:
+      - VERTICAL
+      - HORIZONTAL
+      - BOTH
     outdoor_temperature:
-      name: 'Temp'
+      name: "Temp"
     power_usage:
-      name: 'Power'
+      name: "Power"
     humidity_setpoint:
-      name: 'Hum'
+      name: "Humidity"
   - platform: anova
     name: Anova cooker
     ble_client_id: ble_blah
     unit_of_measurement: c
 
-midea_dongle:
-  uart_id: uart0
-  strength_icon: true
+script:
+  - id: climate_custom
+    then:
+      - climate.control:
+          id: midea_unit
+          custom_preset: FREEZE_PROTECTION
+          custom_fan_mode: SILENT
+  - id: climate_preset
+    then:
+      - climate.control:
+          id: midea_unit
+          preset: SLEEP
 
 switch:
+  - platform: template
+    name: MIDEA_AC_TOGGLE_LIGHT
+    turn_on_action:
+      midea_ac.display_toggle:
+  - platform: template
+    name: MIDEA_AC_SWING_STEP
+    turn_on_action:
+      midea_ac.swing_step:
+  - platform: template
+    name: MIDEA_AC_BEEPER_CONTROL
+    optimistic: true
+    turn_on_action:
+      midea_ac.beeper_on:
+    turn_off_action:
+      midea_ac.beeper_off:
+  - platform: template
+    name: MIDEA_RAW
+    turn_on_action:
+      remote_transmitter.transmit_midea:
+        code: [0xA2, 0x08, 0xFF, 0xFF, 0xFF]
   - platform: gpio
     name: 'MCP23S08 Pin #0'
     pin:
@@ -1860,6 +1917,7 @@ switch:
       inverted: False
   - platform: template
     id: ble1_status
+    optimistic: true
 
 fan:
   - platform: binary
@@ -1868,6 +1926,7 @@ fan:
     oscillation_output: gpio_19
     direction_output: gpio_26
   - platform: speed
+    id: fan_speed
     output: pca_6
     speed_count: 10
     name: 'Living Room Fan 2'
@@ -1877,6 +1936,9 @@ fan:
     oscillation_command_topic: oscillation/command/topic
     speed_state_topic: speed/state/topic
     speed_command_topic: speed/command/topic
+    on_speed_set:
+      then:
+        - logger.log: "Fan speed was changed!"
 
 interval:
   - interval: 10s
@@ -2038,6 +2100,14 @@ display:
     backlight_pin: GPIO4
     lambda: |-
       it.rectangle(0, 0, it.get_width(), it.get_height());
+  - platform: st7920
+    width: 128
+    height: 64
+    cs_pin:
+      number: GPIO23
+      inverted: true
+    lambda: |-
+      it.rectangle(0, 0, it.get_width(), it.get_height());
   - platform: st7735
     model: 'INITR_BLACKTAB'
     cs_pin: GPIO5
diff --git a/tests/test2.yaml b/tests/test2.yaml
index 6807278c0d..54932953d5 100644
--- a/tests/test2.yaml
+++ b/tests/test2.yaml
@@ -246,6 +246,24 @@ sensor:
     id: freezer_temp_source
     reference_voltage: 3.19
     number: 0
+  - platform: airthings_wave_plus
+    ble_client_id: airthings01
+    update_interval: 5min
+    temperature:
+      name: "Wave Plus Temperature"
+    radon:
+      name: "Wave Plus Radon"
+    radon_long_term:
+      name: "Wave Plus Radon Long Term"
+    pressure:
+      name: "Wave Plus Pressure"
+    humidity:
+      name: "Wave Plus Humidity"
+    co2:
+      name: "Wave Plus CO2"
+    tvoc:
+      name: "Wave Plus VOC"
+
 time:
   - platform: homeassistant
     on_time:
@@ -276,6 +294,11 @@ binary_sensor:
   - platform: ble_presence
     service_uuid: '11223344-5566-7788-99aa-bbccddeeff00'
     name: 'BLE Test Service 128 Presence'
+  - platform: ble_presence
+    ibeacon_uuid: '11223344-5566-7788-99aa-bbccddeeff00'
+    ibeacon_major: 100
+    ibeacon_minor: 1
+    name: 'BLE Test iBeacon Presence'
   - platform: esp32_touch
     name: 'ESP32 Touch Pad GPIO27'
     pin: GPIO27
@@ -334,6 +357,12 @@ esp32_ble_tracker:
         - lambda: !lambda |-
             ESP_LOGD("main", "Length of manufacturer data is %i", x.size());
 
+ble_client:
+  - mac_address: 01:02:03:04:05:06
+    id: airthings01
+    
+airthings_ble:
+
 #esp32_ble_beacon:
 #  type: iBeacon
 #  uuid: 'c29ce823-e67a-4e71-bff2-abaa32e77a98'
@@ -431,3 +460,4 @@ interval:
     - logger.log: 'Interval Run'
 
 display:
+
diff --git a/tests/test3.yaml b/tests/test3.yaml
index e35c1e611c..5602481c36 100644
--- a/tests/test3.yaml
+++ b/tests/test3.yaml
@@ -22,6 +22,8 @@ api:
   port: 8000
   password: 'pwd'
   reboot_timeout: 0min
+  encryption:
+    key: 'bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU='
   services:
     - service: hello_world
       variables:
@@ -748,17 +750,6 @@ 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
@@ -949,32 +940,6 @@ 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
diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py
index 9e4ad3d79d..37b4d6db57 100644
--- a/tests/unit_tests/test_core.py
+++ b/tests/unit_tests/test_core.py
@@ -473,7 +473,11 @@ class TestLibrary:
             ("__eq__", core.Library(name="libfoo", version="1.2.3"), True),
             ("__eq__", core.Library(name="libfoo", version="1.2.4"), False),
             ("__eq__", core.Library(name="libbar", version="1.2.3"), False),
-            ("__eq__", core.Library(name="libbar", version=None, repository="file:///test"), False),
+            (
+                "__eq__",
+                core.Library(name="libbar", version=None, repository="file:///test"),
+                False,
+            ),
             ("__eq__", 1000, NotImplemented),
             ("__eq__", "1000", NotImplemented),
             ("__eq__", True, NotImplemented),
diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py
index 3e317589a9..ae7a61e01f 100644
--- a/tests/unit_tests/test_cpp_helpers.py
+++ b/tests/unit_tests/test_cpp_helpers.py
@@ -38,7 +38,7 @@ async def test_register_component(monkeypatch):
     actual = await ch.register_component(var, {})
 
     assert actual is var
-    add_mock.assert_called_once()
+    assert add_mock.call_count == 2
     app_mock.register_component.assert_called_with(var)
     assert core_mock.component_ids == []
 
@@ -77,6 +77,6 @@ async def test_register_component__with_setup_priority(monkeypatch):
 
     assert actual is var
     add_mock.assert_called()
-    assert add_mock.call_count == 3
+    assert add_mock.call_count == 4
     app_mock.register_component.assert_called_with(var)
     assert core_mock.component_ids == []